#!/usr/bin/env node /** * cli:scaffold-coded-entity * * Emits the DESCRIPTOR half of the coded-entities seam: one * `{Entity}CodeKeyDescriptor.cs` per entity + the * `services.AddSmartStackCodeKey<…>()` DI registration between the * `<<< CODED-ENTITY-KEYS-DI >>>` markers. Pair with `scaffold-entity`'s * `codedEntity` input (the ENTITY half: Code column + ICodedEntity). * Idempotent (full-spec marker replacement). * * Usage: * npx --prefer-offline tsx .../scaffold-coded-entity/index.ts --spec-file spec.json --outdir * npx --prefer-offline tsx .../scaffold-coded-entity/index.ts --spec '' [--dry-run] */ import { parseArgs } from 'node:util'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { validate } from './validate.js'; import { patchDi, renderDescriptors } from './generate.js'; import { generateEnvelope, failGenerate, printEnvelope } from '../../../../../lib/output.js'; import type { CodedEntitySpec } from './types.js'; const COMMAND = 'scaffold-coded-entity'; /** Locate the client Infrastructure DI file (same candidates as the sibling seam CLIs). */ function resolveDiHost(outdir: string, appCode: string): string | null { const candidates = [ `src/${appCode}.Infrastructure/DependencyInjection.cs`, `src/${appCode}.Infrastructure/ServiceCollectionExtensions.cs`, `src/${appCode}.Infrastructure/InfrastructureModule.cs`, ]; for (const candidate of candidates) { const fullPath = resolve(join(outdir, candidate)); if (existsSync(fullPath)) return fullPath; } return null; } function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' }, outdir: { type: 'string' }, 'dry-run': { type: 'boolean', default: false }, }, strict: true, }); const rawJson = values['spec-file'] ? safeRead(values['spec-file'] as string) : ((values.spec as string) ?? null); if (rawJson === null) { printEnvelope(failGenerate(COMMAND, ['Either --spec or --spec-file is required'])); process.exit(1); } let raw: unknown; try { raw = JSON.parse(rawJson); } catch { printEnvelope(failGenerate(COMMAND, ['Invalid JSON in spec'])); process.exit(1); } const validation = validate(raw); if (!validation.valid || !validation.data) { printEnvelope(failGenerate(COMMAND, validation.errors)); process.exit(1); } const spec: CodedEntitySpec = validation.data; const outdir = (values.outdir as string) ?? spec.projectPath; const dryRun = !!values['dry-run']; const warnings: string[] = [...validation.warnings]; const descriptors = renderDescriptors(spec); const diHostPath = resolveDiHost(outdir, spec.appCode); if (dryRun) { printEnvelope(generateEnvelope(COMMAND, { data: { dryRun: true, descriptors: descriptors.map(d => d.path), diHostPath, keys: spec.entities.map(e => e.codeKey) }, warnings, nextSteps: [`Would register ${spec.entities.length} code key${spec.entities.length === 1 ? '' : 's'}: ${spec.entities.map(e => e.codeKey).join(', ')}.`], })); return; } const filesCreated: string[] = []; for (const file of descriptors) { const p = resolve(join(outdir, file.path)); mkdirSync(dirname(p), { recursive: true }); writeFileSync(p, file.content, 'utf-8'); filesCreated.push(p); } const filesModified: string[] = []; if (diHostPath !== null) { const source = readFileSync(diHostPath, 'utf-8'); const next = patchDi(source, spec); if (next !== null) { writeFileSync(diHostPath, next, 'utf-8'); filesModified.push(diHostPath); } } else { warnings.push(`No DependencyInjection.cs found under src/${spec.appCode}.Infrastructure/ — register the descriptors manually (AddSmartStackCodeKey).`); } printEnvelope(generateEnvelope(COMMAND, { data: { keys: spec.entities.map(e => e.codeKey), diPatched: diHostPath !== null && filesModified.includes(diHostPath), diHostPath, }, filesCreated, filesModified, warnings, nextSteps: [ 'Run `dotnet build` to verify the descriptors compile.', 'Entity half: scaffold-entity `codedEntity.codeKey` must equal each descriptor Key (ICodedEntity.CodeKey).', 'The ExtensionsDbContext ctor must forward IServiceProvider to the base — otherwise allocation is silently skipped.', 'The keys appear in Administration → Configuration → Code patterns (a CodePattern row only overrides the default).', ], })); } function safeRead(path: string): string { try { return readFileSync(path, 'utf-8'); } catch (err) { printEnvelope(failGenerate(COMMAND, [`Failed to read --spec-file ${path}: ${err instanceof Error ? err.message : String(err)}`])); process.exit(1); } } main();