@svenroth-ai/shipwright 0.25.0 → 0.26.0

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.
Files changed (134) hide show
  1. package/client/dist/assets/{EmbeddedTerminal-5QvKe3_a.js → EmbeddedTerminal-D3wpPNZP.js} +1 -1
  2. package/client/dist/assets/{abnfDiagram-VRR7QNED-CXi9HegQ.js → abnfDiagram-VRR7QNED-BkLIltB3.js} +1 -1
  3. package/client/dist/assets/{arc-DLh4GiqR.js → arc-Sw-Gh0nI.js} +1 -1
  4. package/client/dist/assets/{architectureDiagram-ZJ3FMSHR-DUypQ8dQ.js → architectureDiagram-ZJ3FMSHR-rdxUi9Ks.js} +1 -1
  5. package/client/dist/assets/{blockDiagram-677ZJIJ3-D2cVMKJG.js → blockDiagram-677ZJIJ3-Ch_9dj8a.js} +1 -1
  6. package/client/dist/assets/{c4Diagram-LMCZKHZV-DtruBGJw.js → c4Diagram-LMCZKHZV-CkFlO65x.js} +1 -1
  7. package/client/dist/assets/channel-BYPbLjRl.js +1 -0
  8. package/client/dist/assets/{chunk-2Q5K7J3B-CAfQkFw2.js → chunk-2Q5K7J3B-CypX83xu.js} +1 -1
  9. package/client/dist/assets/{chunk-32BRIVSS-CPWXoLnQ.js → chunk-32BRIVSS-DxVLYGdl.js} +1 -1
  10. package/client/dist/assets/{chunk-5VM5RSS4-xVc3oppQ.js → chunk-5VM5RSS4-Cxk1evhx.js} +1 -1
  11. package/client/dist/assets/{chunk-EX3LRPZG-CUpl6fRR.js → chunk-EX3LRPZG-n9q5S4yJ.js} +1 -1
  12. package/client/dist/assets/{chunk-JWPE2WC7-NejUp56x.js → chunk-JWPE2WC7-jeL4xvir.js} +1 -1
  13. package/client/dist/assets/{chunk-MOJQB5TN-C9-2lLg0.js → chunk-MOJQB5TN-DDSHN0cj.js} +1 -1
  14. package/client/dist/assets/{chunk-RYQCIY6F-BAPtwABy.js → chunk-RYQCIY6F-C0bo383g.js} +1 -1
  15. package/client/dist/assets/{chunk-V7JOEXUC-Dwzihw26.js → chunk-V7JOEXUC-C0jDXXAU.js} +1 -1
  16. package/client/dist/assets/{chunk-VR4S4FIN-9-iArFJM.js → chunk-VR4S4FIN-h-Rr4Pn2.js} +1 -1
  17. package/client/dist/assets/{chunk-XXDRQBXY-DiWHOrqe.js → chunk-XXDRQBXY-BexSo0-s.js} +1 -1
  18. package/client/dist/assets/classDiagram-OUVF2IWQ-CsFIeTju.js +1 -0
  19. package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsFIeTju.js +1 -0
  20. package/client/dist/assets/{cose-bilkent-JH36ORCC-Cp04EOcH.js → cose-bilkent-JH36ORCC-BQXzaioj.js} +1 -1
  21. package/client/dist/assets/{cynefin-VYW2F7L2-D--GN1v8.js → cynefin-VYW2F7L2-BJIofhfe.js} +1 -1
  22. package/client/dist/assets/{cynefinDiagram-TSTJHNR4-BGH0o_qp.js → cynefinDiagram-TSTJHNR4-DmPqTM1T.js} +1 -1
  23. package/client/dist/assets/{dagre-VKFMJZFB-DSKkIKlj.js → dagre-VKFMJZFB-1xljvw6X.js} +1 -1
  24. package/client/dist/assets/{diagram-FQU43EPY-Cs_C4lb-.js → diagram-FQU43EPY-iyL9muCG.js} +1 -1
  25. package/client/dist/assets/{diagram-G47NLZAW-oDtAA8O_.js → diagram-G47NLZAW-DREp2Th6.js} +1 -1
  26. package/client/dist/assets/{diagram-NH7WQ7WH-BiFMjqSH.js → diagram-NH7WQ7WH-h0X3Is2S.js} +1 -1
  27. package/client/dist/assets/{diagram-OA4YK3LP-Ccs3thCj.js → diagram-OA4YK3LP-Bj8YZRFJ.js} +1 -1
  28. package/client/dist/assets/{diagram-WEI45ONY-CdZNqgO3.js → diagram-WEI45ONY-BZOf805d.js} +1 -1
  29. package/client/dist/assets/{ebnfDiagram-CCIWWBDH-CapTA0VL.js → ebnfDiagram-CCIWWBDH-DO5JnXda.js} +1 -1
  30. package/client/dist/assets/{erDiagram-Q63AITRT-BcRNEP8s.js → erDiagram-Q63AITRT-DRgcqg1T.js} +1 -1
  31. package/client/dist/assets/{flowDiagram-23GEKE2U-CJ_zF7e_.js → flowDiagram-23GEKE2U-C3J9QmrW.js} +1 -1
  32. package/client/dist/assets/{ganttDiagram-NO4QXBWP-CnJdcg9A.js → ganttDiagram-NO4QXBWP-BIEl3Tkm.js} +1 -1
  33. package/client/dist/assets/{gitGraphDiagram-IHSO6WYX-YTUQ_ELk.js → gitGraphDiagram-IHSO6WYX-BNF0tFWC.js} +1 -1
  34. package/client/dist/assets/{index-irQhGNpn.js → index-BnTOrFHb.js} +203 -202
  35. package/client/dist/assets/index-CYPc6qxw.css +1 -0
  36. package/client/dist/assets/{infoDiagram-FWYZ7A6U-CLOP_ZWD.js → infoDiagram-FWYZ7A6U-DEyFxiyY.js} +1 -1
  37. package/client/dist/assets/{ishikawaDiagram-FXEZZL3T-aFTIzr7o.js → ishikawaDiagram-FXEZZL3T-B_ehlg3I.js} +1 -1
  38. package/client/dist/assets/{journeyDiagram-5HDEW3XC-CbPJB4xd.js → journeyDiagram-5HDEW3XC-Dp00FyJS.js} +1 -1
  39. package/client/dist/assets/{kanban-definition-HUTT4EX6-BoyhytIS.js → kanban-definition-HUTT4EX6-CTAf16q8.js} +1 -1
  40. package/client/dist/assets/{linear-1VXA-uBg.js → linear-9vNUgmYd.js} +1 -1
  41. package/client/dist/assets/{mermaid.core-D_lO2yK9.js → mermaid.core-A6tjaEVD.js} +4 -4
  42. package/client/dist/assets/{mindmap-definition-LN4V7U3C-iFK7LXMx.js → mindmap-definition-LN4V7U3C-DRW3bmCY.js} +1 -1
  43. package/client/dist/assets/{pegDiagram-2B236MQR-DNmcmfA-.js → pegDiagram-2B236MQR-DJFCjHp2.js} +1 -1
  44. package/client/dist/assets/{pieDiagram-ENE6RG2P-ekx83O5b.js → pieDiagram-ENE6RG2P-KRd-sGvC.js} +1 -1
  45. package/client/dist/assets/{quadrantDiagram-ABIIQ3AL-DayyD1l8.js → quadrantDiagram-ABIIQ3AL-9TLZE3Hu.js} +1 -1
  46. package/client/dist/assets/{railroadDiagram-RFXS5EU6-Bdgz7S6G.js → railroadDiagram-RFXS5EU6-y4tLtjXg.js} +1 -1
  47. package/client/dist/assets/{requirementDiagram-TGXJPOKE-szP3Muld.js → requirementDiagram-TGXJPOKE-B9EOQDQM.js} +1 -1
  48. package/client/dist/assets/{sankeyDiagram-HTMAVEWB-Ct3rzgUj.js → sankeyDiagram-HTMAVEWB-zfwiDzkT.js} +1 -1
  49. package/client/dist/assets/{sequenceDiagram-DBY2YBRQ-_69EfmOU.js → sequenceDiagram-DBY2YBRQ-DW1LWepL.js} +1 -1
  50. package/client/dist/assets/{sizeCapture-X5ZJPWSS-BSOU06J2.js → sizeCapture-X5ZJPWSS-BM81Mik0.js} +1 -1
  51. package/client/dist/assets/{stateDiagram-2N3HPSRC-HmOmFwQm.js → stateDiagram-2N3HPSRC-D0EMIw1w.js} +1 -1
  52. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-B3E4_xxT.js +1 -0
  53. package/client/dist/assets/{swimlanes-5IMT3BWC-CVg_4oT7.js → swimlanes-5IMT3BWC-CKXNCX6M.js} +2 -2
  54. package/client/dist/assets/swimlanesDiagram-G3AALYLV-BegQBqnC.js +8 -0
  55. package/client/dist/assets/{timeline-definition-FHXFAJF6-CBBaDKXm.js → timeline-definition-FHXFAJF6-ByzIfvL5.js} +1 -1
  56. package/client/dist/assets/{vennDiagram-L72KCM5P-CBXiWPMc.js → vennDiagram-L72KCM5P-Xep38xBH.js} +1 -1
  57. package/client/dist/assets/{wardleyDiagram-EHGQE667-uhBCZoW0.js → wardleyDiagram-EHGQE667-DKWDpx6g.js} +1 -1
  58. package/client/dist/assets/{xychartDiagram-FW5EYKEG-xxE_wY5O.js → xychartDiagram-FW5EYKEG-BGcwZTnu.js} +1 -1
  59. package/client/dist/index.html +27 -27
  60. package/lib/claude-cli.mjs +173 -173
  61. package/lib/preflight.mjs +252 -252
  62. package/lib/probe-path.mjs +84 -84
  63. package/lib/server.mjs +369 -369
  64. package/lib/util.mjs +157 -157
  65. package/lib/win32-spawn.mjs +253 -253
  66. package/package.json +68 -68
  67. package/scripts/deploy-procs.mjs +208 -208
  68. package/scripts/deploy-swap.mjs +276 -276
  69. package/scripts/kill-targets.js +140 -140
  70. package/scripts/repair-claude-json.mjs +295 -295
  71. package/server/dist/config/default-actions.json +90 -90
  72. package/server/dist/core/mission-context/association.d.ts +46 -0
  73. package/server/dist/core/mission-context/association.d.ts.map +1 -1
  74. package/server/dist/core/mission-context/association.js +69 -0
  75. package/server/dist/core/mission-context/association.js.map +1 -1
  76. package/server/dist/core/mission-context/resolver-iterate.d.ts +6 -0
  77. package/server/dist/core/mission-context/resolver-iterate.d.ts.map +1 -0
  78. package/server/dist/core/mission-context/resolver-iterate.js +202 -0
  79. package/server/dist/core/mission-context/resolver-iterate.js.map +1 -0
  80. package/server/dist/core/mission-context/resolver-parts.d.ts +20 -0
  81. package/server/dist/core/mission-context/resolver-parts.d.ts.map +1 -1
  82. package/server/dist/core/mission-context/resolver-parts.js +92 -0
  83. package/server/dist/core/mission-context/resolver-parts.js.map +1 -1
  84. package/server/dist/core/mission-context/resolver.d.ts +1 -1
  85. package/server/dist/core/mission-context/resolver.d.ts.map +1 -1
  86. package/server/dist/core/mission-context/resolver.js +17 -199
  87. package/server/dist/core/mission-context/resolver.js.map +1 -1
  88. package/server/dist/core/mission-context/run-id-recovery.d.ts +21 -5
  89. package/server/dist/core/mission-context/run-id-recovery.d.ts.map +1 -1
  90. package/server/dist/core/mission-context/run-id-recovery.js +32 -5
  91. package/server/dist/core/mission-context/run-id-recovery.js.map +1 -1
  92. package/server/dist/core/mission-context/scenario.d.ts +18 -11
  93. package/server/dist/core/mission-context/scenario.d.ts.map +1 -1
  94. package/server/dist/core/mission-context/scenario.js +40 -3
  95. package/server/dist/core/mission-context/scenario.js.map +1 -1
  96. package/server/dist/core/path-guard.d.ts +1 -1
  97. package/server/dist/core/path-guard.d.ts.map +1 -1
  98. package/server/dist/core/path-guard.js +9 -2
  99. package/server/dist/core/path-guard.js.map +1 -1
  100. package/server/dist/core/shipslog-docs-reader.d.ts +4 -0
  101. package/server/dist/core/shipslog-docs-reader.d.ts.map +1 -0
  102. package/server/dist/core/shipslog-docs-reader.js +181 -0
  103. package/server/dist/core/shipslog-docs-reader.js.map +1 -0
  104. package/server/dist/core/shipslog-docs-types.d.ts +15 -0
  105. package/server/dist/core/shipslog-docs-types.d.ts.map +1 -0
  106. package/server/dist/core/shipslog-docs-types.js +14 -0
  107. package/server/dist/core/shipslog-docs-types.js.map +1 -0
  108. package/server/dist/external/design-review/serve.js +27 -27
  109. package/server/dist/external/mission-context/association-write.d.ts +22 -0
  110. package/server/dist/external/mission-context/association-write.d.ts.map +1 -0
  111. package/server/dist/external/mission-context/association-write.js +90 -0
  112. package/server/dist/external/mission-context/association-write.js.map +1 -0
  113. package/server/dist/external/mission-context/routes.d.ts.map +1 -1
  114. package/server/dist/external/mission-context/routes.js +18 -31
  115. package/server/dist/external/mission-context/routes.js.map +1 -1
  116. package/server/dist/external/routes.d.ts.map +1 -1
  117. package/server/dist/external/routes.js +9 -6
  118. package/server/dist/external/routes.js.map +1 -1
  119. package/server/dist/external/shipslog-docs/routes.d.ts +10 -0
  120. package/server/dist/external/shipslog-docs/routes.d.ts.map +1 -0
  121. package/server/dist/external/shipslog-docs/routes.js +41 -0
  122. package/server/dist/external/shipslog-docs/routes.js.map +1 -0
  123. package/server/package.json +1 -1
  124. package/server/profiles/PROFILE_SCHEMA_VERSION +1 -1
  125. package/server/profiles/README.md +27 -27
  126. package/server/profiles/python-plugin-monorepo.json +35 -35
  127. package/server/profiles/supabase-nextjs.json +253 -253
  128. package/server/profiles/vite-hono.json +92 -92
  129. package/client/dist/assets/channel-C_k-9jsa.js +0 -1
  130. package/client/dist/assets/classDiagram-OUVF2IWQ-CsEPq9Ac.js +0 -1
  131. package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsEPq9Ac.js +0 -1
  132. package/client/dist/assets/index-6nZ1bt0a.css +0 -1
  133. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-BW8H33dv.js +0 -1
  134. package/client/dist/assets/swimlanesDiagram-G3AALYLV-BaRlxcPD.js +0 -8
package/lib/preflight.mjs CHANGED
@@ -1,252 +1,252 @@
1
- /**
2
- * preflight.mjs — AC1a, the prerequisite gate (the THIRD silent death).
3
- *
4
- * The plugins do NOT run on Node alone: their hooks shell out to `uv` (159
5
- * `uv run` call sites across the installed hooks.json files), which resolves a
6
- * Python. Without uv + a WORKING Python every hook dies at session start while
7
- * `claude plugin list` still shows a cheerful checkmark. So preflight is a
8
- * loud, actionable gate, not a warning buried above "Command Center running".
9
- *
10
- * The Windows trap this file exists to dodge: `python3` is usually the
11
- * Microsoft-Store App-Execution-Alias stub — `command -v`/`where` FINDS it, but
12
- * running it does nothing. We therefore probe by TEST-RUNNING `--version` and
13
- * accept the first candidate that actually reports one (ported from
14
- * `scripts/verify-setup.sh`), never by mere presence on PATH.
15
- */
16
-
17
- import { spawnSync } from "node:child_process";
18
- import os from "node:os";
19
-
20
- import { MIN_NODE, compareSemver, installHint, isWindows } from "./util.mjs";
21
- import { probeEnv, resolvePosixBin } from "./probe-path.mjs";
22
- import { resolveSpawn } from "./win32-spawn.mjs";
23
-
24
- /**
25
- * Default runner: invoke `<cmd> --version` with `shell: false` and report
26
- * whether it actually ran.
27
- *
28
- * On Windows some of these tools are `.cmd`/`.bat` shims (`claude` from an npm
29
- * install) that a bare `shell: false` spawn CANNOT reach — probing them that way
30
- * would report a perfectly-installed Claude as ABSENT and skip the plugin phase
31
- * forever. That used to be answered with `shell: true`; `resolveSpawn` now finds
32
- * the real file via PATHEXT and routes a shim through an explicit
33
- * `cmd.exe /d /s /c` instead (see lib/win32-spawn.mjs).
34
- *
35
- * CALLER INVARIANT, still load-bearing: `cmd` is a fixed internal literal
36
- * (claude / uv / python3 / python / py / git / gh) and `args` is a fixed internal
37
- * literal too — `["--version"]` for every probe, plus the one `["python",
38
- * "find", ">=3.11"]` uv-can-supply-Python predicate. NO caller passes user input.
39
- * NOTHING here validates that — `defaultRun` is exported from a published `lib/`
40
- * and reaches the cmd.exe wrap with no charset gate of its own (unlike
41
- * `claude-cli.mjs`, which has `SAFE_ARG`). Only `runPreflight`'s call sites
42
- * honour it. A caller passing user input must gate it first.
43
- *
44
- * An unresolvable name yields `ok: false` — exactly the verdict the old shell
45
- * path produced via a non-zero exit. Note what "unresolvable" now means: a bare
46
- * name that PATHEXT cannot place falls back to a direct `CreateProcess` spawn
47
- * rather than being abandoned, because `realpath` cannot follow a Windows
48
- * App-Execution-Alias at all — so a genuinely INSTALLED Microsoft-Store Python
49
- * would otherwise be reported absent. The Store STUB is still rejected, by the
50
- * unchanged `\d+\.\d+` output requirement below rather than by failing to
51
- * resolve. Both verified on Windows 11, 2026-07-31.
52
- * @param {string} cmd @param {string[]} [args]
53
- * @returns {{ ok: boolean, stdout: string, stderr: string, code: number | null }}
54
- */
55
- export function defaultRun(cmd, args = ["--version"], deps = {}) {
56
- const platform = deps.platform ?? process.platform;
57
- const homedir = deps.homedir ?? os.homedir();
58
- const env = probeEnv(platform, homedir, deps.env ?? process.env);
59
- try {
60
- // Windows resolves shims via PATHEXT against the augmented `env`; POSIX
61
- // resolves the absolute path itself so the augmented PATH actually applies.
62
- const plan =
63
- platform === "win32"
64
- ? resolveSpawn([cmd, ...args], { platform, env })
65
- : { command: resolvePosixBin(cmd, env), args };
66
- // UNREACHABLE as of iterate-2026-07-31 (win32-spawn divergence 5: an
67
- // unresolvable bare name comes back as itself). `spawnSync` does NOT throw
68
- // for a missing binary — it returns `{ error: ENOENT, status: null }` — so
69
- // the failure is carried by the `!r.error` term in `ok` below and never
70
- // reaches the catch. (An earlier version of this comment said "lands in the
71
- // catch"; Stage-3 doubt review disproved it. The verdict was always right,
72
- // the stated mechanism was not.) Kept as contract insurance.
73
- if (!plan) return { ok: false, stdout: "", stderr: "", code: null };
74
- const r = spawnSync(plan.command, plan.args, {
75
- encoding: "utf-8",
76
- shell: false,
77
- timeout: 8000,
78
- env,
79
- windowsVerbatimArguments: plan.windowsVerbatimArguments,
80
- });
81
- const stdout = (r.stdout ?? "").toString();
82
- const stderr = (r.stderr ?? "").toString();
83
- // A real interpreter/tool exits 0 AND prints a version somewhere. The MS
84
- // Store python3 stub exits non-zero (or prints its "not found" nag), so
85
- // requiring both status 0 and a digit-bearing line rejects it.
86
- const ok = r.status === 0 && !r.error && /\d+\.\d+/.test(stdout + stderr);
87
- return { ok, stdout, stderr, code: r.status ?? null };
88
- } catch {
89
- return { ok: false, stdout: "", stderr: "", code: null };
90
- }
91
- }
92
-
93
- /** First `\d+.\d+(.\d+)?` token in a `--version` blob, or "". */
94
- export function extractVersion(out) {
95
- const m = /(\d+\.\d+(?:\.\d+)?)/.exec(String(out ?? ""));
96
- return m ? m[1] : "";
97
- }
98
-
99
- /**
100
- * Resolve ONE working Python by test-running `--version` (python3 → python →
101
- * py). Returns `{ bin, version }` or `null`. The MS-Store stub fails the
102
- * `run().ok` test and is skipped — the whole reason this is not `command -v`.
103
- * @param {(cmd: string, args?: string[]) => ReturnType<typeof defaultRun>} run
104
- */
105
- export function resolvePython(run) {
106
- for (const bin of ["python3", "python", "py"]) {
107
- const r = run(bin, ["--version"]);
108
- if (r.ok) return { bin, version: extractVersion(r.stdout + r.stderr) };
109
- }
110
- return null;
111
- }
112
-
113
- /**
114
- * Run the full preflight. Pure over its injected seams (`run`, `nodeVersion`,
115
- * `platform`) so every branch is unit-testable without a real toolchain.
116
- *
117
- * @param {{
118
- * run?: (cmd: string, args?: string[]) => ReturnType<typeof defaultRun>,
119
- * nodeVersion?: string,
120
- * platform?: NodeJS.Platform,
121
- * }} [deps]
122
- */
123
- export function runPreflight(deps = {}) {
124
- const { run = defaultRun, nodeVersion = process.version, platform = process.platform } = deps;
125
- /** @type {{name:string, ok:boolean, detail:string, hint?:string, hard:boolean, optional?:boolean}[]} */
126
- const checks = [];
127
-
128
- // claude — its own gate: absent → skip plugins + loud warning + non-zero exit.
129
- const claude = run("claude", ["--version"]);
130
- checks.push({
131
- name: "claude",
132
- ok: claude.ok,
133
- detail: claude.ok ? extractVersion(claude.stdout + claude.stderr) : "not found",
134
- hint: claude.ok ? undefined : installHint("claude", platform),
135
- hard: true,
136
- });
137
-
138
- // uv — hard requirement for every plugin hook.
139
- const uv = run("uv", ["--version"]);
140
- checks.push({
141
- name: "uv",
142
- ok: uv.ok,
143
- detail: uv.ok ? extractVersion(uv.stdout + uv.stderr) : "not found",
144
- hint: uv.ok ? undefined : installHint("uv", platform),
145
- hard: true,
146
- });
147
-
148
- // python — the stack runs every hook via `uv run`, which resolves a
149
- // uv-MANAGED interpreter. So a working system python3 >= 3.11 satisfies the
150
- // gate, but so does uv being able to PROVIDE 3.11+ — the common Mac case where
151
- // `uv python install 3.11` left system `python3` at 3.9. System probe first
152
- // (TEST-RUN, Store-stub trap); only fall through to uv when it is short.
153
- const py = resolvePython(run);
154
- const sysOk = py != null && compareSemver(py.version, "3.11.0") >= 0;
155
- let pyOk = sysOk;
156
- let pyDetail = py ? `${py.version} (${py.bin})` : "not found (tried python3, python, py)";
157
- if (!pyOk && uv.ok) {
158
- // `uv python find` locates an installed 3.11+ WITHOUT downloading — a clean,
159
- // side-effect-free predicate for "uv can supply Python for the hooks".
160
- const uvPy = run("uv", ["python", "find", ">=3.11"]);
161
- // Gate on EXIT CODE, not defaultRun's version-shaped `ok`: `uv python find`
162
- // prints a PATH, and a resolved interpreter whose path carries no decimal
163
- // (e.g. a `/usr/local/bin/python3` symlink) would fail the `\d+\.\d+` output
164
- // test despite a clean exit — re-introducing the very false "not found" this
165
- // change removes. uv exits 0 on a hit, 2 on a miss (verified 2026-08-22).
166
- if (uvPy.code === 0) {
167
- pyOk = true;
168
- pyDetail = `${extractVersion(uvPy.stdout + uvPy.stderr) || "3.11+"} (uv-managed)`;
169
- }
170
- }
171
- checks.push({
172
- name: "python",
173
- ok: pyOk,
174
- detail: pyDetail,
175
- hint: pyOk ? undefined : installHint("python", platform),
176
- hard: true,
177
- });
178
-
179
- // node — assert the packaged server's minimum.
180
- const nodeOk = compareSemver(nodeVersion, MIN_NODE) >= 0;
181
- checks.push({
182
- name: "node",
183
- ok: nodeOk,
184
- detail: nodeOk ? nodeVersion : `${nodeVersion} (need >= ${MIN_NODE})`,
185
- hint: nodeOk ? undefined : installHint("node", platform),
186
- hard: true,
187
- });
188
-
189
- // git — needed by the SDLC plugins.
190
- const git = run("git", ["--version"]);
191
- checks.push({
192
- name: "git",
193
- ok: git.ok,
194
- detail: git.ok ? extractVersion(git.stdout + git.stderr) : "not found",
195
- hint: git.ok ? undefined : installHint("git", platform),
196
- hard: true,
197
- });
198
-
199
- // gh — optional (PRs). A soft note, never a failure.
200
- const gh = run("gh", ["--version"]);
201
- checks.push({
202
- name: "gh",
203
- ok: gh.ok,
204
- detail: gh.ok ? extractVersion(gh.stdout + gh.stderr) : "not found (optional — needed for PRs)",
205
- hard: false,
206
- optional: true,
207
- });
208
-
209
- const hardFailures = checks.filter((c) => c.hard && !c.ok);
210
- // The plugin phase needs claude + uv + python (hook runtime) AND git —
211
- // `claude plugin marketplace add` clones the marketplace over git, and the
212
- // SDLC plugins are git-based. Node is NOT gated here: it gates the SERVER
213
- // (and npx already blocks a too-old engine), not the plugin install.
214
- const pluginPhaseOk = claude.ok && uv.ok && pyOk && git.ok;
215
-
216
- return {
217
- checks,
218
- python: py,
219
- hasClaude: claude.ok,
220
- hasUv: uv.ok,
221
- hasPython: pyOk,
222
- hasNode: nodeOk,
223
- hasGit: git.ok,
224
- /** Plugins can only be installed when claude + uv + python are all real. */
225
- pluginPhaseOk,
226
- /** Non-zero exit when any hard prerequisite is missing (AC1a). */
227
- exitCode: hardFailures.length,
228
- isWindows: isWindows(platform),
229
- };
230
- }
231
-
232
- /** Render the one honest verdict block a `doctor`-style tool prints. */
233
- export function renderVerdict(result, mark) {
234
- const lines = ["Prerequisites:"];
235
- for (const c of result.checks) {
236
- const glyph = c.ok ? mark.pass : c.optional ? mark.skip : mark.fail;
237
- lines.push(` ${glyph} ${c.name}: ${c.detail}`);
238
- if (!c.ok && c.hint) lines.push(` -> ${c.hint}`);
239
- }
240
- // A just-installed tool lands in ~/.local/bin but the running shell's PATH was
241
- // captured before the install — so "not found" here is often really "not on
242
- // PATH yet". Say so, once, OS-aware, whenever a hard prerequisite is missing.
243
- if (result.checks.some((c) => c.hard && !c.ok)) {
244
- lines.push("");
245
- lines.push(
246
- result.isWindows
247
- ? " Note: if you JUST installed one of these, open a NEW terminal so it lands on your PATH, then re-run."
248
- : " Note: if you JUST installed one of these, open a new terminal (or `source ~/.zshrc` / `~/.bashrc`) so it lands on your PATH, then re-run.",
249
- );
250
- }
251
- return lines.join("\n");
252
- }
1
+ /**
2
+ * preflight.mjs — AC1a, the prerequisite gate (the THIRD silent death).
3
+ *
4
+ * The plugins do NOT run on Node alone: their hooks shell out to `uv` (159
5
+ * `uv run` call sites across the installed hooks.json files), which resolves a
6
+ * Python. Without uv + a WORKING Python every hook dies at session start while
7
+ * `claude plugin list` still shows a cheerful checkmark. So preflight is a
8
+ * loud, actionable gate, not a warning buried above "Command Center running".
9
+ *
10
+ * The Windows trap this file exists to dodge: `python3` is usually the
11
+ * Microsoft-Store App-Execution-Alias stub — `command -v`/`where` FINDS it, but
12
+ * running it does nothing. We therefore probe by TEST-RUNNING `--version` and
13
+ * accept the first candidate that actually reports one (ported from
14
+ * `scripts/verify-setup.sh`), never by mere presence on PATH.
15
+ */
16
+
17
+ import { spawnSync } from "node:child_process";
18
+ import os from "node:os";
19
+
20
+ import { MIN_NODE, compareSemver, installHint, isWindows } from "./util.mjs";
21
+ import { probeEnv, resolvePosixBin } from "./probe-path.mjs";
22
+ import { resolveSpawn } from "./win32-spawn.mjs";
23
+
24
+ /**
25
+ * Default runner: invoke `<cmd> --version` with `shell: false` and report
26
+ * whether it actually ran.
27
+ *
28
+ * On Windows some of these tools are `.cmd`/`.bat` shims (`claude` from an npm
29
+ * install) that a bare `shell: false` spawn CANNOT reach — probing them that way
30
+ * would report a perfectly-installed Claude as ABSENT and skip the plugin phase
31
+ * forever. That used to be answered with `shell: true`; `resolveSpawn` now finds
32
+ * the real file via PATHEXT and routes a shim through an explicit
33
+ * `cmd.exe /d /s /c` instead (see lib/win32-spawn.mjs).
34
+ *
35
+ * CALLER INVARIANT, still load-bearing: `cmd` is a fixed internal literal
36
+ * (claude / uv / python3 / python / py / git / gh) and `args` is a fixed internal
37
+ * literal too — `["--version"]` for every probe, plus the one `["python",
38
+ * "find", ">=3.11"]` uv-can-supply-Python predicate. NO caller passes user input.
39
+ * NOTHING here validates that — `defaultRun` is exported from a published `lib/`
40
+ * and reaches the cmd.exe wrap with no charset gate of its own (unlike
41
+ * `claude-cli.mjs`, which has `SAFE_ARG`). Only `runPreflight`'s call sites
42
+ * honour it. A caller passing user input must gate it first.
43
+ *
44
+ * An unresolvable name yields `ok: false` — exactly the verdict the old shell
45
+ * path produced via a non-zero exit. Note what "unresolvable" now means: a bare
46
+ * name that PATHEXT cannot place falls back to a direct `CreateProcess` spawn
47
+ * rather than being abandoned, because `realpath` cannot follow a Windows
48
+ * App-Execution-Alias at all — so a genuinely INSTALLED Microsoft-Store Python
49
+ * would otherwise be reported absent. The Store STUB is still rejected, by the
50
+ * unchanged `\d+\.\d+` output requirement below rather than by failing to
51
+ * resolve. Both verified on Windows 11, 2026-07-31.
52
+ * @param {string} cmd @param {string[]} [args]
53
+ * @returns {{ ok: boolean, stdout: string, stderr: string, code: number | null }}
54
+ */
55
+ export function defaultRun(cmd, args = ["--version"], deps = {}) {
56
+ const platform = deps.platform ?? process.platform;
57
+ const homedir = deps.homedir ?? os.homedir();
58
+ const env = probeEnv(platform, homedir, deps.env ?? process.env);
59
+ try {
60
+ // Windows resolves shims via PATHEXT against the augmented `env`; POSIX
61
+ // resolves the absolute path itself so the augmented PATH actually applies.
62
+ const plan =
63
+ platform === "win32"
64
+ ? resolveSpawn([cmd, ...args], { platform, env })
65
+ : { command: resolvePosixBin(cmd, env), args };
66
+ // UNREACHABLE as of iterate-2026-07-31 (win32-spawn divergence 5: an
67
+ // unresolvable bare name comes back as itself). `spawnSync` does NOT throw
68
+ // for a missing binary — it returns `{ error: ENOENT, status: null }` — so
69
+ // the failure is carried by the `!r.error` term in `ok` below and never
70
+ // reaches the catch. (An earlier version of this comment said "lands in the
71
+ // catch"; Stage-3 doubt review disproved it. The verdict was always right,
72
+ // the stated mechanism was not.) Kept as contract insurance.
73
+ if (!plan) return { ok: false, stdout: "", stderr: "", code: null };
74
+ const r = spawnSync(plan.command, plan.args, {
75
+ encoding: "utf-8",
76
+ shell: false,
77
+ timeout: 8000,
78
+ env,
79
+ windowsVerbatimArguments: plan.windowsVerbatimArguments,
80
+ });
81
+ const stdout = (r.stdout ?? "").toString();
82
+ const stderr = (r.stderr ?? "").toString();
83
+ // A real interpreter/tool exits 0 AND prints a version somewhere. The MS
84
+ // Store python3 stub exits non-zero (or prints its "not found" nag), so
85
+ // requiring both status 0 and a digit-bearing line rejects it.
86
+ const ok = r.status === 0 && !r.error && /\d+\.\d+/.test(stdout + stderr);
87
+ return { ok, stdout, stderr, code: r.status ?? null };
88
+ } catch {
89
+ return { ok: false, stdout: "", stderr: "", code: null };
90
+ }
91
+ }
92
+
93
+ /** First `\d+.\d+(.\d+)?` token in a `--version` blob, or "". */
94
+ export function extractVersion(out) {
95
+ const m = /(\d+\.\d+(?:\.\d+)?)/.exec(String(out ?? ""));
96
+ return m ? m[1] : "";
97
+ }
98
+
99
+ /**
100
+ * Resolve ONE working Python by test-running `--version` (python3 → python →
101
+ * py). Returns `{ bin, version }` or `null`. The MS-Store stub fails the
102
+ * `run().ok` test and is skipped — the whole reason this is not `command -v`.
103
+ * @param {(cmd: string, args?: string[]) => ReturnType<typeof defaultRun>} run
104
+ */
105
+ export function resolvePython(run) {
106
+ for (const bin of ["python3", "python", "py"]) {
107
+ const r = run(bin, ["--version"]);
108
+ if (r.ok) return { bin, version: extractVersion(r.stdout + r.stderr) };
109
+ }
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Run the full preflight. Pure over its injected seams (`run`, `nodeVersion`,
115
+ * `platform`) so every branch is unit-testable without a real toolchain.
116
+ *
117
+ * @param {{
118
+ * run?: (cmd: string, args?: string[]) => ReturnType<typeof defaultRun>,
119
+ * nodeVersion?: string,
120
+ * platform?: NodeJS.Platform,
121
+ * }} [deps]
122
+ */
123
+ export function runPreflight(deps = {}) {
124
+ const { run = defaultRun, nodeVersion = process.version, platform = process.platform } = deps;
125
+ /** @type {{name:string, ok:boolean, detail:string, hint?:string, hard:boolean, optional?:boolean}[]} */
126
+ const checks = [];
127
+
128
+ // claude — its own gate: absent → skip plugins + loud warning + non-zero exit.
129
+ const claude = run("claude", ["--version"]);
130
+ checks.push({
131
+ name: "claude",
132
+ ok: claude.ok,
133
+ detail: claude.ok ? extractVersion(claude.stdout + claude.stderr) : "not found",
134
+ hint: claude.ok ? undefined : installHint("claude", platform),
135
+ hard: true,
136
+ });
137
+
138
+ // uv — hard requirement for every plugin hook.
139
+ const uv = run("uv", ["--version"]);
140
+ checks.push({
141
+ name: "uv",
142
+ ok: uv.ok,
143
+ detail: uv.ok ? extractVersion(uv.stdout + uv.stderr) : "not found",
144
+ hint: uv.ok ? undefined : installHint("uv", platform),
145
+ hard: true,
146
+ });
147
+
148
+ // python — the stack runs every hook via `uv run`, which resolves a
149
+ // uv-MANAGED interpreter. So a working system python3 >= 3.11 satisfies the
150
+ // gate, but so does uv being able to PROVIDE 3.11+ — the common Mac case where
151
+ // `uv python install 3.11` left system `python3` at 3.9. System probe first
152
+ // (TEST-RUN, Store-stub trap); only fall through to uv when it is short.
153
+ const py = resolvePython(run);
154
+ const sysOk = py != null && compareSemver(py.version, "3.11.0") >= 0;
155
+ let pyOk = sysOk;
156
+ let pyDetail = py ? `${py.version} (${py.bin})` : "not found (tried python3, python, py)";
157
+ if (!pyOk && uv.ok) {
158
+ // `uv python find` locates an installed 3.11+ WITHOUT downloading — a clean,
159
+ // side-effect-free predicate for "uv can supply Python for the hooks".
160
+ const uvPy = run("uv", ["python", "find", ">=3.11"]);
161
+ // Gate on EXIT CODE, not defaultRun's version-shaped `ok`: `uv python find`
162
+ // prints a PATH, and a resolved interpreter whose path carries no decimal
163
+ // (e.g. a `/usr/local/bin/python3` symlink) would fail the `\d+\.\d+` output
164
+ // test despite a clean exit — re-introducing the very false "not found" this
165
+ // change removes. uv exits 0 on a hit, 2 on a miss (verified 2026-08-22).
166
+ if (uvPy.code === 0) {
167
+ pyOk = true;
168
+ pyDetail = `${extractVersion(uvPy.stdout + uvPy.stderr) || "3.11+"} (uv-managed)`;
169
+ }
170
+ }
171
+ checks.push({
172
+ name: "python",
173
+ ok: pyOk,
174
+ detail: pyDetail,
175
+ hint: pyOk ? undefined : installHint("python", platform),
176
+ hard: true,
177
+ });
178
+
179
+ // node — assert the packaged server's minimum.
180
+ const nodeOk = compareSemver(nodeVersion, MIN_NODE) >= 0;
181
+ checks.push({
182
+ name: "node",
183
+ ok: nodeOk,
184
+ detail: nodeOk ? nodeVersion : `${nodeVersion} (need >= ${MIN_NODE})`,
185
+ hint: nodeOk ? undefined : installHint("node", platform),
186
+ hard: true,
187
+ });
188
+
189
+ // git — needed by the SDLC plugins.
190
+ const git = run("git", ["--version"]);
191
+ checks.push({
192
+ name: "git",
193
+ ok: git.ok,
194
+ detail: git.ok ? extractVersion(git.stdout + git.stderr) : "not found",
195
+ hint: git.ok ? undefined : installHint("git", platform),
196
+ hard: true,
197
+ });
198
+
199
+ // gh — optional (PRs). A soft note, never a failure.
200
+ const gh = run("gh", ["--version"]);
201
+ checks.push({
202
+ name: "gh",
203
+ ok: gh.ok,
204
+ detail: gh.ok ? extractVersion(gh.stdout + gh.stderr) : "not found (optional — needed for PRs)",
205
+ hard: false,
206
+ optional: true,
207
+ });
208
+
209
+ const hardFailures = checks.filter((c) => c.hard && !c.ok);
210
+ // The plugin phase needs claude + uv + python (hook runtime) AND git —
211
+ // `claude plugin marketplace add` clones the marketplace over git, and the
212
+ // SDLC plugins are git-based. Node is NOT gated here: it gates the SERVER
213
+ // (and npx already blocks a too-old engine), not the plugin install.
214
+ const pluginPhaseOk = claude.ok && uv.ok && pyOk && git.ok;
215
+
216
+ return {
217
+ checks,
218
+ python: py,
219
+ hasClaude: claude.ok,
220
+ hasUv: uv.ok,
221
+ hasPython: pyOk,
222
+ hasNode: nodeOk,
223
+ hasGit: git.ok,
224
+ /** Plugins can only be installed when claude + uv + python are all real. */
225
+ pluginPhaseOk,
226
+ /** Non-zero exit when any hard prerequisite is missing (AC1a). */
227
+ exitCode: hardFailures.length,
228
+ isWindows: isWindows(platform),
229
+ };
230
+ }
231
+
232
+ /** Render the one honest verdict block a `doctor`-style tool prints. */
233
+ export function renderVerdict(result, mark) {
234
+ const lines = ["Prerequisites:"];
235
+ for (const c of result.checks) {
236
+ const glyph = c.ok ? mark.pass : c.optional ? mark.skip : mark.fail;
237
+ lines.push(` ${glyph} ${c.name}: ${c.detail}`);
238
+ if (!c.ok && c.hint) lines.push(` -> ${c.hint}`);
239
+ }
240
+ // A just-installed tool lands in ~/.local/bin but the running shell's PATH was
241
+ // captured before the install — so "not found" here is often really "not on
242
+ // PATH yet". Say so, once, OS-aware, whenever a hard prerequisite is missing.
243
+ if (result.checks.some((c) => c.hard && !c.ok)) {
244
+ lines.push("");
245
+ lines.push(
246
+ result.isWindows
247
+ ? " Note: if you JUST installed one of these, open a NEW terminal so it lands on your PATH, then re-run."
248
+ : " Note: if you JUST installed one of these, open a new terminal (or `source ~/.zshrc` / `~/.bashrc`) so it lands on your PATH, then re-run.",
249
+ );
250
+ }
251
+ return lines.join("\n");
252
+ }