@caelo-cms/provisioning 0.10.46 → 0.10.48

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 (342) hide show
  1. package/dist/adc-impersonation.d.ts.map +1 -1
  2. package/dist/adc-impersonation.js +23 -1
  3. package/dist/adc-impersonation.js.map +1 -1
  4. package/dist/agent/apply.d.ts +45 -0
  5. package/dist/agent/apply.d.ts.map +1 -0
  6. package/dist/agent/apply.js +83 -0
  7. package/dist/agent/apply.js.map +1 -0
  8. package/dist/agent/cli-provenance.d.ts +42 -0
  9. package/dist/agent/cli-provenance.d.ts.map +1 -0
  10. package/dist/agent/cli-provenance.js +138 -0
  11. package/dist/agent/cli-provenance.js.map +1 -0
  12. package/dist/agent/command.d.ts +79 -0
  13. package/dist/agent/command.d.ts.map +1 -0
  14. package/dist/agent/command.js +454 -0
  15. package/dist/agent/command.js.map +1 -0
  16. package/dist/agent/config.d.ts +30 -0
  17. package/dist/agent/config.d.ts.map +1 -0
  18. package/dist/agent/config.js +60 -0
  19. package/dist/agent/config.js.map +1 -0
  20. package/dist/agent/lock.d.ts +13 -0
  21. package/dist/agent/lock.d.ts.map +1 -0
  22. package/dist/agent/lock.js +100 -0
  23. package/dist/agent/lock.js.map +1 -0
  24. package/dist/agent/queue.d.ts +35 -0
  25. package/dist/agent/queue.d.ts.map +1 -0
  26. package/dist/agent/queue.js +50 -0
  27. package/dist/agent/queue.js.map +1 -0
  28. package/dist/agent/run.d.ts +97 -0
  29. package/dist/agent/run.d.ts.map +1 -0
  30. package/dist/agent/run.js +198 -0
  31. package/dist/agent/run.js.map +1 -0
  32. package/dist/agent/systemd.d.ts +55 -0
  33. package/dist/agent/systemd.d.ts.map +1 -0
  34. package/dist/agent/systemd.js +171 -0
  35. package/dist/agent/systemd.js.map +1 -0
  36. package/dist/agent/units.d.ts +44 -0
  37. package/dist/agent/units.d.ts.map +1 -0
  38. package/dist/agent/units.js +127 -0
  39. package/dist/agent/units.js.map +1 -0
  40. package/dist/agent-db.js +18362 -0
  41. package/dist/cli.js +122 -16
  42. package/dist/cli.js.map +1 -1
  43. package/dist/cloudsql-iam-user.d.ts +39 -0
  44. package/dist/cloudsql-iam-user.d.ts.map +1 -0
  45. package/dist/cloudsql-iam-user.js +119 -0
  46. package/dist/cloudsql-iam-user.js.map +1 -0
  47. package/dist/compose.d.ts +16 -0
  48. package/dist/compose.d.ts.map +1 -1
  49. package/dist/compose.js +26 -1
  50. package/dist/compose.js.map +1 -1
  51. package/dist/dns/index.d.ts +2 -0
  52. package/dist/dns/index.d.ts.map +1 -1
  53. package/dist/dns/index.js +3 -2
  54. package/dist/dns/index.js.map +1 -1
  55. package/dist/dns/manual.d.ts +5 -0
  56. package/dist/dns/manual.d.ts.map +1 -1
  57. package/dist/dns/manual.js +28 -12
  58. package/dist/dns/manual.js.map +1 -1
  59. package/dist/gcloud.d.ts +2 -0
  60. package/dist/gcloud.d.ts.map +1 -1
  61. package/dist/gcloud.js +1 -1
  62. package/dist/gcloud.js.map +1 -1
  63. package/dist/gcp-names.d.ts +15 -0
  64. package/dist/gcp-names.d.ts.map +1 -1
  65. package/dist/gcp-names.js +23 -0
  66. package/dist/gcp-names.js.map +1 -1
  67. package/dist/index.d.ts +6 -0
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +4 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/infra-contract.d.ts +1 -1
  72. package/dist/infra-contract.js +7 -1
  73. package/dist/infra-contract.js.map +1 -1
  74. package/dist/install-job-runner.d.ts +13 -0
  75. package/dist/install-job-runner.d.ts.map +1 -0
  76. package/dist/install-job-runner.js +155 -0
  77. package/dist/install-job-runner.js.map +1 -0
  78. package/dist/install-job.d.ts +210 -0
  79. package/dist/install-job.d.ts.map +1 -0
  80. package/dist/install-job.js +449 -0
  81. package/dist/install-job.js.map +1 -0
  82. package/dist/install-state.d.ts +19 -1
  83. package/dist/install-state.d.ts.map +1 -1
  84. package/dist/install-state.js +31 -3
  85. package/dist/install-state.js.map +1 -1
  86. package/dist/lifecycle.d.ts.map +1 -1
  87. package/dist/lifecycle.js +24 -5
  88. package/dist/lifecycle.js.map +1 -1
  89. package/dist/migration-runner.d.ts +5 -0
  90. package/dist/migration-runner.d.ts.map +1 -1
  91. package/dist/migration-runner.js +1 -1
  92. package/dist/migration-runner.js.map +1 -1
  93. package/dist/operator-access.d.ts +14 -8
  94. package/dist/operator-access.d.ts.map +1 -1
  95. package/dist/operator-access.js +16 -102
  96. package/dist/operator-access.js.map +1 -1
  97. package/dist/run-mode.d.ts +27 -0
  98. package/dist/run-mode.d.ts.map +1 -0
  99. package/dist/run-mode.js +22 -0
  100. package/dist/run-mode.js.map +1 -0
  101. package/dist/runtime-secrets.d.ts +19 -0
  102. package/dist/runtime-secrets.d.ts.map +1 -1
  103. package/dist/runtime-secrets.js +44 -0
  104. package/dist/runtime-secrets.js.map +1 -1
  105. package/dist/self-hosted-roles.d.ts +22 -1
  106. package/dist/self-hosted-roles.d.ts.map +1 -1
  107. package/dist/self-hosted-roles.js +81 -12
  108. package/dist/self-hosted-roles.js.map +1 -1
  109. package/dist/self-hosted-upgrade.d.ts +17 -0
  110. package/dist/self-hosted-upgrade.d.ts.map +1 -1
  111. package/dist/self-hosted-upgrade.js +16 -1
  112. package/dist/self-hosted-upgrade.js.map +1 -1
  113. package/dist/stack-contract.d.ts +6 -0
  114. package/dist/stack-contract.d.ts.map +1 -1
  115. package/dist/stack-contract.js +10 -1
  116. package/dist/stack-contract.js.map +1 -1
  117. package/dist/state-attach.d.ts +23 -0
  118. package/dist/state-attach.d.ts.map +1 -0
  119. package/dist/state-attach.js +112 -0
  120. package/dist/state-attach.js.map +1 -0
  121. package/dist/state-backend.d.ts +11 -0
  122. package/dist/state-backend.d.ts.map +1 -1
  123. package/dist/state-backend.js +20 -0
  124. package/dist/state-backend.js.map +1 -1
  125. package/dist/state-command.d.ts +20 -0
  126. package/dist/state-command.d.ts.map +1 -0
  127. package/dist/state-command.js +237 -0
  128. package/dist/state-command.js.map +1 -0
  129. package/dist/state-migrate.d.ts +121 -0
  130. package/dist/state-migrate.d.ts.map +1 -0
  131. package/dist/state-migrate.js +290 -0
  132. package/dist/state-migrate.js.map +1 -0
  133. package/dist/state-pulumi.d.ts +17 -0
  134. package/dist/state-pulumi.d.ts.map +1 -0
  135. package/dist/state-pulumi.js +218 -0
  136. package/dist/state-pulumi.js.map +1 -0
  137. package/dist/state-store.d.ts +27 -0
  138. package/dist/state-store.d.ts.map +1 -1
  139. package/dist/state-store.js +65 -0
  140. package/dist/state-store.js.map +1 -1
  141. package/dist/upgrade/converge.d.ts +6 -0
  142. package/dist/upgrade/converge.d.ts.map +1 -1
  143. package/dist/upgrade/converge.js +59 -2
  144. package/dist/upgrade/converge.js.map +1 -1
  145. package/dist/upgrade/gcp-lookup.d.ts +6 -0
  146. package/dist/upgrade/gcp-lookup.d.ts.map +1 -1
  147. package/dist/upgrade/gcp-lookup.js +25 -0
  148. package/dist/upgrade/gcp-lookup.js.map +1 -1
  149. package/dist/upgrade/release-images.d.ts +12 -0
  150. package/dist/upgrade/release-images.d.ts.map +1 -1
  151. package/dist/upgrade/release-images.js +31 -0
  152. package/dist/upgrade/release-images.js.map +1 -1
  153. package/dist/upgrade/upgrade.d.ts +5 -0
  154. package/dist/upgrade/upgrade.d.ts.map +1 -1
  155. package/dist/upgrade/upgrade.js +28 -1
  156. package/dist/upgrade/upgrade.js.map +1 -1
  157. package/dist/upgrader-assess.d.ts +82 -0
  158. package/dist/upgrader-assess.d.ts.map +1 -0
  159. package/dist/upgrader-assess.js +116 -0
  160. package/dist/upgrader-assess.js.map +1 -0
  161. package/dist/upgrader-gcp.d.ts +17 -0
  162. package/dist/upgrader-gcp.d.ts.map +1 -0
  163. package/dist/upgrader-gcp.js +87 -0
  164. package/dist/upgrader-gcp.js.map +1 -0
  165. package/dist/upgrader-job.d.ts +131 -0
  166. package/dist/upgrader-job.d.ts.map +1 -0
  167. package/dist/upgrader-job.js +259 -0
  168. package/dist/upgrader-job.js.map +1 -0
  169. package/dist/upgrader-reporter.d.ts +35 -0
  170. package/dist/upgrader-reporter.d.ts.map +1 -0
  171. package/dist/upgrader-reporter.js +79 -0
  172. package/dist/upgrader-reporter.js.map +1 -0
  173. package/dist/upgrader-setup.d.ts +59 -0
  174. package/dist/upgrader-setup.d.ts.map +1 -0
  175. package/dist/upgrader-setup.js +308 -0
  176. package/dist/upgrader-setup.js.map +1 -0
  177. package/dist/wizard.d.ts +6 -0
  178. package/dist/wizard.d.ts.map +1 -1
  179. package/dist/wizard.js +32 -4
  180. package/dist/wizard.js.map +1 -1
  181. package/dist/wizards/gcp-firebase.d.ts +3 -0
  182. package/dist/wizards/gcp-firebase.d.ts.map +1 -1
  183. package/dist/wizards/gcp-firebase.js +1 -0
  184. package/dist/wizards/gcp-firebase.js.map +1 -1
  185. package/dist/wizards/gcp-install-job.d.ts +74 -0
  186. package/dist/wizards/gcp-install-job.d.ts.map +1 -0
  187. package/dist/wizards/gcp-install-job.js +398 -0
  188. package/dist/wizards/gcp-install-job.js.map +1 -0
  189. package/dist/wizards/gcp-prompts.d.ts +3 -0
  190. package/dist/wizards/gcp-prompts.d.ts.map +1 -0
  191. package/dist/wizards/gcp-prompts.js +25 -0
  192. package/dist/wizards/gcp-prompts.js.map +1 -0
  193. package/dist/wizards/gcp-pulumi.d.ts +9 -2
  194. package/dist/wizards/gcp-pulumi.d.ts.map +1 -1
  195. package/dist/wizards/gcp-pulumi.js +8 -6
  196. package/dist/wizards/gcp-pulumi.js.map +1 -1
  197. package/dist/wizards/gcp.d.ts +40 -0
  198. package/dist/wizards/gcp.d.ts.map +1 -1
  199. package/dist/wizards/gcp.js +291 -83
  200. package/dist/wizards/gcp.js.map +1 -1
  201. package/package.json +6 -4
  202. package/src/adapter.ts +97 -0
  203. package/src/adc-impersonation.ts +176 -0
  204. package/src/admin-domain.test.ts +254 -0
  205. package/src/admin-domain.ts +245 -0
  206. package/src/admin-memory.test.ts +151 -0
  207. package/src/agent/apply.test.ts +185 -0
  208. package/src/agent/apply.ts +124 -0
  209. package/src/agent/cli-provenance.test.ts +159 -0
  210. package/src/agent/cli-provenance.ts +191 -0
  211. package/src/agent/command.test.ts +77 -0
  212. package/src/agent/command.ts +564 -0
  213. package/src/agent/config.ts +86 -0
  214. package/src/agent/lock-config.test.ts +97 -0
  215. package/src/agent/lock.ts +110 -0
  216. package/src/agent/queue.test.ts +10 -0
  217. package/src/agent/queue.ts +91 -0
  218. package/src/agent/run.test.ts +382 -0
  219. package/src/agent/run.ts +295 -0
  220. package/src/agent/systemd.ts +209 -0
  221. package/src/agent/units.test.ts +223 -0
  222. package/src/agent/units.ts +163 -0
  223. package/src/azure-front-door.test.ts +119 -0
  224. package/src/azure-front-door.ts +56 -0
  225. package/src/bootstrap-token.ts +20 -0
  226. package/src/caddy.ts +112 -0
  227. package/src/cdn-copy.ts +91 -0
  228. package/src/cert-wait.test.ts +194 -0
  229. package/src/cert-wait.ts +163 -0
  230. package/src/checkpoint-secrets.test.ts +380 -0
  231. package/src/checkpoint-secrets.ts +283 -0
  232. package/src/cli-routing.test.ts +45 -0
  233. package/src/cli-routing.ts +43 -0
  234. package/src/cli.ts +1491 -0
  235. package/src/cloudsql-iam-user.ts +147 -0
  236. package/src/compose.ts +334 -0
  237. package/src/default-firewall.test.ts +411 -0
  238. package/src/default-firewall.ts +342 -0
  239. package/src/deployed-release.test.ts +231 -0
  240. package/src/deployed-release.ts +220 -0
  241. package/src/dns/cloudflare.ts +182 -0
  242. package/src/dns/groups.test.ts +76 -0
  243. package/src/dns/groups.ts +86 -0
  244. package/src/dns/index.ts +56 -0
  245. package/src/dns/manual.ts +130 -0
  246. package/src/dns/types.ts +27 -0
  247. package/src/gateway-credentials.test.ts +180 -0
  248. package/src/gateway-credentials.ts +144 -0
  249. package/src/gcloud-components.test.ts +76 -0
  250. package/src/gcloud-components.ts +85 -0
  251. package/src/gcloud-retry.ts +41 -0
  252. package/src/gcloud-runner.test.ts +61 -0
  253. package/src/gcloud.test.ts +99 -0
  254. package/src/gcloud.ts +322 -0
  255. package/src/gcp-names.ts +135 -0
  256. package/src/index.test.ts +332 -0
  257. package/src/index.ts +114 -0
  258. package/src/infra-contract.ts +32 -0
  259. package/src/install-job-runner.ts +166 -0
  260. package/src/install-job.test.ts +566 -0
  261. package/src/install-job.ts +542 -0
  262. package/src/install-state.test.ts +239 -0
  263. package/src/install-state.ts +583 -0
  264. package/src/internal-token.ts +35 -0
  265. package/src/lifecycle.ts +900 -0
  266. package/src/mcp-iap-stacks.test.ts +58 -0
  267. package/src/mcp-iap.test.ts +149 -0
  268. package/src/mcp-iap.ts +131 -0
  269. package/src/media-volume.test.ts +296 -0
  270. package/src/migration-runner.ts +365 -0
  271. package/src/operator-access-stacks.test.ts +55 -0
  272. package/src/operator-access.test.ts +332 -0
  273. package/src/operator-access.ts +554 -0
  274. package/src/ops-backend.test.ts +320 -0
  275. package/src/ops-backend.ts +414 -0
  276. package/src/provisioner-identity.test.ts +482 -0
  277. package/src/provisioner-identity.ts +407 -0
  278. package/src/redirects-emit.ts +175 -0
  279. package/src/region-data.ts +428 -0
  280. package/src/regions.test.ts +277 -0
  281. package/src/regions.ts +370 -0
  282. package/src/release-contract.ts +57 -0
  283. package/src/release-manifest.test.ts +293 -0
  284. package/src/release-manifest.ts +430 -0
  285. package/src/run-mode.ts +40 -0
  286. package/src/runtime-secrets.test.ts +627 -0
  287. package/src/runtime-secrets.ts +754 -0
  288. package/src/self-command.test.ts +14 -0
  289. package/src/self-command.ts +21 -0
  290. package/src/self-hosted-compose.test.ts +177 -0
  291. package/src/self-hosted-release.ts +240 -0
  292. package/src/self-hosted-roles.integration.test.ts +161 -0
  293. package/src/self-hosted-roles.test.ts +124 -0
  294. package/src/self-hosted-roles.ts +203 -0
  295. package/src/self-hosted-upgrade.test.ts +394 -0
  296. package/src/self-hosted-upgrade.ts +548 -0
  297. package/src/stack-contract-parity.test.ts +199 -0
  298. package/src/stack-contract.test.ts +391 -0
  299. package/src/stack-contract.ts +888 -0
  300. package/src/stack-converge-secrets.test.ts +421 -0
  301. package/src/stack-converge.test.ts +833 -0
  302. package/src/stack-converge.ts +909 -0
  303. package/src/staging-admin.test.ts +246 -0
  304. package/src/staging-admin.ts +324 -0
  305. package/src/state-attach.test.ts +211 -0
  306. package/src/state-attach.ts +132 -0
  307. package/src/state-backend.test.ts +145 -0
  308. package/src/state-backend.ts +262 -0
  309. package/src/state-command.ts +293 -0
  310. package/src/state-migrate.test.ts +479 -0
  311. package/src/state-migrate.ts +434 -0
  312. package/src/state-pulumi.ts +289 -0
  313. package/src/state-store.test.ts +212 -0
  314. package/src/state-store.ts +243 -0
  315. package/src/upgrade/app-roll.ts +486 -0
  316. package/src/upgrade/backup.ts +109 -0
  317. package/src/upgrade/converge.ts +437 -0
  318. package/src/upgrade/gcp-lookup.ts +167 -0
  319. package/src/upgrade/health.test.ts +279 -0
  320. package/src/upgrade/health.ts +305 -0
  321. package/src/upgrade/release-images.test.ts +119 -0
  322. package/src/upgrade/release-images.ts +391 -0
  323. package/src/upgrade/reporter.ts +46 -0
  324. package/src/upgrade/upgrade.test.ts +626 -0
  325. package/src/upgrade/upgrade.ts +249 -0
  326. package/src/upgrader-assess.test.ts +265 -0
  327. package/src/upgrader-assess.ts +233 -0
  328. package/src/upgrader-gcp.ts +123 -0
  329. package/src/upgrader-job.test.ts +408 -0
  330. package/src/upgrader-job.ts +499 -0
  331. package/src/upgrader-reporter.test.ts +46 -0
  332. package/src/upgrader-reporter.ts +105 -0
  333. package/src/upgrader-setup.test.ts +169 -0
  334. package/src/upgrader-setup.ts +355 -0
  335. package/src/wizard.ts +425 -0
  336. package/src/wizards/gcp-cost.ts +188 -0
  337. package/src/wizards/gcp-firebase.ts +72 -0
  338. package/src/wizards/gcp-install-job.ts +506 -0
  339. package/src/wizards/gcp-prompts.ts +26 -0
  340. package/src/wizards/gcp-pulumi.test.ts +246 -0
  341. package/src/wizards/gcp-pulumi.ts +516 -0
  342. package/src/wizards/gcp.ts +1935 -0
@@ -0,0 +1,77 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * The agent's database side on a running install (#721 maintainer
5
+ * decision): Postgres restarts once — on the first agent install, announced
6
+ * first — and never again once it publishes the agent's loopback port.
7
+ */
8
+
9
+ import { describe, expect, it } from "bun:test";
10
+ import type { CommandResult, HostRunner } from "../self-hosted-release.js";
11
+ import { applyDatabaseSide, POSTGRES_RESTART_NOTICE } from "./command.js";
12
+
13
+ function runner(published: string | null): HostRunner & { calls: string[] } {
14
+ const calls: string[] = [];
15
+ return {
16
+ calls,
17
+ async run(argv): Promise<CommandResult> {
18
+ const line = argv.join(" ");
19
+ calls.push(line);
20
+ if (line.endsWith("port postgres 5432")) {
21
+ return published === null
22
+ ? { exitCode: 1, stdout: "", stderr: "no port" }
23
+ : { exitCode: 0, stdout: `${published}\n`, stderr: "" };
24
+ }
25
+ return { exitCode: 0, stdout: "", stderr: "" };
26
+ },
27
+ };
28
+ }
29
+
30
+ const ctx = { caeloDir: "/srv/.caelo", convergeRoles: async () => ({ ok: true as const }) };
31
+
32
+ describe("applyDatabaseSide", () => {
33
+ it("skips the restart when Postgres already publishes the agent's port (re-run)", async () => {
34
+ const r = runner("127.0.0.1:15432");
35
+ const notices: string[] = [];
36
+ expect(await applyDatabaseSide(ctx, r, 15432, { notice: (m) => notices.push(m) })).toEqual({
37
+ ok: true,
38
+ restarted: false,
39
+ });
40
+ expect(r.calls.some((c) => c.includes(" up -d postgres"))).toBe(false);
41
+ expect(notices).toEqual([]);
42
+ });
43
+
44
+ it("restarts Postgres once, after the notice, on the first install", async () => {
45
+ const r = runner(null);
46
+ const notices: string[] = [];
47
+ const out = await applyDatabaseSide(ctx, r, 15432, {
48
+ notice: (m) => notices.push(m),
49
+ sleep: async () => {},
50
+ });
51
+ expect(out).toEqual({ ok: true, restarted: true });
52
+ expect(notices).toEqual([POSTGRES_RESTART_NOTICE]);
53
+ expect(POSTGRES_RESTART_NOTICE).toContain("Postgres restarts once, a few seconds of downtime");
54
+ expect(r.calls.filter((c) => c.includes(" up -d postgres"))).toHaveLength(1);
55
+ });
56
+
57
+ it("restarts when the port is published elsewhere than the configured one", async () => {
58
+ const r = runner("127.0.0.1:15433");
59
+ const out = await applyDatabaseSide(ctx, r, 15432, { notice: () => {}, sleep: async () => {} });
60
+ expect(out).toEqual({ ok: true, restarted: true });
61
+ });
62
+
63
+ it("does not touch Postgres when the login cannot be created", async () => {
64
+ const r = runner(null);
65
+ const out = await applyDatabaseSide(
66
+ {
67
+ ...ctx,
68
+ convergeRoles: async () => ({ ok: false as const, reachable: true, error: "denied" }),
69
+ },
70
+ r,
71
+ 15432,
72
+ { notice: () => {} },
73
+ );
74
+ expect(out.ok).toBe(false);
75
+ expect(r.calls).toEqual([]);
76
+ });
77
+ });
@@ -0,0 +1,564 @@
1
+ // SPDX-License-Identifier: MPL-2.0
2
+
3
+ /**
4
+ * `cms-provision agent <install|status|logs|uninstall>` and the two entry
5
+ * points the agent's own units use (`run`, `apply`) — the self-hosted host
6
+ * agent of #696 Phase 4c. The CLI hands in what it owns (config.json, the
7
+ * Compose renderer, the superuser psql) through {@link AgentCliContext};
8
+ * everything decided here is in agent/run.ts, agent/apply.ts and
9
+ * agent/systemd.ts, which are tested against fakes.
10
+ */
11
+
12
+ import { existsSync, readdirSync, rmSync } from "node:fs";
13
+ import { hostname } from "node:os";
14
+ import { dirname, join } from "node:path";
15
+ import { CAELO_VERSION, PLATFORM_UPDATE_TRIGGERS } from "@caelo-cms/shared";
16
+ import { z } from "zod";
17
+ import type { ReleaseImages } from "../compose.js";
18
+ import { compareSemver, fetchVerifiedReleaseManifest } from "../release-manifest.js";
19
+ import { selfCommand } from "../self-command.js";
20
+ import { bunHostRunner, type HostRunner, readRelease } from "../self-hosted-release.js";
21
+ import type { ConvergeOutcome } from "../self-hosted-roles.js";
22
+ import type { JobLog } from "../upgrader-reporter.js";
23
+ import { applyAgentUpdate } from "./apply.js";
24
+ import {
25
+ installVerifiedCli,
26
+ verifiedCliDir,
27
+ verifiedCliPath,
28
+ verifiedCliRoot,
29
+ } from "./cli-provenance.js";
30
+ import { type AgentConfig, agentConfigFor, agentDbConnection } from "./config.js";
31
+ import { acquireUpgradeLock } from "./lock.js";
32
+ import { loadAgentQueueOpener } from "./queue.js";
33
+ import { type AgentApplyOutcome, type AgentApplyRequest, runAgentOnce } from "./run.js";
34
+ import {
35
+ type AgentHost,
36
+ agentHostProblem,
37
+ agentLogsArgv,
38
+ agentUnitStatus,
39
+ currentAgentHost,
40
+ installAgentUnits,
41
+ installedAgentVersion,
42
+ repinAgentUnits,
43
+ uninstallAgentUnits,
44
+ } from "./systemd.js";
45
+ import { type AgentUnitSpec, agentUnitName } from "./units.js";
46
+
47
+ /** What the CLI owns that the agent commands need. */
48
+ export interface AgentCliContext {
49
+ readonly caeloDir: string;
50
+ readonly domain: string;
51
+ readonly imageMode: "release" | "dev" | undefined;
52
+ /** config.json → agent. */
53
+ agent(): AgentConfig | undefined;
54
+ /** Persist config.json → agent (undefined removes it). */
55
+ saveAgent(agent: AgentConfig | undefined): void;
56
+ /** Re-write the Compose file (with the agent's port) and bootstrap.sh. */
57
+ emitConfig(): void;
58
+ /** The role convergence as the superuser, including the agent's login. */
59
+ convergeRoles(): Promise<ConvergeOutcome>;
60
+ /** The Compose file for `images` from config.json. */
61
+ renderCompose(images: ReleaseImages): string;
62
+ randomSecret(): string;
63
+ }
64
+
65
+ const say = (m: string) => console.log(m);
66
+
67
+ /** The agent's log lines, for journald. */
68
+ const journalLog: JobLog = (entry) => {
69
+ const line = `[${entry.severity}] ${entry.message}`;
70
+ if (entry.severity === "ERROR") console.error(line);
71
+ else console.log(line);
72
+ };
73
+
74
+ function flag(argv: readonly string[], name: string): string | undefined {
75
+ const i = argv.indexOf(`--${name}`);
76
+ return i === -1 ? undefined : argv[i + 1];
77
+ }
78
+
79
+ /** The unit spec for this install, pinned to `version`. */
80
+ function unitSpec(ctx: AgentCliContext, version: string, cliPath?: string): AgentUnitSpec {
81
+ return {
82
+ ...(cliPath ? { cliPath } : {}),
83
+ name: agentUnitName(ctx.domain, ctx.caeloDir),
84
+ domain: ctx.domain,
85
+ version,
86
+ bunPath: process.execPath,
87
+ caeloDir: ctx.caeloDir,
88
+ workDir: dirname(ctx.caeloDir),
89
+ path: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin",
90
+ ...(process.env.DOCKER_HOST ? { dockerHost: process.env.DOCKER_HOST } : {}),
91
+ };
92
+ }
93
+
94
+ /** `docker compose -f <file> …`. */
95
+ function composeArgv(ctx: Pick<AgentCliContext, "caeloDir">, ...args: string[]): string[] {
96
+ return ["docker", "compose", "-f", `${ctx.caeloDir}/docker-compose.yml`, ...args];
97
+ }
98
+
99
+ async function postgresReady(
100
+ ctx: Pick<AgentCliContext, "caeloDir">,
101
+ runner: HostRunner,
102
+ ): Promise<boolean> {
103
+ const r = await runner.run(
104
+ composeArgv(ctx, "exec", "-T", "postgres", "pg_isready", "-U", "caelo"),
105
+ );
106
+ return r.exitCode === 0;
107
+ }
108
+
109
+ /** The notice printed before the one restart Postgres gets for the agent. */
110
+ export const POSTGRES_RESTART_NOTICE =
111
+ "Postgres restarts once, a few seconds of downtime: it is now also published on 127.0.0.1 for the update agent. The admin reconnects by itself.";
112
+
113
+ /**
114
+ * Whether the running Postgres container already publishes `port` on the
115
+ * loopback interface (`docker compose port postgres 5432`).
116
+ */
117
+ export async function postgresPublishedOnLoopback(
118
+ ctx: Pick<AgentCliContext, "caeloDir">,
119
+ runner: HostRunner,
120
+ port: number,
121
+ ): Promise<boolean> {
122
+ const r = await runner.run(composeArgv(ctx, "port", "postgres", "5432"));
123
+ return r.exitCode === 0 && r.stdout.trim().split("\n").includes(`127.0.0.1:${port}`);
124
+ }
125
+
126
+ /**
127
+ * Apply the agent's database side to a RUNNING install: the login, then —
128
+ * only the first time, when the container does not publish the agent's
129
+ * port yet — one `up -d postgres` to publish it (#721: Postgres restarts
130
+ * once, announced first). Re-running finds the port published and leaves
131
+ * Postgres alone; `agent run` / `agent apply` never get here.
132
+ */
133
+ export async function applyDatabaseSide(
134
+ ctx: Pick<AgentCliContext, "caeloDir" | "convergeRoles">,
135
+ runner: HostRunner,
136
+ port: number,
137
+ opts: {
138
+ readonly notice?: (m: string) => void;
139
+ readonly sleep?: (ms: number) => Promise<void>;
140
+ } = {},
141
+ ): Promise<{ ok: true; restarted: boolean } | { ok: false; error: string }> {
142
+ const roles = await ctx.convergeRoles();
143
+ if (!roles.ok) {
144
+ return { ok: false, error: `could not create the agent's database login: ${roles.error}` };
145
+ }
146
+ if (await postgresPublishedOnLoopback(ctx, runner, port)) return { ok: true, restarted: false };
147
+ (opts.notice ?? say)(POSTGRES_RESTART_NOTICE);
148
+ const up = await runner.run(composeArgv(ctx, "up", "-d", "postgres"));
149
+ if (up.exitCode !== 0) {
150
+ return {
151
+ ok: false,
152
+ error: `docker compose up -d postgres failed: ${up.stderr.trim().slice(0, 400)}`,
153
+ };
154
+ }
155
+ const sleep = opts.sleep ?? ((ms: number) => Bun.sleep(ms));
156
+ for (let i = 0; i < 30; i++) {
157
+ if (await postgresReady(ctx, runner)) return { ok: true, restarted: true };
158
+ await sleep(1000);
159
+ }
160
+ return { ok: false, error: "Postgres did not come back within 30 seconds" };
161
+ }
162
+
163
+ export type SetUpAgentResult =
164
+ | {
165
+ readonly kind: "installed";
166
+ readonly unit: string;
167
+ readonly version: string;
168
+ readonly warnings: readonly string[];
169
+ }
170
+ | { readonly kind: "skipped"; readonly reason: string }
171
+ | { readonly kind: "failed"; readonly error: string };
172
+
173
+ /**
174
+ * Set the agent up, or bring it up to date — idempotent, for `agent
175
+ * install`, `init` and `upgrade`. With `database: "live"` the install's
176
+ * Postgres runs and gets the login + port now; with `"fresh"` (init) the
177
+ * volume is new and bootstrap.sh creates the login on first start.
178
+ *
179
+ * Never pins an older release than the one already installed.
180
+ */
181
+ export async function setUpAgent(
182
+ ctx: AgentCliContext,
183
+ opts: {
184
+ readonly database: "live" | "fresh";
185
+ readonly requestedPort?: number;
186
+ /** `agent install` re-enables an uninstalled agent; init/upgrade respect it. */
187
+ readonly explicit: boolean;
188
+ readonly runner?: HostRunner;
189
+ readonly host?: AgentHost;
190
+ },
191
+ ): Promise<SetUpAgentResult> {
192
+ const runner = opts.runner ?? bunHostRunner;
193
+ const host = opts.host ?? currentAgentHost(runner);
194
+ const problem = await agentHostProblem(host);
195
+ if (problem) return { kind: "skipped", reason: problem };
196
+ if (ctx.imageMode === "dev") {
197
+ return {
198
+ kind: "skipped",
199
+ reason:
200
+ "this is a DEV MODE install (it runs a Caelo checkout); there are no releases to install.",
201
+ };
202
+ }
203
+ const existing = ctx.agent();
204
+ if (existing?.disabled && !opts.explicit) {
205
+ return {
206
+ kind: "skipped",
207
+ reason: `the update agent was uninstalled; \`${selfCommand("agent", "install")}\` sets it up again.`,
208
+ };
209
+ }
210
+ let agent: AgentConfig;
211
+ try {
212
+ agent = agentConfigFor(existing, {
213
+ ...(opts.requestedPort !== undefined ? { requestedPort: opts.requestedPort } : {}),
214
+ randomSecret: () => ctx.randomSecret(),
215
+ });
216
+ } catch (e) {
217
+ return { kind: "failed", error: (e as Error).message };
218
+ }
219
+ ctx.saveAgent(agent);
220
+ try {
221
+ ctx.emitConfig();
222
+ } catch (e) {
223
+ return { kind: "failed", error: (e as Error).message };
224
+ }
225
+ const warnings: string[] = [];
226
+ if (opts.database === "live") {
227
+ if (await postgresReady(ctx, runner)) {
228
+ const db = await applyDatabaseSide(ctx, runner, agent.dbPort);
229
+ if (!db.ok) return { kind: "failed", error: db.error };
230
+ } else {
231
+ warnings.push(
232
+ `Postgres is not running, so the agent's database login was not created yet. Start the install (\`docker compose -f ${ctx.caeloDir}/docker-compose.yml up -d\`) and run \`${selfCommand("agent", "install")}\` again.`,
233
+ );
234
+ }
235
+ }
236
+ const name = agentUnitName(ctx.domain, ctx.caeloDir);
237
+ const pinned = installedAgentVersion(host, name);
238
+ // Never back to an older release: a newer pin is one an update verified.
239
+ const newer = pinned !== null && compareSemver(pinned, CAELO_VERSION) > 0;
240
+ const version = newer && pinned ? pinned : CAELO_VERSION;
241
+ const verified = verifiedCliPath(verifiedCliDir(ctx.caeloDir, version));
242
+ const units = await installAgentUnits(
243
+ unitSpec(ctx, version, newer && existsSync(verified) ? verified : undefined),
244
+ host,
245
+ );
246
+ if (!units.ok) return { kind: "failed", error: units.error };
247
+ return { kind: "installed", unit: name, version, warnings: [...warnings, ...units.warnings] };
248
+ }
249
+
250
+ /** Print what {@link setUpAgent} did, for init / upgrade / agent install. */
251
+ export function reportAgentSetup(result: SetUpAgentResult): void {
252
+ if (result.kind === "installed") {
253
+ say(
254
+ `Update agent: ${result.unit}.timer runs Caelo ${result.version}'s agent every minute; Settings → Updates can install updates.`,
255
+ );
256
+ for (const w of result.warnings) console.warn(` ! ${w}`);
257
+ } else if (result.kind === "skipped") {
258
+ say(`Update agent: not set up — ${result.reason}`);
259
+ } else {
260
+ console.warn(
261
+ `Update agent: could not be set up — ${result.error}\n Retry with \`${selfCommand("agent", "install")}\`; updates still install with the upgrade command.`,
262
+ );
263
+ }
264
+ }
265
+
266
+ /** `agent <sub>`. */
267
+ export async function agentCommand(argv: readonly string[], ctx: AgentCliContext): Promise<number> {
268
+ const sub = argv[3];
269
+ switch (sub) {
270
+ case "install":
271
+ return agentInstall(argv, ctx);
272
+ case "status":
273
+ return agentStatus(ctx);
274
+ case "logs":
275
+ return agentLogs(argv, ctx);
276
+ case "uninstall":
277
+ return agentUninstall(ctx);
278
+ case "run":
279
+ return agentRun(ctx);
280
+ case "apply":
281
+ return agentApply(argv, ctx);
282
+ default:
283
+ console.error(
284
+ "Usage: cms-provision agent install [--port <loopback port>] | status | logs [--lines N] [--follow] | uninstall\n" +
285
+ "The update agent lets Settings → Updates install Caelo updates on this server (Linux + systemd).",
286
+ );
287
+ return 2;
288
+ }
289
+ }
290
+
291
+ async function agentInstall(argv: readonly string[], ctx: AgentCliContext): Promise<number> {
292
+ const portArg = flag(argv, "port");
293
+ const requestedPort = portArg === undefined ? undefined : Number(portArg);
294
+ const result = await setUpAgent(ctx, {
295
+ database: "live",
296
+ explicit: true,
297
+ ...(requestedPort !== undefined ? { requestedPort } : {}),
298
+ });
299
+ reportAgentSetup(result);
300
+ if (result.kind !== "installed") return result.kind === "skipped" ? 2 : 1;
301
+ say(`Logs: \`${selfCommand("agent", "logs")}\` — status: \`${selfCommand("agent", "status")}\``);
302
+ return 0;
303
+ }
304
+
305
+ async function agentStatus(ctx: AgentCliContext): Promise<number> {
306
+ const host = currentAgentHost(bunHostRunner);
307
+ const name = agentUnitName(ctx.domain, ctx.caeloDir);
308
+ const agent = ctx.agent();
309
+ const version = installedAgentVersion(host, name);
310
+ say(`Install: ${ctx.domain} (${ctx.caeloDir})`);
311
+ say(
312
+ `Agent: ${version ? `${name} — runs Caelo ${version}` : agent?.disabled ? "uninstalled" : "not installed"}`,
313
+ );
314
+ if (agent && !agent.disabled) say(`Database: 127.0.0.1:${agent.dbPort} (login caelo_agent)`);
315
+ if (!version) {
316
+ say(`Set it up with \`${selfCommand("agent", "install")}\`.`);
317
+ return 1;
318
+ }
319
+ say("");
320
+ say(await agentUnitStatus(host, name));
321
+ return 0;
322
+ }
323
+
324
+ async function agentLogs(argv: readonly string[], ctx: AgentCliContext): Promise<number> {
325
+ const lines = Number(flag(argv, "lines") ?? "200");
326
+ const proc = Bun.spawn(
327
+ agentLogsArgv(agentUnitName(ctx.domain, ctx.caeloDir), {
328
+ lines: Number.isInteger(lines) && lines > 0 ? lines : 200,
329
+ follow: argv.includes("--follow") || argv.includes("-f"),
330
+ }),
331
+ { stdout: "inherit", stderr: "inherit" },
332
+ );
333
+ return proc.exited;
334
+ }
335
+
336
+ async function agentUninstall(ctx: AgentCliContext): Promise<number> {
337
+ const runner = bunHostRunner;
338
+ const host = currentAgentHost(runner);
339
+ const result = await uninstallAgentUnits(host, agentUnitName(ctx.domain, ctx.caeloDir));
340
+ for (const w of result.ok ? result.warnings : []) console.warn(` ! ${w}`);
341
+ const agent = ctx.agent();
342
+ if (agent) {
343
+ // Off until `agent install`: init/upgrade leave it alone, and the
344
+ // Compose file stops publishing the port.
345
+ ctx.saveAgent({ ...agent, disabled: true });
346
+ ctx.emitConfig();
347
+ // Postgres is not restarted for this (it restarts only on the first
348
+ // agent install): the loopback port goes away with its next restart.
349
+ say(
350
+ `The Compose file no longer publishes Postgres on 127.0.0.1:${agent.dbPort}; that takes effect the next time Postgres restarts.`,
351
+ );
352
+ }
353
+ say(
354
+ "Update agent removed. Settings → Updates shows the upgrade command again; " +
355
+ `\`${selfCommand("agent", "install")}\` brings the agent back.`,
356
+ );
357
+ return 0;
358
+ }
359
+
360
+ /** Why an install would fail on this host right now (the heartbeat's `problem`). */
361
+ async function hostPreflight(ctx: AgentCliContext, runner: HostRunner): Promise<string | null> {
362
+ if (ctx.imageMode === "dev") {
363
+ return "This is a DEV MODE install (it runs a Caelo checkout); update the checkout instead.";
364
+ }
365
+ const docker = await runner.run(["docker", "info", "--format", "{{.ServerVersion}}"]);
366
+ if (docker.exitCode !== 0) {
367
+ return `Docker is not reachable for the agent's user (${docker.stderr.trim().slice(0, 300) || `exit ${docker.exitCode}`}). The user that runs the agent must be able to run \`docker\` (the docker group).`;
368
+ }
369
+ const npm = await runner.run(["npm", "--version"]);
370
+ if (npm.exitCode !== 0) {
371
+ return "npm is not installed on the server (or not on the agent's PATH); the agent needs it to verify the provenance of the Caelo release it installs. Install Node.js + npm, then run the agent install command again.";
372
+ }
373
+ const cosign = await runner.run(["cosign", "version"]);
374
+ if (cosign.exitCode !== 0) {
375
+ return "cosign is not installed on the server (or not on the agent's PATH), so release signatures cannot be verified. Install cosign, then run the agent install command again.";
376
+ }
377
+ return null;
378
+ }
379
+
380
+ /** The last `limit` characters of a stream, while copying it to `out`. */
381
+ async function teeTail(
382
+ stream: ReadableStream<Uint8Array>,
383
+ out: (chunk: Uint8Array) => void,
384
+ limit: number,
385
+ ): Promise<string> {
386
+ let tail = "";
387
+ const decoder = new TextDecoder();
388
+ for await (const chunk of stream as unknown as AsyncIterable<Uint8Array>) {
389
+ out(chunk);
390
+ tail = (tail + decoder.decode(chunk, { stream: true })).slice(-limit);
391
+ }
392
+ return tail;
393
+ }
394
+
395
+ /** Env var naming the parent agent's CLI for the child (so it is kept). */
396
+ const PARENT_CLI_ENV = "CAELO_AGENT_PARENT_CLI";
397
+
398
+ /**
399
+ * Remove the verified CLI installs of older releases (each is the
400
+ * provisioning package with its dependencies), keeping `keep` — the
401
+ * child's own and the still-running parent's.
402
+ */
403
+ function pruneVerifiedClis(caeloDir: string, keep: readonly (string | undefined)[]): void {
404
+ const root = verifiedCliRoot(caeloDir);
405
+ let entries: string[];
406
+ try {
407
+ entries = readdirSync(root);
408
+ } catch {
409
+ return;
410
+ }
411
+ for (const entry of entries) {
412
+ const dir = join(root, entry);
413
+ if (keep.some((k) => k?.startsWith(`${dir}/`))) continue;
414
+ rmSync(dir, { recursive: true, force: true });
415
+ }
416
+ }
417
+
418
+ /** Start the target release's `agent apply` and wait for it. */
419
+ async function spawnApply(request: AgentApplyRequest): Promise<AgentApplyOutcome> {
420
+ // The verified install (agent/cli-provenance.ts) — never `bun x`, which
421
+ // would fetch an unverified copy.
422
+ const argv = [
423
+ process.execPath,
424
+ request.cliPath,
425
+ "agent",
426
+ "apply",
427
+ "--request",
428
+ request.requestId,
429
+ "--version",
430
+ request.targetVersion,
431
+ "--trigger",
432
+ request.trigger,
433
+ "--admin-digest",
434
+ request.digests.admin,
435
+ "--gateway-digest",
436
+ request.digests.gateway,
437
+ ];
438
+ try {
439
+ const proc = Bun.spawn(argv, {
440
+ stdout: "pipe",
441
+ stderr: "pipe",
442
+ // Tells the child which verified install it must not prune (ours).
443
+ env: { ...process.env, [PARENT_CLI_ENV]: process.argv[1] ?? "" },
444
+ });
445
+ const [out, err] = await Promise.all([
446
+ teeTail(proc.stdout, (c) => process.stdout.write(c), 1500),
447
+ teeTail(proc.stderr, (c) => process.stderr.write(c), 1500),
448
+ ]);
449
+ const exitCode = await proc.exited;
450
+ return { exitCode, output: (err.trim() || out.trim()).slice(-1500) };
451
+ } catch (e) {
452
+ return { exitCode: 127, output: e instanceof Error ? e.message : String(e) };
453
+ }
454
+ }
455
+
456
+ async function agentRun(ctx: AgentCliContext): Promise<number> {
457
+ const agent = ctx.agent();
458
+ if (!agent || agent.disabled) {
459
+ console.error(
460
+ `The update agent is not set up for ${ctx.domain}; run \`${selfCommand("agent", "install")}\`.`,
461
+ );
462
+ return 2;
463
+ }
464
+ const open = await loadAgentQueueOpener();
465
+ let opened: Awaited<ReturnType<typeof open>>;
466
+ try {
467
+ opened = await open(agentDbConnection(agent));
468
+ } catch (e) {
469
+ journalLog({
470
+ severity: "ERROR",
471
+ message: `cannot reach the install's database on 127.0.0.1:${agent.dbPort} as caelo_agent: ${(e as Error).message}. Is the install running? (\`${selfCommand("agent", "install")}\` re-creates the login.)`,
472
+ });
473
+ return 1;
474
+ }
475
+ try {
476
+ const result = await runAgentOnce({
477
+ queue: opened.queue,
478
+ agentVersion: CAELO_VERSION,
479
+ host: hostname() || "server",
480
+ preflight: () => hostPreflight(ctx, bunHostRunner),
481
+ readInstalled: () => readRelease(ctx.caeloDir),
482
+ fetchManifest: (version) => fetchVerifiedReleaseManifest(version),
483
+ lock: () => acquireUpgradeLock(ctx.caeloDir, "the update agent"),
484
+ verifyTargetCli: (version) =>
485
+ installVerifiedCli(version, verifiedCliDir(ctx.caeloDir, version), {
486
+ runner: bunHostRunner,
487
+ }),
488
+ applyTarget: spawnApply,
489
+ now: () => new Date(),
490
+ log: journalLog,
491
+ });
492
+ return result.exitCode;
493
+ } finally {
494
+ await opened.close();
495
+ }
496
+ }
497
+
498
+ const digest = z.string().regex(/^sha256:[0-9a-f]{64}$/);
499
+ const applyArgs = z.object({
500
+ request: z.string().uuid(),
501
+ version: z.string().regex(/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$/),
502
+ trigger: z.enum(PLATFORM_UPDATE_TRIGGERS),
503
+ adminDigest: digest,
504
+ gatewayDigest: digest,
505
+ });
506
+
507
+ async function agentApply(argv: readonly string[], ctx: AgentCliContext): Promise<number> {
508
+ const parsed = applyArgs.safeParse({
509
+ request: flag(argv, "request"),
510
+ version: flag(argv, "version"),
511
+ trigger: flag(argv, "trigger"),
512
+ adminDigest: flag(argv, "admin-digest"),
513
+ gatewayDigest: flag(argv, "gateway-digest"),
514
+ });
515
+ if (!parsed.success) {
516
+ console.error(
517
+ `agent apply is started by the update agent, not by hand (${parsed.error.issues.map((i) => i.path.join(".")).join(", ")}).`,
518
+ );
519
+ return 2;
520
+ }
521
+ const args = parsed.data;
522
+ const agent = ctx.agent();
523
+ if (!agent) {
524
+ console.error("The update agent is not set up for this install.");
525
+ return 2;
526
+ }
527
+ const open = await loadAgentQueueOpener();
528
+ const opened = await open(agentDbConnection(agent));
529
+ try {
530
+ const { upgradeSelfHosted } = await import("../self-hosted-upgrade.js");
531
+ const host = currentAgentHost(bunHostRunner);
532
+ const { exitCode } = await applyAgentUpdate({
533
+ queue: opened.queue,
534
+ request: { id: args.request, targetVersion: args.version, trigger: args.trigger },
535
+ agentVersion: CAELO_VERSION,
536
+ upgrade: (log) =>
537
+ upgradeSelfHosted(
538
+ {
539
+ version: args.version,
540
+ expectedDigests: { admin: args.adminDigest, gateway: args.gatewayDigest },
541
+ },
542
+ {
543
+ caeloDir: ctx.caeloDir,
544
+ runner: bunHostRunner,
545
+ renderCompose: (images) => ctx.renderCompose(images),
546
+ log,
547
+ },
548
+ ),
549
+ selfUpdate: async () => {
550
+ // This process runs from the verified install the parent made; the
551
+ // agent runs from it from now on (no unverified `bun x`).
552
+ const own = process.argv[1];
553
+ const r = await repinAgentUnits(unitSpec(ctx, CAELO_VERSION, own), host);
554
+ if (!r.ok) return { ok: false, error: r.error };
555
+ pruneVerifiedClis(ctx.caeloDir, [own, process.env[PARENT_CLI_ENV]]);
556
+ return { ok: true };
557
+ },
558
+ log: journalLog,
559
+ });
560
+ return exitCode;
561
+ } finally {
562
+ await opened.close();
563
+ }
564
+ }