@aiviatic/kindling 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../engine/pins.ts","../engine/messages.ts","../engine/emitter.ts","../engine/orchestrate/scaffold.ts","../engine/orchestrate/flags.ts","../engine/orchestrate/launch.ts","../engine/orchestrate/bmad-install.ts","../engine/orchestrate/agent-cli.ts","../engine/probe.ts","../engine/bmad-manifest.ts","../engine/validation-summary.ts","../engine/self-check.ts","../engine/provision/detect.ts","../engine/provision/git-unix.ts","../engine/log.ts","../engine/engine.ts"],"sourcesContent":["import type { Pins } from './contract';\n\n// Frozen pin manifest — the single source of truth for version pins, consumed by the\n// bootstrap copy, install orchestration, the Welcome screen, and the Validation Page.\n// Frozen for the duration of a cohort cycle so home == workshop (NFR-3).\n//\n// `node` is the version Kindling provisions for the user (engine runtime floor is 20+).\n// `bmad` MUST be pinned to the cohort's tested bmad-method version before a real install\n// is shipped (see Story 1.5 / Epic 2). `kindling` is this package's own version.\nexport const pins: Readonly<Pins> = Object.freeze({\n node: '24.16.0',\n bmad: '6.9.0', // frozen for the Cohort #1 cycle (matches this repo's BMad install; tools + install flags verified vs the real 6.9.0 CLI 2026-07-02); bump between cohorts\n kindling: '0.1.2',\n});\n","import { StepId, ErrorCode } from './contract';\n\n// Human-facing, kitchen-table copy. The engine resolves these into events so frontends\n// never compose text (architecture: Communication Patterns). Keyed by StepId; error copy\n// keyed by a stable error code. Keys reference the StepId enum (never raw strings) so this\n// catalog stays in sync with the contract.\nexport const stepMessages: Record<StepId, string> = {\n [StepId.ProvisionNode]: 'Setting up Node - the engine your project runs on.',\n [StepId.ProvisionGit]: 'Setting up Git - it keeps the history of your project safe.',\n [StepId.ProvisionXcodeClt]:\n 'macOS is installing some developer tools. A system dialog popped up: click Install. ' +\n 'This is completely normal and usually takes about 5 minutes. You can grab a coffee.',\n [StepId.ScaffoldGitInit]: 'Creating your project folder and starting its history.',\n [StepId.InstallBmad]:\n 'Installing BMad - the toolkit that powers your project. This is a sizeable download, ' +\n 'so it can take a couple of minutes. Nothing is stuck; hang tight.',\n [StepId.InstallAgentCli]:\n 'Installing your AI coding assistant so it’s ready to run right after setup.',\n [StepId.FinalizeSelfCheck]: 'Double-checking everything is in place.',\n};\n\n// Optional agent-CLI install copy (Story 6.1), one line per per-CLI outcome. `name` is the\n// friendly agent name (e.g. \"Claude Code\"); `manualInstall(pkg)` is the exact fallback command\n// shown when an install fails — the install is machine-global, so a manual run fixes it for good.\nexport const agentCliMessages = {\n working: (name: string): string => `Installing ${name} so you can start right away.`,\n done: (name: string): string => `${name} is installed and ready to run.`,\n skipped: (name: string): string => `${name} is already installed, reusing it.`,\n failed: (name: string): string =>\n `${name} didn’t finish installing - your project is still set up and ready. ` +\n `You can press Retry, or install it yourself later.`,\n manualInstall: (pkg: string): string => `To install it yourself later, run: npm install -g ${pkg}`,\n} as const;\n\n// Scaffold-step outcome copy (one message per terminal outcome — the step has several).\nexport const scaffoldMessages = {\n done: 'Your project folder is ready.',\n skipped: 'Project already set up, nothing to do.',\n blocked:\n 'This folder already has files in it. Kindling won’t change anything without your OK, ' +\n 'pick an empty folder, or confirm before continuing.',\n failed: 'Setting up your project folder ran into a problem. Check the details, then press Retry.',\n} as const;\n\n// Provisioning detection copy (per tool, by detected state).\nexport const provisionMessages = {\n nodePresent: 'Node is already installed, reusing it.',\n nodeQueued: 'Node needs setting up - the engine your project runs on.',\n gitPresent: 'Git is already installed, reusing it.',\n gitQueued: 'Git needs setting up - it keeps the history of your project.',\n gitInstalled: 'Git is set up.',\n xcodeWaiting:\n 'Still installing developer tools… the macOS dialog is doing its thing. This can take a few minutes, hang tight, nothing is stuck.',\n xcodeDone: 'Developer tools are ready - Git is set up.',\n xcodeTimeout:\n 'The developer-tools install is taking longer than expected. If the macOS dialog is still open, let it finish, then press Retry.',\n xcodeInstallFailed:\n 'We couldn’t start the developer-tools install. Make sure you’re connected, then press Retry.',\n gitInstallFailed:\n 'Setting up Git ran into a problem, it may need permission to install. Check the details, then press Retry.',\n} as const;\n\n// Post-install self-check step copy.\nexport const selfCheckMessages = {\n done: 'Everything checks out, you’re ready.',\n failed: 'Some checks didn’t pass, see the readiness details.',\n};\n\n// BMad install step copy.\nexport const installMessages = {\n notPinned: 'BMad isn’t pinned to a version yet, set the cohort version before installing.',\n done: (version: string): string => `BMad ${version} installed.`,\n};\n\n// Error copy: always plain-language, never blames the user, always implies a next step.\n// Keyed by the ErrorCode enum (not raw strings) so it stays in sync with the contract.\nexport const errorMessages: Record<ErrorCode, string> = {\n [ErrorCode.ExecFailed]: 'Something needs a quick fix: a step did not finish. Check the next step below, then press Retry.',\n [ErrorCode.NetworkLost]: 'We lost the connection. Reconnect to the internet, then press Retry.',\n [ErrorCode.BmadInstallFailed]: 'BMad didn’t finish installing. The details are below, press Retry.',\n [ErrorCode.AgentCliInstallFailed]:\n 'Your AI coding assistant didn’t finish installing - your project is still ready. Press Retry, or install it yourself later.',\n [ErrorCode.ExecPolicyBlocked]: 'Windows blocked the script because it’s unsigned, that’s expected, safe, and reversible.',\n [ErrorCode.SmartScreenBlocked]: 'Windows SmartScreen (or your antivirus) paused the script, that’s expected, safe, and reversible.',\n [ErrorCode.ProjectConflict]: 'That folder already has files in it that Kindling didn’t create.',\n};\n\n/**\n * Structured recovery guidance for the in-UI error screen (Story 3.6). The UI RENDERS this; it\n * never composes its own copy. Each entry names the cause and a concrete next step; some carry a\n * one-line fix command (shown with a copy button) or route to a non-destructive choice.\n * - `recovery: 'retry'` → show a Retry button (re-run the failed step).\n * - `recovery: 'choose-folder'` → no destructive default; let the user pick another folder (FR-9).\n */\nexport interface RecoveryGuidance {\n /** Short reassuring framing headline. */\n title: string;\n /** The cause + the concrete next step, plain language. */\n detail: string;\n /** Optional one-line command the user runs, then retries (e.g. the Windows policy fix). */\n fixCommand?: string;\n recovery: 'retry' | 'choose-folder';\n}\n\nexport const recoveryGuidance: Record<ErrorCode, RecoveryGuidance> = {\n [ErrorCode.ExecFailed]: {\n title: 'Something needs a quick fix.',\n detail: 'A step didn’t finish. This usually clears up on a second try, press Retry.',\n recovery: 'retry',\n },\n [ErrorCode.NetworkLost]: {\n title: 'We lost the connection.',\n detail:\n 'A download was interrupted. Reconnect to the internet, then press Retry, Kindling resumes where it left off.',\n recovery: 'retry',\n },\n [ErrorCode.BmadInstallFailed]: {\n title: 'BMad didn’t install.',\n detail:\n 'The install step didn’t finish. This is usually temporary, press Retry. The step details below show what happened.',\n recovery: 'retry',\n },\n [ErrorCode.AgentCliInstallFailed]: {\n // No `fixCommand` here: the exact package differs per CLI (claude-code vs codex), so the\n // concrete, copy-pasteable `npm install -g <package>` line ships in the failed step's own\n // message (agentCliMessages.manualInstall) rather than in this static, error-code-keyed entry.\n title: 'Your AI assistant didn’t install.',\n detail:\n 'Your project is set up and ready either way, this step is optional. Press Retry to try again, or install it yourself later using the command shown in the step details below.',\n recovery: 'retry',\n },\n [ErrorCode.ExecPolicyBlocked]: {\n title: 'Something needs a quick fix.',\n detail:\n 'Windows blocked the script because it’s unsigned, that’s expected, and it’s safe and reversible. Run this one line in PowerShell, then come back and press Retry:',\n fixCommand: 'Set-ExecutionPolicy -Scope Process Bypass',\n recovery: 'retry',\n },\n [ErrorCode.SmartScreenBlocked]: {\n title: 'Windows asked you to confirm.',\n detail:\n 'SmartScreen or your antivirus paused the script, this is expected and safe. Choose “More info”, then “Run anyway”, and press Retry. Nothing was changed on your computer.',\n recovery: 'retry',\n },\n [ErrorCode.ProjectConflict]: {\n title: 'That folder isn’t empty.',\n detail:\n 'It already contains files Kindling didn’t create, so we won’t touch them. Pick a different (empty or new) folder and we’ll set up there.',\n recovery: 'choose-folder',\n },\n};\n","import type { KindlingEvent } from './contract';\n\nexport type EventListener = (event: KindlingEvent) => void;\n\n// Typed, append-only event emitter. The engine is the sole producer. Each emitted event is\n// frozen and never mutated; the log only grows (architecture: append-only event stream).\nexport class EngineEmitter {\n private readonly log: KindlingEvent[] = [];\n private readonly listeners = new Set<EventListener>();\n\n /** Subscribe to events. Returns an unsubscribe function. */\n on(listener: EventListener): () => void {\n this.listeners.add(listener);\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /** Emit an event: freeze it, append to the log, then notify listeners in subscription order. */\n emit(event: KindlingEvent): void {\n // KindlingEvent fields are all primitives by contract, so a shallow freeze fully locks it.\n const frozen = Object.freeze({ ...event });\n this.log.push(frozen);\n // Snapshot listeners and isolate failures: one throwing sink must not stop delivery to the\n // others (Epic 3 runs the browser SSE sink alongside CLI sinks). A bad sink never crashes\n // the engine. (Story 1.7 wires the on-disk failure log to capture such errors.)\n for (const listener of [...this.listeners]) {\n try {\n listener(frozen);\n } catch {\n // swallow — delivery to remaining listeners continues\n }\n }\n }\n\n /** A snapshot of the append-only event log, in emission order. */\n events(): readonly KindlingEvent[] {\n return [...this.log];\n }\n}\n","import { mkdir, readdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport { exec } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from '../contract';\nimport { stepMessages, scaffoldMessages } from '../messages';\n\nconst MARKER = '.kindling.json';\n\n// OS metadata files that don't make a directory \"non-empty\" from the user's perspective.\nconst OS_CRUFT = new Set(['.DS_Store', 'Thumbs.db']);\n\nexport type ScaffoldOutcome = 'created' | 'resumed' | 'skipped' | 'blocked';\n\nexport interface ScaffoldOptions {\n projectDir: string;\n projectName: string;\n emitter: EngineEmitter;\n /** Absolute path to the provisioned git (Epic 2); defaults to PATH lookup. */\n git?: string;\n /** Injectable clock for deterministic tests. */\n now?: () => string;\n}\n\n// A dir counts as \"Kindling's own\" (resumable, not a user project) if it holds only the\n// marker, a .git directory, and/or OS metadata files.\nfunction isTrivial(entry: string): boolean {\n return entry === MARKER || entry === '.git' || OS_CRUFT.has(entry);\n}\n\nexport async function scaffold(opts: ScaffoldOptions): Promise<ScaffoldOutcome> {\n const git = opts.git ?? 'git';\n const now = opts.now ?? (() => new Date().toISOString());\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Scaffold,\n step: StepId.ScaffoldGitInit,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n // Emit Working, do the work, emit a terminal Done — or emit Failed and rethrow so the step\n // always reaches a terminal status (the Epic 3 UI folds events into a per-step state machine).\n const doInit = async (): Promise<void> => {\n emit(Status.Working, stepMessages[StepId.ScaffoldGitInit]);\n try {\n await mkdir(opts.projectDir, { recursive: true });\n await initRepo(git, opts);\n } catch (err) {\n emit(Status.Failed, scaffoldMessages.failed, 'error', ErrorCode.ExecFailed);\n throw err;\n }\n emit(Status.Done, scaffoldMessages.done);\n };\n\n const entries = await readDirSafe(opts.projectDir);\n\n if (entries !== null) {\n const hasMarker = entries.includes(MARKER);\n const foreign = entries.filter((e) => !isTrivial(e));\n\n // Pre-existing, non-Kindling project: refuse to touch it (FR9 safety).\n if (!hasMarker && foreign.length > 0) {\n emit(Status.Failed, scaffoldMessages.blocked, 'error', ErrorCode.ProjectConflict);\n return 'blocked';\n }\n // Already fully scaffolded by Kindling → idempotent skip.\n if (hasMarker && (await hasCommit(git, opts.projectDir))) {\n emit(Status.Skipped, scaffoldMessages.skipped);\n return 'skipped';\n }\n // Marker present (or trivially-empty dir) but no commit yet → (re)build the repo.\n await doInit();\n return hasMarker ? 'resumed' : 'created';\n }\n\n // Fresh: directory does not exist (mkdir happens inside doInit).\n await doInit();\n return 'created';\n}\n\nasync function initRepo(git: string, opts: ScaffoldOptions): Promise<void> {\n await run(git, ['-C', opts.projectDir, 'init', '-b', 'main']);\n await writeFile(\n join(opts.projectDir, MARKER),\n JSON.stringify({ scaffoldedBy: 'kindling', project: opts.projectName }, null, 2) + '\\n',\n );\n await run(git, ['-C', opts.projectDir, 'add', '-A']);\n // This is Kindling's own initial commit: set identity inline (works without global git config),\n // and disable GPG signing + user hooks so a developer's global config can't break setup.\n await run(git, [\n '-C',\n opts.projectDir,\n '-c',\n 'user.name=Kindling',\n '-c',\n 'user.email=kindling@local',\n '-c',\n 'commit.gpgsign=false',\n '-c',\n 'core.hooksPath=',\n 'commit',\n '-m',\n 'Initial commit',\n ]);\n}\n\nasync function hasCommit(git: string, dir: string): Promise<boolean> {\n try {\n const r = await exec(git, ['-C', dir, 'rev-parse', '--verify', 'HEAD']);\n return r.code === 0;\n } catch {\n return false;\n }\n}\n\nasync function run(git: string, args: string[]): Promise<void> {\n const r = await exec(git, args);\n if (r.code !== 0) {\n throw new Error(`git ${args.join(' ')} failed (code ${r.code}): ${r.stderr.trim()}`);\n }\n}\n\n// readdir that returns null when the directory does not exist (vs. throwing).\nasync function readDirSafe(dir: string): Promise<string[] | null> {\n try {\n return await readdir(dir);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;\n throw err;\n }\n}\n","import type { Config } from '../contract';\n\n// Composes the `bmad-method install` argument array from the user's config.\n// Pure (no I/O) so the exact flag contract is unit-testable and changeable in one place.\n//\n// [ASSUMPTION] Flag spelling/multiplicity follows the PRD's documented contract\n// (--directory, --modules, --tools, --pin, --yes, --set k=v). The exact `bmad-method` CLI\n// must be verified when the cohort `pins.bmad` version is frozen (currently a TODO).\nexport type InstallAction = 'install' | 'update';\n\n/**\n * Compose the `bmad-method install` argv. `action` (2.7) selects a fresh install vs a re-run:\n * on a project that already has a BMad install, pass `--action update` so the re-run updates in\n * place rather than erroring/duplicating; a fresh project omits it (defaults to install).\n */\nexport function composeInstallArgs(config: Config, action: InstallAction = 'install'): string[] {\n // Values are joined CSV for --modules/--tools, so a value containing a comma would split\n // into extra tokens. Reject it (defense-in-depth; the UI shouldn't allow it either).\n for (const value of [...config.modules, ...config.ides]) {\n if (value.includes(',')) {\n throw new Error(`module/IDE value may not contain a comma: \"${value}\"`);\n }\n }\n\n const args = ['install', '--yes', '--directory', config.projectDir];\n\n // Re-run on an existing install → update in place (verified flag: --action install|update).\n if (action === 'update') {\n args.push('--action', 'update');\n }\n\n if (config.modules.length > 0) {\n args.push('--modules', config.modules.join(','));\n }\n // NOTE: --tools is required by bmad-method for fresh non-interactive (--yes) installs; the\n // UI's IDE picker guarantees a non-empty selection before Start.\n if (config.ides.length > 0) {\n args.push('--tools', config.ides.join(','));\n }\n // The BMad *version* is pinned via the npx package spec (`bmad-method@<version>` in\n // bmad-install.ts) — verified against the bmad-method 6.9.0 CLI. The `--pin CODE=TAG` flag\n // is for pinning individual module tags (not the core version), so it is NOT emitted here.\n\n if (config.set) {\n // [ASSUMPTION] bmad-method splits `--set key=value` on the FIRST `=`, and `--pin` ordering\n // (appended after --modules/--tools) is positional-agnostic. Verify both when the real CLI\n // is pinned (pins.bmad TODO); composition is isolated here so spelling changes touch one file.\n for (const [key, value] of Object.entries(config.set)) {\n args.push('--set', `${key}=${value}`);\n }\n }\n return args;\n}\n","import { dirname, join } from 'node:path';\n\nexport interface LaunchRuntime {\n /** Absolute path to the provisioned node binary, or null to use `node`/`npx` on PATH\n * (a reused system Node, or nvm sourced same-shell). See ProvisionResult.nodeExe. */\n nodeExe: string | null;\n /** Pinned Kindling version (pins.kindling). */\n kindlingVersion: string;\n}\n\nexport interface LaunchCommand {\n cmd: string;\n args: string[];\n}\n\n/** Path to npx's CLI script beside a provisioned node binary (the npm bundled with the dist). */\nexport function npxCliPath(nodeExe: string): string {\n return join(dirname(nodeExe), 'node_modules', 'npm', 'bin', 'npx-cli.js');\n}\n\n/** Path to npm's CLI script beside a provisioned node binary (the npm bundled with the dist). */\nexport function npmCliPath(nodeExe: string): string {\n return join(dirname(nodeExe), 'node_modules', 'npm', 'bin', 'npm-cli.js');\n}\n\n/**\n * Compose the command to launch Kindling once Node is provisioned (Story 2.6). The clean-runtime\n * rule (AR6): never depend on a freshly-mutated PATH —\n * - `nodeExe === null` → Node is already on PATH (nvm sourced same-shell, or a reused system\n * Node) → plain `npx -y @aiviatic/kindling@<pin>`.\n * - `nodeExe` set (Windows portable, absolute) → invoke npx's CLI through that exact node binary,\n * so the launch works without the portable Node ever being on PATH.\n * Pure — the bootstrap (or a future Windows launcher) spawns the returned command.\n */\nexport function composeLaunchCommand({ nodeExe, kindlingVersion }: LaunchRuntime): LaunchCommand {\n const spec = `@aiviatic/kindling@${kindlingVersion}`;\n if (nodeExe === null) {\n return { cmd: 'npx', args: ['-y', spec] };\n }\n return { cmd: nodeExe, args: [npxCliPath(nodeExe), '-y', spec] };\n}\n","import { randomUUID } from 'node:crypto';\nimport { stat } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level, type Config } from '../contract';\nimport { stepMessages, errorMessages, installMessages } from '../messages';\nimport { composeInstallArgs } from './flags';\n\n// Default \"is BMad already installed here?\" check — a `_bmad` DIRECTORY under the project (the\n// marker `bmad-method install` writes). Require it to be a directory, not just any fs entry, so\n// a stray `_bmad` file/symlink from another tool can't trigger a spurious --action update.\nexport async function defaultBmadInstalled(projectDir: string): Promise<boolean> {\n try {\n return (await stat(join(projectDir, '_bmad'))).isDirectory();\n } catch {\n return false;\n }\n}\n\n/**\n * Result of an install attempt.\n *\n * Failure contract: a **non-zero exit** resolves with `{ ok: false }` (the caller must check\n * `.ok`); a **spawn error** (e.g. npx not found) emits `Failed` then *throws*. Either way a\n * terminal `Failed` event is emitted exactly once. (Note: `scaffold()` throws on a non-zero\n * exit — the orchestrator story must reconcile these two error models; see deferred-work.md.)\n *\n * Version target (Story 7.1 / FR27): `config.bmadTarget` selects the npx package tag. `'pinned'`\n * (default) is byte-for-byte the historical behavior — `bmad-method@<pins.bmad>` with\n * `--action install` (fresh) vs `--action update` (existing `_bmad`, Story 2.7). `'latest'` is the\n * explicit opt-in update variant — `bmad-method@latest` with `--action update` FORCED regardless of\n * the `_bmad`-present detection. `bmadVersion` in the result is the resolved TAG (`<pins.bmad>` or\n * the literal `'latest'`); the concrete version `@latest` resolves to is picked up by the\n * self-check's manifest read (FR26), not here.\n */\nexport interface BmadInstallResult {\n ok: boolean;\n bmadVersion: string;\n}\n\nexport interface BmadInstallOptions {\n config: Config;\n emitter: EngineEmitter;\n /** Injectable subprocess runner (tests pass a fake; never runs a real install). */\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n /**\n * Command used to invoke npx. Default 'npx' (macOS/Linux dev). On Windows this must be\n * the provisioned `node` + npm-cli.js (Epic 2) since `npx.cmd` can't spawn with shell:false\n * — see deferred-work.md.\n */\n npxCommand?: string;\n /**\n * Args prepended to the exec argv, before the `bmad-method@<tag>` spec. Default `[]`. On Windows\n * `npxCommand` is the provisioned `node` and this carries `[npxCliPath(node)]`, so the effective\n * invocation is `node npx-cli.js bmad-method@<tag> …` — the shell:false-safe equivalent of the\n * bare `npx.cmd` shim (which spawn can't find by bare name on Windows). Empty on macOS/Linux.\n */\n npxPrefixArgs?: string[];\n /**\n * Idempotent re-run (2.7): is BMad already installed in this project? When true, the install\n * runs with `--action update` (update in place) instead of a fresh install. Default detects a\n * `_bmad` dir under config.projectDir; injectable for tests.\n */\n bmadAlreadyInstalled?: (projectDir: string) => Promise<boolean>;\n now?: () => string;\n}\n\n// Runs `npx bmad-method@<pin> install …` with composed flags, emitting install.bmad events.\n// Never reports success on failure (FR10): non-zero exit or spawn error → Failed.\nexport async function runBmadInstall(opts: BmadInstallOptions): Promise<BmadInstallResult> {\n const exec = opts.exec ?? defaultExec;\n const npx = opts.npxCommand ?? 'npx';\n const now = opts.now ?? (() => new Date().toISOString());\n // Version target (7.1): default 'pinned'. 'latest' resolves the tag to `latest` and forces an\n // update; 'pinned' keeps the historical `<pins.bmad>` tag + install/update-by-detection behavior.\n const bmadTarget = opts.config.bmadTarget ?? 'pinned';\n const versionTag = bmadTarget === 'latest' ? 'latest' : opts.config.pins.bmad;\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Install,\n step: StepId.InstallBmad,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n emit(Status.Working, stepMessages[StepId.InstallBmad]);\n\n // Fail fast with a clear message if the cohort BMad version was never frozen (don't ship a\n // cryptic `npx bmad-method@0.0.0-TODO` 404). Pinned-target concern only — `latest` never\n // contains TODO.\n if (bmadTarget === 'pinned' && versionTag.includes('TODO')) {\n emit(Status.Failed, installMessages.notPinned, 'error', ErrorCode.BmadInstallFailed);\n return { ok: false, bmadVersion: versionTag };\n }\n\n let result: ExecResult;\n try {\n // Action selection: the `latest` opt-in FORCES `--action update` (you're deliberately moving\n // off the frozen pin). The default `pinned` path keeps 2.7 re-run idempotency: if BMad is\n // already installed here, update in place rather than a fresh install that would error/duplicate.\n let action: 'install' | 'update';\n if (bmadTarget === 'latest') {\n action = 'update';\n } else {\n const alreadyInstalled = opts.bmadAlreadyInstalled ?? defaultBmadInstalled;\n action = (await alreadyInstalled(opts.config.projectDir)) ? 'update' : 'install';\n }\n // Compose inside the try so a composition error (e.g. comma in a module name) still\n // reaches a terminal Failed event.\n // On Windows npxPrefixArgs is `[npxCliPath(node)]` and npx is the provisioned node, so the\n // effective invocation is `node npx-cli.js bmad-method@<tag> …` (shell:false-safe). Empty elsewhere.\n const args = [\n ...(opts.npxPrefixArgs ?? []),\n `bmad-method@${versionTag}`,\n ...composeInstallArgs(opts.config, action),\n ];\n result = await exec(npx, args);\n } catch (err) {\n // spawn error (e.g. npx not found) or composition error — surface a terminal failure with the\n // real error message appended (so a Windows ENOENT isn't a blank \"details below\"), then rethrow.\n const detail = err instanceof Error ? err.message : String(err);\n emit(\n Status.Failed,\n `${errorMessages[ErrorCode.BmadInstallFailed]} (${detail})`,\n 'error',\n ErrorCode.BmadInstallFailed,\n );\n throw err;\n }\n\n if (result.code !== 0) {\n // Surface a truncated tail of the child's stderr (fall back to stdout) so the failure carries\n // the actual npx/bmad error instead of a generic message with nothing behind it.\n const raw = (result.stderr.trim() ? result.stderr : result.stdout).trim();\n const detail = raw.slice(-600).trim();\n const humanMessage = detail\n ? `${errorMessages[ErrorCode.BmadInstallFailed]}\\n\\nDetails:\\n${detail}`\n : errorMessages[ErrorCode.BmadInstallFailed];\n emit(Status.Failed, humanMessage, 'error', ErrorCode.BmadInstallFailed);\n return { ok: false, bmadVersion: versionTag };\n }\n\n emit(Status.Done, installMessages.done(versionTag));\n return { ok: true, bmadVersion: versionTag };\n}\n","import { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level, type Config } from '../contract';\nimport { agentCliMessages } from '../messages';\nimport { probeVersion } from '../probe';\n\n/**\n * Explicit id → npm package table. The scope guard: only these two picked-tool ids are eligible\n * for an agent-CLI install; every other `--tools` id (vscode, cursor, …) is ignored and stays\n * skill-files-only. Kept explicit and in one place so the mapping — and the DELIBERATE unpinned\n * (latest) package spelling — is one edit to change / verify against the registry.\n *\n * `[DECISION]` Unpinned (latest): everywhere else Kindling pins, but the agent CLIs are the\n * deliberate exception — they self-update, and pinning would strand cohort users on a stale\n * binary. So the pkg strings carry NO `@version`; `exec(npm, ['install','-g', pkg])` installs\n * latest.\n */\nexport const AGENT_CLI_TABLE: Record<string, { pkg: string; bin: string; name: string }> = {\n 'claude-code': { pkg: '@anthropic-ai/claude-code', bin: 'claude', name: 'Claude Code' },\n codex: { pkg: '@openai/codex', bin: 'codex', name: 'Codex' },\n};\n\n/** A resolved, eligible agent-CLI descriptor (the id folded into its table row). */\nexport interface AgentCliDescriptor {\n id: string;\n pkg: string;\n bin: string;\n name: string;\n}\n\n/**\n * SSOT accessor: map the requested `installCli` ids to their eligible descriptors, applying the\n * same scope guard + de-dupe as `installAgentCli`. The single source both the install step AND the\n * self-check (Story 6.2) read, so the id→{pkg,bin,name} mapping is never hard-coded twice. A\n * non-eligible id (vscode, cursor, …) is dropped; an empty/absent list yields `[]`.\n */\nexport function eligibleAgentClis(installCli: string[] | undefined): AgentCliDescriptor[] {\n return [...new Set((installCli ?? []).filter((id) => id in AGENT_CLI_TABLE))].map((id) => ({\n id,\n ...AGENT_CLI_TABLE[id],\n }));\n}\n\n/**\n * Outcome of the install-agent-cli step. `ok` reflects only that the step RAN TO COMPLETION —\n * it is INDEPENDENT of individual install failures (which land in `failed`). This is the step's\n * defining property versus every other engine step: it is NON-FATAL, so the engine reaches the\n * self-check / Welcome even if a CLI failed to install (AC-3). The failed ids are surfaced solely\n * as `Failed` events (keyed by ErrorCode.AgentCliInstallFailed) for the error surface / Story 6.2.\n */\nexport interface AgentCliResult {\n ok: boolean;\n installed: string[];\n skipped: string[];\n failed: string[];\n}\n\nexport interface AgentCliOptions {\n config: Config;\n emitter: EngineEmitter;\n /** Injectable subprocess runner (tests pass a fake; NEVER runs a real npm install). */\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n /**\n * Command used to invoke npm. Default 'npm' (macOS/Linux dev). On Windows this must be the\n * provisioned `node` + npm-cli.js (Epic 2) since `npm.cmd` can't spawn with shell:false — the\n * same absolute-node strategy `launch.ts` uses via `npxCliPath` (the npm equivalent is\n * `<dir>/node_modules/npm/bin/npm-cli.js`). Not over-plumbed in 6.1: like bmad-install's\n * `npxCommand`, the seam defaults to 'npm' and the rehearsal / Story 6.2 plugs the abs path in.\n */\n npmCommand?: string;\n /**\n * Args prepended to the exec argv, before `install -g <pkg>`. Default `[]`. On Windows `npmCommand`\n * is the provisioned `node` and this carries `[npmCliPath(node)]`, so the effective invocation is\n * `node npm-cli.js install -g <pkg>` — the shell:false-safe equivalent of the bare `npm.cmd` shim\n * (which spawn can't find by bare name on Windows). Empty on macOS/Linux. Mirrors bmad-install's\n * `npxPrefixArgs`.\n */\n npmPrefixArgs?: string[];\n /**\n * Resolve an agent CLI's bin name to a spawnable form before the idempotent-skip probe. Default:\n * identity. On Windows a global `npm install -g` writes a `claude.cmd`/`.ps1` shim (not a bare\n * `claude` executable), and `exec` uses `shell:false` — so a bare bin can't be spawned there and\n * `probeVersion` would wrongly report \"absent\" and reinstall on every run. This seam is the probe\n * equivalent of `npmCommand`: the Epic-2 Windows wiring / dress rehearsal plugs the resolved shim\n * path in here. Not over-plumbed in 6.1 (defaults to identity, exactly as `npmCommand` defaults\n * to 'npm'). See deferred-work.md → agent-CLI Windows resolution.\n */\n resolveBin?: (bin: string) => string;\n now?: () => string;\n}\n\n/**\n * Optionally installs the picked agent's CLI (Claude Code / Codex) globally at latest, AFTER the\n * BMad install and BEFORE the self-check. Mirrors `bmad-install.ts` (injectable `exec`/`now`, an\n * `npmCommand` seam) but with the opposite failure contract:\n *\n * - **Idempotent skip** (AC-2): probe `<bin> --version` first; if present, emit `Skipped` and do\n * not re-install. The install is machine-global (`npm -g`), so a second run / other project\n * simply skips — which is what makes repeated runs and multiple projects safe.\n * - **Scope guard** (AC-4): only ids in AGENT_CLI_TABLE install; others are ignored.\n * - **NON-fatal failure** (AC-3): a non-zero exit OR a caught spawn error emits ONE `Failed`\n * event (with the manual-install fallback) and CONTINUES to the next CLI — it NEVER throws.\n * A CLI-install failure must not invalidate the successful BMad install.\n *\n * Returns `{ ok: true, ... }` whenever the step ran to completion, regardless of individual\n * failures — the engine step returns `true` unconditionally off the back of this.\n */\nexport async function installAgentCli(opts: AgentCliOptions): Promise<AgentCliResult> {\n const exec = opts.exec ?? defaultExec;\n const npm = opts.npmCommand ?? 'npm';\n const resolveBin = opts.resolveBin ?? ((bin: string) => bin);\n const now = opts.now ?? (() => new Date().toISOString());\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Install,\n step: StepId.InstallAgentCli,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n const installed: string[] = [];\n const skipped: string[] = [];\n const failed: string[] = [];\n\n // Scope guard: keep only eligible ids (AC-4), de-duped so a repeated id can't double-install.\n // Empty/absent installCli ⇒ no eligible ids ⇒ no events, no exec — a silent no-op unchanged.\n const eligible = eligibleAgentClis(opts.config.installCli);\n\n for (const { id, pkg, bin, name } of eligible) {\n\n // Idempotent skip: already present anywhere on the machine → don't reinstall (AC-2). Probe via\n // the resolveBin seam so the Windows shim (claude.cmd) is reachable once wired (see JSDoc).\n if ((await probeVersion(exec, resolveBin(bin))) !== null) {\n emit(Status.Skipped, agentCliMessages.skipped(name));\n skipped.push(id);\n continue;\n }\n\n emit(Status.Working, agentCliMessages.working(name));\n\n // Non-fatal: capture BOTH the non-zero-exit and the spawn-error paths, emit a single Failed\n // event including the manual-install fallback, and continue to the next CLI (never throw).\n const failMessage = `${agentCliMessages.failed(name)} ${agentCliMessages.manualInstall(pkg)}`;\n try {\n // On Windows npmPrefixArgs is `[npmCliPath(node)]` and npm is the provisioned node, so the\n // effective invocation is `node npm-cli.js install -g <pkg>` (shell:false-safe). Empty elsewhere.\n const result = await exec(npm, [...(opts.npmPrefixArgs ?? []), 'install', '-g', pkg]); // NO @version — latest (deliberate)\n if (result.code === 0) {\n emit(Status.Done, agentCliMessages.done(name));\n installed.push(id);\n } else {\n // Surface a truncated tail of the child's stderr (fall back to stdout) so the failure carries\n // the actual npm error instead of only the generic message + manual-install fallback.\n const raw = (result.stderr.trim() ? result.stderr : result.stdout).trim();\n const detail = raw.slice(-600).trim();\n emit(\n Status.Failed,\n detail ? `${failMessage}\\n\\nDetails:\\n${detail}` : failMessage,\n 'error',\n ErrorCode.AgentCliInstallFailed,\n );\n failed.push(id);\n }\n } catch (err) {\n // spawn error (e.g. npm not found by bare name on Windows) — append the real error message\n // so it isn't a blank failure.\n const detail = err instanceof Error ? err.message : String(err);\n emit(\n Status.Failed,\n `${failMessage} (${detail})`,\n 'error',\n ErrorCode.AgentCliInstallFailed,\n );\n failed.push(id);\n }\n }\n\n // ok = \"the step ran to completion\", INDEPENDENT of individual failures (AC-3).\n return { ok: true, installed, skipped, failed };\n}\n","import type { ExecResult } from './exec';\n\nexport type Exec = (cmd: string, args: string[]) => Promise<ExecResult>;\n\n// Shared \"is this tool present, and what version?\" probe. SSOT for the \"present\" definition:\n// the tool counts as present only if `<cmd> --version` exits 0 AND prints non-empty output\n// (stderr used as a fallback for tools that print there). Returns null when absent/unspawnable.\nexport async function probeVersion(exec: Exec, cmd: string): Promise<string | null> {\n try {\n const r = await exec(cmd, ['--version']);\n const out = r.stdout.trim() || r.stderr.trim();\n return r.code === 0 && out ? out : null;\n } catch {\n return null;\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { load } from 'js-yaml';\n\n/**\n * Read the ACTUAL installed BMad version from a project's manifest (FR26).\n *\n * Reads `<projectDir>/_bmad/_config/manifest.yaml` and returns `installation.version` as a string,\n * or `null` on ANY of: file absent, YAML parse error, a non-object document, or a\n * missing/non-string `installation.version`. NEVER throws — it mirrors the tolerance of\n * `probeVersion`/`defaultBmadInstalled`: a \"couldn't read disk reality\" degrades to `null`\n * (honest not-ready on the Validation Page), never a crash or a false pin.\n *\n * Node-side only (uses `node:fs/promises` + `js-yaml`). The PURE summary module\n * (`validation-summary.ts`) must not import this — the browser imports that module.\n */\nexport async function readInstalledBmadVersion(projectDir: string): Promise<string | null> {\n try {\n const raw = await readFile(join(projectDir, '_bmad', '_config', 'manifest.yaml'), 'utf8');\n const doc = load(raw);\n if (typeof doc !== 'object' || doc === null) return null;\n const installation = (doc as { installation?: unknown }).installation;\n if (typeof installation !== 'object' || installation === null) return null;\n const version = (installation as { version?: unknown }).version;\n return typeof version === 'string' ? version : null;\n } catch {\n return null;\n }\n}\n","// The Validation Summary: the structured, copyable proof a successful install produces (FR21).\n// The Epic 4 Validation Page checks this client-side against the cohort's expected pin.\n// Bump SCHEMA_VERSION on any field change.\n\n// v2 (Story 6.2): added the non-blocking `cli` presence field (FR24). A v1 summary pasted into a\n// v2 Validation Page reads as `malformed` (the cohort is a single frozen build, so this is safe).\n// v3 (Story 7.1): added `bmad.installedVersion` — the ACTUAL on-disk BMad version read from the\n// manifest (FR26), distinct from the requested `bmad.pinnedVersion`. Same single-frozen-build\n// safety: a v2 summary pasted into a v3 Validation Page reads as `malformed`.\nexport const SCHEMA_VERSION = 3;\n\n/** Node runtime floor (BMad's hard requirement). */\nexport const NODE_FLOOR_MAJOR = 20;\n\n/**\n * Presence of one requested agent CLI in the final self-check (FR24). Self-describing — it carries\n * `name`/`bin`/`pkg` so the browser Validation Page and both Welcome renderers render copy WITHOUT\n * re-importing the engine's `AGENT_CLI_TABLE`. `pkg` lets the \"install-it-yourself\" notice (AC-8)\n * show the exact `npm install -g <pkg>` command for an absent CLI. NON-blocking: this never feeds\n * `success` (AC-6). Empty list when no CLI was requested (present for schema stability).\n */\nexport interface CliPresence {\n id: string;\n name: string;\n bin: string;\n pkg: string;\n present: boolean;\n}\n\nexport interface ValidationSummary {\n schemaVersion: number;\n kindlingVersion: string;\n os: string;\n arch: string;\n osVersion: string;\n node: { present: boolean; version: string | null; satisfiesFloor: boolean };\n git: { present: boolean; version: string | null };\n /**\n * `pinnedVersion` = the REQUESTED cohort pin (`pins.bmad`); `installed` = the `_bmad` dir is\n * present; `installedVersion` (FR26) = the ACTUAL version read from the manifest — `null` when\n * the manifest is absent/unreadable/unparseable (honest \"couldn't read disk reality\", never a\n * false pin). The Validation Page (Story 7.1) compares `installedVersion` against the expected pin.\n */\n bmad: { pinnedVersion: string; installed: boolean; installedVersion: string | null };\n scaffold: { created: boolean };\n /** Requested agent CLIs + their presence (FR24). NON-blocking — never part of `success`. */\n cli: CliPresence[];\n success: boolean;\n generatedAt: string;\n}\n\nexport interface ValidationFacts {\n kindlingVersion: string;\n os: string;\n arch: string;\n osVersion: string;\n node: { present: boolean; version: string | null; satisfiesFloor: boolean };\n git: { present: boolean; version: string | null };\n bmad: { pinnedVersion: string; installed: boolean; installedVersion: string | null };\n scaffold: { created: boolean };\n cli: CliPresence[];\n generatedAt: string;\n}\n\n// Pure. `success` is a strict AND of the real checks — no false green (FR21 / SM-C1). The `cli`\n// field is DELIBERATELY excluded from `success` (FR24 / AC-6): a CLI that didn't install is\n// reported but never flips the verdict.\nexport function buildValidationSummary(facts: ValidationFacts): ValidationSummary {\n const success =\n facts.node.present &&\n facts.node.satisfiesFloor &&\n facts.git.present &&\n facts.bmad.installed &&\n facts.scaffold.created;\n\n return {\n schemaVersion: SCHEMA_VERSION,\n kindlingVersion: facts.kindlingVersion,\n os: facts.os,\n arch: facts.arch,\n osVersion: facts.osVersion,\n node: facts.node,\n git: facts.git,\n bmad: facts.bmad,\n scaffold: facts.scaffold,\n cli: facts.cli,\n success,\n generatedAt: facts.generatedAt,\n };\n}\n\n/**\n * Runtime shape guard for one `cli` element. The Validation Page parses an UNTRUSTED pasted string\n * whose `cli` can be any JSON (e.g. an adversarial `\"cli\":[null]`) — `looksLikeSummary` never\n * inspects `cli` element shape — so the presence helpers below must not assume it. Dropping a\n * malformed element (rather than throwing) keeps the public page and the Welcome render resilient.\n */\nexport function isCliPresence(c: unknown): c is CliPresence {\n return (\n typeof c === 'object' &&\n c !== null &&\n typeof (c as CliPresence).name === 'string' &&\n typeof (c as CliPresence).bin === 'string' &&\n typeof (c as CliPresence).pkg === 'string' &&\n typeof (c as CliPresence).present === 'boolean'\n );\n}\n\n/**\n * The requested CLIs that ARE present after the run — drives the FR25 login guidance (\"run `claude`\n * and log in\"). Pure + shared so the React Welcome and the static welcome.html render identical copy\n * from one source. Tolerant of a missing/legacy `cli` field or a malformed element (returns []/skips).\n */\nexport function cliLoginGuidance(summary: Pick<ValidationSummary, 'cli'>): CliPresence[] {\n return (summary.cli ?? []).filter(isCliPresence).filter((c) => c.present);\n}\n\n/**\n * The requested CLIs that are ABSENT after the run — drives the calm, non-blocking \"didn't finish\n * installing — here's the one line to install it, then log in\" notice (AC-8). Order-robust: derived\n * from the summary's actual presence, not the transient step row. Skips malformed elements.\n */\nexport function cliMissing(summary: Pick<ValidationSummary, 'cli'>): CliPresence[] {\n return (summary.cli ?? []).filter(isCliPresence).filter((c) => !c.present);\n}\n\n/**\n * The honest BMad version chip (Story 7.2 / FR26). Pure + shared so the React Welcome and the static\n * welcome.html render identical copy from ONE source. When the summary carries an `installedVersion`\n * (7.1's manifest read) that is a non-empty string DIFFERING from the pinned fallback, the run was an\n * update-to-latest, so the pinned \"a stable, tested version\" chip would be a false statement; show\n * the real installed version + \"updated to latest\". Otherwise (absent/null/equal, the default run,\n * unchanged) fall back to the pinned chip. Never blank, never a crash. Node-free (browser-safe).\n */\nexport function bmadVersionLabel(\n summary: { bmad?: { installedVersion?: string | null } } | null | undefined,\n pinnedFallback: string,\n): { version: string; note: string } {\n const installed = summary?.bmad?.installedVersion;\n if (typeof installed === 'string' && installed.length > 0 && installed !== pinnedFallback) {\n return { version: installed, note: 'updated to latest' };\n }\n return { version: pinnedFallback, note: 'a stable, tested version' };\n}\n\n/**\n * Parse a major version from version strings: `v24.16.0`, `24.16.0`, `v20.0`, `v20`,\n * `git version 2.43.0.windows.1`, `v24.0.0-nightly…`. Null if no leading integer is found.\n */\nexport function parseMajor(version: string | null): number | null {\n if (!version) return null;\n const m = /(\\d+)(?:\\.\\d+){0,2}/.exec(version);\n return m ? Number(m[1]) : null;\n}\n","import { release } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from './exec';\nimport { probeVersion } from './probe';\nimport { readInstalledBmadVersion as defaultReadInstalledBmadVersion } from './bmad-manifest';\nimport type { EngineEmitter } from './emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from './contract';\nimport { pins } from './pins';\nimport { stepMessages, selfCheckMessages } from './messages';\nimport {\n buildValidationSummary,\n parseMajor,\n NODE_FLOOR_MAJOR,\n type ValidationSummary,\n type CliPresence,\n} from './validation-summary';\nimport type { AgentCliDescriptor } from './orchestrate/agent-cli';\n\nexport interface SelfCheckOptions {\n /** Outcomes threaded from prior steps (Stories 1.4 / 1.5). */\n scaffoldCreated: boolean;\n bmadInstalled: boolean;\n /** Project directory — where the BMad manifest lives (`<projectDir>/_bmad/_config/manifest.yaml`). */\n projectDir: string;\n emitter: EngineEmitter;\n /** Provisioned binaries (Epic 2); default PATH lookup. */\n node?: string;\n git?: string;\n /**\n * The requested eligible agent CLIs (Story 6.2), threaded from the engine's install-agent-cli\n * step. Each is probed for presence and reported in the summary's non-blocking `cli` field.\n * Absent/empty ⇒ `cli: []`.\n */\n agentClis?: AgentCliDescriptor[];\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n now?: () => string;\n /**\n * Resolve a CLI bin to a spawnable form before the presence probe — the probe equivalent of\n * agent-cli.ts's `resolveBin` seam. Default: identity. On Windows the installed CLI is a\n * `claude.cmd` shim and `exec` is `shell:false`, so a bare-bin probe would wrongly report\n * \"absent\"; the Epic-2 Windows wiring / dress rehearsal plugs the resolved shim path in here.\n */\n resolveBin?: (bin: string) => string;\n /**\n * Read the ACTUAL installed BMad version from the project manifest (FR26). Injectable so unit\n * tests never touch a real fs/manifest (mirrors the `bmadAlreadyInstalled`/`exec` seams).\n * Default: the node-side `readInstalledBmadVersion` helper. Returns `null` on any read/parse\n * failure — reported as `bmad.installedVersion: null`, never a false pin.\n */\n readInstalledBmadVersion?: (projectDir: string) => Promise<string | null>;\n /** Platform facts (injectable for tests); default the running process. */\n platform?: { os: string; arch: string; osVersion: string };\n}\n\n// Verifies Node ≥ floor, Git present, scaffold + BMad done; emits finalize.self-check and\n// returns the Validation Summary. `success` is strict — a \"green\" summary means a real setup.\nexport async function runSelfCheck(opts: SelfCheckOptions): Promise<ValidationSummary> {\n const exec = opts.exec ?? defaultExec;\n const now = opts.now ?? (() => new Date().toISOString());\n const resolveBin = opts.resolveBin ?? ((bin: string) => bin);\n const readInstalledBmadVersion =\n opts.readInstalledBmadVersion ?? defaultReadInstalledBmadVersion;\n const platform = opts.platform ?? {\n os: process.platform,\n arch: process.arch,\n osVersion: release(),\n };\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n summaryJson?: string,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Finalize,\n step: StepId.FinalizeSelfCheck,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n summaryJson,\n });\n };\n\n // Stamp the summary once so generatedAt doesn't drift across the probe awaits / events.\n const generatedAt = now();\n\n emit(Status.Working, stepMessages[StepId.FinalizeSelfCheck]);\n\n const nodeVersion = await probeVersion(exec, opts.node ?? 'node');\n const gitVersion = await probeVersion(exec, opts.git ?? 'git');\n const nodeMajor = parseMajor(nodeVersion);\n\n // Probe each requested eligible CLI for presence via the SAME probe as node/git (reuse — no\n // hand-rolled check), through the resolveBin seam (Windows shim). Non-blocking (AC-6): the\n // result feeds only the `cli` field, never `success`.\n const cli: CliPresence[] = [];\n for (const c of opts.agentClis ?? []) {\n const present = (await probeVersion(exec, resolveBin(c.bin))) !== null;\n cli.push({ id: c.id, name: c.name, bin: c.bin, pkg: c.pkg, present });\n }\n\n // Read disk reality (FR26): the ACTUAL installed BMad version from the manifest. `null` on any\n // absent/unreadable/unparseable manifest — reported honestly, never gated into `success` here.\n const installedVersion = await readInstalledBmadVersion(opts.projectDir);\n\n const summary = buildValidationSummary({\n kindlingVersion: pins.kindling,\n os: platform.os,\n arch: platform.arch,\n osVersion: platform.osVersion,\n node: {\n present: nodeVersion !== null,\n version: nodeVersion,\n satisfiesFloor: nodeMajor !== null && nodeMajor >= NODE_FLOOR_MAJOR,\n },\n git: { present: gitVersion !== null, version: gitVersion },\n bmad: { pinnedVersion: pins.bmad, installed: opts.bmadInstalled, installedVersion },\n scaffold: { created: opts.scaffoldCreated },\n cli,\n generatedAt,\n });\n\n if (summary.success) {\n // Carry the Validation Summary JSON on the success event so the Welcome screen can offer a\n // one-click copy (the exact text the Validation Page expects).\n emit(Status.Done, selfCheckMessages.done, 'info', undefined, JSON.stringify(summary));\n } else {\n emit(Status.Failed, selfCheckMessages.failed, 'error', ErrorCode.ExecFailed);\n }\n\n return summary;\n}\n","import { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport { probeVersion } from '../probe';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, type Level } from '../contract';\nimport { provisionMessages } from '../messages';\nimport { parseMajor, NODE_FLOOR_MAJOR } from '../validation-summary';\n\nexport interface ToolState {\n present: boolean;\n version: string | null;\n}\nexport interface NodeState extends ToolState {\n satisfiesFloor: boolean;\n}\nexport interface DependencyState {\n node: NodeState;\n git: ToolState;\n}\n\nexport interface DetectOptions {\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n node?: string;\n git?: string;\n emitter?: EngineEmitter;\n now?: () => string;\n}\n\n// Detects Git + Node (and whether Node meets the floor) so provisioners can skip vs install.\n// When an emitter is passed, reports each tool's state: reuse → Skipped, needs-install → Queued.\nexport async function detectDependencies(opts: DetectOptions = {}): Promise<DependencyState> {\n const exec = opts.exec ?? defaultExec;\n const now = opts.now ?? (() => new Date().toISOString());\n\n const nodeVersion = await probeVersion(exec, opts.node ?? 'node');\n const nodeMajor = parseMajor(nodeVersion);\n const node: NodeState = {\n present: nodeVersion !== null,\n version: nodeVersion,\n satisfiesFloor: nodeMajor !== null && nodeMajor >= NODE_FLOOR_MAJOR,\n };\n\n const gitVersion = await probeVersion(exec, opts.git ?? 'git');\n const git: ToolState = { present: gitVersion !== null, version: gitVersion };\n\n const emitter = opts.emitter;\n if (emitter) {\n const emit = (step: StepId, status: Status, humanMessage: string, level: Level = 'info'): void => {\n emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step,\n status,\n humanMessage,\n level,\n timestamp: now(),\n });\n };\n // Node reuse requires meeting the floor; an old Node must be upgraded.\n if (node.present && node.satisfiesFloor) {\n emit(StepId.ProvisionNode, Status.Skipped, provisionMessages.nodePresent);\n } else {\n emit(StepId.ProvisionNode, Status.Queued, provisionMessages.nodeQueued);\n }\n if (git.present) {\n emit(StepId.ProvisionGit, Status.Skipped, provisionMessages.gitPresent);\n } else {\n emit(StepId.ProvisionGit, Status.Queued, provisionMessages.gitQueued);\n }\n }\n\n return { node, git };\n}\n","import { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from '../contract';\nimport { stepMessages, provisionMessages } from '../messages';\n\nexport interface ProvisionGitUnixOptions {\n platform: NodeJS.Platform;\n emitter: EngineEmitter;\n /** From 2.1 detection: Git is already present → reuse it (Skipped). */\n alreadyOk?: boolean;\n // Injectable side effects (real shell run is rehearsal-bound; tests inject these):\n /** macOS: trigger the Xcode Command Line Tools install (opens the system dialog). */\n triggerXcodeInstall?: () => Promise<void>;\n /** macOS: true once `xcode-select -p` resolves (CLT — hence Git — installed). */\n checkXcode?: () => Promise<boolean>;\n /** Linux: install Git via the distro package manager (apt primary). */\n installLinuxGit?: () => Promise<void>;\n /** Poll cadence + cap for the Xcode wait (injected so tests don't actually wait). */\n sleep?: (ms: number) => Promise<void>;\n pollIntervalMs?: number;\n maxPolls?: number;\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n now?: () => string;\n}\n\nexport interface ProvisionGitResult {\n ok: boolean;\n}\n\nfunction makeDefaults(exec: (cmd: string, args: string[]) => Promise<ExecResult>) {\n return {\n triggerXcodeInstall: async (): Promise<void> => {\n // `xcode-select --install` returns non-zero if the tools are already installed; that's not\n // an error for us (the alreadyOk guard covers the present case, and the poll confirms).\n await exec('xcode-select', ['--install']);\n },\n checkXcode: async (): Promise<boolean> => (await exec('xcode-select', ['-p'])).code === 0,\n installLinuxGit: async (): Promise<void> => {\n // apt primary (Debian/Ubuntu — the best-effort Linux target). `sudo -n` is non-interactive:\n // with no cached creds it fails fast (clear error) instead of hanging on a password prompt\n // we can't answer (exec's stdin is /dev/null). Real run rehearsal-bound.\n const r = await exec('sudo', ['-n', 'apt-get', 'install', '-y', 'git']);\n if (r.code !== 0) throw new Error(`apt-get install git failed (code ${r.code}): ${r.stderr.trim()}`);\n },\n };\n}\n\n/**\n * Provision Git on macOS (via the Xcode Command Line Tools) or Linux (via apt). macOS triggers\n * the CLT install early and POLLS `xcode-select -p`, emitting a Working event on every tick so the\n * un-silenceable Apple dialog never looks frozen (AC1/AC2). Git already present → Skipped (AC).\n *\n * The real `xcode-select`/apt run is validated at the dress rehearsal; here the shell effects +\n * the clock are injected and the orchestration/poll/events/skip/fail paths are unit-tested.\n */\nexport async function provisionGitUnix(opts: ProvisionGitUnixOptions): Promise<ProvisionGitResult> {\n const exec = opts.exec ?? defaultExec;\n const d = makeDefaults(exec);\n const triggerXcodeInstall = opts.triggerXcodeInstall ?? d.triggerXcodeInstall;\n const checkXcode = opts.checkXcode ?? d.checkXcode;\n const installLinuxGit = opts.installLinuxGit ?? d.installLinuxGit;\n const sleep = opts.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));\n const pollIntervalMs = opts.pollIntervalMs ?? 3000;\n const maxPolls = opts.maxPolls ?? 200; // ~10 min at 3s — the dialog can be slow; never frozen\n const now = opts.now ?? (() => new Date().toISOString());\n const isMac = opts.platform === 'darwin';\n const step = isMac ? StepId.ProvisionXcodeClt : StepId.ProvisionGit;\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n if (opts.alreadyOk) {\n // On macOS the row is the Xcode-CLT step, so use CLT-flavored copy (not \"Git is installed\").\n emit(Status.Skipped, isMac ? provisionMessages.xcodeDone : provisionMessages.gitPresent);\n return { ok: true };\n }\n\n if (isMac) {\n emit(Status.Working, stepMessages[StepId.ProvisionXcodeClt]); // the \"dialog popped up, ~5 min\" copy\n try {\n await triggerXcodeInstall();\n } catch (err) {\n emit(Status.Failed, provisionMessages.xcodeInstallFailed, 'error', ErrorCode.ExecFailed);\n throw err;\n }\n // Poll until the CLT resolve — emitting Working each tick so the UI never looks frozen.\n for (let i = 0; i < maxPolls; i++) {\n if (await checkXcode()) {\n emit(Status.Done, provisionMessages.xcodeDone);\n return { ok: true };\n }\n emit(Status.Working, provisionMessages.xcodeWaiting);\n if (i < maxPolls - 1) await sleep(pollIntervalMs); // no wasted wait before the timeout\n }\n // Soft timeout — not a throw; the dialog may still be open, so Retry re-polls.\n emit(Status.Failed, provisionMessages.xcodeTimeout, 'error', ErrorCode.ExecFailed);\n return { ok: false };\n }\n\n // Linux: install via the distro package manager.\n emit(Status.Working, stepMessages[StepId.ProvisionGit]);\n try {\n await installLinuxGit();\n } catch (err) {\n emit(Status.Failed, provisionMessages.gitInstallFailed, 'error', ErrorCode.ExecFailed);\n throw err;\n }\n emit(Status.Done, provisionMessages.gitInstalled);\n return { ok: true };\n}\n","import { mkdir, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { randomUUID } from 'node:crypto';\nimport type { KindlingEvent, StepId } from './contract';\n\n// Where failure logs live: %LOCALAPPDATA%\\kindling\\logs on Windows, else ~/.kindling/logs.\nexport function defaultLogDir(): string {\n if (process.platform === 'win32' && process.env.LOCALAPPDATA) {\n return join(process.env.LOCALAPPDATA, 'kindling', 'logs');\n }\n return join(homedir(), '.kindling', 'logs');\n}\n\nexport interface FailureLogEntry {\n step: StepId;\n error: string;\n events: readonly KindlingEvent[];\n}\n\nexport interface WriteLogOptions {\n dir?: string;\n now?: () => string;\n}\n\nfunction render(entry: FailureLogEntry, timestamp: string): string {\n const lines: string[] = [\n `Kindling failure report`,\n `Generated: ${timestamp}`,\n `Failed step: ${entry.step}`,\n ``,\n `Error:`,\n entry.error,\n ``,\n `Event log:`,\n ...entry.events.map((e) => ` [${e.status}] ${e.step} - ${e.humanMessage}`),\n ``,\n ];\n return lines.join('\\n');\n}\n\n/**\n * Writes a local, human-readable failure report and returns its path. Local-only (no network).\n * Deliberately does NOT include the config object, environment variables, or any credentials\n * (NFR7) — just the failed step, the error message, and the event log.\n */\nexport async function writeFailureLog(entry: FailureLogEntry, opts: WriteLogOptions = {}): Promise<string> {\n const dir = opts.dir ?? defaultLogDir();\n const timestamp = (opts.now ?? (() => new Date().toISOString()))();\n const safeStamp = timestamp.replace(/[:.]/g, '-');\n // Short random suffix so two failures in the same millisecond don't overwrite each other.\n const suffix = randomUUID().slice(0, 8);\n await mkdir(dir, { recursive: true });\n const path = join(dir, `kindling-report-${safeStamp}-${suffix}.log`);\n await writeFile(path, render(entry, timestamp));\n return path;\n}\n","import { randomUUID } from 'node:crypto';\nimport type { EngineEmitter } from './emitter';\nimport { Phase, StepId, Status, ErrorCode, type Config, type EngineCommands, type Level } from './contract';\nimport { scaffold as defaultScaffold, type ScaffoldOptions, type ScaffoldOutcome } from './orchestrate/scaffold';\nimport { runBmadInstall as defaultRunBmadInstall, type BmadInstallOptions, type BmadInstallResult } from './orchestrate/bmad-install';\nimport { installAgentCli as defaultInstallAgentCli, eligibleAgentClis, type AgentCliOptions, type AgentCliResult } from './orchestrate/agent-cli';\nimport { npxCliPath, npmCliPath } from './orchestrate/launch';\nimport { runSelfCheck as defaultRunSelfCheck, type SelfCheckOptions } from './self-check';\nimport { detectDependencies as defaultDetect, type DetectOptions, type DependencyState } from './provision/detect';\nimport { provisionGitUnix as defaultProvisionGit, type ProvisionGitUnixOptions, type ProvisionGitResult } from './provision/git-unix';\nimport { provisionMessages } from './messages';\nimport type { ValidationSummary } from './validation-summary';\nimport { writeFailureLog as defaultWriteFailureLog, type FailureLogEntry } from './log';\nimport { expandTilde } from './expand-tilde';\n\n// Injectable step implementations so the orchestration logic is unit-testable without real\n// git/npx/shell. Defaults are the real engine functions.\nexport interface EngineDeps {\n detect: (opts: DetectOptions) => Promise<DependencyState>;\n provisionGit: (opts: ProvisionGitUnixOptions) => Promise<ProvisionGitResult>;\n scaffold: (opts: ScaffoldOptions) => Promise<ScaffoldOutcome>;\n runBmadInstall: (opts: BmadInstallOptions) => Promise<BmadInstallResult>;\n installAgentCli: (opts: AgentCliOptions) => Promise<AgentCliResult>;\n runSelfCheck: (opts: SelfCheckOptions) => Promise<ValidationSummary>;\n writeFailureLog: (entry: FailureLogEntry) => Promise<string>;\n /** Host platform — selects the Git provisioning step id (xcode-clt on macOS, else git). */\n platform: NodeJS.Platform;\n}\n\nconst defaultDeps: EngineDeps = {\n detect: defaultDetect,\n provisionGit: defaultProvisionGit,\n scaffold: defaultScaffold,\n runBmadInstall: defaultRunBmadInstall,\n installAgentCli: defaultInstallAgentCli,\n runSelfCheck: defaultRunSelfCheck,\n writeFailureLog: (entry) => defaultWriteFailureLog(entry),\n platform: process.platform,\n};\n\ninterface Step {\n id: StepId;\n run: () => Promise<boolean>; // emits its own events; returns success\n}\n\nexport interface EngineRunResult {\n ok: boolean;\n failedStep: StepId | null;\n summary: ValidationSummary | null;\n logPath: string | null;\n}\n\n/**\n * Orchestrates the full step sequence — provision Node (verify) → provision Git/Xcode →\n * scaffold → install → install-agent-cli (optional, non-fatal) → self-check — tracking which\n * steps completed so retry() can resume without re-running done steps. On a step failure it\n * writes the on-disk failure report (the\n * step itself already emitted its Failed event) and stops. The bootstrap provisions Node before\n * launch; Git/Xcode is provisioned here so the browser shows the never-frozen progress.\n */\nexport class Engine implements EngineCommands<EngineRunResult> {\n private readonly config: Config;\n private readonly deps: EngineDeps;\n private readonly completed = new Set<StepId>();\n private cancelled = false;\n private running = false;\n\n // Outputs threaded between steps.\n private scaffoldCreated = false;\n private bmadInstalled = false;\n private lastSummary: ValidationSummary | null = null;\n\n private readonly steps: Step[];\n\n constructor(\n config: Config,\n private readonly emitter: EngineEmitter,\n deps: Partial<EngineDeps> = {},\n ) {\n // Expand a leading `~` (the UI default is `~/kindling-project`) once, up front, so every step —\n // scaffold, install, self-check — works with a real path instead of a literal `~` folder.\n this.config = { ...config, projectDir: expandTilde(config.projectDir) };\n this.deps = { ...defaultDeps, ...deps };\n // Git provisioning surfaces on the xcode-clt step on macOS (the un-silenceable dialog), the\n // git step elsewhere — so a Retry targets the right row.\n const gitStepId =\n this.deps.platform === 'darwin' ? StepId.ProvisionXcodeClt : StepId.ProvisionGit;\n this.steps = [\n {\n // Node was provisioned by the bootstrap (kindling is running on it). Verify it actually\n // meets the floor before showing a green row — a sub-floor Node shows honestly (Failed)\n // instead of a false Skipped that would only surface as a confusing self-check failure.\n id: StepId.ProvisionNode,\n run: async () => {\n const state = await this.deps.detect({}); // probe only; we drive the row ourselves\n if (state.node.present && state.node.satisfiesFloor) {\n this.emit(StepId.ProvisionNode, Status.Skipped, provisionMessages.nodePresent);\n return true;\n }\n this.emit(StepId.ProvisionNode, Status.Failed, provisionMessages.nodeQueued, 'error', ErrorCode.ExecFailed);\n return false;\n },\n },\n {\n // Git/Xcode CLT runs IN the engine so the browser shows the never-frozen progress (the\n // ~5-min macOS dialog). On Windows, Git is provisioned by the bootstrap (PortableGit) —\n // probe + reflect it (Failed if the bootstrap didn't lay it down, rather than a cryptic\n // scaffold crash later).\n id: gitStepId,\n run: async () => {\n const state = await this.deps.detect({}); // probe only; no emitter (we drive the rows)\n if (this.deps.platform === 'win32') {\n if (state.git.present) {\n this.emit(gitStepId, Status.Skipped, provisionMessages.gitPresent);\n return true;\n }\n this.emit(gitStepId, Status.Failed, provisionMessages.gitInstallFailed, 'error', ErrorCode.ExecFailed);\n return false;\n }\n const result = await this.deps.provisionGit({\n platform: this.deps.platform,\n emitter: this.emitter,\n alreadyOk: state.git.present,\n });\n return result.ok;\n },\n },\n {\n id: StepId.ScaffoldGitInit,\n run: async () => {\n const outcome = await this.deps.scaffold({\n projectDir: this.config.projectDir,\n projectName: this.config.projectName,\n emitter: this.emitter,\n });\n this.scaffoldCreated = outcome !== 'blocked';\n return outcome !== 'blocked';\n },\n },\n {\n id: StepId.InstallBmad,\n run: async () => {\n // Windows: `npx` is a `.cmd` shim that spawn(shell:false) can't find by bare name → ENOENT.\n // The engine runs on the provisioned node (process.execPath), with npx-cli.js beside it, so\n // invoke `node npx-cli.js …` instead. macOS/Linux keep the bare-`npx` default untouched.\n const result = await this.deps.runBmadInstall({\n config: this.config,\n emitter: this.emitter,\n ...(this.deps.platform === 'win32'\n ? { npxCommand: process.execPath, npxPrefixArgs: [npxCliPath(process.execPath)] }\n : {}),\n });\n this.bmadInstalled = result.ok;\n return result.ok;\n },\n },\n {\n // Optional agent-CLI install (Story 6.1). NON-fatal by design: it awaits the step but\n // returns `true` UNCONDITIONALLY, so an individual CLI-install failure never invalidates\n // the successful BMad install — the run still reaches self-check / Welcome. The failure is\n // surfaced only as a Failed event (ErrorCode.AgentCliInstallFailed) for the error surface /\n // Story 6.2 UI. The row is retryable via engine.retry(StepId.InstallAgentCli) — but note a\n // future \"Retry install\" affordance must ALSO re-run self-check to refresh the Welcome's\n // CLI-presence guidance (retry() skips the already-completed self-check). See deferred-work.md.\n id: StepId.InstallAgentCli,\n run: async () => {\n // Non-fatal: the step returns true unconditionally. The install RESULT is intentionally\n // not stored — the self-check re-derives CLI presence by probing (Story 6.2), so the\n // presence report is robust to a mid-run failure regardless of this step's outcome.\n // Windows: same `.cmd`-shim problem as BMad — `npm` can't spawn by bare name (shell:false),\n // so route through `node npm-cli.js …`. macOS/Linux keep the bare-`npm` default.\n await this.deps.installAgentCli({\n config: this.config,\n emitter: this.emitter,\n ...(this.deps.platform === 'win32'\n ? { npmCommand: process.execPath, npmPrefixArgs: [npmCliPath(process.execPath)] }\n : {}),\n });\n return true;\n },\n },\n {\n id: StepId.FinalizeSelfCheck,\n run: async () => {\n const summary = await this.deps.runSelfCheck({\n scaffoldCreated: this.scaffoldCreated,\n bmadInstalled: this.bmadInstalled,\n // Where the BMad manifest lives — self-check reads the ACTUAL installed version (FR26).\n projectDir: this.config.projectDir,\n // Thread the requested eligible CLI descriptors (SSOT accessor over config.installCli)\n // the same way scaffold/bmad outcomes are threaded — self-check probes each for presence.\n agentClis: eligibleAgentClis(this.config.installCli),\n emitter: this.emitter,\n });\n this.lastSummary = summary;\n return summary.success;\n },\n },\n ];\n }\n\n start(config?: Config): Promise<EngineRunResult> {\n void config; // config is fixed at construction; param kept for the EngineCommands contract\n return this.runFrom(0);\n }\n\n // Re-runs from `step` forward, skipping other completed steps. The named step is always\n // re-executed (cleared from `completed`). Returns a structured result for an unknown step\n // rather than throwing, so callers handle one shape.\n retry(step: StepId): Promise<EngineRunResult> {\n const idx = this.steps.findIndex((s) => s.id === step);\n if (idx < 0) {\n return Promise.resolve({ ok: false, failedStep: step, summary: this.lastSummary, logPath: null });\n }\n this.cancelled = false;\n this.completed.delete(step);\n return this.runFrom(idx);\n }\n\n cancel(): void {\n this.cancelled = true;\n }\n\n // Emit a provision-phase event the engine authors directly (the Node row; the Windows\n // git-present row). Step orchestrators emit their own events.\n private emit(\n step: StepId,\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void {\n this.emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step,\n status,\n humanMessage,\n level,\n timestamp: new Date().toISOString(),\n errorCode,\n });\n }\n\n private async runFrom(startIdx: number): Promise<EngineRunResult> {\n // Single-flight: the engine mutates shared state, so overlapping runs are a programming error.\n if (this.running) throw new Error('Engine is already running');\n this.running = true;\n try {\n for (let i = startIdx; i < this.steps.length; i++) {\n const step = this.steps[i];\n if (this.completed.has(step.id)) continue; // resume: skip already-done steps\n if (this.cancelled) {\n return { ok: false, failedStep: null, summary: this.lastSummary, logPath: null };\n }\n\n let ok: boolean;\n try {\n ok = await step.run();\n } catch (err) {\n const logPath = await this.fail(step.id, err);\n return { ok: false, failedStep: step.id, summary: this.lastSummary, logPath };\n }\n\n // Re-check cancellation after the (awaited) step so a mid-step cancel doesn't commit state.\n if (this.cancelled) {\n return { ok: false, failedStep: null, summary: this.lastSummary, logPath: null };\n }\n\n if (!ok) {\n const logPath = await this.fail(step.id, new Error(`Step ${step.id} did not succeed`));\n return { ok: false, failedStep: step.id, summary: this.lastSummary, logPath };\n }\n this.completed.add(step.id);\n }\n return { ok: true, failedStep: null, summary: this.lastSummary, logPath: null };\n } finally {\n this.running = false;\n }\n }\n\n // Writes the failure report; a log-write error must not mask the underlying step failure,\n // so it degrades to logPath: null rather than rejecting the run.\n private async fail(step: StepId, err: unknown): Promise<string | null> {\n const error =\n err instanceof Error\n ? (err.stack ?? err.message)\n : err !== null && typeof err === 'object'\n ? JSON.stringify(err)\n : String(err);\n try {\n return await this.deps.writeFailureLog({ step, error, events: this.emitter.events() });\n } catch {\n return null;\n }\n }\n}\n"],"mappings":"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","names":["join","randomUUID","join","join","exec","randomUUID","randomUUID","exec","exec","randomUUID","join","randomUUID","exec","readInstalledBmadVersion","randomUUID","randomUUID","exec","randomUUID","randomUUID","exec","randomUUID","mkdir","writeFile","join","randomUUID","randomUUID","randomUUID"]}
package/dist/cli/main.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  defaultBmadInstalled,
8
8
  pins,
9
9
  readInstalledBmadVersion
10
- } from "../chunk-6VTQGOJR.js";
10
+ } from "../chunk-DZ2RR3SP.js";
11
11
  import {
12
12
  openBrowser,
13
13
  startServer
@@ -26,7 +26,7 @@ function jsonSink(write) {
26
26
 
27
27
  // cli/verbose-sink.ts
28
28
  function verboseSink(write) {
29
- return (event) => write(`[${event.status}] ${event.step} \u2014 ${event.humanMessage}`);
29
+ return (event) => write(`[${event.status}] ${event.step} - ${event.humanMessage}`);
30
30
  }
31
31
 
32
32
  // cli/server-mode.ts
@@ -212,7 +212,7 @@ async function runServerMode(emitter, deps = {}) {
212
212
  exit(0);
213
213
  };
214
214
  openBrowser2(server.url, { platform: deps.platform });
215
- write(`Kindling is running \u2014 open ${server.url} if it didn't open automatically.`);
215
+ write(`Kindling is running - open ${server.url} if it didn't open automatically.`);
216
216
  return server;
217
217
  }
218
218
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../../cli/main.ts","../../cli/json-sink.ts","../../cli/verbose-sink.ts","../../cli/server-mode.ts","../../server/welcome.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport { EngineEmitter } from '../engine/emitter';\nimport type { EventListener } from '../engine/emitter';\nimport { jsonSink } from './json-sink';\nimport { verboseSink } from './verbose-sink';\nimport { runServerMode } from './server-mode';\n\nexport type Mode = 'server' | 'json' | 'verbose';\n\n/** Pick the run mode from argv. No flag → server (the default product frontend). */\nexport function selectMode(argv: string[]): Mode {\n const { values } = parseArgs({\n args: argv,\n options: { json: { type: 'boolean' }, verbose: { type: 'boolean' } },\n strict: false,\n });\n if (values.json && values.verbose) {\n process.stderr.write('Both --json and --verbose given; using --json.\\n');\n }\n if (values.json) return 'json';\n if (values.verbose) return 'verbose';\n return 'server';\n}\n\n/** Build the stdout sink for a mode. Server mode renders via the browser UI (Epic 3), not stdout. */\nexport function makeSink(mode: Mode, write: (line: string) => void): EventListener {\n if (mode === 'json') return jsonSink(write);\n if (mode === 'verbose') return verboseSink(write);\n return () => {};\n}\n\n/**\n * Wire an EngineEmitter to the chosen sink and return both. The real engine orchestration\n * (Stories 1.4–1.7) drives the returned emitter; this harness just renders its events.\n */\nexport interface RunDeps {\n /** Server-mode starter — injectable so tests don't bind a port / open a browser. */\n startServerMode?: (emitter: EngineEmitter) => void;\n}\n\nexport function run(\n argv: string[],\n write: (line: string) => void = (line) => process.stdout.write(line + '\\n'),\n deps: RunDeps = {},\n): { mode: Mode; emitter: EngineEmitter } {\n const mode = selectMode(argv);\n const emitter = new EngineEmitter();\n emitter.on(makeSink(mode, write));\n if (mode === 'server') {\n // Default: stand up the localhost server + browser UI (FR-12 lifecycle). Fire-and-forget —\n // the server keeps the process alive; a startup failure is surfaced, not thrown.\n const start =\n deps.startServerMode ??\n ((em: EngineEmitter) => {\n void runServerMode(em, { write }).catch((err) =>\n write(`Kindling failed to start the server: ${String(err)}`),\n );\n });\n start(emitter);\n }\n return { mode, emitter };\n}\n","import type { KindlingEvent } from '../engine/contract';\nimport type { EventListener } from '../engine/emitter';\n\n// JSON-lines sink: one parseable JSON object per event. Used by `--json` (CI/debug/technical).\nexport function jsonSink(write: (line: string) => void): EventListener {\n return (event: KindlingEvent) => write(JSON.stringify(event));\n}\n","import type { KindlingEvent } from '../engine/contract';\nimport type { EventListener } from '../engine/emitter';\n\n// Plain human-readable sink for `--verbose`. Interpolates event fields (no raw step literal).\nexport function verboseSink(write: (line: string) => void): EventListener {\n return (event: KindlingEvent) => write(`[${event.status}] ${event.step} — ${event.humanMessage}`);\n}\n","import { fileURLToPath } from 'node:url';\nimport type { Config, EngineCommands, StepId } from '../engine/contract';\nimport { pins } from '../engine/pins';\nimport type { EngineEmitter } from '../engine/emitter';\nimport { Engine } from '../engine/engine';\nimport { defaultBmadInstalled } from '../engine/orchestrate/bmad-install';\nimport { readInstalledBmadVersion as defaultReadInstalledBmadVersion } from '../engine/bmad-manifest';\nimport { startServer as defaultStartServer, type RunningServer, type ServerCommands } from '../server/server';\nimport { openBrowser as defaultOpenBrowser } from '../server/open-browser';\nimport { writeWelcomeHtml as defaultWriteWelcome } from '../server/welcome';\nimport { expandTilde } from '../engine/expand-tilde';\n\n// Injectable seams so the lifecycle is unit-testable without a real install / browser / exit.\nexport interface ServerModeDeps {\n startServer?: typeof defaultStartServer;\n openBrowser?: typeof defaultOpenBrowser;\n writeWelcome?: typeof defaultWriteWelcome;\n /** Builds the engine that a /start drives — injected so tests use a fake. */\n engineFactory?: (config: Config, emitter: EngineEmitter) => EngineCommands<unknown>;\n /** Process exit, after the ephemeral server has written welcome.html + closed. */\n exit?: (code: number) => void;\n write?: (line: string) => void;\n platform?: NodeJS.Platform;\n /** Built UI directory to serve (dist/ui); resolved by the bootstrap. */\n uiDir?: string;\n /**\n * Read-only `_bmad`-dir check for the Configure-time inspect probe (Story 7.2). Default is the\n * engine's `defaultBmadInstalled`; injected as a fake in tests (no real fs).\n */\n bmadAlreadyInstalled?: (dir: string) => Promise<boolean>;\n /**\n * Read the ACTUAL installed BMad version from the manifest (7.1). Default is the engine helper;\n * injected as a fake in tests (no real fs).\n */\n readInstalledBmadVersion?: (dir: string) => Promise<string | null>;\n}\n\n/**\n * Server mode (FR-12 lifecycle). Stands up the localhost server, lazily builds the engine when\n * the browser POSTs /start (the config arrives from the Configure screen), and on the Welcome\n * render-ack writes the self-contained welcome.html (so a refresh works after exit), closes the\n * server, and exits — ONLY on success. On failure nothing acks, so the server stays alive for\n * Retry. The live browser→install→exit run is validated at the dress rehearsal.\n */\nexport async function runServerMode(\n emitter: EngineEmitter,\n deps: ServerModeDeps = {},\n): Promise<RunningServer> {\n const startServer = deps.startServer ?? defaultStartServer;\n const openBrowser = deps.openBrowser ?? defaultOpenBrowser;\n const writeWelcome = deps.writeWelcome ?? defaultWriteWelcome;\n const engineFactory =\n deps.engineFactory ?? ((config, em) => new Engine(config, em));\n const exit = deps.exit ?? ((code) => process.exit(code));\n const write = deps.write ?? ((line) => process.stdout.write(line + '\\n'));\n // The built layout is dist/cli/server-mode.js alongside dist/ui — serve that by default.\n const uiDir = deps.uiDir ?? fileURLToPath(new URL('../ui', import.meta.url));\n const bmadAlreadyInstalled = deps.bmadAlreadyInstalled ?? defaultBmadInstalled;\n const readInstalledBmadVersion = deps.readInstalledBmadVersion ?? defaultReadInstalledBmadVersion;\n\n let lastConfig: Config | null = null;\n let engine: EngineCommands<unknown> | null = null;\n\n const commands: ServerCommands = {\n start: (config) => {\n // Expand a leading `~` ONCE, at intake, so every consumer — the Engine AND the Welcome\n // writer in finish() — sees a real path (the UI default is the literal `~/kindling-project`).\n const c: Config = { ...config, projectDir: expandTilde(config.projectDir) };\n lastConfig = c;\n engine = engineFactory(c, emitter);\n return engine.start(c);\n },\n cancel: () => engine?.cancel(),\n retry: (step: StepId) => engine?.retry(step),\n // Pure read-only fs probe (Story 7.2) — wraps neither the engine nor any run state. Both reads\n // degrade to false/null, so /inspect can't crash on a bad path.\n inspect: async (projectDir) => ({\n isKindlingProject: await bmadAlreadyInstalled(projectDir),\n installedBmadVersion: await readInstalledBmadVersion(projectDir),\n }),\n };\n\n let acked = false; // one-shot: the host shutdown must run at most once\n const server = await startServer({\n emitter,\n commands,\n uiDir,\n onWelcomeAck: () => {\n if (acked) return;\n acked = true;\n void finish().catch(() => exit(0));\n },\n });\n\n // On success: persist the self-contained Welcome page (survives exit), then close + exit.\n const finish = async (): Promise<void> => {\n const done = emitter.events().find((e) => typeof e.summaryJson === 'string');\n if (lastConfig && done?.summaryJson) {\n // Best-effort: persisting welcome.html is a nicety, not required for a successful install —\n // a write failure must not crash the process or leave the server hanging.\n try {\n await writeWelcome(lastConfig.projectDir, {\n bmadVersion: pins.bmad,\n summaryJson: done.summaryJson,\n });\n } catch {\n // ignore — proceed to a clean shutdown regardless\n }\n }\n await server.close();\n exit(0);\n };\n\n openBrowser(server.url, { platform: deps.platform });\n write(`Kindling is running — open ${server.url} if it didn't open automatically.`);\n return server;\n}\n","import { writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n cliLoginGuidance,\n cliMissing,\n bmadVersionLabel,\n type CliPresence,\n type ValidationSummary,\n} from '../engine/validation-summary';\n\nexport interface WelcomeData {\n /** Pinned BMad version — the fallback for the versions table's BMad row. */\n bmadVersion: string;\n /** The engine's Validation Summary JSON (already serialized) — the table reads versions from it. */\n summaryJson: string;\n}\n\n// HTML-escape for safe interpolation into the static page (the summary is machine-generated,\n// but escape defensively so no value can break out of the text/attribute context).\nfunction esc(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n// Join a list of already-escaped fragments with English separators (\", \", \" or \" / \" and \").\nfunction joinHuman(parts: string[], last: string): string {\n if (parts.length <= 1) return parts.join('');\n return parts.slice(0, -1).join(', ') + ` ${last} ` + parts[parts.length - 1];\n}\n\n// Derive the FR25 login line + the AC-8 install-it-yourself notice from the embedded summary's\n// `cli` presence (the SAME pure helpers the React Welcome uses, so the copy is identical). Returns\n// escaped HTML fragments; empty string when the summary has no CLI info (line simply omitted).\nfunction cliGuidanceHtml(summaryJson: string): string {\n let cli: CliPresence[];\n try {\n const parsed = JSON.parse(summaryJson) as Pick<ValidationSummary, 'cli'>;\n cli = Array.isArray(parsed.cli) ? parsed.cli : [];\n } catch {\n return '';\n }\n const present = cliLoginGuidance({ cli });\n const missing = cliMissing({ cli });\n let html = '';\n if (present.length > 0) {\n const cmds = joinHuman(\n present.map((c) => `<code>${esc(c.bin)}</code>`),\n 'or',\n );\n html += ` <p class=\"cli-guidance\"><b>One last step:</b> open a terminal, run ${cmds}, and log in, then you're all set.</p>\\n`;\n }\n if (missing.length > 0) {\n const cmds = joinHuman(\n missing.map((c) => `<code>npm install -g ${esc(c.pkg)}</code>`),\n 'and',\n );\n html += ` <p class=\"cli-guidance\">Your AI assistant didn’t finish installing. Your project is still ready. To install it yourself, run ${cmds}, then start it and log in.</p>\\n`;\n }\n return html;\n}\n\n/**\n * Build the self-contained static Welcome page. It has NO external assets and embeds the\n * Validation Summary inline, so it keeps rendering on refresh after the ephemeral server has\n * exited (FR-12). Pure — returns the HTML string.\n */\n// Read the honest version chip (Story 7.2 / AC-6) from the embedded summary. A latest run reports\n// the ACTUAL installed version + \"updated to latest\"; the default pinned run (absent/null/equal\n// installedVersion) keeps `bmadVersion` + \"a stable, tested version\". Tolerant of malformed JSON.\nfunction versionChip(summaryJson: string, pinnedFallback: string): { version: string; note: string } {\n try {\n const parsed = JSON.parse(summaryJson) as { bmad?: { installedVersion?: string | null } };\n return bmadVersionLabel(parsed, pinnedFallback);\n } catch {\n return bmadVersionLabel(null, pinnedFallback);\n }\n}\n\n// Build the \"what's installed\" table from the embedded summary (mirrors the React Welcome table).\nfunction versionsTableHtml(summaryJson: string, pinnedFallback: string): string {\n let node: { version: string | null } | undefined;\n let git: { version: string | null } | undefined;\n let cli: CliPresence[] = [];\n try {\n const p = JSON.parse(summaryJson) as {\n node?: { version: string | null };\n git?: { version: string | null };\n cli?: CliPresence[];\n };\n node = p.node;\n git = p.git;\n cli = Array.isArray(p.cli) ? p.cli : [];\n } catch {\n // Malformed summary: still show the BMad row (versionChip falls back to the pin).\n }\n const chip = versionChip(summaryJson, pinnedFallback);\n const rows: string[] = [];\n if (node) rows.push(`<tr><th scope=\"row\">Node.js</th><td>${esc(node.version ?? 'Installed')}</td></tr>`);\n if (git) rows.push(`<tr><th scope=\"row\">Git</th><td>${esc(git.version ?? 'Installed')}</td></tr>`);\n rows.push(\n `<tr><th scope=\"row\">BMad Method</th><td><strong>${esc(chip.version)}</strong> &middot; ${esc(chip.note)}</td></tr>`,\n );\n for (const c of cli) {\n rows.push(\n `<tr><th scope=\"row\">${esc(c.name)}</th><td>${c.present ? '&#10003; Installed' : 'Not installed'}</td></tr>`,\n );\n }\n return ` <table class=\"versions\">\\n <caption>Here's what's set up on your computer</caption>\\n <tbody>\\n${rows\n .map((r) => ' ' + r)\n .join('\\n')}\\n </tbody>\\n </table>\\n`;\n}\n\n/**\n * Build the self-contained static Welcome page. NO external assets, so it keeps rendering on refresh\n * after the ephemeral server has exited (FR-12). Shows the versions table + the CLI login step.\n * Pure — returns the HTML string.\n */\nexport function buildWelcomeHtml(data: WelcomeData): string {\n const versionsTable = versionsTableHtml(data.summaryJson, data.bmadVersion);\n const cliGuidance = cliGuidanceHtml(data.summaryJson);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>Kindling: You're ready</title>\n<style>\n :root { color-scheme: dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n background:#1b1410; color:#fff6ee;\n font-family:-apple-system,\"Segoe UI Variable\",\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; }\n main { max-width:640px; padding:48px 32px; }\n h1 { font-size:34px; font-weight:900; letter-spacing:-.02em; margin:0 0 8px; }\n .eyebrow { font-size:12px; font-weight:800; letter-spacing:.16em; text-transform:uppercase;\n color:#ffc24a; margin:0; }\n .lede { font-size:20px; color:#d6b9a3; }\n a { color:#ff6a1f; }\n .cli-guidance { color:#d6b9a3; }\n code { background:#34271e; border:1px solid #5a3c20; border-radius:6px; padding:1px 6px;\n color:#ffc24a; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }\n table.versions { width:100%; border-collapse:collapse; margin:24px 0; text-align:left; }\n .versions caption { text-align:left; color:#d6b9a3; font-weight:700; margin-bottom:8px; }\n .versions th, .versions td { padding:8px 12px; border-bottom:1px solid #463429; }\n .versions th { font-weight:700; color:#d6b9a3; }\n .versions td { color:#fff6ee; }\n</style>\n</head>\n<body>\n<main>\n <p class=\"eyebrow\">All set</p>\n <h1>You're ready &#128293;</h1>\n <p class=\"lede\">Your project is set up with BMad and your tools.</p>\n${versionsTable}${cliGuidance} <p><a href=\"https://aiviatic.com\" target=\"_blank\" rel=\"noreferrer\">Join an Aiviatic workshop</a>, totally optional.</p>\n</main>\n</body>\n</html>\n`;\n}\n\n/** Write the self-contained Welcome page to `dir/welcome.html` (injectable writer for tests). */\nexport async function writeWelcomeHtml(\n dir: string,\n data: WelcomeData,\n write: (path: string, contents: string) => Promise<void> = (p, c) => writeFile(p, c, 'utf8'),\n): Promise<string> {\n const path = join(dir, 'welcome.html');\n await write(path, buildWelcomeHtml(data));\n return path;\n}\n"],"mappings":"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","names":["startServer","openBrowser","readInstalledBmadVersion"]}
1
+ {"version":3,"sources":["../../cli/main.ts","../../cli/json-sink.ts","../../cli/verbose-sink.ts","../../cli/server-mode.ts","../../server/welcome.ts"],"sourcesContent":["import { parseArgs } from 'node:util';\nimport { EngineEmitter } from '../engine/emitter';\nimport type { EventListener } from '../engine/emitter';\nimport { jsonSink } from './json-sink';\nimport { verboseSink } from './verbose-sink';\nimport { runServerMode } from './server-mode';\n\nexport type Mode = 'server' | 'json' | 'verbose';\n\n/** Pick the run mode from argv. No flag → server (the default product frontend). */\nexport function selectMode(argv: string[]): Mode {\n const { values } = parseArgs({\n args: argv,\n options: { json: { type: 'boolean' }, verbose: { type: 'boolean' } },\n strict: false,\n });\n if (values.json && values.verbose) {\n process.stderr.write('Both --json and --verbose given; using --json.\\n');\n }\n if (values.json) return 'json';\n if (values.verbose) return 'verbose';\n return 'server';\n}\n\n/** Build the stdout sink for a mode. Server mode renders via the browser UI (Epic 3), not stdout. */\nexport function makeSink(mode: Mode, write: (line: string) => void): EventListener {\n if (mode === 'json') return jsonSink(write);\n if (mode === 'verbose') return verboseSink(write);\n return () => {};\n}\n\n/**\n * Wire an EngineEmitter to the chosen sink and return both. The real engine orchestration\n * (Stories 1.4–1.7) drives the returned emitter; this harness just renders its events.\n */\nexport interface RunDeps {\n /** Server-mode starter — injectable so tests don't bind a port / open a browser. */\n startServerMode?: (emitter: EngineEmitter) => void;\n}\n\nexport function run(\n argv: string[],\n write: (line: string) => void = (line) => process.stdout.write(line + '\\n'),\n deps: RunDeps = {},\n): { mode: Mode; emitter: EngineEmitter } {\n const mode = selectMode(argv);\n const emitter = new EngineEmitter();\n emitter.on(makeSink(mode, write));\n if (mode === 'server') {\n // Default: stand up the localhost server + browser UI (FR-12 lifecycle). Fire-and-forget —\n // the server keeps the process alive; a startup failure is surfaced, not thrown.\n const start =\n deps.startServerMode ??\n ((em: EngineEmitter) => {\n void runServerMode(em, { write }).catch((err) =>\n write(`Kindling failed to start the server: ${String(err)}`),\n );\n });\n start(emitter);\n }\n return { mode, emitter };\n}\n","import type { KindlingEvent } from '../engine/contract';\nimport type { EventListener } from '../engine/emitter';\n\n// JSON-lines sink: one parseable JSON object per event. Used by `--json` (CI/debug/technical).\nexport function jsonSink(write: (line: string) => void): EventListener {\n return (event: KindlingEvent) => write(JSON.stringify(event));\n}\n","import type { KindlingEvent } from '../engine/contract';\nimport type { EventListener } from '../engine/emitter';\n\n// Plain human-readable sink for `--verbose`. Interpolates event fields (no raw step literal).\nexport function verboseSink(write: (line: string) => void): EventListener {\n return (event: KindlingEvent) => write(`[${event.status}] ${event.step} - ${event.humanMessage}`);\n}\n","import { fileURLToPath } from 'node:url';\nimport type { Config, EngineCommands, StepId } from '../engine/contract';\nimport { pins } from '../engine/pins';\nimport type { EngineEmitter } from '../engine/emitter';\nimport { Engine } from '../engine/engine';\nimport { defaultBmadInstalled } from '../engine/orchestrate/bmad-install';\nimport { readInstalledBmadVersion as defaultReadInstalledBmadVersion } from '../engine/bmad-manifest';\nimport { startServer as defaultStartServer, type RunningServer, type ServerCommands } from '../server/server';\nimport { openBrowser as defaultOpenBrowser } from '../server/open-browser';\nimport { writeWelcomeHtml as defaultWriteWelcome } from '../server/welcome';\nimport { expandTilde } from '../engine/expand-tilde';\n\n// Injectable seams so the lifecycle is unit-testable without a real install / browser / exit.\nexport interface ServerModeDeps {\n startServer?: typeof defaultStartServer;\n openBrowser?: typeof defaultOpenBrowser;\n writeWelcome?: typeof defaultWriteWelcome;\n /** Builds the engine that a /start drives — injected so tests use a fake. */\n engineFactory?: (config: Config, emitter: EngineEmitter) => EngineCommands<unknown>;\n /** Process exit, after the ephemeral server has written welcome.html + closed. */\n exit?: (code: number) => void;\n write?: (line: string) => void;\n platform?: NodeJS.Platform;\n /** Built UI directory to serve (dist/ui); resolved by the bootstrap. */\n uiDir?: string;\n /**\n * Read-only `_bmad`-dir check for the Configure-time inspect probe (Story 7.2). Default is the\n * engine's `defaultBmadInstalled`; injected as a fake in tests (no real fs).\n */\n bmadAlreadyInstalled?: (dir: string) => Promise<boolean>;\n /**\n * Read the ACTUAL installed BMad version from the manifest (7.1). Default is the engine helper;\n * injected as a fake in tests (no real fs).\n */\n readInstalledBmadVersion?: (dir: string) => Promise<string | null>;\n}\n\n/**\n * Server mode (FR-12 lifecycle). Stands up the localhost server, lazily builds the engine when\n * the browser POSTs /start (the config arrives from the Configure screen), and on the Welcome\n * render-ack writes the self-contained welcome.html (so a refresh works after exit), closes the\n * server, and exits — ONLY on success. On failure nothing acks, so the server stays alive for\n * Retry. The live browser→install→exit run is validated at the dress rehearsal.\n */\nexport async function runServerMode(\n emitter: EngineEmitter,\n deps: ServerModeDeps = {},\n): Promise<RunningServer> {\n const startServer = deps.startServer ?? defaultStartServer;\n const openBrowser = deps.openBrowser ?? defaultOpenBrowser;\n const writeWelcome = deps.writeWelcome ?? defaultWriteWelcome;\n const engineFactory =\n deps.engineFactory ?? ((config, em) => new Engine(config, em));\n const exit = deps.exit ?? ((code) => process.exit(code));\n const write = deps.write ?? ((line) => process.stdout.write(line + '\\n'));\n // The built layout is dist/cli/server-mode.js alongside dist/ui — serve that by default.\n const uiDir = deps.uiDir ?? fileURLToPath(new URL('../ui', import.meta.url));\n const bmadAlreadyInstalled = deps.bmadAlreadyInstalled ?? defaultBmadInstalled;\n const readInstalledBmadVersion = deps.readInstalledBmadVersion ?? defaultReadInstalledBmadVersion;\n\n let lastConfig: Config | null = null;\n let engine: EngineCommands<unknown> | null = null;\n\n const commands: ServerCommands = {\n start: (config) => {\n // Expand a leading `~` ONCE, at intake, so every consumer — the Engine AND the Welcome\n // writer in finish() — sees a real path (the UI default is the literal `~/kindling-project`).\n const c: Config = { ...config, projectDir: expandTilde(config.projectDir) };\n lastConfig = c;\n engine = engineFactory(c, emitter);\n return engine.start(c);\n },\n cancel: () => engine?.cancel(),\n retry: (step: StepId) => engine?.retry(step),\n // Pure read-only fs probe (Story 7.2) — wraps neither the engine nor any run state. Both reads\n // degrade to false/null, so /inspect can't crash on a bad path.\n inspect: async (projectDir) => ({\n isKindlingProject: await bmadAlreadyInstalled(projectDir),\n installedBmadVersion: await readInstalledBmadVersion(projectDir),\n }),\n };\n\n let acked = false; // one-shot: the host shutdown must run at most once\n const server = await startServer({\n emitter,\n commands,\n uiDir,\n onWelcomeAck: () => {\n if (acked) return;\n acked = true;\n void finish().catch(() => exit(0));\n },\n });\n\n // On success: persist the self-contained Welcome page (survives exit), then close + exit.\n const finish = async (): Promise<void> => {\n const done = emitter.events().find((e) => typeof e.summaryJson === 'string');\n if (lastConfig && done?.summaryJson) {\n // Best-effort: persisting welcome.html is a nicety, not required for a successful install —\n // a write failure must not crash the process or leave the server hanging.\n try {\n await writeWelcome(lastConfig.projectDir, {\n bmadVersion: pins.bmad,\n summaryJson: done.summaryJson,\n });\n } catch {\n // ignore — proceed to a clean shutdown regardless\n }\n }\n await server.close();\n exit(0);\n };\n\n openBrowser(server.url, { platform: deps.platform });\n write(`Kindling is running - open ${server.url} if it didn't open automatically.`);\n return server;\n}\n","import { writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport {\n cliLoginGuidance,\n cliMissing,\n bmadVersionLabel,\n type CliPresence,\n type ValidationSummary,\n} from '../engine/validation-summary';\n\nexport interface WelcomeData {\n /** Pinned BMad version — the fallback for the versions table's BMad row. */\n bmadVersion: string;\n /** The engine's Validation Summary JSON (already serialized) — the table reads versions from it. */\n summaryJson: string;\n}\n\n// HTML-escape for safe interpolation into the static page (the summary is machine-generated,\n// but escape defensively so no value can break out of the text/attribute context).\nfunction esc(s: string): string {\n return s\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&#39;');\n}\n\n// Join a list of already-escaped fragments with English separators (\", \", \" or \" / \" and \").\nfunction joinHuman(parts: string[], last: string): string {\n if (parts.length <= 1) return parts.join('');\n return parts.slice(0, -1).join(', ') + ` ${last} ` + parts[parts.length - 1];\n}\n\n// Derive the FR25 login line + the AC-8 install-it-yourself notice from the embedded summary's\n// `cli` presence (the SAME pure helpers the React Welcome uses, so the copy is identical). Returns\n// escaped HTML fragments; empty string when the summary has no CLI info (line simply omitted).\nfunction cliGuidanceHtml(summaryJson: string): string {\n let cli: CliPresence[];\n try {\n const parsed = JSON.parse(summaryJson) as Pick<ValidationSummary, 'cli'>;\n cli = Array.isArray(parsed.cli) ? parsed.cli : [];\n } catch {\n return '';\n }\n const present = cliLoginGuidance({ cli });\n const missing = cliMissing({ cli });\n let html = '';\n if (present.length > 0) {\n const cmds = joinHuman(\n present.map((c) => `<code>${esc(c.bin)}</code>`),\n 'or',\n );\n html += ` <p class=\"cli-guidance\"><b>One last step:</b> open a terminal, run ${cmds}, and log in, then you're all set.</p>\\n`;\n }\n if (missing.length > 0) {\n const cmds = joinHuman(\n missing.map((c) => `<code>npm install -g ${esc(c.pkg)}</code>`),\n 'and',\n );\n html += ` <p class=\"cli-guidance\">Your AI assistant didn’t finish installing. Your project is still ready. To install it yourself, run ${cmds}, then start it and log in.</p>\\n`;\n }\n return html;\n}\n\n/**\n * Build the self-contained static Welcome page. It has NO external assets and embeds the\n * Validation Summary inline, so it keeps rendering on refresh after the ephemeral server has\n * exited (FR-12). Pure — returns the HTML string.\n */\n// Read the honest version chip (Story 7.2 / AC-6) from the embedded summary. A latest run reports\n// the ACTUAL installed version + \"updated to latest\"; the default pinned run (absent/null/equal\n// installedVersion) keeps `bmadVersion` + \"a stable, tested version\". Tolerant of malformed JSON.\nfunction versionChip(summaryJson: string, pinnedFallback: string): { version: string; note: string } {\n try {\n const parsed = JSON.parse(summaryJson) as { bmad?: { installedVersion?: string | null } };\n return bmadVersionLabel(parsed, pinnedFallback);\n } catch {\n return bmadVersionLabel(null, pinnedFallback);\n }\n}\n\n// Build the \"what's installed\" table from the embedded summary (mirrors the React Welcome table).\nfunction versionsTableHtml(summaryJson: string, pinnedFallback: string): string {\n let node: { version: string | null } | undefined;\n let git: { version: string | null } | undefined;\n let cli: CliPresence[] = [];\n try {\n const p = JSON.parse(summaryJson) as {\n node?: { version: string | null };\n git?: { version: string | null };\n cli?: CliPresence[];\n };\n node = p.node;\n git = p.git;\n cli = Array.isArray(p.cli) ? p.cli : [];\n } catch {\n // Malformed summary: still show the BMad row (versionChip falls back to the pin).\n }\n const chip = versionChip(summaryJson, pinnedFallback);\n const rows: string[] = [];\n if (node) rows.push(`<tr><th scope=\"row\">Node.js</th><td>${esc(node.version ?? 'Installed')}</td></tr>`);\n if (git) rows.push(`<tr><th scope=\"row\">Git</th><td>${esc(git.version ?? 'Installed')}</td></tr>`);\n rows.push(\n `<tr><th scope=\"row\">BMad Method</th><td><strong>${esc(chip.version)}</strong> &middot; ${esc(chip.note)}</td></tr>`,\n );\n for (const c of cli) {\n rows.push(\n `<tr><th scope=\"row\">${esc(c.name)}</th><td>${c.present ? '&#10003; Installed' : 'Not installed'}</td></tr>`,\n );\n }\n return ` <table class=\"versions\">\\n <caption>Here's what's set up on your computer</caption>\\n <tbody>\\n${rows\n .map((r) => ' ' + r)\n .join('\\n')}\\n </tbody>\\n </table>\\n`;\n}\n\n/**\n * Build the self-contained static Welcome page. NO external assets, so it keeps rendering on refresh\n * after the ephemeral server has exited (FR-12). Shows the versions table + the CLI login step.\n * Pure — returns the HTML string.\n */\nexport function buildWelcomeHtml(data: WelcomeData): string {\n const versionsTable = versionsTableHtml(data.summaryJson, data.bmadVersion);\n const cliGuidance = cliGuidanceHtml(data.summaryJson);\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\" />\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n<title>Kindling: You're ready</title>\n<style>\n :root { color-scheme: dark; }\n body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;\n background:#1b1410; color:#fff6ee;\n font-family:-apple-system,\"Segoe UI Variable\",\"Segoe UI\",Roboto,Helvetica,Arial,sans-serif; }\n main { max-width:640px; padding:48px 32px; }\n h1 { font-size:34px; font-weight:900; letter-spacing:-.02em; margin:0 0 8px; }\n .eyebrow { font-size:12px; font-weight:800; letter-spacing:.16em; text-transform:uppercase;\n color:#ffc24a; margin:0; }\n .lede { font-size:20px; color:#d6b9a3; }\n a { color:#ff6a1f; }\n .cli-guidance { color:#d6b9a3; }\n code { background:#34271e; border:1px solid #5a3c20; border-radius:6px; padding:1px 6px;\n color:#ffc24a; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }\n table.versions { width:100%; border-collapse:collapse; margin:24px 0; text-align:left; }\n .versions caption { text-align:left; color:#d6b9a3; font-weight:700; margin-bottom:8px; }\n .versions th, .versions td { padding:8px 12px; border-bottom:1px solid #463429; }\n .versions th { font-weight:700; color:#d6b9a3; }\n .versions td { color:#fff6ee; }\n</style>\n</head>\n<body>\n<main>\n <p class=\"eyebrow\">All set</p>\n <h1>You're ready &#128293;</h1>\n <p class=\"lede\">Your project is set up with BMad and your tools.</p>\n${versionsTable}${cliGuidance} <p><a href=\"https://aiviatic.com\" target=\"_blank\" rel=\"noreferrer\">Join an Aiviatic workshop</a>, totally optional.</p>\n</main>\n</body>\n</html>\n`;\n}\n\n/** Write the self-contained Welcome page to `dir/welcome.html` (injectable writer for tests). */\nexport async function writeWelcomeHtml(\n dir: string,\n data: WelcomeData,\n write: (path: string, contents: string) => Promise<void> = (p, c) => writeFile(p, c, 'utf8'),\n): Promise<string> {\n const path = join(dir, 'welcome.html');\n await write(path, buildWelcomeHtml(data));\n return path;\n}\n"],"mappings":"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","names":["startServer","openBrowser","readInstalledBmadVersion"]}
@@ -101,6 +101,13 @@ interface BmadInstallOptions {
101
101
  * — see deferred-work.md.
102
102
  */
103
103
  npxCommand?: string;
104
+ /**
105
+ * Args prepended to the exec argv, before the `bmad-method@<tag>` spec. Default `[]`. On Windows
106
+ * `npxCommand` is the provisioned `node` and this carries `[npxCliPath(node)]`, so the effective
107
+ * invocation is `node npx-cli.js bmad-method@<tag> …` — the shell:false-safe equivalent of the
108
+ * bare `npx.cmd` shim (which spawn can't find by bare name on Windows). Empty on macOS/Linux.
109
+ */
110
+ npxPrefixArgs?: string[];
104
111
  /**
105
112
  * Idempotent re-run (2.7): is BMad already installed in this project? When true, the install
106
113
  * runs with `--action update` (update in place) instead of a fresh install. Default detects a
@@ -167,6 +174,14 @@ interface AgentCliOptions {
167
174
  * `npxCommand`, the seam defaults to 'npm' and the rehearsal / Story 6.2 plugs the abs path in.
168
175
  */
169
176
  npmCommand?: string;
177
+ /**
178
+ * Args prepended to the exec argv, before `install -g <pkg>`. Default `[]`. On Windows `npmCommand`
179
+ * is the provisioned `node` and this carries `[npmCliPath(node)]`, so the effective invocation is
180
+ * `node npm-cli.js install -g <pkg>` — the shell:false-safe equivalent of the bare `npm.cmd` shim
181
+ * (which spawn can't find by bare name on Windows). Empty on macOS/Linux. Mirrors bmad-install's
182
+ * `npxPrefixArgs`.
183
+ */
184
+ npmPrefixArgs?: string[];
170
185
  /**
171
186
  * Resolve an agent CLI's bin name to a spawnable form before the idempotent-skip probe. Default:
172
187
  * identity. On Windows a global `npm install -g` writes a `claude.cmd`/`.ps1` shim (not a bare
@@ -9,12 +9,14 @@ import {
9
9
  cliLoginGuidance,
10
10
  cliMissing,
11
11
  composeInstallArgs,
12
+ composeLaunchCommand,
12
13
  defaultBmadInstalled,
13
14
  defaultLogDir,
14
15
  detectDependencies,
15
16
  eligibleAgentClis,
16
17
  errorMessages,
17
18
  installAgentCli,
19
+ npxCliPath,
18
20
  parseMajor,
19
21
  pins,
20
22
  provisionGitUnix,
@@ -26,7 +28,7 @@ import {
26
28
  scaffold,
27
29
  stepMessages,
28
30
  writeFailureLog
29
- } from "../chunk-6VTQGOJR.js";
31
+ } from "../chunk-DZ2RR3SP.js";
30
32
  import {
31
33
  ErrorCode,
32
34
  NON_FATAL_STEPS,
@@ -36,21 +38,8 @@ import {
36
38
  exec
37
39
  } from "../chunk-OU3WSB6B.js";
38
40
 
39
- // engine/orchestrate/launch.ts
40
- import { dirname, join } from "path";
41
- function npxCliPath(nodeExe) {
42
- return join(dirname(nodeExe), "node_modules", "npm", "bin", "npx-cli.js");
43
- }
44
- function composeLaunchCommand({ nodeExe, kindlingVersion }) {
45
- const spec = `@aiviatic/kindling@${kindlingVersion}`;
46
- if (nodeExe === null) {
47
- return { cmd: "npx", args: ["-y", spec] };
48
- }
49
- return { cmd: nodeExe, args: [npxCliPath(nodeExe), "-y", spec] };
50
- }
51
-
52
41
  // engine/provision/node-windows.ts
53
- import { join as join2 } from "path";
42
+ import { join } from "path";
54
43
  import { mkdir, writeFile } from "fs/promises";
55
44
  import { randomUUID } from "crypto";
56
45
  function nodeDistUrl(version, arch = "x64") {
@@ -59,7 +48,7 @@ function nodeDistUrl(version, arch = "x64") {
59
48
  }
60
49
  function nodeExePath(baseDir, version, arch = "x64") {
61
50
  const v = version.startsWith("v") ? version : `v${version}`;
62
- return join2(baseDir, `node-${v}-win-${arch}`, "node.exe");
51
+ return join(baseDir, `node-${v}-win-${arch}`, "node.exe");
63
52
  }
64
53
  async function defaultDownload(url, dest) {
65
54
  const res = await fetch(url);
@@ -96,7 +85,7 @@ async function provisionNodeWindows(opts) {
96
85
  try {
97
86
  await mkdir(opts.baseDir, { recursive: true });
98
87
  const url = nodeDistUrl(opts.version, arch);
99
- const zip = join2(opts.baseDir, `node-${opts.version}-win-${arch}.zip`);
88
+ const zip = join(opts.baseDir, `node-${opts.version}-win-${arch}.zip`);
100
89
  await download(url, zip);
101
90
  await extract(zip, opts.baseDir);
102
91
  } catch (err) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../engine/orchestrate/launch.ts","../../engine/provision/node-windows.ts","../../engine/provision/node-unix.ts"],"sourcesContent":["import { dirname, join } from 'node:path';\n\nexport interface LaunchRuntime {\n /** Absolute path to the provisioned node binary, or null to use `node`/`npx` on PATH\n * (a reused system Node, or nvm sourced same-shell). See ProvisionResult.nodeExe. */\n nodeExe: string | null;\n /** Pinned Kindling version (pins.kindling). */\n kindlingVersion: string;\n}\n\nexport interface LaunchCommand {\n cmd: string;\n args: string[];\n}\n\n/** Path to npx's CLI script beside a provisioned node binary (the npm bundled with the dist). */\nexport function npxCliPath(nodeExe: string): string {\n return join(dirname(nodeExe), 'node_modules', 'npm', 'bin', 'npx-cli.js');\n}\n\n/**\n * Compose the command to launch Kindling once Node is provisioned (Story 2.6). The clean-runtime\n * rule (AR6): never depend on a freshly-mutated PATH —\n * - `nodeExe === null` → Node is already on PATH (nvm sourced same-shell, or a reused system\n * Node) → plain `npx -y @aiviatic/kindling@<pin>`.\n * - `nodeExe` set (Windows portable, absolute) → invoke npx's CLI through that exact node binary,\n * so the launch works without the portable Node ever being on PATH.\n * Pure — the bootstrap (or a future Windows launcher) spawns the returned command.\n */\nexport function composeLaunchCommand({ nodeExe, kindlingVersion }: LaunchRuntime): LaunchCommand {\n const spec = `@aiviatic/kindling@${kindlingVersion}`;\n if (nodeExe === null) {\n return { cmd: 'npx', args: ['-y', spec] };\n }\n return { cmd: nodeExe, args: [npxCliPath(nodeExe), '-y', spec] };\n}\n","import { join } from 'node:path';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from '../contract';\nimport { stepMessages, provisionMessages } from '../messages';\n\nexport type NodeArch = 'x64' | 'arm64';\n\n/** Official Node Windows .zip URL for a pinned version. Pure. */\nexport function nodeDistUrl(version: string, arch: NodeArch = 'x64'): string {\n const v = version.startsWith('v') ? version : `v${version}`;\n return `https://nodejs.org/dist/${v}/node-${v}-win-${arch}.zip`;\n}\n\n/** Absolute path to node.exe inside the extracted distribution. Pure. */\nexport function nodeExePath(baseDir: string, version: string, arch: NodeArch = 'x64'): string {\n const v = version.startsWith('v') ? version : `v${version}`;\n return join(baseDir, `node-${v}-win-${arch}`, 'node.exe');\n}\n\nexport interface ProvisionNodeWindowsOptions {\n /** Pinned Node version (pins.node). */\n version: string;\n /** Install root, e.g. %LOCALAPPDATA%\\kindling\\node. */\n baseDir: string;\n arch?: NodeArch;\n emitter: EngineEmitter;\n /** From 2.1 detection: a Node ≥ floor is already present → skip the download. */\n alreadyOk?: boolean;\n /** Injectable so tests run without network/Windows. Defaults below are spike-validation-pending. */\n download?: (url: string, dest: string) => Promise<void>;\n extract?: (zip: string, destDir: string) => Promise<void>;\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n now?: () => string;\n}\n\n// Default download: global fetch → file. UNVALIDATED on real Windows (spike / rehearsal).\nasync function defaultDownload(url: string, dest: string): Promise<void> {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`download failed: ${res.status} ${url}`);\n const buf = Buffer.from(await res.arrayBuffer());\n await writeFile(dest, buf);\n}\n\nexport interface ProvisionResult {\n ok: boolean;\n /**\n * Absolute path to the provisioned node binary for the launch step (2.6) to invoke directly\n * (the clean-runtime rule — never rely on a mutated PATH). `null` means an existing system\n * Node was reused → the launch resolves `node` from PATH instead.\n */\n nodeExe: string | null;\n}\n\nexport async function provisionNodeWindows(opts: ProvisionNodeWindowsOptions): Promise<ProvisionResult> {\n const arch = opts.arch ?? 'x64';\n const exec = opts.exec ?? defaultExec;\n const download = opts.download ?? defaultDownload;\n // Default extract via Windows 10+ bsdtar (handles .zip); UNVALIDATED here.\n const extract =\n opts.extract ??\n (async (zip: string, destDir: string): Promise<void> => {\n const r = await exec('tar', ['-xf', zip, '-C', destDir]);\n if (r.code !== 0) throw new Error(`extract failed (code ${r.code}): ${r.stderr.trim()}`);\n });\n const now = opts.now ?? (() => new Date().toISOString());\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step: StepId.ProvisionNode,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n if (opts.alreadyOk) {\n emit(Status.Skipped, provisionMessages.nodePresent);\n return { ok: true, nodeExe: null };\n }\n\n emit(Status.Working, stepMessages[StepId.ProvisionNode]);\n try {\n await mkdir(opts.baseDir, { recursive: true }); // download dest + extract dir must exist\n const url = nodeDistUrl(opts.version, arch);\n const zip = join(opts.baseDir, `node-${opts.version}-win-${arch}.zip`);\n await download(url, zip);\n await extract(zip, opts.baseDir);\n } catch (err) {\n emit(Status.Failed, 'Setting up Node ran into a problem downloading or unpacking it. Check your connection, then press Retry.', 'error', ErrorCode.NetworkLost);\n throw err;\n }\n\n const nodeExe = nodeExePath(opts.baseDir, opts.version, arch);\n emit(Status.Done, provisionMessages.nodePresent);\n return { ok: true, nodeExe };\n}\n","import { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from '../contract';\nimport { stepMessages, provisionMessages } from '../messages';\nimport type { ProvisionResult } from './node-windows';\n\n// nvm release the bootstrap pins to (AC: nvm-sh v0.40.x). Embedded so the install is reproducible.\nexport const NVM_VERSION = 'v0.40.3';\n\n/** Absolute path to the nvm-installed node binary for a version. Pure. */\nexport function nvmNodePath(nvmDir: string, version: string): string {\n const v = version.startsWith('v') ? version : `v${version}`;\n return `${nvmDir}/versions/node/${v}/bin/node`;\n}\n\nexport interface ProvisionNodeUnixOptions {\n /** Pinned Node version (pins.node). */\n version: string;\n /** nvm install dir (default ~/.nvm). */\n nvmDir: string;\n emitter: EngineEmitter;\n /** From 2.1 detection: a system Node ≥ floor is already present. Honored only OFF macOS — on\n * macOS we always install the exact pin via nvm so the workshop is reproducible (AC2 reuse is\n * Linux-only; AC3 resolved-version-equals-pins.node). */\n alreadyOk?: boolean;\n /** Host platform — gates the reuse rule above. */\n platform?: NodeJS.Platform;\n /** Injectable side effects so tests run without network/a real shell. Defaults below are\n * shell-based and UNVALIDATED until the dress rehearsal (macOS + Linux). */\n installNvm?: (nvmDir: string) => Promise<void>;\n nvmInstallNode?: (nvmDir: string, version: string) => Promise<void>;\n /** Idempotent re-run check (2.7): is the PINNED node already installed AND runnable? Default\n * runs the resolved binary with --version. Verifying it RUNS (not just that a dir exists)\n * means a partial/dirty prior install is repaired (re-installed), not skipped-into-broken. */\n nodeInstalled?: (nvmDir: string, version: string) => Promise<boolean>;\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n now?: () => string;\n}\n\n// nvm is a shell function, not an executable — so every default runs `bash -c` (a single binary\n// + args, safe under exec's shell:false). Caller values are passed as POSITIONAL args ($1/$2),\n// never interpolated into the script, so a path/version can't break out of the shell context.\n// (NVM_VERSION is our own frozen constant, so it stays inline in the URL.)\nfunction makeDefaults(exec: (cmd: string, args: string[]) => Promise<ExecResult>) {\n const run = async (script: string, args: string[], what: string): Promise<void> => {\n // `kindling` becomes $0; the rest are $1, $2, …\n const r = await exec('bash', ['-c', script, 'kindling', ...args]);\n if (r.code !== 0) throw new Error(`${what} failed (code ${r.code}): ${r.stderr.trim()}`);\n };\n return {\n // Install nvm only if it's not already present at $1 (idempotent).\n installNvm: (nvmDir: string): Promise<void> =>\n run(\n `export NVM_DIR=\"$1\"; [ -s \"$NVM_DIR/nvm.sh\" ] || ` +\n `curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash`,\n [nvmDir],\n 'nvm install',\n ),\n // Source nvm in the same shell, then install the pinned version. We do NOT touch `nvm alias\n // default` — clobbering the user's global default would break their other projects (FR4); the\n // launch step uses the absolute nodeExe path instead.\n nvmInstallNode: (nvmDir: string, version: string): Promise<void> =>\n run(\n `export NVM_DIR=\"$1\"; . \"$NVM_DIR/nvm.sh\"; nvm install \"$2\"`,\n [nvmDir, version],\n 'nvm install node',\n ),\n };\n}\n\n/**\n * Provision the pinned Node on macOS/Linux via nvm, returning the ABSOLUTE node path so the\n * launch step (2.6) never depends on a mutated PATH (the clean-runtime rule). A system Node ≥\n * floor is reused (Skipped, AC2). Emits provision.node events; Failed + rethrow on error.\n *\n * The real nvm install + same-shell source is validated at the dress rehearsal; here the shell\n * effects are injectable and the orchestration/events/skip/fail paths are unit-tested.\n */\nexport async function provisionNodeUnix(opts: ProvisionNodeUnixOptions): Promise<ProvisionResult> {\n const exec = opts.exec ?? defaultExec;\n const defaults = makeDefaults(exec);\n const installNvm = opts.installNvm ?? defaults.installNvm;\n const nvmInstallNode = opts.nvmInstallNode ?? defaults.nvmInstallNode;\n const nodeInstalled =\n opts.nodeInstalled ??\n (async (nvmDir: string, version: string): Promise<boolean> => {\n const want = version.startsWith('v') ? version : `v${version}`;\n try {\n const r = await exec(nvmNodePath(nvmDir, version), ['--version']);\n // Require it to RUN and report the EXACT pinned version — a missing/broken/wrong-version\n // binary at that path → not installed → (re)install repairs it.\n return r.code === 0 && r.stdout.trim() === want;\n } catch {\n return false;\n }\n });\n const now = opts.now ?? (() => new Date().toISOString());\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step: StepId.ProvisionNode,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n // Reuse a system Node only OFF macOS (AC2 Linux-only); on macOS always install the exact pin.\n if (opts.alreadyOk && opts.platform !== 'darwin') {\n emit(Status.Skipped, provisionMessages.nodePresent);\n return { ok: true, nodeExe: null }; // null → the launch uses the system `node` on PATH\n }\n\n // Idempotent re-run (2.7): the pinned nvm node is already installed AND runs → skip, don't\n // reinstall. (A partial/broken install fails this check → falls through to a repairing install.)\n if (await nodeInstalled(opts.nvmDir, opts.version)) {\n emit(Status.Skipped, provisionMessages.nodePresent);\n return { ok: true, nodeExe: nvmNodePath(opts.nvmDir, opts.version) };\n }\n\n emit(Status.Working, stepMessages[StepId.ProvisionNode]);\n try {\n await installNvm(opts.nvmDir);\n await nvmInstallNode(opts.nvmDir, opts.version);\n } catch (err) {\n emit(\n Status.Failed,\n 'Setting up Node ran into a problem downloading or installing it. Check your connection, then press Retry.',\n 'error',\n ErrorCode.NetworkLost,\n );\n throw err;\n }\n\n const nodeExe = nvmNodePath(opts.nvmDir, opts.version);\n emit(Status.Done, provisionMessages.nodePresent);\n return { ok: true, nodeExe };\n}\n"],"mappings":"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","names":["join","join","exec","randomUUID","exec","randomUUID"]}
1
+ {"version":3,"sources":["../../engine/provision/node-windows.ts","../../engine/provision/node-unix.ts"],"sourcesContent":["import { join } from 'node:path';\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from '../contract';\nimport { stepMessages, provisionMessages } from '../messages';\n\nexport type NodeArch = 'x64' | 'arm64';\n\n/** Official Node Windows .zip URL for a pinned version. Pure. */\nexport function nodeDistUrl(version: string, arch: NodeArch = 'x64'): string {\n const v = version.startsWith('v') ? version : `v${version}`;\n return `https://nodejs.org/dist/${v}/node-${v}-win-${arch}.zip`;\n}\n\n/** Absolute path to node.exe inside the extracted distribution. Pure. */\nexport function nodeExePath(baseDir: string, version: string, arch: NodeArch = 'x64'): string {\n const v = version.startsWith('v') ? version : `v${version}`;\n return join(baseDir, `node-${v}-win-${arch}`, 'node.exe');\n}\n\nexport interface ProvisionNodeWindowsOptions {\n /** Pinned Node version (pins.node). */\n version: string;\n /** Install root, e.g. %LOCALAPPDATA%\\kindling\\node. */\n baseDir: string;\n arch?: NodeArch;\n emitter: EngineEmitter;\n /** From 2.1 detection: a Node ≥ floor is already present → skip the download. */\n alreadyOk?: boolean;\n /** Injectable so tests run without network/Windows. Defaults below are spike-validation-pending. */\n download?: (url: string, dest: string) => Promise<void>;\n extract?: (zip: string, destDir: string) => Promise<void>;\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n now?: () => string;\n}\n\n// Default download: global fetch → file. UNVALIDATED on real Windows (spike / rehearsal).\nasync function defaultDownload(url: string, dest: string): Promise<void> {\n const res = await fetch(url);\n if (!res.ok) throw new Error(`download failed: ${res.status} ${url}`);\n const buf = Buffer.from(await res.arrayBuffer());\n await writeFile(dest, buf);\n}\n\nexport interface ProvisionResult {\n ok: boolean;\n /**\n * Absolute path to the provisioned node binary for the launch step (2.6) to invoke directly\n * (the clean-runtime rule — never rely on a mutated PATH). `null` means an existing system\n * Node was reused → the launch resolves `node` from PATH instead.\n */\n nodeExe: string | null;\n}\n\nexport async function provisionNodeWindows(opts: ProvisionNodeWindowsOptions): Promise<ProvisionResult> {\n const arch = opts.arch ?? 'x64';\n const exec = opts.exec ?? defaultExec;\n const download = opts.download ?? defaultDownload;\n // Default extract via Windows 10+ bsdtar (handles .zip); UNVALIDATED here.\n const extract =\n opts.extract ??\n (async (zip: string, destDir: string): Promise<void> => {\n const r = await exec('tar', ['-xf', zip, '-C', destDir]);\n if (r.code !== 0) throw new Error(`extract failed (code ${r.code}): ${r.stderr.trim()}`);\n });\n const now = opts.now ?? (() => new Date().toISOString());\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step: StepId.ProvisionNode,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n if (opts.alreadyOk) {\n emit(Status.Skipped, provisionMessages.nodePresent);\n return { ok: true, nodeExe: null };\n }\n\n emit(Status.Working, stepMessages[StepId.ProvisionNode]);\n try {\n await mkdir(opts.baseDir, { recursive: true }); // download dest + extract dir must exist\n const url = nodeDistUrl(opts.version, arch);\n const zip = join(opts.baseDir, `node-${opts.version}-win-${arch}.zip`);\n await download(url, zip);\n await extract(zip, opts.baseDir);\n } catch (err) {\n emit(Status.Failed, 'Setting up Node ran into a problem downloading or unpacking it. Check your connection, then press Retry.', 'error', ErrorCode.NetworkLost);\n throw err;\n }\n\n const nodeExe = nodeExePath(opts.baseDir, opts.version, arch);\n emit(Status.Done, provisionMessages.nodePresent);\n return { ok: true, nodeExe };\n}\n","import { randomUUID } from 'node:crypto';\nimport { exec as defaultExec, type ExecResult } from '../exec';\nimport type { EngineEmitter } from '../emitter';\nimport { Phase, StepId, Status, ErrorCode, type Level } from '../contract';\nimport { stepMessages, provisionMessages } from '../messages';\nimport type { ProvisionResult } from './node-windows';\n\n// nvm release the bootstrap pins to (AC: nvm-sh v0.40.x). Embedded so the install is reproducible.\nexport const NVM_VERSION = 'v0.40.3';\n\n/** Absolute path to the nvm-installed node binary for a version. Pure. */\nexport function nvmNodePath(nvmDir: string, version: string): string {\n const v = version.startsWith('v') ? version : `v${version}`;\n return `${nvmDir}/versions/node/${v}/bin/node`;\n}\n\nexport interface ProvisionNodeUnixOptions {\n /** Pinned Node version (pins.node). */\n version: string;\n /** nvm install dir (default ~/.nvm). */\n nvmDir: string;\n emitter: EngineEmitter;\n /** From 2.1 detection: a system Node ≥ floor is already present. Honored only OFF macOS — on\n * macOS we always install the exact pin via nvm so the workshop is reproducible (AC2 reuse is\n * Linux-only; AC3 resolved-version-equals-pins.node). */\n alreadyOk?: boolean;\n /** Host platform — gates the reuse rule above. */\n platform?: NodeJS.Platform;\n /** Injectable side effects so tests run without network/a real shell. Defaults below are\n * shell-based and UNVALIDATED until the dress rehearsal (macOS + Linux). */\n installNvm?: (nvmDir: string) => Promise<void>;\n nvmInstallNode?: (nvmDir: string, version: string) => Promise<void>;\n /** Idempotent re-run check (2.7): is the PINNED node already installed AND runnable? Default\n * runs the resolved binary with --version. Verifying it RUNS (not just that a dir exists)\n * means a partial/dirty prior install is repaired (re-installed), not skipped-into-broken. */\n nodeInstalled?: (nvmDir: string, version: string) => Promise<boolean>;\n exec?: (cmd: string, args: string[]) => Promise<ExecResult>;\n now?: () => string;\n}\n\n// nvm is a shell function, not an executable — so every default runs `bash -c` (a single binary\n// + args, safe under exec's shell:false). Caller values are passed as POSITIONAL args ($1/$2),\n// never interpolated into the script, so a path/version can't break out of the shell context.\n// (NVM_VERSION is our own frozen constant, so it stays inline in the URL.)\nfunction makeDefaults(exec: (cmd: string, args: string[]) => Promise<ExecResult>) {\n const run = async (script: string, args: string[], what: string): Promise<void> => {\n // `kindling` becomes $0; the rest are $1, $2, …\n const r = await exec('bash', ['-c', script, 'kindling', ...args]);\n if (r.code !== 0) throw new Error(`${what} failed (code ${r.code}): ${r.stderr.trim()}`);\n };\n return {\n // Install nvm only if it's not already present at $1 (idempotent).\n installNvm: (nvmDir: string): Promise<void> =>\n run(\n `export NVM_DIR=\"$1\"; [ -s \"$NVM_DIR/nvm.sh\" ] || ` +\n `curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_VERSION}/install.sh | bash`,\n [nvmDir],\n 'nvm install',\n ),\n // Source nvm in the same shell, then install the pinned version. We do NOT touch `nvm alias\n // default` — clobbering the user's global default would break their other projects (FR4); the\n // launch step uses the absolute nodeExe path instead.\n nvmInstallNode: (nvmDir: string, version: string): Promise<void> =>\n run(\n `export NVM_DIR=\"$1\"; . \"$NVM_DIR/nvm.sh\"; nvm install \"$2\"`,\n [nvmDir, version],\n 'nvm install node',\n ),\n };\n}\n\n/**\n * Provision the pinned Node on macOS/Linux via nvm, returning the ABSOLUTE node path so the\n * launch step (2.6) never depends on a mutated PATH (the clean-runtime rule). A system Node ≥\n * floor is reused (Skipped, AC2). Emits provision.node events; Failed + rethrow on error.\n *\n * The real nvm install + same-shell source is validated at the dress rehearsal; here the shell\n * effects are injectable and the orchestration/events/skip/fail paths are unit-tested.\n */\nexport async function provisionNodeUnix(opts: ProvisionNodeUnixOptions): Promise<ProvisionResult> {\n const exec = opts.exec ?? defaultExec;\n const defaults = makeDefaults(exec);\n const installNvm = opts.installNvm ?? defaults.installNvm;\n const nvmInstallNode = opts.nvmInstallNode ?? defaults.nvmInstallNode;\n const nodeInstalled =\n opts.nodeInstalled ??\n (async (nvmDir: string, version: string): Promise<boolean> => {\n const want = version.startsWith('v') ? version : `v${version}`;\n try {\n const r = await exec(nvmNodePath(nvmDir, version), ['--version']);\n // Require it to RUN and report the EXACT pinned version — a missing/broken/wrong-version\n // binary at that path → not installed → (re)install repairs it.\n return r.code === 0 && r.stdout.trim() === want;\n } catch {\n return false;\n }\n });\n const now = opts.now ?? (() => new Date().toISOString());\n\n const emit = (\n status: Status,\n humanMessage: string,\n level: Level = 'info',\n errorCode?: ErrorCode,\n ): void => {\n opts.emitter.emit({\n id: randomUUID(),\n phase: Phase.Provision,\n step: StepId.ProvisionNode,\n status,\n humanMessage,\n level,\n timestamp: now(),\n errorCode,\n });\n };\n\n // Reuse a system Node only OFF macOS (AC2 Linux-only); on macOS always install the exact pin.\n if (opts.alreadyOk && opts.platform !== 'darwin') {\n emit(Status.Skipped, provisionMessages.nodePresent);\n return { ok: true, nodeExe: null }; // null → the launch uses the system `node` on PATH\n }\n\n // Idempotent re-run (2.7): the pinned nvm node is already installed AND runs → skip, don't\n // reinstall. (A partial/broken install fails this check → falls through to a repairing install.)\n if (await nodeInstalled(opts.nvmDir, opts.version)) {\n emit(Status.Skipped, provisionMessages.nodePresent);\n return { ok: true, nodeExe: nvmNodePath(opts.nvmDir, opts.version) };\n }\n\n emit(Status.Working, stepMessages[StepId.ProvisionNode]);\n try {\n await installNvm(opts.nvmDir);\n await nvmInstallNode(opts.nvmDir, opts.version);\n } catch (err) {\n emit(\n Status.Failed,\n 'Setting up Node ran into a problem downloading or installing it. Check your connection, then press Retry.',\n 'error',\n ErrorCode.NetworkLost,\n );\n throw err;\n }\n\n const nodeExe = nvmNodePath(opts.nvmDir, opts.version);\n emit(Status.Done, provisionMessages.nodePresent);\n return { ok: true, nodeExe };\n}\n"],"mappings":"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","names":["exec","randomUUID","exec","randomUUID"]}