#!/usr/bin/env node /** * cli:scaffold-dashboard-primitives — Scaffold theme-compliant dashboard * primitives (KpiCard, ChartCard, ListWidget, DashboardGrid, WidgetRenderer + * types + useDatavizPalette) into src/components/dashboard/. * * Idempotent: skips writes if existing content matches; honors `// @customised` * (or block form) at the top of a file. For the locale JSON, deep-merges the * `dashboard.*` namespace so siblings (auth.*, nav.*, entityLookup.*, …) survive. * * Non-blocking guard: ChartCard imports `recharts`; if the target app doesn't * have it installed, we warn + add a nextStep (never fail). */ import { parseArgsOrFail as parseArgs } from '../../../../../lib/cli-args.js'; import { existsSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { validate } from './validate.js'; import { generate } from './generate.js'; import { generateEnvelope, failGenerate, printEnvelope } from '../../../../../lib/output.js'; import { readSpecArg } from '../../../../../lib/spec-arg.js'; import { assertWebProjectRoot } from '../../../../../lib/fs.js'; import { writeFrontendFiles } from '../../../../../lib/frontend-writes.js'; const COMMAND = 'scaffold-dashboard-primitives'; function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' }, dry_run: { type: 'boolean', default: false }, }, strict: true, }); const specSrc = readSpecArg(values) if ('error' in specSrc) { printEnvelope(failGenerate(COMMAND, [specSrc.error])) process.exit(1) } let raw: unknown; try { raw = JSON.parse(specSrc.raw); } catch { printEnvelope(failGenerate(COMMAND, ['Invalid JSON in --spec'])); process.exit(1); } const validation = validate(raw); if (!validation.valid || !validation.spec) { printEnvelope(failGenerate(COMMAND, validation.errors)); process.exit(1); } const spec = validation.spec; // Fail-closed guard: refuse to write into a .NET backend / repo root — the // SAME contract as every other 3.0 frontend scaffold (theme, layout, // ui-primitives). This CLI was the one 3.0 scaffold missing it, so a wrong // projectPath silently landed src/components/dashboard/ outside the web app. try { assertWebProjectRoot(spec.projectPath); } catch (e) { printEnvelope(failGenerate(COMMAND, [e instanceof Error ? e.message : String(e)])); process.exit(1); } const files = generate(spec); const warnings = [...validation.warnings]; const nextSteps: string[] = []; // ChartCard needs recharts in the consuming app (it's a transitive dep of the // package but not guaranteed importable — esp. under pnpm). Warn, never fail. const rechartsPresent = existsSync(resolve(join(spec.projectPath, 'node_modules', 'recharts'))); if (!rechartsPresent) { warnings.push('recharts is not installed in the target app — ChartCard will fail Vite resolution at build. Run `npm i recharts` in the web app.'); } if (values.dry_run) { printEnvelope(generateEnvelope(COMMAND, { data: { dryRun: true, files: files.map((f) => f.path) }, warnings, })); process.exit(0); } // ONE write loop for every frontend primitive scaffolder (lib/frontend-writes) — traversal-safe now, too. const { filesCreated, filesModified, skipped, malformed } = writeFrontendFiles(files, { projectPath: spec.projectPath, force: spec.force }); if (skipped.length > 0) { nextSteps.push(`Skipped ${skipped.length} customised file(s): ${skipped.join(', ')}. Remove the @customised marker to allow overwrites.`); } if (filesCreated.length === 0 && filesModified.length === 0 && skipped.length === 0) { nextSteps.push('Dashboard primitives already up-to-date — no changes needed.'); } if (filesCreated.length > 0 || filesModified.length > 0) { nextSteps.push('scaffold-component now resolves @/components/dashboard/WidgetRenderer for the dashboard view. Re-run audit-dev-frontend to clear DEV-UI-036.'); } printEnvelope(generateEnvelope(COMMAND, { success: malformed.length === 0, data: { fileCount: filesCreated.length + filesModified.length, skipped: skipped.length, malformed, rechartsPresent }, filesCreated, filesModified, errors: malformed.map((p) => `${p} is not valid JSON — NOT merged, NOT overwritten (its other namespaces would be lost): fix or delete the file, then re-run.`), warnings, nextSteps, })); if (malformed.length > 0) process.exit(1); } main();