@svenroth-ai/shipwright 0.25.0 → 0.27.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 (206) hide show
  1. package/client/dist/assets/{EmbeddedTerminal-5QvKe3_a.js → EmbeddedTerminal-CqCQUDtp.js} +1 -1
  2. package/client/dist/assets/{abnfDiagram-VRR7QNED-CXi9HegQ.js → abnfDiagram-VRR7QNED-DX2objTP.js} +1 -1
  3. package/client/dist/assets/{arc-DLh4GiqR.js → arc-DQmjbsFU.js} +1 -1
  4. package/client/dist/assets/{architectureDiagram-ZJ3FMSHR-DUypQ8dQ.js → architectureDiagram-ZJ3FMSHR-DAvCsPCW.js} +1 -1
  5. package/client/dist/assets/{blockDiagram-677ZJIJ3-D2cVMKJG.js → blockDiagram-677ZJIJ3-DC-dlyYI.js} +1 -1
  6. package/client/dist/assets/{c4Diagram-LMCZKHZV-DtruBGJw.js → c4Diagram-LMCZKHZV-DSlmXfhE.js} +1 -1
  7. package/client/dist/assets/channel-BfmIcrp7.js +1 -0
  8. package/client/dist/assets/{chunk-2Q5K7J3B-CAfQkFw2.js → chunk-2Q5K7J3B-C111EHL6.js} +1 -1
  9. package/client/dist/assets/{chunk-32BRIVSS-CPWXoLnQ.js → chunk-32BRIVSS-DrCYs2XG.js} +1 -1
  10. package/client/dist/assets/{chunk-5VM5RSS4-xVc3oppQ.js → chunk-5VM5RSS4-DIejoaXS.js} +1 -1
  11. package/client/dist/assets/{chunk-EX3LRPZG-CUpl6fRR.js → chunk-EX3LRPZG-CGWNeH2g.js} +1 -1
  12. package/client/dist/assets/{chunk-JWPE2WC7-NejUp56x.js → chunk-JWPE2WC7-CCH0SRFY.js} +1 -1
  13. package/client/dist/assets/{chunk-MOJQB5TN-C9-2lLg0.js → chunk-MOJQB5TN-B3OpZGRv.js} +1 -1
  14. package/client/dist/assets/{chunk-RYQCIY6F-BAPtwABy.js → chunk-RYQCIY6F-BKhdp_8v.js} +1 -1
  15. package/client/dist/assets/{chunk-V7JOEXUC-Dwzihw26.js → chunk-V7JOEXUC-B6Bu8x1y.js} +1 -1
  16. package/client/dist/assets/{chunk-VR4S4FIN-9-iArFJM.js → chunk-VR4S4FIN-OgFi2_iB.js} +1 -1
  17. package/client/dist/assets/{chunk-XXDRQBXY-DiWHOrqe.js → chunk-XXDRQBXY-xyjuuhxl.js} +1 -1
  18. package/client/dist/assets/classDiagram-OUVF2IWQ-HfUQZ6Uc.js +1 -0
  19. package/client/dist/assets/classDiagram-v2-EOCWNBFH-HfUQZ6Uc.js +1 -0
  20. package/client/dist/assets/{cose-bilkent-JH36ORCC-Cp04EOcH.js → cose-bilkent-JH36ORCC-BQIH3SCK.js} +1 -1
  21. package/client/dist/assets/{cynefin-VYW2F7L2-D--GN1v8.js → cynefin-VYW2F7L2-oyLAz8te.js} +1 -1
  22. package/client/dist/assets/{cynefinDiagram-TSTJHNR4-BGH0o_qp.js → cynefinDiagram-TSTJHNR4-DoLCsH0l.js} +1 -1
  23. package/client/dist/assets/{dagre-VKFMJZFB-DSKkIKlj.js → dagre-VKFMJZFB-D7djx1_Y.js} +1 -1
  24. package/client/dist/assets/{diagram-FQU43EPY-Cs_C4lb-.js → diagram-FQU43EPY-DFuYE0Vf.js} +1 -1
  25. package/client/dist/assets/{diagram-G47NLZAW-oDtAA8O_.js → diagram-G47NLZAW-CffCZVXk.js} +1 -1
  26. package/client/dist/assets/{diagram-NH7WQ7WH-BiFMjqSH.js → diagram-NH7WQ7WH-BhrHtUlt.js} +1 -1
  27. package/client/dist/assets/{diagram-OA4YK3LP-Ccs3thCj.js → diagram-OA4YK3LP-CwQPbOn0.js} +1 -1
  28. package/client/dist/assets/{diagram-WEI45ONY-CdZNqgO3.js → diagram-WEI45ONY-abtYnYqH.js} +1 -1
  29. package/client/dist/assets/{ebnfDiagram-CCIWWBDH-CapTA0VL.js → ebnfDiagram-CCIWWBDH-DfV7gT4m.js} +1 -1
  30. package/client/dist/assets/{erDiagram-Q63AITRT-BcRNEP8s.js → erDiagram-Q63AITRT-vtbCprgH.js} +1 -1
  31. package/client/dist/assets/{flowDiagram-23GEKE2U-CJ_zF7e_.js → flowDiagram-23GEKE2U-CVCgcyDF.js} +1 -1
  32. package/client/dist/assets/{ganttDiagram-NO4QXBWP-CnJdcg9A.js → ganttDiagram-NO4QXBWP-BclyOgMb.js} +1 -1
  33. package/client/dist/assets/{gitGraphDiagram-IHSO6WYX-YTUQ_ELk.js → gitGraphDiagram-IHSO6WYX-BXT4rIKj.js} +1 -1
  34. package/client/dist/assets/index-CKU9dY18.css +1 -0
  35. package/client/dist/assets/index-DOMl6atu.js +847 -0
  36. package/client/dist/assets/{infoDiagram-FWYZ7A6U-CLOP_ZWD.js → infoDiagram-FWYZ7A6U-Dr4mN24J.js} +1 -1
  37. package/client/dist/assets/{ishikawaDiagram-FXEZZL3T-aFTIzr7o.js → ishikawaDiagram-FXEZZL3T-BBUFWq5C.js} +1 -1
  38. package/client/dist/assets/{journeyDiagram-5HDEW3XC-CbPJB4xd.js → journeyDiagram-5HDEW3XC-CmclipcQ.js} +1 -1
  39. package/client/dist/assets/{kanban-definition-HUTT4EX6-BoyhytIS.js → kanban-definition-HUTT4EX6-eIzUeZ03.js} +1 -1
  40. package/client/dist/assets/{linear-1VXA-uBg.js → linear-DGiT7uKK.js} +1 -1
  41. package/client/dist/assets/{mermaid.core-D_lO2yK9.js → mermaid.core-BcLNGbxW.js} +4 -4
  42. package/client/dist/assets/{mindmap-definition-LN4V7U3C-iFK7LXMx.js → mindmap-definition-LN4V7U3C-C4ANnNiP.js} +1 -1
  43. package/client/dist/assets/{pegDiagram-2B236MQR-DNmcmfA-.js → pegDiagram-2B236MQR-DnMeh3Yh.js} +1 -1
  44. package/client/dist/assets/{pieDiagram-ENE6RG2P-ekx83O5b.js → pieDiagram-ENE6RG2P-CKV_rkhF.js} +1 -1
  45. package/client/dist/assets/{quadrantDiagram-ABIIQ3AL-DayyD1l8.js → quadrantDiagram-ABIIQ3AL-BuUsGwHh.js} +1 -1
  46. package/client/dist/assets/{railroadDiagram-RFXS5EU6-Bdgz7S6G.js → railroadDiagram-RFXS5EU6-BwwdKnFL.js} +1 -1
  47. package/client/dist/assets/{requirementDiagram-TGXJPOKE-szP3Muld.js → requirementDiagram-TGXJPOKE-CpQZouJV.js} +1 -1
  48. package/client/dist/assets/{sankeyDiagram-HTMAVEWB-Ct3rzgUj.js → sankeyDiagram-HTMAVEWB-4fnDUNVa.js} +1 -1
  49. package/client/dist/assets/{sequenceDiagram-DBY2YBRQ-_69EfmOU.js → sequenceDiagram-DBY2YBRQ-BOcaWlM0.js} +1 -1
  50. package/client/dist/assets/{sizeCapture-X5ZJPWSS-BSOU06J2.js → sizeCapture-X5ZJPWSS-_pv-9R0c.js} +1 -1
  51. package/client/dist/assets/{stateDiagram-2N3HPSRC-HmOmFwQm.js → stateDiagram-2N3HPSRC-CFwCOonY.js} +1 -1
  52. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-srkiu3Ib.js +1 -0
  53. package/client/dist/assets/{swimlanes-5IMT3BWC-CVg_4oT7.js → swimlanes-5IMT3BWC-DFa2tzxu.js} +2 -2
  54. package/client/dist/assets/swimlanesDiagram-G3AALYLV-Dq-0K9Zm.js +8 -0
  55. package/client/dist/assets/{timeline-definition-FHXFAJF6-CBBaDKXm.js → timeline-definition-FHXFAJF6-rhGYElgn.js} +1 -1
  56. package/client/dist/assets/{vennDiagram-L72KCM5P-CBXiWPMc.js → vennDiagram-L72KCM5P-BoGjRxyA.js} +1 -1
  57. package/client/dist/assets/{wardleyDiagram-EHGQE667-uhBCZoW0.js → wardleyDiagram-EHGQE667-D-IGv5bH.js} +1 -1
  58. package/client/dist/assets/{xychartDiagram-FW5EYKEG-xxE_wY5O.js → xychartDiagram-FW5EYKEG-D9PFTCfm.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/probes.mjs +10 -4
  64. package/lib/server.mjs +506 -369
  65. package/lib/util.mjs +157 -157
  66. package/lib/win32-spawn.mjs +253 -253
  67. package/package.json +68 -68
  68. package/scripts/deploy-procs.mjs +208 -208
  69. package/scripts/deploy-swap.mjs +276 -276
  70. package/scripts/kill-targets.js +140 -140
  71. package/scripts/repair-claude-json.mjs +295 -295
  72. package/server/dist/config/default-actions.json +90 -90
  73. package/server/dist/core/mission-context/association.d.ts +46 -0
  74. package/server/dist/core/mission-context/association.d.ts.map +1 -1
  75. package/server/dist/core/mission-context/association.js +69 -0
  76. package/server/dist/core/mission-context/association.js.map +1 -1
  77. package/server/dist/core/mission-context/resolver-iterate.d.ts +6 -0
  78. package/server/dist/core/mission-context/resolver-iterate.d.ts.map +1 -0
  79. package/server/dist/core/mission-context/resolver-iterate.js +202 -0
  80. package/server/dist/core/mission-context/resolver-iterate.js.map +1 -0
  81. package/server/dist/core/mission-context/resolver-parts.d.ts +20 -0
  82. package/server/dist/core/mission-context/resolver-parts.d.ts.map +1 -1
  83. package/server/dist/core/mission-context/resolver-parts.js +92 -0
  84. package/server/dist/core/mission-context/resolver-parts.js.map +1 -1
  85. package/server/dist/core/mission-context/resolver.d.ts +1 -1
  86. package/server/dist/core/mission-context/resolver.d.ts.map +1 -1
  87. package/server/dist/core/mission-context/resolver.js +17 -199
  88. package/server/dist/core/mission-context/resolver.js.map +1 -1
  89. package/server/dist/core/mission-context/run-id-recovery.d.ts +21 -5
  90. package/server/dist/core/mission-context/run-id-recovery.d.ts.map +1 -1
  91. package/server/dist/core/mission-context/run-id-recovery.js +32 -5
  92. package/server/dist/core/mission-context/run-id-recovery.js.map +1 -1
  93. package/server/dist/core/mission-context/scenario.d.ts +18 -11
  94. package/server/dist/core/mission-context/scenario.d.ts.map +1 -1
  95. package/server/dist/core/mission-context/scenario.js +40 -3
  96. package/server/dist/core/mission-context/scenario.js.map +1 -1
  97. package/server/dist/core/path-guard.d.ts +1 -1
  98. package/server/dist/core/path-guard.d.ts.map +1 -1
  99. package/server/dist/core/path-guard.js +9 -2
  100. package/server/dist/core/path-guard.js.map +1 -1
  101. package/server/dist/core/sdk-sessions-merge.d.ts +19 -0
  102. package/server/dist/core/sdk-sessions-merge.d.ts.map +1 -1
  103. package/server/dist/core/sdk-sessions-merge.js +19 -5
  104. package/server/dist/core/sdk-sessions-merge.js.map +1 -1
  105. package/server/dist/core/sdk-sessions-refresh.d.ts +50 -0
  106. package/server/dist/core/sdk-sessions-refresh.d.ts.map +1 -0
  107. package/server/dist/core/sdk-sessions-refresh.js +69 -0
  108. package/server/dist/core/sdk-sessions-refresh.js.map +1 -0
  109. package/server/dist/core/sdk-sessions-store.d.ts +2 -0
  110. package/server/dist/core/sdk-sessions-store.d.ts.map +1 -1
  111. package/server/dist/core/sdk-sessions-store.js +7 -9
  112. package/server/dist/core/sdk-sessions-store.js.map +1 -1
  113. package/server/dist/core/shipslog-docs-reader.d.ts +4 -0
  114. package/server/dist/core/shipslog-docs-reader.d.ts.map +1 -0
  115. package/server/dist/core/shipslog-docs-reader.js +181 -0
  116. package/server/dist/core/shipslog-docs-reader.js.map +1 -0
  117. package/server/dist/core/shipslog-docs-types.d.ts +15 -0
  118. package/server/dist/core/shipslog-docs-types.d.ts.map +1 -0
  119. package/server/dist/core/shipslog-docs-types.js +14 -0
  120. package/server/dist/core/shipslog-docs-types.js.map +1 -0
  121. package/server/dist/core/shipwright-plugin-version-reader.d.ts +15 -0
  122. package/server/dist/core/shipwright-plugin-version-reader.d.ts.map +1 -0
  123. package/server/dist/core/shipwright-plugin-version-reader.js +49 -0
  124. package/server/dist/core/shipwright-plugin-version-reader.js.map +1 -0
  125. package/server/dist/external/design-review/serve.js +27 -27
  126. package/server/dist/external/inbox/_derive.d.ts +2 -25
  127. package/server/dist/external/inbox/_derive.d.ts.map +1 -1
  128. package/server/dist/external/inbox/_derive.js.map +1 -1
  129. package/server/dist/external/inbox/_lead.d.ts +23 -0
  130. package/server/dist/external/inbox/_lead.d.ts.map +1 -0
  131. package/server/dist/external/inbox/_lead.js +83 -0
  132. package/server/dist/external/inbox/_lead.js.map +1 -0
  133. package/server/dist/external/inbox/_types.d.ts +32 -0
  134. package/server/dist/external/inbox/_types.d.ts.map +1 -0
  135. package/server/dist/external/inbox/_types.js +10 -0
  136. package/server/dist/external/inbox/_types.js.map +1 -0
  137. package/server/dist/external/inbox/routes.d.ts.map +1 -1
  138. package/server/dist/external/inbox/routes.js +29 -1
  139. package/server/dist/external/inbox/routes.js.map +1 -1
  140. package/server/dist/external/launch/claim-holder-gate.d.ts +42 -0
  141. package/server/dist/external/launch/claim-holder-gate.d.ts.map +1 -0
  142. package/server/dist/external/launch/claim-holder-gate.js +87 -0
  143. package/server/dist/external/launch/claim-holder-gate.js.map +1 -0
  144. package/server/dist/external/launch/routes.d.ts.map +1 -1
  145. package/server/dist/external/launch/routes.js +23 -19
  146. package/server/dist/external/launch/routes.js.map +1 -1
  147. package/server/dist/external/mission-context/association-write.d.ts +22 -0
  148. package/server/dist/external/mission-context/association-write.d.ts.map +1 -0
  149. package/server/dist/external/mission-context/association-write.js +90 -0
  150. package/server/dist/external/mission-context/association-write.js.map +1 -0
  151. package/server/dist/external/mission-context/routes.d.ts.map +1 -1
  152. package/server/dist/external/mission-context/routes.js +18 -31
  153. package/server/dist/external/mission-context/routes.js.map +1 -1
  154. package/server/dist/external/org/lead-question-threads.d.ts +46 -0
  155. package/server/dist/external/org/lead-question-threads.d.ts.map +1 -0
  156. package/server/dist/external/org/lead-question-threads.js +132 -0
  157. package/server/dist/external/org/lead-question-threads.js.map +1 -0
  158. package/server/dist/external/org/mount.d.ts +3 -0
  159. package/server/dist/external/org/mount.d.ts.map +1 -1
  160. package/server/dist/external/org/mount.js +2 -1
  161. package/server/dist/external/org/mount.js.map +1 -1
  162. package/server/dist/external/org/usage.d.ts.map +1 -1
  163. package/server/dist/external/org/usage.js +20 -2
  164. package/server/dist/external/org/usage.js.map +1 -1
  165. package/server/dist/external/routes.d.ts.map +1 -1
  166. package/server/dist/external/routes.js +10 -13
  167. package/server/dist/external/routes.js.map +1 -1
  168. package/server/dist/external/shipslog-docs/routes.d.ts +10 -0
  169. package/server/dist/external/shipslog-docs/routes.d.ts.map +1 -0
  170. package/server/dist/external/shipslog-docs/routes.js +41 -0
  171. package/server/dist/external/shipslog-docs/routes.js.map +1 -0
  172. package/server/dist/routes/diagnostics.d.ts +12 -0
  173. package/server/dist/routes/diagnostics.d.ts.map +1 -1
  174. package/server/dist/routes/diagnostics.js +4 -1
  175. package/server/dist/routes/diagnostics.js.map +1 -1
  176. package/server/dist/routes/org-threads-composite.d.ts +24 -0
  177. package/server/dist/routes/org-threads-composite.d.ts.map +1 -0
  178. package/server/dist/routes/org-threads-composite.js +51 -0
  179. package/server/dist/routes/org-threads-composite.js.map +1 -0
  180. package/server/dist/routes/org.d.ts +5 -0
  181. package/server/dist/routes/org.d.ts.map +1 -1
  182. package/server/dist/routes/org.js +13 -0
  183. package/server/dist/routes/org.js.map +1 -1
  184. package/server/dist/terminal/pty-manager.d.ts +23 -1
  185. package/server/dist/terminal/pty-manager.d.ts.map +1 -1
  186. package/server/dist/terminal/pty-manager.js +22 -3
  187. package/server/dist/terminal/pty-manager.js.map +1 -1
  188. package/server/dist/terminal/pty-write-chunker.d.ts +109 -0
  189. package/server/dist/terminal/pty-write-chunker.d.ts.map +1 -0
  190. package/server/dist/terminal/pty-write-chunker.js +234 -0
  191. package/server/dist/terminal/pty-write-chunker.js.map +1 -0
  192. package/server/dist/types/org.d.ts +19 -0
  193. package/server/dist/types/org.d.ts.map +1 -1
  194. package/server/package.json +1 -1
  195. package/server/profiles/PROFILE_SCHEMA_VERSION +1 -1
  196. package/server/profiles/README.md +27 -27
  197. package/server/profiles/python-plugin-monorepo.json +35 -35
  198. package/server/profiles/supabase-nextjs.json +253 -253
  199. package/server/profiles/vite-hono.json +92 -92
  200. package/client/dist/assets/channel-C_k-9jsa.js +0 -1
  201. package/client/dist/assets/classDiagram-OUVF2IWQ-CsEPq9Ac.js +0 -1
  202. package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsEPq9Ac.js +0 -1
  203. package/client/dist/assets/index-6nZ1bt0a.css +0 -1
  204. package/client/dist/assets/index-irQhGNpn.js +0 -828
  205. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-BW8H33dv.js +0 -1
  206. package/client/dist/assets/swimlanesDiagram-G3AALYLV-BaRlxcPD.js +0 -8
@@ -1,253 +1,253 @@
1
- /**
2
- * win32-spawn.mjs — resolve a command to argv that `child_process` can run with
3
- * `shell: false`, including Windows `.cmd`/`.bat` shims.
4
- *
5
- * WHY THIS EXISTS. On Windows the tools this package probes are shims, not
6
- * executables: `claude` is often `claude.cmd`, `npm`/`npx` always are. Node's
7
- * CVE-2024-27980 hardening EINVAL-blocks a direct `.cmd` spawn under
8
- * `shell: false`, and `CreateProcess` only ever appends `.exe` — so a bare
9
- * `claude` simply is not found. The historical workaround was `shell: true`,
10
- * which buys shim resolution at the price of handing a command line to cmd.exe.
11
- * This module buys the same resolution without the shell: find the real file via
12
- * PATHEXT, then invoke `.cmd`/`.bat` through an explicit `cmd.exe /d /s /c`
13
- * with DISCRETE argv.
14
- *
15
- * MIRROR — this is a deliberate copy of `server/src/core/win32-spawn.ts`
16
- * (ADR-044, reviewed there; audit findings F03 + F31). It is duplicated rather
17
- * than imported because `bootstrapper/` is a separately published npm package
18
- * and DO-NOT #7 forbids cross-package imports.
19
- *
20
- * DIVERGENCES from the server original — the COMPLETE list, so a future drift
21
- * reviewer diffing the two files knows what is intentional:
22
- * 1. Failure SHAPE for an unresolvable BARE name. The server core returns
23
- * `null` and its thin `preview-win32-spawn.ts` wrapper converts that into
24
- * `PreviewProfileInvalidError`; this file returns a bare-name plan instead
25
- * — see divergence 5, which is the real behaviour and SUPERSEDES what this
26
- * entry used to claim. **Corrected 2026-08-01**: it read "returns `null`
27
- * here", which stopped being true when the App-Execution-Alias fix landed
28
- * (iterate-2026-07-31) and added the bare-name fallback. `resolveSpawn`
29
- * below can no longer return `null` on any path. The callers'
30
- * `if (!plan)` branches are therefore currently UNREACHABLE; they are kept
31
- * deliberately as cheap contract insurance, and are annotated as such at
32
- * their own sites rather than left implying a live code path.
33
- * 2. Signature. Here: `resolveSpawn(argv, { platform, env })`, both injectable
34
- * (the bootstrapper has no DI container, and its tests must reach the win32
35
- * branch from Linux). Server: `resolveSpawn(argv, cwd)`, reading the process
36
- * globals directly.
37
- * 3. No `cwd` parameter. A path-like name resolves against `process.cwd()`
38
- * here; the server resolves against the caller-supplied previewed-project
39
- * cwd, because it is handling an UNTRUSTED repo and this is not. The
40
- * security-relevant half is identical: a BARE name resolves from PATH only,
41
- * never cwd, in both.
42
- * 4. `win32ComSpec` is exported and env-parameterised here (server: private).
43
- * 5. An unresolvable BARE name falls back to spawning the bare name itself
44
- * (`{command: name}`) instead of giving up. The server refuses — its Guard 6
45
- * is a FROZEN security guard and stays. Safe here, and NOT a hole, because
46
- * verified on Windows 11: a bare-name spawn under `shell: false` is
47
- * PATH-only — a planted `.\swplantedexe.exe` with `cwd` set to its own
48
- * directory returns ENOENT, so cmd.exe's cwd-first lookup is still not in
49
- * play. It is REQUIRED because `realpathSync.native` cannot follow an
50
- * App-Execution-Alias reparse point at all, so PATHEXT resolution fails for
51
- * a genuinely INSTALLED Microsoft-Store Python exactly as it does for the
52
- * stub — and refusing would hard-fail the installer on a machine with a
53
- * working Python. `CreateProcess` follows the alias fine (verified: the
54
- * bare spawn reaches it and it answers).
55
- *
56
- * WHAT GUARDS THE PARITY. `test/win32-spawn.test.mjs` pins THIS file's contract
57
- * only — it cannot observe the server original changing. The cross-file guard is
58
- * `server/src/test/win32-spawn-mirror-parity.test.ts`, which reads both files and
59
- * fails if a security-load-bearing invariant is present in one and not the other.
60
- * It lives in the SERVER package on purpose: CI runs vitest for `client` and
61
- * `server` only, so a guard placed here would not gate anything.
62
- *
63
- * SECURITY posture, inherited verbatim from the original:
64
- * - A bare EXTENSION-LESS name resolves from PATH **only**, never the current
65
- * directory, so a planted `.\npm.cmd` cannot shadow the real tool. cmd.exe's
66
- * own lookup WOULD search cwd first, which is why an unresolved bare name is
67
- * never delegated to `cmd /c <bare>`; it goes to CreateProcess, which is
68
- * PATH-only (verified on Windows 11). NOTE the narrow wording: a bare name
69
- * that already CARRIES a `.cmd`/`.bat` extension skips resolution and is
70
- * wrapped as given, so `resolveSpawn(["npm.cmd"])` would reach cmd.exe's
71
- * cwd-first lookup. No caller does that (all three pass extension-less names
72
- * or absolute paths), and the case is pinned by a test so the gap cannot be
73
- * re-opened silently.
74
- * - Candidates are realpath-verified before use (same posture as
75
- * `server/src/core/path-guard.ts`).
76
- * - `resolveSpawn` never sets `options.shell`. cmd.exe gets discrete argv when
77
- * no token needs quoting; a spaced token produces the canonical verbatim
78
- * `cmd /d /s /c ""<quoted>" <args>"` line instead.
79
- *
80
- * PATH-FLAVOUR DISCIPLINE (iterate-2026-08-01-win32-spawn-followups). `platform`
81
- * is injected, but `path` follows the REAL host — so on Linux the win32 branch
82
- * used to parse `C:\…` with POSIX rules. Every `path.*` call is classified:
83
- * (a) win32 command-STRING semantics -> `path.win32` (`extname` x2, the
84
- * ComSpec `join`). No filesystem involved, so pinning the flavour is free
85
- * on Windows and makes the injected branch faithful on Linux.
86
- * (b) HOST-filesystem addressing -> the host `path` (`resolve` + `join` in
87
- * `resolveViaPathExt`), because those candidates are handed to
88
- * `realpathSync`/`statSync` and must name a file on the host actually
89
- * running. `PATH` splits on the hardcoded win32 `;`, already
90
- * host-independent. The server original carries the identical audit; the
91
- * parity guard pins both halves.
92
- */
93
-
94
- import path from "node:path";
95
- import { statSync, realpathSync } from "node:fs";
96
-
97
- const WIN32_EXECUTABLE_EXTS = new Set([".exe", ".com"]);
98
- const WIN32_SHIM_EXTS = new Set([".cmd", ".bat"]);
99
-
100
- /** Absolute path to cmd.exe, preferring the environment's own ComSpec. */
101
- export function win32ComSpec(env = process.env) {
102
- const fromEnv = env.ComSpec ?? env.COMSPEC;
103
- if (fromEnv && fromEnv.trim()) return fromEnv;
104
- const root = env.SystemRoot ?? env.windir ?? "C:\\Windows";
105
- // (a) pure string — win32 flavour pinned, so an injected win32 platform on a
106
- // POSIX host emits `…\System32\cmd.exe`, not the mixed `…/System32/cmd.exe`.
107
- return path.win32.join(root, "System32", "cmd.exe");
108
- }
109
-
110
- function looksPathLike(name) {
111
- return name.includes("\\") || name.includes("/") || /^[a-zA-Z]:/.test(name);
112
- }
113
-
114
- /**
115
- * Resolve `name` to a concrete on-disk file via PATHEXT, realpath-verified.
116
- * Returns undefined when nothing resolves.
117
- *
118
- * Every fs error is swallowed and the search continues — NOT only ENOENT.
119
- * Verified 2026-07-31 on Windows 11: `realpathSync.native` on the Microsoft
120
- * Store App-Execution-Alias `…\WindowsApps\python3.EXE` throws **EACCES**, not
121
- * ENOENT. Treating that as fatal would abandon the scan and report a machine
122
- * with a perfectly good `python` as having no Python at all.
123
- */
124
- function resolveViaPathExt(name, env) {
125
- const exts = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
126
- .split(";")
127
- .map((s) => s.trim())
128
- .filter(Boolean);
129
-
130
- const firstFile = (base, withBare) => {
131
- for (const ext of withBare ? ["", ...exts] : exts) {
132
- try {
133
- const real = realpathSync.native(base + ext);
134
- if (statSync(real).isFile()) return real;
135
- } catch {
136
- // no usable file at this candidate (ENOENT, EACCES on a Store alias,
137
- // ELOOP on a broken reparse point) — keep searching
138
- }
139
- }
140
- return undefined;
141
- };
142
-
143
- if (looksPathLike(name)) {
144
- // Explicit path — honour an exact match first, then PATHEXT.
145
- // (b) HOST flavour on purpose — this candidate goes straight to realpath.
146
- return firstFile(path.resolve(name), true);
147
- }
148
- // Bare command — PATH only, never cwd. The empty ext is excluded so an
149
- // extensionless POSIX shim is not matched. `;` is the win32 PATH delimiter,
150
- // hardcoded and therefore already host-independent.
151
- for (const dir of (env.PATH ?? env.Path ?? "").split(";")) {
152
- const trimmed = dir.trim();
153
- if (!trimmed) continue;
154
- // (b) HOST flavour on purpose — see the classification in the header.
155
- const hit = firstFile(path.join(trimmed, name), false);
156
- if (hit) return hit;
157
- }
158
- return undefined;
159
- }
160
-
161
- /**
162
- * Characters cmd.exe would otherwise INTERPRET, and which a surrounding pair of
163
- * double quotes makes literal. A token carrying one of these must therefore be
164
- * quoted even when it has no whitespace — otherwise a PATH directory such as
165
- * `C:\tools&more\` turns `…\tools&more\claude.cmd` into two commands.
166
- *
167
- * DELIBERATELY NOT IN THIS SET, because quoting does not fix them:
168
- * `%` — `%VAR%` expands inside double quotes too. No caller can supply one
169
- * (`SAFE_ARG` in claude-cli.mjs rejects `%`, preflight's args are fixed
170
- * literals), and the server original refuses `%` in its upstream fence.
171
- * `"` — a token containing a quote cannot be wrapped safely by this scheme at
172
- * all; the callers' charset gates are what keep one from arriving.
173
- * Both limits are stated rather than papered over.
174
- */
175
- const WIN32_CMD_SPECIAL = /[&|<>^]/;
176
-
177
- function win32NeedsQuote(token) {
178
- return token === "" || /\s/.test(token) || WIN32_CMD_SPECIAL.test(token);
179
- }
180
-
181
- export function win32CmdWrap(target, rest, env = process.env) {
182
- const parts = [target, ...rest];
183
- const command = win32ComSpec(env);
184
- // No token has a space → discrete argv: cmd.exe /d /s /c runs each token
185
- // literally and the caller keeps shell:false.
186
- if (!parts.some(win32NeedsQuote)) {
187
- return { command, args: ["/d", "/s", "/c", ...parts] };
188
- }
189
- // A token has a space (e.g. `C:\Program Files\nodejs\npm.cmd`). Under `cmd /s`
190
- // Node's own arg-quoting is stripped, so build the canonical
191
- // `cmd /d /s /c ""<quoted-shim>" <args>"` line and pass it verbatim: `/s`
192
- // strips ONLY the outer quote pair, leaving the inner quotes intact.
193
- const inner = parts.map((p) => (win32NeedsQuote(p) ? `"${p}"` : p)).join(" ");
194
- return {
195
- command,
196
- args: ["/d", "/s", "/c", `"${inner}"`],
197
- windowsVerbatimArguments: true,
198
- };
199
- }
200
-
201
- /**
202
- * Compute the `{command, args, windowsVerbatimArguments?}` to hand to
203
- * child_process with `shell: false`. POSIX is a pass-through.
204
- *
205
- * - argv0 is a real `.exe`/`.com` → spawn it directly (no cmd.exe).
206
- * - argv0 is a `.cmd`/`.bat` shim → run through `cmd.exe /d /s /c`.
207
- * - argv0 is a bare name → resolve via PATH+PATHEXT, then as above.
208
- * - nothing resolves for a bare name → the BARE name itself, for CreateProcess
209
- * to resolve PATH-only (divergence 5). This function does NOT return `null`
210
- * on any path; the `@returns` below said `| null` until 2026-08-01 and was
211
- * stale. Callers keep their `if (!plan)` guard as contract insurance.
212
- *
213
- * @param {string[]} argv command followed by its arguments
214
- * @param {{ platform?: string, env?: Record<string, string | undefined> }} [opts]
215
- * injected for tests; defaults read the live process
216
- * @returns {{ command: string, args: string[], windowsVerbatimArguments?: boolean }}
217
- */
218
- export function resolveSpawn(argv, opts = {}) {
219
- const platform = opts.platform ?? process.platform;
220
- const env = opts.env ?? process.env;
221
- if (platform !== "win32") {
222
- return { command: argv[0], args: argv.slice(1) };
223
- }
224
- const name = argv[0];
225
- const rest = argv.slice(1);
226
- // (a) pure string — win32 segment rules, so a POSIX host with an injected
227
- // win32 platform classifies `C:\a.b\tool` the way Windows does.
228
- const ext = path.win32.extname(name).toLowerCase();
229
-
230
- if (WIN32_EXECUTABLE_EXTS.has(ext)) {
231
- return { command: name, args: rest };
232
- }
233
- if (!WIN32_SHIM_EXTS.has(ext)) {
234
- const resolved = resolveViaPathExt(name, env);
235
- if (resolved) {
236
- // (a) `resolved` is a host realpath; `path.win32` reads `/` as a
237
- // separator too, so this is correct on either host.
238
- if (WIN32_EXECUTABLE_EXTS.has(path.win32.extname(resolved).toLowerCase())) {
239
- return { command: resolved, args: rest };
240
- }
241
- return win32CmdWrap(resolved, rest, env);
242
- }
243
- // Unresolved BARE name. Never DELEGATE it to `cmd /d /s /c <bare>` — that is
244
- // cmd.exe's own cwd-first lookup and a planted `.\claude.cmd` would win it.
245
- // Hand it to CreateProcess directly instead: verified PATH-only (a planted
246
- // `.\x.exe` is ENOENT even with cwd set to its directory), and the only way
247
- // to reach an App-Execution-Alias, which realpath cannot follow — that is
248
- // how a real Store-installed Python stays detectable. A truly absent tool
249
- // comes back ENOENT and the callers turn that into their "not found" verdict.
250
- if (!looksPathLike(name)) return { command: name, args: rest };
251
- }
252
- return win32CmdWrap(name, rest, env);
253
- }
1
+ /**
2
+ * win32-spawn.mjs — resolve a command to argv that `child_process` can run with
3
+ * `shell: false`, including Windows `.cmd`/`.bat` shims.
4
+ *
5
+ * WHY THIS EXISTS. On Windows the tools this package probes are shims, not
6
+ * executables: `claude` is often `claude.cmd`, `npm`/`npx` always are. Node's
7
+ * CVE-2024-27980 hardening EINVAL-blocks a direct `.cmd` spawn under
8
+ * `shell: false`, and `CreateProcess` only ever appends `.exe` — so a bare
9
+ * `claude` simply is not found. The historical workaround was `shell: true`,
10
+ * which buys shim resolution at the price of handing a command line to cmd.exe.
11
+ * This module buys the same resolution without the shell: find the real file via
12
+ * PATHEXT, then invoke `.cmd`/`.bat` through an explicit `cmd.exe /d /s /c`
13
+ * with DISCRETE argv.
14
+ *
15
+ * MIRROR — this is a deliberate copy of `server/src/core/win32-spawn.ts`
16
+ * (ADR-044, reviewed there; audit findings F03 + F31). It is duplicated rather
17
+ * than imported because `bootstrapper/` is a separately published npm package
18
+ * and DO-NOT #7 forbids cross-package imports.
19
+ *
20
+ * DIVERGENCES from the server original — the COMPLETE list, so a future drift
21
+ * reviewer diffing the two files knows what is intentional:
22
+ * 1. Failure SHAPE for an unresolvable BARE name. The server core returns
23
+ * `null` and its thin `preview-win32-spawn.ts` wrapper converts that into
24
+ * `PreviewProfileInvalidError`; this file returns a bare-name plan instead
25
+ * — see divergence 5, which is the real behaviour and SUPERSEDES what this
26
+ * entry used to claim. **Corrected 2026-08-01**: it read "returns `null`
27
+ * here", which stopped being true when the App-Execution-Alias fix landed
28
+ * (iterate-2026-07-31) and added the bare-name fallback. `resolveSpawn`
29
+ * below can no longer return `null` on any path. The callers'
30
+ * `if (!plan)` branches are therefore currently UNREACHABLE; they are kept
31
+ * deliberately as cheap contract insurance, and are annotated as such at
32
+ * their own sites rather than left implying a live code path.
33
+ * 2. Signature. Here: `resolveSpawn(argv, { platform, env })`, both injectable
34
+ * (the bootstrapper has no DI container, and its tests must reach the win32
35
+ * branch from Linux). Server: `resolveSpawn(argv, cwd)`, reading the process
36
+ * globals directly.
37
+ * 3. No `cwd` parameter. A path-like name resolves against `process.cwd()`
38
+ * here; the server resolves against the caller-supplied previewed-project
39
+ * cwd, because it is handling an UNTRUSTED repo and this is not. The
40
+ * security-relevant half is identical: a BARE name resolves from PATH only,
41
+ * never cwd, in both.
42
+ * 4. `win32ComSpec` is exported and env-parameterised here (server: private).
43
+ * 5. An unresolvable BARE name falls back to spawning the bare name itself
44
+ * (`{command: name}`) instead of giving up. The server refuses — its Guard 6
45
+ * is a FROZEN security guard and stays. Safe here, and NOT a hole, because
46
+ * verified on Windows 11: a bare-name spawn under `shell: false` is
47
+ * PATH-only — a planted `.\swplantedexe.exe` with `cwd` set to its own
48
+ * directory returns ENOENT, so cmd.exe's cwd-first lookup is still not in
49
+ * play. It is REQUIRED because `realpathSync.native` cannot follow an
50
+ * App-Execution-Alias reparse point at all, so PATHEXT resolution fails for
51
+ * a genuinely INSTALLED Microsoft-Store Python exactly as it does for the
52
+ * stub — and refusing would hard-fail the installer on a machine with a
53
+ * working Python. `CreateProcess` follows the alias fine (verified: the
54
+ * bare spawn reaches it and it answers).
55
+ *
56
+ * WHAT GUARDS THE PARITY. `test/win32-spawn.test.mjs` pins THIS file's contract
57
+ * only — it cannot observe the server original changing. The cross-file guard is
58
+ * `server/src/test/win32-spawn-mirror-parity.test.ts`, which reads both files and
59
+ * fails if a security-load-bearing invariant is present in one and not the other.
60
+ * It lives in the SERVER package on purpose: CI runs vitest for `client` and
61
+ * `server` only, so a guard placed here would not gate anything.
62
+ *
63
+ * SECURITY posture, inherited verbatim from the original:
64
+ * - A bare EXTENSION-LESS name resolves from PATH **only**, never the current
65
+ * directory, so a planted `.\npm.cmd` cannot shadow the real tool. cmd.exe's
66
+ * own lookup WOULD search cwd first, which is why an unresolved bare name is
67
+ * never delegated to `cmd /c <bare>`; it goes to CreateProcess, which is
68
+ * PATH-only (verified on Windows 11). NOTE the narrow wording: a bare name
69
+ * that already CARRIES a `.cmd`/`.bat` extension skips resolution and is
70
+ * wrapped as given, so `resolveSpawn(["npm.cmd"])` would reach cmd.exe's
71
+ * cwd-first lookup. No caller does that (all three pass extension-less names
72
+ * or absolute paths), and the case is pinned by a test so the gap cannot be
73
+ * re-opened silently.
74
+ * - Candidates are realpath-verified before use (same posture as
75
+ * `server/src/core/path-guard.ts`).
76
+ * - `resolveSpawn` never sets `options.shell`. cmd.exe gets discrete argv when
77
+ * no token needs quoting; a spaced token produces the canonical verbatim
78
+ * `cmd /d /s /c ""<quoted>" <args>"` line instead.
79
+ *
80
+ * PATH-FLAVOUR DISCIPLINE (iterate-2026-08-01-win32-spawn-followups). `platform`
81
+ * is injected, but `path` follows the REAL host — so on Linux the win32 branch
82
+ * used to parse `C:\…` with POSIX rules. Every `path.*` call is classified:
83
+ * (a) win32 command-STRING semantics -> `path.win32` (`extname` x2, the
84
+ * ComSpec `join`). No filesystem involved, so pinning the flavour is free
85
+ * on Windows and makes the injected branch faithful on Linux.
86
+ * (b) HOST-filesystem addressing -> the host `path` (`resolve` + `join` in
87
+ * `resolveViaPathExt`), because those candidates are handed to
88
+ * `realpathSync`/`statSync` and must name a file on the host actually
89
+ * running. `PATH` splits on the hardcoded win32 `;`, already
90
+ * host-independent. The server original carries the identical audit; the
91
+ * parity guard pins both halves.
92
+ */
93
+
94
+ import path from "node:path";
95
+ import { statSync, realpathSync } from "node:fs";
96
+
97
+ const WIN32_EXECUTABLE_EXTS = new Set([".exe", ".com"]);
98
+ const WIN32_SHIM_EXTS = new Set([".cmd", ".bat"]);
99
+
100
+ /** Absolute path to cmd.exe, preferring the environment's own ComSpec. */
101
+ export function win32ComSpec(env = process.env) {
102
+ const fromEnv = env.ComSpec ?? env.COMSPEC;
103
+ if (fromEnv && fromEnv.trim()) return fromEnv;
104
+ const root = env.SystemRoot ?? env.windir ?? "C:\\Windows";
105
+ // (a) pure string — win32 flavour pinned, so an injected win32 platform on a
106
+ // POSIX host emits `…\System32\cmd.exe`, not the mixed `…/System32/cmd.exe`.
107
+ return path.win32.join(root, "System32", "cmd.exe");
108
+ }
109
+
110
+ function looksPathLike(name) {
111
+ return name.includes("\\") || name.includes("/") || /^[a-zA-Z]:/.test(name);
112
+ }
113
+
114
+ /**
115
+ * Resolve `name` to a concrete on-disk file via PATHEXT, realpath-verified.
116
+ * Returns undefined when nothing resolves.
117
+ *
118
+ * Every fs error is swallowed and the search continues — NOT only ENOENT.
119
+ * Verified 2026-07-31 on Windows 11: `realpathSync.native` on the Microsoft
120
+ * Store App-Execution-Alias `…\WindowsApps\python3.EXE` throws **EACCES**, not
121
+ * ENOENT. Treating that as fatal would abandon the scan and report a machine
122
+ * with a perfectly good `python` as having no Python at all.
123
+ */
124
+ function resolveViaPathExt(name, env) {
125
+ const exts = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
126
+ .split(";")
127
+ .map((s) => s.trim())
128
+ .filter(Boolean);
129
+
130
+ const firstFile = (base, withBare) => {
131
+ for (const ext of withBare ? ["", ...exts] : exts) {
132
+ try {
133
+ const real = realpathSync.native(base + ext);
134
+ if (statSync(real).isFile()) return real;
135
+ } catch {
136
+ // no usable file at this candidate (ENOENT, EACCES on a Store alias,
137
+ // ELOOP on a broken reparse point) — keep searching
138
+ }
139
+ }
140
+ return undefined;
141
+ };
142
+
143
+ if (looksPathLike(name)) {
144
+ // Explicit path — honour an exact match first, then PATHEXT.
145
+ // (b) HOST flavour on purpose — this candidate goes straight to realpath.
146
+ return firstFile(path.resolve(name), true);
147
+ }
148
+ // Bare command — PATH only, never cwd. The empty ext is excluded so an
149
+ // extensionless POSIX shim is not matched. `;` is the win32 PATH delimiter,
150
+ // hardcoded and therefore already host-independent.
151
+ for (const dir of (env.PATH ?? env.Path ?? "").split(";")) {
152
+ const trimmed = dir.trim();
153
+ if (!trimmed) continue;
154
+ // (b) HOST flavour on purpose — see the classification in the header.
155
+ const hit = firstFile(path.join(trimmed, name), false);
156
+ if (hit) return hit;
157
+ }
158
+ return undefined;
159
+ }
160
+
161
+ /**
162
+ * Characters cmd.exe would otherwise INTERPRET, and which a surrounding pair of
163
+ * double quotes makes literal. A token carrying one of these must therefore be
164
+ * quoted even when it has no whitespace — otherwise a PATH directory such as
165
+ * `C:\tools&more\` turns `…\tools&more\claude.cmd` into two commands.
166
+ *
167
+ * DELIBERATELY NOT IN THIS SET, because quoting does not fix them:
168
+ * `%` — `%VAR%` expands inside double quotes too. No caller can supply one
169
+ * (`SAFE_ARG` in claude-cli.mjs rejects `%`, preflight's args are fixed
170
+ * literals), and the server original refuses `%` in its upstream fence.
171
+ * `"` — a token containing a quote cannot be wrapped safely by this scheme at
172
+ * all; the callers' charset gates are what keep one from arriving.
173
+ * Both limits are stated rather than papered over.
174
+ */
175
+ const WIN32_CMD_SPECIAL = /[&|<>^]/;
176
+
177
+ function win32NeedsQuote(token) {
178
+ return token === "" || /\s/.test(token) || WIN32_CMD_SPECIAL.test(token);
179
+ }
180
+
181
+ export function win32CmdWrap(target, rest, env = process.env) {
182
+ const parts = [target, ...rest];
183
+ const command = win32ComSpec(env);
184
+ // No token has a space → discrete argv: cmd.exe /d /s /c runs each token
185
+ // literally and the caller keeps shell:false.
186
+ if (!parts.some(win32NeedsQuote)) {
187
+ return { command, args: ["/d", "/s", "/c", ...parts] };
188
+ }
189
+ // A token has a space (e.g. `C:\Program Files\nodejs\npm.cmd`). Under `cmd /s`
190
+ // Node's own arg-quoting is stripped, so build the canonical
191
+ // `cmd /d /s /c ""<quoted-shim>" <args>"` line and pass it verbatim: `/s`
192
+ // strips ONLY the outer quote pair, leaving the inner quotes intact.
193
+ const inner = parts.map((p) => (win32NeedsQuote(p) ? `"${p}"` : p)).join(" ");
194
+ return {
195
+ command,
196
+ args: ["/d", "/s", "/c", `"${inner}"`],
197
+ windowsVerbatimArguments: true,
198
+ };
199
+ }
200
+
201
+ /**
202
+ * Compute the `{command, args, windowsVerbatimArguments?}` to hand to
203
+ * child_process with `shell: false`. POSIX is a pass-through.
204
+ *
205
+ * - argv0 is a real `.exe`/`.com` → spawn it directly (no cmd.exe).
206
+ * - argv0 is a `.cmd`/`.bat` shim → run through `cmd.exe /d /s /c`.
207
+ * - argv0 is a bare name → resolve via PATH+PATHEXT, then as above.
208
+ * - nothing resolves for a bare name → the BARE name itself, for CreateProcess
209
+ * to resolve PATH-only (divergence 5). This function does NOT return `null`
210
+ * on any path; the `@returns` below said `| null` until 2026-08-01 and was
211
+ * stale. Callers keep their `if (!plan)` guard as contract insurance.
212
+ *
213
+ * @param {string[]} argv command followed by its arguments
214
+ * @param {{ platform?: string, env?: Record<string, string | undefined> }} [opts]
215
+ * injected for tests; defaults read the live process
216
+ * @returns {{ command: string, args: string[], windowsVerbatimArguments?: boolean }}
217
+ */
218
+ export function resolveSpawn(argv, opts = {}) {
219
+ const platform = opts.platform ?? process.platform;
220
+ const env = opts.env ?? process.env;
221
+ if (platform !== "win32") {
222
+ return { command: argv[0], args: argv.slice(1) };
223
+ }
224
+ const name = argv[0];
225
+ const rest = argv.slice(1);
226
+ // (a) pure string — win32 segment rules, so a POSIX host with an injected
227
+ // win32 platform classifies `C:\a.b\tool` the way Windows does.
228
+ const ext = path.win32.extname(name).toLowerCase();
229
+
230
+ if (WIN32_EXECUTABLE_EXTS.has(ext)) {
231
+ return { command: name, args: rest };
232
+ }
233
+ if (!WIN32_SHIM_EXTS.has(ext)) {
234
+ const resolved = resolveViaPathExt(name, env);
235
+ if (resolved) {
236
+ // (a) `resolved` is a host realpath; `path.win32` reads `/` as a
237
+ // separator too, so this is correct on either host.
238
+ if (WIN32_EXECUTABLE_EXTS.has(path.win32.extname(resolved).toLowerCase())) {
239
+ return { command: resolved, args: rest };
240
+ }
241
+ return win32CmdWrap(resolved, rest, env);
242
+ }
243
+ // Unresolved BARE name. Never DELEGATE it to `cmd /d /s /c <bare>` — that is
244
+ // cmd.exe's own cwd-first lookup and a planted `.\claude.cmd` would win it.
245
+ // Hand it to CreateProcess directly instead: verified PATH-only (a planted
246
+ // `.\x.exe` is ENOENT even with cwd set to its directory), and the only way
247
+ // to reach an App-Execution-Alias, which realpath cannot follow — that is
248
+ // how a real Store-installed Python stays detectable. A truly absent tool
249
+ // comes back ENOENT and the callers turn that into their "not found" verdict.
250
+ if (!looksPathLike(name)) return { command: name, args: rest };
251
+ }
252
+ return win32CmdWrap(name, rest, env);
253
+ }
package/package.json CHANGED
@@ -1,68 +1,68 @@
1
- {
2
- "name": "@svenroth-ai/shipwright",
3
- "version": "0.25.0",
4
- "description": "One command to install AND update Shipwright — the /shipwright-* SDLC plugins and the Command Center. Run `npx @svenroth-ai/shipwright@latest`.",
5
- "type": "module",
6
- "bin": {
7
- "shipwright": "bin/shipwright.mjs"
8
- },
9
- "engines": {
10
- "node": ">=20.12.0"
11
- },
12
- "files": [
13
- "bin/",
14
- "lib/",
15
- "scripts/deploy-swap.mjs",
16
- "scripts/deploy-procs.mjs",
17
- "scripts/repair-claude-json.mjs",
18
- "scripts/kill-targets.js",
19
- "server/dist/",
20
- "server/profiles/",
21
- "server/package.json",
22
- "client/dist/",
23
- "LICENSE",
24
- "README.md"
25
- ],
26
- "scripts": {
27
- "build": "node scripts/build-package.mjs",
28
- "prepack": "node scripts/build-package.mjs",
29
- "test": "vitest run",
30
- "lint": "oxlint .",
31
- "typecheck": "tsc --noEmit"
32
- },
33
- "keywords": [
34
- "shipwright",
35
- "claude-code",
36
- "sdlc",
37
- "installer",
38
- "bootstrapper"
39
- ],
40
- "author": "svenroth.ai",
41
- "license": "MIT",
42
- "repository": {
43
- "type": "git",
44
- "url": "git+https://github.com/svenroth-ai/shipwright-webui.git",
45
- "directory": "bootstrapper"
46
- },
47
- "dependencies": {
48
- "@hono/node-server": "^1.14.0",
49
- "@hono/node-ws": "^1.3.1",
50
- "@lydell/node-pty": "^1.1.0",
51
- "@xterm/addon-serialize": "0.14.0",
52
- "@xterm/headless": "6.0.0",
53
- "cron-parser": "^5.5.0",
54
- "hono": "^4.12.34",
55
- "ignore": "^5.3.2",
56
- "p-queue": "^9.2.0",
57
- "proper-lockfile": "^4.1.0",
58
- "shell-quote": "^1.8.4"
59
- },
60
- "devDependencies": {
61
- "oxlint": "^1.66.0",
62
- "typescript": "^5.7.0",
63
- "vitest": "^4.1.8"
64
- },
65
- "overrides": {
66
- "ws": "^8.21.0"
67
- }
68
- }
1
+ {
2
+ "name": "@svenroth-ai/shipwright",
3
+ "version": "0.27.0",
4
+ "description": "One command to install AND update Shipwright — the /shipwright-* SDLC plugins and the Command Center. Run `npx @svenroth-ai/shipwright@latest`.",
5
+ "type": "module",
6
+ "bin": {
7
+ "shipwright": "bin/shipwright.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=20.12.0"
11
+ },
12
+ "files": [
13
+ "bin/",
14
+ "lib/",
15
+ "scripts/deploy-swap.mjs",
16
+ "scripts/deploy-procs.mjs",
17
+ "scripts/repair-claude-json.mjs",
18
+ "scripts/kill-targets.js",
19
+ "server/dist/",
20
+ "server/profiles/",
21
+ "server/package.json",
22
+ "client/dist/",
23
+ "LICENSE",
24
+ "README.md"
25
+ ],
26
+ "scripts": {
27
+ "build": "node scripts/build-package.mjs",
28
+ "prepack": "node scripts/build-package.mjs",
29
+ "test": "vitest run",
30
+ "lint": "oxlint .",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "keywords": [
34
+ "shipwright",
35
+ "claude-code",
36
+ "sdlc",
37
+ "installer",
38
+ "bootstrapper"
39
+ ],
40
+ "author": "svenroth.ai",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/svenroth-ai/shipwright-webui.git",
45
+ "directory": "bootstrapper"
46
+ },
47
+ "dependencies": {
48
+ "@hono/node-server": "^1.14.0",
49
+ "@hono/node-ws": "^1.3.1",
50
+ "@lydell/node-pty": "^1.1.0",
51
+ "@xterm/addon-serialize": "0.14.0",
52
+ "@xterm/headless": "6.0.0",
53
+ "cron-parser": "^5.5.0",
54
+ "hono": "^4.12.34",
55
+ "ignore": "^5.3.2",
56
+ "p-queue": "^9.2.0",
57
+ "proper-lockfile": "^4.1.0",
58
+ "shell-quote": "^1.8.4"
59
+ },
60
+ "devDependencies": {
61
+ "oxlint": "^1.66.0",
62
+ "typescript": "^5.7.0",
63
+ "vitest": "^4.1.8"
64
+ },
65
+ "overrides": {
66
+ "ws": "^8.21.0"
67
+ }
68
+ }