One CLI for every Spark project: scaffold, run, and test from a single binary.
Every project accumulates its own pile of commands: the docker compose incantations, the container names, the scripts you have to remember. Spark replaces the pile with one binary that adapts to whatever project you run it in. Each project ships a committed spark.yml manifest that names the project and defines its commands; run spark inside a Spark Craft checkout and you get that project's commands, run spark create anywhere and it scaffolds a fresh project from a published boilerplate.
- Adapts to the project — Spark walks up to the nearest
spark.ymland serves that project's commands on top of a set of built-in verbs - Built-in Docker verbs —
up,down,restart,logs,sh, andrundrive any Compose stack with zero configuration - Project scaffolding —
spark createturns a boilerplate repo into a running project: prompts, file copies, renames, token replacement, andgit initincluded - Disposable Craft instances —
spark labmints throwaway Craft CMS installs for plugin development, powered by Spark Craft Lab - Single static binary — no npm, no Ruby, no runtime dependencies; install once and every project just works
Install with Homebrew:
brew install jalendport/tap/sparkOr with the install script (macOS and Linux):
curl -fsSL https://raw.githubusercontent.com/jalendport/spark-cli/master/install.sh | shThe script downloads the latest release binary for your OS and architecture and installs it to /usr/local/bin, falling back to ~/.local/bin when that isn't writable.
Inside any project with a spark.yml, these drive the Docker Compose stack. Extra arguments pass straight through to the underlying command.
A manifest command with the same name as a built-in overrides it (and inherits its aliases); a built-in that can't find a compose file says so and points you at the fix.
Any command defined under commands: in spark.yml becomes a top-level verb. Manifest commands run as shell one-liners from the project root, so pipes and redirects work as written. Extra arguments are substituted for a {args} placeholder, or appended when there is no placeholder.
spark craft migrate/all # runs the manifest's `craft` command with `migrate/all`spark create # pick a boilerplate interactively
spark create craft my-site # scaffold straight into ./my-sitespark create downloads the boilerplate, asks its questions, copies and renames shipped files into place, fills the answers into the project's text files, runs any post commands, and initializes a fresh git repository — then offers to run the boilerplate's setup commands. A failed create leaves nothing behind.
From inside any Craft plugin working copy:
spark lab up # mint + boot + seed a disposable Craft instance
spark lab up --craft 4.16 --db pg # pin the Craft version and database
spark lab destroy --all # tear everything down without a traceSee the Spark Craft Lab repo for the full lab workflow, including the lab/test.twig smoke page every instance serves at /lab-test.
A project's entire Spark surface lives in its committed spark.yml:
name: spark-craft
description: Craft CMS 5 starter with Docker, Vite, Twig, and Vue
commands:
craft: docker compose exec php ./craft {args}
composer:
run: docker compose exec php composer {args}
help: Run Composer inside the php container
create:
prompts:
- key: project_name
label: Project name
copy:
.env.example.dev: .env
rename:
composer.json.project: composer.json
replace:
__PROJECT_NAME__: "{project_name}"
__PROJECT_SLUG__: "{project_name:slug}"
setup:
- spark up --wait
- spark composer craft-setupname/description— shown in the styled help headercommands— string form for a bare shell one-liner, or map form withrunandhelp;{args}marks where extra CLI arguments landcreate— executed byspark createafter download:promptsare asked interactively,copyduplicates shipped files into place (source → destination, e.g. seeding.envfrom a committed.env.example.devwhile keeping the original),renamemoves shipped template files into place (e.g.composer.json.project→composer.json), and the answers fill{key}placeholders inreplace, which is then substituted across the project's text files. A{key:slug}placeholder slugifies the answer, so one prompt can fill both a display name and its slug.spark.ymlitself is exempt fromreplace, so the manifest's own tokens are never rewritten in the new project.postruns shell commands from the new project root.setupruns shell commands too, but only once the project has landed in its final directory and after a confirmation prompt — the place to start docker compose, which needs the real project directory for its naming and bind mounts. Becausespark.ymlis skipped byreplace,postandsetupcommand lines support the same{key}and{key:slug}placeholders asreplacevalues, so an answer can be spliced straight into a command (e.g.docker compose exec -e SITE_NAME="{project_name}" php composer craft-setup). Decline the prompt, or hit a setup command that fails, and the remaining commands are printed verbatim — fully expanded — as next steps so you can finish by hand.project_nameis a reserved prompt key: when you runspark createwithout a directory argument, the CLI already asked for the project name to name the directory, so aproject_nameprompt is pre-answered with what you typed rather than asked again.
Unknown manifest keys warn instead of failing, so older binaries keep working with newer manifests.
Found a bug or need help? Open an issue.
Made by Jalen Davenport
