@askexenow/exe-os 0.9.166 → 0.9.167

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 (244) hide show
  1. package/deploy/compose/backup.sh +45 -7
  2. package/deploy/compose/setup.sh +7 -0
  3. package/deploy/stack-manifests/v0.9.json +40 -1
  4. package/dist/{active-agent-R2KMWMR6.js → active-agent-DGTIJN2U.js} +2 -2
  5. package/dist/{active-agent-CYMM3QQA.js → active-agent-HVMLG6FH.js} +2 -2
  6. package/dist/{agentic-ontology-GKAKYNPE.js → agentic-ontology-S54AFODT.js} +1 -1
  7. package/dist/{backfill-metadata-Z5SYUWAV.js → backfill-metadata-74IWETRF.js} +4 -3
  8. package/dist/{behaviors-QGU6XI5R.js → behaviors-LZVAVHTC.js} +2 -2
  9. package/dist/bin/agentic-ontology-backfill.js +5 -4
  10. package/dist/bin/agentic-reflection-backfill.js +6 -5
  11. package/dist/bin/agentic-semantic-label.js +5 -4
  12. package/dist/bin/backfill-conversations.js +5 -4
  13. package/dist/bin/backfill-responses.js +5 -4
  14. package/dist/bin/backfill-vectors.js +6 -5
  15. package/dist/bin/bulk-sync-postgres.js +6 -5
  16. package/dist/bin/cleanup-stale-review-tasks.js +9 -9
  17. package/dist/bin/cli.js +16 -13
  18. package/dist/bin/daily-summary.js +0 -217
  19. package/dist/bin/deferred-daemon-restart.js +8 -0
  20. package/dist/bin/exe-agent-config.js +1 -1
  21. package/dist/bin/exe-agent.js +10 -10
  22. package/dist/bin/exe-assign.js +7 -6
  23. package/dist/bin/exe-boot.js +20 -19
  24. package/dist/bin/exe-call.js +4 -4
  25. package/dist/bin/exe-cloud.js +3 -3
  26. package/dist/bin/exe-dispatch.js +9 -9
  27. package/dist/bin/exe-doctor.js +1 -1
  28. package/dist/bin/exe-export-behaviors.js +7 -6
  29. package/dist/bin/exe-forget.js +6 -5
  30. package/dist/bin/exe-gateway.js +5 -5
  31. package/dist/bin/exe-heartbeat.js +9 -9
  32. package/dist/bin/exe-kill.js +13 -12
  33. package/dist/bin/exe-launch-agent.js +11 -10
  34. package/dist/bin/exe-new-employee.js +6 -6
  35. package/dist/bin/exe-pending-messages.js +10 -10
  36. package/dist/bin/exe-pending-notifications.js +9 -9
  37. package/dist/bin/exe-pending-reviews.js +9 -9
  38. package/dist/bin/exe-rename.js +4 -4
  39. package/dist/bin/exe-review.js +12 -11
  40. package/dist/bin/exe-search.js +5 -4
  41. package/dist/bin/exe-session-cleanup.js +19 -14
  42. package/dist/bin/exe-settings.js +3 -3
  43. package/dist/bin/exe-start-codex.js +11 -10
  44. package/dist/bin/exe-start-opencode.js +8 -7
  45. package/dist/bin/exe-status.js +10 -10
  46. package/dist/bin/exe-team.js +2 -2
  47. package/dist/bin/git-sweep.js +9 -9
  48. package/dist/bin/graph-backfill.js +4 -3
  49. package/dist/bin/graph-export.js +5 -4
  50. package/dist/bin/import-history.js +171 -0
  51. package/dist/bin/install-launchd.js +41 -0
  52. package/dist/bin/install.js +50 -74
  53. package/dist/bin/intercom-check.js +4 -4
  54. package/dist/bin/postgres-agentic-reflection-backfill.js +2 -2
  55. package/dist/bin/postgres-agentic-semantic-backfill.js +4 -4
  56. package/dist/bin/pre-publish.js +1 -1
  57. package/dist/bin/scan-tasks.js +22 -12
  58. package/dist/bin/setup.js +1 -1
  59. package/dist/bin/shard-migrate.js +4 -3
  60. package/dist/bin/stack-update.js +61 -857
  61. package/dist/bin/vps-backup.js +170 -0
  62. package/dist/bin/vps-health-gate.js +232 -0
  63. package/dist/{capacity-monitor-ZEAE4WP2.js → capacity-monitor-JBZB2S4P.js} +10 -10
  64. package/dist/{catchup-brief-OGWCHENC.js → catchup-brief-HE2EMZS5.js} +12 -11
  65. package/dist/{chunk-DJJNB47C.js → chunk-27DO3EZO.js} +1 -1
  66. package/dist/{chunk-45FYZIHI.js → chunk-32YUET3Y.js} +2 -2
  67. package/dist/{chunk-Y75ECPO5.js → chunk-3FW5LUGI.js} +2 -2
  68. package/dist/{chunk-4OZGQZ4U.js → chunk-3M3O56VT.js} +636 -179
  69. package/dist/{chunk-77WQOD6J.js → chunk-4CXUZ4NI.js} +2 -2
  70. package/dist/{chunk-PBXWPHEK.js → chunk-4VEHJZ6R.js} +1 -1
  71. package/dist/{chunk-TH22QIEC.js → chunk-6A4COFDG.js} +1 -1
  72. package/dist/{chunk-ACBTCC2L.js → chunk-7OJH2A6I.js} +1 -1
  73. package/dist/{chunk-NHCOTCI6.js → chunk-A7SGEBXJ.js} +2 -2
  74. package/dist/{chunk-5MPQSNZF.js → chunk-AUTCT6AY.js} +1 -1
  75. package/dist/{chunk-OEKSTOTE.js → chunk-AZAZ2C75.js} +1 -1
  76. package/dist/chunk-CHCA3ZM2.js +167 -0
  77. package/dist/{chunk-X347L57O.js → chunk-CSTJQDOE.js} +4 -3
  78. package/dist/{chunk-B234R3VW.js → chunk-D7WLV6WD.js} +2 -2
  79. package/dist/{chunk-GMXF3AHJ.js → chunk-DGAONW36.js} +1 -1
  80. package/dist/chunk-EAT5YL3W.js +229 -0
  81. package/dist/{chunk-OD4H5YCJ.js → chunk-EKTQE2R5.js} +8 -8
  82. package/dist/{chunk-Z44PC42G.js → chunk-ELUBA7XL.js} +2 -2
  83. package/dist/{chunk-ZWS6XQER.js → chunk-F5AKOE4P.js} +7 -7
  84. package/dist/{chunk-T5YULDDO.js → chunk-FVI4UBKO.js} +27 -4
  85. package/dist/{chunk-ESRI7MFI.js → chunk-GAN7PW6G.js} +28 -24
  86. package/dist/{chunk-K4OWYJSP.js → chunk-GM2WZTG3.js} +2 -2
  87. package/dist/{chunk-TAB5QGIK.js → chunk-GZYQTPTF.js} +3 -3
  88. package/dist/{chunk-CXDU5DE3.js → chunk-IAUNGATJ.js} +1 -1
  89. package/dist/{chunk-YS63NS6M.js → chunk-IHSM5GR4.js} +1 -1
  90. package/dist/{chunk-23PTS2ZD.js → chunk-IP7KJAUW.js} +117 -15
  91. package/dist/{chunk-D6IMJAV2.js → chunk-J64P2LB2.js} +2 -2
  92. package/dist/{chunk-CXAVSQZM.js → chunk-JXMSCKRM.js} +1 -1
  93. package/dist/{chunk-RQMK3IQH.js → chunk-K4OTJP6N.js} +14 -7
  94. package/dist/{chunk-L7ROZR2H.js → chunk-KXAUMIOX.js} +1 -1
  95. package/dist/{chunk-TPC3LAP7.js → chunk-LGY2BIOT.js} +13 -0
  96. package/dist/{chunk-RPIDSBK7.js → chunk-LLHRJEE4.js} +3 -3
  97. package/dist/{chunk-6WG2VIKC.js → chunk-LM7H6XU4.js} +1 -1
  98. package/dist/{chunk-Y6GMKZZ2.js → chunk-LOFFGJSY.js} +150 -23
  99. package/dist/{chunk-W7SDGBEC.js → chunk-MFI5OXYW.js} +52 -84
  100. package/dist/{chunk-KNPEVPYG.js → chunk-MSSQWF6X.js} +2 -2
  101. package/dist/{chunk-QIQAO3VG.js → chunk-NEFFFKMD.js} +3 -3
  102. package/dist/{chunk-YUC552KZ.js → chunk-NEHONJJC.js} +3 -3
  103. package/dist/{chunk-KZ7SXZ2V.js → chunk-NFMQRLCD.js} +1 -1
  104. package/dist/{chunk-52HCNDPG.js → chunk-O4TATDOV.js} +1 -1
  105. package/dist/{chunk-AR3OYGLB.js → chunk-PEFBRL4S.js} +28 -6
  106. package/dist/{chunk-AEUXUEJG.js → chunk-PEXVU3HU.js} +5 -3
  107. package/dist/chunk-Q2G5C3HV.js +217 -0
  108. package/dist/{chunk-KOO56JVC.js → chunk-Q6N6LDEJ.js} +1 -1
  109. package/dist/{chunk-TXSJ2L5O.js → chunk-QI4IXJN7.js} +1 -1
  110. package/dist/{chunk-HLVQ5Y7B.js → chunk-RE4VLK45.js} +1 -1
  111. package/dist/{chunk-TF6SZGDT.js → chunk-SA2PH6WY.js} +1 -1
  112. package/dist/{chunk-5RSYY7BE.js → chunk-SJYOPYXH.js} +117 -9
  113. package/dist/{chunk-PJGHBANY.js → chunk-TTJE7CCU.js} +1 -1
  114. package/dist/{chunk-A7KEWR6S.js → chunk-TXWQPL2U.js} +1 -1
  115. package/dist/{chunk-XXSJ35J5.js → chunk-U5ZH52FB.js} +2 -2
  116. package/dist/{chunk-G4FDG3LK.js → chunk-UVNDLF74.js} +63 -40
  117. package/dist/{chunk-5OD3AFRW.js → chunk-V6RCZ25F.js} +1 -1
  118. package/dist/{chunk-LHMBIFKD.js → chunk-VYNNN2S3.js} +4 -4
  119. package/dist/chunk-WCYT54XP.js +934 -0
  120. package/dist/{chunk-5AMSQRHT.js → chunk-XGYSTVUH.js} +1 -1
  121. package/dist/{chunk-MKZBHM6A.js → chunk-XLWF3C4R.js} +4 -4
  122. package/dist/{chunk-YL36L2SN.js → chunk-Y7YHLV57.js} +1 -1
  123. package/dist/{chunk-HZC4MR4H.js → chunk-YBKB2PXY.js} +1 -1
  124. package/dist/{chunk-NWM3A4TK.js → chunk-ZDNLKXZA.js} +1 -1
  125. package/dist/{chunk-O7KW6QMH.js → chunk-ZW4TKQUM.js} +15 -5
  126. package/dist/{chunk-6BURHBE6.js → chunk-ZXB44R3E.js} +32 -11
  127. package/dist/co-occurrence-WCED475N.js +73 -0
  128. package/dist/{code-context-index-B6VIWPSF.js → code-context-index-LSZ3DKTJ.js} +2 -2
  129. package/dist/{crdt-sync-XA22KI3S.js → crdt-sync-PBXZTHZC.js} +1 -1
  130. package/dist/{crm-webhook-CIZNOEY4.js → crm-webhook-W7Q25VZU.js} +2 -2
  131. package/dist/{cto-delegation-gate-H5IULFRC.js → cto-delegation-gate-JKULOLMC.js} +8 -8
  132. package/dist/{daemon-orchestration-VO5XQIJL.js → daemon-orchestration-CHV6MB42.js} +13 -11
  133. package/dist/{exe-drift-DMT75WR3.js → exe-drift-PW36OULT.js} +2 -2
  134. package/dist/{exe-export-2RZWOSX6.js → exe-export-XQOD3KE6.js} +6 -5
  135. package/dist/{exe-import-NFNYATHL.js → exe-import-QOFP67LW.js} +6 -5
  136. package/dist/{exe-key-4D7CF3BU.js → exe-key-WQ34UZR6.js} +1 -1
  137. package/dist/{fast-db-init-LAEISZQ2.js → fast-db-init-UKETGWQI.js} +1 -1
  138. package/dist/gateway/index.js +6 -6
  139. package/dist/{git-staleness-M46AYLPP.js → git-staleness-ATV5CGAP.js} +1 -1
  140. package/dist/{git-task-sweep-PXOS56YT.js → git-task-sweep-KXZRIP4T.js} +9 -9
  141. package/dist/{global-procedures-KROQQX54.js → global-procedures-G6IKCYKM.js} +3 -3
  142. package/dist/{graph-auto-extract-QJ2BBJM2.js → graph-auto-extract-ZJXJOLE2.js} +1 -1
  143. package/dist/hooks/bug-report-worker.js +10 -10
  144. package/dist/hooks/codex-stop-task-finalizer.js +10 -10
  145. package/dist/hooks/commit-complete.js +11 -11
  146. package/dist/hooks/error-recall.js +8 -7
  147. package/dist/hooks/exe-heartbeat-hook.js +2 -2
  148. package/dist/hooks/ingest-worker.js +3 -3
  149. package/dist/hooks/ingest.js +9 -9
  150. package/dist/hooks/instructions-loaded.js +3 -3
  151. package/dist/hooks/notification.js +3 -3
  152. package/dist/hooks/post-compact.js +10 -10
  153. package/dist/hooks/post-tool-combined.js +5 -5
  154. package/dist/hooks/pre-compact.js +16 -16
  155. package/dist/hooks/pre-tool-use.js +14 -14
  156. package/dist/hooks/prompt-submit.js +30 -29
  157. package/dist/hooks/session-end.js +46 -25
  158. package/dist/hooks/session-start.js +48 -10
  159. package/dist/hooks/stop.js +17 -17
  160. package/dist/hooks/subagent-stop.js +10 -10
  161. package/dist/hooks/summary-worker.js +17 -16
  162. package/dist/index.js +17 -17
  163. package/dist/{installer-SDBLJBAB.js → installer-DE2LH5EC.js} +4 -4
  164. package/dist/{installer-ZA6QNQ4P.js → installer-M2MDS7HC.js} +4 -4
  165. package/dist/{installer-6KAY6LD6.js → installer-VE23YFXU.js} +4 -4
  166. package/dist/{intercom-queue-K3DVKSPJ.js → intercom-queue-RNM6EPGA.js} +1 -1
  167. package/dist/keyword-extractor-UJHFWVZE.js +11 -0
  168. package/dist/lib/cloud-sync.js +3 -3
  169. package/dist/lib/consolidation.js +5 -4
  170. package/dist/lib/database.js +1 -1
  171. package/dist/lib/db-daemon-client.js +1 -1
  172. package/dist/lib/db.js +1 -1
  173. package/dist/lib/embed-worker.js +98 -0
  174. package/dist/lib/embedder.js +2 -2
  175. package/dist/lib/employee-templates.js +4 -4
  176. package/dist/lib/employees.js +1 -1
  177. package/dist/lib/exe-daemon-client.js +1 -1
  178. package/dist/lib/exe-daemon.js +523 -500
  179. package/dist/lib/hybrid-search.js +5 -6
  180. package/dist/lib/identity.js +1 -1
  181. package/dist/lib/messaging.js +9 -9
  182. package/dist/lib/reminders.js +2 -2
  183. package/dist/lib/schedules.js +5 -4
  184. package/dist/lib/skill-learning.js +3 -3
  185. package/dist/lib/store.js +4 -3
  186. package/dist/lib/task-router.js +2 -2
  187. package/dist/lib/tasks.js +9 -9
  188. package/dist/lib/tmux-routing.js +8 -8
  189. package/dist/lib/tmux-transport.js +1 -1
  190. package/dist/lib/token-spend.js +2 -2
  191. package/dist/lib/transport.js +2 -2
  192. package/dist/lib/ws-client.js +3 -1
  193. package/dist/mcp/register-tools.js +54 -51
  194. package/dist/mcp/server.js +58 -55
  195. package/dist/mcp/tools/complete-reminder.js +3 -3
  196. package/dist/mcp/tools/create-reminder.js +3 -3
  197. package/dist/mcp/tools/create-task.js +11 -11
  198. package/dist/mcp/tools/deactivate-behavior.js +4 -4
  199. package/dist/mcp/tools/list-reminders.js +3 -3
  200. package/dist/mcp/tools/list-tasks.js +11 -11
  201. package/dist/mcp/tools/send-message.js +11 -11
  202. package/dist/mcp/tools/update-task.js +10 -10
  203. package/dist/{mcp-http-config-LK2EDOEJ.js → mcp-http-config-Z2E4VUOF.js} +2 -2
  204. package/dist/{memory-cards-V3DKSRWL.js → memory-cards-SFDKDIAW.js} +1 -1
  205. package/dist/memory-graph-extractor-YD4GNH7T.js +16 -0
  206. package/dist/{memory-poisoning-defense-3B75HS74.js → memory-poisoning-defense-VEGNFELN.js} +1 -1
  207. package/dist/{memory-queue-client-LFPZPPQA.js → memory-queue-client-5HB2XUH7.js} +2 -2
  208. package/dist/{memory-reflection-HTDAUUE5.js → memory-reflection-MTPRQNI6.js} +2 -2
  209. package/dist/{notifications-76VCYXWW.js → notifications-6TCE6OBG.js} +8 -8
  210. package/dist/{orchestrator-CBNSBI5P.js → orchestrator-W2GYJR23.js} +10 -10
  211. package/dist/{plan-limits-SOR3QXKV.js → plan-limits-4EP46323.js} +2 -2
  212. package/dist/{projection-worker-FK5YOEIL.js → projection-worker-EBUYNMU2.js} +1 -1
  213. package/dist/{review-polling-ZLNDUKL4.js → review-polling-2N7KQFZZ.js} +9 -9
  214. package/dist/runtime/index.js +15 -15
  215. package/dist/{session-events-CUSPL25D.js → session-events-K47FHAXJ.js} +9 -9
  216. package/dist/{session-kill-telemetry-FLBRHBDP.js → session-kill-telemetry-275YUXM5.js} +2 -2
  217. package/dist/{session-scope-PX2ABSJO.js → session-scope-XSFJZEER.js} +8 -8
  218. package/dist/{setup-wizard-Y6PBZGFX.js → setup-wizard-UEO7HYLQ.js} +1 -1
  219. package/dist/{skill-refinement-L7PGKCYO.js → skill-refinement-WXBTANDQ.js} +1 -1
  220. package/dist/stack-update-2B2UXREV.js +50 -0
  221. package/dist/{task-enforcement-7FUILB63.js → task-enforcement-2JIJSXPU.js} +14 -16
  222. package/dist/{task-scope-2N45TE32.js → task-scope-W73Z3XWE.js} +8 -8
  223. package/dist/{tasks-crud-ADLCGHGH.js → tasks-crud-HPJKI3QQ.js} +8 -8
  224. package/dist/{tasks-review-PJ2DUI6N.js → tasks-review-MXLMPGNZ.js} +8 -8
  225. package/dist/{token-budget-T5DFXVTM.js → token-budget-BA46CVHX.js} +1 -1
  226. package/dist/{tool-capability-index-6JJN6ZRC.js → tool-capability-index-42VVN5BS.js} +1 -1
  227. package/dist/{tool-telemetry-72PVO5HV.js → tool-telemetry-GZ5E2AUL.js} +1 -1
  228. package/dist/tui/App.js +22 -18
  229. package/dist/{tui-data-63JHE6EZ.js → tui-data-PVXWQCJX.js} +8 -8
  230. package/dist/{worker-gate-REVBJUZ6.js → worker-gate-WTTK64TK.js} +1 -1
  231. package/dist/{workflow-engine-W2WNHJG5.js → workflow-engine-LT3WTT7V.js} +2 -2
  232. package/package.json +1 -1
  233. package/release-notes.json +209 -209
  234. /package/dist/{chunk-BNOZUS6J.js → chunk-6VVCAVRT.js} +0 -0
  235. /package/dist/{chunk-IC7GKK6I.js → chunk-CWQZZ7X3.js} +0 -0
  236. /package/dist/{chunk-ZI2ZVERO.js → chunk-EIW5GOBW.js} +0 -0
  237. /package/dist/{chunk-2BGGDNRD.js → chunk-IPPJEM26.js} +0 -0
  238. /package/dist/{chunk-4ISDU5KR.js → chunk-K5UR73PM.js} +0 -0
  239. /package/dist/{chunk-ZWRTVUQ6.js → chunk-KIMO5S45.js} +0 -0
  240. /package/dist/{chunk-S2FX5KJ4.js → chunk-WBLILGAP.js} +0 -0
  241. /package/dist/{core-memory-PCJ3L46L.js → core-memory-RAC6M67J.js} +0 -0
  242. /package/dist/{entity-boost-GHFPE6A2.js → entity-boost-5FIRFRDC.js} +0 -0
  243. /package/dist/{message-queue-client-CHRQYBH5.js → message-queue-client-PTQ2S7D7.js} +0 -0
  244. /package/dist/{wiki-acl-QYRAYYVQ.js → wiki-acl-MSDRCIAI.js} +0 -0
@@ -0,0 +1,934 @@
1
+ import {
2
+ loadLicense
3
+ } from "./chunk-MVMMULOJ.js";
4
+
5
+ // src/lib/stack-update.ts
6
+ import { execFileSync, spawnSync } from "child_process";
7
+ import { createVerify, randomBytes, verify as verifySignature } from "crypto";
8
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
9
+ import http from "http";
10
+ import https from "https";
11
+ import path from "path";
12
+ import { fileURLToPath } from "url";
13
+ function isSignedEnvelope(value) {
14
+ return !!value && typeof value === "object" && "manifest" in value && "signature" in value;
15
+ }
16
+ function canonicalizeStackManifest(manifest) {
17
+ const clone = JSON.parse(JSON.stringify(manifest));
18
+ delete clone.signature;
19
+ return stableJson(clone);
20
+ }
21
+ function verifyStackManifestSignature(manifest, publicKeyPem) {
22
+ const signature = manifest.signature;
23
+ if (!signature) throw new Error("Stack manifest signature required but missing");
24
+ const payload = Buffer.from(canonicalizeStackManifest(manifest));
25
+ const sig = Buffer.from(signature.signature, "base64");
26
+ let ok = false;
27
+ if (signature.alg === "ed25519") {
28
+ ok = verifySignature(null, payload, publicKeyPem, sig);
29
+ } else if (signature.alg === "rsa-sha256") {
30
+ const verifier = createVerify("RSA-SHA256");
31
+ verifier.update(payload);
32
+ verifier.end();
33
+ ok = verifier.verify(publicKeyPem, sig);
34
+ } else {
35
+ throw new Error(`Unsupported stack manifest signature alg: ${signature.alg}`);
36
+ }
37
+ if (!ok) throw new Error("Stack manifest signature verification failed");
38
+ }
39
+ function stableJson(value) {
40
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
41
+ if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
42
+ const obj = value;
43
+ return `{${Object.keys(obj).sort().map((key) => `${JSON.stringify(key)}:${stableJson(obj[key])}`).join(",")}}`;
44
+ }
45
+ function findLatestBackupEnvFile(envFile) {
46
+ const backupDir = path.join(path.dirname(envFile), ".exe-stack-backups");
47
+ if (!existsSync(backupDir)) return null;
48
+ const backups = readdirSync(backupDir).filter((name) => name.startsWith("env-") && name.endsWith(".bak")).sort();
49
+ const latest = backups.at(-1);
50
+ return latest ? path.join(backupDir, latest) : null;
51
+ }
52
+ async function rollbackStackUpdate(options) {
53
+ const exec = options.exec ?? defaultExec;
54
+ const backupEnvFile = options.lockFile && existsSync(options.lockFile) ? JSON.parse(readFileSync(options.lockFile, "utf8")).backupEnvFile : void 0;
55
+ const rollbackEnv = backupEnvFile && existsSync(backupEnvFile) ? backupEnvFile : findLatestBackupEnvFile(options.envFile);
56
+ if (!rollbackEnv) throw new Error(`No stack backup env found beside ${options.envFile}`);
57
+ const preRollbackBackup = options.envFile + `.pre-rollback-${Date.now()}`;
58
+ try {
59
+ if (existsSync(options.envFile)) copyFileSync(options.envFile, preRollbackBackup);
60
+ } catch {
61
+ }
62
+ writeFileSync(options.envFile, readFileSync(rollbackEnv), { mode: 384 });
63
+ const composeArgs = ["compose", "--file", options.composeFile, "--env-file", options.envFile];
64
+ exec("docker", [...composeArgs, "up", "-d"]);
65
+ return { status: "rolled_back", targetVersion: "previous", changes: [], backupEnvFile: rollbackEnv, lockFile: options.lockFile ?? path.join(path.dirname(options.envFile), ".exe-stack-lock.json") };
66
+ }
67
+ function parseStackManifest(raw, publicKey) {
68
+ const parsedRaw = JSON.parse(raw);
69
+ const parsed = isSignedEnvelope(parsedRaw) ? { ...parsedRaw.manifest, signature: parsedRaw.signature } : parsedRaw;
70
+ if (publicKey) verifyStackManifestSignature(parsed, publicKey);
71
+ if (parsed.schemaVersion !== 1) throw new Error("Unsupported stack manifest schemaVersion");
72
+ if (!parsed.latest || !parsed.stacks || typeof parsed.stacks !== "object") {
73
+ throw new Error("Invalid stack manifest: latest and stacks are required");
74
+ }
75
+ for (const [version, release] of Object.entries(parsed.stacks)) {
76
+ if (!release.version) release.version = version;
77
+ if (!release.services || typeof release.services !== "object") {
78
+ throw new Error(`Invalid stack manifest: release ${version} has no services`);
79
+ }
80
+ for (const [serviceName, service] of Object.entries(release.services)) {
81
+ if (!service.image || !service.env) {
82
+ throw new Error(`Invalid stack manifest: ${version}.${serviceName} requires image and env`);
83
+ }
84
+ }
85
+ }
86
+ return parsed;
87
+ }
88
+ async function loadStackManifest(ref, fetchText = defaultFetchText, publicKey, authToken) {
89
+ if (/^https?:\/\//.test(ref)) return parseStackManifest(await fetchTextWithAuth(ref, fetchText, authToken), publicKey);
90
+ return parseStackManifest(readFileSync(ref, "utf8"), publicKey);
91
+ }
92
+ async function fetchTextWithAuth(ref, fetchText, authToken) {
93
+ if (!authToken || fetchText !== defaultFetchText) return fetchText(ref);
94
+ return defaultFetchText(ref, authToken);
95
+ }
96
+ function parseEnv(raw) {
97
+ const env = /* @__PURE__ */ new Map();
98
+ for (const line of raw.split(/\r?\n/)) {
99
+ const trimmed = line.trim();
100
+ if (!trimmed || trimmed.startsWith("#")) continue;
101
+ const idx = line.indexOf("=");
102
+ if (idx <= 0) continue;
103
+ env.set(line.slice(0, idx).trim(), line.slice(idx + 1));
104
+ }
105
+ return env;
106
+ }
107
+ function patchEnv(raw, updates) {
108
+ const seen = /* @__PURE__ */ new Set();
109
+ const lines = raw.replace(/\n$/, "").split(/\r?\n/);
110
+ const patched = lines.map((line) => {
111
+ const idx = line.indexOf("=");
112
+ if (idx <= 0 || line.trim().startsWith("#")) return line;
113
+ const key = line.slice(0, idx).trim();
114
+ if (!(key in updates)) return line;
115
+ seen.add(key);
116
+ return `${key}=${updates[key]}`;
117
+ });
118
+ for (const [key, value] of Object.entries(updates)) {
119
+ if (!seen.has(key)) patched.push(`${key}=${value}`);
120
+ }
121
+ return patched.join("\n").replace(/\n*$/, "\n");
122
+ }
123
+ function createStackUpdatePlan(manifest, envRaw, targetVersion) {
124
+ const version = targetVersion ?? manifest.latest;
125
+ const release = manifest.stacks[version];
126
+ if (!release) throw new Error(`Stack version ${version} not found in manifest`);
127
+ const env = parseEnv(envRaw);
128
+ const changes = [];
129
+ for (const [serviceName, service] of Object.entries(release.services)) {
130
+ const before = env.get(service.env);
131
+ if (before !== service.image) {
132
+ changes.push({ key: service.env, before, after: service.image, service: serviceName });
133
+ }
134
+ }
135
+ return {
136
+ manifest,
137
+ release,
138
+ targetVersion: version,
139
+ changes,
140
+ breakingChanges: release.breakingChanges ?? []
141
+ };
142
+ }
143
+ var ASKEXE_GHCR_IMAGE = /^(?:ghcr\.io\/askexe|registry\.askexe\.com\/askexe)\/[a-z0-9._/-]+(?::[^:@$/{]+|@sha256:[a-f0-9]{64})$/i;
144
+ function validatePinnedGhcrImage(image, label) {
145
+ const trimmed = image.trim().replace(/^['"]|['"]$/g, "");
146
+ if (!trimmed) return `${label} is empty`;
147
+ if (trimmed.includes("${")) return null;
148
+ if (!trimmed.startsWith("ghcr.io/askexe/") && !trimmed.startsWith("registry.askexe.com/askexe/")) return `${label} must use ghcr.io/askexe/* or registry.askexe.com/askexe/*, got ${trimmed}`;
149
+ if (/:latest(?:$|[\s#])/.test(trimmed)) return `${label} must not use :latest (${trimmed})`;
150
+ if (!ASKEXE_GHCR_IMAGE.test(trimmed)) return `${label} must be pinned with an explicit tag or sha256 digest from ghcr.io/askexe or registry.askexe.com/askexe, got ${trimmed}`;
151
+ return null;
152
+ }
153
+ function validateComposeImageLiteral(image, label) {
154
+ const trimmed = image.trim().replace(/^['"]|['"]$/g, "");
155
+ if (!trimmed) return `${label} is empty`;
156
+ if (trimmed.startsWith("ghcr.io/askexe/") || trimmed.startsWith("registry.askexe.com/askexe/")) return validatePinnedGhcrImage(trimmed, label);
157
+ if (/^(postgres|pgvector\/pgvector|clickhouse\/clickhouse-server|redis|nginx|postgrest\/postgrest|supabase\/gotrue):[^:]+$/i.test(trimmed)) return null;
158
+ return `${label} uses unsupported non-AskExe image ${trimmed}; customer app images must come from pinned ghcr.io/askexe images`;
159
+ }
160
+ function collectProductionDeployGateIssues(plan, envRaw, composeRaw) {
161
+ const issues = [];
162
+ const env = parseEnv(envRaw);
163
+ for (const [serviceName, service] of Object.entries(plan.release.services)) {
164
+ const manifestIssue = validatePinnedGhcrImage(service.image, `manifest ${plan.targetVersion}.${serviceName}.image`);
165
+ if (manifestIssue) issues.push({ kind: "manifest-image", message: manifestIssue });
166
+ const envImage = env.get(service.env);
167
+ if (envImage) {
168
+ const envIssue = validatePinnedGhcrImage(envImage, `env ${service.env}`);
169
+ if (envIssue) issues.push({ kind: "env-image", message: envIssue });
170
+ }
171
+ }
172
+ const lines = composeRaw.split(/\r?\n/);
173
+ lines.forEach((line, index) => {
174
+ if (/^\s*build\s*:/.test(line)) {
175
+ issues.push({ kind: "compose-build", message: `compose line ${index + 1} contains build:, production deploys must pull images` });
176
+ }
177
+ const imageMatch = line.match(/^\s*image\s*:\s*(.+?)\s*(?:#.*)?$/);
178
+ if (imageMatch) {
179
+ const image = imageMatch[1].trim();
180
+ if (image.includes("${")) {
181
+ const fallback = image.match(/:-([^}]+)}/)?.[1];
182
+ if (fallback) {
183
+ const composeIssue = validateComposeImageLiteral(fallback, `compose image fallback on line ${index + 1}`);
184
+ if (composeIssue) issues.push({ kind: "compose-image", message: composeIssue });
185
+ }
186
+ } else {
187
+ const composeIssue = validateComposeImageLiteral(image, `compose image on line ${index + 1}`);
188
+ if (composeIssue) issues.push({ kind: "compose-image", message: composeIssue });
189
+ }
190
+ }
191
+ });
192
+ return issues;
193
+ }
194
+ function assertDeploymentScopeAllowed(plan, persona = "customer") {
195
+ if (persona === "askexe-control-plane") return;
196
+ const blocked = Object.entries(plan.release.services).filter(([, service]) => service.deploymentScope === "askexe-control-plane").map(([name]) => name);
197
+ if (blocked.length > 0) {
198
+ throw new Error(
199
+ `Customer deployment manifest includes AskExe control-plane service(s): ${blocked.join(", ")}. Customer VPSs may deploy customer services and optional agents only.`
200
+ );
201
+ }
202
+ }
203
+ function assertProductionDeployGate(plan, envRaw, composeRaw, options = {}) {
204
+ const issues = collectProductionDeployGateIssues(plan, envRaw, composeRaw);
205
+ if (issues.length === 0) return;
206
+ if (options.breakGlassReason?.trim()) {
207
+ writeBreakGlassAudit(plan, issues, options);
208
+ return;
209
+ }
210
+ const details = issues.map((issue) => `- [${issue.kind}] ${issue.message}`).join("\n");
211
+ throw new Error(
212
+ `Production deploy gate failed. Exe OS deploys must use pinned ghcr.io/askexe or registry.askexe.com/askexe images and must not build from source on the VPS.
213
+ ${details}
214
+ Emergency override requires --break-glass <reason> and writes an audit file.`
215
+ );
216
+ }
217
+ function writeBreakGlassAudit(plan, issues, options) {
218
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
219
+ const stamp = now().toISOString().replace(/[:.]/g, "-");
220
+ const defaultDir = existsSync("exe/output") ? "exe/output" : path.dirname(options.envFile ?? ".");
221
+ const auditFile = options.breakGlassAuditFile ?? path.join(defaultDir, `stack-update-break-glass-${stamp}.md`);
222
+ mkdirSync(path.dirname(auditFile), { recursive: true });
223
+ const body = [
224
+ `# Stack Update Break-Glass Audit \u2014 ${now().toISOString()}`,
225
+ "",
226
+ `Target version: ${plan.targetVersion}`,
227
+ `Reason: ${options.breakGlassReason?.trim()}`,
228
+ "",
229
+ "## Gate failures overridden",
230
+ ...issues.map((issue) => `- [${issue.kind}] ${issue.message}`),
231
+ "",
232
+ "## Required follow-up",
233
+ "Return this deployment to the standard pinned GHCR image path immediately after the emergency is resolved.",
234
+ ""
235
+ ].join("\n");
236
+ writeFileSync(auditFile, body, { mode: 384 });
237
+ console.warn(`[stack-update] BREAK-GLASS deploy override recorded: ${auditFile}`);
238
+ }
239
+ function assertBreakingChangesAllowed(plan, allowedIds) {
240
+ const required = plan.breakingChanges.filter((c) => c.requiresConfirmation !== false);
241
+ const missing = required.filter((c) => !allowedIds.includes(c.id));
242
+ if (missing.length > 0) {
243
+ const details = missing.map((c) => `- ${c.id}: ${c.title}
244
+ ${c.description}
245
+ Action: ${c.requiredAction ?? "Review release notes."}`).join("\n");
246
+ throw new Error(
247
+ `Stack ${plan.targetVersion} has breaking changes that require confirmation:
248
+ ${details}
249
+ Re-run with --allow-breaking ${missing.map((c) => c.id).join(",")}`
250
+ );
251
+ }
252
+ }
253
+ function commandSucceeds(cmd, args = []) {
254
+ const res = spawnSync(cmd, args, { stdio: "ignore" });
255
+ return res.status === 0;
256
+ }
257
+ function shellSucceeds(command) {
258
+ const res = spawnSync("sh", ["-lc", command], { stdio: "ignore" });
259
+ return res.status === 0;
260
+ }
261
+ function resolvePackageRoot() {
262
+ const here = path.dirname(fileURLToPath(import.meta.url));
263
+ const candidates = [
264
+ path.resolve(here, "..", ".."),
265
+ path.resolve(here, ".."),
266
+ process.cwd()
267
+ ];
268
+ for (const c of candidates) {
269
+ if (existsSync(path.join(c, "package.json")) && existsSync(path.join(c, "deploy", "compose", "docker-compose.yml"))) return c;
270
+ }
271
+ return process.cwd();
272
+ }
273
+ function copyTemplateIfMissing(srcRel, dest, created) {
274
+ if (existsSync(dest)) return;
275
+ const src = path.join(resolvePackageRoot(), srcRel);
276
+ if (!existsSync(src)) throw new Error(`Missing packaged stack template: ${srcRel}. Reinstall/update exe-os and retry.`);
277
+ try {
278
+ mkdirSync(path.dirname(dest), { recursive: true });
279
+ } catch (err) {
280
+ if (err.code === "EACCES") {
281
+ const dir = path.dirname(dest);
282
+ throw new Error(
283
+ `Permission denied creating ${dir}. Run this first:
284
+
285
+ sudo mkdir -p ${dir} && sudo chown $(whoami) ${dir}
286
+
287
+ Then re-run stack-update.`
288
+ );
289
+ }
290
+ throw err;
291
+ }
292
+ copyFileSync(src, dest);
293
+ created.push(dest);
294
+ }
295
+ function installDockerUbuntu(exec) {
296
+ if (process.platform !== "linux") throw new Error("Docker auto-install is only supported on Linux. Install Docker manually, then retry.");
297
+ if (!existsSync("/etc/os-release")) throw new Error("Cannot detect Linux distro; install Docker manually, then retry.");
298
+ const osRelease = readFileSync("/etc/os-release", "utf8");
299
+ if (!/ID=(ubuntu|debian)|ID_LIKE=.*debian/.test(osRelease)) {
300
+ throw new Error("Docker auto-install currently supports Ubuntu/Debian only. Install Docker manually, then retry.");
301
+ }
302
+ const script = [
303
+ "set -e",
304
+ "sudo apt-get update",
305
+ "sudo apt-get install -y ca-certificates curl gnupg",
306
+ "sudo install -m 0755 -d /etc/apt/keyrings",
307
+ "if [ ! -f /etc/apt/keyrings/docker.gpg ]; then curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg; sudo chmod a+r /etc/apt/keyrings/docker.gpg; fi",
308
+ ". /etc/os-release",
309
+ 'CODENAME="${VERSION_CODENAME:-bookworm}"',
310
+ 'if [ "${ID:-}" = "debian" ]; then DOCKER_DISTRO=debian; else DOCKER_DISTRO=ubuntu; fi',
311
+ `printf 'deb [arch=%s signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/%s %s stable\\n' "$(dpkg --print-architecture)" "$DOCKER_DISTRO" "$CODENAME" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null`,
312
+ "sudo apt-get update",
313
+ "sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin",
314
+ "sudo systemctl enable --now docker",
315
+ "sudo docker version >/dev/null",
316
+ "sudo docker compose version >/dev/null"
317
+ ].join("\n");
318
+ exec("sh", ["-lc", script]);
319
+ }
320
+ function randomSecret(bytes = 32) {
321
+ return randomBytes(bytes).toString("base64url");
322
+ }
323
+ function randomHexSecret(bytes = 24) {
324
+ return randomBytes(bytes).toString("hex");
325
+ }
326
+ function hydrateEnv(raw, opts) {
327
+ let next = raw;
328
+ const license = opts.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense() || "";
329
+ const domain = opts.domain || process.env.EXE_STACK_DOMAIN || process.env.CUSTOMER_DOMAIN || "";
330
+ const replacements = {};
331
+ const env = parseEnv(raw);
332
+ for (const [key, value] of env.entries()) {
333
+ if (!/CHANGEME/.test(value)) continue;
334
+ if (key === "EXE_LICENSE_KEY" && license) replacements[key] = license;
335
+ else if (key === "MONITOR_AGENT_TOKEN" || key === "MONITOR_AGENT_KEY") continue;
336
+ else if (key === "EXE_GATEWAY_WS_RELAY_AUTH_TOKEN") replacements[key] = randomHexSecret(24);
337
+ else if (key.endsWith("_PASSWORD")) replacements[key] = randomSecret(24);
338
+ else if (key.endsWith("_TOKEN")) replacements[key] = randomHexSecret(32);
339
+ else if (key.endsWith("_SECRET") || key.endsWith("_SALT")) replacements[key] = randomSecret(32);
340
+ else if (key.endsWith("_KEY") && key !== "EXE_LICENSE_KEY") continue;
341
+ }
342
+ if (domain) next = next.replaceAll("CHANGEME_DOMAIN", domain);
343
+ next = patchEnv(next, replacements);
344
+ const remaining = [...parseEnv(next).entries()].filter(([, value]) => /CHANGEME/.test(value)).map(([key, value]) => `${key}=${value}`);
345
+ return { raw: next, hadPlaceholders: /CHANGEME/.test(raw), remaining: [...new Set(remaining)] };
346
+ }
347
+ async function pairMonitorAgent(hubUrl, licenseKey, domain, envFile) {
348
+ if (!hubUrl || !licenseKey || !domain) {
349
+ return { paired: false, error: "Missing hubUrl, licenseKey, or domain for monitor pairing" };
350
+ }
351
+ const registrationUrl = `${hubUrl.replace(/\/+$/, "")}/api/register-agent`;
352
+ try {
353
+ const res = await fetch(registrationUrl, {
354
+ method: "POST",
355
+ headers: {
356
+ "content-type": "application/json",
357
+ authorization: `Bearer ${licenseKey}`
358
+ },
359
+ body: JSON.stringify({ name: domain, host: domain, port: 45876 }),
360
+ signal: AbortSignal.timeout(15e3)
361
+ });
362
+ if (!res.ok) {
363
+ const body = await res.text().catch(() => "");
364
+ return { paired: false, error: `Monitor hub returned HTTP ${res.status}: ${body}` };
365
+ }
366
+ const data = await res.json();
367
+ if (!data.token || !data.key) {
368
+ return { paired: false, error: "Monitor hub response missing token or key" };
369
+ }
370
+ if (existsSync(envFile)) {
371
+ const envRaw = readFileSync(envFile, "utf8");
372
+ const patched = patchEnv(envRaw, {
373
+ MONITOR_AGENT_TOKEN: data.token,
374
+ MONITOR_AGENT_KEY: data.key
375
+ });
376
+ if (patched !== envRaw) {
377
+ writeFileSync(envFile, patched, { mode: 384 });
378
+ }
379
+ }
380
+ return { paired: true, systemName: data.name ?? domain };
381
+ } catch (err) {
382
+ const reason = err instanceof Error ? err.message : String(err);
383
+ return { paired: false, error: `Monitor hub unreachable: ${reason}` };
384
+ }
385
+ }
386
+ function bootstrapStackHost(options) {
387
+ const exec = options.exec ?? defaultExec;
388
+ const createdFiles = [];
389
+ const actions = [];
390
+ let dockerInstalled = commandSucceeds("docker", ["version"]);
391
+ let dockerComposeInstalled = shellSucceeds("docker compose version");
392
+ if ((!dockerInstalled || !dockerComposeInstalled) && options.installDocker) {
393
+ actions.push("install_docker");
394
+ installDockerUbuntu(exec);
395
+ dockerInstalled = commandSucceeds("docker", ["version"]);
396
+ dockerComposeInstalled = shellSucceeds("docker compose version");
397
+ }
398
+ copyTemplateIfMissing("deploy/compose/docker-compose.yml", options.composeFile, createdFiles);
399
+ copyTemplateIfMissing("deploy/compose/.env.customer.example", options.envFile, createdFiles);
400
+ const brandingDest = path.join(path.dirname(options.envFile), "branding.json");
401
+ copyTemplateIfMissing("deploy/compose/gateway.json", path.join(path.dirname(options.envFile), "gateway.json"), createdFiles);
402
+ if (!existsSync(brandingDest)) writeFileSync(brandingDest, JSON.stringify({ brandName: "Hygo", productName: "Hygo OS" }, null, 2) + "\n", { mode: 384 });
403
+ let envHadPlaceholders = false;
404
+ let envRemainingPlaceholders = [];
405
+ if (existsSync(options.envFile)) {
406
+ const envRaw = readFileSync(options.envFile, "utf8");
407
+ const hydrated = hydrateEnv(envRaw, options);
408
+ envHadPlaceholders = hydrated.hadPlaceholders;
409
+ envRemainingPlaceholders = hydrated.remaining;
410
+ if (hydrated.raw !== envRaw) {
411
+ const backupPath = options.envFile + `.bak-${Date.now()}`;
412
+ try {
413
+ copyFileSync(options.envFile, backupPath);
414
+ } catch {
415
+ }
416
+ writeFileSync(options.envFile, hydrated.raw, { mode: 384 });
417
+ actions.push("hydrate_env_secrets");
418
+ }
419
+ }
420
+ return {
421
+ dockerInstalled,
422
+ dockerComposeInstalled,
423
+ composeFileExists: existsSync(options.composeFile),
424
+ envFileExists: existsSync(options.envFile),
425
+ envHadPlaceholders,
426
+ envRemainingPlaceholders,
427
+ licensePresent: !!(options.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense()),
428
+ createdFiles,
429
+ actions
430
+ };
431
+ }
432
+ function assertHostReadyForApply(report) {
433
+ const blockers = [];
434
+ if (!report.dockerInstalled) blockers.push("Docker is not installed/running. Re-run with --yes to auto-install on Ubuntu/Debian, or install Docker manually.");
435
+ if (!report.dockerComposeInstalled) blockers.push("Docker Compose plugin is missing. Re-run with --yes to auto-install on Ubuntu/Debian, or install docker-compose-plugin manually.");
436
+ if (!report.composeFileExists) blockers.push("docker-compose.yml is missing and could not be created.");
437
+ if (!report.envFileExists) blockers.push(".env is missing and could not be created.");
438
+ if (!report.licensePresent) blockers.push("Exe OS license key is missing. Run `exe-os setup`, `exe-os --activate <key>`, or pass --license-key.");
439
+ const hardPlaceholders = report.envRemainingPlaceholders.filter((p) => !/WHATSAPP|API_ROUTER|MONITOR_AGENT/.test(p));
440
+ if (hardPlaceholders.length > 0) blockers.push(`Required .env placeholders remain: ${hardPlaceholders.join(", ")}`);
441
+ if (blockers.length > 0) throw new Error(`Stack host is not ready:
442
+ - ${blockers.join("\n- ")}`);
443
+ }
444
+ function areStackContainersRunning(composeFile, envFile) {
445
+ try {
446
+ const result = spawnSync("docker", ["compose", "--file", composeFile, "--env-file", envFile, "ps", "-q"], {
447
+ stdio: ["pipe", "pipe", "pipe"],
448
+ timeout: 15e3
449
+ });
450
+ return result.status === 0 && (result.stdout?.toString().trim().length ?? 0) > 0;
451
+ } catch {
452
+ return false;
453
+ }
454
+ }
455
+ var CRITICAL_BIND_MOUNTS = [
456
+ { file: ".env", description: "secrets and image tags" },
457
+ { file: "gateway.json", description: "gateway connector config" }
458
+ ];
459
+ var CRITICAL_VOLUMES = [
460
+ "postgres_data",
461
+ "gateway_data",
462
+ "wiki_data",
463
+ "exe_os_data"
464
+ ];
465
+ function assertBindMountsExist(stackDir) {
466
+ const missing = [];
467
+ for (const mount of CRITICAL_BIND_MOUNTS) {
468
+ const fullPath = path.join(stackDir, mount.file);
469
+ if (!existsSync(fullPath)) {
470
+ missing.push(`${mount.file} (${mount.description})`);
471
+ }
472
+ }
473
+ if (missing.length > 0) {
474
+ console.warn(`[stack-update] \u26A0 Missing critical bind mounts:
475
+ - ${missing.join("\n - ")}`);
476
+ console.warn("[stack-update] These files contain customer config that will be lost if not present.");
477
+ }
478
+ }
479
+ function assertVolumesIntact() {
480
+ const missing = [];
481
+ for (const vol of CRITICAL_VOLUMES) {
482
+ const result = spawnSync("docker", ["volume", "inspect", `exe-os_${vol}`], {
483
+ stdio: ["pipe", "pipe", "pipe"],
484
+ timeout: 5e3
485
+ });
486
+ if (result.status !== 0) {
487
+ missing.push(vol);
488
+ }
489
+ }
490
+ if (missing.length > 0) {
491
+ console.warn(`[stack-update] \u26A0 Critical volumes missing after update: ${missing.join(", ")}`);
492
+ console.warn("[stack-update] Data may have been lost. Check if 'docker compose down -v' was used.");
493
+ }
494
+ }
495
+ async function runStackUpdate(options) {
496
+ const exec = options.exec ?? defaultExec;
497
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
498
+ if (options.rollback) return rollbackStackUpdate(options);
499
+ if (options.bootstrap !== false) {
500
+ const report = bootstrapStackHost({ ...options, installDocker: options.installDocker ?? !!options.yes });
501
+ if (!options.dryRun) assertHostReadyForApply(report);
502
+ }
503
+ assertBindMountsExist(path.dirname(options.envFile));
504
+ if (!options.dryRun) {
505
+ try {
506
+ const { runPreflight } = await import("./preflight-3KY5JETE.js");
507
+ const preflightReport = runPreflight({
508
+ composeFile: options.composeFile,
509
+ envFile: options.envFile
510
+ });
511
+ if (!preflightReport.passed) {
512
+ const failures = preflightReport.results.filter((r) => r.status === "fail").map((r) => `${r.check}: ${r.message}`);
513
+ throw new Error(`Preflight blocked deploy:
514
+ - ${failures.join("\n- ")}`);
515
+ }
516
+ console.log("[stack-update] \u2713 Preflight passed");
517
+ } catch (preflightErr) {
518
+ if (preflightErr instanceof Error && preflightErr.message.startsWith("Preflight blocked")) {
519
+ throw preflightErr;
520
+ }
521
+ console.warn("[stack-update] Preflight check skipped (module not available)");
522
+ }
523
+ }
524
+ const hubUrl = options.monitorHubUrl || parseEnv(readFileSync(options.envFile, "utf8")).get("MONITOR_HUB_URL") || "";
525
+ const pairLicense = options.licenseKey || process.env.EXE_LICENSE_KEY || loadLicense() || "";
526
+ const pairDomain = options.domain || process.env.EXE_STACK_DOMAIN || process.env.CUSTOMER_DOMAIN || "";
527
+ if (hubUrl && pairLicense && pairDomain) {
528
+ const envBefore = readFileSync(options.envFile, "utf8");
529
+ const hasPlaceholder = /CHANGEME/.test(parseEnv(envBefore).get("MONITOR_AGENT_TOKEN") ?? "");
530
+ if (hasPlaceholder) {
531
+ const pair = options.pairMonitor ? options.pairMonitor(hubUrl, pairLicense, pairDomain, options.envFile) : pairMonitorAgent(hubUrl, pairLicense, pairDomain, options.envFile);
532
+ const result = await pair;
533
+ if (result.paired) {
534
+ console.log(`[stack-update] Monitor agent paired: ${result.systemName}`);
535
+ } else {
536
+ console.warn(`[stack-update] Monitor pairing skipped: ${result.error}`);
537
+ }
538
+ }
539
+ }
540
+ const manifest = await loadStackManifest(options.manifestRef, options.fetchText, options.manifestPublicKey, options.manifestAuthToken);
541
+ const envRaw = readFileSync(options.envFile, "utf8");
542
+ const plan = createStackUpdatePlan(manifest, envRaw, options.targetVersion);
543
+ assertBreakingChangesAllowed(plan, options.allowedBreakingChangeIds ?? []);
544
+ assertDeploymentScopeAllowed(plan, options.deploymentPersona ?? "customer");
545
+ const plannedEnvRaw = patchEnv(envRaw, Object.fromEntries(plan.changes.map((c) => [c.key, c.after])));
546
+ const composeRaw = readFileSync(options.composeFile, "utf8");
547
+ assertProductionDeployGate(plan, plannedEnvRaw, composeRaw, {
548
+ breakGlassReason: options.breakGlassReason,
549
+ breakGlassAuditFile: options.breakGlassAuditFile,
550
+ now,
551
+ envFile: options.envFile
552
+ });
553
+ const lockFile = options.lockFile ?? path.join(path.dirname(options.envFile), ".exe-stack-lock.json");
554
+ const previousVersion = readCurrentStackVersion(lockFile);
555
+ const containersRunning = plan.changes.length === 0 ? areStackContainersRunning(options.composeFile, options.envFile) : true;
556
+ if (options.dryRun || plan.changes.length === 0 && containersRunning) {
557
+ return { status: "planned", targetVersion: plan.targetVersion, changes: plan.changes, lockFile };
558
+ }
559
+ await postDeployAudit(options, "started", plan.targetVersion, previousVersion);
560
+ try {
561
+ const { preDeployBackup } = await import("./bin/vps-backup.js");
562
+ preDeployBackup(plan.targetVersion);
563
+ } catch {
564
+ }
565
+ const stackDir = path.dirname(options.envFile);
566
+ const backupDir = path.join(stackDir, ".exe-stack-backups");
567
+ mkdirSync(backupDir, { recursive: true });
568
+ const stamp = now().toISOString().replace(/[:.]/g, "-");
569
+ const updateBackupDir = path.join(backupDir, `pre-update-${stamp}`);
570
+ mkdirSync(updateBackupDir, { recursive: true });
571
+ const backupEnvFile = path.join(updateBackupDir, "env.bak");
572
+ writeFileSync(backupEnvFile, envRaw, { mode: 384 });
573
+ const protectedFiles = ["gateway.json", "branding.json"];
574
+ for (const f of protectedFiles) {
575
+ const src = path.join(stackDir, f);
576
+ try {
577
+ if (existsSync(src)) {
578
+ copyFileSync(src, path.join(updateBackupDir, f));
579
+ }
580
+ } catch {
581
+ }
582
+ }
583
+ const cfDir = path.join(stackDir, "cloudflared");
584
+ try {
585
+ if (existsSync(cfDir)) {
586
+ const cfBackup = path.join(updateBackupDir, "cloudflared");
587
+ mkdirSync(cfBackup, { recursive: true });
588
+ for (const f of readdirSync(cfDir)) {
589
+ copyFileSync(path.join(cfDir, f), path.join(cfBackup, f));
590
+ }
591
+ }
592
+ } catch {
593
+ }
594
+ console.log(`[stack-update] Config backed up to ${updateBackupDir}`);
595
+ try {
596
+ const { spawnSync: sp } = await import("child_process");
597
+ const backupSh = path.join(stackDir, "backup.sh");
598
+ if (existsSync(backupSh)) {
599
+ console.log("[stack-update] Uploading pre-update snapshot to R2...");
600
+ const r2Result = sp("bash", [backupSh, "--upload-r2"], {
601
+ encoding: "utf8",
602
+ timeout: 3e5,
603
+ // 5 min max
604
+ cwd: stackDir,
605
+ stdio: ["pipe", "pipe", "pipe"]
606
+ });
607
+ if (r2Result.status === 0) {
608
+ console.log("[stack-update] \u2713 Pre-update snapshot uploaded to R2");
609
+ } else {
610
+ console.warn("[stack-update] R2 upload failed (non-blocking) \u2014 local backup preserved");
611
+ }
612
+ }
613
+ } catch {
614
+ }
615
+ const updates = Object.fromEntries(plan.changes.map((c) => [c.key, c.after]));
616
+ const patched = patchEnv(envRaw, updates);
617
+ const tmp = `${options.envFile}.tmp-${process.pid}`;
618
+ writeFileSync(tmp, patched, { mode: 384 });
619
+ renameSync(tmp, options.envFile);
620
+ const composeArgs = ["compose", "--file", options.composeFile, "--env-file", options.envFile];
621
+ let registryForLogout;
622
+ try {
623
+ const creds = await fetchImageCredentials(options);
624
+ if (creds) {
625
+ (options.dockerLogin ?? defaultDockerLogin)(creds);
626
+ registryForLogout = creds.registry;
627
+ }
628
+ exec("docker", [...composeArgs, "pull"]);
629
+ for (const [serviceName, service] of Object.entries(plan.release.services)) {
630
+ if (!service.migrations?.command) continue;
631
+ const composeServiceName = service.composeService ?? serviceName;
632
+ console.log(`[stack-update] Running migrations for ${composeServiceName}: ${service.migrations.command}`);
633
+ try {
634
+ const migrationArgs = service.migrations.command.split(/\s+/);
635
+ exec("docker", [
636
+ ...composeArgs,
637
+ "run",
638
+ "--rm",
639
+ "--no-deps",
640
+ composeServiceName,
641
+ ...migrationArgs
642
+ ]);
643
+ console.log(`[stack-update] \u2713 Migrations for ${composeServiceName} completed`);
644
+ } catch (migErr) {
645
+ const reason = migErr instanceof Error ? migErr.message : String(migErr);
646
+ console.error(`[stack-update] \u2717 Migration failed for ${composeServiceName}: ${reason}`);
647
+ throw new Error(`Migration failed for ${composeServiceName} \u2014 aborting update. Fix the migration and retry. Error: ${reason}`);
648
+ }
649
+ }
650
+ const RESTART_ORDER = [
651
+ "exe-db",
652
+ // data layer — must be healthy before apps
653
+ "clickhouse",
654
+ "redis",
655
+ "exed",
656
+ // daemon — has its own health endpoint
657
+ "exe-crm",
658
+ // CRM app
659
+ "exe-crm-worker",
660
+ // CRM background worker
661
+ "exe-gateway",
662
+ // gateway — WhatsApp connections
663
+ "exe-wiki"
664
+ // wiki
665
+ ];
666
+ const preSnapshot = spawnSync("docker", ["ps", "--format", "json"], { encoding: "utf8", timeout: 1e4 });
667
+ const preContainerCount = preSnapshot.stdout?.split("\n").filter(Boolean).length ?? 0;
668
+ console.log(`[stack-update] Pre-update snapshot: ${preContainerCount} containers`);
669
+ for (const service of RESTART_ORDER) {
670
+ const psResult = spawnSync("docker", [...composeArgs, "ps", "--quiet", service], {
671
+ encoding: "utf8",
672
+ stdio: ["pipe", "pipe", "pipe"],
673
+ timeout: 1e4
674
+ });
675
+ if (psResult.status !== 0) continue;
676
+ exec("docker", [...composeArgs, "up", "-d", "--no-deps", service]);
677
+ const maxWait = 60;
678
+ let healthy = false;
679
+ for (let i = 0; i < maxWait; i++) {
680
+ const inspectResult = spawnSync(
681
+ "docker",
682
+ ["inspect", "--format", "{{.State.Health.Status}}", service],
683
+ { encoding: "utf8", timeout: 5e3 }
684
+ );
685
+ const status = inspectResult.stdout?.trim();
686
+ if (status === "healthy") {
687
+ healthy = true;
688
+ break;
689
+ }
690
+ if (status === "" || inspectResult.status !== 0) {
691
+ healthy = true;
692
+ break;
693
+ }
694
+ await new Promise((r) => setTimeout(r, 1e3));
695
+ }
696
+ if (!healthy) {
697
+ throw new Error(`Service ${service} failed health check after ${maxWait}s \u2014 aborting update`);
698
+ }
699
+ console.log(`[stack-update] \u2713 ${service} restarted and healthy`);
700
+ }
701
+ const postSnapshot = spawnSync("docker", ["ps", "--format", "json"], { encoding: "utf8", timeout: 1e4 });
702
+ const postContainerCount = postSnapshot.stdout?.split("\n").filter(Boolean).length ?? 0;
703
+ console.log(`[stack-update] Post-update snapshot: ${postContainerCount} containers (was ${preContainerCount})`);
704
+ if (postContainerCount < preContainerCount) {
705
+ console.warn(`[stack-update] \u26A0 Container count dropped from ${preContainerCount} to ${postContainerCount}`);
706
+ }
707
+ await verifyReleaseHealth(plan.release, options.healthRetries ?? 12, options.healthDelayMs ?? 5e3);
708
+ try {
709
+ const { runHealthGate, logResult } = await import("./bin/vps-health-gate.js");
710
+ const gateResult = await runHealthGate();
711
+ logResult(gateResult);
712
+ if (!gateResult.passed) {
713
+ const failed = gateResult.results.filter((r) => r.status === "fail").map((r) => r.check);
714
+ throw new Error(`Health gate failed: ${failed.join(", ")}`);
715
+ }
716
+ console.log("[stack-update] \u2713 Health gate passed");
717
+ } catch (gateErr) {
718
+ if (gateErr instanceof Error && gateErr.message.startsWith("Health gate failed")) {
719
+ throw gateErr;
720
+ }
721
+ console.warn("[stack-update] Health gate check skipped (module not available)");
722
+ }
723
+ assertVolumesIntact();
724
+ try {
725
+ const { runVerifyStack } = await import("./bin/verify-stack.js");
726
+ const verifyReport = await runVerifyStack({ composeFile: options.composeFile, envFile: options.envFile });
727
+ for (const r of verifyReport.results) {
728
+ if (r.status === "fail") console.warn(`[verify-stack] FAIL: ${r.check}: ${r.message}`);
729
+ }
730
+ } catch {
731
+ }
732
+ try {
733
+ const { runHealthGate, logResult: logHealthResult } = await import("./bin/vps-health-gate.js");
734
+ const healthResult = await runHealthGate();
735
+ logHealthResult(healthResult);
736
+ if (!healthResult.passed) {
737
+ const failedChecks = healthResult.results.filter((r) => !r.passed).map((r) => r.check).join(", ");
738
+ console.warn(`[stack-update] \u26A0 Health gate failed: ${failedChecks}. Stack may need attention.`);
739
+ }
740
+ } catch {
741
+ }
742
+ writeFileSync(lockFile, JSON.stringify({ stackVersion: plan.targetVersion, updatedAt: now().toISOString(), backupEnvFile, services: plan.release.services }, null, 2) + "\n");
743
+ await postDeployAudit(options, "success", plan.targetVersion, previousVersion, void 0, { changes: plan.changes.length });
744
+ return { status: "updated", targetVersion: plan.targetVersion, changes: plan.changes, backupEnvFile, lockFile };
745
+ } catch (err) {
746
+ writeFileSync(options.envFile, envRaw, { mode: 384 });
747
+ try {
748
+ exec("docker", [...composeArgs, "up", "-d"]);
749
+ } catch {
750
+ }
751
+ const reason = err instanceof Error ? err.message : String(err);
752
+ await postDeployAudit(options, "failed", plan.targetVersion, previousVersion, reason, { rollbackAttempted: true });
753
+ throw new Error(`Stack update failed and rollback was attempted: ${reason}`);
754
+ } finally {
755
+ if (registryForLogout) {
756
+ try {
757
+ (options.dockerLogout ?? defaultDockerLogout)(registryForLogout);
758
+ } catch {
759
+ }
760
+ }
761
+ }
762
+ }
763
+ async function fetchImageCredentials(options) {
764
+ if (!options.imageCredentialsUrl) return null;
765
+ const res = await fetch(options.imageCredentialsUrl, {
766
+ method: "POST",
767
+ headers: {
768
+ "content-type": "application/json",
769
+ ...options.manifestAuthToken ? { authorization: `Bearer ${options.manifestAuthToken}` } : {}
770
+ },
771
+ body: JSON.stringify({ deviceId: options.deviceId, licenseKey: options.licenseKey })
772
+ });
773
+ if (!res.ok) throw new Error(`Failed to fetch image credentials: HTTP ${res.status}`);
774
+ return await res.json();
775
+ }
776
+ function readCurrentStackVersion(lockFile) {
777
+ if (!existsSync(lockFile)) return void 0;
778
+ try {
779
+ const parsed = JSON.parse(readFileSync(lockFile, "utf8"));
780
+ return parsed.stackVersion;
781
+ } catch {
782
+ return void 0;
783
+ }
784
+ }
785
+ function listAvailableVersions(manifest, currentVersion) {
786
+ const versions = Object.keys(manifest.stacks).sort((a, b) => compareVersions(a, b));
787
+ return versions.map((v) => ({
788
+ version: v,
789
+ releasedAt: manifest.stacks[v]?.releasedAt,
790
+ notes: manifest.stacks[v]?.notes,
791
+ isCurrent: v === currentVersion,
792
+ isLatest: v === manifest.latest
793
+ }));
794
+ }
795
+ function compareVersions(a, b) {
796
+ const pa = a.split(".").map(Number);
797
+ const pb = b.split(".").map(Number);
798
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
799
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
800
+ if (diff !== 0) return diff;
801
+ }
802
+ return 0;
803
+ }
804
+ async function postDeployAudit(options, status, stackVersion, previousVersion, error, metadata) {
805
+ if (!options.auditUrl) return;
806
+ const postJson = options.postJson ?? defaultPostJson;
807
+ try {
808
+ await postJson(options.auditUrl, {
809
+ stackVersion,
810
+ previousVersion,
811
+ status,
812
+ error,
813
+ metadata,
814
+ deviceId: options.deviceId,
815
+ licenseKey: options.licenseKey
816
+ }, options.manifestAuthToken);
817
+ } catch (err) {
818
+ const reason = err instanceof Error ? err.message : String(err);
819
+ console.warn(`[stack-update] deploy audit failed: ${reason}`);
820
+ }
821
+ }
822
+ async function verifyReleaseHealth(release, retries, delayMs) {
823
+ for (const [serviceName, service] of Object.entries(release.services)) {
824
+ if (!service.healthUrl) continue;
825
+ await waitForHttpOk(service.healthUrl, retries, delayMs, serviceName);
826
+ }
827
+ }
828
+ async function waitForHttpOk(url, retries, delayMs, label) {
829
+ let last = "";
830
+ for (let i = 0; i < retries; i++) {
831
+ try {
832
+ const status = await httpStatus(url);
833
+ if (status >= 200 && status < 300) return;
834
+ last = `HTTP ${status}`;
835
+ } catch (err) {
836
+ last = err instanceof Error ? err.message : String(err);
837
+ }
838
+ if (i < retries - 1) await new Promise((resolve) => setTimeout(resolve, delayMs));
839
+ }
840
+ throw new Error(`Health check failed for ${label} (${url}): ${last}`);
841
+ }
842
+ function httpStatus(urlString) {
843
+ return new Promise((resolve, reject) => {
844
+ const url = new URL(urlString);
845
+ const mod = url.protocol === "https:" ? https : http;
846
+ const req = mod.request(url, { method: "GET", timeout: 5e3 }, (res) => {
847
+ res.resume();
848
+ resolve(res.statusCode ?? 0);
849
+ });
850
+ req.on("timeout", () => req.destroy(new Error("timeout")));
851
+ req.on("error", reject);
852
+ req.end();
853
+ });
854
+ }
855
+ function defaultExec(cmd, args, opts) {
856
+ execFileSync(cmd, args, { stdio: "inherit", cwd: opts?.cwd });
857
+ }
858
+ function defaultDockerLogin(creds) {
859
+ execFileSync("docker", ["login", creds.registry, "-u", creds.username, "--password-stdin"], {
860
+ input: creds.password,
861
+ stdio: ["pipe", "inherit", "inherit"]
862
+ });
863
+ }
864
+ function defaultDockerLogout(registry) {
865
+ execFileSync("docker", ["logout", registry], { stdio: "ignore" });
866
+ }
867
+ async function defaultFetchText(ref, authToken) {
868
+ const res = await fetch(ref, {
869
+ headers: authToken ? { authorization: `Bearer ${authToken}` } : void 0
870
+ });
871
+ if (!res.ok) throw new Error(`Failed to fetch ${ref}: HTTP ${res.status}`);
872
+ return res.text();
873
+ }
874
+ async function defaultPostJson(url, body, authToken) {
875
+ const res = await fetch(url, {
876
+ method: "POST",
877
+ headers: {
878
+ "content-type": "application/json",
879
+ ...authToken ? { authorization: `Bearer ${authToken}` } : {}
880
+ },
881
+ body: JSON.stringify(body)
882
+ });
883
+ if (!res.ok) throw new Error(`Failed to POST ${url}: HTTP ${res.status}`);
884
+ }
885
+ function defaultStackPaths() {
886
+ const cwdCompose = path.resolve("docker-compose.yml");
887
+ const cwdEnv = path.resolve(".env");
888
+ const packagedManifest = path.join(resolvePackageRoot(), "deploy", "stack-manifests", "v0.9.json");
889
+ const manifestRef = process.env.EXE_STACK_MANIFEST || (existsSync(packagedManifest) ? packagedManifest : "https://api.askexe.com/stack-manifest.json");
890
+ return {
891
+ composeFile: process.env.EXE_STACK_COMPOSE_FILE || (existsSync(cwdCompose) ? cwdCompose : "/opt/exe-stack/docker-compose.yml"),
892
+ envFile: process.env.EXE_STACK_ENV_FILE || (existsSync(cwdEnv) ? cwdEnv : "/opt/exe-stack/.env"),
893
+ manifestRef,
894
+ // Only call api.askexe.com if explicitly configured or if a remote manifest was requested.
895
+ // Packaged manifests keep cold-start installs unblocked even before update-service entitlements are provisioned.
896
+ auditUrl: process.env.EXE_STACK_AUDIT_URL || (/^https?:\/\//.test(manifestRef) ? "https://api.askexe.com/v1/deploy-audits" : void 0),
897
+ imageCredentialsUrl: process.env.EXE_STACK_IMAGE_CREDENTIALS_URL || (/^https?:\/\//.test(manifestRef) ? "https://api.askexe.com/v1/image-credentials" : void 0),
898
+ // License key IS the auth token for api.askexe.com — no separate update token needed.
899
+ // EXE_STACK_UPDATE_TOKEN kept as legacy fallback during migration.
900
+ manifestAuthToken: process.env.EXE_LICENSE_KEY || loadLicense() || process.env.EXE_STACK_UPDATE_TOKEN || void 0,
901
+ manifestPublicKey: loadDefaultPublicKey()
902
+ };
903
+ }
904
+ function loadDefaultPublicKey() {
905
+ if (process.env.EXE_STACK_PUBLIC_KEY) return process.env.EXE_STACK_PUBLIC_KEY;
906
+ if (process.env.EXE_STACK_PUBLIC_KEY_FILE && existsSync(process.env.EXE_STACK_PUBLIC_KEY_FILE)) {
907
+ return readFileSync(process.env.EXE_STACK_PUBLIC_KEY_FILE, "utf8");
908
+ }
909
+ return void 0;
910
+ }
911
+
912
+ export {
913
+ canonicalizeStackManifest,
914
+ verifyStackManifestSignature,
915
+ findLatestBackupEnvFile,
916
+ rollbackStackUpdate,
917
+ parseStackManifest,
918
+ loadStackManifest,
919
+ parseEnv,
920
+ patchEnv,
921
+ createStackUpdatePlan,
922
+ collectProductionDeployGateIssues,
923
+ assertDeploymentScopeAllowed,
924
+ assertProductionDeployGate,
925
+ assertBreakingChangesAllowed,
926
+ pairMonitorAgent,
927
+ bootstrapStackHost,
928
+ assertHostReadyForApply,
929
+ runStackUpdate,
930
+ readCurrentStackVersion,
931
+ listAvailableVersions,
932
+ verifyReleaseHealth,
933
+ defaultStackPaths
934
+ };