#!/usr/bin/env node /** * cli:menu-node — entry point. * * THE deterministic persist step of `/ba-create-menu`: add, rename or delete * ONE node of the `.smartstack/ba/` menu tree. `mode: check` (default) is the * plan the skill shows the user before AskUserQuestion — nothing is written; * `mode: write` applies it. The LLM authors the business content (label, * Contexte, Hors-périmètre, Sources); this CLI does the shape: folder, * `index.md`, the six placeholders, the parent `## Enfants` splice (anchor * attributes preserved), the downstream code rewrites, `previousCodes=`. * * Invocation (prefer --spec-file on Windows — multi-line inline JSON breaks * under Git Bash / PowerShell quoting): * npx --prefer-offline tsx skills/business-analyse/create-menu/cli/menu-node/index.ts \ * --spec-file /menu-node.json [--workdir ] * npx --prefer-offline tsx skills/business-analyse/create-menu/cli/menu-node/index.ts \ * --spec '{"baRoot":".smartstack/ba","op":"add","level":"section","parent":{"app":"CRM","module":"PIPELINE"},"code":"devis","label":"Devis","contexte":"…"}' * * Drift is DATA (exit 0, `blocked[]` may be non-empty); exit 1 = spec error. */ import { parseArgs } from 'node:util' import { executeEnvelope, failExecute, printEnvelope } from '../../../../lib/output.js' import { readSpecArg } from '../../../../lib/spec-arg.js' import { executeMenuNode } from './execute.js' import type { MenuNodeReport } from './types.js' import { validateSpec } from './validate.js' const COMMAND = 'menu-node' function main(): void { const { values } = parseArgs({ options: { spec: { type: 'string' }, 'spec-file': { type: 'string' }, workdir: { type: 'string' }, }, strict: true, }) const specArg = readSpecArg(values) if ('error' in specArg) { printEnvelope(failExecute(COMMAND, [specArg.error])) process.exit(1) } let raw: unknown try { raw = JSON.parse(specArg.raw) } catch { printEnvelope(failExecute(COMMAND, ['Invalid JSON in --spec / --spec-file'])) process.exit(1) } const validation = validateSpec(raw, values.workdir) if (!validation.valid || !validation.spec || !validation.scope) { printEnvelope(failExecute(COMMAND, validation.errors)) process.exit(1) } const report = executeMenuNode(validation.spec, validation.scope) const nextSteps: string[] = [] if (report.blocked.length > 0) { for (const b of report.blocked) { nextSteps.push(`BLOCKED ${b.code}: ${b.reason}${b.route ? ` → ${b.route}` : ''}${b.candidates && b.candidates.length > 0 ? ` (candidates: ${b.candidates.join(', ')})` : ''}`) } } else if (report.noop) { nextSteps.push(`NO-OP: ${report.node.path} is already in the requested state — nothing to write.`) } else if (report.mode === 'check') { nextSteps.push( `PLAN ${report.op} ${report.level} ${report.node.path}: ${report.plan.filesToCreate.length} file(s) to create, ${report.plan.filesToModify.length} to modify, ${report.plan.directoriesToMove.length} folder(s) to move, ${report.plan.directoriesToRemove.length} to remove. Show it to the user (AskUserQuestion), then re-run with mode=write.`, ) } else { nextSteps.push( `APPLIED ${report.op} ${report.level} ${report.node.path}: ${report.applied?.filesCreated.length ?? 0} created, ${report.applied?.filesModified.length ?? 0} modified, ${report.applied?.directoriesMoved.length ?? 0} moved, ${report.applied?.directoriesRemoved.length ?? 0} removed.`, ) } nextSteps.push(...report.nextSteps) printEnvelope( executeEnvelope(COMMAND, { success: true, data: { op: report.op, mode: report.mode, level: report.level, path: report.node.path, blocked: report.blocked.length, noop: report.noop, applied: report.applied !== null, residual: report.plan.residual.length, }, report, warnings: report.warnings, nextSteps, }), ) process.exit(0) } main()