#!/usr/bin/env tsx /** * cli:run-smoke — index.ts * * Entry point for the runtime smoke test. Parses CLI args, runs the * smoke, prints the standard executeEnvelope on stdout, exits with * a non-zero status when probes fail. */ import { mkdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; import { executeEnvelope, failExecute, printEnvelope } from '../../../../lib/output.js'; import { RunSmokeArgsSchema, type SmokeReport } from './types.js'; import { runSmoke, renderSmokeMarkdown } from './execute.js'; function parseArgs(argv: string[]): Record { const out: Record = {}; for (let i = 0; i < argv.length; i++) { const a = argv[i]; if (!a.startsWith('--')) continue; const key = a.slice(2); const next = argv[i + 1]; if (next === undefined || next.startsWith('--')) { out[key] = true; } else { const num = Number(next); out[key] = Number.isFinite(num) && next.match(/^\d/) ? num : next; i++; } } return out; } function camelize(key: string): string { return key.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); } async function main(): Promise { const raw = parseArgs(process.argv.slice(2)); // The CLI uses kebab-case flags ; the Zod schema uses camelCase. Map. const camel: Record = {}; for (const [k, v] of Object.entries(raw)) { camel[camelize(k)] = v; } const parsed = RunSmokeArgsSchema.safeParse(camel); if (!parsed.success) { printEnvelope( failExecute('run-smoke', [ 'Invalid CLI arguments :', ...parsed.error.issues.map((i) => ` - ${i.path.join('.')}: ${i.message}`), ]), ); process.exit(2); return; } const report = await runSmoke(parsed.data); // Write the on-disk report. Its PRESENCE is what makes "did the gate run?" // verifiable — a missing `_audit/smoke-.md` is itself a Phase 3e // failure (gates.md), so never skip this on a real (non-dryRun) run. const reportPaths: string[] = []; if (!parsed.data.dryRun) { try { const auditDir = path.resolve(parsed.data.projectPath, '_audit'); mkdirSync(auditDir, { recursive: true }); const reportPath = path.join(auditDir, `smoke-${parsed.data.moduleCode}.md`); writeFileSync(reportPath, renderSmokeMarkdown(report, parsed.data.moduleCode), 'utf8'); reportPaths.push(reportPath); } catch { /* a write failure must not mask the probe result — reported via warnings */ } } const errors: string[] = [ ...report.failures.map( (f) => `HTTP ${f.verb} ${f.url} → ${f.status ?? 'no-response'} (${f.failureReason ?? 'unknown'})${f.expectedBy ? ` (declared by ${f.expectedBy})` : ''}`, ), ...report.browser.failures.map( (p) => `BROWSER ${p.url} → ${p.accessState}${p.viteOverlay ? ' (vite-error-overlay)' : ''}${p.consoleErrors[0] ? ` — ${p.consoleErrors[0]}` : ''}${p.failedRequests[0] ? ` — ${p.failedRequests[0].status} ${p.failedRequests[0].url}` : ''}`, ), ...(report.browser.ran && !report.browser.available ? [`BROWSER unavailable (smoke.browser-unavailable): ${report.browser.reason ?? ''}`] : []), ...report.navMenu.mismatches.map((m) => `NAV-MENU ${m}`), ...report.interaction.failures.map( (a) => `ACTION ${a.httpMethod} ${a.url} (${a.entity}.${a.code}, ${a.scope}) → ${a.status ?? 'no-response'} (${a.reason ?? 'unknown'})`, ), ]; const envelope = executeEnvelope('run-smoke', { success: report.passed, report, errors, warnings: [ ...(reportPaths.length === 0 && !parsed.data.dryRun ? ['Could not write the _audit/smoke report to disk.'] : []), ...(report.navMenu.skippedReason ? [`nav-menu check skipped: ${report.navMenu.skippedReason}`] : []), ...(report.interaction.ran && !report.interaction.authenticated && report.interaction.reason ? [`action axis not authenticated (smoke.interaction-unavailable): ${report.interaction.reason}`] : []), ], nextSteps: report.passed ? [] : [ 'Re-run the relevant scaffolder for each failing HTTP probe (cf. report.failures[].expectedBy).', 'A failed browser page = a client crash (console/pageerror) or a CSS/PostCSS 500 (vite-error-overlay / failed module) — fix the emitting scaffolder (scaffold-theme for CSS, scaffold-routes/core-seed for a nav route crash).', 'BROWSER unavailable → install Playwright + Chromium in the web app, then re-run (do not treat as a pass).', ], }); printEnvelope(envelope); process.exit(report.passed ? 0 : 1); } main().catch((err: unknown) => { const message = err instanceof Error ? err.message : String(err); printEnvelope(failExecute('run-smoke', [`Uncaught error : ${message}`])); process.exit(2); });