算法艺术创作
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 快照 · 自动分类 · 以原作者文档为准
Diseñar salida de terminal consistente y de múltiples niveles para una herramienta de línea de comandos.
Usar chalk para crear un objeto de paleta nombrado.
Cargar chalk detrás de un respaldo sin color. El respaldo tiene que suplir cada forma de llamada que usa la paleta, lo cual es más que dejar pasar los strings sin cambios:
// 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(); }
Cuatro invariantes, y un stub más corto se equivoca en cada una de ellas:
(text) => text, no {}. El encadenamiento (chalk.bold.cyan('x')) necesita que cada salto sea a la vez indexable e invocable.new Proxy({}, { get: () => (s) => s }) satisface los estilos directos y rompe las factories: chalk.hex('#FF6B35') es entonces el string '#FF6B35', y llamarlo lanza TypeError: ... is not a function. Las paletas se construyen al cargar el módulo, así que ese respaldo tumba la herramienta en el momento del import — precisamente en la situación donde degradar a texto plano era el objetivo.then es undefined. Un stub que responde a cada propiedad con una función hace que await chalk se cuelgue para siempre: el runtime llama a .then y espera un callback que nadie invoca. Node reporta Detected unsettled top-level await y sale con 13.level es un número. Las comprobaciones de capacidad leen chalk.level >= 1; un stub truthy las abre sin soporte de color detrás.Construir la paleta a partir del objeto que haya sobrevivido a ese import.
Paleta estándar (salida transaccional):
// 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
Paleta cálida (salida ceremonial/narrativa):
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)
};
Reglas de diseño de paleta:
chalk.hex('#FF6B35'))Esperado: Un objeto de paleta con entradas nombradas, y un respaldo que ha sido ejecutado en lugar de solamente escrito.
En caso de fallo: Ejercitar la ruta del respaldo directamente; la paleta es el lugar equivocado para descubrir que está roto. Con el stub en alcance:
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 no cubre esto. Ejercita un chalk funcional que elige no emitir escapes; el respaldo ejercita un chalk que falló al importarse. Las dos rutas no comparten código. Ver Ejemplos Extendidos para el stub de producción anotado, una reproducción del defecto, y una versión ejecutable de las comprobaciones de arriba.
Seleccionar glyphs Unicode o caracteres ASCII para comunicación de estado:
ASCII (compatibilidad máxima):
+ created/installed (green)
- removed/deleted (red)
= skipped/unchanged (dim)
! error/warning (red)
Unicode (más rico, necesita terminal UTF-8):
✦ 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)
Criterios de selección:
--ascii o detección de NO_COLOREsperado: Un conjunto de glyphs que comunica el estado de un vistazo sin depender solo del color.
En caso de fallo: Si un glyph se renderiza como ? o un cuadro en pruebas, reemplazar con el equivalente ASCII. El conjunto +/-/=/! funciona en todas partes.
Cada comando debe soportar cuatro niveles de salida:
| Nivel | Flag | Audiencia | Contenido |
|---|---|---|---|
| Default | (ninguno) | Humano en terminal | Formateado, coloreado, informativo |
| Verbose | --verbose o --ceremonial | Humano queriendo detalle | Desglose por item, secuencias de llegada |
| Quiet | --quiet | Scripts, CI | Líneas mínimas, iconos de estado, sin decoración |
| JSON | --json | Consumidores máquina | Estructurado, parseable, completo |
Patrón de implementación:
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 });
}
Reglas de salida JSON:
Esperado: Cuatro niveles de salida claros con comportamiento consistente entre comandos.
En caso de fallo: Si el modo verbose es demasiado ruidoso, hacerlo opt-in (--ceremonial) en lugar de un nivel de verbosidad gradual.
Definir el tono y estilo que todas las funciones de salida siguen. Esto previene inconsistencia entre comandos.
Reglas de voz de ejemplo (del reporter campfire):
Reglas de voz para salida estándar (no ceremonia):
Esperado: Un conjunto escrito de 3-7 reglas de voz que las funciones de salida deben seguir.
En caso de fallo: Si las reglas se sienten arbitrarias, probarlas: escribir la misma salida con y sin cada regla. Si quitar una regla no cambia la calidad de la salida, la regla no es necesaria.
Organizar la salida en un módulo reporter con funciones enfocadas:
// 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 };
Cada función sigue la misma estructura:
Para salida ceremonial, crear un módulo separado:
// 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) { ... }
Esperado: Funciones reporter que son utilizables independientemente — cada una maneja su propio formato sin depender del estado del llamador.
En caso de fallo: Si las funciones crecen más allá de ~50 líneas, extraer helpers. Una función reporter debe ser fácil de revisar de forma aislada.
Verificar que la salida se renderiza correctamente en diferentes contextos:
# 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'
Verificar:
jq . para verificar)Esperado: La salida es correcta en los seis contextos.
En caso de fallo: Si los códigos ANSI se filtran, asegurar que chalk respeta NO_COLOR. Si Unicode se rompe, proveer un modo de respaldo ASCII. Notar que una suite verde no dice nada sobre el color en ningún sentido: los test runners hacen pipe de stdout, lo que pone chalk.level en 0, así que la salida coloreada y la no coloreada son idénticas byte a byte y las aserciones se cumplen con el color completamente roto. Probar que el color funciona requiere FORCE_COLOR=3 y una aserción sobre una secuencia de escape.
level === 0 y await todos comprobadosjqnew Proxy({}, { get: () => (s) => s }) se lee como completo y sí cubre chalk.dim y chalk.red, pero entonces cada factory retorna un string que el llamador intenta llamar de inmediato. Como las paletas se construyen al cargar el módulo, el TypeError cae en el momento del import — el respaldo falla con más fuerza en el único caso para el que existe. El Paso 1 lista las cuatro invariantes que un stub tiene que satisfacer.--json, emitir solo JSON válido. Una sola línea perdida (como "DRY RUN") rompe los parseadores JSON. Si el comando debe mostrar ambos, separarlos claramente o suprimir el texto humano en modo JSON.Math.max(...items.map(i => i.id.length)) para calcular el padding dinámicamente.+, OK, ERR).--quiet, añade ruido. Limitar la salida ceremonial detrás de flags explícitos.scaffold-cli-command — los comandos que usan esta salidatest-cli-application — probar que la salida coincide con las expectativasbuild-cli-plugin — los plugins reportan resultados a través de este sistema de salida