Core concepts
Five ideas cover almost everything in Zuke: targets, dependencies, the
$ shell, tool wrappers, and parameters. Each one is just
TypeScript, so your editor, type-checker, and debugger all work as usual.
Targets
A target is a unit of work — compile, test, publish. You
define one as a class field with target() and a fluent chain:
compile = target()
.description("Type-check & bundle") // shown in --list
.dependsOn(this.clean, this.restore) // typed references
.executes(async () => {
// ordinary async TypeScript
}); .description() documents the target, .dependsOn()
wires prerequisites, and .executes() holds the action — an
ordinary async function.
Dependencies
Targets reference each other with this.target — real values,
not strings. Zuke builds the dependency graph, then runs targets in
topological order, executing each one exactly once even when
several targets share a prerequisite.
class MyBuild extends Build {
clean = target().executes(async () => { await $`rm -rf dist`; });
restore = target().executes(async () => { await DenoTasks.cache(); });
compile = target()
.dependsOn(this.clean, this.restore) // runs both first, once each
.executes(async () => { /* ... */ });
test = target()
.dependsOn(this.compile)
.executes(async () => { await DenoTasks.test(); });
} Because dependencies are typed references, renaming a target updates every dependant automatically and a typo is a compile error — not a build that silently does the wrong thing.
The $ shell
The $ tagged template from @zuke/core/shell runs
processes with sensible defaults. Interpolated values are escaped, so
untrusted input can't break out of a command:
import { $ } from "jsr:@zuke/core/shell";
await $`deno test -A`; // throws on non-zero exit
const sha = await $`git rev-parse HEAD`.text(); // trimmed stdout
const code = await $`flaky-cmd`.noThrow().code(); // exit code, never throws
// Interpolations are escaped — no shell injection:
const branch = "feature/$(rm -rf /)";
await $`git checkout ${branch}`; // treated as a single literal arg
Chain .text() for trimmed stdout, .code() for the
exit status, or .noThrow() to handle failures yourself.
Tool wrappers
Rather than hand-write CLI strings, reach for the typed
@zuke/* wrappers. Each exposes a fluent, discoverable API over a
real tool — Deno, npm, Bun, Docker, Kubernetes, Playwright, Terraform and
more — all published to JSR:
import { NpmTasks } from "jsr:@zuke/npm";
import { DockerTasks } from "jsr:@zuke/docker";
await NpmTasks.ci(); // npm ci
await NpmTasks.run((s) => s.script("build")); // npm run build
await DockerTasks.build((s) => s.tag("app:latest").file("Dockerfile")); Parameters
Builds often need inputs — a configuration, a version, a target environment. Parameters give you typed values with defaults, sourced from flags or environment variables:
class MyBuild extends Build {
// expose typed parameters via flags or environment
configuration = parameter("Build configuration").default("Debug");
compile = target()
.executes(async () => {
await DenoTasks.check((s) => s.paths("mod.ts"));
console.log(`Built in ${this.configuration.value} mode`);
});
} From here, browse the examples to see these concepts combined into complete, real-world build files.