From b74f7f9d20bcbdf890327e52e5266f93532d9c6e Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Fri, 28 Nov 2025 23:54:17 +1100 Subject: [PATCH 1/8] [#1950] Added auto-build to installer. # Conflicts: # .vortex/installer/tests/Unit/ConfigTest.php --- .vortex/installer/installer.php | 10 +- .vortex/installer/phpstan.neon | 2 + .../installer/playground/task-streaming.php | 196 ++++++ .vortex/installer/playground/task.php | 268 ++++++++ .vortex/installer/playground/tui.php | 187 ++++++ .../installer/src/Command/BuildCommand.php | 213 ++++++ .../src/Command/CheckRequirementsCommand.php | 355 ++++++++++ .../installer/src/Command/InstallCommand.php | 175 ++++- .vortex/installer/src/Logger/FileLogger.php | 164 +++++ .../src/Logger/FileLoggerInterface.php | 39 ++ .../src/Logger/LoggableInterface.php | 36 + .../installer/src/Logger/LoggerInterface.php | 62 ++ .../installer/src/Runner/AbstractRunner.php | 409 +++++++++++ .../installer/src/Runner/CommandRunner.php | 146 ++++ .../installer/src/Runner/ProcessRunner.php | 164 +++++ .../installer/src/Runner/RunnerInterface.php | 88 +++ .../installer/src/{Utils => Task}/Task.php | 40 +- .vortex/installer/src/Task/TaskOutput.php | 123 ++++ .vortex/installer/src/Utils/Config.php | 2 + .vortex/installer/src/Utils/Tui.php | 25 + .../Functional/Command/BuildCommandTest.php | 388 +++++++++++ .../Command/CheckRequirementsCommandTest.php | 395 +++++++++++ .../Functional/Command/InstallCommandTest.php | 502 ++++++++++++++ .vortex/installer/tests/Helpers/TuiOutput.php | 263 ++++++++ .vortex/installer/tests/Traits/TuiTrait.php | 17 + .vortex/installer/tests/Unit/ConfigTest.php | 7 +- .../tests/Unit/Logger/FileLoggerTest.php | 390 +++++++++++ .../tests/Unit/Runner/AbstractRunnerTest.php | 635 ++++++++++++++++++ .../tests/Unit/Runner/CommandRunnerTest.php | 242 +++++++ .../tests/Unit/Runner/ProcessRunnerTest.php | 393 +++++++++++ .../tests/Unit/Task/TaskOutputTest.php | 128 ++++ .vortex/installer/tests/Unit/TaskTest.php | 282 +++++++- .vortex/installer/tests/Unit/TuiTest.php | 80 +++ 33 files changed, 6405 insertions(+), 21 deletions(-) create mode 100644 .vortex/installer/playground/task-streaming.php create mode 100755 .vortex/installer/playground/task.php create mode 100755 .vortex/installer/playground/tui.php create mode 100644 .vortex/installer/src/Command/BuildCommand.php create mode 100644 .vortex/installer/src/Command/CheckRequirementsCommand.php create mode 100644 .vortex/installer/src/Logger/FileLogger.php create mode 100644 .vortex/installer/src/Logger/FileLoggerInterface.php create mode 100644 .vortex/installer/src/Logger/LoggableInterface.php create mode 100644 .vortex/installer/src/Logger/LoggerInterface.php create mode 100644 .vortex/installer/src/Runner/AbstractRunner.php create mode 100644 .vortex/installer/src/Runner/CommandRunner.php create mode 100644 .vortex/installer/src/Runner/ProcessRunner.php create mode 100644 .vortex/installer/src/Runner/RunnerInterface.php rename .vortex/installer/src/{Utils => Task}/Task.php (66%) create mode 100644 .vortex/installer/src/Task/TaskOutput.php create mode 100644 .vortex/installer/tests/Functional/Command/BuildCommandTest.php create mode 100644 .vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php create mode 100644 .vortex/installer/tests/Functional/Command/InstallCommandTest.php create mode 100644 .vortex/installer/tests/Helpers/TuiOutput.php create mode 100644 .vortex/installer/tests/Unit/Logger/FileLoggerTest.php create mode 100644 .vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php create mode 100644 .vortex/installer/tests/Unit/Runner/CommandRunnerTest.php create mode 100644 .vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php create mode 100644 .vortex/installer/tests/Unit/Task/TaskOutputTest.php diff --git a/.vortex/installer/installer.php b/.vortex/installer/installer.php index 4670743e6..0cb07a484 100755 --- a/.vortex/installer/installer.php +++ b/.vortex/installer/installer.php @@ -8,6 +8,8 @@ declare(strict_types=1); +use DrevOps\VortexInstaller\Command\BuildCommand; +use DrevOps\VortexInstaller\Command\CheckRequirementsCommand; use DrevOps\VortexInstaller\Command\InstallCommand; use Symfony\Component\Console\Application; @@ -15,8 +17,10 @@ $application = new Application('Vortex Installer', '@vortex-installer-version@'); -$command = new InstallCommand(); -$application->add($command); -$application->setDefaultCommand($command->getName(), TRUE); +$application->add(new InstallCommand()); +$application->add(new CheckRequirementsCommand()); +$application->add(new BuildCommand()); + +$application->setDefaultCommand('install'); $application->run(); diff --git a/.vortex/installer/phpstan.neon b/.vortex/installer/phpstan.neon index 0e764f9b1..37d4fd00d 100644 --- a/.vortex/installer/phpstan.neon +++ b/.vortex/installer/phpstan.neon @@ -16,6 +16,8 @@ parameters: excludePaths: - vendor/* + treatPhpDocTypesAsCertain: false + ignoreErrors: - # Since tests and data providers do not have to have parameter docblocks, diff --git a/.vortex/installer/playground/task-streaming.php b/.vortex/installer/playground/task-streaming.php new file mode 100644 index 000000000..3370fd29a --- /dev/null +++ b/.vortex/installer/playground/task-streaming.php @@ -0,0 +1,196 @@ +#!/usr/bin/env php +writeln("Line 1 via Tui::output()..."); + usleep(500000); + Tui::output()->writeln("Line 2 via Tui::output()..."); + usleep(500000); + Tui::output()->writeln("Line 3 via Tui::output()..."); + usleep(500000); + return true; + }, + success: 'Tui::output() streaming completed', + streaming: true, +); +echo PHP_EOL; + +// Streaming mode with mixed output. +echo "--- Streaming mode: mixed echo and Tui::output() ---" . PHP_EOL; +Task::action( + label: 'Streaming task with mixed output', + action: function () { + echo "Line 1 via echo...\n"; + usleep(400000); + Tui::output()->writeln("Line 2 via Tui::output()..."); + usleep(400000); + echo "Line 3 via echo...\n"; + usleep(400000); + Tui::output()->writeln("Line 4 via Tui::output()..."); + usleep(400000); + return true; + }, + success: 'Mixed streaming completed', + streaming: true, +); +echo PHP_EOL; + +// Streaming mode with failure. +echo "--- Streaming mode: failure case ---" . PHP_EOL; +Task::action( + label: 'Streaming task that fails', + action: function () { + echo "Starting process...\n"; + usleep(500000); + echo "Error encountered!\n"; + usleep(500000); + return false; + }, + failure: 'Streaming task failed', + streaming: true, +); +echo PHP_EOL; + +// Task after failure - verify output is restored. +echo "--- Task after failure: verify output restoration ---" . PHP_EOL; +Task::action( + label: 'Task after failed streaming', + action: function () { + echo "This echo should be dimmed\n"; + usleep(500000); + Tui::output()->writeln("This Tui::output() should also be dimmed"); + usleep(500000); + return true; + }, + success: 'Output restoration verified', + streaming: true, +); +echo PHP_EOL; + +// Streaming mode without success message (default "OK"). +echo "--- Streaming mode: no success message (default OK) ---" . PHP_EOL; +Task::action( + label: 'Streaming task without success message', + action: function () { + echo "Some output...\n"; + usleep(500000); + return true; + }, + streaming: true, +); +echo PHP_EOL; + +// Streaming mode with nested spinner (simulates build command with requirements check). +echo "--- Streaming mode: nested spinner (cursor control) ---" . PHP_EOL; +Task::action( + label: 'Streaming task with nested spinner', + action: function () { + // The nested command uses spin() which outputs cursor control sequences. + \Laravel\Prompts\spin( + function () { + usleep(300000); + usleep(300000); + usleep(300000); + }, + 'Nested spinner task...' + ); + + echo "AFTER SPINNER 1\n"; + \Laravel\Prompts\spin( + function () { + usleep(1000000); + usleep(1000000); + }, + 'Another nested spinner task...' + ); + + echo "AFTER SPINNER 2\n"; + + return true; + }, + streaming: true, +); +echo PHP_EOL; + +// Streaming mode with colors and styles. +echo "--- Streaming mode: colors and styles ---" . PHP_EOL; +Task::action( + label: 'Streaming task with styled output', + action: function () { + Tui::output()->writeln(Tui::green("Green text")); + usleep(300000); + Tui::output()->writeln(Tui::blue("Blue text")); + usleep(300000); + Tui::output()->writeln(Tui::yellow("Yellow text")); + usleep(300000); + Tui::output()->writeln(Tui::underscore("Underscored text")); + usleep(300000); + Tui::output()->writeln(Tui::bold("Bold text")); + usleep(300000); + Tui::output()->writeln("Mixed: " . Tui::green("green") . " and " . Tui::blue("blue") . " and " . Tui::underscore("underscored")); + usleep(300000); + return true; + }, + success: 'Styled streaming completed', + streaming: true, +); +echo PHP_EOL; + +// Non-streaming task after streaming tasks. +echo "--- Non-streaming task (spinner) after streaming ---" . PHP_EOL; +Task::action( + label: 'Spinner task after streaming', + action: function () { + usleep(1000000); + return true; + }, + success: 'Spinner works after streaming', +); +echo PHP_EOL; + +echo "=== Demo Complete ===" . PHP_EOL; +echo PHP_EOL; diff --git a/.vortex/installer/playground/task.php b/.vortex/installer/playground/task.php new file mode 100755 index 000000000..8d2ef646e --- /dev/null +++ b/.vortex/installer/playground/task.php @@ -0,0 +1,268 @@ +#!/usr/bin/env php + "Success received: $result", +); +echo PHP_EOL; + +// Action returns integer, success callback uses it. +echo "--- Action returns integer ---" . PHP_EOL; +Task::action( + label: 'Action returns integer 42', + action: function () { + sleep(1); + return 42; + }, + success: fn($count) => "Success received integer: $count", +); +echo PHP_EOL; + +// Static success message (not a callback). +echo "--- Static success message (string, not callback) ---" . PHP_EOL; +Task::action( + label: 'Action with static success string', + action: function () { + sleep(1); + return true; + }, + success: 'Static success message', +); +echo PHP_EOL; + +// Task with hint parameter. +echo "--- Hint parameter shown below label ---" . PHP_EOL; +Task::action( + label: 'Label with hint parameter', + action: function () { + sleep(2); + return true; + }, + hint: 'This hint appears dimmed below the label', + success: 'Completed with hint', +); +echo PHP_EOL; + +// Action returns false - triggers failure path. +echo "--- Action returns false, triggers failure ---" . PHP_EOL; +Task::action( + label: 'Action returns false', + action: function () { + sleep(1); + return false; + }, + failure: 'Custom failure message', +); +echo PHP_EOL; + +// Action returns false with default failure message. +echo "--- Action returns false, default failure message ---" . PHP_EOL; +Task::action( + label: 'Action returns false, no failure param', + action: function () { + usleep(500000); + return false; + }, +); +echo PHP_EOL; + +// Action returns array - displayed as sublist. +echo "--- Action returns array, displayed as sublist ---" . PHP_EOL; +Task::action( + label: 'Action returns array of strings', + action: function () { + sleep(1); + return [ + 'Array item 1', + 'Array item 2', + 'Array item 3', + ]; + }, + success: 'Array items shown above', +); +echo PHP_EOL; + +// Dynamic label from closure. +echo "--- Label as closure (evaluated at runtime) ---" . PHP_EOL; +$dynamic_value = 'dynamic_' . rand(100, 999); +Task::action( + label: fn() => "Label from closure: $dynamic_value", + action: function () { + sleep(1); + return true; + }, +); +echo PHP_EOL; + +// Longer spinner duration. +echo "--- Longer duration (3s) to see spinner animation ---" . PHP_EOL; +Task::action( + label: 'Spinner runs for 3 seconds', + action: function () { + sleep(3); + return true; + }, + success: 'Spinner completed', +); +echo PHP_EOL; + +// Very short duration. +echo "--- Very short duration (100ms) ---" . PHP_EOL; +Task::action( + label: 'Spinner for 100ms only', + action: function () { + usleep(100000); + return true; + }, +); +echo PHP_EOL; + +// Multiple tasks in sequence. +echo "--- Multiple sequential tasks ---" . PHP_EOL; +for ($i = 1; $i <= 3; $i++) { + Task::action( + label: "Sequential task $i of 3", + action: function () use ($i) { + usleep($i * 300000); + return true; + }, + success: "Task $i done", + ); +} +echo PHP_EOL; + +// Hint as closure. +echo "--- Hint as closure (evaluated at runtime) ---" . PHP_EOL; +Task::action( + label: 'Task with dynamic hint', + action: function () { + sleep(1); + return true; + }, + hint: fn() => 'Hint from closure: ' . date('H:i:s'), +); +echo PHP_EOL; + +// Success as closure receiving null (when action returns true). +echo "--- Success callback receives true (not useful) ---" . PHP_EOL; +Task::action( + label: 'Action returns true, success gets true', + action: function () { + sleep(1); + return true; + }, + success: fn($result) => "Success callback got: " . var_export($result, true), +); +echo PHP_EOL; + +// Failure as closure. +echo "--- Failure as closure ---" . PHP_EOL; +Task::action( + label: 'Action returns false, failure is closure', + action: function () { + usleep(500000); + return false; + }, + failure: fn() => 'Failure from closure: ' . date('H:i:s'), +); +echo PHP_EOL; + +// Streaming mode - no spinner, output streams during action. +echo "--- Streaming mode: output streams during action ---" . PHP_EOL; +Task::action( + label: 'Streaming task with output', + action: function () { + echo "Line 1 of output...\n"; + usleep(500000); + echo "Line 2 of output...\n"; + usleep(500000); + echo "Line 3 of output...\n"; + usleep(500000); + return true; + }, + success: 'Streaming completed', + streaming: true, +); +echo PHP_EOL; + +// Streaming mode with failure. +echo "--- Streaming mode: action returns false ---" . PHP_EOL; +Task::action( + label: 'Streaming task that fails', + action: function () { + echo "Starting process...\n"; + usleep(500000); + echo "Error encountered!\n"; + usleep(500000); + return false; + }, + failure: 'Streaming task failed', + streaming: true, +); +echo PHP_EOL; + +// Streaming mode with longer output. +echo "--- Streaming mode: simulated build output ---" . PHP_EOL; +Task::action( + label: 'Building project', + action: function () { + $steps = [ + 'Installing dependencies...', + 'Compiling assets...', + 'Running migrations...', + 'Clearing caches...', + 'Build complete.', + ]; + foreach ($steps as $step) { + echo "$step\n"; + usleep(400000); + } + return true; + }, + success: 'Project built successfully', + streaming: true, +); +echo PHP_EOL; + +echo "=== Demo Complete ===" . PHP_EOL; +echo PHP_EOL; diff --git a/.vortex/installer/playground/tui.php b/.vortex/installer/playground/tui.php new file mode 100755 index 000000000..54bca3b7e --- /dev/null +++ b/.vortex/installer/playground/tui.php @@ -0,0 +1,187 @@ +#!/usr/bin/env php + 'Black', + 31 => 'Red', + 32 => 'Green', + 33 => 'Yellow', + 34 => 'Blue', + 35 => 'Magenta', + 36 => 'Cyan', + 37 => 'White', + 90 => 'Bright Black', + 91 => 'Bright Red', + 92 => 'Bright Green', + 93 => 'Bright Yellow', + 94 => 'Bright Blue', + 95 => 'Bright Magenta', + 96 => 'Bright Cyan', + 97 => 'Bright White', +]; + +foreach ($colors as $code => $name) { + $w = 15; + $pad = str_repeat(' ', max(0, $w - strlen($name))); + + // Style codes: 1=bold, 2=dim, 4=underscore. + $normal = sprintf("\033[%sm%s\033[0m%s", $code, $name, $pad); + $dim = sprintf("\033[2;%sm%s\033[0m%s", $code, $name, $pad); + $under = sprintf("\033[4;%sm%s\033[0m%s", $code, $name, $pad); + $under_dim = sprintf("\033[2;4;%sm%s\033[0m%s", $code, $name, $pad); + $bold = sprintf("\033[1;%sm%s\033[0m%s", $code, $name, $pad); + $bold_dim = sprintf("\033[1;2;%sm%s\033[0m%s", $code, $name, $pad); + $bold_under = sprintf("\033[1;4;%sm%s\033[0m%s", $code, $name, $pad); + $bold_under_dim = sprintf("\033[1;2;4;%sm%s\033[0m%s", $code, $name, $pad); + + echo sprintf("%3d: %s %s %s %s %s %s %s %s", $code, $normal, $dim, $under, $under_dim, $bold, $bold_dim, $bold_under, $bold_under_dim) . PHP_EOL; +} +echo PHP_EOL; + +// Tui helper colors. +echo "--- Tui Helper Colors ---" . PHP_EOL; +echo Tui::green("This is green text") . PHP_EOL; +echo Tui::blue("This is blue text") . PHP_EOL; +echo Tui::purple("This is purple text") . PHP_EOL; +echo Tui::yellow("This is yellow text") . PHP_EOL; +echo Tui::cyan("This is cyan text") . PHP_EOL; +echo PHP_EOL; + +// Text styles. +echo "--- Text Styles ---" . PHP_EOL; +echo Tui::bold("This is bold text") . PHP_EOL; +echo Tui::underscore("This is underscored text") . PHP_EOL; +echo Tui::dim("This is dimmed text") . PHP_EOL; +echo "This is normal text for comparison" . PHP_EOL; +echo PHP_EOL; + +// Combinations. +echo "--- Combinations ---" . PHP_EOL; +echo Tui::bold(Tui::green("Bold green text")) . PHP_EOL; +echo Tui::dim(Tui::cyan("Dimmed cyan text")) . PHP_EOL; +echo Tui::bold(Tui::yellow("Bold yellow text")) . PHP_EOL; +echo PHP_EOL; + +// Dim with reset codes (simulating external command output). +echo "--- Dim with embedded resets ---" . PHP_EOL; +$simulated_output = "\033[32mGreen text\033[0m then normal \033[34mblue text\033[0m end"; +echo "Original: " . $simulated_output . PHP_EOL; +echo "Dimmed: " . Tui::dim($simulated_output) . PHP_EOL; +echo PHP_EOL; + +// Multiline. +echo "--- Multiline ---" . PHP_EOL; +$multiline = "Line one\nLine two\nLine three"; +echo Tui::green($multiline) . PHP_EOL; +echo PHP_EOL; +echo Tui::dim($multiline) . PHP_EOL; +echo PHP_EOL; + +// Box. +echo "--- Box ---" . PHP_EOL; +Tui::box("This is content inside a box.\nIt can have multiple lines.", "Box Title"); +echo PHP_EOL; + +// Info/Note/Error (Laravel Prompts styles). +echo "--- Messages ---" . PHP_EOL; +Tui::info("This is an info message"); +Tui::note("This is a note message"); +Tui::success("This is a success message"); +Tui::error("This is an error message"); +echo PHP_EOL; + +// List. +echo "--- List ---" . PHP_EOL; +Tui::list([ + 'Project name' => 'my_project', + 'Machine name' => 'my_project', + 'Organization' => 'My Organization', + 'Services' => Tui::LIST_SECTION_TITLE, + 'Database' => 'MySQL 8.0', + 'Cache' => 'Redis', + 'Search' => 'Solr', +], 'Configuration Summary'); +echo PHP_EOL; + +// Terminal width. +$term_width = Tui::terminalWidth(); +echo "--- Terminal Info ---" . PHP_EOL; +echo "Terminal width: " . $term_width . " columns" . PHP_EOL; +echo PHP_EOL; + +// Ruler function. +$make_ruler = function(int $width): string { + $ruler_top = ''; + $ruler_num = ''; + $ruler_bot = ''; + for ($i = 1; $i <= $width; $i++) { + if ($i % 10 === 0) { + $ruler_top .= '|'; + $num = (string)$i; + $ruler_num .= $num[strlen($num) - 2] ?? ' '; + $ruler_bot .= $num[strlen($num) - 1]; + } elseif ($i % 5 === 0) { + $ruler_top .= '+'; + $ruler_num .= ' '; + $ruler_bot .= '5'; + } else { + $ruler_top .= '-'; + $ruler_num .= ' '; + $ruler_bot .= ' '; + } + } + return $ruler_top . PHP_EOL . $ruler_num . PHP_EOL . $ruler_bot; +}; + +// Visual terminal boundaries with ruler. +echo "--- Terminal Boundaries with Ruler ---" . PHP_EOL; +echo $make_ruler($term_width) . PHP_EOL; +echo "|" . str_repeat(" ", $term_width - 2) . "|" . PHP_EOL; +echo str_repeat("=", $term_width) . PHP_EOL; +echo PHP_EOL; + +// Center. +echo "--- Centered Text (within terminal width: $term_width) ---" . PHP_EOL; +echo "|" . str_repeat("-", $term_width - 2) . "|" . PHP_EOL; +echo Tui::center("Centered Title", $term_width) . PHP_EOL; +echo Tui::center("Line 1\nLine 2\nLonger Line 3", $term_width) . PHP_EOL; +echo "|" . str_repeat("-", $term_width - 2) . "|" . PHP_EOL; +echo PHP_EOL; + +// Center with fixed width. +$fixed_width = 60; +echo "--- Centered Text (fixed width: $fixed_width) ---" . PHP_EOL; +echo "|" . str_repeat("-", $fixed_width - 2) . "|" . PHP_EOL; +echo Tui::center("Centered Title", $fixed_width) . PHP_EOL; +echo Tui::center("Short\nMedium text\nThis is a longer line", $fixed_width) . PHP_EOL; +echo "|" . str_repeat("-", $fixed_width - 2) . "|" . PHP_EOL; +echo PHP_EOL; + +echo "=== Demo Complete ===" . PHP_EOL; +echo PHP_EOL; diff --git a/.vortex/installer/src/Command/BuildCommand.php b/.vortex/installer/src/Command/BuildCommand.php new file mode 100644 index 000000000..43450a885 --- /dev/null +++ b/.vortex/installer/src/Command/BuildCommand.php @@ -0,0 +1,213 @@ +setName('build'); + $this->setDescription('Build the site using ahoy build.'); + $this->setHelp('Checks requirements and runs ahoy build to set up the local site.'); + $this->addOption(static::OPTION_PROFILE, 'p', InputOption::VALUE_NONE, 'Build from install profile instead of loading database.'); + $this->addOption(static::OPTION_SKIP_REQUIREMENTS_CHECK, NULL, InputOption::VALUE_NONE, 'Skip checking for required tools.'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + Tui::init($output); + + $this->isProfile = (bool) $input->getOption(static::OPTION_PROFILE); + $cwd = getcwd() ?: '.'; + + if (!$input->getOption(static::OPTION_SKIP_REQUIREMENTS_CHECK)) { + $requirements_ok = Task::action( + label: 'Checking requirements', + action: function (): bool { + $runner = (new CommandRunner($this->getApplication()))->disableLog(); + $runner->run('check-requirements', [], ['--no-summary' => '1']); + + return $runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; + }, + failure: 'Missing requirements. Run: ./installer.php check-requirements', + streaming: TRUE, + ); + + if (!$requirements_ok) { + return Command::FAILURE; + } + } + + $build_ok = Task::action( + label: 'Building site', + action: function () use ($cwd): bool { + $env = [ + 'AHOY_CONFIRM_RESPONSE' => 'y', + 'AHOY_CONFIRM_WAIT_SKIP' => '1', + ]; + + if ($this->isProfile) { + $env['VORTEX_PROVISION_TYPE'] = 'profile'; + } + + $this->runner = $this->getRunner()->setCwd($cwd); + $this->runner->run('ahoy build', env: $env); + + return $this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; + }, + success: fn(bool $result): string => $result ? 'Build completed' : 'Build failed', + failure: 'Build failed', + streaming: TRUE, + ); + + if ($build_ok) { + $this->showSuccessSummary(); + return Command::SUCCESS; + } + + $this->showFailureSummary(); + return Command::FAILURE; + } + + /** + * Get the project machine name from .env. + */ + protected function getProjectMachineName(): string { + $cwd = getcwd() ?: '.'; + $env_file = $cwd . '/.env'; + + if (file_exists($env_file)) { + $content = file_get_contents($env_file); + if ($content !== FALSE && preg_match('/^VORTEX_PROJECT=(.+)$/m', $content, $matches)) { + return trim($matches[1]); + } + } + + return basename($cwd); + } + + /** + * Display success summary. + */ + protected function showSuccessSummary(): void { + $output = ''; + $title = 'Build completed successfully!'; + + $output .= 'Site URL: http://' . $this->getProjectMachineName() . '.docker.amazee.io' . PHP_EOL; + $output .= 'Login: ahoy login' . PHP_EOL; + $output .= PHP_EOL; + + $log_path = $this->runner->getLogger()->getPath(); + if ($log_path) { + $output .= 'Log file: ' . $log_path . PHP_EOL; + $output .= PHP_EOL; + } + + $output .= 'Next steps:' . PHP_EOL; + if ($this->isProfile) { + $output .= ' - Export database: ahoy export-db' . PHP_EOL; + } + $output .= ' - Review hosting/provisioning docs' . PHP_EOL; + + Tui::box($output, $title); + } + + /** + * Display failure summary. + */ + protected function showFailureSummary(): void { + Tui::line(''); + + $command = $this->runner->getCommand(); + if ($command) { + Tui::line('Failed at: ' . $command); + } + + $exit_code = $this->runner->getExitCode(); + Tui::line('Exit code: ' . $exit_code); + + $log_path = $this->runner->getLogger()->getPath(); + if ($log_path) { + Tui::line('Log file: ' . $log_path); + } + + Tui::line(''); + + // Show last 10 lines of output for context. + $runner_output = $this->runner->getOutput(as_array: TRUE); + + if (!is_array($runner_output)) { + throw new \RuntimeException('Runner output is not an array.'); + } + + $last_lines = array_slice($runner_output, -10); + if (!empty($last_lines)) { + Tui::line('Last output:'); + foreach ($last_lines as $last_line) { + Tui::line(' ' . $last_line); + } + } + } + + /** + * Get the process runner. + * + * Factory method that returns existing runner or creates new one. + */ + protected function getRunner(): ProcessRunner { + // Return already-set runner if available (for testing). + return $this->runner ?? (new ProcessRunner()); + } + + /** + * Set the process runner. + * + * Allows dependency injection for testing. + */ + public function setRunner(ProcessRunner $runner): void { + $this->runner = $runner; + } + +} diff --git a/.vortex/installer/src/Command/CheckRequirementsCommand.php b/.vortex/installer/src/Command/CheckRequirementsCommand.php new file mode 100644 index 000000000..e382567e1 --- /dev/null +++ b/.vortex/installer/src/Command/CheckRequirementsCommand.php @@ -0,0 +1,355 @@ + + */ + protected array $present = []; + + /** + * Missing tools with installation instructions. + * + * @var array + */ + protected array $missing = []; + + /** + * {@inheritdoc} + */ + protected function configure(): void { + $this->setName('check-requirements'); + $this->setDescription('Check if required tools are installed and running.'); + $this->setHelp('Checks for Docker, Docker Compose, Ahoy, and Pygmy.'); + $this->addOption(static::OPTION_ONLY, 'o', InputOption::VALUE_REQUIRED, sprintf('Comma-separated list of requirements to check. Available: %s.', implode(', ', static::REQUIREMENTS))); + $this->addOption(static::OPTION_NO_SUMMARY, NULL, InputOption::VALUE_NONE, 'Hide summary with tool versions.'); + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + Tui::init($output); + + $only = $input->getOption(static::OPTION_ONLY); + $requirements = $this->validateRequirements($only ? array_map(trim(...), explode(',', (string) $only)) : NULL); + + $this->runner = $this->getRunner(); + $this->present = []; + $this->missing = []; + + if (in_array(static::REQ_DOCKER, $requirements, TRUE)) { + Task::action( + label: 'Checking Docker', + action: fn(): bool => $this->checkDocker(), + success: fn(bool $result): string => $result ? 'Docker is available' : 'Docker is missing', + ); + } + + if (in_array(static::REQ_DOCKER_COMPOSE, $requirements, TRUE)) { + Task::action( + label: 'Checking Docker Compose', + action: fn(): bool => $this->checkDockerCompose(), + success: fn(bool $result): string => $result ? 'Docker Compose is available' : 'Docker Compose is missing', + ); + } + + if (in_array(static::REQ_AHOY, $requirements, TRUE)) { + Task::action( + label: 'Checking Ahoy', + action: fn(): bool => $this->checkAhoy(), + success: fn(bool $result): string => $result ? 'Ahoy is available' : 'Ahoy is missing', + ); + } + + if (in_array(static::REQ_PYGMY, $requirements, TRUE)) { + Task::action( + label: 'Checking Pygmy', + action: fn(): bool => $this->checkPygmy(), + success: fn(bool $result): string => $result ? 'Pygmy is running' : 'Pygmy is not running', + ); + } + + if (!$input->getOption(static::OPTION_NO_SUMMARY)) { + $summary = $this->getResultsSummary(); + Tui::box($summary['content'], $summary['title']); + } + elseif (empty($this->missing)) { + Tui::success('All requirements met.'); + } + + return empty($this->missing) ? Command::SUCCESS : Command::FAILURE; + } + + /** + * Validate and return requirements to check. + * + * @param array|null $only + * Array of requirement names to check. NULL to check all. + * + * @return array + * Array of validated requirement names. + * + * @throws \InvalidArgumentException + * If an unknown requirement is specified. + */ + protected function validateRequirements(?array $only): array { + if ($only !== NULL) { + $unknown = array_diff($only, static::REQUIREMENTS); + if (!empty($unknown)) { + throw new \InvalidArgumentException(sprintf('Unknown requirements: %s. Available: %s.', implode(', ', $unknown), implode(', ', static::REQUIREMENTS))); + } + } + + return $only ?? static::REQUIREMENTS; + } + + /** + * Get present tools. + * + * @return array + * An array of present tools with tool name as key and path as value. + */ + public function getPresent(): array { + return $this->present; + } + + /** + * Get missing tools. + * + * @return array + * An array of missing tools with tool name as key and message as value. + */ + public function getMissing(): array { + return $this->missing; + } + + /** + * Get all check results merged. + * + * @return array + * Combined array of present and missing tools. + */ + public function getResults(): array { + return array_merge($this->present, $this->missing); + } + + /** + * Get a formatted summary of check results. + * + * @return array{title: string, content: string} + * Array with 'title' and 'content' keys for the summary. + */ + public function getResultsSummary(): array { + $content = ''; + + if (!empty($this->present)) { + $content .= 'Present:' . PHP_EOL; + foreach ($this->present as $tool => $status) { + $content .= ' - ' . $tool . ': ' . $status . PHP_EOL; + } + } + + if (!empty($this->missing)) { + if (!empty($content)) { + $content .= PHP_EOL; + } + $content .= 'Missing:' . PHP_EOL; + foreach ($this->missing as $tool => $instruction) { + $content .= ' - ' . $tool . ': ' . $instruction . PHP_EOL; + } + $content .= PHP_EOL; + + return [ + 'title' => 'Missing requirements', + 'content' => $content, + ]; + } + + return [ + 'title' => 'All requirements met', + 'content' => $content, + ]; + } + + /** + * Check if Docker is available. + */ + protected function checkDocker(): bool { + $result = $this->commandExists('docker'); + if ($result) { + $this->present['Docker'] = $this->getCommandVersion('docker --version'); + } + else { + $this->missing['Docker'] = 'https://www.docker.com/get-started'; + } + return $result; + } + + /** + * Check if Docker Compose is available. + */ + protected function checkDockerCompose(): bool { + $result = $this->dockerComposeExists(); + if ($result) { + $this->present['Docker Compose'] = $this->getCommandVersion('docker compose version'); + } + else { + $this->missing['Docker Compose'] = 'https://docs.docker.com/compose/install/'; + } + return $result; + } + + /** + * Check if Ahoy is available. + */ + protected function checkAhoy(): bool { + $result = $this->commandExists('ahoy'); + if ($result) { + $this->present['Ahoy'] = $this->getCommandVersion('ahoy --version'); + } + else { + $this->missing['Ahoy'] = 'https://github.com/ahoy-cli/ahoy'; + } + return $result; + } + + /** + * Check if Pygmy is running. + */ + protected function checkPygmy(): bool { + if (!$this->commandExists('pygmy')) { + $this->missing['Pygmy'] = 'Run: pygmy up'; + return FALSE; + } + + $version = $this->getCommandVersion('pygmy version'); + + $this->runner->run('pygmy status'); + if ($this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { + $this->present['Pygmy'] = $version; + return TRUE; + } + + $this->runner->run('docker ps --format "{{.Names}}" | grep -q amazeeio'); + // @phpstan-ignore-next-line notIdentical.alwaysFalse + if ($this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { + $this->present['Pygmy'] = $version; + return TRUE; + } + + $this->missing['Pygmy'] = 'Run: pygmy up'; + return FALSE; + } + + /** + * Check if a command exists. + */ + protected function commandExists(string $command): bool { + $this->runner->run(sprintf('command -v %s', escapeshellarg($command))); + return $this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; + } + + /** + * Check if Docker Compose exists. + */ + protected function dockerComposeExists(): bool { + $this->runner->run('docker compose version'); + if ($this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { + return TRUE; + } + + return $this->commandExists('docker-compose'); + } + + /** + * Get command version output. + * + * @param string $command + * The command to run. + * @param int $lines + * Number of lines to retrieve from the output. Defaults to 1. + */ + protected function getCommandVersion(string $command, int $lines = 1): string { + $this->runner->run($command); + $raw_output = $this->runner->getOutput(FALSE, $lines); + $output = trim(is_string($raw_output) ? $raw_output : implode(PHP_EOL, $raw_output)); + return empty($output) ? 'Available' : $output; + } + + /** + * Get the process runner instance. + * + * Factory method to create the runner, allowing tests to override this + * to inject mocks via setRunner(). + * + * @return \DrevOps\VortexInstaller\Runner\ProcessRunner + * The process runner instance. + */ + protected function getRunner(): ProcessRunner { + // Return already-set runner if available (for testing). + return $this->runner ?? (new ProcessRunner())->disableLog()->disableStreaming(); + } + + /** + * Set the process runner instance. + * + * Allows dependency injection for testing. + * + * @param \DrevOps\VortexInstaller\Runner\ProcessRunner $runner + * The process runner instance. + */ + public function setRunner(ProcessRunner $runner): void { + $this->runner = $runner; + } + +} diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index 0e2f5a598..bad8388f4 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -5,12 +5,16 @@ namespace DrevOps\VortexInstaller\Command; use DrevOps\VortexInstaller\Downloader\Downloader; +use DrevOps\VortexInstaller\Prompts\Handlers\Starter; use DrevOps\VortexInstaller\Prompts\PromptManager; +use DrevOps\VortexInstaller\Runner\CommandRunner; +use DrevOps\VortexInstaller\Runner\ProcessRunner; +use DrevOps\VortexInstaller\Runner\RunnerInterface; +use DrevOps\VortexInstaller\Task\Task; use DrevOps\VortexInstaller\Utils\Config; use DrevOps\VortexInstaller\Utils\Env; use DrevOps\VortexInstaller\Utils\File; use DrevOps\VortexInstaller\Utils\Strings; -use DrevOps\VortexInstaller\Utils\Task; use DrevOps\VortexInstaller\Utils\Tui; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; @@ -41,6 +45,8 @@ class InstallCommand extends Command { const OPTION_NO_CLEANUP = 'no-cleanup'; + const OPTION_BUILD = 'build'; + /** * Defines default command name. * @@ -58,11 +64,26 @@ class InstallCommand extends Command { */ protected PromptManager $promptManager; + /** + * The command runner. + */ + protected ?CommandRunner $runner = NULL; + + /** + * The process runner. + */ + protected ?ProcessRunner $processRunner = NULL; + + /** + * The downloader. + */ + protected ?Downloader $downloader = NULL; + /** * {@inheritdoc} */ protected function configure(): void { - $this->setName('Vortex Installer'); + $this->setName('install'); $this->setDescription('Install Vortex from remote or local repository.'); $this->setHelp(<<addOption(static::OPTION_CONFIG, 'c', InputOption::VALUE_REQUIRED, 'A JSON string with options or a path to a JSON file.'); $this->addOption(static::OPTION_URI, 'l', InputOption::VALUE_REQUIRED, 'Remote or local repository URI with an optional git ref set after @.'); $this->addOption(static::OPTION_NO_CLEANUP, NULL, InputOption::VALUE_NONE, 'Do not remove installer after successful installation.'); + $this->addOption(static::OPTION_BUILD, 'b', InputOption::VALUE_NONE, 'Run auto-build after installation without prompting.'); } /** @@ -127,7 +149,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int Task::action( label: 'Downloading Vortex', action: function (): string { - $version = (new Downloader())->download($this->config->get(Config::REPO), $this->config->get(Config::REF), $this->config->get(Config::TMP)); + $version = $this->getDownloader()->download($this->config->get(Config::REPO), $this->config->get(Config::REF), $this->config->get(Config::TMP)); $this->config->set(Config::VERSION, $version); return $version; }, @@ -155,7 +177,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int Task::action( label: 'Preparing demo content', - action: fn(): string|array => $this->handleDemo(), + action: fn(): string|array => $this->prepareDemo(), success: 'Demo content prepared', ); } @@ -168,6 +190,34 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->footer(); + $should_build = $this->config->get(Config::BUILD_NOW); + if (!$should_build && !$this->config->getNoInteraction()) { + $should_build = Tui::confirm( + label: 'Run the site build now?', + default: TRUE, + hint: 'Takes ~5-10 min; output will be streamed. You can skip and run later with: ahoy build', + ); + } + + if ($should_build) { + $build_ok = Task::action( + label: 'Building site', + action: fn(): bool => $this->runBuildCommand($output), + streaming: TRUE, + ); + + if (!$build_ok) { + Tui::error('Build failed. The site was installed but build process encountered errors.'); + Tui::line(''); + Tui::line('Next steps:'); + Tui::line(' - Run: ahoy build'); + Tui::line(' - Or inspect logs for details'); + Tui::line(''); + + return Command::FAILURE; + } + } + // Cleanup should take place only in case of the successful installation. // Otherwise, the user should be able to re-run the installer. register_shutdown_function([$this, 'cleanup']); @@ -176,19 +226,28 @@ protected function execute(InputInterface $input, OutputInterface $output): int } protected function checkRequirements(): void { - if (passthru('command -v git >/dev/null') === FALSE) { + $runner = $this->getProcessRunner(); + + $runner->run('command -v git >/dev/null'); + if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { throw new \RuntimeException('Missing git.'); } - if (passthru('command -v curl >/dev/null') === FALSE) { + $runner->run('command -v curl >/dev/null'); + // @phpstan-ignore-next-line notIdentical.alwaysFalse + if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { throw new \RuntimeException('Missing curl.'); } - if (passthru('command -v tar >/dev/null') === FALSE) { + $runner->run('command -v tar >/dev/null'); + // @phpstan-ignore-next-line notIdentical.alwaysFalse + if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { throw new \RuntimeException('Missing tar.'); } - if (passthru('command -v composer >/dev/null') === FALSE) { + $runner->run('command -v composer >/dev/null'); + // @phpstan-ignore-next-line notIdentical.alwaysFalse + if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { throw new \RuntimeException('Missing Composer.'); } } @@ -259,6 +318,9 @@ protected function resolveOptions(array $arguments, array $options): void { // Set no-cleanup flag. $this->config->set(Config::NO_CLEANUP, (bool) $options[static::OPTION_NO_CLEANUP]); + + // Set build-now flag. + $this->config->set(Config::BUILD_NOW, (bool) $options[static::OPTION_BUILD]); } protected function prepareDestination(): array { @@ -327,7 +389,13 @@ protected function copyFiles(): void { } } - protected function handleDemo(): array|string { + /** + * Prepare demo content if in demo mode. + * + * @return array|string + * Array of messages or a single message. + */ + protected function prepareDemo(): array|string { if (empty($this->config->get(Config::IS_DEMO))) { return 'Not a demo mode.'; } @@ -368,6 +436,26 @@ protected function handleDemo(): array|string { return $messages; } + /** + * Run the 'build' command. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output + * The output interface. + * + * @return bool + * TRUE if the build command succeeded, FALSE otherwise. + */ + protected function runBuildCommand(OutputInterface $output): bool { + $responses = $this->promptManager->getResponses(); + $starter = $responses[Starter::id()] ?? Starter::LOAD_DATABASE_DEMO; + $is_profile = in_array($starter, [Starter::INSTALL_PROFILE_CORE, Starter::INSTALL_PROFILE_DRUPALCMS], TRUE); + + $runner = $this->getRunner(); + $runner->run('build', args: $is_profile ? ['--profile' => '1'] : [], output: $output); + + return $runner->getExitCode() === Command::SUCCESS; + } + protected function header(): void { $logo_large = <<runner ?? new CommandRunner($this->getApplication()); + } + + /** + * Set the command runner. + * + * @param \DrevOps\VortexInstaller\Runner\CommandRunner $runner + * The command runner. + */ + public function setRunner(CommandRunner $runner): void { + $this->runner = $runner; + } + + /** + * Get the process runner. + * + * Provides a default ProcessRunner instance or returns the injected one. + * This allows tests to inject mocks via setProcessRunner(). + * + * @return \DrevOps\VortexInstaller\Runner\ProcessRunner + * The process runner. + */ + protected function getProcessRunner(): ProcessRunner { + return $this->processRunner ?? (new ProcessRunner())->disableLog()->disableStreaming(); + } + + /** + * Set the process runner. + * + * @param \DrevOps\VortexInstaller\Runner\ProcessRunner $runner + * The process runner. + */ + public function setProcessRunner(ProcessRunner $runner): void { + $this->processRunner = $runner; + } + + /** + * Get the downloader. + * + * Provides a default Downloader instance or returns the injected one. + * This allows tests to inject mocks via setDownloader(). + * + * @return \DrevOps\VortexInstaller\Downloader\Downloader + * The downloader. + */ + protected function getDownloader(): Downloader { + return $this->downloader ?? new Downloader(); + } + + /** + * Set the downloader. + * + * @param \DrevOps\VortexInstaller\Downloader\Downloader $downloader + * The downloader. + */ + public function setDownloader(Downloader $downloader): void { + $this->downloader = $downloader; + } + } diff --git a/.vortex/installer/src/Logger/FileLogger.php b/.vortex/installer/src/Logger/FileLogger.php new file mode 100644 index 000000000..cbe958be1 --- /dev/null +++ b/.vortex/installer/src/Logger/FileLogger.php @@ -0,0 +1,164 @@ +enabled) { + return FALSE; + } + + $name = $this->buildFilename($command, $args); + $this->path = $this->getDir() . '/' . static::LOG_DIR . '/' . $name . '-' . date('Y-m-d-His') . '.log'; + + $log_dir = dirname($this->path); + if (!is_dir($log_dir)) { + File::mkdir($log_dir); + } + + // @phpstan-ignore-next-line + $this->handle = fopen($this->path, 'w'); + + return $this->handle !== FALSE; + } + + /** + * {@inheritdoc} + */ + public function write(string $content): void { + if ($this->handle !== NULL) { + fwrite($this->handle, $content); + } + } + + /** + * {@inheritdoc} + */ + public function close(): void { + if ($this->handle !== NULL) { + fclose($this->handle); + $this->handle = NULL; + } + } + + /** + * {@inheritdoc} + */ + public function getPath(): ?string { + return $this->path; + } + + /** + * {@inheritdoc} + */ + public function isEnabled(): bool { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function enable(): static { + $this->enabled = TRUE; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function disable(): static { + $this->enabled = FALSE; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function setDir(string $dir): static { + $this->dir = $dir; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getDir(): string { + if ($this->dir === '') { + $this->dir = (string) getcwd(); + } + + return $this->dir; + } + + /** + * Build log filename from command and arguments. + * + * @param string $command + * The base command. + * @param array $args + * Command arguments (not options). + * + * @return string + * Sanitized filename suitable for log file. + */ + protected function buildFilename(string $command, array $args = []): string { + $parts = [$command]; + + // Only include positional arguments, not options (starting with -). + foreach ($args as $arg) { + if (!str_starts_with($arg, '-')) { + $parts[] = $arg; + } + } + + // Sanitize for use in filename. + $name = implode('-', $parts); + $name = (string) preg_replace('/[^a-zA-Z0-9\-_]/', '-', $name); + $name = (string) preg_replace('/-+/', '-', $name); + $name = trim($name, '-'); + + return $name !== '' ? $name : 'runner'; + } + +} diff --git a/.vortex/installer/src/Logger/FileLoggerInterface.php b/.vortex/installer/src/Logger/FileLoggerInterface.php new file mode 100644 index 000000000..f8c2b6a8a --- /dev/null +++ b/.vortex/installer/src/Logger/FileLoggerInterface.php @@ -0,0 +1,39 @@ + $args + * Command arguments (positional, not options). + * + * @return bool + * TRUE if log was opened, FALSE if logging is disabled. + */ + public function open(string $command, array $args = []): bool; + + /** + * Write content to the log. + * + * @param string $content + * Content to write. + */ + public function write(string $content): void; + + /** + * Close the log. + */ + public function close(): void; + + /** + * Check if logging is enabled. + * + * @return bool + * TRUE if logging is enabled. + */ + public function isEnabled(): bool; + + /** + * Enable logging. + * + * @return static + * The logger instance for method chaining. + */ + public function enable(): static; + + /** + * Disable logging. + * + * @return static + * The logger instance for method chaining. + */ + public function disable(): static; + +} diff --git a/.vortex/installer/src/Runner/AbstractRunner.php b/.vortex/installer/src/Runner/AbstractRunner.php new file mode 100644 index 000000000..050b80da9 --- /dev/null +++ b/.vortex/installer/src/Runner/AbstractRunner.php @@ -0,0 +1,409 @@ + + */ + protected int $exitCode = 0; + + /** + * The output from the last run. + */ + protected string $output = ''; + + /** + * The working directory. + */ + protected string $cwd = ''; + + /** + * The logger instance. + */ + protected FileLoggerInterface $logger; + + /** + * Whether to stream output to console. + */ + protected bool $shouldStream = TRUE; + + /** + * {@inheritdoc} + */ + public function getLogger(): FileLoggerInterface { + if (!isset($this->logger)) { + $this->logger = new FileLogger(); + } + + return $this->logger; + } + + /** + * Initialize the logger for a command execution. + * + * Sets the logger directory and opens a log file for the command. + * + * @param string $command + * The command name for the log filename. + * @param array $args + * Positional arguments to include in the log filename. + * + * @return \DrevOps\VortexInstaller\Logger\FileLoggerInterface + * The initialized logger instance. + */ + protected function initLogger(string $command, array $args = []): FileLoggerInterface { + $logger = $this->getLogger(); + $logger->setDir($this->getCwd()); + $logger->open($command, $args); + + return $logger; + } + + /** + * Resolve the output interface, defaulting to ConsoleOutput. + * + * @param \Symfony\Component\Console\Output\OutputInterface|null $output + * The output interface or NULL to use default. + * + * @return \Symfony\Component\Console\Output\OutputInterface + * The resolved output interface. + */ + protected function resolveOutput(?OutputInterface $output): OutputInterface { + return $output ?? Tui::output(); + } + + /** + * {@inheritdoc} + */ + public function setCwd(string $cwd): static { + $this->cwd = $cwd; + $this->getLogger()->setDir($cwd); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCwd(): string { + if ($this->cwd === '') { + $cwd = getcwd(); + if ($cwd === FALSE) { + throw new \RuntimeException('Unable to determine current working directory.'); + } + $this->cwd = $cwd; + $this->getLogger()->setDir($this->cwd); + } + + return $this->cwd; + } + + /** + * {@inheritdoc} + */ + public function enableLog(): static { + $this->getLogger()->enable(); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function disableLog(): static { + $this->getLogger()->disable(); + + return $this; + } + + /** + * Enable streaming output to console. + * + * @return static + * The runner instance for method chaining. + */ + public function enableStreaming(): static { + $this->shouldStream = TRUE; + + return $this; + } + + /** + * Disable streaming output to console. + * + * When disabled, output is still captured but not written to console. + * + * @return static + * The runner instance for method chaining. + */ + public function disableStreaming(): static { + $this->shouldStream = FALSE; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getCommand(): ?string { + return $this->command; + } + + /** + * Get the exit code from the last run. + * + * @return int<0, 255> + * The exit code. + */ + public function getExitCode(): int { + if ($this->exitCode < 0 || $this->exitCode > 255) { + throw new \RuntimeException(sprintf('Exit code %d is out of valid range (0-255).', $this->exitCode)); + } + + return $this->exitCode; + } + + /** + * {@inheritdoc} + */ + public function getOutput(bool $as_array = FALSE, ?int $lines = NULL): string | array { + $output_lines = explode(PHP_EOL, $this->output); + + if ($lines !== NULL) { + $output_lines = array_slice($output_lines, 0, $lines); + } + + if ($as_array) { + return $output_lines; + } + + return implode(PHP_EOL, $output_lines); + } + + /** + * Reset state before a new run. + */ + protected function reset(): void { + $this->command = NULL; + $this->output = ''; + $this->exitCode = self::EXIT_SUCCESS; + } + + /** + * Parse a command string into an array of parts. + * + * Handles quoted arguments and escaping properly. Supports both single + * and double quotes. Also supports the end-of-options marker (--) which + * stops option parsing and treats all subsequent tokens as positional + * arguments. + * + * Note: This parser intentionally allows backslash escaping inside single + * quotes (e.g., 'It\'s working'), which deviates from POSIX shell behavior + * where backslashes are literal inside single quotes. This provides more + * intuitive escaping for users. + * + * @param string $command + * The command string to parse. + * + * @return array + * Array with command as first element and arguments as subsequent elements. + */ + protected function parseCommand(string $command): array { + $command = trim($command); + if (empty($command)) { + throw new \InvalidArgumentException('Command cannot be empty.'); + } + + $parts = []; + $current = ''; + $in_quotes = FALSE; + $quote_char = ''; + $escaped = FALSE; + $length = strlen($command); + $has_content = FALSE; + $end_of_options_found = FALSE; + + for ($i = 0; $i < $length; $i++) { + $char = $command[$i]; + + if ($escaped) { + $current .= $char; + $escaped = FALSE; + $has_content = TRUE; + continue; + } + + if ($char === '\\') { + $escaped = TRUE; + continue; + } + + if (!$in_quotes && ($char === '"' || $char === "'")) { + $in_quotes = TRUE; + $quote_char = $char; + $has_content = TRUE; + continue; + } + + if ($in_quotes && $char === $quote_char) { + $in_quotes = FALSE; + $quote_char = ''; + continue; + } + + if (!$in_quotes && ($char === ' ' || $char === "\t")) { + if ($current !== '' || $has_content) { + // Check for end-of-options marker (--) only if not already found + // and not inside quotes. + if (!$end_of_options_found && $current === '--') { + $end_of_options_found = TRUE; + // Add the -- marker to the parts array so it reaches the command. + $parts[] = $current; + $current = ''; + $has_content = FALSE; + continue; + } + + $parts[] = $current; + $current = ''; + $has_content = FALSE; + } + continue; + } + + $current .= $char; + $has_content = TRUE; + } + + if ($in_quotes) { + throw new \InvalidArgumentException('Unclosed quote in command string.'); + } + + if ($escaped) { + throw new \InvalidArgumentException('Trailing escape character in command string.'); + } + + if ($current !== '' || $has_content) { + $parts[] = $current; + } + + return $parts; + } + + /** + * Build a command string for display or logging. + * + * Produces a shell-safe command string that can be copy-pasted. + * Arguments containing spaces or special characters are properly quoted. + * + * @param string $command + * The base command. + * @param array $args + * Command arguments. + * @param array $opts + * Command options. + * + * @return string + * The formatted command string. + */ + protected function buildCommandString(string $command, array $args = [], array $opts = []): string { + $parts = [$command]; + + $formatted_args = $this->formatArgs($args); + $formatted_opts = $this->formatArgs($opts); + + foreach ($formatted_args as $formatted_arg) { + $parts[] = $this->quoteArgument($formatted_arg); + } + + foreach ($formatted_opts as $formatted_opt) { + $parts[] = $this->quoteArgument($formatted_opt); + } + + return implode(' ', $parts); + } + + /** + * Quote an argument if it contains special characters. + * + * @param string $argument + * The argument to quote. + * + * @return string + * The quoted argument if needed, otherwise the original. + */ + protected function quoteArgument(string $argument): string { + // If argument is empty, return empty quoted string. + if ($argument === '') { + return "''"; + } + + // Check if argument needs quoting (contains spaces, quotes, or shell + // special chars). + if (preg_match('/[\s"\'\\\\$`!*?#~<>|;&(){}[\]]/', $argument)) { + // Use single quotes and escape any single quotes within. + $escaped = str_replace("'", "'\\''", $argument); + return "'" . $escaped . "'"; + } + + return $argument; + } + + /** + * Format arguments for display or logging. + * + * @param array $args + * The arguments to format. + * + * @return array + * Formatted arguments as strings. + */ + protected function formatArgs(array $args): array { + $formatted = []; + + foreach ($args as $key => $value) { + if (is_int($key)) { + // Positional argument. + if (is_bool($value)) { + if ($value) { + $formatted[] = '1'; + } + } + else { + $formatted[] = (string) $value; + } + } + elseif (is_bool($value)) { + // Named argument/option. + if ($value) { + $formatted[] = $key; + } + } + else { + $formatted[] = $key . '=' . $value; + } + } + + return $formatted; + } + +} diff --git a/.vortex/installer/src/Runner/CommandRunner.php b/.vortex/installer/src/Runner/CommandRunner.php new file mode 100644 index 000000000..6651f486d --- /dev/null +++ b/.vortex/installer/src/Runner/CommandRunner.php @@ -0,0 +1,146 @@ +reset(); + + // Merge args and inputs (options) for ArrayInput. + $input_args = array_merge($args, $inputs); + $this->command = $this->buildCommandString($command, $args, $inputs); + + // Validate command existence and prepare input (also validated). + $symfony_command = $this->application->find($command); + $input = new ArrayInput($input_args); + + $positional_args = array_values(array_filter($args, fn($key): bool => is_int($key), ARRAY_FILTER_USE_KEY)); + $logger = $this->initLogger($command, $positional_args); + + $output = $this->resolveOutput($output); + + // Create composite output that captures, streams, and logs. + [$composite_output, $buffered_output] = $this->createCompositeOutput($output, $logger); + + $exit_code = $symfony_command->run($input, $composite_output); + + if ($exit_code < 0 || $exit_code > 255) { + throw new \RuntimeException('Command exited with invalid exit code: ' . $exit_code); + } + + match ($exit_code) { + Command::SUCCESS => $this->exitCode = self::EXIT_SUCCESS, + Command::FAILURE => $this->exitCode = self::EXIT_FAILURE, + 127 => $this->exitCode = self::EXIT_COMMAND_NOT_FOUND, + default => $this->exitCode = self::EXIT_INVALID, + }; + + $this->exitCode = $exit_code; + $this->output = $buffered_output->fetch(); + + $logger->close(); + + return $this; + } + + /** + * Create a composite output that captures, streams, and logs. + * + * @param \Symfony\Component\Console\Output\OutputInterface $output + * The output interface to stream to. + * @param \DrevOps\VortexInstaller\Logger\LoggerInterface $logger + * The logger to write to. + * + * @return array{0: \Symfony\Component\Console\Output\OutputInterface, 1: \Symfony\Component\Console\Output\BufferedOutput} + * Array with [composite_output, buffered_output]. + */ + protected function createCompositeOutput(OutputInterface $output, LoggerInterface $logger): array { + $buffered_output = new BufferedOutput(); + + $composite_output = new class($buffered_output, $output, $logger, $this->shouldStream) extends BufferedOutput { + + public function __construct( + private readonly BufferedOutput $bufferedOutput, + private readonly OutputInterface $output, + private readonly LoggerInterface $logger, + private readonly bool $shouldStream, + ) { + parent::__construct(); + } + + /** + * Write a message to the output and log. + * + * @param string|iterable $messages + * The message or messages to write. + * @param bool $newline + * Whether to add a newline after the message. + * @param int $options + * Write options. + */ + public function write(string | iterable $messages, bool $newline = FALSE, int $options = 0): void { + $this->bufferedOutput->write($messages, $newline, $options); + + if ($this->shouldStream) { + $this->output->write($messages, $newline, $options); + } + + $text = is_iterable($messages) ? implode($newline ? PHP_EOL : '', (array) $messages) : $messages; + $this->logger->write($text . ($newline ? PHP_EOL : '')); + } + + /** + * Write a message with a newline to the output and log. + * + * @param string|iterable $messages + * The message or messages to write. + * @param int $options + * Write options. + */ + public function writeln(string | iterable $messages, int $options = 0): void { + $this->bufferedOutput->writeln($messages, $options); + if ($this->shouldStream) { + $this->output->writeln($messages, $options); + } + $text = is_iterable($messages) ? implode(PHP_EOL, (array) $messages) : $messages; + $this->logger->write($text . PHP_EOL); + } + + public function fetch(): string { + return $this->bufferedOutput->fetch(); + } + + }; + + return [$composite_output, $buffered_output]; + } + +} diff --git a/.vortex/installer/src/Runner/ProcessRunner.php b/.vortex/installer/src/Runner/ProcessRunner.php new file mode 100644 index 000000000..32f907cd9 --- /dev/null +++ b/.vortex/installer/src/Runner/ProcessRunner.php @@ -0,0 +1,164 @@ +reset(); + + // Parse and resolve the command. + [$base_command, $parsed_args] = $this->resolveCommand($command); + + $all_args = $this->prepareArguments($parsed_args, $args); + + $this->validateEnvironmentVars($env); + + // Build full command array. + $cmd = array_merge([$base_command], $all_args); + + // Store command string for logging with proper quoting. + $this->command = $this->buildCommandString($base_command, $all_args); + + $logger = $this->initLogger($base_command, $parsed_args); + $output = $this->resolveOutput($output); + + // Prepare inputs for interactive processes. + $input_string = empty($inputs) ? NULL : implode(PHP_EOL, $inputs) . PHP_EOL; + + $process = new Process($cmd, $this->getCwd(), $env ?: NULL, $input_string); + $process->setTimeout(NULL); + $process->setIdleTimeout(NULL); + + $process->run(function ($type, string|iterable $buffer) use ($logger, $output): void { + $buffer = is_iterable($buffer) ? implode("\n", (array) $buffer) : $buffer; + $this->output = $buffer; + if ($this->shouldStream) { + $output->write($buffer); + } + $logger->write($buffer); + }); + + $logger->close(); + + $exit_code = $process->getExitCode(); + + if ($exit_code < 0 || $exit_code > 255) { + throw new \RuntimeException('Command exited with invalid exit code: ' . $exit_code); + } + + match ($exit_code) { + Command::SUCCESS => $this->exitCode = self::EXIT_SUCCESS, + Command::FAILURE => $this->exitCode = self::EXIT_FAILURE, + 127 => $this->exitCode = self::EXIT_COMMAND_NOT_FOUND, + default => $this->exitCode = self::EXIT_INVALID, + }; + + $this->exitCode = $exit_code; + + return $this; + } + + /** + * Parse and resolve the command, validating it exists. + * + * @param string $command + * The command string to parse. + * + * @return array{0: string, 1: array} + * Array with [resolved_command_path, parsed_arguments]. + * + * @throws \InvalidArgumentException + * When command contains invalid characters or cannot be found. + */ + protected function resolveCommand(string $command): array { + $parsed = $this->parseCommand($command); + $base_command = array_shift($parsed); + + // Validate the base command contains only allowed characters. + if (preg_match('/[^a-zA-Z0-9_\-.\/]/', (string) $base_command)) { + throw new \InvalidArgumentException(sprintf('Invalid command: %s. Only alphanumeric characters, dots, dashes, underscores and slashes are allowed.', $base_command)); + } + + // If command is a path (contains /), check if it exists directly. + if (str_contains((string) $base_command, '/')) { + $resolved = $base_command; + // Check relative to cwd if not absolute. + if (!str_starts_with((string) $base_command, '/')) { + $full_path = $this->getCwd() . '/' . $base_command; + if (is_executable($full_path)) { + $resolved = $full_path; + } + } + } + else { + // Use ExecutableFinder for commands without path. + $finder = new ExecutableFinder(); + $resolved = $finder->find($base_command); + + if ($resolved === NULL) { + throw new \InvalidArgumentException(sprintf('Command not found: %s. Ensure the command is installed and available in PATH.', $base_command)); + } + } + + return [$resolved, $parsed]; + } + + /** + * Prepare arguments by merging and validating them. + * + * @param array $parsed_args + * Arguments parsed from the command string. + * @param array $additional_args + * Additional arguments passed to run(). + * + * @return array + * Merged and validated arguments as strings. + * + * @throws \InvalidArgumentException + * When an argument is not a scalar value. + */ + protected function prepareArguments(array $parsed_args, array $additional_args): array { + $all_args = array_merge($parsed_args, $this->formatArgs($additional_args)); + + foreach ($all_args as &$arg) { + $arg = (string) $arg; + } + unset($arg); + + return $all_args; + } + + /** + * Validate environment variables are scalar values. + * + * @param array $env + * Environment variables to validate. + * + * @throws \InvalidArgumentException + * When an environment variable is not a scalar value. + */ + protected function validateEnvironmentVars(array $env): void { + foreach ($env as $env_value) { + if (!is_scalar($env_value)) { + throw new \InvalidArgumentException('All environment variables must be scalar values.'); + } + } + } + +} diff --git a/.vortex/installer/src/Runner/RunnerInterface.php b/.vortex/installer/src/Runner/RunnerInterface.php new file mode 100644 index 000000000..f2f0503d0 --- /dev/null +++ b/.vortex/installer/src/Runner/RunnerInterface.php @@ -0,0 +1,88 @@ + $args + * Additional command arguments. + * @param array $inputs + * Interactive inputs for the command. + * @param array $env + * Environment variables. + * @param \Symfony\Component\Console\Output\OutputInterface|null $output + * Output interface. Defaults to STDOUT if NULL. + * + * @return self + * The runner instance for method chaining. + */ + public function run(string $command, array $args = [], array $inputs = [], array $env = [], ?OutputInterface $output = NULL): self; + + /** + * Get the last command that was run. + */ + public function getCommand(): ?string; + + /** + * Get the exit code from the last run. + * + * @return int<0, 255> + * The exit code. + */ + public function getExitCode(): int; + + /** + * Get the output from the last run. + * + * @param bool $as_array + * Whether to return output as array of lines. Defaults to FALSE. + * @param int|null $lines + * Number of lines to return. NULL returns all lines. + * + * @return string|array + * Output as string or array of lines. + */ + public function getOutput(bool $as_array = FALSE, ?int $lines = NULL): string | array; + + /** + * Set the working directory. + * + * @param string $cwd + * The working directory path. + * + * @return static + * The runner instance for method chaining. + */ + public function setCwd(string $cwd): static; + + /** + * Get the working directory. + * + * @return string + * The working directory path. + */ + public function getCwd(): string; + +} diff --git a/.vortex/installer/src/Utils/Task.php b/.vortex/installer/src/Task/Task.php similarity index 66% rename from .vortex/installer/src/Utils/Task.php rename to .vortex/installer/src/Task/Task.php index 13c7f6537..6d09c317a 100644 --- a/.vortex/installer/src/Utils/Task.php +++ b/.vortex/installer/src/Task/Task.php @@ -2,8 +2,10 @@ declare(strict_types=1); -namespace DrevOps\VortexInstaller\Utils; +namespace DrevOps\VortexInstaller\Task; +use DrevOps\VortexInstaller\Utils\Strings; +use DrevOps\VortexInstaller\Utils\Tui; use function Laravel\Prompts\spin; class Task { @@ -18,7 +20,8 @@ public static function action( \Closure|string|null $hint = NULL, \Closure|string|null $success = NULL, \Closure|string|null $failure = NULL, - ): void { + bool $streaming = FALSE, + ): mixed { $label = is_callable($label) ? $label() : $label; if (!is_callable($action)) { @@ -27,10 +30,30 @@ public static function action( $label = Tui::normalizeText($label); - // @phpstan-ignore-next-line - $return = spin($action, Tui::yellow($label)); + if ($streaming) { + $original_output = Tui::output(); - self::label($label, $hint && is_callable($hint) ? $hint() : $hint, is_array($return) ? $return : NULL, Strings::isAsciiStart($label) ? 2 : 3); + $task_output = new TaskOutput($original_output); + + static::start($label); + + Tui::setOutput($task_output); + + ob_start(function (string|iterable $buffer) use ($task_output): string { + $task_output->write($buffer); + return ''; + }, 1); + + $return = $action(); + + ob_end_clean(); + Tui::setOutput($original_output); + } + else { + // @phpstan-ignore-next-line + $return = spin($action, Tui::yellow($label)); + self::label($label, $hint && is_callable($hint) ? $hint() : $hint, is_array($return) ? $return : NULL, Strings::isAsciiStart($label) ? 2 : 3); + } if ($return === FALSE) { $failure = $failure && is_callable($failure) ? $failure() : $failure; @@ -40,6 +63,8 @@ public static function action( $success = $success && is_callable($success) ? $success($return) : $success; static::ok($success ? Tui::normalizeText($success) : 'OK'); } + + return $return; } protected static function label(string $message, ?string $hint = NULL, ?array $sublist = NULL, int $sublist_indent = 3): void { @@ -74,4 +99,9 @@ protected static function ok(string $text = 'OK'): void { Tui::note(str_repeat(Tui::caretUp(), 4)); } + protected static function start(string $label): void { + $message = '✦ ' . $label; + Tui::line(Tui::blue(Tui::normalizeText($message))); + } + } diff --git a/.vortex/installer/src/Task/TaskOutput.php b/.vortex/installer/src/Task/TaskOutput.php new file mode 100644 index 000000000..bde86539d --- /dev/null +++ b/.vortex/installer/src/Task/TaskOutput.php @@ -0,0 +1,123 @@ + $messages + * The message or messages to write. + * @param bool $newline + * Whether to add a newline after the message. + * @param int $options + * Write options. + */ + public function write(string | iterable $messages, bool $newline = FALSE, int $options = 0): void { + $dimmed = is_iterable($messages) + ? array_map(fn(string $m): string => Tui::dim($m), (array) $messages) + : Tui::dim($messages); + $this->wrapped->write($dimmed, $newline, $options); + } + + /** + * Writes a message to the output and adds a newline at the end. + * + * @param string|iterable $messages + * The message or messages to write. + * @param int $options + * Write options. + */ + public function writeln(string | iterable $messages, int $options = 0): void { + $dimmed = is_iterable($messages) + ? array_map(fn(string $m): string => Tui::dim($m), (array) $messages) + : Tui::dim($messages); + $this->wrapped->writeln($dimmed, $options); + } + + /** + * {@inheritdoc} + */ + public function setVerbosity(int $level): void { + $this->wrapped->setVerbosity($level); + } + + /** + * {@inheritdoc} + */ + public function getVerbosity(): int { + return $this->wrapped->getVerbosity(); + } + + /** + * {@inheritdoc} + */ + public function isQuiet(): bool { + return $this->wrapped->isQuiet(); + } + + /** + * {@inheritdoc} + */ + public function isVerbose(): bool { + return $this->wrapped->isVerbose(); + } + + /** + * {@inheritdoc} + */ + public function isVeryVerbose(): bool { + return $this->wrapped->isVeryVerbose(); + } + + /** + * {@inheritdoc} + */ + public function isDebug(): bool { + return $this->wrapped->isDebug(); + } + + /** + * {@inheritdoc} + */ + public function setDecorated(bool $decorated): void { + $this->wrapped->setDecorated($decorated); + } + + /** + * {@inheritdoc} + */ + public function isDecorated(): bool { + return $this->wrapped->isDecorated(); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(OutputFormatterInterface $formatter): void { + $this->wrapped->setFormatter($formatter); + } + + /** + * {@inheritdoc} + */ + public function getFormatter(): OutputFormatterInterface { + return $this->wrapped->getFormatter(); + } + +} diff --git a/.vortex/installer/src/Utils/Config.php b/.vortex/installer/src/Utils/Config.php index b81c8fa52..eaf68e48d 100644 --- a/.vortex/installer/src/Utils/Config.php +++ b/.vortex/installer/src/Utils/Config.php @@ -39,6 +39,8 @@ final class Config { const string NO_CLEANUP = 'VORTEX_INSTALLER_NO_CLEANUP'; + const string BUILD_NOW = 'VORTEX_INSTALLER_BUILD_NOW'; + /** * Store of configuration values. * diff --git a/.vortex/installer/src/Utils/Tui.php b/.vortex/installer/src/Utils/Tui.php index f9ff94dee..e978938a6 100644 --- a/.vortex/installer/src/Utils/Tui.php +++ b/.vortex/installer/src/Utils/Tui.php @@ -7,7 +7,9 @@ use Laravel\Prompts\Prompt; use Laravel\Prompts\Terminal; use Symfony\Component\Console\Output\OutputInterface; +use function Laravel\Prompts\confirm; use function Laravel\Prompts\error; +use function Laravel\Prompts\info; use function Laravel\Prompts\intro; use function Laravel\Prompts\note; use function Laravel\Prompts\table; @@ -42,6 +44,11 @@ public static function output(): OutputInterface { return static::$output; } + public static function setOutput(OutputInterface $output): void { + static::$output = $output; + Prompt::setOutput($output); + } + public static function info(string $message): void { intro($message); } @@ -50,10 +57,26 @@ public static function note(string $message): void { note($message); } + public static function success(string $message): void { + info($message); + } + public static function error(string $message): void { error('✕ ' . $message); } + public static function confirm(string $label, bool $default = TRUE, ?string $hint = NULL): bool { + if (!static::$isInteractive) { + return $default; + } + + return confirm( + label: $label, + default: $default, + hint: $hint ?? '', + ); + } + public static function line(string $message, int $padding = 1): void { static::$output->writeln(str_repeat(' ', max(0, $padding)) . $message); } @@ -87,6 +110,8 @@ public static function underscore(string $text): string { } public static function dim(string $text): string { + // Replace reset codes with reset+dim to maintain dim through color resets. + $text = str_replace("\033[0m", "\033[0m\033[2m", $text); return static::escapeMultiline($text, 2, 22); } diff --git a/.vortex/installer/tests/Functional/Command/BuildCommandTest.php b/.vortex/installer/tests/Functional/Command/BuildCommandTest.php new file mode 100644 index 000000000..f81faf4b7 --- /dev/null +++ b/.vortex/installer/tests/Functional/Command/BuildCommandTest.php @@ -0,0 +1,388 @@ +createMock(ProcessRunner::class); + + // Set up common default behaviors. + $current_command = ''; + $mock_runner->method('run') + ->willReturnCallback(function (string $command) use ($mock_runner, &$current_command): MockObject { + $current_command = $command; + return $mock_runner; + }); + + // Mock getOutput() to handle both string and array returns. + $mock_runner->method('getOutput')->willReturnCallback(fn(bool $as_array = FALSE): array|string => $as_array ? ['Mock build output line 1', 'Mock build output line 2'] : 'Mock build output'); + $mock_runner->method('getCommand')->willReturn('ahoy build'); + + // Set up getExitCode using the provided callback. + $mock_runner->method('getExitCode') + ->willReturnCallback(function () use ($exit_code_callback, &$current_command) { + return $exit_code_callback($current_command); + }); + + // Mock logger to prevent errors in showSuccessSummary/showFailureSummary. + $mock_logger = $this->createMock(FileLoggerInterface::class); + $mock_logger->method('getPath')->willReturn('/tmp/mock.log'); + $mock_runner->method('getLogger')->willReturn($mock_logger); + + // Mock setCwd to return runner for method chaining. + $mock_runner->method('setCwd')->willReturn($mock_runner); + + // Create command and inject mock runner using setRunner(). + $command = new BuildCommand(); + $command->setRunner($mock_runner); + + // Initialize application with our command. + static::applicationInitFromCommand($command); + + // Always register CheckRequirementsCommand with mocked runner. + // Use provided callback or default to success (exit code 0). + $requirements_runner = $this->createMock(ProcessRunner::class); + + $current_requirements_command = ''; + $requirements_runner->method('run') + ->willReturnCallback(function (string $command) use ($requirements_runner, &$current_requirements_command): MockObject { + $current_requirements_command = $command; + return $requirements_runner; + }); + + $requirements_runner->method('getOutput')->willReturn('version 1.0.0'); + + // Use provided callback or default to always returning 0 (success). + $final_requirements_callback = $requirements_exit_callback ?? fn(string $current_command): int => 0; + $requirements_runner->method('getExitCode') + ->willReturnCallback(function () use ($final_requirements_callback, &$current_requirements_command) { + return $final_requirements_callback($current_requirements_command); + }); + + $check_command = new CheckRequirementsCommand(); + $check_command->setRunner($requirements_runner); + $this->applicationGet()->add($check_command); + + // Run build with provided inputs. + $this->applicationRun($command_inputs, [], $expect_failure); + + // Assert output. + if (!empty($output_assertions)) { + $this->assertApplicationAnyOutputContainsOrNot($output_assertions); + } + } + + /** + * Data provider for testBuildWithMockedRunner. + * + * @return array, + * expect_failure: bool, + * output_assertions: array, + * requirements_exit_callback?: ?\Closure + * }> + */ + public static function dataProviderBuildCommand(): array { + return [ + // ----------------------------------------------------------------------- + // Requirements check scenarios. + // ----------------------------------------------------------------------- + 'Build runs requirements check by default (success)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => [], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + TuiOutput::absent([ + TuiOutput::BUILD_EXPORT_DATABASE, + ]), + ), + 'requirements_exit_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + ], + + 'Requirements check fails - one missing (Docker)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => [], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + TuiOutput::CHECK_REQUIREMENTS_MISSING, + ]), + TuiOutput::absent([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + ), + 'requirements_exit_callback' => function (string $current_command): int { + // Docker command fails, others succeed. + if ((str_contains($current_command, "command -v 'docker'") || str_contains($current_command, 'command -v docker')) + && !str_contains($current_command, 'compose') + && !str_contains($current_command, 'docker --version')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + ], + + 'Requirements check fails - all missing' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => [], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + TuiOutput::CHECK_REQUIREMENTS_MISSING, + ]), + TuiOutput::absent([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + ), + 'requirements_exit_callback' => fn(string $current_command): int => RunnerInterface::EXIT_COMMAND_NOT_FOUND, + ], + + // ----------------------------------------------------------------------- + // Basic build scenarios. + // ----------------------------------------------------------------------- + 'Build with skip requirements check (success)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + TuiOutput::absent([ + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + TuiOutput::BUILD_EXPORT_DATABASE, + ]), + ), + + ], + + // ----------------------------------------------------------------------- + // Profile flag scenarios. + // ----------------------------------------------------------------------- + 'Build with profile flag and skip requirements (success)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => [ + '--profile' => TRUE, + '--skip-requirements-check' => TRUE, + ], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + TuiOutput::BUILD_EXPORT_DATABASE, + ]), + TuiOutput::absent([ + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + ]), + ), + + ], + + 'Build with profile flag and requirements check (success)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--profile' => TRUE], + 'expect_failure' => FALSE, + 'output_assertions' => TuiOutput::present([ + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + TuiOutput::BUILD_EXPORT_DATABASE, + ]), + 'requirements_exit_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + ], + + 'Build with profile shows export database step' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => [ + '--profile' => TRUE, + '--skip-requirements-check' => TRUE, + ], + 'expect_failure' => FALSE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + '* ' . TuiOutput::BUILD_EXPORT_DATABASE . ' ahoy export-db', + ], + + ], + + // ----------------------------------------------------------------------- + // Build failure scenarios. + // ----------------------------------------------------------------------- + 'Build failure (ahoy build fails, exit code 1)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_FAILURE, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_FAILED, + ]), + ['* ' . TuiOutput::INSTALL_EXIT_CODE . ' 1'], + TuiOutput::absent([ + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + ), + + ], + + 'Build failure with profile (ahoy build fails, exit code 1)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_FAILURE, + 'command_inputs' => [ + '--profile' => TRUE, + '--skip-requirements-check' => TRUE, + ], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_FAILED, + ]), + ['* ' . TuiOutput::INSTALL_EXIT_CODE . ' 1'], + TuiOutput::absent([ + TuiOutput::BUILD_BUILD_COMPLETED, + TuiOutput::BUILD_EXPORT_DATABASE, + ]), + ), + + ], + + 'Build failure (ahoy build fails, exit code 2)' => [ + 'exit_code_callback' => fn(string $current_command): int => 2, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_FAILED, + ]), + ['* ' . TuiOutput::INSTALL_EXIT_CODE . ' 2'], + TuiOutput::absent([ + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + ), + + ], + + 'Build failure (ahoy build fails, exit code 127)' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_COMMAND_NOT_FOUND, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_FAILED, + ]), + ['* ' . TuiOutput::INSTALL_EXIT_CODE . ' 127'], + TuiOutput::absent([ + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + ), + + ], + + 'Build failure shows log file path' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_FAILURE, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_FAILED, + ]), + ['* ' . TuiOutput::INSTALL_LOG_FILE . ' /tmp/mock.log'], + ), + + ], + + // ----------------------------------------------------------------------- + // Success output verification scenarios. + // ----------------------------------------------------------------------- + 'Build success shows log file path' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + ]), + ['* ' . TuiOutput::INSTALL_LOG_FILE . ' /tmp/mock.log'], + ), + + ], + + 'Build success shows site URL' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + TuiOutput::INSTALL_LOGIN, + ]), + ['* ' . TuiOutput::BUILD_SITE_URL . ' http://'], + ), + + ], + + 'Build success shows next steps' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--skip-requirements-check' => TRUE], + 'expect_failure' => FALSE, + 'output_assertions' => TuiOutput::present([ + TuiOutput::BUILD_BUILDING_SITE, + TuiOutput::BUILD_BUILD_COMPLETED, + TuiOutput::INSTALL_NEXT_STEPS, + TuiOutput::INSTALL_REVIEW_DOCS, + ]), + + ], + ]; + } + +} diff --git a/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php b/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php new file mode 100644 index 000000000..57d2f5ae8 --- /dev/null +++ b/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php @@ -0,0 +1,395 @@ +createMock(ProcessRunner::class); + + // Set up common default behaviors. + $current_command = ''; + $mock_runner->method('run') + ->willReturnCallback(function (string $command) use ($mock_runner, &$current_command): MockObject { + $current_command = $command; + return $mock_runner; + }); + + $mock_runner->method('getOutput')->willReturn('version 1.0.0'); + + // Set up getExitCode using the provided callback. + $mock_runner->method('getExitCode') + ->willReturnCallback(function () use ($exit_code_callback, &$current_command) { + return $exit_code_callback($current_command); + }); + + // Create command and inject mock runner using setRunner(). + $command = new CheckRequirementsCommand(); + $command->setRunner($mock_runner); + + // Initialize application with our command. + static::applicationInitFromCommand($command); + + // Run check with provided inputs. + $this->applicationRun($command_inputs, [], $expect_failure); + + if (!empty($output_assertions)) { + $this->assertApplicationAnyOutputContainsOrNot($output_assertions); + } + } + + /** + * Data provider for testCheckWithMockedRunner. + * + * @return array, + * expect_failure: bool, + * output_assertions: array + * }> + */ + public static function dataProviderCheckRequirementsCommand(): array { + return [ + 'Check all requirements' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => [], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + [ + '* Docker: version 1.0.0', + '* Docker Compose: version 1.0.0', + '* Ahoy: version 1.0.0', + '* Pygmy: version 1.0.0', + ], + ), + ], + + 'All requirements missing' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_COMMAND_NOT_FOUND, + 'command_inputs' => [], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + [ + '* Docker:', + '* Docker Compose:', + '* Ahoy:', + '* Pygmy:', + ], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + ]), + ), + ], + + 'Check only Docker' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--only' => 'docker'], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ['* Docker: version 1.0.0'], + ['! Ahoy:', '! Pygmy:'], + ), + ], + + 'Check only Docker and Ahoy' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--only' => 'docker,ahoy'], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + [ + '* Docker: version 1.0.0', + '* Ahoy: version 1.0.0', + ], + ['! Pygmy:'], + ), + ], + + 'Check with no-summary option' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--no-summary' => TRUE], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + ]), + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ), + ], + + 'Docker missing' => [ + 'exit_code_callback' => function (string $current_command): int { + // Docker command fails. + if ((str_contains($current_command, "command -v 'docker'") || str_contains($current_command, 'command -v docker')) + && !str_contains($current_command, 'compose') + && !str_contains($current_command, 'docker --version')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'docker'], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ['* Docker:'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_DOCKER_AVAILABLE, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ), + ], + + 'Ahoy missing' => [ + 'exit_code_callback' => function (string $current_command): int { + // Ahoy command fails. + if (str_contains($current_command, "command -v 'ahoy'") || str_contains($current_command, 'command -v ahoy')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'ahoy'], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ['* Ahoy:'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_AHOY_AVAILABLE, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ), + ], + + 'Pygmy command not found' => [ + 'exit_code_callback' => function (string $current_command): int { + // Pygmy command does not exist (return 127). + if (str_contains($current_command, "command -v 'pygmy'") || str_contains($current_command, 'command -v pygmy')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'pygmy'], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ['* Pygmy:'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_PYGMY_RUNNING, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ), + ], + + 'Pygmy status command succeeds' => [ + 'exit_code_callback' => fn(string $current_command): int => + // Pygmy command exists and status succeeds. + RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--only' => 'pygmy'], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ['* Pygmy: version 1.0.0'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ), + ], + + 'Pygmy status fails but amazeeio containers found' => [ + 'exit_code_callback' => function (string $current_command): int { + // Pygmy command exists. + if (str_contains($current_command, "command -v 'pygmy'") || str_contains($current_command, 'command -v pygmy')) { + return RunnerInterface::EXIT_SUCCESS; + } + // Pygmy status fails. + if (str_contains($current_command, 'pygmy status')) { + return RunnerInterface::EXIT_FAILURE; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'pygmy'], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ['* Pygmy: version 1.0.0'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ), + ], + + 'Pygmy status fails and no amazeeio containers' => [ + 'exit_code_callback' => function (string $current_command): int { + // Pygmy command exists. + if (str_contains($current_command, "command -v 'pygmy'") || str_contains($current_command, 'command -v pygmy')) { + return RunnerInterface::EXIT_SUCCESS; + } + // Pygmy status fails. + if (str_contains($current_command, 'pygmy status')) { + return RunnerInterface::EXIT_FAILURE; + } + // No amazeeio containers. + if (str_contains($current_command, 'docker ps') && str_contains($current_command, 'amazeeio')) { + return RunnerInterface::EXIT_FAILURE; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'pygmy'], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ['* Pygmy:'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_PYGMY_RUNNING, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ), + ], + + 'Docker Compose via modern syntax' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--only' => 'docker-compose'], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ['* Docker Compose: version 1.0.0'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ), + ], + + 'Docker Compose via legacy command' => [ + 'exit_code_callback' => function (string $current_command): int { + // Modern syntax fails. + if (str_contains($current_command, 'docker compose version')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'docker-compose'], + 'expect_failure' => FALSE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ['* Docker Compose: version 1.0.0'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ), + ], + + 'Docker Compose missing completely' => [ + 'exit_code_callback' => function (string $current_command): int { + // Both docker compose and docker-compose commands fail. + if (str_contains($current_command, 'docker compose version') || + str_contains($current_command, "command -v 'docker-compose'") || + str_contains($current_command, 'command -v docker-compose')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'command_inputs' => ['--only' => 'docker-compose'], + 'expect_failure' => TRUE, + 'output_assertions' => array_merge( + TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_MISSING_LABEL, + ]), + ['* Docker Compose:'], + TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_DOCKER_COMPOSE_AVAILABLE, + TuiOutput::CHECK_REQUIREMENTS_PRESENT_LABEL, + ]), + ), + ], + + 'Invalid requirement name' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--only' => 'invalid'], + 'expect_failure' => TRUE, + 'output_assertions' => [ + '* ' . TuiOutput::CHECK_REQUIREMENTS_UNKNOWN . ' invalid', + '* Available: docker, docker-compose, ahoy', + ], + ], + + 'Mixed valid and invalid requirements' => [ + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'command_inputs' => ['--only' => 'docker,invalid'], + 'expect_failure' => TRUE, + 'output_assertions' => [ + '* ' . TuiOutput::CHECK_REQUIREMENTS_UNKNOWN . ' invalid', + '* Available: docker, docker-compose, ahoy', + ], + ], + ]; + } + +} diff --git a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php new file mode 100644 index 000000000..42a9a1e38 --- /dev/null +++ b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php @@ -0,0 +1,502 @@ +createMock(ProcessRunner::class); + $install_process_runner_command = ''; + $install_process_runner->method('run') + ->willReturnCallback(function (string $command) use ($install_process_runner, &$install_process_runner_command): MockObject { + $install_process_runner_command = $command; + return $install_process_runner; + }); + $install_process_runner->method('getExitCode') + ->willReturnCallback(function () use ($install_process_runner_exit_callback, &$install_process_runner_command) { + return $install_process_runner_exit_callback($install_process_runner_command); + }); + + // 2. Mock ProcessRunner for BuildCommand (runs 'ahoy build'). + $build_runner = $this->createMock(ProcessRunner::class); + $build_runner_command = ''; + $build_runner->method('run') + ->willReturnCallback(function (string $command) use ($build_runner, &$build_runner_command): MockObject { + $build_runner_command = $command; + return $build_runner; + }); + $build_runner->method('getExitCode') + ->willReturnCallback(function () use ($build_runner_exit_callback, &$build_runner_command) { + return $build_runner_exit_callback($build_runner_command); + }); + // Mock other BuildCommand runner methods. + $build_runner->method('getOutput')->willReturnCallback(fn(bool $as_array = FALSE): array | string => $as_array ? ['Mock build output line 1', 'Mock build output line 2'] : 'Mock build output'); + $build_runner->method('getCommand')->willReturn('ahoy build'); + $mock_logger = $this->createMock(FileLoggerInterface::class); + $mock_logger->method('getPath')->willReturn('/tmp/mock.log'); + $build_runner->method('getLogger')->willReturn($mock_logger); + $build_runner->method('setCwd')->willReturn($build_runner); + + // 3. Mock ProcessRunner for CheckRequirementsCommand. + $check_requirements_runner = $this->createMock(ProcessRunner::class); + $check_requirements_runner_command = ''; + $check_requirements_runner->method('run') + ->willReturnCallback(function (string $command) use ($check_requirements_runner, &$check_requirements_runner_command): MockObject { + $check_requirements_runner_command = $command; + return $check_requirements_runner; + }); + $check_requirements_runner->method('getOutput')->willReturn('version 1.0.0'); + $check_requirements_runner->method('getExitCode') + ->willReturnCallback(function () use ($check_requirements_runner_exit_callback, &$check_requirements_runner_command) { + return $check_requirements_runner_exit_callback($check_requirements_runner_command); + }); + + // Create and configure InstallCommand. + $command = new InstallCommand(); + $command->setProcessRunner($install_process_runner); + + if ($download_should_fail) { + $mock_downloader = $this->createMock(Downloader::class); + $mock_downloader->method('download') + ->willThrowException(new \RuntimeException('Failed to download Vortex.')); + $command->setDownloader($mock_downloader); + } + else { + // Download from root as a real repository. This is long, but there is + // no other way to test the rest of the installation process without + // having all files in place. + $command_inputs['--' . InstallCommand::OPTION_URI] = File::dir(static::$root); + } + + // Initialize application and register mocked commands. + static::applicationInitFromCommand($command); + + $check_command = new CheckRequirementsCommand(); + $check_command->setRunner($check_requirements_runner); + $this->applicationGet()->add($check_command); + + $build_command = new BuildCommand(); + $build_command->setRunner($build_runner); + $this->applicationGet()->add($build_command); + + $command_inputs[InstallCommand::ARG_DESTINATION] = self::$sut; + + $this->applicationRun($command_inputs, [], $expect_failure); + + if (!empty($output_assertions)) { + $this->assertApplicationAnyOutputContainsOrNot($output_assertions); + } + } + + /** + * Data provider for testInstallCommand. + * + * @return array, + * install_process_runner_exit_callback: \Closure, + * build_runner_exit_callback: \Closure, + * check_requirements_runner_exit_callback: \Closure, + * expect_failure: bool, + * output_assertions: array, + * download_should_fail?: bool + * }> + */ + public static function dataProviderInstallCommand(): array { + return [ + 'Install without build flag, skips build' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => FALSE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + TuiOutput::INSTALL_PREPARING_DESTINATION, + TuiOutput::INSTALL_COPYING_FILES, + TuiOutput::INSTALL_PREPARING_DEMO, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_BUILDING, + ]), + ], + ], + + 'Install with config JSON string succeeds' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + InstallCommand::OPTION_CONFIG => '{"VORTEX_PROJECT_NAME":"test_project"}', + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => FALSE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + ]), + ], + ], + + 'Install with no-cleanup flag succeeds' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + InstallCommand::OPTION_NO_CLEANUP => TRUE, + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => FALSE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + ]), + ], + ], + + // ----------------------------------------------------------------------- + // Install command fails requirements check. + // ----------------------------------------------------------------------- + 'Requirements of install command check fails, missing git' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => function (string $command): int { + // Git command fails. + if (str_contains($command, 'command -v git')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_ERROR_MISSING_GIT, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_STARTING, + ]), + ], + ], + + 'Requirements of install command check fails, missing curl' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => function (string $command): int { + // Curl command fails. + if (str_contains($command, 'command -v curl')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_ERROR_MISSING_CURL, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_STARTING, + ]), + ], + ], + + 'Requirements of install command check fails, missing tar' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => function (string $command): int { + // Tar command fails. + if (str_contains($command, 'command -v tar')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_ERROR_MISSING_TAR, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_STARTING, + ]), + ], + ], + + 'Requirements of install command check fails, missing composer' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => function (string $command): int { + // Composer command fails. + if (str_contains($command, 'command -v composer')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_ERROR_MISSING_COMPOSER, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_STARTING, + ]), + ], + ], + + 'Requirements of install command check fails, multiple missing tools' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => function (string $command): int { + // Both git and curl fail. + if (str_contains($command, 'command -v git')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + if (str_contains($command, 'command -v curl')) { + return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + } + return RunnerInterface::EXIT_SUCCESS; + }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_ERROR_MISSING_GIT, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_STARTING, + ]), + ], + ], + + // ----------------------------------------------------------------------- + // Download failures. + // ----------------------------------------------------------------------- + 'Download fails' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_ERROR_DOWNLOAD_FAILED, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_CUSTOMIZING, + TuiOutput::INSTALL_PREPARING_DESTINATION, + ]), + ], + 'download_should_fail' => TRUE, + ], + + // ----------------------------------------------------------------------- + // Sub-commands: build with check-requirements. + // ----------------------------------------------------------------------- + 'Install with build flag succeeds' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + InstallCommand::OPTION_BUILD => TRUE, + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => TuiOutput::buildRunnerSuccess(), + 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsSuccess(), + 'expect_failure' => FALSE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + TuiOutput::INSTALL_PREPARING_DESTINATION, + TuiOutput::INSTALL_COPYING_FILES, + TuiOutput::INSTALL_PREPARING_DEMO, + TuiOutput::INSTALL_BUILDING, + TuiOutput::INSTALL_BUILD_SUCCESS, + TuiOutput::INSTALL_LOGIN, + TuiOutput::INSTALL_LOG_FILE, + TuiOutput::INSTALL_NEXT_STEPS, + TuiOutput::INSTALL_REVIEW_DOCS, + ]), + ], + ], + + 'Install with build flag and profile starter succeeds' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + InstallCommand::OPTION_BUILD => TRUE, + InstallCommand::OPTION_CONFIG => '{"VORTEX_STARTER":"install_profile_core"}', + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => TuiOutput::buildRunnerSuccessProfile(), + 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsSuccess(), + 'expect_failure' => FALSE, + 'output_assertions' => [ + // Install command output - should be present. + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + TuiOutput::INSTALL_PREPARING_DESTINATION, + TuiOutput::INSTALL_COPYING_FILES, + TuiOutput::INSTALL_PREPARING_DEMO, + TuiOutput::INSTALL_BUILDING, + ]), + // Check requirements output - should be present. + ...TuiOutput::present([ + TuiOutput::CHECK_REQUIREMENTS_CHECKING_DOCKER, + TuiOutput::CHECK_REQUIREMENTS_CHECKING_DOCKER_COMPOSE, + TuiOutput::CHECK_REQUIREMENTS_CHECKING_AHOY, + TuiOutput::CHECK_REQUIREMENTS_CHECKING_PYGMY, + TuiOutput::CHECK_REQUIREMENTS_DOCKER_AVAILABLE, + TuiOutput::CHECK_REQUIREMENTS_DOCKER_COMPOSE_AVAILABLE, + TuiOutput::CHECK_REQUIREMENTS_AHOY_AVAILABLE, + TuiOutput::CHECK_REQUIREMENTS_PYGMY_RUNNING, + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + ]), + // Build output (profile) - should be present. + ...TuiOutput::present([ + TuiOutput::BUILD_ASSEMBLE_DOCKER, + TuiOutput::BUILD_ASSEMBLE_COMPOSER, + TuiOutput::BUILD_ASSEMBLE_YARN, + TuiOutput::BUILD_PROVISION_START, + TuiOutput::BUILD_PROVISION_PROJECT_INFO, + TuiOutput::BUILD_PROVISION_TYPE_PROFILE, + TuiOutput::BUILD_PROVISION_END, + ]), + // Final install output - should be present. + ...TuiOutput::present([ + TuiOutput::INSTALL_BUILD_SUCCESS, + TuiOutput::INSTALL_LOGIN, + TuiOutput::INSTALL_LOG_FILE, + TuiOutput::INSTALL_NEXT_STEPS, + TuiOutput::INSTALL_REVIEW_DOCS, + ]), + // Negative assertions - should be absent. + ...TuiOutput::absent([ + TuiOutput::BUILD_PROVISION_TYPE_DB, + TuiOutput::INSTALL_BUILD_FAILED, + TuiOutput::INSTALL_EXIT_CODE, + TuiOutput::CHECK_REQUIREMENTS_MISSING, + TuiOutput::CHECK_REQUIREMENTS_DOCKER_MISSING, + TuiOutput::CHECK_REQUIREMENTS_DOCKER_COMPOSE_MISSING, + TuiOutput::CHECK_REQUIREMENTS_AHOY_MISSING, + TuiOutput::CHECK_REQUIREMENTS_PYGMY_NOT_RUNNING, + ]), + ], + ], + + 'Install with build flag fails' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + InstallCommand::OPTION_BUILD => TRUE, + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => TuiOutput::buildRunnerFailure(), + 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsSuccess(), + 'expect_failure' => FALSE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + TuiOutput::INSTALL_PREPARING_DESTINATION, + TuiOutput::INSTALL_COPYING_FILES, + TuiOutput::INSTALL_PREPARING_DEMO, + TuiOutput::INSTALL_BUILDING, + TuiOutput::INSTALL_BUILD_FAILED, + TuiOutput::INSTALL_EXIT_CODE, + TuiOutput::INSTALL_LOG_FILE, + ]), + ...TuiOutput::absent([ + TuiOutput::INSTALL_BUILD_SUCCESS, + ]), + ], + ], + + 'Install with build flag and requirements of check-requirements command check fails' => [ + 'command_inputs' => self::tuiOptions([ + InstallCommand::OPTION_NO_INTERACTION => TRUE, + InstallCommand::OPTION_BUILD => TRUE, + ]), + 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'build_runner_exit_callback' => TuiOutput::buildRunnerSuccess(), + 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsFailure(), + 'expect_failure' => TRUE, + 'output_assertions' => [ + ...TuiOutput::present([ + TuiOutput::INSTALL_STARTING, + TuiOutput::INSTALL_DOWNLOADING, + TuiOutput::INSTALL_CUSTOMIZING, + TuiOutput::INSTALL_PREPARING_DESTINATION, + TuiOutput::INSTALL_COPYING_FILES, + TuiOutput::INSTALL_PREPARING_DEMO, + TuiOutput::INSTALL_BUILDING, + TuiOutput::BUILD_CHECKING_REQUIREMENTS, + TuiOutput::CHECK_REQUIREMENTS_MISSING, + ]), + ...TuiOutput::absent([ + TuiOutput::CHECK_REQUIREMENTS_ALL_MET, + TuiOutput::INSTALL_BUILD_SUCCESS, + ]), + ], + ], + ]; + } + +} diff --git a/.vortex/installer/tests/Helpers/TuiOutput.php b/.vortex/installer/tests/Helpers/TuiOutput.php new file mode 100644 index 000000000..4fd9d4693 --- /dev/null +++ b/.vortex/installer/tests/Helpers/TuiOutput.php @@ -0,0 +1,263 @@ + $constants + * Array of constant values. + * + * @return array + * Array with '* ' prefix added to each constant. + */ + public static function present(array $constants): array { + return array_map(fn($c) => '* ' . $c, $constants); + } + + /** + * Mark constants as absent (should NOT contain in output). + * + * @param array $constants + * Array of constant values. + * + * @return array + * Array with '! ' prefix added to each constant. + */ + public static function absent(array $constants): array { + return array_map(fn($c) => '! ' . $c, $constants); + } + + /** + * Echo constants as output lines. + * + * @param array $constants + * Array of constant values to echo. + */ + public static function echo(array $constants): void { + foreach ($constants as $constant) { + echo $constant . PHP_EOL; + } + } + + /** + * Create a successful build runner callback. + * + * Simulates a successful build with database provisioning. + * + * @return \Closure + * Closure that echoes build output and returns success exit code. + */ + public static function buildRunnerSuccess(): \Closure { + return function (string $command): int { + self::echo([ + self::BUILD_ASSEMBLE_DOCKER, + self::BUILD_ASSEMBLE_COMPOSER, + self::BUILD_ASSEMBLE_YARN, + self::BUILD_PROVISION_START, + self::BUILD_PROVISION_PROJECT_INFO, + self::BUILD_PROVISION_TYPE_DB, + self::BUILD_PROVISION_END, + ]); + return RunnerInterface::EXIT_SUCCESS; + }; + } + + /** + * Create a successful build runner callback with profile provisioning. + * + * Simulates a successful build using install profile instead of database. + * + * @return \Closure + * Closure that echoes build output and returns success exit code. + */ + public static function buildRunnerSuccessProfile(): \Closure { + return function (string $command): int { + self::echo([ + self::BUILD_ASSEMBLE_DOCKER, + self::BUILD_ASSEMBLE_COMPOSER, + self::BUILD_ASSEMBLE_YARN, + self::BUILD_PROVISION_START, + self::BUILD_PROVISION_PROJECT_INFO, + self::BUILD_PROVISION_TYPE_PROFILE, + self::BUILD_PROVISION_END, + ]); + return RunnerInterface::EXIT_SUCCESS; + }; + } + + /** + * Create a failed build runner callback. + * + * Simulates a build that starts but fails during provisioning. + * + * @return \Closure + * Closure that echoes partial build output and returns failure exit code. + */ + public static function buildRunnerFailure(): \Closure { + return function (string $command): int { + self::echo([ + self::BUILD_ASSEMBLE_DOCKER, + self::BUILD_ASSEMBLE_COMPOSER, + self::BUILD_ASSEMBLE_YARN, + self::BUILD_PROVISION_START, + ]); + return RunnerInterface::EXIT_FAILURE; + }; + } + + /** + * Create a successful check requirements callback. + * + * Simulates all requirements checks passing. + * + * @return \Closure + * Closure that echoes requirements check output and returns success. + */ + public static function checkRequirementsSuccess(): \Closure { + return function (string $command): int { + self::echo([ + self::CHECK_REQUIREMENTS_CHECKING_DOCKER, + self::CHECK_REQUIREMENTS_DOCKER_AVAILABLE, + self::CHECK_REQUIREMENTS_CHECKING_DOCKER_COMPOSE, + self::CHECK_REQUIREMENTS_DOCKER_COMPOSE_AVAILABLE, + self::CHECK_REQUIREMENTS_CHECKING_AHOY, + self::CHECK_REQUIREMENTS_AHOY_AVAILABLE, + self::CHECK_REQUIREMENTS_CHECKING_PYGMY, + self::CHECK_REQUIREMENTS_PYGMY_RUNNING, + self::CHECK_REQUIREMENTS_ALL_MET, + ]); + return RunnerInterface::EXIT_SUCCESS; + }; + } + + /** + * Create a failed check requirements callback. + * + * Simulates requirements checks with missing tools. + * + * @return \Closure + * Closure that echoes requirements check output and returns failure. + */ + public static function checkRequirementsFailure(): \Closure { + return function (string $command): int { + self::echo([ + self::CHECK_REQUIREMENTS_CHECKING_DOCKER, + self::CHECK_REQUIREMENTS_DOCKER_AVAILABLE, + self::CHECK_REQUIREMENTS_CHECKING_DOCKER_COMPOSE, + self::CHECK_REQUIREMENTS_DOCKER_COMPOSE_MISSING, + self::CHECK_REQUIREMENTS_MISSING, + ]); + return RunnerInterface::EXIT_FAILURE; + }; + } + +} diff --git a/.vortex/installer/tests/Traits/TuiTrait.php b/.vortex/installer/tests/Traits/TuiTrait.php index 7c8bfb597..82188e4c8 100644 --- a/.vortex/installer/tests/Traits/TuiTrait.php +++ b/.vortex/installer/tests/Traits/TuiTrait.php @@ -34,4 +34,21 @@ protected static function tuiTeardown(): void { Prompt::validateUsing(NULL); } + /** + * Helper to create command options array with '--' prefix. + * + * @param array $options + * Array of option constants as keys and their values. + * + * @return array + * Array with '--' prefix added to each option key. + */ + protected static function tuiOptions(array $options): array { + $result = []; + foreach ($options as $option => $value) { + $result['--' . $option] = $value; + } + return $result; + } + } diff --git a/.vortex/installer/tests/Unit/ConfigTest.php b/.vortex/installer/tests/Unit/ConfigTest.php index 62ac1ef81..50dca04ea 100644 --- a/.vortex/installer/tests/Unit/ConfigTest.php +++ b/.vortex/installer/tests/Unit/ConfigTest.php @@ -218,7 +218,7 @@ public static function dataProviderIsQuiet(): array { 'boolean_true' => [TRUE, TRUE], 'boolean_false' => [FALSE, FALSE], 'string_true' => ['true', TRUE], - // Non-empty string is truthy. + // Non-empty string is truthy. 'string_false' => ['false', TRUE], 'string_empty' => ['', FALSE], 'integer_zero' => [0, FALSE], @@ -256,7 +256,7 @@ public static function dataProviderGetNoInteraction(): array { 'boolean_true' => [TRUE, TRUE], 'boolean_false' => [FALSE, FALSE], 'string_true' => ['true', TRUE], - // Non-empty string is truthy. + // Non-empty string is truthy. 'string_false' => ['false', TRUE], 'string_empty' => ['', FALSE], 'integer_zero' => [0, FALSE], @@ -294,7 +294,7 @@ public static function dataProviderIsVortexProject(): array { 'boolean_true' => [TRUE, TRUE], 'boolean_false' => [FALSE, FALSE], 'string_true' => ['true', TRUE], - // Non-empty string is truthy. + // Non-empty string is truthy. 'string_false' => ['false', TRUE], 'string_empty' => ['', FALSE], 'integer_zero' => [0, FALSE], @@ -318,6 +318,7 @@ public function testConstants(): void { $this->assertEquals('VORTEX_INSTALLER_NO_INTERACTION', Config::NO_INTERACTION); $this->assertEquals('VORTEX_INSTALLER_QUIET', Config::QUIET); $this->assertEquals('VORTEX_INSTALLER_NO_CLEANUP', Config::NO_CLEANUP); + $this->assertEquals('VORTEX_INSTALLER_BUILD_NOW', Config::BUILD_NOW); } public function testEnvironmentVariablePrecedenceInConstructor(): void { diff --git a/.vortex/installer/tests/Unit/Logger/FileLoggerTest.php b/.vortex/installer/tests/Unit/Logger/FileLoggerTest.php new file mode 100644 index 000000000..70580dd95 --- /dev/null +++ b/.vortex/installer/tests/Unit/Logger/FileLoggerTest.php @@ -0,0 +1,390 @@ +disable(); + } + + $this->assertEquals($initial_state, $logger->isEnabled()); + + $result = $logger->enable(); + $this->assertEquals($after_enable, $logger->isEnabled()); + $this->assertInstanceOf(FileLogger::class, $result, 'enable() should return self for method chaining'); + + $result = $logger->disable(); + $this->assertEquals($after_disable, $logger->isEnabled()); + $this->assertInstanceOf(FileLogger::class, $result, 'disable() should return self for method chaining'); + } + + /** + * Test setDir and getDir methods. + */ + #[DataProvider('dataProviderDirectoryManagement')] + public function testDirectoryManagement(string $dir, bool $test_default): void { + $logger = new FileLogger(); + + // Test default directory uses getcwd(). + if ($test_default) { + $this->assertEquals(getcwd(), $logger->getDir()); + } + else { + // Test setDir sets custom directory. + $result = $logger->setDir($dir); + $this->assertEquals($dir, $logger->getDir()); + $this->assertInstanceOf(FileLogger::class, $result, 'setDir() should return self for method chaining'); + + // Test getDir returns the set directory. + $this->assertEquals($dir, $logger->getDir()); + } + } + + /** + * Test open method with enabled logging. + */ + #[DataProvider('dataProviderOpen')] + public function testOpen(string $command, array $args, bool $enabled, ?string $expected_pattern, ?string $expected_exception, ?string $expected_message): void { + if ($expected_exception !== NULL) { + /** @var class-string<\Throwable> $expected_exception */ + $this->expectException($expected_exception); + $this->expectExceptionMessage($expected_message ?? ''); + } + + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + + if (!$enabled) { + $logger->disable(); + } + + $result = $logger->open($command, $args); + + if (!$enabled) { + $this->assertFalse($result, 'open() should return FALSE when logger is disabled'); + $this->assertNull($logger->getPath(), 'getPath() should return NULL when logger is disabled'); + } + else { + $this->assertTrue($result, 'open() should return TRUE when logger is enabled'); + $path = $logger->getPath(); + $this->assertNotNull($path, 'getPath() should return path after successful open()'); + + if ($expected_pattern !== NULL) { + $this->assertMatchesRegularExpression($expected_pattern, $path, 'Log file path should match expected pattern'); + } + + // Verify log directory was created. + $log_dir = dirname($path); + $this->assertDirectoryExists($log_dir, 'Log directory should be created'); + + // Verify log file was created. + $this->assertFileExists($path, 'Log file should be created'); + + $logger->close(); + File::remove($path); + } + } + + /** + * Test write method. + */ + #[DataProvider('dataProviderWrite')] + public function testWrite(string $content, bool $is_open, int $expected_writes): void { + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + + if ($is_open) { + $logger->open('test-command'); + $path = $logger->getPath(); + $this->assertNotNull($path); + } + + // Write content multiple times. + for ($i = 0; $i < $expected_writes; $i++) { + $logger->write($content); + } + + if ($is_open) { + $logger->close(); + $path = $logger->getPath(); + + // Verify content was written. + $written_content = file_get_contents((string) $path); + $expected_content = str_repeat($content, $expected_writes); + $this->assertEquals($expected_content, $written_content, 'Written content should match expected content'); + + File::remove((string) $path); + } + else { + // When logger is not open, write() should be a no-op. + // We can't directly verify this, but we ensure no errors occur. + // @phpstan-ignore-next-line + $this->assertTrue(TRUE, 'write() should not throw error when logger is not open'); + } + } + + /** + * Test close method. + */ + public function testClose(): void { + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + + // Test close when no file is open (should be no-op). + $logger->close(); + // @phpstan-ignore-next-line + $this->assertTrue(TRUE, 'close() should not throw error when no file is open'); + + // Test close after opening. + $logger->open('test-command'); + $path = $logger->getPath(); + $this->assertNotNull($path); + + $logger->close(); + + // Verify file is closed by attempting to write (should be no-op). + $logger->write('should not be written'); + + // File should still exist but content should not be written after close. + $content = file_get_contents($path); + $this->assertEquals('', $content, 'No content should be written after close()'); + + // Test multiple close calls (idempotent). + $logger->close(); + $logger->close(); + // @phpstan-ignore-next-line + $this->assertTrue(TRUE, 'Multiple close() calls should not throw error'); + + File::remove($path); + } + + /** + * Test getPath method. + */ + public function testGetPath(): void { + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + + // Test getPath before open() is called. + $this->assertNull($logger->getPath(), 'getPath() should return NULL before open() is called'); + + // Test getPath after open(). + $logger->open('test-command'); + $path = $logger->getPath(); + // @phpstan-ignore-next-line + $this->assertNotNull($path, 'getPath() should return path after open()'); + $this->assertStringContainsString('test-command', (string) $path, 'Path should contain command name'); + + $logger->close(); + File::remove((string) $path); + + // Test getPath when logging is disabled before open. + $logger2 = new FileLogger(); + $logger2->setDir(self::$tmp); + $logger2->disable(); + $result = $logger2->open('test-command-disabled'); + $this->assertFalse($result, 'open() should return FALSE when disabled'); + $this->assertNull($logger2->getPath(), 'getPath() should return NULL when logging is disabled'); + } + + /** + * Test buildFilename method. + */ + #[DataProvider('dataProviderBuildFilename')] + public function testBuildFilename(string $command, array $args, string $expected): void { + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + + $logger->open($command, $args); + $path = $logger->getPath(); + + if ($path !== NULL) { + $filename = basename($path, '.log'); + // Remove timestamp suffix (format: -YYYY-MM-DD-HHMMSS). + $filename = (string) preg_replace('/-\d{4}-\d{2}-\d{2}-\d{6}$/', '', $filename); + + $this->assertEquals($expected, $filename, 'Filename should match expected pattern'); + + $logger->close(); + File::remove($path); + } + } + + /** + * Data provider for enable/disable tests. + */ + public static function dataProviderEnableDisable(): array { + return [ + 'initially enabled' => [ + 'initial_state' => TRUE, + 'after_enable' => TRUE, + 'after_disable' => FALSE, + ], + 'initially disabled' => [ + 'initial_state' => FALSE, + 'after_enable' => TRUE, + 'after_disable' => FALSE, + ], + ]; + } + + /** + * Data provider for directory paths. + */ + public static function dataProviderDirectoryManagement(): array { + return [ + 'default directory (cwd)' => [ + 'dir' => '', + 'test_default' => TRUE, + ], + 'absolute path' => [ + 'dir' => '/tmp/test-dir', + 'test_default' => FALSE, + ], + 'relative path' => [ + 'dir' => './test-dir', + 'test_default' => FALSE, + ], + ]; + } + + /** + * Data provider for open scenarios. + */ + public static function dataProviderOpen(): array { + return [ + 'simple command, enabled' => [ + 'command' => 'test-command', + 'args' => [], + 'enabled' => TRUE, + 'expected_pattern' => '/test-command-\d{4}-\d{2}-\d{2}-\d{6}\.log$/', + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with positional args' => [ + 'command' => 'install', + 'args' => ['project', 'arg2'], + 'enabled' => TRUE, + 'expected_pattern' => '/install-project-arg2-\d{4}-\d{2}-\d{2}-\d{6}\.log$/', + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with option args (filtered)' => [ + 'command' => 'test', + 'args' => ['positional', '--option=value', '-f'], + 'enabled' => TRUE, + 'expected_pattern' => '/test-positional-\d{4}-\d{2}-\d{2}-\d{6}\.log$/', + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with special characters' => [ + 'command' => 'test:command', + 'args' => ['arg/with/slashes', 'arg with spaces'], + 'enabled' => TRUE, + 'expected_pattern' => '/test-command-arg-with-slashes-arg-with-spaces-\d{4}-\d{2}-\d{2}-\d{6}\.log$/', + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'disabled logger' => [ + 'command' => 'test-disabled', + 'args' => [], + 'enabled' => FALSE, + 'expected_pattern' => NULL, + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + ]; + } + + /** + * Data provider for write content. + */ + public static function dataProviderWrite(): array { + return [ + 'single write, logger open' => [ + 'content' => 'Test log entry', + 'is_open' => TRUE, + 'expected_writes' => 1, + ], + 'multiple writes, logger open' => [ + 'content' => 'Line of text', + 'is_open' => TRUE, + 'expected_writes' => 3, + ], + 'empty content, logger open' => [ + 'content' => '', + 'is_open' => TRUE, + 'expected_writes' => 1, + ], + 'multiline content, logger open' => [ + 'content' => "Line 1\nLine 2\nLine 3\n", + 'is_open' => TRUE, + 'expected_writes' => 1, + ], + 'write when logger not open (no-op)' => [ + 'content' => 'Should not be written', + 'is_open' => FALSE, + 'expected_writes' => 1, + ], + ]; + } + + /** + * Data provider for filename building. + */ + public static function dataProviderBuildFilename(): array { + return [ + 'command only' => [ + 'command' => 'test-command', + 'args' => [], + 'expected' => 'test-command', + ], + 'command with positional args' => [ + 'command' => 'install', + 'args' => ['project', 'theme'], + 'expected' => 'install-project-theme', + ], + 'command with options (filtered)' => [ + 'command' => 'run', + 'args' => ['script', '--verbose', '-f', 'value'], + 'expected' => 'run-script-value', + ], + 'special characters sanitized' => [ + 'command' => 'test/command:name', + 'args' => ['arg@with#special', 'arg with spaces'], + 'expected' => 'test-command-name-arg-with-special-arg-with-spaces', + ], + 'multiple consecutive hyphens collapsed' => [ + 'command' => 'test---command', + 'args' => ['arg***value'], + 'expected' => 'test-command-arg-value', + ], + 'empty result fallback' => [ + 'command' => '---', + 'args' => ['--option', '-f'], + 'expected' => 'runner', + ], + ]; + } + +} diff --git a/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php b/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php new file mode 100644 index 000000000..1067ec5ea --- /dev/null +++ b/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php @@ -0,0 +1,635 @@ +getLogger(); + $this->assertInstanceOf(FileLogger::class, $logger1); + + $logger2 = $runner->getLogger(); + $this->assertSame($logger1, $logger2, 'getLogger() should return the same instance on subsequent calls'); + } + + /** + * Test getCwd returns current directory by default. + */ + public function testGetCwdReturnsCurrentDirectory(): void { + $runner = new ConcreteRunner(); + + $cwd = $runner->getCwd(); + $this->assertEquals(getcwd(), $cwd); + } + + /** + * Test setCwd sets custom directory. + */ + public function testSetCwdSetsCustomDirectory(): void { + $runner = new ConcreteRunner(); + + $result = $runner->setCwd('/custom/path'); + $this->assertEquals('/custom/path', $runner->getCwd()); + $this->assertInstanceOf(AbstractRunner::class, $result, 'setCwd() should return self for method chaining'); + } + + /** + * Test setCwd updates logger directory. + */ + public function testSetCwdUpdatesLoggerDirectory(): void { + $runner = new ConcreteRunner(); + $logger = $runner->getLogger(); + + $runner->setCwd(self::$tmp); + + $this->assertEquals(self::$tmp, $logger->getDir()); + } + + /** + * Test enableLog calls logger's enable. + */ + public function testEnableLog(): void { + $runner = new ConcreteRunner(); + $logger = $runner->getLogger(); + + $logger->disable(); + $this->assertFalse($logger->isEnabled()); + + $result = $runner->enableLog(); + $this->assertTrue($logger->isEnabled()); + $this->assertInstanceOf(AbstractRunner::class, $result, 'enableLog() should return self for method chaining'); + } + + /** + * Test disableLog calls logger's disable. + */ + public function testDisableLog(): void { + $runner = new ConcreteRunner(); + $logger = $runner->getLogger(); + + $this->assertTrue($logger->isEnabled()); + + $result = $runner->disableLog(); + $this->assertFalse($logger->isEnabled()); + $this->assertInstanceOf(AbstractRunner::class, $result, 'disableLog() should return self for method chaining'); + } + + /** + * Test enableStreaming sets internal flag. + */ + public function testEnableStreaming(): void { + $runner = new ConcreteRunner(); + + // Streaming is enabled by default. + $this->assertTrue($runner->shouldStream()); + + $runner->disableStreaming(); + $this->assertFalse($runner->shouldStream()); + + $result = $runner->enableStreaming(); + $this->assertTrue($runner->shouldStream()); + $this->assertInstanceOf(AbstractRunner::class, $result, 'enableStreaming() should return self for method chaining'); + } + + /** + * Test disableStreaming sets internal flag. + */ + public function testDisableStreaming(): void { + $runner = new ConcreteRunner(); + + $this->assertTrue($runner->shouldStream()); + + $result = $runner->disableStreaming(); + $this->assertFalse($runner->shouldStream()); + $this->assertInstanceOf(AbstractRunner::class, $result, 'disableStreaming() should return self for method chaining'); + } + + /** + * Test getCommand returns NULL initially. + */ + public function testGetCommandInitiallyNull(): void { + $runner = new ConcreteRunner(); + + $this->assertNull($runner->getCommand()); + } + + /** + * Test getExitCode returns 0 initially. + */ + public function testGetExitCodeInitiallyZero(): void { + $runner = new ConcreteRunner(); + + $this->assertEquals(0, $runner->getExitCode()); + } + + /** + * Test getOutput returns empty string initially. + */ + public function testGetOutputInitiallyEmpty(): void { + $runner = new ConcreteRunner(); + + $this->assertEquals('', $runner->getOutput()); + } + + /** + * Test getOutput with as_array parameter. + */ + #[DataProvider('providerGetOutputVariations')] + public function testGetOutputVariations(string $output, bool $as_array, ?int $lines, string | array $expected): void { + $runner = new ConcreteRunner(); + $runner->setOutput($output); + + $result = $runner->getOutput($as_array, $lines); + + $this->assertEquals($expected, $result); + } + + /** + * Test parseCommand with various formats. + */ + #[DataProvider('providerParseCommand')] + public function testParseCommand(string $command, array $expected, ?string $expected_exception, ?string $expected_message): void { + if ($expected_exception !== NULL) { + /** @var class-string<\Throwable> $expected_exception */ + $this->expectException($expected_exception); + $this->expectExceptionMessage($expected_message ?? ''); + } + + $runner = new ConcreteRunner(); + $result = $runner->parseCommandPublic($command); + + if ($expected_exception === NULL) { + $this->assertEquals($expected, $result); + } + } + + /** + * Test buildCommandString with various arguments. + */ + #[DataProvider('providerBuildCommandString')] + public function testBuildCommandString(string $command, array $args, array $opts, string $expected): void { + $runner = new ConcreteRunner(); + + $result = $runner->buildCommandStringPublic($command, $args, $opts); + + $this->assertEquals($expected, $result); + } + + /** + * Test quoteArgument method. + */ + #[DataProvider('providerQuoteArgument')] + public function testQuoteArgument(string $argument, string $expected): void { + $runner = new ConcreteRunner(); + + $result = $runner->quoteArgumentPublic($argument); + + $this->assertEquals($expected, $result); + } + + /** + * Test formatArgs method. + */ + #[DataProvider('providerFormatArgs')] + public function testFormatArgs(array $args, array $expected): void { + $runner = new ConcreteRunner(); + + $result = $runner->formatArgsPublic($args); + + $this->assertEquals($expected, $result); + } + + /** + * Test reset method. + */ + public function testReset(): void { + $runner = new ConcreteRunner(); + + $runner->setCommand('test-command'); + $runner->setOutput('test output'); + $runner->setExitCode(1); + + $this->assertEquals('test-command', $runner->getCommand()); + $this->assertEquals('test output', $runner->getOutput()); + $this->assertEquals(1, $runner->getExitCode()); + + $runner->resetPublic(); + + $this->assertNull($runner->getCommand()); + $this->assertEquals('', $runner->getOutput()); + $this->assertEquals(0, $runner->getExitCode()); + } + + /** + * Test initLogger sets correct directory and opens log. + */ + public function testInitLogger(): void { + $runner = new ConcreteRunner(); + $runner->setCwd(self::$tmp); + + $logger = $runner->initLoggerPublic('test-command', ['arg1', 'arg2']); + + $this->assertInstanceOf(FileLogger::class, $logger); + $this->assertEquals(self::$tmp, $logger->getDir()); + + $path = $logger->getPath(); + $this->assertNotNull($path); + $this->assertStringContainsString('test-command', $path); + $this->assertStringContainsString('arg1', $path); + $this->assertStringContainsString('arg2', $path); + + $logger->close(); + } + + /** + * Test resolveOutput with NULL uses default. + */ + public function testResolveOutputWithNull(): void { + $runner = new ConcreteRunner(); + + // Initialize Tui with a mock output first. + $mock_output = $this->createMock(OutputInterface::class); + Tui::init($mock_output); + + $output = $runner->resolveOutputPublic(NULL); + + $this->assertInstanceOf(OutputInterface::class, $output); + $this->assertSame($mock_output, $output); + } + + /** + * Test resolveOutput with provided output. + */ + public function testResolveOutputWithProvided(): void { + $runner = new ConcreteRunner(); + $mock_output = $this->createMock(OutputInterface::class); + + $output = $runner->resolveOutputPublic($mock_output); + + $this->assertSame($mock_output, $output); + } + + /** + * Data provider for getOutput variations. + */ + public static function providerGetOutputVariations(): array { + return [ + 'string output, as_array=false, no limit' => [ + 'output' => "Line 1\nLine 2\nLine 3", + 'as_array' => FALSE, + 'lines' => NULL, + 'expected' => "Line 1\nLine 2\nLine 3", + ], + 'string output, as_array=true, no limit' => [ + 'output' => "Line 1\nLine 2\nLine 3", + 'as_array' => TRUE, + 'lines' => NULL, + 'expected' => ['Line 1', 'Line 2', 'Line 3'], + ], + 'string output, as_array=false, limit=2' => [ + 'output' => "Line 1\nLine 2\nLine 3", + 'as_array' => FALSE, + 'lines' => 2, + 'expected' => "Line 1\nLine 2", + ], + 'string output, as_array=true, limit=2' => [ + 'output' => "Line 1\nLine 2\nLine 3", + 'as_array' => TRUE, + 'lines' => 2, + 'expected' => ['Line 1', 'Line 2'], + ], + 'empty output, as_array=false' => [ + 'output' => '', + 'as_array' => FALSE, + 'lines' => NULL, + 'expected' => '', + ], + 'empty output, as_array=true' => [ + 'output' => '', + 'as_array' => TRUE, + 'lines' => NULL, + 'expected' => [''], + ], + ]; + } + + /** + * Data provider for parseCommand. + */ + public static function providerParseCommand(): array { + return [ + 'simple command' => [ + 'command' => 'echo', + 'expected' => ['echo'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with arguments' => [ + 'command' => 'echo hello world', + 'expected' => ['echo', 'hello', 'world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with single-quoted argument' => [ + 'command' => "echo 'hello world'", + 'expected' => ['echo', 'hello world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with double-quoted argument' => [ + 'command' => 'echo "hello world"', + 'expected' => ['echo', 'hello world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with escaped character' => [ + 'command' => 'echo hello\\ world', + 'expected' => ['echo', 'hello world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with escaped quote inside single quotes' => [ + 'command' => "echo 'It\\'s working'", + 'expected' => ['echo', "It's working"], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with mixed quotes' => [ + 'command' => 'echo "hello" \'world\'', + 'expected' => ['echo', 'hello', 'world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with end-of-options marker' => [ + 'command' => 'echo -- --not-an-option', + 'expected' => ['echo', '--', '--not-an-option'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'empty command' => [ + 'command' => '', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Command cannot be empty.', + ], + 'whitespace only command' => [ + 'command' => ' ', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Command cannot be empty.', + ], + 'unclosed single quote' => [ + 'command' => "echo 'unclosed", + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Unclosed quote in command string.', + ], + 'unclosed double quote' => [ + 'command' => 'echo "unclosed', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Unclosed quote in command string.', + ], + 'trailing escape' => [ + 'command' => 'echo trailing\\', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Trailing escape character in command string.', + ], + ]; + } + + /** + * Data provider for buildCommandString. + */ + public static function providerBuildCommandString(): array { + return [ + 'command only' => [ + 'command' => 'echo', + 'args' => [], + 'opts' => [], + 'expected' => 'echo', + ], + 'command with positional args' => [ + 'command' => 'echo', + 'args' => ['hello', 'world'], + 'opts' => [], + 'expected' => 'echo hello world', + ], + 'command with named options' => [ + 'command' => 'echo', + 'args' => [], + 'opts' => ['--verbose' => TRUE, '--format' => 'json'], + 'expected' => 'echo --verbose --format=json', + ], + 'command with mixed args and options' => [ + 'command' => 'echo', + 'args' => ['hello'], + 'opts' => ['--verbose' => TRUE], + 'expected' => 'echo hello --verbose', + ], + 'argument with spaces requires quoting' => [ + 'command' => 'echo', + 'args' => ['hello world'], + 'opts' => [], + 'expected' => "echo 'hello world'", + ], + 'empty string argument' => [ + 'command' => 'echo', + 'args' => [''], + 'opts' => [], + 'expected' => "echo ''", + ], + ]; + } + + /** + * Data provider for quoteArgument. + */ + public static function providerQuoteArgument(): array { + return [ + 'simple string (no quoting)' => [ + 'argument' => 'hello', + 'expected' => 'hello', + ], + 'string with spaces' => [ + 'argument' => 'hello world', + 'expected' => "'hello world'", + ], + 'string with single quote' => [ + 'argument' => "It's working", + 'expected' => "'It'\\''s working'", + ], + 'string with double quote' => [ + 'argument' => 'Say "hello"', + 'expected' => "'Say \"hello\"'", + ], + 'string with shell special chars' => [ + 'argument' => 'test$variable', + 'expected' => "'test\$variable'", + ], + 'empty string' => [ + 'argument' => '', + 'expected' => "''", + ], + 'string with backslash' => [ + 'argument' => 'path\\to\\file', + 'expected' => "'path\\to\\file'", + ], + ]; + } + + /** + * Data provider for formatArgs. + */ + public static function providerFormatArgs(): array { + return [ + 'positional args' => [ + 'args' => ['arg1', 'arg2'], + 'expected' => ['arg1', 'arg2'], + ], + 'named args with string values' => [ + 'args' => ['--option' => 'value', '--flag' => 'enabled'], + 'expected' => ['--option=value', '--flag=enabled'], + ], + 'named args with bool TRUE' => [ + 'args' => ['--verbose' => TRUE], + 'expected' => ['--verbose'], + ], + 'named args with bool FALSE (excluded)' => [ + 'args' => ['--verbose' => FALSE], + 'expected' => [], + ], + 'positional args with bool TRUE' => [ + 'args' => [TRUE], + 'expected' => ['1'], + ], + 'positional args with bool FALSE (excluded)' => [ + 'args' => [FALSE], + 'expected' => [], + ], + 'mixed positional and named' => [ + 'args' => ['pos1', '--opt' => 'val', 'pos2'], + 'expected' => ['pos1', '--opt=val', 'pos2'], + ], + ]; + } + +} + +/** + * Concrete runner implementation for testing AbstractRunner. + */ +class ConcreteRunner extends AbstractRunner { + + /** + * {@inheritdoc} + */ + public function run(string $command, array $args = [], array $inputs = [], array $env = [], ?OutputInterface $output = NULL): static { + // Simple implementation for testing. + $this->command = $command; + return $this; + } + + /** + * Public wrapper for parseCommand. + */ + public function parseCommandPublic(string $command): array { + return $this->parseCommand($command); + } + + /** + * Public wrapper for buildCommandString. + */ + public function buildCommandStringPublic(string $command, array $args = [], array $opts = []): string { + return $this->buildCommandString($command, $args, $opts); + } + + /** + * Public wrapper for quoteArgument. + */ + public function quoteArgumentPublic(string $argument): string { + return $this->quoteArgument($argument); + } + + /** + * Public wrapper for formatArgs. + */ + public function formatArgsPublic(array $args): array { + return $this->formatArgs($args); + } + + /** + * Public wrapper for reset. + */ + public function resetPublic(): void { + $this->reset(); + } + + /** + * Public setter for command (for testing). + */ + public function setCommand(string $command): void { + $this->command = $command; + } + + /** + * Public setter for output (for testing). + */ + public function setOutput(string $output): void { + $this->output = $output; + } + + /** + * Public setter for exitCode (for testing). + */ + public function setExitCode(int $exitCode): void { + if ($exitCode < 0 || $exitCode > 255) { + throw new \RuntimeException('Exit code is out of valid range (0-255).'); + } + + $this->exitCode = $exitCode; + } + + /** + * Public getter for shouldStream (for testing). + */ + public function shouldStream(): bool { + return $this->shouldStream; + } + + /** + * Public wrapper for initLogger. + */ + public function initLoggerPublic(string $command, array $args = []): FileLoggerInterface { + return $this->initLogger($command, $args); + } + + /** + * Public wrapper for resolveOutput. + */ + public function resolveOutputPublic(?OutputInterface $output): OutputInterface { + return $this->resolveOutput($output); + } + +} diff --git a/.vortex/installer/tests/Unit/Runner/CommandRunnerTest.php b/.vortex/installer/tests/Unit/Runner/CommandRunnerTest.php new file mode 100644 index 000000000..d6dc0def7 --- /dev/null +++ b/.vortex/installer/tests/Unit/Runner/CommandRunnerTest.php @@ -0,0 +1,242 @@ +assertInstanceOf(CommandRunner::class, $runner); + } + + /** + * Test run with valid command. + */ + public function testRunWithValidCommand(): void { + $application = new Application(); + $command = new TestCommand('test:command'); + $application->add($command); + + $runner = new CommandRunner($application); + $runner->setCwd(self::$tmp); + + $output = new BufferedOutput(); + Tui::init($output); + + $result = $runner->run('test:command'); + + $this->assertInstanceOf(CommandRunner::class, $result); + $this->assertEquals(0, $runner->getExitCode()); + $runner_output = $runner->getOutput(); + $this->assertStringContainsString('Test output', is_string($runner_output) ? $runner_output : implode(PHP_EOL, $runner_output)); + } + + /** + * Test run with streaming enabled/disabled. + */ + #[DataProvider('dataProviderRunWithStreaming')] + public function testRunWithStreaming(bool $streaming_enabled, bool $should_have_output): void { + $application = new Application(); + $command = new TestCommand('test:command'); + $application->add($command); + + $runner = new CommandRunner($application); + $runner->setCwd(self::$tmp); + + if (!$streaming_enabled) { + $runner->disableStreaming(); + } + + $output = new BufferedOutput(); + Tui::init($output); + + $runner->run('test:command', [], [], [], $output); + + $stream_content = $output->fetch(); + + if ($should_have_output) { + $this->assertStringContainsString('Test output', $stream_content); + } + else { + $this->assertStringNotContainsString('Test output', $stream_content); + } + + // Output should always be captured in runner. + $output = $runner->getOutput(); + $this->assertStringContainsString('Test output', is_string($output) ? $output : implode(PHP_EOL, $output)); + } + + /** + * Test createCompositeOutput method using reflection. + */ + public function testCreateCompositeOutput(): void { + $application = new Application(); + $runner = new CommandRunner($application); + $runner->setCwd(self::$tmp); + + $output = new BufferedOutput(); + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + $logger->open('test'); + + // Use reflection to access protected method. + $reflection = new \ReflectionClass($runner); + $method = $reflection->getMethod('createCompositeOutput'); + + [$composite_output, $buffered_output] = $method->invoke($runner, $output, $logger); + + $this->assertInstanceOf(OutputInterface::class, $composite_output); + $this->assertInstanceOf(BufferedOutput::class, $buffered_output); + + // Test composite output behavior. + $composite_output->write('Test message'); + $this->assertStringContainsString('Test message', $buffered_output->fetch()); + + $composite_output->writeln('Another line'); + $this->assertStringContainsString('Another line', $buffered_output->fetch()); + + $logger->close(); + } + + /** + * Test composite output with iterable messages. + */ + public function testCompositeOutputWithIterableMessages(): void { + $application = new Application(); + $runner = new CommandRunner($application); + $runner->setCwd(self::$tmp); + + $output = new BufferedOutput(); + $logger = new FileLogger(); + $logger->setDir(self::$tmp); + $logger->open('test'); + + // Use reflection to access protected method. + $reflection = new \ReflectionClass($runner); + $method = $reflection->getMethod('createCompositeOutput'); + + [$composite_output, $buffered_output] = $method->invoke($runner, $output, $logger); + + // Test with iterable messages. + $composite_output->write(['Line 1', 'Line 2']); + $content = $buffered_output->fetch(); + $this->assertStringContainsString('Line 1', $content); + $this->assertStringContainsString('Line 2', $content); + + $composite_output->writeln(['Line 3', 'Line 4']); + $content = $buffered_output->fetch(); + $this->assertStringContainsString('Line 3', $content); + $this->assertStringContainsString('Line 4', $content); + + $logger->close(); + } + + /** + * Test run with options. + */ + public function testRunWithOptions(): void { + $application = new Application(); + $command = new TestCommand('test:command'); + $application->add($command); + + $runner = new CommandRunner($application); + $runner->setCwd(self::$tmp); + + $output = new BufferedOutput(); + Tui::init($output); + + // Test without options since test command doesn't define any. + $runner->run('test:command', []); + + $this->assertEquals(0, $runner->getExitCode()); + } + + /** + * Test run captures exit code. + */ + public function testRunCapturesExitCode(): void { + $application = new Application(); + $command = new TestCommandWithExitCode('test:error'); + $application->add($command); + + $runner = new CommandRunner($application); + $runner->setCwd(self::$tmp); + + $output = new BufferedOutput(); + Tui::init($output); + + $runner->run('test:error'); + + $this->assertEquals(1, $runner->getExitCode()); + } + + /** + * Data provider for streaming modes. + */ + public static function dataProviderRunWithStreaming(): array { + return [ + 'streaming enabled' => [ + 'streaming_enabled' => TRUE, + 'should_have_output' => TRUE, + ], + 'streaming disabled' => [ + 'streaming_enabled' => FALSE, + 'should_have_output' => FALSE, + ], + ]; + } + +} + +/** + * Test command for testing CommandRunner. + */ +class TestCommand extends Command { + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln('Test output'); + return 0; + } + +} + +/** + * Test command that returns non-zero exit code. + */ +class TestCommandWithExitCode extends Command { + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int { + $output->writeln('Error output'); + return 1; + } + +} diff --git a/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php b/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php new file mode 100644 index 000000000..c15ef1a16 --- /dev/null +++ b/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php @@ -0,0 +1,393 @@ + $expected_exception */ + $this->expectException($expected_exception); + $this->expectExceptionMessage($expected_message ?? ''); + } + + $runner = new ProcessRunner(); + $runner->setCwd(self::$tmp); + + // Initialize Tui for output. + $output = new BufferedOutput(); + Tui::init($output); + + $result = $runner->run($command, $args); + + if ($expected_exception === NULL) { + $this->assertInstanceOf(ProcessRunner::class, $result); + $this->assertEquals($expected_exit_code, $runner->getExitCode()); + $output = $runner->getOutput(); + $this->assertMatchesRegularExpression($expected_output_pattern, is_string($output) ? $output : implode(PHP_EOL, $output)); + $this->assertNotNull($runner->getCommand()); + } + } + + /** + * Test run with output streaming. + */ + #[DataProvider('dataProviderRunWithStreaming')] + public function testRunWithStreaming(bool $streaming_enabled, bool $should_have_output_in_stream): void { + $runner = new ProcessRunner(); + $runner->setCwd(self::$tmp); + + if (!$streaming_enabled) { + $runner->disableStreaming(); + } + + $output = new BufferedOutput(); + Tui::init($output); + + $runner->run('echo "test output"', []); + + $output_content = $output->fetch(); + + if ($should_have_output_in_stream) { + $this->assertStringContainsString('test output', $output_content); + } + else { + $this->assertStringNotContainsString('test output', $output_content); + } + + // Output should always be captured in runner. + $runner_output = $runner->getOutput(); + $this->assertStringContainsString('test output', is_string($runner_output) ? $runner_output : implode(PHP_EOL, $runner_output)); + } + + /** + * Test resolveCommand with various command types. + */ + #[DataProvider('dataProviderResolveCommand')] + public function testResolveCommand(string $command, bool $expect_success, ?string $expected_exception, ?string $expected_message): void { + if ($expected_exception !== NULL) { + /** @var class-string<\Throwable> $expected_exception */ + $this->expectException($expected_exception); + $this->expectExceptionMessage($expected_message ?? ''); + } + + $runner = new TestableProcessRunner(); + $runner->setCwd(self::$tmp); + + [$resolved, $parsed] = $runner->resolveCommandPublic($command); + + if ($expect_success) { + $this->assertNotEmpty($resolved); + $this->assertIsArray($parsed); + } + } + + /** + * Test prepareArguments method. + */ + #[DataProvider('dataProviderPrepareArguments')] + public function testPrepareArguments(array $parsed_args, array $additional_args, array $expected, ?string $expected_exception, ?string $expected_message): void { + if ($expected_exception !== NULL) { + /** @var class-string<\Throwable> $expected_exception */ + $this->expectException($expected_exception); + $this->expectExceptionMessage($expected_message ?? ''); + } + + $runner = new TestableProcessRunner(); + + $result = $runner->prepareArgumentsPublic($parsed_args, $additional_args); + + if ($expected_exception === NULL) { + $this->assertEquals($expected, $result); + } + } + + /** + * Test validateEnvironmentVars method. + */ + #[DataProvider('dataProviderValidateEnvironmentVars')] + public function testValidateEnvironmentVars(array $env, ?string $expected_exception, ?string $expected_message): void { + if ($expected_exception !== NULL) { + /** @var class-string<\Throwable> $expected_exception */ + $this->expectException($expected_exception); + $this->expectExceptionMessage($expected_message ?? ''); + } + + $runner = new TestableProcessRunner(); + + $runner->validateEnvironmentVarsPublic($env); + + if ($expected_exception === NULL) { + // @phpstan-ignore-next-line + $this->assertTrue(TRUE, 'Validation passed'); + } + } + + /** + * Test run with environment variables. + */ + public function testRunWithEnvironmentVariables(): void { + $runner = new ProcessRunner(); + $runner->setCwd(self::$tmp); + + $output = new BufferedOutput(); + Tui::init($output); + + // Use printenv command which is more reliable for testing env vars. + // On Windows, we skip this test as printenv may not be available. + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('Environment variable test not compatible with Windows.'); + } + + $runner->run('printenv TEST_VAR', [], [], ['TEST_VAR' => 'test_value']); + + $output = $runner->getOutput(); + $this->assertStringContainsString('test_value', is_string($output) ? $output : implode(PHP_EOL, $output)); + } + + /** + * Test run with working directory. + */ + public function testRunWithWorkingDirectory(): void { + $runner = new ProcessRunner(); + $test_dir = self::$tmp . '/test_subdir'; + File::mkdir($test_dir); + + $runner->setCwd($test_dir); + + $output = new BufferedOutput(); + Tui::init($output); + + $runner->run('pwd', []); + + $output = $runner->getOutput(); + $this->assertStringContainsString($test_dir, is_string($output) ? $output : implode(PHP_EOL, $output)); + } + + /** + * Test resolveCommand with relative path. + */ + public function testResolveCommandWithRelativePath(): void { + $runner = new TestableProcessRunner(); + $test_dir = self::$tmp . '/test_scripts'; + File::mkdir($test_dir); + + // Create an executable script. + $script_path = $test_dir . '/test_script.sh'; + File::dump($script_path, "#!/bin/sh\necho 'test'\n"); + chmod($script_path, 0755); + + $runner->setCwd(self::$tmp); + + [$resolved, $parsed] = $runner->resolveCommandPublic('test_scripts/test_script.sh'); + + $this->assertEquals($test_dir . '/test_script.sh', $resolved); + $this->assertEmpty($parsed); + } + + /** + * Test prepareArguments with object that can't be cast to scalar. + */ + public function testPrepareArgumentsWithNonScalarAfterFormatting(): void { + $runner = new TestableProcessRunner(); + + // Create a test object that formatArgs will add to the array, + // but which will fail the scalar check. + // However, formatArgs will cast it to string first, so this is hard + // to trigger. + // Let's test with an actual non-scalar after formatArgs processes it. + // Since formatArgs always produces strings, line 126 might be unreachable + // through normal usage. Let's document this. + // For now, just test that normal args work. + $result = $runner->prepareArgumentsPublic(['test'], ['arg1', 'arg2']); + + $this->assertEquals(['test', 'arg1', 'arg2'], $result); + } + + /** + * Data provider for run command tests. + */ + public static function dataProviderRun(): array { + return [ + 'simple echo command' => [ + 'command' => 'echo', + 'args' => ['hello', 'world'], + 'expected_output_pattern' => '/hello world/', + 'expected_exit_code' => 0, + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with single argument' => [ + 'command' => 'echo "test message"', + 'args' => [], + 'expected_output_pattern' => '/test message/', + 'expected_exit_code' => 0, + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command not found' => [ + 'command' => 'nonexistent_command_12345', + 'args' => [], + 'expected_output_pattern' => '//', + 'expected_exit_code' => 0, + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Command not found', + ], + 'command with invalid characters' => [ + 'command' => '$invalid-cmd', + 'args' => [], + 'expected_output_pattern' => '//', + 'expected_exit_code' => 0, + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Invalid command', + ], + ]; + } + + /** + * Data provider for streaming modes. + */ + public static function dataProviderRunWithStreaming(): array { + return [ + 'streaming enabled' => [ + 'streaming_enabled' => TRUE, + 'should_have_output_in_stream' => TRUE, + ], + 'streaming disabled' => [ + 'streaming_enabled' => FALSE, + 'should_have_output_in_stream' => FALSE, + ], + ]; + } + + /** + * Data provider for resolveCommand tests. + */ + public static function dataProviderResolveCommand(): array { + return [ + 'simple command (echo)' => [ + 'command' => 'echo', + 'expect_success' => TRUE, + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with arguments' => [ + 'command' => 'echo hello', + 'expect_success' => TRUE, + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command not in PATH' => [ + 'command' => 'nonexistent_cmd_xyz', + 'expect_success' => FALSE, + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Command not found', + ], + 'command with invalid characters' => [ + 'command' => 'echo$test', + 'expect_success' => FALSE, + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Invalid command', + ], + ]; + } + + /** + * Data provider for prepareArguments tests. + */ + public static function dataProviderPrepareArguments(): array { + return [ + 'merge parsed and additional args' => [ + 'parsed_args' => ['arg1', 'arg2'], + 'additional_args' => ['arg3', 'arg4'], + 'expected' => ['arg1', 'arg2', 'arg3', 'arg4'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'convert numeric args to strings' => [ + 'parsed_args' => ['test'], + 'additional_args' => [123, 456], + 'expected' => ['test', '123', '456'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'boolean arguments' => [ + 'parsed_args' => [], + 'additional_args' => ['--verbose' => TRUE, '--quiet' => FALSE], + 'expected' => ['--verbose'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + ]; + } + + /** + * Data provider for environment variables tests. + */ + public static function dataProviderValidateEnvironmentVars(): array { + return [ + 'valid scalar env vars' => [ + 'env' => ['VAR1' => 'value1', 'VAR2' => 'value2'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'empty env vars' => [ + 'env' => [], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'non-scalar env var throws exception' => [ + 'env' => ['VAR1' => ['array']], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'All environment variables must be scalar values', + ], + ]; + } + +} + +/** + * Testable ProcessRunner that exposes protected methods. + */ +class TestableProcessRunner extends ProcessRunner { + + /** + * Public wrapper for resolveCommand. + */ + public function resolveCommandPublic(string $command): array { + return $this->resolveCommand($command); + } + + /** + * Public wrapper for prepareArguments. + */ + public function prepareArgumentsPublic(array $parsed_args, array $additional_args): array { + return $this->prepareArguments($parsed_args, $additional_args); + } + + /** + * Public wrapper for validateEnvironmentVars. + */ + public function validateEnvironmentVarsPublic(array $env): void { + $this->validateEnvironmentVars($env); + } + +} diff --git a/.vortex/installer/tests/Unit/Task/TaskOutputTest.php b/.vortex/installer/tests/Unit/Task/TaskOutputTest.php new file mode 100644 index 000000000..04b70386d --- /dev/null +++ b/.vortex/installer/tests/Unit/Task/TaskOutputTest.php @@ -0,0 +1,128 @@ +assertInstanceOf(TaskOutput::class, $output); + } + + /** + * Test write method dims single message. + */ + public function testWriteSingleMessage(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $output->write('Test message'); + + $content = $wrapped->fetch(); + // The message should be dimmed (wrapped with ANSI codes). + $this->assertStringContainsString('Test message', $content); + } + + /** + * Test write method dims iterable messages. + */ + public function testWriteIterableMessages(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $output->write(['Line 1', 'Line 2']); + + $content = $wrapped->fetch(); + $this->assertStringContainsString('Line 1', $content); + $this->assertStringContainsString('Line 2', $content); + } + + /** + * Test writeln method dims single message. + */ + public function testWritelnSingleMessage(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $output->writeln('Test message'); + + $content = $wrapped->fetch(); + $this->assertStringContainsString('Test message', $content); + } + + /** + * Test writeln method dims iterable messages. + */ + public function testWritelnIterableMessages(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $output->writeln(['Line 1', 'Line 2']); + + $content = $wrapped->fetch(); + $this->assertStringContainsString('Line 1', $content); + $this->assertStringContainsString('Line 2', $content); + } + + /** + * Test verbosity delegation. + */ + public function testVerbosityDelegation(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + $this->assertEquals(OutputInterface::VERBOSITY_DEBUG, $output->getVerbosity()); + + $this->assertFalse($output->isQuiet()); + $this->assertTrue($output->isVerbose()); + $this->assertTrue($output->isVeryVerbose()); + $this->assertTrue($output->isDebug()); + } + + /** + * Test decoration delegation. + */ + public function testDecorationDelegation(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $output->setDecorated(TRUE); + $this->assertTrue($output->isDecorated()); + + $output->setDecorated(FALSE); + $this->assertFalse($output->isDecorated()); + } + + /** + * Test formatter delegation. + */ + public function testFormatterDelegation(): void { + $wrapped = new BufferedOutput(); + $output = new TaskOutput($wrapped); + + $formatter = new OutputFormatter(); + $output->setFormatter($formatter); + + $this->assertSame($formatter, $output->getFormatter()); + } + +} diff --git a/.vortex/installer/tests/Unit/TaskTest.php b/.vortex/installer/tests/Unit/TaskTest.php index 1f79d7ccb..d225103f4 100644 --- a/.vortex/installer/tests/Unit/TaskTest.php +++ b/.vortex/installer/tests/Unit/TaskTest.php @@ -6,7 +6,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; -use DrevOps\VortexInstaller\Utils\Task; +use DrevOps\VortexInstaller\Task\Task; use DrevOps\VortexInstaller\Utils\Tui; use Symfony\Component\Console\Output\BufferedOutput; @@ -131,4 +131,284 @@ public function testActionInvalidArgument(): void { Task::action('Test label'); } + /** + * Test streaming mode with OutputInterface usage. + * + * This tests when the closure uses Tui methods (or any OutputInterface + * methods) to write output. + */ + public function testActionStreamingWithOutputInterface(): void { + $output = new BufferedOutput(); + Tui::init($output); + + $result = Task::action( + label: 'Processing with output', + action: function (): string { + // Use Tui methods that write to the output interface. + Tui::line('Line 1 from Tui'); + Tui::line('Line 2 from Tui'); + return 'done'; + }, + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action executed successfully. + $this->assertEquals('done', $result); + + // Verify the start message is shown. + $this->assertStringContainsString('Processing with output', $actual); + + // Verify the output from Tui methods is captured and dimmed. + // TaskOutput wraps messages with dim ANSI codes. + $this->assertStringContainsString('Line 1 from Tui', $actual); + $this->assertStringContainsString('Line 2 from Tui', $actual); + + // Verify success message is shown. + $this->assertStringContainsString('✓ OK', $actual); + } + + /** + * Test streaming mode with echo/print statements. + * + * This tests when the closure uses echo/print (PHP output buffering + * captures these). + */ + public function testActionStreamingWithEchoPrint(): void { + $output = new BufferedOutput(); + Tui::init($output); + + $result = Task::action( + label: 'Processing with echo', + action: function (): string { + // Use echo/print statements. + echo "Echo output line 1\n"; + echo "Echo output line 2\n"; + print "Print output line 3\n"; + return 'completed'; + }, + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action executed successfully. + $this->assertEquals('completed', $result); + + // Verify the start message is shown. + $this->assertStringContainsString('Processing with echo', $actual); + + // Verify echo/print output is captured via output buffering. + $this->assertStringContainsString('Echo output line 1', $actual); + $this->assertStringContainsString('Echo output line 2', $actual); + $this->assertStringContainsString('Print output line 3', $actual); + + // Verify success message is shown. + $this->assertStringContainsString('✓ OK', $actual); + } + + /** + * Test streaming mode with both OutputInterface and echo/print. + * + * This tests that both types of output are captured correctly in the same + * action. + */ + public function testActionStreamingWithMixedOutput(): void { + $output = new BufferedOutput(); + Tui::init($output); + + $result = Task::action( + label: 'Processing mixed output', + action: function (): array { + // Mix OutputInterface usage and echo statements. + Tui::line('From Tui line 1'); + echo "From echo line 1\n"; + Tui::line('From Tui line 2'); + echo "From echo line 2\n"; + print "From print line 3\n"; + return ['result1', 'result2']; + }, + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action executed successfully. + $this->assertEquals(['result1', 'result2'], $result); + + // Verify both types of output are captured. + $this->assertStringContainsString('From Tui line 1', $actual); + $this->assertStringContainsString('From echo line 1', $actual); + $this->assertStringContainsString('From Tui line 2', $actual); + $this->assertStringContainsString('From echo line 2', $actual); + $this->assertStringContainsString('From print line 3', $actual); + + // Verify success message is shown. + $this->assertStringContainsString('✓ OK', $actual); + } + + /** + * Test streaming mode with failure. + */ + public function testActionStreamingWithFailure(): void { + $output = new BufferedOutput(); + Tui::init($output); + + $result = Task::action( + label: 'Processing that fails', + action: function (): false { + echo "Some output before failure\n"; + Tui::line('More output'); + return FALSE; + }, + failure: 'Operation failed', + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action returned FALSE. + $this->assertFalse($result); + + // Verify output is captured even on failure. + $this->assertStringContainsString('Some output before failure', $actual); + $this->assertStringContainsString('More output', $actual); + + // Verify failure message is shown. + $this->assertStringContainsString('Operation failed', $actual); + } + + /** + * Test streaming mode with custom success message. + */ + public function testActionStreamingWithCustomSuccessMessage(): void { + $output = new BufferedOutput(); + Tui::init($output); + + $result = Task::action( + label: 'Processing data', + action: function (): int { + echo "Processing item 1\n"; + echo "Processing item 2\n"; + return 42; + }, + success: fn(mixed $result): string => sprintf('Processed %s items', $result), + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action executed successfully. + $this->assertEquals(42, $result); + + // Verify output is captured. + $this->assertStringContainsString('Processing item 1', $actual); + $this->assertStringContainsString('Processing item 2', $actual); + + // Verify custom success message with result is shown. + $this->assertStringContainsString('Processed 42 items', $actual); + } + + /** + * Test that output is restored after streaming. + */ + public function testActionStreamingRestoresOutput(): void { + $output = new BufferedOutput(); + Tui::init($output); + + // Store original output. + $original_output = Tui::output(); + + Task::action( + label: 'Testing output restoration', + action: function (): string { + echo "Some streamed output\n"; + return 'done'; + }, + streaming: TRUE, + ); + + // Verify output is restored to the original. + $restored_output = Tui::output(); + $this->assertSame($original_output, $restored_output); + } + + /** + * Test streaming with no output from action. + */ + public function testActionStreamingWithNoOutput(): void { + $output = new BufferedOutput(); + Tui::init($output); + + $result = Task::action( + label: 'Silent processing', + action: fn(): string => 'completed', + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action executed successfully. + $this->assertEquals('completed', $result); + + // Verify start message and success are shown even with no action output. + $this->assertStringContainsString('Silent processing', $actual); + $this->assertStringContainsString('✓ OK', $actual); + } + + /** + * Test streaming with action that directly uses the output parameter. + * + * This verifies that when the action callback receives and uses the output + * parameter directly (not through Tui), the streaming functionality properly + * captures all output and doesn't let it spill elsewhere. + */ + public function testActionStreamingWithDirectOutputUsage(): void { + $output = new BufferedOutput(); + Tui::init($output); + + // Track what was captured in the main output. + $result = Task::action( + label: 'Processing with direct output', + action: function (): string { + // Get the current output (which should be TaskOutput during streaming). + $current_output = Tui::output(); + + // Write directly to the output parameter. + $current_output->writeln('Direct output line 1'); + $current_output->writeln('Direct output line 2'); + + // Mix with echo. + echo "Echo during streaming\n"; + + // Write more to output. + $current_output->write('Direct output line 3'); + + return 'done'; + }, + streaming: TRUE, + ); + + $actual = $output->fetch(); + + // Verify the action executed successfully. + $this->assertEquals('done', $result); + + // Verify all output was captured and not spilled. + $this->assertStringContainsString('Direct output line 1', $actual); + $this->assertStringContainsString('Direct output line 2', $actual); + $this->assertStringContainsString('Direct output line 3', $actual); + $this->assertStringContainsString('Echo during streaming', $actual); + + // Verify the label and success are shown. + $this->assertStringContainsString('Processing with direct output', $actual); + $this->assertStringContainsString('✓ OK', $actual); + + // The key verification: all output should be in the buffered output, + // not leaked anywhere else. We verify this by checking that the + // BufferedOutput received everything. + $this->assertNotEmpty($actual); + } + } diff --git a/.vortex/installer/tests/Unit/TuiTest.php b/.vortex/installer/tests/Unit/TuiTest.php index ec47495cb..d70173f2c 100644 --- a/.vortex/installer/tests/Unit/TuiTest.php +++ b/.vortex/installer/tests/Unit/TuiTest.php @@ -871,4 +871,84 @@ public static function dataProviderNormalizeText(): array { ]; } + /** + * Test setOutput method. + */ + public function testSetOutput(): void { + $output1 = new BufferedOutput(); + $output2 = new BufferedOutput(); + + Tui::init($output1); + $this->assertSame($output1, Tui::output()); + + Tui::setOutput($output2); + $this->assertSame($output2, Tui::output()); + } + + /** + * Test success method. + */ + public function testSuccess(): void { + $output = new BufferedOutput(); + Tui::init($output); + + Tui::success('Operation succeeded'); + + $actual = $output->fetch(); + $this->assertStringContainsString('Operation succeeded', $actual); + } + + /** + * Test line method. + */ + public function testLine(): void { + $output = new BufferedOutput(); + Tui::init($output); + + Tui::line('Test line'); + + $actual = $output->fetch(); + $this->assertStringContainsString('Test line', $actual); + } + + /** + * Test line method with custom padding. + */ + public function testLineWithPadding(): void { + $output = new BufferedOutput(); + Tui::init($output); + + Tui::line('Test line', 5); + + $actual = $output->fetch(); + $this->assertStringContainsString(' Test line', $actual); + } + + /** + * Test confirm in non-interactive mode (returns default). + */ + public function testConfirmNonInteractive(): void { + $output = new BufferedOutput(); + Tui::init($output, FALSE); + + // In non-interactive mode, confirm should return the default value. + $result = Tui::confirm('Confirm action?', TRUE); + $this->assertTrue($result); + + $result = Tui::confirm('Confirm action?', FALSE); + $this->assertFalse($result); + } + + /** + * Test getChar in non-interactive mode. + */ + public function testGetCharNonInteractive(): void { + $output = new BufferedOutput(); + Tui::init($output, FALSE); + + // In non-interactive mode, getChar should return empty string. + $result = Tui::getChar(); + $this->assertEquals('', $result); + } + } From 61f187f81c5e1e24c7ab9dc96ddbcff08e5a4b48 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Mon, 1 Dec 2025 13:17:42 +1100 Subject: [PATCH 2/8] Updated installer to use `--destination` option instead of an argument. --- .github/workflows/vortex-release.yml | 2 +- .github/workflows/vortex-test-installer.yml | 2 +- .../installer/src/Command/InstallCommand.php | 25 ++++++++----------- .../Functional/Command/InstallCommandTest.php | 5 ++-- .../tests/Functional/FunctionalTestCase.php | 13 +++++----- .../installer/tests/Functional/PharTest.php | 7 +++--- .vortex/tests/phpunit/Traits/SutTrait.php | 9 +++---- 7 files changed, 30 insertions(+), 33 deletions(-) diff --git a/.github/workflows/vortex-release.yml b/.github/workflows/vortex-release.yml index c6011c133..1f49a28ee 100644 --- a/.github/workflows/vortex-release.yml +++ b/.github/workflows/vortex-release.yml @@ -77,7 +77,7 @@ jobs: - name: Test PHAR run: | ./build/installer.phar --version - ./build/installer.phar --no-interaction --no-cleanup test || exit 1 + ./build/installer.phar --no-interaction --no-cleanup --destination=test || exit 1 working-directory: .vortex/installer - name: Upload artifact diff --git a/.github/workflows/vortex-test-installer.yml b/.github/workflows/vortex-test-installer.yml index d25127d09..48ebaf3fa 100644 --- a/.github/workflows/vortex-test-installer.yml +++ b/.github/workflows/vortex-test-installer.yml @@ -86,7 +86,7 @@ jobs: working-directory: .vortex/installer - name: Test PHAR - run: ./build/installer.phar --no-interaction example || exit 1 + run: ./build/installer.phar --no-interaction --destination=example || exit 1 working-directory: .vortex/installer env: GITHUB_TOKEN: ${{ secrets.PACKAGE_TOKEN }} diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index bad8388f4..9185c3dae 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -17,7 +17,6 @@ use DrevOps\VortexInstaller\Utils\Strings; use DrevOps\VortexInstaller\Utils\Tui; use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -31,7 +30,7 @@ */ class InstallCommand extends Command { - const ARG_DESTINATION = 'destination'; + const OPTION_DESTINATION = 'destination'; const OPTION_ROOT = 'root'; @@ -87,23 +86,22 @@ protected function configure(): void { $this->setDescription('Install Vortex from remote or local repository.'); $this->setHelp(<<addArgument(static::ARG_DESTINATION, InputArgument::OPTIONAL, 'Destination directory. Optional. Defaults to the current directory.'); - + $this->addOption(static::OPTION_DESTINATION, NULL, InputOption::VALUE_REQUIRED, 'Destination directory. Defaults to the current directory.'); $this->addOption(static::OPTION_ROOT, NULL, InputOption::VALUE_REQUIRED, 'Path to the root for file path resolution. If not specified, current directory is used.'); $this->addOption(static::OPTION_NO_INTERACTION, 'n', InputOption::VALUE_NONE, 'Do not ask any interactive question.'); $this->addOption(static::OPTION_CONFIG, 'c', InputOption::VALUE_REQUIRED, 'A JSON string with options or a path to a JSON file.'); @@ -116,8 +114,7 @@ protected function configure(): void { * {@inheritdoc} */ protected function execute(InputInterface $input, OutputInterface $output): int { - // @see https://github.com/drevops/vortex/issues/1502 - if ($input->getOption('help') || $input->getArgument('destination') == 'help') { + if ($input->getOption('help')) { $output->write($this->getHelp()); return Command::SUCCESS; @@ -207,11 +204,11 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); if (!$build_ok) { - Tui::error('Build failed. The site was installed but build process encountered errors.'); + Tui::error('Site build failed. Vortex was installed, but the site build process encountered errors.'); Tui::line(''); Tui::line('Next steps:'); Tui::line(' - Run: ahoy build'); - Tui::line(' - Or inspect logs for details'); + Tui::line(' - Or inspect logs for details with `ahoy logs`'); Tui::line(''); return Command::FAILURE; @@ -286,7 +283,7 @@ protected function resolveOptions(array $arguments, array $options): void { } // Set destination directory. - $dst = !empty($arguments['destination']) && is_scalar($arguments[static::ARG_DESTINATION]) ? strval($arguments[static::ARG_DESTINATION]) : NULL; + $dst = !empty($options[static::OPTION_DESTINATION]) && is_scalar($options[static::OPTION_DESTINATION]) ? strval($options[static::OPTION_DESTINATION]) : NULL; $dst = $dst ?: Env::get(Config::DST, $this->config->get(Config::DST, $this->config->get(Config::ROOT))); $dst = File::realpath($dst); $this->config->set(Config::DST, $dst, TRUE); diff --git a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php index 42a9a1e38..87c7e5c87 100644 --- a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php @@ -90,8 +90,7 @@ public function testInstallCommand( if ($download_should_fail) { $mock_downloader = $this->createMock(Downloader::class); - $mock_downloader->method('download') - ->willThrowException(new \RuntimeException('Failed to download Vortex.')); + $mock_downloader->method('download')->willThrowException(new \RuntimeException('Failed to download Vortex.')); $command->setDownloader($mock_downloader); } else { @@ -112,7 +111,7 @@ public function testInstallCommand( $build_command->setRunner($build_runner); $this->applicationGet()->add($build_command); - $command_inputs[InstallCommand::ARG_DESTINATION] = self::$sut; + $command_inputs['--' . InstallCommand::OPTION_DESTINATION] = self::$sut; $this->applicationRun($command_inputs, [], $expect_failure); diff --git a/.vortex/installer/tests/Functional/FunctionalTestCase.php b/.vortex/installer/tests/Functional/FunctionalTestCase.php index b81edff30..3864fd085 100644 --- a/.vortex/installer/tests/Functional/FunctionalTestCase.php +++ b/.vortex/installer/tests/Functional/FunctionalTestCase.php @@ -85,14 +85,15 @@ protected function tearDown(): void { protected function runNonInteractiveInstall(?string $dst = NULL, array $options = [], bool $expect_fail = FALSE): void { $dst ??= static::$sut; - if ($dst !== '' && $dst !== '0') { - $args[InstallCommand::ARG_DESTINATION] = $dst; - } - $defaults = [ InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_URI => File::dir(static::$root), ]; + + if ($dst !== '' && $dst !== '0') { + $defaults[InstallCommand::OPTION_DESTINATION] = $dst; + } + $options += $defaults; foreach ($options as $option => $value) { @@ -110,7 +111,7 @@ protected function runInteractiveInstall(array $answers = [], ?string $dst = NUL $this->runNonInteractiveInstall($dst, $options + [InstallCommand::OPTION_NO_INTERACTION => FALSE], $expect_fail); } - protected function assertSutContains(string|array $needles): void { + protected function assertSutContains(string | array $needles): void { $needles = is_array($needles) ? $needles : [$needles]; foreach ($needles as $needle) { @@ -127,7 +128,7 @@ protected function assertSutContains(string|array $needles): void { } } - protected function assertSutNotContains(string|array $needles): void { + protected function assertSutNotContains(string | array $needles): void { $needles = is_array($needles) ? $needles : [$needles]; foreach ($needles as $needle) { diff --git a/.vortex/installer/tests/Functional/PharTest.php b/.vortex/installer/tests/Functional/PharTest.php index f42cb7bc4..e71283d78 100644 --- a/.vortex/installer/tests/Functional/PharTest.php +++ b/.vortex/installer/tests/Functional/PharTest.php @@ -76,7 +76,7 @@ public function testPharOptionHelp(): void { $this->runInstallationWithPhar($this->pharFile, ['help' => TRUE]); $this->assertProcessSuccessful(); - $this->assertProcessOutputContains('Vortex Installer'); + $this->assertProcessOutputContains('Install Vortex from remote or local repository'); $this->assertProcessOutputNotContains('Welcome to the Vortex non-interactive installer'); $this->assertFileDoesNotExist(static::$sut . DIRECTORY_SEPARATOR . 'composer.json', 'Composer file should NOT be created when --help flag is used'); $this->assertFileExists($this->pharFile, 'PHAR file should NOT be removed when --help option is used'); @@ -103,10 +103,11 @@ protected static function buildPhar(string $dst): void { } protected function runInstallationWithPhar(string $phar_path, array $options = [], array $inputs = []): void { - $arguments = [$phar_path, static::$sut]; + $arguments = [$phar_path]; $defaults = [ - 'uri' => File::dir(static::$root), + InstallCommand::OPTION_DESTINATION => static::$sut, + InstallCommand::OPTION_URI => File::dir(static::$root), ]; $options += $defaults; diff --git a/.vortex/tests/phpunit/Traits/SutTrait.php b/.vortex/tests/phpunit/Traits/SutTrait.php index 26d613f1e..d14991aa3 100644 --- a/.vortex/tests/phpunit/Traits/SutTrait.php +++ b/.vortex/tests/phpunit/Traits/SutTrait.php @@ -77,14 +77,13 @@ protected function runInstaller(array $arguments = []): void { $this->cmd('composer --working-dir=' . static::$root . '/.vortex/installer install --no-interaction --no-progress'); } - $arguments = array_merge([ - '--no-interaction', - static::locationsSut(), - ], $arguments); + // @todo Convert options to $arguments once + // ProcessTrait::processParseCommand() is fixed. + $cmd = sprintf('php .vortex/installer/installer.php --no-interaction --destination=%s', static::locationsSut()); $this->logNote('Run the installer script'); $this->cmd( - 'php .vortex/installer/installer.php', + $cmd, arg: $arguments, env: static::$sutInstallerEnv + [ // Use a unique temporary directory for each installer run. From b0a821fddb5e50c215cf7d8ada41237084d9cfaf Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Tue, 2 Dec 2025 15:13:58 +1100 Subject: [PATCH 3/8] Updated data provider names in testHandlerProcess --- .../Functional/Handlers/AbstractHandlerProcessTestCase.php | 4 ++-- .../Handlers/AiCodeInstructionsHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/BaselineHandlerProcessTest.php | 2 +- .../Functional/Handlers/CiProviderHandlerProcessTest.php | 2 +- .../Functional/Handlers/CodeProviderHandlerProcessTest.php | 2 +- .../Handlers/DatabaseDownloadSourceHandlerProcessTest.php | 2 +- .../Handlers/DependencyUpdatesProviderHandlerProcessTest.php | 2 +- .../Functional/Handlers/DeployTypeHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/DocsHandlerProcessTest.php | 2 +- .../Handlers/HostingProjectNameHandlerProcessTest.php | 2 +- .../Functional/Handlers/HostingProviderHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/ModulesHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/NamesHandlerProcessTest.php | 2 +- .../Handlers/NotificationChannelsHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/ProfileHandlerProcessTest.php | 2 +- .../Functional/Handlers/ProvisionTypeHandlerProcessTest.php | 2 +- .../Functional/Handlers/PullRequestHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/ServicesHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/StarterHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/ThemeHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/TimezoneHandlerProcessTest.php | 2 +- .../tests/Functional/Handlers/ToolsHandlerProcessTest.php | 2 +- .../Functional/Handlers/VersionSchemeHandlerProcessTest.php | 2 +- 23 files changed, 24 insertions(+), 24 deletions(-) diff --git a/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php b/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php index 79ef2311f..7fa9c9808 100644 --- a/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php +++ b/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php @@ -36,7 +36,7 @@ protected function setUp(): void { chdir(static::$sut); } - #[DataProvider('dataProviderInstall')] + #[DataProvider('dataProviderHandlerProcess')] #[RunInSeparateProcess] public function testHandlerProcess( ?SerializableClosure $before = NULL, @@ -67,7 +67,7 @@ public function testHandlerProcess( } } - abstract public static function dataProviderInstall(): array; + abstract public static function dataProviderHandlerProcess(): array; protected function assertCommon(): void { $this->assertDirectoryEqualsDirectory(static::$root . '/scripts/vortex', static::$sut . '/scripts/vortex', 'Vortex scripts were not modified.'); diff --git a/.vortex/installer/tests/Functional/Handlers/AiCodeInstructionsHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/AiCodeInstructionsHandlerProcessTest.php index 6b3b527e1..5cde15eb2 100644 --- a/.vortex/installer/tests/Functional/Handlers/AiCodeInstructionsHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/AiCodeInstructionsHandlerProcessTest.php @@ -12,7 +12,7 @@ #[CoversClass(AiCodeInstructions::class)] class AiCodeInstructionsHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'ai instructions, claude' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(AiCodeInstructions::id()), AiCodeInstructions::CLAUDE)), diff --git a/.vortex/installer/tests/Functional/Handlers/BaselineHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/BaselineHandlerProcessTest.php index f20775b09..3dbcfd9a6 100644 --- a/.vortex/installer/tests/Functional/Handlers/BaselineHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/BaselineHandlerProcessTest.php @@ -65,7 +65,7 @@ #[CoversClass(Tui::class)] class BaselineHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ static::BASELINE_DATASET => [ NULL, diff --git a/.vortex/installer/tests/Functional/Handlers/CiProviderHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/CiProviderHandlerProcessTest.php index 286ae9bfb..c5f33072e 100644 --- a/.vortex/installer/tests/Functional/Handlers/CiProviderHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/CiProviderHandlerProcessTest.php @@ -13,7 +13,7 @@ #[CoversClass(CiProvider::class)] class CiProviderHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'ciprovider, gha' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/CodeProviderHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/CodeProviderHandlerProcessTest.php index fc61f6864..fdd088570 100644 --- a/.vortex/installer/tests/Functional/Handlers/CodeProviderHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/CodeProviderHandlerProcessTest.php @@ -13,7 +13,7 @@ #[CoversClass(CodeProvider::class)] class CodeProviderHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'code provider, github' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(CodeProvider::id()), CodeProvider::GITHUB)), diff --git a/.vortex/installer/tests/Functional/Handlers/DatabaseDownloadSourceHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/DatabaseDownloadSourceHandlerProcessTest.php index cda90c9e7..0a3e980b2 100644 --- a/.vortex/installer/tests/Functional/Handlers/DatabaseDownloadSourceHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/DatabaseDownloadSourceHandlerProcessTest.php @@ -15,7 +15,7 @@ #[CoversClass(DatabaseImage::class)] class DatabaseDownloadSourceHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'db download source, url' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(DatabaseDownloadSource::id()), DatabaseDownloadSource::URL)), diff --git a/.vortex/installer/tests/Functional/Handlers/DependencyUpdatesProviderHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/DependencyUpdatesProviderHandlerProcessTest.php index 35e77f19c..e23549692 100644 --- a/.vortex/installer/tests/Functional/Handlers/DependencyUpdatesProviderHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/DependencyUpdatesProviderHandlerProcessTest.php @@ -13,7 +13,7 @@ #[CoversClass(DependencyUpdatesProvider::class)] class DependencyUpdatesProviderHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'deps updates provider, ci, gha' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(DependencyUpdatesProvider::id()), DependencyUpdatesProvider::RENOVATEBOT_CI)), diff --git a/.vortex/installer/tests/Functional/Handlers/DeployTypeHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/DeployTypeHandlerProcessTest.php index de7639e10..de82cb18e 100644 --- a/.vortex/installer/tests/Functional/Handlers/DeployTypeHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/DeployTypeHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(DeployTypes::class)] class DeployTypeHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'deploy types, artifact' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(DeployTypes::id()), Converter::toList([DeployTypes::ARTIFACT], ',', TRUE))), diff --git a/.vortex/installer/tests/Functional/Handlers/DocsHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/DocsHandlerProcessTest.php index 189b66975..c7b9ce8ef 100644 --- a/.vortex/installer/tests/Functional/Handlers/DocsHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/DocsHandlerProcessTest.php @@ -12,7 +12,7 @@ #[CoversClass(PreserveDocsProject::class)] class DocsHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'preserve docs project, enabled' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(PreserveDocsProject::id()), Env::TRUE)), diff --git a/.vortex/installer/tests/Functional/Handlers/HostingProjectNameHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/HostingProjectNameHandlerProcessTest.php index c04919ff0..d227969d7 100644 --- a/.vortex/installer/tests/Functional/Handlers/HostingProjectNameHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/HostingProjectNameHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(HostingProjectName::class)] class HostingProjectNameHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'hosting project name - acquia' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/HostingProviderHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/HostingProviderHandlerProcessTest.php index 2b26aaf01..14d332218 100644 --- a/.vortex/installer/tests/Functional/Handlers/HostingProviderHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/HostingProviderHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(HostingProvider::class)] class HostingProviderHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'hosting, acquia' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/ModulesHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/ModulesHandlerProcessTest.php index 8267e12ce..33cefdf25 100644 --- a/.vortex/installer/tests/Functional/Handlers/ModulesHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/ModulesHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(Modules::class)] class ModulesHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'modules, no admin_toolbar' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/NamesHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/NamesHandlerProcessTest.php index b2a857c23..50ab13c3b 100644 --- a/.vortex/installer/tests/Functional/Handlers/NamesHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/NamesHandlerProcessTest.php @@ -18,7 +18,7 @@ #[CoversClass(Name::class)] class NamesHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'names' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/NotificationChannelsHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/NotificationChannelsHandlerProcessTest.php index dee3580e7..23076b1ff 100644 --- a/.vortex/installer/tests/Functional/Handlers/NotificationChannelsHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/NotificationChannelsHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(NotificationChannels::class)] class NotificationChannelsHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'notification_channels, all' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/ProfileHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/ProfileHandlerProcessTest.php index 954df2341..343133511 100644 --- a/.vortex/installer/tests/Functional/Handlers/ProfileHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/ProfileHandlerProcessTest.php @@ -12,7 +12,7 @@ #[CoversClass(Profile::class)] class ProfileHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'profile, minimal' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(Profile::id()), Profile::MINIMAL)), diff --git a/.vortex/installer/tests/Functional/Handlers/ProvisionTypeHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/ProvisionTypeHandlerProcessTest.php index 544a69044..be9bb4419 100644 --- a/.vortex/installer/tests/Functional/Handlers/ProvisionTypeHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/ProvisionTypeHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(ProvisionType::class)] class ProvisionTypeHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'provision, database' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(ProvisionType::id()), ProvisionType::DATABASE)), diff --git a/.vortex/installer/tests/Functional/Handlers/PullRequestHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/PullRequestHandlerProcessTest.php index d9a17c99d..6a887ff62 100644 --- a/.vortex/installer/tests/Functional/Handlers/PullRequestHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/PullRequestHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(LabelMergeConflictsPr::class)] class PullRequestHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'assign author PR, enabled' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(AssignAuthorPr::id()), Env::TRUE)), diff --git a/.vortex/installer/tests/Functional/Handlers/ServicesHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/ServicesHandlerProcessTest.php index 28ffb16c8..5682ebf7c 100644 --- a/.vortex/installer/tests/Functional/Handlers/ServicesHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/ServicesHandlerProcessTest.php @@ -15,7 +15,7 @@ #[CoversClass(Services::class)] class ServicesHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'services, no clamav' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/StarterHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/StarterHandlerProcessTest.php index c36881ae7..8c6b2400b 100644 --- a/.vortex/installer/tests/Functional/Handlers/StarterHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/StarterHandlerProcessTest.php @@ -13,7 +13,7 @@ #[CoversClass(Starter::class)] class StarterHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'starter, demo db' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(Starter::id()), Starter::LOAD_DATABASE_DEMO)), diff --git a/.vortex/installer/tests/Functional/Handlers/ThemeHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/ThemeHandlerProcessTest.php index 6b9c1a9c5..96d9a83af 100644 --- a/.vortex/installer/tests/Functional/Handlers/ThemeHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/ThemeHandlerProcessTest.php @@ -13,7 +13,7 @@ #[CoversClass(Theme::class)] class ThemeHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'theme, olivero' => [ static::cw(fn() => Env::put(PromptManager::makeEnvName(Theme::id()), Theme::OLIVERO)), diff --git a/.vortex/installer/tests/Functional/Handlers/TimezoneHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/TimezoneHandlerProcessTest.php index f7e5f23c9..ae9ab473a 100644 --- a/.vortex/installer/tests/Functional/Handlers/TimezoneHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/TimezoneHandlerProcessTest.php @@ -14,7 +14,7 @@ #[CoversClass(Timezone::class)] class TimezoneHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'timezone, gha' => [ diff --git a/.vortex/installer/tests/Functional/Handlers/ToolsHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/ToolsHandlerProcessTest.php index b5a6945e0..3942f83c8 100644 --- a/.vortex/installer/tests/Functional/Handlers/ToolsHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/ToolsHandlerProcessTest.php @@ -15,7 +15,7 @@ #[CoversClass(Tools::class)] class ToolsHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'tools, none' => [ static::cw(function (): void { diff --git a/.vortex/installer/tests/Functional/Handlers/VersionSchemeHandlerProcessTest.php b/.vortex/installer/tests/Functional/Handlers/VersionSchemeHandlerProcessTest.php index 750f2b1c7..24bc09638 100644 --- a/.vortex/installer/tests/Functional/Handlers/VersionSchemeHandlerProcessTest.php +++ b/.vortex/installer/tests/Functional/Handlers/VersionSchemeHandlerProcessTest.php @@ -13,7 +13,7 @@ #[CoversClass(VersionScheme::class)] class VersionSchemeHandlerProcessTest extends AbstractHandlerProcessTestCase { - public static function dataProviderInstall(): array { + public static function dataProviderHandlerProcess(): array { return [ 'version scheme, calver' => [ static::cw(function (): void { From 12afa8292fc5026a9015720519e5aa926f5864d0 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Tue, 2 Dec 2025 15:16:13 +1100 Subject: [PATCH 4/8] Moved to traits for runners and commands. --- .vortex/installer/phpunit.xml | 2 +- .../installer/src/Command/BuildCommand.php | 57 +-- .../src/Command/CheckRequirementsCommand.php | 63 ++- .../installer/src/Command/InstallCommand.php | 120 ++---- .../Runner/CommandRunnerAwareInterface.php | 28 ++ .../src/Runner/CommandRunnerAwareTrait.php | 43 ++ .../Runner/ExecutableFinderAwareInterface.php | 30 ++ .../src/Runner/ExecutableFinderAwareTrait.php | 46 +++ .../installer/src/Runner/ProcessRunner.php | 13 +- .../Runner/ProcessRunnerAwareInterface.php | 28 ++ .../src/Runner/ProcessRunnerAwareTrait.php | 41 ++ .../Functional/Command/BuildCommandTest.php | 81 ++-- .../Command/CheckRequirementsCommandTest.php | 73 ++-- .../Functional/Command/InstallCommandTest.php | 107 ++--- .../tests/Functional/FunctionalTestCase.php | 5 +- .vortex/installer/tests/Helpers/TuiOutput.php | 8 +- .../tests/Unit/Runner/AbstractRunnerTest.php | 372 +++++++++++------- .../tests/Unit/Runner/ProcessRunnerTest.php | 14 + 18 files changed, 692 insertions(+), 439 deletions(-) create mode 100644 .vortex/installer/src/Runner/CommandRunnerAwareInterface.php create mode 100644 .vortex/installer/src/Runner/CommandRunnerAwareTrait.php create mode 100644 .vortex/installer/src/Runner/ExecutableFinderAwareInterface.php create mode 100644 .vortex/installer/src/Runner/ExecutableFinderAwareTrait.php create mode 100644 .vortex/installer/src/Runner/ProcessRunnerAwareInterface.php create mode 100644 .vortex/installer/src/Runner/ProcessRunnerAwareTrait.php diff --git a/.vortex/installer/phpunit.xml b/.vortex/installer/phpunit.xml index b621a7294..0757f711e 100644 --- a/.vortex/installer/phpunit.xml +++ b/.vortex/installer/phpunit.xml @@ -1,5 +1,5 @@ -getApplication()))->disableLog(); - $runner->run('check-requirements', [], ['--no-summary' => '1']); + $command_runner = $this->getCommandRunner()->disableLog(); + $command_runner->run('check-requirements', [], ['--no-summary' => '1']); - return $runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; + return $command_runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; }, failure: 'Missing requirements. Run: ./installer.php check-requirements', streaming: TRUE, @@ -92,10 +92,10 @@ protected function execute(InputInterface $input, OutputInterface $output): int $env['VORTEX_PROVISION_TYPE'] = 'profile'; } - $this->runner = $this->getRunner()->setCwd($cwd); - $this->runner->run('ahoy build', env: $env); + $this->processRunner = $this->getProcessRunner()->setCwd($cwd); + $this->processRunner->run('ahoy build', env: $env); - return $this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; + return $this->processRunner->getExitCode() === RunnerInterface::EXIT_SUCCESS; }, success: fn(bool $result): string => $result ? 'Build completed' : 'Build failed', failure: 'Build failed', @@ -139,7 +139,7 @@ protected function showSuccessSummary(): void { $output .= 'Login: ahoy login' . PHP_EOL; $output .= PHP_EOL; - $log_path = $this->runner->getLogger()->getPath(); + $log_path = $this->processRunner->getLogger()->getPath(); if ($log_path) { $output .= 'Log file: ' . $log_path . PHP_EOL; $output .= PHP_EOL; @@ -160,15 +160,15 @@ protected function showSuccessSummary(): void { protected function showFailureSummary(): void { Tui::line(''); - $command = $this->runner->getCommand(); + $command = $this->processRunner->getCommand(); if ($command) { Tui::line('Failed at: ' . $command); } - $exit_code = $this->runner->getExitCode(); + $exit_code = $this->processRunner->getExitCode(); Tui::line('Exit code: ' . $exit_code); - $log_path = $this->runner->getLogger()->getPath(); + $log_path = $this->processRunner->getLogger()->getPath(); if ($log_path) { Tui::line('Log file: ' . $log_path); } @@ -176,7 +176,7 @@ protected function showFailureSummary(): void { Tui::line(''); // Show last 10 lines of output for context. - $runner_output = $this->runner->getOutput(as_array: TRUE); + $runner_output = $this->processRunner->getOutput(as_array: TRUE); if (!is_array($runner_output)) { throw new \RuntimeException('Runner output is not an array.'); @@ -191,23 +191,4 @@ protected function showFailureSummary(): void { } } - /** - * Get the process runner. - * - * Factory method that returns existing runner or creates new one. - */ - protected function getRunner(): ProcessRunner { - // Return already-set runner if available (for testing). - return $this->runner ?? (new ProcessRunner()); - } - - /** - * Set the process runner. - * - * Allows dependency injection for testing. - */ - public function setRunner(ProcessRunner $runner): void { - $this->runner = $runner; - } - } diff --git a/.vortex/installer/src/Command/CheckRequirementsCommand.php b/.vortex/installer/src/Command/CheckRequirementsCommand.php index e382567e1..c9a0ca3c0 100644 --- a/.vortex/installer/src/Command/CheckRequirementsCommand.php +++ b/.vortex/installer/src/Command/CheckRequirementsCommand.php @@ -4,7 +4,11 @@ namespace DrevOps\VortexInstaller\Command; +use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareInterface; +use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareTrait; use DrevOps\VortexInstaller\Runner\ProcessRunner; +use DrevOps\VortexInstaller\Runner\ProcessRunnerAwareInterface; +use DrevOps\VortexInstaller\Runner\ProcessRunnerAwareTrait; use DrevOps\VortexInstaller\Runner\RunnerInterface; use DrevOps\VortexInstaller\Task\Task; use DrevOps\VortexInstaller\Utils\Tui; @@ -16,7 +20,10 @@ /** * Check requirements command. */ -class CheckRequirementsCommand extends Command { +class CheckRequirementsCommand extends Command implements ProcessRunnerAwareInterface, ExecutableFinderAwareInterface { + + use ProcessRunnerAwareTrait; + use ExecutableFinderAwareTrait; const string OPTION_ONLY = 'only'; @@ -44,11 +51,6 @@ class CheckRequirementsCommand extends Command { */ public static $defaultName = 'check-requirements'; - /** - * The process runner. - */ - protected ProcessRunner $runner; - /** * Present tools. * @@ -83,7 +85,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $only = $input->getOption(static::OPTION_ONLY); $requirements = $this->validateRequirements($only ? array_map(trim(...), explode(',', (string) $only)) : NULL); - $this->runner = $this->getRunner(); + $this->processRunner ??= $this->getProcessRunner(); $this->present = []; $this->missing = []; @@ -146,7 +148,7 @@ protected function validateRequirements(?array $only): array { if ($only !== NULL) { $unknown = array_diff($only, static::REQUIREMENTS); if (!empty($unknown)) { - throw new \InvalidArgumentException(sprintf('Unknown requirements: %s. Available: %s.', implode(', ', $unknown), implode(', ', static::REQUIREMENTS))); + throw new \InvalidArgumentException(sprintf("Unknown requirements: %s.\nAvailable: %s.", implode(', ', $unknown), implode(', ', static::REQUIREMENTS))); } } @@ -274,20 +276,21 @@ protected function checkPygmy(): bool { $version = $this->getCommandVersion('pygmy version'); - $this->runner->run('pygmy status'); - if ($this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { + $this->processRunner->run('pygmy status'); + if ($this->processRunner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { $this->present['Pygmy'] = $version; return TRUE; } - $this->runner->run('docker ps --format "{{.Names}}" | grep -q amazeeio'); + $this->processRunner->run('docker ps --format "{{.Names}}" | grep -q amazeeio'); // @phpstan-ignore-next-line notIdentical.alwaysFalse - if ($this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { + if ($this->processRunner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { $this->present['Pygmy'] = $version; return TRUE; } $this->missing['Pygmy'] = 'Run: pygmy up'; + return FALSE; } @@ -295,16 +298,15 @@ protected function checkPygmy(): bool { * Check if a command exists. */ protected function commandExists(string $command): bool { - $this->runner->run(sprintf('command -v %s', escapeshellarg($command))); - return $this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; + return $this->getExecutableFinder()->find($command) !== NULL; } /** * Check if Docker Compose exists. */ protected function dockerComposeExists(): bool { - $this->runner->run('docker compose version'); - if ($this->runner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { + $this->processRunner->run('docker compose version'); + if ($this->processRunner->getExitCode() === RunnerInterface::EXIT_SUCCESS) { return TRUE; } @@ -320,36 +322,17 @@ protected function dockerComposeExists(): bool { * Number of lines to retrieve from the output. Defaults to 1. */ protected function getCommandVersion(string $command, int $lines = 1): string { - $this->runner->run($command); - $raw_output = $this->runner->getOutput(FALSE, $lines); + $this->processRunner->run($command); + $raw_output = $this->processRunner->getOutput(FALSE, $lines); $output = trim(is_string($raw_output) ? $raw_output : implode(PHP_EOL, $raw_output)); return empty($output) ? 'Available' : $output; } /** - * Get the process runner instance. - * - * Factory method to create the runner, allowing tests to override this - * to inject mocks via setRunner(). - * - * @return \DrevOps\VortexInstaller\Runner\ProcessRunner - * The process runner instance. - */ - protected function getRunner(): ProcessRunner { - // Return already-set runner if available (for testing). - return $this->runner ?? (new ProcessRunner())->disableLog()->disableStreaming(); - } - - /** - * Set the process runner instance. - * - * Allows dependency injection for testing. - * - * @param \DrevOps\VortexInstaller\Runner\ProcessRunner $runner - * The process runner instance. + * {@inheritdoc} */ - public function setRunner(ProcessRunner $runner): void { - $this->runner = $runner; + public function getProcessRunner(): ProcessRunner { + return $this->processRunner ?? (new ProcessRunner())->disableLog()->disableStreaming(); } } diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index 9185c3dae..1ab9684e5 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -7,9 +7,10 @@ use DrevOps\VortexInstaller\Downloader\Downloader; use DrevOps\VortexInstaller\Prompts\Handlers\Starter; use DrevOps\VortexInstaller\Prompts\PromptManager; -use DrevOps\VortexInstaller\Runner\CommandRunner; -use DrevOps\VortexInstaller\Runner\ProcessRunner; -use DrevOps\VortexInstaller\Runner\RunnerInterface; +use DrevOps\VortexInstaller\Runner\CommandRunnerAwareInterface; +use DrevOps\VortexInstaller\Runner\CommandRunnerAwareTrait; +use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareInterface; +use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareTrait; use DrevOps\VortexInstaller\Task\Task; use DrevOps\VortexInstaller\Utils\Config; use DrevOps\VortexInstaller\Utils\Env; @@ -28,7 +29,10 @@ * * @package DrevOps\VortexInstaller\Command */ -class InstallCommand extends Command { +class InstallCommand extends Command implements CommandRunnerAwareInterface, ExecutableFinderAwareInterface { + + use CommandRunnerAwareTrait; + use ExecutableFinderAwareTrait; const OPTION_DESTINATION = 'destination'; @@ -63,16 +67,6 @@ class InstallCommand extends Command { */ protected PromptManager $promptManager; - /** - * The command runner. - */ - protected ?CommandRunner $runner = NULL; - - /** - * The process runner. - */ - protected ?ProcessRunner $processRunner = NULL; - /** * The downloader. */ @@ -174,7 +168,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int Task::action( label: 'Preparing demo content', - action: fn(): string|array => $this->prepareDemo(), + action: fn(): string | array => $this->prepareDemo(), success: 'Demo content prepared', ); } @@ -223,29 +217,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int } protected function checkRequirements(): void { - $runner = $this->getProcessRunner(); - - $runner->run('command -v git >/dev/null'); - if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { - throw new \RuntimeException('Missing git.'); - } - - $runner->run('command -v curl >/dev/null'); - // @phpstan-ignore-next-line notIdentical.alwaysFalse - if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { - throw new \RuntimeException('Missing curl.'); - } - - $runner->run('command -v tar >/dev/null'); - // @phpstan-ignore-next-line notIdentical.alwaysFalse - if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { - throw new \RuntimeException('Missing tar.'); - } + $required_commands = [ + 'git', + 'curl', + 'tar', + 'composer', + ]; - $runner->run('command -v composer >/dev/null'); - // @phpstan-ignore-next-line notIdentical.alwaysFalse - if ($runner->getExitCode() !== RunnerInterface::EXIT_SUCCESS) { - throw new \RuntimeException('Missing Composer.'); + foreach ($required_commands as $required_command) { + if ($this->getExecutableFinder()->find($required_command) === NULL) { + throw new \RuntimeException(sprintf('Missing required command: %s.', $required_command)); + } } } @@ -283,14 +265,20 @@ protected function resolveOptions(array $arguments, array $options): void { } // Set destination directory. - $dst = !empty($options[static::OPTION_DESTINATION]) && is_scalar($options[static::OPTION_DESTINATION]) ? strval($options[static::OPTION_DESTINATION]) : NULL; - $dst = $dst ?: Env::get(Config::DST, $this->config->get(Config::DST, $this->config->get(Config::ROOT))); + $dst_from_option = !empty($options[static::OPTION_DESTINATION]) && is_scalar($options[static::OPTION_DESTINATION]) ? strval($options[static::OPTION_DESTINATION]) : NULL; + $dst_from_env = Env::get(Config::DST); + $dst_from_config = $this->config->get(Config::DST); + $dst_from_root = $this->config->get(Config::ROOT); + + $dst = $dst_from_option ?: ($dst_from_env ?: ($dst_from_config ?: $dst_from_root)); $dst = File::realpath($dst); $this->config->set(Config::DST, $dst, TRUE); // Load values from the destination .env file, if it exists. - if (File::exists($this->config->getDst() . '/.env')) { - Env::putFromDotenv($this->config->getDst() . '/.env'); + $dest_env_file = $this->config->getDst() . '/.env'; + + if (File::exists($dest_env_file)) { + Env::putFromDotenv($dest_env_file); } [$repo, $ref] = Downloader::parseUri($options[static::OPTION_URI] ?: 'https://github.com/drevops/vortex.git@stable'); @@ -392,7 +380,7 @@ protected function copyFiles(): void { * @return array|string * Array of messages or a single message. */ - protected function prepareDemo(): array|string { + protected function prepareDemo(): array | string { if (empty($this->config->get(Config::IS_DEMO))) { return 'Not a demo mode.'; } @@ -447,7 +435,7 @@ protected function runBuildCommand(OutputInterface $output): bool { $starter = $responses[Starter::id()] ?? Starter::LOAD_DATABASE_DEMO; $is_profile = in_array($starter, [Starter::INSTALL_PROFILE_CORE, Starter::INSTALL_PROFILE_DRUPALCMS], TRUE); - $runner = $this->getRunner(); + $runner = $this->getCommandRunner(); $runner->run('build', args: $is_profile ? ['--profile' => '1'] : [], output: $output); return $runner->getExitCode() === Command::SUCCESS; @@ -633,52 +621,6 @@ public function cleanup(): void { } } - /** - * Get the command runner. - * - * Provides a default CommandRunner instance or returns the injected one. - * This allows tests to inject mocks via setRunner(). - * - * @return \DrevOps\VortexInstaller\Runner\CommandRunner - * The command runner. - */ - protected function getRunner(): CommandRunner { - return $this->runner ?? new CommandRunner($this->getApplication()); - } - - /** - * Set the command runner. - * - * @param \DrevOps\VortexInstaller\Runner\CommandRunner $runner - * The command runner. - */ - public function setRunner(CommandRunner $runner): void { - $this->runner = $runner; - } - - /** - * Get the process runner. - * - * Provides a default ProcessRunner instance or returns the injected one. - * This allows tests to inject mocks via setProcessRunner(). - * - * @return \DrevOps\VortexInstaller\Runner\ProcessRunner - * The process runner. - */ - protected function getProcessRunner(): ProcessRunner { - return $this->processRunner ?? (new ProcessRunner())->disableLog()->disableStreaming(); - } - - /** - * Set the process runner. - * - * @param \DrevOps\VortexInstaller\Runner\ProcessRunner $runner - * The process runner. - */ - public function setProcessRunner(ProcessRunner $runner): void { - $this->processRunner = $runner; - } - /** * Get the downloader. * diff --git a/.vortex/installer/src/Runner/CommandRunnerAwareInterface.php b/.vortex/installer/src/Runner/CommandRunnerAwareInterface.php new file mode 100644 index 000000000..589d40b2b --- /dev/null +++ b/.vortex/installer/src/Runner/CommandRunnerAwareInterface.php @@ -0,0 +1,28 @@ +commandRunner ?? new CommandRunner($this->getApplication()); + } + + /** + * Set the command runner. + * + * Allows dependency injection for testing. + * + * @param \DrevOps\VortexInstaller\Runner\CommandRunner $runner + * The command runner instance. + */ + public function setCommandRunner(CommandRunner $runner): void { + $this->commandRunner = $runner; + } + +} diff --git a/.vortex/installer/src/Runner/ExecutableFinderAwareInterface.php b/.vortex/installer/src/Runner/ExecutableFinderAwareInterface.php new file mode 100644 index 000000000..fb6f1f5af --- /dev/null +++ b/.vortex/installer/src/Runner/ExecutableFinderAwareInterface.php @@ -0,0 +1,30 @@ +executableFinder === NULL) { + $this->executableFinder = new ExecutableFinder(); + } + return $this->executableFinder; + } + + /** + * Set the executable finder. + * + * Allows dependency injection for testing. + * + * @param \Symfony\Component\Process\ExecutableFinder $finder + * The executable finder instance. + */ + public function setExecutableFinder(ExecutableFinder $finder): void { + $this->executableFinder = $finder; + } + +} diff --git a/.vortex/installer/src/Runner/ProcessRunner.php b/.vortex/installer/src/Runner/ProcessRunner.php index 32f907cd9..0bc2659ae 100644 --- a/.vortex/installer/src/Runner/ProcessRunner.php +++ b/.vortex/installer/src/Runner/ProcessRunner.php @@ -6,13 +6,14 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Process\ExecutableFinder; use Symfony\Component\Process\Process; /** * Runner for shell commands via Symfony Process. */ -class ProcessRunner extends AbstractRunner { +class ProcessRunner extends AbstractRunner implements ExecutableFinderAwareInterface { + + use ExecutableFinderAwareTrait; /** * {@inheritdoc} @@ -90,6 +91,11 @@ protected function resolveCommand(string $command): array { $parsed = $this->parseCommand($command); $base_command = array_shift($parsed); + // Defensive check: prevent using 'command' utility. + if ($base_command === 'command') { + throw new \InvalidArgumentException('Using the "command" utility is not allowed. Use Symfony\Component\Process\ExecutableFinder to check if a command exists instead.'); + } + // Validate the base command contains only allowed characters. if (preg_match('/[^a-zA-Z0-9_\-.\/]/', (string) $base_command)) { throw new \InvalidArgumentException(sprintf('Invalid command: %s. Only alphanumeric characters, dots, dashes, underscores and slashes are allowed.', $base_command)); @@ -108,8 +114,7 @@ protected function resolveCommand(string $command): array { } else { // Use ExecutableFinder for commands without path. - $finder = new ExecutableFinder(); - $resolved = $finder->find($base_command); + $resolved = $this->getExecutableFinder()->find($base_command); if ($resolved === NULL) { throw new \InvalidArgumentException(sprintf('Command not found: %s. Ensure the command is installed and available in PATH.', $base_command)); diff --git a/.vortex/installer/src/Runner/ProcessRunnerAwareInterface.php b/.vortex/installer/src/Runner/ProcessRunnerAwareInterface.php new file mode 100644 index 000000000..ea114b965 --- /dev/null +++ b/.vortex/installer/src/Runner/ProcessRunnerAwareInterface.php @@ -0,0 +1,28 @@ +processRunner ?? new ProcessRunner(); + } + + /** + * Set the process runner. + * + * Allows dependency injection for testing. + * + * @param \DrevOps\VortexInstaller\Runner\ProcessRunner $runner + * The process runner instance. + */ + public function setProcessRunner(ProcessRunner $runner): void { + $this->processRunner = $runner; + } + +} diff --git a/.vortex/installer/tests/Functional/Command/BuildCommandTest.php b/.vortex/installer/tests/Functional/Command/BuildCommandTest.php index f81faf4b7..89ad21583 100644 --- a/.vortex/installer/tests/Functional/Command/BuildCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/BuildCommandTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Process\ExecutableFinder; /** * Functional tests for BuildCommand. @@ -31,24 +32,25 @@ public function testBuildCommand( bool $expect_failure, array $output_assertions, ?\Closure $requirements_exit_callback = NULL, + ?\Closure $requirements_finder_callback = NULL, ): void { // Create a mock ProcessRunner for ahoy build. - $mock_runner = $this->createMock(ProcessRunner::class); + $build_process_runner = $this->createMock(ProcessRunner::class); // Set up common default behaviors. $current_command = ''; - $mock_runner->method('run') - ->willReturnCallback(function (string $command) use ($mock_runner, &$current_command): MockObject { + $build_process_runner->method('run') + ->willReturnCallback(function (string $command) use ($build_process_runner, &$current_command): MockObject { $current_command = $command; - return $mock_runner; + return $build_process_runner; }); // Mock getOutput() to handle both string and array returns. - $mock_runner->method('getOutput')->willReturnCallback(fn(bool $as_array = FALSE): array|string => $as_array ? ['Mock build output line 1', 'Mock build output line 2'] : 'Mock build output'); - $mock_runner->method('getCommand')->willReturn('ahoy build'); + $build_process_runner->method('getOutput')->willReturnCallback(fn(bool $as_array = FALSE): array|string => $as_array ? ['Mock build output line 1', 'Mock build output line 2'] : 'Mock build output'); + $build_process_runner->method('getCommand')->willReturn('ahoy build'); // Set up getExitCode using the provided callback. - $mock_runner->method('getExitCode') + $build_process_runner->method('getExitCode') ->willReturnCallback(function () use ($exit_code_callback, &$current_command) { return $exit_code_callback($current_command); }); @@ -56,20 +58,30 @@ public function testBuildCommand( // Mock logger to prevent errors in showSuccessSummary/showFailureSummary. $mock_logger = $this->createMock(FileLoggerInterface::class); $mock_logger->method('getPath')->willReturn('/tmp/mock.log'); - $mock_runner->method('getLogger')->willReturn($mock_logger); + $build_process_runner->method('getLogger')->willReturn($mock_logger); // Mock setCwd to return runner for method chaining. - $mock_runner->method('setCwd')->willReturn($mock_runner); + $build_process_runner->method('setCwd')->willReturn($build_process_runner); - // Create command and inject mock runner using setRunner(). - $command = new BuildCommand(); - $command->setRunner($mock_runner); + // Always register CheckRequirementsCommand with mocked runner and finder. + // Mock ExecutableFinder. + $requirements_finder = $this->createMock(ExecutableFinder::class); + $final_finder_callback = $requirements_finder_callback ?? fn(string $name): string => '/usr/bin/' . $name; + $requirements_finder->method('find') + ->willReturnCallback(fn(string $name) => $final_finder_callback($name)); + + // Mock ExecutableFinder for BuildCommand's ProcessRunner. + $build_process_runner->method('getExecutableFinder')->willReturn($requirements_finder); + + // Create command and inject mock runner using setProcessRunner(). + $build_command = new BuildCommand(); + $build_command->setProcessRunner($build_process_runner); // Initialize application with our command. - static::applicationInitFromCommand($command); + static::applicationInitFromCommand($build_command); - // Always register CheckRequirementsCommand with mocked runner. - // Use provided callback or default to success (exit code 0). + // Mock ProcessRunner - use provided callback or default to + // success (exit code 0). $requirements_runner = $this->createMock(ProcessRunner::class); $current_requirements_command = ''; @@ -88,8 +100,12 @@ public function testBuildCommand( return $final_requirements_callback($current_requirements_command); }); + // Mock ExecutableFinder for CheckRequirementsCommand's ProcessRunner. + $requirements_runner->method('getExecutableFinder')->willReturn($requirements_finder); + $check_command = new CheckRequirementsCommand(); - $check_command->setRunner($requirements_runner); + $check_command->setExecutableFinder($requirements_finder); + $check_command->setProcessRunner($requirements_runner); $this->applicationGet()->add($check_command); // Run build with provided inputs. @@ -109,7 +125,8 @@ public function testBuildCommand( * command_inputs: array, * expect_failure: bool, * output_assertions: array, - * requirements_exit_callback?: ?\Closure + * requirements_exit_callback?: ?\Closure, + * requirements_finder_callback?: ?\Closure * }> */ public static function dataProviderBuildCommand(): array { @@ -117,7 +134,7 @@ public static function dataProviderBuildCommand(): array { // ----------------------------------------------------------------------- // Requirements check scenarios. // ----------------------------------------------------------------------- - 'Build runs requirements check by default (success)' => [ + 'Build runs requirements check by default, success' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => [], 'expect_failure' => FALSE, @@ -134,7 +151,7 @@ public static function dataProviderBuildCommand(): array { 'requirements_exit_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, ], - 'Requirements check fails - one missing (Docker)' => [ + 'Requirements check fails - one missing, Docker' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => [], 'expect_failure' => TRUE, @@ -148,15 +165,8 @@ public static function dataProviderBuildCommand(): array { TuiOutput::BUILD_BUILD_COMPLETED, ]), ), - 'requirements_exit_callback' => function (string $current_command): int { - // Docker command fails, others succeed. - if ((str_contains($current_command, "command -v 'docker'") || str_contains($current_command, 'command -v docker')) - && !str_contains($current_command, 'compose') - && !str_contains($current_command, 'docker --version')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; - } - return RunnerInterface::EXIT_SUCCESS; - }, + 'requirements_exit_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, + 'requirements_finder_callback' => fn(string $name): ?string => $name === 'docker' ? NULL : '/usr/bin/' . $name, ], 'Requirements check fails - all missing' => [ @@ -174,12 +184,13 @@ public static function dataProviderBuildCommand(): array { ]), ), 'requirements_exit_callback' => fn(string $current_command): int => RunnerInterface::EXIT_COMMAND_NOT_FOUND, + 'requirements_finder_callback' => fn(string $name): ?string => NULL, ], // ----------------------------------------------------------------------- // Basic build scenarios. // ----------------------------------------------------------------------- - 'Build with skip requirements check (success)' => [ + 'Build with skip requirements check, success' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--skip-requirements-check' => TRUE], 'expect_failure' => FALSE, @@ -199,7 +210,7 @@ public static function dataProviderBuildCommand(): array { // ----------------------------------------------------------------------- // Profile flag scenarios. // ----------------------------------------------------------------------- - 'Build with profile flag and skip requirements (success)' => [ + 'Build with profile flag and skip requirements, success' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => [ '--profile' => TRUE, @@ -219,7 +230,7 @@ public static function dataProviderBuildCommand(): array { ], - 'Build with profile flag and requirements check (success)' => [ + 'Build with profile flag and requirements check, success' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--profile' => TRUE], 'expect_failure' => FALSE, @@ -252,7 +263,7 @@ public static function dataProviderBuildCommand(): array { // ----------------------------------------------------------------------- // Build failure scenarios. // ----------------------------------------------------------------------- - 'Build failure (ahoy build fails, exit code 1)' => [ + 'Build failure, ahoy build fails, exit code 1' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_FAILURE, 'command_inputs' => ['--skip-requirements-check' => TRUE], 'expect_failure' => TRUE, @@ -269,7 +280,7 @@ public static function dataProviderBuildCommand(): array { ], - 'Build failure with profile (ahoy build fails, exit code 1)' => [ + 'Build failure with profile, ahoy build fails, exit code 1' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_FAILURE, 'command_inputs' => [ '--profile' => TRUE, @@ -290,7 +301,7 @@ public static function dataProviderBuildCommand(): array { ], - 'Build failure (ahoy build fails, exit code 2)' => [ + 'Build failure, ahoy build fails, exit code 2' => [ 'exit_code_callback' => fn(string $current_command): int => 2, 'command_inputs' => ['--skip-requirements-check' => TRUE], 'expect_failure' => TRUE, @@ -307,7 +318,7 @@ public static function dataProviderBuildCommand(): array { ], - 'Build failure (ahoy build fails, exit code 127)' => [ + 'Build failure, ahoy build fails, exit code 127' => [ 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_COMMAND_NOT_FOUND, 'command_inputs' => ['--skip-requirements-check' => TRUE], 'expect_failure' => TRUE, diff --git a/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php b/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php index 57d2f5ae8..afc750340 100644 --- a/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php @@ -12,6 +12,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Process\ExecutableFinder; /** * Functional tests for CheckRequirementsCommand. @@ -24,11 +25,17 @@ class CheckRequirementsCommandTest extends FunctionalTestCase { */ #[DataProvider('dataProviderCheckRequirementsCommand')] public function testCheckRequirementsCommand( + \Closure $executable_finder_callback, \Closure $exit_code_callback, array $command_inputs, bool $expect_failure, array $output_assertions, ): void { + // Create a mock ExecutableFinder. + $mock_finder = $this->createMock(ExecutableFinder::class); + $mock_finder->method('find') + ->willReturnCallback(fn(string $name) => $executable_finder_callback($name)); + // Create a mock ProcessRunner. $mock_runner = $this->createMock(ProcessRunner::class); @@ -48,9 +55,10 @@ public function testCheckRequirementsCommand( return $exit_code_callback($current_command); }); - // Create command and inject mock runner using setRunner(). + // Create command and inject mocks using setters. $command = new CheckRequirementsCommand(); - $command->setRunner($mock_runner); + $command->setExecutableFinder($mock_finder); + $command->setProcessRunner($mock_runner); // Initialize application with our command. static::applicationInitFromCommand($command); @@ -67,6 +75,7 @@ public function testCheckRequirementsCommand( * Data provider for testCheckWithMockedRunner. * * @return array, * expect_failure: bool, @@ -76,6 +85,7 @@ public function testCheckRequirementsCommand( public static function dataProviderCheckRequirementsCommand(): array { return [ 'Check all requirements' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => [], 'expect_failure' => FALSE, @@ -94,6 +104,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'All requirements missing' => [ + 'executable_finder_callback' => fn(string $name): ?string => NULL, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_COMMAND_NOT_FOUND, 'command_inputs' => [], 'expect_failure' => TRUE, @@ -116,6 +127,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Check only Docker' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'docker'], 'expect_failure' => FALSE, @@ -130,6 +142,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Check only Docker and Ahoy' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'docker,ahoy'], 'expect_failure' => FALSE, @@ -147,6 +160,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Check with no-summary option' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--no-summary' => TRUE], 'expect_failure' => FALSE, @@ -162,15 +176,8 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Docker missing' => [ - 'exit_code_callback' => function (string $current_command): int { - // Docker command fails. - if ((str_contains($current_command, "command -v 'docker'") || str_contains($current_command, 'command -v docker')) - && !str_contains($current_command, 'compose') - && !str_contains($current_command, 'docker --version')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; - } - return RunnerInterface::EXIT_SUCCESS; - }, + 'executable_finder_callback' => fn(string $name): ?string => $name === 'docker' ? NULL : '/usr/bin/' . $name, + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'docker'], 'expect_failure' => TRUE, 'output_assertions' => array_merge( @@ -187,13 +194,8 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Ahoy missing' => [ - 'exit_code_callback' => function (string $current_command): int { - // Ahoy command fails. - if (str_contains($current_command, "command -v 'ahoy'") || str_contains($current_command, 'command -v ahoy')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; - } - return RunnerInterface::EXIT_SUCCESS; - }, + 'executable_finder_callback' => fn(string $name): ?string => $name === 'ahoy' ? NULL : '/usr/bin/' . $name, + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'ahoy'], 'expect_failure' => TRUE, 'output_assertions' => array_merge( @@ -210,13 +212,8 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Pygmy command not found' => [ - 'exit_code_callback' => function (string $current_command): int { - // Pygmy command does not exist (return 127). - if (str_contains($current_command, "command -v 'pygmy'") || str_contains($current_command, 'command -v pygmy')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; - } - return RunnerInterface::EXIT_SUCCESS; - }, + 'executable_finder_callback' => fn(string $name): ?string => $name === 'pygmy' ? NULL : '/usr/bin/' . $name, + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'pygmy'], 'expect_failure' => TRUE, 'output_assertions' => array_merge( @@ -233,9 +230,8 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Pygmy status command succeeds' => [ - 'exit_code_callback' => fn(string $current_command): int => - // Pygmy command exists and status succeeds. - RunnerInterface::EXIT_SUCCESS, + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, + 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'pygmy'], 'expect_failure' => FALSE, 'output_assertions' => array_merge( @@ -251,11 +247,8 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Pygmy status fails but amazeeio containers found' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => function (string $current_command): int { - // Pygmy command exists. - if (str_contains($current_command, "command -v 'pygmy'") || str_contains($current_command, 'command -v pygmy')) { - return RunnerInterface::EXIT_SUCCESS; - } // Pygmy status fails. if (str_contains($current_command, 'pygmy status')) { return RunnerInterface::EXIT_FAILURE; @@ -277,11 +270,8 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Pygmy status fails and no amazeeio containers' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => function (string $current_command): int { - // Pygmy command exists. - if (str_contains($current_command, "command -v 'pygmy'") || str_contains($current_command, 'command -v pygmy')) { - return RunnerInterface::EXIT_SUCCESS; - } // Pygmy status fails. if (str_contains($current_command, 'pygmy status')) { return RunnerInterface::EXIT_FAILURE; @@ -308,6 +298,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Docker Compose via modern syntax' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'docker-compose'], 'expect_failure' => FALSE, @@ -324,6 +315,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Docker Compose via legacy command' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => function (string $current_command): int { // Modern syntax fails. if (str_contains($current_command, 'docker compose version')) { @@ -346,11 +338,10 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Docker Compose missing completely' => [ + 'executable_finder_callback' => fn(string $name): ?string => $name === 'docker-compose' ? NULL : '/usr/bin/' . $name, 'exit_code_callback' => function (string $current_command): int { - // Both docker compose and docker-compose commands fail. - if (str_contains($current_command, 'docker compose version') || - str_contains($current_command, "command -v 'docker-compose'") || - str_contains($current_command, 'command -v docker-compose')) { + // Modern docker compose command fails. + if (str_contains($current_command, 'docker compose version')) { return RunnerInterface::EXIT_COMMAND_NOT_FOUND; } return RunnerInterface::EXIT_SUCCESS; @@ -371,6 +362,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Invalid requirement name' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'invalid'], 'expect_failure' => TRUE, @@ -381,6 +373,7 @@ public static function dataProviderCheckRequirementsCommand(): array { ], 'Mixed valid and invalid requirements' => [ + 'executable_finder_callback' => fn(string $name): string => '/usr/bin/' . $name, 'exit_code_callback' => fn(string $current_command): int => RunnerInterface::EXIT_SUCCESS, 'command_inputs' => ['--only' => 'docker,invalid'], 'expect_failure' => TRUE, diff --git a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php index 87c7e5c87..0797baa42 100644 --- a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php @@ -17,6 +17,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\MockObject\MockObject; +use Symfony\Component\Process\ExecutableFinder; /** * Functional tests for InstallCommand. @@ -30,25 +31,17 @@ class InstallCommandTest extends FunctionalTestCase { #[DataProvider('dataProviderInstallCommand')] public function testInstallCommand( array $command_inputs, - \Closure $install_process_runner_exit_callback, + \Closure $install_executable_finder_find_callback, \Closure $build_runner_exit_callback, \Closure $check_requirements_runner_exit_callback, bool $expect_failure, array $output_assertions, bool $download_should_fail = FALSE, ): void { - // 1. Mock ProcessRunner for InstallCommand (requirements checking). - $install_process_runner = $this->createMock(ProcessRunner::class); - $install_process_runner_command = ''; - $install_process_runner->method('run') - ->willReturnCallback(function (string $command) use ($install_process_runner, &$install_process_runner_command): MockObject { - $install_process_runner_command = $command; - return $install_process_runner; - }); - $install_process_runner->method('getExitCode') - ->willReturnCallback(function () use ($install_process_runner_exit_callback, &$install_process_runner_command) { - return $install_process_runner_exit_callback($install_process_runner_command); - }); + // 1. Mock ExecutableFinder for InstallCommand (requirements checking). + $executable_finder = $this->createMock(ExecutableFinder::class); + $executable_finder->method('find') + ->willReturnCallback(fn(string $name) => $install_executable_finder_find_callback($name)); // 2. Mock ProcessRunner for BuildCommand (runs 'ahoy build'). $build_runner = $this->createMock(ProcessRunner::class); @@ -69,6 +62,8 @@ public function testInstallCommand( $mock_logger->method('getPath')->willReturn('/tmp/mock.log'); $build_runner->method('getLogger')->willReturn($mock_logger); $build_runner->method('setCwd')->willReturn($build_runner); + // Mock ExecutableFinder for BuildCommand's ProcessRunner. + $build_runner->method('getExecutableFinder')->willReturn($executable_finder); // 3. Mock ProcessRunner for CheckRequirementsCommand. $check_requirements_runner = $this->createMock(ProcessRunner::class); @@ -83,15 +78,17 @@ public function testInstallCommand( ->willReturnCallback(function () use ($check_requirements_runner_exit_callback, &$check_requirements_runner_command) { return $check_requirements_runner_exit_callback($check_requirements_runner_command); }); + // Mock ExecutableFinder for CheckRequirementsCommand's ProcessRunner. + $check_requirements_runner->method('getExecutableFinder')->willReturn($executable_finder); // Create and configure InstallCommand. - $command = new InstallCommand(); - $command->setProcessRunner($install_process_runner); + $install_command = new InstallCommand(); + $install_command->setExecutableFinder($executable_finder); if ($download_should_fail) { $mock_downloader = $this->createMock(Downloader::class); $mock_downloader->method('download')->willThrowException(new \RuntimeException('Failed to download Vortex.')); - $command->setDownloader($mock_downloader); + $install_command->setDownloader($mock_downloader); } else { // Download from root as a real repository. This is long, but there is @@ -101,14 +98,15 @@ public function testInstallCommand( } // Initialize application and register mocked commands. - static::applicationInitFromCommand($command); + static::applicationInitFromCommand($install_command); $check_command = new CheckRequirementsCommand(); - $check_command->setRunner($check_requirements_runner); + $check_command->setExecutableFinder($executable_finder); + $check_command->setProcessRunner($check_requirements_runner); $this->applicationGet()->add($check_command); $build_command = new BuildCommand(); - $build_command->setRunner($build_runner); + $build_command->setProcessRunner($build_runner); $this->applicationGet()->add($build_command); $command_inputs['--' . InstallCommand::OPTION_DESTINATION] = self::$sut; @@ -125,7 +123,7 @@ public function testInstallCommand( * * @return array, - * install_process_runner_exit_callback: \Closure, + * install_executable_finder_find_callback: \Closure, * build_runner_exit_callback: \Closure, * check_requirements_runner_exit_callback: \Closure, * expect_failure: bool, @@ -139,7 +137,7 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => FALSE, @@ -163,7 +161,7 @@ public static function dataProviderInstallCommand(): array { InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_CONFIG => '{"VORTEX_PROJECT_NAME":"test_project"}', ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => FALSE, @@ -181,7 +179,7 @@ public static function dataProviderInstallCommand(): array { InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_NO_CLEANUP => TRUE, ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => FALSE, @@ -201,12 +199,12 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => function (string $command): int { + 'install_executable_finder_find_callback' => function (string $command): ?string { // Git command fails. - if (str_contains($command, 'command -v git')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + if (str_contains($command, 'git')) { + return NULL; } - return RunnerInterface::EXIT_SUCCESS; + return '/usr/bin/' . $command; }, 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, @@ -225,13 +223,14 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => function (string $command): int { + 'install_executable_finder_find_callback' => function (string $command): ?string { // Curl command fails. - if (str_contains($command, 'command -v curl')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + if (str_contains($command, 'curl')) { + return NULL; } - return RunnerInterface::EXIT_SUCCESS; + return '/usr/bin/' . $command; }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => TRUE, @@ -249,13 +248,14 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => function (string $command): int { + 'install_executable_finder_find_callback' => function (string $command): ?string { // Tar command fails. - if (str_contains($command, 'command -v tar')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + if (str_contains($command, 'tar')) { + return NULL; } - return RunnerInterface::EXIT_SUCCESS; + return '/usr/bin/' . $command; }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => TRUE, @@ -273,13 +273,14 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => function (string $command): int { + 'install_executable_finder_find_callback' => function (string $command): ?string { // Composer command fails. - if (str_contains($command, 'command -v composer')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + if (str_contains($command, 'composer')) { + return NULL; } - return RunnerInterface::EXIT_SUCCESS; + return '/usr/bin/' . $command; }, + 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => TRUE, @@ -297,15 +298,17 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => function (string $command): int { - // Both git and curl fail. - if (str_contains($command, 'command -v git')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + 'install_executable_finder_find_callback' => function (string $command): ?string { + // Both git and curl commands fails. + if (str_contains($command, 'git')) { + return NULL; } - if (str_contains($command, 'command -v curl')) { - return RunnerInterface::EXIT_COMMAND_NOT_FOUND; + + if (str_contains($command, 'curl')) { + return NULL; } - return RunnerInterface::EXIT_SUCCESS; + + return '/usr/bin/' . $command; }, 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, @@ -327,7 +330,7 @@ public static function dataProviderInstallCommand(): array { 'command_inputs' => self::tuiOptions([ InstallCommand::OPTION_NO_INTERACTION => TRUE, ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'check_requirements_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, 'expect_failure' => TRUE, @@ -353,7 +356,7 @@ public static function dataProviderInstallCommand(): array { InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_BUILD => TRUE, ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => TuiOutput::buildRunnerSuccess(), 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsSuccess(), 'expect_failure' => FALSE, @@ -381,7 +384,7 @@ public static function dataProviderInstallCommand(): array { InstallCommand::OPTION_BUILD => TRUE, InstallCommand::OPTION_CONFIG => '{"VORTEX_STARTER":"install_profile_core"}', ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => TuiOutput::buildRunnerSuccessProfile(), 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsSuccess(), 'expect_failure' => FALSE, @@ -445,10 +448,10 @@ public static function dataProviderInstallCommand(): array { InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_BUILD => TRUE, ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => TuiOutput::buildRunnerFailure(), 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsSuccess(), - 'expect_failure' => FALSE, + 'expect_failure' => TRUE, 'output_assertions' => [ ...TuiOutput::present([ TuiOutput::INSTALL_STARTING, @@ -473,7 +476,7 @@ public static function dataProviderInstallCommand(): array { InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_BUILD => TRUE, ]), - 'install_process_runner_exit_callback' => fn(string $command): int => RunnerInterface::EXIT_SUCCESS, + 'install_executable_finder_find_callback' => fn(string $command): string => '/usr/bin/' . $command, 'build_runner_exit_callback' => TuiOutput::buildRunnerSuccess(), 'check_requirements_runner_exit_callback' => TuiOutput::checkRequirementsFailure(), 'expect_failure' => TRUE, diff --git a/.vortex/installer/tests/Functional/FunctionalTestCase.php b/.vortex/installer/tests/Functional/FunctionalTestCase.php index 3864fd085..b9eb232f4 100644 --- a/.vortex/installer/tests/Functional/FunctionalTestCase.php +++ b/.vortex/installer/tests/Functional/FunctionalTestCase.php @@ -88,12 +88,9 @@ protected function runNonInteractiveInstall(?string $dst = NULL, array $options $defaults = [ InstallCommand::OPTION_NO_INTERACTION => TRUE, InstallCommand::OPTION_URI => File::dir(static::$root), + InstallCommand::OPTION_DESTINATION => $dst, ]; - if ($dst !== '' && $dst !== '0') { - $defaults[InstallCommand::OPTION_DESTINATION] = $dst; - } - $options += $defaults; foreach ($options as $option => $value) { diff --git a/.vortex/installer/tests/Helpers/TuiOutput.php b/.vortex/installer/tests/Helpers/TuiOutput.php index 4fd9d4693..a336da535 100644 --- a/.vortex/installer/tests/Helpers/TuiOutput.php +++ b/.vortex/installer/tests/Helpers/TuiOutput.php @@ -79,13 +79,13 @@ class TuiOutput { const INSTALL_EXIT_CODE = 'Exit code:'; - const INSTALL_ERROR_MISSING_GIT = 'Installation failed with an error: Missing git.'; + const INSTALL_ERROR_MISSING_GIT = 'Installation failed with an error: Missing required command: git.'; - const INSTALL_ERROR_MISSING_CURL = 'Installation failed with an error: Missing curl.'; + const INSTALL_ERROR_MISSING_CURL = 'Installation failed with an error: Missing required command: curl.'; - const INSTALL_ERROR_MISSING_TAR = 'Installation failed with an error: Missing tar.'; + const INSTALL_ERROR_MISSING_TAR = 'Installation failed with an error: Missing required command: tar.'; - const INSTALL_ERROR_MISSING_COMPOSER = 'Installation failed with an error: Missing Composer.'; + const INSTALL_ERROR_MISSING_COMPOSER = 'Installation failed with an error: Missing required command: Composer.'; const INSTALL_ERROR_DOWNLOAD_FAILED = 'Installation failed with an error: Failed to download Vortex.'; diff --git a/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php b/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php index 1067ec5ea..86168bbb4 100644 --- a/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php +++ b/.vortex/installer/tests/Unit/Runner/AbstractRunnerTest.php @@ -151,23 +151,10 @@ public function testGetOutputInitiallyEmpty(): void { $this->assertEquals('', $runner->getOutput()); } - /** - * Test getOutput with as_array parameter. - */ - #[DataProvider('providerGetOutputVariations')] - public function testGetOutputVariations(string $output, bool $as_array, ?int $lines, string | array $expected): void { - $runner = new ConcreteRunner(); - $runner->setOutput($output); - - $result = $runner->getOutput($as_array, $lines); - - $this->assertEquals($expected, $result); - } - /** * Test parseCommand with various formats. */ - #[DataProvider('providerParseCommand')] + #[DataProvider('dataProviderParseCommand')] public function testParseCommand(string $command, array $expected, ?string $expected_exception, ?string $expected_message): void { if ($expected_exception !== NULL) { /** @var class-string<\Throwable> $expected_exception */ @@ -184,39 +171,197 @@ public function testParseCommand(string $command, array $expected, ?string $expe } /** - * Test buildCommandString with various arguments. - */ - #[DataProvider('providerBuildCommandString')] - public function testBuildCommandString(string $command, array $args, array $opts, string $expected): void { - $runner = new ConcreteRunner(); - - $result = $runner->buildCommandStringPublic($command, $args, $opts); - - $this->assertEquals($expected, $result); - } - - /** - * Test quoteArgument method. - */ - #[DataProvider('providerQuoteArgument')] - public function testQuoteArgument(string $argument, string $expected): void { - $runner = new ConcreteRunner(); - - $result = $runner->quoteArgumentPublic($argument); - - $this->assertEquals($expected, $result); - } - - /** - * Test formatArgs method. + * Data provider for parseCommand. */ - #[DataProvider('providerFormatArgs')] - public function testFormatArgs(array $args, array $expected): void { - $runner = new ConcreteRunner(); - - $result = $runner->formatArgsPublic($args); - - $this->assertEquals($expected, $result); + public static function dataProviderParseCommand(): array { + return [ + 'simple command' => [ + 'command' => 'echo', + 'expected' => ['echo'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with arguments' => [ + 'command' => 'echo hello world', + 'expected' => ['echo', 'hello', 'world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with single-quoted argument' => [ + 'command' => "echo 'hello world'", + 'expected' => ['echo', 'hello world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with double-quoted argument' => [ + 'command' => 'echo "hello world"', + 'expected' => ['echo', 'hello world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with escaped character' => [ + 'command' => 'echo hello\\ world', + 'expected' => ['echo', 'hello world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with escaped quote inside single quotes' => [ + 'command' => "echo 'It\\'s working'", + 'expected' => ['echo', "It's working"], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with mixed quotes' => [ + 'command' => 'echo "hello" \'world\'', + 'expected' => ['echo', 'hello', 'world'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with end-of-options marker' => [ + 'command' => 'echo -- --not-an-option', + 'expected' => ['echo', '--', '--not-an-option'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with single short option' => [ + 'command' => 'ls -l', + 'expected' => ['ls', '-l'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with multiple short options' => [ + 'command' => 'ls -la', + 'expected' => ['ls', '-la'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with separate short options' => [ + 'command' => 'ls -l -a -h', + 'expected' => ['ls', '-l', '-a', '-h'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with long option' => [ + 'command' => 'ls --all', + 'expected' => ['ls', '--all'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with long option with equals value' => [ + 'command' => 'composer require --dev=phpunit', + 'expected' => ['composer', 'require', '--dev=phpunit'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with long option with space-separated value' => [ + 'command' => 'git commit -m "commit message"', + 'expected' => ['git', 'commit', '-m', 'commit message'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with option value with equals' => [ + 'command' => 'command --option=value', + 'expected' => ['command', '--option=value'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with option value with spaces' => [ + 'command' => 'command --option="value with spaces"', + 'expected' => ['command', '--option=value with spaces'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with mixed options and arguments' => [ + 'command' => 'ls -la /path/to/dir', + 'expected' => ['ls', '-la', '/path/to/dir'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with options and quoted arguments' => [ + 'command' => 'grep -r "search term" /path/to/dir', + 'expected' => ['grep', '-r', 'search term', '/path/to/dir'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'complex command with multiple options and arguments' => [ + 'command' => 'docker run -it --rm --name=mycontainer -v /host:/container ubuntu:latest bash', + 'expected' => ['docker', 'run', '-it', '--rm', '--name=mycontainer', '-v', '/host:/container', 'ubuntu:latest', 'bash'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with option containing special characters' => [ + 'command' => 'curl -H "Authorization: Bearer token123"', + 'expected' => ['curl', '-H', 'Authorization: Bearer token123'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with multiple long options with values' => [ + 'command' => 'command --option1=value1 --option2=value2 --flag', + 'expected' => ['command', '--option1=value1', '--option2=value2', '--flag'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with mixed short and long options' => [ + 'command' => 'command -a -b --long-option --another=value arg1 arg2', + 'expected' => ['command', '-a', '-b', '--long-option', '--another=value', 'arg1', 'arg2'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with options before and after arguments' => [ + 'command' => 'find /path -name "*.txt" -type f', + 'expected' => ['find', '/path', '-name', '*.txt', '-type', 'f'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with option value containing equals sign' => [ + 'command' => 'command --url="http://example.com?param=value"', + 'expected' => ['command', '--url=http://example.com?param=value'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with negative number argument' => [ + 'command' => 'command -n -42', + 'expected' => ['command', '-n', '-42'], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'command with option and empty string value' => [ + 'command' => 'command --option=""', + 'expected' => ['command', '--option='], + 'expected_exception' => NULL, + 'expected_message' => NULL, + ], + 'empty command' => [ + 'command' => '', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Command cannot be empty.', + ], + 'whitespace only command' => [ + 'command' => ' ', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Command cannot be empty.', + ], + 'unclosed single quote' => [ + 'command' => "echo 'unclosed", + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Unclosed quote in command string.', + ], + 'unclosed double quote' => [ + 'command' => 'echo "unclosed', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Unclosed quote in command string.', + ], + 'trailing escape' => [ + 'command' => 'echo trailing\\', + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Trailing escape character in command string.', + ], + ]; } /** @@ -289,10 +434,23 @@ public function testResolveOutputWithProvided(): void { $this->assertSame($mock_output, $output); } + /** + * Test getOutput with as_array parameter. + */ + #[DataProvider('dataProviderGetOutputVariations')] + public function testGetOutputVariations(string $output, bool $as_array, ?int $lines, string | array $expected): void { + $runner = new ConcreteRunner(); + $runner->setOutput($output); + + $result = $runner->getOutput($as_array, $lines); + + $this->assertEquals($expected, $result); + } + /** * Data provider for getOutput variations. */ - public static function providerGetOutputVariations(): array { + public static function dataProviderGetOutputVariations(): array { return [ 'string output, as_array=false, no limit' => [ 'output' => "Line 1\nLine 2\nLine 3", @@ -334,95 +492,21 @@ public static function providerGetOutputVariations(): array { } /** - * Data provider for parseCommand. + * Test buildCommandString with various arguments. */ - public static function providerParseCommand(): array { - return [ - 'simple command' => [ - 'command' => 'echo', - 'expected' => ['echo'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with arguments' => [ - 'command' => 'echo hello world', - 'expected' => ['echo', 'hello', 'world'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with single-quoted argument' => [ - 'command' => "echo 'hello world'", - 'expected' => ['echo', 'hello world'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with double-quoted argument' => [ - 'command' => 'echo "hello world"', - 'expected' => ['echo', 'hello world'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with escaped character' => [ - 'command' => 'echo hello\\ world', - 'expected' => ['echo', 'hello world'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with escaped quote inside single quotes' => [ - 'command' => "echo 'It\\'s working'", - 'expected' => ['echo', "It's working"], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with mixed quotes' => [ - 'command' => 'echo "hello" \'world\'', - 'expected' => ['echo', 'hello', 'world'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'command with end-of-options marker' => [ - 'command' => 'echo -- --not-an-option', - 'expected' => ['echo', '--', '--not-an-option'], - 'expected_exception' => NULL, - 'expected_message' => NULL, - ], - 'empty command' => [ - 'command' => '', - 'expected' => [], - 'expected_exception' => \InvalidArgumentException::class, - 'expected_message' => 'Command cannot be empty.', - ], - 'whitespace only command' => [ - 'command' => ' ', - 'expected' => [], - 'expected_exception' => \InvalidArgumentException::class, - 'expected_message' => 'Command cannot be empty.', - ], - 'unclosed single quote' => [ - 'command' => "echo 'unclosed", - 'expected' => [], - 'expected_exception' => \InvalidArgumentException::class, - 'expected_message' => 'Unclosed quote in command string.', - ], - 'unclosed double quote' => [ - 'command' => 'echo "unclosed', - 'expected' => [], - 'expected_exception' => \InvalidArgumentException::class, - 'expected_message' => 'Unclosed quote in command string.', - ], - 'trailing escape' => [ - 'command' => 'echo trailing\\', - 'expected' => [], - 'expected_exception' => \InvalidArgumentException::class, - 'expected_message' => 'Trailing escape character in command string.', - ], - ]; + #[DataProvider('dataProviderBuildCommandString')] + public function testBuildCommandString(string $command, array $args, array $opts, string $expected): void { + $runner = new ConcreteRunner(); + + $result = $runner->buildCommandStringPublic($command, $args, $opts); + + $this->assertEquals($expected, $result); } /** * Data provider for buildCommandString. */ - public static function providerBuildCommandString(): array { + public static function dataProviderBuildCommandString(): array { return [ 'command only' => [ 'command' => 'echo', @@ -463,10 +547,22 @@ public static function providerBuildCommandString(): array { ]; } + /** + * Test quoteArgument method. + */ + #[DataProvider('dataProviderQuoteArgument')] + public function testQuoteArgument(string $argument, string $expected): void { + $runner = new ConcreteRunner(); + + $result = $runner->quoteArgumentPublic($argument); + + $this->assertEquals($expected, $result); + } + /** * Data provider for quoteArgument. */ - public static function providerQuoteArgument(): array { + public static function dataProviderQuoteArgument(): array { return [ 'simple string (no quoting)' => [ 'argument' => 'hello', @@ -499,10 +595,22 @@ public static function providerQuoteArgument(): array { ]; } + /** + * Test formatArgs method. + */ + #[DataProvider('dataProviderFormatArgs')] + public function testFormatArgs(array $args, array $expected): void { + $runner = new ConcreteRunner(); + + $result = $runner->formatArgsPublic($args); + + $this->assertEquals($expected, $result); + } + /** * Data provider for formatArgs. */ - public static function providerFormatArgs(): array { + public static function dataProviderFormatArgs(): array { return [ 'positional args' => [ 'args' => ['arg1', 'arg2'], diff --git a/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php b/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php index c15ef1a16..72ed4f32b 100644 --- a/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php +++ b/.vortex/installer/tests/Unit/Runner/ProcessRunnerTest.php @@ -259,6 +259,14 @@ public static function dataProviderRun(): array { 'expected_exception' => \InvalidArgumentException::class, 'expected_message' => 'Invalid command', ], + 'command utility is not allowed' => [ + 'command' => 'command', + 'args' => ['-v', 'ls'], + 'expected_output_pattern' => '//', + 'expected_exit_code' => 0, + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Using the "command" utility is not allowed. Use Symfony\Component\Process\ExecutableFinder', + ], ]; } @@ -307,6 +315,12 @@ public static function dataProviderResolveCommand(): array { 'expected_exception' => \InvalidArgumentException::class, 'expected_message' => 'Invalid command', ], + 'command utility is not allowed' => [ + 'command' => 'command', + 'expect_success' => FALSE, + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Using the "command" utility is not allowed. Use Symfony\Component\Process\ExecutableFinder to check if a command exists instead.', + ], ]; } From b94714f2b0e3a31479bd72c906964d9bc10177cf Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Tue, 2 Dec 2025 15:56:20 +1100 Subject: [PATCH 5/8] Fixed tests. --- .../Functional/Handlers/AbstractHandlerProcessTestCase.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php b/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php index 7fa9c9808..2a774a2fa 100644 --- a/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php +++ b/.vortex/installer/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php @@ -27,6 +27,12 @@ abstract class AbstractHandlerProcessTestCase extends FunctionalTestCase { protected function setUp(): void { parent::setUp(); + static::envUnsetPrefix('VORTEX_'); + static::envUnsetPrefix('DRUPAL_'); + static::envUnsetPrefix('LAGOON_'); + static::envUnset('WEBROOT'); + static::envUnset('TZ'); + static::applicationInitFromCommand(InstallCommand::class); // Use a two-words name for the sut directory. From a5b122a895c21b28c89f8b5f4b0fcad64202af84 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Tue, 2 Dec 2025 16:10:45 +1100 Subject: [PATCH 6/8] Updated video generation script and updated build now to use VORTEX_INSTALLER_PROMPT_BUILD_NOW. --- .vortex/docs/.utils/update-installer-video.sh | 26 +- .vortex/docs/static/img/installer.json | 368 ++++++++++-------- .vortex/docs/static/img/installer.svg | 2 +- .../installer/src/Command/InstallCommand.php | 14 +- 4 files changed, 231 insertions(+), 179 deletions(-) diff --git a/.vortex/docs/.utils/update-installer-video.sh b/.vortex/docs/.utils/update-installer-video.sh index 6ec818a8f..1b6173389 100755 --- a/.vortex/docs/.utils/update-installer-video.sh +++ b/.vortex/docs/.utils/update-installer-video.sh @@ -183,7 +183,8 @@ proc wait_and_enter {} { ####################### # Start the installer # ####################### -spawn php installer.php star_wars +set env(VORTEX_INSTALLER_PROMPT_BUILD_NOW) 0 +spawn php installer.php --destination=star_wars # Wait for the welcome screen and let it proceed expect { @@ -235,13 +236,13 @@ while {1} { after 2000 safe_send "\r" } - "─┘" { - wait_and_enter - } "Finished installing Vortex" { # Installation completed, break out of loop break } + "─┘" { + wait_and_enter + } timeout { puts "Timeout during installation" break @@ -251,7 +252,22 @@ while {1} { break } } -# sleep 1 +} + +# Handle the final "Run the site build now?" prompt separately +# Default is "No" via VORTEX_INSTALLER_PROMPT_BUILD_NOW=0 env var +expect { + "Run the site build now?" { + after 2000 + # Just press Enter to accept the default (No) + safe_send "\r" + } + timeout { + puts "Timeout waiting for build prompt" + } + eof { + puts "End of file before build prompt" + } } expect eof diff --git a/.vortex/docs/static/img/installer.json b/.vortex/docs/static/img/installer.json index 65ad35060..44e35d48c 100644 --- a/.vortex/docs/static/img/installer.json +++ b/.vortex/docs/static/img/installer.json @@ -1,170 +1,198 @@ -{"version": 2, "width": 120, "height": 36, "timestamp": 1763622409, "env": {"SHELL": "/opt/homebrew/opt/bash/bin/bash", "TERM": "xterm-256color"}, "title": "Vortex Installer Demo"} -[0.478342, "o", "\r\r\n \u001b[36m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m ██╗ ██╗ ██████╗ ██████╗ ████████╗ ███████╗ ██╗ ██╗\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██╔═══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝ ╚██╗██╔╝\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██║ ██║ ██████╔╝ ██║ █████╗ ╚███╔╝\u001b[39m\r\r\n \u001b[36m ╚██╗ ██╔╝ █"] -[0.478515, "o", "█║ ██║ ██╔══██╗ ██║ ██╔══╝ ██╔██╗\u001b[39m\r\r\n \u001b[36m ╚████╔╝ ╚██████╔╝ ██║ ██║ ██║ ███████╗ ██╔╝ ██╗\u001b[39m\r\r\n \u001b[36m ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m Drupal project template\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m by DrevOps\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m─────────────────────────────────────────────────────────────────────────────────────────────────────────────"] -[0.478595, "o", "─────"] -[0.478629, "o", "────\u001b[39m\r\r\n \u001b[2m Installer version: development\u001b[22m\r\r\n\r\r\n"] -[0.481616, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mWelcome to the Vortex interactive installer\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m───────────────────────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m This tool will guide you through installing the latest \u001b[4mstable\u001b[0m version of Vortex into\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m your project.\u001b[39m\u001b[39m "] -[0.48171, "o", " \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m You will be asked a few questions to tailor the configuration to your site.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m No changes will be made until you confirm everything at the end.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+C\u001b[39m at any time to exit the installer.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+U\u001b[39m at any time to go back to the previous step.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└───────"] -[0.481783, "o", "─────"] -[0.481892, "o", "──────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n \u001b[2mPress any key to continue...\u001b[22m\r\r\n"] -[3.494352, "o", "\r\r\n \u001b[46m\u001b[30m General information \u001b[39m\u001b[49m\r\r\n\r\r\n"] -[3.506182, "o", "\u001b[?25l"] -[3.51349, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[3.667991, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Site\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[4.705685, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m S\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m St\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────"] -[4.705925, "o", "──"] -[4.706003, "o", "───────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[5.710695, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[5.713612, "o", "\u001b[1G"] -[5.71364, "o", "\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite name \u001b[2m(1/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[5.713793, "o", "\u001b[?25h"] -[5.732882, "o", "\u001b[?25l"] -[5.73602, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name for the project directory and in the code.\u001b[39m\r\r\n"] -[6.751806, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[6.75192, "o", "\u001b[?25h"] -[6.768354, "o", "\u001b[?25l"] -[6.770646, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars Org\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[6.925847, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars \u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Org\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────"] -[6.926041, "o", "───"] -[7.954645, "o", "───────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m R\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Re\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────"] -[7.954857, "o", "────"] -[7.955289, "o", "─────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[8.959141, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[9.964259, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the co"] -[9.96435, "o", "de.\u001b[39m\r"] -[9.964386, "o", "\r\r\n"] -[10.970065, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Domain name without pro"] -[10.970181, "o", "tocol and"] -[10.970276, "o", " trailing slash.\u001b[39m\r\r\n"] -[11.974107, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Drupal \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m "] -[11.97428, "o", " "] -[11.974702, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloadin"] -[11.974849, "o", "g an exist"] -[12.977637, "o", "ing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Drupal, loaded from the demo database \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────────────"] -[12.977727, "o", "──"] -[12.977764, "o", "───────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇. Applies only on the first run of the installer.\u001b[39m\r\r\n"] -[13.999325, "o", "\u001b[1G\u001b[24A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m "] -[13.999488, "o", " "] -[13.999663, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloading an existing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Drupal, loaded from the demo database "] -[13.999676, "o", " "] -[13.999749, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[15.004475, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProfile \u001b[2m(7/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mMinimal\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDemo Umami\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCustom (next prompt)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal profile to use.\u001b[39m\r\r\n"] -[16.009791, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProfile \u001b[2m(7/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mModules \u001b[2m(8/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Admin toolbar \u001b[36m┃\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCoffee\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig spli"] -[16.010105, "o", "t\u001b[22m "] -[17.289367, "o", " \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig update\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mEnvironment indicator\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPathauto\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedirect\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRobots.txt\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSeckit\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mShield\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────"] -[17.28958, "o", "────────────────────────── 11 selected ┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more modules.\u001b[39m\r\r\n\u001b[1G\u001b[14A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mModules \u001b[2m(8/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Admin toolbar \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Coffee \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config split \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config update \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Environment indicator \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Pathauto \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redirect \u001b[90m"] -[17.289607, "o", "│\u001b[39m\r\r\n\u001b[90m │"] -[17.28991, "o", "\u001b[39m Robots.txt \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Seckit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Shield \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Stage file proxy \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────"] -[17.290649, "o", "───"] -[17.29079, "o", "───────┘\u001b[39m\r\r\n\u001b[90m We will use this name in custom modules\u001b[39m\r\r\n"] -[18.294279, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTheme \u001b[2m(10/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOlivero\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mClaro\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mStark\u001b[22m "] -[18.294948, "o", " \u001b[90"] -[18.298906, "o", "m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal theme to use.\u001b[39m\r\r\n"] -[19.301136, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTheme \u001b[2m(10/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use"] -[19.302079, "o", " this nam"] -[19.304146, "o", "e as a custom theme name\u001b[39m\r\r\n"] -[20.307642, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Code repository \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m "] -[20.307813, "o", " "] -[20.307868, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your code repository provider.\u001b[39m\r\r\n"] -[21.310876, "o", "\u001b[1G\u001b[11A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────"] -[21.311123, "o", "──────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[22.315529, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] -[22.315602, "o", " "] -[22.315779, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b"] -[22.315795, "o", "[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your version scheme.\u001b[39m\r\r\n"] -[23.35745, "o", "\u001b[1G\u001b[22A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] -[23.357564, "o", " "] -[23.357592, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[24.359229, "o", "\r\r\n \u001b[46m\u001b[30m Environment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTimezone \u001b[2m(14/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2mUTC\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇, or start typing to select the timezone for your project.\u001b[39m\r\r\n"] -[25.365169, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTimezone \u001b[2m(14/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mServices \u001b[2m(15/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSolr\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedis\u001b[22m "] -[25.365311, "o", " "] -[25.365472, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more services.\u001b[39m\r\r\n"] -[26.370513, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mServices \u001b[2m(15/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Solr \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redis \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼"] -[26.370636, "o", "\u001b[39m \u001b[2m"] -[26.370799, "o", "PHPStan\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHP Mess Detector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPUnit\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mBehat\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more tools.\u001b[39m\r\r\n"] -[27.375616, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPStan \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP Mess Detector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPUnit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Behat \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Hosting \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m"] -[27.375784, "o", " \u001b[36mHosting "] -[27.37598, "o", "provider \u001b[2m(17/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia Cloud\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select your hosting provider.\u001b[39m\r\r\n"] -[28.380188, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Custom directory where the web se"] -[28.38028, "o", "rver serv"] -[28.380413, "o", "es the site.\u001b[39m\r\r\n"] -[29.383619, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Deployment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m ◻ Code artifact \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mLagoon webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mContainer image\u001b[22m "] -[29.383741, "o", " \u001b[9"] -[29.383809, "o", "0m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCustom webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more deployment types.\u001b[39m\r\r\n"] -[30.388074, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom webhook \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Workflow \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProvision type \u001b[2m(20/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an environment using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │"] -[30.388172, "o", "\u001b[39m "] -[30.389685, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─"] -[30.389705, "o", "───"] -[31.393218, "o", "──────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the provision type.\u001b[39m\r\r\n\u001b[1G\u001b[21A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProvision type \u001b[2m(20/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an e"] -[31.393369, "o", "nvironment using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. "] -[31.393451, "o", " "] -[32.396823, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mFTP download\u001b[22m "] -[32.396966, "o", " "] -[32.397028, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia backup\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon environment\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mContainer registry\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the database download source.\u001b[39m\r\r\n"] -[33.403406, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[34.407552, "o", "\r\r\n \u001b[46m\u001b[30m Notifications \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mGitHub\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mJIRA\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mNew Relic\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mSlack\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mWebhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────"] -[34.407722, "o", "──────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more notification channels.\u001b[39m\r\r\n"] -[35.413038, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Continuous Integration \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCircleCI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m "] -[35.41311, "o", " \u001b["] -[35.413275, "o", "90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the CI provider.\u001b[39m\r\r\n"] -[36.417213, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Automations \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mRenovate self-hosted in CI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r"] -[36.417341, "o", "\r\n\u001b[90m └"] -[36.417402, "o", "──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the dependency updates provider.\u001b[39m\r\r\n"] -[37.422067, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[39m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep the PRs organized.\u001b[39m\r\r\n"] -[38.426261, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[22m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep quickly identify PRs that need attention.\u001b[39m\r\r\n"] -[39.430198, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Documentation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to maintain the project documentation"] -[39.430306, "o", " within the "] -[39.430341, "o", "repository.\u001b[39m\r\r\n"] -[40.434917, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m AI \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────"] -[40.435179, "o", "────────────────────────┘\u001b[39m\r\r\n\u001b[90m Provides AI coding assistants with better context about the project.\u001b[39m\r\r\n"] -[41.438358, "o", "\u001b[1G\u001b[6A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m Installation summary \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n\r\r\n\r\r\n \u001b[90m┌────────────────────────────────────┬─────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mGeneral information\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m"] -[41.439698, "o", "│\u001b[39m\r\r\n \u001b"] -[41.440978, "o", "[90m│\u001b[39m\u001b[39m Site name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Star Wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Site machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Public domain \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star-wars.com \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDrupal\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Starter \u001b[39m\u001b[90m│\u001b[39m\u001b[39m load_demodb \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Modul"] -[41.444719, "o", "es "] -[42.449748, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m admin_toolbar, coffee, config_split,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m config_update, environment_indicator,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m pathauto, redirect, robotstxt, seckit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m shield, stage_file_proxy \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Webroot \u001b[39m\u001b[90m│\u001b[39m\u001b[39m web \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Profile \u001b[39m\u001b[90m│\u001b[39m\u001b[39m standard \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Module prefix \u001b[39m\u001b[90m│\u001b[39m\u001b[39m sw \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Theme machine name "] -[42.449957, "o", " "] -[42.450111, "o", "\u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mCode repository\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Code provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m github \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Version scheme \u001b[39m\u001b[90m│\u001b[39m\u001b[39m calver \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mEnvironment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Timezone \u001b[39m\u001b[90m│\u001b[39m\u001b[39m UTC \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Services \u001b[39m\u001b[90m│\u001b[39m\u001b[39m clamav, redis, solr \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Tools \u001b[39m\u001b"] -[42.450223, "o", "[90m│\u001b[39m\u001b[39m phpcs, phpmd, phpstan, rector, phpunit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m behat \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mHosting\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Hosting provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m none \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDeployment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Deployment types \u001b[39m\u001b[90m│\u001b[39m\u001b[39m webhook \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mWorkflow\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Provision type "] -[42.450301, "o", " "] -[42.450404, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m database \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Database source \u001b[39m\u001b[90m│\u001b[39m\u001b[39m url \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mNotifications\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Channels \u001b[39m\u001b[90m│\u001b[39m\u001b[39m email \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mContinuous Integration\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m CI provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m gha \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAutomations\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Dependency update"] -[42.45043, "o", "s provi"] -[42.450505, "o", "der \u001b[39m\u001b[90m│\u001b[39m\u001b[39m renovatebot_app \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-assign PR author \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-add a CONFLICT label to PRs \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDocumentation\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Preserve project documentation \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAI\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m AI code assistant instructions \u001b[39m\u001b[90m│\u001b[39m\u001b[39m claude \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mLocations\u001b[22m\u001b[39m "] -[42.450526, "o", " "] -[42.450705, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Current directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Destination directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo/star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex repository \u001b[39m\u001b[90m│\u001b[39m\u001b[39m https://github.com/drevops/vortex.git \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex reference \u001b[39m\u001b[90m│\u001b[39m\u001b[39m stable \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└────────────────────────────────────┴─────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n Vortex will be installed into your project's directory \"/home/user/www/demo/star_wars\"\r\r\n"] -[44.455413, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProceed with installing Vortex?\u001b[39m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[46.460936, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProceed with installing Vortex?\u001b[22m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Starting project installation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A"] -[46.462314, "o", "\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33"] -[47.465396, "o", "mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[999D\u001b[2A\u001b[J\u001b[?25h\r\r\n \u001b[34m✦ Downloading Vortex\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloading from \"https://github.com/drevops/vortex.git\" repository at commit \"HEAD\"\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Vortex downloaded (25.10.0)\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCus"] -[47.465496, "o", "tomizing Vortex for your project\u001b[39m\r"] -[47.465583, "o", "\r\r\n"] -[47.51952, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] -[47.519546, "o", "\r\r\n \u001b[34m✦ Customizing Vortex for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[47.519657, "o", "\r\r\n \u001b[32m✓ Vortex was customized for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l"] -[47.523855, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing destination directory\u001b[39m\r\r\n"] -[47.553284, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h\r\r\n \u001b[34m✦ Preparing destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mCreated directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mInitialising a new Git repository in directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Destination directory is ready\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[47.553462, "o", "\u001b[?25l"] -[47.556207, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCopying files to the destination directory\u001b[39m\r\r\n"] -[47.639703, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCopying files to the destination directory\u001b[39m\r\r\n"] -[47.649892, "o", "\u001b[999D\u001b[2A\u001b[J"] -[47.650167, "o", "\u001b[?25h\r\r\n \u001b[34m✦ Copying files to the destination directory\u001b[39m\r\r\n\r\r\n"] -[47.650352, "o", "\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Files copied to destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l"] -[47.652852, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[47.74774, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[47.826607, "o", "\u001b[1G\u001b[2A\u001b[J"] -[47.826719, "o", "\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[47.906043, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[47.984261, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.062969, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.154902, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.23427, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.315394, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.393959, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.472987, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[48.531191, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] -[48.531344, "o", "\r\r\n \u001b[34m✦ Preparing demo content\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mCreated data directory \"/home/user/www/demo/star_wars/.data\".\u001b[22m\r\r\n\r\r\n"] -[48.531482, "o", "\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mNo database dump file was found in \"/home/user/www/demo/star_wars/.data\" directory.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloaded demo database from https://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Demo content prepared\u001b[39m\r\r\n\r\r\n"] -[48.531618, "o", "\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[48.550699, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mFinished installing Vortex\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m──────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Next steps:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Add and commit all files:\u001b[39m\u001b[39m \u001b[39"] -[48.550826, "o", "m\u001b[90m"] -[48.550911, "o", "│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m cd /home/user/www/demo/star_wars\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git add -A\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git commit -m \"Initial commit.\"\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Build project locally:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m ahoy build\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Setup integration with your hosting and CI/CD providers:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m "] -[48.550937, "o", " See https://www.vortextemplate.com/docs/getting-started/installation\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└──────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +{"version": 2, "width": 120, "height": 36, "timestamp": 1764655473, "env": {"SHELL": "/opt/homebrew/opt/bash/bin/bash", "TERM": "xterm-256color"}, "title": "Vortex Installer Demo"} +[0.840819, "o", "\r\r\n \u001b[36m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m ██╗ ██╗ ██████╗ ██████╗ ████████╗ ███████╗ ██╗ ██╗\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██╔═══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝ ╚██╗██╔╝\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██║ ██║ ██████╔╝ ██║ █████╗ ╚███╔╝\u001b[39m\r\r\n \u001b[36m ╚██╗ ██╔╝ █"] +[0.840932, "o", "█║ "] +[0.841026, "o", " ██║ ██╔══██╗ ██║ ██╔══╝ ██╔██╗\u001b[39m\r\r\n \u001b[36m ╚████╔╝ ╚██████╔╝ ██║ ██║ ██║ ███████╗ ██╔╝ ██╗\u001b[39m\r\r\n \u001b[36m ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m Drupal project template\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m by DrevOps\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m────────────────────────────────────────────────────────────────────────────────────────────────────────────────"] +[0.841092, "o", "──────\u001b[39m\r\r\n \u001b[2m Installer version: development\u001b[22m\r\r\n\r\r\n"] +[0.845454, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mWelcome to the Vortex interactive installer\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m───────────────────────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m This tool will guide you through installing the latest \u001b[4mstable\u001b[0m version of Vortex into\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m your project.\u001b[39m\u001b[39m "] +[0.845518, "o", " "] +[0.845607, "o", " \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m You will be asked a few questions to tailor the configuration to your site.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m No changes will be made until you confirm everything at the end.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+C\u001b[39m at any time to exit the installer.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+U\u001b[39m at any time to go back to the previous step.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└─────────"] +[0.845619, "o", "─────────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n \u001b[2mPress any key to continue...\u001b[22m\r\r\n"] +[3.864003, "o", "\r\r\n \u001b[46m\u001b[30m General information \u001b[39m\u001b[49m\r\r\n\r\r\n"] +[3.879169, "o", "\u001b[?25l"] +[3.88275, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[4.038551, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star w\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Site\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────"] +[4.039215, "o", "──"] +[5.21384, "o", "──────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m S\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m St\u001b[7m \u001b[27m "] +[5.214001, "o", " "] +[5.214114, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[6.215286, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[6.221311, "o", "\u001b[1G\u001b[5A\u001b[J"] +[6.221472, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite name \u001b[2m(1/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[6.221561, "o", "\u001b[?25h"] +[6.241267, "o", "\u001b[?25l"] +[6.244205, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name for the project directory and in the code.\u001b[39m\r\r\n"] +[7.261344, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[7.261466, "o", "\u001b[?25h"] +[7.27681, "o", "\u001b[?25l"] +[7.279385, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars Org\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[7.430226, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars Or\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Org\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────"] +[7.430625, "o", "───"] +[8.059313, "o", "───────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m R\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Re\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────"] +[8.05962, "o", "────"] +[8.059972, "o", "─────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[9.062181, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[10.06744, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the co"] +[10.067591, "o", "de.\u001b[39m\r"] +[10.06766, "o", "\r\r\n"] +[11.071139, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Domain name without pro"] +[11.074283, "o", "tocol and trailing slash.\u001b[39m\r\r\n"] +[12.075991, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Drupal \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m "] +[12.076223, "o", " "] +[12.079808, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloadin"] +[12.079885, "o", "g an exist"] +[13.090106, "o", "ing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Drupal, loaded from the demo database \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────────────"] +[13.090685, "o", "──"] +[13.091136, "o", "───────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇. Applies only on the first run of the installer.\u001b[39m\r\r\n"] +[14.10674, "o", "\u001b[1G\u001b[24A\u001b[J"] +[14.106966, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m "] +[14.107149, "o", " \u001b[90m│\u001b"] +[14.107332, "o", "[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloading an existing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Drupal, loaded from the demo database "] +[14.107394, "o", " "] +[14.107518, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[15.11281, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProfile \u001b[2m(7/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mMinimal\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDemo Umami\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCustom (next prompt)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal profile to use.\u001b[39m\r\r\n"] +[16.11528, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProfile \u001b[2m(7/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mModules \u001b[2m(8/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Admin toolbar \u001b[36m┃\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCoffee\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig spli"] +[16.115543, "o", "t\u001b[22m "] +[16.118574, "o", " \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig update\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mEnvironment indicator\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPathauto\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedirect\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRobots.txt\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSeckit\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mShield\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────"] +[16.118694, "o", "───"] +[17.117229, "o", "─────────────────────── 11 selected ┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more modules.\u001b[39m\r\r\n\u001b[1G\u001b[14A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mModules \u001b[2m(8/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Admin toolbar \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Coffee \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config split \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config update \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Environment indicator \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Pathauto \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redirect \u001b[90m│\u001b[39m\r"] +[17.117509, "o", "\r\n\u001b[90m │"] +[17.118036, "o", "\u001b[39m Robots.txt \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Seckit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Shield \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Stage file proxy \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[18.123949, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in custom modules\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b"] +[18.124889, "o", "[39m\r\r\n\r\r\n"] +[18.127179, "o", "\u001b[?25h"] +[19.127441, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTheme \u001b[2m(10/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOlivero\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mClaro\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mStark\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal theme to use.\u001b[39m\r\r\n\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTheme \u001b[2m(10/29)\u001b[22m\u001b[22m \u001b[90m──────"] +[19.127716, "o", "───"] +[19.128108, "o", "──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[20.134753, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name as a custom theme name\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25"] +[20.135709, "o", "h\r\r\n \u001b[46m\u001b[30m Code repository \u001b[39m\u001b[49m\r\r\n\r\r\n"] +[21.138143, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────"] +[21.138435, "o", "───"] +[21.140192, "o", "──────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your code repository provider.\u001b[39m\r\r\n"] +[22.153215, "o", "\u001b[1G\u001b[11A\u001b[J"] +[22.153389, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────────"] +[22.153469, "o", "───"] +[22.153665, "o", "───┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[23.15683, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] +[23.157796, "o", " "] +[23.15942, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b"] +[23.160367, "o", "[90m│\u001b["] +[23.160662, "o", "39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your version scheme.\u001b[39m\r\r\n"] +[24.16253, "o", "\u001b[1G\u001b[22A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] +[24.162722, "o", " "] +[24.162851, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[25.165499, "o", "\r\r\n \u001b[46m\u001b[30m Environment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTimezone \u001b[2m(14/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2mUTC\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇, or start typing to select the timezone for your project.\u001b[39m\r\r\n\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTimezone \u001b[2m(14/29)\u001b[22m\u001b[22m \u001b[90m"] +[25.166003, "o", "────"] +[25.166847, "o", "────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[26.17112, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mServices \u001b[2m(15/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSolr\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedis\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more services.\u001b[39m\r\r\n\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mServices \u001b[2m(15/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b"] +[26.171487, "o", "[39m\r\r\n\u001b["] +[26.172268, "o", "90m │\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Solr \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redis \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[27.176628, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPStan\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHP Mess Detector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPUnit\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mBehat\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────────────────────────────────"] +[27.177066, "o", "───"] +[27.178326, "o", "────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more tools.\u001b[39m\r\r\n"] +[28.201416, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPStan \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP Mess Detector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPUnit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Behat \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[28.201615, "o", "\u001b[?25h"] +[29.205679, "o", "\r\r\n \u001b[46m\u001b[30m Hosting \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia Cloud\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select your hosting provider.\u001b[39m\r\r\n"] +[30.208196, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Custom directory where the web se"] +[30.20839, "o", "rver serv"] +[30.208513, "o", "es the site.\u001b[39m\r\r\n"] +[31.211469, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Deployment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m ◻ Code artifact \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mLagoon webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mContainer image\u001b[22m "] +[31.212248, "o", " \u001b[9"] +[31.21228, "o", "0m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCustom webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more deployment types.\u001b[39m\r\r\n"] +[32.215057, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom webhook \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Workflow \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProvision type \u001b[2m(20/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an environment using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │"] +[32.215141, "o", "\u001b[39m "] +[32.216876, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─"] +[32.216953, "o", "───"] +[33.216347, "o", "──────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the provision type.\u001b[39m\r\r\n\u001b[1G\u001b[21A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProvision type \u001b[2m(20/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an e"] +[33.216688, "o", "nvironment"] +[33.217229, "o", " using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. "] +[33.217672, "o", " "] +[34.221311, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mFTP download\u001b[22m "] +[34.222427, "o", " "] +[34.22485, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia backup\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon environment\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mContainer registry\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the database download source.\u001b[39m\r\r\n"] +[35.235415, "o", "\u001b[1G\u001b[10A\u001b[J"] +[35.2356, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[36.238818, "o", "\r\r\n \u001b[46m\u001b[30m Notifications \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mGitHub\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mJIRA\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mNew Relic\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mSlack\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mWebhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────"] +[36.240389, "o", "────"] +[36.242187, "o", "──────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more notification channels.\u001b[39m\r\r\n"] +[37.244645, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Continuous Integration \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCircleCI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m "] +[37.246047, "o", " \u001b["] +[37.247627, "o", "90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the CI provider.\u001b[39m\r\r\n"] +[38.248334, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Automations \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mRenovate self-hosted in CI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r"] +[38.249205, "o", "\r\n\u001b[90m └"] +[38.251919, "o", "──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the dependency updates provider.\u001b[39m\r\r\n"] +[39.257656, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[39m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep the PRs organized.\u001b[39m\r\r\n"] +[40.259594, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[22m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep quickly identify PRs that need attention.\u001b[39m\r\r\n"] +[41.262681, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Documentation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to maintain the project documentation"] +[41.262958, "o", " within the "] +[41.263289, "o", "repository.\u001b[39m\r\r\n"] +[42.265349, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m AI \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────"] +[42.2657, "o", "────"] +[42.266021, "o", "────────────────────┘\u001b[39m\r\r\n\u001b[90m Provides AI coding assistants with better context about the project.\u001b[39m\r\r\n"] +[43.27428, "o", "\u001b[1G\u001b[6A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m Installation summary \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n\r\r\n\r\r\n \u001b[90m┌────────────────────────────────────┬─────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mGeneral information\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m"] +[43.274613, "o", "│\u001b[39m\r\r\n \u001b"] +[43.27486, "o", "[90m│\u001b[39m\u001b[39m Site name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Star Wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Site machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Public domain \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star-wars.com \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDrupal\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Starter \u001b[39m\u001b[90m│\u001b[39m\u001b[39m load_demodb \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Modul"] +[43.274908, "o", "es "] +[44.27636, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m admin_toolbar, coffee, config_split,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m config_update, environment_indicator,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m pathauto, redirect, robotstxt, seckit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m shield, stage_file_proxy \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Webroot \u001b[39m\u001b[90m│\u001b[39m\u001b[39m web \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Profile \u001b[39m\u001b[90m│\u001b[39m\u001b[39m standard \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Module prefix \u001b[39m\u001b[90m│\u001b[39m\u001b[39m sw \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Theme machine name "] +[44.276895, "o", " "] +[44.277621, "o", "\u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mCode repository\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Code provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m github \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Version scheme \u001b[39m\u001b[90m│\u001b[39m\u001b[39m calver \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mEnvironment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Timezone \u001b[39m\u001b[90m│\u001b[39m\u001b[39m UTC \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Services \u001b[39m\u001b[90m│\u001b[39m\u001b[39m clamav, redis, solr \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Tools \u001b[39m\u001b"] +[44.277652, "o", "[90m│"] +[44.27835, "o", "\u001b[39m\u001b[39m phpcs, phpmd, phpstan, rector, phpunit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m behat \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mHosting\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Hosting provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m none \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDeployment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Deployment types \u001b[39m\u001b[90m│\u001b[39m\u001b[39m webhook \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mWorkflow\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Provision type "] +[44.278986, "o", " "] +[44.279772, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m database \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Database source \u001b[39m\u001b[90m│\u001b[39m\u001b[39m url \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mNotifications\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Channels \u001b[39m\u001b[90m│\u001b[39m\u001b[39m email \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mContinuous Integration\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m CI provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m gha \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAutomations\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Dependency update"] +[44.280026, "o", "s provi"] +[44.280221, "o", "der \u001b[39m\u001b[90m│\u001b[39m\u001b[39m renovatebot_app \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-assign PR author \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-add a CONFLICT label to PRs \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDocumentation\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Preserve project documentation \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAI\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m AI code assistant instructions \u001b[39m\u001b[90m│\u001b[39m\u001b[39m claude \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mLocations\u001b[22m\u001b[39m "] +[44.280377, "o", " "] +[44.280569, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Current directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Destination directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo/star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex repository \u001b[39m\u001b[90m│\u001b[39m\u001b[39m https://github.com/drevops/vortex.git \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex reference \u001b[39m\u001b[90m│\u001b[39m\u001b[39m stable \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└────────────────────────────────────┴─────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n Vortex will be installed into your project's directory \"/home/user/www/demo/star_wars\"\r\r\n"] +[46.285204, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProceed with installing Vortex?\u001b[39m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[48.291277, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProceed with installing Vortex?\u001b[22m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Starting project installation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A"] +[48.291531, "o", "\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33"] +[49.295189, "o", "mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[999D\u001b[2A\u001b[J\u001b[?25h\r\r\n \u001b[34m✦ Downloading Vortex\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloading from \"https://github.com/drevops/vortex.git\" repository at commit \"HEAD\"\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Vortex downloaded (25.10.0)\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your "] +[49.295282, "o", "project\u001b[39m\r\r\n\r\r\n"] +[49.317526, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.395145, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.474095, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.552786, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.631362, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.709716, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.794823, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.873343, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.951648, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.03009, "o", "\u001b[1G\u001b[2A\u001b[J"] +[50.030168, "o", "\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.109915, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.188201, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.266657, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.331642, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] +[50.331739, "o", "\r\r\n \u001b[34m✦ Customizing Vortex for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.331791, "o", "\r\r\n \u001b[32m✓ Vortex was customized for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.331895, "o", "\u001b[?25l"] +[50.334529, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing destination directory\u001b[39m\r\r\n"] +[50.357073, "o", "\u001b[999D\u001b[2A\u001b[J"] +[50.357107, "o", "\u001b[?25h"] +[50.357167, "o", "\r\r\n \u001b[34m✦ Preparing destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.357248, "o", "\r\r\n \u001b[2mCreated directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mInitialising a new Git repository in directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n"] +[50.357333, "o", "\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Destination directory is ready\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.357512, "o", "\u001b[?25l"] +[50.360021, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCopying files to the destination directory\u001b[39m\r\r\n"] +[50.467015, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] +[50.467136, "o", "\r\r\n \u001b[34m✦ Copying files to the destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.467228, "o", "\r\r\n \u001b[32m✓ Files copied to destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.467285, "o", "\u001b[?25l"] +[50.469587, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.697201, "o", "\u001b[1G\u001b[2A\u001b[J"] +[50.697357, "o", "\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.779249, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.871299, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.966621, "o", "\u001b[1G\u001b[2A\u001b[J"] +[50.966846, "o", "\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[51.023631, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] +[51.023807, "o", "\r\r\n \u001b[34m✦ Preparing demo content\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mCreated data directory \"/home/user/www/demo/star_wars/.data\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[51.024032, "o", "\r\r\n \u001b[2mNo database dump file was found in \"/home/user/www/demo/star_wars/.data\" directory.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloaded demo database from https://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Demo content prepared\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[51.05361, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mFinished installing Vortex\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m──────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Next steps:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Add and commit all files:\u001b[39m\u001b[39m \u001b[39"] +[51.05369, "o", "m\u001b[90m"] +[51.053819, "o", "│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m cd /home/user/www/demo/star_wars\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git add -A\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git commit -m \"Initial commit.\"\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Build project locally:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m ahoy build\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Setup integration with your hosting and CI/CD providers:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m "] +[51.053902, "o", " See htt"] +[51.05399, "o", "ps://www.vortextemplate.com/docs/getting-started/installation\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└──────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[51.064788, "o", "\u001b[?25l"] +[51.067767, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRun the site build now?\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○ Yes /\u001b[22m \u001b[32m●\u001b[39m No \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Takes ~5-10 min; output will be streamed. You can skip and run later with: ahoy build\u001b[39m\r\r\n"] +[53.079357, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRun the site build now?\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m No \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] diff --git a/.vortex/docs/static/img/installer.svg b/.vortex/docs/static/img/installer.svg index 365472179..45749e6f2 100644 --- a/.vortex/docs/static/img/installer.svg +++ b/.vortex/docs/static/img/installer.svg @@ -1 +1 @@ -──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────██╗██╗██████╗██████╗████████╗███████╗██╗██╗██║██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝██║██║██║██║██████╔╝██║█████╗╚███╔╝╚██╗██╔╝██║██║██╔══██╗██║██╔══╝██╔██╗╚████╔╝╚██████╔╝██║██║██║███████╗██╔╝██╗╚═══╝╚═════╝╚═╝╚═╝╚═╝╚══════╝╚═╝╚═╝DrupalprojecttemplatebyDrevOpsInstallerversion:development┌──────────────────────────────────────────────────────────────────────────────────────┐WelcometotheVortexinteractiveinstaller───────────────────────────────────────────ThistoolwillguideyouthroughinstallingthelateststableversionofVortexintoyourproject.Youwillbeaskedafewquestionstotailortheconfigurationtoyoursite.Nochangeswillbemadeuntilyouconfirmeverythingattheend.PressCtrl+Catanytimetoexittheinstaller.PressCtrl+Uatanytimetogobacktothepreviousstep.└──────────────────────────────────────────────────────────────────────────────────────┘Pressanykeytocontinue... General information Sitename(1/29)────────────────────────────────────────────┐└──────────────────────────────────────────────────────────────┘Wewillusethisnameintheprojectanddocumentation.St StarWars Sitename(1/29)────────────────────────────────────────────┐StarWarsstar_wars Sitemachinename(2/29)────────────────────────────────────┐star_warsOrganizationname(3/29)────────────────────────────────────┐E.g.MyOrgRe Organizationname(3/29)────────────────────────────────────┐RebellionOrganizationmachinename(4/29)────────────────────────────┐rebellion Wewillusethisnameinthecode.Organizationmachinename(4/29)────────────────────────────┐rebellionPublicdomain(5/29)────────────────────────────────────────┐star-wars.com Publicdomain(5/29)────────────────────────────────────────┐star-wars.com Drupal Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Choosehowyoursitewillbecreatedthefirsttimeafterthisinstallerfinishes:Drupal,installedfromprofileCreatesanewsitebypopulatingafreshdatabasefromoneofthestandardDrupalinstallationprofiles.DrupalCMS,installedfromprofilefromtheDrupalCMSrecipe.Drupal,loadedfromthedemodatabaseCreatesasitebyloadinganexistingdemodatabaseprovidedwiththeinstaller.├─────────────────────────────────────────────────────────────────────┤Drupal,installedfromprofileDrupalCMS,installedfromprofileDrupal,loadedfromthedemodatabase└─────────────────────────────────────────────────────────────────────┘Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)DrupalCMS,installedfromprofileDrupal,loadedfromthedemodatabaseDrupal,loadedfromthedemodatabaseProfile(7/29)──────────────────────────────────────────────┐StandardModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldStagefileproxyCustommodulesprefix(9/29)────────────────────────────────┐sw └────────────────────────────────────────────────────Custommodulesprefix(9/29)────────────────────────────────┐swTheme(10/29)───────────────────────────────────────────────┐OliveroClaroStarkTheme(10/29)───────────────────────────────────────────────┐Custom(nextprompt)Customthememachinename(11/29)───────────────────────────┐Customthememachinename(11/29)───────────────────────────┐ Code repository Repositoryprovider(12/29)─────────────────────────────────┐VortexoffersfullautomationwithGitHub,whilesupportforotherprovidersislimited.├──────────────────────────────────────────────────────────────┤OtherRepositoryprovider(12/29)─────────────────────────────────┐GitHubReleaseversioningscheme(13/29)───────────────────────────┐Chooseyourversioningscheme:CalendarVersioning(CalVer)year.month.patch(E.g.,24.1.0)https://calver.orgSemanticVersioning(SemVer)major.minor.patch(E.g.,1.0.0)major.minor.patch(E.g.,1.0.0)https://semver.orgOtherCustomversioningschemeofyourchoice.CalendarVersioning(CalVer)SemanticVersioning(SemVer)Releaseversioningscheme(13/29)───────────────────────────┐CalendarVersioning(CalVer) Environment Timezone(14/29)────────────────────────────────────────────┐UTCServices(15/29)────────────────────────────────────────────┐ClamAVSolrRedisServices(15/29)────────────────────────────────────────────┐ClamAVSolrRedisDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehat Hosting Hostingprovider(17/29)────────────────────────────────────┐NoneCustomwebrootdirectory(18/29)───────────────────────────┐web Customwebrootdirectory(18/29)───────────────────────────┐web Deployment Deploymenttypes(19/29)────────────────────────────────────┐CodeartifactLagoonwebhookContainerimageDeploymenttypes(19/29)────────────────────────────────────┐Customwebhook Workflow Provisiontype(20/29)──────────────────────────────────────┐Provisioningsetsupthesiteinanenvironmentusinganalreadyassembledcodebase.ImportfromdatabasedumpProvisionsthesitebyimportingadatabasedumptypicallycopiedfromproductionintolowerenvironments.InstallfromprofileProvisionsthesitebyinstallingafreshDrupalsitefromaprofileeverytimeanenvironmentiscreated.Provisiontype(20/29)──────────────────────────────────────┐created.ImportfromdatabasedumpDatabasesource(21/29)─────────────────────────────────────┐URLdownloadFTPdownloadNoneDatabasesource(21/29)─────────────────────────────────────┐URLdownload Notifications Notificationchannels(22/29)───────────────────────────────┐EmailGitHubJIRANewRelicSlackWebhookNotificationchannels(22/29)───────────────────────────────┐Email Continuous Integration ContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActionsCircleCINoneContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActions Automations Dependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappRenovateself-hostedinCIDependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappYes/NoAuto-assigntheauthortotheirPR?(25/29)─────────────────┐Yes└─────────────────────────────────────────────────────────────────┘Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes Documentation Preserveprojectdocumentation?(27/29)─────────────────────┐Preserveprojectdocumentation?(27/29)─────────────────────┐ AI AIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaudeAIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaude Installation summary ┌────────────────────────────────────┬─────────────────────────────────────────┐GeneralinformationSitenameStarWarsSitemachinenamestar_warsOrganizationnameRebellionOrganizationmachinenamerebellionPublicdomainstar-wars.comDrupalStarterload_demodbModulesadmin_toolbar,coffee,config_split,config_update,environment_indicator,pathauto,redirect,robotstxt,seckit,shield,stage_file_proxyWebrootwebProfilestandardModuleprefixswThememachinenameThememachinenamestar_warsCoderepositoryCodeprovidergithubVersionschemecalverEnvironmentTimezoneUTCServicesclamav,redis,solrToolsphpcs,phpmd,phpstan,rector,phpunit,behatHostingHostingprovidernoneDeploymentDeploymenttypeswebhookWorkflowProvisiontypeProvisiontypedatabaseDatabasesourceurlNotificationsChannelsemailContinuousIntegrationCIproviderghaAutomationsDependencyupdatesproviderrenovatebot_appAuto-assignPRauthorYesAuto-addaCONFLICTlabeltoPRsYesDocumentationPreserveprojectdocumentationYesAIAIcodeassistantinstructionsclaudeLocationsLocationsCurrentdirectory/home/user/www/demoDestinationdirectory/home/user/www/demo/star_warsVortexrepositoryhttps://github.com/drevops/vortex.gitVortexreferencestable└────────────────────────────────────┴─────────────────────────────────────────┘Vortexwillbeinstalledintoyourproject'sdirectory"/home/user/www/demo/star_wars"ProceedwithinstallingVortex?─────────────────────────────┐ Starting project installation DownloadingVortexDownloadingfrom"https://github.com/drevops/vortex.git"repositoryatcommit"HEAD"Vortexdownloaded(25.10.0)CustomizingVortexforyourprojectCustomizingVortexforyourprojectVortexwascustomizedforyourprojectPreparingdestinationdirectoryCreateddirectory"/home/user/www/demo/star_wars".InitialisinganewGitrepositoryindirectory"/home/user/www/demo/star_wars".DestinationdirectoryisreadyCopyingfilestothedestinationdirectoryFilescopiedtodestinationdirectoryPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentCreateddatadirectory"/home/user/www/demo/star_wars/.data".Nodatabasedumpfilewasfoundin"/home/user/www/demo/star_wars/.data"directory.Downloadeddemodatabasefromhttps://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.Democontentprepared┌──────────────────────────────────────────────────────────────────────────┐FinishedinstallingVortex──────────────────────────Nextsteps:Addandcommitallfiles:Addandcommitallfiles:cd/home/user/www/demo/star_warsgitadd-Agitcommit-m"Initialcommit."Buildprojectlocally:ahoybuildSetupintegrationwithyourhostingandCI/CDproviders:╚██╗██╔╝───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────yourproject.└───────└────────────starwars E.g.MySite└─────────────────────────────────────└───────────────────────────────────────Sitemachinename(2/29)────────────────────────────────────┐Wewillusethisnamefortheprojectdirectoryandinthecode.StarWarsOrg └────────────────────────────────────────────└───────────────────────────────────────────────└─────└─────────Rebellion WewillusethisnameinthecoDomainnamewithoutproDomainnamewithoutprotocolandDomainnamewithoutprotocolandtrailingslash.CreatesasitebyloadinCreatesasitebyloadinganexist└────────────────────────────────────────────────────────────└──────────────────────────────────────────────────────────────Useand⬇.Appliesonlyonthefirstrunoftheinstaller.Profile(7/29)──────────────────────────────────────────────┐StandardMinimalDemoUmamiCustom(nextprompt)UseandtoselectwhichDrupalprofiletouse.ConfigspliConfigsplitConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShield└───────────────────────Redirect└───────────────────────────────────────────────────────WewillusethisnameincustommodulesStarkCustom(nextprompt)UseandtoselectwhichDrupalthemetouse.WewilluseWewillusethisnamWewillusethisnameasacustomthemenameGitHubUseandtoselectyourcoderepositoryprovider.OtherUseandtoselectyourversionscheme.Timezone(14/29)────────────────────────────────────────────┐UTC UTCUseand⬇,orstarttypingtoselectthetimezoneforyourproject.RedisUse⬆,andSpacebartoselectoneormoreservices.PHPStanRectorPHPMessDetectorPHPUnitBehatUse⬆,andSpacebartoselectoneormoretools.HostingHostingprovider(17/29)────────────────────────────────────┐AcquiaCloudLagoonNoneUse⬆,andSpacebartoselectyourhostingprovider.CustomdirectorywherethewebseCustomdirectorywherethewebserverservCustomdirectorywherethewebserverservesthesite.ContainerimageCustomwebhookUse⬆,andSpacebartoselectoneormoredeploymenttypes.├─├────ProvisioningsetsupthesiteinaneFTPdownloadAcquiabackupLagoonenvironmentContainerregistryUseandtoselectthedatabasedownloadsource.└────────────────────────────────────────Use⬆,andSpacebartoselectoneormorenotificationchannels.UseandtoselecttheCIprovider.Useandtoselectthedependencyupdatesprovider.Auto-assigntheauthortotheirPR?(25/29)─────────────────┐HelpstokeepthePRsorganized.Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes/NoHelpstokeepquicklyidentifyPRsthatneedattention.HelpstomaintaintheprojectdocumentationHelpstomaintaintheprojectdocumentationwithintheHelpstomaintaintheprojectdocumentationwithintherepository.└──────────────────────────────────────ProvidesAIcodingassistantswithbettercontextabouttheproject.GeneralinformationModulModulesToolsDependencyupdateDependencyupdatesproviProceedwithinstallingVortex?─────────────────────────────┐DownloadingVortexCusPreparingdestinationdirectoryCopyingfilestothedestinationdirectoryCopyingfilestothedestinationdirectoryPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentSeehttps://www.vortextemplate.com/docs/getting-started/installation└──────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────██╗██╗██████╗██████╗████████╗███████╗██╗██╗██║██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝██║██║██║██║██████╔╝██║█████╗╚███╔╝╚██╗██╔╝██║██║██╔══██╗██║██╔══╝██╔██╗╚████╔╝╚██████╔╝██║██║██║███████╗██╔╝██╗╚═══╝╚═════╝╚═╝╚═╝╚═╝╚══════╝╚═╝╚═╝DrupalprojecttemplatebyDrevOpsInstallerversion:development┌──────────────────────────────────────────────────────────────────────────────────────┐WelcometotheVortexinteractiveinstaller───────────────────────────────────────────ThistoolwillguideyouthroughinstallingthelateststableversionofVortexintoyourproject.yourproject.Youwillbeaskedafewquestionstotailortheconfigurationtoyoursite.Nochangeswillbemadeuntilyouconfirmeverythingattheend.PressCtrl+Catanytimetoexittheinstaller.PressCtrl+Uatanytimetogobacktothepreviousstep.└─────────└──────────────────────────────────────────────────────────────────────────────────────┘Pressanykeytocontinue... General information Sitename(1/29)────────────────────────────────────────────┐└──────────────────────────────────────────────────────────────┘Wewillusethisnameintheprojectanddocumentation.E.g.MySiteSt Sitename(1/29)────────────────────────────────────────────┐StarWarsSitemachinename(2/29)────────────────────────────────────┐star_warsOrganizationname(3/29)────────────────────────────────────┐E.g.MyOrg└────────────────────────────────────────────Re Organizationname(3/29)────────────────────────────────────┐RebellionOrganizationmachinename(4/29)────────────────────────────┐rebellion Wewillusethisnameinthecode.Organizationmachinename(4/29)────────────────────────────┐rebellionPublicdomain(5/29)────────────────────────────────────────┐star-wars.com Publicdomain(5/29)────────────────────────────────────────┐star-wars.com Drupal Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Choosehowyoursitewillbecreatedthefirsttimeafterthisinstallerfinishes:Drupal,installedfromprofileCreatesanewsitebypopulatingafreshdatabasefromoneofthestandardDrupalinstallationprofiles.DrupalCMS,installedfromprofilefromtheDrupalCMSrecipe.Drupal,loadedfromthedemodatabaseCreatesasitebyloadinganexistingdemodatabaseprovidedwiththeinstaller.├─────────────────────────────────────────────────────────────────────┤Drupal,installedfromprofileDrupalCMS,installedfromprofileDrupal,loadedfromthedemodatabase└─────────────────────────────────────────────────────────────────────┘Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Drupal,loadedfromthedemodatabaseDrupal,loadedfromthedemodatabaseProfile(7/29)──────────────────────────────────────────────┐StandardModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldStagefileproxyCustommodulesprefix(9/29)────────────────────────────────┐swTheme(10/29)───────────────────────────────────────────────┐Custom(nextprompt)Customthememachinename(11/29)───────────────────────────┐ Code repository Repositoryprovider(12/29)─────────────────────────────────┐VortexoffersfullautomationwithGitHub,whilesupportforotherprovidersislimited.├──────────────────────────────────────────────────────────────┤GitHubOtherRepositoryprovider(12/29)─────────────────────────────────┐GitHubReleaseversioningscheme(13/29)───────────────────────────┐Chooseyourversioningscheme:CalendarVersioning(CalVer)year.month.patch(E.g.,24.1.0)https://calver.orgSemanticVersioning(SemVer)major.minor.patch(E.g.,1.0.0)major.minor.patch(E.g.,1.0.0)https://semver.orgOtherCustomversioningschemeofyourchoice.CalendarVersioning(CalVer)SemanticVersioning(SemVer)Releaseversioningscheme(13/29)───────────────────────────┐CalendarVersioning(CalVer) Environment Timezone(14/29)────────────────────────────────────────────┐UTCServices(15/29)────────────────────────────────────────────┐ClamAVSolrRedisDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehatDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehat Hosting Hostingprovider(17/29)────────────────────────────────────┐NoneCustomwebrootdirectory(18/29)───────────────────────────┐web Customwebrootdirectory(18/29)───────────────────────────┐web Deployment Deploymenttypes(19/29)────────────────────────────────────┐CodeartifactLagoonwebhookContainerimageDeploymenttypes(19/29)────────────────────────────────────┐Customwebhook Workflow Provisiontype(20/29)──────────────────────────────────────┐Provisioningsetsupthesiteinanenvironmentusinganalreadyassembledcodebase.ImportfromdatabasedumpProvisionsthesitebyimportingadatabasedumptypicallycopiedfromproductionintolowerenvironments.InstallfromprofileProvisionsthesitebyinstallingafreshDrupalsitefromaprofileeverytimeanenvironmentiscreated.Provisiontype(20/29)──────────────────────────────────────┐created.ImportfromdatabasedumpDatabasesource(21/29)─────────────────────────────────────┐URLdownloadFTPdownloadNoneDatabasesource(21/29)─────────────────────────────────────┐URLdownload Notifications Notificationchannels(22/29)───────────────────────────────┐EmailGitHubJIRANewRelicSlackWebhookNotificationchannels(22/29)───────────────────────────────┐Email Continuous Integration ContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActionsCircleCINoneContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActions Automations Dependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappRenovateself-hostedinCIDependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappYes/NoAuto-assigntheauthortotheirPR?(25/29)─────────────────┐Yes└─────────────────────────────────────────────────────────────────┘Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes Documentation Preserveprojectdocumentation?(27/29)─────────────────────┐Preserveprojectdocumentation?(27/29)─────────────────────┐ AI AIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaudeAIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaude Installation summary ┌────────────────────────────────────┬─────────────────────────────────────────┐GeneralinformationSitenameStarWarsSitemachinenamestar_warsOrganizationnameRebellionOrganizationmachinenamerebellionPublicdomainstar-wars.comDrupalStarterload_demodbModulesadmin_toolbar,coffee,config_split,config_update,environment_indicator,pathauto,redirect,robotstxt,seckit,shield,stage_file_proxyWebrootwebProfilestandardModuleprefixswThememachinenameThememachinenamestar_warsCoderepositoryCodeprovidergithubVersionschemecalverEnvironmentTimezoneUTCServicesclamav,redis,solrToolsphpcs,phpmd,phpstan,rector,phpunit,behatHostingHostingprovidernoneDeploymentDeploymenttypeswebhookWorkflowProvisiontypeProvisiontypedatabaseDatabasesourceurlNotificationsChannelsemailContinuousIntegrationCIproviderghaAutomationsDependencyupdatesproviderrenovatebot_appAuto-assignPRauthorYesAuto-addaCONFLICTlabeltoPRsYesDocumentationPreserveprojectdocumentationYesAIAIcodeassistantinstructionsclaudeLocationsLocationsCurrentdirectory/home/user/www/demoDestinationdirectory/home/user/www/demo/star_warsVortexrepositoryhttps://github.com/drevops/vortex.gitVortexreferencestable└────────────────────────────────────┴─────────────────────────────────────────┘Vortexwillbeinstalledintoyourproject'sdirectory"/home/user/www/demo/star_wars"ProceedwithinstallingVortex?─────────────────────────────┐ Starting project installation DownloadingVortexDownloadingfrom"https://github.com/drevops/vortex.git"repositoryatcommit"HEAD"Vortexdownloaded(25.10.0)CustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectVortexwascustomizedforyourprojectPreparingdestinationdirectoryCreateddirectory"/home/user/www/demo/star_wars".InitialisinganewGitrepositoryindirectory"/home/user/www/demo/star_wars".DestinationdirectoryisreadyCopyingfilestothedestinationdirectoryFilescopiedtodestinationdirectoryPreparingdemocontentCreateddatadirectory"/home/user/www/demo/star_wars/.data".Nodatabasedumpfilewasfoundin"/home/user/www/demo/star_wars/.data"directory.Downloadeddemodatabasefromhttps://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.Democontentprepared┌──────────────────────────────────────────────────────────────────────────┐FinishedinstallingVortex──────────────────────────Nextsteps:Addandcommitallfiles:Addandcommitallfiles:cd/home/user/www/demo/star_warsgitadd-Agitcommit-m"Initialcommit."Buildprojectlocally:ahoybuildSetupintegrationwithyourhostingandCI/CDproviders:Seehttps://www.vortextemplate.com/docs/getting-started/installation└──────────────────────────────────────────────────────────────────────────┘╚██╗██╔╝╚██╗██╔╝██║────────────────────────────────────────────────────────────────────────────────────────────────────────────────starwars └──────────────────────────────────└────────────────────────────────────St StarWars Sitemachinename(2/29)────────────────────────────────────┐star_wars Wewillusethisnamefortheprojectdirectoryandinthecode.StarWarsOrg └───────────────────────────────────────────────└─────Rebellion WewillusethisnameinthecoDomainnamewithoutproDomainnamewithoutprotocolandtrailingslash.CreatesasitebyloadinCreatesasitebyloadinganexist└────────────────────────────────────────────────────────────└──────────────────────────────────────────────────────────────Useand⬇.Appliesonlyonthefirstrunoftheinstaller.DrupalCMS,installedfromprofileProfile(7/29)──────────────────────────────────────────────┐StandardMinimalDemoUmamiCustom(nextprompt)UseandtoselectwhichDrupalprofiletouse.ConfigspliConfigsplit└───────────────────────└──────────────────────────Theme(10/29)──────Theme(10/29)─────────└────────────Useandtoselectyourcoderepositoryprovider.└────────────────────────────────────────────────────────└───────────────────────────────────────────────────────────OtherUseandtoselectyourversionscheme.Timezone(14/29)Timezone(14/29)────└───────────────────────────────────────────────────└──────────────────────────────────────────────────────Use⬆,andSpacebartoselectoneormoretools.Hostingprovider(17/29)────────────────────────────────────┐AcquiaCloudLagoonNoneUse⬆,andSpacebartoselectyourhostingprovider.CustomdirectorywherethewebseCustomdirectorywherethewebserverservCustomdirectorywherethewebserverservesthesite.ContainerimageCustomwebhookUse⬆,andSpacebartoselectoneormoredeploymenttypes.├─├────ProvisioningsetsupthesiteinaneProvisioningsetsupthesiteinanenvironmentFTPdownloadAcquiabackupLagoonenvironmentContainerregistryUseandtoselectthedatabasedownloadsource.└────────────────────────────────────────Use⬆,andSpacebartoselectoneormorenotificationchannels.UseandtoselecttheCIprovider.Useandtoselectthedependencyupdatesprovider.Auto-assigntheauthortotheirPR?(25/29)─────────────────┐HelpstokeepthePRsorganized.Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes/NoHelpstokeepquicklyidentifyPRsthatneedattention.HelpstomaintaintheprojectdocumentationHelpstomaintaintheprojectdocumentationwithintheHelpstomaintaintheprojectdocumentationwithintherepository.└──────────────────────────────────────└──────────────────────────────────────────ProvidesAIcodingassistantswithbettercontextabouttheproject.GeneralinformationModulModulesToolsToolsDependencyupdateDependencyupdatesproviProceedwithinstallingVortex?─────────────────────────────┐DownloadingVortexCustomizingVortexforyourCustomizingVortexforyourprojectCustomizingVortexforyourprojectPreparingdestinationdirectoryCopyingfilestothedestinationdirectoryPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentSeehttRunthesitebuildnow?─────────────────────────────────────┐Yes/NoTakes~5-10min;outputwillbestreamed.Youcanskipandrunlaterwith:ahoybuildRunthesitebuildnow?─────────────────────────────────────┐No \ No newline at end of file diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index 1ab9684e5..e380f6310 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -181,11 +181,19 @@ protected function execute(InputInterface $input, OutputInterface $output): int $this->footer(); - $should_build = $this->config->get(Config::BUILD_NOW); - if (!$should_build && !$this->config->getNoInteraction()) { + // Should build by default. + $should_build = TRUE; + // Requested build via `--build` option. Defaults to FALSE. + $requested_build = (bool) $this->config->get(Config::BUILD_NOW); + // Non-interactive: respect the `--build` option. + if ($this->config->getNoInteraction()) { + $should_build = $requested_build; + } + // Interactive: ask only if `--build` option was not provided. + elseif (!$requested_build) { $should_build = Tui::confirm( label: 'Run the site build now?', - default: TRUE, + default: (bool) Env::get('VORTEX_INSTALLER_PROMPT_BUILD_NOW', TRUE), hint: 'Takes ~5-10 min; output will be streamed. You can skip and run later with: ahoy build', ); } From e934c5ec3664ff421df4d04f10089450acbb7d00 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Tue, 2 Dec 2025 17:44:45 +1100 Subject: [PATCH 7/8] Updated footers. --- .vortex/docs/static/img/installer.json | 384 +++++++++--------- .vortex/docs/static/img/installer.svg | 2 +- .../installer/src/Command/InstallCommand.php | 102 ++++- .../src/Prompts/Handlers/AbstractHandler.php | 7 + .../src/Prompts/Handlers/CiProvider.php | 32 ++ .../src/Prompts/Handlers/HandlerInterface.php | 11 + .../src/Prompts/Handlers/HostingProvider.php | 32 ++ .../src/Prompts/Handlers/Internal.php | 5 +- .../src/Prompts/Handlers/Starter.php | 31 -- .../installer/src/Prompts/PromptManager.php | 37 +- .../Functional/Command/BuildCommandTest.php | 2 +- .../Functional/Command/InstallCommandTest.php | 49 ++- .vortex/installer/tests/Helpers/TuiOutput.php | 53 ++- 13 files changed, 494 insertions(+), 253 deletions(-) diff --git a/.vortex/docs/static/img/installer.json b/.vortex/docs/static/img/installer.json index 44e35d48c..4b2c8fbae 100644 --- a/.vortex/docs/static/img/installer.json +++ b/.vortex/docs/static/img/installer.json @@ -1,198 +1,186 @@ -{"version": 2, "width": 120, "height": 36, "timestamp": 1764655473, "env": {"SHELL": "/opt/homebrew/opt/bash/bin/bash", "TERM": "xterm-256color"}, "title": "Vortex Installer Demo"} -[0.840819, "o", "\r\r\n \u001b[36m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m ██╗ ██╗ ██████╗ ██████╗ ████████╗ ███████╗ ██╗ ██╗\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██╔═══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝ ╚██╗██╔╝\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██║ ██║ ██████╔╝ ██║ █████╗ ╚███╔╝\u001b[39m\r\r\n \u001b[36m ╚██╗ ██╔╝ █"] -[0.840932, "o", "█║ "] -[0.841026, "o", " ██║ ██╔══██╗ ██║ ██╔══╝ ██╔██╗\u001b[39m\r\r\n \u001b[36m ╚████╔╝ ╚██████╔╝ ██║ ██║ ██║ ███████╗ ██╔╝ ██╗\u001b[39m\r\r\n \u001b[36m ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m Drupal project template\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m by DrevOps\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m────────────────────────────────────────────────────────────────────────────────────────────────────────────────"] -[0.841092, "o", "──────\u001b[39m\r\r\n \u001b[2m Installer version: development\u001b[22m\r\r\n\r\r\n"] -[0.845454, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mWelcome to the Vortex interactive installer\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m───────────────────────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m This tool will guide you through installing the latest \u001b[4mstable\u001b[0m version of Vortex into\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m your project.\u001b[39m\u001b[39m "] -[0.845518, "o", " "] -[0.845607, "o", " \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m You will be asked a few questions to tailor the configuration to your site.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m No changes will be made until you confirm everything at the end.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+C\u001b[39m at any time to exit the installer.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+U\u001b[39m at any time to go back to the previous step.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└─────────"] -[0.845619, "o", "─────────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n \u001b[2mPress any key to continue...\u001b[22m\r\r\n"] -[3.864003, "o", "\r\r\n \u001b[46m\u001b[30m General information \u001b[39m\u001b[49m\r\r\n\r\r\n"] -[3.879169, "o", "\u001b[?25l"] -[3.88275, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[4.038551, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star w\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Site\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────"] -[4.039215, "o", "──"] -[5.21384, "o", "──────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m S\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m St\u001b[7m \u001b[27m "] -[5.214001, "o", " "] -[5.214114, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[6.215286, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[6.221311, "o", "\u001b[1G\u001b[5A\u001b[J"] -[6.221472, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite name \u001b[2m(1/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[6.221561, "o", "\u001b[?25h"] -[6.241267, "o", "\u001b[?25l"] -[6.244205, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name for the project directory and in the code.\u001b[39m\r\r\n"] -[7.261344, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[7.261466, "o", "\u001b[?25h"] -[7.27681, "o", "\u001b[?25l"] -[7.279385, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars Org\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[7.430226, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars Or\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Org\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────"] -[7.430625, "o", "───"] -[8.059313, "o", "───────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m R\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Re\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────"] -[8.05962, "o", "────"] -[8.059972, "o", "─────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[9.062181, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] -[10.06744, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the co"] -[10.067591, "o", "de.\u001b[39m\r"] -[10.06766, "o", "\r\r\n"] -[11.071139, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Domain name without pro"] -[11.074283, "o", "tocol and trailing slash.\u001b[39m\r\r\n"] -[12.075991, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Drupal \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m "] -[12.076223, "o", " "] -[12.079808, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloadin"] -[12.079885, "o", "g an exist"] -[13.090106, "o", "ing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Drupal, loaded from the demo database \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────────────"] -[13.090685, "o", "──"] -[13.091136, "o", "───────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇. Applies only on the first run of the installer.\u001b[39m\r\r\n"] -[14.10674, "o", "\u001b[1G\u001b[24A\u001b[J"] -[14.106966, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m "] -[14.107149, "o", " \u001b[90m│\u001b"] -[14.107332, "o", "[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloading an existing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Drupal, loaded from the demo database "] -[14.107394, "o", " "] -[14.107518, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[15.11281, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProfile \u001b[2m(7/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mMinimal\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDemo Umami\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCustom (next prompt)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal profile to use.\u001b[39m\r\r\n"] -[16.11528, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProfile \u001b[2m(7/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mModules \u001b[2m(8/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Admin toolbar \u001b[36m┃\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCoffee\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig spli"] -[16.115543, "o", "t\u001b[22m "] -[16.118574, "o", " \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig update\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mEnvironment indicator\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPathauto\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedirect\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRobots.txt\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSeckit\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mShield\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────"] -[16.118694, "o", "───"] -[17.117229, "o", "─────────────────────── 11 selected ┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more modules.\u001b[39m\r\r\n\u001b[1G\u001b[14A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mModules \u001b[2m(8/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Admin toolbar \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Coffee \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config split \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config update \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Environment indicator \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Pathauto \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redirect \u001b[90m│\u001b[39m\r"] -[17.117509, "o", "\r\n\u001b[90m │"] -[17.118036, "o", "\u001b[39m Robots.txt \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Seckit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Shield \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Stage file proxy \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[18.123949, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in custom modules\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b"] -[18.124889, "o", "[39m\r\r\n\r\r\n"] -[18.127179, "o", "\u001b[?25h"] -[19.127441, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTheme \u001b[2m(10/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOlivero\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mClaro\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mStark\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal theme to use.\u001b[39m\r\r\n\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTheme \u001b[2m(10/29)\u001b[22m\u001b[22m \u001b[90m──────"] -[19.127716, "o", "───"] -[19.128108, "o", "──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[20.134753, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name as a custom theme name\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25"] -[20.135709, "o", "h\r\r\n \u001b[46m\u001b[30m Code repository \u001b[39m\u001b[49m\r\r\n\r\r\n"] -[21.138143, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────"] -[21.138435, "o", "───"] -[21.140192, "o", "──────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your code repository provider.\u001b[39m\r\r\n"] -[22.153215, "o", "\u001b[1G\u001b[11A\u001b[J"] -[22.153389, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────────"] -[22.153469, "o", "───"] -[22.153665, "o", "───┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[23.15683, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] -[23.157796, "o", " "] -[23.15942, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b"] -[23.160367, "o", "[90m│\u001b["] -[23.160662, "o", "39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your version scheme.\u001b[39m\r\r\n"] -[24.16253, "o", "\u001b[1G\u001b[22A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] -[24.162722, "o", " "] -[24.162851, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[25.165499, "o", "\r\r\n \u001b[46m\u001b[30m Environment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTimezone \u001b[2m(14/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2mUTC\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇, or start typing to select the timezone for your project.\u001b[39m\r\r\n\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTimezone \u001b[2m(14/29)\u001b[22m\u001b[22m \u001b[90m"] -[25.166003, "o", "────"] -[25.166847, "o", "────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[26.17112, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mServices \u001b[2m(15/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSolr\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedis\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more services.\u001b[39m\r\r\n\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mServices \u001b[2m(15/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b"] -[26.171487, "o", "[39m\r\r\n\u001b["] -[26.172268, "o", "90m │\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Solr \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redis \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[27.176628, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPStan\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHP Mess Detector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPUnit\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mBehat\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────────────────────────────────"] -[27.177066, "o", "───"] -[27.178326, "o", "────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more tools.\u001b[39m\r\r\n"] -[28.201416, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPStan \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP Mess Detector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPUnit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Behat \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[28.201615, "o", "\u001b[?25h"] -[29.205679, "o", "\r\r\n \u001b[46m\u001b[30m Hosting \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia Cloud\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select your hosting provider.\u001b[39m\r\r\n"] -[30.208196, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Custom directory where the web se"] -[30.20839, "o", "rver serv"] -[30.208513, "o", "es the site.\u001b[39m\r\r\n"] -[31.211469, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Deployment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m ◻ Code artifact \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mLagoon webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mContainer image\u001b[22m "] -[31.212248, "o", " \u001b[9"] -[31.21228, "o", "0m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCustom webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more deployment types.\u001b[39m\r\r\n"] -[32.215057, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom webhook \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Workflow \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProvision type \u001b[2m(20/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an environment using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │"] -[32.215141, "o", "\u001b[39m "] -[32.216876, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─"] -[32.216953, "o", "───"] -[33.216347, "o", "──────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the provision type.\u001b[39m\r\r\n\u001b[1G\u001b[21A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProvision type \u001b[2m(20/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an e"] -[33.216688, "o", "nvironment"] -[33.217229, "o", " using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. "] -[33.217672, "o", " "] -[34.221311, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mFTP download\u001b[22m "] -[34.222427, "o", " "] -[34.22485, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia backup\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon environment\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mContainer registry\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the database download source.\u001b[39m\r\r\n"] -[35.235415, "o", "\u001b[1G\u001b[10A\u001b[J"] -[35.2356, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] -[36.238818, "o", "\r\r\n \u001b[46m\u001b[30m Notifications \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mGitHub\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mJIRA\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mNew Relic\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mSlack\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mWebhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────"] -[36.240389, "o", "────"] -[36.242187, "o", "──────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more notification channels.\u001b[39m\r\r\n"] -[37.244645, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Continuous Integration \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCircleCI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m "] -[37.246047, "o", " \u001b["] -[37.247627, "o", "90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the CI provider.\u001b[39m\r\r\n"] -[38.248334, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Automations \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mRenovate self-hosted in CI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r"] -[38.249205, "o", "\r\n\u001b[90m └"] -[38.251919, "o", "──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the dependency updates provider.\u001b[39m\r\r\n"] -[39.257656, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[39m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep the PRs organized.\u001b[39m\r\r\n"] -[40.259594, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[22m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep quickly identify PRs that need attention.\u001b[39m\r\r\n"] -[41.262681, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Documentation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to maintain the project documentation"] -[41.262958, "o", " within the "] -[41.263289, "o", "repository.\u001b[39m\r\r\n"] -[42.265349, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m AI \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────"] -[42.2657, "o", "────"] -[42.266021, "o", "────────────────────┘\u001b[39m\r\r\n\u001b[90m Provides AI coding assistants with better context about the project.\u001b[39m\r\r\n"] -[43.27428, "o", "\u001b[1G\u001b[6A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m Installation summary \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n\r\r\n\r\r\n \u001b[90m┌────────────────────────────────────┬─────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mGeneral information\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m"] -[43.274613, "o", "│\u001b[39m\r\r\n \u001b"] -[43.27486, "o", "[90m│\u001b[39m\u001b[39m Site name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Star Wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Site machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Public domain \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star-wars.com \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDrupal\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Starter \u001b[39m\u001b[90m│\u001b[39m\u001b[39m load_demodb \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Modul"] -[43.274908, "o", "es "] -[44.27636, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m admin_toolbar, coffee, config_split,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m config_update, environment_indicator,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m pathauto, redirect, robotstxt, seckit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m shield, stage_file_proxy \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Webroot \u001b[39m\u001b[90m│\u001b[39m\u001b[39m web \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Profile \u001b[39m\u001b[90m│\u001b[39m\u001b[39m standard \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Module prefix \u001b[39m\u001b[90m│\u001b[39m\u001b[39m sw \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Theme machine name "] -[44.276895, "o", " "] -[44.277621, "o", "\u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mCode repository\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Code provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m github \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Version scheme \u001b[39m\u001b[90m│\u001b[39m\u001b[39m calver \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mEnvironment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Timezone \u001b[39m\u001b[90m│\u001b[39m\u001b[39m UTC \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Services \u001b[39m\u001b[90m│\u001b[39m\u001b[39m clamav, redis, solr \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Tools \u001b[39m\u001b"] -[44.277652, "o", "[90m│"] -[44.27835, "o", "\u001b[39m\u001b[39m phpcs, phpmd, phpstan, rector, phpunit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m behat \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mHosting\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Hosting provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m none \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDeployment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Deployment types \u001b[39m\u001b[90m│\u001b[39m\u001b[39m webhook \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mWorkflow\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Provision type "] -[44.278986, "o", " "] -[44.279772, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m database \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Database source \u001b[39m\u001b[90m│\u001b[39m\u001b[39m url \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mNotifications\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Channels \u001b[39m\u001b[90m│\u001b[39m\u001b[39m email \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mContinuous Integration\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m CI provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m gha \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAutomations\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Dependency update"] -[44.280026, "o", "s provi"] -[44.280221, "o", "der \u001b[39m\u001b[90m│\u001b[39m\u001b[39m renovatebot_app \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-assign PR author \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-add a CONFLICT label to PRs \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDocumentation\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Preserve project documentation \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAI\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m AI code assistant instructions \u001b[39m\u001b[90m│\u001b[39m\u001b[39m claude \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mLocations\u001b[22m\u001b[39m "] -[44.280377, "o", " "] -[44.280569, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Current directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Destination directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo/star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex repository \u001b[39m\u001b[90m│\u001b[39m\u001b[39m https://github.com/drevops/vortex.git \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex reference \u001b[39m\u001b[90m│\u001b[39m\u001b[39m stable \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└────────────────────────────────────┴─────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n Vortex will be installed into your project's directory \"/home/user/www/demo/star_wars\"\r\r\n"] -[46.285204, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProceed with installing Vortex?\u001b[39m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[48.291277, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProceed with installing Vortex?\u001b[22m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Starting project installation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A"] -[48.291531, "o", "\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33"] -[49.295189, "o", "mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[999D\u001b[2A\u001b[J\u001b[?25h\r\r\n \u001b[34m✦ Downloading Vortex\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloading from \"https://github.com/drevops/vortex.git\" repository at commit \"HEAD\"\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Vortex downloaded (25.10.0)\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your "] -[49.295282, "o", "project\u001b[39m\r\r\n\r\r\n"] -[49.317526, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.395145, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.474095, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.552786, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.631362, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.709716, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.794823, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.873343, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[49.951648, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[50.03009, "o", "\u001b[1G\u001b[2A\u001b[J"] -[50.030168, "o", "\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[50.109915, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[50.188201, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[50.266657, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] -[50.331642, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] -[50.331739, "o", "\r\r\n \u001b[34m✦ Customizing Vortex for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[50.331791, "o", "\r\r\n \u001b[32m✓ Vortex was customized for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[50.331895, "o", "\u001b[?25l"] -[50.334529, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing destination directory\u001b[39m\r\r\n"] -[50.357073, "o", "\u001b[999D\u001b[2A\u001b[J"] -[50.357107, "o", "\u001b[?25h"] -[50.357167, "o", "\r\r\n \u001b[34m✦ Preparing destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[50.357248, "o", "\r\r\n \u001b[2mCreated directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mInitialising a new Git repository in directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n"] -[50.357333, "o", "\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Destination directory is ready\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[50.357512, "o", "\u001b[?25l"] -[50.360021, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCopying files to the destination directory\u001b[39m\r\r\n"] -[50.467015, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] -[50.467136, "o", "\r\r\n \u001b[34m✦ Copying files to the destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[50.467228, "o", "\r\r\n \u001b[32m✓ Files copied to destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[50.467285, "o", "\u001b[?25l"] -[50.469587, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[50.697201, "o", "\u001b[1G\u001b[2A\u001b[J"] -[50.697357, "o", "\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[50.779249, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[50.871299, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[50.966621, "o", "\u001b[1G\u001b[2A\u001b[J"] -[50.966846, "o", "\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] -[51.023631, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] -[51.023807, "o", "\r\r\n \u001b[34m✦ Preparing demo content\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mCreated data directory \"/home/user/www/demo/star_wars/.data\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[51.024032, "o", "\r\r\n \u001b[2mNo database dump file was found in \"/home/user/www/demo/star_wars/.data\" directory.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloaded demo database from https://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Demo content prepared\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] -[51.05361, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mFinished installing Vortex\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m──────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Next steps:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Add and commit all files:\u001b[39m\u001b[39m \u001b[39"] -[51.05369, "o", "m\u001b[90m"] -[51.053819, "o", "│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m cd /home/user/www/demo/star_wars\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git add -A\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git commit -m \"Initial commit.\"\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Build project locally:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m ahoy build\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Setup integration with your hosting and CI/CD providers:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m "] -[51.053902, "o", " See htt"] -[51.05399, "o", "ps://www.vortextemplate.com/docs/getting-started/installation\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└──────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] -[51.064788, "o", "\u001b[?25l"] -[51.067767, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRun the site build now?\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○ Yes /\u001b[22m \u001b[32m●\u001b[39m No \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Takes ~5-10 min; output will be streamed. You can skip and run later with: ahoy build\u001b[39m\r\r\n"] -[53.079357, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRun the site build now?\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m No \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +{"version": 2, "width": 120, "height": 36, "timestamp": 1764660658, "env": {"SHELL": "/opt/homebrew/opt/bash/bin/bash", "TERM": "xterm-256color"}, "title": "Vortex Installer Demo"} +[0.841414, "o", "\r\r\n \u001b[36m──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m ██╗ ██╗ ██████╗ ██████╗ ████████╗ ███████╗ ██╗ ██╗\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██╔═══██╗ ██╔══██╗ ╚══██╔══╝ ██╔════╝ ╚██╗██╔╝\u001b[39m\r\r\n \u001b[36m ██║ ██║ ██║ ██║ ██████╔╝ ██║ █████╗ ╚███╔╝\u001b[39m\r\r\n \u001b[36m ╚██╗ ██╔╝ █"] +[0.841523, "o", "█║ "] +[0.841588, "o", " ██║ ██╔══██╗ ██║ ██╔══╝ ██╔██╗\u001b[39m\r\r\n \u001b[36m ╚████╔╝ ╚██████╔╝ ██║ ██║ ██║ ███████╗ ██╔╝ ██╗\u001b[39m\r\r\n \u001b[36m ╚═══╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═╝\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m Drupal project template\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m by DrevOps\u001b[39m\r\r\n \u001b[36m\u001b[39m\r\r\n \u001b[36m────────────────────────────────────────────────────────────────────────────────────────────────────────────────"] +[0.841623, "o", "──────\u001b[39m\r\r\n \u001b[2m Installer version: development\u001b[22m\r\r\n\r\r\n"] +[0.845958, "o", "\r\r\n \u001b[90m┌──────────────────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mWelcome to the Vortex interactive installer\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m───────────────────────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m This tool will guide you through installing the latest \u001b[4mstable\u001b[0m version of Vortex into\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m your project.\u001b[39m\u001b[39m "] +[0.846039, "o", " \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m You will be asked a few questions to tailor the configuration to your site.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m No changes will be made until you confirm everything at the end.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+C\u001b[39m at any time to exit the installer.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Press \u001b[33mCtrl+U\u001b[39m at any time to go back to the previous step.\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└───────"] +[0.846072, "o", "─────"] +[0.846152, "o", "──────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n \u001b[2mPress any key to continue...\u001b[22m\r\r\n"] +[3.866037, "o", "\r\r\n \u001b[46m\u001b[30m General information \u001b[39m\u001b[49m\r\r\n\r\r\n"] +[3.882799, "o", "\u001b[?25l"] +[3.888159, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[4.043486, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star wa\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Site\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────"] +[4.043636, "o", "──"] +[4.781206, "o", "──────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m S\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m St\u001b[7m \u001b[27m "] +[4.781657, "o", " "] +[4.782813, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[5.784456, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite name \u001b[2m(1/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[5.798878, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite name \u001b[2m(1/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[5.799051, "o", "\u001b[?25h"] +[5.82485, "o", "\u001b[?25l"] +[5.828239, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name for the project directory and in the code.\u001b[39m\r\r\n"] +[6.852015, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mSite machine name \u001b[2m(2/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[6.875763, "o", "\u001b[?25l"] +[6.87863, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star Wars Org\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[7.02972, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Star War\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m\u001b[7mE\u001b[27m.g. My Org\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────"] +[7.029966, "o", "───"] +[8.315377, "o", "───────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m R\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Re\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────"] +[8.315666, "o", "────"] +[8.316008, "o", "─────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[9.317732, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the project and documentation.\u001b[39m\r\r\n"] +[10.321878, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization name \u001b[2m(3/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in the co"] +[10.322255, "o", "de.\u001b[39m\r"] +[10.322793, "o", "\r\r\n"] +[11.327259, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mOrganization machine name \u001b[2m(4/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m rebellion \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Domain name without pro"] +[11.329369, "o", "tocol and"] +[11.332374, "o", " trailing slash.\u001b[39m\r\r\n"] +[12.339811, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPublic domain \u001b[2m(5/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star-wars.com \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Drupal \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m "] +[12.340081, "o", " "] +[12.342523, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloadin"] +[12.342741, "o", "g an exist"] +[13.346275, "o", "ing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDrupal CMS, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Drupal, loaded from the demo database \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────────────"] +[13.347143, "o", "──"] +[13.347242, "o", "───────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇. Applies only on the first run of the installer.\u001b[39m\r\r\n"] +[14.37127, "o", "\u001b[1G\u001b[24A\u001b[J"] +[14.371501, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHow would you like your site to be created on the first run? \u001b[2m(6/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose how your site will be created the first time after this \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m installer finishes: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, installed from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from one of the standard Drupal installation profiles. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal CMS, installed from profile\u001b[22m "] +[14.371523, "o", " \u001b[90m│\u001b"] +[14.371948, "o", "[39m\r\r\n\u001b[90m │\u001b[39m Creates a new site by \u001b[4mpopulating a fresh database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m from the Drupal CMS recipe. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mDrupal, loaded from the demo database\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Creates a site by \u001b[4mloading an existing demo database\u001b[0m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m provided with the installer. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─────────────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Drupal, loaded from the demo database "] +[14.372456, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[15.379188, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProfile \u001b[2m(7/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mMinimal\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mDemo Umami\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCustom (next prompt)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal profile to use.\u001b[39m\r\r\n"] +[16.387369, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProfile \u001b[2m(7/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Standard \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mModules \u001b[2m(8/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Admin toolbar \u001b[36m┃\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCoffee\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig spli"] +[16.387932, "o", "t\u001b[22m "] +[16.389386, "o", " \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mConfig update\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mEnvironment indicator\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPathauto\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedirect\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRobots.txt\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSeckit\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mShield\u001b[22m \u001b[90m│\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────"] +[16.389471, "o", "───"] +[17.394244, "o", "─────────────────────── 11 selected ┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more modules.\u001b[39m\r\r\n\u001b[1G\u001b[14A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mModules \u001b[2m(8/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Admin toolbar \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Coffee \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config split \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Config update \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Environment indicator \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Pathauto \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redirect \u001b[90m│\u001b[39m\r"] +[17.394667, "o", "\r\n\u001b[90m │"] +[17.39499, "o", "\u001b[39m Robots.txt \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Seckit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Shield \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Stage file proxy \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[18.399481, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name in custom modules\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom modules prefix \u001b[2m(9/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m sw \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b"] +[18.399738, "o", "[39m\r\r\n\r\r\n"] +[18.400256, "o", "\u001b[?25h"] +[19.404382, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTheme \u001b[2m(10/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOlivero\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mClaro\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mStark\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select which Drupal theme to use.\u001b[39m\r\r\n\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTheme \u001b[2m(10/29)\u001b[22m\u001b[22m \u001b[90m──────"] +[19.404554, "o", "───"] +[19.404587, "o", "──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom (next prompt) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[20.407938, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m We will use this name as a custom theme name\u001b[39m\r\r\n\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom theme machine name \u001b[2m(11/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m star_wars \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25"] +[20.408101, "o", "h\r\r\n \u001b[46m\u001b"] +[20.40824, "o", "[30m Code repository \u001b[39m\u001b[49m\r\r\n\r\r\n"] +[21.41223, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────"] +[21.412544, "o", "───"] +[21.413356, "o", "──────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your code repository provider.\u001b[39m\r\r\n"] +[22.420813, "o", "\u001b[1G\u001b[11A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRepository provider \u001b[2m(12/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Vortex offers full automation with GitHub, while support for \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m other providers is limited. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────────────────"] +[22.420988, "o", "───"] +[22.421024, "o", "───────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[23.422723, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] +[23.422868, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m "] +[23.423282, "o", " \u001b[90m│\u001b["] +[23.423552, "o", "39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select your version scheme.\u001b[39m\r\r\n"] +[24.4358, "o", "\u001b[1G\u001b[22A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRelease versioning scheme \u001b[2m(13/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Choose your versioning scheme: \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mCalendar Versioning (CalVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4myear.month.patch\u001b[0m (E.g., \u001b[4m24.1.0\u001b[0m) \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://calver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mSemantic Versioning (SemVer)\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[4mmajor.minor.patch\u001b[0m (E.g., \u001b[4m1.0.0\u001b[0m) "] +[24.436106, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m https://semver.org \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom versioning scheme of your choice. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Calendar Versioning (CalVer) \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[25.437616, "o", "\r\r\n \u001b[46m\u001b[30m Environment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mTimezone \u001b[2m(14/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2mUTC\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇, or start typing to select the timezone for your project.\u001b[39m\r\r\n\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mTimezone \u001b[2m(14/29)\u001b[22m\u001b[22m \u001b[90m"] +[25.437968, "o", "────"] +[25.438385, "o", "────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m UTC \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[26.442409, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mServices \u001b[2m(15/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mSolr\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRedis\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more services.\u001b[39m\r\r\n\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mServices \u001b[2m(15/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────────────┐\u001b"] +[26.444439, "o", "[39m\r\r\n\u001b["] +[26.44603, "o", "90m │\u001b[39m ClamAV \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Solr \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Redis \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[27.447981, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPStan\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mRector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHP Mess Detector\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mPHPUnit\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mBehat\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └───────────────────────────────────────────────────"] +[27.448246, "o", "───"] +[27.448931, "o", "────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more tools.\u001b[39m\r\r\n"] +[28.46156, "o", "\u001b[1G\u001b[10A\u001b[J"] +[28.461693, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDevelopment tools \u001b[2m(16/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP CodeSniffer \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPStan \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Rector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHP Mess Detector \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m PHPUnit \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Behat \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[29.465223, "o", "\r\r\n \u001b[46m\u001b[30m Hosting \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia Cloud\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mOther\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select your hosting provider.\u001b[39m\r\r\n"] +[30.466712, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mHosting provider \u001b[2m(17/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m None \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web\u001b[7m \u001b[27m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Custom directory where the web se"] +[30.466868, "o", "rver serv"] +[30.46698, "o", "es the site.\u001b[39m\r\r\n"] +[31.472441, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mCustom web root directory \u001b[2m(18/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m web \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Deployment \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[39m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m ◻ Code artifact \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mLagoon webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mContainer image\u001b[22m "] +[31.472754, "o", " \u001b[9"] +[31.473081, "o", "0m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m◼\u001b[39m \u001b[2mCustom webhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more deployment types.\u001b[39m\r\r\n"] +[32.477362, "o", "\u001b[1G\u001b[8A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDeployment types \u001b[2m(19/29)\u001b[22m\u001b[22m \u001b[90m────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Custom webhook \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Workflow \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProvision type \u001b[2m(20/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an environment using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │"] +[32.477988, "o", "\u001b[39m "] +[32.480754, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├─"] +[32.480971, "o", "───"] +[33.483389, "o", "──────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the provision type.\u001b[39m\r\r\n\u001b[1G\u001b[21A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProvision type \u001b[2m(20/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisioning sets up the site in an e"] +[33.483595, "o", "nvironment"] +[33.483911, "o", " using an \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m already assembled codebase. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mImport from database dump\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by importing a database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m typically copied from production into lower \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m environments. \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m ○ \u001b[1mInstall from profile\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m Provisions the site by installing a fresh Drupal \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m site from a profile every time an environment is \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m created. "] +[33.484278, "o", " "] +[34.487757, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[90m│\u001b[39m\r\r\n\u001b[90m ├──────────────────────────────────────────────────────────────┤\u001b[39m\r\r\n\u001b[90m │\u001b[39m Import from database dump \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mFTP download\u001b[22m "] +[34.487863, "o", " "] +[34.487923, "o", " \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mAcquia backup\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mLagoon environment\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mContainer registry\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the database download source.\u001b[39m\r\r\n"] +[35.508326, "o", "\u001b[1G\u001b[10A\u001b[J"] +[35.508471, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDatabase source \u001b[2m(21/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m URL download \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[36.51256, "o", "\r\r\n \u001b[46m\u001b[30m Notifications \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[39m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m› ◼\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mGitHub\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mJIRA\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mNew Relic\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mSlack\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m◻\u001b[22m \u001b[2mWebhook\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └────────────────────────────────────────"] +[36.513313, "o", "────"] +[36.513344, "o", "──────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆, ⬇ and Space bar to select one or more notification channels.\u001b[39m\r\r\n"] +[37.51677, "o", "\u001b[1G\u001b[10A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mNotification channels \u001b[2m(22/29)\u001b[22m\u001b[22m \u001b[90m───────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Email \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Continuous Integration \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mCircleCI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m "] +[37.517051, "o", " \u001b["] +[37.517452, "o", "90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the CI provider.\u001b[39m\r\r\n"] +[38.522235, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mContinuous Integration provider \u001b[2m(23/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m GitHub Actions \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Automations \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mRenovate self-hosted in CI\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r"] +[38.522565, "o", "\r\n\u001b[90m └"] +[38.523624, "o", "──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Use ⬆ and ⬇ to select the dependency updates provider.\u001b[39m\r\r\n"] +[39.52549, "o", "\u001b[1G\u001b[7A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mDependency updates provider \u001b[2m(24/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Renovate GitHub app \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[39m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep the PRs organized.\u001b[39m\r\r\n"] +[40.530026, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-assign the author to their PR? \u001b[2m(25/29)\u001b[22m\u001b[22m \u001b[90m─────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[39m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to keep quickly identify PRs that need attention.\u001b[39m\r\r\n"] +[41.531194, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAuto-add a CONFLICT label to a PR when conflicts occur? \u001b[2m(26/29)\u001b[22m\u001b[22m \u001b[90m┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └─────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Documentation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[39m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Helps to maintain the project documentation"] +[41.531377, "o", " within the "] +[41.5314, "o", "repository.\u001b[39m\r\r\n"] +[42.533393, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mPreserve project documentation? \u001b[2m(27/29)\u001b[22m\u001b[22m \u001b[90m─────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m AI \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[39m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[36m›\u001b[39m \u001b[36m●\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○\u001b[22m \u001b[2mNone\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────"] +[42.533615, "o", "────"] +[42.533763, "o", "────────────────────┘\u001b[39m\r\r\n\u001b[90m Provides AI coding assistants with better context about the project.\u001b[39m\r\r\n"] +[43.538126, "o", "\u001b[1G\u001b[6A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mAI code assistant instructions \u001b[2m(28/29)\u001b[22m\u001b[22m \u001b[90m──────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Anthropic Claude \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m Installation summary \u001b[39m\u001b[49m\r\r\n \u001b[46m\u001b[30m \u001b[39m\u001b[49m\r\r\n\r\r\n\r\r\n \u001b[90m┌────────────────────────────────────┬─────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mGeneral information\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m"] +[43.538638, "o", "│\u001b[39m\r\r\n \u001b"] +[43.540167, "o", "[90m│\u001b[39m\u001b[39m Site name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Star Wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Site machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Organization machine name \u001b[39m\u001b[90m│\u001b[39m\u001b[39m rebellion \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Public domain \u001b[39m\u001b[90m│\u001b[39m\u001b[39m star-wars.com \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDrupal\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Starter \u001b[39m\u001b[90m│\u001b[39m\u001b[39m load_demodb \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Modul"] +[43.541217, "o", "es "] +[44.541043, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m admin_toolbar, coffee, config_split,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m config_update, environment_indicator,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m pathauto, redirect, robotstxt, seckit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m shield, stage_file_proxy \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Webroot \u001b[39m\u001b[90m│\u001b[39m\u001b[39m web \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Profile \u001b[39m\u001b[90m│\u001b[39m\u001b[39m standard \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Module prefix \u001b[39m\u001b[90m│\u001b[39m\u001b[39m sw \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Theme machine name "] +[44.541148, "o", " "] +[44.541321, "o", "\u001b[39m\u001b[90m│\u001b[39m\u001b[39m star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mCode repository\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Code provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m github \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Version scheme \u001b[39m\u001b[90m│\u001b[39m\u001b[39m calver \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mEnvironment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Timezone \u001b[39m\u001b[90m│\u001b[39m\u001b[39m UTC \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Services \u001b[39m\u001b[90m│\u001b[39m\u001b[39m clamav, redis, solr \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Tools \u001b[39m\u001b"] +[44.541391, "o", "[90m│\u001b[39m\u001b[39m phpcs, phpmd, phpstan, rector, phpunit,\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m behat \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mHosting\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Hosting provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m none \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDeployment\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Deployment types \u001b[39m\u001b[90m│\u001b[39m\u001b[39m webhook \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mWorkflow\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Provision type "] +[44.541471, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m database \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Database source \u001b[39m\u001b[90m│\u001b[39m\u001b[39m url \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mNotifications\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Channels \u001b[39m\u001b[90m│\u001b[39m\u001b[39m email \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mContinuous Integration\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m CI provider \u001b[39m\u001b[90m│\u001b[39m\u001b[39m gha \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAutomations\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Dep"] +[44.541504, "o", "endency updates provi"] +[44.541598, "o", "der \u001b[39m\u001b[90m│\u001b[39m\u001b[39m renovatebot_app \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-assign PR author \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Auto-add a CONFLICT label to PRs \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mDocumentation\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Preserve project documentation \u001b[39m\u001b[90m│\u001b[39m\u001b[39m Yes \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mAI\u001b[22m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m AI code assistant instructions \u001b[39m\u001b[90m│\u001b[39m\u001b[39m claude \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[36m\u001b[1mLocations\u001b[22m\u001b[39m "] +[44.541648, "o", " "] +[44.541748, "o", " \u001b[39m\u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Current directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Destination directory \u001b[39m\u001b[90m│\u001b[39m\u001b[39m /home/user/www/demo/star_wars \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex repository \u001b[39m\u001b[90m│\u001b[39m\u001b[39m https://github.com/drevops/vortex.git \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Vortex reference \u001b[39m\u001b[90m│\u001b[39m\u001b[39m stable \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└────────────────────────────────────┴─────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n Vortex will be installed into your project's directory \"/home/user/www/demo/star_wars\"\r\r\n"] +[46.54542, "o", "\u001b[?25l\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mProceed with installing Vortex?\u001b[39m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[32m●\u001b[39m Yes \u001b[2m/ ○ No\u001b[22m \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[48.547474, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mProceed with installing Vortex?\u001b[22m \u001b[90m─────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m Yes \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h\r\r\n \u001b[46m\u001b[30m Starting project installation \u001b[39m\u001b[49m\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A"] +[48.552249, "o", "\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33"] +[49.55125, "o", "mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mDownloading Vortex\u001b[39m\r\r\n\u001b[999D\u001b[2A\u001b[J\u001b[?25h\r\r\n \u001b[34m✦ Downloading Vortex\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloading from \"https://github.com/drevops/vortex.git\" repository at commit \"HEAD\"\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Vortex downloaded (25.10.0)\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m"] +[49.55139, "o", " \u001b[33mCustomizing Vortex for your projec"] +[49.551457, "o", "t\u001b[39m\r\r\n"] +[49.630872, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.710724, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.789418, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.867882, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[49.945315, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.02445, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.102266, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.181219, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠤\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.259517, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠄\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.33792, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠆\u001b[39m \u001b[33mCustomizing Vortex for your project\u001b[39m\r\r\n"] +[50.4053, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] +[50.405412, "o", "\r\r\n \u001b[34m✦ Customizing Vortex for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[32m✓ Vortex was customized for your project\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.405481, "o", "\u001b[?25l"] +[50.407836, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing destination directory\u001b[39m\r\r\n"] +[50.43339, "o", "\u001b[999D\u001b[2A\u001b[J"] +[50.433505, "o", "\u001b[?25h\r\r\n \u001b[34m✦ Preparing destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.433562, "o", "\r\r\n \u001b[2mCreated directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mInitialising a new Git repository in directory \"/home/user/www/demo/star_wars\".\u001b[22m\r\r\n\r\r\n"] +[50.433636, "o", "\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.433705, "o", "\r\r\n \u001b[32m✓ Destination directory is ready\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l"] +[50.436018, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mCopying files to the destination directory\u001b[39m\r\r\n"] +[50.544688, "o", "\u001b[999D\u001b[2A\u001b[J\u001b[?25h"] +[50.544783, "o", "\r\r\n \u001b[34m✦ Copying files to the destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[50.544828, "o", "\r\r\n \u001b[32m✓ Files copied to destination directory\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\u001b[?25l"] +[50.547326, "o", "\r\r\n \u001b[36m⠂\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.703662, "o", "\u001b[1G\u001b[2A\u001b[J"] +[50.70377, "o", "\r\r\n \u001b[36m⠒\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.782612, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠐\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.874655, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠰\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[50.985615, "o", "\u001b[1G\u001b[2A\u001b[J\r\r\n \u001b[36m⠠\u001b[39m \u001b[33mPreparing demo content\u001b[39m\r\r\n"] +[51.050122, "o", "\u001b[999D\u001b[2A\u001b[J"] +[51.050317, "o", "\u001b[?25h"] +[51.050524, "o", "\r\r\n \u001b[34m✦ Preparing demo content\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mCreated data directory \"/home/user/www/demo/star_wars/.data\".\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[51.050747, "o", "\r\r\n \u001b[2mNo database dump file was found in \"/home/user/www/demo/star_wars/.data\" directory.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n\r\r\n \u001b[2mDownloaded demo database from https://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.\u001b[22m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[51.050937, "o", "\r\r\n \u001b[32m✓ Demo content prepared\u001b[39m\r\r\n\r\r\n\r\r\n \u001b[A\u001b[A\u001b[A\u001b[A\r\r\n\r\r\n"] +[51.073816, "o", "\r\r\n \u001b[90m┌───────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mFinished installing Vortex\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m──────────────────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Add and commit all files:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git add -A\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m git commit -m \"Initial commit.\"\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└───────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] +[51.091305, "o", "\u001b[?25l"] +[51.093791, "o", "\r\r\n\u001b[90m ┌\u001b[39m \u001b[36mRun the site build now?\u001b[39m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m \u001b[2m○ Yes /\u001b[22m \u001b[32m●\u001b[39m No \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\u001b[90m Takes ~5-10 min; output will be streamed. You can skip and run later with: ahoy build\u001b[39m\r\r\n"] +[53.112736, "o", "\u001b[1G\u001b[5A\u001b[J\r\r\n\u001b[90m ┌\u001b[39m \u001b[2mRun the site build now?\u001b[22m \u001b[90m─────────────────────────────────────┐\u001b[39m\r\r\n\u001b[90m │\u001b[39m No \u001b[90m│\u001b[39m\r\r\n\u001b[90m └──────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n\u001b[?25h"] +[53.125829, "o", "\r\r\n \u001b[90m┌────────────────────────────────────────────────────────────────────────────────────────┐\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32mReady to build\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[32m──────────────\u001b[39m\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Build the site:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m ahoy build\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m"] +[53.125952, "o", " \u001b[39m\u001b"] +[53.125992, "o", "[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m Setup GitHub Actions:\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m https://www.vortextemplate.com/docs/continuous-integration/github-actions#onboarding\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m│\u001b[39m\u001b[39m \u001b[39m\u001b[90m│\u001b[39m\r\r\n \u001b[90m└────────────────────────────────────────────────────────────────────────────────────────┘\u001b[39m\r\r\n\r\r\n"] diff --git a/.vortex/docs/static/img/installer.svg b/.vortex/docs/static/img/installer.svg index 45749e6f2..767182f6e 100644 --- a/.vortex/docs/static/img/installer.svg +++ b/.vortex/docs/static/img/installer.svg @@ -1 +1 @@ -──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────██╗██╗██████╗██████╗████████╗███████╗██╗██╗██║██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝██║██║██║██║██████╔╝██║█████╗╚███╔╝╚██╗██╔╝██║██║██╔══██╗██║██╔══╝██╔██╗╚████╔╝╚██████╔╝██║██║██║███████╗██╔╝██╗╚═══╝╚═════╝╚═╝╚═╝╚═╝╚══════╝╚═╝╚═╝DrupalprojecttemplatebyDrevOpsInstallerversion:development┌──────────────────────────────────────────────────────────────────────────────────────┐WelcometotheVortexinteractiveinstaller───────────────────────────────────────────ThistoolwillguideyouthroughinstallingthelateststableversionofVortexintoyourproject.yourproject.Youwillbeaskedafewquestionstotailortheconfigurationtoyoursite.Nochangeswillbemadeuntilyouconfirmeverythingattheend.PressCtrl+Catanytimetoexittheinstaller.PressCtrl+Uatanytimetogobacktothepreviousstep.└─────────└──────────────────────────────────────────────────────────────────────────────────────┘Pressanykeytocontinue... General information Sitename(1/29)────────────────────────────────────────────┐└──────────────────────────────────────────────────────────────┘Wewillusethisnameintheprojectanddocumentation.E.g.MySiteSt Sitename(1/29)────────────────────────────────────────────┐StarWarsSitemachinename(2/29)────────────────────────────────────┐star_warsOrganizationname(3/29)────────────────────────────────────┐E.g.MyOrg└────────────────────────────────────────────Re Organizationname(3/29)────────────────────────────────────┐RebellionOrganizationmachinename(4/29)────────────────────────────┐rebellion Wewillusethisnameinthecode.Organizationmachinename(4/29)────────────────────────────┐rebellionPublicdomain(5/29)────────────────────────────────────────┐star-wars.com Publicdomain(5/29)────────────────────────────────────────┐star-wars.com Drupal Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Choosehowyoursitewillbecreatedthefirsttimeafterthisinstallerfinishes:Drupal,installedfromprofileCreatesanewsitebypopulatingafreshdatabasefromoneofthestandardDrupalinstallationprofiles.DrupalCMS,installedfromprofilefromtheDrupalCMSrecipe.Drupal,loadedfromthedemodatabaseCreatesasitebyloadinganexistingdemodatabaseprovidedwiththeinstaller.├─────────────────────────────────────────────────────────────────────┤Drupal,installedfromprofileDrupalCMS,installedfromprofileDrupal,loadedfromthedemodatabase└─────────────────────────────────────────────────────────────────────┘Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Drupal,loadedfromthedemodatabaseDrupal,loadedfromthedemodatabaseProfile(7/29)──────────────────────────────────────────────┐StandardModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldStagefileproxyCustommodulesprefix(9/29)────────────────────────────────┐swTheme(10/29)───────────────────────────────────────────────┐Custom(nextprompt)Customthememachinename(11/29)───────────────────────────┐ Code repository Repositoryprovider(12/29)─────────────────────────────────┐VortexoffersfullautomationwithGitHub,whilesupportforotherprovidersislimited.├──────────────────────────────────────────────────────────────┤GitHubOtherRepositoryprovider(12/29)─────────────────────────────────┐GitHubReleaseversioningscheme(13/29)───────────────────────────┐Chooseyourversioningscheme:CalendarVersioning(CalVer)year.month.patch(E.g.,24.1.0)https://calver.orgSemanticVersioning(SemVer)major.minor.patch(E.g.,1.0.0)major.minor.patch(E.g.,1.0.0)https://semver.orgOtherCustomversioningschemeofyourchoice.CalendarVersioning(CalVer)SemanticVersioning(SemVer)Releaseversioningscheme(13/29)───────────────────────────┐CalendarVersioning(CalVer) Environment Timezone(14/29)────────────────────────────────────────────┐UTCServices(15/29)────────────────────────────────────────────┐ClamAVSolrRedisDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehatDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehat Hosting Hostingprovider(17/29)────────────────────────────────────┐NoneCustomwebrootdirectory(18/29)───────────────────────────┐web Customwebrootdirectory(18/29)───────────────────────────┐web Deployment Deploymenttypes(19/29)────────────────────────────────────┐CodeartifactLagoonwebhookContainerimageDeploymenttypes(19/29)────────────────────────────────────┐Customwebhook Workflow Provisiontype(20/29)──────────────────────────────────────┐Provisioningsetsupthesiteinanenvironmentusinganalreadyassembledcodebase.ImportfromdatabasedumpProvisionsthesitebyimportingadatabasedumptypicallycopiedfromproductionintolowerenvironments.InstallfromprofileProvisionsthesitebyinstallingafreshDrupalsitefromaprofileeverytimeanenvironmentiscreated.Provisiontype(20/29)──────────────────────────────────────┐created.ImportfromdatabasedumpDatabasesource(21/29)─────────────────────────────────────┐URLdownloadFTPdownloadNoneDatabasesource(21/29)─────────────────────────────────────┐URLdownload Notifications Notificationchannels(22/29)───────────────────────────────┐EmailGitHubJIRANewRelicSlackWebhookNotificationchannels(22/29)───────────────────────────────┐Email Continuous Integration ContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActionsCircleCINoneContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActions Automations Dependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappRenovateself-hostedinCIDependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappYes/NoAuto-assigntheauthortotheirPR?(25/29)─────────────────┐Yes└─────────────────────────────────────────────────────────────────┘Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes Documentation Preserveprojectdocumentation?(27/29)─────────────────────┐Preserveprojectdocumentation?(27/29)─────────────────────┐ AI AIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaudeAIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaude Installation summary ┌────────────────────────────────────┬─────────────────────────────────────────┐GeneralinformationSitenameStarWarsSitemachinenamestar_warsOrganizationnameRebellionOrganizationmachinenamerebellionPublicdomainstar-wars.comDrupalStarterload_demodbModulesadmin_toolbar,coffee,config_split,config_update,environment_indicator,pathauto,redirect,robotstxt,seckit,shield,stage_file_proxyWebrootwebProfilestandardModuleprefixswThememachinenameThememachinenamestar_warsCoderepositoryCodeprovidergithubVersionschemecalverEnvironmentTimezoneUTCServicesclamav,redis,solrToolsphpcs,phpmd,phpstan,rector,phpunit,behatHostingHostingprovidernoneDeploymentDeploymenttypeswebhookWorkflowProvisiontypeProvisiontypedatabaseDatabasesourceurlNotificationsChannelsemailContinuousIntegrationCIproviderghaAutomationsDependencyupdatesproviderrenovatebot_appAuto-assignPRauthorYesAuto-addaCONFLICTlabeltoPRsYesDocumentationPreserveprojectdocumentationYesAIAIcodeassistantinstructionsclaudeLocationsLocationsCurrentdirectory/home/user/www/demoDestinationdirectory/home/user/www/demo/star_warsVortexrepositoryhttps://github.com/drevops/vortex.gitVortexreferencestable└────────────────────────────────────┴─────────────────────────────────────────┘Vortexwillbeinstalledintoyourproject'sdirectory"/home/user/www/demo/star_wars"ProceedwithinstallingVortex?─────────────────────────────┐ Starting project installation DownloadingVortexDownloadingfrom"https://github.com/drevops/vortex.git"repositoryatcommit"HEAD"Vortexdownloaded(25.10.0)CustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectVortexwascustomizedforyourprojectPreparingdestinationdirectoryCreateddirectory"/home/user/www/demo/star_wars".InitialisinganewGitrepositoryindirectory"/home/user/www/demo/star_wars".DestinationdirectoryisreadyCopyingfilestothedestinationdirectoryFilescopiedtodestinationdirectoryPreparingdemocontentCreateddatadirectory"/home/user/www/demo/star_wars/.data".Nodatabasedumpfilewasfoundin"/home/user/www/demo/star_wars/.data"directory.Downloadeddemodatabasefromhttps://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.Democontentprepared┌──────────────────────────────────────────────────────────────────────────┐FinishedinstallingVortex──────────────────────────Nextsteps:Addandcommitallfiles:Addandcommitallfiles:cd/home/user/www/demo/star_warsgitadd-Agitcommit-m"Initialcommit."Buildprojectlocally:ahoybuildSetupintegrationwithyourhostingandCI/CDproviders:Seehttps://www.vortextemplate.com/docs/getting-started/installation└──────────────────────────────────────────────────────────────────────────┘╚██╗██╔╝╚██╗██╔╝██║────────────────────────────────────────────────────────────────────────────────────────────────────────────────starwars └──────────────────────────────────└────────────────────────────────────St StarWars Sitemachinename(2/29)────────────────────────────────────┐star_wars Wewillusethisnamefortheprojectdirectoryandinthecode.StarWarsOrg └───────────────────────────────────────────────└─────Rebellion WewillusethisnameinthecoDomainnamewithoutproDomainnamewithoutprotocolandtrailingslash.CreatesasitebyloadinCreatesasitebyloadinganexist└────────────────────────────────────────────────────────────└──────────────────────────────────────────────────────────────Useand⬇.Appliesonlyonthefirstrunoftheinstaller.DrupalCMS,installedfromprofileProfile(7/29)──────────────────────────────────────────────┐StandardMinimalDemoUmamiCustom(nextprompt)UseandtoselectwhichDrupalprofiletouse.ConfigspliConfigsplit└───────────────────────└──────────────────────────Theme(10/29)──────Theme(10/29)─────────└────────────Useandtoselectyourcoderepositoryprovider.└────────────────────────────────────────────────────────└───────────────────────────────────────────────────────────OtherUseandtoselectyourversionscheme.Timezone(14/29)Timezone(14/29)────└───────────────────────────────────────────────────└──────────────────────────────────────────────────────Use⬆,andSpacebartoselectoneormoretools.Hostingprovider(17/29)────────────────────────────────────┐AcquiaCloudLagoonNoneUse⬆,andSpacebartoselectyourhostingprovider.CustomdirectorywherethewebseCustomdirectorywherethewebserverservCustomdirectorywherethewebserverservesthesite.ContainerimageCustomwebhookUse⬆,andSpacebartoselectoneormoredeploymenttypes.├─├────ProvisioningsetsupthesiteinaneProvisioningsetsupthesiteinanenvironmentFTPdownloadAcquiabackupLagoonenvironmentContainerregistryUseandtoselectthedatabasedownloadsource.└────────────────────────────────────────Use⬆,andSpacebartoselectoneormorenotificationchannels.UseandtoselecttheCIprovider.Useandtoselectthedependencyupdatesprovider.Auto-assigntheauthortotheirPR?(25/29)─────────────────┐HelpstokeepthePRsorganized.Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes/NoHelpstokeepquicklyidentifyPRsthatneedattention.HelpstomaintaintheprojectdocumentationHelpstomaintaintheprojectdocumentationwithintheHelpstomaintaintheprojectdocumentationwithintherepository.└──────────────────────────────────────└──────────────────────────────────────────ProvidesAIcodingassistantswithbettercontextabouttheproject.GeneralinformationModulModulesToolsToolsDependencyupdateDependencyupdatesproviProceedwithinstallingVortex?─────────────────────────────┐DownloadingVortexCustomizingVortexforyourCustomizingVortexforyourprojectCustomizingVortexforyourprojectPreparingdestinationdirectoryCopyingfilestothedestinationdirectoryPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentSeehttRunthesitebuildnow?─────────────────────────────────────┐Yes/NoTakes~5-10min;outputwillbestreamed.Youcanskipandrunlaterwith:ahoybuildRunthesitebuildnow?─────────────────────────────────────┐No \ No newline at end of file +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────██╗██╗██████╗██████╗████████╗███████╗██╗██╗██║██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝██║██║██║██║██████╔╝██║█████╗╚███╔╝╚██╗██╔╝██║██║██╔══██╗██║██╔══╝██╔██╗╚████╔╝╚██████╔╝██║██║██║███████╗██╔╝██╗╚═══╝╚═════╝╚═╝╚═╝╚═╝╚══════╝╚═╝╚═╝DrupalprojecttemplatebyDrevOpsInstallerversion:development┌──────────────────────────────────────────────────────────────────────────────────────┐WelcometotheVortexinteractiveinstaller───────────────────────────────────────────ThistoolwillguideyouthroughinstallingthelateststableversionofVortexintoyourproject.Youwillbeaskedafewquestionstotailortheconfigurationtoyoursite.Nochangeswillbemadeuntilyouconfirmeverythingattheend.PressCtrl+Catanytimetoexittheinstaller.PressCtrl+Uatanytimetogobacktothepreviousstep.└────────────└──────────────────────────────────────────────────────────────────────────────────────┘Pressanykeytocontinue... General information Sitename(1/29)────────────────────────────────────────────┐└──────────────────────────────────────────────────────────────┘Wewillusethisnameintheprojectanddocumentation.E.g.MySiteSt Sitename(1/29)────────────────────────────────────────────┐StarWarsSitemachinename(2/29)────────────────────────────────────┐star_warsOrganizationname(3/29)────────────────────────────────────┐E.g.MyOrg└────────────────────────────────────────────Re └─────────Organizationname(3/29)────────────────────────────────────┐RebellionOrganizationmachinename(4/29)────────────────────────────┐rebellion Wewillusethisnameinthecode.Organizationmachinename(4/29)────────────────────────────┐rebellionPublicdomain(5/29)────────────────────────────────────────┐star-wars.com Publicdomain(5/29)────────────────────────────────────────┐star-wars.com Drupal Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Choosehowyoursitewillbecreatedthefirsttimeafterthisinstallerfinishes:Drupal,installedfromprofileCreatesanewsitebypopulatingafreshdatabasefromoneofthestandardDrupalinstallationprofiles.DrupalCMS,installedfromprofilefromtheDrupalCMSrecipe.Drupal,loadedfromthedemodatabaseCreatesasitebyloadinganexistingdemodatabaseprovidedwiththeinstaller.├─────────────────────────────────────────────────────────────────────┤Drupal,installedfromprofileDrupalCMS,installedfromprofileDrupal,loadedfromthedemodatabase└─────────────────────────────────────────────────────────────────────┘Howwouldyoulikeyoursitetobecreatedonthefirstrun?(6/29)Drupal,loadedfromthedemodatabaseProfile(7/29)──────────────────────────────────────────────┐StandardModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldModules(8/29)──────────────────────────────────────────────┐AdmintoolbarCoffeeConfigsplitConfigupdateEnvironmentindicatorPathautoRedirectRobots.txtSeckitShieldStagefileproxyCustommodulesprefix(9/29)────────────────────────────────┐swTheme(10/29)───────────────────────────────────────────────┐Custom(nextprompt)Customthememachinename(11/29)───────────────────────────┐ Code repository Repositoryprovider(12/29)─────────────────────────────────┐VortexoffersfullautomationwithGitHub,whilesupportforotherprovidersislimited.├──────────────────────────────────────────────────────────────┤GitHubOtherRepositoryprovider(12/29)─────────────────────────────────┐GitHubReleaseversioningscheme(13/29)───────────────────────────┐Chooseyourversioningscheme:CalendarVersioning(CalVer)year.month.patch(E.g.,24.1.0)https://calver.orgSemanticVersioning(SemVer)major.minor.patch(E.g.,1.0.0)major.minor.patch(E.g.,1.0.0)https://semver.orgOtherCustomversioningschemeofyourchoice.CalendarVersioning(CalVer)SemanticVersioning(SemVer)Releaseversioningscheme(13/29)───────────────────────────┐CalendarVersioning(CalVer) Environment Timezone(14/29)────────────────────────────────────────────┐UTCServices(15/29)────────────────────────────────────────────┐ClamAVSolrRedisDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehatDevelopmenttools(16/29)───────────────────────────────────┐PHPCodeSnifferPHPStanRectorPHPMessDetectorPHPUnitBehat Hosting Hostingprovider(17/29)────────────────────────────────────┐NoneCustomwebrootdirectory(18/29)───────────────────────────┐web Customwebrootdirectory(18/29)───────────────────────────┐web Deployment Deploymenttypes(19/29)────────────────────────────────────┐CodeartifactLagoonwebhookContainerimageDeploymenttypes(19/29)────────────────────────────────────┐Customwebhook Workflow Provisiontype(20/29)──────────────────────────────────────┐Provisioningsetsupthesiteinanenvironmentusinganalreadyassembledcodebase.ImportfromdatabasedumpProvisionsthesitebyimportingadatabasedumptypicallycopiedfromproductionintolowerenvironments.InstallfromprofileProvisionsthesitebyinstallingafreshDrupalsitefromaprofileeverytimeanenvironmentiscreated.Provisiontype(20/29)──────────────────────────────────────┐created.ImportfromdatabasedumpDatabasesource(21/29)─────────────────────────────────────┐URLdownloadFTPdownloadNoneDatabasesource(21/29)─────────────────────────────────────┐URLdownload Notifications Notificationchannels(22/29)───────────────────────────────┐EmailGitHubJIRANewRelicSlackWebhookNotificationchannels(22/29)───────────────────────────────┐Email Continuous Integration ContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActionsCircleCINoneContinuousIntegrationprovider(23/29)─────────────────────┐GitHubActions Automations Dependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappRenovateself-hostedinCIDependencyupdatesprovider(24/29)─────────────────────────┐RenovateGitHubappYes/NoAuto-assigntheauthortotheirPR?(25/29)─────────────────┐Yes└─────────────────────────────────────────────────────────────────┘Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes Documentation Preserveprojectdocumentation?(27/29)─────────────────────┐Preserveprojectdocumentation?(27/29)─────────────────────┐ AI AIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaudeAIcodeassistantinstructions(28/29)──────────────────────┐AnthropicClaude Installation summary ┌────────────────────────────────────┬─────────────────────────────────────────┐GeneralinformationSitenameStarWarsSitemachinenamestar_warsOrganizationnameRebellionOrganizationmachinenamerebellionPublicdomainstar-wars.comDrupalStarterload_demodbModulesadmin_toolbar,coffee,config_split,config_update,environment_indicator,pathauto,redirect,robotstxt,seckit,shield,stage_file_proxyWebrootwebProfilestandardModuleprefixswThememachinenameThememachinenamestar_warsCoderepositoryCodeprovidergithubVersionschemecalverEnvironmentTimezoneUTCServicesclamav,redis,solrToolsphpcs,phpmd,phpstan,rector,phpunit,behatHostingHostingprovidernoneDeploymentDeploymenttypeswebhookWorkflowProvisiontypedatabaseDatabasesourceurlNotificationsChannelsemailContinuousIntegrationCIproviderghaAutomationsDependencyupdatesproviderrenovatebot_appAuto-assignPRauthorYesAuto-addaCONFLICTlabeltoPRsYesDocumentationPreserveprojectdocumentationYesAIAIcodeassistantinstructionsclaudeLocationsLocationsCurrentdirectory/home/user/www/demoDestinationdirectory/home/user/www/demo/star_warsVortexrepositoryhttps://github.com/drevops/vortex.gitVortexreferencestable└────────────────────────────────────┴─────────────────────────────────────────┘Vortexwillbeinstalledintoyourproject'sdirectory"/home/user/www/demo/star_wars"ProceedwithinstallingVortex?─────────────────────────────┐ Starting project installation DownloadingVortexDownloadingfrom"https://github.com/drevops/vortex.git"repositoryatcommit"HEAD"Vortexdownloaded(25.10.0)CustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectVortexwascustomizedforyourprojectPreparingdestinationdirectoryCreateddirectory"/home/user/www/demo/star_wars".InitialisinganewGitrepositoryindirectory"/home/user/www/demo/star_wars".DestinationdirectoryisreadyCopyingfilestothedestinationdirectoryFilescopiedtodestinationdirectoryPreparingdemocontentCreateddatadirectory"/home/user/www/demo/star_wars/.data".Nodatabasedumpfilewasfoundin"/home/user/www/demo/star_wars/.data"directory.Downloadeddemodatabasefromhttps://github.com/drevops/vortex/releases/download/25.4.0/db_d11.demo.sql.Democontentprepared┌───────────────────────────────────┐FinishedinstallingVortex──────────────────────────Addandcommitallfiles:gitadd-Agitcommit-m"Initialcommit."└───────────────────────────────────┘Runthesitebuildnow?─────────────────────────────────────┐No┌────────────────────────────────────────────────────────────────────────────────────────┐Readytobuild──────────────Buildthesite:ahoybuild╚██╗██╔╝╚██╗██╔╝██║────────────────────────────────────────────────────────────────────────────────────────────────────────────────yourproject.└───────starwars └──────────────────────────────────└────────────────────────────────────St StarWars Sitemachinename(2/29)────────────────────────────────────┐star_wars Wewillusethisnamefortheprojectdirectoryandinthecode.StarWarsOrg └───────────────────────────────────────────────└─────Rebellion WewillusethisnameinthecoDomainnamewithoutproDomainnamewithoutprotocolandDomainnamewithoutprotocolandtrailingslash.CreatesasitebyloadinCreatesasitebyloadinganexist└────────────────────────────────────────────────────────────└──────────────────────────────────────────────────────────────Useand⬇.Appliesonlyonthefirstrunoftheinstaller.DrupalCMS,installedfromprofileDrupal,loadedfromthedemodatabaseProfile(7/29)──────────────────────────────────────────────┐StandardMinimalDemoUmamiCustom(nextprompt)UseandtoselectwhichDrupalprofiletouse.ConfigspliConfigsplit└───────────────────────└──────────────────────────Theme(10/29)──────Theme(10/29)─────────Useandtoselectyourcoderepositoryprovider.└────────────────────────────────────────────────────└───────────────────────────────────────────────────────OtherUseandtoselectyourversionscheme.Timezone(14/29)Timezone(14/29)────└───────────────────────────────────────────────────└──────────────────────────────────────────────────────Use⬆,andSpacebartoselectoneormoretools.Hostingprovider(17/29)────────────────────────────────────┐AcquiaCloudLagoonNoneUse⬆,andSpacebartoselectyourhostingprovider.CustomdirectorywherethewebseCustomdirectorywherethewebserverservCustomdirectorywherethewebserverservesthesite.ContainerimageCustomwebhookUse⬆,andSpacebartoselectoneormoredeploymenttypes.├─├────ProvisioningsetsupthesiteinaneProvisioningsetsupthesiteinanenvironmentFTPdownloadAcquiabackupLagoonenvironmentContainerregistryUseandtoselectthedatabasedownloadsource.└────────────────────────────────────────Use⬆,andSpacebartoselectoneormorenotificationchannels.UseandtoselecttheCIprovider.Useandtoselectthedependencyupdatesprovider.Auto-assigntheauthortotheirPR?(25/29)─────────────────┐HelpstokeepthePRsorganized.Auto-addaCONFLICTlabeltoaPRwhenconflictsoccur?(26/29)Yes/NoHelpstokeepquicklyidentifyPRsthatneedattention.HelpstomaintaintheprojectdocumentationHelpstomaintaintheprojectdocumentationwithintheHelpstomaintaintheprojectdocumentationwithintherepository.└──────────────────────────────────────└──────────────────────────────────────────ProvidesAIcodingassistantswithbettercontextabouttheproject.GeneralinformationModulModulesToolsProvisiontypeDepDependencyupdatesproviProceedwithinstallingVortex?─────────────────────────────┐DownloadingVortexCustomizingVortexforyourprojecCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectCustomizingVortexforyourprojectPreparingdestinationdirectoryCopyingfilestothedestinationdirectoryPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentPreparingdemocontentRunthesitebuildnow?─────────────────────────────────────┐Yes/NoTakes~5-10min;outputwillbestreamed.Youcanskipandrunlaterwith:ahoybuildSetupGitHubActions:https://www.vortextemplate.com/docs/continuous-integration/github-actions#onboarding└────────────────────────────────────────────────────────────────────────────────────────┘ \ No newline at end of file diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index e380f6310..f4f511d2e 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -50,6 +50,12 @@ class InstallCommand extends Command implements CommandRunnerAwareInterface, Exe const OPTION_BUILD = 'build'; + const BUILD_RESULT_SUCCESS = 'success'; + + const BUILD_RESULT_SKIPPED = 'skipped'; + + const BUILD_RESULT_FAILED = 'failed'; + /** * Defines default command name. * @@ -206,15 +212,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int ); if (!$build_ok) { - Tui::error('Site build failed. Vortex was installed, but the site build process encountered errors.'); - Tui::line(''); - Tui::line('Next steps:'); - Tui::line(' - Run: ahoy build'); - Tui::line(' - Or inspect logs for details with `ahoy logs`'); - Tui::line(''); + $this->footerBuildFailed(); return Command::FAILURE; } + + $this->footerBuildSucceeded(); + } + else { + $this->footerBuildSkipped(); } // Cleanup should take place only in case of the successful installation. @@ -553,7 +559,6 @@ public function footer(): void { } else { $title = 'Finished installing Vortex'; - $output .= 'Next steps:' . PHP_EOL; // Check for required tools and provide conditional instructions. $missing_tools = $this->checkRequiredTools(); @@ -562,17 +567,94 @@ public function footer(): void { foreach ($missing_tools as $tool => $instructions) { $tools_output .= sprintf(' %s: %s', $tool, $instructions) . PHP_EOL; } - $tools_output .= PHP_EOL; $output .= Strings::wrapLines($tools_output, $prefix); + $output .= PHP_EOL; } - // Allow post-install handlers to add their messages. - $output .= Strings::wrapLines($this->promptManager->runPostInstall(), $prefix); + $output .= 'Add and commit all files:' . PHP_EOL; + $output .= $prefix . 'git add -A' . PHP_EOL; + $output .= $prefix . 'git commit -m "Initial commit."' . PHP_EOL; } Tui::box($output, $title); } + /** + * Display footer after build succeeded. + */ + public function footerBuildSucceeded(): void { + $output = ''; + $prefix = ' '; + + $output .= 'Get site info:' . $prefix . 'ahoy info' . PHP_EOL; + $output .= 'Login:' . $prefix . $prefix . $prefix . 'ahoy login' . PHP_EOL; + $output .= PHP_EOL; + + $handler_output = $this->promptManager->runPostBuild(self::BUILD_RESULT_SUCCESS); + if (!empty($handler_output)) { + $output .= $handler_output; + } + + Tui::box($output, 'Site is ready'); + } + + /** + * Display footer after build was skipped. + */ + public function footerBuildSkipped(): void { + $output = ''; + $prefix = ' '; + + $responses = $this->promptManager->getResponses(); + $starter = $responses[Starter::id()] ?? Starter::LOAD_DATABASE_DEMO; + $is_profile = in_array($starter, [Starter::INSTALL_PROFILE_CORE, Starter::INSTALL_PROFILE_DRUPALCMS], TRUE); + + $output .= 'Build the site:' . PHP_EOL; + if ($is_profile) { + $output .= $prefix . 'VORTEX_PROVISION_TYPE=profile ahoy build' . PHP_EOL; + } + else { + $output .= $prefix . 'ahoy build' . PHP_EOL; + } + $output .= PHP_EOL; + + if ($is_profile) { + $output .= 'Export database after build:' . PHP_EOL; + $output .= $prefix . 'ahoy export-db db.sql' . PHP_EOL; + $output .= PHP_EOL; + } + + $handler_output = $this->promptManager->runPostBuild(self::BUILD_RESULT_SKIPPED); + if (!empty($handler_output)) { + $output .= $handler_output; + } + + Tui::box($output, 'Ready to build'); + } + + /** + * Display footer after build failed. + */ + public function footerBuildFailed(): void { + $output = ''; + $prefix = ' '; + + $output .= 'Vortex was installed, but the build process failed.' . PHP_EOL; + $output .= PHP_EOL; + $output .= 'Troubleshooting:' . PHP_EOL; + $output .= $prefix . 'Check logs:' . $prefix . $prefix . 'ahoy logs' . PHP_EOL; + $output .= $prefix . 'Retry build:' . $prefix . 'ahoy build' . PHP_EOL; + $output .= $prefix . 'Diagnostics:' . $prefix . 'ahoy doctor' . PHP_EOL; + $output .= PHP_EOL; + + $handler_output = $this->promptManager->runPostBuild(self::BUILD_RESULT_FAILED); + if (!empty($handler_output)) { + $output .= $handler_output; + } + + Tui::box($output, 'Build encountered errors'); + } + /** * Check for required development tools. * diff --git a/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php b/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php index dd2ebef5b..6f843ec68 100644 --- a/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php +++ b/.vortex/installer/src/Prompts/Handlers/AbstractHandler.php @@ -170,6 +170,13 @@ public function postInstall(): ?string { return NULL; } + /** + * {@inheritdoc} + */ + public function postBuild(string $result): ?string { + return NULL; + } + /** * Check that Vortex is installed for this project. * diff --git a/.vortex/installer/src/Prompts/Handlers/CiProvider.php b/.vortex/installer/src/Prompts/Handlers/CiProvider.php index fb5e069b3..b05364887 100644 --- a/.vortex/installer/src/Prompts/Handlers/CiProvider.php +++ b/.vortex/installer/src/Prompts/Handlers/CiProvider.php @@ -119,4 +119,36 @@ public function process(): void { } } + /** + * {@inheritdoc} + */ + public function postInstall(): ?string { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function postBuild(string $result): ?string { + if ($this->isInstalled()) { + return NULL; + } + + $v = $this->getResponseAsString(); + + if ($v === self::GITHUB_ACTIONS) { + return 'Setup GitHub Actions:' . PHP_EOL + . ' https://www.vortextemplate.com/docs/continuous-integration/github-actions#onboarding' . PHP_EOL + . PHP_EOL; + } + + if ($v === self::CIRCLECI) { + return 'Setup CircleCI:' . PHP_EOL + . ' https://www.vortextemplate.com/docs/continuous-integration/circleci#onboarding' . PHP_EOL + . PHP_EOL; + } + + return NULL; + } + } diff --git a/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php b/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php index cc4f4dcd9..593c6e7d4 100644 --- a/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php +++ b/.vortex/installer/src/Prompts/Handlers/HandlerInterface.php @@ -184,4 +184,15 @@ public function process(): void; */ public function postInstall(): ?string; + /** + * Actions to perform and messages to print after build is complete. + * + * @param string $result + * The result of the build operation. + * + * @return string|null + * The output to display, or NULL if none. + */ + public function postBuild(string $result): ?string; + } diff --git a/.vortex/installer/src/Prompts/Handlers/HostingProvider.php b/.vortex/installer/src/Prompts/Handlers/HostingProvider.php index c01d521eb..ba02b46dd 100644 --- a/.vortex/installer/src/Prompts/Handlers/HostingProvider.php +++ b/.vortex/installer/src/Prompts/Handlers/HostingProvider.php @@ -127,4 +127,36 @@ protected function removeLagoon(): void { File::removeTokenAsync('SETTINGS_PROVIDER_LAGOON'); } + /** + * {@inheritdoc} + */ + public function postInstall(): ?string { + return NULL; + } + + /** + * {@inheritdoc} + */ + public function postBuild(string $result): ?string { + if ($this->isInstalled()) { + return NULL; + } + + $v = $this->getResponseAsString(); + + if ($v === self::ACQUIA) { + return 'Setup Acquia hosting:' . PHP_EOL + . ' https://www.vortextemplate.com/docs/hosting/acquia#onboarding' . PHP_EOL + . PHP_EOL; + } + + if ($v === self::LAGOON) { + return 'Setup Lagoon hosting:' . PHP_EOL + . ' https://www.vortextemplate.com/docs/hosting/lagoon#onboarding' . PHP_EOL + . PHP_EOL; + } + + return NULL; + } + } diff --git a/.vortex/installer/src/Prompts/Handlers/Internal.php b/.vortex/installer/src/Prompts/Handlers/Internal.php index ba4c6f20c..efa2fd6e8 100644 --- a/.vortex/installer/src/Prompts/Handlers/Internal.php +++ b/.vortex/installer/src/Prompts/Handlers/Internal.php @@ -166,7 +166,10 @@ protected function processDemoMode(array $responses, string $dir): void { $this->config->set(Config::IS_DEMO, $is_demo); } - public function postInstall():?string { + /** + * {@inheritdoc} + */ + public function postInstall(): ?string { $output = ''; if (!$this->isInstalled()) { diff --git a/.vortex/installer/src/Prompts/Handlers/Starter.php b/.vortex/installer/src/Prompts/Handlers/Starter.php index d6b85852d..e632d2961 100644 --- a/.vortex/installer/src/Prompts/Handlers/Starter.php +++ b/.vortex/installer/src/Prompts/Handlers/Starter.php @@ -132,35 +132,4 @@ public function process(): void { } } - /** - * {@inheritdoc} - */ - public function postInstall(): ?string { - if ($this->isInstalled()) { - return NULL; - } - - $output = ''; - - if ($this->response == self::LOAD_DATABASE_DEMO) { - $output .= 'Build project locally:' . PHP_EOL; - $output .= ' ahoy build' . PHP_EOL; - $output .= PHP_EOL; - } - elseif ($this->response == self::INSTALL_PROFILE_CORE || $this->response == self::INSTALL_PROFILE_DRUPALCMS) { - $output .= 'Build project locally:' . PHP_EOL; - $output .= ' VORTEX_PROVISION_TYPE=profile ahoy build' . PHP_EOL; - $output .= PHP_EOL; - $output .= 'Export database:' . PHP_EOL; - $output .= ' ahoy export-db db.sql' . PHP_EOL; - $output .= PHP_EOL; - } - - // @todo Update to use separate steps for hosting and CI/CD configuration. - $output .= 'Setup integration with your hosting and CI/CD providers:' . PHP_EOL; - $output .= ' See https://www.vortextemplate.com/docs/getting-started/installation'; - - return $output . PHP_EOL; - } - } diff --git a/.vortex/installer/src/Prompts/PromptManager.php b/.vortex/installer/src/Prompts/PromptManager.php index c16e44f80..a4eae4412 100644 --- a/.vortex/installer/src/Prompts/PromptManager.php +++ b/.vortex/installer/src/Prompts/PromptManager.php @@ -324,8 +324,10 @@ public function runPostInstall(): string { $output = ''; $ids = [ - Internal::id(), Starter::id(), + HostingProvider::id(), + CiProvider::id(), + Internal::id(), ]; foreach ($ids as $id) { @@ -343,6 +345,39 @@ public function runPostInstall(): string { return $output; } + /** + * Run all post-build processors. + * + * @param string $result + * The result of the build operation. + * + * @return string + * The combined output from all post-build processors. + */ + public function runPostBuild(string $result): string { + $output = ''; + + $ids = [ + Starter::id(), + HostingProvider::id(), + CiProvider::id(), + ]; + + foreach ($ids as $id) { + if (!array_key_exists($id, $this->handlers)) { + throw new \RuntimeException(sprintf('Handler for "%s" not found.', $id)); + } + + $handler_output = $this->handlers[$id]->postBuild($result); + + if (is_string($handler_output) && !empty($handler_output)) { + $output .= $handler_output; + } + } + + return $output; + } + /** * Check if the installation should proceed. * diff --git a/.vortex/installer/tests/Functional/Command/BuildCommandTest.php b/.vortex/installer/tests/Functional/Command/BuildCommandTest.php index 89ad21583..5da5ef224 100644 --- a/.vortex/installer/tests/Functional/Command/BuildCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/BuildCommandTest.php @@ -389,7 +389,7 @@ public static function dataProviderBuildCommand(): array { TuiOutput::BUILD_BUILDING_SITE, TuiOutput::BUILD_BUILD_COMPLETED, TuiOutput::INSTALL_NEXT_STEPS, - TuiOutput::INSTALL_REVIEW_DOCS, + TuiOutput::BUILD_REVIEW_DOCS, ]), ], diff --git a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php index 0797baa42..e8d7fc447 100644 --- a/.vortex/installer/tests/Functional/Command/InstallCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/InstallCommandTest.php @@ -149,9 +149,17 @@ public static function dataProviderInstallCommand(): array { TuiOutput::INSTALL_PREPARING_DESTINATION, TuiOutput::INSTALL_COPYING_FILES, TuiOutput::INSTALL_PREPARING_DEMO, + TuiOutput::FOOTER_FINISHED_INSTALLING, + TuiOutput::FOOTER_GIT_ADD, + TuiOutput::FOOTER_GIT_COMMIT, + TuiOutput::FOOTER_READY_TO_BUILD, + TuiOutput::FOOTER_BUILD_THE_SITE, + TuiOutput::FOOTER_AHOY_BUILD, + TuiOutput::POSTBUILD_SETUP_GHA, ]), ...TuiOutput::absent([ TuiOutput::INSTALL_BUILDING, + TuiOutput::FOOTER_SITE_READY, ]), ], ], @@ -370,10 +378,19 @@ public static function dataProviderInstallCommand(): array { TuiOutput::INSTALL_PREPARING_DEMO, TuiOutput::INSTALL_BUILDING, TuiOutput::INSTALL_BUILD_SUCCESS, + TuiOutput::FOOTER_FINISHED_INSTALLING, + TuiOutput::FOOTER_GIT_ADD, + TuiOutput::FOOTER_GIT_COMMIT, + TuiOutput::FOOTER_SITE_READY, + TuiOutput::FOOTER_GET_SITE_INFO, + TuiOutput::FOOTER_AHOY_INFO, TuiOutput::INSTALL_LOGIN, - TuiOutput::INSTALL_LOG_FILE, - TuiOutput::INSTALL_NEXT_STEPS, - TuiOutput::INSTALL_REVIEW_DOCS, + TuiOutput::FOOTER_AHOY_LOGIN, + TuiOutput::POSTBUILD_SETUP_GHA, + ]), + ...TuiOutput::absent([ + TuiOutput::FOOTER_READY_TO_BUILD, + TuiOutput::FOOTER_BUILD_ERRORS, ]), ], ], @@ -424,10 +441,15 @@ public static function dataProviderInstallCommand(): array { // Final install output - should be present. ...TuiOutput::present([ TuiOutput::INSTALL_BUILD_SUCCESS, + TuiOutput::FOOTER_FINISHED_INSTALLING, + TuiOutput::FOOTER_GIT_ADD, + TuiOutput::FOOTER_GIT_COMMIT, + TuiOutput::FOOTER_SITE_READY, + TuiOutput::FOOTER_GET_SITE_INFO, + TuiOutput::FOOTER_AHOY_INFO, TuiOutput::INSTALL_LOGIN, - TuiOutput::INSTALL_LOG_FILE, - TuiOutput::INSTALL_NEXT_STEPS, - TuiOutput::INSTALL_REVIEW_DOCS, + TuiOutput::FOOTER_AHOY_LOGIN, + TuiOutput::POSTBUILD_SETUP_GHA, ]), // Negative assertions - should be absent. ...TuiOutput::absent([ @@ -439,6 +461,8 @@ public static function dataProviderInstallCommand(): array { TuiOutput::CHECK_REQUIREMENTS_DOCKER_COMPOSE_MISSING, TuiOutput::CHECK_REQUIREMENTS_AHOY_MISSING, TuiOutput::CHECK_REQUIREMENTS_PYGMY_NOT_RUNNING, + TuiOutput::FOOTER_READY_TO_BUILD, + TuiOutput::FOOTER_BUILD_ERRORS, ]), ], ], @@ -462,11 +486,20 @@ public static function dataProviderInstallCommand(): array { TuiOutput::INSTALL_PREPARING_DEMO, TuiOutput::INSTALL_BUILDING, TuiOutput::INSTALL_BUILD_FAILED, - TuiOutput::INSTALL_EXIT_CODE, - TuiOutput::INSTALL_LOG_FILE, + TuiOutput::FOOTER_FINISHED_INSTALLING, + TuiOutput::FOOTER_GIT_ADD, + TuiOutput::FOOTER_GIT_COMMIT, + TuiOutput::FOOTER_BUILD_ERRORS, + TuiOutput::FOOTER_BUILD_FAILED_MESSAGE, + TuiOutput::FOOTER_TROUBLESHOOTING, + TuiOutput::FOOTER_CHECK_LOGS, + TuiOutput::FOOTER_AHOY_BUILD, + TuiOutput::FOOTER_DIAGNOSTICS, ]), ...TuiOutput::absent([ TuiOutput::INSTALL_BUILD_SUCCESS, + TuiOutput::FOOTER_SITE_READY, + TuiOutput::FOOTER_READY_TO_BUILD, ]), ], ], diff --git a/.vortex/installer/tests/Helpers/TuiOutput.php b/.vortex/installer/tests/Helpers/TuiOutput.php index a336da535..39721473d 100644 --- a/.vortex/installer/tests/Helpers/TuiOutput.php +++ b/.vortex/installer/tests/Helpers/TuiOutput.php @@ -69,16 +69,63 @@ class TuiOutput { const INSTALL_BUILD_FAILED = 'Build failed'; - const INSTALL_LOGIN = 'Login: ahoy login'; + const INSTALL_LOGIN = 'Login:'; const INSTALL_LOG_FILE = 'Log file:'; const INSTALL_NEXT_STEPS = 'Next steps:'; - const INSTALL_REVIEW_DOCS = 'Review hosting/provisioning docs'; + // Handler-specific post-build messages. + const POSTBUILD_SETUP_GHA = 'Setup GitHub Actions:'; + + const POSTBUILD_SETUP_CIRCLECI = 'Setup CircleCI:'; + + const POSTBUILD_SETUP_ACQUIA = 'Setup Acquia hosting:'; + + const POSTBUILD_SETUP_LAGOON = 'Setup Lagoon hosting:'; const INSTALL_EXIT_CODE = 'Exit code:'; + // Footer: Site is ready (build succeeded). + const FOOTER_SITE_READY = 'Site is ready'; + + const FOOTER_GET_SITE_INFO = 'Get site info:'; + + const FOOTER_AHOY_LOGIN = 'ahoy login'; + + const FOOTER_AHOY_INFO = 'ahoy info'; + + // Footer: Ready to build (build skipped). + const FOOTER_READY_TO_BUILD = 'Ready to build'; + + const FOOTER_BUILD_THE_SITE = 'Build the site:'; + + const FOOTER_AHOY_BUILD = 'ahoy build'; + + const FOOTER_AHOY_BUILD_PROFILE = 'VORTEX_PROVISION_TYPE=profile ahoy build'; + + const FOOTER_EXPORT_DATABASE = 'Export database after build:'; + + const FOOTER_AHOY_EXPORT_DB = 'ahoy export-db db.sql'; + + // Footer: Build encountered errors (build failed). + const FOOTER_BUILD_ERRORS = 'Build encountered errors'; + + const FOOTER_BUILD_FAILED_MESSAGE = 'Vortex was installed, but the build process failed.'; + + const FOOTER_TROUBLESHOOTING = 'Troubleshooting:'; + + const FOOTER_CHECK_LOGS = 'ahoy logs'; + + const FOOTER_DIAGNOSTICS = 'ahoy doctor'; + + // Footer: Installation complete. + const FOOTER_FINISHED_INSTALLING = 'Finished installing Vortex'; + + const FOOTER_GIT_ADD = 'git add -A'; + + const FOOTER_GIT_COMMIT = 'git commit -m "Initial commit."'; + const INSTALL_ERROR_MISSING_GIT = 'Installation failed with an error: Missing required command: git.'; const INSTALL_ERROR_MISSING_CURL = 'Installation failed with an error: Missing required command: curl.'; @@ -101,6 +148,8 @@ class TuiOutput { const BUILD_SITE_URL = 'Site URL:'; + const BUILD_REVIEW_DOCS = 'Review hosting/provisioning docs'; + // Check requirements labels. const CHECK_REQUIREMENTS_PRESENT_LABEL = 'Present:'; From 6670d671cfcbf47f2874b80942aadc2b3321a932 Mon Sep 17 00:00:00 2001 From: Alex Skrypnyk Date: Tue, 2 Dec 2025 20:50:06 +1100 Subject: [PATCH 8/8] Updated feedback. --- .vortex/installer/src/Command/InstallCommand.php | 5 +++-- .vortex/installer/src/Runner/AbstractRunner.php | 2 +- .../installer/src/Runner/CommandRunnerAwareTrait.php | 2 +- .vortex/installer/src/Runner/ProcessRunner.php | 11 ++++++++--- .../installer/src/Runner/ProcessRunnerAwareTrait.php | 2 +- .../Command/CheckRequirementsCommandTest.php | 2 +- .../tests/Unit/Runner/ProcessRunnerTest.php | 12 +++++++++--- .vortex/tests/phpunit/Traits/SutTrait.php | 10 +++++----- 8 files changed, 29 insertions(+), 17 deletions(-) diff --git a/.vortex/installer/src/Command/InstallCommand.php b/.vortex/installer/src/Command/InstallCommand.php index f4f511d2e..1c204ad95 100644 --- a/.vortex/installer/src/Command/InstallCommand.php +++ b/.vortex/installer/src/Command/InstallCommand.php @@ -11,6 +11,7 @@ use DrevOps\VortexInstaller\Runner\CommandRunnerAwareTrait; use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareInterface; use DrevOps\VortexInstaller\Runner\ExecutableFinderAwareTrait; +use DrevOps\VortexInstaller\Runner\RunnerInterface; use DrevOps\VortexInstaller\Task\Task; use DrevOps\VortexInstaller\Utils\Config; use DrevOps\VortexInstaller\Utils\Env; @@ -452,7 +453,7 @@ protected function runBuildCommand(OutputInterface $output): bool { $runner = $this->getCommandRunner(); $runner->run('build', args: $is_profile ? ['--profile' => '1'] : [], output: $output); - return $runner->getExitCode() === Command::SUCCESS; + return $runner->getExitCode() === RunnerInterface::EXIT_SUCCESS; } protected function header(): void { @@ -721,7 +722,7 @@ public function cleanup(): void { * The downloader. */ protected function getDownloader(): Downloader { - return $this->downloader ?? new Downloader(); + return $this->downloader ??= new Downloader(); } /** diff --git a/.vortex/installer/src/Runner/AbstractRunner.php b/.vortex/installer/src/Runner/AbstractRunner.php index 050b80da9..e6a067909 100644 --- a/.vortex/installer/src/Runner/AbstractRunner.php +++ b/.vortex/installer/src/Runner/AbstractRunner.php @@ -205,7 +205,7 @@ public function getOutput(bool $as_array = FALSE, ?int $lines = NULL): string | protected function reset(): void { $this->command = NULL; $this->output = ''; - $this->exitCode = self::EXIT_SUCCESS; + $this->exitCode = RunnerInterface::EXIT_SUCCESS; } /** diff --git a/.vortex/installer/src/Runner/CommandRunnerAwareTrait.php b/.vortex/installer/src/Runner/CommandRunnerAwareTrait.php index dc1341f93..ae78cff74 100644 --- a/.vortex/installer/src/Runner/CommandRunnerAwareTrait.php +++ b/.vortex/installer/src/Runner/CommandRunnerAwareTrait.php @@ -25,7 +25,7 @@ trait CommandRunnerAwareTrait { */ public function getCommandRunner(): CommandRunner { // @phpstan-ignore-next-line - return $this->commandRunner ?? new CommandRunner($this->getApplication()); + return $this->commandRunner ??= new CommandRunner($this->getApplication()); } /** diff --git a/.vortex/installer/src/Runner/ProcessRunner.php b/.vortex/installer/src/Runner/ProcessRunner.php index 0bc2659ae..a8937c501 100644 --- a/.vortex/installer/src/Runner/ProcessRunner.php +++ b/.vortex/installer/src/Runner/ProcessRunner.php @@ -141,7 +141,11 @@ protected function resolveCommand(string $command): array { protected function prepareArguments(array $parsed_args, array $additional_args): array { $all_args = array_merge($parsed_args, $this->formatArgs($additional_args)); - foreach ($all_args as &$arg) { + foreach ($all_args as $key => &$arg) { + if (!is_scalar($arg)) { + $value_repr = get_debug_type($arg); + throw new \InvalidArgumentException(sprintf('Argument at index "%s" must be a scalar value, %s given.', $key, $value_repr)); + } $arg = (string) $arg; } unset($arg); @@ -159,9 +163,10 @@ protected function prepareArguments(array $parsed_args, array $additional_args): * When an environment variable is not a scalar value. */ protected function validateEnvironmentVars(array $env): void { - foreach ($env as $env_value) { + foreach ($env as $key => $env_value) { if (!is_scalar($env_value)) { - throw new \InvalidArgumentException('All environment variables must be scalar values.'); + $value_repr = get_debug_type($env_value); + throw new \InvalidArgumentException(sprintf('Environment variable "%s" must be a scalar value, %s given.', $key, $value_repr)); } } } diff --git a/.vortex/installer/src/Runner/ProcessRunnerAwareTrait.php b/.vortex/installer/src/Runner/ProcessRunnerAwareTrait.php index 7243edd93..9987059ea 100644 --- a/.vortex/installer/src/Runner/ProcessRunnerAwareTrait.php +++ b/.vortex/installer/src/Runner/ProcessRunnerAwareTrait.php @@ -23,7 +23,7 @@ trait ProcessRunnerAwareTrait { * The process runner instance. */ public function getProcessRunner(): ProcessRunner { - return $this->processRunner ?? new ProcessRunner(); + return $this->processRunner ??= new ProcessRunner(); } /** diff --git a/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php b/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php index afc750340..ac8ed7297 100644 --- a/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php +++ b/.vortex/installer/tests/Functional/Command/CheckRequirementsCommandTest.php @@ -72,7 +72,7 @@ public function testCheckRequirementsCommand( } /** - * Data provider for testCheckWithMockedRunner. + * Data provider for testCheckRequirementsCommand. * * @return arrayvalidateEnvironmentVarsPublic($env); if ($expected_exception === NULL) { - // @phpstan-ignore-next-line - $this->assertTrue(TRUE, 'Validation passed'); + $this->addToAssertionCount(1); } } @@ -350,6 +349,13 @@ public static function dataProviderPrepareArguments(): array { 'expected_exception' => NULL, 'expected_message' => NULL, ], + 'non-scalar argument throws exception' => [ + 'parsed_args' => ['arg1', ['array']], + 'additional_args' => [], + 'expected' => [], + 'expected_exception' => \InvalidArgumentException::class, + 'expected_message' => 'Argument at index "1" must be a scalar value, array given.', + ], ]; } @@ -371,7 +377,7 @@ public static function dataProviderValidateEnvironmentVars(): array { 'non-scalar env var throws exception' => [ 'env' => ['VAR1' => ['array']], 'expected_exception' => \InvalidArgumentException::class, - 'expected_message' => 'All environment variables must be scalar values', + 'expected_message' => 'Environment variable "VAR1" must be a scalar value, array given.', ], ]; } diff --git a/.vortex/tests/phpunit/Traits/SutTrait.php b/.vortex/tests/phpunit/Traits/SutTrait.php index d14991aa3..1d48df82b 100644 --- a/.vortex/tests/phpunit/Traits/SutTrait.php +++ b/.vortex/tests/phpunit/Traits/SutTrait.php @@ -74,12 +74,12 @@ protected function runInstaller(array $arguments = []): void { if (!is_dir(static::$root . '/.vortex/installer/vendor')) { $this->logNote('Installing dependencies of the Vortex installer'); - $this->cmd('composer --working-dir=' . static::$root . '/.vortex/installer install --no-interaction --no-progress'); + $this->cmd('composer --working-dir=' . escapeshellarg(static::$root . '/.vortex/installer') . ' install --no-interaction --no-progress'); } // @todo Convert options to $arguments once // ProcessTrait::processParseCommand() is fixed. - $cmd = sprintf('php .vortex/installer/installer.php --no-interaction --destination=%s', static::locationsSut()); + $cmd = sprintf('php .vortex/installer/installer.php --no-interaction --destination=%s', escapeshellarg(static::locationsSut())); $this->logNote('Run the installer script'); $this->cmd( @@ -134,14 +134,14 @@ protected function buildInstaller(): string { if (!is_dir($installer_dir)) { $this->logNote('Installing dependencies of the Vortex installer'); - $this->cmd('composer --working-dir=' . $installer_dir . ' install --no-interaction --no-progress'); + $this->cmd('composer --working-dir=' . escapeshellarg($installer_dir) . ' install --no-interaction --no-progress'); $this->assertDirectoryExists($installer_dir . '/vendor', 'Vortex installer vendor directory should exist after installing dependencies'); } - $this->cmd('composer --working-dir=' . $installer_dir . ' build', env: ['SHELL_VERBOSITY' => -1], txt: 'Build the Vortex installer PHAR'); + $this->cmd('composer --working-dir=' . escapeshellarg($installer_dir) . ' build', env: ['SHELL_VERBOSITY' => -1], txt: 'Build the Vortex installer PHAR'); $this->assertFileExists($installer_phar, 'Installer PHAR should be built'); - $this->cmd('php ' . $installer_phar . ' --version'); + $this->cmd('php ' . escapeshellarg($installer_phar) . ' --version'); $this->logNote('Built Vortex installer: ' . trim($this->processGet()->getOutput())); return $installer_phar;