Skip to content

Commit f2e93fc

Browse files
committed
[#2302] Added support for prompts schema to make the installer AI-friendly.
1 parent 6749c7a commit f2e93fc

61 files changed

Lines changed: 1978 additions & 228 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.vortex/installer/src/Command/InstallCommand.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareInterface;
1515
use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareTrait;
1616
use DrevOps\VortexInstaller\Runner\RunnerInterface;
17+
use DrevOps\VortexInstaller\Schema\ConfigValidator;
18+
use DrevOps\VortexInstaller\Schema\SchemaGenerator;
1719
use DrevOps\VortexInstaller\Task\Task;
1820
use DrevOps\VortexInstaller\Utils\Config;
1921
use DrevOps\VortexInstaller\Utils\Env;
@@ -53,6 +55,12 @@ class InstallCommand extends Command implements CommandRunnerAwareInterface, Exe
5355

5456
const OPTION_BUILD = 'build';
5557

58+
const OPTION_SCHEMA = 'schema';
59+
60+
const OPTION_VALIDATE = 'validate';
61+
62+
const OPTION_AGENT_HELP = 'agent-help';
63+
5664
const BUILD_RESULT_SUCCESS = 'success';
5765

5866
const BUILD_RESULT_SKIPPED = 'skipped';
@@ -127,6 +135,9 @@ protected function configure(): void {
127135
$this->addOption(static::OPTION_URI, 'l', InputOption::VALUE_REQUIRED, 'Remote or local repository URI with an optional git ref set after @.');
128136
$this->addOption(static::OPTION_NO_CLEANUP, NULL, InputOption::VALUE_NONE, 'Do not remove installer after successful installation.');
129137
$this->addOption(static::OPTION_BUILD, 'b', InputOption::VALUE_NONE, 'Run auto-build after installation without prompting.');
138+
$this->addOption(static::OPTION_SCHEMA, NULL, InputOption::VALUE_NONE, 'Output prompt schema as JSON.');
139+
$this->addOption(static::OPTION_VALIDATE, NULL, InputOption::VALUE_NONE, 'Validate config without installing.');
140+
$this->addOption(static::OPTION_AGENT_HELP, NULL, InputOption::VALUE_NONE, 'Output instructions for AI agents on how to use the installer.');
130141
}
131142

132143
/**
@@ -139,6 +150,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int
139150
return Command::SUCCESS;
140151
}
141152

153+
if ($input->getOption(static::OPTION_AGENT_HELP)) {
154+
return $this->handleAgentHelp($output);
155+
}
156+
157+
if ($input->getOption(static::OPTION_SCHEMA)) {
158+
return $this->handleSchema($input, $output);
159+
}
160+
161+
if ($input->getOption(static::OPTION_VALIDATE)) {
162+
return $this->handleValidate($input, $output);
163+
}
164+
142165
Tui::init($output);
143166

144167
try {
@@ -266,6 +289,162 @@ protected function execute(InputInterface $input, OutputInterface $output): int
266289
return Command::SUCCESS;
267290
}
268291

292+
/**
293+
* Handle --schema option.
294+
*/
295+
protected function handleSchema(InputInterface $input, OutputInterface $output): int {
296+
$config = Config::fromString('{}');
297+
$prompt_manager = new PromptManager($config);
298+
299+
$generator = new SchemaGenerator();
300+
$schema = $generator->generate($prompt_manager->getHandlers());
301+
302+
$output->write((string) json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
303+
304+
return Command::SUCCESS;
305+
}
306+
307+
/**
308+
* Handle --validate option.
309+
*/
310+
protected function handleValidate(InputInterface $input, OutputInterface $output): int {
311+
$config_option = $input->getOption(static::OPTION_CONFIG);
312+
313+
if (empty($config_option) || !is_string($config_option)) {
314+
$output->writeln('The --validate option requires --config.');
315+
316+
return Command::FAILURE;
317+
}
318+
319+
$config_json = is_file($config_option) ? (string) file_get_contents($config_option) : $config_option;
320+
$user_config = json_decode($config_json, TRUE);
321+
322+
if (!is_array($user_config)) {
323+
$output->writeln('Invalid JSON in --config.');
324+
325+
return Command::FAILURE;
326+
}
327+
328+
$config = Config::fromString('{}');
329+
$prompt_manager = new PromptManager($config);
330+
331+
$validator = new ConfigValidator();
332+
$result = $validator->validate($user_config, $prompt_manager->getHandlers());
333+
334+
$output->write((string) json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
335+
336+
return $result['valid'] ? Command::SUCCESS : Command::FAILURE;
337+
}
338+
339+
/**
340+
* Handle --agent-help option.
341+
*
342+
* Outputs instructions for AI agents on how to use the installer
343+
* programmatically via --schema and --validate.
344+
*/
345+
protected function handleAgentHelp(OutputInterface $output): int {
346+
$text = <<<'AGENT_HELP'
347+
# Vortex Installer - AI Agent Instructions
348+
349+
You are interacting with the Vortex installer, a CLI tool that sets up Drupal
350+
projects from the Vortex template. This guide explains how to use the installer
351+
programmatically.
352+
353+
## Workflow
354+
355+
1. **Discover prompts**: Run with `--schema` to get a JSON manifest of all
356+
available configuration prompts, their types, valid values, defaults, and
357+
dependencies.
358+
359+
2. **Build a config**: Using the schema, construct a JSON object where keys are
360+
either prompt IDs (e.g., `hosting_provider`) or environment variable names
361+
(e.g., `VORTEX_INSTALLER_PROMPT_HOSTING_PROVIDER`). Set values according to
362+
the prompt types and allowed options from the schema.
363+
364+
3. **Validate the config**: Run with `--validate --config='<json>'` to check
365+
your config without performing an installation. The output is a JSON object
366+
with `valid`, `errors`, `warnings`, and `resolved` fields.
367+
368+
4. **Install**: Run with `--no-interaction --config='<json>' --destination=<dir>`
369+
to perform the actual installation using your validated config.
370+
371+
## Commands
372+
373+
```bash
374+
# Get the prompt schema
375+
php installer.php --schema
376+
377+
# Validate a config (JSON string)
378+
php installer.php --validate --config='{"name":"My Project","hosting_provider":"lagoon"}'
379+
380+
# Validate a config (JSON file)
381+
php installer.php --validate --config=config.json
382+
383+
# Install non-interactively
384+
php installer.php --no-interaction --config='<json>' --destination=./my-project
385+
```
386+
387+
## Schema Format
388+
389+
The `--schema` output contains a `prompts` array. Each prompt has:
390+
391+
- `id`: The prompt identifier (use as config key).
392+
- `env`: The environment variable name (alternative config key).
393+
- `type`: One of `text`, `select`, `multiselect`, `confirm`, `suggest`.
394+
- `label`: Human-readable label.
395+
- `description`: Optional description text.
396+
- `options`: For `select`/`multiselect`, an array of `{value, label}` objects
397+
representing the allowed values.
398+
- `default`: The default value if not provided.
399+
- `required`: Whether the prompt requires a value.
400+
- `depends_on`: Dependency conditions. If set, this prompt only applies when
401+
the referenced prompt has one of the specified values. A `_system` key
402+
indicates a system-state dependency (not config-based).
403+
404+
## Value Types by Prompt Type
405+
406+
- `text` / `suggest`: string value.
407+
- `select`: string value matching one of the option values.
408+
- `multiselect`: array of strings, each matching an option value.
409+
- `confirm`: boolean (`true` or `false`).
410+
411+
## Dependencies
412+
413+
Some prompts depend on other prompts. For example, `hosting_project_name`
414+
depends on `hosting_provider` being `lagoon` or `acquia`. If you set
415+
`hosting_provider` to `none`, you do not need to provide `hosting_project_name`.
416+
417+
When a dependency is not met:
418+
- Omitting the dependent value is OK (it will be skipped).
419+
- Providing a value triggers a warning (it will be ignored).
420+
421+
When a dependency is met:
422+
- Required prompts must have a value or they produce an error.
423+
424+
## Validation Output
425+
426+
The `--validate` output contains:
427+
428+
- `valid`: boolean - whether the config is valid.
429+
- `errors`: array of `{prompt, message}` objects for invalid values.
430+
- `warnings`: array of `{prompt, message}` objects for ignored values.
431+
- `resolved`: object with the final merged config (your values + defaults).
432+
433+
## Tips
434+
435+
- Start with `--schema` to understand what prompts exist.
436+
- Provide values only for prompts you want to customize; defaults will be
437+
used for the rest.
438+
- Use `--validate` to check your config before installing.
439+
- The `resolved` field in validation output shows the complete config that
440+
would be used, including defaults.
441+
AGENT_HELP;
442+
443+
$output->write($text);
444+
445+
return Command::SUCCESS;
446+
}
447+
269448
protected function checkRequirements(): void {
270449
$required_commands = [
271450
'git',

.vortex/installer/src/Prompts/Handlers/AbstractHandler.php

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace DrevOps\VortexInstaller\Prompts\Handlers;
66

7+
use DrevOps\VortexInstaller\Prompts\PromptType;
78
use DrevOps\VortexInstaller\Utils\Config;
89
use DrevOps\VortexInstaller\Utils\Converter;
910

@@ -61,6 +62,49 @@ public static function id(): string {
6162
return Converter::machine(Converter::pascal2snake(str_replace('Handler', '', basename($filename, '.php'))));
6263
}
6364

65+
/**
66+
* {@inheritdoc}
67+
*/
68+
public static function envName(): string {
69+
return Converter::constant('VORTEX_INSTALLER_PROMPT_' . static::id());
70+
}
71+
72+
/**
73+
* {@inheritdoc}
74+
*/
75+
public function type(): PromptType {
76+
$options = $this->options([]);
77+
78+
if (is_array($options)) {
79+
if (array_is_list($options)) {
80+
return PromptType::Suggest;
81+
}
82+
83+
$default = $this->default([]);
84+
85+
if (is_array($default)) {
86+
return PromptType::MultiSelect;
87+
}
88+
89+
return PromptType::Select;
90+
}
91+
92+
$default = $this->default([]);
93+
94+
if (is_bool($default)) {
95+
return PromptType::Confirm;
96+
}
97+
98+
return PromptType::Text;
99+
}
100+
101+
/**
102+
* {@inheritdoc}
103+
*/
104+
public function dependsOn(): ?array {
105+
return NULL;
106+
}
107+
64108
/**
65109
* {@inheritdoc}
66110
*/

.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,13 @@ public function options(array $responses): ?array {
6464
return $options;
6565
}
6666

67+
/**
68+
* {@inheritdoc}
69+
*/
70+
public function dependsOn(): ?array {
71+
return [ProvisionType::id() => [ProvisionType::DATABASE]];
72+
}
73+
6774
/**
6875
* {@inheritdoc}
6976
*/

.vortex/installer/src/Prompts/Handlers/DatabaseImage.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@ public function placeholder(array $responses): ?string {
4040
return parent::placeholder($responses);
4141
}
4242

43+
/**
44+
* {@inheritdoc}
45+
*/
46+
public function dependsOn(): ?array {
47+
return [DatabaseDownloadSource::id() => [DatabaseDownloadSource::CONTAINER_REGISTRY]];
48+
}
49+
4350
/**
4451
* {@inheritdoc}
4552
*/

.vortex/installer/src/Prompts/Handlers/HandlerInterface.php

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace DrevOps\VortexInstaller\Prompts\Handlers;
66

7+
use DrevOps\VortexInstaller\Prompts\PromptType;
8+
79
/**
810
* Interface HandlerInterface.
911
*
@@ -13,11 +15,49 @@
1315
*/
1416
interface HandlerInterface {
1517

18+
/**
19+
* Reserved dependency key for system-state conditions.
20+
*/
21+
const DEPENDS_ON_SYSTEM = '_system';
22+
23+
/**
24+
* Reserved dependency value for fresh install condition.
25+
*/
26+
const DEPENDS_ON_FRESH_INSTALL = '_fresh_install';
27+
1628
/**
1729
* The unique identifier of the handler.
1830
*/
1931
public static function id(): string;
2032

33+
/**
34+
* Get the environment variable name for this handler.
35+
*
36+
* @return string
37+
* The environment variable name in the format VORTEX_INSTALLER_PROMPT_*.
38+
*/
39+
public static function envName(): string;
40+
41+
/**
42+
* Get the prompt type for this handler.
43+
*
44+
* @return \DrevOps\VortexInstaller\Prompts\PromptType
45+
* The prompt type enum case.
46+
*/
47+
public function type(): PromptType;
48+
49+
/**
50+
* Get dependency conditions for this handler.
51+
*
52+
* Returns an associative array where keys are handler IDs and values are
53+
* arrays of acceptable values. The handler should only run when the
54+
* dependency handler's response matches one of the acceptable values.
55+
*
56+
* @return array<string, array<mixed>>|null
57+
* The dependency conditions, or NULL if no dependencies.
58+
*/
59+
public function dependsOn(): ?array;
60+
2161
/**
2262
* Label for of the handler.
2363
*

.vortex/installer/src/Prompts/Handlers/HostingProjectName.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ public function isRequired(): bool {
3838
return TRUE;
3939
}
4040

41+
/**
42+
* {@inheritdoc}
43+
*/
44+
public function dependsOn(): ?array {
45+
return [HostingProvider::id() => [HostingProvider::LAGOON, HostingProvider::ACQUIA]];
46+
}
47+
4148
/**
4249
* {@inheritdoc}
4350
*/

.vortex/installer/src/Prompts/Handlers/MigrationDownloadSource.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,13 @@ public function options(array $responses): ?array {
5858
return $options;
5959
}
6060

61+
/**
62+
* {@inheritdoc}
63+
*/
64+
public function dependsOn(): ?array {
65+
return [Migration::id() => [TRUE]];
66+
}
67+
6168
/**
6269
* {@inheritdoc}
6370
*/

0 commit comments

Comments
 (0)