Console output

@zuke/console is the output engine behind every Zuke build — the ruled banners, the colored status lines, and the aligned Build Summary all come from it. It's also a package you can import directly: a levelled logger with tag-based markup and a set of layout primitives (line, rule, box, table) that render cleanly to a terminal and degrade gracefully in CI.

Import

The package exports a single namespaced object, ConsoleTasks. It's conventional to alias it to Log:

import { ConsoleTasks as Log } from "jsr:@zuke/console";

Logging & levels

Seven methods form a severity ladder. trace, debug, info, log, and success write to stdout; warn and error write to stderr. Every message accepts markup.

Log.trace("resolved 42 source files");      // most verbose
Log.debug("cache hit for 'check'");
Log.info("pushing [bold]core@1.2.0[/]");      // → stdout
Log.log("a plain line — no icon, no styling");// → stdout
Log.success("published [bold]4[/] packages"); // ✔ is added for you
Log.warn("coverage [yellow]94.2%[/] — below gate");   // → stderr
Log.error("type-check failed", { error: err });       // → stderr, prints the cause
output
· resolved 42 source files
 cache hit for 'check'
 pushing core@1.2.0
a plain line — no icon, no styling
 published 4 packages
 coverage 94.2% — below gate
 type-check failed
    ↳ TypeError: expected string, got number

Only messages at or above the active level are printed — set it to "debug" in CI, "info" locally, or "silent" to mute everything. error() takes an optional { error } so it can print the underlying cause, and under GitHub Actions warn/error also emit ::warning:: / ::error:: annotations.

Markup

Wrap text in [style]…[/] tags. A tag carries one or more space-separated style names, and [/] closes the most recent tag, restoring the surrounding style — so tags nest cleanly.

Log.info("[bold]core[/] is [green]ready[/]");            // one tag each
Log.info("[red bold]2 errors[/] in [underline]mod.ts[/]"); // combine styles
Log.info("[yellow]outer [cyan]inner[/] still yellow[/]");  // [/] closes the nearest tag
Log.info("[muted]12 files[/] scanned");                    // semantic theme token

// Literal brackets: double them. [[ renders as [ and ]] as ].
Log.info("array access looks like arr[[0]]");

// Escaping untrusted text so it can't smuggle in tags:
Log.info("branch: " + Log.escape(userInput));
output
 core is ready
 2 errors in mod.ts
 outer inner still yellow
 12 files scanned
 array access looks like arr[0]
 branch: release/[beta]

The recognised style names are:

  • Attributesbold, dim, italic, underline, and reset.
  • Colorsblack, red, green, yellow, blue, magenta, cyan, white, and gray.
  • Semantic tokenssuccess, warn, error, info, debug, trace, and muted. These resolve through the active theme, so they stay consistent (and re-theme-able) across your output.

A literal bracket is written by doubling it — [[ renders as [ and ]] as ]. To print caller-supplied text safely, pass it through Log.escape(text), which doubles every bracket so nothing is mistaken for a tag.

Lines & rules

line draws a horizontal divider; rule is a line with a centered title. Both accept the same options — char (the glyph to repeat), width, and style (an array of style names).

Log.line();                              // a full-width ── divider
Log.line({ char: "", width: 40 });      // custom glyph and width
Log.line({ char: "·", style: ["dim"] }); // styled divider

Log.rule("Deploy");                       // a title centered in the rule
Log.rule("Tests", { style: ["cyan"] });   // rules take the same options as line()
Log.rule();                               // untitled — identical to line()
output
──────────────────────────────────────────────
════════════════════════════════════════
··············································
─────────────────── Deploy ───────────────────
─────────────────── Tests ────────────────────
──────────────────────────────────────────────

Boxes

box frames content in a single-line border. Pass a string or an array of lines, plus optional title, padding, width, a border style, and a titleStyle.

// A single string, or an array of pre-split lines:
Log.box("Deployed core@1.2.0 to production.");

Log.box(
  ["core   1.2.0   ✔", "cli    1.2.0   ✔", "cmd    1.2.0   ✔"],
  {
    title: "Release",       // shown in the top border
    padding: 1,             // blank cells inside the border
    width: 40,              // fixed width (defaults to fit the content)
    border: ["green"],      // style the border glyphs…
    titleStyle: ["bold"],   // …and the title
  },
);
output
┌────────────────────────────────────┐
│ Deployed core@1.2.0 to production. │
└────────────────────────────────────┘

 Release ─────────────────────────────┐
                                      
  core   1.2.0   ✔                    
  cli    1.2.0   ✔                    
  cmd    1.2.0   ✔                    
                                      
└──────────────────────────────────────┘

border and titleStyle take an array of style names (e.g. ["green"] or ["dim"]) — they color the border glyphs and title, not the box shape, which is always drawn with Unicode box-drawing characters.

Tables

table renders aligned columns. Columns are described by a header and an optional align ("left" — the default — or "right"); rows are arrays of strings. Options control the look:

Log.table(
  [
    { header: "Package", align: "left" },   // align: "left" | "right"
    { header: "Version" },                   // defaults to left
    { header: "Size", align: "right" },
  ],
  [
    ["@zuke/core", "1.2.0", "18 kB"],
    ["@zuke/cli", "1.2.0", "12 kB"],
    ["@zuke/cmd", "1.2.0", "6 kB"],
  ],
  {
    divider: true,            // rule between header and body
    separator: "  ",          // gap between columns
    headerStyle: ["bold"],
    dividerStyle: ["dim"],
  },
);
output
Package     Version   Size
──────────────────────────
@zuke/core  1.2.0    18 kB
@zuke/cli   1.2.0    12 kB
@zuke/cmd   1.2.0     6 kB

divider draws a rule under the header, separator sets the gap between columns, and headerStyle / dividerStyle style those rows. Cell text may contain markup, and column widths are computed automatically from the content.

Headers & summary

Two helpers produce the framing Zuke prints around a run. header(name) is the ruled banner before a target; summary(reports, totalMs, ok) is the final status-and-timing table. The default renderer calls both for you — you'd only invoke them directly if you're driving the run loop yourself.

// The ruled banner Zuke prints before each target runs:
Log.header("build");

// The aligned end-of-run summary table. 'reports' is the per-target
// result set the runner collects; totalMs is wall-clock time; ok is the
// overall verdict. The default renderer calls this for you — reach for it
// directly only when you drive the run loop yourself.
Log.summary(reports, totalMs, ok);
output

build


Build Summary
────────────────────────────
Target   Status     Duration
────────────────────────────
install  Succeeded     12.4s
build    Succeeded      2.2s
────────────────────────────
Total                  14.6s

Groups

Wrap a burst of output in a named group. In a plain terminal the name is printed as a header; under GitHub Actions the pair becomes a collapsible ::group:: / ::endgroup:: block.

Log.group("Install");
Log.info("added 214 packages in 12.4s");
Log.endGroup();
// Under GitHub Actions these become collapsible ::group:: / ::endgroup:: blocks.
output — plain terminal
────────────────── Install ──────────────────
 added 214 packages in 12.4s

Configuration

configure sets the log level, output sink, theme, color mode, width, and GitHub-annotation mode. level() reads the active threshold and reset() restores the defaults.

Log.configure({
  level: "debug",   // "trace" | "debug" | "info" | "warn" | "error" | "silent"
  color: true,      // force ANSI colour on/off (auto-detected from the TTY otherwise)
  width: 100,       // width used for rules, boxes, and wrapping
  github: true,     // emit ::group:: / ::warning:: / ::error:: annotations
  // sink:  a custom { out, err } target; theme: a custom palette
});

Log.level();        // read the active threshold
Log.reset();        // restore every option to its default

configure prints nothing itself — it changes what later calls emit. With level: "debug" the trace line above stays hidden while debug and up now print; with github: true, group and warn/error switch to workflow-command syntax.

Renderer integration

@zuke/console is wired into a build through a renderer. Pass consoleRenderer to run() to route every banner and summary through it, or build a custom-themed one with createConsoleRenderer(theme).

import { Build, run } from "jsr:@zuke/core";
import {
  consoleRenderer,
  createConsoleRenderer,
  defaultTheme,
} from "jsr:@zuke/console";

// Route the whole build's banners and summary through @zuke/console:
await run(MyBuild, { renderer: consoleRenderer });

// …or ship your own palette by building a renderer from a Theme:
const renderer = createConsoleRenderer(defaultTheme);
await run(MyBuild, { renderer });
output — a full run through consoleRenderer
$ ./zuke build


build

  ▸ astro build — 4 pages in 2.2s
 build succeeded in 2.2s

Build Summary
────────────────────────────
Target   Status     Duration
────────────────────────────
clean    Succeeded      0.1s
build    Succeeded      2.2s
────────────────────────────
Total                   2.3s

✔ Build succeeded — 2/2 targets in 2.3s

Start from defaultTheme and override the tokens you want to recolor — the semantic markup tokens (success, warn, muted, …) then follow your palette everywhere they appear, in both your own Log calls and Zuke's built-in output.

  • Getting started — install Zuke and run your first target; the run log you see is @zuke/console at work.
  • Core library — the other batteries in @zuke/core: file tasks, globbing, HTTP, and more.
  • Examples — complete build files that produce this output.