/** * run-cli — spawn a colocated CLI entry point (`index.ts`) through tsx, the way * the orchestrator invokes it, and parse the JSON envelope it prints. * * Why a real spawn: every scaffolder used to be tested only through * `generate()` with hand-filled fixtures, so `index.ts` handing the RAW spec * to `generate()` (Zod defaults never applied) stayed invisible to a * 2442-green suite. An entry test runs the documented invocation end to end. */ import { spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const here = dirname(fileURLToPath(import.meta.url)) /** Repository root (templates/skills/lib/__tests__/helpers → 5 levels up). */ export const REPO_ROOT = resolve(here, '../../../../..') const TSX_CLI = resolve(REPO_ROOT, 'node_modules/tsx/dist/cli.mjs') export interface CliRun { status: number | null stdout: string stderr: string /** The last JSON document on stdout (the envelope), or null when stdout is not JSON. */ envelope: any | null } export function runCli(indexPath: string, args: string[], opts: { cwd?: string } = {}): CliRun { if (!existsSync(TSX_CLI)) throw new Error(`tsx not found at ${TSX_CLI} — run npm install`) const r = spawnSync(process.execPath, [TSX_CLI, indexPath, ...args], { cwd: opts.cwd ?? REPO_ROOT, encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024, }) const stdout = r.stdout ?? '' const start = stdout.indexOf('{') let envelope: any | null = null if (start >= 0) { try { envelope = JSON.parse(stdout.slice(start)) } catch { envelope = null } } return { status: r.status, stdout, stderr: r.stderr ?? '', envelope } }