Files
boocode/packages/ion/src/cli/utils.ts
indifferentketchup 02063072ab chore: add ion package, codesight wiki, work plans, ascli config
New @boocode/ion package (v0.0.1) for inference optimization network.
.codesight/ wiki artifacts for codebase documentation.
.omo/ work plans for openspec cleanup and enhanced file panel.
2026-06-07 22:16:45 +00:00

239 lines
6.7 KiB
TypeScript

/**
* CLI utility functions for the Ion workflow engine.
*
* Provides formatting, table rendering, and JSON output helpers
* used across all CLI commands.
*/
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface CliOptions {
/** Working directory override. */
cwd?: string;
/** Output as JSON (suppresses all other output). */
json?: boolean;
/** Path to the workflow store database. */
store?: string;
/** Path to the database file. */
dbPath?: string;
/** Output file path (for convert command). */
output?: string;
}
// ---------------------------------------------------------------------------
// Duration formatting
// ---------------------------------------------------------------------------
/**
* Format a duration in milliseconds into a human-readable string.
*
* @example
* formatDuration(90500) // "1m 30s"
* formatDuration(3661000) // "1h 1m"
* formatDuration(500) // "0s"
*/
export function formatDuration(ms: number): string {
if (ms < 0) ms = 0;
const seconds = Math.floor(ms / 1000) % 60;
const minutes = Math.floor(ms / 60000) % 60;
const hours = Math.floor(ms / 3600000);
if (hours > 0) {
return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
}
if (minutes > 0) {
return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`;
}
return `${seconds}s`;
}
// ---------------------------------------------------------------------------
// Timestamp formatting
// ---------------------------------------------------------------------------
/**
* Format a Date into an ISO-like timestamp suitable for CLI display.
*
* @example
* formatTimestamp(new Date('2025-06-07T14:30:00Z'))
* // "2025-06-07 14:30:00"
*/
export function formatTimestamp(date: Date): string {
const y = date.getFullYear();
const mo = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
const h = String(date.getHours()).padStart(2, '0');
const mi = String(date.getMinutes()).padStart(2, '0');
const s = String(date.getSeconds()).padStart(2, '0');
return `${y}-${mo}-${d} ${h}:${mi}:${s}`;
}
// ---------------------------------------------------------------------------
// String truncation
// ---------------------------------------------------------------------------
/**
* Truncate a string to `max` characters, appending an ellipsis if truncated.
*
* @example
* truncate('hello world', 8) // "hello..."
* truncate('hi', 8) // "hi"
*/
export function truncate(str: string, max: number): string {
if (str.length <= max) return str;
if (max <= 3) return str.slice(0, max);
return str.slice(0, max - 3) + '...';
}
// ---------------------------------------------------------------------------
// Table rendering
// ---------------------------------------------------------------------------
export interface TableColumn {
/** Column header label. */
header: string;
/** Minimum column width. */
minWidth?: number;
/** Field name to extract from each row object. */
field: string;
}
/**
* Print a formatted table to stdout.
*
* @param rows - Array of row objects.
* @param columns - Column definitions with header labels and field names.
*
* @example
* printTable(
* [{ name: 'deploy', desc: 'Deploy app' }],
* [{ header: 'Name', field: 'name' }, { header: 'Description', field: 'desc' }],
* )
*/
export function printTable(
rows: Record<string, unknown>[],
columns: TableColumn[],
): void {
if (rows.length === 0) {
console.log('(no results)');
return;
}
// Compute column widths.
const widths: number[] = columns.map((col) => {
const headerLen = col.header.length;
const dataLen = Math.max(
...rows.map((row) => {
const val = row[col.field];
const str = val === undefined || val === null ? '' : String(val);
return str.length;
}),
0,
);
const min = col.minWidth ?? 0;
return Math.max(headerLen, dataLen, min);
});
// Header row.
const headerLine = columns
.map((col, i) => col.header.padEnd(widths[i]!))
.join(' ');
console.log(headerLine);
// Separator.
const sepLine = widths.map((w) => '-'.repeat(w)).join(' ');
console.log(sepLine);
// Data rows.
for (const row of rows) {
const line = columns
.map((col, i) => {
const val = row[col.field];
const str = val === undefined || val === null ? '' : String(val);
return str.padEnd(widths[i]!);
})
.join(' ');
console.log(line);
}
}
// ---------------------------------------------------------------------------
// JSON output
// ---------------------------------------------------------------------------
/**
* Print a data structure as formatted JSON to stdout.
* Uses 2-space indentation.
*/
export function printJson(data: unknown): void {
console.log(JSON.stringify(data, null, 2));
}
// ---------------------------------------------------------------------------
// Argument parsing
// ---------------------------------------------------------------------------
/**
* Parse CLI arguments into positional args and named options.
*
* Supports `--flag` (boolean) and `--key value` (string) formats.
* Everything after `--` is treated as positional.
*
* @example
* parseArgs(['run', 'deploy', '--json', '--cwd', '/tmp'])
* // { args: ['run', 'deploy'], options: { json: true, cwd: '/tmp' } }
*/
export function parseArgs(argv: string[]): {
args: string[];
options: Record<string, string | boolean>;
} {
const args: string[] = [];
const options: Record<string, string | boolean> = {};
let i = 0;
while (i < argv.length) {
const token = argv[i]!;
if (token === '--') {
args.push(...argv.slice(i + 1));
break;
}
if (token.startsWith('--')) {
const key = token.slice(2);
const next = argv[i + 1];
if (next && !next.startsWith('--')) {
options[key] = next;
i += 2;
} else {
options[key] = true;
i += 1;
}
} else {
args.push(token);
i += 1;
}
}
return { args, options };
}
/**
* Build a CliOptions object from parsed options.
* Extracts known CLI flags into their typed fields.
*/
export function buildCliOptions(
options: Record<string, string | boolean>,
): CliOptions {
return {
cwd: typeof options.cwd === 'string' ? options.cwd : undefined,
json: options.json === true,
store: typeof options.store === 'string' ? options.store : undefined,
dbPath: typeof options['db-path'] === 'string' ? options['db-path'] : undefined,
output: typeof options.output === 'string' ? options.output : undefined,
};
}