CLI
The Vortex CLI is a self-contained Symfony Console application that turns the template into a project shaped by your answers. This page is for contributors who change the CLI. It explains how the CLI works, how its tests and snapshots run, how it's released, and how its demo video gets recorded. To install Vortex with it, see Installation instead.
The CLI lives in the same repository as the template, at .vortex/cli/.
That way, a single pull request can change a template file together with
the CLI logic that handles it, and the 2 stay in step.
Architecture
The CLI is written in PHP rather than shell because it touches many parts
of the template and has to manage every permutation of the template's
variants. PHP also gives it a terminal UI through
PHP TUI, which groups the questions into panels that
you move between freely. Pass --no-interaction and it asks nothing at
all: it installs new and existing projects, and updates them, unattended,
which suits automated pipelines and CI environments.
The CLI holds only the project-specific parts: the form definition in
VortexForm, with the questions and their panels, and the handler classes
that carry the Vortex behavior. A single adapter, TuiAdapter, turns a
handler into a form field. Collection, conditionals, and rendering belong
to the PHP TUI library, which is developed and released in its own
repository. Each handler knows how to read its own value out of an existing
project, and the library decides when to ask for it.
The binary is vortex, and it has 5 commands: install, update,
configure, doctor, and build. Running it without a command picks one
from the state of the target directory: an existing Vortex project is
reconfigured, and anything else gets a fresh install.
Each question pre-fills its answer, and you can change it when the CLI
asks. With --no-interaction, the pre-filled value is the answer. The CLI
takes it from the first of these sources that has one:
- Prompt answers - a JSON object passed with
--prompts, either inline or as a path to a JSON file. Its keys are the prompt IDs that--schemaprints, and--validatechecks the object without changing anything. - Environment variables - 1 per prompt, named
VORTEX_CLI_INSTALL_PROMPT_<ID>. - Discovery - auto-detection from existing project files, for example
composer.json,.env, or hosting-specific files like.lagoon.yml. - Handler defaults - fallback values defined by each handler.
So a run with --no-interaction and --prompts installs a whole project
without a single question, which is how the CLI's own tests drive every
scenario.
--config takes a different JSON object: the CLI's own settings, keyed by
their VORTEX_CLI_INSTALL_* names, such as
VORTEX_CLI_INSTALL_TEMPLATE_REPO. An environment variable with the same
name wins over the value in the JSON.
An installation runs in this order:
- You download the PHAR from https://www.vortextemplate.com/install and
run it with
php vortex.phar.InstallCommandorchestrates the rest. - The form collects the answers, applying conditionals and discovery as it goes.
- Each handler covers 1 aspect of the template, such as the CI provider, hosting, services, the theme, or the deployment types. It queues file operations based on the answers.
- The queued file operations run at once. Conditional tokens decide which content stays.
The result is a fully customized Drupal project.
Conditional token system
The install command handles replacements and additions with string substitution. It also uses a token system to exclude whole blocks of content from template files. For an optional feature, you wrap the relevant content in token markers instead of writing logic that removes lines from configuration files, scripts, or documentation. When the feature isn't selected, the install command removes everything between the markers.
In a Markdown file, the markers are link reference comments:
[//]: # (#;< TOKEN_NAME)
Content removed if feature not selected
[//]: # (#;> TOKEN_NAME)
In a shell script, a YAML file, or another file that takes # comments,
the markers are comment lines:
#;< TOKEN_NAME
Content removed if feature not selected
#;> TOKEN_NAME
The install command matches any line that contains a marker and removes the
whole line, so a marker can also sit inside another comment syntax, such as
<!-- #;< TOOL_BEHAT --> in phpcs.xml.
A token with a ! prefix, such as #;< !PROVISION_TYPE_PROFILE, works the
other way round: the install command removes its content when the feature
is selected. Content between VORTEX_DEV markers always goes, because it
exists only for maintaining Vortex itself.
After the other handlers have run, the install command deletes every
remaining line that contains #;. That removes the markers of the selected
features, which keep their content, and the internal comments that start
with #;. It also deletes every ##### prefix followed by a space. A line
that the template keeps commented out with that prefix becomes active in
the consumer site, as the VORTEX_DB_IMAGE example in .env does.
Distribution
The PHAR is published to 3 stable paths, each served with no file extension:
| Path | Contents |
|---|---|
https://www.vortextemplate.com/install | The current major's pin |
https://www.vortextemplate.com/v1/install | The 1.x line |
https://www.vortextemplate.com/v2/install | The 2.x line |
The bare /install path is a copy of the current major's pin, which the
VORTEX_CURRENT_MAJOR repository variable selects. Each per-major path is
built from that major's own branch, so 1.x publishes the installer it
ships while 2.x publishes this CLI.
The documented download filename is vortex.phar:
curl -SsL https://www.vortextemplate.com/install > vortex.phar && php vortex.phar
Environment variables
Variables are scoped by what they configure. CLI-level settings use the
VORTEX_CLI_ prefix, and settings that belong to the install command use
VORTEX_CLI_INSTALL_.
| Variable | Purpose |
|---|---|
VORTEX_CLI_VERSION | Overrides the stamped version |
VORTEX_CLI_URL | Where vortex-update downloads the PHAR |
VORTEX_CLI_PATH | Local PHAR path, overrides the URL |
VORTEX_CLI_URL_CACHE_BUST | Cache-busting parameter for the URL |
VORTEX_CLI_INSTALL_TMP_DIR | Working directory for the downloaded source |
VORTEX_CLI_INSTALL_TEMPLATE_REPO | Template repository to install from |
VORTEX_CLI_INSTALL_PROMPT_<ID> | Answer for a single prompt |
The superseded VORTEX_INSTALLER_* names still resolve. When a current
name is unset, the matching legacy name supplies the value, and the run
prints a deprecation notice that names its replacement.
Testing
The CLI has unit tests for individual components and functional tests for full installation scenarios.
Unit testing
Unit tests in tests/Unit/ check each component in isolation, with
PHPUnit's own mock objects standing in for collaborators. They cover the
core utilities, such as Yaml, File, Git, Validator, and Strings,
along with the downloaders, the runners, the prompt schema, and the
discovery logic of the handlers.
Functional testing with snapshots
Each scenario runs the install command to create a fresh project from the Vortex template with 1 combination of answers, then checks the resulting files. A single template change can affect more than 100 of these scenarios. Snapshot testing shows that impact as a set of diffs, so you can review a change across every scenario before it reaches a consumer site.
The scenarios live in functional test classes in
tests/Functional/Handlers/ that extend AbstractHandlerProcessTestCase,
most of them dedicated to 1 handler. BaselineHandlerProcessTest covers
the default installation. A scenario that changes an answer passes it to
the install command through --prompts.
These tests use the
alexskrypnyk/snapshot library
for snapshot-based testing. The snapshot system works on a baseline + diff
pattern:
_baseline/contains the complete reference installation: a full set of template files as they would appear after a default installation.- Each scenario directory, for example
services_no_clamav/,hosting_acquia/, ordeploy_types_all_circleci/, contains only the delta changes from the baseline.
When a test runs, the snapshot library applies the scenario's diffs on top of the baseline and compares the result against the actual output. Storing only the differences, instead of a copy of the entire template for each scenario, keeps the fixture files maintainable.
The fixture directories in .vortex/cli/tests/Fixtures/handler_process/
include scenarios for these areas, among many others:
- CI providers: GitHub Actions, CircleCI
- Hosting: Acquia, Lagoon
- Services: Solr, the Redis-compatible cache, ClamAV (enabled and disabled combinations)
- Deployment: artifact, webhook, Lagoon, container image, all combined
- Database sources: Acquia, Lagoon, FTP, S3, URL, container registry
Running tests
Run the CLI tests from the .vortex/cli/ directory:
cd .vortex/cli
# Install Composer dependencies.
composer install
# Run all tests.
composer test
# Run only handler functional tests.
./vendor/bin/phpunit --filter "Handlers\\\\"
# Run a specific handler test.
./vendor/bin/phpunit --filter "ServicesHandlerProcessTest"
composer test runs without code coverage. composer test-coverage runs
the same suite with coverage, the way CI does. CI runs it on PHP 8.3 and
8.4, after it checks that composer.json is normalized and runs
composer lint.
Updating snapshots
Never edit fixture files by hand. When the template or the CLI logic
changes, regenerate the snapshots from the .vortex/ directory:
cd .vortex
ahoy update-snapshots
# Manual (for debugging specific scenarios).
cd .vortex/cli
UPDATE_SNAPSHOTS=1 ./vendor/bin/phpunit --filter "testHandlerProcess.*baseline"
The regeneration runs the install command for every permutation, starting a fresh project for each scenario, and updates the fixture files to match the current output. The changes land as diffs in version control, where you can review exactly how a template or CLI change affects each scenario.
Commit your template and CLI changes before running
ahoy update-snapshots. The snapshots regenerate from the committed
baseline, so the run misses any uncommitted change. ahoy update-snapshots
is the only sanctioned way to regenerate fixtures: invoking the test suite
with UPDATE_SNAPSHOTS by hand bypasses part of the workflow and produces
partial, inconsistent fixtures.
Releasing
Box packages the CLI as a PHAR
archive: composer build in .vortex/cli, or ahoy build-cli from
.vortex/, writes it to .build/vortex.phar. The Vortex - Release docs
workflow builds it on every tag and publishes it with the documentation at
https://www.vortextemplate.com/install, so the
release process needs no separate CLI step.
The workflow sets the version that the PHAR reports: the tag name for a tag build, or the commit SHA for a branch build. When you run the workflow by hand, you can enter a version that the build reports instead.
The workflow also publishes from every branch whose name contains
release-docs, release-cli, or release-installer, which lets you test
CLI changes before a formal release. Those builds go to the live site, so
until the next publish, everyone who downloads the PHAR gets that branch's
build.
Install demo video
The documentation includes an interactive recording of the install flow,
captured with asciinema.
.vortex/docs/.utils/update-videos.php produces it in these steps:
- Builds the PHAR from source.
- Generates an Expect script that answers the panels with paced keystrokes. It types at a human speed and pauses long enough for each screen to be read.
- Records the session with
asciinema recinto a JSON cast file. - Checks that the recorded run created the project, so a mistimed recording never reaches the published assets.
- Post-processes the recording, replacing local paths with stable ones.
- Decodes the cast to text at
.artifacts/videos/cli-install.txtand stops if the output reports an error, a warning, or a failure. - Converts the cast file to SVG with a custom
svg-term-render.jsscript, and renders 1 frame of it to a PNG poster.
The output files are stored in .vortex/docs/static/img/ as
cli-install.*. The custom AsciinemaPlayer React component plays the
JSON cast on the documentation pages, and the repository's README.md and
the site's home page embed the SVG.
To update the video:
cd .vortex
ahoy update-videos cli-install
The script reaches each panel by counting arrow keys rather than by
matching text. A panel redraws in place, so a text match would land on a
line that's already been overwritten. Adding, removing, or reordering a
panel means adjusting those counts in build_install_expect_script().
sharp-cli, which renders the PNG posters, is a declared dependency of
.vortex/docs, so yarn.lock pins it and the existing
yarn install --frozen-lockfile installs it.
asciinema is downloaded rather than installed from a lockfile, so the
workflows that regenerate the video in CI pin it explicitly.
.github/workflows/vortex-release.yml and
.github/workflows/vortex-test-cli.yml each set ASCIINEMA_VERSION and
ASCIINEMA_SHA256, and they check the download against that checksum
before running it. Bump both workflows together, so the release and the CLI
test always generate assets with identical tooling.