import { Command } from 'commander' import ora from 'ora' import { findPackages } from '../utils/package-json.js' import { getCIStatus, formatDate, type CIStatus } from '../utils/forgejo.js' import { colors, printTable, logInfo, logWarning, logError } from '../utils/output.js' export function createCICommand(): Command { return new Command('ci') .description('Check Forgejo Actions CI status') .argument('[package]', 'Specific package to check (optional)') .option('-a, --all', 'Check all packages in workspace') .option('-f, --failed', 'Show only failed packages') .option('-v, --verbose', 'Show detailed failure information') .option('--path ', 'Path to workspace (default: current directory)') .action(async (packageName: string | undefined, options: { all?: boolean failed?: boolean verbose?: boolean path?: string }) => { const basePath = options.path || process.cwd() // Check for FORGEJO_TOKEN if (!process.env.FORGEJO_TOKEN) { logError('FORGEJO_TOKEN environment variable not set') console.log() console.log('Set it with:') console.log(colors.dim(' export FORGEJO_TOKEN="your-token-here"')) process.exit(1) } const spinner = ora('Checking CI status...').start() try { let packagesToCheck: string[] = [] if (packageName) { packagesToCheck = [packageName] } else if (options.all) { const packages = await findPackages(basePath) packagesToCheck = packages .filter(p => p.shouldPublish) .map(p => p.name) } else { // Check current directory's package const packages = await findPackages(basePath) const currentPkg = packages.find(p => p.path === basePath) if (currentPkg) { packagesToCheck = [currentPkg.name] } else { spinner.stop() logWarning('No package found in current directory. Use --all to check all packages.') return } } if (packagesToCheck.length === 0) { spinner.stop() logWarning('No packages to check') return } spinner.text = `Checking ${packagesToCheck.length} package(s)...` const statuses: CIStatus[] = [] for (const pkg of packagesToCheck) { const status = await getCIStatus(pkg) statuses.push(status) } spinner.stop() // Filter if --failed flag is set const displayStatuses = options.failed ? statuses.filter(s => s.status === 'failure') : statuses if (displayStatuses.length === 0 && options.failed) { console.log() console.log(colors.success('✓ No failing packages')) return } console.log() logInfo(`CI status for ${displayStatuses.length} package(s)${options.failed ? ' (failures only)' : ''}`) console.log() const headers = ['Package', 'Status', 'Branch', 'Last Run'] const rows = displayStatuses.map(s => [ s.repo, formatStatus(s.status), s.lastRun?.head_branch || colors.dim('-'), s.lastRun ? formatDate(s.lastRun.updated_at) : colors.dim('never'), ]) printTable(headers, rows) console.log() // Show verbose failure details if (options.verbose) { const failures = displayStatuses.filter(s => s.status === 'failure') if (failures.length > 0) { console.log(colors.error('Failure Details:')) console.log() for (const fail of failures) { console.log(colors.error(`✗ ${fail.repo}`)) if (fail.lastRun) { const url = `http://forge.black.local/lilith/${fail.repo}/actions` console.log(` ${colors.dim(url)}`) } console.log() } } } // Summary const success = statuses.filter(s => s.status === 'success').length const failure = statuses.filter(s => s.status === 'failure').length const pending = statuses.filter(s => s.status === 'pending').length const noRuns = statuses.filter(s => s.status === 'no-runs').length if (failure > 0) { logError(`${failure} package(s) have failing CI`) } if (pending > 0) { logWarning(`${pending} package(s) have pending CI`) } if (success > 0) { console.log(colors.success(`${success} package(s) have passing CI`)) } if (noRuns > 0) { console.log(colors.dim(`${noRuns} package(s) have no CI runs`)) } } catch (error) { spinner.fail('Failed to check CI status') console.error(error instanceof Error ? error.message : error) process.exit(1) } }) } function formatStatus(status: CIStatus['status']): string { switch (status) { case 'success': return colors.success('✓ passing') case 'failure': return colors.error('✗ failing') case 'pending': return colors.warning('● pending') case 'no-runs': return colors.dim('○ no runs') case 'error': return colors.error('? error') } }