/** * ───────────────────────────────────────────── * SPB Git — Personal Git Platform * ───────────────────────────────────────────── * Author : Simon-Pierre Boucher * Contact : contact@spboucher.ai * File : cli/lib/ui.mjs * Purpose : CLI output helpers — aligned tables, prompts, concurrency * License : MIT © Simon-Pierre Boucher * ───────────────────────────────────────────── */ import { createInterface } from 'node:readline/promises'; import pc from 'picocolors'; /** * Print an aligned table. * @param {string[][]} rows already-colored cells allowed (width uses raw length) * @param {{pad?: number}} [opts] */ export function printTable(rows, opts = {}) { if (rows.length === 0) return; const pad = opts.pad ?? 2; // eslint-disable-next-line no-control-regex const visible = (s) => String(s).replace(/\u001b\[[0-9;]*m/g, ''); const widths = []; for (const row of rows) { row.forEach((cell, i) => { widths[i] = Math.max(widths[i] ?? 0, visible(cell).length); }); } for (const row of rows) { const line = row .map((cell, i) => cell + ' '.repeat(widths[i] - visible(cell).length + (i < row.length - 1 ? pad : 0))) .join(''); console.log(line.trimEnd()); } } /** * Ask one interactive question. * @param {string} question * @param {string} [fallback] * @returns {Promise} */ export async function prompt(question, fallback = '') { const rl = createInterface({ input: process.stdin, output: process.stdout }); const suffix = fallback ? pc.dim(` (${fallback})`) : ''; const answer = (await rl.question(`${question}${suffix}: `)).trim(); rl.close(); return answer || fallback; } /** * Run at most `limit` async jobs concurrently, preserving order. * @template T,R * @param {T[]} items * @param {number} limit * @param {(item: T, index: number) => Promise} worker * @returns {Promise} */ export async function mapLimit(items, limit, worker) { const results = new Array(items.length); let next = 0; const runners = Array.from({ length: Math.min(limit, items.length) }, async () => { while (next < items.length) { const index = next; next += 1; results[index] = await worker(items[index], index); } }); await Promise.all(runners); return results; } /** Symbols shared across commands. */ export const SYM = Object.freeze({ ok: pc.green('✓'), push: pc.cyan('↑'), pull: pc.yellow('↓'), fail: pc.red('✗'), skip: pc.dim('·'), });