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