@jungjaehoon/mama-os 0.78.0 → 0.79.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 (288) hide show
  1. package/CHANGELOG.md +311 -0
  2. package/README.md +38 -24
  3. package/db/migrations/002-model-runs-parent-index.sql +11 -0
  4. package/db/migrations/003-upkeep-settled.sql +15 -0
  5. package/dist/api/google-read-actions.d.ts +30 -0
  6. package/dist/api/google-read-actions.d.ts.map +1 -0
  7. package/dist/api/google-read-actions.js +256 -0
  8. package/dist/api/google-read-actions.js.map +1 -0
  9. package/dist/api/record-actions.d.ts.map +1 -1
  10. package/dist/api/record-actions.js +23 -5
  11. package/dist/api/record-actions.js.map +1 -1
  12. package/dist/api/report-handler.d.ts +0 -6
  13. package/dist/api/report-handler.d.ts.map +1 -1
  14. package/dist/api/report-handler.js +0 -54
  15. package/dist/api/report-handler.js.map +1 -1
  16. package/dist/api/security-events.d.ts +1 -1
  17. package/dist/api/security-events.d.ts.map +1 -1
  18. package/dist/api/security-events.js.map +1 -1
  19. package/dist/api/source-actions.d.ts +9 -0
  20. package/dist/api/source-actions.d.ts.map +1 -1
  21. package/dist/api/source-actions.js +49 -0
  22. package/dist/api/source-actions.js.map +1 -1
  23. package/dist/api/viewer-request-security.d.ts +14 -1
  24. package/dist/api/viewer-request-security.d.ts.map +1 -1
  25. package/dist/api/viewer-request-security.js +26 -3
  26. package/dist/api/viewer-request-security.js.map +1 -1
  27. package/dist/api/viewer-server.d.ts +7 -0
  28. package/dist/api/viewer-server.d.ts.map +1 -1
  29. package/dist/api/viewer-server.js +32 -0
  30. package/dist/api/viewer-server.js.map +1 -1
  31. package/dist/api/work-actions.d.ts +19 -2
  32. package/dist/api/work-actions.d.ts.map +1 -1
  33. package/dist/api/work-actions.js +15 -0
  34. package/dist/api/work-actions.js.map +1 -1
  35. package/dist/cli/commands/backup.d.ts +7 -6
  36. package/dist/cli/commands/backup.d.ts.map +1 -1
  37. package/dist/cli/commands/backup.js +44 -7
  38. package/dist/cli/commands/backup.js.map +1 -1
  39. package/dist/cli/commands/control.d.ts +22 -4
  40. package/dist/cli/commands/control.d.ts.map +1 -1
  41. package/dist/cli/commands/control.js +172 -11
  42. package/dist/cli/commands/control.js.map +1 -1
  43. package/dist/cli/commands/daemon.d.ts +13 -2
  44. package/dist/cli/commands/daemon.d.ts.map +1 -1
  45. package/dist/cli/commands/daemon.js +211 -5
  46. package/dist/cli/commands/daemon.js.map +1 -1
  47. package/dist/cli/commands/init-questions.d.ts +98 -0
  48. package/dist/cli/commands/init-questions.d.ts.map +1 -0
  49. package/dist/cli/commands/init-questions.js +420 -0
  50. package/dist/cli/commands/init-questions.js.map +1 -0
  51. package/dist/cli/commands/init-section.d.ts +13 -0
  52. package/dist/cli/commands/init-section.d.ts.map +1 -0
  53. package/dist/cli/commands/init-section.js +375 -0
  54. package/dist/cli/commands/init-section.js.map +1 -0
  55. package/dist/cli/commands/init.d.ts +56 -2
  56. package/dist/cli/commands/init.d.ts.map +1 -1
  57. package/dist/cli/commands/init.js +562 -351
  58. package/dist/cli/commands/init.js.map +1 -1
  59. package/dist/cli/commands/node-move.d.ts +20 -0
  60. package/dist/cli/commands/node-move.d.ts.map +1 -0
  61. package/dist/cli/commands/node-move.js +289 -0
  62. package/dist/cli/commands/node-move.js.map +1 -0
  63. package/dist/cli/commands/register-owner.d.ts +3 -0
  64. package/dist/cli/commands/register-owner.d.ts.map +1 -1
  65. package/dist/cli/commands/register-owner.js +2 -0
  66. package/dist/cli/commands/register-owner.js.map +1 -1
  67. package/dist/cli/commands/restore.d.ts +2 -0
  68. package/dist/cli/commands/restore.d.ts.map +1 -1
  69. package/dist/cli/commands/restore.js +7 -0
  70. package/dist/cli/commands/restore.js.map +1 -1
  71. package/dist/cli/commands/service.d.ts +16 -0
  72. package/dist/cli/commands/service.d.ts.map +1 -1
  73. package/dist/cli/commands/service.js +36 -8
  74. package/dist/cli/commands/service.js.map +1 -1
  75. package/dist/cli/commands/tools.d.ts +1 -1
  76. package/dist/cli/commands/tools.d.ts.map +1 -1
  77. package/dist/cli/commands/tools.js +8 -3
  78. package/dist/cli/commands/tools.js.map +1 -1
  79. package/dist/cli/commands/uninstall.d.ts.map +1 -1
  80. package/dist/cli/commands/uninstall.js +78 -5
  81. package/dist/cli/commands/uninstall.js.map +1 -1
  82. package/dist/cli/commands/update.d.ts +59 -0
  83. package/dist/cli/commands/update.d.ts.map +1 -0
  84. package/dist/cli/commands/update.js +948 -0
  85. package/dist/cli/commands/update.js.map +1 -0
  86. package/dist/cli/index.js +8 -4
  87. package/dist/cli/index.js.map +1 -1
  88. package/dist/cli/launch-files.d.ts +36 -0
  89. package/dist/cli/launch-files.d.ts.map +1 -1
  90. package/dist/cli/launch-files.js +94 -1
  91. package/dist/cli/launch-files.js.map +1 -1
  92. package/dist/cli/prompt.d.ts +2 -1
  93. package/dist/cli/prompt.d.ts.map +1 -1
  94. package/dist/cli/prompt.js +5 -2
  95. package/dist/cli/prompt.js.map +1 -1
  96. package/dist/cli/runtime/backup.d.ts +17 -1
  97. package/dist/cli/runtime/backup.d.ts.map +1 -1
  98. package/dist/cli/runtime/backup.js +32 -4
  99. package/dist/cli/runtime/backup.js.map +1 -1
  100. package/dist/cli/runtime/daemon-control.d.ts +92 -7
  101. package/dist/cli/runtime/daemon-control.d.ts.map +1 -1
  102. package/dist/cli/runtime/daemon-control.js +183 -8
  103. package/dist/cli/runtime/daemon-control.js.map +1 -1
  104. package/dist/cli/runtime/default-roots.d.ts +27 -0
  105. package/dist/cli/runtime/default-roots.d.ts.map +1 -0
  106. package/dist/cli/runtime/default-roots.js +61 -0
  107. package/dist/cli/runtime/default-roots.js.map +1 -0
  108. package/dist/cli/runtime/diagnostics.d.ts +25 -0
  109. package/dist/cli/runtime/diagnostics.d.ts.map +1 -1
  110. package/dist/cli/runtime/diagnostics.js +249 -63
  111. package/dist/cli/runtime/diagnostics.js.map +1 -1
  112. package/dist/cli/runtime/init-state.d.ts +20 -0
  113. package/dist/cli/runtime/init-state.d.ts.map +1 -0
  114. package/dist/cli/runtime/init-state.js +51 -0
  115. package/dist/cli/runtime/init-state.js.map +1 -0
  116. package/dist/cli/runtime/install-kind.d.ts +54 -0
  117. package/dist/cli/runtime/install-kind.d.ts.map +1 -0
  118. package/dist/cli/runtime/install-kind.js +164 -0
  119. package/dist/cli/runtime/install-kind.js.map +1 -0
  120. package/dist/cli/runtime/node-move-journal.d.ts +44 -0
  121. package/dist/cli/runtime/node-move-journal.d.ts.map +1 -0
  122. package/dist/cli/runtime/node-move-journal.js +48 -0
  123. package/dist/cli/runtime/node-move-journal.js.map +1 -0
  124. package/dist/cli/runtime/release-check.d.ts +94 -0
  125. package/dist/cli/runtime/release-check.d.ts.map +1 -0
  126. package/dist/cli/runtime/release-check.js +316 -0
  127. package/dist/cli/runtime/release-check.js.map +1 -0
  128. package/dist/cli/runtime/setup-page.d.ts +57 -0
  129. package/dist/cli/runtime/setup-page.d.ts.map +1 -0
  130. package/dist/cli/runtime/setup-page.js +266 -0
  131. package/dist/cli/runtime/setup-page.js.map +1 -0
  132. package/dist/cli/runtime/source-tools.d.ts +60 -0
  133. package/dist/cli/runtime/source-tools.d.ts.map +1 -0
  134. package/dist/cli/runtime/source-tools.js +170 -0
  135. package/dist/cli/runtime/source-tools.js.map +1 -0
  136. package/dist/cli/runtime/telegram-pairing.d.ts +73 -0
  137. package/dist/cli/runtime/telegram-pairing.d.ts.map +1 -0
  138. package/dist/cli/runtime/telegram-pairing.js +184 -0
  139. package/dist/cli/runtime/telegram-pairing.js.map +1 -0
  140. package/dist/cli/runtime/update-journal.d.ts +135 -0
  141. package/dist/cli/runtime/update-journal.d.ts.map +1 -0
  142. package/dist/cli/runtime/update-journal.js +144 -0
  143. package/dist/cli/runtime/update-journal.js.map +1 -0
  144. package/dist/cli/runtime/update-progress.d.ts +95 -0
  145. package/dist/cli/runtime/update-progress.d.ts.map +1 -0
  146. package/dist/cli/runtime/update-progress.js +154 -0
  147. package/dist/cli/runtime/update-progress.js.map +1 -0
  148. package/dist/cli/runtime/update-request.d.ts +58 -0
  149. package/dist/cli/runtime/update-request.d.ts.map +1 -0
  150. package/dist/cli/runtime/update-request.js +172 -0
  151. package/dist/cli/runtime/update-request.js.map +1 -0
  152. package/dist/connectors/calendar/index.d.ts +5 -1
  153. package/dist/connectors/calendar/index.d.ts.map +1 -1
  154. package/dist/connectors/calendar/index.js +19 -28
  155. package/dist/connectors/calendar/index.js.map +1 -1
  156. package/dist/connectors/chatwork/index.d.ts.map +1 -1
  157. package/dist/connectors/chatwork/index.js +7 -3
  158. package/dist/connectors/chatwork/index.js.map +1 -1
  159. package/dist/connectors/config-loader.d.ts +5 -0
  160. package/dist/connectors/config-loader.d.ts.map +1 -1
  161. package/dist/connectors/config-loader.js +8 -0
  162. package/dist/connectors/config-loader.js.map +1 -1
  163. package/dist/connectors/discord/index.d.ts.map +1 -1
  164. package/dist/connectors/discord/index.js +4 -3
  165. package/dist/connectors/discord/index.js.map +1 -1
  166. package/dist/connectors/drive/index.d.ts.map +1 -1
  167. package/dist/connectors/drive/index.js +15 -13
  168. package/dist/connectors/drive/index.js.map +1 -1
  169. package/dist/connectors/framework/gws-utils.d.ts +62 -0
  170. package/dist/connectors/framework/gws-utils.d.ts.map +1 -1
  171. package/dist/connectors/framework/gws-utils.js +133 -3
  172. package/dist/connectors/framework/gws-utils.js.map +1 -1
  173. package/dist/connectors/framework/types.d.ts +24 -0
  174. package/dist/connectors/framework/types.d.ts.map +1 -1
  175. package/dist/connectors/framework/types.js +9 -0
  176. package/dist/connectors/framework/types.js.map +1 -1
  177. package/dist/connectors/gmail/index.d.ts.map +1 -1
  178. package/dist/connectors/gmail/index.js +14 -13
  179. package/dist/connectors/gmail/index.js.map +1 -1
  180. package/dist/connectors/index.d.ts +4 -4
  181. package/dist/connectors/index.d.ts.map +1 -1
  182. package/dist/connectors/index.js +4 -10
  183. package/dist/connectors/index.js.map +1 -1
  184. package/dist/connectors/notion/index.d.ts.map +1 -1
  185. package/dist/connectors/notion/index.js +5 -3
  186. package/dist/connectors/notion/index.js.map +1 -1
  187. package/dist/connectors/sheets/index.d.ts.map +1 -1
  188. package/dist/connectors/sheets/index.js +17 -12
  189. package/dist/connectors/sheets/index.js.map +1 -1
  190. package/dist/connectors/slack/index.d.ts.map +1 -1
  191. package/dist/connectors/slack/index.js +9 -3
  192. package/dist/connectors/slack/index.js.map +1 -1
  193. package/dist/connectors/telegram/index.d.ts.map +1 -1
  194. package/dist/connectors/telegram/index.js +4 -3
  195. package/dist/connectors/telegram/index.js.map +1 -1
  196. package/dist/connectors/trello/index.d.ts +1 -1
  197. package/dist/connectors/trello/index.d.ts.map +1 -1
  198. package/dist/connectors/trello/index.js +10 -5
  199. package/dist/connectors/trello/index.js.map +1 -1
  200. package/dist/gateways/discord.d.ts +6 -0
  201. package/dist/gateways/discord.d.ts.map +1 -1
  202. package/dist/gateways/discord.js +38 -27
  203. package/dist/gateways/discord.js.map +1 -1
  204. package/dist/gateways/slack.d.ts +6 -0
  205. package/dist/gateways/slack.d.ts.map +1 -1
  206. package/dist/gateways/slack.js +38 -27
  207. package/dist/gateways/slack.js.map +1 -1
  208. package/dist/gateways/telegram-commands.d.ts +124 -0
  209. package/dist/gateways/telegram-commands.d.ts.map +1 -0
  210. package/dist/gateways/telegram-commands.js +131 -0
  211. package/dist/gateways/telegram-commands.js.map +1 -0
  212. package/dist/gateways/telegram.d.ts +20 -0
  213. package/dist/gateways/telegram.d.ts.map +1 -1
  214. package/dist/gateways/telegram.js +138 -34
  215. package/dist/gateways/telegram.js.map +1 -1
  216. package/dist/gateways/turn-contract.d.ts +11 -0
  217. package/dist/gateways/turn-contract.d.ts.map +1 -1
  218. package/dist/runtime/action-surface.d.ts +1 -1
  219. package/dist/runtime/action-surface.d.ts.map +1 -1
  220. package/dist/runtime/action-surface.js +10 -0
  221. package/dist/runtime/action-surface.js.map +1 -1
  222. package/dist/runtime/backend-security.d.ts +23 -2
  223. package/dist/runtime/backend-security.d.ts.map +1 -1
  224. package/dist/runtime/backend-security.js +50 -5
  225. package/dist/runtime/backend-security.js.map +1 -1
  226. package/dist/runtime/member-paths.d.ts +15 -7
  227. package/dist/runtime/member-paths.d.ts.map +1 -1
  228. package/dist/runtime/member-paths.js +64 -12
  229. package/dist/runtime/member-paths.js.map +1 -1
  230. package/dist/runtime/member-session.d.ts +4 -0
  231. package/dist/runtime/member-session.d.ts.map +1 -1
  232. package/dist/runtime/member-session.js +23 -2
  233. package/dist/runtime/member-session.js.map +1 -1
  234. package/dist/runtime/native-session.d.ts +2 -0
  235. package/dist/runtime/native-session.d.ts.map +1 -1
  236. package/dist/runtime/native-session.js +1 -0
  237. package/dist/runtime/native-session.js.map +1 -1
  238. package/dist/runtime/owner-runtime.d.ts +63 -1
  239. package/dist/runtime/owner-runtime.d.ts.map +1 -1
  240. package/dist/runtime/owner-runtime.js +180 -3
  241. package/dist/runtime/owner-runtime.js.map +1 -1
  242. package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
  243. package/dist/runtime/owner-system-prompt.js +0 -1
  244. package/dist/runtime/owner-system-prompt.js.map +1 -1
  245. package/dist/runtime/principal-access.d.ts +2 -0
  246. package/dist/runtime/principal-access.d.ts.map +1 -1
  247. package/dist/runtime/principal-access.js +7 -0
  248. package/dist/runtime/principal-access.js.map +1 -1
  249. package/dist/runtime/principal-sessions.d.ts.map +1 -1
  250. package/dist/runtime/principal-sessions.js +8 -1
  251. package/dist/runtime/principal-sessions.js.map +1 -1
  252. package/dist/runtime/record-orders.d.ts.map +1 -1
  253. package/dist/runtime/record-orders.js +3 -29
  254. package/dist/runtime/record-orders.js.map +1 -1
  255. package/dist/runtime/report-scheduler.d.ts +9 -2
  256. package/dist/runtime/report-scheduler.d.ts.map +1 -1
  257. package/dist/runtime/report-scheduler.js +37 -10
  258. package/dist/runtime/report-scheduler.js.map +1 -1
  259. package/dist/runtime/stimulus-delivery.d.ts +3 -0
  260. package/dist/runtime/stimulus-delivery.d.ts.map +1 -1
  261. package/dist/runtime/stimulus-delivery.js +19 -3
  262. package/dist/runtime/stimulus-delivery.js.map +1 -1
  263. package/dist/runtime/tools-paths.d.ts +7 -0
  264. package/dist/runtime/tools-paths.d.ts.map +1 -1
  265. package/dist/runtime/tools-paths.js +11 -5
  266. package/dist/runtime/tools-paths.js.map +1 -1
  267. package/dist/runtime/upkeep-close.d.ts +17 -0
  268. package/dist/runtime/upkeep-close.d.ts.map +1 -0
  269. package/dist/runtime/upkeep-close.js +127 -0
  270. package/dist/runtime/upkeep-close.js.map +1 -0
  271. package/dist/runtime/upkeep-orders.d.ts +124 -0
  272. package/dist/runtime/upkeep-orders.d.ts.map +1 -0
  273. package/dist/runtime/upkeep-orders.js +310 -0
  274. package/dist/runtime/upkeep-orders.js.map +1 -0
  275. package/dist/runtime/upkeep-reconcile.d.ts +25 -0
  276. package/dist/runtime/upkeep-reconcile.d.ts.map +1 -0
  277. package/dist/runtime/upkeep-reconcile.js +231 -0
  278. package/dist/runtime/upkeep-reconcile.js.map +1 -0
  279. package/dist/runtime/upkeep-writeback.d.ts +22 -0
  280. package/dist/runtime/upkeep-writeback.d.ts.map +1 -0
  281. package/dist/runtime/upkeep-writeback.js +203 -0
  282. package/dist/runtime/upkeep-writeback.js.map +1 -0
  283. package/dist/utils/untrusted-content.d.ts.map +1 -1
  284. package/dist/utils/untrusted-content.js +8 -1
  285. package/dist/utils/untrusted-content.js.map +1 -1
  286. package/install/install.sh +457 -0
  287. package/install/node.json +9 -0
  288. package/package.json +3 -4
@@ -0,0 +1,948 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.npmEnvironment = npmEnvironment;
7
+ exports.runUpdate = runUpdate;
8
+ exports.takeLock = takeLock;
9
+ exports.doctorFailures = doctorFailures;
10
+ const node_child_process_1 = require("node:child_process");
11
+ const node_crypto_1 = require("node:crypto");
12
+ const node_fs_1 = require("node:fs");
13
+ const node_os_1 = require("node:os");
14
+ const node_path_1 = require("node:path");
15
+ const node_util_1 = require("node:util");
16
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
17
+ const db_manager_1 = require("@jungjaehoon/mama-core/db-manager");
18
+ const viewer_server_js_1 = require("../../api/viewer-server.js");
19
+ const package_version_js_1 = require("../../package-version.js");
20
+ const backend_security_js_1 = require("../../runtime/backend-security.js");
21
+ const config_js_1 = require("../../runtime/config.js");
22
+ const migrations_source_js_1 = require("../../storage/migrations-source.js");
23
+ const toolkit_state_js_1 = require("../../tools/toolkit-state.js");
24
+ const launch_files_js_1 = require("../launch-files.js");
25
+ const prompt_js_1 = require("../prompt.js");
26
+ const daemon_control_js_1 = require("../runtime/daemon-control.js");
27
+ const diagnostics_js_1 = require("../runtime/diagnostics.js");
28
+ const backend_probe_js_1 = require("../runtime/backend-probe.js");
29
+ const node_move_journal_js_1 = require("../runtime/node-move-journal.js");
30
+ const install_kind_js_1 = require("../runtime/install-kind.js");
31
+ const release_check_js_1 = require("../runtime/release-check.js");
32
+ const backup_js_1 = require("../runtime/backup.js");
33
+ const update_journal_js_1 = require("../runtime/update-journal.js");
34
+ const update_progress_js_1 = require("../runtime/update-progress.js");
35
+ const backup_js_2 = require("./backup.js");
36
+ const node_move_js_1 = require("./node-move.js");
37
+ const control_js_1 = require("./control.js");
38
+ const init_js_1 = require("./init.js");
39
+ const restore_js_1 = require("./restore.js");
40
+ const service_js_1 = require("./service.js");
41
+ const tools_js_1 = require("./tools.js");
42
+ /*
43
+ * `mama update` (docs/development/work/update.md, PR 3) runs as two programs. The parent is the
44
+ * old code: everything it uses is imported above, statically, because the CLI's commands load
45
+ * lazily and from step 8 on its own files are renamed; from then it runs loaded code and Node's
46
+ * built-ins only (better-sqlite3's binding is loaded when step 1 takes update.lock). The child,
47
+ * `update --continue`, is the new code; a rollback finishes in the previous version's
48
+ * `update --rollback --continue`.
49
+ */
50
+ const USAGE = 'Usage: mama update [<version>] [--dry-run] | mama update --rollback | mama update --node <folder>';
51
+ /** How long the new version may take to boot, as `mama init` waits (init.ts). */
52
+ const BOOT_WAIT_MS = 600_000;
53
+ /** `npm install` and `npm rebuild` download or build the native modules; the rest answer quickly. */
54
+ const NPM_INSTALL_TIMEOUT_MS = 15 * 60_000;
55
+ const NPM_TIMEOUT_MS = 60_000;
56
+ function message(error) {
57
+ return error instanceof Error ? error.message : String(error);
58
+ }
59
+ /** The npm beside the pinned Node, run with that Node first on PATH: npm's `#!/usr/bin/env node`
60
+ * and the install scripts that pick native binaries then use the Node the daemon runs. */
61
+ async function defaultNpm(args, nodePath, env) {
62
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, node_path_1.join)((0, node_path_1.dirname)(nodePath), 'npm'), args, {
63
+ env: npmEnvironment(env, nodePath),
64
+ // A rebuild (the Node move's step 2) downloads or builds the native modules as an install does.
65
+ timeout: args[0] === 'install' || args[0] === 'rebuild' ? NPM_INSTALL_TIMEOUT_MS : NPM_TIMEOUT_MS,
66
+ maxBuffer: 16 * 1024 * 1024,
67
+ encoding: 'utf8',
68
+ });
69
+ return stdout;
70
+ }
71
+ /** npm's environment: already scrubbed, with the pinned Node's folder first on PATH. */
72
+ function npmEnvironment(env, nodePath) {
73
+ return {
74
+ ...env,
75
+ PATH: [(0, node_path_1.dirname)(nodePath), ...(env.PATH ?? '').split(node_path_1.delimiter).filter(Boolean)].join(node_path_1.delimiter),
76
+ };
77
+ }
78
+ function defaultRunCli(nodePath, cliPath, args, env) {
79
+ return new Promise((resolve, reject) => {
80
+ const child = (0, node_child_process_1.spawn)(nodePath, [cliPath, ...args], { stdio: 'inherit', env });
81
+ // A Ctrl-C reaches the child as well; the child's end is what decides, and this side then
82
+ // rolls back, so the parent does not die with it.
83
+ const ignore = () => { };
84
+ process.on('SIGINT', ignore);
85
+ process.on('SIGTERM', ignore);
86
+ const done = () => {
87
+ process.off('SIGINT', ignore);
88
+ process.off('SIGTERM', ignore);
89
+ };
90
+ child.once('error', (error) => {
91
+ done();
92
+ reject(error);
93
+ });
94
+ child.once('exit', (code, signal) => {
95
+ done();
96
+ resolve({ code, signal });
97
+ });
98
+ });
99
+ }
100
+ async function defaultNodeVersion(nodePath) {
101
+ const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)(nodePath, ['--version'], {
102
+ timeout: NPM_TIMEOUT_MS,
103
+ encoding: 'utf8',
104
+ });
105
+ const version = /^v(\d+\.\d+\.\d+)$/.exec(stdout.trim())?.[1];
106
+ if (version === undefined) {
107
+ throw new Error(`${nodePath} --version printed ${JSON.stringify(stdout.trim().slice(0, 80))}`);
108
+ }
109
+ return version;
110
+ }
111
+ function defaultDeps(options, home, configPath) {
112
+ const service = { ...options, home, configPath };
113
+ const write = options.write;
114
+ const env = options.env ?? process.env;
115
+ const waitSetting = env.MAMA_UPDATE_WAIT_MS;
116
+ const wait = waitSetting === undefined ? BOOT_WAIT_MS : Number(waitSetting);
117
+ if (!Number.isSafeInteger(wait) || wait <= 0) {
118
+ throw new prompt_js_1.CliInputError(`MAMA_UPDATE_WAIT_MS must be a whole number of milliseconds above 0, not ${JSON.stringify(waitSetting)}`);
119
+ }
120
+ return {
121
+ npm: defaultNpm,
122
+ stop: async (now) => await (0, control_js_1.runStop)(now ? ['--now'] : [], service),
123
+ start: async () => {
124
+ await (0, service_js_1.startService)(service);
125
+ },
126
+ installService: async (nodePath, cliPath) => {
127
+ await (0, service_js_1.installService)([], { ...service, nodePath, cliPath });
128
+ },
129
+ backup: async (prefix, now) => await (0, backup_js_2.runBackup)(['--include-secrets'], {
130
+ ...service,
131
+ archivePrefix: prefix,
132
+ now: () => now,
133
+ }),
134
+ serviceRuns: () => (0, service_js_1.serviceRuns)(service),
135
+ restore: async (archive) => await (0, restore_js_1.runRestore)([archive], { home, configPath, write, updateRollback: true }),
136
+ toolsInstall: async () => await (0, tools_js_1.runTools)(['install'], { home, configPath, write }),
137
+ diagnostics: async () => await (0, diagnostics_js_1.runDiagnostics)({ home, configPath, offline: true }),
138
+ control: async (op) => await (0, daemon_control_js_1.requestDaemonControl)((0, daemon_control_js_1.daemonControlPaths)((0, node_path_1.dirname)(configPath)), op, 10_000),
139
+ health: init_js_1.viewerHealth,
140
+ runCli: defaultRunCli,
141
+ lock: takeLock,
142
+ nodeVersion: defaultNodeVersion,
143
+ nodeCheck: async (nodePath, cliPath) => (0, diagnostics_js_1.nodeCheck)(nodePath, cliPath, backend_probe_js_1.defaultProbeRun, (0, node_path_1.dirname)(configPath)),
144
+ version: package_version_js_1.resolvePackageVersion,
145
+ runningPackage: install_kind_js_1.runningPackageRoot,
146
+ telegramCall: diagnostics_js_1.defaultTelegramCall,
147
+ // The release checks shorten the wait so a candidate that never boots fails in minutes.
148
+ bootWaitMs: wait,
149
+ pollMs: 1_000,
150
+ };
151
+ }
152
+ function parse(args) {
153
+ const flags = new Set(args.filter((arg) => arg.startsWith('--')));
154
+ const words = args.filter((arg) => !arg.startsWith('--'));
155
+ const only = (...names) => flags.size === names.length && names.every((name) => flags.has(name)) && words.length === 0;
156
+ if (only('--continue'))
157
+ return { kind: 'continue' };
158
+ if (only('--rollback'))
159
+ return { kind: 'rollback' };
160
+ if (only('--rollback', '--continue'))
161
+ return { kind: 'rollback-continue' };
162
+ if (args.length === 2 && args[0] === '--node' && !args[1].startsWith('--')) {
163
+ return { kind: 'node', folder: args[1] };
164
+ }
165
+ if ([...flags].every((flag) => flag === '--dry-run') && flags.size <= 1 && words.length <= 1) {
166
+ const version = words[0] ?? null;
167
+ if (version !== null && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
168
+ throw new prompt_js_1.CliInputError(`${USAGE} (a version such as 0.79.0)`);
169
+ }
170
+ return { kind: 'update', version, dryRun: flags.has('--dry-run') };
171
+ }
172
+ throw new prompt_js_1.CliInputError(USAGE);
173
+ }
174
+ async function runUpdate(args, options = {}) {
175
+ const mode = parse(args);
176
+ const home = options.home ?? (0, node_os_1.homedir)();
177
+ const configPath = options.configPath ?? (0, config_js_1.defaultConfigPath)(home);
178
+ const root = (0, node_path_1.dirname)(configPath);
179
+ const write = options.write ?? ((line) => console.log(line));
180
+ const deps = { ...defaultDeps(options, home, configPath), ...options.deps };
181
+ const env = (0, backend_security_js_1.backendEnvironment)(options.env ?? process.env);
182
+ const context = {
183
+ home,
184
+ configPath,
185
+ root,
186
+ paths: (0, update_journal_js_1.updatePaths)(root),
187
+ write,
188
+ deps,
189
+ env,
190
+ progress: progressSink(env, root, write, deps.telegramCall),
191
+ };
192
+ try {
193
+ if (mode.kind === 'continue')
194
+ return await continueUpdate(context);
195
+ if (mode.kind === 'rollback-continue')
196
+ return await finishRollback(context);
197
+ if (mode.kind === 'rollback')
198
+ return await resolveJournal(context);
199
+ if (mode.kind === 'node')
200
+ return await (0, node_move_js_1.moveNode)(context, mode.folder, options);
201
+ return await update(context, mode.version, mode.dryRun, options);
202
+ }
203
+ catch (error) {
204
+ context.progress.send({ kind: 'error', code: errorCode(error) });
205
+ throw error;
206
+ }
207
+ finally {
208
+ await context.progress.flush();
209
+ }
210
+ }
211
+ /**
212
+ * An /update's job sets `MAMA_UPDATE_PROGRESS=telegram` (runtime/update.sh); the children it runs
213
+ * inherit it, so the new version's steps and a rollback report to the same chat.
214
+ */
215
+ function progressSink(env, root, write, call) {
216
+ const setting = env.MAMA_UPDATE_PROGRESS;
217
+ if (setting === undefined)
218
+ return update_progress_js_1.NO_PROGRESS;
219
+ if (setting !== 'telegram') {
220
+ throw new prompt_js_1.CliInputError(`MAMA_UPDATE_PROGRESS must be telegram or unset, not ${JSON.stringify(setting)}`);
221
+ }
222
+ return (0, update_progress_js_1.telegramProgress)({ root, log: write, call });
223
+ }
224
+ /** A refusal with the stable code an /update's progress carries instead of its text. */
225
+ function refusal(code, text) {
226
+ return Object.assign(new prompt_js_1.CliInputError(text), { code });
227
+ }
228
+ /** What the chat is told of an error: its code, or whether it was a refusal. */
229
+ function errorCode(error) {
230
+ return ((0, update_progress_js_1.progressCode)(error?.code) ??
231
+ (error instanceof prompt_js_1.CliInputError ? 'refused' : 'error'));
232
+ }
233
+ /**
234
+ * One updater per install: `update.lock` (which `/update`, PR 4, reads before it starts one), and
235
+ * the continuation's own lock for the child's life.
236
+ */
237
+ function takeLock(paths, command) {
238
+ try {
239
+ return (0, daemon_control_js_1.acquireInstallLock)(paths, command);
240
+ }
241
+ catch (error) {
242
+ if (!(error instanceof daemon_control_js_1.InstallBusyError))
243
+ throw error;
244
+ const holder = error.holder;
245
+ throw refusal('update.busy', holder === null
246
+ ? `Another mama update holds ${paths.lockPath}`
247
+ : `Already updating since ${new Date(holder.startedAt).toLocaleString('sv-SE')} (${holder.command}, pid ${holder.pid})`);
248
+ }
249
+ }
250
+ /**
251
+ * The continuation child's lock, for as long as it writes the journal. Its parent holds
252
+ * update.lock; a parent killed meanwhile frees that one, and `--rollback` then finds this one held
253
+ * and waits, instead of a second writer acting on the same journal.
254
+ */
255
+ function takeContinuation(context, command) {
256
+ return context.deps.lock(context.paths.continuation, command);
257
+ }
258
+ /** Refuse while a continuation child (a killed parent's) still runs. */
259
+ function refuseWhileContinuing(paths) {
260
+ const child = (0, daemon_control_js_1.lockHolder)(paths.continuation);
261
+ if (child !== null) {
262
+ throw new prompt_js_1.CliInputError(`The update's ${child.command} (pid ${child.pid}) still runs and writes the journal; wait for it to end, then run this again`);
263
+ }
264
+ }
265
+ function packageManifest(packageRoot) {
266
+ const manifest = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(packageRoot, 'package.json'), 'utf8'));
267
+ const bin = typeof manifest.bin === 'string'
268
+ ? [String(manifest.name).split('/').pop()]
269
+ : Object.keys((manifest.bin ?? {}));
270
+ return { version: String(manifest.version), bin: bin.sort() };
271
+ }
272
+ /** Failures as `mama update` compares them: name and code, or name and detail without one. */
273
+ function doctorFailures(checks) {
274
+ return checks
275
+ .filter((check) => check.status === 'fail')
276
+ .map((check) => ({ name: check.name, code: check.code ?? check.detail }));
277
+ }
278
+ function schemaStored(databasePath) {
279
+ return (0, db_manager_1.readSchemaState)(databasePath, [
280
+ { name: db_manager_1.CORE_MIGRATION_SOURCE, dir: db_manager_1.MIGRATIONS_DIR },
281
+ migrations_source_js_1.STANDALONE_MIGRATION_SOURCE,
282
+ ]).map(({ name, stored }) => ({ name, stored }));
283
+ }
284
+ function readOnly(databasePath, read) {
285
+ const db = new better_sqlite3_1.default(databasePath, { readonly: true, fileMustExist: true });
286
+ try {
287
+ return read(db);
288
+ }
289
+ finally {
290
+ db.close();
291
+ }
292
+ }
293
+ function lastInputId(databasePath) {
294
+ return readOnly(databasePath, (db) => db.prepare('SELECT MAX(id) AS id FROM mailbox_inputs').get().id ??
295
+ 0);
296
+ }
297
+ function inputsAfter(databasePath, id) {
298
+ return readOnly(databasePath, (db) => db.prepare('SELECT COUNT(*) AS count FROM mailbox_inputs WHERE id > ?').get(id).count);
299
+ }
300
+ function sha256(path) {
301
+ return (0, node_crypto_1.createHash)('sha256').update((0, node_fs_1.readFileSync)(path)).digest('hex');
302
+ }
303
+ /** The installed toolkit's requirements hash, or null when none is installed. */
304
+ function toolkitHash(toolsRoot) {
305
+ const toolkit = (0, toolkit_state_js_1.readDocumentToolkit)(toolsRoot);
306
+ if (toolkit === null)
307
+ return null;
308
+ return JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(toolkit.versionDir, 'manifest.json'), 'utf8')).requirementsSha256;
309
+ }
310
+ /** Copy a file over another by a rename, so a reader sees the old one or the new one. */
311
+ function copyOver(from, to) {
312
+ const staged = `${to}.${process.pid}.update`;
313
+ (0, node_fs_1.copyFileSync)(from, staged);
314
+ (0, node_fs_1.renameSync)(staged, to);
315
+ }
316
+ /** Write the journal; a step it begins is the step an /update's progress reports. */
317
+ function save(context, journal) {
318
+ (0, update_journal_js_1.writeUpdateJournal)(context.paths.journal, journal);
319
+ if (journal.phase === 'prepare' || journal.phase === 'candidate') {
320
+ context.progress.send({ kind: 'step', from: journal.from, to: journal.to, step: journal.step });
321
+ }
322
+ }
323
+ function statusSummary(reply) {
324
+ if (reply === null)
325
+ return 'the daemon does not answer its control socket';
326
+ if (reply.state === 'stopped')
327
+ return 'the daemon is stopped';
328
+ if (reply.reply.ok !== true)
329
+ return `the daemon refused status: ${String(reply.reply.error)}`;
330
+ const status = reply.reply.status;
331
+ const turns = (status.runningTurns ?? []).reduce((sum, turn) => sum + turn.count, 0);
332
+ const waiting = (status.inputs ?? []).map((input) => `${input.count} ${input.status}`);
333
+ return `the daemon runs ${String(status.version)} (${String(status.stage)}); ${turns} turn${turns === 1 ? '' : 's'} running; inputs ${waiting.length === 0 ? 'none waiting' : waiting.join(', ')}`;
334
+ }
335
+ /** Steps 1-9, the parent: the old code. */
336
+ async function update(context, version, dryRun, options) {
337
+ const { home, root, write, deps, env, paths } = context;
338
+ // Step 1: the kind and the package.
339
+ const install = (0, install_kind_js_1.installKind)(root, home);
340
+ if (install.kind === 'checkout') {
341
+ write(`mama update serves npm installs; the service runs the checkout ${install.workspaceRoot}, which updates by git:`);
342
+ for (const step of (0, install_kind_js_1.updateSteps)(install))
343
+ write(` ${step}`);
344
+ return 1;
345
+ }
346
+ (0, install_kind_js_1.requireServicePackage)(install, deps.runningPackage());
347
+ // Nothing else would start the daemon again after the stop (decision 7), and only start.sh
348
+ // reads the hold file.
349
+ (0, node_move_js_1.requireHoldingService)(root, options);
350
+ (0, daemon_control_js_1.refuseWhileRestoring)(root);
351
+ const lock = deps.lock(paths.lock, 'update');
352
+ try {
353
+ const open = (0, update_journal_js_1.readUpdateJournal)(paths.journal);
354
+ if (open !== null) {
355
+ throw refusal('update.open', `An earlier update (${open.from} → ${open.to}) is unfinished; resolve it first: ${(0, update_journal_js_1.rollbackCommand)(open)}`);
356
+ }
357
+ const move = (0, node_move_journal_js_1.readNodeMoveJournal)((0, node_move_journal_js_1.nodeMovePlaces)(root).journal);
358
+ if (move !== null) {
359
+ throw refusal('node_move.open', `A move of the managed Node (${move.from} → ${move.to}) is unfinished; run the install command again, which finishes or undoes it: ${node_move_journal_js_1.INSTALL_COMMAND}`);
360
+ }
361
+ // Read now: from step 8 on, this package's own files are elsewhere.
362
+ const old = packageManifest(install.packageRoot);
363
+ const from = old.version;
364
+ const packageCli = (0, node_path_1.relative)(install.packageRoot, (0, node_fs_1.realpathSync)(install.cliPath));
365
+ const leftovers = (0, update_journal_js_1.leftoverStaging)((0, node_path_1.dirname)(install.packageRoot));
366
+ // Step 2: the target.
367
+ const npm = (args) => deps.npm(args, install.nodePath, env);
368
+ const prefixArgs = ['--prefix', install.prefix, '--no-update-notifier'];
369
+ let registry = '';
370
+ let target;
371
+ try {
372
+ registry = (await npm(['config', 'get', 'registry', ...prefixArgs])).trim();
373
+ target = (await npm([
374
+ 'view',
375
+ `${install_kind_js_1.PACKAGE_NAME}@${version ?? 'latest'}`,
376
+ 'version',
377
+ '--registry',
378
+ registry,
379
+ ...prefixArgs,
380
+ ])).trim();
381
+ }
382
+ catch (error) {
383
+ throw refusal(`npm.${(0, release_check_js_1.npmErrorCode)(error)}`, `Could not ask ${registry || 'the registry'} for ${install_kind_js_1.PACKAGE_NAME}@${version ?? 'latest'}: npm ${(0, release_check_js_1.npmErrorCode)(error)}; nothing changed`);
384
+ }
385
+ if (!/^\d+\.\d+\.\d+\S*$/.test(target)) {
386
+ throw refusal('npm.no_version', `${registry} has no single ${install_kind_js_1.PACKAGE_NAME}@${version ?? 'latest'} (npm printed ${JSON.stringify(target.slice(0, 80))}); nothing changed`);
387
+ }
388
+ const order = (0, release_check_js_1.compareReleases)(target, from);
389
+ if (order === 0) {
390
+ write(`already on ${from}`);
391
+ context.progress.send({ kind: 'current', version: from });
392
+ return 0;
393
+ }
394
+ if (order < 0) {
395
+ // R5: the newer version may have migrated the database past what the older one reads.
396
+ throw refusal('update.older', `Refused: ${target} is older than ${from}, which runs now; mama update does not go back a version`);
397
+ }
398
+ // The target runs on start.sh's Node, which the update does not change.
399
+ let engines;
400
+ try {
401
+ engines = (await npm([
402
+ 'view',
403
+ `${install_kind_js_1.PACKAGE_NAME}@${target}`,
404
+ 'engines.node',
405
+ '--registry',
406
+ registry,
407
+ ...prefixArgs,
408
+ ])).trim();
409
+ }
410
+ catch (error) {
411
+ throw refusal(`npm.${(0, release_check_js_1.npmErrorCode)(error)}`, `Could not ask ${registry} for ${install_kind_js_1.PACKAGE_NAME}@${target}'s engines: npm ${(0, release_check_js_1.npmErrorCode)(error)}; nothing changed`);
412
+ }
413
+ if (engines !== '') {
414
+ const running = await deps.nodeVersion(install.nodePath);
415
+ let fits;
416
+ try {
417
+ fits = (0, release_check_js_1.nodeSatisfies)(engines, running);
418
+ }
419
+ catch (error) {
420
+ throw refusal('update.engines', `Refused: ${target}'s ${message(error)}; nothing changed`);
421
+ }
422
+ if (!fits) {
423
+ throw refusal('update.engines', `Refused: ${target} needs Node.js ${engines} (its engines), and start.sh runs ${running}; nothing changed. ${install.nodePath === (0, node_move_journal_js_1.managedNodePath)(root)
424
+ ? `Run the install command again, which moves the managed Node: ${node_move_journal_js_1.INSTALL_COMMAND}`
425
+ : 'Install a Node in that range, run mama service install with it, then mama update'}`);
426
+ }
427
+ }
428
+ // Step 3: what doctor fails on already is not held against the new version.
429
+ const failuresBefore = doctorFailures(await deps.diagnostics());
430
+ // Step 4: the plan.
431
+ const places = (0, update_journal_js_1.packagePlaces)(install.prefix, from, target);
432
+ const now = await deps.control('status').catch(() => null);
433
+ write(`update: ${install_kind_js_1.PACKAGE_NAME} ${from} → ${target} (npm install, prefix ${install.prefix})`);
434
+ write(`registry: ${registry}`);
435
+ write(`now: ${statusSummary(now)}`);
436
+ if (failuresBefore.length > 0) {
437
+ write(`doctor fails already on: ${failuresBefore.map((failure) => failure.name).join(', ')} (not held against ${target})`);
438
+ }
439
+ for (const leftover of leftovers) {
440
+ write(`${dryRun ? 'would remove' : 'removing'} ${leftover}, left by an interrupted update`);
441
+ }
442
+ write('steps:');
443
+ for (const line of [
444
+ `stage ${target} in ${places.staging}; a failure changes nothing`,
445
+ 'drain: new inputs wait, running turns finish; then stop the service',
446
+ `snapshot the install to ${(0, node_path_1.join)(root, 'backups', `mama-update-${from}-<time>.tar.gz`)} (credentials included)`,
447
+ `swap: ${places.package} goes to ${places.aside}, ${target} takes its place`,
448
+ `${target} rewrites start.sh and the service file, runs doctor, starts with its claims held, and is confirmed once it runs and its viewer answers`,
449
+ `on any failure, ${from} is put back and started; the database is restored only if ${target} migrated it`,
450
+ ]) {
451
+ write(` ${line}`);
452
+ }
453
+ if (dryRun)
454
+ return 0;
455
+ context.progress.send({ kind: 'plan', from, to: target });
456
+ for (const leftover of leftovers)
457
+ (0, node_fs_1.rmSync)(leftover, { recursive: true, force: true });
458
+ // Step 5: stage.
459
+ write(`staging ${target}`);
460
+ context.progress.send({ kind: 'step', from, to: target, step: 5 });
461
+ try {
462
+ await npm([
463
+ 'install',
464
+ '-g',
465
+ '--prefix',
466
+ places.staging,
467
+ `${install_kind_js_1.PACKAGE_NAME}@${target}`,
468
+ '--registry',
469
+ registry,
470
+ '--no-update-notifier',
471
+ '--no-audit',
472
+ '--no-fund',
473
+ ]);
474
+ }
475
+ catch (error) {
476
+ (0, node_fs_1.rmSync)(places.staging, { recursive: true, force: true });
477
+ throw refusal(`npm.${(0, release_check_js_1.npmErrorCode)(error)}`, `Staging ${target} failed: npm ${(0, release_check_js_1.npmErrorCode)(error)}; nothing else changed`);
478
+ }
479
+ const staged = (0, node_fs_1.existsSync)(places.staged) ? packageManifest(places.staged) : null;
480
+ // The links in <prefix>/bin name the old package's commands, and the swap keeps them.
481
+ if (staged?.version !== target || staged.bin.join(' ') !== old.bin.join(' ')) {
482
+ (0, node_fs_1.rmSync)(places.staging, { recursive: true, force: true });
483
+ throw refusal(staged === null || staged.version !== target ? 'update.not_staged' : 'update.bin_changed', staged === null || staged.version !== target
484
+ ? `npm did not stage ${target} at ${places.staged}; nothing else changed`
485
+ : `${target}'s commands (${staged.bin.join(', ')}) are not ${from}'s (${old.bin.join(', ')}); the swap would leave its links wrong, so install it with npm install -g ${install_kind_js_1.PACKAGE_NAME}@${target}`);
486
+ }
487
+ // Step 6: the journal, the drain, the stop.
488
+ const journal = {
489
+ format: 'mama-update',
490
+ phase: 'prepare',
491
+ step: 6,
492
+ from,
493
+ to: target,
494
+ prefix: install.prefix,
495
+ registry,
496
+ nodePath: install.nodePath,
497
+ packageCli,
498
+ previousCli: (0, node_path_1.join)(places.aside, packageCli),
499
+ failuresBefore,
500
+ snapshot: null,
501
+ snapshotStaging: null,
502
+ schemaBefore: null,
503
+ lastInputId: null,
504
+ toolkitBefore: null,
505
+ toolkitInstalled: false,
506
+ copies: [],
507
+ failure: null,
508
+ startedAt: new Date().toISOString(),
509
+ };
510
+ save(context, journal);
511
+ if ((await deps.stop(false)) !== 0) {
512
+ (0, update_journal_js_1.removeUpdateJournal)(paths.journal);
513
+ (0, node_fs_1.rmSync)(places.staging, { recursive: true, force: true });
514
+ context.progress.send({
515
+ kind: 'not_updated',
516
+ from,
517
+ to: target,
518
+ step: 6,
519
+ code: 'stop.failed',
520
+ });
521
+ // A stop can fail after the service manager took it (a release that took too long), so the
522
+ // install may be down now; this names which, as mama backup --restart does.
523
+ write(deps.serviceRuns()
524
+ ? `not updated: the stop did not finish (above); ${from} stays installed and still runs`
525
+ : `not updated: the stop did not finish cleanly (above) and the service is not running; ${from} stays installed, and mama service start starts it again`);
526
+ return 1;
527
+ }
528
+ // From the stop on, a start (a reboot, an owner's mama service start) finds the hold and
529
+ // exits: a parent that dies after the stop leaves the install stopped, not half-changed.
530
+ (0, node_fs_1.writeFileSync)(paths.hold, `${process.pid}\n`, { mode: 0o600 });
531
+ // Step 7: the snapshot, what the rollback compares with, and the launch files. Its files are
532
+ // named first, so an update cut off mid-backup removes what that backup left.
533
+ const startedBackup = new Date();
534
+ const planned = (0, backup_js_1.backupFiles)((0, node_path_1.join)((0, node_fs_1.realpathSync)(root), 'backups'), startedBackup, `mama-update-${from}`);
535
+ journal.step = 7;
536
+ journal.snapshot = planned.final;
537
+ journal.snapshotStaging = planned.staging;
538
+ save(context, journal);
539
+ try {
540
+ // Named by the same backupFiles as the journal's planned paths; the rollback restores and
541
+ // removes the journal's path, so any other one is an error, not a snapshot.
542
+ const archive = await deps.backup(`mama-update-${from}`, startedBackup);
543
+ if (archive !== planned.final) {
544
+ (0, node_fs_1.rmSync)(archive, { force: true });
545
+ throw new Error(`the snapshot was written to ${archive}, not ${planned.final}`);
546
+ }
547
+ const config = (0, config_js_1.loadConfig)({ home, path: context.configPath, onIgnored: () => { } });
548
+ journal.schemaBefore = schemaStored(config.database.path);
549
+ journal.lastInputId = lastInputId(config.database.path);
550
+ journal.toolkitBefore = toolkitHash(config.tools_root);
551
+ (0, node_fs_1.mkdirSync)(paths.copies, { recursive: true, mode: 0o700 });
552
+ journal.copies = [(0, node_path_1.join)(root, 'start.sh'), (0, service_js_1.serviceFile)(options)].map((path) => {
553
+ const copy = (0, node_path_1.join)(paths.copies, path.split('/').pop());
554
+ (0, node_fs_1.copyFileSync)(path, copy);
555
+ return { path, copy };
556
+ });
557
+ save(context, journal);
558
+ }
559
+ catch (error) {
560
+ write(`not updated: ${message(error)}`);
561
+ context.progress.send({
562
+ kind: 'not_updated',
563
+ from,
564
+ to: target,
565
+ step: 7,
566
+ code: (0, update_progress_js_1.progressCode)(error.code) ?? 'snapshot.failed',
567
+ });
568
+ (0, update_journal_js_1.removeSnapshot)(journal);
569
+ (0, node_fs_1.rmSync)(paths.copies, { recursive: true, force: true });
570
+ (0, node_fs_1.rmSync)(places.staging, { recursive: true, force: true });
571
+ // The hold goes before the journal: a cut here leaves a journal, which names its fix.
572
+ (0, node_fs_1.rmSync)(paths.hold, { force: true });
573
+ (0, update_journal_js_1.removeUpdateJournal)(paths.journal);
574
+ try {
575
+ await deps.start();
576
+ write(`${from} started again`);
577
+ }
578
+ catch (startError) {
579
+ write(`${from} did not start again either: ${message(startError)} (mama service start)`);
580
+ }
581
+ return 1;
582
+ }
583
+ // Step 8: the swap, on the hold written after the stop. From here a failure rolls back.
584
+ try {
585
+ journal.step = 8;
586
+ save(context, journal);
587
+ (0, node_fs_1.renameSync)(places.package, places.aside);
588
+ (0, node_fs_1.renameSync)(places.staged, places.package);
589
+ }
590
+ catch (error) {
591
+ journal.failure = { step: 8, message: message(error) };
592
+ await rollBack(context, journal);
593
+ return 1;
594
+ }
595
+ // Step 9: the new version carries on.
596
+ journal.step = 9;
597
+ save(context, journal);
598
+ // The new version's own reports come after this one's.
599
+ await context.progress.flush();
600
+ let result;
601
+ try {
602
+ result = await deps.runCli(journal.nodePath, (0, node_path_1.join)(places.package, packageCli), ['update', '--continue'], env);
603
+ }
604
+ catch (error) {
605
+ result = { code: null, signal: null };
606
+ journal.failure = { step: 9, message: `the new version did not start: ${message(error)}` };
607
+ save(context, journal);
608
+ }
609
+ const after = (0, update_journal_js_1.readUpdateJournal)(paths.journal);
610
+ if (after === null)
611
+ return result.code === 0 ? 0 : 1;
612
+ // Committed: the candidate may have run inputs, so it is finished, never rolled back.
613
+ if (after.phase === 'committed')
614
+ return await finishCommitted(context, after);
615
+ after.failure ??= {
616
+ step: after.step,
617
+ message: result.signal !== null
618
+ ? `the new version's update ended on ${result.signal}`
619
+ : `the new version's update exited ${String(result.code)} at step ${after.step}`,
620
+ };
621
+ await rollBack(context, after);
622
+ return 1;
623
+ }
624
+ finally {
625
+ lock.release();
626
+ }
627
+ }
628
+ /**
629
+ * Steps 10-14, the child: the new code, under the parent's update.lock. A failure records its
630
+ * step and exits non-zero; the parent rolls back.
631
+ */
632
+ async function continueUpdate(context) {
633
+ const own = takeContinuation(context, 'update --continue');
634
+ try {
635
+ return await continueUpdateHeld(context);
636
+ }
637
+ finally {
638
+ own.release();
639
+ }
640
+ }
641
+ /** What to print when the parent that would roll back is gone. */
642
+ function orphanNote(context, journal) {
643
+ return (0, daemon_control_js_1.lockHolder)(context.paths.lock) === null
644
+ ? `; the updater that started this is gone, so roll back with: ${(0, update_journal_js_1.rollbackCommand)(journal)}`
645
+ : '';
646
+ }
647
+ async function continueUpdateHeld(context) {
648
+ const { home, root, write, deps, paths } = context;
649
+ const journal = (0, update_journal_js_1.readUpdateJournal)(paths.journal);
650
+ if (journal === null || journal.phase !== 'prepare' || journal.step < 9) {
651
+ throw new prompt_js_1.CliInputError('mama update --continue runs only under mama update');
652
+ }
653
+ if (deps.version() !== journal.to) {
654
+ throw new prompt_js_1.CliInputError(`This is ${deps.version()}; the update continues in ${journal.to}`);
655
+ }
656
+ const places = (0, update_journal_js_1.packagePlaces)(journal.prefix, journal.from, journal.to);
657
+ const step = (number) => {
658
+ journal.step = number;
659
+ save(context, journal);
660
+ };
661
+ try {
662
+ // Step 10: start.sh and the service file from this version's templates.
663
+ step(10);
664
+ await deps.installService(journal.nodePath, (0, node_path_1.join)(places.package, journal.packageCli));
665
+ // Step 11: nothing doctor did not fail on before; this update's own journal does not count.
666
+ step(11);
667
+ const fresh = doctorFailures(await deps.diagnostics()).filter((failure) => failure.code !== 'update.open' &&
668
+ !journal.failuresBefore.some((before) => before.name === failure.name && before.code === failure.code));
669
+ if (fresh.length > 0) {
670
+ throw Object.assign(new Error(`mama doctor --offline fails where it did not before: ${fresh.map((failure) => `${failure.name} (${failure.code})`).join(', ')}`),
671
+ // The first new failure's stable code, for an /update's chat; a detail is not a code.
672
+ { code: (0, update_progress_js_1.progressCode)(fresh[0].code) ?? 'doctor.failed' });
673
+ }
674
+ // Step 12: the toolkit, when this version locks other requirements.
675
+ step(12);
676
+ const lock = sha256((0, node_path_1.join)(toolkit_state_js_1.TOOLKIT_ASSETS, 'requirements.lock'));
677
+ if (journal.toolkitBefore !== null && journal.toolkitBefore !== lock) {
678
+ journal.toolkitInstalled = true;
679
+ save(context, journal);
680
+ await deps.toolsInstall();
681
+ }
682
+ // Step 13: the candidate boots with its claims held, so its recovery runs nothing.
683
+ journal.phase = 'candidate';
684
+ step(13);
685
+ (0, node_fs_1.rmSync)(paths.hold, { force: true });
686
+ await deps.start();
687
+ // Step 14: running at this version, and its viewer answers.
688
+ step(14);
689
+ const port = (0, viewer_server_js_1.resolveApiPort)((0, launch_files_js_1.startExports)((0, launch_files_js_1.readStartScript)((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, 'start.sh'), 'utf8'), home))
690
+ .MAMA_API_PORT);
691
+ await (0, init_js_1.waitForBoot)({
692
+ status: () => deps.control('status'),
693
+ health: () => deps.health(port),
694
+ timeoutMs: deps.bootWaitMs,
695
+ pollMs: deps.pollMs,
696
+ write,
697
+ version: journal.to,
698
+ next: `the update rolls back to ${journal.from}`,
699
+ });
700
+ }
701
+ catch (error) {
702
+ journal.failure = { step: journal.step, message: message(error) };
703
+ save(context, journal);
704
+ write(`update step ${journal.step} failed: ${message(error)}${orphanNote(context, journal)}`);
705
+ context.progress.send({
706
+ kind: 'failed',
707
+ from: journal.from,
708
+ to: journal.to,
709
+ step: journal.step,
710
+ code: (0, update_progress_js_1.progressCode)(error?.code),
711
+ });
712
+ return 1;
713
+ }
714
+ // On disk before the release: from here the candidate claims, and nothing rolls it back.
715
+ journal.phase = 'committed';
716
+ save(context, journal);
717
+ return await finishCommitted(context, journal);
718
+ }
719
+ /** Step 14's end: release the candidate's claims, delete what the rollback would have needed. */
720
+ async function finishCommitted(context, journal) {
721
+ const { paths, write, deps } = context;
722
+ const reply = await deps.control('release');
723
+ if (reply.state === 'running' && reply.reply.ok !== true) {
724
+ throw new prompt_js_1.CliInputError(`${journal.to} refused the release: ${String(reply.reply.error)}; ${(0, update_journal_js_1.rollbackCommand)(journal)} finishes it`);
725
+ }
726
+ const places = (0, update_journal_js_1.packagePlaces)(journal.prefix, journal.from, journal.to);
727
+ (0, update_journal_js_1.removeSnapshot)(journal);
728
+ (0, node_fs_1.rmSync)(paths.copies, { recursive: true, force: true });
729
+ (0, node_fs_1.rmSync)(places.aside, { recursive: true, force: true });
730
+ (0, node_fs_1.rmSync)(places.staging, { recursive: true, force: true });
731
+ (0, node_fs_1.rmSync)(paths.hold, { force: true });
732
+ // Last: a cleanup cut off leaves a committed journal, which --rollback finishes.
733
+ (0, update_journal_js_1.removeUpdateJournal)(paths.journal);
734
+ context.progress.send({
735
+ kind: 'updated',
736
+ from: journal.from,
737
+ to: journal.to,
738
+ running: reply.state === 'running',
739
+ });
740
+ write(reply.state === 'running'
741
+ ? `updated: ${journal.from} → ${journal.to}; it runs and claims again`
742
+ : `updated: ${journal.from} → ${journal.to}; the daemon is not running now (mama service start)`);
743
+ // An install the install command made: the new release's Node pin, which only that command
744
+ // moves to (install.md, "Moving the managed Node").
745
+ if (journal.nodePath === (0, node_move_journal_js_1.managedNodePath)(context.root)) {
746
+ const pin = JSON.parse((0, node_fs_1.readFileSync)((0, node_path_1.join)(places.package, 'install', 'node.json'), 'utf8')).version;
747
+ const running = await deps.nodeVersion(journal.nodePath);
748
+ if (pin !== running) {
749
+ write(` ${journal.to} pins Node.js ${pin}, and MAMA runs on ${running}: run the install command again to move it: ${node_move_journal_js_1.INSTALL_COMMAND}`);
750
+ }
751
+ }
752
+ return 0;
753
+ }
754
+ /** `mama update --rollback`: resolve the journal a killed updater left. */
755
+ async function resolveJournal(context) {
756
+ const lock = context.deps.lock(context.paths.lock, 'update --rollback');
757
+ try {
758
+ refuseWhileContinuing(context.paths);
759
+ const journal = (0, update_journal_js_1.readUpdateJournal)(context.paths.journal);
760
+ if (journal === null) {
761
+ context.write('no update is unfinished');
762
+ return 0;
763
+ }
764
+ if (journal.phase === 'committed')
765
+ return await finishCommitted(context, journal);
766
+ journal.failure ??= {
767
+ step: journal.step,
768
+ message: `the updater ended at step ${journal.step}`,
769
+ };
770
+ return (await rollBack(context, journal)) ? 0 : 1;
771
+ }
772
+ finally {
773
+ lock.release();
774
+ }
775
+ }
776
+ /**
777
+ * The rollback's stop. A daemon that answers as the held candidate runs nothing, and one that does
778
+ * not answer has nothing to drain: `--now`. One that answers without a hold claims work (the old
779
+ * version a rollback already started, or a daemon the stop at step 6 never reached), so it drains.
780
+ */
781
+ async function stopForRollback(context, journal) {
782
+ const reply = await context.deps.control('status').catch(() => null);
783
+ const status = reply?.state === 'running' && reply.reply.ok === true
784
+ ? reply.reply.status
785
+ : null;
786
+ // The control socket answers before boot reads the journal, so a candidate restarted under
787
+ // KeepAlive can answer without its hold yet: at `candidate`, running at <to> is the candidate.
788
+ const heldCandidate = journal.phase === 'candidate' &&
789
+ (status?.update?.to === journal.to || status?.version === journal.to);
790
+ return await context.deps.stop(status === null || heldCandidate);
791
+ }
792
+ /**
793
+ * Rollback steps 1-3, in whichever CLI runs (the parent, or `--rollback`): stop, hold, put the
794
+ * previous package back, and hand the rest to it. Only renames and the service manager's stop,
795
+ * so it runs from loaded code. True when the previous version runs again.
796
+ */
797
+ async function rollBack(context, journal) {
798
+ const { paths, write, deps, env } = context;
799
+ const places = (0, update_journal_js_1.packagePlaces)(journal.prefix, journal.from, journal.to);
800
+ write(`rolling back to ${journal.from}: step ${journal.failure?.step ?? journal.step} failed: ${journal.failure?.message ?? 'unknown'}`);
801
+ context.progress.send({ kind: 'rolling_back', from: journal.from, to: journal.to });
802
+ try {
803
+ if ((await stopForRollback(context, journal)) !== 0)
804
+ throw new Error('the service did not stop');
805
+ (0, node_fs_1.writeFileSync)(paths.hold, `${process.pid}\n`, { mode: 0o600 });
806
+ // The previous version boots without a hold from here.
807
+ journal.phase = 'rollback';
808
+ save(context, journal);
809
+ if ((0, node_fs_1.existsSync)(places.aside)) {
810
+ if ((0, node_fs_1.existsSync)(places.package)) {
811
+ (0, node_fs_1.rmSync)(places.staged, { recursive: true, force: true });
812
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(places.staged), { recursive: true });
813
+ (0, node_fs_1.renameSync)(places.package, places.staged);
814
+ }
815
+ (0, node_fs_1.renameSync)(places.aside, places.package);
816
+ }
817
+ await context.progress.flush();
818
+ const result = await deps.runCli(journal.nodePath, (0, node_path_1.join)(places.package, journal.packageCli), ['update', '--rollback', '--continue'], env);
819
+ if (result.code === 0)
820
+ return true;
821
+ context.progress.send({ kind: 'rollback_stopped', from: journal.from, to: journal.to });
822
+ // What it printed may be cut off (a signal); the failed step and the command are said here.
823
+ (0, node_fs_1.writeFileSync)(paths.hold, `${process.pid}\n`, { mode: 0o600 });
824
+ const current = (0, update_journal_js_1.readUpdateJournal)(paths.journal) ?? journal;
825
+ write(`rollback did not finish: ${journal.from}'s update --rollback --continue ${result.signal === null ? `exited ${String(result.code)}` : `ended on ${result.signal}`}; the update had failed at step ${current.failure?.step ?? current.step}: ${current.failure?.message ?? 'unknown'}. The hold file is in place. Resolve it with: ${(0, update_journal_js_1.rollbackCommand)(current)}`);
826
+ return false;
827
+ }
828
+ catch (error) {
829
+ (0, node_fs_1.writeFileSync)(paths.hold, `${process.pid}\n`, { mode: 0o600 });
830
+ context.progress.send({ kind: 'rollback_stopped', from: journal.from, to: journal.to });
831
+ const current = (0, update_journal_js_1.readUpdateJournal)(paths.journal) ?? journal;
832
+ write(`rollback stopped: ${message(error)}. The install stays stopped on its hold file; the journal, the snapshot and both package folders are kept. Resolve it with: ${(0, update_journal_js_1.rollbackCommand)(current)}`);
833
+ return false;
834
+ }
835
+ }
836
+ /** Rollback steps 4-7, in the previous version's CLI with its package back in place. */
837
+ async function finishRollback(context) {
838
+ const own = takeContinuation(context, 'update --rollback --continue');
839
+ try {
840
+ return await finishRollbackHeld(context);
841
+ }
842
+ finally {
843
+ own.release();
844
+ }
845
+ }
846
+ /**
847
+ * `npm ls -g --json`'s answer for MAMA OS. npm exits 1 for problems elsewhere in the prefix
848
+ * (ELSPROBLEMS) and still prints the tree, so the tree decides, not the exit.
849
+ */
850
+ async function installedVersion(context, journal) {
851
+ let text;
852
+ try {
853
+ text = await context.deps.npm(['ls', '-g', '--prefix', journal.prefix, '--depth=0', '--json', '--no-update-notifier'], journal.nodePath, context.env);
854
+ }
855
+ catch (error) {
856
+ const stdout = error.stdout;
857
+ if (typeof stdout !== 'string' || stdout.trim() === '')
858
+ throw error;
859
+ text = stdout;
860
+ }
861
+ const listed = JSON.parse(text);
862
+ return listed.dependencies?.[install_kind_js_1.PACKAGE_NAME]?.version;
863
+ }
864
+ async function finishRollbackHeld(context) {
865
+ const { home, root, write, deps, paths } = context;
866
+ const journal = (0, update_journal_js_1.readUpdateJournal)(paths.journal);
867
+ if (journal === null || journal.phase !== 'rollback') {
868
+ throw new prompt_js_1.CliInputError('mama update --rollback --continue runs only under a rollback');
869
+ }
870
+ if (deps.version() !== journal.from) {
871
+ throw new prompt_js_1.CliInputError(`This is ${deps.version()}; the rollback continues in ${journal.from}: ${(0, update_journal_js_1.rollbackCommand)(journal)}`);
872
+ }
873
+ const places = (0, update_journal_js_1.packagePlaces)(journal.prefix, journal.from, journal.to);
874
+ try {
875
+ // 4: the saved launch files, not regenerated ones: a missing backend cannot block this.
876
+ for (const { path, copy } of journal.copies) {
877
+ if ((0, node_fs_1.existsSync)(copy))
878
+ copyOver(copy, path);
879
+ }
880
+ // 5: the database only if the candidate migrated it; a held candidate ran no turn.
881
+ let dropped = null;
882
+ if (journal.snapshot !== null && journal.schemaBefore !== null) {
883
+ const config = (0, config_js_1.loadConfig)({ home, path: context.configPath, onIgnored: () => { } });
884
+ const now = schemaStored(config.database.path);
885
+ const before = new Map(journal.schemaBefore.map((source) => [source.name, source.stored]));
886
+ const moved = now.some((source) => source.stored !== (before.get(source.name) ?? 0));
887
+ if (moved) {
888
+ dropped = inputsAfter(config.database.path, journal.lastInputId ?? 0);
889
+ await deps.restore(journal.snapshot);
890
+ }
891
+ }
892
+ // 6: tools_root is outside the backup; this version's toolkit again.
893
+ if (journal.toolkitInstalled)
894
+ await deps.toolsInstall();
895
+ // 7: start the previous version and check it.
896
+ (0, node_fs_1.rmSync)(paths.hold, { force: true });
897
+ await deps.start();
898
+ const port = (0, viewer_server_js_1.resolveApiPort)((0, launch_files_js_1.startExports)((0, launch_files_js_1.readStartScript)((0, node_fs_1.readFileSync)((0, node_path_1.join)(root, 'start.sh'), 'utf8'), home))
899
+ .MAMA_API_PORT);
900
+ await (0, init_js_1.waitForBoot)({
901
+ status: () => deps.control('status'),
902
+ health: () => deps.health(port),
903
+ timeoutMs: deps.bootWaitMs,
904
+ pollMs: deps.pollMs,
905
+ write,
906
+ version: journal.from,
907
+ next: 'the rollback stops here',
908
+ });
909
+ const installed = await installedVersion(context, journal);
910
+ if (installed !== journal.from) {
911
+ throw new Error(`npm ls -g shows ${install_kind_js_1.PACKAGE_NAME} ${String(installed)}, not ${journal.from}`);
912
+ }
913
+ (0, update_journal_js_1.removeSnapshot)(journal);
914
+ (0, node_fs_1.rmSync)(paths.copies, { recursive: true, force: true });
915
+ (0, node_fs_1.rmSync)(places.staging, { recursive: true, force: true });
916
+ (0, update_journal_js_1.removeUpdateJournal)(paths.journal);
917
+ context.progress.send({
918
+ kind: 'rolled_back',
919
+ from: journal.from,
920
+ to: journal.to,
921
+ step: journal.failure?.step ?? journal.step,
922
+ });
923
+ write(`rolled back: ${journal.from} runs again; step ${journal.failure?.step ?? journal.step} of the update to ${journal.to} failed: ${journal.failure?.message ?? 'unknown'}`);
924
+ write(dropped === null
925
+ ? ` the database was kept: ${journal.to} did not migrate it`
926
+ : ` ${journal.to} migrated the database, so the snapshot was restored: ${dropped} input(s) accepted after it were dropped, and Codex sessions start over`);
927
+ return 0;
928
+ }
929
+ catch (error) {
930
+ // Stopped on the hold file, everything kept, so this can be run again.
931
+ let stopped;
932
+ try {
933
+ // The previous version may have started and run turns: it drains (stopForRollback).
934
+ stopped =
935
+ (await stopForRollback(context, journal)) === 0
936
+ ? 'The install stays stopped on its hold file'
937
+ : 'The hold file is in place, but the daemon did not stop (above) and may still run';
938
+ }
939
+ catch (stopError) {
940
+ stopped = `The hold file is in place, but stopping the daemon failed (${message(stopError)}) and it may still run`;
941
+ }
942
+ (0, node_fs_1.writeFileSync)(paths.hold, `${process.pid}\n`, { mode: 0o600 });
943
+ context.progress.send({ kind: 'rollback_stopped', from: journal.from, to: journal.to });
944
+ write(`rollback stopped: ${message(error)}. ${stopped}; the journal, the snapshot and both package folders are kept. Run again: ${(0, update_journal_js_1.rollbackCommand)(journal)}`);
945
+ return 1;
946
+ }
947
+ }
948
+ //# sourceMappingURL=update.js.map