Visual primitives
Compose boxes, tables, spinners, progress, links, trees, diffs, and more with the same output system.
Visual primitives are small on purpose. Some return strings for composition. Others print directly through the shared output system.
Primitive overview
Prop
Type
Box
Use cli.box() to frame content in a titled section.
await cli.run(async () => {
cli.box({
title: "Deployment",
content: "Ready to deploy to production.",
});
});┌─ Deployment ───────────────────┐
│ Ready to deploy to production. │
└─────────────────────────────────┘Table
Use cli.table() to return aligned column text. It composes directly into cli.box().
const summary = cli.table(
["Field", "Value"],
[
["project", "oscli"],
["teamSize", 3],
["approved", true],
],
);Field Value
─────────
project oscli
teamSize 3
approved trueSpinner
Use cli.spin() around one async operation.
await cli.run(async () => {
await cli.spin("Installing packages", async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
});
});⠸ Installing packages... 0.2s
✓ Installed packages 0.3sIf you do not pass doneLabel, oscli tries to convert common gerunds to a
past-tense completion label.
Progress
Use cli.progress() when your work already has named steps.
await cli.run(async () => {
const steps = ["Validate", "Build", "Finalize"] as const;
await cli.progress("Running steps", steps, async (step) => {
if (step === "Validate") {
await new Promise((resolve) => setTimeout(resolve, 150));
}
if (step === "Build") {
await new Promise((resolve) => setTimeout(resolve, 150));
}
if (step === "Finalize") {
await new Promise((resolve) => setTimeout(resolve, 150));
}
});
});⠸ Running steps [######--------------] [00:01] 33%
✓ Running steps [####################] [00:03] 100%Pass { style } as the fourth argument to change the bar appearance.
Available styles: "hash" (default), "line", "steps", "braille", "block", "gradient".
await cli.progress("Building", steps, fn, { style: "block" });Divider
Use cli.divider() for visual section boundaries.
await cli.run(async () => {
cli.divider("Results");
});───────────────── Results ────────────────Link
Use cli.link() for docs or support URLs. Renders as a clickable OSC 8 hyperlink in supported terminals, otherwise falls back to label (url).
await cli.run(async () => {
cli.link("oscli docs", "https://github.com/aidankmcalister/oscli");
});oscli docs (https://github.com/aidankmcalister/oscli)Tree
Use cli.tree() when you want to render directory-style output inside another
primitive.
const structure = cli.tree({
src: {
"index.ts": null,
"client.ts": null,
},
"package.json": null,
});
cli.box({
title: "Project",
content: structure,
});┌─ Project ──────────────┐
│ ├─ src │
│ │ ├─ index.ts │
│ │ └─ client.ts │
│ └─ package.json │
└────────────────────────┘Diff
Use cli.diff() when you want to show a text change without pulling in an
external diff library.
await cli.run(async () => {
cli.diff("hello\nworld", "hello\noscli");
}); hello
- world
+ oscliNote
Use cli.note() for bordered callout blocks — info panels, warning notices, or structured metadata.
await cli.run(async () => {
cli.note(
"Environment production\nRegion us-east-1\nRuntime Node.js 20",
"Deploying",
);
});┌─ Deploying ──────────────────┐
│ Environment production │
│ Region us-east-1 │
│ Runtime Node.js 20 │
└──────────────────────────────┘Banner
Use cli.banner() for rounded bordered boxes — version upgrade nags, announcements, or notices.
await cli.run(async () => {
cli.banner({
message: "Update available: 1.2.0 → 1.4.1\nRun npm i -g oscli to upgrade",
});
});╭───────────────────────────────────╮
│ Update available: 1.2.0 → 1.4.1 │
│ Run npm i -g oscli to upgrade │
╰───────────────────────────────────╯Key-Value
Use cli.keyValue() for aligned key-value pairs — summaries, compact diffs, or config output.
await cli.run(async () => {
cli.keyValue({
name: "my-app → my-app-v2",
region: "us-west-2 → us-east-1",
replicas: "1 → 3",
});
});name my-app → my-app-v2
region us-west-2 → us-east-1
replicas 1 → 3Leaders
Use cli.leaders() for dot-leader lines connecting a label to a right-aligned value.
await cli.run(async () => {
cli.leaders("Build", "✔ 1.2s");
cli.leaders("Typecheck", "✔ 0.8s");
cli.leaders("Lint", "▲ 0.3s");
cli.leaders("Test", "✗ 2.1s");
});Build ·························· ✔ 1.2s
Typecheck ······················ ✔ 0.8s
Lint ··························· ▲ 0.3s
Test ··························· ✗ 2.1sCode Frame
Use cli.codeFrame() to show errors with inline source annotation and an optional hint.
await cli.run(async () => {
cli.codeFrame({
file: "src/index.ts",
line: 12,
column: 5,
message: "Type 'string' is not assignable to type 'number'",
source: [
"const port: number = process.env.PORT",
" ^^^^^^^^^^^^^^^^",
],
hint: "wrap with Number() or use parseInt()",
});
});src/index.ts:12:5
✗ Type 'string' is not assignable to type 'number'
11 │ const port: number = process.env.PORT
12 │ ^^^^^^^^^^^^^^^^
│ ^^^^^^^^^^^^^^^^
Hint: wrap with Number() or use parseInt()Composable — cli.table() and cli.tree() return strings so you can pass
them into other primitives (e.g. cli.box({ content: cli.table(...) })).
Display — cli.box(), cli.note(), cli.banner(), cli.diff(),
cli.keyValue(), cli.leaders(), cli.codeFrame(), cli.divider(), and
cli.link() print to stdout immediately.