Files
OmniRoute/bin/cli/output.mjs
diegosouzapw 2e494f8f07 feat(cli): Fase 0.3 — helpers base + convenções (api, i18n, output, runtime)
- bin/cli/CONVENTIONS.md: fonte normativa de flags, exit codes, output,
  retry/backoff, i18n, secrets, auditoria de ações destrutivas
- bin/cli/api.mjs: apiFetch() com retry/backoff, Retry-After, ApiError,
  statusToExitCode, isServerUp; computeBackoff/shouldRetryStatus exportados
- bin/cli/runtime.mjs: withRuntime/withHttp/withDb — server-first / DB-fallback;
  ServerOfflineError com exitCode 3
- bin/cli/i18n.mjs: t() com Map achatado (sem bracket em prototype), interpolação
  {vars}, setLocale/detectLocale/resetForTests; hardened contra __proto__ traversal
- bin/cli/output.mjs: emit() (table/json/jsonl/csv), EXIT_CODES, maskSecret,
  printSuccess/printError/printWarning/exitWith; output → stdout, diagnóstico → stderr
- bin/cli/locales/en.json + pt-BR.json: strings base (setup/doctor/providers/
  keys/combo/serve/backup/update/health/mcp/tunnel)
- bin/cli/README.md: mapa da estrutura e guia de uso dos helpers
- tests/unit/cli-exit-codes.test.ts: 10 casos — EXIT_CODES, statusToExitCode,
  backoff exponencial, jitter ±25%, t() i18n com pt-BR e anti-__proto__
- .env.example + docs/reference/ENVIRONMENT.md: documentar 4 novas env vars CLI
  (OMNIROUTE_LANG, OMNIROUTE_CLI_TOKEN, OMNIROUTE_HTTP_TIMEOUT_MS, OMNIROUTE_VERBOSE)
- scripts/check/check-env-doc-sync.mjs: adicionar LC_MESSAGES ao allowlist de sistema
2026-05-14 21:42:57 -03:00

108 lines
2.7 KiB
JavaScript

const MASK_RE = /sk-[A-Za-z0-9]{4,}/g;
export const EXIT_CODES = Object.freeze({
SUCCESS: 0,
ERROR: 1,
INVALID_ARG: 2,
SERVER_OFFLINE: 3,
AUTH: 4,
RATE_LIMIT: 5,
TIMEOUT: 124,
});
export function maskSecret(value) {
if (typeof value !== "string") return value;
return value.replace(MASK_RE, (m) => `${m.slice(0, 5)}***${m.slice(-4)}`);
}
function toRows(data) {
if (Array.isArray(data)) return data;
if (data !== null && typeof data === "object") return [data];
return [{ value: data }];
}
function renderTable(rows) {
if (rows.length === 0) {
process.stdout.write("(empty)\n");
return;
}
const keys = Array.from(
rows.reduce((acc, row) => {
for (const k of Object.keys(row)) acc.add(k);
return acc;
}, new Set())
);
const widths = keys.map((k) => Math.max(k.length, ...rows.map((r) => String(r[k] ?? "").length)));
const sep = widths.map((w) => "-".repeat(w)).join("-+-");
const header = keys.map((k, i) => k.padEnd(widths[i])).join(" | ");
process.stdout.write(`${header}\n${sep}\n`);
for (const row of rows) {
const line = keys.map((k, i) => String(row[k] ?? "").padEnd(widths[i])).join(" | ");
process.stdout.write(`${line}\n`);
}
}
function renderCsv(rows) {
if (rows.length === 0) return;
const keys = Object.keys(rows[0]);
process.stdout.write(keys.map(csvEscape).join(",") + "\n");
for (const row of rows) {
process.stdout.write(keys.map((k) => csvEscape(String(row[k] ?? ""))).join(",") + "\n");
}
}
function csvEscape(value) {
if (/[",\r\n]/.test(value)) return `"${value.replace(/"/g, '""')}"`;
return value;
}
export function emit(data, opts = {}) {
const format = opts.output || "table";
const rows = toRows(data);
switch (format) {
case "json":
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
break;
case "jsonl":
for (const row of rows) process.stdout.write(JSON.stringify(row) + "\n");
break;
case "csv":
renderCsv(rows);
break;
default:
renderTable(rows);
}
}
export function printHeading(title, quiet = false) {
if (quiet) return;
process.stderr.write(`\n\x1b[1m\x1b[36m${title}\x1b[0m\n\n`);
}
export function printSuccess(message, quiet = false) {
if (quiet) return;
process.stderr.write(`\x1b[32m✔ ${message}\x1b[0m\n`);
}
export function printInfo(message, quiet = false) {
if (quiet) return;
process.stderr.write(`\x1b[2m${message}\x1b[0m\n`);
}
export function printWarning(message) {
process.stderr.write(`\x1b[33m⚠ ${message}\x1b[0m\n`);
}
export function printError(message) {
process.stderr.write(`\x1b[31m✖ ${message}\x1b[0m\n`);
}
export function exitWith(code, message) {
if (message) printError(message);
process.exit(code);
}