#!/usr/bin/env node import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { dateRange, readCommits, readIssues, readSessions, mergeSources, summarize, formatTable, SourceError } from './ledger.mjs'; import { readT3, defaultT3Path } from './t3.mjs'; import { readQueue, readSeatsDir, issueStates, queueChecks, formatQueue, checkSeatName } from './queue-checks.mjs'; const usage = 'Usage: node packages/ledger/src/cli.mjs --since YYYY-MM-DD [--until YYYY-MM-DD] [--json] [--no-issues] [--no-t3 | --t3-db PATH] [--no-queue | --unsupported-runtime SEAT ...]'; export async function main(args, root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../..')) { let since, until, t3Db, json = false, noIssues = false, noT3 = false, noQueue = false; const exempt = new Set(); const seen = new Set(); for (let i = 0; i < args.length; i++) { const flag = args[i]; // A runtime is declared unsupported once per seat, so only that flag repeats. if (flag === '--unsupported-runtime') { const seat = checkSeatName(args[++i]); if (exempt.has(seat)) throw new SourceError(`Duplicate --unsupported-runtime ${seat}`); exempt.add(seat); continue; } if (seen.has(flag)) throw new SourceError(`Duplicate option: ${flag}`); seen.add(flag); if (flag === '--help') { console.log(usage); return; } if (flag === '--json') json = true; else if (flag === '--no-issues') noIssues = true; else if (flag === '--no-t3') noT3 = true; else if (flag === '--no-queue') noQueue = true; else if (flag === '--t3-db') { t3Db = args[++i]; if (!t3Db || t3Db.startsWith('--')) throw new SourceError('--t3-db requires a path'); } else if (flag === '--since' || flag === '--until') { const value = args[++i]; if (!value || value.startsWith('--')) throw new SourceError(`${flag} requires a date`); if (flag === '--since') since = value; else until = value; } else throw new SourceError('Unknown option; ' + usage); } if (!since) throw new SourceError(usage); if (noT3 && t3Db !== undefined) throw new SourceError('--no-t3 and --t3-db cannot be combined'); if (noQueue && exempt.size) throw new SourceError('--no-queue and --unsupported-runtime cannot be combined'); const range = dateRange(since, until); // The queue loads before any Gitea call, so a bad queue.json costs none. const queue = noQueue ? null : readQueue(root); const commits = readCommits(root, range); // Fixture tools may be placed first on PATH. The repository client is the // default without requiring installation or reading auth material here. const priorPath = process.env.PATH; process.env.PATH = `${priorPath ?? ''}${path.delimiter}${path.join(root, 'scripts')}`; let issues, states = null; try { issues = noIssues ? null : readIssues(root, range); if (queue && !noIssues) states = issueStates(root, queue.rows, issues); } finally { if (priorPath === undefined) delete process.env.PATH; else process.env.PATH = priorPath; } // T3 is on by default. A missing or unreadable database refuses the report. const t3 = noT3 ? null : await readT3(root, range, t3Db === undefined ? { dbPath: defaultT3Path(), isDefault: true } : { dbPath: t3Db, isDefault: false }); const sessions = mergeSources(await readSessions(root, range), t3); const report = summarize(range, commits, issues, sessions); report.queue = queue ? queueChecks(queue, { issues: states, seats: readSeatsDir(), exempt, range }) : { checked: false }; // The queue section prints above the weekly table. console.log(json ? JSON.stringify(report, null, 2) : [...formatQueue(report.queue), formatTable(report)].join('\n')); return report; } if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { main(process.argv.slice(2)).catch(error => { console.error(error instanceof SourceError ? error.message : 'Ledger failed: cannot read source evidence'); process.exitCode = error.exitCode ?? 1; }); }