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.
This commit is contained in:
239
packages/ion/src/cli/utils.ts
Normal file
239
packages/ion/src/cli/utils.ts
Normal file
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user