import { defineInterceptor } from '../core/interceptors.ts'; import { isAsyncIterator, isIterator } from '../core/results.ts'; import type { OutputConfig } from '../output/output-indicator.ts'; import { createOutputIndicator, formatDeclarativeOutput } from '../output/output-indicator.ts'; import { resolveOutputFormat } from '../output/styling.ts'; import type { AnyPadroneBuilder, CommandTypesBase, InterceptorErrorContext, InterceptorErrorResult, InterceptorExecuteContext, InterceptorExecuteResult, } from '../types/index.ts'; // ── Helpers ───────────────────────────────────────────────────────────── /** * Outputs each value and collects into a result. * For iterators: outputs each yielded value, returns collected array. * For promises: awaits, then recurses. * For other values: outputs directly, returns as-is. */ function outputAndCollect(value: unknown, output: (...args: unknown[]) => void): unknown { if (value == null) return value; if (isAsyncIterator(value)) { return (async () => { const items: unknown[] = []; const iter = (value as any)[Symbol.asyncIterator](); while (true) { const { done, value: item } = await iter.next(); if (done) break; items.push(item); if (item != null) output(item); } return items; })(); } if (typeof value !== 'string' && !Array.isArray(value) && isIterator(value)) { const items: unknown[] = []; const iter = (value as any)[Symbol.iterator](); while (true) { const { done, value: item } = iter.next(); if (done) break; items.push(item); if (item != null) output(item); } return items; } if (value instanceof Promise) { return value.then((resolved) => outputAndCollect(resolved, output)); } output(value); return value; } // ── Interceptor ───────────────────────────────────────────────────────── const autoOutputMeta = { id: 'padrone:auto-output', name: 'padrone:auto-output', order: -1100 } as const; function createAutoOutputInterceptor(outputConfig?: OutputConfig, errorOutput?: boolean) { return defineInterceptor(autoOutputMeta, () => ({ error(ctx: InterceptorErrorContext, next: () => InterceptorErrorResult | Promise) { const handleResult = (er: InterceptorErrorResult): InterceptorErrorResult => { if (!er.error || errorOutput === false || ctx.caller !== 'cli' || ctx.phase !== 'execute') return er; const message = er.error instanceof Error ? er.error.message : String(er.error); ctx.runtime.error(message); return er; }; const result = next(); if (result instanceof Promise) return result.then(handleResult); return handleResult(result); }, execute(ctx: InterceptorExecuteContext, next) { const outputCtx = resolveOutputFormat(ctx.runtime, ctx.caller); const indicator = createOutputIndicator(ctx.runtime.output, outputCtx); const handleResult = (e: InterceptorExecuteResult): InterceptorExecuteResult | Promise => { // If the action already called output.*, skip auto-output if (indicator.called) return e; const autoOutput = (value: unknown): unknown => { if (value == null) return value; // Declarative output config: format the return value through the primitive if (outputConfig) { const rendered = formatDeclarativeOutput(value, outputConfig, outputCtx); if (rendered !== undefined) { ctx.runtime.output(rendered); return value; } } return outputAndCollect(value, ctx.runtime.output); }; if (e.result instanceof Promise) { return { result: e.result.then(autoOutput) }; } const collected = autoOutput(e.result); if (collected instanceof Promise) return collected.then((v) => ({ result: v })); return { result: collected }; }; const executedOrPromise = next({ context: { output: indicator } }); if (executedOrPromise instanceof Promise) return executedOrPromise.then(handleResult); return handleResult(executedOrPromise); }, })); } // ── Extension ─────────────────────────────────────────────────────────── export type PadroneAutoOutputOptions = { /** Disable auto-output entirely. */ disabled?: boolean; /** * Declarative output format for the command's return value. * When set, auto-output formats the return value through the specified primitive * instead of passing it raw to `runtime.output`. * Ignored when the action calls `ctx.context.output.*` explicitly. * * ```ts * // Format return value as a table * c.extend(padroneAutoOutput({ output: 'table' })) * * // Format with options * c.extend(padroneAutoOutput({ output: { type: 'table', options: { border: false } } })) * ``` */ output?: OutputConfig; /** * Automatically print unhandled errors to stderr in CLI mode. * Skips errors already handled by other extensions (routing, validation, signal). * @default true */ errorOutput?: boolean; }; /** * Extension that automatically writes a command's return value to output after execution. * * - Values are passed directly to the runtime's `output` function (no stringification). * - Promises are awaited before output. * - Iterators and async iterators are consumed, outputting each yielded value as it arrives. * The result is replaced with the collected array so `drain()` still works. * - `undefined` and `null` results produce no output. * * Also injects `ctx.context.output` with format-aware output primitives (table, tree, list, kv). * When action handlers use these methods, auto-output skips to avoid double output. * * Included in the default extensions. Can also be applied per-command: * ```ts * createPadrone('my-cli') * .command('users', (c) => * c.extend(padroneAutoOutput({ output: 'table' })) * .action(() => fetchUsers()) * ) * ``` */ export function padroneAutoOutput(options?: PadroneAutoOutputOptions): (builder: T) => T { const interceptor = options?.disabled ? defineInterceptor({ ...autoOutputMeta, disabled: true }, () => ({})) : createAutoOutputInterceptor(options?.output, options?.errorOutput); return ((builder: AnyPadroneBuilder) => builder.intercept(interceptor)) as any; }