/** * CLI Output Formatter * * Provides formatting utilities for JSON, table, and plain text output. * * @module cli/output */ import { formatTable as formatAsciiTable } from './formatting.js' /** * Output format type */ export type OutputFormat = 'json' | 'table' | 'plain' /** * Output formatter options */ export interface OutputOptions { format?: OutputFormat | undefined noColor?: boolean | undefined verbose?: boolean | undefined } /** * Get current output format from options and environment */ export function getOutputFormat(options: OutputOptions): OutputFormat { if (options.format) return options.format if (process.env['POSTGRES_DO_OUTPUT_FORMAT']) { return process.env['POSTGRES_DO_OUTPUT_FORMAT'] as OutputFormat } return 'table' } /** * Check if colors should be used */ export function shouldUseColor(options: OutputOptions): boolean { if (options.noColor) return false if (process.env['NO_COLOR']) return false return process.stdout.isTTY ?? false } // ANSI color codes const colors = { reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', magenta: '\x1b[35m', cyan: '\x1b[36m', white: '\x1b[37m', gray: '\x1b[90m', } /** * Apply color to text */ function colorize(text: string, color: keyof typeof colors, options: OutputOptions): string { if (!shouldUseColor(options)) return text return `${colors[color]}${text}${colors.reset}` } /** * Format data based on output format */ export function formatOutput( data: unknown, options: OutputOptions = {} ): string { const format = getOutputFormat(options) switch (format) { case 'json': return formatJson(data) case 'table': return formatTableOutput(data, options) case 'plain': return formatPlain(data) default: return formatTableOutput(data, options) } } /** * Format data as JSON */ export function formatJson(data: unknown): string { return JSON.stringify(data, null, 2) } /** * Format data as plain text */ export function formatPlain(data: unknown): string { if (data === null || data === undefined) return '' if (typeof data === 'string') return data if (typeof data !== 'object') return String(data) if (Array.isArray(data)) { return data.map((item) => formatPlain(item)).join('\n') } // Object: key=value pairs return Object.entries(data as Record) .map(([key, value]) => `${key}=${formatValue(value)}`) .join('\n') } /** * Format a single value for plain text */ function formatValue(value: unknown): string { if (value === null || value === undefined) return '' if (typeof value === 'object') return JSON.stringify(value) return String(value) } /** * Format data as ASCII table */ export function formatTableOutput(data: unknown, options: OutputOptions = {}): string { if (data === null || data === undefined) return '' if (typeof data !== 'object') return String(data) if (Array.isArray(data)) { if (data.length === 0) return colorize('(empty)', 'dim', options) // Check if array of objects if (typeof data[0] === 'object' && data[0] !== null) { return formatObjectArray(data as Record[], options) } // Array of primitives return data.map((item) => String(item)).join('\n') } // Single object return formatSingleObject(data as Record, options) } /** * Format an array of objects as a table */ function formatObjectArray( data: Record[], options: OutputOptions ): string { if (data.length === 0) return colorize('(empty)', 'dim', options) // Get all unique keys const keys = new Set() for (const item of data) { for (const key of Object.keys(item)) { keys.add(key) } } const headers = Array.from(keys) const rows = data.map((item) => headers.map((key) => formatCellValue(item[key])) ) return formatAsciiTable(headers, rows) } /** * Format a single object as key-value pairs */ function formatSingleObject( data: Record, options: OutputOptions ): string { const maxKeyLength = Math.max(...Object.keys(data).map((k) => k.length)) return Object.entries(data) .map(([key, value]) => { const paddedKey = key.padEnd(maxKeyLength) const formattedKey = colorize(paddedKey, 'cyan', options) return `${formattedKey} ${formatCellValue(value)}` }) .join('\n') } /** * Format a cell value for table display */ function formatCellValue(value: unknown): string { if (value === null) return 'null' if (value === undefined) return '' if (typeof value === 'boolean') return value ? 'true' : 'false' if (value instanceof Date) return value.toISOString() if (typeof value === 'object') return JSON.stringify(value) return String(value) } /** * Print formatted output to stdout */ export function print(data: unknown, options: OutputOptions = {}): void { console.log(formatOutput(data, options)) } /** * Print error output to stderr */ export function printError(message: string, options: OutputOptions = {}): void { const format = getOutputFormat(options) if (format === 'json') { console.error(JSON.stringify({ error: message })) } else { console.error(colorize(`Error: ${message}`, 'red', options)) } } /** * Print success message */ export function printSuccess(message: string, options: OutputOptions = {}): void { const format = getOutputFormat(options) if (format === 'json') { console.log(JSON.stringify({ success: true, message })) } else { console.log(colorize(`Success: ${message}`, 'green', options)) } } /** * Print warning message */ export function printWarning(message: string, options: OutputOptions = {}): void { const format = getOutputFormat(options) if (format === 'json') { console.log(JSON.stringify({ warning: message })) } else { console.log(colorize(`Warning: ${message}`, 'yellow', options)) } } /** * Print info message */ export function printInfo(message: string, options: OutputOptions = {}): void { const format = getOutputFormat(options) if (format === 'json') { // Skip info messages in JSON mode unless verbose if (options.verbose) { console.log(JSON.stringify({ info: message })) } } else { console.log(colorize(message, 'blue', options)) } } /** * Print verbose message (only if verbose is enabled) */ export function printVerbose(message: string, options: OutputOptions = {}): void { if (!options.verbose) return const format = getOutputFormat(options) if (format === 'json') { console.log(JSON.stringify({ verbose: message })) } else { console.log(colorize(`[verbose] ${message}`, 'dim', options)) } } /** * Create a result object for commands */ export interface CommandResult { success: boolean data?: T | undefined message?: string | undefined error?: string | undefined } /** * Print a command result */ export function printResult(result: CommandResult, options: OutputOptions = {}): void { const format = getOutputFormat(options) if (format === 'json') { console.log(JSON.stringify(result, null, 2)) return } if (result.success) { if (result.message) { printSuccess(result.message, options) } if (result.data !== undefined) { print(result.data, options) } } else { if (result.error) { printError(result.error, options) } } } /** * Format bytes to human readable string */ export function formatBytes(bytes: number): string { const units = ['B', 'KB', 'MB', 'GB', 'TB'] let unitIndex = 0 let value = bytes while (value >= 1024 && unitIndex < units.length - 1) { value /= 1024 unitIndex++ } return `${value.toFixed(unitIndex === 0 ? 0 : 2)} ${units[unitIndex]}` } /** * Format duration to human readable string */ export function formatDuration(ms: number): string { if (ms < 1000) return `${ms}ms` if (ms < 60000) return `${(ms / 1000).toFixed(2)}s` const minutes = Math.floor(ms / 60000) const seconds = ((ms % 60000) / 1000).toFixed(0) return `${minutes}m ${seconds}s` }