#!/usr/bin/env node /** * cli:scaffold-seed * Responsabilité : Generate IClientSeedDataProvider for a module * Appelé par Claude Code via skill:backend-seed-data * * Usage : * npx --prefer-offline tsx skills/development/backend/seed-data/cli/scaffold-seed/index.ts \ * --spec '{"module":"hrm","appCode":"MyApp","navigation":[...],"projectPath":"/path"}' */ import { parseArgs } from 'node:util' import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, resolve, join } from 'node:path' import { validate } from './validate.js' import { generate, seedDiPatches } from './generate.js' import { ScaffoldSeedInputSchema } from './types.js' import { generateEnvelope, failGenerate, printEnvelope } from '../../../../../lib/output.js' import { mergeLineIntoMarkerBlock } from '../../../../../lib/di-markers.js' const COMMAND = 'scaffold-seed' function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, outdir: { type: 'string' }, dry_run: { type: 'boolean', default: false }, }, strict: true, }) if (!values.spec) { printEnvelope(failGenerate(COMMAND, ['--spec is required'])) process.exit(1) } let raw: unknown try { raw = JSON.parse(values.spec) } catch { printEnvelope(failGenerate(COMMAND, ['Invalid JSON in --spec'])) process.exit(1) } const validation = validate(raw) if (!validation.valid) { printEnvelope(failGenerate(COMMAND, validation.errors)) process.exit(1) } // Parse through the schema so every default (referenceData, order, …) is // applied — the raw cast used to hand generate() a shape without defaults. const spec = ScaffoldSeedInputSchema.parse(raw) const files = generate(spec) if (values.dry_run) { printEnvelope(generateEnvelope(COMMAND, { data: { dryRun: true, files: files.map(f => f.path) }, warnings: validation.warnings })) process.exit(0) } const outdir = values.outdir ?? spec.projectPath const written: string[] = [] for (const file of files) { const fullPath = resolve(join(outdir, file.path)) mkdirSync(dirname(fullPath), { recursive: true }) writeFileSync(fullPath, file.content, 'utf-8') written.push(fullPath) } // ── DI registrations — LANDED, never merely suggested. An unregistered // provider is inert while DEV-API-030 counts the entity as populatable (it sees // the file's Set()) — the "table empty forever, every audit green" shape // §25 exists to close. Idempotent via lib/di-markers. const warnings = [...validation.warnings] const filesModified: string[] = [] for (const patch of seedDiPatches(spec)) { const host = patch.hostCandidates.map(c => resolve(join(outdir, c))).find(p => existsSync(p)) if (!host) { warnings.push( `No DI host found (${patch.hostCandidates.join(' / ')}) — the registration below is NOT landed, so the seed will never run:\n${patch.line}`, ) continue } const r = mergeLineIntoMarkerBlock(readFileSync(host, 'utf-8'), patch.begin, patch.end, patch.line) if (r.status === 'patched') { writeFileSync(host, r.source, 'utf-8') if (!filesModified.includes(host)) filesModified.push(host) } else if (r.status === 'failed') { warnings.push(`DI host ${host} has no insertion point (no 'return services;') — add manually:\n${patch.line}`) } } printEnvelope(generateEnvelope(COMMAND, { data: { module: spec.module, fileCount: written.length }, filesCreated: written, filesModified, warnings, nextSteps: [ 'Seed providers are DI-registered automatically (SEED-PROVIDERS-DI marker block) — check the warnings if a DI host was not found.', ...(spec.referenceData.length > 0 ? ['The reference-data provider resolves IExtensionsDbContext through its constructor — no extra wiring.'] : []), ], })) } main()