Programmatic API
A zuke.ts is usually driven by the CLI. But
@zuke/core exports the same building
blocks the CLI uses, so you can drive a build yourself — run
a target from another program, assert on the result in a test, or introspect
a build's command surface without shelling out and parsing
--help. Every name below is a real export of
mod.ts; the CLI is just a thin layer over them.
The building blocks
run is the top of the stack — the one call a build file makes.
Underneath it, execute runs a resolved plan, and a family of
pure functions inspect the dependency graph:
import {
discoverTargets, // (build) => Map<string, TargetBuilder>
execute, // (build, rootTarget, options?) => Promise<BuildResult>
executionSet, // (rootTarget) => Set<TargetBuilder>
findCycle, // (targets) => string[] | null
GraphError,
plan, // (rootTarget) => TargetBuilder[] (topological order)
validateGraph, // (targets) => void (throws GraphError)
} from "jsr:@zuke/core"; run — the entry point
run(BuildClass, options?) is what you call at the bottom of a
build file. It instantiates the build, parses Deno.args, runs the
requested command, and sets the process exit code. It returns
Promise<void> and calls Deno.exit itself, so
it's the launcher, not the seam you drive a build through in code —
for that, reach for execute.
import { run } from "jsr:@zuke/core";
import { consoleRenderer } from "jsr:@zuke/console";
class MyBuild extends Build { /* … */ }
// The same entry point the ./zuke launcher uses, with a plugin and a renderer.
await run(MyBuild, {
plugins: [timingPlugin],
renderer: consoleRenderer,
}); run acts only when its module is the program's entry point;
imported elsewhere (for example under test) it does nothing, so no
import.meta.main guard is needed. Its RunOptions
is deliberately small — { args?, plugins?, renderer? }.
The richer knobs (cache, parallel,
remoteCache, state, …) live on
ExecuteOptions, because on the CLI they
come from flags; drive execute directly to set them in code.
execute — run a plan in code
execute(build, rootTarget, options?) is the workhorse. It
resolves the build's parameters, plans
the graph from rootTarget, runs each target body in order (or
concurrently with parallel), and resolves to a
BuildResult — it never calls Deno.exit, which is
exactly what makes it testable. The rootTarget is a
TargetBuilder, which you get from
discoverTargets.
import { discoverTargets, execute } from "jsr:@zuke/core";
import { MyBuild } from "./zuke.ts";
const build = new MyBuild();
const target = discoverTargets(build).get("test");
if (target) {
const result = await execute(build, target, { parallel: true, cache: false });
console.log(result.ok ? "green" : "red");
} The result carries everything the caller needs to react to the run:
interface BuildResult {
ok: boolean; // every executed target passed (also true for a suspended run)
executed: string[]; // target names, in execution order
error?: unknown; // the error that aborted the run, if any
suspended?: boolean; // parked at a .waitsFor(...) gate; state was saved
cancelled?: boolean; // stopped via signal / `zuke cancel`; ok is false
runId?: string; // this run's id, for `zuke runs show` / `zuke cancel`
} ok is true when every executed target passed —
including a run that suspended at a
.waitsFor(...) gate (the process would exit 0). A
cancelled run — via options.signal / Ctrl-C, or
another process running zuke cancel
— is ok: false with cancelled: true, after its
compensations have run.
execute options
ExecuteOptions mirrors the CLI flags, plus a few seams that exist
only for embedding and testing. Every field is optional.
ExecuteOptions | Type | Effect |
|---|---|---|
silent | boolean | Suppress all banner/summary output. |
reporter | Reporter | Custom output sink ({ info(line), error(line) }); overrides silent. |
renderer | Renderer | Restyle the per-target banners and summary (see below). |
plugins | Plugin[] | Lifecycle observers invoked alongside the build's hooks. |
skip | string[] | Target names to skip even if planned (--skip). |
parallel | boolean | number | Run independent targets concurrently; a number caps concurrency, true uses the CPU count (--parallel). |
cache | boolean | BuildCache | Incremental caching; false disables it (--no-cache). A BuildCache is used directly. |
remoteCache | RemoteCacheStore | false | Share outputs across machines; false uses the local cache only (--no-remote-cache). Ignored when cache is false or a supplied BuildCache. |
params | Record<string, string> | Raw parameter values, keyed by property name. |
dryRun | boolean | Print the plan without running any body or touching the cache (--dry-run). |
affected | AffectedOptions | Restrict the run to targets affected by files changed since a git base (--affected). |
state | boolean | Persist durable run state under .zuke/runs when nothing else configures a store (--state). |
stateStore | StateStore | false | Supply the durable state store directly; false disables state entirely. |
actor | string | Who to attribute the run to in its state record (--actor). |
signal | AbortSignal | Cancel the run when this aborts, running the succeeded targets' compensations. |
readEnv | (name) => string | undefined | Environment reader for parameter fallback; defaults to Deno.env.get (a test seam). |
prompt | (flag, desc) => string | undefined | Prompt for a missing required parameter; defaults to an interactive TTY prompt off CI (a test seam). |
github | boolean | Force GitHub Actions output formatting on/off (auto-detected otherwise). |
color | boolean | Force ANSI colour on/off (auto-detected otherwise). |
One field is intentionally absent above: resume. It carries the
continuation state of a suspended run and is set by
resumeRun, not passed by hand — call
resumeRun to continue a
suspended run rather than constructing it yourself.
discoverTargets — find targets
discoverTargets(build) introspects a build instance and returns a
Map<string, TargetBuilder> of every declared target, keyed
by its property name. Discovery recurses into plain object fields, so a
reusable component contributes its targets under a dotted
path (release.publish). This is how you turn a target name into
the rootTarget that execute
needs.
import { discoverGroups, discoverTargets } from "jsr:@zuke/core";
const build = new MyBuild();
const targets = discoverTargets(build); // Map<string, TargetBuilder>
targets.get("test"); // a top-level target
targets.get("release.publish"); // a component target, under its dotted path
// discoverGroups(build) does the same for declared parallel groups.
describeCli — inspect the surface
describeCli(build) returns a structured CliDescription
of a build's whole command surface — its reserved commands, option flags,
targets (with descriptions and dependencies), and
parameters — the same data that backs
zuke --list --json. Use it to build tooling around a build in
code, with no --help text to parse.
import { describeCli } from "jsr:@zuke/core";
const cli = describeCli(new MyBuild());
for (const t of cli.targets) console.log(t.name, "→", t.dependsOn.join(", "));
// Drop .secret() parameters from the surface (what `zuke register` writes):
const safe = describeCli(new MyBuild(), { omitSecrets: true });
Each CliTargetInfo carries name,
description, dependsOn, default, and
unlisted; each CliParameterInfo distinguishes the
property name (the key execute's params
map and MCP tool calls use) from its kebab-case flag. Pass
{ omitSecrets: true } to drop .secret()
parameters — the posture the build registry uses
so a secret never crosses the run boundary.
Renderer — custom output
The per-target banners and the end-of-build summary are produced by a
Renderer. Each method is pure — it returns the
lines to print rather than writing them — so the executor keeps control of
the output streams and a custom renderer stays unit-testable. Zuke ships
defaultRenderer; spread it and override only the hooks you care
about, or inject consoleRenderer from
@zuke/console for an alternative look.
import { defaultRenderer, execute, type Renderer } from "jsr:@zuke/core";
const quiet: Renderer = {
...defaultRenderer,
// Override only the hooks you want to change…
targetHeader: () => [],
};
await execute(build, target, { renderer: quiet });
Renderer methodProduces targetHeader(style, name)the banner that opens a target's section targetPassFooter(style, name, ms)the footer after a target body succeeds targetFailFooter(style, name, ms, error)the failure footer, split into { info, error } lines targetDryRunFooter(style, name)the footer for a dry-run target that never executed summaryBlock(style, reports, totalMs, ok)the end-of-build summary table and verdict jobSummaryMarkdown(reports, totalMs, ok)the GitHub Actions job-summary Markdown
A renderer receives each TargetReport and the Style
palette, so custom rendering doesn't have to reimplement colour handling.
BuildCache — inject a cache
The cache option normally takes a boolean, but it also accepts a
BuildCache instance directly. This is mainly a test seam: supply
a pre-seeded or isolated cache so a run's cache
behaviour is deterministic without touching .zuke/cache.json.
import { execute, type BuildCache } from "jsr:@zuke/core";
// A minimal in-memory cache — never touches .zuke/cache.json.
const cache: BuildCache = {
upToDate: async () => false,
record: async () => {},
save: async () => {},
};
await execute(build, target, { cache });
The interface is three methods —
upToDate(target) (is a target's fingerprint unchanged and its
outputs still present?), record(target) (store the fingerprint
after a successful run), and save() (persist the store) — so a
custom in-memory implementation is a few lines when you need one.
Graph inspection
The planning primitives are exported as pure functions, so you can reason
about a build's graph without running it:
Function Returns plan(root)TargetBuilder[] — the deduplicated topological order a run would execute executionSet(root)Set<TargetBuilder> — root plus the transitive closure of its dependencies findCycle(targets)string[] | null — the names forming a dependency cycle, or null validateGraph(targets)void — throws GraphError on a missing reference or a cycle
import {
discoverTargets,
GraphError,
plan,
validateGraph,
} from "jsr:@zuke/core";
const build = new MyBuild();
const targets = discoverTargets(build);
try {
validateGraph(targets); // throws GraphError on a missing ref or a cycle
} catch (error) {
if (error instanceof GraphError) console.error(error.message);
}
// The topological order a run would execute, without running anything:
const order = plan(targets.get("release"));
console.log(`${order.length} targets, in execution order`);
See Using Zuke as a library for the file, HTTP,
and install helpers a build shares with these, and
Extending Zuke for the
Plugin lifecycle that run and execute
both accept.