Core library

Beyond running processes, @zuke/core ships a small standard library so a build rarely needs an extra dependency. Everything here is exported from jsr:@zuke/core, works on the bare Deno runtime, and is designed to be unit-testable.

File operations

FileTasks groups the filesystem operations a clean/package target reaches for, in the same namespaced shape as the tool wrappers — but it runs no subprocess, so methods take direct arguments instead of a settings-lambda. They're written to be idempotent: remove tolerates a missing target (like rm -f) and returns whether anything was deleted, and a recursive createDirectory is a no-op when the directory already exists.

import { FileTasks } from "jsr:@zuke/core";

await FileTasks.cleanDirectory("dist");          // empty it (if it exists)
await FileTasks.createDirectory("dist/assets");  // mkdir -p
await FileTasks.copy("static", "dist/static");   // recursive
await FileTasks.move("build.log", "logs/build.log");
const removed = await FileTasks.remove("tmp", { recursive: true }); // rm -rf → boolean

if (await FileTasks.exists("deno.json")) {
  const cfg = await FileTasks.readJson<{ name: string }>("deno.json");
  await FileTasks.writeText("VERSION", cfg.name);
}

Globbing

glob(pattern, { cwd? }) expands a pattern to the matching paths — relative to cwd and sorted for determinism. It is dependency-free, walks from the pattern's static prefix, and does not follow symlinked directories. Supported syntax: * (any run of non-/), ** (any run including /), ? (a single non-/), and brace alternation {a,b}.

import { glob } from "jsr:@zuke/core";

const sources = await glob("src/**/*.ts");                 // sorted, relative
const specs = await glob("**/*.{test,spec}.ts", { cwd: "packages" });

await DenoTasks.fmt((s) => s.check().paths(...sources));

HTTP

Fetch over HTTP from a build script, built on the platform fetch. httpDownload(url, dest) streams a URL to a file, while httpText(url) and httpJson(url) return the body. All accept { headers, fetch } — the fetch seam makes them unit-testable — and throw an HttpError (carrying .status) on a non-2xx response.

import { httpDownload, httpJson, httpText } from "jsr:@zuke/core";

await httpDownload("https://example.com/tool.tar.gz", ".zuke/tool.tar.gz");
const notes = await httpText("https://example.com/CHANGELOG.txt");
const release = await httpJson<{ tag_name: string }>(
  "https://api.github.com/repos/zuke-build/zuke/releases/latest",
);
// a non-2xx response throws an HttpError carrying .status

Compression & archives

Pack build artifacts without a dependency. gzip() / gunzip() wrap the platform CompressionStream; tar() / untar() read and write the POSIX ustar format in memory; and the file helpers createTarGzip(files, dest, { cwd }) / extractTarGzip(src, destDir) produce and unpack .tar.gz archives. Archives use a fixed mtime, so output is reproducible.

import { createTarGzip, extractTarGzip, gzip } from "jsr:@zuke/core";

await createTarGzip(["dist/app.js", "README.md"], "artifact.tar.gz");
await extractTarGzip("artifact.tar.gz", "out");

// in-memory primitives, built on the platform CompressionStream
const packed = await gzip(new TextEncoder().encode("hello"));

Installing tools

Prepare an environment by fetching a CLI one of Zuke's wrappers drives, then point the wrapper at it. installRelease({ name, url, destDir }) is the low-level primitive: it resolves a per-platform URL (via an ({ os, arch }) => string callback and hostPlatform()), downloads it, and returns the installed binary's AbsolutePath — ready for .toolPath(...). Set archive: "tar.gz" or archive: "zip" to unpack an archive and take binaryPath from inside; the default "raw" installs the download as the binary itself.

import { installRelease } from "jsr:@zuke/core";
import { CmdTasks } from "jsr:@zuke/cmd";

const arches = { x86_64: "amd64", aarch64: "arm64" } as const;

const bin = await installRelease({
  name: "helm",
  destDir: ".zuke/bin",
  archive: "tar.gz",                       // unpack a tarball ("raw" by default)
  binaryPath: `${Deno.build.os}-${arches[Deno.build.arch]}/helm`,
  url: (p) =>
    `https://get.helm.sh/helm-v3.14.0-${p.osLabel({ macos: "darwin" })}-${
      arches[p.arch]
    }.tar.gz`,
});

// returns the installed binary's AbsolutePath — ready for .toolPath(...)
await CmdTasks.exec(String(bin), (s) => s.args("version"));

For the fluent, build-facing surface — ToolTasks.install() for one tool, toolchain() for many, with SHA-256 pinning, verification, and caching — see the dedicated Installing tools guide. The download seam keeps it unit-testable, and on Windows the filename gains an .exe suffix. Both .tar.gz and .zip assets are unpacked (via extractTarGzip and extractZip), so the same call handles the tarballs and zips release pages ship.

Assertions

Fail a target fast with a clear message when an expectation doesn't hold. assert(condition, message?) throws when the condition is falsy; assertExists(value, message?) throws on null/undefined and returns the value narrowed to its non-nullable type; fail(message) always throws. The async assertFileExists(path) / assertDirectoryExists(path) check the filesystem. All throw an AssertionError.

import { assert, assertExists, assertFileExists, fail } from "jsr:@zuke/core";

const token = assertExists(Deno.env.get("TOKEN"), "TOKEN is required"); // narrows
assert(this.environment.value !== "", "environment must be set");
await assertFileExists("dist/app.js");

if (unreachable) fail("should never happen"); // always throws AssertionError

Paths

TypeScript has no operator overloading, so a literal / path join isn't possible — absolutePath gets as close as the language allows. The returned AbsolutePath is callable (and has an equivalent .join(...)), immutable, and normalised, with accessors like .name, .stem, .extension, .parent(), and .relativeTo(...).

import { absolutePath, repoRoot } from "jsr:@zuke/core";

const root = absolutePath("/app");
const main = root("src", "main.ts");  // callable → /app/src/main.ts
main.name;             // "main.ts"
main.stem;             // "main"
main.parent();         // AbsolutePath → /app/src
main.relativeTo(root); // "src/main.ts"

repoRoot("dist");      // <repo root>/dist — resolved at runtime via zuke.json
await $`deno run ${main}`;  // toString() drops straight into $`` and args()

repoRoot is absolutePath anchored at your repository root — the directory containing the zuke.json that zuke setup scaffolds. It's resolved at runtime by walking up from the working directory, so nothing machine-specific is ever committed.

CI & host detection

isCI() and ciHost() (e.g. "github-actions", "gitlab-ci", "local") let a build branch on where it runs — pair them with .onlyWhen(...) to gate work like deploys to CI:

import { ciHost, isCI } from "jsr:@zuke/core";

deploy = target()
  .onlyWhen(() => isCI())          // run only on a CI runner
  .executes(async () => {
    // "github-actions" | "gitlab-ci" | "local" | …
    console.log(`running on ${ciHost()}`);
  });

From here, see Core concepts for the build model or Examples for complete build files.