@omnitron-dev/omnitron 0.2.0 → 0.3.1

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 (1299) hide show
  1. package/README.md +4 -0
  2. package/dist/BUILD.json +5 -0
  3. package/dist/cli/omnitron.js +375 -29
  4. package/dist/cli/omnitron.js.map +1 -1
  5. package/dist/cluster/cluster.rpc-service.d.ts +40 -0
  6. package/dist/cluster/cluster.rpc-service.d.ts.map +1 -1
  7. package/dist/cluster/cluster.rpc-service.js +49 -3
  8. package/dist/cluster/cluster.rpc-service.js.map +1 -1
  9. package/dist/cluster/config-sync.d.ts.map +1 -1
  10. package/dist/cluster/config-sync.js.map +1 -1
  11. package/dist/cluster/leader-election.d.ts +90 -2
  12. package/dist/cluster/leader-election.d.ts.map +1 -1
  13. package/dist/cluster/leader-election.js +176 -22
  14. package/dist/cluster/leader-election.js.map +1 -1
  15. package/dist/cluster/mesh-link.d.ts +136 -0
  16. package/dist/cluster/mesh-link.d.ts.map +1 -0
  17. package/dist/cluster/mesh-link.js +212 -0
  18. package/dist/cluster/mesh-link.js.map +1 -0
  19. package/dist/cluster/mesh.d.ts +59 -0
  20. package/dist/cluster/mesh.d.ts.map +1 -0
  21. package/dist/cluster/mesh.js +117 -0
  22. package/dist/cluster/mesh.js.map +1 -0
  23. package/dist/cluster/slave-connector.d.ts +193 -9
  24. package/dist/cluster/slave-connector.d.ts.map +1 -1
  25. package/dist/cluster/slave-connector.js +409 -37
  26. package/dist/cluster/slave-connector.js.map +1 -1
  27. package/dist/commands/audit.d.ts +65 -0
  28. package/dist/commands/audit.d.ts.map +1 -0
  29. package/dist/commands/audit.js +233 -0
  30. package/dist/commands/audit.js.map +1 -0
  31. package/dist/commands/backup.d.ts +4 -0
  32. package/dist/commands/backup.d.ts.map +1 -1
  33. package/dist/commands/backup.js +255 -33
  34. package/dist/commands/backup.js.map +1 -1
  35. package/dist/commands/cluster.d.ts.map +1 -1
  36. package/dist/commands/cluster.js +78 -47
  37. package/dist/commands/cluster.js.map +1 -1
  38. package/dist/commands/daemon-cmd.d.ts +7 -1
  39. package/dist/commands/daemon-cmd.d.ts.map +1 -1
  40. package/dist/commands/daemon-cmd.js +87 -21
  41. package/dist/commands/daemon-cmd.js.map +1 -1
  42. package/dist/commands/daemon-required.d.ts +34 -0
  43. package/dist/commands/daemon-required.d.ts.map +1 -0
  44. package/dist/commands/daemon-required.js +92 -0
  45. package/dist/commands/daemon-required.js.map +1 -0
  46. package/dist/commands/deploy.d.ts +70 -8
  47. package/dist/commands/deploy.d.ts.map +1 -1
  48. package/dist/commands/deploy.js +99 -64
  49. package/dist/commands/deploy.js.map +1 -1
  50. package/dist/commands/discover.d.ts +25 -4
  51. package/dist/commands/discover.d.ts.map +1 -1
  52. package/dist/commands/discover.js +155 -102
  53. package/dist/commands/discover.js.map +1 -1
  54. package/dist/commands/doctor.d.ts +293 -0
  55. package/dist/commands/doctor.d.ts.map +1 -0
  56. package/dist/commands/doctor.js +1706 -0
  57. package/dist/commands/doctor.js.map +1 -0
  58. package/dist/commands/env.d.ts +8 -1
  59. package/dist/commands/env.d.ts.map +1 -1
  60. package/dist/commands/env.js +13 -8
  61. package/dist/commands/env.js.map +1 -1
  62. package/dist/commands/exec.d.ts.map +1 -1
  63. package/dist/commands/exec.js +3 -2
  64. package/dist/commands/exec.js.map +1 -1
  65. package/dist/commands/fleet-asking.d.ts +87 -0
  66. package/dist/commands/fleet-asking.d.ts.map +1 -0
  67. package/dist/commands/fleet-asking.js +176 -0
  68. package/dist/commands/fleet-asking.js.map +1 -0
  69. package/dist/commands/fleet-recover.d.ts +2 -0
  70. package/dist/commands/fleet-recover.d.ts.map +1 -0
  71. package/dist/commands/fleet-recover.js +30 -0
  72. package/dist/commands/fleet-recover.js.map +1 -0
  73. package/dist/commands/fleet.d.ts +18 -0
  74. package/dist/commands/fleet.d.ts.map +1 -1
  75. package/dist/commands/fleet.js +314 -74
  76. package/dist/commands/fleet.js.map +1 -1
  77. package/dist/commands/health-check.d.ts +8 -6
  78. package/dist/commands/health-check.d.ts.map +1 -1
  79. package/dist/commands/health-check.js +26 -23
  80. package/dist/commands/health-check.js.map +1 -1
  81. package/dist/commands/health.d.ts.map +1 -1
  82. package/dist/commands/health.js +22 -8
  83. package/dist/commands/health.js.map +1 -1
  84. package/dist/commands/host-ingress.d.ts +3 -0
  85. package/dist/commands/host-ingress.d.ts.map +1 -0
  86. package/dist/commands/host-ingress.js +31 -0
  87. package/dist/commands/host-ingress.js.map +1 -0
  88. package/dist/commands/infra.d.ts +42 -1
  89. package/dist/commands/infra.d.ts.map +1 -1
  90. package/dist/commands/infra.js +418 -49
  91. package/dist/commands/infra.js.map +1 -1
  92. package/dist/commands/init.d.ts.map +1 -1
  93. package/dist/commands/init.js +21 -36
  94. package/dist/commands/init.js.map +1 -1
  95. package/dist/commands/inspect-graph.d.ts.map +1 -1
  96. package/dist/commands/inspect-graph.js +2 -6
  97. package/dist/commands/inspect-graph.js.map +1 -1
  98. package/dist/commands/inspect.d.ts.map +1 -1
  99. package/dist/commands/inspect.js +110 -14
  100. package/dist/commands/inspect.js.map +1 -1
  101. package/dist/commands/k8s.d.ts.map +1 -1
  102. package/dist/commands/k8s.js +23 -18
  103. package/dist/commands/k8s.js.map +1 -1
  104. package/dist/commands/kb.d.ts.map +1 -1
  105. package/dist/commands/kb.js +88 -29
  106. package/dist/commands/kb.js.map +1 -1
  107. package/dist/commands/list.d.ts.map +1 -1
  108. package/dist/commands/list.js +21 -9
  109. package/dist/commands/list.js.map +1 -1
  110. package/dist/commands/log-health.d.ts +120 -0
  111. package/dist/commands/log-health.d.ts.map +1 -0
  112. package/dist/commands/log-health.js +212 -0
  113. package/dist/commands/log-health.js.map +1 -0
  114. package/dist/commands/logs.d.ts +96 -2
  115. package/dist/commands/logs.d.ts.map +1 -1
  116. package/dist/commands/logs.js +522 -196
  117. package/dist/commands/logs.js.map +1 -1
  118. package/dist/commands/metrics.d.ts.map +1 -1
  119. package/dist/commands/metrics.js +35 -17
  120. package/dist/commands/metrics.js.map +1 -1
  121. package/dist/commands/monit.d.ts.map +1 -1
  122. package/dist/commands/monit.js +3 -2
  123. package/dist/commands/monit.js.map +1 -1
  124. package/dist/commands/node.d.ts +56 -0
  125. package/dist/commands/node.d.ts.map +1 -1
  126. package/dist/commands/node.js +297 -69
  127. package/dist/commands/node.js.map +1 -1
  128. package/dist/commands/output.d.ts +10 -0
  129. package/dist/commands/output.d.ts.map +1 -1
  130. package/dist/commands/output.js +41 -1
  131. package/dist/commands/output.js.map +1 -1
  132. package/dist/commands/pipeline.d.ts.map +1 -1
  133. package/dist/commands/pipeline.js +33 -19
  134. package/dist/commands/pipeline.js.map +1 -1
  135. package/dist/commands/project.d.ts +19 -0
  136. package/dist/commands/project.d.ts.map +1 -1
  137. package/dist/commands/project.js +125 -27
  138. package/dist/commands/project.js.map +1 -1
  139. package/dist/commands/release-store.d.ts +28 -0
  140. package/dist/commands/release-store.d.ts.map +1 -0
  141. package/dist/commands/release-store.js +158 -0
  142. package/dist/commands/release-store.js.map +1 -0
  143. package/dist/commands/release.d.ts +61 -0
  144. package/dist/commands/release.d.ts.map +1 -0
  145. package/dist/commands/release.js +555 -0
  146. package/dist/commands/release.js.map +1 -0
  147. package/dist/commands/reload.d.ts +16 -0
  148. package/dist/commands/reload.d.ts.map +1 -1
  149. package/dist/commands/reload.js +21 -5
  150. package/dist/commands/reload.js.map +1 -1
  151. package/dist/commands/remote.d.ts +40 -0
  152. package/dist/commands/remote.d.ts.map +1 -1
  153. package/dist/commands/remote.js +146 -35
  154. package/dist/commands/remote.js.map +1 -1
  155. package/dist/commands/restart.d.ts +16 -0
  156. package/dist/commands/restart.d.ts.map +1 -1
  157. package/dist/commands/restart.js +21 -5
  158. package/dist/commands/restart.js.map +1 -1
  159. package/dist/commands/scale.d.ts.map +1 -1
  160. package/dist/commands/scale.js +4 -3
  161. package/dist/commands/scale.js.map +1 -1
  162. package/dist/commands/secret.d.ts +54 -0
  163. package/dist/commands/secret.d.ts.map +1 -1
  164. package/dist/commands/secret.js +236 -34
  165. package/dist/commands/secret.js.map +1 -1
  166. package/dist/commands/service-units.d.ts +181 -0
  167. package/dist/commands/service-units.d.ts.map +1 -0
  168. package/dist/commands/service-units.js +291 -0
  169. package/dist/commands/service-units.js.map +1 -0
  170. package/dist/commands/service.d.ts +39 -0
  171. package/dist/commands/service.d.ts.map +1 -0
  172. package/dist/commands/service.js +370 -0
  173. package/dist/commands/service.js.map +1 -0
  174. package/dist/commands/spinner.d.ts +29 -0
  175. package/dist/commands/spinner.d.ts.map +1 -0
  176. package/dist/commands/spinner.js +44 -0
  177. package/dist/commands/spinner.js.map +1 -0
  178. package/dist/commands/stack.d.ts +49 -1
  179. package/dist/commands/stack.d.ts.map +1 -1
  180. package/dist/commands/stack.js +325 -12
  181. package/dist/commands/stack.js.map +1 -1
  182. package/dist/commands/start.d.ts +16 -0
  183. package/dist/commands/start.d.ts.map +1 -1
  184. package/dist/commands/start.js +20 -3
  185. package/dist/commands/start.js.map +1 -1
  186. package/dist/commands/status.d.ts +9 -0
  187. package/dist/commands/status.d.ts.map +1 -1
  188. package/dist/commands/status.js +45 -29
  189. package/dist/commands/status.js.map +1 -1
  190. package/dist/commands/stop.d.ts +16 -0
  191. package/dist/commands/stop.d.ts.map +1 -1
  192. package/dist/commands/stop.js +34 -6
  193. package/dist/commands/stop.js.map +1 -1
  194. package/dist/commands/tor.d.ts +14 -7
  195. package/dist/commands/tor.d.ts.map +1 -1
  196. package/dist/commands/tor.js +113 -33
  197. package/dist/commands/tor.js.map +1 -1
  198. package/dist/commands/up.d.ts +85 -1
  199. package/dist/commands/up.d.ts.map +1 -1
  200. package/dist/commands/up.js +180 -32
  201. package/dist/commands/up.js.map +1 -1
  202. package/dist/commands/webapp.d.ts.map +1 -1
  203. package/dist/commands/webapp.js +2 -1
  204. package/dist/commands/webapp.js.map +1 -1
  205. package/dist/config/defaults.d.ts +16 -0
  206. package/dist/config/defaults.d.ts.map +1 -1
  207. package/dist/config/defaults.js +26 -3
  208. package/dist/config/defaults.js.map +1 -1
  209. package/dist/config/define-ecosystem.d.ts.map +1 -1
  210. package/dist/config/define-ecosystem.js +2 -0
  211. package/dist/config/define-ecosystem.js.map +1 -1
  212. package/dist/config/index.d.ts +1 -1
  213. package/dist/config/index.d.ts.map +1 -1
  214. package/dist/config/index.js +1 -1
  215. package/dist/config/index.js.map +1 -1
  216. package/dist/config/loader.d.ts +45 -1
  217. package/dist/config/loader.d.ts.map +1 -1
  218. package/dist/config/loader.js +64 -6
  219. package/dist/config/loader.js.map +1 -1
  220. package/dist/config/omnitron-config.schema.d.ts.map +1 -1
  221. package/dist/config/omnitron-config.schema.js +16 -0
  222. package/dist/config/omnitron-config.schema.js.map +1 -1
  223. package/dist/config/types.d.ts +416 -44
  224. package/dist/config/types.d.ts.map +1 -1
  225. package/dist/daemon/config-reload.d.ts +27 -0
  226. package/dist/daemon/config-reload.d.ts.map +1 -0
  227. package/dist/daemon/config-reload.js +42 -0
  228. package/dist/daemon/config-reload.js.map +1 -0
  229. package/dist/daemon/daemon-client.d.ts +136 -2
  230. package/dist/daemon/daemon-client.d.ts.map +1 -1
  231. package/dist/daemon/daemon-client.js +307 -21
  232. package/dist/daemon/daemon-client.js.map +1 -1
  233. package/dist/daemon/daemon-entry.js +90 -30
  234. package/dist/daemon/daemon-entry.js.map +1 -1
  235. package/dist/daemon/daemon-scheduler.d.ts +4 -0
  236. package/dist/daemon/daemon-scheduler.d.ts.map +1 -1
  237. package/dist/daemon/daemon-scheduler.js +137 -7
  238. package/dist/daemon/daemon-scheduler.js.map +1 -1
  239. package/dist/daemon/daemon-state-store.service.d.ts +263 -0
  240. package/dist/daemon/daemon-state-store.service.d.ts.map +1 -0
  241. package/dist/daemon/daemon-state-store.service.js +446 -0
  242. package/dist/daemon/daemon-state-store.service.js.map +1 -0
  243. package/dist/daemon/daemon.d.ts +194 -4
  244. package/dist/daemon/daemon.d.ts.map +1 -1
  245. package/dist/daemon/daemon.js +1160 -157
  246. package/dist/daemon/daemon.js.map +1 -1
  247. package/dist/daemon/daemon.module.d.ts.map +1 -1
  248. package/dist/daemon/daemon.module.js +163 -85
  249. package/dist/daemon/daemon.module.js.map +1 -1
  250. package/dist/daemon/daemon.rpc-service.d.ts +90 -1
  251. package/dist/daemon/daemon.rpc-service.d.ts.map +1 -1
  252. package/dist/daemon/daemon.rpc-service.js +439 -97
  253. package/dist/daemon/daemon.rpc-service.js.map +1 -1
  254. package/dist/daemon/pid-manager.d.ts +16 -3
  255. package/dist/daemon/pid-manager.d.ts.map +1 -1
  256. package/dist/daemon/pid-manager.js +31 -4
  257. package/dist/daemon/pid-manager.js.map +1 -1
  258. package/dist/daemon/spawn-daemon.d.ts +73 -0
  259. package/dist/daemon/spawn-daemon.d.ts.map +1 -0
  260. package/dist/daemon/spawn-daemon.js +101 -0
  261. package/dist/daemon/spawn-daemon.js.map +1 -0
  262. package/dist/daemon/state-store.d.ts +65 -3
  263. package/dist/daemon/state-store.d.ts.map +1 -1
  264. package/dist/daemon/state-store.js +100 -58
  265. package/dist/daemon/state-store.js.map +1 -1
  266. package/dist/database/connection.d.ts +71 -0
  267. package/dist/database/connection.d.ts.map +1 -0
  268. package/dist/database/connection.js +129 -0
  269. package/dist/database/connection.js.map +1 -0
  270. package/dist/database/dialect.d.ts +60 -0
  271. package/dist/database/dialect.d.ts.map +1 -0
  272. package/dist/database/dialect.js +80 -0
  273. package/dist/database/dialect.js.map +1 -0
  274. package/dist/database/migrate.d.ts +5 -2
  275. package/dist/database/migrate.d.ts.map +1 -1
  276. package/dist/database/migrate.js +23 -33
  277. package/dist/database/migrate.js.map +1 -1
  278. package/dist/database/migration-runner.d.ts +50 -0
  279. package/dist/database/migration-runner.d.ts.map +1 -0
  280. package/dist/database/migration-runner.js +143 -0
  281. package/dist/database/migration-runner.js.map +1 -0
  282. package/dist/database/migrations/006_login_throttle.d.ts +17 -0
  283. package/dist/database/migrations/006_login_throttle.d.ts.map +1 -0
  284. package/dist/database/migrations/006_login_throttle.js +33 -0
  285. package/dist/database/migrations/006_login_throttle.js.map +1 -0
  286. package/dist/database/migrations/007_sync_ingested.d.ts +21 -0
  287. package/dist/database/migrations/007_sync_ingested.d.ts.map +1 -0
  288. package/dist/database/migrations/007_sync_ingested.js +39 -0
  289. package/dist/database/migrations/007_sync_ingested.js.map +1 -0
  290. package/dist/database/migrations/008_node_health_version_text.d.ts +24 -0
  291. package/dist/database/migrations/008_node_health_version_text.d.ts.map +1 -0
  292. package/dist/database/migrations/008_node_health_version_text.js +35 -0
  293. package/dist/database/migrations/008_node_health_version_text.js.map +1 -0
  294. package/dist/database/migrations/009_node_health_unmeasured.d.ts +40 -0
  295. package/dist/database/migrations/009_node_health_unmeasured.d.ts.map +1 -0
  296. package/dist/database/migrations/009_node_health_unmeasured.js +63 -0
  297. package/dist/database/migrations/009_node_health_unmeasured.js.map +1 -0
  298. package/dist/database/migrations/010_metrics_raw_had_no_reader.d.ts +22 -0
  299. package/dist/database/migrations/010_metrics_raw_had_no_reader.d.ts.map +1 -0
  300. package/dist/database/migrations/010_metrics_raw_had_no_reader.js +26 -0
  301. package/dist/database/migrations/010_metrics_raw_had_no_reader.js.map +1 -0
  302. package/dist/database/migrations/011_alerts_nobody_had_to_write.d.ts +56 -0
  303. package/dist/database/migrations/011_alerts_nobody_had_to_write.d.ts.map +1 -0
  304. package/dist/database/migrations/011_alerts_nobody_had_to_write.js +79 -0
  305. package/dist/database/migrations/011_alerts_nobody_had_to_write.js.map +1 -0
  306. package/dist/database/migrations/012_a_disk_nobody_watched.d.ts +26 -0
  307. package/dist/database/migrations/012_a_disk_nobody_watched.d.ts.map +1 -0
  308. package/dist/database/migrations/012_a_disk_nobody_watched.js +40 -0
  309. package/dist/database/migrations/012_a_disk_nobody_watched.js.map +1 -0
  310. package/dist/database/migrations/013_an_alarm_delivered.d.ts +14 -0
  311. package/dist/database/migrations/013_an_alarm_delivered.d.ts.map +1 -0
  312. package/dist/database/migrations/013_an_alarm_delivered.js +28 -0
  313. package/dist/database/migrations/013_an_alarm_delivered.js.map +1 -0
  314. package/dist/database/migrations/index.d.ts +21 -0
  315. package/dist/database/migrations/index.d.ts.map +1 -0
  316. package/dist/database/migrations/index.js +47 -0
  317. package/dist/database/migrations/index.js.map +1 -0
  318. package/dist/database/schema.d.ts +27 -11
  319. package/dist/database/schema.d.ts.map +1 -1
  320. package/dist/database/sqlite-date-binding.d.ts +71 -0
  321. package/dist/database/sqlite-date-binding.d.ts.map +1 -0
  322. package/dist/database/sqlite-date-binding.js +174 -0
  323. package/dist/database/sqlite-date-binding.js.map +1 -0
  324. package/dist/execution/data-channel.d.ts +48 -0
  325. package/dist/execution/data-channel.d.ts.map +1 -0
  326. package/dist/execution/data-channel.js +55 -0
  327. package/dist/execution/data-channel.js.map +1 -0
  328. package/dist/execution/execution.service.d.ts +146 -0
  329. package/dist/execution/execution.service.d.ts.map +1 -1
  330. package/dist/execution/execution.service.js +330 -10
  331. package/dist/execution/execution.service.js.map +1 -1
  332. package/dist/index.d.ts +1 -1
  333. package/dist/index.d.ts.map +1 -1
  334. package/dist/infrastructure/bare-metal-plan.d.ts +208 -0
  335. package/dist/infrastructure/bare-metal-plan.d.ts.map +1 -0
  336. package/dist/infrastructure/bare-metal-plan.js +363 -0
  337. package/dist/infrastructure/bare-metal-plan.js.map +1 -0
  338. package/dist/infrastructure/bare-metal-runner.d.ts +65 -0
  339. package/dist/infrastructure/bare-metal-runner.d.ts.map +1 -0
  340. package/dist/infrastructure/bare-metal-runner.js +354 -0
  341. package/dist/infrastructure/bare-metal-runner.js.map +1 -0
  342. package/dist/infrastructure/config-payload.d.ts +149 -0
  343. package/dist/infrastructure/config-payload.d.ts.map +1 -0
  344. package/dist/infrastructure/config-payload.js +235 -0
  345. package/dist/infrastructure/config-payload.js.map +1 -0
  346. package/dist/infrastructure/container-runtime.d.ts +170 -10
  347. package/dist/infrastructure/container-runtime.d.ts.map +1 -1
  348. package/dist/infrastructure/container-runtime.js +565 -52
  349. package/dist/infrastructure/container-runtime.js.map +1 -1
  350. package/dist/infrastructure/gateway-reachability.d.ts +85 -0
  351. package/dist/infrastructure/gateway-reachability.d.ts.map +1 -0
  352. package/dist/infrastructure/gateway-reachability.js +116 -0
  353. package/dist/infrastructure/gateway-reachability.js.map +1 -0
  354. package/dist/infrastructure/gateway-redis-db.d.ts +29 -0
  355. package/dist/infrastructure/gateway-redis-db.d.ts.map +1 -0
  356. package/dist/infrastructure/gateway-redis-db.js +32 -0
  357. package/dist/infrastructure/gateway-redis-db.js.map +1 -0
  358. package/dist/infrastructure/host-ingress-config.d.ts +12 -0
  359. package/dist/infrastructure/host-ingress-config.d.ts.map +1 -0
  360. package/dist/infrastructure/host-ingress-config.js +197 -0
  361. package/dist/infrastructure/host-ingress-config.js.map +1 -0
  362. package/dist/infrastructure/host-ingress-lock.d.ts +4 -0
  363. package/dist/infrastructure/host-ingress-lock.d.ts.map +1 -0
  364. package/dist/infrastructure/host-ingress-lock.js +36 -0
  365. package/dist/infrastructure/host-ingress-lock.js.map +1 -0
  366. package/dist/infrastructure/host-ingress-runtime.d.ts +25 -0
  367. package/dist/infrastructure/host-ingress-runtime.d.ts.map +1 -0
  368. package/dist/infrastructure/host-ingress-runtime.js +114 -0
  369. package/dist/infrastructure/host-ingress-runtime.js.map +1 -0
  370. package/dist/infrastructure/host-ingress-takeover.d.ts +18 -0
  371. package/dist/infrastructure/host-ingress-takeover.d.ts.map +1 -0
  372. package/dist/infrastructure/host-ingress-takeover.js +142 -0
  373. package/dist/infrastructure/host-ingress-takeover.js.map +1 -0
  374. package/dist/infrastructure/host-ingress-types.d.ts +173 -0
  375. package/dist/infrastructure/host-ingress-types.d.ts.map +1 -0
  376. package/dist/infrastructure/host-ingress-types.js +2 -0
  377. package/dist/infrastructure/host-ingress-types.js.map +1 -0
  378. package/dist/infrastructure/host-ingress.service.d.ts +47 -0
  379. package/dist/infrastructure/host-ingress.service.d.ts.map +1 -0
  380. package/dist/infrastructure/host-ingress.service.js +514 -0
  381. package/dist/infrastructure/host-ingress.service.js.map +1 -0
  382. package/dist/infrastructure/host-inspection.d.ts +223 -0
  383. package/dist/infrastructure/host-inspection.d.ts.map +1 -0
  384. package/dist/infrastructure/host-inspection.js +501 -0
  385. package/dist/infrastructure/host-inspection.js.map +1 -0
  386. package/dist/infrastructure/infrastructure-gate.d.ts.map +1 -1
  387. package/dist/infrastructure/infrastructure.service.d.ts +223 -7
  388. package/dist/infrastructure/infrastructure.service.d.ts.map +1 -1
  389. package/dist/infrastructure/infrastructure.service.js +500 -93
  390. package/dist/infrastructure/infrastructure.service.js.map +1 -1
  391. package/dist/infrastructure/known-machines.d.ts +76 -0
  392. package/dist/infrastructure/known-machines.d.ts.map +1 -0
  393. package/dist/infrastructure/known-machines.js +94 -0
  394. package/dist/infrastructure/known-machines.js.map +1 -0
  395. package/dist/infrastructure/node-credentials.d.ts +45 -0
  396. package/dist/infrastructure/node-credentials.d.ts.map +1 -0
  397. package/dist/infrastructure/node-credentials.js +80 -0
  398. package/dist/infrastructure/node-credentials.js.map +1 -0
  399. package/dist/infrastructure/orphan-containers.d.ts +99 -0
  400. package/dist/infrastructure/orphan-containers.d.ts.map +1 -0
  401. package/dist/infrastructure/orphan-containers.js +106 -0
  402. package/dist/infrastructure/orphan-containers.js.map +1 -0
  403. package/dist/infrastructure/phantom-endpoint-janitor.d.ts +20 -0
  404. package/dist/infrastructure/phantom-endpoint-janitor.d.ts.map +1 -1
  405. package/dist/infrastructure/phantom-endpoint-janitor.js +56 -6
  406. package/dist/infrastructure/phantom-endpoint-janitor.js.map +1 -1
  407. package/dist/infrastructure/presets/gateway-openresty.d.ts.map +1 -1
  408. package/dist/infrastructure/presets/gateway-openresty.js +29 -3
  409. package/dist/infrastructure/presets/gateway-openresty.js.map +1 -1
  410. package/dist/infrastructure/presets/minio.d.ts.map +1 -1
  411. package/dist/infrastructure/presets/minio.js +36 -3
  412. package/dist/infrastructure/presets/minio.js.map +1 -1
  413. package/dist/infrastructure/presets/postgres.d.ts.map +1 -1
  414. package/dist/infrastructure/presets/postgres.js +69 -2
  415. package/dist/infrastructure/presets/postgres.js.map +1 -1
  416. package/dist/infrastructure/presets/redis.d.ts.map +1 -1
  417. package/dist/infrastructure/presets/redis.js +13 -0
  418. package/dist/infrastructure/presets/redis.js.map +1 -1
  419. package/dist/infrastructure/presets/registry.d.ts.map +1 -1
  420. package/dist/infrastructure/presets/registry.js +81 -12
  421. package/dist/infrastructure/presets/registry.js.map +1 -1
  422. package/dist/infrastructure/presets/tor.d.ts +38 -0
  423. package/dist/infrastructure/presets/tor.d.ts.map +1 -1
  424. package/dist/infrastructure/presets/tor.js +78 -69
  425. package/dist/infrastructure/presets/tor.js.map +1 -1
  426. package/dist/infrastructure/presets/torrc.d.ts +54 -0
  427. package/dist/infrastructure/presets/torrc.d.ts.map +1 -0
  428. package/dist/infrastructure/presets/torrc.js +153 -0
  429. package/dist/infrastructure/presets/torrc.js.map +1 -0
  430. package/dist/infrastructure/presets/types.d.ts +17 -0
  431. package/dist/infrastructure/presets/types.d.ts.map +1 -1
  432. package/dist/infrastructure/provisioning-outcome.d.ts +85 -0
  433. package/dist/infrastructure/provisioning-outcome.d.ts.map +1 -0
  434. package/dist/infrastructure/provisioning-outcome.js +122 -0
  435. package/dist/infrastructure/provisioning-outcome.js.map +1 -0
  436. package/dist/infrastructure/server-registry.d.ts +27 -0
  437. package/dist/infrastructure/server-registry.d.ts.map +1 -1
  438. package/dist/infrastructure/server-registry.js +60 -5
  439. package/dist/infrastructure/server-registry.js.map +1 -1
  440. package/dist/infrastructure/service-binding.d.ts +60 -0
  441. package/dist/infrastructure/service-binding.d.ts.map +1 -0
  442. package/dist/infrastructure/service-binding.js +90 -0
  443. package/dist/infrastructure/service-binding.js.map +1 -0
  444. package/dist/infrastructure/service-credentials.d.ts +105 -0
  445. package/dist/infrastructure/service-credentials.d.ts.map +1 -0
  446. package/dist/infrastructure/service-credentials.js +141 -0
  447. package/dist/infrastructure/service-credentials.js.map +1 -0
  448. package/dist/infrastructure/service-resolver.d.ts +166 -12
  449. package/dist/infrastructure/service-resolver.d.ts.map +1 -1
  450. package/dist/infrastructure/service-resolver.js +535 -82
  451. package/dist/infrastructure/service-resolver.js.map +1 -1
  452. package/dist/infrastructure/shipped-config.d.ts +43 -0
  453. package/dist/infrastructure/shipped-config.d.ts.map +1 -0
  454. package/dist/infrastructure/shipped-config.js +159 -0
  455. package/dist/infrastructure/shipped-config.js.map +1 -0
  456. package/dist/infrastructure/stack-infra-manager.d.ts +16 -1
  457. package/dist/infrastructure/stack-infra-manager.d.ts.map +1 -1
  458. package/dist/infrastructure/stack-infra-manager.js +81 -53
  459. package/dist/infrastructure/stack-infra-manager.js.map +1 -1
  460. package/dist/infrastructure/types.d.ts +263 -6
  461. package/dist/infrastructure/types.d.ts.map +1 -1
  462. package/dist/mcp/mcp-bridge.d.ts.map +1 -1
  463. package/dist/mcp/mcp-bridge.js.map +1 -1
  464. package/dist/mcp/tool-groups/apps.tools.d.ts +2 -1
  465. package/dist/mcp/tool-groups/apps.tools.d.ts.map +1 -1
  466. package/dist/mcp/tool-groups/apps.tools.js +2 -2
  467. package/dist/mcp/tool-groups/apps.tools.js.map +1 -1
  468. package/dist/mcp/tool-groups/infra.tools.d.ts +16 -2
  469. package/dist/mcp/tool-groups/infra.tools.d.ts.map +1 -1
  470. package/dist/mcp/tool-groups/infra.tools.js +51 -41
  471. package/dist/mcp/tool-groups/infra.tools.js.map +1 -1
  472. package/dist/mcp/tool-groups/kb.tools.d.ts.map +1 -1
  473. package/dist/mcp/tool-groups/kb.tools.js +4 -6
  474. package/dist/mcp/tool-groups/kb.tools.js.map +1 -1
  475. package/dist/mcp/tool-groups/management.tools.d.ts +20 -3
  476. package/dist/mcp/tool-groups/management.tools.d.ts.map +1 -1
  477. package/dist/mcp/tool-groups/management.tools.js +158 -125
  478. package/dist/mcp/tool-groups/management.tools.js.map +1 -1
  479. package/dist/mcp/tool-groups/monitoring.tools.d.ts +10 -1
  480. package/dist/mcp/tool-groups/monitoring.tools.d.ts.map +1 -1
  481. package/dist/mcp/tool-groups/monitoring.tools.js +65 -18
  482. package/dist/mcp/tool-groups/monitoring.tools.js.map +1 -1
  483. package/dist/mcp/unavailable-tools.d.ts +35 -0
  484. package/dist/mcp/unavailable-tools.d.ts.map +1 -0
  485. package/dist/mcp/unavailable-tools.js +59 -0
  486. package/dist/mcp/unavailable-tools.js.map +1 -0
  487. package/dist/monitoring/app-health.indicator.d.ts.map +1 -1
  488. package/dist/monitoring/app-health.indicator.js +12 -0
  489. package/dist/monitoring/app-health.indicator.js.map +1 -1
  490. package/dist/monitoring/desktop-notifier.d.ts +26 -0
  491. package/dist/monitoring/desktop-notifier.d.ts.map +1 -0
  492. package/dist/monitoring/desktop-notifier.js +49 -0
  493. package/dist/monitoring/desktop-notifier.js.map +1 -0
  494. package/dist/monitoring/docker-health.indicator.d.ts +17 -1
  495. package/dist/monitoring/docker-health.indicator.d.ts.map +1 -1
  496. package/dist/monitoring/docker-health.indicator.js +38 -8
  497. package/dist/monitoring/docker-health.indicator.js.map +1 -1
  498. package/dist/monitoring/event-loop-stall.indicator.d.ts +26 -0
  499. package/dist/monitoring/event-loop-stall.indicator.d.ts.map +1 -0
  500. package/dist/monitoring/event-loop-stall.indicator.js +47 -0
  501. package/dist/monitoring/event-loop-stall.indicator.js.map +1 -0
  502. package/dist/monitoring/event-loop-watch.d.ts +124 -0
  503. package/dist/monitoring/event-loop-watch.d.ts.map +1 -0
  504. package/dist/monitoring/event-loop-watch.js +179 -0
  505. package/dist/monitoring/event-loop-watch.js.map +1 -0
  506. package/dist/monitoring/host-disk.d.ts +6 -0
  507. package/dist/monitoring/host-disk.d.ts.map +1 -0
  508. package/dist/monitoring/host-disk.js +26 -0
  509. package/dist/monitoring/host-disk.js.map +1 -0
  510. package/dist/monitoring/log-line.d.ts +188 -0
  511. package/dist/monitoring/log-line.d.ts.map +1 -0
  512. package/dist/monitoring/log-line.js +458 -0
  513. package/dist/monitoring/log-line.js.map +1 -0
  514. package/dist/monitoring/log-manager.d.ts +89 -8
  515. package/dist/monitoring/log-manager.d.ts.map +1 -1
  516. package/dist/monitoring/log-manager.js +246 -77
  517. package/dist/monitoring/log-manager.js.map +1 -1
  518. package/dist/observability/metrics-bridge.d.ts.map +1 -1
  519. package/dist/observability/metrics-server.d.ts.map +1 -1
  520. package/dist/operations.d.ts +16 -0
  521. package/dist/operations.d.ts.map +1 -0
  522. package/dist/operations.js +13 -0
  523. package/dist/operations.js.map +1 -0
  524. package/dist/orchestrator/app-handle.d.ts +157 -2
  525. package/dist/orchestrator/app-handle.d.ts.map +1 -1
  526. package/dist/orchestrator/app-handle.js +165 -3
  527. package/dist/orchestrator/app-handle.js.map +1 -1
  528. package/dist/orchestrator/app-health.d.ts +33 -0
  529. package/dist/orchestrator/app-health.d.ts.map +1 -0
  530. package/dist/orchestrator/app-health.js +74 -0
  531. package/dist/orchestrator/app-health.js.map +1 -0
  532. package/dist/orchestrator/app-traffic.d.ts +14 -0
  533. package/dist/orchestrator/app-traffic.d.ts.map +1 -0
  534. package/dist/orchestrator/app-traffic.js +33 -0
  535. package/dist/orchestrator/app-traffic.js.map +1 -0
  536. package/dist/orchestrator/bootstrap-loader-child.d.ts +19 -0
  537. package/dist/orchestrator/bootstrap-loader-child.d.ts.map +1 -0
  538. package/dist/orchestrator/bootstrap-loader-child.js +43 -0
  539. package/dist/orchestrator/bootstrap-loader-child.js.map +1 -0
  540. package/dist/orchestrator/bootstrap-loader.d.ts +72 -0
  541. package/dist/orchestrator/bootstrap-loader.d.ts.map +1 -1
  542. package/dist/orchestrator/bootstrap-loader.js +350 -5
  543. package/dist/orchestrator/bootstrap-loader.js.map +1 -1
  544. package/dist/orchestrator/bootstrap-process.d.ts +99 -4
  545. package/dist/orchestrator/bootstrap-process.d.ts.map +1 -1
  546. package/dist/orchestrator/bootstrap-process.js +332 -20
  547. package/dist/orchestrator/bootstrap-process.js.map +1 -1
  548. package/dist/orchestrator/build-service.d.ts +6 -12
  549. package/dist/orchestrator/build-service.d.ts.map +1 -1
  550. package/dist/orchestrator/build-service.js +120 -83
  551. package/dist/orchestrator/build-service.js.map +1 -1
  552. package/dist/orchestrator/call-exposed-service.d.ts +7 -0
  553. package/dist/orchestrator/call-exposed-service.d.ts.map +1 -0
  554. package/dist/orchestrator/call-exposed-service.js +50 -0
  555. package/dist/orchestrator/call-exposed-service.js.map +1 -0
  556. package/dist/orchestrator/classic-launcher.d.ts +1 -0
  557. package/dist/orchestrator/classic-launcher.d.ts.map +1 -1
  558. package/dist/orchestrator/classic-launcher.js +40 -4
  559. package/dist/orchestrator/classic-launcher.js.map +1 -1
  560. package/dist/orchestrator/file-watcher.d.ts +79 -1
  561. package/dist/orchestrator/file-watcher.d.ts.map +1 -1
  562. package/dist/orchestrator/file-watcher.js +193 -7
  563. package/dist/orchestrator/file-watcher.js.map +1 -1
  564. package/dist/orchestrator/http-routes.d.ts +4 -0
  565. package/dist/orchestrator/http-routes.d.ts.map +1 -0
  566. package/dist/orchestrator/http-routes.js +19 -0
  567. package/dist/orchestrator/http-routes.js.map +1 -0
  568. package/dist/orchestrator/module-worker-process.d.ts.map +1 -1
  569. package/dist/orchestrator/module-worker-process.js +10 -0
  570. package/dist/orchestrator/module-worker-process.js.map +1 -1
  571. package/dist/orchestrator/orchestrator.service.d.ts +385 -11
  572. package/dist/orchestrator/orchestrator.service.d.ts.map +1 -1
  573. package/dist/orchestrator/orchestrator.service.js +1738 -364
  574. package/dist/orchestrator/orchestrator.service.js.map +1 -1
  575. package/dist/orchestrator/owned-pids.d.ts +83 -0
  576. package/dist/orchestrator/owned-pids.d.ts.map +1 -0
  577. package/dist/orchestrator/owned-pids.js +93 -0
  578. package/dist/orchestrator/owned-pids.js.map +1 -0
  579. package/dist/orchestrator/port-conflict.d.ts +102 -0
  580. package/dist/orchestrator/port-conflict.d.ts.map +1 -0
  581. package/dist/orchestrator/port-conflict.js +131 -0
  582. package/dist/orchestrator/port-conflict.js.map +1 -0
  583. package/dist/orchestrator/process-janitor.d.ts +78 -4
  584. package/dist/orchestrator/process-janitor.d.ts.map +1 -1
  585. package/dist/orchestrator/process-janitor.js +246 -30
  586. package/dist/orchestrator/process-janitor.js.map +1 -1
  587. package/dist/orchestrator/ps-metrics.d.ts +37 -0
  588. package/dist/orchestrator/ps-metrics.d.ts.map +1 -0
  589. package/dist/orchestrator/ps-metrics.js +60 -0
  590. package/dist/orchestrator/ps-metrics.js.map +1 -0
  591. package/dist/orchestrator/service-router.d.ts +58 -2
  592. package/dist/orchestrator/service-router.d.ts.map +1 -1
  593. package/dist/orchestrator/service-router.js +157 -25
  594. package/dist/orchestrator/service-router.js.map +1 -1
  595. package/dist/orchestrator/topology-query.d.ts +49 -0
  596. package/dist/orchestrator/topology-query.d.ts.map +1 -0
  597. package/dist/orchestrator/topology-query.js +54 -0
  598. package/dist/orchestrator/topology-query.js.map +1 -0
  599. package/dist/project/artifact-builder.d.ts +174 -3
  600. package/dist/project/artifact-builder.d.ts.map +1 -1
  601. package/dist/project/artifact-builder.js +454 -42
  602. package/dist/project/artifact-builder.js.map +1 -1
  603. package/dist/project/build-decision.d.ts +58 -0
  604. package/dist/project/build-decision.d.ts.map +1 -0
  605. package/dist/project/build-decision.js +59 -0
  606. package/dist/project/build-decision.js.map +1 -0
  607. package/dist/project/config-resolver.d.ts +6 -4
  608. package/dist/project/config-resolver.d.ts.map +1 -1
  609. package/dist/project/config-resolver.js +128 -74
  610. package/dist/project/config-resolver.js.map +1 -1
  611. package/dist/project/declared-config.d.ts +42 -0
  612. package/dist/project/declared-config.d.ts.map +1 -0
  613. package/dist/project/declared-config.js +60 -0
  614. package/dist/project/declared-config.js.map +1 -0
  615. package/dist/project/dependency-analyzer.d.ts.map +1 -1
  616. package/dist/project/dependency-analyzer.js.map +1 -1
  617. package/dist/project/deploy-phases.d.ts +35 -0
  618. package/dist/project/deploy-phases.d.ts.map +1 -0
  619. package/dist/project/deploy-phases.js +75 -0
  620. package/dist/project/deploy-phases.js.map +1 -0
  621. package/dist/project/exec-in-group.d.ts +49 -0
  622. package/dist/project/exec-in-group.d.ts.map +1 -0
  623. package/dist/project/exec-in-group.js +100 -0
  624. package/dist/project/exec-in-group.js.map +1 -0
  625. package/dist/project/node-app-config.d.ts +101 -0
  626. package/dist/project/node-app-config.d.ts.map +1 -0
  627. package/dist/project/node-app-config.js +134 -0
  628. package/dist/project/node-app-config.js.map +1 -0
  629. package/dist/project/node-app-health.d.ts +76 -0
  630. package/dist/project/node-app-health.d.ts.map +1 -0
  631. package/dist/project/node-app-health.js +119 -0
  632. package/dist/project/node-app-health.js.map +1 -0
  633. package/dist/project/operator-account.d.ts +293 -0
  634. package/dist/project/operator-account.d.ts.map +1 -0
  635. package/dist/project/operator-account.js +373 -0
  636. package/dist/project/operator-account.js.map +1 -0
  637. package/dist/project/registry.d.ts +45 -8
  638. package/dist/project/registry.d.ts.map +1 -1
  639. package/dist/project/registry.js +214 -64
  640. package/dist/project/registry.js.map +1 -1
  641. package/dist/project/requirements-scanner.d.ts +1 -0
  642. package/dist/project/requirements-scanner.d.ts.map +1 -1
  643. package/dist/project/requirements-scanner.js +28 -3
  644. package/dist/project/requirements-scanner.js.map +1 -1
  645. package/dist/project/stack-env.d.ts +37 -0
  646. package/dist/project/stack-env.d.ts.map +1 -0
  647. package/dist/project/stack-env.js +85 -0
  648. package/dist/project/stack-env.js.map +1 -0
  649. package/dist/project/stand-containers.d.ts +11 -0
  650. package/dist/project/stand-containers.d.ts.map +1 -0
  651. package/dist/project/stand-containers.js +27 -0
  652. package/dist/project/stand-containers.js.map +1 -0
  653. package/dist/project/working-tree.d.ts +62 -0
  654. package/dist/project/working-tree.d.ts.map +1 -0
  655. package/dist/project/working-tree.js +101 -0
  656. package/dist/project/working-tree.js.map +1 -0
  657. package/dist/release/artifact-store.d.ts +360 -0
  658. package/dist/release/artifact-store.d.ts.map +1 -0
  659. package/dist/release/artifact-store.js +1040 -0
  660. package/dist/release/artifact-store.js.map +1 -0
  661. package/dist/release/attest-on-node.d.ts +93 -0
  662. package/dist/release/attest-on-node.d.ts.map +1 -0
  663. package/dist/release/attest-on-node.js +207 -0
  664. package/dist/release/attest-on-node.js.map +1 -0
  665. package/dist/release/attest.d.ts +135 -0
  666. package/dist/release/attest.d.ts.map +1 -0
  667. package/dist/release/attest.js +199 -0
  668. package/dist/release/attest.js.map +1 -0
  669. package/dist/release/build-run.d.ts +151 -0
  670. package/dist/release/build-run.d.ts.map +1 -0
  671. package/dist/release/build-run.js +609 -0
  672. package/dist/release/build-run.js.map +1 -0
  673. package/dist/release/builder.d.ts +57 -0
  674. package/dist/release/builder.d.ts.map +1 -0
  675. package/dist/release/builder.js +120 -0
  676. package/dist/release/builder.js.map +1 -0
  677. package/dist/release/daemon-omni.d.ts +41 -0
  678. package/dist/release/daemon-omni.d.ts.map +1 -0
  679. package/dist/release/daemon-omni.js +59 -0
  680. package/dist/release/daemon-omni.js.map +1 -0
  681. package/dist/release/definition-inputs.d.ts +34 -0
  682. package/dist/release/definition-inputs.d.ts.map +1 -0
  683. package/dist/release/definition-inputs.js +90 -0
  684. package/dist/release/definition-inputs.js.map +1 -0
  685. package/dist/release/delivered.d.ts +53 -0
  686. package/dist/release/delivered.d.ts.map +1 -0
  687. package/dist/release/delivered.js +81 -0
  688. package/dist/release/delivered.js.map +1 -0
  689. package/dist/release/layout.d.ts +61 -0
  690. package/dist/release/layout.d.ts.map +1 -0
  691. package/dist/release/layout.js +114 -0
  692. package/dist/release/layout.js.map +1 -0
  693. package/dist/release/load.d.ts +44 -0
  694. package/dist/release/load.d.ts.map +1 -0
  695. package/dist/release/load.js +84 -0
  696. package/dist/release/load.js.map +1 -0
  697. package/dist/release/manifest.d.ts +299 -0
  698. package/dist/release/manifest.d.ts.map +1 -0
  699. package/dist/release/manifest.js +176 -0
  700. package/dist/release/manifest.js.map +1 -0
  701. package/dist/release/migrations.d.ts +32 -0
  702. package/dist/release/migrations.d.ts.map +1 -0
  703. package/dist/release/migrations.js +51 -0
  704. package/dist/release/migrations.js.map +1 -0
  705. package/dist/release/publish.d.ts +71 -0
  706. package/dist/release/publish.d.ts.map +1 -0
  707. package/dist/release/publish.js +102 -0
  708. package/dist/release/publish.js.map +1 -0
  709. package/dist/release/store.d.ts +174 -0
  710. package/dist/release/store.d.ts.map +1 -0
  711. package/dist/release/store.js +257 -0
  712. package/dist/release/store.js.map +1 -0
  713. package/dist/services/alert.rpc-service.d.ts +11 -15
  714. package/dist/services/alert.rpc-service.d.ts.map +1 -1
  715. package/dist/services/alert.rpc-service.js +29 -14
  716. package/dist/services/alert.rpc-service.js.map +1 -1
  717. package/dist/services/alert.service.d.ts +96 -59
  718. package/dist/services/alert.service.d.ts.map +1 -1
  719. package/dist/services/alert.service.js +455 -70
  720. package/dist/services/alert.service.js.map +1 -1
  721. package/dist/services/anonymous-input.d.ts +26 -0
  722. package/dist/services/anonymous-input.d.ts.map +1 -0
  723. package/dist/services/anonymous-input.js +44 -0
  724. package/dist/services/anonymous-input.js.map +1 -0
  725. package/dist/services/audit.rpc-service.d.ts +27 -0
  726. package/dist/services/audit.rpc-service.d.ts.map +1 -0
  727. package/dist/services/audit.rpc-service.js +57 -0
  728. package/dist/services/audit.rpc-service.js.map +1 -0
  729. package/dist/services/audit.service.d.ts +165 -0
  730. package/dist/services/audit.service.d.ts.map +1 -0
  731. package/dist/services/audit.service.js +242 -0
  732. package/dist/services/audit.service.js.map +1 -0
  733. package/dist/services/auth-context.d.ts +21 -1
  734. package/dist/services/auth-context.d.ts.map +1 -1
  735. package/dist/services/auth-context.js +51 -5
  736. package/dist/services/auth-context.js.map +1 -1
  737. package/dist/services/auth.rpc-service.d.ts +0 -1
  738. package/dist/services/auth.rpc-service.d.ts.map +1 -1
  739. package/dist/services/auth.rpc-service.js +23 -9
  740. package/dist/services/auth.rpc-service.js.map +1 -1
  741. package/dist/services/auth.service.d.ts +47 -33
  742. package/dist/services/auth.service.d.ts.map +1 -1
  743. package/dist/services/auth.service.js +155 -23
  744. package/dist/services/auth.service.js.map +1 -1
  745. package/dist/services/backup-pipeline.d.ts +97 -0
  746. package/dist/services/backup-pipeline.d.ts.map +1 -0
  747. package/dist/services/backup-pipeline.js +286 -0
  748. package/dist/services/backup-pipeline.js.map +1 -0
  749. package/dist/services/backup-schedule.d.ts +69 -0
  750. package/dist/services/backup-schedule.d.ts.map +1 -0
  751. package/dist/services/backup-schedule.js +132 -0
  752. package/dist/services/backup-schedule.js.map +1 -0
  753. package/dist/services/backup.rpc-service.d.ts +31 -2
  754. package/dist/services/backup.rpc-service.d.ts.map +1 -1
  755. package/dist/services/backup.rpc-service.js +53 -0
  756. package/dist/services/backup.rpc-service.js.map +1 -1
  757. package/dist/services/backup.service.d.ts +458 -14
  758. package/dist/services/backup.service.d.ts.map +1 -1
  759. package/dist/services/backup.service.js +1368 -170
  760. package/dist/services/backup.service.js.map +1 -1
  761. package/dist/services/bundle-build-worker.d.ts +19 -0
  762. package/dist/services/bundle-build-worker.d.ts.map +1 -0
  763. package/dist/services/bundle-build-worker.js +40 -0
  764. package/dist/services/bundle-build-worker.js.map +1 -0
  765. package/dist/services/bundle-builder.d.ts +490 -0
  766. package/dist/services/bundle-builder.d.ts.map +1 -0
  767. package/dist/services/bundle-builder.js +1159 -0
  768. package/dist/services/bundle-builder.js.map +1 -0
  769. package/dist/services/bundle-worker-protocol.d.ts +19 -0
  770. package/dist/services/bundle-worker-protocol.d.ts.map +1 -0
  771. package/dist/services/bundle-worker-protocol.js +32 -0
  772. package/dist/services/bundle-worker-protocol.js.map +1 -0
  773. package/dist/services/deploy.rpc-service.d.ts +4 -1
  774. package/dist/services/deploy.rpc-service.d.ts.map +1 -1
  775. package/dist/services/deploy.rpc-service.js +10 -0
  776. package/dist/services/deploy.rpc-service.js.map +1 -1
  777. package/dist/services/deploy.service.d.ts +14 -24
  778. package/dist/services/deploy.service.d.ts.map +1 -1
  779. package/dist/services/deploy.service.js +46 -5
  780. package/dist/services/deploy.service.js.map +1 -1
  781. package/dist/services/discovery.rpc-service.d.ts +2 -1
  782. package/dist/services/discovery.rpc-service.d.ts.map +1 -1
  783. package/dist/services/discovery.rpc-service.js.map +1 -1
  784. package/dist/services/discovery.service.d.ts +32 -28
  785. package/dist/services/discovery.service.d.ts.map +1 -1
  786. package/dist/services/discovery.service.js +108 -133
  787. package/dist/services/discovery.service.js.map +1 -1
  788. package/dist/services/event-broadcaster.rpc-service.d.ts +36 -23
  789. package/dist/services/event-broadcaster.rpc-service.d.ts.map +1 -1
  790. package/dist/services/event-broadcaster.rpc-service.js +70 -32
  791. package/dist/services/event-broadcaster.rpc-service.js.map +1 -1
  792. package/dist/services/event-broadcaster.service.d.ts.map +1 -1
  793. package/dist/services/event-broadcaster.service.js.map +1 -1
  794. package/dist/services/fleet.rpc-service.d.ts +2 -1
  795. package/dist/services/fleet.rpc-service.d.ts.map +1 -1
  796. package/dist/services/fleet.rpc-service.js +3 -1
  797. package/dist/services/fleet.rpc-service.js.map +1 -1
  798. package/dist/services/fleet.service.d.ts +32 -34
  799. package/dist/services/fleet.service.d.ts.map +1 -1
  800. package/dist/services/fleet.service.js +66 -10
  801. package/dist/services/fleet.service.js.map +1 -1
  802. package/dist/services/health-check.rpc-service.d.ts +2 -1
  803. package/dist/services/health-check.rpc-service.d.ts.map +1 -1
  804. package/dist/services/health-check.rpc-service.js.map +1 -1
  805. package/dist/services/health-check.service.d.ts +50 -36
  806. package/dist/services/health-check.service.d.ts.map +1 -1
  807. package/dist/services/health-check.service.js +179 -182
  808. package/dist/services/health-check.service.js.map +1 -1
  809. package/dist/services/host-operation-barrier.d.ts +8 -0
  810. package/dist/services/host-operation-barrier.d.ts.map +1 -0
  811. package/dist/services/host-operation-barrier.js +44 -0
  812. package/dist/services/host-operation-barrier.js.map +1 -0
  813. package/dist/services/host-upgrade.d.ts +129 -0
  814. package/dist/services/host-upgrade.d.ts.map +1 -0
  815. package/dist/services/host-upgrade.js +352 -0
  816. package/dist/services/host-upgrade.js.map +1 -0
  817. package/dist/services/infrastructure.rpc-service.d.ts +312 -2
  818. package/dist/services/infrastructure.rpc-service.d.ts.map +1 -1
  819. package/dist/services/infrastructure.rpc-service.js +584 -11
  820. package/dist/services/infrastructure.rpc-service.js.map +1 -1
  821. package/dist/services/kubernetes.rpc-service.d.ts +2 -1
  822. package/dist/services/kubernetes.rpc-service.d.ts.map +1 -1
  823. package/dist/services/kubernetes.rpc-service.js.map +1 -1
  824. package/dist/services/kubernetes.service.d.ts +5 -39
  825. package/dist/services/kubernetes.service.d.ts.map +1 -1
  826. package/dist/services/kubernetes.service.js +29 -5
  827. package/dist/services/kubernetes.service.js.map +1 -1
  828. package/dist/services/local-bundle.d.ts +160 -0
  829. package/dist/services/local-bundle.d.ts.map +1 -0
  830. package/dist/services/local-bundle.js +242 -0
  831. package/dist/services/local-bundle.js.map +1 -0
  832. package/dist/services/lockout.d.ts +28 -0
  833. package/dist/services/lockout.d.ts.map +1 -0
  834. package/dist/services/lockout.js +41 -0
  835. package/dist/services/lockout.js.map +1 -0
  836. package/dist/services/log-collector.rpc-service.d.ts +16 -0
  837. package/dist/services/log-collector.rpc-service.d.ts.map +1 -1
  838. package/dist/services/log-collector.rpc-service.js +16 -0
  839. package/dist/services/log-collector.rpc-service.js.map +1 -1
  840. package/dist/services/log-collector.service.d.ts +172 -54
  841. package/dist/services/log-collector.service.d.ts.map +1 -1
  842. package/dist/services/log-collector.service.js +492 -102
  843. package/dist/services/log-collector.service.js.map +1 -1
  844. package/dist/services/log-retention.d.ts +50 -0
  845. package/dist/services/log-retention.d.ts.map +1 -0
  846. package/dist/services/log-retention.js +52 -0
  847. package/dist/services/log-retention.js.map +1 -0
  848. package/dist/services/maintenance-lock.d.ts +162 -0
  849. package/dist/services/maintenance-lock.d.ts.map +1 -0
  850. package/dist/services/maintenance-lock.js +316 -0
  851. package/dist/services/maintenance-lock.js.map +1 -0
  852. package/dist/services/master-address.d.ts +107 -0
  853. package/dist/services/master-address.d.ts.map +1 -0
  854. package/dist/services/master-address.js +154 -0
  855. package/dist/services/master-address.js.map +1 -0
  856. package/dist/services/migration-output.d.ts +16 -0
  857. package/dist/services/migration-output.d.ts.map +1 -0
  858. package/dist/services/migration-output.js +22 -0
  859. package/dist/services/migration-output.js.map +1 -0
  860. package/dist/services/netron-telemetry-transport.d.ts.map +1 -1
  861. package/dist/services/netron-telemetry-transport.js.map +1 -1
  862. package/dist/services/node-deploy-lease.d.ts +171 -0
  863. package/dist/services/node-deploy-lease.d.ts.map +1 -0
  864. package/dist/services/node-deploy-lease.js +460 -0
  865. package/dist/services/node-deploy-lease.js.map +1 -0
  866. package/dist/services/node-health.repository.d.ts +63 -4
  867. package/dist/services/node-health.repository.d.ts.map +1 -1
  868. package/dist/services/node-health.repository.js +108 -67
  869. package/dist/services/node-health.repository.js.map +1 -1
  870. package/dist/services/node-manager.rpc-service.d.ts +360 -10
  871. package/dist/services/node-manager.rpc-service.d.ts.map +1 -1
  872. package/dist/services/node-manager.rpc-service.js +754 -34
  873. package/dist/services/node-manager.rpc-service.js.map +1 -1
  874. package/dist/services/node-manager.service.d.ts +267 -95
  875. package/dist/services/node-manager.service.d.ts.map +1 -1
  876. package/dist/services/node-manager.service.js +805 -103
  877. package/dist/services/node-manager.service.js.map +1 -1
  878. package/dist/services/node-upgrade.d.ts +129 -0
  879. package/dist/services/node-upgrade.d.ts.map +1 -0
  880. package/dist/services/node-upgrade.js +161 -0
  881. package/dist/services/node-upgrade.js.map +1 -0
  882. package/dist/services/node-upgrade.service.d.ts +232 -0
  883. package/dist/services/node-upgrade.service.d.ts.map +1 -0
  884. package/dist/services/node-upgrade.service.js +468 -0
  885. package/dist/services/node-upgrade.service.js.map +1 -0
  886. package/dist/services/pipeline.rpc-service.d.ts +6 -1
  887. package/dist/services/pipeline.rpc-service.d.ts.map +1 -1
  888. package/dist/services/pipeline.rpc-service.js +9 -0
  889. package/dist/services/pipeline.rpc-service.js.map +1 -1
  890. package/dist/services/pipeline.service.d.ts +17 -46
  891. package/dist/services/pipeline.service.d.ts.map +1 -1
  892. package/dist/services/pipeline.service.js +84 -9
  893. package/dist/services/pipeline.service.js.map +1 -1
  894. package/dist/services/project.rpc-service.d.ts +140 -5
  895. package/dist/services/project.rpc-service.d.ts.map +1 -1
  896. package/dist/services/project.rpc-service.js +260 -22
  897. package/dist/services/project.rpc-service.js.map +1 -1
  898. package/dist/services/project.service.d.ts +687 -15
  899. package/dist/services/project.service.d.ts.map +1 -1
  900. package/dist/services/project.service.js +2846 -337
  901. package/dist/services/project.service.js.map +1 -1
  902. package/dist/services/redeploy-decision.d.ts +176 -0
  903. package/dist/services/redeploy-decision.d.ts.map +1 -0
  904. package/dist/services/redeploy-decision.js +142 -0
  905. package/dist/services/redeploy-decision.js.map +1 -0
  906. package/dist/services/release.rpc-service.d.ts +159 -0
  907. package/dist/services/release.rpc-service.d.ts.map +1 -0
  908. package/dist/services/release.rpc-service.js +395 -0
  909. package/dist/services/release.rpc-service.js.map +1 -0
  910. package/dist/services/release.service.d.ts +114 -0
  911. package/dist/services/release.service.d.ts.map +1 -0
  912. package/dist/services/release.service.js +208 -0
  913. package/dist/services/release.service.js.map +1 -0
  914. package/dist/services/remote-deployer.service.d.ts +580 -8
  915. package/dist/services/remote-deployer.service.d.ts.map +1 -1
  916. package/dist/services/remote-deployer.service.js +1786 -169
  917. package/dist/services/remote-deployer.service.js.map +1 -1
  918. package/dist/services/remote-ops.service.d.ts +51 -9
  919. package/dist/services/remote-ops.service.d.ts.map +1 -1
  920. package/dist/services/remote-ops.service.js +235 -39
  921. package/dist/services/remote-ops.service.js.map +1 -1
  922. package/dist/services/remote-provisioner.d.ts +124 -0
  923. package/dist/services/remote-provisioner.d.ts.map +1 -0
  924. package/dist/services/remote-provisioner.js +258 -0
  925. package/dist/services/remote-provisioner.js.map +1 -0
  926. package/dist/services/secrets.rpc-service.d.ts +51 -2
  927. package/dist/services/secrets.rpc-service.d.ts.map +1 -1
  928. package/dist/services/secrets.rpc-service.js +81 -2
  929. package/dist/services/secrets.rpc-service.js.map +1 -1
  930. package/dist/services/secrets.service.d.ts +108 -9
  931. package/dist/services/secrets.service.d.ts.map +1 -1
  932. package/dist/services/secrets.service.js +313 -48
  933. package/dist/services/secrets.service.js.map +1 -1
  934. package/dist/services/slave-storage.service.d.ts +107 -0
  935. package/dist/services/slave-storage.service.d.ts.map +1 -1
  936. package/dist/services/slave-storage.service.js +171 -2
  937. package/dist/services/slave-storage.service.js.map +1 -1
  938. package/dist/services/static-carry-forward.d.ts +110 -0
  939. package/dist/services/static-carry-forward.d.ts.map +1 -0
  940. package/dist/services/static-carry-forward.js +433 -0
  941. package/dist/services/static-carry-forward.js.map +1 -0
  942. package/dist/services/sync-policy.d.ts +167 -0
  943. package/dist/services/sync-policy.d.ts.map +1 -0
  944. package/dist/services/sync-policy.js +161 -0
  945. package/dist/services/sync-policy.js.map +1 -0
  946. package/dist/services/sync-summary.d.ts +39 -0
  947. package/dist/services/sync-summary.d.ts.map +1 -0
  948. package/dist/services/sync-summary.js +62 -0
  949. package/dist/services/sync-summary.js.map +1 -0
  950. package/dist/services/sync.rpc-service.d.ts +19 -7
  951. package/dist/services/sync.rpc-service.d.ts.map +1 -1
  952. package/dist/services/sync.rpc-service.js +36 -5
  953. package/dist/services/sync.rpc-service.js.map +1 -1
  954. package/dist/services/sync.service.d.ts +350 -18
  955. package/dist/services/sync.service.d.ts.map +1 -1
  956. package/dist/services/sync.service.js +864 -116
  957. package/dist/services/sync.service.js.map +1 -1
  958. package/dist/services/system-info.rpc-service.d.ts +2 -1
  959. package/dist/services/system-info.rpc-service.d.ts.map +1 -1
  960. package/dist/services/system-info.rpc-service.js.map +1 -1
  961. package/dist/services/system-info.service.d.ts +56 -68
  962. package/dist/services/system-info.service.d.ts.map +1 -1
  963. package/dist/services/system-info.service.js +240 -127
  964. package/dist/services/system-info.service.js.map +1 -1
  965. package/dist/services/telemetry.rpc-service.d.ts.map +1 -1
  966. package/dist/services/telemetry.rpc-service.js +9 -3
  967. package/dist/services/telemetry.rpc-service.js.map +1 -1
  968. package/dist/services/telemetry.service.d.ts +4 -2
  969. package/dist/services/telemetry.service.d.ts.map +1 -1
  970. package/dist/services/telemetry.service.js +4 -2
  971. package/dist/services/telemetry.service.js.map +1 -1
  972. package/dist/services/trace-collector.rpc-service.d.ts +2 -1
  973. package/dist/services/trace-collector.rpc-service.d.ts.map +1 -1
  974. package/dist/services/trace-collector.rpc-service.js.map +1 -1
  975. package/dist/services/trace-collector.service.d.ts +2 -41
  976. package/dist/services/trace-collector.service.d.ts.map +1 -1
  977. package/dist/services/trace-collector.service.js +27 -7
  978. package/dist/services/trace-collector.service.js.map +1 -1
  979. package/dist/services/wait-for-postgres.d.ts +28 -0
  980. package/dist/services/wait-for-postgres.d.ts.map +1 -0
  981. package/dist/services/wait-for-postgres.js +77 -0
  982. package/dist/services/wait-for-postgres.js.map +1 -0
  983. package/dist/shared/alert-expression.d.ts +60 -0
  984. package/dist/shared/alert-expression.d.ts.map +1 -0
  985. package/dist/shared/alert-expression.js +124 -0
  986. package/dist/shared/alert-expression.js.map +1 -0
  987. package/dist/shared/audit-outcome.d.ts +39 -0
  988. package/dist/shared/audit-outcome.d.ts.map +1 -0
  989. package/dist/shared/audit-outcome.js +45 -0
  990. package/dist/shared/audit-outcome.js.map +1 -0
  991. package/dist/shared/build-freshness.d.ts +59 -0
  992. package/dist/shared/build-freshness.d.ts.map +1 -0
  993. package/dist/shared/build-freshness.js +113 -0
  994. package/dist/shared/build-freshness.js.map +1 -0
  995. package/dist/shared/build-stamp.d.ts +26 -0
  996. package/dist/shared/build-stamp.d.ts.map +1 -0
  997. package/dist/shared/build-stamp.js +36 -0
  998. package/dist/shared/build-stamp.js.map +1 -0
  999. package/dist/shared/describe-error.d.ts +22 -0
  1000. package/dist/shared/describe-error.d.ts.map +1 -0
  1001. package/dist/shared/describe-error.js +39 -0
  1002. package/dist/shared/describe-error.js.map +1 -0
  1003. package/dist/shared/dto/alerts.d.ts +101 -0
  1004. package/dist/shared/dto/alerts.d.ts.map +1 -0
  1005. package/dist/shared/dto/alerts.js +9 -0
  1006. package/dist/shared/dto/alerts.js.map +1 -0
  1007. package/dist/shared/dto/auth.d.ts +49 -0
  1008. package/dist/shared/dto/auth.d.ts.map +1 -0
  1009. package/dist/shared/dto/auth.js +14 -0
  1010. package/dist/shared/dto/auth.js.map +1 -0
  1011. package/dist/shared/dto/backups.d.ts +16 -0
  1012. package/dist/shared/dto/backups.d.ts.map +1 -0
  1013. package/dist/shared/dto/backups.js +9 -0
  1014. package/dist/shared/dto/backups.js.map +1 -0
  1015. package/dist/shared/dto/deploy.d.ts +28 -0
  1016. package/dist/shared/dto/deploy.d.ts.map +1 -0
  1017. package/dist/shared/dto/deploy.js +8 -0
  1018. package/dist/shared/dto/deploy.js.map +1 -0
  1019. package/dist/shared/dto/discovery.d.ts +24 -0
  1020. package/dist/shared/dto/discovery.d.ts.map +1 -0
  1021. package/dist/shared/dto/discovery.js +9 -0
  1022. package/dist/shared/dto/discovery.js.map +1 -0
  1023. package/dist/shared/dto/fleet.d.ts +35 -0
  1024. package/dist/shared/dto/fleet.d.ts.map +1 -0
  1025. package/dist/shared/dto/fleet.js +8 -0
  1026. package/dist/shared/dto/fleet.js.map +1 -0
  1027. package/dist/shared/dto/health.d.ts +26 -0
  1028. package/dist/shared/dto/health.d.ts.map +1 -0
  1029. package/dist/shared/dto/health.js +9 -0
  1030. package/dist/shared/dto/health.js.map +1 -0
  1031. package/dist/shared/dto/kubernetes.d.ts +44 -0
  1032. package/dist/shared/dto/kubernetes.d.ts.map +1 -0
  1033. package/dist/shared/dto/kubernetes.js +9 -0
  1034. package/dist/shared/dto/kubernetes.js.map +1 -0
  1035. package/dist/shared/dto/logs.d.ts +77 -0
  1036. package/dist/shared/dto/logs.d.ts.map +1 -0
  1037. package/dist/shared/dto/logs.js +10 -0
  1038. package/dist/shared/dto/logs.js.map +1 -0
  1039. package/dist/shared/dto/metrics.d.ts +65 -0
  1040. package/dist/shared/dto/metrics.d.ts.map +1 -0
  1041. package/dist/shared/dto/metrics.js +17 -0
  1042. package/dist/shared/dto/metrics.js.map +1 -0
  1043. package/dist/shared/dto/nodes.d.ts +347 -0
  1044. package/dist/shared/dto/nodes.d.ts.map +1 -0
  1045. package/dist/shared/dto/nodes.js +8 -0
  1046. package/dist/shared/dto/nodes.js.map +1 -0
  1047. package/dist/shared/dto/pipelines.d.ts +53 -0
  1048. package/dist/shared/dto/pipelines.d.ts.map +1 -0
  1049. package/dist/shared/dto/pipelines.js +9 -0
  1050. package/dist/shared/dto/pipelines.js.map +1 -0
  1051. package/dist/shared/dto/project.d.ts +142 -8
  1052. package/dist/shared/dto/project.d.ts.map +1 -1
  1053. package/dist/shared/dto/services.d.ts +858 -7
  1054. package/dist/shared/dto/services.d.ts.map +1 -1
  1055. package/dist/shared/dto/system-info.d.ts +93 -0
  1056. package/dist/shared/dto/system-info.d.ts.map +1 -0
  1057. package/dist/shared/dto/system-info.js +9 -0
  1058. package/dist/shared/dto/system-info.js.map +1 -0
  1059. package/dist/shared/dto/traces.d.ts +49 -0
  1060. package/dist/shared/dto/traces.d.ts.map +1 -0
  1061. package/dist/shared/dto/traces.js +9 -0
  1062. package/dist/shared/dto/traces.js.map +1 -0
  1063. package/dist/shared/env-config.d.ts +74 -0
  1064. package/dist/shared/env-config.d.ts.map +1 -0
  1065. package/dist/shared/env-config.js +99 -0
  1066. package/dist/shared/env-config.js.map +1 -0
  1067. package/dist/shared/events.d.ts +45 -39
  1068. package/dist/shared/events.d.ts.map +1 -1
  1069. package/dist/shared/events.js +6 -0
  1070. package/dist/shared/events.js.map +1 -1
  1071. package/dist/shared/format.d.ts +1 -1
  1072. package/dist/shared/format.d.ts.map +1 -1
  1073. package/dist/shared/format.js +4 -0
  1074. package/dist/shared/format.js.map +1 -1
  1075. package/dist/shared/local-infra-host.d.ts +28 -0
  1076. package/dist/shared/local-infra-host.d.ts.map +1 -0
  1077. package/dist/shared/local-infra-host.js +28 -0
  1078. package/dist/shared/local-infra-host.js.map +1 -0
  1079. package/dist/shared/node-check.d.ts +37 -0
  1080. package/dist/shared/node-check.d.ts.map +1 -0
  1081. package/dist/shared/node-check.js +41 -0
  1082. package/dist/shared/node-check.js.map +1 -0
  1083. package/dist/shared/paths.d.ts +29 -0
  1084. package/dist/shared/paths.d.ts.map +1 -0
  1085. package/dist/shared/paths.js +54 -0
  1086. package/dist/shared/paths.js.map +1 -0
  1087. package/dist/shared/pnpm.d.ts +29 -0
  1088. package/dist/shared/pnpm.d.ts.map +1 -0
  1089. package/dist/shared/pnpm.js +57 -0
  1090. package/dist/shared/pnpm.js.map +1 -0
  1091. package/dist/shared/private-files.d.ts +29 -0
  1092. package/dist/shared/private-files.d.ts.map +1 -0
  1093. package/dist/shared/private-files.js +109 -0
  1094. package/dist/shared/private-files.js.map +1 -0
  1095. package/dist/shared/redact-env.d.ts +24 -0
  1096. package/dist/shared/redact-env.d.ts.map +1 -0
  1097. package/dist/shared/redact-env.js +36 -0
  1098. package/dist/shared/redact-env.js.map +1 -0
  1099. package/dist/shared/release-reading.d.ts +85 -0
  1100. package/dist/shared/release-reading.d.ts.map +1 -0
  1101. package/dist/shared/release-reading.js +81 -0
  1102. package/dist/shared/release-reading.js.map +1 -0
  1103. package/dist/shared/roles.d.ts +58 -3
  1104. package/dist/shared/roles.d.ts.map +1 -1
  1105. package/dist/shared/roles.js +55 -0
  1106. package/dist/shared/roles.js.map +1 -1
  1107. package/dist/shared/rpcauth.d.ts +13 -0
  1108. package/dist/shared/rpcauth.d.ts.map +1 -0
  1109. package/dist/shared/rpcauth.js +23 -0
  1110. package/dist/shared/rpcauth.js.map +1 -0
  1111. package/dist/shared/shell-escape.d.ts +9 -0
  1112. package/dist/shared/shell-escape.d.ts.map +1 -0
  1113. package/dist/shared/shell-escape.js +11 -0
  1114. package/dist/shared/shell-escape.js.map +1 -0
  1115. package/dist/shared/sync-reading.d.ts +45 -0
  1116. package/dist/shared/sync-reading.d.ts.map +1 -0
  1117. package/dist/shared/sync-reading.js +42 -0
  1118. package/dist/shared/sync-reading.js.map +1 -0
  1119. package/dist/shared/tokens.d.ts +19 -0
  1120. package/dist/shared/tokens.d.ts.map +1 -1
  1121. package/dist/shared/tokens.js +28 -0
  1122. package/dist/shared/tokens.js.map +1 -1
  1123. package/dist/webapp/webapp.service.d.ts +33 -2
  1124. package/dist/webapp/webapp.service.d.ts.map +1 -1
  1125. package/dist/webapp/webapp.service.js +143 -31
  1126. package/dist/webapp/webapp.service.js.map +1 -1
  1127. package/dist/workers/health-monitor-process.d.ts.map +1 -1
  1128. package/dist/workers/health-monitor-process.js +10 -0
  1129. package/dist/workers/health-monitor-process.js.map +1 -1
  1130. package/dist/workers/health-monitor.service.d.ts +45 -3
  1131. package/dist/workers/health-monitor.service.d.ts.map +1 -1
  1132. package/dist/workers/health-monitor.service.js +152 -25
  1133. package/dist/workers/health-monitor.service.js.map +1 -1
  1134. package/dist/workers/system-worker-manager.d.ts +43 -0
  1135. package/dist/workers/system-worker-manager.d.ts.map +1 -1
  1136. package/dist/workers/system-worker-manager.js +150 -3
  1137. package/dist/workers/system-worker-manager.js.map +1 -1
  1138. package/dist/workers/types.d.ts +8 -2
  1139. package/dist/workers/types.d.ts.map +1 -1
  1140. package/package.json +68 -30
  1141. package/webapp/dist/BUILD.json +5 -0
  1142. package/webapp/dist/assets/FormControlLabel-BbSxpQwN.js +1 -0
  1143. package/webapp/dist/assets/Grid-DizFZYue.js +1 -0
  1144. package/webapp/dist/assets/Switch-Bfiqzx8l.js +1 -0
  1145. package/webapp/dist/assets/alerts-DH_RVRS3.js +1 -0
  1146. package/webapp/dist/assets/apps-B96355sz.js +1 -0
  1147. package/webapp/dist/assets/audit-CYGtbi4k.js +1 -0
  1148. package/webapp/dist/assets/client-BmqFExWT.js +1 -0
  1149. package/webapp/dist/assets/constants-veKGFkC6.js +1 -0
  1150. package/webapp/dist/assets/containers-B_TpThln.js +2 -0
  1151. package/webapp/dist/assets/daemon-memory-CCPZ4cV_.js +1 -0
  1152. package/webapp/dist/assets/dashboard-DHf2_65N.js +1 -0
  1153. package/webapp/dist/assets/dashboard-builder-CWY2pjeG.js +1 -0
  1154. package/webapp/dist/assets/deploy-release-dialog-Dp-zpSAd.js +1 -0
  1155. package/webapp/dist/assets/deployments-tJI6O3fK.js +1 -0
  1156. package/webapp/dist/assets/detail-B2zP1Hto.js +8 -0
  1157. package/webapp/dist/assets/detail-CoIrwmsy.js +1 -0
  1158. package/webapp/dist/assets/detail-DWaGaOtL.js +2 -0
  1159. package/webapp/dist/assets/formatters-BqjL73N3.js +1 -0
  1160. package/webapp/dist/assets/index-Dvo0iprx.css +1 -0
  1161. package/webapp/dist/assets/index-vMnY_4jR.js +412 -0
  1162. package/webapp/dist/assets/logs-DTRjOfX8.js +12 -0
  1163. package/webapp/dist/assets/maplibre-gl-DowHoZAx.js +814 -0
  1164. package/webapp/dist/assets/metrics-UCajtdox.js +2 -0
  1165. package/webapp/dist/assets/nodes-DuTVU0f6.js +7 -0
  1166. package/webapp/dist/assets/pipelines-C8BeDcAl.js +4 -0
  1167. package/webapp/dist/assets/projects-Dp_zFfLY.js +1 -0
  1168. package/webapp/dist/assets/react-DlmbjZJV.js +1 -0
  1169. package/webapp/dist/assets/react-apexcharts.esm-C8pn45ml.js +97 -0
  1170. package/webapp/dist/assets/release-bits-BzTKz5fM.js +1 -0
  1171. package/webapp/dist/assets/release-build-panel-DcW9YcXQ.js +1 -0
  1172. package/webapp/dist/assets/releases-B4hcr1bE.js +2 -0
  1173. package/webapp/dist/assets/rolldown-runtime-XKyaHtl9.js +1 -0
  1174. package/webapp/dist/assets/settings-DIteZTbe.js +1 -0
  1175. package/webapp/dist/assets/settled-pair-C7SjZuns.js +1 -0
  1176. package/webapp/dist/assets/sign-in-D4kMBOHp.js +1 -0
  1177. package/webapp/dist/assets/stacks-Brnf-EB7.js +1 -0
  1178. package/webapp/dist/assets/sync-reading-C3PnJ7AX.js +1 -0
  1179. package/webapp/dist/assets/system-info-CFDNVKkU.js +1 -0
  1180. package/webapp/dist/assets/topology-7WeTUsDN.js +12 -0
  1181. package/webapp/dist/assets/topology-DLioOiRN.css +1 -0
  1182. package/webapp/dist/assets/traces-9c3gwCN-.js +1 -0
  1183. package/webapp/dist/assets/use-stack-context-CnA5FTHu.js +1 -0
  1184. package/webapp/dist/index.html +6 -6
  1185. package/dist/commands/dev.d.ts +0 -23
  1186. package/dist/commands/dev.d.ts.map +0 -1
  1187. package/dist/commands/dev.js +0 -76
  1188. package/dist/commands/dev.js.map +0 -1
  1189. package/dist/config/define-app.d.ts +0 -6
  1190. package/dist/config/define-app.d.ts.map +0 -1
  1191. package/dist/config/define-app.js +0 -38
  1192. package/dist/config/define-app.js.map +0 -1
  1193. package/dist/infrastructure/gateway-generator.d.ts +0 -63
  1194. package/dist/infrastructure/gateway-generator.d.ts.map +0 -1
  1195. package/dist/infrastructure/gateway-generator.js +0 -253
  1196. package/dist/infrastructure/gateway-generator.js.map +0 -1
  1197. package/dist/monitoring/health.service.d.ts +0 -14
  1198. package/dist/monitoring/health.service.d.ts.map +0 -1
  1199. package/dist/monitoring/health.service.js +0 -50
  1200. package/dist/monitoring/health.service.js.map +0 -1
  1201. package/dist/monitoring/metrics.service.d.ts +0 -14
  1202. package/dist/monitoring/metrics.service.d.ts.map +0 -1
  1203. package/dist/monitoring/metrics.service.js +0 -40
  1204. package/dist/monitoring/metrics.service.js.map +0 -1
  1205. package/dist/orchestrator/bootstrap-runner.d.ts +0 -15
  1206. package/dist/orchestrator/bootstrap-runner.d.ts.map +0 -1
  1207. package/dist/orchestrator/bootstrap-runner.js +0 -152
  1208. package/dist/orchestrator/bootstrap-runner.js.map +0 -1
  1209. package/dist/orchestrator/topology-proxy.d.ts +0 -65
  1210. package/dist/orchestrator/topology-proxy.d.ts.map +0 -1
  1211. package/dist/orchestrator/topology-proxy.js +0 -209
  1212. package/dist/orchestrator/topology-proxy.js.map +0 -1
  1213. package/dist/orchestrator/topology-relay.d.ts +0 -111
  1214. package/dist/orchestrator/topology-relay.d.ts.map +0 -1
  1215. package/dist/orchestrator/topology-relay.js +0 -255
  1216. package/dist/orchestrator/topology-relay.js.map +0 -1
  1217. package/dist/services/auth-middleware.d.ts +0 -47
  1218. package/dist/services/auth-middleware.d.ts.map +0 -1
  1219. package/dist/services/auth-middleware.js +0 -158
  1220. package/dist/services/auth-middleware.js.map +0 -1
  1221. package/dist/services/environment.rpc-service.d.ts +0 -22
  1222. package/dist/services/environment.rpc-service.d.ts.map +0 -1
  1223. package/dist/services/environment.rpc-service.js +0 -65
  1224. package/dist/services/environment.rpc-service.js.map +0 -1
  1225. package/dist/services/environment.service.d.ts +0 -87
  1226. package/dist/services/environment.service.d.ts.map +0 -1
  1227. package/dist/services/environment.service.js +0 -220
  1228. package/dist/services/environment.service.js.map +0 -1
  1229. package/dist/services/metrics-collector.service.d.ts +0 -103
  1230. package/dist/services/metrics-collector.service.d.ts.map +0 -1
  1231. package/dist/services/metrics-collector.service.js +0 -235
  1232. package/dist/services/metrics-collector.service.js.map +0 -1
  1233. package/dist/services/metrics.rpc-service.d.ts +0 -41
  1234. package/dist/services/metrics.rpc-service.d.ts.map +0 -1
  1235. package/dist/services/metrics.rpc-service.js +0 -90
  1236. package/dist/services/metrics.rpc-service.js.map +0 -1
  1237. package/dist/services/metrics.service.d.ts +0 -72
  1238. package/dist/services/metrics.service.d.ts.map +0 -1
  1239. package/dist/services/metrics.service.js +0 -185
  1240. package/dist/services/metrics.service.js.map +0 -1
  1241. package/dist/supervisor/omnitron-supervisor.d.ts +0 -21
  1242. package/dist/supervisor/omnitron-supervisor.d.ts.map +0 -1
  1243. package/dist/supervisor/omnitron-supervisor.js +0 -43
  1244. package/dist/supervisor/omnitron-supervisor.js.map +0 -1
  1245. package/dist/webapp/static-server.d.ts +0 -24
  1246. package/dist/webapp/static-server.d.ts.map +0 -1
  1247. package/dist/webapp/static-server.js +0 -152
  1248. package/dist/webapp/static-server.js.map +0 -1
  1249. package/webapp/dist/assets/Badge-D2ZBP5iK.js +0 -1
  1250. package/webapp/dist/assets/Card-vO88g06B.js +0 -1
  1251. package/webapp/dist/assets/CardActions-C9h7Q2wW.js +0 -1
  1252. package/webapp/dist/assets/CardContent-BA_Xmmhk.js +0 -1
  1253. package/webapp/dist/assets/CardHeader-ceIs__6x.js +0 -1
  1254. package/webapp/dist/assets/Chip-Ckk0xUhK.js +0 -1
  1255. package/webapp/dist/assets/DialogActions-BhHM7hov.js +0 -1
  1256. package/webapp/dist/assets/DialogContentText-BT2JQRmK.js +0 -1
  1257. package/webapp/dist/assets/FormControlLabel-CcQByVS8.js +0 -1
  1258. package/webapp/dist/assets/Grid-14PETRQW.js +0 -1
  1259. package/webapp/dist/assets/InputAdornment-B8aHIsQ7.js +0 -1
  1260. package/webapp/dist/assets/Skeleton-B74-Ds6O.js +0 -37
  1261. package/webapp/dist/assets/Switch-DvWD2wh4.js +0 -1
  1262. package/webapp/dist/assets/SwitchBase-CDU4nY8O.js +0 -1
  1263. package/webapp/dist/assets/Tab-DcCOYYp5.js +0 -1
  1264. package/webapp/dist/assets/TableContainer-CzTh4mu-.js +0 -1
  1265. package/webapp/dist/assets/TableRow-BySEreZR.js +0 -2
  1266. package/webapp/dist/assets/TextField-BLf-BHz2.js +0 -1
  1267. package/webapp/dist/assets/alerts-DnRbT41C.js +0 -1
  1268. package/webapp/dist/assets/apps-CdR0VH2d.js +0 -1
  1269. package/webapp/dist/assets/chunk-DtcAfS_W.js +0 -1
  1270. package/webapp/dist/assets/client-DU8I_lDc.js +0 -2
  1271. package/webapp/dist/assets/constants-BgnT-Xdy.js +0 -1
  1272. package/webapp/dist/assets/containers-Dw-rRXV1.js +0 -1
  1273. package/webapp/dist/assets/dashboard-CzgB0uZz.js +0 -1
  1274. package/webapp/dist/assets/dashboard-builder-DsA0yg33.js +0 -1
  1275. package/webapp/dist/assets/deployments-DtQAoIcQ.js +0 -1
  1276. package/webapp/dist/assets/detail-BJR-G7JG.js +0 -1
  1277. package/webapp/dist/assets/detail-nlCeSltl.js +0 -8
  1278. package/webapp/dist/assets/formatters-Do41SRmE.js +0 -1
  1279. package/webapp/dist/assets/index-ByQDwp2r.js +0 -566
  1280. package/webapp/dist/assets/index-CHVXOsTS.css +0 -1
  1281. package/webapp/dist/assets/isMuiElement-DwF2naaF.js +0 -1
  1282. package/webapp/dist/assets/logs-Kn8o4H7k.js +0 -11
  1283. package/webapp/dist/assets/metrics-DN8Zu5wT.js +0 -2
  1284. package/webapp/dist/assets/nodes-Bgch7AtN.js +0 -7
  1285. package/webapp/dist/assets/pipelines-Cry3qm-S.js +0 -4
  1286. package/webapp/dist/assets/popper-BJFSj6rc.js +0 -1
  1287. package/webapp/dist/assets/preload-helper-DSXbuxSR.js +0 -1
  1288. package/webapp/dist/assets/projects-Dym2GI1U.js +0 -1
  1289. package/webapp/dist/assets/react-apexcharts.esm-CSGK5Cxn.js +0 -958
  1290. package/webapp/dist/assets/react-apexcharts.esm-DEAWOwx5.js +0 -1003
  1291. package/webapp/dist/assets/settings-va3zTcow.js +0 -1
  1292. package/webapp/dist/assets/sign-in-ik35umbp.js +0 -1
  1293. package/webapp/dist/assets/stacks-CVFOnxbF.js +0 -1
  1294. package/webapp/dist/assets/system-info-npIXAjX2.js +0 -1
  1295. package/webapp/dist/assets/topology-CHpVij2M.css +0 -1
  1296. package/webapp/dist/assets/topology-bDe70vLs.js +0 -12
  1297. package/webapp/dist/assets/traces-DIIcT8DF.js +0 -1
  1298. package/webapp/dist/assets/use-stack-context-DHLwZv1r.js +0 -1
  1299. /package/webapp/dist/assets/{fs-CgZLtHiQ.js → fs-Ccl3sAcp.js} +0 -0
@@ -13,23 +13,248 @@
13
13
  *
14
14
  * Artifact path on remote: /opt/omnitron/artifacts/<project>/<app>/<version>/
15
15
  * Remote daemon config: /etc/omnitron/omnitron.config.ts
16
+ *
17
+ * NEVER EXECUTED, as of 2026-09-14. Measured rather than assumed: no log in
18
+ * `~/.omnitron/logs` carries a single line this class emits ("Starting
19
+ * deployment to node", "Slave provisioned", "Deployment successful"), no
20
+ * artifact directory exists, and — decisively — no project config declares
21
+ * `stacks.nodes`, which `startRemoteStack` requires before it can reach here.
22
+ * So both entry points are unreachable in every current configuration.
23
+ *
24
+ * This matters to anyone reading the code below and taking it for a working
25
+ * path. It is careful code — arguments are quoted, path segments validated,
26
+ * heredocs replaced with base64 — and none of that has ever met a real host.
27
+ * Its first run will be its first test. Treat a green read of this file as
28
+ * evidence about intent, not about behaviour.
29
+ *
30
+ * The first thing that first run found, 2026-09-14: **it could not log in.**
31
+ * Every command here went through `ssh -o BatchMode=yes` with an optional
32
+ * `-i <keyfile>`, and `BatchMode=yes` disables every interactive method —
33
+ * password and key passphrase both. The console's Add Node dialog collects
34
+ * exactly those, encrypts them in the daemon's vault, and the health monitor
35
+ * uses them through `ExecutionService.ssh()` on every check round. So the
36
+ * product had two SSH implementations: one that can present what the operator
37
+ * gave it, and one — this — that cannot, on the path where it matters most.
38
+ *
39
+ * Measured against the test host, whose SSH answers a password in 311 ms:
40
+ *
41
+ * ssh -o BatchMode=yes root@<host> 'echo ok'
42
+ * → root@<host>: Permission denied (publickey,password).
43
+ *
44
+ * That is the answer to "why can a machine added in the console not be
45
+ * deployed to": not policy, not a missing feature — the deployer could not
46
+ * authenticate as the operator had arranged. It goes through the same
47
+ * `ExecutionService` as the checks now, and takes a `DeployTarget` carrying
48
+ * the credentials rather than an `IStackNode` whose `ISSHConfig` has nowhere
49
+ * to put them.
16
50
  */
17
- import { execFile } from 'node:child_process';
18
- import { promisify } from 'node:util';
19
- const exec = promisify(execFile);
20
- /** Escape a string for safe use inside a single-quoted shell argument. */
21
- function shellEscape(s) {
22
- return "'" + s.replace(/'/g, "'\\''") + "'";
51
+ import { PLATFORM_PROBE, parsePlatformProbe, planProvisioning, describePlan, } from './remote-provisioner.js';
52
+ import { installSteps, installPreflightCommand, versionDir, activateSteps, pruneSteps } from './bundle-builder.js';
53
+ import { reachabilityRule, reachabilityCommand } from '../infrastructure/gateway-reachability.js';
54
+ import { readNodeHealth, readNodeStatus } from '../project/node-app-health.js';
55
+ import { decideRedeploy, decideNodeStackStart, decideSlaveDaemonRestart, artifactChanged, readChecksumCommand, parseRecordedChecksum, ARTIFACT_CHECKSUM_FILE, NODE_CONFIG_HASH_FILE, } from './redeploy-decision.js';
56
+ import { deliveredProbeCommand, parseDeliveredProbe, checkDelivered, } from '../release/delivered.js';
57
+ import crypto from 'node:crypto';
58
+ import { AsyncLocalStorage } from 'node:async_hooks';
59
+ import { assertControlledDaemonPort, assertServingBundle, bundleProbeCommand, contractPreflightCommand, executeHostUpgrade, hookCommand, inventoryCommand, planHostUpgrade, receiptCommand, verifyRestoredInventory, updateContractBindingsCommand, runtimeProbeCommand, runtimeStartCommand, runtimeStopCommand, appHealthCommand, pendingReceiptsCommand, recoverHostUpgrade, stoppedDaemonCommand, } from './host-upgrade.js';
60
+ import { gunzipSync } from 'node:zlib';
61
+ import { shellEscape } from '../shared/shell-escape.js';
62
+ import { fromDataChannel, throughDataChannel } from '../execution/data-channel.js';
63
+ /** A node that could not be asked at all — the SSH call itself failed. */
64
+ const UNREADABLE = -3;
65
+ import { withNodeLeases } from './node-deploy-lease.js';
66
+ import { hostOperationBarrierCommand } from './host-operation-barrier.js';
67
+ import { migrationOutput } from './migration-output.js';
68
+ /**
69
+ * A name that is safe to place in a remote filesystem path.
70
+ *
71
+ * Project, app and version names are interpolated into
72
+ * `/opt/omnitron/artifacts/<project>/<app>/<version>` and the result is sent
73
+ * to a remote shell running as the SSH user, which defaults to root. Quoting
74
+ * makes that safe against injection but not against the path itself: a
75
+ * project literally named `../..` resolves outside the artifact root, and
76
+ * `mkdir -p` then creates it there. Quoting is not a substitute for the
77
+ * value meaning what the path assumes it means.
78
+ *
79
+ * These names come from the project registry and the console's own forms —
80
+ * operator-supplied, not attacker-supplied in the ordinary case. It is
81
+ * defence in depth, and the cost of it is a regular expression.
82
+ *
83
+ * @throws Error naming the segment and what is allowed.
84
+ */
85
+ export function assertRemotePathSegment(kind, value) {
86
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value) || value.includes('..')) {
87
+ throw new Error(`Refusing to deploy: ${kind} ${JSON.stringify(value)} is not usable in a remote path. ` +
88
+ `Allowed: letters, digits, dot, dash and underscore, starting with a letter or digit, and no "..".`);
89
+ }
90
+ return value;
91
+ }
92
+ /**
93
+ * Where an app's artifact lives on a node — one place, because the install
94
+ * writes there and the step that starts the app records there.
95
+ */
96
+ export function artifactDirOnNode(project, app, version) {
97
+ return (`/opt/omnitron/artifacts/${assertRemotePathSegment('project name', project)}` +
98
+ `/${assertRemotePathSegment('app name', app)}` +
99
+ `/${assertRemotePathSegment('version', version)}`);
100
+ }
101
+ /** The credentials half, in the shape `ExecutionService` takes. */
102
+ function sshTargetOf(target) {
103
+ const ssh = { host: target.host };
104
+ if (target.sshPort != null)
105
+ ssh.port = target.sshPort;
106
+ if (target.username)
107
+ ssh.username = target.username;
108
+ if (target.privateKey)
109
+ ssh.privateKey = target.privateKey;
110
+ if (target.passphrase)
111
+ ssh.passphrase = target.passphrase;
112
+ if (target.password)
113
+ ssh.password = target.password;
114
+ return ssh;
115
+ }
116
+ /** Project a node declared in a project config into a deploy target. */
117
+ export function stackNodeToDeployTarget(node) {
118
+ const target = { host: node.host };
119
+ if (node.port != null)
120
+ target.daemonPort = node.port;
121
+ if (node.ssh?.port != null)
122
+ target.sshPort = node.ssh.port;
123
+ if (node.ssh?.user)
124
+ target.username = node.ssh.user;
125
+ // A path, read by the engine. `ISSHConfig` has no passphrase field, so a
126
+ // key declared here must be one that needs none — which is worth knowing
127
+ // when a deployment from a config fails and the same node works from the
128
+ // console.
129
+ if (node.ssh?.privateKey)
130
+ target.privateKey = node.ssh.privateKey;
131
+ if (node.apps)
132
+ target.apps = node.apps;
133
+ if (node.label)
134
+ target.label = node.label;
135
+ return target;
23
136
  }
137
+ /**
138
+ * Fill a stack node's connection details from the node registry.
139
+ *
140
+ * A stack says WHICH host an app runs on; the node registry says HOW to
141
+ * reach it — the user, the port, and the key or password, which are held
142
+ * encrypted in the daemon's vault because they are credentials and a config
143
+ * file in a repository is not where those go.
144
+ *
145
+ * Nothing joined the two. `stackNodeToDeployTarget` read only the stack's
146
+ * own `ssh` block, so deploying to a machine that the console had already
147
+ * registered, provisioned and been connected to failed at the first
148
+ * connection:
149
+ *
150
+ * Invalid SSH options: Either privateKey or password must be provided
151
+ *
152
+ * — for a node the same daemon was, at that moment, holding an SSH tunnel to.
153
+ *
154
+ * The stack's explicit values win where it has them: an operator who wrote
155
+ * `ssh.user` in the stack config meant it, and a registry entry is the
156
+ * default, not an override. Everything the stack leaves out comes from the
157
+ * registry.
158
+ */
159
+ export function withNodeCredentials(target, registered) {
160
+ if (!registered)
161
+ return target;
162
+ const merged = { ...target };
163
+ if (merged.sshPort == null && registered.port != null)
164
+ merged.sshPort = registered.port;
165
+ if (!merged.username && registered.username)
166
+ merged.username = registered.username;
167
+ if (!merged.privateKey && registered.privateKey)
168
+ merged.privateKey = registered.privateKey;
169
+ if (!merged.passphrase && registered.passphrase)
170
+ merged.passphrase = registered.passphrase;
171
+ if (!merged.password && registered.password)
172
+ merged.password = registered.password;
173
+ return merged;
174
+ }
175
+ /**
176
+ * How long a freshly provisioned slave has to answer.
177
+ *
178
+ * A daemon start is an application boot: a DI container, a module graph, a
179
+ * SQLite open. On a loaded host this is tens of seconds, and the old
180
+ * three-second sleep was shorter than the work by an order of magnitude.
181
+ */
182
+ const SLAVE_START_TIMEOUT_MS = 120_000;
183
+ /** How often it is asked, while it is starting. */
184
+ const DAEMON_POLL_MS = 3_000;
24
185
  // =============================================================================
25
186
  // RemoteDeployer
26
187
  // =============================================================================
188
+ /**
189
+ * The parts of a postgres URL, for the environment a migrator expects.
190
+ *
191
+ * Returns null rather than guessing at a URL it cannot read: a migration run
192
+ * against half-parsed connection details is worse than one that does not run,
193
+ * because it can succeed somewhere nobody meant.
194
+ */
195
+ function parseDatabaseUrl(url) {
196
+ try {
197
+ const u = new URL(url);
198
+ if (!u.protocol.startsWith('postgres'))
199
+ return null;
200
+ return {
201
+ host: u.hostname,
202
+ port: u.port || '5432',
203
+ database: decodeURIComponent(u.pathname.replace(/^\//, '')),
204
+ user: decodeURIComponent(u.username),
205
+ password: decodeURIComponent(u.password),
206
+ };
207
+ }
208
+ catch {
209
+ return null;
210
+ }
211
+ }
212
+ /**
213
+ * Is this a gzip stream of a COMPLETE tar archive?
214
+ *
215
+ * Two ways to be short, told apart: a gzip stream cut off fails to
216
+ * decompress («unexpected end of file»), and a tar cut off inside a gzip
217
+ * that finished — `tar` killed, `gzip` still closing its own stream —
218
+ * decompresses to something that is not a whole number of 512-byte blocks,
219
+ * or does not end in the two zero blocks every tar writer closes an archive
220
+ * with. Answers `true` or the reason it is not.
221
+ */
222
+ export function wholeTarGz(archive) {
223
+ let tar;
224
+ try {
225
+ tar = gunzipSync(archive);
226
+ }
227
+ catch (err) {
228
+ return `the gzip stream is broken: ${err.message}`;
229
+ }
230
+ if (tar.length < 1024 || tar.length % 512 !== 0)
231
+ return `the tar inside is ${tar.length} bytes, not whole blocks`;
232
+ return tar.subarray(tar.length - 1024).every((b) => b === 0) ? true : 'the tar inside has no end-of-archive marker';
233
+ }
27
234
  export class RemoteDeployer {
28
235
  logger;
236
+ execution;
237
+ deploymentLeases = new AsyncLocalStorage();
29
238
  /** Active deployment progress handlers */
30
239
  progressHandlers = [];
31
- constructor(logger) {
240
+ /**
241
+ * The last thing that happened to each app on each node.
242
+ *
243
+ * A deployment publishes progress as it goes and the console polls; without
244
+ * somewhere for an event to WAIT, those two never meet, and a deployment
245
+ * that takes a quarter of an hour showed the operator one row that said
246
+ * `deploying` from beginning to end. Everything that said WHERE it was —
247
+ * transferring, installing, the message naming the failing step — reached a
248
+ * handler that re-emitted it to nobody.
249
+ *
250
+ * Keyed by node and app, so this is bounded by the fleet rather than by how
251
+ * often anyone deploys, and the entry for an app is always its current
252
+ * state rather than a scroll an operator has to read to the end of.
253
+ */
254
+ lastProgress = new Map();
255
+ constructor(logger, execution) {
32
256
  this.logger = logger;
257
+ this.execution = execution;
33
258
  }
34
259
  /**
35
260
  * Register a progress handler for real-time deployment updates.
@@ -45,34 +270,266 @@ export class RemoteDeployer {
45
270
  /**
46
271
  * Deploy an artifact to a single remote node.
47
272
  */
48
- async deployToNode(node, artifact, project) {
273
+ async deployToNode(target, artifact, project, options) {
49
274
  const startTime = Date.now();
50
- const nodeKey = `${node.host}:${node.port ?? 9700}`;
275
+ const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
51
276
  this.logger.info({ node: nodeKey, app: artifact.app, version: artifact.version }, 'Starting deployment to node');
52
277
  try {
53
278
  // 1. Verify SSH connectivity
54
279
  this.emitProgress(nodeKey, artifact.app, 'pending', 0, 'Connecting via SSH...');
55
- await this.verifySSH(node);
280
+ await this.verifySSH(target);
56
281
  // 2. Ensure remote directory structure
57
- const remotePath = `/opt/omnitron/artifacts/${project}/${artifact.app}/${artifact.version}`;
58
- await this.sshExec(node, `mkdir -p ${shellEscape(remotePath)}`);
282
+ const remotePath = artifactDirOnNode(project, artifact.app, artifact.version);
283
+ await this.sshExec(target, `mkdir -p ${shellEscape(remotePath)}`);
284
+ // 2a. What the node already has.
285
+ //
286
+ // A master's boot-time autostart deploys every remote stack again, so
287
+ // the same artifact was rebuilt, transferred, unpacked, `npm
288
+ // install`ed and its app restarted on every master restart — four
289
+ // times in one afternoon here, twenty-four application restarts on a
290
+ // node whose files were identical each time.
291
+ //
292
+ // The comparison is the artifact's own sha256 against the one the node
293
+ // recorded when the app last came up healthy on it. `options.force`
294
+ // deploys anyway, for an operator who has reason to doubt the record —
295
+ // `omnitron stack start --reinstall`, which until 2026-09-24 nothing
296
+ // could pass: the escape hatch this comment named had no door.
297
+ if (options?.force !== true) {
298
+ const recorded = parseRecordedChecksum(await this.sshExec(target, readChecksumCommand(shellEscape(remotePath))));
299
+ if (artifactChanged(recorded, artifact.checksum) === false) {
300
+ const duration = Date.now() - startTime;
301
+ this.emitProgress(nodeKey, artifact.app, 'success', 75, 'Already installed — unchanged');
302
+ this.logger.info({ node: nodeKey, app: artifact.app, version: artifact.version, sha: recorded.slice(0, 12) }, 'The node already has this artifact — not transferring it again');
303
+ return {
304
+ node: nodeKey,
305
+ app: artifact.app,
306
+ version: artifact.version,
307
+ status: 'success',
308
+ duration,
309
+ unchanged: true,
310
+ };
311
+ }
312
+ }
59
313
  // 3. Transfer artifact
60
314
  this.emitProgress(nodeKey, artifact.app, 'transferring', 20, 'Transferring artifact...');
61
315
  const remoteFile = `${remotePath}/${artifact.app}-${artifact.version}.tar.gz`;
62
- await this.scpTransfer(node, artifact.path, remoteFile);
63
- // 4. Extract on remote
316
+ await this.scpTransfer(target, artifact.path, remoteFile);
317
+ // 3b. And ask the node whether what it has is what was sent.
318
+ //
319
+ // This used to go straight to `tar -xzf`. The `.artifact-sha256`
320
+ // written further down reads like this check and is not one: it holds
321
+ // `artifact.checksum`, the hash of the build INPUTS, it is written
322
+ // AFTER the unpack, and it is compared against the next build rather
323
+ // than against the file. So a truncated transfer reached tar, which
324
+ // failed with whatever tar says about a corrupt archive, and one that
325
+ // happened to end on a record boundary unpacked a subset and reported
326
+ // success.
327
+ //
328
+ // Skipped only when the artifact carries no tarball hash — an
329
+ // artifact found on disk by `listArtifacts` rather than built here.
330
+ // That is an absence of information, not a verdict, and it is said out
331
+ // loud rather than passed over in silence.
332
+ if (artifact.tarballSha256) {
333
+ const probe = await this.sshExec(target, deliveredProbeCommand(remoteFile));
334
+ const verdict = checkDelivered({ sha256: artifact.tarballSha256, bytes: artifact.size }, parseDeliveredProbe(probe));
335
+ if (!verdict.ok) {
336
+ const duration = Date.now() - startTime;
337
+ this.emitProgress(nodeKey, artifact.app, 'failed', 30, verdict.because);
338
+ this.logger.error({ node: nodeKey, app: artifact.app, file: remoteFile }, `Artifact transfer failed verification — ${verdict.because}`);
339
+ return {
340
+ node: nodeKey,
341
+ app: artifact.app,
342
+ version: artifact.version,
343
+ status: 'failed',
344
+ duration,
345
+ error: verdict.because,
346
+ };
347
+ }
348
+ }
349
+ else {
350
+ this.logger.warn({ node: nodeKey, app: artifact.app }, 'Artifact carries no tarball hash — the transfer was not verified');
351
+ }
352
+ // 4. Extract on remote, into an empty directory.
353
+ //
354
+ // `tar -xzf` over whatever was there before leaves everything the new
355
+ // archive does not name — and what was there before is a `node_modules`
356
+ // of a different SHAPE. The first deployment after this builder changed
357
+ // put an npm manifest beside a pnpm tree and `npm install` answered
358
+ //
359
+ // npm error Cannot read properties of null (reading 'edgesOut')
360
+ //
361
+ // which is npm walking a dependency graph through symlinks it did not
362
+ // create. The same reasoning as `installSteps`, which removes the
363
+ // version directory before unpacking for exactly this: an install
364
+ // reconciling against a tree from another tool is not this version.
365
+ //
366
+ // Everything except the archive just uploaded, and only inside the
367
+ // artifact's own version directory — every segment of which was checked
368
+ // by `assertRemotePathSegment` above.
64
369
  this.emitProgress(nodeKey, artifact.app, 'extracting', 50, 'Extracting artifact...');
65
- await this.sshExec(node, `cd ${shellEscape(remotePath)} && tar -xzf ${shellEscape(`${artifact.app}-${artifact.version}.tar.gz`)}`);
66
- // 5. Install production dependencies
67
- this.emitProgress(nodeKey, artifact.app, 'extracting', 65, 'Installing dependencies...');
68
- await this.sshExec(node, `cd ${shellEscape(remotePath)} && npm install --production --ignore-scripts 2>/dev/null || true`);
69
- // 6. Signal remote daemon to restart the app
370
+ const archiveName = `${artifact.app}-${artifact.version}.tar.gz`;
371
+ await this.sshExec(target, `cd ${shellEscape(remotePath)} && ` +
372
+ `find . -mindepth 1 -maxdepth 1 ! -name ${shellEscape(archiveName)} -exec rm -rf {} + && ` +
373
+ `tar -xzf ${shellEscape(archiveName)}`);
374
+ // 5. Install the dependencies ON THE NODE.
375
+ //
376
+ // Three shapes of this step, and the two that were wrong are worth
377
+ // keeping written down because each looked finished.
378
+ //
379
+ // It first ran `npm install --production --ignore-scripts 2>/dev/null
380
+ // || true`. That could not work even once — the manifests use pnpm's
381
+ // `workspace:` protocol, npm answers `EUNSUPPORTEDPROTOCOL` — and the
382
+ // `|| true` meant nobody found out.
383
+ //
384
+ // It then trusted the artifact to arrive complete, from `pnpm deploy`,
385
+ // and checked `test -d node_modules`. The artifact was NOT complete:
386
+ // every `@omnitron-dev/*` package was a symlink into a home directory
387
+ // on the master, twenty-three of them, and a directory of dangling
388
+ // symlinks passes `test -d`. A symlink to `/opt/omnitron/current`
389
+ // patched the one the first import happened to name; the other
390
+ // twenty-two were waiting behind it.
391
+ //
392
+ // Now the artifact carries `vendor/*.tgz` and a manifest that installs
393
+ // them, exactly like the daemon's own bundle, and the install runs
394
+ // here. That is not a cost to be minimised — it is the only step that
395
+ // happens on a machine that knows it is Linux, which is what decides
396
+ // between `@esbuild/darwin-arm64` and `@esbuild/linux-x64`.
397
+ this.emitProgress(nodeKey, artifact.app, 'installing', 60, 'Installing dependencies on the node...');
398
+ try {
399
+ await this.sshExec(target, `cd ${shellEscape(remotePath)} && npm install --omit=dev --no-audit --no-fund`, 900_000);
400
+ }
401
+ catch (err) {
402
+ const duration = Date.now() - startTime;
403
+ const detail = `dependency install failed on the node: ${err.message.slice(0, 400)}`;
404
+ this.emitProgress(nodeKey, artifact.app, 'failed', 60, detail);
405
+ this.logger.error({ node: nodeKey, app: artifact.app, path: remotePath }, detail);
406
+ return { node: nodeKey, app: artifact.app, version: artifact.version, status: 'failed', duration, error: detail };
407
+ }
408
+ // 5a. One physical copy of every package the daemon shares with the app.
409
+ //
410
+ // A supervised app does not run alone in its process: omnitron's own
411
+ // `bootstrap-process.js` is the entry point, and the app's modules are
412
+ // loaded into it. Titan keeps identities at module scope — the
413
+ // `Container` class among them — so two physical installations of
414
+ // `@omnitron-dev/titan` in one process means `@Inject(Container)`
415
+ // resolves against a class that is not the one the daemon registered,
416
+ // and the failure reads `Dependency 'Container' not found`.
417
+ //
418
+ // `bootstrap-process.ts` checks for exactly this and refuses to start,
419
+ // which is how a freshly installed artifact answered:
420
+ //
421
+ // Titan package identity mismatch detected between omnitron daemon
422
+ // and app 'geo'. Daemon resolves @omnitron-dev/titan from
423
+ // /opt/omnitron/versions/…; App resolves … from /opt/omnitron/
424
+ // artifacts/sample/geo/0.0.1/node_modules/@omnitron-dev/titan
425
+ //
426
+ // That is the correct refusal, and the answer is not to install less:
427
+ // the vendored copies are what let `npm install` resolve a coherent
428
+ // tree in the first place. They are replaced afterwards by links to
429
+ // the daemon's own, computed ON THE NODE from what the daemon actually
430
+ // has, so the set stays right as the daemon's closure changes rather
431
+ // than being a list here that drifts from it.
432
+ //
433
+ // `@omnitron-dev/omnitron` is deliberately NOT one of them, and this is
434
+ // the second time that lesson has been paid for. The daemon IS that
435
+ // package, so it is not inside its own `node_modules`, and linking to
436
+ // the install root instead gives node a directory whose package.json
437
+ // carries `name`, `version`, `type` and `dependencies` and NO `main`
438
+ // and NO `exports` — `bundleRootManifest` writes an install manifest,
439
+ // not a library manifest. Resolution then falls back to `index.js`,
440
+ // which is not there:
441
+ //
442
+ // Cannot find package
443
+ // '.../node_modules/@omnitron-dev/omnitron/index.js'
444
+ //
445
+ // — the exact error this whole line of work started from. The vendored
446
+ // copy that `pnpm pack` produces from `apps/omnitron` has the real
447
+ // manifest, so it is left alone. Its identity is not shared across the
448
+ // process boundary the way titan's is: `defineSystem` returns data that
449
+ // the daemon's bootstrap reads, not a class anything does `instanceof`
450
+ // against.
451
+ //
452
+ // `rm -rf` before each `ln -sfn`: linking onto an existing DIRECTORY
453
+ // puts the link INSIDE it, leaving a directory with no package.json
454
+ // that node reports as a missing `index.js` — a message about a file
455
+ // that was never the problem.
456
+ const daemonRoot = (await this.inspectInstalledBundle(target)).root;
457
+ await this.sshExec(target, `set -e; APP=${shellEscape(`${remotePath}/node_modules/@omnitron-dev`)}; ` +
458
+ `mkdir -p "$APP"; ` +
459
+ `for p in ${shellEscape(daemonRoot)}/node_modules/@omnitron-dev/*; do ` +
460
+ `[ -e "$p" ] || continue; n=$(basename "$p"); ` +
461
+ `rm -rf "$APP/$n"; ln -sfn "$p" "$APP/$n"; done`);
462
+ // The install either produced a tree that resolves or it did not, and
463
+ // `test -d node_modules` cannot tell those apart — it was true for the
464
+ // whole time nothing worked. Asking node to RESOLVE the package the
465
+ // app imports first is the smallest question with a real answer.
466
+ //
467
+ // The package, not a file inside it: my first version of this resolved
468
+ // `@omnitron-dev/omnitron/package.json` and a correct install answered
469
+ // `ERR_PACKAGE_PATH_NOT_EXPORTED`, because a package with an `exports`
470
+ // map publishes what it lists and nothing else. A probe that fails on
471
+ // a healthy tree is worse than none — it would have failed every
472
+ // deployment from here on, for the one reason that is not a fault.
473
+ this.emitProgress(nodeKey, artifact.app, 'extracting', 70, 'Checking dependencies...');
474
+ const resolved = await this.sshExec(target, `cd ${shellEscape(remotePath)} && node -e ${shellEscape("const r=require('module').createRequire(process.cwd()+'/package.json');" +
475
+ "r.resolve('@omnitron-dev/omnitron');r.resolve('@omnitron-dev/titan')")} >/dev/null 2>&1 && echo resolves || echo broken`).catch(() => 'broken');
476
+ if (resolved.trim() !== 'resolves') {
477
+ const duration = Date.now() - startTime;
478
+ const detail = 'the installed tree cannot resolve @omnitron-dev/omnitron or @omnitron-dev/titan, ' +
479
+ 'so the app will fail at its first import';
480
+ this.emitProgress(nodeKey, artifact.app, 'failed', 70, detail);
481
+ this.logger.error({ node: nodeKey, app: artifact.app, path: remotePath }, detail);
482
+ return { node: nodeKey, app: artifact.app, version: artifact.version, status: 'failed', duration, error: detail };
483
+ }
484
+ // 5b. No record yet. The next deployment reads `.artifact-sha256` as
485
+ // «the app runs this», and at this point it does not: the files are
486
+ // unpacked, the process still runs what it started with. The record
487
+ // used to be written here, and on sample/test (2026-09-24) a deployment
488
+ // that stalled after installing three artifacts left them recorded;
489
+ // the retry read «installed, running» and left all three on the code
490
+ // they had started with an hour earlier. It is written once the app
491
+ // has come up healthy on these files — `recordRunningArtifact`.
492
+ // 6. Installed. Starting is a SEPARATE phase, and the order matters:
493
+ // a node cannot start an app it has no definition for, and the
494
+ // definitions are written once all the artifacts have landed. Starting
495
+ // here made the two mutually exclusive — the restart failed with
496
+ // `Unknown app`, the result was marked failed, and the registration,
497
+ // which only writes apps whose deployment succeeded, wrote nothing.
498
+ if (options?.startAfterInstall === false) {
499
+ const duration = Date.now() - startTime;
500
+ this.emitProgress(nodeKey, artifact.app, 'success', 75, 'Installed');
501
+ return {
502
+ node: nodeKey,
503
+ app: artifact.app,
504
+ version: artifact.version,
505
+ status: 'success',
506
+ duration,
507
+ ...(artifact.checksum ? { checksum: artifact.checksum } : {}),
508
+ };
509
+ }
70
510
  this.emitProgress(nodeKey, artifact.app, 'restarting', 80, 'Restarting app on remote...');
71
- await this.signalRemoteDaemon(node, artifact.app);
511
+ const started = await this.signalRemoteDaemon(target, artifact.app);
72
512
  // 7. Verify health
73
513
  this.emitProgress(nodeKey, artifact.app, 'verifying', 90, 'Verifying health...');
74
- await this.verifyHealth(node, artifact.app);
514
+ const health = await this.verifyHealth(target, artifact.app, project);
75
515
  const duration = Date.now() - startTime;
516
+ // An artifact on disk is not a running application. Reporting success
517
+ // for the transfer alone is what let a fleet show six deployed apps and
518
+ // run none of them.
519
+ if (!started.ok || !health.online) {
520
+ const why = !started.ok ? started.detail : health.detail;
521
+ this.emitProgress(nodeKey, artifact.app, 'failed', 90, `artifact installed, app not running: ${why}`);
522
+ this.logger.error({ node: nodeKey, app: artifact.app, version: artifact.version, duration, detail: why }, 'Artifact installed, but the app is not running on the node');
523
+ return {
524
+ node: nodeKey,
525
+ app: artifact.app,
526
+ version: artifact.version,
527
+ status: 'failed',
528
+ duration,
529
+ error: `artifact installed, app not running: ${why}`,
530
+ };
531
+ }
532
+ await this.recordRunningArtifact(target, remotePath, artifact.app, artifact.checksum);
76
533
  this.emitProgress(nodeKey, artifact.app, 'success', 100, `Deployed in ${Math.round(duration / 1000)}s`);
77
534
  this.logger.info({ node: nodeKey, app: artifact.app, version: artifact.version, duration }, 'Deployment successful');
78
535
  return { node: nodeKey, app: artifact.app, version: artifact.version, status: 'success', duration };
@@ -88,24 +545,29 @@ export class RemoteDeployer {
88
545
  /**
89
546
  * Deploy artifacts to all nodes in a stack (parallel).
90
547
  */
91
- async deployToStack(nodes, artifacts, project, options) {
548
+ async deployToStack(targets, artifacts, project, options) {
92
549
  const concurrency = options?.concurrency ?? 3;
93
550
  const results = [];
94
551
  // Build deployment matrix: each app to each node (or node-specific apps)
95
552
  const tasks = [];
96
- for (const node of nodes) {
553
+ for (const target of targets) {
97
554
  for (const artifact of artifacts) {
98
555
  // If node has explicit app list, only deploy matching apps
99
- if (node.apps && !node.apps.includes(artifact.app))
556
+ if (target.apps && !target.apps.includes(artifact.app))
100
557
  continue;
101
- tasks.push({ node, artifact });
558
+ tasks.push({ target, artifact });
102
559
  }
103
560
  }
104
561
  // Execute with concurrency limit
105
562
  const executing = new Set();
106
563
  for (const task of tasks) {
107
564
  const promise = (async () => {
108
- const result = await this.deployToNode(task.node, task.artifact, project);
565
+ // Install only when the definitions are coming: the app cannot be
566
+ // started before the node knows it exists.
567
+ const result = await this.deployToNode(task.target, task.artifact, project, {
568
+ startAfterInstall: !options?.apps?.length,
569
+ ...(options?.force === true ? { force: true } : {}),
570
+ });
109
571
  results.push(result);
110
572
  })();
111
573
  executing.add(promise);
@@ -115,8 +577,392 @@ export class RemoteDeployer {
115
577
  }
116
578
  }
117
579
  await Promise.all(executing);
580
+ // Tell each node what it now has. After the transfers, because a config
581
+ // naming an artifact that has not landed yet is a config the node will
582
+ // fail on — and only for the apps that actually arrived, which is what
583
+ // `results` knows and the artifact list does not.
584
+ if (options?.apps?.length) {
585
+ for (const target of targets) {
586
+ // The SAME key `deployToNode` builds — `host:daemonPort`. Comparing
587
+ // against the SSH port here matched nothing, so the registration and
588
+ // the start phase would both have run over an empty list and reported
589
+ // nothing wrong.
590
+ const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
591
+ const landed = results
592
+ .filter((r) => r.node === nodeKey && r.status === 'success')
593
+ .map((r) => ({ app: r.app, version: r.version }));
594
+ const registration = await this.registerNodeApps(target, project, options.apps, landed, options.appEnv, options.alertSink);
595
+ // Schema before the apps that read it — and an app whose schema did
596
+ // not arrive does not get its new code started on the old one.
597
+ const migrationFailed = await this.migrateNodeApps(target, project, landed, options.appEnv);
598
+ // And the path from the gateway to them, which is the one crossing
599
+ // no configuration can open by itself. See `gateway-reachability`.
600
+ if (options.stack)
601
+ await this.openGatewayPath(target, project, options.stack);
602
+ // What the node is running right now, asked once for the whole node
603
+ // rather than once per app: the decision below needs it for every
604
+ // app, and `omnitron status --json` answers about all of them.
605
+ const runningNow = await this.appsOnline(target, project);
606
+ // Now that the node knows what these apps are, start them. Their
607
+ // result is upgraded in place, so a caller reading `results` sees
608
+ // running apps rather than installed files.
609
+ for (const entry of landed) {
610
+ const result = results.find((r) => r.app === entry.app && r.node === nodeKey);
611
+ if (!result)
612
+ continue;
613
+ // Before anything decides to restart it. The code that was just
614
+ // unpacked expects the schema its migrations produce; starting it
615
+ // on the schema they failed to produce is the one outcome worse
616
+ // than not deploying — it can come up healthy and fail on a request.
617
+ const schemaError = migrationFailed.get(entry.app);
618
+ if (schemaError !== undefined) {
619
+ result.status = 'failed';
620
+ result.error = `database migrations failed, so the new code was not started: ${schemaError}`;
621
+ this.emitProgress(result.node, entry.app, 'failed', 80, result.error);
622
+ continue;
623
+ }
624
+ // An app whose artifact did not move, whose configuration did not
625
+ // move, and which is running, has nothing that a restart would
626
+ // change — and a restart is downtime. See `decideRedeploy`.
627
+ const decision = decideRedeploy({
628
+ // `unchanged` is set by `deployToNode` when the node's recorded
629
+ // checksum matched the artifact's; anything else means files
630
+ // were written.
631
+ artifactChanged: result.unchanged === true ? false : true,
632
+ online: runningNow.has(entry.app),
633
+ configChanged: registration.changed,
634
+ });
635
+ if (decision.action === 'leave') {
636
+ this.emitProgress(result.node, entry.app, 'success', 100, 'Running — nothing to change');
637
+ this.logger.info({ node: result.node, app: entry.app, version: entry.version, because: decision.because }, 'Left running — this deployment changes nothing for this app');
638
+ continue;
639
+ }
640
+ const started = await this.signalRemoteDaemon(target, entry.app);
641
+ const health = started.ok
642
+ ? await this.verifyHealth(target, entry.app, project)
643
+ : { online: false, detail: started.detail };
644
+ if (health.online) {
645
+ await this.recordRunningArtifact(target, artifactDirOnNode(project, entry.app, entry.version), entry.app, result.checksum);
646
+ this.emitProgress(result.node, entry.app, 'success', 100, 'Running');
647
+ // Said, because its opposite is. «Left running» above logs when
648
+ // this deployment leaves an application alone; restarting one
649
+ // logged nothing at all, so a log full of six «Left running»
650
+ // lines and a log of six silent restarts differ only by the
651
+ // absence of something — and absence is what a reader cannot
652
+ // see. Measured 2026-09-22: six artifacts delivered and every
653
+ // application restarted, with nothing in the log between
654
+ // «Opened the gateway's path» and «Stack started» to say so.
655
+ this.logger.info({
656
+ node: result.node,
657
+ app: entry.app,
658
+ version: entry.version,
659
+ because: decision.because,
660
+ }, 'Restarted onto the artifact this deployment installed');
661
+ continue;
662
+ }
663
+ // Installed and not running is a failure, and it is the state this
664
+ // fleet reported as success for as long as it has existed.
665
+ result.status = 'failed';
666
+ result.error = `artifact installed, app not running: ${started.ok ? health.detail : started.detail}`;
667
+ this.emitProgress(result.node, entry.app, 'failed', 90, result.error);
668
+ this.logger.error({ node: result.node, app: entry.app, version: entry.version, detail: result.error }, 'Artifact installed, but the app is not running on the node');
669
+ }
670
+ }
671
+ }
118
672
  return results;
119
673
  }
674
+ /**
675
+ * Give a node the config that tells it what to run.
676
+ *
677
+ * Written, then registered. Registering a path with no config in it leaves
678
+ * the daemon with a project it cannot read, which reports as a broken
679
+ * project rather than a missing file.
680
+ *
681
+ * Reported and not thrown: artifacts that transferred are on the node
682
+ * either way, and failing the whole deployment over the registration would
683
+ * discard a transfer that succeeded. The operator needs to know the apps
684
+ * are not runnable, which is what the error says.
685
+ */
686
+ /**
687
+ * Let the gateway reach the applications it proxies to.
688
+ *
689
+ * The gateway is a container and the apps are host processes, so every
690
+ * `/api/*` request crosses out of docker and into the host's INPUT chain.
691
+ * On a hardened host that chain drops it, and the gateway answers a
692
+ * correct, unhelpful `503` three seconds later while the upstream it could
693
+ * not reach is listening and healthy on the same machine.
694
+ *
695
+ * Measured on the test node with everything else already right: apps on
696
+ * `0.0.0.0:3001-3007`, answering `404` to the host itself, timing out from
697
+ * the gateway, `ufw` active with `22/tcp` its only rule and INPUT policy
698
+ * DROP.
699
+ *
700
+ * Derived, not configured: the subnet comes from the network the gateway
701
+ * is actually on, so a stack cannot be given a rule for the wrong one, and
702
+ * a subnet that is not a private range is refused outright rather than
703
+ * turned into a firewall rule nobody can explain.
704
+ *
705
+ * Never fatal. A host whose firewall cannot be read is a host that gets a
706
+ * message; stopping a deployment over it would trade a visible 503 for an
707
+ * invisible one.
708
+ */
709
+ async openGatewayPath(target, project, stack) {
710
+ const network = `${project}-${stack}_default`;
711
+ try {
712
+ // No `2>/dev/null || true`: a network that cannot be inspected is the
713
+ // reason the path stays shut, and discarding it leaves an operator
714
+ // with a 503 and no sentence. The failure is caught below and named.
715
+ const subnet = (await this.sshExec(target, `docker network inspect ${shellEscape(network)} --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'`)).trim();
716
+ if (!subnet) {
717
+ this.logger.warn({ node: target.host, network }, 'The stack network reports no subnet — the gateway’s path to the applications was left as it was');
718
+ return;
719
+ }
720
+ const rule = reachabilityRule(subnet, project, stack);
721
+ if (!rule) {
722
+ this.logger.warn({ node: target.host, network, subnet }, 'Refusing to open a path from this subnet — it is not a private range');
723
+ return;
724
+ }
725
+ const outcome = (await this.sshExec(target, reachabilityCommand(rule))).trim();
726
+ if (outcome === 'allowed') {
727
+ this.logger.info({ node: target.host, subnet: rule.subnet, ports: `${rule.fromPort}-${rule.toPort}` }, 'Opened the gateway’s path to the applications on this host');
728
+ }
729
+ else if (outcome === 'failed') {
730
+ this.logger.error({ node: target.host, subnet: rule.subnet }, 'Could not open the gateway’s path — every /api/* request will answer 503 while the apps are healthy');
731
+ }
732
+ else if (outcome.startsWith('ufw-unreadable')) {
733
+ // Not `ufw-inactive`: nothing is known about the firewall, and if it
734
+ // is active it is dropping the gateway's traffic right now.
735
+ this.logger.warn({ node: target.host, subnet: rule.subnet, reason: outcome.slice('ufw-unreadable'.length).replace(/^:\s*/, '') }, 'Could not read the node’s firewall — if it is active, the gateway’s path to the applications is closed and /api/* will answer 503');
736
+ }
737
+ }
738
+ catch (err) {
739
+ this.logger.warn({ node: target.host, network, error: err.message.slice(0, 200) }, 'Could not check the gateway’s path to the applications');
740
+ }
741
+ }
742
+ /**
743
+ * Run each app's migrations, on the node, against the node's database.
744
+ *
745
+ * They had never run there. `ProjectService` migrates by executing
746
+ * `apps/<name>/src/database/migrate.ts` from the PROJECT PATH against
747
+ * `localhost` — both of which name the machine the master is on. A node has
748
+ * no project sources and a different localhost, so a remote stack's
749
+ * databases were created and left empty.
750
+ *
751
+ * Measured on the test node after six apps installed and five started:
752
+ * every one of the six databases existed and held ZERO tables. Five apps
753
+ * came up anyway, because they touch no table at boot; `payments` does, and
754
+ * said so —
755
+ *
756
+ * Failed to call @PostConstruct 'bootstrap' on 'PlatformRevenueService':
757
+ * relation "accounts" does not exist
758
+ *
759
+ * — which is the only reason anyone found out. A platform with no schema
760
+ * that reports five of six apps healthy is a worse state than one that
761
+ * refuses to start.
762
+ *
763
+ * The artifact already carries what is needed: `dist/database/migrate.js`
764
+ * beside `dist/database/migrations/` — 160 compiled files for payments. The
765
+ * same build the app itself runs from, so there is no second copy of the
766
+ * schema to drift.
767
+ *
768
+ * Returns the apps whose migrations failed, with the reason, and the
769
+ * caller does not start their new code.
770
+ *
771
+ * This used to say: «Failure is reported and does not stop the deployment:
772
+ * an app whose migrations fail will fail its own start with a message
773
+ * about the table it wanted, which is more specific than anything this
774
+ * step could say.» True for one of the two ways it goes. An app that
775
+ * fails to START is caught by `verifyHealth` and marked failed — that
776
+ * branch was covered, and the sentence is right about it. An app that
777
+ * starts fine and fails on the first REQUEST that touches the missing
778
+ * table passes `verifyHealth`, the stack reports `started`, and the only
779
+ * trace is one ERROR line in the master's log. The justification covered
780
+ * the branch that was already safe.
781
+ *
782
+ * Idempotence is what makes refusing cheap: the migrator records each
783
+ * migration it applied, in a transaction with the migration itself, so
784
+ * running the batch again after the cause is fixed applies exactly what
785
+ * is missing. That holds because `HardenedMigrationRunner` defaults
786
+ * `useTransactions` to true (`titan-database/src/migration/
787
+ * hardened-runner.ts:171`) and no sample `migrate.ts` overrides it — it is
788
+ * conditional in the runner (`:502`), so a migrator that turned it off
789
+ * would make a refused deployment leave a half-applied migration behind.
790
+ */
791
+ async migrateNodeApps(target, project, landed, appEnv) {
792
+ const failed = new Map();
793
+ for (const entry of landed) {
794
+ const databaseUrl = appEnv?.[entry.app]?.['DATABASE_URL'];
795
+ if (!databaseUrl)
796
+ continue;
797
+ const dir = `/opt/omnitron/artifacts/${assertRemotePathSegment('project name', project)}` +
798
+ `/${assertRemotePathSegment('app name', entry.app)}` +
799
+ `/${assertRemotePathSegment('version', entry.version)}`;
800
+ const script = `${dir}/dist/database/migrate.js`;
801
+ // A node that could not be asked is not an app without a migrator.
802
+ // This command exits 0 either way, so `sshExec` throws only when the
803
+ // question never arrived — and that was answered `'no'`: migrations
804
+ // skipped, new code started on the old schema. The worst case of the
805
+ // docblock above, reached through a dropped SSH connection instead of
806
+ // a failed migration, and refused the same way.
807
+ let present;
808
+ try {
809
+ present = await this.sshExec(target, `test -f ${shellEscape(script)} && echo yes || echo no`);
810
+ }
811
+ catch (err) {
812
+ const message = `the node could not be asked whether there are any to apply — ${err.message}`;
813
+ this.logger.error({ node: target.host, app: entry.app, error: message.slice(0, 400) }, 'Could not ask the node for the app\'s migrations — its new code will not be started');
814
+ failed.set(entry.app, message.slice(0, 300));
815
+ continue;
816
+ }
817
+ if (present.trim() !== 'yes')
818
+ continue;
819
+ this.emitProgress(`${target.host}:${target.daemonPort ?? 9700}`, entry.app, 'installing', 75, 'Applying database migrations...');
820
+ // Both spellings, because the migrators read the second one.
821
+ //
822
+ // `apps/payments/src/database/migrate.ts` declares
823
+ // `envPrefix: 'PAYMENTS__DATABASE'` and builds its connection from
824
+ // `PAYMENTS__DATABASE__HOST` / `__PORT` / `__USER` / `__PASSWORD` /
825
+ // `__DATABASE`. Given only `DATABASE_URL` it falls back to its own
826
+ // defaults and reports `password authentication failed for user
827
+ // "postgres"` — against a password that is correct and that it never
828
+ // read. All six apps failed that way, which is what a convention
829
+ // nobody wrote down costs when a second caller appears.
830
+ //
831
+ // `ProjectService` passes both, with the comment "App-specific env
832
+ // vars (various naming conventions)". This is the second caller.
833
+ const parsed = parseDatabaseUrl(databaseUrl);
834
+ const prefix = entry.app.toUpperCase().replace(/-/g, '_');
835
+ const dbEnv = parsed
836
+ ? {
837
+ [`${prefix}__DATABASE__HOST`]: parsed.host,
838
+ [`${prefix}__DATABASE__PORT`]: parsed.port,
839
+ [`${prefix}__DATABASE__DATABASE`]: parsed.database,
840
+ [`${prefix}__DATABASE__USER`]: parsed.user,
841
+ [`${prefix}__DATABASE__PASSWORD`]: parsed.password,
842
+ }
843
+ : {};
844
+ try {
845
+ // `cd` into the artifact so the script resolves its own imports, and
846
+ // every value in the environment rather than the command line, where
847
+ // it would be in every process listing on the host.
848
+ const assignments = Object.entries({ DATABASE_URL: databaseUrl, ...dbEnv })
849
+ .map(([k, v]) => `${k}=${shellEscape(v)}`)
850
+ .join(' ');
851
+ // What the migrations said, in order, before the verdict (see
852
+ // `migration-output.ts`). The masked channel on purpose: these lines
853
+ // are for reading, not parsing, and a migrator that ever printed a
854
+ // credential must not put it in the master's log.
855
+ const said = await this.sshExec(target, `cd ${shellEscape(dir)} && ${assignments} node ${shellEscape(script)}`, 600_000);
856
+ for (const line of migrationOutput(said)) {
857
+ this.logger.info({ node: target.host, app: entry.app, line }, 'Migration said');
858
+ }
859
+ this.logger.info({ node: target.host, app: entry.app }, 'Database migrations applied on the node');
860
+ }
861
+ catch (err) {
862
+ const message = err.message;
863
+ this.logger.error({ node: target.host, app: entry.app, error: message.slice(0, 400) }, 'Database migrations failed on the node — its new code will not be started');
864
+ failed.set(entry.app, message.slice(0, 300));
865
+ }
866
+ }
867
+ return failed;
868
+ }
869
+ /**
870
+ * Write the node's app definitions, and say whether they changed.
871
+ *
872
+ * The caller decides whether to restart the applications, and "the
873
+ * configuration moved" is half of that decision — identical files with a
874
+ * different environment is a different deployment. Compared by the hash of
875
+ * the rendered body against one written beside it, rather than by reading
876
+ * the file back: the body holds every generated credential on the node,
877
+ * and it should cross the wire once, in the direction it has to.
878
+ */
879
+ async registerNodeApps(target, project, apps, landed, appEnv, alertSink) {
880
+ if (landed.length === 0) {
881
+ this.logger.warn({ host: target.host, project }, 'No artifact reached this node — nothing to register');
882
+ return { changed: false };
883
+ }
884
+ const { renderNodeAppConfig } = await import('../project/node-app-config.js');
885
+ const dir = `/opt/omnitron/projects/${assertRemotePathSegment('project name', project)}`;
886
+ const body = renderNodeAppConfig({
887
+ project,
888
+ artifactRoot: '/opt/omnitron/artifacts',
889
+ apps,
890
+ artifacts: landed,
891
+ appEnv,
892
+ alertSink,
893
+ });
894
+ const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
895
+ try {
896
+ await this.sshExec(target, `mkdir -p ${shellEscape(dir)}`);
897
+ const previous = parseRecordedChecksum(await this.sshExec(target, readChecksumCommand(shellEscape(dir), NODE_CONFIG_HASH_FILE)));
898
+ const changed = previous !== bodyHash;
899
+ // Through a here-document: the config carries braces, quotes and
900
+ // newlines, and a single-quoted argument would need every quote in it
901
+ // escaped by hand — which is how a generated file acquires a syntax
902
+ // error nobody can see in the source that generated it.
903
+ const configPath = `${dir}/omnitron.config.mjs`;
904
+ // `umask 077` around the write, not a `chmod` after it: the file exists
905
+ // with its contents between the two, and what it contains is the
906
+ // generated password of every service on this node. It was written 0644
907
+ // for as long as it has existed — measured on the test node — which was
908
+ // harmless only because it held no credentials to leak.
909
+ await this.sshExec(target, `umask 077 && cat > ${shellEscape(configPath)} <<'OMNITRON_EOF'\n${body}\nOMNITRON_EOF`);
910
+ await this.sshExec(target, `chmod 600 ${shellEscape(configPath)}`);
911
+ await this.sshExec(target, `printf %s ${shellEscape(bodyHash)} > ${shellEscape(`${dir}/${NODE_CONFIG_HASH_FILE}`)}`);
912
+ const added = await this.sshExec(target, `omnitron project add ${shellEscape(project)} ${shellEscape(dir)} 2>&1`);
913
+ // Registering the project is not starting it. The node reads the
914
+ // definitions and waits: measured, `omnitron status` answered
915
+ // `appsTotal: 0` with all six apps listed in the file it had just been
916
+ // given, and `omnitron restart main` still said `Unknown app: main`.
917
+ //
918
+ // `startStack` is how a project's apps are started, which is why the
919
+ // generated config carries a stack for them to be in.
920
+ const { NODE_STACK } = await import('../project/node-app-config.js');
921
+ // ...and whether it has to be told at all. This command starts
922
+ // everything the node's config lists, so it overruled the per-app
923
+ // decision taken below it — measured 2026-09-22, six «Left running»
924
+ // lines about six applications this same deployment had restarted
925
+ // twenty seconds earlier. See `decideNodeStackStart`.
926
+ const start = decideNodeStackStart({
927
+ configChanged: changed,
928
+ apps: landed.map((l) => l.app),
929
+ online: await this.appsOnline(target, project),
930
+ });
931
+ const out = start.action === 'start'
932
+ ? await this.sshExec(target, `omnitron stack start ${shellEscape(project)} ${shellEscape(NODE_STACK)} 2>&1`, 300_000)
933
+ : '';
934
+ void added;
935
+ // The node's CLI prints its refusals and exits zero — `omnitron project
936
+ // add` answered `No omnitron.config.ts found at …` and this logged
937
+ // `The node now knows what to run` over it, twice, while the node knew
938
+ // nothing. An exit code that does not carry the outcome means the
939
+ // OUTPUT is the outcome.
940
+ const refused = /\bno .*found\b|\bfailed\b|\berror\b|not found|cannot/i.test(`${added}\n${out}`);
941
+ if (refused) {
942
+ this.logger.error({ host: target.host, project, dir, detail: `${added}\n${out}`.trim().slice(0, 300) }, 'The node refused the apps it was given — its artifacts are installed and it will run none of them');
943
+ return { changed };
944
+ }
945
+ this.logger.info({
946
+ host: target.host,
947
+ project,
948
+ dir,
949
+ apps: landed.map((l) => l.app),
950
+ changed,
951
+ started: start.action === 'start',
952
+ because: start.because,
953
+ detail: out.trim().slice(0, 200),
954
+ }, start.action === 'start'
955
+ ? 'The node now knows what to run'
956
+ : 'The node is already running these apps with this configuration — its stack was not restarted');
957
+ return { changed };
958
+ }
959
+ catch (err) {
960
+ this.logger.error({ host: target.host, project, dir, error: err.message }, 'Could not tell the node what to run — its artifacts are installed and its daemon does not know the apps');
961
+ // Unknown, and unknown must not read as "nothing moved": an app whose
962
+ // config may have changed is an app to restart.
963
+ return { changed: true };
964
+ }
965
+ }
120
966
  /**
121
967
  * Provision a remote node as a slave daemon.
122
968
  *
@@ -134,191 +980,952 @@ export class RemoteDeployer {
134
980
  * @param masterHost - The master daemon's reachable address (from slave's perspective)
135
981
  * @param masterPort - The master daemon's fleet TCP port
136
982
  */
137
- async provisionSlaveNode(node, masterHost, masterPort, project) {
138
- const nodeKey = `${node.host}:${node.port ?? 9700}`;
983
+ async provisionSlaveNode(target,
984
+ /**
985
+ * Where this node should dial to reach the master, or null when there is
986
+ * nowhere — a master behind NAT, which is where remote stacks are
987
+ * started from.
988
+ *
989
+ * Null is not a failure. Replication is master-PULL: the master opens
990
+ * the connection, over SSH when the node's daemon port is closed, and
991
+ * drains the node's buffer. `omnitron up --slave` takes the address
992
+ * optionally for exactly this reason.
993
+ */
994
+ masterHost, masterPort, project, options = {}) {
995
+ const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
139
996
  try {
140
997
  // 1. Verify SSH access
141
998
  this.emitProgress(nodeKey, '*', 'pending', 0, 'Connecting via SSH...');
142
- await this.verifySSH(node);
143
- // 2. Ensure runtime (Node.js or Bun)
144
- this.emitProgress(nodeKey, '*', 'extracting', 10, 'Checking runtime...');
145
- const hasNode = await this.sshExec(node, 'which node 2>/dev/null || which bun 2>/dev/null || echo ""').catch(() => '');
146
- if (!hasNode.trim()) {
147
- this.logger.info({ host: node.host }, 'Installing Node.js on remote node...');
148
- this.emitProgress(nodeKey, '*', 'extracting', 15, 'Installing Node.js...');
149
- try {
150
- // Install Node.js via official installer (works on most Linux distros)
151
- await this.sshExec(node, 'curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && apt-get install -y nodejs 2>/dev/null || (curl -fsSL https://rpm.nodesource.com/setup_22.x | bash - && yum install -y nodejs) 2>/dev/null || (apk add --no-cache nodejs npm)', 120_000);
152
- }
153
- catch (err) {
154
- this.logger.error({ host: node.host, error: err.message }, 'Failed to install Node.js');
155
- return false;
156
- }
999
+ await this.verifySSH(target);
1000
+ // 2-3. Prepare the host: whatever it is, and whatever it is missing.
1001
+ //
1002
+ // This was two probes and a `||` chain of package-manager installs that
1003
+ // only worked on three Linux families, added a vendor repository to the
1004
+ // host as a side effect, and on failure reported the error from the
1005
+ // LAST alternative — `apk` on a Debian machine that has never had it.
1006
+ //
1007
+ // Now the host is asked what it is in one round trip, a plan is derived
1008
+ // from the answer, and the plan is a value: it can be logged before it
1009
+ // runs and read in a test without a machine. See
1010
+ // `remote-provisioner.ts` for what it decides and why.
1011
+ this.emitProgress(nodeKey, '*', 'extracting', 10, 'Inspecting the host...');
1012
+ const facts = parsePlatformProbe(await this.sshExec(target, PLATFORM_PROBE, 60_000));
1013
+ const plan = planProvisioning(facts, {
1014
+ ...(options.nodeVersion ? { nodeVersion: options.nodeVersion } : {}),
1015
+ ...(options.installRuntime === false ? { usePackageManager: false } : {}),
1016
+ });
1017
+ this.logger.info({
1018
+ host: target.host,
1019
+ os: facts.os,
1020
+ arch: facts.arch,
1021
+ distro: facts.distro,
1022
+ packageManager: facts.packageManager,
1023
+ node: facts.node,
1024
+ omnitron: facts.omnitron,
1025
+ plan: describePlan(plan),
1026
+ }, 'Host inspected');
1027
+ if (plan.refusal) {
1028
+ this.logger.error({ host: target.host, reason: plan.refusal }, 'Cannot prepare this host');
1029
+ this.emitProgress(nodeKey, '*', 'failed', 0, plan.refusal);
1030
+ return false;
157
1031
  }
158
- // 3. Install omnitron
159
- this.emitProgress(nodeKey, '*', 'extracting', 30, 'Installing omnitron...');
160
- const hasOmnitron = await this.sshExec(node, 'which omnitron 2>/dev/null || echo ""').catch(() => '');
161
- if (!hasOmnitron.trim()) {
1032
+ let progress = 15;
1033
+ const stride = plan.steps.length > 0 ? Math.floor(30 / plan.steps.length) : 0;
1034
+ for (const step of plan.steps) {
1035
+ this.emitProgress(nodeKey, '*', 'extracting', progress, step.what);
1036
+ if (step.touchesPackageManager) {
1037
+ // Said at warning level: this is the one kind of step that changes
1038
+ // how the machine gets its software, and the machines this reaches
1039
+ // are not blank.
1040
+ this.logger.warn({ host: target.host, step: step.what }, 'Using the host package manager');
1041
+ }
162
1042
  try {
163
- await this.sshExec(node, 'npm install -g @omnitron-dev/omnitron', 120_000);
1043
+ await this.sshExec(target, step.command, step.timeoutMs);
164
1044
  }
165
1045
  catch (err) {
166
- this.logger.error({ host: node.host, error: err.message }, 'Failed to install omnitron');
1046
+ // The step that failed, by name, rather than the last alternative's
1047
+ // error message.
1048
+ this.logger.error({ host: target.host, step: step.what, error: err.message }, 'Host preparation step failed');
1049
+ this.emitProgress(nodeKey, '*', 'failed', progress, `${step.what}: ${err.message}`);
167
1050
  return false;
168
1051
  }
1052
+ progress += stride;
169
1053
  }
170
- // 4. Generate slave config
1054
+ // 4. Configure and start the slave, through omnitron's own setup path.
1055
+ //
1056
+ // This wrote `/etc/omnitron/omnitron.config.ts` containing `role`,
1057
+ // `master` and a `daemon` block, then ran a bare `omnitron up` beside
1058
+ // it. Every one of those keys was read by nothing.
1059
+ //
1060
+ // `IEcosystemConfig` — the schema of that file — has no `role`, no
1061
+ // `master` and no `daemon`. The daemon boots from
1062
+ // `~/.omnitron/config.json`, whose `SavedDaemonConfig` carried role and
1063
+ // master and nothing about transports. So a provisioned slave would
1064
+ // have come up as a MASTER, bound to loopback, with no master address
1065
+ // and no sync: three of the four things this step exists to arrange,
1066
+ // silently not arranged.
1067
+ //
1068
+ // `omnitron up --slave <host>:<port>` is the path that writes what the
1069
+ // daemon reads, and it is the same one an operator uses by hand. One
1070
+ // way to configure a slave instead of two, and the one that is
1071
+ // exercised.
171
1072
  this.emitProgress(nodeKey, '*', 'extracting', 50, 'Configuring slave daemon...');
172
- const configDir = '/etc/omnitron';
173
- const configContent = this.generateSlaveConfig(masterHost, masterPort, node.port ?? 9700, project);
174
- await this.sshExec(node, `mkdir -p ${shellEscape(configDir)}`);
175
- // Write config via heredoc (avoids shell escaping issues with complex content)
176
- await this.sshExec(node, `cat > ${shellEscape(configDir + '/omnitron.config.ts')} << 'OMNITRON_EOF'\n${configContent}\nOMNITRON_EOF`);
177
- // 5. Start slave daemon (or restart if already running)
178
- this.emitProgress(nodeKey, '*', 'restarting', 70, 'Starting slave daemon...');
179
- await this.sshExec(node, `cd ${shellEscape(configDir)} && (omnitron down 2>/dev/null; omnitron up) &`).catch(() => {
180
- // Background start — may "fail" because SSH returns before daemon fully starts
1073
+ assertRemotePathSegment('project name', project);
1074
+ const masterAddr = masterHost ? `${masterHost}:${masterPort}` : null;
1075
+ // 5. Start the slave daemon — if it is not already the one we want.
1076
+ //
1077
+ // `omnitron down` stops the daemon AND every application under it, so
1078
+ // this step is the most expensive thing a deployment does to a node
1079
+ // that needed nothing. See `decideSlaveDaemonRestart`: it took three
1080
+ // deployments to attribute, because the node-side `stack start` was
1081
+ // restarting them too and either one alone explains the pids.
1082
+ const daemonNow = readNodeStatus((await this.askNodeStatus(target)).raw);
1083
+ const daemonDecision = decideSlaveDaemonRestart({
1084
+ hostChanged: plan.steps.length > 0,
1085
+ daemon: daemonNow
1086
+ ? {
1087
+ running: typeof daemonNow.pid === 'number' || typeof daemonNow.uptime === 'number',
1088
+ ...(daemonNow.role ? { role: daemonNow.role } : {}),
1089
+ }
1090
+ : null,
181
1091
  });
182
- // 6. Wait briefly and verify daemon is running
1092
+ if (daemonDecision.action === 'leave') {
1093
+ this.emitProgress(nodeKey, '*', 'restarting', 70, 'Slave daemon already running');
1094
+ this.logger.info({ host: target.host, because: daemonDecision.because, apps: daemonNow?.apps.length ?? 0 }, 'The node daemon was left alone — this deployment changes nothing under it');
1095
+ }
1096
+ else {
1097
+ // Said as loudly as the other branch. A decision that only speaks
1098
+ // when it declines is a decision an operator cannot audit: this one
1099
+ // took three deployments to attribute and a fourth to explain,
1100
+ // because «restart» left nothing behind but the restart itself.
1101
+ this.logger.info({
1102
+ host: target.host,
1103
+ because: daemonDecision.because,
1104
+ steps: plan.steps.length,
1105
+ role: daemonNow?.role ?? null,
1106
+ pid: daemonNow?.pid ?? null,
1107
+ uptime: daemonNow?.uptime ?? null,
1108
+ }, 'Taking the node daemon down and back up — and everything under it');
1109
+ this.emitProgress(nodeKey, '*', 'restarting', 70, 'Starting slave daemon...');
1110
+ await this.sshExec(target, masterAddr
1111
+ ? `omnitron down 2>/dev/null; omnitron up --slave ${shellEscape(masterAddr)} --no-infra`
1112
+ // `--slave` with no address still sets the role; the node buffers
1113
+ // locally and waits to be pulled from, which is what it would do
1114
+ // with an address it cannot reach anyway.
1115
+ : 'omnitron down 2>/dev/null; omnitron up --slave --no-infra', 180_000).catch((err) => {
1116
+ // Reported, not swallowed. The verification below tells us whether the
1117
+ // daemon came up; this tells us what it said on the way.
1118
+ this.logger.warn({ host: target.host, error: err.message }, 'Slave start command did not return cleanly — verifying anyway');
1119
+ });
1120
+ }
1121
+ // 6. Verify the daemon is running, and mean it.
1122
+ //
1123
+ // This slept three seconds, pinged once, and on `unreachable` logged
1124
+ // "may still be starting" — then reported success regardless. Measured
1125
+ // on the first real run: the start command failed with
1126
+ // `omnitron: command not found`, the ping found nothing, and the
1127
+ // function returned `true`. A caller that trusts that goes on to ship
1128
+ // artifacts to a node with no daemon, and the console shows a node
1129
+ // that was "provisioned" and answers nothing.
1130
+ //
1131
+ // A daemon that is starting will answer within the window; one that is
1132
+ // not there will not answer within any window. Polling tells them
1133
+ // apart, which one sample cannot.
183
1134
  this.emitProgress(nodeKey, '*', 'verifying', 90, 'Verifying slave daemon...');
184
- await new Promise((resolve) => setTimeout(resolve, 3000));
1135
+ const started = await this.awaitDaemon(target, SLAVE_START_TIMEOUT_MS);
1136
+ if (!started.ok) {
1137
+ this.logger.error({ host: target.host, waitedMs: SLAVE_START_TIMEOUT_MS, detail: started.detail }, 'Slave daemon did not come up');
1138
+ this.emitProgress(nodeKey, '*', 'failed', 90, `the slave daemon did not answer within ${Math.round(SLAVE_START_TIMEOUT_MS / 1000)}s: ${started.detail}`);
1139
+ return false;
1140
+ }
1141
+ this.emitProgress(nodeKey, '*', 'success', 100, `Slave provisioned — ${started.detail}`);
1142
+ this.logger.info({ host: target.host, masterHost, masterPort, daemon: started.detail }, 'Slave node provisioned');
1143
+ return true;
1144
+ }
1145
+ catch (err) {
1146
+ this.emitProgress(nodeKey, '*', 'failed', 0, err.message);
1147
+ this.logger.error({ host: target.host, error: err.message }, 'Failed to provision slave node');
1148
+ return false;
1149
+ }
1150
+ }
1151
+ // ===========================================================================
1152
+ // Private — SSH Operations
1153
+ // ===========================================================================
1154
+ /**
1155
+ * Wait for the remote daemon to answer, or report why it did not.
1156
+ *
1157
+ * `omnitron ping` on the far side prints its pid, uptime and version when
1158
+ * the daemon is up, and fails otherwise. The last failure is carried out of
1159
+ * the loop so a caller can say what the host said rather than only that
1160
+ * time ran out.
1161
+ */
1162
+ async awaitDaemon(target, timeoutMs) {
1163
+ const deadline = Date.now() + timeoutMs;
1164
+ let last = 'no answer';
1165
+ while (Date.now() < deadline) {
185
1166
  try {
186
- const pingResult = await this.sshExec(node, 'omnitron ping 2>/dev/null || echo "unreachable"', 10_000);
187
- if (pingResult.includes('unreachable')) {
188
- this.logger.warn({ host: node.host }, 'Slave daemon not yet responding — may still be starting');
1167
+ const answer = await this.sshExec(target, 'omnitron ping', 15_000);
1168
+ // "It printed something" is not "it is running". `omnitron ping`
1169
+ // used to exit 0 while printing that the daemon is not running —
1170
+ // fixed, but a node runs whatever version it has, and this code
1171
+ // upgrades nodes from older ones. The answer is read, not assumed.
1172
+ if (/daemon is running/i.test(answer)) {
1173
+ return { ok: true, detail: answer.split('\n').filter(Boolean).pop().trim() };
189
1174
  }
1175
+ last = answer.split('\n').filter(Boolean).pop()?.trim() || 'the ping printed nothing';
190
1176
  }
191
- catch {
192
- this.logger.warn({ host: node.host }, 'Could not verify slave daemon — it may still be starting');
1177
+ catch (err) {
1178
+ last = err.message;
193
1179
  }
194
- this.emitProgress(nodeKey, '*', 'success', 100, 'Slave provisioned');
195
- this.logger.info({ host: node.host, masterHost, masterPort }, 'Slave node provisioned');
1180
+ await new Promise((resolve) => setTimeout(resolve, DAEMON_POLL_MS));
1181
+ }
1182
+ return { ok: false, detail: last };
1183
+ }
1184
+ /**
1185
+ * Install a locally built omnitron on a node, beside whatever it is
1186
+ * running.
1187
+ *
1188
+ * The counterpart to installing from the registry, and the only channel
1189
+ * that can put THIS tree on a node: the published package is whatever was
1190
+ * last released, which on 2026-09-14 was five months and 224 commits
1191
+ * behind, under the same version number.
1192
+ *
1193
+ * Nothing is made live here. The archive is transferred, unpacked into its
1194
+ * own version directory, installed, and run — and `activate` is a separate
1195
+ * call, so a caller that stops after this has spent disk and changed
1196
+ * nothing about what the node is serving.
1197
+ */
1198
+ async installBundle(target, archivePath, version, prefix = '/opt/omnitron') {
1199
+ const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
1200
+ const layout = { prefix, version };
1201
+ try {
1202
+ // Reject path traversal before any connection, upload or filesystem mutation.
1203
+ versionDir(layout);
1204
+ const fs = await import('node:fs/promises');
1205
+ const archiveSha256 = crypto.createHash('sha256').update(await fs.readFile(archivePath)).digest('hex');
1206
+ // Content-addressed transfer cannot replace an earlier archive with different bytes.
1207
+ const remoteArchive = `${prefix}/releases/${version}.${archiveSha256}.tar.gz`;
1208
+ this.emitProgress(nodeKey, '*', 'pending', 0, 'Connecting via SSH...');
1209
+ await this.verifySSH(target);
1210
+ this.emitProgress(nodeKey, '*', 'transferring', 10, `Transferring ${version}...`);
1211
+ await this.sshExec(target, installPreflightCommand(layout));
1212
+ await this.scpTransfer(target, archivePath, remoteArchive);
1213
+ let progress = 30;
1214
+ for (const step of installSteps(layout, remoteArchive, archiveSha256)) {
1215
+ this.emitProgress(nodeKey, '*', 'extracting', progress, step.what);
1216
+ const output = await this.sshExec(target, step.command, step.timeoutMs);
1217
+ if (step.what.includes('runs')) {
1218
+ // The version the node reports has to be the version we shipped.
1219
+ // A mismatch means the archive, the directory and the manifest
1220
+ // disagree — and the one thing a fleet upgrade cannot tolerate is
1221
+ // a node that reports a version it is not running.
1222
+ const reported = output.trim().split('\n').pop()?.trim();
1223
+ if (reported !== version) {
1224
+ throw new Error(`The installed copy reports ${JSON.stringify(reported)}, not ${JSON.stringify(version)}.`);
1225
+ }
1226
+ }
1227
+ progress += 20;
1228
+ }
1229
+ this.emitProgress(nodeKey, '*', 'success', 100, `${version} installed (not yet current)`);
1230
+ this.logger.info({ host: target.host, version, prefix }, 'Bundle installed beside the running version');
196
1231
  return true;
197
1232
  }
198
1233
  catch (err) {
199
- this.emitProgress(nodeKey, '*', 'failed', 0, err.message);
200
- this.logger.error({ host: node.host, error: err.message }, 'Failed to provision slave node');
1234
+ const message = err.message;
1235
+ this.emitProgress(nodeKey, '*', 'failed', 0, message);
1236
+ this.logger.error({ host: target.host, version, error: message }, 'Bundle install failed');
201
1237
  return false;
202
1238
  }
203
1239
  }
204
1240
  /**
205
- * Generate omnitron.config.ts content for a slave daemon.
1241
+ * `activateBundle` with the platform held. `omnitron down; up` stops every
1242
+ * app on the node, so each stack gateway there holds its platform
1243
+ * (`maintenance-lock.ts`) — warned first — until its apps answer through it
1244
+ * again, for up to three minutes after the restart; past that the lock is
1245
+ * left to expire by itself. A node with no gateway activates as before.
206
1246
  */
207
- generateSlaveConfig(masterHost, masterPort, slavePort, project) {
208
- return `/**
209
- * Omnitron Slave Configuration
210
- * Auto-generated by master during remote/cluster deployment.
211
- * Project: ${project}
212
- */
213
- export default {
214
- name: '${project}',
215
- apps: [],
216
- role: 'slave',
217
- master: { host: '${masterHost}', port: ${masterPort} },
218
- sync: {
219
- interval: 30000,
220
- batchSize: 1000,
221
- },
222
- daemon: {
223
- socketPath: '~/.omnitron/daemon.sock',
224
- port: ${slavePort},
225
- host: '0.0.0.0',
226
- httpPort: 9800,
227
- pidFile: '~/.omnitron/daemon.pid',
228
- stateFile: '~/.omnitron/daemon.state',
229
- },
230
- supervision: {
231
- strategy: 'one_for_one',
232
- maxRestarts: 10,
233
- window: 60000,
234
- backoff: { type: 'exponential', initial: 1000, max: 30000, factor: 2 },
235
- },
236
- monitoring: {
237
- healthCheck: { interval: 30000, timeout: 10000 },
238
- metrics: { interval: 15000, retention: 86400000 },
239
- },
240
- logging: {
241
- level: 'info',
242
- directory: '~/.omnitron/logs',
243
- maxSize: '50m',
244
- maxFiles: 10,
245
- compress: false,
246
- },
247
- env: 'production',
248
- };
249
- `;
1247
+ async activateBundleHeld(target, version, prefix = '/opt/omnitron', keepVersions = 3) {
1248
+ assertControlledDaemonPort(target.daemonPort);
1249
+ return this.underLease(target, `held host upgrade to ${version}`, async () => {
1250
+ await this.assertNoPendingHostOperations(target, prefix);
1251
+ const { runningGatewaySites, MaintenanceLock, DEFAULT_MAINTENANCE_TIMING } = await import('./maintenance-lock.js');
1252
+ const run = (script) => this.sshExecData(target, script, 30_000);
1253
+ const locks = [];
1254
+ for (const site of await runningGatewaySites(run, `omnitron ${version}`)) {
1255
+ const lock = await MaintenanceLock.open(site, this.logger, { ...DEFAULT_MAINTENANCE_TIMING, probeWithinMs: 180_000 });
1256
+ if (lock)
1257
+ locks.push(lock);
1258
+ }
1259
+ try {
1260
+ let activated = await this.activateBundle(target, version, prefix, keepVersions, async () => {
1261
+ for (const lock of locks) {
1262
+ const failed = await lock.probe(['/', '/api/main/health']);
1263
+ if (failed.length)
1264
+ throw new Error(`Gateway verification failed: ${failed.join(', ')}`);
1265
+ }
1266
+ });
1267
+ while (locks.length > 0) {
1268
+ const lifted = await locks.shift().liftWhenAnswering(['/', '/api/main/health']);
1269
+ if (!lifted.lifted) {
1270
+ activated = false;
1271
+ this.logger.error({ host: target.host, version, failed: lifted.failed }, 'After the upgrade the platform does not answer through its gateway — maintenance expires by itself');
1272
+ }
1273
+ }
1274
+ return activated;
1275
+ }
1276
+ finally {
1277
+ for (const lock of locks)
1278
+ await lock.lift('failed').catch(() => undefined);
1279
+ }
1280
+ });
250
1281
  }
251
- // ===========================================================================
252
- // Private — SSH Operations
253
- // ===========================================================================
254
- async verifySSH(node) {
255
- await this.sshExec(node, 'echo ok', 10_000);
256
- }
257
- async sshExec(node, command, timeout = 60_000) {
258
- const sshArgs = this.buildSSHArgs(node);
259
- sshArgs.push(command);
260
- const { stdout } = await exec('ssh', sshArgs, { timeout });
261
- return stdout.trim();
262
- }
263
- async scpTransfer(node, localPath, remotePath) {
264
- const user = node.ssh?.user ?? 'root';
265
- const port = node.ssh?.port ?? 22;
266
- const scpArgs = [
267
- '-o', 'StrictHostKeyChecking=accept-new',
268
- '-o', 'ConnectTimeout=10',
269
- '-P', String(port),
270
- ];
271
- if (node.ssh?.privateKey) {
272
- scpArgs.push('-i', node.ssh.privateKey);
273
- }
274
- scpArgs.push(localPath, `${user}@${node.host}:${remotePath}`);
275
- await exec('scp', scpArgs, { timeout: 300_000 }); // 5 min for large artifacts
276
- }
277
- async signalRemoteDaemon(node, appName) {
1282
+ /** Check all durable operator workflows; a recovery can allow only its exact active journal. */
1283
+ async assertNoPendingHostOperations(target, prefix = '/opt/omnitron', allowance) {
1284
+ await this.sshExecData(target, hostOperationBarrierCommand(prefix, allowance));
1285
+ }
1286
+ /** Require a settled host and the actual serving immutable daemon build. */
1287
+ async inspectInstalledBundle(target, prefix = '/opt/omnitron', allowance) {
1288
+ assertControlledDaemonPort(target.daemonPort);
1289
+ await this.assertNoPendingHostOperations(target, prefix, allowance);
1290
+ const pending = JSON.parse(await this.sshExecData(target, pendingReceiptsCommand(prefix)));
1291
+ if (pending.length)
1292
+ throw new Error(`Unfinished host upgrade ${pending.map(r => r.id).join(', ')}; recover it before deploying an app`);
1293
+ const bundle = JSON.parse(await this.sshExecData(target, bundleProbeCommand(prefix)));
1294
+ const status = JSON.parse(await this.sshExecData(target, inventoryCommand(bundle, false)));
1295
+ assertServingBundle(bundle, status);
1296
+ await this.sshExecData(target, runtimeProbeCommand(bundle, status.pid));
1297
+ return bundle;
1298
+ }
1299
+ async activateBundle(target, version, prefix = '/opt/omnitron', keepVersions = 3, verifyPlatform, allowance) {
1300
+ assertControlledDaemonPort(target.daemonPort);
1301
+ if (!/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(version) || version.includes('..'))
1302
+ throw new Error('Invalid installed bundle version');
1303
+ if (!Number.isSafeInteger(keepVersions) || keepVersions < 1)
1304
+ throw new Error('keepVersions must be a positive integer');
1305
+ return this.underLease(target, `host upgrade to ${version}`, async () => {
1306
+ const nodeKey = `${target.host}:${target.daemonPort ?? 9700}`;
1307
+ try {
1308
+ await this.assertNoPendingHostOperations(target, prefix, allowance);
1309
+ const pending = JSON.parse(await this.sshExecData(target, pendingReceiptsCommand(prefix)));
1310
+ if (pending.length) {
1311
+ if (pending.length !== 1)
1312
+ throw new Error('Multiple unfinished host upgrades; recover an explicit receipt ID first');
1313
+ const recovered = await this.recoverBundleUpgrade(target, pending[0].id, prefix, verifyPlatform, allowance);
1314
+ this.logger.warn({ receipt: pending[0].id, recovered }, 'Recovered the unfinished host upgrade; retry the requested upgrade separately');
1315
+ return false;
1316
+ }
1317
+ const previous = await this.inspectInstalledBundle(target, prefix, allowance);
1318
+ const candidate = JSON.parse(await this.sshExecData(target, bundleProbeCommand(prefix, version)));
1319
+ await this.sshExec(target, `${shellEscape(`${candidate.root}/dist/cli/omnitron.js`)} --version`, 120_000);
1320
+ const inventory = JSON.parse(await this.sshExecData(target, inventoryCommand(previous, true)));
1321
+ inventory.runtime = JSON.parse(await this.sshExecData(target, runtimeProbeCommand(previous, inventory.status.pid)));
1322
+ const plan = planHostUpgrade(previous, candidate, inventory);
1323
+ await this.sshExec(target, contractPreflightCommand(plan.apps));
1324
+ for (const app of plan.apps) {
1325
+ await this.sshExec(target, hookCommand(app, previous, 'verify'), 330_000);
1326
+ if (plan.online.includes(app.name))
1327
+ await this.sshExec(target, appHealthCommand(app, previous), 330_000);
1328
+ }
1329
+ const id = `${new Date().toISOString().replace(/[:.]/g, '-')}-${crypto.randomUUID()}`;
1330
+ const receiptPath = `${prefix}/upgrade-receipts/${id}.json`;
1331
+ const receipt = await executeHostUpgrade(plan, id, this.hostUpgradeActions(target, prefix, keepVersions, plan, verifyPlatform));
1332
+ const ok = receipt.outcome === 'upgraded';
1333
+ this.emitProgress(nodeKey, '*', ok ? 'success' : 'failed', 100, `${receipt.outcome}; recovery receipt: ${receiptPath}`);
1334
+ this.logger[ok ? 'info' : 'error']({ host: target.host, version, receipt: receiptPath, outcome: receipt.outcome }, 'Controlled host upgrade finished');
1335
+ return ok;
1336
+ }
1337
+ catch (error) {
1338
+ const message = error.message;
1339
+ this.emitProgress(nodeKey, '*', 'failed', 0, message);
1340
+ this.logger.error({ host: target.host, version, error: message }, 'Host upgrade refused or preparation failed');
1341
+ return false;
1342
+ }
1343
+ });
1344
+ }
1345
+ /** Recover a recorded interrupted upgrade under the same node lease; never prunes or changes app release versions. */
1346
+ async recoverBundleUpgrade(target, id, prefix = '/opt/omnitron', verifyPlatform, allowance) {
1347
+ assertControlledDaemonPort(target.daemonPort);
1348
+ return this.underLease(target, `recover host upgrade ${id}`, async () => {
1349
+ await this.assertNoPendingHostOperations(target, prefix, allowance);
1350
+ const records = JSON.parse(await this.sshExecData(target, pendingReceiptsCommand(prefix, id)));
1351
+ if (records.length !== 1 || records[0].id !== id)
1352
+ throw new Error('No unfinished host upgrade with that receipt ID');
1353
+ const receipt = records[0];
1354
+ const plan = planHostUpgrade(receipt.plan.previous, receipt.plan.candidate, receipt.plan.inventory);
1355
+ const installed = JSON.parse(await this.sshExecData(target, bundleProbeCommand(prefix, plan.previous.version)));
1356
+ if (JSON.stringify(installed) !== JSON.stringify(plan.previous))
1357
+ throw new Error('Recorded previous bundle changed; recovery refused');
1358
+ await this.sshExec(target, contractPreflightCommand(plan.apps));
1359
+ const result = await recoverHostUpgrade(receipt, this.hostUpgradeActions(target, prefix, 3, plan, verifyPlatform));
1360
+ this.logger[result.outcome === 'rolled-back' ? 'info' : 'error']({ receipt: `${prefix}/upgrade-receipts/${id}.json`, outcome: result.outcome }, 'Host upgrade recovery finished');
1361
+ return result.outcome === 'rolled-back';
1362
+ });
1363
+ }
1364
+ hostUpgradeActions(target, prefix, keepVersions, plan, verifyPlatform) {
1365
+ return {
1366
+ save: async (receipt) => { await this.sshExec(target, receiptCommand(prefix, receipt)); },
1367
+ stop: async (bundle) => {
1368
+ await this.sshExec(target, runtimeStopCommand(plan.inventory.runtime, bundle), 180_000);
1369
+ await this.sshExec(target, stoppedDaemonCommand(bundle), 30_000);
1370
+ },
1371
+ activate: async (bundle) => {
1372
+ for (const step of activateSteps({ prefix, version: bundle.version }))
1373
+ await this.sshExec(target, step.command, step.timeoutMs);
1374
+ },
1375
+ rebind: async (app, bundle) => { await this.sshExec(target, hookCommand(app, bundle, 'rebind'), 330_000); },
1376
+ start: async (bundle) => {
1377
+ await this.sshExec(target, runtimeStartCommand(plan.inventory.runtime, plan.previous, bundle, [plan.previous.root, plan.candidate.root]), 180_000);
1378
+ },
1379
+ verify: async (bundle) => {
1380
+ const deadline = Date.now() + 180_000;
1381
+ let lastError;
1382
+ do {
1383
+ try {
1384
+ const status = JSON.parse(await this.sshExecData(target, inventoryCommand(bundle, false), 30_000));
1385
+ verifyRestoredInventory(plan, bundle, status);
1386
+ const runtime = JSON.parse(await this.sshExecData(target, runtimeProbeCommand(bundle, status.pid)));
1387
+ if (runtime.kind !== plan.inventory.runtime.kind || (runtime.kind === 'systemd' && plan.inventory.runtime.kind === 'systemd' &&
1388
+ (runtime.scope !== plan.inventory.runtime.scope || runtime.enabled !== plan.inventory.runtime.enabled)))
1389
+ throw new Error('Supervisor state was not restored');
1390
+ for (const app of plan.apps) {
1391
+ await this.sshExec(target, hookCommand(app, bundle, 'verify'), 330_000);
1392
+ if (plan.online.includes(app.name))
1393
+ await this.sshExec(target, appHealthCommand(app, bundle), 330_000);
1394
+ }
1395
+ if (verifyPlatform)
1396
+ await verifyPlatform();
1397
+ await this.sshExec(target, updateContractBindingsCommand(plan, bundle));
1398
+ return;
1399
+ }
1400
+ catch (error) {
1401
+ lastError = error;
1402
+ }
1403
+ if (Date.now() < deadline)
1404
+ await new Promise(resolve => setTimeout(resolve, 2_000));
1405
+ } while (Date.now() < deadline);
1406
+ throw new Error(`Host and application verification failed: ${String(lastError)}`);
1407
+ },
1408
+ prune: async (protectedRoots) => {
1409
+ for (const step of pruneSteps({ prefix, version: plan.candidate.version }, keepVersions, protectedRoots))
1410
+ await this.sshExec(target, step.command, step.timeoutMs);
1411
+ },
1412
+ };
1413
+ }
1414
+ async verifySSH(target) {
1415
+ await this.sshExec(target, 'echo ok', 10_000);
1416
+ }
1417
+ /**
1418
+ * Run a command on the node, through the daemon's one SSH implementation.
1419
+ *
1420
+ * Throws on a non-zero exit, which is what the callers above are written
1421
+ * against — they use `|| echo ""` where they want to see a failure as an
1422
+ * answer, and expect a throw everywhere else. `ExecutionService.ssh` reports
1423
+ * failure in `exitCode` rather than raising, so the check has to be here;
1424
+ * without it every `if (!result.trim())` in this file would read a failed
1425
+ * command as an empty answer, and the two probes in `provisionSlaveNode`
1426
+ * would install a runtime onto a host that already has one.
1427
+ */
1428
+ /**
1429
+ * How a node deploy lease reaches its node: the lease module writes the
1430
+ * scripts, the deployer owns the SSH. See `node-deploy-lease.ts`.
1431
+ */
1432
+ leaseRunner(target) {
1433
+ // Through the data channel: a lease answer carries the holder's record,
1434
+ // `{"token":…}`, which the masker rewrote into `"token": [REDACTED]` on
1435
+ // every read — a refusal could never say who held the node.
1436
+ return async (script) => {
1437
+ // Confirmation itself must use the raw channel, never recursively ask
1438
+ // for another confirmation of the lease being acquired or renewed.
1439
+ const result = await this.readFromNodeRaw(target, script, 30_000);
1440
+ if (result.code !== 0)
1441
+ throw new Error(`Lease transport failed: ${result.stderr || result.stdout}`);
1442
+ return result.stdout.trim();
1443
+ };
1444
+ }
1445
+ /**
1446
+ * Run `work` holding the machine's deploy lease — for what changes a node
1447
+ * outside a stack deployment. `fleet upgrade`'s install and activate went
1448
+ * to the node with no lease at all, and are exactly a second writer: the
1449
+ * activation restarts the node's daemon under whatever else is deploying.
1450
+ */
1451
+ async underLease(target, purpose, work) {
1452
+ const machine = `${target.host}:${target.sshPort ?? 22}`;
1453
+ const held = this.deploymentLeases.getStore();
1454
+ if (held?.has(machine))
1455
+ return work();
1456
+ return withNodeLeases([{ node: machine, machine, run: this.leaseRunner(target) }], purpose, this.logger, async (leases) => {
1457
+ if (!leases.has(machine)) {
1458
+ throw new Error(`Could not reach ${machine} to lease it for ${purpose}: ${leases.unreachable.get(machine) ?? 'no answer'}`);
1459
+ }
1460
+ const context = new Map(held);
1461
+ context.set(machine, { confirm: step => leases.confirm(machine, step), guard: (command, timeoutMs) => leases.guard(machine, command, timeoutMs) });
1462
+ return this.deploymentLeases.run(context, work);
1463
+ });
1464
+ }
1465
+ /** Confirm a held lease before the next node operation; outside a lease this is a no-op. */
1466
+ async confirmLease(target, step) {
1467
+ const machine = `${target.host}:${target.sshPort ?? 22}`;
1468
+ await this.deploymentLeases.getStore()?.get(machine)?.confirm(step);
1469
+ }
1470
+ /** The node holds its execution fence for the command lifetime, beyond a lost controller. */
1471
+ guardedCommand(target, command, timeout) {
1472
+ const lease = this.deploymentLeases.getStore()?.get(`${target.host}:${target.sshPort ?? 22}`);
1473
+ return lease ? { command: lease.guard(command, timeout), timeout: timeout + 45_000 } : { command, timeout };
1474
+ }
1475
+ async sshExec(target, command, timeout = 60_000) {
1476
+ await this.confirmLease(target, 'remote command');
1477
+ const guarded = this.guardedCommand(target, command, timeout);
1478
+ const result = await this.execution.ssh(sshTargetOf(target), guarded.command, { timeout: guarded.timeout });
1479
+ if (result.exitCode !== 0) {
1480
+ const detail = result.stderr || result.stdout || `exit ${result.exitCode}`;
1481
+ throw new Error(`ssh ${target.username ?? 'root'}@${target.host}: ${detail}`);
1482
+ }
1483
+ return result.stdout.trim();
1484
+ }
1485
+ /** `sshExec` for a command whose stdout is data — see `readFromNode`. */
1486
+ async sshExecData(target, command, timeout = 60_000) {
1487
+ const result = await this.readFromNode(target, command, timeout);
1488
+ if (result.code !== 0) {
1489
+ throw new Error(`ssh ${target.username ?? 'root'}@${target.host}: ${result.stderr || result.stdout || `exit ${result.code}`}`);
1490
+ }
1491
+ return result.stdout.trim();
1492
+ }
1493
+ /**
1494
+ * Run a command on a node and hand back what it said, exit code included.
1495
+ *
1496
+ * `sshExec` turns every non-zero exit into a throw, which is right for a
1497
+ * deployment step — a step either happened or it did not. A PROBE's exit
1498
+ * code is information: the attestation producer exits 1 when probes failed
1499
+ * (a fact to keep) and 2 when it could not measure at all (nothing to
1500
+ * keep), and a throw would flatten the two into one.
1501
+ */
1502
+ async runOnNode(target, command, timeoutMs = 600_000) {
1503
+ await this.confirmLease(target, 'remote command');
1504
+ const guarded = this.guardedCommand(target, command, timeoutMs);
1505
+ const result = await this.execution.ssh(sshTargetOf(target), guarded.command, { timeout: guarded.timeout });
1506
+ return { stdout: result.stdout, stderr: result.stderr, code: result.exitCode ?? -1 };
1507
+ }
1508
+ /**
1509
+ * `runOnNode` for a command whose stdout is DATA — a record to parse, a
1510
+ * line to store — carried past the transport's masker
1511
+ * (`execution/data-channel.ts`). Its callers are named there and here: the
1512
+ * lease runner (holder records), the attestation (the producer's line), the
1513
+ * operator tool (its answer line). What it returns is unmasked; none of
1514
+ * them logs it whole.
1515
+ *
1516
+ * A channel that came back as something other than its encoding is a
1517
+ * failure with its own code, -2, and its words on stderr — never data.
1518
+ */
1519
+ async readFromNode(target, command, timeoutMs = 600_000) {
1520
+ await this.confirmLease(target, 'remote data command');
1521
+ const guarded = this.guardedCommand(target, command, timeoutMs);
1522
+ return this.readFromNodeRaw(target, guarded.command, guarded.timeout);
1523
+ }
1524
+ async readFromNodeRaw(target, command, timeoutMs) {
1525
+ const result = await this.execution.ssh(sshTargetOf(target), throughDataChannel(command), { timeout: timeoutMs });
1526
+ const data = fromDataChannel(result.stdout);
1527
+ if (!data.ok) {
1528
+ return { stdout: '', stderr: [data.because, result.stderr].filter(Boolean).join(' | '), code: result.exitCode === 0 ? -2 : (result.exitCode ?? -1) };
1529
+ }
1530
+ return { stdout: data.text, stderr: result.stderr, code: result.exitCode ?? -1 };
1531
+ }
1532
+ /** Use this inside underLease so uploads share the command confirmation boundary. */
1533
+ async uploadToNode(target, localPath, remotePath) {
1534
+ await this.confirmLease(target, 'file upload');
1535
+ const lease = this.deploymentLeases.getStore()?.get(`${target.host}:${target.sshPort ?? 22}`);
1536
+ if (!lease) {
1537
+ await this.execution.uploadFile(sshTargetOf(target), localPath, remotePath);
1538
+ return;
1539
+ }
1540
+ await this.execution.uploadFile(sshTargetOf(target), localPath, remotePath, {
1541
+ runControl: async (command, timeout) => {
1542
+ const result = await this.runOnNode(target, command, timeout);
1543
+ return { stdout: result.stdout, stderr: result.stderr, exitCode: result.code };
1544
+ },
1545
+ });
1546
+ }
1547
+ async scpTransfer(target, localPath, remotePath) {
1548
+ await this.uploadToNode(target, localPath, remotePath);
1549
+ }
1550
+ /**
1551
+ * Ask the node's daemon to run the app whose artifact just landed.
1552
+ *
1553
+ * This was `omnitron restart <app> 2>/dev/null || true`, and the `|| true`
1554
+ * was hiding the whole of the remote deployment's last mile.
1555
+ *
1556
+ * Measured on the test node, running the command by hand:
1557
+ *
1558
+ * omnitron restart main → Failed: Unknown app: main
1559
+ *
1560
+ * The node's daemon has no app definitions — its own log says `No projects
1561
+ * registered`, and `omnitron status --json` answers `appsTotal: 0` — because
1562
+ * nothing in this deployment ever tells it about the apps. Artifacts land
1563
+ * at `/opt/omnitron/artifacts/<project>/<app>/<version>/` complete with
1564
+ * `config`, `dist` and `package.json`, and a project is "a directory with
1565
+ * omnitron.config.ts", which an artifact is not.
1566
+ *
1567
+ * That gap is not closed here — it is a design question about how a node
1568
+ * learns what to run. What is closed here is its INVISIBILITY: the shell
1569
+ * discarded the only sentence that said so, and the caller reported
1570
+ * `Deployment successful`.
1571
+ */
1572
+ /**
1573
+ * Ship a built frontend to a node and say where it landed.
1574
+ *
1575
+ * The gateway's config travels inside the provisioning RPC — six files, 65
1576
+ * KB. A frontend build is 32 MB, and an RPC argument is the wrong shape for
1577
+ * that: it is held whole in memory on both sides and blocks the call it
1578
+ * rides on. So it goes the way artifacts already go — tar over SSH, extract
1579
+ * on the far side — which is the mechanism this class exists for.
1580
+ *
1581
+ * Idempotent by content: the archive's hash names the directory, so a build
1582
+ * that has not changed is transferred once and every later provisioning pass
1583
+ * finds it already there. A build that HAS changed lands beside the old one
1584
+ * and the gateway's spec hash changes with the path, which is what makes the
1585
+ * container pick it up.
1586
+ */
1587
+ async uploadStaticBundle(target, localDir, remoteRoot,
1588
+ /**
1589
+ * The stack's link inside `remoteRoot`, `current-<project>-<stack>`
1590
+ * (`gatewayStaticLinkName`). Given, the delivery points it at the build it
1591
+ * just unpacked — which is what lets the gateway mount the ROOT once and
1592
+ * never be recreated for a new build. Omitted, nothing is linked and the
1593
+ * delivery is exactly what it was.
1594
+ */
1595
+ link) {
1596
+ const { createHash } = await import('node:crypto');
1597
+ const fsp = await import('node:fs/promises');
1598
+ const os = await import('node:os');
1599
+ const path = await import('node:path');
1600
+ const stat = await fsp.stat(localDir).catch(() => null);
1601
+ if (!stat?.isDirectory())
1602
+ throw new Error(`No such directory to serve: ${localDir}`);
1603
+ const staging = await fsp.mkdtemp(path.join(os.tmpdir(), 'omnitron-static-'));
1604
+ const archive = path.join(staging, 'static.tar.gz');
1605
+ try {
1606
+ // `-C` so the archive holds the directory's CONTENTS, not a path from
1607
+ // this machine: the node mounts what is inside, and a leading
1608
+ // `apps/portal/dist/` would put every file one level too deep.
1609
+ // `COPYFILE_DISABLE=1`: macOS `tar` writes an AppleDouble sidecar for
1610
+ // every file with an extended attribute, and this archive is unpacked
1611
+ // straight into the gateway's web root. Measured there:
1612
+ // `._index.html`, `._assets`, `._docs-static` — 163 bytes each, served
1613
+ // to anyone who asks for them.
1614
+ // `--no-xattrs` for the same reason as `COPYFILE_DISABLE`, one layer
1615
+ // down. macOS stamps `com.apple.provenance` on files it has seen, and
1616
+ // bsdtar carries it as a `LIBARCHIVE.xattr.*` header that GNU tar on
1617
+ // the node does not know. Measured: 29 319 characters of
1618
+ // `tar: Ignoring unknown extended header keyword` in one error, with
1619
+ // the four lines that named the actual failure at the very end. The
1620
+ // warnings were harmless and the noise was not — it is what a reader
1621
+ // sees first, and it says nothing.
1622
+ // `| gzip -n` rather than tar's own `-z`, for the digest below.
1623
+ //
1624
+ // gzip writes the time of compression into its header, so the same
1625
+ // directory packed twice a second apart produces two different sums —
1626
+ // and this sum is what the remote directory is NAMED after. Measured:
1627
+ // two archives of one file, one second apart, gave 2 distinct sha256
1628
+ // with `-czf` and 1 with `| gzip -n`.
1629
+ //
1630
+ // Two consequences, and the second is the load-bearing one. A peer
1631
+ // repacking the bundle to check what the node holds could compare
1632
+ // only its LENGTH, because the sums were guaranteed to differ — an
1633
+ // «almost proof» where a proof was one flag away. And the skip on an
1634
+ // unchanged build never fired: every deployment minted a new digest,
1635
+ // so `test -d <digest>` was always false and the same bytes crossed
1636
+ // the link again. (omni-74, after their size-matched, sum-mismatched
1637
+ // comparison.)
1638
+ const packed = await this.execution.exec(`COPYFILE_DISABLE=1 tar --no-xattrs -cf - -C ${shellEscape(localDir)} . | gzip -n > ${shellEscape(archive)}`);
1639
+ // Read, and judged whole, BEFORE it is summed. `exec` never throws, and
1640
+ // this pipeline's status is gzip's alone, so a `tar` that stopped
1641
+ // halfway left a short archive that was then summed, sent, checked
1642
+ // against its own short sum — and failed only on the node: «gzip:
1643
+ // stdin: unexpected end of file» (sample-202609221949-23dce2f6 to test,
1644
+ // 2026-09-22). The sum proves the file ARRIVED as it left; nothing
1645
+ // proved it left whole.
1646
+ const content = await fsp.readFile(archive).catch(() => null);
1647
+ const verdict = content ? wholeTarGz(content) : 'no archive was written';
1648
+ if (packed.exitCode !== 0 || verdict !== true) {
1649
+ const words = (packed.stderr || '').trim().split('\n').slice(-3).join(' | ');
1650
+ throw new Error(`Packing ${localDir} for the node failed (exit ${packed.exitCode}; ${verdict === true ? 'archive whole' : verdict})` +
1651
+ (words ? `: ${words}` : '') +
1652
+ ' — nothing was sent');
1653
+ }
1654
+ const bytes = content.length;
1655
+ const digest = createHash('sha256').update(content).digest('hex').slice(0, 16);
1656
+ const remoteDir = `${remoteRoot}/${digest}`;
1657
+ const remoteFile = `${remoteDir}.tar.gz`;
1658
+ // Already there: the same build was sent before. Transferring it again
1659
+ // costs 32 MB over a link that may be an SSH tunnel, for no change.
1660
+ //
1661
+ // The question is «is the BUNDLE there», and `test -d` answered «is
1662
+ // the DIRECTORY there» — two different statements, and the first
1663
+ // deployment made them disagree. `mkdir -p` had succeeded, the
1664
+ // transfer had landed short, and `tar` had died on the truncated
1665
+ // stream: the directory existed, holding nothing or part of a tree.
1666
+ // The next run would have seen it, answered «the node already has
1667
+ // this build», and skipped the delivery — reporting a saved transfer
1668
+ // where there was a missing portal. (omni-74 saw this coming before
1669
+ // the second attempt reached the step.)
1670
+ //
1671
+ // So readiness is a RECORD, written last, exactly as the app
1672
+ // artifacts do it with `.artifact-sha256`: a marker that cannot
1673
+ // outlive a failed unpack because it is written after one succeeds.
1674
+ //
1675
+ // Not `test -s index.html` either, which was the first idea: that is
1676
+ // a sign the CONTENT is present, and half a tree contains an
1677
+ // index.html too. The marker is a sign the OPERATION completed.
1678
+ // (omni-74 drew that distinction.)
1679
+ // It sits beside the directory rather than inside it — everything
1680
+ // inside is served to anyone who asks.
1681
+ const marker = `${remoteDir}.delivered`;
1682
+ const exists = await this.sshExec(target, `test -s ${shellEscape(marker)} && test -d ${shellEscape(remoteDir)} && echo yes || echo no`).catch(() => 'no');
1683
+ //
1684
+ // What «already there» excuses is the TRANSFER, not the deployment.
1685
+ // This used to `return` here, and under the old mount that was exact:
1686
+ // the gateway mounted the build directory itself, so a container
1687
+ // recreated with this digest as its source served this digest, and
1688
+ // nothing else had to happen. Serving through
1689
+ // `current-<project>-<stack>` moves that fact into the link, and a
1690
+ // deployment that does not point the link has not deployed.
1691
+ //
1692
+ // The case is a ROLLBACK, and only a rollback: forward releases carry
1693
+ // new content, so a new digest, so this is false. Deploy A, deploy B,
1694
+ // then ask for A again — A's bundle and its marker are still on the
1695
+ // node, the early return fired, and the link stayed on B. The gateway
1696
+ // went on serving B while the release said A, with nothing anywhere
1697
+ // saying so. Which is the version you least want wrong.
1698
+ const alreadyThere = exists.trim() === 'yes';
1699
+ // Whatever a previous attempt left half-written is not a starting
1700
+ // point: unpacking onto it would merge two trees.
1701
+ if (!alreadyThere) {
1702
+ await this.sshExec(target, `rm -rf ${shellEscape(remoteDir)} ${shellEscape(marker)}`).catch(() => undefined);
1703
+ await this.sshExec(target, `mkdir -p ${shellEscape(remoteDir)}`);
1704
+ await this.uploadToNode(target, archive, remoteFile);
1705
+ // `uploadFile` already refuses a file that landed SHORT. That catches
1706
+ // the likely failure and not the other one: a transfer that completes
1707
+ // and corrupts in the middle has the right length and the wrong
1708
+ // bytes, and `tar` would report it three steps later as a broken
1709
+ // archive rather than as a bad transfer.
1710
+ //
1711
+ // The sum costs nothing to obtain here, because it is already the
1712
+ // name of the directory this is going into. `sha256sum` on the node,
1713
+ // `shasum -a 256` where that is what exists — asking for both and
1714
+ // taking whichever answers beats assuming the platform.
1715
+ // (omni-74's objection: the size check was one class of failure, and
1716
+ // the stronger check needed no new information.)
1717
+ const sum = await this.sshExec(target, `sha256sum ${shellEscape(remoteFile)} 2>/dev/null || shasum -a 256 ${shellEscape(remoteFile)}`);
1718
+ const landed = sum.trim().split(/\s+/)[0]?.slice(0, 16);
1719
+ if (landed !== digest) {
1720
+ await this.sshExec(target, `rm -f ${shellEscape(remoteFile)}`).catch(() => undefined);
1721
+ throw new Error(`The static bundle arrived corrupt: sent sha256 ${digest}, the node has ${landed ?? 'no readable sum'}. ` +
1722
+ 'Nothing was unpacked and the partial file was removed.');
1723
+ }
1724
+ await this.sshExec(target, `tar -xzf ${shellEscape(remoteFile)} -C ${shellEscape(remoteDir)} && rm -f ${shellEscape(remoteFile)}`);
1725
+ }
1726
+ // A tab opened before this deployment asks for chunks of the build it
1727
+ // loaded; they are carried into this one, and builds nobody serves go
1728
+ // (`static-carry-forward.ts`). Before the marker: it is part of what
1729
+ // «delivered» means now.
1730
+ await this.carryPreviousBuildForward(target, remoteRoot, remoteDir);
1731
+ // AFTER the carry-forward, because that reads the link to find the build
1732
+ // open tabs loaded — the one being replaced.
1733
+ if (link)
1734
+ await this.pointStaticLinkAt(target, remoteRoot, link, digest);
1735
+ // Last, and only now: the unpack is what this records.
1736
+ if (!alreadyThere)
1737
+ await this.sshExec(target, `printf %s ${shellEscape(digest)} > ${shellEscape(marker)}`);
1738
+ this.logger.info({ host: target.host, remoteDir, bytes: alreadyThere ? 0 : bytes, transferred: !alreadyThere }, alreadyThere ? 'The node already had this build — pointed at it' : 'Static bundle delivered to the node');
1739
+ return { remoteDir, bytes: alreadyThere ? 0 : bytes };
1740
+ }
1741
+ finally {
1742
+ await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
1743
+ }
1744
+ }
1745
+ /**
1746
+ * Point a stack's `current-<project>-<stack>` at the build just unpacked.
1747
+ *
1748
+ * This is the whole reason the gateway stops being recreated on a
1749
+ * deployment: it mounts the static ROOT, whose path never changes, and
1750
+ * serves `/var/www/portal/<link>`. nginx resolves `root` per request and
1751
+ * this config sets no `open_file_cache`, so the swap is picked up with no
1752
+ * reload and no restart. What it replaces cost **1 s of 000** on the
1753
+ * gateway, measured on the release-4 deployment (81 s total).
1754
+ *
1755
+ * RELATIVE, so it resolves inside the container as well as on the host — the
1756
+ * host path is not the container path.
1757
+ *
1758
+ * And swapped with `mv -T`, never `ln -sfn`: `-f` unlinks before it
1759
+ * symlinks, and a request arriving in that window gets a 404 from a web root
1760
+ * that does not exist. `rename(2)` has no window at all. The temporary name
1761
+ * carries the digest so two deliveries racing on one node cannot collide on
1762
+ * it.
1763
+ */
1764
+ async pointStaticLinkAt(target, root, link, digest) {
1765
+ const tmp = `${root}/${link}.${digest}.tmp`;
1766
+ await this.sshExec(target, `ln -s ${shellEscape(digest)} ${shellEscape(tmp)} && mv -T ${shellEscape(tmp)} ${shellEscape(`${root}/${link}`)}`);
1767
+ this.logger.info({ host: target.host, link, digest }, 'The gateway now serves this build through its link');
1768
+ }
1769
+ async carryPreviousBuildForward(target, input, fresh) {
1770
+ if (typeof input !== 'string') {
1771
+ const { immutableAssetsRoot, immutableAssetsScript } = await import('./static-carry-forward.js');
1772
+ const expectedRoot = immutableAssetsRoot(input.owner);
1773
+ // The native execution fence serializes every publisher, including reentrant calls.
1774
+ return this.underLease(target, 'retain immutable public assets', async () => {
1775
+ await this.assertNoPendingHostOperations(target);
1776
+ const fs = await import('node:fs/promises');
1777
+ const { tmpdir } = await import('node:os');
1778
+ const { join } = await import('node:path');
1779
+ const { randomUUID } = await import('node:crypto');
1780
+ const temporary = await fs.mkdtemp(join(tmpdir(), 'omnitron-static-assets-'));
1781
+ const local = join(temporary, 'request.json');
1782
+ const remote = '/tmp/omnitron-static-assets-' + randomUUID() + '.json';
1783
+ try {
1784
+ await fs.writeFile(local, JSON.stringify(input), { mode: 0o600, flag: 'wx' });
1785
+ await this.uploadToNode(target, local, remote);
1786
+ const output = await this.sshExec(target, immutableAssetsScript(remote), 300_000);
1787
+ const receipt = JSON.parse(output);
1788
+ if (receipt.assetsRoot !== expectedRoot || !/^[a-f0-9]{64}$/.test(receipt.manifestSha256) ||
1789
+ ![receipt.files, receipt.added, receipt.reused, receipt.bytes].every((n) => Number.isSafeInteger(n) && n >= 0)) {
1790
+ throw new Error('Invalid immutable asset publication receipt');
1791
+ }
1792
+ this.logger.info({ host: target.host, owner: input.owner, ...receipt }, 'Immutable public asset bytes retained');
1793
+ return receipt;
1794
+ }
1795
+ finally {
1796
+ await fs.rm(temporary, { recursive: true, force: true });
1797
+ await this.sshExec(target, 'rm -f -- ' + shellEscape(remote)).catch(() => undefined);
1798
+ }
1799
+ });
1800
+ }
1801
+ const root = input;
1802
+ if (!fresh)
1803
+ throw new Error('Missing legacy static build');
1804
+ try {
1805
+ const { GATEWAY_MOUNTS_COMMAND, readGatewayMounts, carryForwardScript } = await import('./static-carry-forward.js');
1806
+ const mounts = readGatewayMounts(await this.sshExec(target, GATEWAY_MOUNTS_COMMAND).catch(() => ''));
1807
+ const script = carryForwardScript({ root, fresh, serving: mounts.serving, mounted: mounts.mounted, keep: 5 });
1808
+ const said = await this.sshExec(target, `bash -c ${shellEscape(script)}`, 300_000);
1809
+ this.logger.info({ host: target.host, fresh, serving: mounts.serving, said: said.trim() }, 'The build open tabs loaded is carried forward; builds nobody serves are removed');
1810
+ }
1811
+ catch (err) {
1812
+ this.logger.warn({ host: target.host, fresh, error: err.message.slice(0, 400) }, 'Could not carry the previous build forward — open tabs will reload onto the new one');
1813
+ }
1814
+ }
1815
+ async signalRemoteDaemon(target, appName) {
278
1816
  try {
279
- // Try RPC restart via omnitron CLI on remote
280
- await this.sshExec(node, `omnitron restart ${shellEscape(appName)} 2>/dev/null || true`);
1817
+ // No `2>/dev/null`, no `|| true`: the failure IS the information.
1818
+ const out = await this.sshExec(target, `omnitron restart ${shellEscape(appName)} 2>&1`);
1819
+ const failed = /unknown app|failed|not found|no such/i.test(out);
1820
+ if (failed) {
1821
+ this.logger.error({ host: target.host, app: appName, detail: out.trim().slice(0, 300) }, 'The node refused to start this app — its artifact is installed and its daemon does not know the app');
1822
+ return { ok: false, detail: out.trim().slice(0, 300) };
1823
+ }
1824
+ return { ok: true, detail: out.trim().slice(0, 200) };
281
1825
  }
282
- catch {
283
- // Non-critical — daemon may not be running
284
- this.logger.debug({ host: node.host, app: appName }, 'Remote daemon restart signal failed');
1826
+ catch (err) {
1827
+ const detail = err.message;
1828
+ this.logger.error({ host: target.host, app: appName, error: detail }, 'Could not reach the node to start this app');
1829
+ return { ok: false, detail };
285
1830
  }
286
1831
  }
287
- async verifyHealth(node, appName) {
288
- // Simple health check: ping remote daemon and check app status
1832
+ /**
1833
+ * Record that the app now runs this artifact — written only after it came
1834
+ * up healthy on it, never at unpack time, so a deployment interrupted
1835
+ * between installing and starting leaves the old record and the next one
1836
+ * ships and starts again. No checksum, no record: the next deployment then
1837
+ * cannot tell, and «cannot tell» deploys.
1838
+ *
1839
+ * A write that fails is said and not fatal: the app runs; the next
1840
+ * deployment transfers once more than it needed to.
1841
+ */
1842
+ async recordRunningArtifact(target, remotePath, app, checksum) {
1843
+ if (!checksum)
1844
+ return;
289
1845
  try {
290
- const status = await this.sshExec(node, `omnitron status --json 2>/dev/null || echo "{}"`, 15_000);
291
- const parsed = JSON.parse(status);
292
- if (parsed?.apps) {
293
- const app = parsed.apps.find((a) => a.name === appName);
294
- if (app?.status === 'online')
295
- return;
1846
+ await this.sshExec(target, `printf %s ${shellEscape(checksum)} > ${shellEscape(`${remotePath}/${ARTIFACT_CHECKSUM_FILE}`)}`);
1847
+ }
1848
+ catch (err) {
1849
+ this.logger.warn({ node: target.host, app, error: err.message }, 'The app runs its new artifact and the node could not record which — the next deployment ships it again');
1850
+ }
1851
+ }
1852
+ /**
1853
+ * The apps this node reports as online, by their bare names.
1854
+ *
1855
+ * One question for the whole node. The per-app check beside this one asks
1856
+ * the same command, and asking it six times to decide six restarts is six
1857
+ * SSH round trips for one answer.
1858
+ *
1859
+ * An empty set on any failure: not knowing what is running is not a claim
1860
+ * that nothing is, and the caller's fallback — restart it — is the safe
1861
+ * reading of "we could not tell".
1862
+ */
1863
+ async appsOnline(target, project) {
1864
+ try {
1865
+ const answer = await this.askNodeStatus(target);
1866
+ const status = readNodeStatus(answer.raw);
1867
+ if (!status) {
1868
+ this.logger.warn({ node: target.host, project, said: answer.words }, 'The node did not answer what it is running — every app will be restarted');
1869
+ return new Set();
296
1870
  }
1871
+ return new Set(status.apps
1872
+ .filter((app) => app.status === 'online' && typeof app.name === 'string')
1873
+ .map((app) => {
1874
+ const name = app.name;
1875
+ return name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
1876
+ }));
297
1877
  }
298
- catch {
299
- // Health check is best-effort
1878
+ catch (err) {
1879
+ this.logger.warn({ node: target.host, project, error: err.message }, 'Could not read what this node is running — every app will be restarted');
1880
+ return new Set();
300
1881
  }
301
1882
  }
302
- buildSSHArgs(node) {
303
- const user = node.ssh?.user ?? 'root';
304
- const port = node.ssh?.port ?? 22;
305
- const args = [
306
- '-o', 'StrictHostKeyChecking=accept-new',
307
- '-o', 'ConnectTimeout=10',
308
- '-o', 'BatchMode=yes',
309
- '-p', String(port),
310
- ];
311
- if (node.ssh?.privateKey) {
312
- args.push('-i', node.ssh.privateKey);
1883
+ /**
1884
+ * Whether the app is actually running on the node.
1885
+ *
1886
+ * This returned `void` on every path — a match, a mismatch, a parse failure,
1887
+ * an unreachable node — under a comment reading "Health check is
1888
+ * best-effort". A verification that cannot fail is not a verification, and
1889
+ * it is worse than none: its presence in the sequence is what persuades a
1890
+ * reader that the deployment was checked.
1891
+ *
1892
+ * The reading itself is in `readNodeHealth`, where the test can reach it.
1893
+ */
1894
+ async verifyHealth(target, appName, project) {
1895
+ const answer = await this.askNodeStatus(target);
1896
+ if (answer.code === UNREADABLE) {
1897
+ return { online: false, detail: `could not read the node's status: ${answer.words}` };
1898
+ }
1899
+ const health = readNodeHealth(answer.raw, appName, project);
1900
+ return health.online || !answer.words ? health : { online: false, detail: `${health.detail}: ${answer.words}` };
1901
+ }
1902
+ /**
1903
+ * `omnitron status --json` on a node: its stdout is DATA, so it comes
1904
+ * through the data channel — past the transport's masker, which rewrites a
1905
+ * value such as `"token": …` into something that is no longer JSON — and
1906
+ * its stderr stays words, masked, and separate. `2>&1` used to fold the two
1907
+ * together, so an answer that could not be parsed was quoted into the log
1908
+ * whole. Three readers: the daemon check before a restart, `appsOnline`,
1909
+ * `verifyHealth`. None logs `raw`.
1910
+ *
1911
+ * `UNREADABLE` when the node could not be asked at all, with the reason as
1912
+ * the words.
1913
+ */
1914
+ async askNodeStatus(target) {
1915
+ try {
1916
+ const answer = await this.readFromNode(target, 'omnitron status --json', 15_000);
1917
+ return { raw: answer.stdout, words: answer.stderr.trim().split('\n')[0] ?? '', code: answer.code };
1918
+ }
1919
+ catch (err) {
1920
+ return { raw: '', words: err.message, code: UNREADABLE };
313
1921
  }
314
- args.push(`${user}@${node.host}`);
315
- return args;
316
1922
  }
317
1923
  // ===========================================================================
318
1924
  // Private — Progress
319
1925
  // ===========================================================================
320
1926
  emitProgress(node, app, status, progress, message) {
321
1927
  const event = { node, app, status, progress, message };
1928
+ this.lastProgress.set(`${node}\u0000${app}`, { ...event, at: new Date().toISOString() });
322
1929
  for (const handler of this.progressHandlers) {
323
1930
  try {
324
1931
  handler(event);
@@ -328,5 +1935,15 @@ export default {
328
1935
  }
329
1936
  }
330
1937
  }
1938
+ /**
1939
+ * What each app on each node is doing, newest first.
1940
+ *
1941
+ * For a console that polls. Returned as a list rather than the map so a
1942
+ * caller cannot hold a reference that keeps changing underneath it while
1943
+ * it renders.
1944
+ */
1945
+ getProgress() {
1946
+ return [...this.lastProgress.values()].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : 0));
1947
+ }
331
1948
  }
332
1949
  //# sourceMappingURL=remote-deployer.service.js.map