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.