From 032882814ba8488df321533eb13d2144e1c7c78b Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Wed, 16 Jul 2025 07:01:48 +1000 Subject: [PATCH] Refactored installer prompts into decoupled handlers. --- .vortex/installer/composer.lock | 84 +-- .../installer/src/Command/InstallCommand.php | 14 +- .../src/Prompts/Handlers/AbstractHandler.php | 120 +++- .../Prompts/Handlers/AiCodeInstructions.php | 31 + .../src/Prompts/Handlers/AssignAuthorPr.php | 21 + .../src/Prompts/Handlers/CiProvider.php | 38 ++ .../src/Prompts/Handlers/CodeProvider.php | 31 + .../Handlers/DatabaseDownloadSource.php | 62 ++ .../src/Prompts/Handlers/DatabaseImage.php | 73 +++ .../Handlers/DependencyUpdatesProvider.php | 32 + .../src/Prompts/Handlers/DeployType.php | 56 ++ .../installer/src/Prompts/Handlers/Domain.php | 54 ++ .../src/Prompts/Handlers/GithubRepo.php | 59 ++ .../src/Prompts/Handlers/GithubToken.php | 77 +++ .../src/Prompts/Handlers/HandlerInterface.php | 130 +++- .../src/Prompts/Handlers/HostingProvider.php | 40 ++ .../src/Prompts/Handlers/Internal.php | 7 + .../Handlers/LabelMergeConflictsPr.php | 21 + .../src/Prompts/Handlers/MachineName.php | 53 ++ .../src/Prompts/Handlers/ModulePrefix.php | 53 ++ .../installer/src/Prompts/Handlers/Name.php | 51 ++ .../installer/src/Prompts/Handlers/Org.php | 54 ++ .../src/Prompts/Handlers/OrgMachineName.php | 54 ++ .../Handlers/PreserveDocsOnboarding.php | 21 + .../Prompts/Handlers/PreserveDocsProject.php | 21 + .../src/Prompts/Handlers/Profile.php | 94 ++- .../src/Prompts/Handlers/ProfileCustom.php | 86 +++ .../src/Prompts/Handlers/ProvisionType.php | 31 + .../src/Prompts/Handlers/Services.php | 32 + .../installer/src/Prompts/Handlers/Theme.php | 46 ++ .../src/Prompts/Handlers/Webroot.php | 84 +++ .../installer/src/Prompts/PromptManager.php | 597 +++++++----------- .vortex/installer/src/Utils/Env.php | 2 +- .vortex/installer/tests/Unit/EnvTest.php | 2 +- .../tests/Unit/PromptManagerTest.php | 46 +- 35 files changed, 1778 insertions(+), 499 deletions(-) create mode 100644 .vortex/installer/src/Prompts/Handlers/ProfileCustom.php diff --git a/.vortex/installer/composer.lock b/.vortex/installer/composer.lock index 6afd2b253..85cc18cdd 100644 --- a/.vortex/installer/composer.lock +++ b/.vortex/installer/composer.lock @@ -239,16 +239,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.5", + "version": "v0.3.6", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1" + "reference": "86a8b692e8661d0fb308cec64f3d176821323077" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/57b8f7efe40333cdb925700891c7d7465325d3b1", - "reference": "57b8f7efe40333cdb925700891c7d7465325d3b1", + "url": "https://api.github.com/repos/laravel/prompts/zipball/86a8b692e8661d0fb308cec64f3d176821323077", + "reference": "86a8b692e8661d0fb308cec64f3d176821323077", "shasum": "" }, "require": { @@ -292,9 +292,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.5" + "source": "https://github.com/laravel/prompts/tree/v0.3.6" }, - "time": "2025-02-11T13:34:40+00:00" + "time": "2025-07-07T14:17:42+00:00" }, { "name": "psr/container", @@ -2325,16 +2325,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.13.1", + "version": "1.13.3", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", - "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36", + "reference": "faed855a7b5f4d4637717c2b3863e277116beb36", "shasum": "" }, "require": { @@ -2373,7 +2373,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.3" }, "funding": [ { @@ -2381,7 +2381,7 @@ "type": "tidelift" } ], - "time": "2025-04-29T12:36:36+00:00" + "time": "2025-07-05T12:25:42+00:00" }, { "name": "nikic/php-parser", @@ -2561,16 +2561,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { @@ -2602,9 +2602,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2025-02-19T13:28:12+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "phpstan/phpstan", @@ -3001,16 +3001,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.25", + "version": "11.5.27", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "864ab32b3ff52058f917c5b19b3cef821e4a4f1b" + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/864ab32b3ff52058f917c5b19b3cef821e4a4f1b", - "reference": "864ab32b3ff52058f917c5b19b3cef821e4a4f1b", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/446d43867314781df7e9adf79c3ec7464956fd8f", + "reference": "446d43867314781df7e9adf79c3ec7464956fd8f", "shasum": "" }, "require": { @@ -3020,7 +3020,7 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.13.1", + "myclabs/deep-copy": "^1.13.3", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", "php": ">=8.2", @@ -3082,7 +3082,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.25" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.27" }, "funding": [ { @@ -3106,20 +3106,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T04:36:07+00:00" + "time": "2025-07-11T04:10:06+00:00" }, { "name": "rector/rector", - "version": "2.1.0", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/rectorphp/rector.git", - "reference": "d513dea45a94394b660e15c155d1fa27826f8e30" + "reference": "d0917c069bb0d9bb06ed111cf052510f609015a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/d513dea45a94394b660e15c155d1fa27826f8e30", - "reference": "d513dea45a94394b660e15c155d1fa27826f8e30", + "url": "https://api.github.com/repos/rectorphp/rector/zipball/d0917c069bb0d9bb06ed111cf052510f609015a4", + "reference": "d0917c069bb0d9bb06ed111cf052510f609015a4", "shasum": "" }, "require": { @@ -3158,7 +3158,7 @@ ], "support": { "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.1.0" + "source": "https://github.com/rectorphp/rector/tree/2.1.1" }, "funding": [ { @@ -3166,7 +3166,7 @@ "type": "github" } ], - "time": "2025-06-24T20:26:57+00:00" + "time": "2025-07-10T11:31:31+00:00" }, { "name": "sebastian/cli-parser", @@ -4098,16 +4098,16 @@ }, { "name": "slevomat/coding-standard", - "version": "8.19.0", + "version": "8.19.1", "source": { "type": "git", "url": "https://github.com/slevomat/coding-standard.git", - "reference": "9cc50509c3912d465a9f6898069f64cf2abf4e8a" + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/9cc50509c3912d465a9f6898069f64cf2abf4e8a", - "reference": "9cc50509c3912d465a9f6898069f64cf2abf4e8a", + "url": "https://api.github.com/repos/slevomat/coding-standard/zipball/458d665acd49009efebd7e0cb385d71ae9ac3220", + "reference": "458d665acd49009efebd7e0cb385d71ae9ac3220", "shasum": "" }, "require": { @@ -4147,7 +4147,7 @@ ], "support": { "issues": "https://github.com/slevomat/coding-standard/issues", - "source": "https://github.com/slevomat/coding-standard/tree/8.19.0" + "source": "https://github.com/slevomat/coding-standard/tree/8.19.1" }, "funding": [ { @@ -4159,20 +4159,20 @@ "type": "tidelift" } ], - "time": "2025-06-07T15:02:20+00:00" + "time": "2025-06-09T17:53:57+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.0", + "version": "3.13.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186" + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/65ff2489553b83b4597e89c3b8b721487011d186", - "reference": "65ff2489553b83b4597e89c3b8b721487011d186", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", + "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", "shasum": "" }, "require": { @@ -4243,7 +4243,7 @@ "type": "thanks_dev" } ], - "time": "2025-05-11T03:36:00+00:00" + "time": "2025-06-17T22:17:01+00:00" }, { "name": "staabm/side-effects-detector", diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index fc7461951..41fc40412 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -102,7 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int static::header(); - $pm->prompt(); + $pm->runPrompts(); Tui::list($pm->getResponsesSummary(), 'Installation summary'); @@ -116,36 +116,36 @@ protected function execute(InputInterface $input, OutputInterface $output): int Tui::action( label: '⬇️ Downloading Vortex', - hint: fn(): string => sprintf('Downloading from "%s" repository at commit "%s"', ...Downloader::parseUri($this->config->get(Config::REPO))), - success: 'Vortex downloaded', action: function (): void { $version = (new Downloader())->download($this->config->get(Config::REPO), $this->config->get(Config::REF), $this->config->get(Config::TMP)); $this->config->set(Config::VERSION, $version); }, + hint: fn(): string => sprintf('Downloading from "%s" repository at commit "%s"', ...Downloader::parseUri($this->config->get(Config::REPO))), + success: 'Vortex downloaded', ); Tui::action( label: 'βš™οΈ Customizing Vortex for your project', + action: fn() => $pm->runProcessors(), success: 'Vortex was customized for your project', - action: fn() => $pm->process(), ); Tui::action( label: 'πŸ₯£ Preparing destination directory', - success: 'Destination directory is ready', action: fn(): array => $this->prepareDestination(), + success: 'Destination directory is ready', ); Tui::action( label: '➑️ Copying files to the destination directory', - success: 'Files copied to destination directory', action: fn() => $this->copyFiles(), + success: 'Files copied to destination directory', ); Tui::action( label: '🎬 Preparing demo content', - success: 'Demo content prepared', action: fn(): string|array => $this->handleDemo(), + success: 'Demo content prepared', ); // @todo Implement the demo mode. diff --git a/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php b/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php index c6a8c5882..77fd64dba 100644 --- a/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php +++ b/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php @@ -20,7 +20,7 @@ abstract class AbstractHandler implements HandlerInterface { protected string $tmpDir; /** - * The webroot directory name. + * The webroot directory. */ protected string $webroot; @@ -37,9 +37,11 @@ abstract class AbstractHandler implements HandlerInterface { protected array $responses; /** - * The configuration object. + * Constructor. */ - public function __construct(protected Config $config) { + public function __construct( + protected Config $config, + ) { $this->dstDir = $this->config->getDst(); $this->tmpDir = $this->config->get(Config::TMP); } @@ -47,53 +49,119 @@ public function __construct(protected Config $config) { /** * {@inheritdoc} */ - public function setWebroot(string $webroot): static { - $this->webroot = $webroot; + public static function id(): string { + $reflector = new \ReflectionClass(static::class); - return $this; + $filename = $reflector->getFileName(); + + if ($filename === FALSE) { + throw new \RuntimeException(sprintf('Could not determine the filename of the handler class %s.', static::class)); + } + + return Converter::machine(Converter::pascal2snake(str_replace('Handler', '', basename($filename, '.php')))); } /** * {@inheritdoc} */ - public function setResponses(array $responses): static { - $this->responses = $responses; - $this->setWebroot($responses[Webroot::id()] ?? Webroot::WEB); - // Set the response for current handler as a shorthand. - // Some handlers may want to perform an action on the empty responses, so - // it is up to the handler's processor to check for the presence of the - // value in a set response. - $this->response = $this->responses[static::id()] ?? NULL; + public static function description(array $responses): ?string { + return NULL; + } - return $this; + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return NULL; } /** * {@inheritdoc} - * - * Automatically generate the handler ID based on the class name. */ - public static function id(): string { - $reflector = new \ReflectionClass(static::class); + public function placeholder(array $responses): ?string { + return NULL; + } - $filename = $reflector->getFileName(); + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return FALSE; + } - if ($filename === FALSE) { - throw new \RuntimeException(sprintf('Could not determine the filename of the handler class %s.', static::class)); - } + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return NULL; + } - return Converter::machine(Converter::pascal2snake(str_replace('Handler', '', basename($filename, '.php')))); + /** + * {@inheritdoc} + */ + public function shouldRun(array $responses): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function resolvedValue(array $responses): null|string|bool|array { + return NULL; } /** * {@inheritdoc} */ - abstract public function discover(): null|string|bool|array; + public function resolvedMessage(array $responses): ?string { + return NULL; + } /** * {@inheritdoc} */ - abstract public function process(): void; + public function setResponses(array $responses): static { + $this->responses = $responses; + $this->setWebroot($responses[Webroot::id()] ?? Webroot::WEB); + + // Set the response for the current handler as a shorthand. + // Some handlers may want to perform an action on the empty responses, so + // it is up to the handler's processor to check for the presence of the + // value in a set response. + $this->response = $this->responses[static::id()] ?? NULL; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setWebroot(string $webroot): static { + $this->webroot = $webroot; + + return $this; + } /** * Check that Vortex is installed for this project. diff --git a/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php b/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php index a5ed1fb28..26306f94b 100644 --- a/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php +++ b/.vortex/installer/src/Prompts/Handlers/AiCodeInstructions.php @@ -10,6 +10,37 @@ class AiCodeInstructions extends AbstractHandler { const CLAUDE = 'claude'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ€– AI code assistant instructions'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Helps AI coding assistants to understand the project better.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::CLAUDE => 'Anthropic Claude', + self::NONE => 'None', + ]; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return self::NONE; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php b/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php index 9acabf4ad..c9ce23078 100644 --- a/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php +++ b/.vortex/installer/src/Prompts/Handlers/AssignAuthorPr.php @@ -6,6 +6,27 @@ class AssignAuthorPr extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ‘€ Auto-assign the author to their PR?'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Helps to keep the PRs organized.'; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return TRUE; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/CiProvider.php b/.vortex/installer/src/Prompts/Handlers/CiProvider.php index a3618367e..d0b71652a 100644 --- a/.vortex/installer/src/Prompts/Handlers/CiProvider.php +++ b/.vortex/installer/src/Prompts/Handlers/CiProvider.php @@ -14,6 +14,44 @@ class CiProvider extends AbstractHandler { const CIRCLECI = 'circleci'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ”„ Continuous Integration provider'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Both providers support equivalent workflow.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + $options = [ + self::NONE => 'None', + self::GITHUB_ACTIONS => 'GitHub Actions', + self::CIRCLECI => 'CircleCI', + ]; + + if (isset($responses[CodeProvider::id()]) && $responses[CodeProvider::id()] !== CodeProvider::GITHUB) { + unset($options[self::GITHUB_ACTIONS]); + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return self::GITHUB_ACTIONS; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/CodeProvider.php b/.vortex/installer/src/Prompts/Handlers/CodeProvider.php index e254e354a..e161bfcc0 100644 --- a/.vortex/installer/src/Prompts/Handlers/CodeProvider.php +++ b/.vortex/installer/src/Prompts/Handlers/CodeProvider.php @@ -12,6 +12,37 @@ class CodeProvider extends AbstractHandler { const OTHER = 'other'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ—„οΈ Repository provider'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Vortex offers full automation with GitHub, while support for other providers is limited.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::GITHUB => 'GitHub', + self::OTHER => 'Other', + ]; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return self::GITHUB; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php b/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php index 30c041c9b..688acfe2b 100644 --- a/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php +++ b/.vortex/installer/src/Prompts/Handlers/DatabaseDownloadSource.php @@ -21,6 +21,68 @@ class DatabaseDownloadSource extends AbstractHandler { const NONE = 'none'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ“‘ Database source'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'The database can be downloaded as an exported dump file or pre-packaged in a container image.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + $options = [ + self::URL => '🌍 URL download', + self::FTP => 'πŸ“‚ FTP download', + self::ACQUIA => 'πŸ’§ Acquia backup', + self::LAGOON => '🌊 Lagoon environment', + self::CONTAINER_REGISTRY => '🐳 Container registry', + self::NONE => 'πŸ™… None', + ]; + + if (isset($responses[HostingProvider::id()])) { + if ($responses[HostingProvider::id()] === HostingProvider::ACQUIA) { + unset($options[self::LAGOON]); + } + + if ($responses[HostingProvider::id()] === HostingProvider::LAGOON) { + unset($options[self::ACQUIA]); + } + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function shouldRun(array $responses): bool { + return isset($responses[ProvisionType::id()]) && $responses[ProvisionType::id()] !== ProvisionType::PROFILE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[HostingProvider::id()])) { + return match ($responses[HostingProvider::id()]) { + HostingProvider::ACQUIA => self::ACQUIA, + HostingProvider::LAGOON => self::LAGOON, + default => self::URL, + }; + } + + return self::URL; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php b/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php index 65a7b0972..5cab273e7 100644 --- a/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php +++ b/.vortex/installer/src/Prompts/Handlers/DatabaseImage.php @@ -4,11 +4,70 @@ namespace DrevOps\VortexInstaller\Prompts\Handlers; +use DrevOps\VortexInstaller\Utils\Converter; use DrevOps\VortexInstaller\Utils\Env; use DrevOps\VortexInstaller\Utils\File; +use DrevOps\VortexInstaller\Utils\Validator; class DatabaseImage extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🏷️ What is your database container image name and a tag?'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Use "latest" tag for the latest version. CI will be building this image overnight.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + // Generate placeholder from OrgMachineName and MachineName if available. + if (isset($responses[OrgMachineName::id()]) && isset($responses[MachineName::id()]) + && !empty($responses[OrgMachineName::id()]) && !empty($responses[MachineName::id()])) { + return sprintf('E.g. %s/%s-data:latest', + Converter::phpNamespace($responses[OrgMachineName::id()]), + Converter::phpNamespace($responses[MachineName::id()]) + ); + } + + return parent::placeholder($responses); + } + + /** + * {@inheritdoc} + */ + public function shouldRun(array $responses): bool { + return $responses[DatabaseDownloadSource::id()] === DatabaseDownloadSource::CONTAINER_REGISTRY; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if ( + isset($responses[OrgMachineName::id()]) && + isset($responses[MachineName::id()]) && + !empty($responses[OrgMachineName::id()]) && + !empty($responses[MachineName::id()]) + ) { + return sprintf( + '%s/%s-data:latest', + Converter::phpNamespace($responses[OrgMachineName::id()]), + Converter::phpNamespace($responses[MachineName::id()]) + ); + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -16,6 +75,20 @@ public function discover(): null|string|bool|array { return Env::getFromDotenv('VORTEX_DB_IMAGE', $this->dstDir); } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Validator::containerImage($v) ? NULL : 'Please enter a valid container image name with an optional tag.'; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn($v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php b/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php index f4e0bdf2f..fac20a505 100644 --- a/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php +++ b/.vortex/installer/src/Prompts/Handlers/DependencyUpdatesProvider.php @@ -14,6 +14,38 @@ class DependencyUpdatesProvider extends AbstractHandler { const RENOVATEBOT_APP = 'renovatebot_app'; + /** + * {@inheritdoc} + */ + public function label(): string { + return '⬆️ Dependency updates provider'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Use a self-hosted service if you cannot install a GitHub app.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::RENOVATEBOT_CI => 'πŸ€– + πŸ”„ Renovate self-hosted in CI', + self::RENOVATEBOT_APP => 'πŸ€– Renovate GitHub app', + self::NONE => '🚫 None', + ]; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return self::RENOVATEBOT_CI; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/DeployType.php b/.vortex/installer/src/Prompts/Handlers/DeployType.php index 6fa6391da..3245a1db2 100644 --- a/.vortex/installer/src/Prompts/Handlers/DeployType.php +++ b/.vortex/installer/src/Prompts/Handlers/DeployType.php @@ -18,6 +18,62 @@ class DeployType extends AbstractHandler { const WEBHOOK = 'webhook'; + /** + * {@inheritdoc} + */ + public function label(): string { + return '🚚 Deployment types'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'You can deploy code using one or more methods.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + $options = [ + self::ARTIFACT => 'πŸ“¦ Code artifact', + self::LAGOON => '🌊 Lagoon webhook', + self::CONTAINER_IMAGE => '🐳 Container image', + self::WEBHOOK => '🌐 Custom webhook', + ]; + + // Remove Lagoon option for Acquia hosting. + if (isset($responses[HostingProvider::id()]) && $responses[HostingProvider::id()] === HostingProvider::ACQUIA) { + unset($options[self::LAGOON]); + } + + return $options; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + $defaults = []; + + if (isset($responses[HostingProvider::id()])) { + if ($responses[HostingProvider::id()] === HostingProvider::LAGOON) { + $defaults[] = self::LAGOON; + } + + if ($responses[HostingProvider::id()] === HostingProvider::ACQUIA) { + $defaults[] = self::ARTIFACT; + } + } + + if (empty($defaults)) { + $defaults[] = self::WEBHOOK; + } + + return $defaults; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Domain.php b/.vortex/installer/src/Prompts/Handlers/Domain.php index d857b149d..986e9fb2d 100644 --- a/.vortex/installer/src/Prompts/Handlers/Domain.php +++ b/.vortex/installer/src/Prompts/Handlers/Domain.php @@ -7,9 +7,49 @@ use DrevOps\VortexInstaller\Utils\Converter; use DrevOps\VortexInstaller\Utils\Env; use DrevOps\VortexInstaller\Utils\File; +use DrevOps\VortexInstaller\Utils\Validator; class Domain extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🌐 Public domain'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Domain name without protocol and trailing slash.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. example.com'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[MachineName::id()]) && !empty($responses[MachineName::id()])) { + return Converter::kebab($responses[MachineName::id()]) . '.com'; + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -23,6 +63,20 @@ public function discover(): null|string|bool|array { return NULL; } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Validator::domain($v) ? NULL : 'Please enter a valid domain name.'; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => Converter::domain($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/GithubRepo.php b/.vortex/installer/src/Prompts/Handlers/GithubRepo.php index 48568cb27..42d331892 100644 --- a/.vortex/installer/src/Prompts/Handlers/GithubRepo.php +++ b/.vortex/installer/src/Prompts/Handlers/GithubRepo.php @@ -5,9 +5,54 @@ namespace DrevOps\VortexInstaller\Prompts\Handlers; use DrevOps\VortexInstaller\Utils\Git; +use DrevOps\VortexInstaller\Utils\Validator; class GithubRepo extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🏷️ What is your GitHub project name?'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name to create new or find an existing repository.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. myorg/myproject'; + } + + /** + * {@inheritdoc} + */ + public function shouldRun(array $responses): bool { + return !empty($responses[GithubToken::id()]); + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if ( + isset($responses[OrgMachineName::id()]) && + isset($responses[MachineName::id()]) && + !empty($responses[OrgMachineName::id()]) && + !empty($responses[MachineName::id()]) + ) { + return $responses[OrgMachineName::id()] . '/' . $responses[MachineName::id()]; + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -28,6 +73,20 @@ public function discover(): null|string|bool|array { return Git::extractOwnerRepo($remote); } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn(string $v): ?string => !empty($v) && !Validator::githubProject($v) ? 'Please enter a valid project name in the format "myorg/myproject"' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/GithubToken.php b/.vortex/installer/src/Prompts/Handlers/GithubToken.php index 679d8bc67..d20f9454a 100644 --- a/.vortex/installer/src/Prompts/Handlers/GithubToken.php +++ b/.vortex/installer/src/Prompts/Handlers/GithubToken.php @@ -8,6 +8,37 @@ class GithubToken extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + if (!empty($this->discover())) { + return 'GitHub access token is already set in the environment.'; + } + return 'πŸ”‘ GitHub access token (optional)'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return Env::get('GITHUB_TOKEN') ? 'Read from GITHUB_TOKEN environment variable.' : 'Create a new token with "repo" scopes at https://github.com/settings/tokens/new'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. ghp_1234567890'; + } + + /** + * {@inheritdoc} + */ + public function shouldRun(array $responses): bool { + return $responses[CodeProvider::id()] === CodeProvider::GITHUB; + } + /** * {@inheritdoc} */ @@ -15,6 +46,38 @@ public function discover(): null|string|bool|array { return Env::getFromDotenv('GITHUB_TOKEN', $this->dstDir); } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => !empty($v) && !str_starts_with($v, 'ghp_') ? 'Please enter a valid token starting with "ghp_"' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + + /** + * {@inheritdoc} + */ + public function resolvedValue(array $responses): null|string|bool|array { + $discovered = $this->discover(); + if (!empty($discovered)) { + return $discovered; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function resolvedMessage(array $responses): ?string { + return 'GitHub access token is already set in the environment.'; + } + /** * {@inheritdoc} */ @@ -22,4 +85,18 @@ public function process(): void { // @todo Implement this. } + /** + * Get the informational note for GitHub token requirement. + */ + public static function description(array $responses): ?string { + return "We need a token to create repositories and manage webhooks.\nIt won't be saved anywhere in the file system.\nYou may skip entering the token, but then Vortex will have to skip several operations."; + } + + /** + * Get the discovered value for display. + */ + public function getDiscoveredValue(): string { + return ''; + } + } diff --git a/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php b/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php index c7a07fc46..820b0e1b2 100644 --- a/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php +++ b/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php @@ -18,6 +18,85 @@ interface HandlerInterface { */ public static function id(): string; + /** + * Label for of the handler. + * + * @return string + * The label for the handler. + */ + public function label(): string; + + /** + * Optional description for the handler. + * + * @param array $responses + * Array of collected responses. + * + * @return string|null + * The description text, or NULL if not applicable. + */ + public static function description(array $responses): ?string; + + /** + * Optional hint for the handler. + * + * @param array $responses + * Array of collected responses. + * + * @return string|null + * The hint text for the handler, or NULL if none. + */ + public function hint(array $responses): ?string; + + /** + * Optional placeholder. + * + * @return string|null + * The placeholder text for the handler, or NULL if none. + */ + public function placeholder(array $responses): ?string; + + /** + * Get whether the handler's value is required. + * + * @return bool + * TRUE if the handler is required, FALSE otherwise. + */ + public function isRequired(): bool; + + /** + * Get the options for select/multiselect handlers. + * + * @param array $responses + * Array of collected responses. + * + * @return array|null + * The options array, or null if not applicable. + */ + public function options(array $responses): ?array; + + /** + * Check if the handler should run based on collected responses. + * + * @param array $responses + * Array of collected responses. + * + * @return bool + * The condition callback, or null if not conditional. + */ + public function shouldRun(array $responses): bool; + + /** + * The default value for the handler. + * + * @param array $responses + * Array of collected responses. + * + * @return string|bool|array|null + * The default value for the handler. + */ + public function default(array $responses): null|string|bool|array; + /** * Discover the value from the environment. * @@ -27,9 +106,51 @@ public static function id(): string; public function discover(): null|string|bool|array; /** - * Process the discovered value once all the responses are collected. + * The validate callback. + * + * @return callable|null + * The validate callback, or null if none. */ - public function process(): void; + public function validate(): ?callable; + + /** + * The transform callback. + * + * @return callable|null + * The transform callback, or null if none. + */ + public function transform(): ?callable; + + /** + * Get a resolved value if this handler's value is already determined. + * + * If this returns a non-empty value, the caller should use this value + * instead of prompting the user for input. This allows handlers to + * encapsulate logic for when values are discovered from environment, + * auto-selected based on other responses, or otherwise pre-determined. + * + * @param array $responses + * Current form responses for context-aware resolution. + * + * @return string|bool|array|null + * The resolved value if determined, null/empty if user input is needed. + */ + public function resolvedValue(array $responses): null|string|bool|array; + + /** + * Get a message to display when showing the resolved value. + * + * This is used by handlerManager to show an appropriate message (via + * info(), ok(), etc.) when using a resolved value instead of handlering + * for input. + * + * @param array $responses + * Current form responses for context-aware message generation. + * + * @return string|null + * The message to display, or null if no message needed. + */ + public function resolvedMessage(array $responses): ?string; /** * Set the responses. @@ -51,4 +172,9 @@ public function setResponses(array $responses): static; */ public function setWebroot(string $webroot): static; + /** + * Process the discovered value once all the responses are collected. + */ + public function process(): void; + } diff --git a/.vortex/installer/src/Prompts/Handlers/HostingProvider.php b/.vortex/installer/src/Prompts/Handlers/HostingProvider.php index 8f8132603..4d15fd832 100644 --- a/.vortex/installer/src/Prompts/Handlers/HostingProvider.php +++ b/.vortex/installer/src/Prompts/Handlers/HostingProvider.php @@ -17,6 +17,46 @@ class HostingProvider extends AbstractHandler { const OTHER = 'other'; + /** + * {@inheritdoc} + */ + public function label(): string { + return '☁️ Hosting provider'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Select the hosting provider where the project is hosted. The web root directory will be set accordingly.'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::ACQUIA => 'πŸ’§ Acquia Cloud', + self::LAGOON => '🌊 Lagoon', + self::OTHER => '🧩 Other', + self::NONE => '🚫 None', + ]; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return 'none'; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Internal.php b/.vortex/installer/src/Prompts/Handlers/Internal.php index 70916acbd..27bf01370 100644 --- a/.vortex/installer/src/Prompts/Handlers/Internal.php +++ b/.vortex/installer/src/Prompts/Handlers/Internal.php @@ -11,6 +11,13 @@ class Internal extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return 'Internal processing'; + } + public function discover(): null|string|bool|array { // Noop. return NULL; diff --git a/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php b/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php index f18925632..2aca3e824 100644 --- a/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php +++ b/.vortex/installer/src/Prompts/Handlers/LabelMergeConflictsPr.php @@ -6,6 +6,27 @@ class LabelMergeConflictsPr extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🎫 Auto-add a CONFLICT label to a PR when conflicts occur?'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Helps to keep quickly identify PRs that need attention.'; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return TRUE; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/MachineName.php b/.vortex/installer/src/Prompts/Handlers/MachineName.php index 55624b846..e0875d62c 100644 --- a/.vortex/installer/src/Prompts/Handlers/MachineName.php +++ b/.vortex/installer/src/Prompts/Handlers/MachineName.php @@ -10,6 +10,45 @@ class MachineName extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🏷️ Site machine name'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name for the project directory and in the code.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. my_site'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[Name::id()]) && !empty($responses[Name::id()])) { + return Converter::machineExtended($responses[Name::id()]); + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -23,6 +62,20 @@ public function discover(): null|string|bool|array { return NULL; } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php b/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php index 459329e36..c78326a5e 100644 --- a/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php +++ b/.vortex/installer/src/Prompts/Handlers/ModulePrefix.php @@ -9,6 +9,45 @@ class ModulePrefix extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🧩 Module prefix'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name for custom modules.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. ms (for My Site)'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[MachineName::id()]) && !empty($responses[MachineName::id()])) { + return Converter::abbreviation(Converter::machine($responses[MachineName::id()]), 4, ['_']); + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -27,6 +66,20 @@ public function discover(): null|string|bool|array { return empty($path) ? NULL : str_replace('_base', '', basename($path)); } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Converter::machine($v) !== $v ? 'Please enter a valid module prefix: only lowercase letters, numbers, and underscores are allowed.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Name.php b/.vortex/installer/src/Prompts/Handlers/Name.php index bc3340ad5..55d424157 100644 --- a/.vortex/installer/src/Prompts/Handlers/Name.php +++ b/.vortex/installer/src/Prompts/Handlers/Name.php @@ -5,10 +5,47 @@ namespace DrevOps\VortexInstaller\Prompts\Handlers; use DrevOps\VortexInstaller\Utils\Composer; +use DrevOps\VortexInstaller\Utils\Converter; +use DrevOps\VortexInstaller\Utils\Env; use DrevOps\VortexInstaller\Utils\File; class Name extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🏷️ Site name'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name in the project and in the documentation.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. My Site'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return Converter::label(Env::get('VORTEX_PROJECT', basename((string) $this->config->getDst()))); + } + /** * {@inheritdoc} */ @@ -22,6 +59,20 @@ public function discover(): null|string|bool|array { return NULL; } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid project name.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Org.php b/.vortex/installer/src/Prompts/Handlers/Org.php index b0ca0b72a..3e701a9a6 100644 --- a/.vortex/installer/src/Prompts/Handlers/Org.php +++ b/.vortex/installer/src/Prompts/Handlers/Org.php @@ -5,10 +5,50 @@ namespace DrevOps\VortexInstaller\Prompts\Handlers; use DrevOps\VortexInstaller\Utils\Composer; +use DrevOps\VortexInstaller\Utils\Converter; use DrevOps\VortexInstaller\Utils\File; class Org extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🏒 Organization name'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name in the project and in the documentation.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. My Org'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[Name::id()]) && !empty($responses[Name::id()])) { + return Converter::label($responses[Name::id()]) . ' Org'; + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -22,6 +62,20 @@ public function discover(): null|string|bool|array { return NULL; } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid organization name.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php b/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php index cdee3318b..2f6a330f0 100644 --- a/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php +++ b/.vortex/installer/src/Prompts/Handlers/OrgMachineName.php @@ -5,10 +5,50 @@ namespace DrevOps\VortexInstaller\Prompts\Handlers; use DrevOps\VortexInstaller\Utils\Composer; +use DrevOps\VortexInstaller\Utils\Converter; use DrevOps\VortexInstaller\Utils\File; class OrgMachineName extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🏒 Organization machine name'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name for the project directory and in the code.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. my_org'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[Org::id()]) && !empty($responses[Org::id()])) { + return Converter::machineExtended($responses[Org::id()]); + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -22,6 +62,20 @@ public function discover(): null|string|bool|array { return NULL; } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid organisation machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php b/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php index aabee43d2..d49326d02 100644 --- a/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php +++ b/.vortex/installer/src/Prompts/Handlers/PreserveDocsOnboarding.php @@ -8,6 +8,27 @@ class PreserveDocsOnboarding extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ“‹ Preserve onboarding checklist?'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Helps to track onboarding to Vortex within the repository.'; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return TRUE; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php b/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php index e8f3435aa..250f419d7 100644 --- a/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php +++ b/.vortex/installer/src/Prompts/Handlers/PreserveDocsProject.php @@ -8,6 +8,27 @@ class PreserveDocsProject extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ“š Preserve project documentation?'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Helps to maintain the project documentation within the repository.'; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return TRUE; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Profile.php b/.vortex/installer/src/Prompts/Handlers/Profile.php index 73958e42e..8b60dc1fa 100644 --- a/.vortex/installer/src/Prompts/Handlers/Profile.php +++ b/.vortex/installer/src/Prompts/Handlers/Profile.php @@ -20,28 +20,54 @@ class Profile extends AbstractHandler { /** * {@inheritdoc} */ - public function discover(): null|string|bool|array { - if ($this->isInstalled()) { - $value = Env::getFromDotenv('DRUPAL_PROFILE', $this->dstDir); - if (!empty($value)) { - return $value; - } - } + public function label(): string { + return '🧾 Profile'; + } - $locations = [ - $this->dstDir . sprintf('/%s/profiles/*/*.info', $this->webroot), - $this->dstDir . sprintf('/%s/profiles/*/*.info.yml', $this->webroot), - $this->dstDir . sprintf('/%s/profiles/custom/*/*.info', $this->webroot), - $this->dstDir . sprintf('/%s/profiles/custom/*/*.info.yml', $this->webroot), + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Select which profile to use'; + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::STANDARD => 'Standard', + self::MINIMAL => 'Minimal', + self::DEMO_UMAMI => 'Demo Umami', + self::CUSTOM => 'Custom', ]; + } - $path = File::findMatchingPath($locations); + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return self::STANDARD; + } - if (empty($path)) { - return NULL; + /** + * {@inheritdoc} + */ + public function discover(): null|string|bool|array { + $value = $this->discoverName(); + + if (!is_null($value)) { + return in_array($value, [self::STANDARD, self::MINIMAL, self::DEMO_UMAMI]) ? $value : self::CUSTOM; } - return str_replace(['.info.yml', '.info'], '', basename($path)); + return NULL; } /** @@ -49,6 +75,12 @@ public function discover(): null|string|bool|array { */ public function process(): void { $v = $this->getResponseAsString(); + + // If user selected 'custom', use the ProfileCustom response instead. + if ($v === self::CUSTOM && isset($this->responses['profile_custom'])) { + $v = $this->responses['profile_custom']; + } + $t = $this->tmpDir; $w = $this->webroot; @@ -69,4 +101,34 @@ public function process(): void { } } + /** + * Discover the profile name from the filesystem or environment. + * + * @return null|string|bool|array + * The profile name if found, NULL if not found. + */ + public function discoverName(): null|string|bool|array { + if ($this->isInstalled()) { + $value = Env::getFromDotenv('DRUPAL_PROFILE', $this->dstDir); + if (!empty($value)) { + return $value; + } + } + + $locations = [ + $this->dstDir . sprintf('/%s/profiles/*/*.info', $this->webroot), + $this->dstDir . sprintf('/%s/profiles/*/*.info.yml', $this->webroot), + $this->dstDir . sprintf('/%s/profiles/custom/*/*.info', $this->webroot), + $this->dstDir . sprintf('/%s/profiles/custom/*/*.info.yml', $this->webroot), + ]; + + $path = File::findMatchingPath($locations); + + if (empty($path)) { + return NULL; + } + + return str_replace(['.info.yml', '.info'], '', basename($path)); + } + } diff --git a/.vortex/installer/src/Prompts/Handlers/ProfileCustom.php b/.vortex/installer/src/Prompts/Handlers/ProfileCustom.php new file mode 100644 index 000000000..88d037c1b --- /dev/null +++ b/.vortex/installer/src/Prompts/Handlers/ProfileCustom.php @@ -0,0 +1,86 @@ +config); + $profile_handler->setWebroot($this->webroot); + $discovered = $profile_handler->discoverName(); + + // Only return discovered value if it's a custom profile. + if (!empty($discovered) && !in_array($discovered, [Profile::STANDARD, Profile::MINIMAL, Profile::DEMO_UMAMI])) { + return $discovered; + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn(string $v): ?string => !empty($v) && Converter::machine($v) !== $v ? + 'Please enter a valid profile name: only lowercase letters, numbers, and underscores are allowed.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + + /** + * {@inheritdoc} + */ + public function process(): void { + // This handler doesn't need processing - the Profile handler will handle + // the final result. + } + +} diff --git a/.vortex/installer/src/Prompts/Handlers/ProvisionType.php b/.vortex/installer/src/Prompts/Handlers/ProvisionType.php index 602449c8a..d172aad67 100644 --- a/.vortex/installer/src/Prompts/Handlers/ProvisionType.php +++ b/.vortex/installer/src/Prompts/Handlers/ProvisionType.php @@ -13,6 +13,37 @@ class ProvisionType extends AbstractHandler { const PROFILE = 'profile'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ¦‹ Provision type'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Selecting "Profile" will install site from a profile rather than a database dump.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::DATABASE => 'Import from database dump', + self::PROFILE => 'Install from profile', + ]; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return self::DATABASE; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Services.php b/.vortex/installer/src/Prompts/Handlers/Services.php index cdd65f1a0..fbbb27bd2 100644 --- a/.vortex/installer/src/Prompts/Handlers/Services.php +++ b/.vortex/installer/src/Prompts/Handlers/Services.php @@ -15,6 +15,38 @@ class Services extends AbstractHandler { const SOLR = 'solr'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ”Œ Services'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Select the services you want to use in the project.'; + } + + /** + * {@inheritdoc} + */ + public function options(array $responses): ?array { + return [ + self::CLAMAV => '🦠 ClamAV', + self::SOLR => 'πŸ” Solr', + self::VALKEY => 'πŸ—ƒοΈ Valkey', + ]; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + return [self::CLAMAV, self::SOLR, self::VALKEY]; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Theme.php b/.vortex/installer/src/Prompts/Handlers/Theme.php index a0c2bb72e..2a219df5d 100644 --- a/.vortex/installer/src/Prompts/Handlers/Theme.php +++ b/.vortex/installer/src/Prompts/Handlers/Theme.php @@ -10,6 +10,38 @@ class Theme extends AbstractHandler { + /** + * {@inheritdoc} + */ + public function label(): string { + return '🎨 Theme machine name'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'We will use this name for the theme directory. Leave empty to skip the theme scaffold.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. mytheme'; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + if (isset($responses[MachineName::id()]) && !empty($responses[MachineName::id()])) { + return Converter::machine($responses[MachineName::id()]); + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -30,6 +62,20 @@ public function discover(): null|string|bool|array { return str_replace(['.info.yml', '.info'], '', basename($path)); } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => !empty($v) && Converter::machine($v) !== $v ? 'Please enter a valid theme machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => trim($v); + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/Handlers/Webroot.php b/.vortex/installer/src/Prompts/Handlers/Webroot.php index cc0559974..3e0ce1d61 100644 --- a/.vortex/installer/src/Prompts/Handlers/Webroot.php +++ b/.vortex/installer/src/Prompts/Handlers/Webroot.php @@ -7,6 +7,7 @@ use DrevOps\VortexInstaller\Utils\Composer; use DrevOps\VortexInstaller\Utils\Env; use DrevOps\VortexInstaller\Utils\File; +use DrevOps\VortexInstaller\Utils\Validator; class Webroot extends AbstractHandler { @@ -14,6 +15,50 @@ class Webroot extends AbstractHandler { const DOCROOT = 'docroot'; + /** + * {@inheritdoc} + */ + public function label(): string { + return 'πŸ“ Custom web root directory'; + } + + /** + * {@inheritdoc} + */ + public function hint(array $responses): ?string { + return 'Custom directory where the web server serves the site.'; + } + + /** + * {@inheritdoc} + */ + public function placeholder(array $responses): ?string { + return 'E.g. ' . implode(', ', [self::WEB, self::DOCROOT]); + } + + /** + * {@inheritdoc} + */ + public function isRequired(): bool { + return TRUE; + } + + /** + * {@inheritdoc} + */ + public function default(array $responses): null|string|bool|array { + // Auto-select webroot based on hosting provider. + if (isset($responses[HostingProvider::id()])) { + return match ($responses[HostingProvider::id()]) { + HostingProvider::ACQUIA => self::DOCROOT, + HostingProvider::LAGOON => self::WEB, + default => self::WEB, + }; + } + + return NULL; + } + /** * {@inheritdoc} */ @@ -31,6 +76,45 @@ public function discover(): null|string|bool|array { return $value; } + /** + * {@inheritdoc} + */ + public function validate(): ?callable { + return fn($v): ?string => Validator::dirname($v) ? NULL : 'Please enter a valid webroot name: only lowercase letters, numbers, and underscores are allowed.'; + } + + /** + * {@inheritdoc} + */ + public function transform(): ?callable { + return fn(string $v): string => rtrim($v, DIRECTORY_SEPARATOR); + } + + /** + * {@inheritdoc} + */ + public function resolvedValue(array $responses): null|string|bool|array { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function resolvedMessage(array $responses): ?string { + if ( + isset($responses[HostingProvider::id()]) && + $responses[HostingProvider::id()] !== HostingProvider::OTHER + ) { + $webroot = $this->default($responses); + if (is_array($webroot)) { + throw new \InvalidArgumentException('Web root must be a string, got: ' . gettype($webroot)); + } + return sprintf('Web root will be set to "%s".', (string) $webroot); + } + + return NULL; + } + /** * {@inheritdoc} */ diff --git a/.vortex/installer/src/Prompts/PromptManager.php b/.vortex/installer/src/Prompts/PromptManager.php index 65723ccf3..dfbde6db1 100644 --- a/.vortex/installer/src/Prompts/PromptManager.php +++ b/.vortex/installer/src/Prompts/PromptManager.php @@ -28,6 +28,7 @@ use DrevOps\VortexInstaller\Prompts\Handlers\PreserveDocsOnboarding; use DrevOps\VortexInstaller\Prompts\Handlers\PreserveDocsProject; use DrevOps\VortexInstaller\Prompts\Handlers\Profile; +use DrevOps\VortexInstaller\Prompts\Handlers\ProfileCustom; use DrevOps\VortexInstaller\Prompts\Handlers\ProvisionType; use DrevOps\VortexInstaller\Prompts\Handlers\Services; use DrevOps\VortexInstaller\Prompts\Handlers\Theme; @@ -36,7 +37,6 @@ use DrevOps\VortexInstaller\Utils\Converter; use DrevOps\VortexInstaller\Utils\Env; use DrevOps\VortexInstaller\Utils\Tui; -use DrevOps\VortexInstaller\Utils\Validator; use Symfony\Component\Console\Output\OutputInterface; use function Laravel\Prompts\confirm; use function Laravel\Prompts\form; @@ -55,24 +55,24 @@ */ class PromptManager { - /** - * Array of responses. - */ - protected array $responses = []; - /** * Total number of top-level responses. * * Used to display the progress of the prompts. */ - protected int $totalResponses = 21; + const TOTAL_RESPONSES = 21; + + /** + * Array of responses. + */ + protected array $responses = []; /** - * Current response number. + * Current response index. * * Used to display the progress of the prompts. */ - protected int $currentResponse = 0; + protected int $currentResponseIndex = 0; /** * Array of handlers. @@ -94,12 +94,14 @@ public function __construct( } /** - * Prompt for responses. + * Run prompts to get responses. * * If non-interactive mode is used, the values provided by $this->default() * method, including discovery from the existing codebase, will be used. */ - public function prompt(): void { + public function runPrompts(): void { + // Set verbosity for TUI output based on the config. This will be reset + // after the prompt is completed. $original_verbosity = Tui::output()->getVerbosity(); if ($this->config->getNoInteraction()) { Tui::output()->setVerbosity(OutputInterface::VERBOSITY_QUIET); @@ -112,368 +114,112 @@ public function prompt(): void { // phpcs:disable Drupal.WhiteSpace.ScopeIndent.IncorrectExact $responses = form() ->intro('General information') - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🏷️ Site name'), - hint: 'We will use this name in the project and in the documentation.', - placeholder: 'E.g. My Site', - required: TRUE, - default: $this->default($n, Converter::label(Env::get('VORTEX_PROJECT', basename((string) $this->config->getDst())))), - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid project name.' : NULL, - ), Name::id()) - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🏷️ Site machine name'), - hint: 'We will use this name for the project directory and in the code.', - placeholder: 'E.g. my_site', - required: TRUE, - default: $this->default($n, Converter::machineExtended($r[Name::id()])), - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL, - ), MachineName::id()) - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🏒 Organization name'), - hint: 'We will use this name in the project and in the documentation.', - placeholder: 'E.g. My Org', - required: TRUE, - default: $this->default('org', Converter::label($r[Name::id()]) . ' Org'), - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => Converter::label($v) !== $v ? 'Please enter a valid organization name.' : NULL, - ), Org::id()) - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🏒 Organization machine name'), - hint: 'We will use this name for the project directory and in the code.', - placeholder: 'E.g. my_org', - required: TRUE, - default: $this->default($n, Converter::machineExtended($r[Org::id()])), - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => Converter::machineExtended($v) !== $v ? 'Please enter a valid organisation machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL, - ), OrgMachineName::id()) - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🌐 Public domain'), - hint: 'Domain name without protocol and trailing slash.', - placeholder: 'E.g. example.com', - required: TRUE, - default: $this->default($n, Converter::kebab($r[MachineName::id()]) . '.com'), - transform: fn(string $v): string => Converter::domain($v), - validate: fn($v): ?string => Validator::domain($v) ? NULL : 'Please enter a valid domain name.', - ), Domain::id()) + ->add(fn($r, $pr, $n): string => text(...$this->args(Name::class)), Name::id()) + ->add(fn($r, $pr, $n): string => text(...$this->args(MachineName::class, NULL, $r)), MachineName::id()) + ->add(fn($r, $pr, $n): string => text(...$this->args(Org::class, NULL, $r)), Org::id()) + ->add(fn($r, $pr, $n): string => text(...$this->args(OrgMachineName::class, NULL, $r)), OrgMachineName::id()) + ->add(fn($r, $pr, $n): string => text(...$this->args(Domain::class, NULL, $r)), Domain::id()) ->intro('Code repository') - - ->add(fn($r, $pr, $n): int|string => select( - label: $this->label('πŸ—„οΈ Repository provider'), - hint: 'Vortex offers full automation with GitHub, while support for other providers is limited.', - options: [ - CodeProvider::GITHUB => 'GitHub', - CodeProvider::OTHER => 'Other', - ], - default: $this->default($n, 'github'), - ), CodeProvider::id()) - + ->add(fn($r, $pr, $n): int|string => select(...$this->args(CodeProvider::class)), CodeProvider::id()) ->addIf( - fn($r): bool => $r[CodeProvider::id()] === CodeProvider::GITHUB, - fn($r, $pr, $n) => Tui::note("We need a token to create repositories and manage webhooks.\nIt won't be saved anywhere in the file system.\nYou may skip entering the token, but then Vortex will have to skip several operations."), + fn($r): bool => $this->handlers[GithubToken::id()]->shouldRun($r), + fn($r, $pr, $n) => Tui::note('' . GithubToken::description($r) . '') ) - ->addIf( - fn($r): bool => $r[CodeProvider::id()] === CodeProvider::GITHUB, + fn($r): bool => $this->handlers[GithubToken::id()]->shouldRun($r), function ($r, $pr, $n): string { - $value = $this->default($n); - if (!empty($value)) { - Tui::ok($this->label('GitHub access token is already set in the environment.', 'a')); - - return $value; + $handler = $this->handlers[GithubToken::id()]; + $resolved_value = $handler->resolvedValue($r); + if (!empty($resolved_value)) { + Tui::ok($this->label((string) $handler->resolvedMessage($r), 'a')); + return is_string($resolved_value) ? $resolved_value : ''; } - - return password( - label: $this->label('πŸ”‘ GitHub access token (optional)', 'a'), - hint: Env::get('GITHUB_TOKEN') ? 'Read from GITHUB_TOKEN environment variable.' : 'Create a new token with "repo" scopes at https://github.com/settings/tokens/new', - placeholder: 'E.g. ghp_1234567890', - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => !empty($v) && !str_starts_with($v, 'ghp_') ? 'Please enter a valid token starting with "ghp_"' : NULL, - ); - }, GithubToken::id()) - + else { + return password(...$this->args(GithubToken::class)); + } + }, + GithubToken::id() + ) ->addIf( - fn($r): bool => !empty($r[GithubToken::id()]), - fn($r, $pr, $n): string => text( - label: $this->label('🏷️ What is your GitHub project name?', 'b'), - hint: 'We will use this name to create new or find an existing repository.', - placeholder: 'E.g. myorg/myproject', - default: $this->default($n, $r[OrgMachineName::id()] . '/' . $r[MachineName::id()]), - transform: fn(string $v): string => trim($v), - validate: fn(string $v): ?string => !empty($v) && !Validator::githubProject($v) ? 'Please enter a valid project name in the format "myorg/myproject"' : NULL, - ), GithubRepo::id()) + fn($r): bool => $this->handlers[GithubRepo::id()]->shouldRun($r), + fn($r, $pr, $n): string => text(...$this->args(GithubRepo::class, NULL, $r)), + GithubRepo::id() + ) ->intro('Drupal') - - ->add(function ($r, $pr, $n): int|string { - $profile = select( - label: $this->label('🧾 Profile'), - hint: 'Select which profile to use', - options: [ - Profile::STANDARD => 'Standard', - Profile::MINIMAL => 'Minimal', - Profile::DEMO_UMAMI => 'Demo Umami', - Profile::CUSTOM => 'Custom', - ], - required: TRUE, - default: empty($this->default($n)) ? Profile::STANDARD : Profile::CUSTOM, - ); - - if ($profile === Profile::CUSTOM) { - $profile = text( - label: $this->label('🧾 Custom profile machine name', 'a'), - placeholder: 'E.g. my_profile', - required: TRUE, - default: $this->default($n), - transform: fn(string $v): string => trim($v), - validate: fn(string $v): ?string => !empty($v) && Converter::machine($v) !== $v ? 'Please enter a valid profile name: only lowercase letters, numbers, and underscores are allowed.' : NULL, - ); - } - - return $profile; - }, Profile::id()) - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🧩 Module prefix'), - hint: 'We will use this name for custom modules.', - placeholder: 'E.g. ms (for My Site)', - required: TRUE, - default: $this->default($n, Converter::abbreviation(Converter::machine($r[MachineName::id()]), 4, ['_'])), - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => Converter::machine($v) !== $v ? 'Please enter a valid module prefix: only lowercase letters, numbers, and underscores are allowed.' : NULL, - ), ModulePrefix::id()) - - ->add(fn($r, $pr, $n): string => text( - label: $this->label('🎨 Theme machine name'), - hint: 'We will use this name for the theme directory. Leave empty to skip the theme scaffold.', - placeholder: 'E.g. mytheme', - default: $this->default($n, Converter::machine($r[MachineName::id()])), - transform: fn(string $v): string => trim($v), - validate: fn($v): ?string => !empty($v) && Converter::machine($v) !== $v ? 'Please enter a valid theme machine name: only lowercase letters, numbers, and underscores are allowed.' : NULL, - ), Theme::id()) + ->add( + fn($r, $pr, $n): int|string => select(...$this->args(Profile::class)), + Profile::id() + ) + ->addIf( + fn($r): bool => $this->handlers[ProfileCustom::id()]->shouldRun($r), + fn($r, $pr, $n): string => text(...$this->args(ProfileCustom::class)), + ProfileCustom::id() + ) + ->add(fn($r, $pr, $n): string => text(...$this->args(ModulePrefix::class, NULL, $r)), ModulePrefix::id()) + ->add(fn($r, $pr, $n): string => text(...$this->args(Theme::class, NULL, $r)), Theme::id()) ->intro('Services') - - ->add(fn($r, $pr, $n): array => multiselect( - label: $this->label('πŸ”Œ Services'), - hint: 'Select the services you want to use in the project.', - options: [ - Services::CLAMAV => '🦠 ClamAV', - Services::SOLR => 'πŸ” Solr', - Services::VALKEY => 'πŸ—ƒοΈ Valkey', - ], - default: $this->default($n, [Services::CLAMAV, Services::SOLR, Services::VALKEY]), - ), Services::id()) + ->add(fn($r, $pr, $n): array => multiselect(...$this->args(Services::class)), Services::id()) ->intro('Hosting') - - ->add(fn($r, $pr, $n): int|string => select( - label: $this->label('☁️ Hosting provider'), - hint: 'Select the hosting provider where the project is hosted. The web root directory will be set accordingly.', - options: [ - HostingProvider::ACQUIA => 'πŸ’§ Acquia Cloud', - HostingProvider::LAGOON => '🌊 Lagoon', - HostingProvider::OTHER => '🧩 Other', - HostingProvider::NONE => '🚫 None', - ], - required: TRUE, - default: $this->default($n, 'none'), - ), HostingProvider::id()) - - ->add(function (array $r, $pr, $n): string|bool|array { - if ($r[HostingProvider::id()] !== HostingProvider::OTHER) { - $webroot = match ($r[HostingProvider::id()]) { - HostingProvider::ACQUIA => Webroot::DOCROOT, - HostingProvider::LAGOON => Webroot::WEB, - default => $this->default($n, Webroot::WEB) - }; - - info(sprintf('Web root will be set to "%s".', $webroot)); - } - else { - $webroot = text( - label: $this->label('πŸ“ Custom web root directory', 'a'), - hint: 'Custom directory where the web server serves the site.', - placeholder: 'E.g. ' . implode(', ', [Webroot::WEB, Webroot::DOCROOT]), - required: TRUE, - default: $this->default($n, Webroot::WEB), - transform: fn(string $v): string => rtrim($v, DIRECTORY_SEPARATOR), - validate: fn($v): ?string => Validator::dirname($v) ? NULL : 'Please enter a valid webroot name: only lowercase letters, numbers, and underscores are allowed.', - ); - } - return $webroot; - }, Webroot::id()) + ->add(fn($r, $pr, $n): int|string => select(...$this->args(HostingProvider::class)), HostingProvider::id()) + ->add( + function (array $r, $pr, $n): string { + $handler = $this->handlers[Webroot::id()]; + $resolved = $handler->resolvedValue($r); + if (!is_null($resolved)) { + info($handler->resolvedMessage($r)); + return match (TRUE) { + is_string($resolved) => $resolved, + is_array($resolved) => json_encode($resolved) ?: 'array', + default => 'true', + }; + } + else { + return text(...$this->args(Webroot::class, NULL, $r)); + } + }, + Webroot::id() + ) ->intro('Deployment') - - ->add(function (array $r, $pr, $n): array { - $defaults = []; - - $options = [ - DeployType::ARTIFACT => 'πŸ“¦ Code artifact', - DeployType::LAGOON => '🌊 Lagoon webhook', - DeployType::CONTAINER_IMAGE => '🐳 Container image', - DeployType::WEBHOOK => '🌐 Custom webhook', - ]; - - if ($r[HostingProvider::id()] === HostingProvider::LAGOON) { - $defaults[] = DeployType::LAGOON; - } - - if ($r[HostingProvider::id()] === HostingProvider::ACQUIA) { - $defaults[] = DeployType::ARTIFACT; - unset($options[DeployType::LAGOON]); - } - - if (empty($defaults)) { - $defaults[] = DeployType::WEBHOOK; - } - - return multiselect( - label: $this->label('🚚 Deployment types'), - hint: 'You can deploy code using one or more methods.', - options: $options, - default: $this->default($n, $defaults), - ); - }, DeployType::id()) + ->add(fn($r, $pr, $n): array => multiselect(...$this->args(DeployType::class, NULL, $r)), DeployType::id()) ->intro('Workflow') - ->add(fn($r, $pr, $n) => Tui::note('Provisioning is the process of setting up the site in the environment with an already assembled codebase.')) - - ->add(fn($r, $pr, $n): int|string => select( - label: $this->label('πŸ¦‹ Provision type'), - hint: 'Selecting "Profile" will install site from a profile rather than a database dump.', - options: [ - ProvisionType::DATABASE => 'Import from database dump', - ProvisionType::PROFILE => 'Install from profile', - ], - default: $this->default($n, ProvisionType::DATABASE), - ), ProvisionType::id()) - - ->add(function (array $r, $pr, $n): int|string { - if ($r[ProvisionType::id()] === ProvisionType::PROFILE) { - return DatabaseDownloadSource::NONE; - } - - $options = [ - DatabaseDownloadSource::URL => '🌍 URL download', - DatabaseDownloadSource::FTP => 'πŸ“‚ FTP download', - DatabaseDownloadSource::ACQUIA => 'πŸ’§ Acquia backup', - DatabaseDownloadSource::LAGOON => '🌊 Lagoon environment', - DatabaseDownloadSource::CONTAINER_REGISTRY => '🐳 Container registry', - DatabaseDownloadSource::NONE => '🚫 None', - ]; - - if ($r[HostingProvider::id()] === HostingProvider::ACQUIA) { - unset($options[DatabaseDownloadSource::LAGOON]); - } - - if ($r[HostingProvider::id()] === HostingProvider::LAGOON) { - unset($options[DatabaseDownloadSource::ACQUIA]); - } - - return select( - label: $this->label('πŸ“‘ Database source', 'a'), - hint: 'The database can be downloaded as an exported dump file or pre-packaged in a container image.', - options: $options, - default: $this->default($n, match ($r[HostingProvider::id()]) { - HostingProvider::ACQUIA => DatabaseDownloadSource::ACQUIA, - HostingProvider::LAGOON => DatabaseDownloadSource::LAGOON, - default => DatabaseDownloadSource::URL, - }), - ); - }, DatabaseDownloadSource::id()) - + ->add(fn($r, $pr, $n): int|string => select(...$this->args(ProvisionType::class)), ProvisionType::id()) ->addIf( - fn($r): bool => $r[DatabaseDownloadSource::id()] === DatabaseDownloadSource::CONTAINER_REGISTRY, - fn($r, $pr, $n): string => text( - label: $this->label('🏷️ What is your database container image name and a tag?', 'a'), - hint: 'Use "latest" tag for the latest version. CI will be building this image overnight.', - placeholder: sprintf('E.g. %s/%s-data:latest', Converter::phpNamespace($r[OrgMachineName::id()]), Converter::phpNamespace($r[MachineName::id()])), - default: $this->default($n, sprintf('%s/%s-data:latest', Converter::phpNamespace($r[OrgMachineName::id()]), Converter::phpNamespace($r[MachineName::id()]))), - transform: fn($v): string => trim($v), - validate: fn($v): ?string => Validator::containerImage($v) ? NULL : 'Please enter a valid container image name with an optional tag.', - ), DatabaseImage::id()) + fn($r): bool => $this->handlers[DatabaseDownloadSource::id()]->shouldRun($r), + fn($r, $pr, $n): int|string => select(...$this->args(DatabaseDownloadSource::class, NULL, $r)), + DatabaseDownloadSource::id() + ) + ->addIf( + fn($r): bool => $this->handlers[DatabaseImage::id()]->shouldRun($r), + function ($r, $pr, $n): string { + $handler = $this->handlers[DatabaseImage::id()]; + $args = $this->args(DatabaseImage::class, NULL, $r); + $args['placeholder'] = $handler->placeholder($r); + return text(...$args); + }, + DatabaseImage::id() + ) ->intro('Continuous Integration') - - ->add(function (array $r, $pr, $n): int|string { - $options = [ - CiProvider::NONE => 'None', - CiProvider::GITHUB_ACTIONS => 'GitHub Actions', - CiProvider::CIRCLECI => 'CircleCI', - ]; - - if ($r[CodeProvider::id()] !== CodeProvider::GITHUB) { - unset($options[CiProvider::GITHUB_ACTIONS]); - } - - return select( - label: $this->label('πŸ”„ Continuous Integration provider'), - hint: 'Both providers support equivalent workflow.', - options: $options, - default: $this->default($n, CiProvider::GITHUB_ACTIONS), - ); - }, CiProvider::id()) + ->add(fn(array $r, $pr, $n): int|string => select(...$this->args(CiProvider::class, NULL, $r)), CiProvider::id()) ->intro('Automations') - - ->add(fn($r, $pr, $n): int|string => select( - label: $this->label('⬆️ Dependency updates provider'), - hint: 'Use a self-hosted service if you can’t install a GitHub app.', - options: [ - DependencyUpdatesProvider::RENOVATEBOT_CI => 'πŸ€– + πŸ”„ Renovate self-hosted in CI', - DependencyUpdatesProvider::RENOVATEBOT_APP => 'πŸ€– Renovate GitHub app', - DependencyUpdatesProvider::NONE => '🚫 None', - ], - default: $this->default($n, DependencyUpdatesProvider::RENOVATEBOT_CI), - ), DependencyUpdatesProvider::id()) - - ->add(fn($r, $pr, $n): bool => confirm( - label: $this->label('πŸ‘€ Auto-assign the author to their PR?'), - hint: 'Helps to keep the PRs organized.', - default: $this->default($n, TRUE), - ), AssignAuthorPr::id()) - - ->add(fn($r, $pr, $n): bool => confirm( - label: $this->label('🎫 Auto-add a CONFLICT label to a PR when conflicts occur?'), - hint: 'Helps to keep quickly identify PRs that need attention.', - default: $this->default($n, TRUE), - ), LabelMergeConflictsPr::id()) + ->add(fn($r, $pr, $n): int|string => select(...$this->args(DependencyUpdatesProvider::class)), DependencyUpdatesProvider::id()) + ->add(fn($r, $pr, $n): bool => confirm(...$this->args(AssignAuthorPr::class)), AssignAuthorPr::id()) + ->add(fn($r, $pr, $n): bool => confirm(...$this->args(LabelMergeConflictsPr::class)), LabelMergeConflictsPr::id()) ->intro('Documentation') - - ->add(fn($r, $pr, $n): bool => confirm( - label: $this->label('πŸ“š Preserve project documentation?'), - hint: 'Helps to maintain the project documentation within the repository.', - default: $this->default($n, TRUE), - ), PreserveDocsProject::id()) - - ->add(fn($r, $pr, $n): bool => confirm( - label: $this->label('πŸ“‹ Preserve onboarding checklist?'), - hint: 'Helps to track onboarding to Vortex within the repository.', - default: $this->default($n, TRUE), - ), PreserveDocsOnboarding::id()) + ->add(fn($r, $pr, $n): bool => confirm(...$this->args(PreserveDocsProject::class)), PreserveDocsProject::id()) + ->add(fn($r, $pr, $n): bool => confirm(...$this->args(PreserveDocsOnboarding::class)), PreserveDocsOnboarding::id()) ->intro('AI') - - ->add(fn($r, $pr, $n): int|string => select( - label: $this->label('πŸ€– AI code assistant instructions'), - hint: 'Helps AI coding assistants to understand the project better.', - options: [ - AiCodeInstructions::CLAUDE => 'Anthropic Claude', - AiCodeInstructions::NONE => 'None', - ], - default: $this->default($n, AiCodeInstructions::NONE), - ), AiCodeInstructions::id()) + ->add(fn($r, $pr, $n): int|string => select(...$this->args(AiCodeInstructions::class)), AiCodeInstructions::id()) ->submit(); @@ -488,6 +234,19 @@ function ($r, $pr, $n): string { return !is_numeric($key); }, ARRAY_FILTER_USE_KEY); + // Handle Profile custom name merging. + if (isset($responses[Profile::id()]) && $responses[Profile::id()] === Profile::CUSTOM && isset($responses[ProfileCustom::id()])) { + $responses[Profile::id()] = $responses[ProfileCustom::id()]; + } + + // Always remove ProfileCustom key (it's only used for internal merging) + unset($responses[ProfileCustom::id()]); + + // Handle DatabaseDownloadSource when ProvisionType is PROFILE. + if (isset($responses[ProvisionType::id()]) && $responses[ProvisionType::id()] === ProvisionType::PROFILE) { + $responses[DatabaseDownloadSource::id()] = DatabaseDownloadSource::NONE; + } + if ($this->config->getNoInteraction()) { Tui::output()->setVerbosity($original_verbosity); } @@ -497,6 +256,12 @@ function ($r, $pr, $n): string { /** * Get all received responses. + * + * Used to provide direct access to the responses values. + * + * @return array + * An associative array of responses, where keys are handler IDs and values + * are the responses provided by the user or discovered by handlers. */ public function getResponses(): array { return $this->responses; @@ -505,9 +270,9 @@ public function getResponses(): array { /** * Run all processors. */ - public function process(): void { + public function runProcessors(): void { // Run processors in the reverse order of how they are defined in the - // PromptManager to ensure that the handlers for string replacements process + // runPrompts() to ensure that the handlers for string replacements process // more specific values first, and the more generic ones last. $ids = [ Webroot::id(), @@ -525,7 +290,9 @@ public function process(): void { HostingProvider::id(), Services::id(), GithubRepo::id(), + GithubToken::id(), CodeProvider::id(), + ProfileCustom::id(), Profile::id(), Domain::id(), ModulePrefix::id(), @@ -547,6 +314,15 @@ public function process(): void { } } + /** + * Check if the installation should proceed. + * + * This method checks the configuration for the no-interaction mode and + * prompts the user for confirmation if not in no-interaction mode. + * + * @return bool + * TRUE if the installation should proceed, FALSE otherwise. + */ public function shouldProceed(): bool { $proceed = TRUE; @@ -557,7 +333,7 @@ public function shouldProceed(): bool { ); } - // Kill-switch to not proceed with install. If false, the installer will not + // Kill-switch to not proceed with install. If FALSE, the installer will not // proceed despite the answer received above. if (!$this->config->get(Config::PROCEED)) { $proceed = FALSE; @@ -566,12 +342,8 @@ public function shouldProceed(): bool { return $proceed; } - public static function makeEnvName(string $id): string { - return Converter::constant('VORTEX_INSTALL_PROMPT_' . $id); - } - public function getResponsesSummary(): array { - $responses = $this->getResponses(); + $responses = $this->responses; $values['General information'] = Tui::LIST_SECTION_TITLE; $values['🏷️ Site name'] = $responses[Name::id()]; @@ -641,6 +413,19 @@ public function getResponsesSummary(): array { return $values; } + /** + * Generate an environment variable name for a prompt. + * + * @param string $id + * The prompt ID. + * + * @return string + * The environment variable name. + */ + public static function makeEnvName(string $id): string { + return Converter::constant('VORTEX_INSTALL_PROMPT_' . $id); + } + /** * Generate a label for a prompt. * @@ -654,37 +439,12 @@ public function getResponsesSummary(): array { */ protected function label(string $text, ?string $suffix = NULL): string { if (is_null($suffix)) { - $this->currentResponse++; - } - - $suffix = $suffix !== NULL ? $this->currentResponse . '.' . $suffix : $this->currentResponse; - - return $text . ' ' . Tui::dim('(' . $suffix . '/' . $this->totalResponses . ')'); - } - - /** - * Get a default value for a response. - * - * @param string $id - * The response name. - * @param string $default - * The default value to return. - */ - protected function default(string $id, string|bool|array $default = ''): mixed { - // Allow to set the value from the environment variable. - $env_var = static::makeEnvName($id); - $env_val = getenv($env_var); - if (is_string($env_val)) { - return Env::toValue($env_val); + $this->currentResponseIndex++; } - if (!array_key_exists($id, $this->handlers)) { - return $default; - } - - $discovered = $this->handlers[$id]->discover(); + $suffix = $suffix !== NULL ? $this->currentResponseIndex . '.' . $suffix : $this->currentResponseIndex; - return is_null($discovered) ? $default : $discovered; + return $text . ' ' . Tui::dim('(' . $suffix . '/' . static::TOTAL_RESPONSES . ')'); } /** @@ -714,11 +474,11 @@ protected function initHandlers(): void { $classes[] = $class; } - // Discover webroot and set for all handlers to help with paths resolution. + // Discover web root and set for all handlers to help with paths resolution. $webroot = (new Webroot($this->config))->discover() ?: Webroot::WEB; if (!is_string($webroot)) { - throw new \RuntimeException('Webroot could not be discovered.'); + throw new \RuntimeException('Web root could not be discovered.'); } foreach ($classes as $class) { @@ -728,4 +488,75 @@ protected function initHandlers(): void { } } + /** + * Convert handler properties to Laravel prompts. + * + * Do not optimize this method to ease debugging and future changes. + * + * @param string $handler_class + * The handler class name. + * The handler id. + * @param mixed $default_override + * Optional override for the default value (for response dependencies). + * @param array $responses + * Current form responses for context-aware methods. + * + * @return array + * Array of prompt arguments suitable for Laravel prompts. + */ + private function args(string $handler_class, mixed $default_override = NULL, array $responses = []): array { + $id = $handler_class::id(); + + if (!array_key_exists($id, $this->handlers)) { + throw new \RuntimeException(sprintf('Handler for "%s" not found.', $id)); + } + + $handler = $this->handlers[$handler_class::id()]; + + $args = [ + 'label' => $this->label($handler->label()), + 'hint' => $handler->hint($responses), + 'placeholder' => $handler->placeholder($responses), + 'transform' => $handler->transform(), + 'validate' => $handler->validate(), + ]; + + if ($handler->isRequired()) { + $args['required'] = TRUE; + } + + $options = $handler->options($responses); + if (is_array($options) && $options !== []) { + $args['options'] = $options; + } + + // Find appropriate default value. + $default_from_handler = $handler->default($responses); + + $env_var = static::makeEnvName($id); + $env_val = Env::get($env_var); + $default_from_env = is_null($env_val) ? NULL : Env::toValue($env_val); + + $default_from_discovery = $this->handlers[$id]->discover(); + + if (!is_null($default_from_env)) { + $default = $default_from_env; + } + elseif (!is_null($default_from_discovery)) { + $default = $default_from_discovery; + } + elseif (!is_null($default_override)) { + $default = $default_override; + } + else { + $default = $default_from_handler; + } + + if (!is_null($default) && $default !== '') { + $args['default'] = $default; + } + + return array_filter($args, fn($value): bool => $value !== NULL); + } + } diff --git a/.vortex/installer/src/Utils/Env.php b/.vortex/installer/src/Utils/Env.php index 54b2e3cba..8e5b3e192 100644 --- a/.vortex/installer/src/Utils/Env.php +++ b/.vortex/installer/src/Utils/Env.php @@ -18,7 +18,7 @@ class Env { public static function get(string $name, mixed $default = NULL): mixed { $vars = getenv(); - return !isset($vars[$name]) || $vars[$name] === '' ? $default : $vars[$name]; + return $vars[$name] ?? $default; } public static function put(string $name, string $value): void { diff --git a/.vortex/installer/tests/Unit/EnvTest.php b/.vortex/installer/tests/Unit/EnvTest.php index ae4f8b341..dc1508b50 100644 --- a/.vortex/installer/tests/Unit/EnvTest.php +++ b/.vortex/installer/tests/Unit/EnvTest.php @@ -61,7 +61,7 @@ public static function dataProviderGet(): array { ['VAR', 'VAL1', NULL, 'VAL1'], ['VAR', 'VAL1', 'VAL2', 'VAL1'], ['VAR', '', 'DEF1', 'DEF1'], - ['VAR', '', NULL, NULL], + ['VAR', '', NULL, ''], ['VAR', '', 'VAL2', 'VAL2'], ]; } diff --git a/.vortex/installer/tests/Unit/PromptManagerTest.php b/.vortex/installer/tests/Unit/PromptManagerTest.php index 3ca08abe7..f935b92b4 100644 --- a/.vortex/installer/tests/Unit/PromptManagerTest.php +++ b/.vortex/installer/tests/Unit/PromptManagerTest.php @@ -110,11 +110,11 @@ protected function tearDown(): void { * Test responses. * * @code - * composer test -- --filter=testPrompt@"name of the data provider" + * composer test -- --filter=testRunPrompts@"name of the data provider" * @endcode */ - #[DataProvider('dataProviderPrompt')] - public function testPrompt( + #[DataProvider('dataProviderRunPrompts')] + public function testRunPrompts( array $answers, array|string $expected, ?callable $before = NULL, @@ -133,12 +133,12 @@ public function testPrompt( $before($this, $config); } - $answers = array_replace(static::defaultAnswers(), $answers); + $answers = array_replace(static::defaultTuiAnswers(), $answers); $keystrokes = static::tuiKeystrokes($answers, 40); Prompt::fake($keystrokes); $pm = new PromptManager($config); - $pm->prompt(); + $pm->runPrompts(); if (!$exception) { $actual = $pm->getResponses(); @@ -146,7 +146,13 @@ public function testPrompt( } } - public static function defaultAnswers(): array { + /** + * The default answers for TUI prompts used in tests. + * + * @return array + * An associative array of prompt IDs and their default values. + */ + public static function defaultTuiAnswers(): array { return [ Name::id() => static::TUI_DEFAULT, MachineName::id() => static::TUI_DEFAULT, @@ -175,7 +181,8 @@ public static function defaultAnswers(): array { ]; } - public static function dataProviderPrompt(): array { + public static function dataProviderRunPrompts(): array { + // Expected defaults for a new project. $expected_defaults = [ Name::id() => 'myproject', MachineName::id() => 'myproject', @@ -204,6 +211,7 @@ public static function dataProviderPrompt(): array { AiCodeInstructions::id() => AiCodeInstructions::NONE, ]; + // Expected values for a pre-installed project. $expected_installed = [ CiProvider::id() => CiProvider::NONE, DependencyUpdatesProvider::id() => DependencyUpdatesProvider::NONE, @@ -213,7 +221,8 @@ public static function dataProviderPrompt(): array { PreserveDocsOnboarding::id() => FALSE, ] + $expected_defaults; - $expected_discovered = [ + // Expected values for a custom project. Used for testing discovery. + $expected_custom = [ Name::id() => 'Discovered project', MachineName::id() => 'discovered_project', Org::id() => 'Discovered project Org', @@ -232,9 +241,9 @@ public static function dataProviderPrompt(): array { 'project name - discovery' => [ [], - $expected_discovered, + $expected_custom, function (PromptManagerTest $test): void { - $test->stubComposerJsonValue('description', 'Drupal 10 Standard installation of Discovered project for Discovered project Org'); + $test->stubComposerJsonValue('description', 'Drupal 11 Standard installation of Discovered project for Discovered project Org'); }, ], 'invalid project name' => [ @@ -248,7 +257,7 @@ function (PromptManagerTest $test): void { Name::id() => 'myproject', MachineName::id() => 'discovered_project', Org::id() => 'myproject Org', - ] + $expected_discovered, + ] + $expected_custom, function (PromptManagerTest $test): void { $test->stubComposerJsonValue('name', 'discovered_project_org/discovered_project'); }, @@ -260,7 +269,7 @@ function (PromptManagerTest $test): void { MachineName::id() => 'discovered-project', Org::id() => 'myproject Org', GithubRepo::id() => 'discovered_project_org/discovered-project', - ] + $expected_discovered, + ] + $expected_custom, function (PromptManagerTest $test): void { $test->stubComposerJsonValue('name', 'discovered_project_org/discovered-project'); }, @@ -272,9 +281,9 @@ function (PromptManagerTest $test): void { 'org name - discovery' => [ [], - $expected_discovered, + $expected_custom, function (PromptManagerTest $test): void { - $test->stubComposerJsonValue('description', 'Drupal 10 Standard installation of Discovered project for Discovered project Org'); + $test->stubComposerJsonValue('description', 'Drupal 11 Standard installation of Discovered project for Discovered project Org'); }, ], 'org name - invalid' => [ @@ -288,7 +297,7 @@ function (PromptManagerTest $test): void { Name::id() => 'myproject', MachineName::id() => 'discovered_project', Org::id() => 'myproject Org', - ] + $expected_discovered, + ] + $expected_custom, function (PromptManagerTest $test): void { $test->stubComposerJsonValue('name', 'discovered_project_org/discovered_project'); }, @@ -301,7 +310,7 @@ function (PromptManagerTest $test): void { Org::id() => 'myproject Org', OrgMachineName::id() => 'discovered-project-org', GithubRepo::id() => 'discovered-project-org/discovered_project', - ] + $expected_discovered, + ] + $expected_custom, function (PromptManagerTest $test): void { $test->stubComposerJsonValue('name', 'discovered-project-org/discovered_project'); }, @@ -415,7 +424,6 @@ function (PromptManagerTest $test, Config $config): void { [Profile::id() => Key::DOWN . Key::ENTER], [Profile::id() => 'minimal'] + $expected_defaults, ], - 'profile - custom' => [ [Profile::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER . 'myprofile'], [Profile::id() => 'myprofile'] + $expected_defaults, @@ -647,7 +655,6 @@ function (PromptManagerTest $test, Config $config): void { 'database image - discovery' => [ [ - GithubRepo::id() => static::TUI_SKIP, DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER, ], [ @@ -660,7 +667,6 @@ function (PromptManagerTest $test, Config $config): void { ], 'database image - valid' => [ [ - GithubRepo::id() => static::TUI_SKIP, DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER, DatabaseImage::id() => 'myregistry/myimage:mytag', ], @@ -668,7 +674,6 @@ function (PromptManagerTest $test, Config $config): void { ], 'database image - invalid' => [ [ - GithubRepo::id() => static::TUI_SKIP, DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER, DatabaseImage::id() => 'myregistry:myimage:mytag', ], @@ -676,7 +681,6 @@ function (PromptManagerTest $test, Config $config): void { ], 'database image - invalid - capitalization' => [ [ - GithubRepo::id() => static::TUI_SKIP, DatabaseDownloadSource::id() => Key::DOWN . Key::DOWN . Key::DOWN . Key::DOWN . Key::ENTER, DatabaseImage::id() => 'MyRegistry/MyImage:mytag', ],