@svenroth-ai/shipwright 0.25.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/client/dist/assets/{EmbeddedTerminal-5QvKe3_a.js → EmbeddedTerminal-D3wpPNZP.js} +1 -1
  2. package/client/dist/assets/{abnfDiagram-VRR7QNED-CXi9HegQ.js → abnfDiagram-VRR7QNED-BkLIltB3.js} +1 -1
  3. package/client/dist/assets/{arc-DLh4GiqR.js → arc-Sw-Gh0nI.js} +1 -1
  4. package/client/dist/assets/{architectureDiagram-ZJ3FMSHR-DUypQ8dQ.js → architectureDiagram-ZJ3FMSHR-rdxUi9Ks.js} +1 -1
  5. package/client/dist/assets/{blockDiagram-677ZJIJ3-D2cVMKJG.js → blockDiagram-677ZJIJ3-Ch_9dj8a.js} +1 -1
  6. package/client/dist/assets/{c4Diagram-LMCZKHZV-DtruBGJw.js → c4Diagram-LMCZKHZV-CkFlO65x.js} +1 -1
  7. package/client/dist/assets/channel-BYPbLjRl.js +1 -0
  8. package/client/dist/assets/{chunk-2Q5K7J3B-CAfQkFw2.js → chunk-2Q5K7J3B-CypX83xu.js} +1 -1
  9. package/client/dist/assets/{chunk-32BRIVSS-CPWXoLnQ.js → chunk-32BRIVSS-DxVLYGdl.js} +1 -1
  10. package/client/dist/assets/{chunk-5VM5RSS4-xVc3oppQ.js → chunk-5VM5RSS4-Cxk1evhx.js} +1 -1
  11. package/client/dist/assets/{chunk-EX3LRPZG-CUpl6fRR.js → chunk-EX3LRPZG-n9q5S4yJ.js} +1 -1
  12. package/client/dist/assets/{chunk-JWPE2WC7-NejUp56x.js → chunk-JWPE2WC7-jeL4xvir.js} +1 -1
  13. package/client/dist/assets/{chunk-MOJQB5TN-C9-2lLg0.js → chunk-MOJQB5TN-DDSHN0cj.js} +1 -1
  14. package/client/dist/assets/{chunk-RYQCIY6F-BAPtwABy.js → chunk-RYQCIY6F-C0bo383g.js} +1 -1
  15. package/client/dist/assets/{chunk-V7JOEXUC-Dwzihw26.js → chunk-V7JOEXUC-C0jDXXAU.js} +1 -1
  16. package/client/dist/assets/{chunk-VR4S4FIN-9-iArFJM.js → chunk-VR4S4FIN-h-Rr4Pn2.js} +1 -1
  17. package/client/dist/assets/{chunk-XXDRQBXY-DiWHOrqe.js → chunk-XXDRQBXY-BexSo0-s.js} +1 -1
  18. package/client/dist/assets/classDiagram-OUVF2IWQ-CsFIeTju.js +1 -0
  19. package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsFIeTju.js +1 -0
  20. package/client/dist/assets/{cose-bilkent-JH36ORCC-Cp04EOcH.js → cose-bilkent-JH36ORCC-BQXzaioj.js} +1 -1
  21. package/client/dist/assets/{cynefin-VYW2F7L2-D--GN1v8.js → cynefin-VYW2F7L2-BJIofhfe.js} +1 -1
  22. package/client/dist/assets/{cynefinDiagram-TSTJHNR4-BGH0o_qp.js → cynefinDiagram-TSTJHNR4-DmPqTM1T.js} +1 -1
  23. package/client/dist/assets/{dagre-VKFMJZFB-DSKkIKlj.js → dagre-VKFMJZFB-1xljvw6X.js} +1 -1
  24. package/client/dist/assets/{diagram-FQU43EPY-Cs_C4lb-.js → diagram-FQU43EPY-iyL9muCG.js} +1 -1
  25. package/client/dist/assets/{diagram-G47NLZAW-oDtAA8O_.js → diagram-G47NLZAW-DREp2Th6.js} +1 -1
  26. package/client/dist/assets/{diagram-NH7WQ7WH-BiFMjqSH.js → diagram-NH7WQ7WH-h0X3Is2S.js} +1 -1
  27. package/client/dist/assets/{diagram-OA4YK3LP-Ccs3thCj.js → diagram-OA4YK3LP-Bj8YZRFJ.js} +1 -1
  28. package/client/dist/assets/{diagram-WEI45ONY-CdZNqgO3.js → diagram-WEI45ONY-BZOf805d.js} +1 -1
  29. package/client/dist/assets/{ebnfDiagram-CCIWWBDH-CapTA0VL.js → ebnfDiagram-CCIWWBDH-DO5JnXda.js} +1 -1
  30. package/client/dist/assets/{erDiagram-Q63AITRT-BcRNEP8s.js → erDiagram-Q63AITRT-DRgcqg1T.js} +1 -1
  31. package/client/dist/assets/{flowDiagram-23GEKE2U-CJ_zF7e_.js → flowDiagram-23GEKE2U-C3J9QmrW.js} +1 -1
  32. package/client/dist/assets/{ganttDiagram-NO4QXBWP-CnJdcg9A.js → ganttDiagram-NO4QXBWP-BIEl3Tkm.js} +1 -1
  33. package/client/dist/assets/{gitGraphDiagram-IHSO6WYX-YTUQ_ELk.js → gitGraphDiagram-IHSO6WYX-BNF0tFWC.js} +1 -1
  34. package/client/dist/assets/{index-irQhGNpn.js → index-BnTOrFHb.js} +203 -202
  35. package/client/dist/assets/index-CYPc6qxw.css +1 -0
  36. package/client/dist/assets/{infoDiagram-FWYZ7A6U-CLOP_ZWD.js → infoDiagram-FWYZ7A6U-DEyFxiyY.js} +1 -1
  37. package/client/dist/assets/{ishikawaDiagram-FXEZZL3T-aFTIzr7o.js → ishikawaDiagram-FXEZZL3T-B_ehlg3I.js} +1 -1
  38. package/client/dist/assets/{journeyDiagram-5HDEW3XC-CbPJB4xd.js → journeyDiagram-5HDEW3XC-Dp00FyJS.js} +1 -1
  39. package/client/dist/assets/{kanban-definition-HUTT4EX6-BoyhytIS.js → kanban-definition-HUTT4EX6-CTAf16q8.js} +1 -1
  40. package/client/dist/assets/{linear-1VXA-uBg.js → linear-9vNUgmYd.js} +1 -1
  41. package/client/dist/assets/{mermaid.core-D_lO2yK9.js → mermaid.core-A6tjaEVD.js} +4 -4
  42. package/client/dist/assets/{mindmap-definition-LN4V7U3C-iFK7LXMx.js → mindmap-definition-LN4V7U3C-DRW3bmCY.js} +1 -1
  43. package/client/dist/assets/{pegDiagram-2B236MQR-DNmcmfA-.js → pegDiagram-2B236MQR-DJFCjHp2.js} +1 -1
  44. package/client/dist/assets/{pieDiagram-ENE6RG2P-ekx83O5b.js → pieDiagram-ENE6RG2P-KRd-sGvC.js} +1 -1
  45. package/client/dist/assets/{quadrantDiagram-ABIIQ3AL-DayyD1l8.js → quadrantDiagram-ABIIQ3AL-9TLZE3Hu.js} +1 -1
  46. package/client/dist/assets/{railroadDiagram-RFXS5EU6-Bdgz7S6G.js → railroadDiagram-RFXS5EU6-y4tLtjXg.js} +1 -1
  47. package/client/dist/assets/{requirementDiagram-TGXJPOKE-szP3Muld.js → requirementDiagram-TGXJPOKE-B9EOQDQM.js} +1 -1
  48. package/client/dist/assets/{sankeyDiagram-HTMAVEWB-Ct3rzgUj.js → sankeyDiagram-HTMAVEWB-zfwiDzkT.js} +1 -1
  49. package/client/dist/assets/{sequenceDiagram-DBY2YBRQ-_69EfmOU.js → sequenceDiagram-DBY2YBRQ-DW1LWepL.js} +1 -1
  50. package/client/dist/assets/{sizeCapture-X5ZJPWSS-BSOU06J2.js → sizeCapture-X5ZJPWSS-BM81Mik0.js} +1 -1
  51. package/client/dist/assets/{stateDiagram-2N3HPSRC-HmOmFwQm.js → stateDiagram-2N3HPSRC-D0EMIw1w.js} +1 -1
  52. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-B3E4_xxT.js +1 -0
  53. package/client/dist/assets/{swimlanes-5IMT3BWC-CVg_4oT7.js → swimlanes-5IMT3BWC-CKXNCX6M.js} +2 -2
  54. package/client/dist/assets/swimlanesDiagram-G3AALYLV-BegQBqnC.js +8 -0
  55. package/client/dist/assets/{timeline-definition-FHXFAJF6-CBBaDKXm.js → timeline-definition-FHXFAJF6-ByzIfvL5.js} +1 -1
  56. package/client/dist/assets/{vennDiagram-L72KCM5P-CBXiWPMc.js → vennDiagram-L72KCM5P-Xep38xBH.js} +1 -1
  57. package/client/dist/assets/{wardleyDiagram-EHGQE667-uhBCZoW0.js → wardleyDiagram-EHGQE667-DKWDpx6g.js} +1 -1
  58. package/client/dist/assets/{xychartDiagram-FW5EYKEG-xxE_wY5O.js → xychartDiagram-FW5EYKEG-BGcwZTnu.js} +1 -1
  59. package/client/dist/index.html +27 -27
  60. package/lib/claude-cli.mjs +173 -173
  61. package/lib/preflight.mjs +252 -252
  62. package/lib/probe-path.mjs +84 -84
  63. package/lib/server.mjs +369 -369
  64. package/lib/util.mjs +157 -157
  65. package/lib/win32-spawn.mjs +253 -253
  66. package/package.json +68 -68
  67. package/scripts/deploy-procs.mjs +208 -208
  68. package/scripts/deploy-swap.mjs +276 -276
  69. package/scripts/kill-targets.js +140 -140
  70. package/scripts/repair-claude-json.mjs +295 -295
  71. package/server/dist/config/default-actions.json +90 -90
  72. package/server/dist/core/mission-context/association.d.ts +46 -0
  73. package/server/dist/core/mission-context/association.d.ts.map +1 -1
  74. package/server/dist/core/mission-context/association.js +69 -0
  75. package/server/dist/core/mission-context/association.js.map +1 -1
  76. package/server/dist/core/mission-context/resolver-iterate.d.ts +6 -0
  77. package/server/dist/core/mission-context/resolver-iterate.d.ts.map +1 -0
  78. package/server/dist/core/mission-context/resolver-iterate.js +202 -0
  79. package/server/dist/core/mission-context/resolver-iterate.js.map +1 -0
  80. package/server/dist/core/mission-context/resolver-parts.d.ts +20 -0
  81. package/server/dist/core/mission-context/resolver-parts.d.ts.map +1 -1
  82. package/server/dist/core/mission-context/resolver-parts.js +92 -0
  83. package/server/dist/core/mission-context/resolver-parts.js.map +1 -1
  84. package/server/dist/core/mission-context/resolver.d.ts +1 -1
  85. package/server/dist/core/mission-context/resolver.d.ts.map +1 -1
  86. package/server/dist/core/mission-context/resolver.js +17 -199
  87. package/server/dist/core/mission-context/resolver.js.map +1 -1
  88. package/server/dist/core/mission-context/run-id-recovery.d.ts +21 -5
  89. package/server/dist/core/mission-context/run-id-recovery.d.ts.map +1 -1
  90. package/server/dist/core/mission-context/run-id-recovery.js +32 -5
  91. package/server/dist/core/mission-context/run-id-recovery.js.map +1 -1
  92. package/server/dist/core/mission-context/scenario.d.ts +18 -11
  93. package/server/dist/core/mission-context/scenario.d.ts.map +1 -1
  94. package/server/dist/core/mission-context/scenario.js +40 -3
  95. package/server/dist/core/mission-context/scenario.js.map +1 -1
  96. package/server/dist/core/path-guard.d.ts +1 -1
  97. package/server/dist/core/path-guard.d.ts.map +1 -1
  98. package/server/dist/core/path-guard.js +9 -2
  99. package/server/dist/core/path-guard.js.map +1 -1
  100. package/server/dist/core/shipslog-docs-reader.d.ts +4 -0
  101. package/server/dist/core/shipslog-docs-reader.d.ts.map +1 -0
  102. package/server/dist/core/shipslog-docs-reader.js +181 -0
  103. package/server/dist/core/shipslog-docs-reader.js.map +1 -0
  104. package/server/dist/core/shipslog-docs-types.d.ts +15 -0
  105. package/server/dist/core/shipslog-docs-types.d.ts.map +1 -0
  106. package/server/dist/core/shipslog-docs-types.js +14 -0
  107. package/server/dist/core/shipslog-docs-types.js.map +1 -0
  108. package/server/dist/external/design-review/serve.js +27 -27
  109. package/server/dist/external/mission-context/association-write.d.ts +22 -0
  110. package/server/dist/external/mission-context/association-write.d.ts.map +1 -0
  111. package/server/dist/external/mission-context/association-write.js +90 -0
  112. package/server/dist/external/mission-context/association-write.js.map +1 -0
  113. package/server/dist/external/mission-context/routes.d.ts.map +1 -1
  114. package/server/dist/external/mission-context/routes.js +18 -31
  115. package/server/dist/external/mission-context/routes.js.map +1 -1
  116. package/server/dist/external/routes.d.ts.map +1 -1
  117. package/server/dist/external/routes.js +9 -6
  118. package/server/dist/external/routes.js.map +1 -1
  119. package/server/dist/external/shipslog-docs/routes.d.ts +10 -0
  120. package/server/dist/external/shipslog-docs/routes.d.ts.map +1 -0
  121. package/server/dist/external/shipslog-docs/routes.js +41 -0
  122. package/server/dist/external/shipslog-docs/routes.js.map +1 -0
  123. package/server/package.json +1 -1
  124. package/server/profiles/PROFILE_SCHEMA_VERSION +1 -1
  125. package/server/profiles/README.md +27 -27
  126. package/server/profiles/python-plugin-monorepo.json +35 -35
  127. package/server/profiles/supabase-nextjs.json +253 -253
  128. package/server/profiles/vite-hono.json +92 -92
  129. package/client/dist/assets/channel-C_k-9jsa.js +0 -1
  130. package/client/dist/assets/classDiagram-OUVF2IWQ-CsEPq9Ac.js +0 -1
  131. package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsEPq9Ac.js +0 -1
  132. package/client/dist/assets/index-6nZ1bt0a.css +0 -1
  133. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-BW8H33dv.js +0 -1
  134. package/client/dist/assets/swimlanesDiagram-G3AALYLV-BaRlxcPD.js +0 -8
@@ -1,173 +1,173 @@
1
- /**
2
- * claude-cli.mjs — the OS-touching default seams for plugins.mjs.
3
- *
4
- * Split out (like deploy-procs.mjs vs kill-targets.js) so plugins.mjs stays
5
- * pure and fully unit-testable. Everything here talks to the real `claude`
6
- * binary, the real `~/.claude/plugins/` tree, or the network — and every one
7
- * of them is injectable, so tests never touch any of it.
8
- *
9
- * ARCHITECTURE FENCE: this drives the `claude` PLUGIN CLI as an installer. It
10
- * NEVER starts a `claude` session and never writes `~/.claude/projects/**`,
11
- * `shipwright_run_config.json`, or `run_loop_state.json` (CLAUDE.md rule 1 /
12
- * DO-NOT #12). The only verbs used are `plugin marketplace …` and `plugin
13
- * install|update …`.
14
- */
15
-
16
- import { spawnSync } from "node:child_process";
17
- import { existsSync, readFileSync } from "node:fs";
18
- import os from "node:os";
19
- import path from "node:path";
20
-
21
- import { MANIFEST_RAW_URL, buildInstalledMap } from "./plugins.mjs";
22
- import { resolveSpawn } from "./win32-spawn.mjs";
23
-
24
- /**
25
- * Argument charset gate for the `claude` CLI. Exported so BOTH directions can be
26
- * asserted without starting a process (a gate that refuses everything would pass
27
- * a refusal-only test). See `defaultRunClaude` for why it is still load-bearing
28
- * after the shell:true removal.
29
- */
30
- export const SAFE_ARG = /^[A-Za-z0-9@._/:-]+$/;
31
-
32
- /** The one "we could not run claude at all" verdict, identical on every platform. */
33
- const notFound = () => ({
34
- ok: false,
35
- code: null,
36
- stdout: "",
37
- stderr: "claude not found on PATH",
38
- });
39
-
40
- /**
41
- * Run the real `claude` CLI, always with `shell: false`.
42
- *
43
- * On Windows `claude` is often a `.cmd` shim, which a bare `shell: false` spawn
44
- * cannot reach: `CreateProcess` only ever appends `.exe`, and Node's
45
- * CVE-2024-27980 hardening EINVAL-blocks a direct `.cmd`. That used to be
46
- * answered with `shell: true`; it is now answered by `resolveSpawn`, which finds
47
- * the real file via PATHEXT and invokes a shim through an explicit
48
- * `cmd.exe /d /s /c` with discrete argv (see lib/win32-spawn.mjs).
49
- *
50
- * SAFE_ARG STAYS, and is still load-bearing — do not remove it as newly
51
- * redundant. A `.cmd` target is still ultimately parsed by cmd.exe, and Node
52
- * quotes an argument for spaces, not for `&` / `|` / `^` / `%`. The charset gate
53
- * is what guarantees no token can carry one. What changed is that an `.exe`
54
- * target (the common case — `claude.exe` from the native installer) now involves
55
- * no cmd.exe at all.
56
- * @param {string[]} args
57
- */
58
- export function defaultRunClaude(args) {
59
- for (const a of args) {
60
- if (!SAFE_ARG.test(a)) {
61
- return { ok: false, code: null, stdout: "", stderr: `refused unsafe claude arg: ${a}` };
62
- }
63
- }
64
- const plan = resolveSpawn(["claude", ...args]);
65
- if (!plan) {
66
- // UNREACHABLE as of iterate-2026-07-31: `resolveSpawn` no longer returns
67
- // null — an unresolvable bare name comes back as the bare name for
68
- // CreateProcess to resolve PATH-only (win32-spawn divergence 5), and the
69
- // ENOENT that follows is turned into `notFound()` below. What actually
70
- // prevents cmd.exe's cwd-first lookup is that fallback, NOT this branch.
71
- // Kept as contract insurance: if the resolver ever regains a null path,
72
- // this is already the right answer. Verified 2026-08-01.
73
- return notFound();
74
- }
75
- const r = spawnSync(plan.command, plan.args, {
76
- encoding: "utf-8",
77
- shell: false,
78
- timeout: 120_000,
79
- windowsVerbatimArguments: plan.windowsVerbatimArguments,
80
- });
81
- // POSIX takes `resolveSpawn`'s pass-through branch, so an absent `claude`
82
- // arrives here as an ENOENT spawn error with EMPTY stderr instead of the
83
- // win32 resolver's null. Report the SAME verdict on both platforms — a caller
84
- // should not have to know which OS swallowed the message.
85
- if (r.error && r.error.code === "ENOENT") return notFound();
86
- return {
87
- ok: r.status === 0 && !r.error,
88
- code: r.status ?? null,
89
- stdout: (r.stdout ?? "").toString(),
90
- stderr: (r.stderr ?? "").toString(),
91
- };
92
- }
93
-
94
- /** `~/.claude/plugins` root (override via CLAUDE_PLUGINS_ROOT for tests). */
95
- export function pluginsRoot(env = process.env, home = os.homedir()) {
96
- return env.CLAUDE_PLUGINS_ROOT || path.join(home, ".claude", "plugins");
97
- }
98
-
99
- export function installedPluginsPath(env, home) {
100
- return path.join(pluginsRoot(env, home), "installed_plugins.json");
101
- }
102
-
103
- export function marketplaceDir(env, home) {
104
- return path.join(pluginsRoot(env, home), "marketplaces", "shipwright");
105
- }
106
-
107
- export function cacheRoot(env, home) {
108
- return path.join(pluginsRoot(env, home), "cache", "shipwright");
109
- }
110
-
111
- /** Read + parse installed_plugins.json into a `{ name: version }` map (empty on any error). */
112
- export function defaultSnapshotInstalled(env = process.env, home = os.homedir()) {
113
- try {
114
- const raw = readFileSync(installedPluginsPath(env, home), "utf-8");
115
- return buildInstalledMap(JSON.parse(raw));
116
- } catch {
117
- return {};
118
- }
119
- }
120
-
121
- /** Precedence 1 — the local marketplace clone materialised by `marketplace add`. */
122
- export function defaultReadLocalManifest(env = process.env, home = os.homedir()) {
123
- const p = path.join(marketplaceDir(env, home), ".claude-plugin", "marketplace.json");
124
- try {
125
- if (!existsSync(p)) return null;
126
- return { text: readFileSync(p, "utf-8"), source: p };
127
- } catch {
128
- return null;
129
- }
130
- }
131
-
132
- /** Precedence 2 — GitHub raw. Offline-safe: any failure resolves to null. */
133
- export function makeFetchRemoteManifest(deps = {}) {
134
- const {
135
- fetchImpl = typeof fetch === "function" ? fetch : undefined,
136
- url = MANIFEST_RAW_URL,
137
- timeoutMs = 4000,
138
- } = deps;
139
- return async () => {
140
- if (!fetchImpl) return null;
141
- const ctrl = new AbortController();
142
- const timer = setTimeout(() => ctrl.abort(), timeoutMs);
143
- try {
144
- const res = await fetchImpl(url, { signal: ctrl.signal });
145
- if (!res || !res.ok) return null;
146
- return { text: await res.text(), source: url };
147
- } catch {
148
- return null;
149
- } finally {
150
- clearTimeout(timer);
151
- }
152
- };
153
- }
154
-
155
- /** Precedence 3 — explicit path override (also the test seam). */
156
- export function defaultReadOverrideManifest(env = process.env) {
157
- const p = env.SHIPWRIGHT_MARKETPLACE_MANIFEST;
158
- if (!p) return null;
159
- // A set-but-unreadable override is an OPERATOR error, not a fall-through:
160
- // surface it loudly rather than silently sliding to "no list".
161
- return { text: readFileSync(p, "utf-8"), source: p };
162
- }
163
-
164
- /** Bundle the three resolver seams with their real implementations. */
165
- export function defaultResolverSeams(deps = {}) {
166
- const env = deps.env ?? process.env;
167
- const home = deps.home ?? os.homedir();
168
- return {
169
- readLocalManifest: () => defaultReadLocalManifest(env, home),
170
- fetchRemoteManifest: makeFetchRemoteManifest(deps),
171
- readOverrideManifest: () => defaultReadOverrideManifest(env),
172
- };
173
- }
1
+ /**
2
+ * claude-cli.mjs — the OS-touching default seams for plugins.mjs.
3
+ *
4
+ * Split out (like deploy-procs.mjs vs kill-targets.js) so plugins.mjs stays
5
+ * pure and fully unit-testable. Everything here talks to the real `claude`
6
+ * binary, the real `~/.claude/plugins/` tree, or the network — and every one
7
+ * of them is injectable, so tests never touch any of it.
8
+ *
9
+ * ARCHITECTURE FENCE: this drives the `claude` PLUGIN CLI as an installer. It
10
+ * NEVER starts a `claude` session and never writes `~/.claude/projects/**`,
11
+ * `shipwright_run_config.json`, or `run_loop_state.json` (CLAUDE.md rule 1 /
12
+ * DO-NOT #12). The only verbs used are `plugin marketplace …` and `plugin
13
+ * install|update …`.
14
+ */
15
+
16
+ import { spawnSync } from "node:child_process";
17
+ import { existsSync, readFileSync } from "node:fs";
18
+ import os from "node:os";
19
+ import path from "node:path";
20
+
21
+ import { MANIFEST_RAW_URL, buildInstalledMap } from "./plugins.mjs";
22
+ import { resolveSpawn } from "./win32-spawn.mjs";
23
+
24
+ /**
25
+ * Argument charset gate for the `claude` CLI. Exported so BOTH directions can be
26
+ * asserted without starting a process (a gate that refuses everything would pass
27
+ * a refusal-only test). See `defaultRunClaude` for why it is still load-bearing
28
+ * after the shell:true removal.
29
+ */
30
+ export const SAFE_ARG = /^[A-Za-z0-9@._/:-]+$/;
31
+
32
+ /** The one "we could not run claude at all" verdict, identical on every platform. */
33
+ const notFound = () => ({
34
+ ok: false,
35
+ code: null,
36
+ stdout: "",
37
+ stderr: "claude not found on PATH",
38
+ });
39
+
40
+ /**
41
+ * Run the real `claude` CLI, always with `shell: false`.
42
+ *
43
+ * On Windows `claude` is often a `.cmd` shim, which a bare `shell: false` spawn
44
+ * cannot reach: `CreateProcess` only ever appends `.exe`, and Node's
45
+ * CVE-2024-27980 hardening EINVAL-blocks a direct `.cmd`. That used to be
46
+ * answered with `shell: true`; it is now answered by `resolveSpawn`, which finds
47
+ * the real file via PATHEXT and invokes a shim through an explicit
48
+ * `cmd.exe /d /s /c` with discrete argv (see lib/win32-spawn.mjs).
49
+ *
50
+ * SAFE_ARG STAYS, and is still load-bearing — do not remove it as newly
51
+ * redundant. A `.cmd` target is still ultimately parsed by cmd.exe, and Node
52
+ * quotes an argument for spaces, not for `&` / `|` / `^` / `%`. The charset gate
53
+ * is what guarantees no token can carry one. What changed is that an `.exe`
54
+ * target (the common case — `claude.exe` from the native installer) now involves
55
+ * no cmd.exe at all.
56
+ * @param {string[]} args
57
+ */
58
+ export function defaultRunClaude(args) {
59
+ for (const a of args) {
60
+ if (!SAFE_ARG.test(a)) {
61
+ return { ok: false, code: null, stdout: "", stderr: `refused unsafe claude arg: ${a}` };
62
+ }
63
+ }
64
+ const plan = resolveSpawn(["claude", ...args]);
65
+ if (!plan) {
66
+ // UNREACHABLE as of iterate-2026-07-31: `resolveSpawn` no longer returns
67
+ // null — an unresolvable bare name comes back as the bare name for
68
+ // CreateProcess to resolve PATH-only (win32-spawn divergence 5), and the
69
+ // ENOENT that follows is turned into `notFound()` below. What actually
70
+ // prevents cmd.exe's cwd-first lookup is that fallback, NOT this branch.
71
+ // Kept as contract insurance: if the resolver ever regains a null path,
72
+ // this is already the right answer. Verified 2026-08-01.
73
+ return notFound();
74
+ }
75
+ const r = spawnSync(plan.command, plan.args, {
76
+ encoding: "utf-8",
77
+ shell: false,
78
+ timeout: 120_000,
79
+ windowsVerbatimArguments: plan.windowsVerbatimArguments,
80
+ });
81
+ // POSIX takes `resolveSpawn`'s pass-through branch, so an absent `claude`
82
+ // arrives here as an ENOENT spawn error with EMPTY stderr instead of the
83
+ // win32 resolver's null. Report the SAME verdict on both platforms — a caller
84
+ // should not have to know which OS swallowed the message.
85
+ if (r.error && r.error.code === "ENOENT") return notFound();
86
+ return {
87
+ ok: r.status === 0 && !r.error,
88
+ code: r.status ?? null,
89
+ stdout: (r.stdout ?? "").toString(),
90
+ stderr: (r.stderr ?? "").toString(),
91
+ };
92
+ }
93
+
94
+ /** `~/.claude/plugins` root (override via CLAUDE_PLUGINS_ROOT for tests). */
95
+ export function pluginsRoot(env = process.env, home = os.homedir()) {
96
+ return env.CLAUDE_PLUGINS_ROOT || path.join(home, ".claude", "plugins");
97
+ }
98
+
99
+ export function installedPluginsPath(env, home) {
100
+ return path.join(pluginsRoot(env, home), "installed_plugins.json");
101
+ }
102
+
103
+ export function marketplaceDir(env, home) {
104
+ return path.join(pluginsRoot(env, home), "marketplaces", "shipwright");
105
+ }
106
+
107
+ export function cacheRoot(env, home) {
108
+ return path.join(pluginsRoot(env, home), "cache", "shipwright");
109
+ }
110
+
111
+ /** Read + parse installed_plugins.json into a `{ name: version }` map (empty on any error). */
112
+ export function defaultSnapshotInstalled(env = process.env, home = os.homedir()) {
113
+ try {
114
+ const raw = readFileSync(installedPluginsPath(env, home), "utf-8");
115
+ return buildInstalledMap(JSON.parse(raw));
116
+ } catch {
117
+ return {};
118
+ }
119
+ }
120
+
121
+ /** Precedence 1 — the local marketplace clone materialised by `marketplace add`. */
122
+ export function defaultReadLocalManifest(env = process.env, home = os.homedir()) {
123
+ const p = path.join(marketplaceDir(env, home), ".claude-plugin", "marketplace.json");
124
+ try {
125
+ if (!existsSync(p)) return null;
126
+ return { text: readFileSync(p, "utf-8"), source: p };
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ /** Precedence 2 — GitHub raw. Offline-safe: any failure resolves to null. */
133
+ export function makeFetchRemoteManifest(deps = {}) {
134
+ const {
135
+ fetchImpl = typeof fetch === "function" ? fetch : undefined,
136
+ url = MANIFEST_RAW_URL,
137
+ timeoutMs = 4000,
138
+ } = deps;
139
+ return async () => {
140
+ if (!fetchImpl) return null;
141
+ const ctrl = new AbortController();
142
+ const timer = setTimeout(() => ctrl.abort(), timeoutMs);
143
+ try {
144
+ const res = await fetchImpl(url, { signal: ctrl.signal });
145
+ if (!res || !res.ok) return null;
146
+ return { text: await res.text(), source: url };
147
+ } catch {
148
+ return null;
149
+ } finally {
150
+ clearTimeout(timer);
151
+ }
152
+ };
153
+ }
154
+
155
+ /** Precedence 3 — explicit path override (also the test seam). */
156
+ export function defaultReadOverrideManifest(env = process.env) {
157
+ const p = env.SHIPWRIGHT_MARKETPLACE_MANIFEST;
158
+ if (!p) return null;
159
+ // A set-but-unreadable override is an OPERATOR error, not a fall-through:
160
+ // surface it loudly rather than silently sliding to "no list".
161
+ return { text: readFileSync(p, "utf-8"), source: p };
162
+ }
163
+
164
+ /** Bundle the three resolver seams with their real implementations. */
165
+ export function defaultResolverSeams(deps = {}) {
166
+ const env = deps.env ?? process.env;
167
+ const home = deps.home ?? os.homedir();
168
+ return {
169
+ readLocalManifest: () => defaultReadLocalManifest(env, home),
170
+ fetchRemoteManifest: makeFetchRemoteManifest(deps),
171
+ readOverrideManifest: () => defaultReadOverrideManifest(env),
172
+ };
173
+ }