Progress
Spinners and determinate progress indicators for terminal tasks
@crustjs/progress writes progress indicators to stderr by default. It supports task-scoped spinners, imperative handles, determinate progress, themes, injected output, and static non-TTY output.
Install
npm install @crustjs/progressQuick example
await spinner({
message: "Building",
task: async ({ updateMessage }) => {
updateMessage("Running checks");
},
});spinner() starts the indicator, awaits task, and prints a final line. The task receives updateMessage().
✓ Running checksAPI
Call spinner({ task }) for a task-scoped indicator. Call spinner() without task for a SpinnerHandle, or progress() for a ProgressHandle.
spinner(options)
await spinner({ message: "Building", task: async () => build() });
// success: ✓ Building
// failure: ✗ BuildingA failed task prints the error line, then rejects with the task error. Non-TTY output omits animation and cursor-control sequences.
Prop
Type
Imperative spinner
const handle = spinner({ message: "Deploying" });
handle.start();
handle.updateMessage("Uploading");
handle.stop("success", "Deployed"); // ✓ Deployedstop() defaults to "success" and later calls do nothing.
Prop
Type
progress(options)
const bar = progress({ total: 2, message: "Files" });
bar.start();
bar.advance(1);
bar.advance(1);
bar.stop("success", "Files complete");progress() renders <message> (<current>/<total>). advance() defaults to one unit, changes the count, and may replace the message.
✓ Files complete (2/2)Prop
Type
Calling advance() before start() changes the count without repainting. The updated count appears after the handle starts or in its final line.
Prop
Type
SIGINT policy
const handle = spinner({ message: "Working", sigint: false });SpinnerSigintPolicy is "exit" | false. The default "exit" policy restores the cursor and re-raises SIGINT when no other listener remains. Use false when the application owns cleanup, and call stop() from that cleanup.
Custom spinner frames
spinner({ message: "Working", spinner: { frames: ["⊶", "⊷"], interval: 150 } });Built-in names are "dots", "line", "arc", and "bounce". A custom frame array must not be empty.
Themes
import { spinner } from "@crustjs/progress";
import { cyan } from "@crustjs/style";
spinner({ message: "Working", theme: { spinner: cyan } });A ProgressTheme has spinner, message, success, and error style slots. Per-call values merge over defaultTheme; PartialProgressTheme names the override type.
Output sinks
const transcript: string[] = [];
const sink: ProgressSink = { isTTY: false, write: (text) => transcript.push(text) };
await withTerminalIO({ output: sink }, () =>
spinner({ message: "Deploying", task: async () => {} }),
);A per-call sink wins over ambient terminal output, which wins over process.stderr. withTerminalIO(io, fn) takes the same optional { input, output } streams as @crustjs/prompts and shares its ambient scope; withProgressSink() is the output-only alias.
Non-interactive behavior
✓ Deploying
✗ Deploy failedA non-TTY sink gets one final line with no animation or cursor-control sequences. updateMessage() still determines the final message.
Styling depends on @crustjs/style color control, not sink.isTTY. Use plain theme functions when captured output must contain no ANSI escapes.