Design terminal output for a CLI tool with chalk colors, Unicode glyphs, multiple verbosity levels (human, verbose, quiet, JSON), and consistent voice rules. Covers color palette selection, status indicator design, reporter function architecture, ceremony/narrative output variants, and cross-terminal compatibility. Use when building a new CLI reporter module, adding warm narrative output to an existing tool, standardizing output across multiple commands, or designing machine-readable JSON alongside human-readable text.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Design terminal output for a CLI tool with chalk colors, Unicode glyphs, multiple verbosity levels (human, verbose, quiet, JSON), and consistent voice rules. Covers color palette selection, status indicator design, reporter function architecture, ceremony/narrative output variants, and cross-terminal compatibility. Use when building a new CLI reporter module, adding warm narrative output to an existing tool, standardizing output across multiple commands, or designing machine-readable JSON alongside human-readable text.
Required: CLI name + audience (devs, ops, end users)
Required: Commands needing formatting
Optional: Ceremony/narrative variant?
Optional: Branding (palette, tone)
Do
Step 1: Color palette
chalk → named palette.
Load chalk behind no-color fallback. Fallback must stand in for every call shape palette uses — more than passing strings thru:
// A factory returns a *function*; a direct style returns a string. Enumerate// this list against the installed chalk, not from memory — chalk 6 added the// three underline* variants, and a list that omits them is wrong for those names.constFACTORIES = newSet(['ansi256', 'bgAnsi256', 'bgHex', 'bgRgb', 'hex',
'rgb', 'underlineAnsi256', 'underlineHex', 'underlineRgb']);
functionmakeChalkStub() {
returnnewProxy((text) => text, {
get(target, prop) {
if (prop === 'then') returnundefined; // must not be a thenable
(prop === ) ;
( prop === ) .(target, prop);
.(prop) ? () : ();
},
});
}
chalk;
{ chalk = ( ()).; }
{ chalk = (); }
if
'level'
return
0
// no color support, truthfully
if
typeof
'symbol'
return
Reflect
get
return
FACTORIES
has
() =>
makeChalkStub
makeChalkStub
let
try
await
import
'chalk'
default
catch
makeChalkStub
4 invariants, shorter stub gets each wrong:
Proxy target callable — (text) => text, not {}. Chain (chalk.bold.cyan('x')) → every hop indexable + callable.
Factories return fn.new Proxy({}, { get: () => (s) => s }) OK for direct styles, breaks factories: chalk.hex('#FF6B35') = string'#FF6B35', call it → TypeError: ... is not a function. Palettes built at module load → that fallback kills tool at import time — exactly where degrading to plain text was the point.
then = undefined. Stub answering every prop w/ fn → await chalk hangs forever: runtime calls .then, waits for callback nobody fires. Node: Detected unsettled top-level await, exit 13.
level = number. Capability gates read chalk.level >= 1; truthy stub opens them w/ no color behind.
Build palette from whichever obj survived that import.
Standard (transactional):
// Status colorsconst ok = chalk.green; // successconst fail = chalk.red; // errorsconst warn = chalk.yellow; // warningsconst info = chalk.cyan; // identifiers, namesconst dim = chalk.dim; // secondary info, pathsconst bold = chalk.bold; // headers
Always no-color fallback + check it vs call shapes palette really uses — warm palette above near-all factories
Hex for custom (chalk.hex('#FF6B35'))
Fail/err → red regardless
Name by semantic role not visual
Share 1 stub across modules, no rebuild per import site → else same defect hunted + fixed in every copy
→ Palette obj w/ named entries + fallback that ran, not merely written.
If err: Exercise fallback path direct; palette = wrong place to find it broken. Stub in scope:
console.assert(chalk.dim('x') === 'x'); // direct styleconsole.assert(chalk.hex('#fff')('x') === 'x'); // factory — the usual defectconsole.assert(chalk.bold.cyan('x') === 'x'); // chainconsole.assert(chalk.level === 0); // capability gate stays shutawait chalk; // must not hang
NO_COLOR=1 no cover this. It runs working chalk choosing no escapes; fallback runs chalk that failed import. 2 paths share no code. See More Ex → annotated prod stub, defect repro, runnable ver of checks above.
✦ item/skill/practice (spark)
◉ active/burning state
◎ cooling/embers state
○ cold/dormant state
◌ available/not installed
✗ failed item
✓ success (use sparingly — not all terminals render it well)
Criteria:
ASCII → CI/piped
Unicode → interactive
Both via --ascii flag or NO_COLOR
Test: macOS Terminal, Windows Terminal, VS Code, SSH
→ Glyph set communicates status at glance w/o color alone.
If err: Glyph renders ? or box → ASCII equiv. +/-/=/! works everywhere.
Step 3: Verbosity levels
Every cmd supports 4:
Level
Flag
Audience
Content
Default
(none)
Human at terminal
Formatted, colored, informative
Verbose
--verbose or --ceremonial
Human wanting detail
Per-item breakdown, arrival sequences
Quiet
--quiet
Scripts, CI
Minimal lines, status icons, no decoration
JSON
--json
Machine consumers
Structured, parseable, complete
Pattern:
functionoutput(data, options) {
if (options.json) {
console.log(JSON.stringify(data, null, 2));
return;
}
if (options.quiet) {
for (const item of data.items) {
const icon = item.ok ? '+' : '!';
console.log(`${icon}${item.id}`);
}
return;
}
// Default (or verbose) human outputprintFormatted(data, { verbose: options.verbose });
}
JSON rules:
Always valid (no mix w/ human text)
Include all human data + machine fields
Consistent keys across cmds
Exit 0 success, 1 err (regardless of mode)
→ 4 clear levels, consistent behavior across cmds.
If err: Verbose too noisy → opt-in (--ceremonial) not graduated.
Step 4: Voice rules
Tone + style. Prevents inconsistency.
Ex (campfire reporter):
Present tense, active: "mystic arrives" not "mystic has been installed"
No exclamation: Quiet confidence.
Metaphor replaces jargon: "practices" not "dependencies" (ceremony only)
Failures honest, not catastrophic: "A spark was lost" not "ERROR: installation failed with exit code 1"
Closing line reflects state: Every op ends summary
No emoji: Unicode glyphs carry visual weight w/o decorative
Every word info: If no understanding → remove
Standard (non-ceremony):
Concise, factual lines
Status icon + item ID + ctx
Summary line w/ counts
Err msgs suggest actions
→ 3-7 voice rules output fns follow.
If err: Rules arbitrary → test. Write same output w/ + w/o rule. If no change → rule not needed.
→ Independent fns, handle own formatting w/o caller state.
If err: Fn >~50 lines → extract helpers. Reviewable in isolation.
Step 6: Test across envs
# With colors (interactive terminal)
node cli/index.js list --domains
# Without colors (piped)
node cli/index.js list --domains | cat# With NO_COLOR environment variable
NO_COLOR=1 node cli/index.js list --domains
# JSON mode (parseable)
node cli/index.js campfire --json | jq .
# In CI (typically no TTY)
CI=true node cli/index.js audit
# The no-color fallback. A failed import cannot be provoked with an env var, so# assert on the stub itself in the suite rather than reaching it through the CLI.# Pass a glob, not a directory: `node --test <dir>` stopped expanding at Node 22.
node --test'cli/test/*.test.js'
Check:
Colors in interactive
No ANSI leaks in piped
JSON valid (jq .)
Unicode in target terminals
Col align w/ varying widths
No-color fallback answers every call shape palette uses, asserted in suite not hand-demoed once
→ Output correct in all 6 contexts.
If err: ANSI leaks → chalk respects NO_COLOR. Unicode breaks → ASCII fallback. Green suite says nothing about color either way: test runners pipe stdout → chalk.level 0 → colored + uncolored out byte-identical, assertions hold w/ color fully broken. Prove color works → FORCE_COLOR=3 + assert on escape seq.
Check
Palette has no-color fallback + fallback ran: direct style, factory, chain, level === 0, await all checked
Status indicators work color + no-color
All 4 verbosity levels useful
JSON valid + jq-parseable
Voice rules docs + followed
Reporter fns handle empty/null
Tested: terminal, piped, NO_COLOR, CI
Traps
No-color fallback covering direct styles only: new Proxy({}, { get: () => (s) => s }) reads complete, does cover chalk.dim + chalk.red, but every factory then returns string caller immediately tries to call. Palettes built at module load → TypeError lands at import time — fallback fails hardest in the 1 case it exists for. Step 1 lists 4 invariants stub must satisfy.
Mix human + JSON: --json only valid JSON. Stray line ("DRY RUN") breaks parsers. Suppress human in JSON mode.
Hardcoded col widths: Varies. Math.max(...items.map(i => i.id.length)) dyn.
Color w/o meaning: Color-only → colorblind + piped lose info. Pair w/ text (+, OK, ERR).