@xec-sh/cli
Version:
Xec: The Universal Shell for TypeScript
387 lines • 12.5 kB
JavaScript
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