Switch to double quotes and reformat codebase with Biome
This commit is contained in:
@@ -47,16 +47,16 @@ export type {
|
||||
ListResult,
|
||||
AnyCommandResult,
|
||||
CommandError,
|
||||
} from './types.js'
|
||||
} from "./types.js";
|
||||
|
||||
// Renderers
|
||||
export { renderTable, renderTableHeader, renderTableRow } from './table.js'
|
||||
export { renderJson, renderJsonLine } from './json.js'
|
||||
export { renderYaml, renderYamlDoc } from './yaml.js'
|
||||
export { renderQuiet } from './quiet.js'
|
||||
export { renderTable, renderTableHeader, renderTableRow } from "./table.js";
|
||||
export { renderJson, renderJsonLine } from "./json.js";
|
||||
export { renderYaml, renderYamlDoc } from "./yaml.js";
|
||||
export { renderQuiet } from "./quiet.js";
|
||||
|
||||
// Main render function
|
||||
export { render, renderError, toCommandError, defaultOutputOptions } from './render.js'
|
||||
export { render, renderError, toCommandError, defaultOutputOptions } from "./render.js";
|
||||
|
||||
// Command wrapper
|
||||
export { withOutput, createOutputOptions, type CommandOptions } from './with-output.js'
|
||||
export { withOutput, createOutputOptions, type CommandOptions } from "./with-output.js";
|
||||
|
||||
@@ -4,41 +4,38 @@
|
||||
* Renders structured data as formatted JSON for machine consumption.
|
||||
*/
|
||||
|
||||
import type { AnyCommandResult, OutputOptions } from './types.js'
|
||||
import type { AnyCommandResult, OutputOptions } from "./types.js";
|
||||
|
||||
/** Render command result as JSON */
|
||||
export function renderJson<T>(
|
||||
result: AnyCommandResult<T>,
|
||||
_options: OutputOptions
|
||||
): string {
|
||||
const { schema } = result
|
||||
export function renderJson<T>(result: AnyCommandResult<T>, _options: OutputOptions): string {
|
||||
const { schema } = result;
|
||||
|
||||
// Apply custom serializer if provided
|
||||
if (schema.serialize) {
|
||||
if (result.type === 'list') {
|
||||
if (result.type === "list") {
|
||||
// If all items serialize to the same object, return just one
|
||||
// This handles the case where a list of key-value rows should serialize
|
||||
// to a single structured object
|
||||
const serialized = result.data.map((item) => schema.serialize!(item))
|
||||
const serialized = result.data.map((item) => schema.serialize!(item));
|
||||
if (serialized.length > 0) {
|
||||
const first = JSON.stringify(serialized[0])
|
||||
const allSame = serialized.every((s) => JSON.stringify(s) === first)
|
||||
const first = JSON.stringify(serialized[0]);
|
||||
const allSame = serialized.every((s) => JSON.stringify(s) === first);
|
||||
if (allSame) {
|
||||
return JSON.stringify(serialized[0], null, 2)
|
||||
return JSON.stringify(serialized[0], null, 2);
|
||||
}
|
||||
}
|
||||
return JSON.stringify(serialized, null, 2)
|
||||
return JSON.stringify(serialized, null, 2);
|
||||
} else {
|
||||
const serialized = schema.serialize(result.data)
|
||||
return JSON.stringify(serialized, null, 2)
|
||||
const serialized = schema.serialize(result.data);
|
||||
return JSON.stringify(serialized, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
return JSON.stringify(result.data, null, 2)
|
||||
return JSON.stringify(result.data, null, 2);
|
||||
}
|
||||
|
||||
/** Render a single item as JSON line (for NDJSON streaming) */
|
||||
export function renderJsonLine<T>(item: T, serialize?: (data: T) => unknown): string {
|
||||
const output = serialize ? serialize(item) : item
|
||||
return JSON.stringify(output)
|
||||
const output = serialize ? serialize(item) : item;
|
||||
return JSON.stringify(output);
|
||||
}
|
||||
|
||||
@@ -4,24 +4,21 @@
|
||||
* Outputs only ID fields, one per line. Useful for scripting and pipelines.
|
||||
*/
|
||||
|
||||
import type { AnyCommandResult, OutputOptions } from './types.js'
|
||||
import type { AnyCommandResult, OutputOptions } from "./types.js";
|
||||
|
||||
/** Extract ID from item using schema definition */
|
||||
function getId<T>(item: T, idField: keyof T | ((item: T) => string)): string {
|
||||
if (typeof idField === 'function') {
|
||||
return idField(item)
|
||||
if (typeof idField === "function") {
|
||||
return idField(item);
|
||||
}
|
||||
return String(item[idField])
|
||||
return String(item[idField]);
|
||||
}
|
||||
|
||||
/** Render command result in quiet mode (IDs only) */
|
||||
export function renderQuiet<T>(
|
||||
result: AnyCommandResult<T>,
|
||||
_options: OutputOptions
|
||||
): string {
|
||||
if (result.type === 'single') {
|
||||
return getId(result.data, result.schema.idField)
|
||||
export function renderQuiet<T>(result: AnyCommandResult<T>, _options: OutputOptions): string {
|
||||
if (result.type === "single") {
|
||||
return getId(result.data, result.schema.idField);
|
||||
} else {
|
||||
return result.data.map((item) => getId(item, result.schema.idField)).join('\n')
|
||||
return result.data.map((item) => getId(item, result.schema.idField)).join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,100 +4,97 @@
|
||||
* Selects the appropriate renderer based on output options.
|
||||
*/
|
||||
|
||||
import chalk from 'chalk'
|
||||
import YAML from 'yaml'
|
||||
import type { AnyCommandResult, CommandError, OutputOptions } from './types.js'
|
||||
import { renderTable } from './table.js'
|
||||
import { renderJson } from './json.js'
|
||||
import { renderYaml } from './yaml.js'
|
||||
import { renderQuiet } from './quiet.js'
|
||||
import chalk from "chalk";
|
||||
import YAML from "yaml";
|
||||
import type { AnyCommandResult, CommandError, OutputOptions } from "./types.js";
|
||||
import { renderTable } from "./table.js";
|
||||
import { renderJson } from "./json.js";
|
||||
import { renderYaml } from "./yaml.js";
|
||||
import { renderQuiet } from "./quiet.js";
|
||||
|
||||
/** Default output options */
|
||||
export const defaultOutputOptions: OutputOptions = {
|
||||
format: 'table',
|
||||
format: "table",
|
||||
quiet: false,
|
||||
noHeaders: false,
|
||||
noColor: false,
|
||||
}
|
||||
};
|
||||
|
||||
/** Render command result to string based on output options */
|
||||
export function render<T>(
|
||||
result: AnyCommandResult<T>,
|
||||
options: Partial<OutputOptions> = {}
|
||||
options: Partial<OutputOptions> = {},
|
||||
): string {
|
||||
const opts: OutputOptions = { ...defaultOutputOptions, ...options }
|
||||
const opts: OutputOptions = { ...defaultOutputOptions, ...options };
|
||||
|
||||
// Quiet mode takes precedence
|
||||
if (opts.quiet) {
|
||||
return renderQuiet(result, opts)
|
||||
return renderQuiet(result, opts);
|
||||
}
|
||||
|
||||
// Dispatch to format-specific renderer
|
||||
switch (opts.format) {
|
||||
case 'json':
|
||||
return renderJson(result, opts)
|
||||
case 'yaml':
|
||||
return renderYaml(result, opts)
|
||||
case 'table':
|
||||
case "json":
|
||||
return renderJson(result, opts);
|
||||
case "yaml":
|
||||
return renderYaml(result, opts);
|
||||
case "table":
|
||||
default:
|
||||
return renderTable(result, opts)
|
||||
return renderTable(result, opts);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert an unknown error to a CommandError */
|
||||
export function toCommandError(error: unknown): CommandError {
|
||||
if (isCommandError(error)) {
|
||||
return error
|
||||
return error;
|
||||
}
|
||||
|
||||
if (error instanceof Error) {
|
||||
return {
|
||||
code: 'UNKNOWN_ERROR',
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: error.message,
|
||||
details: error.stack,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
code: 'UNKNOWN_ERROR',
|
||||
code: "UNKNOWN_ERROR",
|
||||
message: String(error),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Type guard for CommandError */
|
||||
function isCommandError(error: unknown): error is CommandError {
|
||||
return (
|
||||
typeof error === 'object' &&
|
||||
typeof error === "object" &&
|
||||
error !== null &&
|
||||
'code' in error &&
|
||||
'message' in error &&
|
||||
typeof (error as CommandError).code === 'string' &&
|
||||
typeof (error as CommandError).message === 'string'
|
||||
)
|
||||
"code" in error &&
|
||||
"message" in error &&
|
||||
typeof (error as CommandError).code === "string" &&
|
||||
typeof (error as CommandError).message === "string"
|
||||
);
|
||||
}
|
||||
|
||||
/** Render an error to string based on output options */
|
||||
export function renderError(
|
||||
error: CommandError,
|
||||
options: Partial<OutputOptions> = {}
|
||||
): string {
|
||||
const opts: OutputOptions = { ...defaultOutputOptions, ...options }
|
||||
export function renderError(error: CommandError, options: Partial<OutputOptions> = {}): string {
|
||||
const opts: OutputOptions = { ...defaultOutputOptions, ...options };
|
||||
|
||||
if (opts.format === 'json') {
|
||||
return JSON.stringify({ error }, null, 2)
|
||||
if (opts.format === "json") {
|
||||
return JSON.stringify({ error }, null, 2);
|
||||
}
|
||||
|
||||
if (opts.format === 'yaml') {
|
||||
return YAML.stringify({ error })
|
||||
if (opts.format === "yaml") {
|
||||
return YAML.stringify({ error });
|
||||
}
|
||||
|
||||
// Table/default format: human-readable error
|
||||
const prefix = opts.noColor ? 'Error: ' : chalk.red('Error: ')
|
||||
const message = error.message
|
||||
const prefix = opts.noColor ? "Error: " : chalk.red("Error: ");
|
||||
const message = error.message;
|
||||
|
||||
if (error.details && typeof error.details === 'string') {
|
||||
return `${prefix}${message}\n${error.details}`
|
||||
if (error.details && typeof error.details === "string") {
|
||||
return `${prefix}${message}\n${error.details}`;
|
||||
}
|
||||
|
||||
return `${prefix}${message}`
|
||||
return `${prefix}${message}`;
|
||||
}
|
||||
|
||||
@@ -4,49 +4,40 @@
|
||||
* Renders structured data as aligned ASCII tables with optional color support.
|
||||
*/
|
||||
|
||||
import chalk, { type ChalkInstance } from 'chalk'
|
||||
import type {
|
||||
AnyCommandResult,
|
||||
ColumnDef,
|
||||
OutputOptions,
|
||||
OutputSchema,
|
||||
} from './types.js'
|
||||
import chalk, { type ChalkInstance } from "chalk";
|
||||
import type { AnyCommandResult, ColumnDef, OutputOptions, OutputSchema } from "./types.js";
|
||||
|
||||
// ANSI escape code regex for stripping colors when measuring width
|
||||
const ANSI_REGEX =
|
||||
// eslint-disable-next-line no-control-regex
|
||||
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g
|
||||
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g;
|
||||
|
||||
/** Strip ANSI escape codes from a string */
|
||||
function stripAnsi(str: string): string {
|
||||
return str.replace(ANSI_REGEX, '')
|
||||
return str.replace(ANSI_REGEX, "");
|
||||
}
|
||||
|
||||
/** Get visible string length (excluding ANSI codes) */
|
||||
function visibleLength(str: string): number {
|
||||
return stripAnsi(str).length
|
||||
return stripAnsi(str).length;
|
||||
}
|
||||
|
||||
/** Pad a cell to the specified width with alignment */
|
||||
function padCell(
|
||||
cell: string,
|
||||
width: number,
|
||||
align: 'left' | 'right' | 'center'
|
||||
): string {
|
||||
const visible = visibleLength(cell)
|
||||
const padding = Math.max(0, width - visible)
|
||||
function padCell(cell: string, width: number, align: "left" | "right" | "center"): string {
|
||||
const visible = visibleLength(cell);
|
||||
const padding = Math.max(0, width - visible);
|
||||
|
||||
switch (align) {
|
||||
case 'right':
|
||||
return ' '.repeat(padding) + cell
|
||||
case 'center': {
|
||||
const left = Math.floor(padding / 2)
|
||||
const right = padding - left
|
||||
return ' '.repeat(left) + cell + ' '.repeat(right)
|
||||
case "right":
|
||||
return " ".repeat(padding) + cell;
|
||||
case "center": {
|
||||
const left = Math.floor(padding / 2);
|
||||
const right = padding - left;
|
||||
return " ".repeat(left) + cell + " ".repeat(right);
|
||||
}
|
||||
case 'left':
|
||||
case "left":
|
||||
default:
|
||||
return cell + ' '.repeat(padding)
|
||||
return cell + " ".repeat(padding);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,15 +56,15 @@ function applyColor(str: string, colorName: string): string {
|
||||
grey: chalk.grey,
|
||||
dim: chalk.dim,
|
||||
bold: chalk.bold,
|
||||
}
|
||||
};
|
||||
|
||||
const colorFn = colorMap[colorName]
|
||||
return colorFn ? colorFn(str) : str
|
||||
const colorFn = colorMap[colorName];
|
||||
return colorFn ? colorFn(str) : str;
|
||||
}
|
||||
|
||||
/** Extract value from item using field definition */
|
||||
function getValue<T>(item: T, field: keyof T | ((item: T) => unknown)): unknown {
|
||||
return typeof field === 'function' ? field(item) : item[field]
|
||||
return typeof field === "function" ? field(item) : item[field];
|
||||
}
|
||||
|
||||
/** Render a single table row */
|
||||
@@ -81,106 +72,99 @@ function renderRow<T>(
|
||||
item: T,
|
||||
columns: ColumnDef<T>[],
|
||||
widths: number[],
|
||||
options: OutputOptions
|
||||
options: OutputOptions,
|
||||
): string {
|
||||
return columns
|
||||
.map((col, colIndex) => {
|
||||
const value = getValue(item, col.field)
|
||||
let cell = String(value ?? '')
|
||||
const width = widths[colIndex]
|
||||
const value = getValue(item, col.field);
|
||||
let cell = String(value ?? "");
|
||||
const width = widths[colIndex];
|
||||
|
||||
// Apply color if enabled
|
||||
if (col.color && !options.noColor) {
|
||||
const colorName = col.color(value, item)
|
||||
const colorName = col.color(value, item);
|
||||
if (colorName) {
|
||||
cell = applyColor(cell, colorName)
|
||||
cell = applyColor(cell, colorName);
|
||||
}
|
||||
}
|
||||
|
||||
return padCell(cell, width ?? 0, col.align ?? 'left')
|
||||
return padCell(cell, width ?? 0, col.align ?? "left");
|
||||
})
|
||||
.join(' ')
|
||||
.join(" ");
|
||||
}
|
||||
|
||||
/** Render header row */
|
||||
function renderHeader<T>(
|
||||
columns: ColumnDef<T>[],
|
||||
widths: number[],
|
||||
options: OutputOptions
|
||||
options: OutputOptions,
|
||||
): string {
|
||||
const headerRow = columns
|
||||
.map((col, i) => padCell(col.header, widths[i] ?? 0, col.align ?? 'left'))
|
||||
.join(' ')
|
||||
.map((col, i) => padCell(col.header, widths[i] ?? 0, col.align ?? "left"))
|
||||
.join(" ");
|
||||
|
||||
return options.noColor ? headerRow : chalk.bold(headerRow)
|
||||
return options.noColor ? headerRow : chalk.bold(headerRow);
|
||||
}
|
||||
|
||||
/** Calculate column widths based on content and hints */
|
||||
function calculateWidths<T>(
|
||||
data: T[],
|
||||
columns: ColumnDef<T>[],
|
||||
includeHeaders: boolean
|
||||
): number[] {
|
||||
function calculateWidths<T>(data: T[], columns: ColumnDef<T>[], includeHeaders: boolean): number[] {
|
||||
return columns.map((col) => {
|
||||
// Start with header width if including headers
|
||||
let maxWidth = includeHeaders ? col.header.length : 0
|
||||
let maxWidth = includeHeaders ? col.header.length : 0;
|
||||
|
||||
// Check all data values
|
||||
for (const item of data) {
|
||||
const value = getValue(item, col.field)
|
||||
const str = String(value ?? '')
|
||||
maxWidth = Math.max(maxWidth, visibleLength(str))
|
||||
const value = getValue(item, col.field);
|
||||
const str = String(value ?? "");
|
||||
maxWidth = Math.max(maxWidth, visibleLength(str));
|
||||
}
|
||||
|
||||
// Apply width hint if specified (minimum width)
|
||||
if (col.width) {
|
||||
maxWidth = Math.max(maxWidth, col.width)
|
||||
maxWidth = Math.max(maxWidth, col.width);
|
||||
}
|
||||
|
||||
return maxWidth
|
||||
})
|
||||
return maxWidth;
|
||||
});
|
||||
}
|
||||
|
||||
/** Render a list result as a table */
|
||||
export function renderTable<T>(
|
||||
result: AnyCommandResult<T>,
|
||||
options: OutputOptions
|
||||
): string {
|
||||
const { schema } = result
|
||||
const data = result.type === 'list' ? result.data : [result.data]
|
||||
export function renderTable<T>(result: AnyCommandResult<T>, options: OutputOptions): string {
|
||||
const { schema } = result;
|
||||
const data = result.type === "list" ? result.data : [result.data];
|
||||
|
||||
if (data.length === 0) {
|
||||
return ''
|
||||
return "";
|
||||
}
|
||||
|
||||
const columns = schema.columns as ColumnDef<T>[]
|
||||
const includeHeaders = !options.noHeaders
|
||||
const widths = calculateWidths(data, columns, includeHeaders)
|
||||
const columns = schema.columns as ColumnDef<T>[];
|
||||
const includeHeaders = !options.noHeaders;
|
||||
const widths = calculateWidths(data, columns, includeHeaders);
|
||||
|
||||
const lines: string[] = []
|
||||
const lines: string[] = [];
|
||||
|
||||
// Add header row
|
||||
if (includeHeaders) {
|
||||
lines.push(renderHeader(columns, widths, options))
|
||||
lines.push(renderHeader(columns, widths, options));
|
||||
}
|
||||
|
||||
// Add data rows
|
||||
for (const item of data) {
|
||||
lines.push(renderRow(item, columns, widths, options))
|
||||
lines.push(renderRow(item, columns, widths, options));
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/** Render just a table header (for streaming) */
|
||||
export function renderTableHeader<T>(
|
||||
schema: OutputSchema<T>,
|
||||
options: OutputOptions,
|
||||
widths?: number[]
|
||||
widths?: number[],
|
||||
): string {
|
||||
const columns = schema.columns
|
||||
const actualWidths = widths ?? columns.map((col) => col.width ?? col.header.length)
|
||||
return renderHeader(columns, actualWidths, options)
|
||||
const columns = schema.columns;
|
||||
const actualWidths = widths ?? columns.map((col) => col.width ?? col.header.length);
|
||||
return renderHeader(columns, actualWidths, options);
|
||||
}
|
||||
|
||||
/** Render just a table row (for streaming) */
|
||||
@@ -188,9 +172,9 @@ export function renderTableRow<T>(
|
||||
item: T,
|
||||
schema: OutputSchema<T>,
|
||||
options: OutputOptions,
|
||||
widths?: number[]
|
||||
widths?: number[],
|
||||
): string {
|
||||
const columns = schema.columns
|
||||
const actualWidths = widths ?? columns.map((col) => col.width ?? col.header.length)
|
||||
return renderRow(item, columns, actualWidths, options)
|
||||
const columns = schema.columns;
|
||||
const actualWidths = widths ?? columns.map((col) => col.width ?? col.header.length);
|
||||
return renderRow(item, columns, actualWidths, options);
|
||||
}
|
||||
|
||||
@@ -6,74 +6,74 @@
|
||||
*/
|
||||
|
||||
/** Supported output formats */
|
||||
export type OutputFormat = 'table' | 'json' | 'yaml'
|
||||
export type OutputFormat = "table" | "json" | "yaml";
|
||||
|
||||
/** Options controlling output rendering */
|
||||
export interface OutputOptions {
|
||||
/** Output format (table, json, yaml) */
|
||||
format: OutputFormat
|
||||
format: OutputFormat;
|
||||
/** Minimal output - IDs only */
|
||||
quiet: boolean
|
||||
quiet: boolean;
|
||||
/** Omit table headers */
|
||||
noHeaders: boolean
|
||||
noHeaders: boolean;
|
||||
/** Disable color output */
|
||||
noColor: boolean
|
||||
noColor: boolean;
|
||||
}
|
||||
|
||||
/** Column definition for table output */
|
||||
export interface ColumnDef<T> {
|
||||
/** Header text for the column */
|
||||
header: string
|
||||
header: string;
|
||||
/** Field key or accessor function */
|
||||
field: keyof T | ((item: T) => unknown)
|
||||
field: keyof T | ((item: T) => unknown);
|
||||
/** Optional width hint (characters) */
|
||||
width?: number
|
||||
width?: number;
|
||||
/** Optional alignment */
|
||||
align?: 'left' | 'right' | 'center'
|
||||
align?: "left" | "right" | "center";
|
||||
/** Optional color function - returns chalk color name */
|
||||
color?: (value: unknown, item: T) => string | undefined
|
||||
color?: (value: unknown, item: T) => string | undefined;
|
||||
}
|
||||
|
||||
/** Schema describing how to render command output */
|
||||
export interface OutputSchema<T> {
|
||||
/** Field to use for quiet mode (--quiet outputs just this) */
|
||||
idField: keyof T | ((item: T) => string)
|
||||
idField: keyof T | ((item: T) => string);
|
||||
/** Column definitions for table output */
|
||||
columns: ColumnDef<T>[]
|
||||
columns: ColumnDef<T>[];
|
||||
/** Optional: transform data before JSON/YAML output */
|
||||
serialize?: (data: T) => unknown
|
||||
serialize?: (data: T) => unknown;
|
||||
}
|
||||
|
||||
/** Result type for commands returning a single item */
|
||||
export interface SingleResult<T> {
|
||||
type: 'single'
|
||||
type: "single";
|
||||
/** The structured data to render */
|
||||
data: T
|
||||
data: T;
|
||||
/** Schema describing how to render this data (for item type T) */
|
||||
schema: OutputSchema<T>
|
||||
schema: OutputSchema<T>;
|
||||
}
|
||||
|
||||
/** Result type for commands returning a list */
|
||||
export interface ListResult<T> {
|
||||
type: 'list'
|
||||
type: "list";
|
||||
/** The structured data to render */
|
||||
data: T[]
|
||||
data: T[];
|
||||
/** Schema describing how to render this data (for item type T) */
|
||||
schema: OutputSchema<T>
|
||||
schema: OutputSchema<T>;
|
||||
}
|
||||
|
||||
/** Union type for all command results */
|
||||
export type AnyCommandResult<T> = SingleResult<T> | ListResult<T>
|
||||
export type AnyCommandResult<T> = SingleResult<T> | ListResult<T>;
|
||||
|
||||
/** Base interface for command results (deprecated, use SingleResult or ListResult) */
|
||||
export type CommandResult<T> = SingleResult<T> | ListResult<T>
|
||||
export type CommandResult<T> = SingleResult<T> | ListResult<T>;
|
||||
|
||||
/** Structured error for command failures */
|
||||
export interface CommandError {
|
||||
/** Machine-readable error code */
|
||||
code: string
|
||||
code: string;
|
||||
/** Human-readable message */
|
||||
message: string
|
||||
message: string;
|
||||
/** Additional context */
|
||||
details?: unknown
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
@@ -4,51 +4,51 @@
|
||||
* Wraps command handlers to automatically render results and handle errors.
|
||||
*/
|
||||
|
||||
import type { Command } from 'commander'
|
||||
import type { AnyCommandResult, CommandError, OutputOptions } from './types.js'
|
||||
import { render, renderError, toCommandError, defaultOutputOptions } from './render.js'
|
||||
import type { Command } from "commander";
|
||||
import type { AnyCommandResult, CommandError, OutputOptions } from "./types.js";
|
||||
import { render, renderError, toCommandError, defaultOutputOptions } from "./render.js";
|
||||
|
||||
/** Options that include output settings from global options */
|
||||
export interface CommandOptions extends Partial<OutputOptions> {
|
||||
[key: string]: unknown
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function normalizeFormat(raw: unknown): OutputOptions['format'] {
|
||||
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
|
||||
function normalizeFormat(raw: unknown): OutputOptions["format"] {
|
||||
const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
||||
|
||||
// Common user expectation: "cli" means "table/human"
|
||||
if (value === 'cli') return 'table'
|
||||
if (value === "cli") return "table";
|
||||
|
||||
if (value === 'table' || value === 'json' || value === 'yaml') return value
|
||||
if (value === "table" || value === "json" || value === "yaml") return value;
|
||||
|
||||
const error: CommandError = {
|
||||
code: 'INVALID_FORMAT',
|
||||
code: "INVALID_FORMAT",
|
||||
message: `Unsupported output format: ${String(raw)}`,
|
||||
details: 'Supported formats: table, json, yaml',
|
||||
}
|
||||
throw error
|
||||
details: "Supported formats: table, json, yaml",
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
/** Extract output options from command options */
|
||||
function extractOutputOptions(options: CommandOptions): OutputOptions {
|
||||
const hasStructuredOutputSchema =
|
||||
typeof options.outputSchema === 'string' && options.outputSchema.trim().length > 0
|
||||
typeof options.outputSchema === "string" && options.outputSchema.trim().length > 0;
|
||||
|
||||
if (hasStructuredOutputSchema) {
|
||||
return {
|
||||
format: 'json',
|
||||
format: "json",
|
||||
quiet: false,
|
||||
noHeaders: options.headers === false,
|
||||
noColor: options.color === false,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
format: options.json ? 'json' : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
||||
format: options.json ? "json" : normalizeFormat(options.format ?? defaultOutputOptions.format),
|
||||
quiet: options.quiet ?? defaultOutputOptions.quiet,
|
||||
noHeaders: options.headers === false, // Commander uses --no-headers -> headers: false
|
||||
noColor: options.color === false, // Commander uses --no-color -> color: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,37 +71,35 @@ function extractOutputOptions(options: CommandOptions): OutputOptions {
|
||||
* ```
|
||||
*/
|
||||
export function withOutput<T, Args extends unknown[]>(
|
||||
handler: (...args: [...Args, CommandOptions, Command]) => Promise<AnyCommandResult<T>>
|
||||
handler: (...args: [...Args, CommandOptions, Command]) => Promise<AnyCommandResult<T>>,
|
||||
): (...args: [...Args, CommandOptions, Command]) => Promise<void> {
|
||||
return async (...args) => {
|
||||
// Last two args are options and command
|
||||
const command = args[args.length - 1] as Command
|
||||
const command = args[args.length - 1] as Command;
|
||||
// Use optsWithGlobals() to get both local and global options
|
||||
const options = command.optsWithGlobals() as CommandOptions
|
||||
const outputOptions = extractOutputOptions(options)
|
||||
const options = command.optsWithGlobals() as CommandOptions;
|
||||
const outputOptions = extractOutputOptions(options);
|
||||
|
||||
try {
|
||||
const result = await handler(...args)
|
||||
const output = render(result, outputOptions)
|
||||
const result = await handler(...args);
|
||||
const output = render(result, outputOptions);
|
||||
|
||||
if (output) {
|
||||
process.stdout.write(output + '\n')
|
||||
process.stdout.write(output + "\n");
|
||||
}
|
||||
} catch (error) {
|
||||
const commandError = toCommandError(error)
|
||||
const errorOutput = renderError(commandError, outputOptions)
|
||||
process.stderr.write(errorOutput + '\n')
|
||||
process.exit(1)
|
||||
const commandError = toCommandError(error);
|
||||
const errorOutput = renderError(commandError, outputOptions);
|
||||
process.stderr.write(errorOutput + "\n");
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to create output options from partial input.
|
||||
* Useful for testing or manual rendering.
|
||||
*/
|
||||
export function createOutputOptions(
|
||||
partial: Partial<OutputOptions> = {}
|
||||
): OutputOptions {
|
||||
return { ...defaultOutputOptions, ...partial }
|
||||
export function createOutputOptions(partial: Partial<OutputOptions> = {}): OutputOptions {
|
||||
return { ...defaultOutputOptions, ...partial };
|
||||
}
|
||||
|
||||
@@ -4,42 +4,39 @@
|
||||
* Renders structured data as YAML for machine consumption and human readability.
|
||||
*/
|
||||
|
||||
import YAML from 'yaml'
|
||||
import type { AnyCommandResult, OutputOptions } from './types.js'
|
||||
import YAML from "yaml";
|
||||
import type { AnyCommandResult, OutputOptions } from "./types.js";
|
||||
|
||||
/** Render command result as YAML */
|
||||
export function renderYaml<T>(
|
||||
result: AnyCommandResult<T>,
|
||||
_options: OutputOptions
|
||||
): string {
|
||||
const { schema } = result
|
||||
export function renderYaml<T>(result: AnyCommandResult<T>, _options: OutputOptions): string {
|
||||
const { schema } = result;
|
||||
|
||||
// Apply custom serializer if provided
|
||||
if (schema.serialize) {
|
||||
if (result.type === 'list') {
|
||||
if (result.type === "list") {
|
||||
// If all items serialize to the same object, return just one
|
||||
// This handles the case where a list of key-value rows should serialize
|
||||
// to a single structured object
|
||||
const serialized = result.data.map((item) => schema.serialize!(item))
|
||||
const serialized = result.data.map((item) => schema.serialize!(item));
|
||||
if (serialized.length > 0) {
|
||||
const first = JSON.stringify(serialized[0])
|
||||
const allSame = serialized.every((s) => JSON.stringify(s) === first)
|
||||
const first = JSON.stringify(serialized[0]);
|
||||
const allSame = serialized.every((s) => JSON.stringify(s) === first);
|
||||
if (allSame) {
|
||||
return YAML.stringify(serialized[0])
|
||||
return YAML.stringify(serialized[0]);
|
||||
}
|
||||
}
|
||||
return YAML.stringify(serialized)
|
||||
return YAML.stringify(serialized);
|
||||
} else {
|
||||
const serialized = schema.serialize(result.data)
|
||||
return YAML.stringify(serialized)
|
||||
const serialized = schema.serialize(result.data);
|
||||
return YAML.stringify(serialized);
|
||||
}
|
||||
}
|
||||
|
||||
return YAML.stringify(result.data)
|
||||
return YAML.stringify(result.data);
|
||||
}
|
||||
|
||||
/** Render a single item as YAML document (for streaming) */
|
||||
export function renderYamlDoc<T>(item: T, serialize?: (data: T) => unknown): string {
|
||||
const output = serialize ? serialize(item) : item
|
||||
return YAML.stringify(output)
|
||||
const output = serialize ? serialize(item) : item;
|
||||
return YAML.stringify(output);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user