#!/usr/bin/env node /** * scaffold-screen-controller — index.ts * * Reads pagespecs/..md filtered by (section, entity), generates * the {EntityPlural}ScreenController.cs + one {Entity}{View}ScreenDto per * read screen, and writes them under spec.projectPath. Returns a JSON * envelope on stdout (success | error | files generated count | todos). * * Exit code 0 on success, 1 on filesystem/parse failure, 2 on invalid spec. */ import { parseArgsOrFail as parseArgs } from '../../../../../lib/cli-args.js' import { generate, legacyPaths } from './generate.js' import { guardedRm } from '../../../../../lib/guarded-rm.js' import { writeGeneratedFiles } from '../../../../../lib/generated-writes.js' import { parsePagespecsForSection } from './parse-pagespec.js' import type { Result } from './types.js' import { validate } from './validate.js' async function main(): Promise { const args = parseArgs({ options: { spec: { type: 'string' }, json: { type: 'boolean', default: false }, }, allowPositionals: false, }) if (!args.values.spec) { emit(fail(['--spec is required'])) process.exit(2) } let raw: unknown try { raw = JSON.parse(args.values.spec) } catch { emit(fail(['--spec must be valid JSON'])) process.exit(2) } const v = validate(raw) if (!v.valid) { emit(fail(v.errors)) process.exit(2) } const spec = v.data! // Parse pagespecs filtered to (section, entity). const { pagespecs, warnings } = parsePagespecsForSection(spec.moduleDir, spec.section, spec.entity) if (pagespecs.length === 0) { emit(fail([`No pagespecs found in ${spec.moduleDir}/pagespecs/ matching section="${spec.section}" entity="${spec.entity}"`], warnings.map(w => `${w.file}: ${w.reason}`))) process.exit(1) } // Generate + write const out = generate(spec, pagespecs) // Idempotent relocate: delete the pre-classification copies (per-module Screens/ // bucket) before writing the new //
/ files. // Guarded sweep (lib/guarded-rm): an @customised file at a legacy path is preserved. // The sweep's result rides the payload — `filesRemoved` for the deletions, one warning per // preserved legacy file — since lot 11 (A-WARN-12, 2026-09-24); nothing goes to stderr. const legacySweep = guardedRm(legacyPaths(spec, out.files), { outdir: spec.projectPath }) // Guarded write (lib/generated-writes): an `@customised` controller/DTO is skipped, never overwritten. const writes = writeGeneratedFiles(out.files, { outdir: spec.projectPath }) const sweepWarnings = [...legacySweep.preserved.map((x) => `legacy path ${x} kept: marked @customised`), ...writes.skipped.map((x) => `${x} kept: marked @customised — not overwritten`)] emit({ success: true, errors: [], filesGenerated: writes.written.length, files: out.files, todos: out.todos, filesRemoved: legacySweep.removed, skipped: writes.skipped, warnings: [...sweepWarnings, ...warnings.map(w => `${w.file}: ${w.reason}`)], }) process.exit(0) } /** * JSON on stdout, ALWAYS — the Phase 2b gate reads `todos[]` / `success` from it. * The human summary used to be the default and a failure left stdout EMPTY unless * --json (audit 2026-09-24, A-WARN-01); the flag is still accepted, without effect. */ function emit(payload: Result): void { console.log(JSON.stringify(payload, null, 2)) } function fail(errors: string[], warnings: string[] = []): Result { return { success: false, errors, filesGenerated: 0, files: [], todos: [], filesRemoved: [], skipped: [], warnings } } main().catch(err => { emit(fail([`Unexpected error: ${(err as Error).message}`])) process.exit(2) })