Skip to content

[#2846] Grew the Vortex CLI into a multi-verb tool with 'update', 'configure' and 'doctor'. - #2887

Merged
AlexSkrypnyk merged 12 commits into
2.xfrom
feature/2846-cli-commands
Aug 5, 2026
Merged

[#2846] Grew the Vortex CLI into a multi-verb tool with 'update', 'configure' and 'doctor'.#2887
AlexSkrypnyk merged 12 commits into
2.xfrom
feature/2846-cli-commands

Conversation

@AlexSkrypnyk

@AlexSkrypnyk AlexSkrypnyk commented Aug 4, 2026

Copy link
Copy Markdown
Member

Closes #2846

Summary

Grows the Vortex CLI from a single-purpose installer into a multi-verb tool. The command surface expands from install (default), check-requirements, build to install, update, configure, doctor, build, plus a hidden route command that becomes the new default. A bare invocation now resolves by the state of the target directory: an existing Vortex project is reconfigured, anything else gets a fresh install. The download/collect/process/copy flow shared by install and update is extracted into AbstractInstallCommand, and the --schema/--validate/--agent-help agent surface is extracted into AgentSurfaceTrait so it answers identically on install, update and configure. Three pre-existing defects in the renamed doctor command are fixed along the way, a wrong-major CLI download in the tooling is corrected, and a test-isolation bug that made unit test outcomes depend on suite order is resolved.

Changes

Routing

RouteCommand is the new default command (hidden from list). It resolves a bare invocation by the state of the target directory - an existing Vortex project routes to configure, anything else routes to install. It declares no options of its own (ignoreValidationErrors()) and passes input through untouched; the target directory is read from the raw input rather than a bound option, since a destination that does not exist yet is the ordinary fresh-install case.

update command

New first-class verb replacing "re-run install over an existing project". --to names the target template version, resolved against the official repository; an explicit --uri names both repository and ref and wins over --to. The cross-major refusal on the destination, and its pointer to the matching release, carry over unchanged.

--to is additionally gated on the version being requested. The destination gate compares the project against the build and says nothing about the requested ref, and releasePrefix constrains only the stable resolution - so a named version resolved straight to an archive was a route to pull a template across a breaking boundary into a project the build considered compatible. A --to whose major differs from the build's is now refused, naming the release that can serve it. A branch, tag alias or commit carries no major and is left alone, as is an unstamped build.

configure command

New verb that reconfigures a project in place with no download - the project is both the tree answers are read from and the tree they are written to. --apply is honoured on both the interactive and scripted paths (ConfigureCommandTest data-provides over both, since command interactivity is decoupled from prompt interactivity). Without --apply nothing on disk changes: a scripted run reports the collected answers as JSON on stdout, which doubles as a way to read a project's current configuration. Writing is refused outside a Vortex project and confirmed first when a person is watching.

doctor command

Replaces check-requirements - the class is renamed, not rewritten, so every reported behaviour survives: the four checked tools, the installed-vs-running distinction, per-tool install instructions, the version summary, --only. Two new tests pin that surface.

Three pre-existing defects are fixed:

  • The Pygmy fallback piped docker ps --format "{{.Names}}" | grep -q amazeeio into a runner that executes without a shell, so the pipe was never interpreted and the branch could never succeed (the dead code carried a @phpstan-ignore notIdentical.alwaysFalse). The container list is now read and matched in PHP.
  • Both Docker probes assumed Docker was resolvable. ProcessRunner::resolveCommand() throws Command not found for an unresolvable binary, so on a host without Docker the command aborted instead of reporting Docker as missing - which is the one thing it exists to do. Both probes now guard on commandExists('docker').
  • Docker Compose detection now reports the version of whichever form is present (docker compose version or docker-compose --version), so a legacy-only host no longer reports a bare "Available" with no version.

Shared machinery

  • AbstractInstallCommand holds the download/collect/process/copy flow shared by install and update; both are now thin subclasses. A failed run is framed by the destination the same way the header already was, so a run can no longer announce itself as an update and fail as an installation.
  • AgentSurfaceTrait puts --schema/--validate/--agent-help on install, update and configure from one place, so a downloaded binary run with no arguments can describe its own questions whichever verb it resolves to. An existing but unreadable --prompts file is reported as unreadable rather than as broken JSON, matching what OptionsResolver already raises for the same condition.
  • Project::isVortex() gives project detection one home; OptionsResolver and RouteCommand both call it instead of duplicating the README badge check.

Behaviour change worth flagging

Closing guidance (the "review the changes" box) is now suppressed on non-interactive runs, so a scripted caller's stdout is not polluted with guidance nobody is reading. Progress and status output is unaffected, and build-failure output still prints on every path, since it explains a non-zero exit code rather than guiding a person. One consequence: ahoy update-vortex, which runs the CLI with --no-interaction, no longer prints its closing box.

Test isolation

Running the CLI putenv()s the destination's .env into the process, and the unit discovery tests read exactly those variables, so a functional test running first made a large batch of unrelated unit tests fail depending on suite order. The working directory had the same problem, since locations are derived from it. Both are now cleared on teardown, making the suite independent of execution order.

Consumers

  • .vortex/tooling/src/vortex-update now calls the update verb explicitly instead of relying on the default command.
  • The same script downloaded the CLI from https://www.vortextemplate.com/v1/install on the 2.x branch. That default is inherited from main, where it is correct and where the script states the rationale itself: pinned to the major-specific path so a project always updates within its own major line. On 2.x it meant fetching the 1.x CLI, which was already wrong before this change - the downloaded 1.x build would refuse the 2.x destination through the major gate - and calling update would have turned that refusal into Command "update" is not defined. Now pinned to /v2/install, which is both the documented intent and the build carrying the commands the script calls.
  • The template test harness (SutTrait) gains runCli($verb, ...) with runInstall() and runUpdate() as thin wrappers; the two CLI scenarios in CliTest now exercise runUpdate().

Docs

cli.mdx covers the full command surface, including a note that configure is subtractive on this engine: handlers that remove things (a service, a CI provider's files, the AI agent instructions) take effect, but placeholder-filling answers such as the site name do not, because the placeholders were consumed at install time. update is what brings template changes back in. This subtractive behaviour is inherited from the shared processing pipeline - changing it means changing how answers are processed, which is out of scope for this change.

Before / After

BEFORE                                     AFTER

$ vortex.phar                              $ vortex.phar
  always resolves to `install`               resolves to hidden `route`, which picks by target dir:

                                                    target directory
                                                          |
                                              +-----------+-----------+
                                              |                       |
                                     already a Vortex           nothing there
                                          project                   yet
                                              |                       |
                                         configure                install
                                       (in place,               (download +
                                        --apply to write)        apply)

Command surface:                           Command surface:
  install (default)                          install
  check-requirements                         update        <- new
  build                                      configure     <- new
                                             doctor        <- renamed from check-requirements
                                             build
                                             route         <- new, hidden, is now the default

Shared code:                               Shared code:
  install/update logic duplicated            AbstractInstallCommand::doInstall()
  (update did not exist)                       used by InstallCommand and UpdateCommand

  --schema/--validate/--agent-help           AgentSurfaceTrait
  wired into install only                      wired into install, update and configure alike

Major-boundary gates:                      Major-boundary gates:
  destination project vs build               destination project vs build
  (requested version unchecked)              requested `--to` version vs build   <- new

`ahoy update-vortex` on a 2.x project:     `ahoy update-vortex` on a 2.x project:
  downloads /v1/install                      downloads /v2/install
  -> 1.x build, refused by major gate        -> 2.x build, carries `update`

Option lookups in 'OptionsResolver::resolve()' now tolerate an absent key so verbs can define only the options they offer.
The class moves across rather than being rewritten, so every reported behaviour survives: the four checked tools, the installed-and-running distinction, per-tool install instructions, the version summary and '--only'. Two tests pin that surface so it cannot quietly shrink.
The questions are declared by the build rather than by a verb, so '--schema', '--validate' and '--agent-help' answer identically wherever the trait is mounted. A bare invocation resolves to a different verb depending on the target directory, and an agent must be able to describe the questions either way.
…mand'.

Downloading, collecting answers, processing and copying is one flow shared by every verb that applies a template, so it moves to a base class and 'install' becomes a facade over it. Closing guidance is now suppressed on non-interactive runs so a caller's stdout stays clean; build failure output still prints on every path because it explains a non-zero exit code. The header names the operation, which follows the destination state rather than the command name.
Updating is now a verb of its own rather than re-running 'install' over an existing project. '--to' names the target template version directly; an explicit '--uri' names both repository and ref, so it stays the more specific input and wins. The cross-major refusal and its pointer to the matching release carry over unchanged.
Reconfigures a project in place, with no template download: the project is both the tree answers are read from and the tree they are written to. The working directory is set past the environment so an ambient variable cannot redirect the write target, and a relative destination is resolved before any value is derived from it. '--apply' is honoured on both the interactive and the scripted path - the two differ only in how answers are reported, so there is no second branch to forget and nothing excluded from coverage. Writing is refused outside a Vortex project, and confirmed first when a person is watching.
An existing Vortex project is reconfigured, anything else gets a fresh install. The router declares no options of its own and passes the input through, so the selected verb sees exactly what was typed and there is no second copy of the option set to drift. It reads the destination from the raw input rather than a bound option, because a destination that does not exist yet is the ordinary fresh-install case. The agent surface is reachable through both routes, so a downloaded binary run with no arguments can still describe its own questions. Each verb is also exercised from the built PHAR.
'vortex-update' calls the verb rather than re-running an install through the default command. The template harness gains a 'runUpdate()' alongside 'runInstall()', and the two CLI scenarios that install and then re-run against a newer template now exercise the update verb, which is what they were always describing.
The CLI reference now covers every verb rather than installation alone, including what 'configure' can and cannot change in an already-installed project. The agent instructions describe the verbs, the bare invocation entry point and 'configure --apply', so an agent can drive the whole surface from '--agent-help'.
Running the CLI loads the destination's '.env' into the process, so a functional test left variables such as 'VORTEX_PROJECT' resolving in whichever test ran next - and the discovery tests read exactly those. Locations are derived from the working directory, so a test that chdir'd elsewhere resolved later paths against the wrong root. Both are now cleared on teardown, which makes the suite independent of execution order.
The Pygmy fallback piped 'docker ps' into 'grep', but commands run without a shell, so the pipe was never interpreted and the branch could not succeed - the container list is now read and matched in PHP. Docker Compose reports the version of whichever form is present rather than assuming the modern subcommand, so a legacy-only host no longer reports a bare 'Available'. An injected process runner is given the working directory like any other, and 'build' now runs the checks against the directory being built. '--destination' gains the '-d' shortcut on every verb, matching what the router already accepts, and the '--uri' description names '#' as the ref separator.
@coderabbitai

coderabbitai Bot commented Aug 4, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The CLI now supports commandless routing, shared installation logic, template updates, in-place configuration, agent surfaces, and environment diagnostics. Tests, PHAR coverage, tooling, and documentation reflect the expanded command set.

Changes

CLI command surface

Layer / File(s) Summary
Shared installation and agent foundation
.vortex/cli/src/Command/*, .vortex/cli/src/Utils/*, .vortex/cli/src/Prompts/*, .vortex/cli/src/Schema/*
Adds shared installation logic, agent options, project detection, compatibility checks, cleanup, and update-aware output.
Configure, update, and default routing
.vortex/cli/src/Command/ConfigureCommand.php, .vortex/cli/src/Command/UpdateCommand.php, .vortex/cli/src/Command/RouteCommand.php, .vortex/cli/vortex, .vortex/cli/tests/Functional/Command/*, .vortex/tooling/*
Adds project configuration, version-targeted updates, commandless routing, application registration, and related functional coverage.
Doctor diagnostics and build integration
.vortex/cli/src/Command/DoctorCommand.php, .vortex/cli/src/Command/BuildCommand.php, .vortex/cli/tests/Functional/Command/*, .vortex/cli/tests/Helpers/TuiOutput.php
Renames requirements checks to doctor, improves Docker Compose and Pygmy detection, and updates build and install diagnostics.
Documentation and regression coverage
.vortex/docs/*, .vortex/cli/tests/Functional/PharTest.php, .vortex/cli/tests/Unit/*, .vortex/tests/phpunit/*
Documents the command surface and updates PHAR, project, environment, and template-level test coverage.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • drevops/vortex#2847 — The new install, update, configure, and project-detection model is the command flow that this issue proposes to unify with a manifest-based operation flow.

Possibly related PRs

  • drevops/vortex#2860 — The documentation changes revise the CLI commands and workflows covered by that PR.

Suggested labels: A3

Poem

A rabbit hops through routes anew,
Install and update paths come through.
Configure answers, doctor checks,
Agent schemas guide the next steps.
“Clean commands!” the rabbit sings,
While tests guard all the strings.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 79.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement routing, update, configure, doctor, agent surfaces, diagnostics, consumers, tests, and documentation requirements [#2846].
Out of Scope Changes check ✅ Passed The code, tests, tooling, and documentation changes support the linked issue objectives and contain no unrelated scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: expanding the Vortex CLI into a multi-verb tool with update, configure, and doctor commands.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/2846-cli-commands

Comment @coderabbitai help to get the list of available commands.

@AlexSkrypnyk AlexSkrypnyk added the A2 Working clone index A2 label Aug 4, 2026
@github-project-automation github-project-automation Bot moved this to BACKLOG in Vortex 2.x Aug 4, 2026
@github-actions

This comment has been minimized.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.vortex/cli/src/Command/AbstractInstallCommand.php:
- Around line 221-226: Update the exception handling in doInstall() to frame the
failure message using the same destination-derived operation wording as
InstallPresenter::header(), so update failures are reported as updates rather
than installations. Keep the existing error details and failure return intact,
and update the corresponding INSTALL_ERROR_* constants in TuiOutput.php to match
the revised wording.

In @.vortex/cli/src/Command/AgentSurfaceTrait.php:
- Around line 99-108: Update the --prompts handling in AgentSurfaceTrait to
detect an existing but unreadable file and report the established precise
read-error message instead of treating it as invalid JSON. Decode the prompt
content only once into an associative array, validate that decoding succeeded
and produced an object-shaped associative structure using
is_array()/array_is_list(), while preserving empty JSON object {} as valid if
required, then reuse that decoded value as $user_config.

In @.vortex/cli/src/Command/DoctorCommand.php:
- Around line 321-325: Guard both Docker probes in hasAmazeeioContainers and the
related fallback path with processRunner->commandExists('docker') before calling
ProcessRunner::run(). Return the existing unavailable result (FALSE or NULL as
appropriate) when Docker is absent, and add tests covering each probe path
without Docker installed.

In @.vortex/cli/src/Command/UpdateCommand.php:
- Around line 84-96: Update targetUri() to validate a non-empty --to version
against the CLI major before constructing the repository reference, using the
existing assertMajorCompatibility() behavior or related version symbols. Reject
mismatched requested majors and preserve explicit --uri precedence and
same-major reference generation.

In @.vortex/docs/content/cli.mdx:
- Line 147: Revise the documentation statement near the closing guidance
description so it does not promise clean stdout for every non-interactive
command. State only that footer guidance is suppressed, or limit the JSON-only
stdout claim specifically to configure; preserve installation status output for
non-interactive install runs.

In @.vortex/tooling/src/vortex-update:
- Around line 94-100: Ensure the packaged v1 Vortex CLI artifact supports the
update command before deploying this script. If that artifact cannot be updated,
modify the TASK running the CLI to detect the installed CLI version and invoke a
compatible command instead of unconditionally calling update.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 26b08a69-14fa-4258-8da2-e4f6cb078603

📥 Commits

Reviewing files that changed from the base of the PR and between 8ff4348 and c2d668b.

📒 Files selected for processing (33)
  • .vortex/cli/src/Command/AbstractInstallCommand.php
  • .vortex/cli/src/Command/AgentSurfaceTrait.php
  • .vortex/cli/src/Command/BuildCommand.php
  • .vortex/cli/src/Command/ConfigureCommand.php
  • .vortex/cli/src/Command/DestinationAwareTrait.php
  • .vortex/cli/src/Command/DoctorCommand.php
  • .vortex/cli/src/Command/InstallCommand.php
  • .vortex/cli/src/Command/RouteCommand.php
  • .vortex/cli/src/Command/UpdateCommand.php
  • .vortex/cli/src/Prompts/InstallPresenter.php
  • .vortex/cli/src/Prompts/PromptManager.php
  • .vortex/cli/src/Schema/AgentHelp.php
  • .vortex/cli/src/Utils/OptionsResolver.php
  • .vortex/cli/src/Utils/Project.php
  • .vortex/cli/tests/Functional/Command/BuildCommandTest.php
  • .vortex/cli/tests/Functional/Command/ConfigureCommandTest.php
  • .vortex/cli/tests/Functional/Command/DoctorCommandTest.php
  • .vortex/cli/tests/Functional/Command/InstallCommandTest.php
  • .vortex/cli/tests/Functional/Command/RouteCommandTest.php
  • .vortex/cli/tests/Functional/Command/UpdateCommandTest.php
  • .vortex/cli/tests/Functional/FunctionalTestCase.php
  • .vortex/cli/tests/Functional/Handlers/AbstractHandlerProcessTestCase.php
  • .vortex/cli/tests/Functional/PharTest.php
  • .vortex/cli/tests/Helpers/TuiOutput.php
  • .vortex/cli/tests/Unit/ProjectTest.php
  • .vortex/cli/tests/Unit/UnitTestCase.php
  • .vortex/cli/vortex
  • .vortex/docs/content/cli.mdx
  • .vortex/docs/content/contributing/maintenance/cli.mdx
  • .vortex/tests/phpunit/Functional/CliTest.php
  • .vortex/tests/phpunit/Traits/SutTrait.php
  • .vortex/tooling/src/vortex-update
  • .vortex/tooling/tests/Unit/UpdateVortexTest.php

Comment thread .vortex/cli/src/Command/AbstractInstallCommand.php
Comment thread .vortex/cli/src/Command/AgentSurfaceTrait.php
Comment thread .vortex/cli/src/Command/DoctorCommand.php
Comment thread .vortex/cli/src/Command/UpdateCommand.php
Comment thread .vortex/docs/content/cli.mdx Outdated
Comment thread .vortex/tooling/src/vortex-update
@AlexSkrypnyk

This comment has been minimized.

1 similar comment
@AlexSkrypnyk

This comment has been minimized.

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.40698% with 33 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.66%. Comparing base (8ff4348) to head (2006d34).

Files with missing lines Patch % Lines
.vortex/cli/src/Command/AbstractInstallCommand.php 85.81% 20 Missing ⚠️
...vortex/cli/tests/Functional/FunctionalTestCase.php 0.00% 5 Missing ⚠️
.vortex/cli/src/Command/ConfigureCommand.php 94.02% 4 Missing ⚠️
.vortex/cli/src/Prompts/PromptManager.php 0.00% 2 Missing ⚠️
.vortex/cli/src/Command/DoctorCommand.php 95.65% 1 Missing ⚠️
...tional/Handlers/AbstractHandlerProcessTestCase.php 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##              2.x    #2887      +/-   ##
==========================================
- Coverage   88.15%   87.66%   -0.49%     
==========================================
  Files          98      104       +6     
  Lines        5412     5562     +150     
  Branches        3        3              
==========================================
+ Hits         4771     4876     +105     
- Misses        641      686      +45     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

📖 Documentation preview for this pull request has been deployed to Netlify:

https://6a726d1135a11fbe1a355b9b--vortex-docs.netlify.app

This preview is rebuilt on every commit and is not the production documentation site.

…gets, and pinned the update endpoint.

The Docker probes now check the binary is resolvable first: the runner refuses to execute a command it cannot find, so 'doctor' aborted on a host without Docker instead of reporting it as missing - the one thing the command exists to do. A named '--to' version from another major is refused, since the destination gate compares the project against the build and says nothing about the version being requested, leaving a route to pull a template across a breaking boundary. The tooling downloads the CLI from the major-specific path matching its own line, so an update fetches a build that carries the commands it calls. A failed run is now framed by the destination like the header already was, an unreadable answers file is reported as unreadable rather than as broken JSON, and the docs no longer promise clean stdout for every non-interactive command.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Code coverage (threshold: 90%)

  Classes: 100.00% (1/1)
  Methods: 100.00% (2/2)
  Lines:   100.00% (153/153)
Per-class coverage
Drupal\ys_demo\Plugin\Block\CounterBlock
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 10/ 10)

@AlexSkrypnyk

This comment has been minimized.

2 similar comments
@AlexSkrypnyk

This comment has been minimized.

@AlexSkrypnyk

Copy link
Copy Markdown
Member Author

Code coverage (threshold: 90%)

  Classes: 100.00% (1/1)
  Methods: 100.00% (2/2)
  Lines:   100.00% (153/153)
Per-class coverage
Drupal\ys_demo\Plugin\Block\CounterBlock
  Methods: 100.00% ( 2/ 2)   Lines: 100.00% ( 10/ 10)

@AlexSkrypnyk AlexSkrypnyk added the Needs review Pull request needs a review from assigned developers label Aug 4, 2026
@AlexSkrypnyk
AlexSkrypnyk merged commit 631f29f into 2.x Aug 5, 2026
37 checks passed
@AlexSkrypnyk
AlexSkrypnyk deleted the feature/2846-cli-commands branch August 5, 2026 01:20
@github-project-automation github-project-automation Bot moved this from BACKLOG to Release queue in Vortex 1.x Aug 5, 2026
@github-project-automation github-project-automation Bot moved this from BACKLOG to Release queue in Vortex 2.x Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A2 Working clone index A2 Needs review Pull request needs a review from assigned developers

Projects

Status: Release queue
Status: Release queue

Development

Successfully merging this pull request may close these issues.

1 participant