@omnitron-dev/omnitron 0.1.3 → 0.3.0

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