#!/usr/bin/env node /** * cli:uat-provision — Tenant + one user per role for UAT, via the app's admin API. * * Pipeline: validate spec (plan roles + API URL + initial admin) → idempotent * provisioning (probe → admin login → ensure tenant → ensure users → login-verify * each) → persist `uat-users.json` (gitignored). `--dry_run` prints the planned * actions without touching the API or the filesystem. */ import { parseArgs } from 'node:util'; import { join } from 'node:path'; import { executeEnvelope, failExecute, printEnvelope } from '../../../lib/output.js'; import { loadUsersFile, usersFileRelPath } from '../lib/users-file.js'; import { emailForRole } from './plan-actions.js'; import { validate } from './validate.js'; import { executeProvision } from './execute.js'; const COMMAND = 'uat-provision'; 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 v = await validate(raw); if (!v.valid || !v.context) { printEnvelope(failExecute(COMMAND, v.errors)); process.exit(1); return; } const ctx = v.context; if (values.dry_run || ctx.spec.dryRun) { const existing = loadUsersFile(join(ctx.projectRoot, usersFileRelPath(ctx.application))); const have = new Set((existing?.users ?? []).map((u) => u.role)); printEnvelope( executeEnvelope(COMMAND, { data: { dryRun: true, apiUrl: ctx.apiUrl, apiUrlSource: ctx.apiUrlSource, adminEmail: ctx.adminEmail, adminSource: ctx.adminSource, tenant: { name: ctx.spec.tenantName, slug: ctx.spec.tenantSlug }, plan: ctx.planRelPath, actions: ctx.roles.map((role) => ({ role, email: have.has(role) ? existing?.users.find((u) => u.role === role)?.email : emailForRole(role, ctx.spec.emailDomain), action: have.has(role) ? 'verify' : 'create', })), }, warnings: v.warnings, }), ); process.exit(0); } const outcome = await executeProvision(ctx); printEnvelope( executeEnvelope(COMMAND, { success: outcome.success, data: { apiUrl: ctx.apiUrl, application: ctx.application, tenant: outcome.report.tenant, usersFile: outcome.report.usersFile, provisioned: outcome.report.users.filter((u) => u.status !== 'failed').length, failed: outcome.report.users.filter((u) => u.status === 'failed').length, }, report: outcome.report as unknown as Record, errors: outcome.errors, warnings: [...v.warnings, ...outcome.warnings], nextSteps: outcome.success ? [ `Credentials written to ${outcome.report.usersFile} (gitignored).`, 'Run the axes: `/uat api` then `/uat ui`, or everything at once with `/uat run`.', ] : ['Fix the reported role failures, then re-run /uat provision (idempotent).'], }), ); process.exit(outcome.success ? 0 : 1); } void main();