CLI reference
The complete command and flag reference. Every entry here is grounded in the
argument parser (cli_spec.ts, cli.ts) and the
launcher CLI (@zuke/cli), not the help text alone — so what you
read is what the parser actually accepts.
Install the global CLI (project scaffolding) once:
deno install -A -g -n zuke jsr:@zuke/cli Scaffold zuke.ts and the ./zuke launcher:
zuke setup Everything after setup runs through the scaffolded launcher (./zuke build is deno run -A zuke.ts build):
./zuke build Two commands named zuke
There are two distinct programs, and the flags below belong to one or the other:
- The global CLI (
@zuke/cli, installed withdeno install) is the pre-build tool: it scaffolds a project (setup,import), documents packages (doc), and reports its own--version. You run it aszukebefore a build file exists. - The build launcher — the
./zukescriptsetupwrites, which is justdeno run -A zuke.ts— drives your build: it runs targets and hosts every build-aware command (graph,generate-ci,completions,runs,resume,cancel,mcp,register, and its owndoc).
Below, zuke denotes the global CLI and ./zuke the
build launcher. Both accept --help/-h.
The reserved command words (graph, generate-ci,
completions, mcp, resume,
runs, cancel, register,
doc) are not runnable as target names — a target called
graph can't be launched by name. The parser, --help,
and shell completion all read this list from a single source of truth, so
they never drift out of sync.
Project: setup, import, doc, version
zuke setup scaffolds a starter zuke.ts and a
./zuke launcher into a directory. It prompts for the build class
name and whether to overwrite unless --yes is given or stdin
isn't a terminal.
zuke setup # scaffold into the current directory
zuke setup --dir services/api # scaffold into a subdirectory
zuke setup --name ApiBuild --yes # name the Build class, skip prompts
zuke setup --launcher-name make # write ./make when a zuke/ dir is in the way
zuke setup --mcp # also write .mcp.json so agents find the build
zuke setup --no-bootstrap-deno # launchers that require Deno on PATH zuke setup | Effect |
|---|---|
--dir <path> | directory to scaffold into (default .) |
--name <Class> | the Build subclass name for zuke.ts (default MyBuild) |
--launcher-name <name> | base name for the launcher scripts — used when a zuke/ directory already occupies the name. Must be a single path segment |
--force, -f | overwrite existing files |
--yes, -y | accept defaults without prompting |
--mcp | also write a .mcp.json registering the build's own MCP server, so an agent client picks the build up from the first commit. An existing file is merged around its other servers, and an existing zuke entry is kept unless --force is set |
--allow-run | register the MCP server with execution enabled; implies --mcp |
--bootstrap-deno / --no-bootstrap-deno | which launchers to write. The default (and what --yes takes) installs a pinned, checksum-verified Deno when none is on PATH, so a checkout needs nothing up front; --no- writes launchers that require Deno on PATH and fail closed without it — for a project that must never download a tool from its build entry point |
zuke import takes the same flags plus --from, and
generates a zuke.ts from an existing project's build scripts —
a package.json's scripts or a Makefile.
Without --from it auto-detects the source.
zuke import # auto-detect package.json scripts or a Makefile
zuke import --from package.json # force the source
zuke import --from makefile --yes zuke import | Effect |
|---|---|
--from <source> | package.json or makefile; auto-detected when omitted |
--dir, --name, --force, --yes | as for setup |
zuke doc <package> prints a package's API by running
deno doc from an isolated empty directory. A
bare name resolves to a Zuke package (core →
jsr:@zuke/core); a scoped name (@scope/pkg) becomes
jsr:@scope/pkg; and any explicit
jsr:/npm:/https:/file:/path
specifier passes through. The isolation matters inside a Node repo: run
plainly, deno doc resolves the repo's
node_modules/@types/* and buries the API under
type-resolution warnings; the empty working directory has nothing to
resolve. A relative path is pinned to your real directory first.
zuke doc core # → deno doc jsr:@zuke/core
zuke doc @scope/pkg # a scoped package
zuke doc jsr:@zuke/deno # a jsr:/npm:/https:/file: specifier, passed through
# The build launcher has its own 'doc' command, but it only passes through
# explicit specifiers/paths (jsr:/npm:/https:/file:/relative) — no bare-name
# 'core' → jsr:@zuke/core mapping, so give the full specifier:
./zuke doc jsr:@zuke/deno zuke --version (or -V) prints the CLI version.
./zuke completions
A build-launcher command: it emits a shell-completion script for
bash, zsh, or fish that completes your
build's target names, the reserved commands, the built-in flags, and any
declared parameters as --flag candidates. Unlisted targets stay
hidden, exactly as in --list. It takes an explicit sub-action
first — print or install — then the shell.
# Print a script and source it in the current shell:
source <(./zuke completions print bash)
source <(./zuke completions print zsh)
./zuke completions print fish | source
# Or wire it into your shell's startup once:
./zuke completions install zsh | Sub-action | Effect |
|---|---|
print <shell> | write the script to stdout (source it yourself) |
install <shell> | write the script under your config directory (honouring $XDG_CONFIG_HOME) and wire it into the shell's startup. Idempotent — an already-sourced rc file is left untouched |
The script is a static snapshot of the build it was generated from, so
regenerate and re-source it after you add, rename, or remove targets — the
same model as deno completions.
./zuke outdated
Compares the version your deno.lock resolves for every
jsr: specifier against the latest each package publishes, and
prints the ones that are behind.
./zuke outdated # report jsr: pins the lock resolves behind latest
./zuke outdated --exit-code # exit 1 if anything is behind or unverifiable @zuke/git 1.5.0 → 1.11.0
@zuke/gcloud 1.1.0 → 1.3.0
2 packages are behind. Delete these entries from the lock (or the lock file) and re-run …
It covers the case nothing else does. A build whose specifiers are written
inline — jsr:@zuke/git@^1 in zuke.ts rather than in
a deno.json imports map — gets no signal from
deno outdated, which reads manifests. The lock keeps resolving
the versions recorded when the build was written and --frozen is
content with that, so a build can sit several minors behind a wrapper for
months, still hand-rolling a command the package has since typed. Reading the
lock is the point: it records what a run actually resolves,
which is the number a stale pin hides.
A package the registry can't answer for — a private scope, a rename, an
offline runner — doesn't fail the report, but it is named in it.
Otherwise a run behind a proxy that reached nothing at all would print
"every package is at its latest release", the confident wrong answer this
command exists to prevent. --exit-code exits 1 when
anything is behind or could not be checked, for the same
reason; without it the command is a report and always exits 0.
A missing lock file is an outright error.
It needs the network, which is why it's a command you run rather than a line
in --list or the run summary — those stay offline and instant.
To refresh afterwards, remove the stale entries from deno.lock
(or delete the lock) and re-run: neither --reload flavour
re-resolves an inline specifier, and deno outdated --update
reads manifests, so it can't see one either.
Running targets
./zuke <target> runs the target and all its transitive
dependencies in order. With no positional argument, the launcher runs a
target literally named default if one exists, otherwise it
prints --list. Each target prints a start banner and a
✔/✘ line with its duration; a failure aborts the
rest and exits 1.
./zuke build # run 'build' and all its dependencies
./zuke # run the 'default' target, else print --list
./zuke test --skip lint # run 'test' but skip the 'lint' dependency
./zuke ci --parallel # run independent targets concurrently
./zuke ci --parallel=4 # cap the number in flight
./zuke ci --affected=origin/main # only targets reachable from changed files
./zuke deploy --dry-run # print the plan without executing any body
./zuke build --no-cache # ignore the incremental cache; re-run all | Flag | Behaviour |
|---|---|
--skip <dep> | run the target but skip the named dependency (repeatable) |
--parallel[=N] | run independent targets concurrently, still completing every dependency before its dependents. N caps the number in flight (default: the host CPU count). See execution model |
--affected[=<base>] | run only the targets a git diff since <base> can reach (default base HEAD); a target with no declared .inputs() can't be proven unaffected, so it always runs. See affected targets |
--no-cache | ignore the incremental cache and re-run every target |
--no-remote-cache | use the local cache only; skip the remote cache store (neither restore nor upload). --no-cache disables both |
--dry-run | resolve the plan and print every target that would run (honouring --skip and each onlyWhen) without executing any body or touching the cache |
--state | persist a durable run record under .zuke/runs when no store is otherwise configured |
--actor <name> | attribute the run to <name> in its state record (else ZUKE_ACTOR, the CI actor, or "anonymous") |
--list, -l | list all targets with descriptions and dependencies |
--json | emit the whole build surface (commands, flags, targets, parameters) as JSON — for tools and agents |
./zuke --list # targets with descriptions and dependencies
./zuke --list --json # the whole build surface (commands, flags, targets) as JSON An unrecognised --flag is a hard error. Zuke
names it, suggests the nearest known flag when one is within two edits
(--dry-rn → --dry-run), and exits 1
without running anything — a typo used to be silently ignored, which meant
--dry-rn ran the build for real. The same applies to an
unknown target name. --help still wins when it appears
alongside a bad flag, a bare -- separator is skipped, and a
built-in given an inline value it does not accept
(--skip=lint) is told to pass the value as the next argument
instead.
Graph & CI
./zuke graph shows the dependency graph. By default it prints the
terminal target → deps adjacency listing; --output=html
renders an interactive Cytoscape page to
<repo root>/.zuke/graph.html and opens it (Cytoscape loads
from a pinned CDN, so the first view needs internet access).
./zuke graph # print the 'target → deps' adjacency listing
./zuke graph --output=html # render an interactive page to .zuke/graph.html and open it
./zuke graph --output=html --no-open | Flag | Behaviour |
|---|---|
--output=html | render the interactive HTML page instead of terminal text (--output text is the default) |
--no-open | with --output=html, write the file without opening a browser |
./zuke generate-ci writes the CI configuration files the build
declares via cicd(). Running any target regenerates them too, so
the command is mainly for CI verification.
./zuke generate-ci # write the CI files declared via cicd()
./zuke generate-ci --check # verify they are current instead (fail if stale) — use on CI | Flag | Behaviour |
|---|---|
--check | verify the declared files are current instead of writing them, failing if any has drifted (use on CI, so an ephemeral checkout is never left dirty) |
State & orchestration
These commands read and steer persisted run records.
They resolve the state store the same way a run does — ZUKE_STATE_URL
/ ZUKE_STATE_DIR, the build's stateStore() override,
or the default .zuke/runs — and report a friendly error when no
store is configured.
./zuke ci --state # persist a run record under .zuke/runs
./zuke runs list # one row per run, newest first
./zuke runs list --status failed --limit 20
./zuke runs list --target deploy --since 2026-07-01T00:00:00Z
./zuke runs list --counts --json # aggregate { total, byStatus }
./zuke runs show <run-id> --json # one run's full per-target status
./zuke runs prune --keep 90d --keep-last 50 --dry-run ./zuke runs takes a sub-action: list,
show <id>, or prune.
./zuke runs | Behaviour |
|---|---|
list | one row per run — id, status, root target, actor, creation time — newest first |
list --status <s> | keep only runs with this status: running, suspended, cancelling, succeeded, failed, or cancelled |
list --target <t> | keep only runs whose graph contains that target |
list --since <iso> | keep only runs created at/after an ISO-8601 timestamp |
list --limit <n> | return at most the newest N (must be a positive integer) |
list --counts | print aggregate counts (a total and one line per status) instead of rows; with --json emits { total, byStatus } |
show <id> | reconstruct one run in full: header, resolved non-secret parameters, per-target status/duration/error, and any signals received |
prune --keep <age> | keep runs newer than an age (e.g. 90d); older terminal runs become eligible |
prune --keep-last <n> | always keep the newest N terminal runs |
prune --dry-run | report what would be deleted without deleting; at least one --keep/--keep-last rule is required |
--json | on list/show, emit machine-readable output (the summary array or the whole record) |
prune never touches non-terminal runs
(suspended, running, cancelling) — a
run is deleted only when it is terminal and matches neither keep rule. See
retention for who owns pruning on each
backend.
./zuke resume <id> continues a run parked at a
.waitsFor() gate; ./zuke cancel <id> stops a
run and unwinds its .onCancel(...) compensations in reverse
order. Resumption is exactly-once — concurrent resumers race
a compare-and-swap and all but one get AlreadyResumedError. See
suspend & resume and
cancellation.
./zuke resume <run-id> # continue a suspended run
./zuke resume <run-id> --signal approved --data '{"by":"alice"}'
./zuke resume --check # reap abandoned runs, then re-check waits & timeouts
./zuke resume <run-id> --resume-degraded # continue despite a lost state write
./zuke cancel <run-id> --actor ci-bot # cancel and run compensations | Flag | Behaviour |
|---|---|
resume --signal <name> | deliver a named external signal to the run |
resume --data <json> | with --signal, the signal's JSON payload (default {}; capped at 64 KiB) |
resume --check [<id>] | the cron/webhook entry point. Makes three passes: reap abandoned runs (a running record whose lease can be acquired belongs to a dead process, so it returns to suspended and is resumed in the same sweep — or settles failed with its compensations if it is past the build's deadline()), then finish runs left cancelling by a dead settler, then re-check predicate waits and enforce timeouts across suspended runs |
resume --force-graph | continue even if the build graph changed since the run was suspended |
resume --resume-degraded | continue a resume whose record is degraded (a state write was permanently lost, so a target that succeeded may still be recorded running — a resume would run it a second time). Also accepted by --check, which otherwise counts a degraded run as failed on every sweep |
resume --actor <name>, cancel --actor <name> | attribute the resume/cancellation in the audit trail |
Ctrl-C (or SIGTERM) during a run triggers the same
graceful cancellation in-process; a second signal forces an immediate exit.
./zuke force
An operator sometimes has to take a step off a live run: one that cannot
succeed, or one a person completed by hand.
./zuke force <id> <target> --outcome skipped|succeeded
records an override on the record, and
the executor settles that target without running its body —
ahead of its onlyWhen conditions and its cache, because forcing
outranks what the build would work out for itself. Dependents proceed either
way.
# settle a step that cannot succeed, or one a person completed by hand
./zuke force <run-id> migrate --outcome succeeded --reason "ran by DBA"
./zuke force <run-id> smokeTest --outcome skipped --reason "env is down"
The two outcomes differ under a later cancellation. A forced
succeeded asserts the target's effects exist, so it is
compensated like any other succeeded target; a forced skipped
never happened, so it isn't — exactly like a target a condition skipped.
It is refused, naming the rule, when the target has already settled (the
record is the account of what happened, and rewriting a settled outcome would
make it untrue), when the run is terminal, when the target is not in the
run's graph, or when the build declared it off-limits via
unforceable(). An override lands for any target the run has not
started yet, which in practice means the next resume.
./zuke runs show prints every override, and over
MCP the same operation is the
force_target tool.
A resume refuses a run whose record is
degraded and names the risk, because it
re-runs every target the record does not show as succeeded —
which for a deploy or a release would mean doing it twice.
--resume-degraded accepts that risk; use it once you know
those targets are safe to repeat. zuke cancel faces the same
gap from the other side and widens its compensation walk to every target
whose success the record cannot rule out.
MCP & registry
./zuke mcp runs a Model Context Protocol
server over the build so an AI agent operates the pipeline through typed tool
calls. It is read-only by default (inspect targets,
parameters, graph, and runs); --allow-run adds one
run:<target> tool per target. Every mutating or denied call
is written to an audit trail (./zuke runs show mcp-audit).
./zuke mcp # read-only MCP server on stdio
./zuke mcp --allow-run # also expose run:<target> tools
./zuke mcp --allow-run='build,test:*' --protect='deploy,release'
./zuke mcp --allow-run --confirm-destructive # destructive runs return a plan first
./zuke mcp --http 127.0.0.1:8787 # streamable-HTTP transport
./zuke mcp --registry --max-concurrent-runs 8 # serve every registered build
./zuke register # record this build in the registry | Flag | Behaviour |
|---|---|
--allow-run[=<globs>] | let agents execute targets, not just inspect them; an optional comma-separated glob allow-list exposes only matching targets as run tools (others stay invisible) |
--protect <globs> | require an operator token (ZUKE_OPERATOR_TOKEN) as a tool-call argument for any run whose plan touches a matching target — so a protected deploy reached as a dependency of an unprotected release still demands it. Fail-closed: an unresolvable plan is denied, not assumed harmless |
--confirm-destructive | make a destructive run tool return its plan until called with confirm:true (a .readOnly() target is exempt) |
--registry | serve the build registry instead of this one build — expose every registered pipeline's targets, re-read live so a newly-registered build appears with no restart |
--max-concurrent-runs <n> | with --registry, cap concurrent run-tool spawns (default 4); a call past the cap gets an immediate busy error, read tools are never counted |
--http <host:port> | serve the streamable-HTTP transport instead of stdio. A bare <port> binds 127.0.0.1; a non-loopback host requires a bearer token (ZUKE_MCP_TOKEN) |
--allowed-origin <origin> | with --http, permit an extra browser Origin (repeatable). By default a loopback bind accepts only loopback origins — the drive-by / DNS-rebinding guard; a request with no Origin (a CLI client) is always allowed |
There is no --token flag. The HTTP bearer
token is read only from the ZUKE_MCP_TOKEN environment
variable, and the operator token from ZUKE_OPERATOR_TOKEN —
never from an argument that would land in a process listing or shell
history.
./zuke register records this build in the build registry (its
targets, parameters, and launch location, excluding secrets) so a
registry-backed MCP server can discover it. It is idempotent, accepts
--actor <name> and --json, and writes to
.zuke/builds unless ZUKE_REGISTRY_URL/_DIR
or the build's registry() configures a store. See
the registry.
Environment variables
These configure state, the registry, the remote cache, MCP authorization, and
tool resolution without a build-file change — the usual way to point the same
build at production infrastructure from CI. For each pluggable backend, the
_URL variable (with an optional _TOKEN) selects the
HTTP backend; the _DIR variable selects the filesystem backend.
# Never a flag — the loopback-only guard reads it from the environment:
ZUKE_MCP_TOKEN=… ./zuke mcp --http 0.0.0.0:8787
# Configure a remote cache with no build-file change:
ZUKE_REMOTE_CACHE_URL=https://cache.example.com ZUKE_REMOTE_CACHE_TOKEN=… ./zuke ci
ZUKE_REMOTE_CACHE_DIR=/mnt/zuke-cache ./zuke ci
# Configure durable state / the registry the same way:
ZUKE_STATE_URL=https://state.example.com ZUKE_STATE_TOKEN=… ./zuke ci --state
ZUKE_STATE_DIR=.zuke/runs ./zuke runs list | Variable | Effect |
|---|---|
ZUKE_MCP_TOKEN | bearer token required by a non-loopback mcp --http bind |
ZUKE_OPERATOR_TOKEN | operator token a protected (--protect) MCP run tool must carry |
ZUKE_STATE_URL / ZUKE_STATE_TOKEN | select an HttpStateStore for durable run state (see store precedence) |
ZUKE_STATE_DIR | select a filesystem state store at this directory |
ZUKE_REGISTRY_URL / ZUKE_REGISTRY_TOKEN | select an HttpBuildRegistry for register and registry-backed MCP |
ZUKE_REGISTRY_DIR | select a filesystem build registry at this directory |
ZUKE_REMOTE_CACHE_URL / ZUKE_REMOTE_CACHE_TOKEN | select an HttpCacheStore for the remote cache |
ZUKE_REMOTE_CACHE_DIR | select a FileSystemCacheStore at this directory |
ZUKE_ACTOR | default actor for a run when --actor is absent (before the CI actor and "anonymous") |
ZUKE_LOG_LEVEL | seeds the console threshold — trace, debug, info (the fallback), warn, error, or silent. See Console output. Colour follows NO_COLOR and TTY detection; GitHub annotation mode follows GITHUB_ACTIONS |
ZUKE_TOOL_RESOLUTION | ambient override for how tool wrappers locate their binary: path (bare name on $PATH) or node_modules (npx-style node_modules/.bin walk). See installing tools |
Store precedence is always: an explicit execute(...) option,
then the build's typed override (stateStore(),
registry(), remoteCache()), then these
environment variables. An unreadable ZUKE_TOOL_RESOLUTION (no
--allow-env) or an unrecognised value is treated as unset.