@aiviatic/kindling 0.1.0 → 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.
package/dist/cli/main.js CHANGED
@@ -7,11 +7,11 @@ import {
7
7
  defaultBmadInstalled,
8
8
  pins,
9
9
  readInstalledBmadVersion
10
- } from "../chunk-MW7UAGER.js";
10
+ } from "../chunk-DZ2RR3SP.js";
11
11
  import {
12
12
  openBrowser,
13
13
  startServer
14
- } from "../chunk-IS6LC3HK.js";
14
+ } from "../chunk-DTSAPFB2.js";
15
15
  import {
16
16
  expandTilde
17
17
  } from "../chunk-OU3WSB6B.js";
@@ -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-MW7UAGER.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"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  openBrowser,
3
3
  startServer
4
- } from "../chunk-IS6LC3HK.js";
4
+ } from "../chunk-DTSAPFB2.js";
5
5
  import "../chunk-OU3WSB6B.js";
6
6
  export {
7
7
  openBrowser,