Crust logoCrust

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/progress

Quick 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 checks

API

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: ✗ Building

A 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"); // ✓ Deployed

stop() 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 failed

A 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.

On this page