Skip to main content
Version: v2

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:

  1. 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 --schema prints, and --validate checks the object without changing anything.
  2. Environment variables - 1 per prompt, named VORTEX_CLI_INSTALL_PROMPT_<ID>.
  3. Discovery - auto-detection from existing project files, for example composer.json, .env, or hosting-specific files like .lagoon.yml.
  4. 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:

  1. You download the PHAR from https://www.vortextemplate.com/install and run it with php vortex.phar. InstallCommand orchestrates the rest.
  2. The form collects the answers, applying conditionals and discovery as it goes.
  3. 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.
  4. 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:

PathContents
https://www.vortextemplate.com/installThe current major's pin
https://www.vortextemplate.com/v1/installThe 1.x line
https://www.vortextemplate.com/v2/installThe 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_.

VariablePurpose
VORTEX_CLI_VERSIONOverrides the stamped version
VORTEX_CLI_URLWhere vortex-update downloads the PHAR
VORTEX_CLI_PATHLocal PHAR path, overrides the URL
VORTEX_CLI_URL_CACHE_BUSTCache-busting parameter for the URL
VORTEX_CLI_INSTALL_TMP_DIRWorking directory for the downloaded source
VORTEX_CLI_INSTALL_TEMPLATE_REPOTemplate 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/, or deploy_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.

warning

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:

  1. Builds the PHAR from source.
  2. 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.
  3. Records the session with asciinema rec into a JSON cast file.
  4. Checks that the recorded run created the project, so a mistimed recording never reaches the published assets.
  5. Post-processes the recording, replacing local paths with stable ones.
  6. Decodes the cast to text at .artifacts/videos/cli-install.txt and stops if the output reports an error, a warning, or a failure.
  7. Converts the cast file to SVG with a custom svg-term-render.js script, 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.