Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions .vortex/installer/src/Command/InstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareTrait;
use DrevOps\VortexInstaller\Runner\RunnerInterface;
use DrevOps\VortexInstaller\Schema\AgentHelp;
use DrevOps\VortexInstaller\Schema\ConfigValidator;
use DrevOps\VortexInstaller\Schema\SchemaGenerator;
use DrevOps\VortexInstaller\Schema\SchemaValidator;
use DrevOps\VortexInstaller\Task\Task;
use DrevOps\VortexInstaller\Utils\Config;
use DrevOps\VortexInstaller\Utils\Env;
Expand Down Expand Up @@ -62,6 +62,8 @@ class InstallCommand extends Command implements CommandRunnerAwareInterface, Exe

const OPTION_VALIDATE = 'validate';

const OPTION_PROMPTS = 'prompts';

const OPTION_AGENT_HELP = 'agent-help';

/**
Expand Down Expand Up @@ -142,6 +144,7 @@ protected function configure(): void {
$this->addOption(static::OPTION_URI, 'l', InputOption::VALUE_REQUIRED, 'Remote or local repository URI with an optional git ref set after @.');
$this->addOption(static::OPTION_NO_CLEANUP, NULL, InputOption::VALUE_NONE, 'Do not remove installer after successful installation.');
$this->addOption(static::OPTION_BUILD, 'b', InputOption::VALUE_NONE, 'Run auto-build after installation without prompting.');
$this->addOption(static::OPTION_PROMPTS, 'p', InputOption::VALUE_REQUIRED, 'A JSON string with prompt answers or a path to a JSON file. Keys are prompt IDs from --schema.');
$this->addOption(static::OPTION_SCHEMA, NULL, InputOption::VALUE_NONE, 'Output prompt schema as JSON.');
$this->addOption(static::OPTION_VALIDATE, NULL, InputOption::VALUE_NONE, 'Validate config without installing.');
$this->addOption(static::OPTION_AGENT_HELP, NULL, InputOption::VALUE_NONE, 'Output instructions for AI agents on how to use the installer.');
Expand Down Expand Up @@ -306,8 +309,8 @@ protected function handleSchema(InputInterface $input, OutputInterface $output):
$config = Config::fromString('{}');
$prompt_manager = new PromptManager($config);

$generator = new SchemaGenerator();
$schema = $generator->generate($prompt_manager->getHandlers());
$generator = new SchemaGenerator($prompt_manager->getHandlers());
$schema = $generator->generate();

$output->write((string) json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

Expand All @@ -318,28 +321,30 @@ protected function handleSchema(InputInterface $input, OutputInterface $output):
* Handle --validate option.
*/
protected function handleValidate(InputInterface $input, OutputInterface $output): int {
$config_option = $input->getOption(static::OPTION_CONFIG);
$prompts_option = $input->getOption(static::OPTION_PROMPTS);

if (empty($config_option) || !is_string($config_option)) {
$output->writeln('The --validate option requires --config.');
if (empty($prompts_option) || !is_string($prompts_option)) {
$output->writeln('The --validate option requires --prompts.');

return Command::FAILURE;
}

$config_json = is_file($config_option) ? (string) file_get_contents($config_option) : $config_option;
$user_config = json_decode($config_json, TRUE);
$prompts_json = is_file($prompts_option) ? (string) file_get_contents($prompts_option) : $prompts_option;
$decoded = json_decode($prompts_json);

if (!is_array($user_config)) {
$output->writeln('Invalid JSON in --config.');
if (!$decoded instanceof \stdClass) {
$output->writeln('Invalid JSON in --prompts. Expected a JSON object.');

return Command::FAILURE;
}

$user_config = json_decode($prompts_json, TRUE);

$config = Config::fromString('{}');
$prompt_manager = new PromptManager($config);

$validator = new ConfigValidator();
$result = $validator->validate($user_config, $prompt_manager->getHandlers());
$validator = new SchemaValidator($prompt_manager->getHandlers());
$result = $validator->validate($user_config);

$output->write((string) json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));

Expand Down
62 changes: 48 additions & 14 deletions .vortex/installer/src/Prompts/PromptManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@
use DrevOps\VortexInstaller\Prompts\Handlers\Tools;
use DrevOps\VortexInstaller\Prompts\Handlers\VersionScheme;
use DrevOps\VortexInstaller\Prompts\Handlers\Webroot;
use DrevOps\VortexInstaller\Schema\SchemaValidator;
use DrevOps\VortexInstaller\Utils\Config;
use DrevOps\VortexInstaller\Utils\Converter;
use DrevOps\VortexInstaller\Utils\Env;
use DrevOps\VortexInstaller\Utils\Tui;
use Symfony\Component\Console\Output\OutputInterface;
use function Laravel\Prompts\confirm;
Expand Down Expand Up @@ -87,6 +87,15 @@ class PromptManager {
*/
protected array $handlers = [];

/**
* Prompt overrides from --prompts CLI option.
*
* Keyed by handler ID with validated values.
*
* @var array<string, mixed>
*/
protected array $promptOverrides = [];

/**
* PromptManager constructor.
*
Expand All @@ -97,6 +106,7 @@ public function __construct(
protected Config $config,
) {
$this->initHandlers();
$this->resolvePromptOverrides();
}

/**
Expand Down Expand Up @@ -577,6 +587,39 @@ protected function initHandlers(): void {
}
}

/**
* Resolve prompt overrides from --prompts CLI option.
*
* Reads the raw prompt array from Config, normalizes keys to handler IDs,
* validates values against handler types and options, and stores the
* validated overrides.
*
* @throws \RuntimeException
* If any prompt value is invalid.
*/
private function resolvePromptOverrides(): void {
$raw = $this->config->get(Config::PROMPTS);

if (!is_array($raw) || empty($raw)) {
return;
}

$validator = new SchemaValidator($this->handlers);
$result = $validator->validate($raw);

if (!empty($result['errors'])) {
$messages = array_map(fn(array $error): string => sprintf('%s: %s', $error['prompt'], $error['message']), $result['errors']);
throw new \RuntimeException(sprintf('Invalid --prompts values: %s', implode('; ', $messages)));
}

// Use the resolved values which include defaults for missing prompts.
foreach ($raw as $key => $value) {
if (isset($this->handlers[$key])) {
$this->promptOverrides[$key] = $value;
}
}
}

/**
* Dispatch a prompt using the handler's type enum.
*
Expand Down Expand Up @@ -646,22 +689,13 @@ private function args(string $handler_class, mixed $default_override = NULL, arr

// Find appropriate default value.
$default_from_handler = $handler->default($responses);
// Create the env var name.
$var_name = $handler::envName();
// Get from config.
$config_val = $this->config->get($var_name);
$default_from_config = is_null($config_val) ? NULL : $config_val;
// Get from env.
$env_val = Env::get($var_name);
$default_from_env = is_null($env_val) ? NULL : Env::toValue($env_val);
// Get from prompt overrides (--prompts CLI option).
$default_from_prompts = $this->promptOverrides[$id] ?? NULL;
// Get from discovery.
$default_from_discovery = $this->handlers[$id]->discover();

if (!is_null($default_from_config)) {
$default = $default_from_config;
}
elseif (!is_null($default_from_env)) {
$default = $default_from_env;
if (!is_null($default_from_prompts)) {
$default = $default_from_prompts;
}
elseif (!is_null($default_from_discovery)) {
$default = $default_from_discovery;
Expand Down
44 changes: 26 additions & 18 deletions .vortex/installer/src/Schema/AgentHelp.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,40 +31,48 @@ public static function render(): string {
available configuration prompts, their types, valid values, defaults, and
dependencies.

2. **Build a config**: Using the schema, construct a JSON object where keys are
either prompt IDs (e.g., `hosting_provider`) or environment variable names
(e.g., `VORTEX_INSTALLER_PROMPT_HOSTING_PROVIDER`). Set values according to
the prompt types and allowed options from the schema.
2. **Build prompt answers**: Using the schema, construct a JSON object where
keys are prompt IDs (the `id` field from the schema). Set values according
to the prompt types and allowed options.

3. **Validate the config**: Run with `--validate --config='<json>'` to check
your config without performing an installation. The output is a JSON object
with `valid`, `errors`, `warnings`, and `resolved` fields.
3. **Validate**: Run with `--validate --prompts='<json>'` to check your prompt
answers without performing an installation. The output is a JSON object with
`valid`, `errors`, `warnings`, and `resolved` fields.

4. **Install**: Run with `--no-interaction --config='<json>' --destination=<dir>`
to perform the actual installation using your validated config.
4. **Install**: Run with `--no-interaction --prompts='<json>' --destination=<dir>`
to perform the actual installation using your validated prompt answers.

## Commands

```bash
# Get the prompt schema
php installer.php --schema

# Validate a config (JSON string)
php installer.php --validate --config='{"name":"My Project","hosting_provider":"lagoon"}'
# Validate prompt answers (JSON string)
php installer.php --validate --prompts='{"name":"My Project","hosting_provider":"lagoon"}'

# Validate a config (JSON file)
php installer.php --validate --config=config.json
# Validate prompt answers (JSON file)
php installer.php --validate --prompts=prompts.json

# Install non-interactively
php installer.php --no-interaction --config='<json>' --destination=./my-project
php installer.php --no-interaction --prompts='<json>' --destination=./my-project
```

## Options

- `--prompts` (`-p`): JSON object of prompt answers, keyed by prompt ID.
Accepts a JSON string or a path to a JSON file.
- `--config` (`-c`): JSON object of installer configuration (repository, ref,
and other internal settings). Not for prompt answers.
- `--destination`: Target directory for installation.
- `--no-interaction` (`-n`): Non-interactive mode. Prompts without answers in
`--prompts` use discovered or default values.

## Schema Format

The `--schema` output contains a `prompts` array. Each prompt has:

- `id`: The prompt identifier (use as config key).
- `env`: The environment variable name (alternative config key).
- `id`: The prompt identifier (use as key in `--prompts` JSON).
- `type`: One of `text`, `select`, `multiselect`, `confirm`, `suggest`.
- `label`: Human-readable label.
- `description`: Optional description text.
Expand All @@ -80,7 +88,7 @@ public static function render(): string {

- `text` / `suggest`: string value.
- `select`: string value matching one of the option values.
- `multiselect`: array of strings, each matching an option value.
- `multiselect`: JSON array of strings, each matching an option value.
- `confirm`: boolean (`true` or `false`).

## Dependencies
Expand Down Expand Up @@ -110,7 +118,7 @@ public static function render(): string {
- Start with `--schema` to understand what prompts exist.
- Provide values only for prompts you want to customize; defaults will be
used for the rest.
- Use `--validate` to check your config before installing.
- Use `--validate` to check your prompt answers before installing.
- The `resolved` field in validation output shows the complete config that
would be used, including defaults.
AGENT_HELP;
Expand Down
15 changes: 11 additions & 4 deletions .vortex/installer/src/Schema/SchemaGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,32 @@
class SchemaGenerator {

/**
* Generate schema from handlers.
* Constructor.
*
* @param array<string, \DrevOps\VortexInstaller\Prompts\Handlers\HandlerInterface> $handlers
* An associative array of handler instances keyed by handler ID.
*/
public function __construct(
protected array $handlers,
) {
}

/**
* Generate schema from handlers.
*
* @return array<string, mixed>
* The schema structure with a 'prompts' key.
*/
public function generate(array $handlers): array {
public function generate(): array {
$prompts = [];

foreach ($handlers as $id => $handler) {
foreach ($this->handlers as $id => $handler) {
if (in_array($id, static::getExcludedHandlers(), TRUE)) {
continue;
}

$prompts[] = [
'id' => $handler::id(),
'env' => $handler::envName(),
'type' => $handler->type()->value,
'label' => $handler->label(),
'description' => $handler::description([]),
Expand Down
Loading
Loading