@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.
- package/client/dist/assets/{EmbeddedTerminal-5QvKe3_a.js → EmbeddedTerminal-CqCQUDtp.js} +1 -1
- package/client/dist/assets/{abnfDiagram-VRR7QNED-CXi9HegQ.js → abnfDiagram-VRR7QNED-DX2objTP.js} +1 -1
- package/client/dist/assets/{arc-DLh4GiqR.js → arc-DQmjbsFU.js} +1 -1
- package/client/dist/assets/{architectureDiagram-ZJ3FMSHR-DUypQ8dQ.js → architectureDiagram-ZJ3FMSHR-DAvCsPCW.js} +1 -1
- package/client/dist/assets/{blockDiagram-677ZJIJ3-D2cVMKJG.js → blockDiagram-677ZJIJ3-DC-dlyYI.js} +1 -1
- package/client/dist/assets/{c4Diagram-LMCZKHZV-DtruBGJw.js → c4Diagram-LMCZKHZV-DSlmXfhE.js} +1 -1
- package/client/dist/assets/channel-BfmIcrp7.js +1 -0
- package/client/dist/assets/{chunk-2Q5K7J3B-CAfQkFw2.js → chunk-2Q5K7J3B-C111EHL6.js} +1 -1
- package/client/dist/assets/{chunk-32BRIVSS-CPWXoLnQ.js → chunk-32BRIVSS-DrCYs2XG.js} +1 -1
- package/client/dist/assets/{chunk-5VM5RSS4-xVc3oppQ.js → chunk-5VM5RSS4-DIejoaXS.js} +1 -1
- package/client/dist/assets/{chunk-EX3LRPZG-CUpl6fRR.js → chunk-EX3LRPZG-CGWNeH2g.js} +1 -1
- package/client/dist/assets/{chunk-JWPE2WC7-NejUp56x.js → chunk-JWPE2WC7-CCH0SRFY.js} +1 -1
- package/client/dist/assets/{chunk-MOJQB5TN-C9-2lLg0.js → chunk-MOJQB5TN-B3OpZGRv.js} +1 -1
- package/client/dist/assets/{chunk-RYQCIY6F-BAPtwABy.js → chunk-RYQCIY6F-BKhdp_8v.js} +1 -1
- package/client/dist/assets/{chunk-V7JOEXUC-Dwzihw26.js → chunk-V7JOEXUC-B6Bu8x1y.js} +1 -1
- package/client/dist/assets/{chunk-VR4S4FIN-9-iArFJM.js → chunk-VR4S4FIN-OgFi2_iB.js} +1 -1
- package/client/dist/assets/{chunk-XXDRQBXY-DiWHOrqe.js → chunk-XXDRQBXY-xyjuuhxl.js} +1 -1
- package/client/dist/assets/classDiagram-OUVF2IWQ-HfUQZ6Uc.js +1 -0
- package/client/dist/assets/classDiagram-v2-EOCWNBFH-HfUQZ6Uc.js +1 -0
- package/client/dist/assets/{cose-bilkent-JH36ORCC-Cp04EOcH.js → cose-bilkent-JH36ORCC-BQIH3SCK.js} +1 -1
- package/client/dist/assets/{cynefin-VYW2F7L2-D--GN1v8.js → cynefin-VYW2F7L2-oyLAz8te.js} +1 -1
- package/client/dist/assets/{cynefinDiagram-TSTJHNR4-BGH0o_qp.js → cynefinDiagram-TSTJHNR4-DoLCsH0l.js} +1 -1
- package/client/dist/assets/{dagre-VKFMJZFB-DSKkIKlj.js → dagre-VKFMJZFB-D7djx1_Y.js} +1 -1
- package/client/dist/assets/{diagram-FQU43EPY-Cs_C4lb-.js → diagram-FQU43EPY-DFuYE0Vf.js} +1 -1
- package/client/dist/assets/{diagram-G47NLZAW-oDtAA8O_.js → diagram-G47NLZAW-CffCZVXk.js} +1 -1
- package/client/dist/assets/{diagram-NH7WQ7WH-BiFMjqSH.js → diagram-NH7WQ7WH-BhrHtUlt.js} +1 -1
- package/client/dist/assets/{diagram-OA4YK3LP-Ccs3thCj.js → diagram-OA4YK3LP-CwQPbOn0.js} +1 -1
- package/client/dist/assets/{diagram-WEI45ONY-CdZNqgO3.js → diagram-WEI45ONY-abtYnYqH.js} +1 -1
- package/client/dist/assets/{ebnfDiagram-CCIWWBDH-CapTA0VL.js → ebnfDiagram-CCIWWBDH-DfV7gT4m.js} +1 -1
- package/client/dist/assets/{erDiagram-Q63AITRT-BcRNEP8s.js → erDiagram-Q63AITRT-vtbCprgH.js} +1 -1
- package/client/dist/assets/{flowDiagram-23GEKE2U-CJ_zF7e_.js → flowDiagram-23GEKE2U-CVCgcyDF.js} +1 -1
- package/client/dist/assets/{ganttDiagram-NO4QXBWP-CnJdcg9A.js → ganttDiagram-NO4QXBWP-BclyOgMb.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-IHSO6WYX-YTUQ_ELk.js → gitGraphDiagram-IHSO6WYX-BXT4rIKj.js} +1 -1
- package/client/dist/assets/index-CKU9dY18.css +1 -0
- package/client/dist/assets/index-DOMl6atu.js +847 -0
- package/client/dist/assets/{infoDiagram-FWYZ7A6U-CLOP_ZWD.js → infoDiagram-FWYZ7A6U-Dr4mN24J.js} +1 -1
- package/client/dist/assets/{ishikawaDiagram-FXEZZL3T-aFTIzr7o.js → ishikawaDiagram-FXEZZL3T-BBUFWq5C.js} +1 -1
- package/client/dist/assets/{journeyDiagram-5HDEW3XC-CbPJB4xd.js → journeyDiagram-5HDEW3XC-CmclipcQ.js} +1 -1
- package/client/dist/assets/{kanban-definition-HUTT4EX6-BoyhytIS.js → kanban-definition-HUTT4EX6-eIzUeZ03.js} +1 -1
- package/client/dist/assets/{linear-1VXA-uBg.js → linear-DGiT7uKK.js} +1 -1
- package/client/dist/assets/{mermaid.core-D_lO2yK9.js → mermaid.core-BcLNGbxW.js} +4 -4
- package/client/dist/assets/{mindmap-definition-LN4V7U3C-iFK7LXMx.js → mindmap-definition-LN4V7U3C-C4ANnNiP.js} +1 -1
- package/client/dist/assets/{pegDiagram-2B236MQR-DNmcmfA-.js → pegDiagram-2B236MQR-DnMeh3Yh.js} +1 -1
- package/client/dist/assets/{pieDiagram-ENE6RG2P-ekx83O5b.js → pieDiagram-ENE6RG2P-CKV_rkhF.js} +1 -1
- package/client/dist/assets/{quadrantDiagram-ABIIQ3AL-DayyD1l8.js → quadrantDiagram-ABIIQ3AL-BuUsGwHh.js} +1 -1
- package/client/dist/assets/{railroadDiagram-RFXS5EU6-Bdgz7S6G.js → railroadDiagram-RFXS5EU6-BwwdKnFL.js} +1 -1
- package/client/dist/assets/{requirementDiagram-TGXJPOKE-szP3Muld.js → requirementDiagram-TGXJPOKE-CpQZouJV.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-HTMAVEWB-Ct3rzgUj.js → sankeyDiagram-HTMAVEWB-4fnDUNVa.js} +1 -1
- package/client/dist/assets/{sequenceDiagram-DBY2YBRQ-_69EfmOU.js → sequenceDiagram-DBY2YBRQ-BOcaWlM0.js} +1 -1
- package/client/dist/assets/{sizeCapture-X5ZJPWSS-BSOU06J2.js → sizeCapture-X5ZJPWSS-_pv-9R0c.js} +1 -1
- package/client/dist/assets/{stateDiagram-2N3HPSRC-HmOmFwQm.js → stateDiagram-2N3HPSRC-CFwCOonY.js} +1 -1
- package/client/dist/assets/stateDiagram-v2-6OUMAXLB-srkiu3Ib.js +1 -0
- package/client/dist/assets/{swimlanes-5IMT3BWC-CVg_4oT7.js → swimlanes-5IMT3BWC-DFa2tzxu.js} +2 -2
- package/client/dist/assets/swimlanesDiagram-G3AALYLV-Dq-0K9Zm.js +8 -0
- package/client/dist/assets/{timeline-definition-FHXFAJF6-CBBaDKXm.js → timeline-definition-FHXFAJF6-rhGYElgn.js} +1 -1
- package/client/dist/assets/{vennDiagram-L72KCM5P-CBXiWPMc.js → vennDiagram-L72KCM5P-BoGjRxyA.js} +1 -1
- package/client/dist/assets/{wardleyDiagram-EHGQE667-uhBCZoW0.js → wardleyDiagram-EHGQE667-D-IGv5bH.js} +1 -1
- package/client/dist/assets/{xychartDiagram-FW5EYKEG-xxE_wY5O.js → xychartDiagram-FW5EYKEG-D9PFTCfm.js} +1 -1
- package/client/dist/index.html +27 -27
- package/lib/claude-cli.mjs +173 -173
- package/lib/preflight.mjs +252 -252
- package/lib/probe-path.mjs +84 -84
- package/lib/probes.mjs +10 -4
- package/lib/server.mjs +506 -369
- package/lib/util.mjs +157 -157
- package/lib/win32-spawn.mjs +253 -253
- package/package.json +68 -68
- package/scripts/deploy-procs.mjs +208 -208
- package/scripts/deploy-swap.mjs +276 -276
- package/scripts/kill-targets.js +140 -140
- package/scripts/repair-claude-json.mjs +295 -295
- package/server/dist/config/default-actions.json +90 -90
- package/server/dist/core/mission-context/association.d.ts +46 -0
- package/server/dist/core/mission-context/association.d.ts.map +1 -1
- package/server/dist/core/mission-context/association.js +69 -0
- package/server/dist/core/mission-context/association.js.map +1 -1
- package/server/dist/core/mission-context/resolver-iterate.d.ts +6 -0
- package/server/dist/core/mission-context/resolver-iterate.d.ts.map +1 -0
- package/server/dist/core/mission-context/resolver-iterate.js +202 -0
- package/server/dist/core/mission-context/resolver-iterate.js.map +1 -0
- package/server/dist/core/mission-context/resolver-parts.d.ts +20 -0
- package/server/dist/core/mission-context/resolver-parts.d.ts.map +1 -1
- package/server/dist/core/mission-context/resolver-parts.js +92 -0
- package/server/dist/core/mission-context/resolver-parts.js.map +1 -1
- package/server/dist/core/mission-context/resolver.d.ts +1 -1
- package/server/dist/core/mission-context/resolver.d.ts.map +1 -1
- package/server/dist/core/mission-context/resolver.js +17 -199
- package/server/dist/core/mission-context/resolver.js.map +1 -1
- package/server/dist/core/mission-context/run-id-recovery.d.ts +21 -5
- package/server/dist/core/mission-context/run-id-recovery.d.ts.map +1 -1
- package/server/dist/core/mission-context/run-id-recovery.js +32 -5
- package/server/dist/core/mission-context/run-id-recovery.js.map +1 -1
- package/server/dist/core/mission-context/scenario.d.ts +18 -11
- package/server/dist/core/mission-context/scenario.d.ts.map +1 -1
- package/server/dist/core/mission-context/scenario.js +40 -3
- package/server/dist/core/mission-context/scenario.js.map +1 -1
- package/server/dist/core/path-guard.d.ts +1 -1
- package/server/dist/core/path-guard.d.ts.map +1 -1
- package/server/dist/core/path-guard.js +9 -2
- package/server/dist/core/path-guard.js.map +1 -1
- package/server/dist/core/sdk-sessions-merge.d.ts +19 -0
- package/server/dist/core/sdk-sessions-merge.d.ts.map +1 -1
- package/server/dist/core/sdk-sessions-merge.js +19 -5
- package/server/dist/core/sdk-sessions-merge.js.map +1 -1
- package/server/dist/core/sdk-sessions-refresh.d.ts +50 -0
- package/server/dist/core/sdk-sessions-refresh.d.ts.map +1 -0
- package/server/dist/core/sdk-sessions-refresh.js +69 -0
- package/server/dist/core/sdk-sessions-refresh.js.map +1 -0
- package/server/dist/core/sdk-sessions-store.d.ts +2 -0
- package/server/dist/core/sdk-sessions-store.d.ts.map +1 -1
- package/server/dist/core/sdk-sessions-store.js +7 -9
- package/server/dist/core/sdk-sessions-store.js.map +1 -1
- package/server/dist/core/shipslog-docs-reader.d.ts +4 -0
- package/server/dist/core/shipslog-docs-reader.d.ts.map +1 -0
- package/server/dist/core/shipslog-docs-reader.js +181 -0
- package/server/dist/core/shipslog-docs-reader.js.map +1 -0
- package/server/dist/core/shipslog-docs-types.d.ts +15 -0
- package/server/dist/core/shipslog-docs-types.d.ts.map +1 -0
- package/server/dist/core/shipslog-docs-types.js +14 -0
- package/server/dist/core/shipslog-docs-types.js.map +1 -0
- package/server/dist/core/shipwright-plugin-version-reader.d.ts +15 -0
- package/server/dist/core/shipwright-plugin-version-reader.d.ts.map +1 -0
- package/server/dist/core/shipwright-plugin-version-reader.js +49 -0
- package/server/dist/core/shipwright-plugin-version-reader.js.map +1 -0
- package/server/dist/external/design-review/serve.js +27 -27
- package/server/dist/external/inbox/_derive.d.ts +2 -25
- package/server/dist/external/inbox/_derive.d.ts.map +1 -1
- package/server/dist/external/inbox/_derive.js.map +1 -1
- package/server/dist/external/inbox/_lead.d.ts +23 -0
- package/server/dist/external/inbox/_lead.d.ts.map +1 -0
- package/server/dist/external/inbox/_lead.js +83 -0
- package/server/dist/external/inbox/_lead.js.map +1 -0
- package/server/dist/external/inbox/_types.d.ts +32 -0
- package/server/dist/external/inbox/_types.d.ts.map +1 -0
- package/server/dist/external/inbox/_types.js +10 -0
- package/server/dist/external/inbox/_types.js.map +1 -0
- package/server/dist/external/inbox/routes.d.ts.map +1 -1
- package/server/dist/external/inbox/routes.js +29 -1
- package/server/dist/external/inbox/routes.js.map +1 -1
- package/server/dist/external/launch/claim-holder-gate.d.ts +42 -0
- package/server/dist/external/launch/claim-holder-gate.d.ts.map +1 -0
- package/server/dist/external/launch/claim-holder-gate.js +87 -0
- package/server/dist/external/launch/claim-holder-gate.js.map +1 -0
- package/server/dist/external/launch/routes.d.ts.map +1 -1
- package/server/dist/external/launch/routes.js +23 -19
- package/server/dist/external/launch/routes.js.map +1 -1
- package/server/dist/external/mission-context/association-write.d.ts +22 -0
- package/server/dist/external/mission-context/association-write.d.ts.map +1 -0
- package/server/dist/external/mission-context/association-write.js +90 -0
- package/server/dist/external/mission-context/association-write.js.map +1 -0
- package/server/dist/external/mission-context/routes.d.ts.map +1 -1
- package/server/dist/external/mission-context/routes.js +18 -31
- package/server/dist/external/mission-context/routes.js.map +1 -1
- package/server/dist/external/org/lead-question-threads.d.ts +46 -0
- package/server/dist/external/org/lead-question-threads.d.ts.map +1 -0
- package/server/dist/external/org/lead-question-threads.js +132 -0
- package/server/dist/external/org/lead-question-threads.js.map +1 -0
- package/server/dist/external/org/mount.d.ts +3 -0
- package/server/dist/external/org/mount.d.ts.map +1 -1
- package/server/dist/external/org/mount.js +2 -1
- package/server/dist/external/org/mount.js.map +1 -1
- package/server/dist/external/org/usage.d.ts.map +1 -1
- package/server/dist/external/org/usage.js +20 -2
- package/server/dist/external/org/usage.js.map +1 -1
- package/server/dist/external/routes.d.ts.map +1 -1
- package/server/dist/external/routes.js +10 -13
- package/server/dist/external/routes.js.map +1 -1
- package/server/dist/external/shipslog-docs/routes.d.ts +10 -0
- package/server/dist/external/shipslog-docs/routes.d.ts.map +1 -0
- package/server/dist/external/shipslog-docs/routes.js +41 -0
- package/server/dist/external/shipslog-docs/routes.js.map +1 -0
- package/server/dist/routes/diagnostics.d.ts +12 -0
- package/server/dist/routes/diagnostics.d.ts.map +1 -1
- package/server/dist/routes/diagnostics.js +4 -1
- package/server/dist/routes/diagnostics.js.map +1 -1
- package/server/dist/routes/org-threads-composite.d.ts +24 -0
- package/server/dist/routes/org-threads-composite.d.ts.map +1 -0
- package/server/dist/routes/org-threads-composite.js +51 -0
- package/server/dist/routes/org-threads-composite.js.map +1 -0
- package/server/dist/routes/org.d.ts +5 -0
- package/server/dist/routes/org.d.ts.map +1 -1
- package/server/dist/routes/org.js +13 -0
- package/server/dist/routes/org.js.map +1 -1
- package/server/dist/terminal/pty-manager.d.ts +23 -1
- package/server/dist/terminal/pty-manager.d.ts.map +1 -1
- package/server/dist/terminal/pty-manager.js +22 -3
- package/server/dist/terminal/pty-manager.js.map +1 -1
- package/server/dist/terminal/pty-write-chunker.d.ts +109 -0
- package/server/dist/terminal/pty-write-chunker.d.ts.map +1 -0
- package/server/dist/terminal/pty-write-chunker.js +234 -0
- package/server/dist/terminal/pty-write-chunker.js.map +1 -0
- package/server/dist/types/org.d.ts +19 -0
- package/server/dist/types/org.d.ts.map +1 -1
- package/server/package.json +1 -1
- package/server/profiles/PROFILE_SCHEMA_VERSION +1 -1
- package/server/profiles/README.md +27 -27
- package/server/profiles/python-plugin-monorepo.json +35 -35
- package/server/profiles/supabase-nextjs.json +253 -253
- package/server/profiles/vite-hono.json +92 -92
- package/client/dist/assets/channel-C_k-9jsa.js +0 -1
- package/client/dist/assets/classDiagram-OUVF2IWQ-CsEPq9Ac.js +0 -1
- package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsEPq9Ac.js +0 -1
- package/client/dist/assets/index-6nZ1bt0a.css +0 -1
- package/client/dist/assets/index-irQhGNpn.js +0 -828
- package/client/dist/assets/stateDiagram-v2-6OUMAXLB-BW8H33dv.js +0 -1
- package/client/dist/assets/swimlanesDiagram-G3AALYLV-BaRlxcPD.js +0 -8
package/lib/win32-spawn.mjs
CHANGED
|
@@ -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.
|
|
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
|
+
}
|