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.