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.
239 lines
6.7 KiB
TypeScript
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,
|
|
};
|
|
} |