/** Output formatting and process exit helpers for the CLI. */ import { readFile } from 'node:fs/promises'; let prettyMode = false; export function setPretty(v: boolean): void { prettyMode = v; } export function output(data: unknown): void { const json = prettyMode ? JSON.stringify(data, null, 2) : JSON.stringify(data); process.stdout.write(json + '\n'); } export function outputSuccess(data: Record = {}): void { output({ ok: true, ...data }); } export function outputError(message: string, code = 1): never { process.stderr.write(JSON.stringify({ error: message }) + '\n'); process.exit(code); } /** Read all of stdin when not a TTY (piped input). */ export async function readStdin(): Promise { if (process.stdin.isTTY) return ''; const chunks: Buffer[] = []; for await (const chunk of process.stdin) chunks.push(chunk as Buffer); return Buffer.concat(chunks).toString('utf-8'); } /** * Resolve a CLI argument that may be: * - `@filepath` → read file contents * - `-` → read from stdin * - raw string → use as-is * - undefined → fall back to stdin (if piped) */ export async function resolveArg(arg: string | undefined): Promise { if (arg === '-') { const stdin = await readStdin(); if (!stdin.trim()) outputError('No data received from stdin.'); return stdin.trim(); } if (arg && arg.startsWith('@')) { const filePath = arg.slice(1); try { return (await readFile(filePath, 'utf-8')).trim(); } catch { outputError(`Cannot read file: ${filePath}`); } } if (arg) return arg; // No explicit arg — try stdin if piped const stdin = await readStdin(); if (stdin.trim()) return stdin.trim(); outputError('No data provided. Pass as argument, @filepath, or pipe via stdin.'); } /** Parse JSON from a CLI positional arg, @filepath, or stdin. */ export async function parseJsonArg(arg: string | undefined): Promise { const raw = await resolveArg(arg); try { return JSON.parse(raw); } catch { outputError(`Invalid JSON: ${raw.slice(0, 200)}...`); } }