UNPKG

@xec-sh/cli

Version:

Xec: The Universal Shell for TypeScript

387 lines 12.5 kB
import chalk from 'chalk'; import { table } from 'table'; import * as yaml from 'js-yaml'; export class OutputFormatter { constructor() { this.format = 'text'; this.quiet = false; this.verbose = false; this.colors = true; } setFormat(format) { this.format = format; } setQuiet(quiet) { this.quiet = quiet; } setVerbose(verbose) { this.verbose = verbose; } setColors(colors) { this.colors = colors; } header(title) { if (this.quiet) return; const formatted = this.colors ? chalk.bold.underline(title) : title; console.log(); console.log(formatted); console.log(); } success(message) { if (this.quiet) return; const formatted = this.colors ? chalk.green(`✓ ${message}`) : `✓ ${message}`; console.log(formatted); } error(message) { const text = message instanceof Error ? message.message : message; const formatted = this.colors ? chalk.red(`✗ ${text}`) : `✗ ${text}`; console.error(formatted); } warn(message) { if (this.quiet) return; const formatted = this.colors ? chalk.yellow(`⚠ ${message}`) : `⚠ ${message}`; console.warn(formatted); } info(message) { if (this.quiet) return; console.log(message); } debug(message) { if (!this.verbose || this.quiet) return; const formatted = this.colors ? chalk.gray(`[DEBUG] ${message}`) : `[DEBUG] ${message}`; console.log(formatted); } output(data, title) { if (this.quiet) return; if (title && this.format === 'text') { console.log(chalk.bold(title)); console.log(); } switch (this.format) { case 'json': console.log(JSON.stringify(data, null, 2)); break; case 'yaml': console.log(yaml.dump(data, { lineWidth: -1, noRefs: true })); break; case 'csv': this.outputCsv(data); break; default: this.outputText(data); } } table(data) { if (this.quiet) return; switch (this.format) { case 'json': const jsonData = data.rows.map(row => { const obj = {}; data.columns.forEach((col, index) => { obj[col.header] = row[index] || ''; }); return obj; }); console.log(JSON.stringify(jsonData, null, 2)); break; case 'yaml': const yamlData = data.rows.map(row => { const obj = {}; data.columns.forEach((col, index) => { obj[col.header] = row[index] || ''; }); return obj; }); console.log(yaml.dump(yamlData, { lineWidth: -1, noRefs: true })); break; case 'csv': this.outputCsv([data.columns.map(col => col.header), ...data.rows]); break; default: this.outputTable(data); } } keyValue(data, title) { if (this.quiet) return; if (title && this.format === 'text') { console.log(chalk.bold(title)); console.log(); } switch (this.format) { case 'json': console.log(JSON.stringify(data, null, 2)); break; case 'yaml': console.log(yaml.dump(data, { lineWidth: -1, noRefs: true })); break; case 'csv': const csvRows = Object.entries(data).map(([key, value]) => [key, String(value)]); this.outputCsv([['Key', 'Value'], ...csvRows]); break; default: this.outputKeyValue(data); } } list(items, title) { if (this.quiet) return; if (title && this.format === 'text') { console.log(chalk.bold(title)); console.log(); } switch (this.format) { case 'json': console.log(JSON.stringify(items, null, 2)); break; case 'yaml': console.log(yaml.dump(items, { lineWidth: -1, noRefs: true })); break; case 'csv': items.forEach(item => console.log(item)); break; default: items.forEach(item => console.log(` ${chalk.cyan('•')} ${item}`)); } } status(status, message) { if (this.quiet) return; const symbols = { success: chalk.green('✓'), warning: chalk.yellow('⚠'), error: chalk.red('✗'), info: chalk.blue('ℹ'), }; const colors = { success: chalk.green, warning: chalk.yellow, error: chalk.red, info: chalk.blue, }; if (this.format === 'text') { console.log(`${symbols[status]} ${colors[status](message)}`); } else { const data = { status, message }; this.output(data); } } progress(current, total, message) { if (this.quiet || this.format !== 'text') return; const percentage = Math.round((current / total) * 100); const barLength = 20; const filledLength = Math.round((percentage / 100) * barLength); const bar = '█'.repeat(filledLength) + '░'.repeat(barLength - filledLength); const progressText = `${bar} ${percentage}% (${current}/${total})`; const fullText = message ? `${message} ${progressText}` : progressText; console.log(fullText); } diff(before, after, title) { if (this.quiet) return; if (title && this.format === 'text') { console.log(chalk.bold(title)); console.log(); } if (this.format === 'text') { const beforeLines = before.split('\n'); const afterLines = after.split('\n'); console.log(chalk.red('- Before:')); beforeLines.forEach(line => console.log(chalk.red(` ${line}`))); console.log(chalk.green('+ After:')); afterLines.forEach(line => console.log(chalk.green(` ${line}`))); } else { const data = { before, after }; this.output(data); } } simpleTable(data, columns) { if (this.quiet) return; if (this.format === 'json') { this.json(data); return; } if (this.format === 'yaml') { this.yaml(data); return; } if (data.length === 0) { this.info('No data to display'); return; } const cols = columns || Object.keys(data[0]); const widths = {}; for (const col of cols) { widths[col] = col.length; for (const row of data) { const value = String(row[col] ?? ''); widths[col] = Math.max(widths[col], value.length); } } const header = cols.map(col => col.padEnd(widths[col] || 0)).join(' '); console.log(header); console.log(cols.map(col => '-'.repeat(widths[col] || 0)).join(' ')); for (const row of data) { const line = cols.map(col => String(row[col] ?? '').padEnd(widths[col] || 0)).join(' '); console.log(line); } } json(data) { if (this.quiet) return; console.log(JSON.stringify(data, null, 2)); } yaml(data) { if (this.quiet) return; console.log(yaml.dump(data, { lineWidth: -1, noRefs: true })); } spinner(message) { return new CLISpinner(message, { quiet: this.quiet, colors: this.colors, }); } outputText(data) { if (typeof data === 'string') { console.log(data); } else if (typeof data === 'object' && data !== null) { this.outputKeyValue(data); } else { console.log(String(data)); } } outputKeyValue(data) { Object.entries(data).forEach(([key, value]) => { const formattedKey = chalk.bold(key); if (typeof value === 'object' && value !== null) { console.log(`${formattedKey}:`); if (Array.isArray(value)) { value.forEach(item => console.log(` ${chalk.cyan('•')} ${item}`)); } else { Object.entries(value).forEach(([subKey, subValue]) => { console.log(` ${subKey}: ${subValue}`); }); } } else { console.log(`${formattedKey}: ${value}`); } }); } outputTable(data) { const tableData = [ data.columns.map(col => chalk.bold(col.header)), ...data.rows ]; const config = { border: { topBody: `─`, topJoin: `┬`, topLeft: `┌`, topRight: `┐`, bottomBody: `─`, bottomJoin: `┴`, bottomLeft: `└`, bottomRight: `┘`, bodyLeft: `│`, bodyRight: `│`, bodyJoin: `│`, joinBody: `─`, joinLeft: `├`, joinRight: `┤`, joinJoin: `┼` }, columnDefault: { paddingLeft: 1, paddingRight: 1, }, columns: data.columns.map(col => ({ width: col.width, alignment: col.align || 'left', wrapWord: col.wrap !== false, })), }; console.log(table(tableData, config)); } outputCsv(data) { if (Array.isArray(data) && Array.isArray(data[0])) { data.forEach(row => { console.log(row.map((cell) => `"${String(cell).replace(/"/g, '""')}"`).join(',')); }); } else { console.log(JSON.stringify(data)); } } } class CLISpinner { constructor(message, config) { this.message = message; this.config = config; this.frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; this.currentFrame = 0; this.isSpinning = false; } start() { if (this.config.quiet || this.isSpinning) return; this.isSpinning = true; this.interval = setInterval(() => { const frame = this.frames[this.currentFrame]; const text = this.config.colors ? chalk.cyan(`${frame} ${this.message}`) : `${frame} ${this.message}`; process.stdout.write(`\r${text}`); this.currentFrame = (this.currentFrame + 1) % this.frames.length; }, 80); } stop() { if (this.interval) { clearInterval(this.interval); this.interval = undefined; } if (this.isSpinning) { process.stdout.write('\r' + ' '.repeat(this.message.length + 3) + '\r'); this.isSpinning = false; } } succeed(message) { this.stop(); if (!this.config.quiet) { const text = message || this.message; const formatted = this.config.colors ? chalk.green(`✓ ${text}`) : `✓ ${text}`; console.log(formatted); } } fail(message) { this.stop(); if (!this.config.quiet) { const text = message || this.message; const formatted = this.config.colors ? chalk.red(`✗ ${text}`) : `✗ ${text}`; console.log(formatted); } } update(message) { this.message = message; } } //# sourceMappingURL=output-formatter.js.map