threadforge 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +52 -20
  2. package/bin/forge.js +2 -1058
  3. package/bin/host-commands.d.ts +2 -0
  4. package/bin/host-commands.d.ts.map +1 -0
  5. package/bin/host-commands.js +7 -8
  6. package/bin/platform-commands.d.ts +2 -0
  7. package/bin/platform-commands.d.ts.map +1 -0
  8. package/bin/platform-commands.js +118 -36
  9. package/dist/cli/base-command.d.ts +12 -0
  10. package/dist/cli/base-command.d.ts.map +1 -0
  11. package/dist/cli/base-command.js +25 -0
  12. package/dist/cli/base-command.js.map +1 -0
  13. package/dist/cli/commands/build.d.ts +10 -0
  14. package/dist/cli/commands/build.d.ts.map +1 -0
  15. package/dist/cli/commands/build.js +110 -0
  16. package/dist/cli/commands/build.js.map +1 -0
  17. package/dist/cli/commands/deploy.d.ts +12 -0
  18. package/dist/cli/commands/deploy.d.ts.map +1 -0
  19. package/dist/cli/commands/deploy.js +143 -0
  20. package/dist/cli/commands/deploy.js.map +1 -0
  21. package/dist/cli/commands/dev.d.ts +10 -0
  22. package/dist/cli/commands/dev.d.ts.map +1 -0
  23. package/dist/cli/commands/dev.js +138 -0
  24. package/dist/cli/commands/dev.js.map +1 -0
  25. package/dist/cli/commands/generate.d.ts +10 -0
  26. package/dist/cli/commands/generate.d.ts.map +1 -0
  27. package/dist/cli/commands/generate.js +76 -0
  28. package/dist/cli/commands/generate.js.map +1 -0
  29. package/dist/cli/commands/host.d.ts +8 -0
  30. package/dist/cli/commands/host.d.ts.map +1 -0
  31. package/dist/cli/commands/host.js +20 -0
  32. package/dist/cli/commands/host.js.map +1 -0
  33. package/dist/cli/commands/init.d.ts +16 -0
  34. package/dist/cli/commands/init.d.ts.map +1 -0
  35. package/dist/cli/commands/init.js +246 -0
  36. package/dist/cli/commands/init.js.map +1 -0
  37. package/dist/cli/commands/platform.d.ts +8 -0
  38. package/dist/cli/commands/platform.d.ts.map +1 -0
  39. package/dist/cli/commands/platform.js +20 -0
  40. package/dist/cli/commands/platform.js.map +1 -0
  41. package/dist/cli/commands/restart.d.ts +8 -0
  42. package/dist/cli/commands/restart.d.ts.map +1 -0
  43. package/dist/cli/commands/restart.js +13 -0
  44. package/dist/cli/commands/restart.js.map +1 -0
  45. package/dist/cli/commands/scaffold/frontend.d.ts +10 -0
  46. package/dist/cli/commands/scaffold/frontend.d.ts.map +1 -0
  47. package/dist/cli/commands/scaffold/frontend.js +130 -0
  48. package/dist/cli/commands/scaffold/frontend.js.map +1 -0
  49. package/dist/cli/commands/scaffold/react.d.ts +7 -0
  50. package/dist/cli/commands/scaffold/react.d.ts.map +1 -0
  51. package/dist/cli/commands/scaffold/react.js +12 -0
  52. package/dist/cli/commands/scaffold/react.js.map +1 -0
  53. package/dist/cli/commands/scale.d.ts +8 -0
  54. package/dist/cli/commands/scale.d.ts.map +1 -0
  55. package/dist/cli/commands/scale.js +13 -0
  56. package/dist/cli/commands/scale.js.map +1 -0
  57. package/dist/cli/commands/start.d.ts +10 -0
  58. package/dist/cli/commands/start.d.ts.map +1 -0
  59. package/dist/cli/commands/start.js +71 -0
  60. package/dist/cli/commands/start.js.map +1 -0
  61. package/dist/cli/commands/status.d.ts +11 -0
  62. package/dist/cli/commands/status.d.ts.map +1 -0
  63. package/dist/cli/commands/status.js +60 -0
  64. package/dist/cli/commands/status.js.map +1 -0
  65. package/dist/cli/commands/stop.d.ts +10 -0
  66. package/dist/cli/commands/stop.d.ts.map +1 -0
  67. package/dist/cli/commands/stop.js +89 -0
  68. package/dist/cli/commands/stop.js.map +1 -0
  69. package/dist/cli/util/config-discovery.d.ts +8 -0
  70. package/dist/cli/util/config-discovery.d.ts.map +1 -0
  71. package/dist/cli/util/config-discovery.js +70 -0
  72. package/dist/cli/util/config-discovery.js.map +1 -0
  73. package/dist/cli/util/config-patcher.d.ts +17 -0
  74. package/dist/cli/util/config-patcher.d.ts.map +1 -0
  75. package/dist/cli/util/config-patcher.js +439 -0
  76. package/dist/cli/util/config-patcher.js.map +1 -0
  77. package/dist/cli/util/frontend-dev.d.ts +8 -0
  78. package/dist/cli/util/frontend-dev.d.ts.map +1 -0
  79. package/dist/cli/util/frontend-dev.js +117 -0
  80. package/dist/cli/util/frontend-dev.js.map +1 -0
  81. package/dist/cli/util/process.d.ts +5 -0
  82. package/dist/cli/util/process.d.ts.map +1 -0
  83. package/dist/cli/util/process.js +17 -0
  84. package/dist/cli/util/process.js.map +1 -0
  85. package/dist/cli/util/templates.d.ts +10 -0
  86. package/dist/cli/util/templates.d.ts.map +1 -0
  87. package/dist/cli/util/templates.js +157 -0
  88. package/dist/cli/util/templates.js.map +1 -0
  89. package/dist/core/AlertSink.d.ts +83 -0
  90. package/dist/core/AlertSink.d.ts.map +1 -0
  91. package/dist/core/AlertSink.js +126 -0
  92. package/dist/core/AlertSink.js.map +1 -0
  93. package/dist/core/DirectMessageBus.d.ts +88 -0
  94. package/dist/core/DirectMessageBus.d.ts.map +1 -0
  95. package/dist/core/DirectMessageBus.js +352 -0
  96. package/dist/core/DirectMessageBus.js.map +1 -0
  97. package/dist/core/EndpointResolver.d.ts +111 -0
  98. package/dist/core/EndpointResolver.d.ts.map +1 -0
  99. package/dist/core/EndpointResolver.js +336 -0
  100. package/dist/core/EndpointResolver.js.map +1 -0
  101. package/dist/core/ForgeContext.d.ts +221 -0
  102. package/dist/core/ForgeContext.d.ts.map +1 -0
  103. package/dist/core/ForgeContext.js +1169 -0
  104. package/dist/core/ForgeContext.js.map +1 -0
  105. package/dist/core/ForgeEndpoints.d.ts +71 -0
  106. package/dist/core/ForgeEndpoints.d.ts.map +1 -0
  107. package/dist/core/ForgeEndpoints.js +442 -0
  108. package/dist/core/ForgeEndpoints.js.map +1 -0
  109. package/dist/core/ForgeHost.d.ts +82 -0
  110. package/dist/core/ForgeHost.d.ts.map +1 -0
  111. package/dist/core/ForgeHost.js +107 -0
  112. package/dist/core/ForgeHost.js.map +1 -0
  113. package/dist/core/ForgePlatform.d.ts +96 -0
  114. package/dist/core/ForgePlatform.d.ts.map +1 -0
  115. package/dist/core/ForgePlatform.js +136 -0
  116. package/dist/core/ForgePlatform.js.map +1 -0
  117. package/dist/core/ForgeWebSocket.d.ts +56 -0
  118. package/dist/core/ForgeWebSocket.d.ts.map +1 -0
  119. package/dist/core/ForgeWebSocket.js +415 -0
  120. package/dist/core/ForgeWebSocket.js.map +1 -0
  121. package/dist/core/Ingress.d.ts +329 -0
  122. package/dist/core/Ingress.d.ts.map +1 -0
  123. package/dist/core/Ingress.js +694 -0
  124. package/dist/core/Ingress.js.map +1 -0
  125. package/dist/core/Interceptors.d.ts +134 -0
  126. package/dist/core/Interceptors.d.ts.map +1 -0
  127. package/dist/core/Interceptors.js +416 -0
  128. package/dist/core/Interceptors.js.map +1 -0
  129. package/dist/core/Logger.d.ts +20 -0
  130. package/dist/core/Logger.d.ts.map +1 -0
  131. package/dist/core/Logger.js +77 -0
  132. package/dist/core/Logger.js.map +1 -0
  133. package/dist/core/MessageBus.d.ts +15 -0
  134. package/dist/core/MessageBus.d.ts.map +1 -0
  135. package/dist/core/MessageBus.js +18 -0
  136. package/dist/core/MessageBus.js.map +1 -0
  137. package/dist/core/Prometheus.d.ts +80 -0
  138. package/dist/core/Prometheus.d.ts.map +1 -0
  139. package/dist/core/Prometheus.js +332 -0
  140. package/dist/core/Prometheus.js.map +1 -0
  141. package/dist/core/RequestContext.d.ts +214 -0
  142. package/dist/core/RequestContext.d.ts.map +1 -0
  143. package/dist/core/RequestContext.js +556 -0
  144. package/dist/core/RequestContext.js.map +1 -0
  145. package/dist/core/Router.d.ts +45 -0
  146. package/dist/core/Router.d.ts.map +1 -0
  147. package/dist/core/Router.js +285 -0
  148. package/dist/core/Router.js.map +1 -0
  149. package/dist/core/RoutingStrategy.d.ts +116 -0
  150. package/dist/core/RoutingStrategy.d.ts.map +1 -0
  151. package/dist/core/RoutingStrategy.js +306 -0
  152. package/dist/core/RoutingStrategy.js.map +1 -0
  153. package/dist/core/RpcConfig.d.ts +72 -0
  154. package/dist/core/RpcConfig.d.ts.map +1 -0
  155. package/dist/core/RpcConfig.js +127 -0
  156. package/dist/core/RpcConfig.js.map +1 -0
  157. package/dist/core/SignatureCache.d.ts +81 -0
  158. package/dist/core/SignatureCache.d.ts.map +1 -0
  159. package/dist/core/SignatureCache.js +172 -0
  160. package/dist/core/SignatureCache.js.map +1 -0
  161. package/dist/core/StaticFileServer.d.ts +34 -0
  162. package/dist/core/StaticFileServer.d.ts.map +1 -0
  163. package/dist/core/StaticFileServer.js +497 -0
  164. package/dist/core/StaticFileServer.js.map +1 -0
  165. package/dist/core/Supervisor.d.ts +198 -0
  166. package/dist/core/Supervisor.d.ts.map +1 -0
  167. package/dist/core/Supervisor.js +1418 -0
  168. package/dist/core/Supervisor.js.map +1 -0
  169. package/dist/core/ThreadAllocator.d.ts +52 -0
  170. package/dist/core/ThreadAllocator.d.ts.map +1 -0
  171. package/dist/core/ThreadAllocator.js +174 -0
  172. package/dist/core/ThreadAllocator.js.map +1 -0
  173. package/dist/core/WorkerChannelManager.d.ts +130 -0
  174. package/dist/core/WorkerChannelManager.d.ts.map +1 -0
  175. package/dist/core/WorkerChannelManager.js +956 -0
  176. package/dist/core/WorkerChannelManager.js.map +1 -0
  177. package/dist/core/config-enums.d.ts +41 -0
  178. package/dist/core/config-enums.d.ts.map +1 -0
  179. package/dist/core/config-enums.js +59 -0
  180. package/dist/core/config-enums.js.map +1 -0
  181. package/dist/core/config.d.ts +159 -0
  182. package/dist/core/config.d.ts.map +1 -0
  183. package/dist/core/config.js +694 -0
  184. package/dist/core/config.js.map +1 -0
  185. package/dist/core/host-config.d.ts +146 -0
  186. package/dist/core/host-config.d.ts.map +1 -0
  187. package/dist/core/host-config.js +312 -0
  188. package/dist/core/host-config.js.map +1 -0
  189. package/dist/core/ipc-errors.d.ts +27 -0
  190. package/dist/core/ipc-errors.d.ts.map +1 -0
  191. package/dist/core/ipc-errors.js +36 -0
  192. package/dist/core/ipc-errors.js.map +1 -0
  193. package/dist/core/network-utils.d.ts +35 -0
  194. package/dist/core/network-utils.d.ts.map +1 -0
  195. package/dist/core/network-utils.js +145 -0
  196. package/dist/core/network-utils.js.map +1 -0
  197. package/dist/core/platform-config.d.ts +142 -0
  198. package/dist/core/platform-config.d.ts.map +1 -0
  199. package/dist/core/platform-config.js +299 -0
  200. package/dist/core/platform-config.js.map +1 -0
  201. package/dist/decorators/ServiceProxy.d.ts +175 -0
  202. package/dist/decorators/ServiceProxy.d.ts.map +1 -0
  203. package/dist/decorators/ServiceProxy.js +969 -0
  204. package/dist/decorators/ServiceProxy.js.map +1 -0
  205. package/dist/decorators/index.d.ts +146 -0
  206. package/dist/decorators/index.d.ts.map +1 -0
  207. package/dist/decorators/index.js +545 -0
  208. package/dist/decorators/index.js.map +1 -0
  209. package/dist/deploy/NginxGenerator.d.ts +165 -0
  210. package/dist/deploy/NginxGenerator.d.ts.map +1 -0
  211. package/dist/deploy/NginxGenerator.js +781 -0
  212. package/dist/deploy/NginxGenerator.js.map +1 -0
  213. package/dist/deploy/PlatformManifestGenerator.d.ts +43 -0
  214. package/dist/deploy/PlatformManifestGenerator.d.ts.map +1 -0
  215. package/dist/deploy/PlatformManifestGenerator.js +80 -0
  216. package/dist/deploy/PlatformManifestGenerator.js.map +1 -0
  217. package/dist/deploy/RouteManifestGenerator.d.ts +42 -0
  218. package/dist/deploy/RouteManifestGenerator.d.ts.map +1 -0
  219. package/dist/deploy/RouteManifestGenerator.js +105 -0
  220. package/dist/deploy/RouteManifestGenerator.js.map +1 -0
  221. package/dist/deploy/index.d.ts +210 -0
  222. package/dist/deploy/index.d.ts.map +1 -0
  223. package/dist/deploy/index.js +918 -0
  224. package/dist/deploy/index.js.map +1 -0
  225. package/dist/frontend/FrontendDevLifecycle.d.ts +26 -0
  226. package/dist/frontend/FrontendDevLifecycle.d.ts.map +1 -0
  227. package/dist/frontend/FrontendDevLifecycle.js +60 -0
  228. package/dist/frontend/FrontendDevLifecycle.js.map +1 -0
  229. package/dist/frontend/FrontendPluginOrchestrator.d.ts +64 -0
  230. package/dist/frontend/FrontendPluginOrchestrator.d.ts.map +1 -0
  231. package/dist/frontend/FrontendPluginOrchestrator.js +167 -0
  232. package/dist/frontend/FrontendPluginOrchestrator.js.map +1 -0
  233. package/dist/frontend/SiteResolver.d.ts +33 -0
  234. package/dist/frontend/SiteResolver.d.ts.map +1 -0
  235. package/dist/frontend/SiteResolver.js +53 -0
  236. package/dist/frontend/SiteResolver.js.map +1 -0
  237. package/dist/frontend/StaticMountRegistry.d.ts +36 -0
  238. package/dist/frontend/StaticMountRegistry.d.ts.map +1 -0
  239. package/dist/frontend/StaticMountRegistry.js +94 -0
  240. package/dist/frontend/StaticMountRegistry.js.map +1 -0
  241. package/dist/frontend/index.d.ts +7 -0
  242. package/dist/frontend/index.d.ts.map +1 -0
  243. package/{src → dist}/frontend/index.js +4 -2
  244. package/dist/frontend/index.js.map +1 -0
  245. package/dist/frontend/pathUtils.d.ts +8 -0
  246. package/dist/frontend/pathUtils.d.ts.map +1 -0
  247. package/dist/frontend/pathUtils.js +17 -0
  248. package/dist/frontend/pathUtils.js.map +1 -0
  249. package/dist/frontend/plugins/index.d.ts +2 -0
  250. package/dist/frontend/plugins/index.d.ts.map +1 -0
  251. package/{src → dist}/frontend/plugins/index.js +1 -1
  252. package/dist/frontend/plugins/index.js.map +1 -0
  253. package/dist/frontend/plugins/viteFrontend.d.ts +51 -0
  254. package/dist/frontend/plugins/viteFrontend.d.ts.map +1 -0
  255. package/dist/frontend/plugins/viteFrontend.js +134 -0
  256. package/dist/frontend/plugins/viteFrontend.js.map +1 -0
  257. package/dist/frontend/types.d.ts +25 -0
  258. package/dist/frontend/types.d.ts.map +1 -0
  259. package/dist/frontend/types.js +2 -0
  260. package/dist/frontend/types.js.map +1 -0
  261. package/dist/index.d.ts +17 -0
  262. package/dist/index.d.ts.map +1 -0
  263. package/dist/index.js +32 -0
  264. package/dist/index.js.map +1 -0
  265. package/dist/internals.d.ts +21 -0
  266. package/dist/internals.d.ts.map +1 -0
  267. package/{src → dist}/internals.js +12 -14
  268. package/dist/internals.js.map +1 -0
  269. package/dist/plugins/PluginManager.d.ts +209 -0
  270. package/dist/plugins/PluginManager.d.ts.map +1 -0
  271. package/dist/plugins/PluginManager.js +365 -0
  272. package/dist/plugins/PluginManager.js.map +1 -0
  273. package/dist/plugins/ScopedPostgres.d.ts +78 -0
  274. package/dist/plugins/ScopedPostgres.d.ts.map +1 -0
  275. package/dist/plugins/ScopedPostgres.js +190 -0
  276. package/dist/plugins/ScopedPostgres.js.map +1 -0
  277. package/dist/plugins/ScopedRedis.d.ts +88 -0
  278. package/dist/plugins/ScopedRedis.d.ts.map +1 -0
  279. package/dist/plugins/ScopedRedis.js +169 -0
  280. package/dist/plugins/ScopedRedis.js.map +1 -0
  281. package/dist/plugins/index.d.ts +289 -0
  282. package/dist/plugins/index.d.ts.map +1 -0
  283. package/dist/plugins/index.js +1942 -0
  284. package/dist/plugins/index.js.map +1 -0
  285. package/dist/plugins/types.d.ts +59 -0
  286. package/dist/plugins/types.d.ts.map +1 -0
  287. package/dist/plugins/types.js +2 -0
  288. package/dist/plugins/types.js.map +1 -0
  289. package/dist/registry/ServiceRegistry.d.ts +305 -0
  290. package/dist/registry/ServiceRegistry.d.ts.map +1 -0
  291. package/dist/registry/ServiceRegistry.js +735 -0
  292. package/dist/registry/ServiceRegistry.js.map +1 -0
  293. package/dist/scaling/ScaleAdvisor.d.ts +214 -0
  294. package/dist/scaling/ScaleAdvisor.d.ts.map +1 -0
  295. package/dist/scaling/ScaleAdvisor.js +526 -0
  296. package/dist/scaling/ScaleAdvisor.js.map +1 -0
  297. package/dist/services/Service.d.ts +164 -0
  298. package/dist/services/Service.d.ts.map +1 -0
  299. package/dist/services/Service.js +106 -0
  300. package/dist/services/Service.js.map +1 -0
  301. package/dist/services/worker-bootstrap.d.ts +15 -0
  302. package/dist/services/worker-bootstrap.d.ts.map +1 -0
  303. package/dist/services/worker-bootstrap.js +744 -0
  304. package/dist/services/worker-bootstrap.js.map +1 -0
  305. package/dist/templates/auth-service.d.ts +42 -0
  306. package/dist/templates/auth-service.d.ts.map +1 -0
  307. package/dist/templates/auth-service.js +54 -0
  308. package/dist/templates/auth-service.js.map +1 -0
  309. package/dist/templates/identity-service.d.ts +50 -0
  310. package/dist/templates/identity-service.d.ts.map +1 -0
  311. package/dist/templates/identity-service.js +62 -0
  312. package/dist/templates/identity-service.js.map +1 -0
  313. package/dist/types/contract.d.ts +120 -0
  314. package/dist/types/contract.d.ts.map +1 -0
  315. package/dist/types/contract.js +69 -0
  316. package/dist/types/contract.js.map +1 -0
  317. package/package.json +78 -20
  318. package/src/core/DirectMessageBus.js +0 -364
  319. package/src/core/EndpointResolver.js +0 -259
  320. package/src/core/ForgeContext.js +0 -2236
  321. package/src/core/ForgeHost.js +0 -122
  322. package/src/core/ForgePlatform.js +0 -145
  323. package/src/core/Ingress.js +0 -768
  324. package/src/core/Interceptors.js +0 -420
  325. package/src/core/MessageBus.js +0 -321
  326. package/src/core/Prometheus.js +0 -305
  327. package/src/core/RequestContext.js +0 -413
  328. package/src/core/RoutingStrategy.js +0 -330
  329. package/src/core/Supervisor.js +0 -1349
  330. package/src/core/ThreadAllocator.js +0 -196
  331. package/src/core/WorkerChannelManager.js +0 -879
  332. package/src/core/config.js +0 -637
  333. package/src/core/host-config.js +0 -311
  334. package/src/core/network-utils.js +0 -166
  335. package/src/core/platform-config.js +0 -308
  336. package/src/decorators/ServiceProxy.js +0 -904
  337. package/src/decorators/index.js +0 -571
  338. package/src/deploy/NginxGenerator.js +0 -865
  339. package/src/deploy/PlatformManifestGenerator.js +0 -96
  340. package/src/deploy/RouteManifestGenerator.js +0 -112
  341. package/src/deploy/index.js +0 -984
  342. package/src/frontend/FrontendDevLifecycle.js +0 -65
  343. package/src/frontend/FrontendPluginOrchestrator.js +0 -187
  344. package/src/frontend/SiteResolver.js +0 -63
  345. package/src/frontend/StaticMountRegistry.js +0 -90
  346. package/src/frontend/plugins/viteFrontend.js +0 -79
  347. package/src/frontend/types.js +0 -35
  348. package/src/index.js +0 -58
  349. package/src/plugins/PluginManager.js +0 -537
  350. package/src/plugins/ScopedPostgres.js +0 -192
  351. package/src/plugins/ScopedRedis.js +0 -142
  352. package/src/plugins/index.js +0 -1756
  353. package/src/registry/ServiceRegistry.js +0 -797
  354. package/src/scaling/ScaleAdvisor.js +0 -442
  355. package/src/services/Service.js +0 -195
  356. package/src/services/worker-bootstrap.js +0 -679
  357. package/src/templates/auth-service.js +0 -65
  358. package/src/templates/identity-service.js +0 -75
@@ -1,984 +0,0 @@
1
- /**
2
- * Deployment System
3
- *
4
- * The problem: you have 8 services and 3 machines. You need to
5
- * decide what runs where, generate the right configs for each
6
- * machine, ensure discovery works, handle rolling deploys, and
7
- * do it all without the developer manually editing addresses.
8
- *
9
- * ═══════════════════════════════════════════════════════════════
10
- * THE DEPLOYMENT MANIFEST
11
- * ═══════════════════════════════════════════════════════════════
12
- *
13
- * One file describes your entire cluster: forge.deploy.js
14
- *
15
- * export default {
16
- * cluster: 'my-saas-prod',
17
- *
18
- * nodes: {
19
- * 'web-1': {
20
- * host: '10.0.1.10',
21
- * services: ['gateway', 'auth'],
22
- * role: 'edge',
23
- * },
24
- * 'api-1': {
25
- * host: '10.0.1.20',
26
- * services: ['users', 'billing', 'search'],
27
- * role: 'api',
28
- * },
29
- * 'worker-1': {
30
- * host: '10.0.1.30',
31
- * services: ['notifications', 'analytics', 'media'],
32
- * role: 'worker',
33
- * },
34
- * },
35
- *
36
- * // Optional: override per-node settings
37
- * overrides: {
38
- * 'api-1': {
39
- * users: { threads: 4, weight: 5 },
40
- * },
41
- * },
42
- *
43
- * registry: 'multicast', // or 'redis://...' or 'etcd://...'
44
- *
45
- * httpBasePort: 4000,
46
- * };
47
- *
48
- * From this manifest, `forge deploy` generates:
49
- *
50
- * 1. Per-node forge.config.js (services this node runs locally,
51
- * all others as type: 'remote' with addresses)
52
- *
53
- * 2. Docker Compose / docker-compose.yml for local dev that
54
- * simulates the multi-machine topology
55
- *
56
- * 3. Systemd unit files for production deployment
57
- *
58
- * 4. HTTP-based inter-service communication for all services
59
- * that cross machine boundaries
60
- *
61
- * 5. Deployment scripts (rsync + restart)
62
- *
63
- * ═══════════════════════════════════════════════════════════════
64
- * NODE CONFIG GENERATION
65
- * ═══════════════════════════════════════════════════════════════
66
- *
67
- * For web-1 (runs gateway + auth), the generated config looks like:
68
- *
69
- * // AUTO-GENERATED by forge deploy — do not edit
70
- * export default defineServices({
71
- * // Local services (this machine runs these)
72
- * gateway: {
73
- * entry: './services/gateway.js',
74
- * type: 'edge',
75
- * port: 3000,
76
- * threads: 'auto',
77
- * connects: ['auth', 'users', 'search'],
78
- * },
79
- * auth: {
80
- * entry: './services/auth.js',
81
- * type: 'internal',
82
- * connects: ['users'],
83
- * },
84
- *
85
- * // Remote services (on other machines, reached via HTTP)
86
- * users: { type: 'remote', address: 'http://10.0.1.20:4000' },
87
- * billing: { type: 'remote', address: 'http://10.0.1.20:4001' },
88
- * search: { type: 'remote', address: 'http://10.0.1.20:4002' },
89
- * notifications: { type: 'remote', address: 'http://10.0.1.30:4000' },
90
- * analytics: { type: 'remote', address: 'http://10.0.1.30:4001' },
91
- * media: { type: 'remote', address: 'http://10.0.1.30:4002' },
92
- * }, {
93
- * registryMode: 'multicast',
94
- * host: '10.0.1.10',
95
- * });
96
- *
97
- * The developer's service code is IDENTICAL on every machine.
98
- * Only the generated config differs.
99
- */
100
-
101
- import fs from "node:fs";
102
- import path from "node:path";
103
- import { resolveAppDomains } from "../core/platform-config.js";
104
- import {
105
- buildNginxServiceMap,
106
- buildNginxWebSocketRoutes,
107
- generateNginxConfig,
108
- generatePlatformNginxConfig,
109
- } from "./NginxGenerator.js";
110
- import { generatePlatformManifest } from "./PlatformManifestGenerator.js";
111
-
112
- // ── Port assignment helper ────────────────────────────────────
113
-
114
- /**
115
- * Assign HTTP ports to all services across all nodes.
116
- *
117
- * Edge services keep their configured port; internal/background services
118
- * get sequential ports starting from httpBasePort, skipping any already
119
- * claimed by edge services on that node.
120
- *
121
- * @param {Object} manifest - Validated manifest with .nodes
122
- * @param {Object} serviceConfigs - Base service configs
123
- * @param {number} httpBasePort - Starting port for auto-assignment
124
- * @returns {Object} servicePorts — { serviceName: [{ host, port }, ...] }
125
- */
126
- function _assignPorts(manifest, serviceConfigs, httpBasePort) {
127
- const servicePorts = {};
128
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
129
- const usedPorts = new Set();
130
- for (const svcName of node.services) {
131
- const existingPort = serviceConfigs[svcName]?.port;
132
- if (existingPort) usedPorts.add(existingPort);
133
- }
134
- let nextPort = httpBasePort;
135
- for (const svcName of node.services) {
136
- const existingPort = serviceConfigs[svcName]?.port;
137
- if (existingPort) {
138
- if (!servicePorts[svcName]) servicePorts[svcName] = [];
139
- servicePorts[svcName].push({ host: node.host, port: existingPort });
140
- } else {
141
- while (usedPorts.has(nextPort)) nextPort++;
142
- if (nextPort > 65535) {
143
- throw new Error(`Port exhaustion on node "${nodeName}": cannot allocate ports beyond 65535`);
144
- }
145
- const port = nextPort++;
146
- usedPorts.add(port);
147
- if (!servicePorts[svcName]) servicePorts[svcName] = [];
148
- servicePorts[svcName].push({ host: node.host, port });
149
- }
150
- }
151
- }
152
- return servicePorts;
153
- }
154
-
155
- // ── Input validation for deploy safety ───────────────────────
156
-
157
- const NODE_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]*$/;
158
- const HOSTNAME_RE = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
159
- const IPV4_RE = /^(\d{1,3}\.){3}\d{1,3}$/;
160
- const IPV6_RE = /^[0-9a-fA-F:]+$/;
161
- const SHELL_METACHAR_RE = /[`$;|&<>(){}[\]\\'"!\s]/;
162
-
163
- function validateNodeName(name) {
164
- if (!NODE_NAME_RE.test(name)) {
165
- throw new Error(`Invalid node name "${name}": must start with alphanumeric and contain only [a-zA-Z0-9_-]`);
166
- }
167
- }
168
-
169
- function validateHostname(host, nodeName) {
170
- // H-DEPLOY-2: Reject hosts with shell metacharacters first
171
- if (SHELL_METACHAR_RE.test(host)) {
172
- throw new Error(`Invalid host "${host}" for node "${nodeName}": contains shell metacharacters`);
173
- }
174
- if (HOSTNAME_RE.test(host) || IPV4_RE.test(host) || IPV6_RE.test(host)) {
175
- return;
176
- }
177
- throw new Error(`Invalid host "${host}" for node "${nodeName}": must be a valid hostname, IPv4, or IPv6 address`);
178
- }
179
-
180
- /**
181
- * Load and validate a deployment manifest.
182
- *
183
- * @param {Object} manifest - The deployment manifest
184
- * @param {Object} serviceConfigs - The base service configs from forge.config.js
185
- * @returns {Object} Validated manifest with computed fields
186
- */
187
- export function loadManifest(manifest, serviceConfigs) {
188
- if (!manifest.nodes || Object.keys(manifest.nodes).length === 0) {
189
- throw new Error("Deployment manifest must define at least one node");
190
- }
191
-
192
- // Validate nodes
193
- const allAssigned = new Set();
194
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
195
- validateNodeName(nodeName);
196
- if (!node.host) {
197
- throw new Error(`Node "${nodeName}" is missing a host address`);
198
- }
199
- validateHostname(node.host, nodeName);
200
- if (!node.services || node.services.length === 0) {
201
- throw new Error(`Node "${nodeName}" has no services assigned`);
202
- }
203
- for (const svc of node.services) {
204
- allAssigned.add(svc);
205
- }
206
- }
207
-
208
- // Check for unassigned services
209
- for (const svcName of Object.keys(serviceConfigs)) {
210
- if (!allAssigned.has(svcName)) {
211
- throw new Error(
212
- `Service "${svcName}" from forge.config.js is not assigned to any node. ` +
213
- `Add it to a node in forge.deploy.js.`,
214
- );
215
- }
216
- }
217
-
218
- // Build the map: service → [nodes]
219
- const serviceToNodes = {};
220
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
221
- for (const svc of node.services) {
222
- if (!serviceToNodes[svc]) serviceToNodes[svc] = [];
223
- serviceToNodes[svc].push({ node: nodeName, host: node.host });
224
- }
225
- }
226
-
227
- return {
228
- ...manifest,
229
- serviceToNode: serviceToNodes, // now an array
230
- allServices: Object.keys(serviceConfigs),
231
- };
232
- }
233
-
234
- /**
235
- * Generate per-node forge.config.js files.
236
- *
237
- * Each node gets a config where:
238
- * - Services it runs are defined with their full config (entry, type, etc.)
239
- * - Services on OTHER nodes are defined as type: 'remote' with HTTP addresses
240
- *
241
- * @param {Object} manifest - Validated manifest
242
- * @param {Object} serviceConfigs - Base service configs
243
- * @param {Object} options
244
- * @returns {Map<string, string>} nodeName → generated config content
245
- */
246
- export function generateNodeConfigs(manifest, serviceConfigs, _options = {}) {
247
- const configs = new Map();
248
- const httpBasePort = manifest.httpBasePort ?? 4000;
249
- const registryMode = manifest.registry ?? "multicast";
250
-
251
- // Assign HTTP ports: edge services keep their port, others get sequential ports.
252
- // Each service needs an HTTP port so remote machines can reach /__forge/invoke.
253
-
254
- // Global port collision check BEFORE assignment
255
- const globalPorts = new Map(); // "nodeName:port" → service name
256
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
257
- for (const svcName of node.services) {
258
- const port = serviceConfigs[svcName]?.port;
259
- if (port) {
260
- const key = `${nodeName}:${port}`;
261
- const existing = globalPorts.get(key);
262
- if (existing) {
263
- throw new Error(
264
- `Port ${port} collision on node "${nodeName}": services "${existing}" and "${svcName}" both claim port ${port}`,
265
- );
266
- }
267
- globalPorts.set(key, svcName);
268
- }
269
- }
270
- }
271
-
272
- const servicePorts = _assignPorts(manifest, serviceConfigs, httpBasePort);
273
-
274
- // Final duplicate check per node
275
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
276
- const nodePorts = node.services.map((s) => servicePorts[s]?.find((e) => e.host === node.host)?.port).filter(Boolean);
277
- const uniquePorts = new Set(nodePorts);
278
- if (uniquePorts.size !== nodePorts.length) {
279
- // M-DEPLOY-1: Include both service names and node name in collision message
280
- const portToServices = {};
281
- for (const svc of node.services) {
282
- const port = servicePorts[svc]?.find((e) => e.host === node.host)?.port;
283
- if (port) {
284
- if (!portToServices[port]) portToServices[port] = [];
285
- portToServices[port].push(svc);
286
- }
287
- }
288
- const dupeDescs = Object.entries(portToServices)
289
- .filter(([, svcs]) => svcs.length > 1)
290
- .map(([p, svcs]) => `port ${p} (services: ${svcs.join(", ")})`);
291
- throw new Error(
292
- `Port collision on node "${nodeName}": ${dupeDescs.join("; ")}. Note: ports can be reused across different nodes.`,
293
- );
294
- }
295
- }
296
-
297
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
298
- const overrides = manifest.overrides?.[nodeName] ?? {};
299
-
300
- let config = `// forge.config.js — Auto-generated for node: ${nodeName}
301
- // Host: ${node.host} | Role: ${node.role ?? "general"}
302
- // Generated by: forge deploy
303
- // Do not edit manually — regenerate with: forge deploy --generate
304
-
305
- import { defineServices } from 'threadforge';
306
-
307
- export default defineServices({
308
-
309
- // ── Local services (this node runs these) ──
310
- `;
311
-
312
- // Local services
313
- for (const svcName of node.services) {
314
- const svc = serviceConfigs[svcName];
315
- if (!svc) {
316
- throw new Error(`Node "${nodeName}" references service "${svcName}" which has no configuration`);
317
- }
318
-
319
- const override = overrides[svcName] ?? {};
320
- const merged = { ...svc, ...override };
321
- const assignedPort = servicePorts[svcName]?.find((e) => e.host === node.host)?.port;
322
-
323
- config += `\n ${svcName}: {\n`;
324
- config += ` entry: '${merged.entry}',\n`;
325
- config += ` type: '${merged.type}',\n`;
326
-
327
- // All services get an HTTP port in multi-machine mode
328
- config += ` port: ${assignedPort},\n`;
329
-
330
- if (merged.threads) {
331
- config += ` threads: ${typeof merged.threads === "string" ? `'${merged.threads}'` : merged.threads},\n`;
332
- }
333
-
334
- if (merged.weight && merged.weight > 1) {
335
- config += ` weight: ${merged.weight},\n`;
336
- }
337
-
338
- if (merged.connects && merged.connects.length > 0) {
339
- config += ` connects: [${merged.connects.map((c) => `'${c}'`).join(", ")}],\n`;
340
- }
341
-
342
- if (merged.group) {
343
- config += ` group: '${merged.group}',\n`;
344
- }
345
-
346
- if (merged.websocket) {
347
- config += ` websocket: ${JSON.stringify(merged.websocket)},\n`;
348
- }
349
-
350
- if (merged.routing && merged.routing.strategy !== "round-robin") {
351
- config += ` routing: ${JSON.stringify(merged.routing)},\n`;
352
- }
353
-
354
- config += ` },\n`;
355
- }
356
-
357
- // Remote services
358
- const remoteServices = manifest.allServices.filter((s) => !node.services.includes(s));
359
-
360
- if (remoteServices.length > 0) {
361
- config += `\n // ── Remote services (on other machines, via HTTP) ──\n`;
362
-
363
- for (const svcName of remoteServices) {
364
- const remote = servicePorts[svcName]?.[0]; // Use first endpoint as primary
365
- if (!remote) continue;
366
-
367
- const targetNodes = manifest.serviceToNode[svcName];
368
- const targetLabel = Array.isArray(targetNodes)
369
- ? targetNodes.map((n) => n.node).join(", ")
370
- : (targetNodes?.node ?? "unknown");
371
- config += `\n ${svcName}: {\n`;
372
- config += ` type: 'remote',\n`;
373
- config += ` address: 'http://${remote.host}:${remote.port}',`;
374
- config += ` // ${targetLabel}\n`;
375
- if (servicePorts[svcName]?.length > 1) {
376
- config += ` endpoints: ${JSON.stringify(servicePorts[svcName])},\n`;
377
- }
378
- config += ` },\n`;
379
- }
380
- }
381
-
382
- config += `\n}, {\n`;
383
- config += ` registryMode: '${registryMode}',\n`;
384
- config += ` host: '${node.host}',\n`;
385
- config += ` metricsPort: ${manifest.metricsPort ?? 9090},\n`;
386
- config += `});\n`;
387
-
388
- configs.set(nodeName, config);
389
- }
390
-
391
- return configs;
392
- }
393
-
394
- /**
395
- * Generate Docker Compose for local development that simulates
396
- * the multi-machine topology.
397
- */
398
- export function generateDockerCompose(manifest, serviceConfigs) {
399
- let compose = `# docker-compose.yml — Auto-generated by forge deploy
400
- # Simulates multi-machine topology for local development
401
-
402
- services:
403
- `;
404
-
405
- // Compute HTTP ports using the shared helper
406
- const httpBasePort = manifest.httpBasePort ?? 4000;
407
- const portMap = _assignPorts(manifest, serviceConfigs, httpBasePort);
408
-
409
- // Build a flat nodeName:svc → port lookup for Docker Compose (keyed per-node)
410
- const servicePorts = {};
411
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
412
- for (const svc of node.services) {
413
- const entry = portMap[svc]?.find((e) => e.host === node.host);
414
- if (entry) servicePorts[`${nodeName}:${svc}`] = entry.port;
415
- }
416
- }
417
-
418
- // Build a service→endpoint map using Docker hostnames instead of IPs
419
- const serviceEndpointMap = {};
420
- for (const [nn, nd] of Object.entries(manifest.nodes)) {
421
- for (const svc of nd.services) {
422
- const port = servicePorts[`${nn}:${svc}`];
423
- if (port) {
424
- if (!serviceEndpointMap[svc]) serviceEndpointMap[svc] = [];
425
- serviceEndpointMap[svc].push({ host: nn, port: Number(port), remote: true });
426
- }
427
- }
428
- }
429
-
430
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
431
- // Build FORGE_SERVICE_ENDPOINTS using Docker hostnames for this node (JSON format)
432
- const endpointMap = {};
433
- for (const [svc, endpoints] of Object.entries(serviceEndpointMap)) {
434
- if (!node.services.includes(svc)) {
435
- endpointMap[svc] = endpoints;
436
- }
437
- }
438
- const endpointsEnv = Object.keys(endpointMap).length > 0 ? JSON.stringify(endpointMap) : "";
439
- // YAML single-quoted strings escape ' as '' (per YAML spec §7.3.2).
440
- // The value is JSON (double-quoted keys/values) so single quotes are
441
- // unlikely, but we handle them defensively for correctness.
442
- const yamlSafeEndpoints = endpointsEnv.replace(/'/g, "''");
443
-
444
- compose += `
445
- ${nodeName}:
446
- build: .
447
- hostname: ${nodeName}
448
- restart: on-failure
449
- env_file: ./.env
450
- environment:
451
- - FORGE_NODE=${nodeName}
452
- - FORGE_HOST=${nodeName}
453
- - FORGE_REGION=${manifest.region ?? "local"}${yamlSafeEndpoints ? `\n - FORGE_SERVICE_ENDPOINTS='${yamlSafeEndpoints}'` : ""}
454
- volumes:
455
- - ./deploy/${nodeName}/forge.config.js:/app/deploy/${nodeName}/forge.config.js:ro
456
- working_dir: /app
457
- command: node bin/forge.js start --config ./deploy/${nodeName}/forge.config.js
458
- stop_grace_period: 35s
459
- healthcheck:
460
- test: ["CMD", "wget", "-q", "--spider", "http://localhost:9090/health"]
461
- interval: 10s
462
- timeout: 3s
463
- retries: 3
464
- start_period: 30s
465
- logging:
466
- driver: json-file
467
- options:
468
- max-size: "10m"
469
- max-file: "3"
470
- deploy:
471
- resources:
472
- limits:
473
- cpus: "2"
474
- memory: 2G
475
- `;
476
-
477
- // Expose HTTP ports for all services (edge + internal with assigned ports)
478
- const ports = node.services
479
- .map((svc) => ({
480
- svc,
481
- port: servicePorts[`${nodeName}:${svc}`],
482
- type: serviceConfigs[svc]?.type ?? "internal",
483
- }))
484
- .filter((p) => p.port);
485
-
486
- if (ports.length > 0) {
487
- const edgePorts = ports.filter(p => p.type === "edge");
488
- const internalPorts = ports.filter(p => p.type !== "edge");
489
- if (edgePorts.length > 0) {
490
- compose += ` ports:\n`;
491
- for (const { svc, port } of edgePorts) {
492
- compose += ` - "${port}:${port}" # ${svc} HTTP\n`;
493
- }
494
- }
495
- if (internalPorts.length > 0) {
496
- compose += ` expose:\n`;
497
- for (const { svc, port } of internalPorts) {
498
- compose += ` - "${port}" # ${svc} HTTP (internal)\n`;
499
- }
500
- }
501
- }
502
-
503
- compose += ` networks:\n`;
504
- compose += ` - forge-net\n`;
505
- }
506
-
507
- compose += `
508
- networks:
509
- forge-net:
510
- driver: bridge
511
- `;
512
-
513
- return compose;
514
- }
515
-
516
- /**
517
- * Generate a systemd service unit file for a node.
518
- */
519
- export function generateSystemdUnit(nodeName, node, manifest) {
520
- const endpoints = node.endpoints || {};
521
- const envFilePath = `/etc/threadforge/${nodeName}.env`;
522
- const envFileContent = `FORGE_SERVICE_ENDPOINTS=${JSON.stringify(endpoints)}`;
523
-
524
- return `[Unit]
525
- Description=ThreadForge - ${nodeName}
526
- After=network-online.target
527
- Wants=network-online.target
528
- StartLimitIntervalSec=300
529
- StartLimitBurst=5
530
-
531
- [Service]
532
- Type=simple
533
- User=forge
534
- Group=forge
535
- WorkingDirectory=/opt/forge
536
- ExecStart=/usr/bin/node bin/forge.js start --config ./deploy/${nodeName}/forge.config.js
537
- ExecStop=/bin/kill -SIGTERM $MAINPID
538
- Restart=always
539
- RestartSec=5
540
- TimeoutStopSec=30
541
- Environment=NODE_ENV=production
542
- Environment=FORGE_NODE=${nodeName}
543
- Environment=FORGE_HOST=${node.host}
544
- Environment=FORGE_REGION=${manifest.region ?? "production"}
545
- EnvironmentFile=${envFilePath}
546
- EnvironmentFile=-/etc/threadforge/secrets/${nodeName}.env
547
-
548
- # Security hardening
549
- NoNewPrivileges=true
550
- PrivateTmp=true
551
- ProtectSystem=strict
552
- ReadWritePaths=/opt/forge
553
-
554
- # Resource limits
555
- LimitNOFILE=65536
556
- MemoryMax=2G
557
-
558
- [Install]
559
- WantedBy=multi-user.target
560
- `;
561
- }
562
-
563
- /**
564
- * Generate the environment file content for a systemd unit.
565
- * Write this to /etc/threadforge/<nodeName>.env
566
- *
567
- * @param {string} nodeName
568
- * @param {Object} node
569
- * @returns {{ path: string, content: string }}
570
- */
571
- export function generateSystemdEnvFile(nodeName, node) {
572
- const endpoints = node.endpoints || {};
573
- return {
574
- path: `/etc/threadforge/${nodeName}.env`,
575
- content: `FORGE_SERVICE_ENDPOINTS='${JSON.stringify(endpoints).replace(/'/g, "'\\''")}'\n`,
576
- };
577
- }
578
-
579
- /**
580
- * Generate a deployment script that syncs code and restarts services.
581
- */
582
- export function generateDeployScript(manifest) {
583
- let script = `#!/bin/bash
584
- # deploy.sh — Auto-generated by forge deploy
585
- # Usage: ./deploy.sh [node-name] Deploy to a specific node
586
- # ./deploy.sh Deploy to all nodes
587
- # ./deploy.sh --rolling Rolling deploy (one at a time)
588
- # ./deploy.sh --rollback <node> Rollback a node to latest backup
589
-
590
- set -euo pipefail
591
-
592
- DEPLOY_DIR="/opt/forge"
593
- PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
594
- BACKUP_BASE="/opt/forge/backups"
595
-
596
- # ── Rollback helper ──────────────────────────────────────────
597
- rollback() {
598
- local node_host="\$1"
599
- local node_name="\$2"
600
- echo "Rolling back \${node_name} (\${node_host})..."
601
- local latest
602
- latest=\$(ssh "forge@\${node_host}" "ls -1t \\"\${BACKUP_BASE}\\" 2>/dev/null | head -1")
603
- if [ -z "\$latest" ]; then
604
- echo " No backup found for \${node_name}"
605
- return 1
606
- fi
607
- ssh "forge@\${node_host}" "rsync -a \\"\${BACKUP_BASE}/\${latest}/\\" \\"\${DEPLOY_DIR}/\\""
608
- ssh "forge@\${node_host}" "sudo systemctl restart forge-\${node_name}"
609
- echo " Rolled back to backup: \${latest}"
610
- }
611
-
612
- `;
613
-
614
- // Validate node names to prevent shell injection
615
- for (const nodeName of Object.keys(manifest.nodes)) {
616
- if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(nodeName)) {
617
- throw new Error(`Invalid node name "${nodeName}": must contain only alphanumeric characters, hyphens, and underscores`);
618
- }
619
- }
620
-
621
- // Node deployment functions
622
- for (const [nodeName, node] of Object.entries(manifest.nodes)) {
623
- script += `deploy_${nodeName.replace(/-/g, "_")}() {
624
- echo "Deploying to ${nodeName} (${node.host})..."
625
-
626
- # Create timestamped backup of current deployment
627
- local BACKUP_DIR="\${BACKUP_BASE}/\$(date +%Y%m%d-%H%M%S)"
628
- ssh "forge@${node.host}" "mkdir -p \\"\${BACKUP_DIR}\\" && rsync -a \\"\${DEPLOY_DIR}/\\" \\"\${BACKUP_DIR}/\\" 2>/dev/null || true"
629
-
630
- # Sync code (excluding node_modules, .git)
631
- rsync -azP --delete \\
632
- --exclude 'node_modules' \\
633
- --exclude '.git' \\
634
- --exclude 'deploy/*/node_modules' \\
635
- "\${PROJECT_DIR}/" "forge@${node.host}:\${DEPLOY_DIR}/"
636
-
637
- # Install dependencies on remote
638
- ssh "forge@${node.host}" "cd \\"\${DEPLOY_DIR}\\" && npm ci --omit=dev"
639
-
640
- # Generate route manifests if needed
641
- ssh "forge@${node.host}" "cd \\"\${DEPLOY_DIR}\\" && node bin/forge.js generate --config ./deploy/${nodeName}/forge.config.js" 2>/dev/null || true
642
-
643
- # Restart service
644
- ssh "forge@${node.host}" "sudo systemctl restart forge-${nodeName}"
645
-
646
- # Wait for health check
647
- echo -n " Waiting for health..."
648
- for i in \$(seq 1 30); do
649
- if ssh "forge@${node.host}" "curl -sf http://localhost:9090/health" >/dev/null 2>&1; then
650
- echo " \u2713 healthy"
651
- return 0
652
- fi
653
- echo -n "."
654
- sleep 1
655
- done
656
- echo " \u2717 FAILED — rolling back..."
657
- rollback "${node.host}" "${nodeName}"
658
- return 1
659
- }
660
-
661
- `;
662
- }
663
-
664
- // Main dispatch
665
- const nodeNames = Object.keys(manifest.nodes);
666
- const funcNames = nodeNames.map((n) => `deploy_${n.replace(/-/g, "_")}`);
667
-
668
- // Build a node→host map for rollback dispatch
669
- const nodeHostEntries = Object.entries(manifest.nodes).map(
670
- ([n, nd]) => ` "${n}") rollback "${nd.host}" "${n}" ;;`,
671
- );
672
-
673
- script += `# \u2500\u2500 Main \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
674
-
675
- if [ "\${1:-}" = "--rollback" ]; then
676
- if [ -z "\${2:-}" ]; then
677
- echo "Usage: ./deploy.sh --rollback <node-name>"
678
- echo "Available: ${nodeNames.join(", ")}"
679
- exit 1
680
- fi
681
- [[ "$2" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "Invalid node name: $2"; exit 1; }
682
- case "$2" in
683
- ${nodeHostEntries.join("\n")}
684
- *) echo "Unknown node: $2"; echo "Available: ${nodeNames.join(", ")}"; exit 1 ;;
685
- esac
686
- elif [ "\${1:-}" = "--rolling" ]; then
687
- echo "Rolling deploy to ${nodeNames.length} nodes..."
688
- for fn in ${funcNames.join(" ")}; do
689
- $fn
690
- echo " Draining for 10s..."
691
- sleep 10
692
- done
693
- elif [ -n "\${1:-}" ]; then
694
- # Validate node name to prevent command injection
695
- [[ "$1" =~ ^[a-zA-Z0-9_-]+$ ]] || { echo "Invalid node name: $1"; exit 1; }
696
- fn="deploy_\${1//-/_}"
697
- if type "$fn" &>/dev/null; then
698
- $fn
699
- else
700
- echo "Unknown node: $1"
701
- echo "Available: ${nodeNames.join(", ")}"
702
- exit 1
703
- fi
704
- else
705
- echo "Deploying to all ${nodeNames.length} nodes in parallel..."
706
- PIDS=()
707
- ${funcNames.map((fn) => `${fn} & PIDS+=($!)`).join("\n ")}
708
- FAIL=0
709
- for pid in "\${PIDS[@]}"; do
710
- wait $pid || FAIL=1
711
- done
712
- if [ $FAIL -ne 0 ]; then
713
- echo "ERROR: One or more deployments failed"
714
- exit 1
715
- fi
716
- fi
717
-
718
- echo "Deploy complete."
719
- `;
720
-
721
- return script;
722
- }
723
-
724
- /**
725
- * Generate a Dockerfile for the deployment.
726
- *
727
- * @param {Object} manifest - Validated manifest
728
- * @param {Object} serviceConfigs - Base service configs
729
- * @returns {string} Dockerfile content
730
- */
731
- export function generateDockerfile(manifest, serviceConfigs) {
732
- // Find the first edge service port, or fall back to httpBasePort
733
- let httpPort = manifest.httpBasePort ?? 4000;
734
- for (const svc of Object.values(serviceConfigs)) {
735
- if (svc.type === "edge" && svc.port) {
736
- httpPort = svc.port;
737
- break;
738
- }
739
- }
740
-
741
- return `FROM node:20.11-slim AS base
742
- RUN apt-get update && apt-get install -y tini && rm -rf /var/lib/apt/lists/*
743
- WORKDIR /app
744
- COPY package*.json ./
745
- RUN npm ci --omit=dev
746
- COPY . .
747
- ENV NODE_ENV=production
748
- EXPOSE ${httpPort}
749
- ENTRYPOINT ["/usr/bin/tini", "--"]
750
- CMD ["node", "bin/forge.js", "start"]
751
- `;
752
- }
753
-
754
- /**
755
- * Generate a .dockerignore file.
756
- *
757
- * @returns {string} .dockerignore content
758
- */
759
- export function generateDockerignore() {
760
- return `.git
761
- node_modules
762
- forgeproxy/target
763
- .env
764
- secrets/
765
- *.log
766
- deploy/
767
- `;
768
- }
769
-
770
- /**
771
- * Generate all deployment artifacts from a manifest.
772
- *
773
- * @param {Object} manifest
774
- * @param {Object} serviceConfigs
775
- * @param {string} outputDir
776
- */
777
- export function generateAll(manifest, serviceConfigs, outputDir) {
778
- const validated = loadManifest(manifest, serviceConfigs);
779
-
780
- // Per-node configs
781
- const nodeConfigs = generateNodeConfigs(validated, serviceConfigs);
782
- for (const [nodeName, config] of nodeConfigs) {
783
- const dir = path.join(outputDir, nodeName);
784
- fs.mkdirSync(dir, { recursive: true });
785
- fs.writeFileSync(path.join(dir, "forge.config.js"), config);
786
- }
787
-
788
- // Docker Compose
789
- const compose = generateDockerCompose(validated, serviceConfigs);
790
- fs.writeFileSync(path.join(outputDir, "docker-compose.yml"), compose);
791
-
792
- // Compute endpoint map for systemd env files using shared helper
793
- const httpBasePort = validated.httpBasePort ?? 4000;
794
- const servicePorts = _assignPorts(validated, serviceConfigs, httpBasePort);
795
-
796
- // Systemd units + env files
797
- for (const [nodeName, node] of Object.entries(validated.nodes)) {
798
- // Build endpoint map for this node: remote services → host:port
799
- const endpointMap = {};
800
- for (const [svcName, endpoints] of Object.entries(servicePorts)) {
801
- if (!node.services.includes(svcName)) {
802
- endpointMap[svcName] = endpoints.map(e => ({ host: e.host, port: Number(e.port), remote: true }));
803
- }
804
- }
805
- const nodeWithEndpoints = { ...node, endpoints: endpointMap };
806
-
807
- const unit = generateSystemdUnit(nodeName, nodeWithEndpoints, validated);
808
- fs.writeFileSync(path.join(outputDir, `forge-${nodeName}.service`), unit);
809
-
810
- const envFile = generateSystemdEnvFile(nodeName, nodeWithEndpoints);
811
- fs.writeFileSync(path.join(outputDir, `${nodeName}.env`), envFile.content);
812
- }
813
-
814
- // Deploy script
815
- const deployScript = generateDeployScript(validated);
816
- const scriptPath = path.join(outputDir, "deploy.sh");
817
- fs.writeFileSync(scriptPath, deployScript);
818
- fs.chmodSync(scriptPath, "755");
819
-
820
- // Nginx config — routes directly to edge services, no gateway
821
- const nginxServices = buildNginxServiceMap(validated, serviceConfigs);
822
- const websocketRoutes = buildNginxWebSocketRoutes(serviceConfigs, nginxServices, {
823
- websockets: manifest.websockets ?? false,
824
- websocketPaths: manifest.websocketPaths ?? ["/ws"],
825
- });
826
- const nginxConfig = generateNginxConfig({
827
- domain: manifest.domain ?? "localhost",
828
- services: nginxServices,
829
- ssl: manifest.ssl,
830
- rateLimits: manifest.rateLimits ?? {},
831
- staticDir: manifest.staticDir,
832
- websocketRoutes,
833
- // Backward compatibility for older manifests that don't define service.websocket
834
- websockets: manifest.websockets ?? false,
835
- websocketPaths: manifest.websocketPaths ?? ["/ws"],
836
- maxBodySize: manifest.maxBodySize ?? 10,
837
- });
838
- fs.writeFileSync(path.join(outputDir, "nginx.conf"), nginxConfig);
839
-
840
- // C1: Generate Dockerfile
841
- const dockerfile = generateDockerfile(manifest, serviceConfigs);
842
- fs.writeFileSync(path.join(outputDir, "Dockerfile"), dockerfile);
843
-
844
- // H4: Generate .dockerignore
845
- const dockerignore = generateDockerignore();
846
- fs.writeFileSync(path.join(outputDir, ".dockerignore"), dockerignore);
847
-
848
- // C-DEPLOY-1: Generate .env.example template for secrets management
849
- const envExample = `# ThreadForge Deployment Secrets — DO NOT COMMIT TO VERSION CONTROL
850
- # Copy to .env and fill in values
851
- DATABASE_URL=
852
- REDIS_URL=
853
- JWT_SECRET=CHANGE_ME_BEFORE_DEPLOY
854
- NODE_ENV=production
855
- FORGE_METRICS_PORT=9090
856
- FORGE_METRICS_BIND=127.0.0.1
857
- FORGE_CLUSTER_SECRET=
858
- FORGE_LOG_LEVEL=info
859
- `;
860
- fs.writeFileSync(path.join(outputDir, ".env.example"), envExample);
861
-
862
- // Generate .gitignore to protect .env from accidental commits
863
- const gitignore = `# Secrets — never commit
864
- .env
865
- `;
866
- fs.writeFileSync(path.join(outputDir, ".gitignore"), gitignore);
867
-
868
- return {
869
- nodes: [...nodeConfigs.keys()],
870
- files: [
871
- ...[...nodeConfigs.keys()].map((n) => `${n}/forge.config.js`),
872
- "docker-compose.yml",
873
- "nginx.conf",
874
- ...Object.keys(validated.nodes).map((n) => `forge-${n}.service`),
875
- ...Object.keys(validated.nodes).map((n) => `${n}.env`),
876
- "deploy.sh",
877
- "Dockerfile",
878
- ".dockerignore",
879
- ".env.example",
880
- ".gitignore",
881
- ],
882
- };
883
- }
884
-
885
- /**
886
- * Generate all platform deployment artifacts.
887
- *
888
- * @param {Object} platformConfig - The platform block from the user config
889
- * @param {Object} services - Flat namespaced service map from resolveHostConfig
890
- * @param {Object} hostMeta - From resolveHostConfig().hostMeta
891
- * @param {string} outputDir - Directory to write artifacts to
892
- * @param {Object} [options]
893
- * @param {Object} [options.sites] - Resolved site map from resolveHostConfig().sites
894
- * @param {{sites?: Array, mounts?: Object}} [options.frontendManifest] - Output from forge build
895
- * @returns {Object} { files: string[] }
896
- */
897
- export function generatePlatformAll(platformConfig, services, hostMeta, outputDir, options = {}) {
898
- const deployDir = path.join(outputDir, "deploy");
899
- const routesDir = path.join(outputDir, "routes");
900
- fs.mkdirSync(deployDir, { recursive: true });
901
- fs.mkdirSync(routesDir, { recursive: true });
902
-
903
- const files = [];
904
- const sites = options.sites ?? {};
905
-
906
- /** @type {Record<string, string>} */
907
- const builtOutDirBySite = {};
908
- if (Array.isArray(options.frontendManifest?.sites)) {
909
- for (const site of options.frontendManifest.sites) {
910
- if (site?.siteId && site?.outDir) {
911
- builtOutDirBySite[site.siteId] = site.outDir;
912
- }
913
- }
914
- }
915
-
916
- /** @type {Record<string, any[]>} */
917
- const mountsFromManifest = {};
918
- if (options.frontendManifest?.mounts && typeof options.frontendManifest.mounts === "object") {
919
- for (const [siteId, mounts] of Object.entries(options.frontendManifest.mounts)) {
920
- if (Array.isArray(mounts)) mountsFromManifest[siteId] = mounts;
921
- }
922
- }
923
-
924
- const apps = {};
925
- for (const [appId, app] of Object.entries(platformConfig.apps ?? {})) {
926
- const site = sites?.[appId];
927
- const frontend = site?.frontend ? { ...site.frontend } : (app.frontend ? { ...app.frontend } : null);
928
- if (frontend && builtOutDirBySite[appId]) {
929
- frontend.outDir = builtOutDirBySite[appId];
930
- }
931
-
932
- const staticMounts = mountsFromManifest[appId]
933
- ? mountsFromManifest[appId]
934
- : (() => {
935
- if (frontend) {
936
- return [{
937
- siteId: appId,
938
- domains: site?.domains ?? resolveAppDomains(app),
939
- basePath: frontend.basePath ?? "/",
940
- dir: frontend.outDir,
941
- spaFallback: frontend.spaFallback ?? true,
942
- cachePolicy: "short",
943
- }];
944
- }
945
- const legacyStatic = site?.staticDir ?? app.static ?? null;
946
- if (!legacyStatic) return [];
947
- return [{
948
- siteId: appId,
949
- domains: site?.domains ?? resolveAppDomains(app),
950
- basePath: "/static",
951
- dir: legacyStatic,
952
- spaFallback: false,
953
- cachePolicy: "short",
954
- }];
955
- })();
956
-
957
- apps[appId] = {
958
- domains: resolveAppDomains(app),
959
- ssl: app.ssl ?? null,
960
- static: site?.staticDir ?? app.static ?? null,
961
- frontend,
962
- staticMounts,
963
- services: app.services ?? [],
964
- };
965
- }
966
-
967
- // Generate platform nginx config
968
- const nginxConfig = generatePlatformNginxConfig({
969
- apps,
970
- services,
971
- hostMeta,
972
- defaultSsl: platformConfig.ssl ?? null,
973
- maxBodySize: platformConfig.maxBodySize ?? 10,
974
- });
975
- fs.writeFileSync(path.join(deployDir, "nginx.conf"), nginxConfig);
976
- files.push("deploy/nginx.conf");
977
-
978
- // Generate platform.yaml manifest
979
- const manifest = generatePlatformManifest(platformConfig, hostMeta, sites);
980
- fs.writeFileSync(path.join(routesDir, "platform.yaml"), manifest);
981
- files.push("routes/platform.yaml");
982
-
983
- return { files };
984
- }