@svenroth-ai/shipwright 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/client/dist/assets/{EmbeddedTerminal-5QvKe3_a.js → EmbeddedTerminal-CqCQUDtp.js} +1 -1
  2. package/client/dist/assets/{abnfDiagram-VRR7QNED-CXi9HegQ.js → abnfDiagram-VRR7QNED-DX2objTP.js} +1 -1
  3. package/client/dist/assets/{arc-DLh4GiqR.js → arc-DQmjbsFU.js} +1 -1
  4. package/client/dist/assets/{architectureDiagram-ZJ3FMSHR-DUypQ8dQ.js → architectureDiagram-ZJ3FMSHR-DAvCsPCW.js} +1 -1
  5. package/client/dist/assets/{blockDiagram-677ZJIJ3-D2cVMKJG.js → blockDiagram-677ZJIJ3-DC-dlyYI.js} +1 -1
  6. package/client/dist/assets/{c4Diagram-LMCZKHZV-DtruBGJw.js → c4Diagram-LMCZKHZV-DSlmXfhE.js} +1 -1
  7. package/client/dist/assets/channel-BfmIcrp7.js +1 -0
  8. package/client/dist/assets/{chunk-2Q5K7J3B-CAfQkFw2.js → chunk-2Q5K7J3B-C111EHL6.js} +1 -1
  9. package/client/dist/assets/{chunk-32BRIVSS-CPWXoLnQ.js → chunk-32BRIVSS-DrCYs2XG.js} +1 -1
  10. package/client/dist/assets/{chunk-5VM5RSS4-xVc3oppQ.js → chunk-5VM5RSS4-DIejoaXS.js} +1 -1
  11. package/client/dist/assets/{chunk-EX3LRPZG-CUpl6fRR.js → chunk-EX3LRPZG-CGWNeH2g.js} +1 -1
  12. package/client/dist/assets/{chunk-JWPE2WC7-NejUp56x.js → chunk-JWPE2WC7-CCH0SRFY.js} +1 -1
  13. package/client/dist/assets/{chunk-MOJQB5TN-C9-2lLg0.js → chunk-MOJQB5TN-B3OpZGRv.js} +1 -1
  14. package/client/dist/assets/{chunk-RYQCIY6F-BAPtwABy.js → chunk-RYQCIY6F-BKhdp_8v.js} +1 -1
  15. package/client/dist/assets/{chunk-V7JOEXUC-Dwzihw26.js → chunk-V7JOEXUC-B6Bu8x1y.js} +1 -1
  16. package/client/dist/assets/{chunk-VR4S4FIN-9-iArFJM.js → chunk-VR4S4FIN-OgFi2_iB.js} +1 -1
  17. package/client/dist/assets/{chunk-XXDRQBXY-DiWHOrqe.js → chunk-XXDRQBXY-xyjuuhxl.js} +1 -1
  18. package/client/dist/assets/classDiagram-OUVF2IWQ-HfUQZ6Uc.js +1 -0
  19. package/client/dist/assets/classDiagram-v2-EOCWNBFH-HfUQZ6Uc.js +1 -0
  20. package/client/dist/assets/{cose-bilkent-JH36ORCC-Cp04EOcH.js → cose-bilkent-JH36ORCC-BQIH3SCK.js} +1 -1
  21. package/client/dist/assets/{cynefin-VYW2F7L2-D--GN1v8.js → cynefin-VYW2F7L2-oyLAz8te.js} +1 -1
  22. package/client/dist/assets/{cynefinDiagram-TSTJHNR4-BGH0o_qp.js → cynefinDiagram-TSTJHNR4-DoLCsH0l.js} +1 -1
  23. package/client/dist/assets/{dagre-VKFMJZFB-DSKkIKlj.js → dagre-VKFMJZFB-D7djx1_Y.js} +1 -1
  24. package/client/dist/assets/{diagram-FQU43EPY-Cs_C4lb-.js → diagram-FQU43EPY-DFuYE0Vf.js} +1 -1
  25. package/client/dist/assets/{diagram-G47NLZAW-oDtAA8O_.js → diagram-G47NLZAW-CffCZVXk.js} +1 -1
  26. package/client/dist/assets/{diagram-NH7WQ7WH-BiFMjqSH.js → diagram-NH7WQ7WH-BhrHtUlt.js} +1 -1
  27. package/client/dist/assets/{diagram-OA4YK3LP-Ccs3thCj.js → diagram-OA4YK3LP-CwQPbOn0.js} +1 -1
  28. package/client/dist/assets/{diagram-WEI45ONY-CdZNqgO3.js → diagram-WEI45ONY-abtYnYqH.js} +1 -1
  29. package/client/dist/assets/{ebnfDiagram-CCIWWBDH-CapTA0VL.js → ebnfDiagram-CCIWWBDH-DfV7gT4m.js} +1 -1
  30. package/client/dist/assets/{erDiagram-Q63AITRT-BcRNEP8s.js → erDiagram-Q63AITRT-vtbCprgH.js} +1 -1
  31. package/client/dist/assets/{flowDiagram-23GEKE2U-CJ_zF7e_.js → flowDiagram-23GEKE2U-CVCgcyDF.js} +1 -1
  32. package/client/dist/assets/{ganttDiagram-NO4QXBWP-CnJdcg9A.js → ganttDiagram-NO4QXBWP-BclyOgMb.js} +1 -1
  33. package/client/dist/assets/{gitGraphDiagram-IHSO6WYX-YTUQ_ELk.js → gitGraphDiagram-IHSO6WYX-BXT4rIKj.js} +1 -1
  34. package/client/dist/assets/index-CKU9dY18.css +1 -0
  35. package/client/dist/assets/index-DOMl6atu.js +847 -0
  36. package/client/dist/assets/{infoDiagram-FWYZ7A6U-CLOP_ZWD.js → infoDiagram-FWYZ7A6U-Dr4mN24J.js} +1 -1
  37. package/client/dist/assets/{ishikawaDiagram-FXEZZL3T-aFTIzr7o.js → ishikawaDiagram-FXEZZL3T-BBUFWq5C.js} +1 -1
  38. package/client/dist/assets/{journeyDiagram-5HDEW3XC-CbPJB4xd.js → journeyDiagram-5HDEW3XC-CmclipcQ.js} +1 -1
  39. package/client/dist/assets/{kanban-definition-HUTT4EX6-BoyhytIS.js → kanban-definition-HUTT4EX6-eIzUeZ03.js} +1 -1
  40. package/client/dist/assets/{linear-1VXA-uBg.js → linear-DGiT7uKK.js} +1 -1
  41. package/client/dist/assets/{mermaid.core-D_lO2yK9.js → mermaid.core-BcLNGbxW.js} +4 -4
  42. package/client/dist/assets/{mindmap-definition-LN4V7U3C-iFK7LXMx.js → mindmap-definition-LN4V7U3C-C4ANnNiP.js} +1 -1
  43. package/client/dist/assets/{pegDiagram-2B236MQR-DNmcmfA-.js → pegDiagram-2B236MQR-DnMeh3Yh.js} +1 -1
  44. package/client/dist/assets/{pieDiagram-ENE6RG2P-ekx83O5b.js → pieDiagram-ENE6RG2P-CKV_rkhF.js} +1 -1
  45. package/client/dist/assets/{quadrantDiagram-ABIIQ3AL-DayyD1l8.js → quadrantDiagram-ABIIQ3AL-BuUsGwHh.js} +1 -1
  46. package/client/dist/assets/{railroadDiagram-RFXS5EU6-Bdgz7S6G.js → railroadDiagram-RFXS5EU6-BwwdKnFL.js} +1 -1
  47. package/client/dist/assets/{requirementDiagram-TGXJPOKE-szP3Muld.js → requirementDiagram-TGXJPOKE-CpQZouJV.js} +1 -1
  48. package/client/dist/assets/{sankeyDiagram-HTMAVEWB-Ct3rzgUj.js → sankeyDiagram-HTMAVEWB-4fnDUNVa.js} +1 -1
  49. package/client/dist/assets/{sequenceDiagram-DBY2YBRQ-_69EfmOU.js → sequenceDiagram-DBY2YBRQ-BOcaWlM0.js} +1 -1
  50. package/client/dist/assets/{sizeCapture-X5ZJPWSS-BSOU06J2.js → sizeCapture-X5ZJPWSS-_pv-9R0c.js} +1 -1
  51. package/client/dist/assets/{stateDiagram-2N3HPSRC-HmOmFwQm.js → stateDiagram-2N3HPSRC-CFwCOonY.js} +1 -1
  52. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-srkiu3Ib.js +1 -0
  53. package/client/dist/assets/{swimlanes-5IMT3BWC-CVg_4oT7.js → swimlanes-5IMT3BWC-DFa2tzxu.js} +2 -2
  54. package/client/dist/assets/swimlanesDiagram-G3AALYLV-Dq-0K9Zm.js +8 -0
  55. package/client/dist/assets/{timeline-definition-FHXFAJF6-CBBaDKXm.js → timeline-definition-FHXFAJF6-rhGYElgn.js} +1 -1
  56. package/client/dist/assets/{vennDiagram-L72KCM5P-CBXiWPMc.js → vennDiagram-L72KCM5P-BoGjRxyA.js} +1 -1
  57. package/client/dist/assets/{wardleyDiagram-EHGQE667-uhBCZoW0.js → wardleyDiagram-EHGQE667-D-IGv5bH.js} +1 -1
  58. package/client/dist/assets/{xychartDiagram-FW5EYKEG-xxE_wY5O.js → xychartDiagram-FW5EYKEG-D9PFTCfm.js} +1 -1
  59. package/client/dist/index.html +27 -27
  60. package/lib/claude-cli.mjs +173 -173
  61. package/lib/preflight.mjs +252 -252
  62. package/lib/probe-path.mjs +84 -84
  63. package/lib/probes.mjs +10 -4
  64. package/lib/server.mjs +506 -369
  65. package/lib/util.mjs +157 -157
  66. package/lib/win32-spawn.mjs +253 -253
  67. package/package.json +68 -68
  68. package/scripts/deploy-procs.mjs +208 -208
  69. package/scripts/deploy-swap.mjs +276 -276
  70. package/scripts/kill-targets.js +140 -140
  71. package/scripts/repair-claude-json.mjs +295 -295
  72. package/server/dist/config/default-actions.json +90 -90
  73. package/server/dist/core/mission-context/association.d.ts +46 -0
  74. package/server/dist/core/mission-context/association.d.ts.map +1 -1
  75. package/server/dist/core/mission-context/association.js +69 -0
  76. package/server/dist/core/mission-context/association.js.map +1 -1
  77. package/server/dist/core/mission-context/resolver-iterate.d.ts +6 -0
  78. package/server/dist/core/mission-context/resolver-iterate.d.ts.map +1 -0
  79. package/server/dist/core/mission-context/resolver-iterate.js +202 -0
  80. package/server/dist/core/mission-context/resolver-iterate.js.map +1 -0
  81. package/server/dist/core/mission-context/resolver-parts.d.ts +20 -0
  82. package/server/dist/core/mission-context/resolver-parts.d.ts.map +1 -1
  83. package/server/dist/core/mission-context/resolver-parts.js +92 -0
  84. package/server/dist/core/mission-context/resolver-parts.js.map +1 -1
  85. package/server/dist/core/mission-context/resolver.d.ts +1 -1
  86. package/server/dist/core/mission-context/resolver.d.ts.map +1 -1
  87. package/server/dist/core/mission-context/resolver.js +17 -199
  88. package/server/dist/core/mission-context/resolver.js.map +1 -1
  89. package/server/dist/core/mission-context/run-id-recovery.d.ts +21 -5
  90. package/server/dist/core/mission-context/run-id-recovery.d.ts.map +1 -1
  91. package/server/dist/core/mission-context/run-id-recovery.js +32 -5
  92. package/server/dist/core/mission-context/run-id-recovery.js.map +1 -1
  93. package/server/dist/core/mission-context/scenario.d.ts +18 -11
  94. package/server/dist/core/mission-context/scenario.d.ts.map +1 -1
  95. package/server/dist/core/mission-context/scenario.js +40 -3
  96. package/server/dist/core/mission-context/scenario.js.map +1 -1
  97. package/server/dist/core/path-guard.d.ts +1 -1
  98. package/server/dist/core/path-guard.d.ts.map +1 -1
  99. package/server/dist/core/path-guard.js +9 -2
  100. package/server/dist/core/path-guard.js.map +1 -1
  101. package/server/dist/core/sdk-sessions-merge.d.ts +19 -0
  102. package/server/dist/core/sdk-sessions-merge.d.ts.map +1 -1
  103. package/server/dist/core/sdk-sessions-merge.js +19 -5
  104. package/server/dist/core/sdk-sessions-merge.js.map +1 -1
  105. package/server/dist/core/sdk-sessions-refresh.d.ts +50 -0
  106. package/server/dist/core/sdk-sessions-refresh.d.ts.map +1 -0
  107. package/server/dist/core/sdk-sessions-refresh.js +69 -0
  108. package/server/dist/core/sdk-sessions-refresh.js.map +1 -0
  109. package/server/dist/core/sdk-sessions-store.d.ts +2 -0
  110. package/server/dist/core/sdk-sessions-store.d.ts.map +1 -1
  111. package/server/dist/core/sdk-sessions-store.js +7 -9
  112. package/server/dist/core/sdk-sessions-store.js.map +1 -1
  113. package/server/dist/core/shipslog-docs-reader.d.ts +4 -0
  114. package/server/dist/core/shipslog-docs-reader.d.ts.map +1 -0
  115. package/server/dist/core/shipslog-docs-reader.js +181 -0
  116. package/server/dist/core/shipslog-docs-reader.js.map +1 -0
  117. package/server/dist/core/shipslog-docs-types.d.ts +15 -0
  118. package/server/dist/core/shipslog-docs-types.d.ts.map +1 -0
  119. package/server/dist/core/shipslog-docs-types.js +14 -0
  120. package/server/dist/core/shipslog-docs-types.js.map +1 -0
  121. package/server/dist/core/shipwright-plugin-version-reader.d.ts +15 -0
  122. package/server/dist/core/shipwright-plugin-version-reader.d.ts.map +1 -0
  123. package/server/dist/core/shipwright-plugin-version-reader.js +49 -0
  124. package/server/dist/core/shipwright-plugin-version-reader.js.map +1 -0
  125. package/server/dist/external/design-review/serve.js +27 -27
  126. package/server/dist/external/inbox/_derive.d.ts +2 -25
  127. package/server/dist/external/inbox/_derive.d.ts.map +1 -1
  128. package/server/dist/external/inbox/_derive.js.map +1 -1
  129. package/server/dist/external/inbox/_lead.d.ts +23 -0
  130. package/server/dist/external/inbox/_lead.d.ts.map +1 -0
  131. package/server/dist/external/inbox/_lead.js +83 -0
  132. package/server/dist/external/inbox/_lead.js.map +1 -0
  133. package/server/dist/external/inbox/_types.d.ts +32 -0
  134. package/server/dist/external/inbox/_types.d.ts.map +1 -0
  135. package/server/dist/external/inbox/_types.js +10 -0
  136. package/server/dist/external/inbox/_types.js.map +1 -0
  137. package/server/dist/external/inbox/routes.d.ts.map +1 -1
  138. package/server/dist/external/inbox/routes.js +29 -1
  139. package/server/dist/external/inbox/routes.js.map +1 -1
  140. package/server/dist/external/launch/claim-holder-gate.d.ts +42 -0
  141. package/server/dist/external/launch/claim-holder-gate.d.ts.map +1 -0
  142. package/server/dist/external/launch/claim-holder-gate.js +87 -0
  143. package/server/dist/external/launch/claim-holder-gate.js.map +1 -0
  144. package/server/dist/external/launch/routes.d.ts.map +1 -1
  145. package/server/dist/external/launch/routes.js +23 -19
  146. package/server/dist/external/launch/routes.js.map +1 -1
  147. package/server/dist/external/mission-context/association-write.d.ts +22 -0
  148. package/server/dist/external/mission-context/association-write.d.ts.map +1 -0
  149. package/server/dist/external/mission-context/association-write.js +90 -0
  150. package/server/dist/external/mission-context/association-write.js.map +1 -0
  151. package/server/dist/external/mission-context/routes.d.ts.map +1 -1
  152. package/server/dist/external/mission-context/routes.js +18 -31
  153. package/server/dist/external/mission-context/routes.js.map +1 -1
  154. package/server/dist/external/org/lead-question-threads.d.ts +46 -0
  155. package/server/dist/external/org/lead-question-threads.d.ts.map +1 -0
  156. package/server/dist/external/org/lead-question-threads.js +132 -0
  157. package/server/dist/external/org/lead-question-threads.js.map +1 -0
  158. package/server/dist/external/org/mount.d.ts +3 -0
  159. package/server/dist/external/org/mount.d.ts.map +1 -1
  160. package/server/dist/external/org/mount.js +2 -1
  161. package/server/dist/external/org/mount.js.map +1 -1
  162. package/server/dist/external/org/usage.d.ts.map +1 -1
  163. package/server/dist/external/org/usage.js +20 -2
  164. package/server/dist/external/org/usage.js.map +1 -1
  165. package/server/dist/external/routes.d.ts.map +1 -1
  166. package/server/dist/external/routes.js +10 -13
  167. package/server/dist/external/routes.js.map +1 -1
  168. package/server/dist/external/shipslog-docs/routes.d.ts +10 -0
  169. package/server/dist/external/shipslog-docs/routes.d.ts.map +1 -0
  170. package/server/dist/external/shipslog-docs/routes.js +41 -0
  171. package/server/dist/external/shipslog-docs/routes.js.map +1 -0
  172. package/server/dist/routes/diagnostics.d.ts +12 -0
  173. package/server/dist/routes/diagnostics.d.ts.map +1 -1
  174. package/server/dist/routes/diagnostics.js +4 -1
  175. package/server/dist/routes/diagnostics.js.map +1 -1
  176. package/server/dist/routes/org-threads-composite.d.ts +24 -0
  177. package/server/dist/routes/org-threads-composite.d.ts.map +1 -0
  178. package/server/dist/routes/org-threads-composite.js +51 -0
  179. package/server/dist/routes/org-threads-composite.js.map +1 -0
  180. package/server/dist/routes/org.d.ts +5 -0
  181. package/server/dist/routes/org.d.ts.map +1 -1
  182. package/server/dist/routes/org.js +13 -0
  183. package/server/dist/routes/org.js.map +1 -1
  184. package/server/dist/terminal/pty-manager.d.ts +23 -1
  185. package/server/dist/terminal/pty-manager.d.ts.map +1 -1
  186. package/server/dist/terminal/pty-manager.js +22 -3
  187. package/server/dist/terminal/pty-manager.js.map +1 -1
  188. package/server/dist/terminal/pty-write-chunker.d.ts +109 -0
  189. package/server/dist/terminal/pty-write-chunker.d.ts.map +1 -0
  190. package/server/dist/terminal/pty-write-chunker.js +234 -0
  191. package/server/dist/terminal/pty-write-chunker.js.map +1 -0
  192. package/server/dist/types/org.d.ts +19 -0
  193. package/server/dist/types/org.d.ts.map +1 -1
  194. package/server/package.json +1 -1
  195. package/server/profiles/PROFILE_SCHEMA_VERSION +1 -1
  196. package/server/profiles/README.md +27 -27
  197. package/server/profiles/python-plugin-monorepo.json +35 -35
  198. package/server/profiles/supabase-nextjs.json +253 -253
  199. package/server/profiles/vite-hono.json +92 -92
  200. package/client/dist/assets/channel-C_k-9jsa.js +0 -1
  201. package/client/dist/assets/classDiagram-OUVF2IWQ-CsEPq9Ac.js +0 -1
  202. package/client/dist/assets/classDiagram-v2-EOCWNBFH-CsEPq9Ac.js +0 -1
  203. package/client/dist/assets/index-6nZ1bt0a.css +0 -1
  204. package/client/dist/assets/index-irQhGNpn.js +0 -828
  205. package/client/dist/assets/stateDiagram-v2-6OUMAXLB-BW8H33dv.js +0 -1
  206. package/client/dist/assets/swimlanesDiagram-G3AALYLV-BaRlxcPD.js +0 -8
@@ -1,276 +1,276 @@
1
- #!/usr/bin/env node
2
- /**
3
- * deploy-swap.mjs — the detached half of the production deploy.
4
- *
5
- * The deploy scripts used to kill the old server and start the new one inline.
6
- * Run from the Command Center's embedded terminal, the caller is a DESCENDANT of
7
- * the Hono server it kills (Hono -> node-pty shell -> claude -> the script), so
8
- * the kill tore down the ConPTY and took the caller with it: the "start the new
9
- * server" step never ran. Fresh build on disk, NO server, no diagnostic — the
10
- * process that would have printed one was the one that died (outage 2026-07-14).
11
- *
12
- * The caller spawns THIS helper detached *before* any kill happens, so it outlives
13
- * that cascade and finishes what the caller cannot. Empirically verified: a
14
- * `Start-Process` (Windows) / `nohup` (POSIX) child survives the kill.
15
- *
16
- * Contract — the caller owns install + build (a failed build must leave the running
17
- * server untouched: it never gets here); this helper owns kill -> start ->
18
- * readiness -> post-restart heal -> durable status. Full story + evidence:
19
- * .shipwright/planning/iterate/2026-07-14-deploy-self-kill.md
20
- *
21
- * Usage (from the deploy scripts, detached): node scripts/deploy-swap.mjs --port 3847
22
- */
23
-
24
- import fs from 'node:fs';
25
- import os from 'node:os';
26
- import path from 'node:path';
27
- import { spawn, spawnSync } from 'node:child_process';
28
- import { fileURLToPath } from 'node:url';
29
- import { stopOldServer, waitForServerUp } from './deploy-procs.mjs';
30
-
31
- const DEFAULT_PORT = 3847;
32
- const READY_TIMEOUT_MS = 12_000;
33
- const PORT_FREE_TIMEOUT_MS = 5_000;
34
- const SWAP_LOG_CAP_BYTES = 256 * 1024;
35
-
36
- const here = path.dirname(fileURLToPath(import.meta.url));
37
- const repo = path.resolve(here, '..');
38
- const serverDir = path.join(repo, 'server');
39
- const logDir = path.join(os.homedir(), '.shipwright-webui');
40
- const SERVER_LOG = path.join(logDir, 'server-manual.log');
41
- const SWAP_LOG = path.join(logDir, 'deploy-swap.log');
42
- const STATUS_FILE = path.join(logDir, 'deploy-status.json');
43
-
44
- // --- Pure helpers (unit-tested in deploy-swap.test.mjs) --------------------
45
-
46
- /**
47
- * Resolve the Hono port. Mirrors the callers' guards (the .ps1's `^\d{1,5}$`,
48
- * which also keeps the cast under Int32.MaxValue, and the .sh's `${PORT:-3847}`)
49
- * so caller and swapper can never disagree on the target.
50
- *
51
- * @param {Record<string, unknown>} [env]
52
- * @returns {number}
53
- */
54
- const isValidPort = (raw) => /^\d{1,5}$/.test(raw) && Number(raw) > 0;
55
-
56
- export function resolvePort(env = {}) {
57
- const raw = env?.PORT === undefined || env?.PORT === null ? '' : String(env.PORT);
58
- return isValidPort(raw) ? Number(raw) : DEFAULT_PORT;
59
- }
60
-
61
- /**
62
- * `--port` (passed by the caller, which already resolved it for its own readiness
63
- * poll and operator messages) wins over the environment. One rule, one resolved
64
- * value in every sink; a malformed flag degrades instead of throwing.
65
- *
66
- * @param {string[]} [argv]
67
- * @param {Record<string, unknown>} [env]
68
- * @returns {{ port: number }}
69
- */
70
- export function parseArgs(argv = [], env = {}) {
71
- const i = argv.indexOf('--port');
72
- const raw = i !== -1 && argv[i + 1] !== undefined ? String(argv[i + 1]) : '';
73
- return { port: isValidPort(raw) ? Number(raw) : resolvePort(env) };
74
- }
75
-
76
- /**
77
- * The durable record of the deploy outcome. Inside an embedded terminal the caller
78
- * is killed mid-deploy, so it cannot be the one to report success or failure —
79
- * that is precisely how the 2026-07-14 outage stayed invisible for hours.
80
- *
81
- * @param {{ok: unknown, port: unknown, pid?: unknown, ts: unknown, error?: unknown, readiness?: string}} o
82
- */
83
- export function buildStatus({ ok, port, pid = null, ts, error = null, readiness = 'listener' }) {
84
- return {
85
- ok: Boolean(ok),
86
- port: Number(port),
87
- pid: pid === null || pid === undefined ? null : Number(pid),
88
- ts: Number(ts),
89
- error: error ?? null,
90
- // How `ok` was established: 'listener' (the new child owns the port — the
91
- // strong claim) or 'process-alive' (no lsof on this host; the child was
92
- // merely still running). The caller reports the verdict, so it must be able
93
- // to tell the two apart.
94
- readiness,
95
- };
96
- }
97
-
98
- // --- Side-effecting steps --------------------------------------------------
99
-
100
- function log(msg) {
101
- const line = `[deploy-swap] ${new Date().toISOString()} ${msg}`;
102
- console.log(line);
103
- try {
104
- fs.appendFileSync(SWAP_LOG, line + '\n');
105
- } catch {
106
- // The log is a convenience; never fail the deploy over it.
107
- }
108
- }
109
-
110
- /** Last lines of the server log — the only thing that explains an EADDRINUSE or a crash on boot. */
111
- function serverLogTail(lines = 12) {
112
- try {
113
- return fs
114
- .readFileSync(SERVER_LOG, 'utf-8')
115
- .split(/\r?\n/)
116
- .filter(Boolean)
117
- .slice(-lines)
118
- .join(' | ');
119
- } catch {
120
- return '(no server log)';
121
- }
122
- }
123
-
124
- /**
125
- * Open the server log — and NEVER let it block the deploy.
126
- *
127
- * `w` (truncate) matches the old `> $log`. But the file can be held by another
128
- * instance: a second webui on a different PORT shares this path, and a server
129
- * launched the old way (a `cmd` redirect) holds it with restrictive sharing, so
130
- * openSync throws EBUSY. Losing the log is a nuisance; losing the SERVER because
131
- * we could not open a log file would be the very outage this helper exists to
132
- * prevent. Degrade: truncate -> append -> no log at all.
133
- */
134
- function openServerLog() {
135
- for (const flags of ['w', 'a']) {
136
- try {
137
- return fs.openSync(SERVER_LOG, flags);
138
- } catch { /* held by another instance — try the next mode */ }
139
- }
140
- log(`WARNING: ${SERVER_LOG} is not writable — starting the server without a log`);
141
- return 'ignore';
142
- }
143
-
144
- /** Start the freshly built server, detached, so it outlives THIS helper too. */
145
- function startServer(port) {
146
- const out = openServerLog();
147
- const child = spawn(
148
- process.execPath,
149
- ['--env-file-if-exists=../.env.local', 'dist/index.js'],
150
- {
151
- cwd: serverDir,
152
- env: { ...process.env, PORT: String(port) },
153
- detached: true,
154
- stdio: ['ignore', out, out],
155
- shell: false,
156
- },
157
- );
158
- child.unref();
159
- return child;
160
- }
161
-
162
- /**
163
- * Post-restart ~/.claude.json heal. It used to sit in the CALLER — i.e. in code
164
- * already dead by the time it matters, because the server-kill it waits on is the
165
- * same kill that takes the caller down. Best effort: never gated on.
166
- */
167
- function heal(label) {
168
- try {
169
- spawnSync(process.execPath, [path.join(here, 'repair-claude-json.mjs')], {
170
- stdio: 'ignore',
171
- });
172
- log(`~/.claude.json integrity check done (${label})`);
173
- } catch {
174
- log(`~/.claude.json integrity check skipped (${label})`);
175
- }
176
- }
177
-
178
- function writeStatus(status) {
179
- try {
180
- fs.writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2) + '\n');
181
- } catch (e) {
182
- log(`could not write ${STATUS_FILE}: ${String(e)}`);
183
- }
184
- }
185
-
186
- /**
187
- * Keep the swapper's history, but bounded. server-manual.log is truncated on every
188
- * deploy; this one is appended to (it must survive the restart that truncates the
189
- * other), so without a cap it grows forever.
190
- */
191
- function capSwapLog() {
192
- try {
193
- if (fs.statSync(SWAP_LOG).size <= SWAP_LOG_CAP_BYTES) return;
194
- const kept = fs.readFileSync(SWAP_LOG, 'utf-8').slice(-SWAP_LOG_CAP_BYTES / 2);
195
- fs.writeFileSync(SWAP_LOG, kept);
196
- } catch { /* no log yet, or unreadable — nothing to cap */ }
197
- }
198
-
199
- async function main() {
200
- fs.mkdirSync(logDir, { recursive: true });
201
- capSwapLog();
202
- // A stale status file from a PREVIOUS deploy must never be mistaken for this
203
- // run's verdict by the (possibly still alive) caller.
204
- fs.rmSync(STATUS_FILE, { force: true });
205
-
206
- const { port } = parseArgs(process.argv.slice(2), process.env);
207
- const finish = (ok, extra) => {
208
- // The verdict is written BEFORE the (best-effort, possibly slow) heal, so a
209
- // caller waiting on it is never blocked by a step that cannot change it.
210
- writeStatus(buildStatus({ ok, port, ts: Date.now(), ...extra }));
211
- log(ok ? `OK — ${extra.pid ? `server up on ${port} (pid ${extra.pid})` : 'done'}` : `FAILED — ${extra.error}`);
212
- return ok;
213
- };
214
-
215
- log(`swap starting (port ${port}, pid ${process.pid})`);
216
-
217
- // 1. Stop the old server. From here on the caller may die at any moment — this
218
- // kill tears down the ConPTY that hosts it.
219
- const { freed, killed, survivor } = await stopOldServer(port, {
220
- timeoutMs: PORT_FREE_TIMEOUT_MS,
221
- });
222
- log(killed.length ? `stopped PID(s): ${killed.join(' ')}` : `nothing was listening on ${port}`);
223
-
224
- if (!freed) {
225
- // The port never came free. Starting now would only hit EADDRINUSE and leave
226
- // the machine with NOTHING. The old server is still up — worse than a fresh
227
- // one, far better than none. Say so loudly and stop here.
228
- heal('kill-failed');
229
- return finish(false, {
230
- readiness: 'listener',
231
- error:
232
- `port ${port} is still held by PID ${survivor} after ${PORT_FREE_TIMEOUT_MS} ms — ` +
233
- `the OLD server is still running and was NOT replaced (nothing new was started)`,
234
- });
235
- }
236
-
237
- // 2. Start the fresh build.
238
- const child = startServer(port);
239
- log(`started node dist/index.js (pid ${child.pid})`);
240
-
241
- // 3. Confirm OUR child took the port (or, without lsof, that it is alive).
242
- const { ok, readiness, exited, exitCode } = await waitForServerUp(port, child, {
243
- timeoutMs: READY_TIMEOUT_MS,
244
- });
245
-
246
- // 4. Heal ~/.claude.json in the clean window: the old embedded sessions are
247
- // dead and a UI reload has not spawned new ones yet.
248
- heal('post-restart');
249
-
250
- return finish(ok, {
251
- pid: ok ? child.pid : null,
252
- readiness,
253
- error: ok
254
- ? null
255
- : exited
256
- ? `the new server exited on startup (code ${exitCode}) — ${serverLogTail()}`
257
- : `the new server did not take port ${port} within ${READY_TIMEOUT_MS} ms — ${serverLogTail()}`,
258
- });
259
- }
260
-
261
- // Only run when invoked as a script — importing it (tests) must stay side-effect free.
262
- if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
263
- main()
264
- .then((ok) => process.exit(ok ? 0 : 1))
265
- .catch((e) => {
266
- // A throw AFTER the kill (spawn EACCES, an unwritable log) would otherwise
267
- // leave no server AND no status file — the failure mode that made the
268
- // original outage invisible. The record survives even the unexpected.
269
- try {
270
- const { port } = parseArgs(process.argv.slice(2), process.env);
271
- writeStatus(buildStatus({ ok: false, port, ts: Date.now(), error: `swap crashed: ${String(e)}` }));
272
- } catch { /* nothing left to do */ }
273
- log(`CRASHED — ${String(e)}`);
274
- process.exit(1);
275
- });
276
- }
1
+ #!/usr/bin/env node
2
+ /**
3
+ * deploy-swap.mjs — the detached half of the production deploy.
4
+ *
5
+ * The deploy scripts used to kill the old server and start the new one inline.
6
+ * Run from the Command Center's embedded terminal, the caller is a DESCENDANT of
7
+ * the Hono server it kills (Hono -> node-pty shell -> claude -> the script), so
8
+ * the kill tore down the ConPTY and took the caller with it: the "start the new
9
+ * server" step never ran. Fresh build on disk, NO server, no diagnostic — the
10
+ * process that would have printed one was the one that died (outage 2026-07-14).
11
+ *
12
+ * The caller spawns THIS helper detached *before* any kill happens, so it outlives
13
+ * that cascade and finishes what the caller cannot. Empirically verified: a
14
+ * `Start-Process` (Windows) / `nohup` (POSIX) child survives the kill.
15
+ *
16
+ * Contract — the caller owns install + build (a failed build must leave the running
17
+ * server untouched: it never gets here); this helper owns kill -> start ->
18
+ * readiness -> post-restart heal -> durable status. Full story + evidence:
19
+ * .shipwright/planning/iterate/2026-07-14-deploy-self-kill.md
20
+ *
21
+ * Usage (from the deploy scripts, detached): node scripts/deploy-swap.mjs --port 3847
22
+ */
23
+
24
+ import fs from 'node:fs';
25
+ import os from 'node:os';
26
+ import path from 'node:path';
27
+ import { spawn, spawnSync } from 'node:child_process';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { stopOldServer, waitForServerUp } from './deploy-procs.mjs';
30
+
31
+ const DEFAULT_PORT = 3847;
32
+ const READY_TIMEOUT_MS = 12_000;
33
+ const PORT_FREE_TIMEOUT_MS = 5_000;
34
+ const SWAP_LOG_CAP_BYTES = 256 * 1024;
35
+
36
+ const here = path.dirname(fileURLToPath(import.meta.url));
37
+ const repo = path.resolve(here, '..');
38
+ const serverDir = path.join(repo, 'server');
39
+ const logDir = path.join(os.homedir(), '.shipwright-webui');
40
+ const SERVER_LOG = path.join(logDir, 'server-manual.log');
41
+ const SWAP_LOG = path.join(logDir, 'deploy-swap.log');
42
+ const STATUS_FILE = path.join(logDir, 'deploy-status.json');
43
+
44
+ // --- Pure helpers (unit-tested in deploy-swap.test.mjs) --------------------
45
+
46
+ /**
47
+ * Resolve the Hono port. Mirrors the callers' guards (the .ps1's `^\d{1,5}$`,
48
+ * which also keeps the cast under Int32.MaxValue, and the .sh's `${PORT:-3847}`)
49
+ * so caller and swapper can never disagree on the target.
50
+ *
51
+ * @param {Record<string, unknown>} [env]
52
+ * @returns {number}
53
+ */
54
+ const isValidPort = (raw) => /^\d{1,5}$/.test(raw) && Number(raw) > 0;
55
+
56
+ export function resolvePort(env = {}) {
57
+ const raw = env?.PORT === undefined || env?.PORT === null ? '' : String(env.PORT);
58
+ return isValidPort(raw) ? Number(raw) : DEFAULT_PORT;
59
+ }
60
+
61
+ /**
62
+ * `--port` (passed by the caller, which already resolved it for its own readiness
63
+ * poll and operator messages) wins over the environment. One rule, one resolved
64
+ * value in every sink; a malformed flag degrades instead of throwing.
65
+ *
66
+ * @param {string[]} [argv]
67
+ * @param {Record<string, unknown>} [env]
68
+ * @returns {{ port: number }}
69
+ */
70
+ export function parseArgs(argv = [], env = {}) {
71
+ const i = argv.indexOf('--port');
72
+ const raw = i !== -1 && argv[i + 1] !== undefined ? String(argv[i + 1]) : '';
73
+ return { port: isValidPort(raw) ? Number(raw) : resolvePort(env) };
74
+ }
75
+
76
+ /**
77
+ * The durable record of the deploy outcome. Inside an embedded terminal the caller
78
+ * is killed mid-deploy, so it cannot be the one to report success or failure —
79
+ * that is precisely how the 2026-07-14 outage stayed invisible for hours.
80
+ *
81
+ * @param {{ok: unknown, port: unknown, pid?: unknown, ts: unknown, error?: unknown, readiness?: string}} o
82
+ */
83
+ export function buildStatus({ ok, port, pid = null, ts, error = null, readiness = 'listener' }) {
84
+ return {
85
+ ok: Boolean(ok),
86
+ port: Number(port),
87
+ pid: pid === null || pid === undefined ? null : Number(pid),
88
+ ts: Number(ts),
89
+ error: error ?? null,
90
+ // How `ok` was established: 'listener' (the new child owns the port — the
91
+ // strong claim) or 'process-alive' (no lsof on this host; the child was
92
+ // merely still running). The caller reports the verdict, so it must be able
93
+ // to tell the two apart.
94
+ readiness,
95
+ };
96
+ }
97
+
98
+ // --- Side-effecting steps --------------------------------------------------
99
+
100
+ function log(msg) {
101
+ const line = `[deploy-swap] ${new Date().toISOString()} ${msg}`;
102
+ console.log(line);
103
+ try {
104
+ fs.appendFileSync(SWAP_LOG, line + '\n');
105
+ } catch {
106
+ // The log is a convenience; never fail the deploy over it.
107
+ }
108
+ }
109
+
110
+ /** Last lines of the server log — the only thing that explains an EADDRINUSE or a crash on boot. */
111
+ function serverLogTail(lines = 12) {
112
+ try {
113
+ return fs
114
+ .readFileSync(SERVER_LOG, 'utf-8')
115
+ .split(/\r?\n/)
116
+ .filter(Boolean)
117
+ .slice(-lines)
118
+ .join(' | ');
119
+ } catch {
120
+ return '(no server log)';
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Open the server log — and NEVER let it block the deploy.
126
+ *
127
+ * `w` (truncate) matches the old `> $log`. But the file can be held by another
128
+ * instance: a second webui on a different PORT shares this path, and a server
129
+ * launched the old way (a `cmd` redirect) holds it with restrictive sharing, so
130
+ * openSync throws EBUSY. Losing the log is a nuisance; losing the SERVER because
131
+ * we could not open a log file would be the very outage this helper exists to
132
+ * prevent. Degrade: truncate -> append -> no log at all.
133
+ */
134
+ function openServerLog() {
135
+ for (const flags of ['w', 'a']) {
136
+ try {
137
+ return fs.openSync(SERVER_LOG, flags);
138
+ } catch { /* held by another instance — try the next mode */ }
139
+ }
140
+ log(`WARNING: ${SERVER_LOG} is not writable — starting the server without a log`);
141
+ return 'ignore';
142
+ }
143
+
144
+ /** Start the freshly built server, detached, so it outlives THIS helper too. */
145
+ function startServer(port) {
146
+ const out = openServerLog();
147
+ const child = spawn(
148
+ process.execPath,
149
+ ['--env-file-if-exists=../.env.local', 'dist/index.js'],
150
+ {
151
+ cwd: serverDir,
152
+ env: { ...process.env, PORT: String(port) },
153
+ detached: true,
154
+ stdio: ['ignore', out, out],
155
+ shell: false,
156
+ },
157
+ );
158
+ child.unref();
159
+ return child;
160
+ }
161
+
162
+ /**
163
+ * Post-restart ~/.claude.json heal. It used to sit in the CALLER — i.e. in code
164
+ * already dead by the time it matters, because the server-kill it waits on is the
165
+ * same kill that takes the caller down. Best effort: never gated on.
166
+ */
167
+ function heal(label) {
168
+ try {
169
+ spawnSync(process.execPath, [path.join(here, 'repair-claude-json.mjs')], {
170
+ stdio: 'ignore',
171
+ });
172
+ log(`~/.claude.json integrity check done (${label})`);
173
+ } catch {
174
+ log(`~/.claude.json integrity check skipped (${label})`);
175
+ }
176
+ }
177
+
178
+ function writeStatus(status) {
179
+ try {
180
+ fs.writeFileSync(STATUS_FILE, JSON.stringify(status, null, 2) + '\n');
181
+ } catch (e) {
182
+ log(`could not write ${STATUS_FILE}: ${String(e)}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Keep the swapper's history, but bounded. server-manual.log is truncated on every
188
+ * deploy; this one is appended to (it must survive the restart that truncates the
189
+ * other), so without a cap it grows forever.
190
+ */
191
+ function capSwapLog() {
192
+ try {
193
+ if (fs.statSync(SWAP_LOG).size <= SWAP_LOG_CAP_BYTES) return;
194
+ const kept = fs.readFileSync(SWAP_LOG, 'utf-8').slice(-SWAP_LOG_CAP_BYTES / 2);
195
+ fs.writeFileSync(SWAP_LOG, kept);
196
+ } catch { /* no log yet, or unreadable — nothing to cap */ }
197
+ }
198
+
199
+ async function main() {
200
+ fs.mkdirSync(logDir, { recursive: true });
201
+ capSwapLog();
202
+ // A stale status file from a PREVIOUS deploy must never be mistaken for this
203
+ // run's verdict by the (possibly still alive) caller.
204
+ fs.rmSync(STATUS_FILE, { force: true });
205
+
206
+ const { port } = parseArgs(process.argv.slice(2), process.env);
207
+ const finish = (ok, extra) => {
208
+ // The verdict is written BEFORE the (best-effort, possibly slow) heal, so a
209
+ // caller waiting on it is never blocked by a step that cannot change it.
210
+ writeStatus(buildStatus({ ok, port, ts: Date.now(), ...extra }));
211
+ log(ok ? `OK — ${extra.pid ? `server up on ${port} (pid ${extra.pid})` : 'done'}` : `FAILED — ${extra.error}`);
212
+ return ok;
213
+ };
214
+
215
+ log(`swap starting (port ${port}, pid ${process.pid})`);
216
+
217
+ // 1. Stop the old server. From here on the caller may die at any moment — this
218
+ // kill tears down the ConPTY that hosts it.
219
+ const { freed, killed, survivor } = await stopOldServer(port, {
220
+ timeoutMs: PORT_FREE_TIMEOUT_MS,
221
+ });
222
+ log(killed.length ? `stopped PID(s): ${killed.join(' ')}` : `nothing was listening on ${port}`);
223
+
224
+ if (!freed) {
225
+ // The port never came free. Starting now would only hit EADDRINUSE and leave
226
+ // the machine with NOTHING. The old server is still up — worse than a fresh
227
+ // one, far better than none. Say so loudly and stop here.
228
+ heal('kill-failed');
229
+ return finish(false, {
230
+ readiness: 'listener',
231
+ error:
232
+ `port ${port} is still held by PID ${survivor} after ${PORT_FREE_TIMEOUT_MS} ms — ` +
233
+ `the OLD server is still running and was NOT replaced (nothing new was started)`,
234
+ });
235
+ }
236
+
237
+ // 2. Start the fresh build.
238
+ const child = startServer(port);
239
+ log(`started node dist/index.js (pid ${child.pid})`);
240
+
241
+ // 3. Confirm OUR child took the port (or, without lsof, that it is alive).
242
+ const { ok, readiness, exited, exitCode } = await waitForServerUp(port, child, {
243
+ timeoutMs: READY_TIMEOUT_MS,
244
+ });
245
+
246
+ // 4. Heal ~/.claude.json in the clean window: the old embedded sessions are
247
+ // dead and a UI reload has not spawned new ones yet.
248
+ heal('post-restart');
249
+
250
+ return finish(ok, {
251
+ pid: ok ? child.pid : null,
252
+ readiness,
253
+ error: ok
254
+ ? null
255
+ : exited
256
+ ? `the new server exited on startup (code ${exitCode}) — ${serverLogTail()}`
257
+ : `the new server did not take port ${port} within ${READY_TIMEOUT_MS} ms — ${serverLogTail()}`,
258
+ });
259
+ }
260
+
261
+ // Only run when invoked as a script — importing it (tests) must stay side-effect free.
262
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url))) {
263
+ main()
264
+ .then((ok) => process.exit(ok ? 0 : 1))
265
+ .catch((e) => {
266
+ // A throw AFTER the kill (spawn EACCES, an unwritable log) would otherwise
267
+ // leave no server AND no status file — the failure mode that made the
268
+ // original outage invisible. The record survives even the unexpected.
269
+ try {
270
+ const { port } = parseArgs(process.argv.slice(2), process.env);
271
+ writeStatus(buildStatus({ ok: false, port, ts: Date.now(), error: `swap crashed: ${String(e)}` }));
272
+ } catch { /* nothing left to do */ }
273
+ log(`CRASHED — ${String(e)}`);
274
+ process.exit(1);
275
+ });
276
+ }