#!/usr/bin/env node /** * cli:uat-run — One command, the whole UAT: plan (drift-gated) → app health * (boot via `ss dev up` when allowed) → provision (tenant + 1 user/role) → * API axis → UI axis (real clicks) → single-file HTML report. Both axes share * one run id; a failed phase skips its dependents but the report still renders * the artifacts that exist. */ import { parseArgs } from 'node:util'; import { existsSync, } from 'node:fs'; import { join, resolve } from 'node:path'; import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js'; import { defaultPlanLocation } from '../lib/plan-io.js'; import { usersFileRelPath } from '../lib/users-file.js'; import { UatRunInputSchema } from './types.js'; import { executeRun } from './execute.js'; const COMMAND = 'uat-run'; async function main(): Promise { let values: { spec?: string; dry_run?: boolean }; try { values = parseArgs({ options: { spec: { type: 'string' }, dry_run: { type: 'boolean', default: false }, }, strict: true, }).values; } catch (e) { printEnvelope(failExecute(COMMAND, [`Invalid arguments: ${(e as Error).message}`])); process.exit(1); return; } if (!values.spec) { printEnvelope(failExecute(COMMAND, ['--spec is required'])); process.exit(1); } let raw: unknown; try { raw = JSON.parse(values.spec); } catch { printEnvelope(failExecute(COMMAND, ['Invalid JSON in --spec'])); process.exit(1); } const parsed = UatRunInputSchema.safeParse(raw); if (!parsed.success) { printEnvelope(failExecute(COMMAND, parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`))); process.exit(1); return; } const spec = parsed.data; if (values.dry_run || spec.dryRun) { const projectRoot = resolve(spec.projectPath); const location = defaultPlanLocation(projectRoot, spec.path, spec.name); const usersFile = usersFileRelPath(location.application); printEnvelope( executeEnvelope(COMMAND, { data: { dryRun: true, plan: { path: location.relPath, exists: existsSync(location.absPath), refreshPlan: spec.refreshPlan }, usersFile: { path: usersFile, exists: existsSync(join(projectRoot, usersFile)) }, phases: [ `health (bootIfDown=${spec.bootIfDown})`, 'plan (generate/drift-check)', spec.skipProvision ? 'provision SKIPPED' : 'provision', spec.skipApi ? 'api SKIPPED' : 'api', spec.skipUi ? 'ui SKIPPED' : `ui (headless=${spec.headless}, writes=${spec.writes}, screenshots=${spec.screenshots})`, spec.skipReport ? 'report SKIPPED' : 'report', ], }, }), ); process.exit(0); } const outcome = await executeRun(spec); printEnvelope( executeEnvelope(COMMAND, { success: outcome.success, data: outcome.data, report: { phases: outcome.phases } as unknown as Record, errors: outcome.errors, warnings: outcome.warnings, nextSteps: [ ...(outcome.data.report ? [`Open the report: ${outcome.data.report as string}`] : []), ...(outcome.success ? outcome.data.apiAllPassed === false || outcome.data.uiAllPassed === false ? ['Assertion failures are detailed in the report — triage them with the team.'] : ['Run green across both axes.'] : ['Fix the failed phase(s) above, then re-run /uat run (idempotent).']), ], }), ); process.exit(outcome.success ? 0 : 1); } void main();