import yaml from 'js-yaml' export type RawOutput = { readonly kind: 'raw' readonly data: string } export type TableCell = string | number | boolean | null | undefined export type TableColumn = { readonly name: string readonly priority: number } export type TablePrintable = { readonly tableColumns: () => readonly TableColumn[] readonly tableRows: () => readonly (readonly TableCell[])[] readonly json: () => unknown } export type FormattedPrintable = { readonly text: () => string readonly json: () => unknown } export type NamePrintable = { readonly name: () => string } export type JsonPrintable = { readonly json: () => unknown } export type TableOutput = { readonly kind: 'table' readonly format: string readonly data: TRow } export type FormattedOutput = { readonly kind: 'formatted' readonly format: string readonly data: TData } export type CommandOutput = RawOutput | TableOutput | FormattedOutput export function raw(data: string): RawOutput { return { kind: 'raw', data } } export function table(opts: { readonly format: string readonly data: TRow }): TableOutput { return { kind: 'table', ...opts } } export function formatted(opts: { readonly format: string readonly data: TData }): FormattedOutput { return { kind: 'formatted', ...opts } } export function stringifyOutput(output: CommandOutput): string { switch (output.kind) { case 'raw': return output.data case 'table': return stringifyTableOutput(output) case 'formatted': return stringifyFormattedOutput(output) } } function stringifyFormattedOutput(output: FormattedOutput): string { switch (output.format) { case '': case 'text': return output.data.text() default: return stringifyJsonLike(output.format, output.data, 'json, name, text, yaml') } } function stringifyTableOutput(output: TableOutput): string { switch (output.format) { case '': case 'wide': return renderTable(output) default: return stringifyJsonLike(output.format, output.data, 'json, name, wide, yaml') } } function stringifyJsonLike( format: string, data: TablePrintable | FormattedPrintable, allowed: string, ): string { switch (format) { case 'json': return `${JSON.stringify(data.json(), null, 2)}\n` case 'yaml': return yaml.dump(data.json(), { indent: 2, lineWidth: -1 }) case 'name': return `${toName(data)}\n` default: throw new Error(`output format ${JSON.stringify(format)} not supported, allowed: ${allowed}`) } } function renderTable(output: TableOutput): string { const wide = output.format === 'wide' const columns = output.data.tableColumns() const keep: number[] = [] for (let i = 0; i < columns.length; i++) { const column = columns[i] if (column !== undefined && (column.priority === 0 || wide)) keep.push(i) } const rows = [ keep.map(i => columns[i]?.name ?? ''), ...output.data.tableRows().map(row => keep.map((idx) => { const cell = row[idx] return cell === null || cell === undefined ? '' : String(cell) })), ] return formatTable(rows) } function isWideCodePoint(cp: number): boolean { return ( (cp >= 0x1100 && cp <= 0x115F) || cp === 0x2329 || cp === 0x232A || (cp >= 0x2E80 && cp <= 0x3247) || (cp >= 0x3250 && cp <= 0x4DBF) || (cp >= 0x4E00 && cp <= 0xA4C6) || (cp >= 0xA960 && cp <= 0xA97C) || (cp >= 0xAC00 && cp <= 0xD7A3) || (cp >= 0xF900 && cp <= 0xFAFF) || (cp >= 0xFE10 && cp <= 0xFE19) || (cp >= 0xFE30 && cp <= 0xFE6B) || (cp >= 0xFF01 && cp <= 0xFF60) || (cp >= 0xFFE0 && cp <= 0xFFE6) || (cp >= 0x1B000 && cp <= 0x1B001) || (cp >= 0x1F200 && cp <= 0x1F251) || (cp >= 0x20000 && cp <= 0x3FFFD) ) } function displayWidth(s: string): number { let w = 0 for (const ch of s) w += isWideCodePoint(ch.codePointAt(0) ?? 0) ? 2 : 1 return w } function formatTable(rows: readonly (readonly string[])[]): string { if (rows.length === 0) return '' const colCount = rows[0]?.length ?? 0 const widths: number[] = Array.from({ length: colCount }, () => 0) const rowWidths: number[][] = [] for (const row of rows) { const rw: number[] = Array.from({ length: colCount }, (_v, i) => displayWidth(row[i] ?? '')) rowWidths.push(rw) for (let i = 0; i < colCount; i++) { const w = rw[i] ?? 0 if (w > (widths[i] ?? 0)) widths[i] = w } } const lines = rows.map((row, rowIdx) => { const rw = rowWidths[rowIdx] ?? [] const cells: string[] = [] for (let i = 0; i < colCount; i++) { const cell = row[i] ?? '' const isLast = i === colCount - 1 if (isLast) { cells.push(cell) } else { const pad = (widths[i] ?? 0) - (rw[i] ?? 0) + 2 cells.push(cell + ' '.repeat(pad)) } } return cells.join('') }) return `${lines.join('\n')}\n` } function toName(data: TablePrintable | FormattedPrintable): string { if (!isNamePrintable(data)) throw new Error('name output requires data.name()') return data.name() } function isNamePrintable(data: TablePrintable | FormattedPrintable): data is (TablePrintable | FormattedPrintable) & NamePrintable { return typeof (data as { name?: unknown }).name === 'function' }