算法艺术创作
algorithmic-art
通过代码和可调参数创作生成艺术与交互式视觉效果。
design-cli-output【设计与创意】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 reporte…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Consistent, multi-level terminal output for a command-line tool.
Use chalk. Make named palette object.
Load chalk behind no-color fallback. Fallback must stand in for every call shape palette uses — more than passing strings through:
// 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.
const FACTORIES = new Set(['ansi256', 'bgAnsi256', 'bgHex', 'bgRgb', 'hex',
'rgb', 'underlineAnsi256', 'underlineHex', 'underlineRgb']);
function makeChalkStub() {
return new Proxy((text) => text, {
get(target, prop) {
if (prop === 'then') return undefined; // must not be a thenable
if (prop === 'level') return 0; // no color support, truthfully
if (typeof prop === 'symbol') return Reflect.get(target, prop);
return FACTORIES.has(prop) ? () => makeChalkStub() : makeChalkStub();
},
});
}
let chalk;
try { chalk = (await import('chalk')).default; }
catch { chalk = makeChalkStub(); }
Four invariants. Shorter stub gets each one wrong:
(text) => text, not {}. Chaining
(chalk.bold.cyan('x')) needs every hop both indexable + callable.new Proxy({}, { get: () => (s) => s })
satisfies direct styles, breaks factories: chalk.hex('#FF6B35') is then
string '#FF6B35', and calling it throws
TypeError: ... is not a function. Palettes built at module load, so that
fallback takes tool down at import time — exactly where degrading to plain
text was point.then is undefined. Stub answering every property with function makes
await chalk hang forever: runtime calls .then, waits for callback nobody
invokes. Node reports Detected unsettled top-level await, exits 13.level is number. Capability gates read chalk.level >= 1; truthy stub
opens them with no color support behind.Build palette from whichever object survived that import.
Standard palette (transactional output):
// Status colors
const ok = chalk.green; // success
const fail = chalk.red; // errors
const warn = chalk.yellow; // warnings
const info = chalk.cyan; // identifiers, names
const dim = chalk.dim; // secondary info, paths
const bold = chalk.bold; // headers
Warm palette (ceremony/narrative output):
const C = {
flame: chalk.hex('#FF6B35'), // active elements, fire
amber: chalk.hex('#FFB347'), // arriving items, warm highlights
spark: chalk.hex('#FFF4E0'), // individual items (sparks/skills)
ember: chalk.hex('#8B4513'), // cold/dormant states
warm: chalk.hex('#D4A574'), // neutral warm text
dim: chalk.dim, // background, secondary
fail: chalk.red, // errors stay red (honest)
};
Palette rules:
chalk.hex('#FF6B35'))Got: Palette object. Named entries. Fallback executed, not merely written.
If fail: Exercise fallback path directly; palette is wrong place to discover it broken. With stub in scope:
console.assert(chalk.dim('x') === 'x'); // direct style
console.assert(chalk.hex('#fff')('x') === 'x'); // factory — the usual defect
console.assert(chalk.bold.cyan('x') === 'x'); // chain
console.assert(chalk.level === 0); // capability gate stays shut
await chalk; // must not hang
NO_COLOR=1 does not cover this. It exercises working chalk that chooses not
to emit escapes; fallback exercises chalk that failed to import. Two paths share
no code. See
Extended Examples
for annotated production stub, reproduction of defect, runnable version of checks
above.
Unicode glyphs or ASCII chars for status.
ASCII (max compat):
+ created/installed (green)
- removed/deleted (red)
= skipped/unchanged (dim)
! error/warning (red)
Unicode (richer, needs UTF-8 terminal):
✦ 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)
Rules:
--ascii flag or NO_COLOR detectionGot: Glyph set communicates status at a glance. No color needed.
If fail: Glyph renders as ? or box? Swap for ASCII. +/-/=/! works everywhere.
Every command supports 4 output levels:
| 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:
function output(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 output
printFormatted(data, { verbose: options.verbose });
}
JSON rules:
Got: Four clear output levels. Consistent across commands.
If fail: Verbose too noisy? Make opt-in (--ceremonial) not graduated level.
Tone + style all output follows. Stops inconsistency.
Example voice rules (campfire reporter):
Voice rules for standard (non-ceremony) output:
Got: Written set of 3-7 voice rules. Output funcs must follow.
If fail: Rules feel arbitrary? Test: write output with + without each rule. Removing rule doesn't change quality? Rule not needed.
Organize output into reporter module. Focused functions.
// reporter.js — standard output
export function printResults(results) { ... }
export function printItemTable(items) { ... }
export function printDetections(detections) { ... }
export function printAudit(auditResults) { ... }
export function printDryRun() { ... }
export function warn(msg) { ... }
export function error(msg) { ... }
export { chalk };
Each function same structure:
Ceremony output? Separate module:
// campfire-reporter.js — warm narrative output
export function printArrival({ teamId, agents, results, ceremonial }) { ... }
export function printScatter({ teamId, agents, results }) { ... }
export function printTend(fires) { ... }
export function printCampfireList({ teams, state, reg }) { ... }
export function printFireSummary({ team, fireData, reg }) { ... }
export function printJson(data) { ... }
Got: Reporter functions independently usable. Each handles own formatting, no caller state dep.
If fail: Function > ~50 lines? Extract helpers. Reporter must be reviewable alone.
Check output renders right in different contexts:
# 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:
jq .)Got: Output correct in all 6 contexts.
If fail: ANSI codes leak? Ensure chalk respects NO_COLOR. Unicode breaks?
Provide ASCII fallback mode. Note green suite says nothing about color either
way: test runners pipe stdout, which puts chalk.level at 0, so colored +
uncolored output byte-identical — assertions hold with color entirely broken.
Proving color works needs FORCE_COLOR=3 + assertion on escape sequence.
level === 0, await all checkedjqnew Proxy({}, { get: () => (s) => s }) reads complete and does cover chalk.dim + chalk.red, but every factory then returns string caller immediately tries to call. Palettes built at module load, so TypeError lands at import time — fallback fails hardest in the one case it exists for. Step 1 lists four invariants stub must satisfy.--json mode, only valid JSON. One stray line ("DRY RUN") breaks JSON parsers. If must show both, separate clearly or suppress human text in JSON mode.Math.max(...items.map(i => i.id.length)) for dynamic padding.+, OK, ERR).--quiet mode = noise. Gate ceremony behind explicit flags.scaffold-cli-command — commands that use this outputtest-cli-application — testing output matches expectationsbuild-cli-plugin — plugins report results through this output system