/** * `celilo module check ` — drift detection for third-party modules. * * Runs every checker in services/module-validator against the module * at (defaults to ".") and reports a human-friendly summary or * a structured JSON payload. * * Flags: * --no-build Skip `bunx tsc --noEmit`. Useful for fast iteration * or when the module has no TypeScript surface. * --json Emit the structured Check[] payload instead of the * formatted text. Good for CI. * --strict Treat warnings as failures (any non-OK is non-zero). * * Exit codes: * - all checks pass (or only warns) → success * - one or more fails → CommandError (CLI exits 1) * - --strict turns warns into fails too * * Pure filesystem + manifest + npm. No DB writes. */ import { resolve } from 'node:path'; import { type Check, runChecks } from '../../services/module-validator'; import { hasFlag } from '../parser'; import type { CommandResult } from '../types'; interface CheckOptions { noBuild: boolean; json: boolean; strict: boolean; } function parseOptions(flags: Record): CheckOptions { return { noBuild: hasFlag(flags, 'no-build'), json: hasFlag(flags, 'json'), strict: hasFlag(flags, 'strict'), }; } function summarize(checks: Check[]): { ok: number; warn: number; fail: number } { const summary = { ok: 0, warn: 0, fail: 0 }; for (const c of checks) summary[c.status]++; return summary; } function statusIcon(status: Check['status']): string { switch (status) { case 'ok': return '✓'; case 'warn': return '!'; case 'fail': return '✗'; } } function formatTextReport(modulePath: string, checks: Check[]): string { const lines: string[] = [`Module check: ${modulePath}`, '']; const byCategory = new Map(); for (const check of checks) { const list = byCategory.get(check.category) ?? []; list.push(check); byCategory.set(check.category, list); } const order: Check['category'][] = [ 'manifest_schema', 'contract_version', 'capability', 'workspace_dep', 'git_hygiene', 'typescript_build', ]; for (const category of order) { const items = byCategory.get(category); if (!items || items.length === 0) continue; lines.push(formatCategoryHeader(category)); for (const c of items) { lines.push(` ${statusIcon(c.status)} ${c.name}`); lines.push(` ${c.message}`); if (c.suggestedValue && c.status !== 'ok') { lines.push(` suggest: ${c.suggestedValue}`); } if (c.migrationUrl) { lines.push(` migration: ${c.migrationUrl}`); } } lines.push(''); } const summary = summarize(checks); lines.push(`Summary: ${summary.ok} ok, ${summary.warn} warn, ${summary.fail} fail`); return lines.join('\n'); } function formatCategoryHeader(category: Check['category']): string { switch (category) { case 'manifest_schema': return 'Manifest schema:'; case 'contract_version': return 'Contract version:'; case 'capability': return 'Capability versions:'; case 'workspace_dep': return 'Workspace deps (@celilo/*):'; case 'git_hygiene': return 'Publish readiness (git):'; case 'typescript_build': return 'TypeScript build:'; } } function formatJsonReport(modulePath: string, checks: Check[]): string { return JSON.stringify( { module: { path: modulePath }, checks, summary: summarize(checks), }, null, 2, ); } export async function handleModuleCheck( args: string[], flags: Record, ): Promise { const options = parseOptions(flags); const modulePath = resolve(args[0] ?? '.'); const checks = await runChecks(modulePath, { noBuild: options.noBuild }); const summary = summarize(checks); const message = options.json ? formatJsonReport(modulePath, checks) : formatTextReport(modulePath, checks); const hasFails = summary.fail > 0; const hasWarns = summary.warn > 0; const failed = hasFails || (options.strict && hasWarns); if (failed) { return { success: false, error: message, }; } return { success: true, message, rawOutput: options.json, data: { checks, summary }, }; }