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
package/bin/forge.js CHANGED
@@ -1,1059 +1,3 @@
1
1
  #!/usr/bin/env node
2
-
3
- /**
4
- * ThreadForge CLI
5
- *
6
- * Commands:
7
- * forge dev - Start in development mode with hot reload
8
- * forge start - Start in production mode
9
- * forge build - Build frontend sites via frontend plugins
10
- * forge stop - Stop a running supervisor (SIGTERM)
11
- * forge init [dir] - Scaffold a new ThreadForge project
12
- * forge status - Show runtime status (connects to metrics endpoint)
13
- *
14
- * TODO: implement `forge scale <svc> <n>` and `forge restart <svc>` commands
15
- */
16
-
17
- import fs from "node:fs";
18
- import path from "node:path";
19
- import { fileURLToPath, pathToFileURL } from "node:url";
20
- import { loadConfig } from "../src/core/config.js";
21
- import { Supervisor } from "../src/core/Supervisor.js";
22
- import { bindFrontendDevCleanup, wrapShutdownWithCleanup } from "../src/frontend/FrontendDevLifecycle.js";
23
- import { resolveFrontendSites, resolveSitesMap } from "../src/frontend/SiteResolver.js";
24
-
25
- const args = process.argv.slice(2);
26
- const command = args[0];
27
-
28
- const FORGE_CONFIG_NAMES = ["forge.config.js", "forge.config.mjs", "threadforge.config.js", "threadforge.config.mjs"];
29
- const FORGE_PLATFORM_CONFIG_NAMES = ["forge.platform.js", "forge.platform.mjs", "forge.host.js", "forge.host.mjs"];
30
-
31
- function resolveConfigSearchNames(commandName = command) {
32
- if (["dev", "start", "build", "status", "stop"].includes(commandName)) {
33
- return [...FORGE_CONFIG_NAMES, ...FORGE_PLATFORM_CONFIG_NAMES];
34
- }
35
- return [...FORGE_CONFIG_NAMES];
36
- }
37
-
38
- function printNoConfigError(commandName = command) {
39
- const names = resolveConfigSearchNames(commandName);
40
- console.error(" Error: No config file found in current directory.");
41
- console.error(` Searched for: ${names.join(", ")}`);
42
- console.error(" Run `forge init` to create one, or use `--config <path>` to specify manually.");
43
- }
44
-
45
- async function findConfig(commandName = command) {
46
- const searchNames = resolveConfigSearchNames(commandName);
47
- const cwd = process.cwd();
48
-
49
- // Check for --config flag
50
- const configIdx = args.indexOf("--config");
51
- if (configIdx !== -1 && args[configIdx + 1]) {
52
- const resolved = path.resolve(cwd, args[configIdx + 1]);
53
- if (resolved !== cwd && !resolved.startsWith(cwd + path.sep)) {
54
- console.error("Error: Config path must be within the project directory");
55
- process.exit(1);
56
- }
57
- return resolved;
58
- }
59
-
60
- for (const name of searchNames) {
61
- const fullPath = path.join(cwd, name);
62
- try {
63
- await fs.promises.access(fullPath);
64
- return fullPath;
65
- } catch {}
66
- }
67
-
68
- return null;
69
- }
70
-
71
- async function stopFrontendDevHandle(handle) {
72
- if (!handle) return;
73
- if (typeof handle === "function") {
74
- await handle();
75
- return;
76
- }
77
-
78
- for (const method of ["dispose", "stop", "close", "kill"]) {
79
- if (typeof handle[method] === "function") {
80
- await handle[method]();
81
- return;
82
- }
83
- }
84
-
85
- if (handle.process?.pid && typeof handle.process.kill === "function") {
86
- handle.process.kill("SIGTERM");
87
- }
88
- }
89
-
90
- async function startFrontendDevSessions(config) {
91
- const frontendSites = resolveFrontendSites(config);
92
- if (frontendSites.length === 0) {
93
- return null;
94
- }
95
-
96
- const { FrontendPluginOrchestrator } = await import("../src/frontend/FrontendPluginOrchestrator.js");
97
- const orchestrator = new FrontendPluginOrchestrator();
98
- const frontendPlugins = config.frontendPlugins ?? [];
99
-
100
- orchestrator.register(frontendPlugins);
101
- await orchestrator.validateAll({ logger: console, mode: "dev", config });
102
- await orchestrator.registerAll({ logger: console, mode: "dev", config });
103
-
104
- if (orchestrator.plugins.size === 0) {
105
- throw new Error(
106
- "Frontend sites are configured, but no frontendPlugins are registered in config.",
107
- );
108
- }
109
-
110
- const handles = [];
111
- let disposed = false;
112
- const cleanup = async () => {
113
- if (disposed) return;
114
- disposed = true;
115
- for (const session of [...handles].reverse()) {
116
- try {
117
- await stopFrontendDevHandle(session.handle);
118
- } catch (err) {
119
- console.warn(` [dev] Failed to stop frontend session "${session.siteId}": ${err.message}`);
120
- }
121
- }
122
- await orchestrator.disposeAll({ logger: console, mode: "dev", config });
123
- };
124
-
125
- let started = 0;
126
- try {
127
- for (const site of frontendSites) {
128
- const plugin = orchestrator.resolvePluginForSite(site);
129
- if (!plugin.capabilities.includes("frontend-dev")) {
130
- console.log(` [dev] Skipping frontend site "${site.siteId}" (${plugin.name} has no frontend-dev capability)`);
131
- continue;
132
- }
133
-
134
- const handle = await orchestrator.devSite(site, { logger: console, mode: "dev", config });
135
- handles.push({ siteId: site.siteId, plugin: plugin.name, handle });
136
- started++;
137
- }
138
- } catch (err) {
139
- await cleanup();
140
- throw err;
141
- }
142
-
143
- if (started > 0) {
144
- console.log(` [dev] Frontend dev sessions started: ${started}`);
145
- }
146
-
147
- return { cleanup };
148
- }
149
-
150
- async function cmdStart(isDev = false) {
151
- const configPath = await findConfig(isDev ? "dev" : "start");
152
- if (!configPath) {
153
- printNoConfigError(isDev ? "dev" : "start");
154
- process.exit(1);
155
- }
156
-
157
- let config;
158
- try {
159
- const configUrl = pathToFileURL(configPath).href;
160
- config = await loadConfig(configUrl);
161
- config._configUrl = configUrl; // workers reimport this for plugins
162
- } catch (err) {
163
- console.error(`\n \u2717 Error loading ${path.basename(configPath)}:`);
164
- console.error(` ${err.message}`);
165
- if (err.code === "ERR_MODULE_NOT_FOUND") {
166
- console.error(" Hint: Check your import paths and run `npm install` to ensure dependencies are installed.");
167
- }
168
- process.exit(1);
169
- }
170
-
171
- if (isDev) {
172
- config.watch = true;
173
- }
174
-
175
- const fileWatchers = [];
176
- const closeDevWatchers = async () => {
177
- for (const watcher of fileWatchers.splice(0)) {
178
- try {
179
- watcher.close();
180
- } catch {}
181
- }
182
- };
183
-
184
- let frontendDev = null;
185
- if (isDev) {
186
- try {
187
- frontendDev = await startFrontendDevSessions(config);
188
- } catch (err) {
189
- console.error(` Frontend dev setup failed: ${err.message}`);
190
- process.exit(1);
191
- }
192
- }
193
-
194
- // D11: Warn when `connects` targets undefined services
195
- if (config.services) {
196
- const serviceNames = new Set(Object.keys(config.services));
197
- for (const [name, svc] of Object.entries(config.services)) {
198
- if (Array.isArray(svc.connects)) {
199
- for (const target of svc.connects) {
200
- if (!serviceNames.has(target)) {
201
- console.warn(` Warning: service "${name}" connects to "${target}", which is not defined in the config.`);
202
- }
203
- }
204
- }
205
- }
206
- }
207
-
208
- const supervisor = new Supervisor(config);
209
- const devLifecycle = isDev
210
- ? bindFrontendDevCleanup(
211
- async () => {
212
- await closeDevWatchers();
213
- if (frontendDev?.cleanup) {
214
- await frontendDev.cleanup();
215
- }
216
- },
217
- { logger: console },
218
- )
219
- : null;
220
- if (devLifecycle) {
221
- wrapShutdownWithCleanup(supervisor, devLifecycle.runCleanup);
222
- }
223
- try {
224
- await supervisor.start();
225
- } catch (err) {
226
- if (devLifecycle) {
227
- await devLifecycle.runCleanup();
228
- devLifecycle.dispose();
229
- }
230
- console.error(`\n \u2717 Startup failed:`);
231
- console.error(` ${err.message}`);
232
- process.exit(1);
233
- }
234
-
235
- // D15: In dev mode, watch for file changes and log them
236
- if (isDev) {
237
- const watchDirs = new Set();
238
- for (const svc of Object.values(config.services)) {
239
- if (svc.entry) {
240
- const entryDir = path.dirname(path.resolve(process.cwd(), svc.entry));
241
- watchDirs.add(entryDir);
242
- }
243
- }
244
- for (const dir of watchDirs) {
245
- try {
246
- const watcher = fs.watch(dir, { recursive: true }, (eventType, filename) => {
247
- if (filename && (filename.endsWith('.js') || filename.endsWith('.mjs') || filename.endsWith('.ts'))) {
248
- console.log(` [dev] File change detected: ${filename} (${eventType})`);
249
- }
250
- });
251
- fileWatchers.push(watcher);
252
- // Prevent dev file-watch handles from blocking process exit during shutdown.
253
- if (typeof watcher.unref === "function") watcher.unref();
254
- } catch {
255
- // Directory may not exist or be unwatchable — skip silently
256
- }
257
- }
258
- }
259
- }
260
-
261
- function detectLocalThreadforgeDependency(cwd) {
262
- const linkedPackagePath = path.join(cwd, "node_modules", "threadforge");
263
- try {
264
- const stat = fs.lstatSync(linkedPackagePath);
265
- if (!stat.isSymbolicLink()) return null;
266
-
267
- const realPath = fs.realpathSync(linkedPackagePath);
268
- const relativePath = path.relative(cwd, realPath) || ".";
269
- const normalized = (relativePath.startsWith(".") ? relativePath : `./${relativePath}`).split(path.sep).join("/");
270
- return `file:${normalized}`;
271
- } catch {
272
- return null;
273
- }
274
- }
275
-
276
- function resolveInitDependency(cwd, source = "auto") {
277
- if (source === "npm") {
278
- // Use npm dist-tag so generated package.json is directly installable.
279
- return { spec: "latest", label: "npm" };
280
- }
281
-
282
- if (source === "github") {
283
- return { spec: "github:ChrisBland/threadforge", label: "github" };
284
- }
285
-
286
- if (source === "local" || source === "auto") {
287
- const localSpec = detectLocalThreadforgeDependency(cwd);
288
- if (localSpec) {
289
- return { spec: localSpec, label: "local-link" };
290
- }
291
- if (source === "local") {
292
- throw new Error('No linked local ThreadForge found at ./node_modules/threadforge. Run "npm link threadforge" first.');
293
- }
294
- }
295
-
296
- return { spec: "github:ChrisBland/threadforge", label: "github" };
297
- }
298
-
299
- function parseInitArgs(initArgs = []) {
300
- let targetDir = null;
301
- let source = "auto";
302
-
303
- for (let i = 0; i < initArgs.length; i++) {
304
- const token = initArgs[i];
305
-
306
- if (token === "--source") {
307
- const value = initArgs[i + 1];
308
- if (!value) {
309
- console.error(" Error: --source requires a value: auto, github, local, npm");
310
- process.exit(1);
311
- }
312
- source = value.toLowerCase();
313
- i++;
314
- continue;
315
- }
316
-
317
- if (token.startsWith("--source=")) {
318
- source = token.slice("--source=".length).toLowerCase();
319
- continue;
320
- }
321
-
322
- if (token === "--local") {
323
- source = "local";
324
- continue;
325
- }
326
-
327
- if (token === "--help" || token === "-h") {
328
- console.log("\n Usage: forge init [dir] [--source auto|github|local|npm]\n");
329
- process.exit(0);
330
- }
331
-
332
- if (token.startsWith("-")) {
333
- console.error(` Unknown init option: ${token}`);
334
- process.exit(1);
335
- }
336
-
337
- if (targetDir !== null) {
338
- console.error(` Unexpected argument: ${token}`);
339
- console.error(" Usage: forge init [dir] [--source auto|github|local|npm]");
340
- process.exit(1);
341
- }
342
-
343
- targetDir = token;
344
- }
345
-
346
- if (!["auto", "github", "local", "npm"].includes(source)) {
347
- console.error(` Invalid --source value: ${source}`);
348
- console.error(" Valid values: auto, github, local, npm");
349
- process.exit(1);
350
- }
351
-
352
- return { targetDir, source };
353
- }
354
-
355
- async function cmdInit(targetDir, options = {}) {
356
- let cwd = process.cwd();
357
- const source = options.source ?? "auto";
358
-
359
- if (targetDir) {
360
- cwd = path.resolve(cwd, targetDir);
361
- fs.mkdirSync(cwd, { recursive: true });
362
- }
363
-
364
- const configPath = path.join(cwd, "forge.config.js");
365
-
366
- if (fs.existsSync(configPath)) {
367
- console.error(" forge.config.js already exists in this directory.");
368
- process.exit(1);
369
- }
370
-
371
- let dependency;
372
- let dependencySourceLabel;
373
- try {
374
- const resolved = resolveInitDependency(cwd, source);
375
- dependency = resolved.spec;
376
- dependencySourceLabel = resolved.label;
377
- } catch (err) {
378
- console.error(` Error: ${err.message}`);
379
- process.exit(1);
380
- }
381
-
382
- // Create package.json if it doesn't exist
383
- const pkgPath = path.join(cwd, "package.json");
384
- if (!fs.existsSync(pkgPath)) {
385
- const projectName = path.basename(cwd);
386
- fs.writeFileSync(
387
- pkgPath,
388
- `${JSON.stringify(
389
- {
390
- name: projectName,
391
- version: "1.0.0",
392
- type: "module",
393
- scripts: {
394
- dev: "forge dev",
395
- start: "forge start",
396
- },
397
- dependencies: {
398
- threadforge: dependency,
399
- },
400
- },
401
- null,
402
- 2,
403
- )}\n`,
404
- );
405
- }
406
-
407
- // Always use package name for imports to ensure generated code works after npm install
408
- const configImportPath = "threadforge";
409
- const serviceImportPath = "threadforge";
410
-
411
- // Create project structure
412
- const dirs = ["services"];
413
- for (const dir of dirs) {
414
- fs.mkdirSync(path.join(cwd, dir), { recursive: true });
415
- }
416
-
417
- // Write config file
418
- fs.writeFileSync(
419
- configPath,
420
- `import { defineServices } from '${configImportPath}';
421
-
422
- export default defineServices({
423
- api: {
424
- entry: './services/api.js',
425
- type: 'edge',
426
- port: 3000,
427
- threads: 'auto',
428
- weight: 2,
429
- // connects: ['otherService'], // add services this one calls
430
- },
431
- });
432
- `,
433
- );
434
-
435
- // Write example service
436
- fs.writeFileSync(
437
- path.join(cwd, "services", "api.js"),
438
- `import { Service } from '${serviceImportPath}';
439
-
440
- export default class ApiService extends Service {
441
- static contract = {
442
- expose: ['getGreeting'],
443
- routes: [
444
- { method: 'GET', path: '/health', handler: 'healthCheck' },
445
- { method: 'GET', path: '/hello/:name', handler: 'hello' },
446
- ],
447
- };
448
-
449
- async onStart(ctx) {
450
- if (ctx.workerId === 0) {
451
- ctx.logger.info('API service ready');
452
- }
453
- }
454
-
455
- async healthCheck() {
456
- return { status: 'ok', worker: this.ctx.workerId };
457
- }
458
-
459
- async hello(body, params) {
460
- return { message: \`Hello, \${params.name}!\`, worker: this.ctx.workerId };
461
- }
462
-
463
- async getGreeting(name) {
464
- return { message: \`Hello, \${name}!\` };
465
- }
466
-
467
- async onMessage(from, payload) {
468
- this.ctx.logger.info(\`Message from \${from}\`, payload);
469
- }
470
- }
471
- `,
472
- );
473
-
474
- const shouldSuggestCd = Boolean(targetDir) && path.resolve(process.cwd(), targetDir) !== process.cwd();
475
- const cdStep = shouldSuggestCd ? `cd "${path.basename(cwd)}" && ` : "";
476
- const usingLocalLink = dependencySourceLabel === "local-link";
477
- console.log(`
478
- \u26A1 ThreadForge project initialized!
479
-
480
- Created:
481
- forge.config.js - Service configuration
482
- services/api.js - Example API service
483
-
484
- Dependency source: ${dependency}
485
-
486
- Next steps:
487
- 1. ${cdStep}npm install
488
- 2. npm run dev
489
- 3. curl http://localhost:3000/health
490
-
491
- ${usingLocalLink ? "" : "Tip: for local linked development, run `forge init [dir] --source local` after `npm link threadforge`."}
492
- `);
493
- }
494
-
495
- async function resolveMetricsPort() {
496
- const config = await findConfig(command).then(async (p) => {
497
- if (!p) return null;
498
- try { return await loadConfig(pathToFileURL(p).href); } catch { return null; }
499
- });
500
- return config?.metricsPort ?? parseInt(process.env.FORGE_METRICS_PORT || "9090", 10);
501
- }
502
-
503
- async function cmdStatus() {
504
- const metricsPort = await resolveMetricsPort();
505
- try {
506
- const res = await fetch(`http://localhost:${metricsPort}/status`);
507
- const data = await res.json();
508
-
509
- // M-CLI-2: --json flag outputs raw JSON and exits
510
- if (args.includes("--json")) {
511
- console.log(JSON.stringify(data, null, 2));
512
- return;
513
- }
514
-
515
- console.log("");
516
- console.log(" \u26A1 ThreadForge Status");
517
- console.log(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
518
- console.log(` Uptime: ${Math.floor(data.uptime)}s`);
519
- console.log("");
520
-
521
- console.log(" \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\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\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510");
522
- console.log(" \u2502 Group \u2502 Services \u2502 Port \u2502 Workers \u2502 PIDs \u2502");
523
- console.log(" \u251C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\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\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524");
524
-
525
- for (const pg of data.processGroups) {
526
- const group = pg.group.replace("_isolated:", "").padEnd(16);
527
- const svcNames = pg.services.map((s) => s.name).join(", ");
528
- const services = svcNames.substring(0, 25).padEnd(25);
529
- const edgeService = pg.services.find((s) => s.type === "edge");
530
- const port = edgeService ? String(edgeService.port).padEnd(5) : " \u2014 ";
531
- const workers = String(pg.workers).padEnd(7);
532
- const pidList = pg.pids.join(", ");
533
- const pids = (pidList.length > 16 ? `${pg.pids.length} workers` : pidList).padEnd(16);
534
- console.log(` \u2502 ${group} \u2502 ${services} \u2502 ${port} \u2502 ${workers} \u2502 ${pids} \u2502`);
535
- }
536
-
537
- console.log(" \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\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\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518");
538
- console.log(` CPUs: ${data.totalCpus} total`);
539
- console.log("");
540
- } catch (_err) {
541
- console.error(" Could not connect to ThreadForge runtime.");
542
- console.error("");
543
- console.error(" Checklist:");
544
- console.error(" - Is the runtime running? (forge start / forge dev)");
545
- console.error(` - Is the metrics port correct? (currently ${metricsPort})`);
546
- console.error(" - Is a firewall blocking the connection?");
547
- process.exit(1);
548
- }
549
- }
550
-
551
- function isProcessAlive(pid) {
552
- try {
553
- process.kill(pid, 0);
554
- return true;
555
- } catch (err) {
556
- if (err.code === "ESRCH") return false;
557
- if (err.code === "EPERM") return true;
558
- throw err;
559
- }
560
- }
561
-
562
- async function cmdStop() {
563
- let pid = null;
564
-
565
- // H4: Try reading PID from .forge.pid first
566
- const pidFilePath = path.join(process.cwd(), ".forge.pid");
567
- try {
568
- const pidStr = fs.readFileSync(pidFilePath, "utf8").trim();
569
- const filePid = Number(pidStr);
570
- if (Number.isInteger(filePid) && filePid > 1 && isProcessAlive(filePid)) {
571
- pid = filePid;
572
- }
573
- } catch {}
574
-
575
- // Fall back to metrics endpoint if PID file is missing or stale
576
- if (!pid) {
577
- const metricsPort = await resolveMetricsPort();
578
- let data;
579
- try {
580
- const res = await fetch(`http://localhost:${metricsPort}/status`);
581
- if (!res.ok) throw new Error(`status endpoint returned ${res.status}`);
582
- data = await res.json();
583
- } catch (_err) {
584
- console.error(" Could not connect to ThreadForge runtime.");
585
- console.error(" Ensure it is running and the metrics port is correct.");
586
- process.exit(1);
587
- }
588
-
589
- pid = Number(data?.supervisorPid);
590
- if (!Number.isInteger(pid) || pid < 2) {
591
- console.error(" Could not determine supervisor PID from /status.");
592
- process.exit(1);
593
- }
594
- }
595
- if (pid === process.pid) {
596
- console.error(" Refusing to signal current CLI process.");
597
- process.exit(1);
598
- }
599
-
600
- try {
601
- process.kill(pid, "SIGTERM");
602
- } catch (err) {
603
- if (err.code === "ESRCH") {
604
- console.log(` Runtime already stopped (PID ${pid} not found).`);
605
- return;
606
- }
607
- if (err.code === "EPERM") {
608
- console.error(` Permission denied stopping PID ${pid}.`);
609
- process.exit(1);
610
- }
611
- throw err;
612
- }
613
-
614
- const waitMs = 15000;
615
- const deadline = Date.now() + waitMs;
616
- while (Date.now() < deadline) {
617
- if (!isProcessAlive(pid)) {
618
- console.log(` \u2713 Stopped ThreadForge supervisor (PID ${pid}).`);
619
- return;
620
- }
621
- await new Promise((resolve) => setTimeout(resolve, 100));
622
- }
623
-
624
- // Final short settle window to reduce false "still draining" reports
625
- // when the process exits right at the timeout boundary.
626
- const settleMs = 1000;
627
- const settleDeadline = Date.now() + settleMs;
628
- while (Date.now() < settleDeadline) {
629
- if (!isProcessAlive(pid)) {
630
- console.log(` \u2713 Stopped ThreadForge supervisor (PID ${pid}).`);
631
- return;
632
- }
633
- await new Promise((resolve) => setTimeout(resolve, 100));
634
- }
635
-
636
- console.log(` Shutdown in progress for supervisor PID ${pid} (waited ${Math.floor(waitMs / 1000)}s).`);
637
- console.log(" It may still be draining (or may have exited moments ago). Run `forge status` to confirm.");
638
- }
639
-
640
- function printHelp() {
641
- console.log(`
642
- \u26A1 ThreadForge CLI
643
-
644
- Usage: forge <command> [options]
645
-
646
- Commands:
647
- init [dir] Scaffold a new ThreadForge project
648
- dev Start in development mode
649
- start Start in production mode
650
- build Build frontend sites via frontend plugins
651
- stop Stop a running ThreadForge supervisor
652
- status Show runtime status
653
- generate Generate route manifests for ForgeProxy
654
- deploy Generate multi-machine deployment artifacts
655
- scale <svc> <n> Scale a service (not yet implemented)
656
- restart <svc> Restart a service (not yet implemented)
657
- host <subcommand> Multi-project hosting (init, start, status, ...)
658
- platform <subcommand> Platform mode (init, start, add, generate, ...)
659
-
660
- Options:
661
- --config <path> Path to config file (default: forge.config.js)
662
- --source <mode> Init dependency source: auto|github|local|npm
663
- --local Shorthand for: forge init --source local
664
- --version, -v Show version number
665
- --help Show this help message
666
-
667
- Examples:
668
- forge init
669
- forge init my-app
670
- forge init . --source local
671
- forge dev
672
- forge start --config ./my-config.js
673
- forge build
674
- forge stop
675
- forge generate
676
- forge status
677
- `);
678
- }
679
-
680
- async function cmdBuild() {
681
- const configPath = await findConfig("build");
682
- if (!configPath) {
683
- printNoConfigError("build");
684
- process.exit(1);
685
- }
686
-
687
- let config;
688
- try {
689
- const configUrl = pathToFileURL(configPath).href;
690
- config = await loadConfig(configUrl);
691
- } catch (err) {
692
- console.error(`\n ✗ Error loading ${path.basename(configPath)}:`);
693
- console.error(` ${err.message}`);
694
- process.exit(1);
695
- }
696
-
697
- const sitesMap = resolveSitesMap(config);
698
- const frontendSites = Object.values(sitesMap).filter((site) => site?.frontend);
699
- if (frontendSites.length === 0) {
700
- console.log(" No frontend sites configured. Nothing to build.");
701
- return;
702
- }
703
-
704
- const { FrontendPluginOrchestrator } = await import("../src/frontend/FrontendPluginOrchestrator.js");
705
- const orchestrator = new FrontendPluginOrchestrator();
706
- const frontendPlugins = config.frontendPlugins ?? [];
707
-
708
- try {
709
- orchestrator.register(frontendPlugins);
710
- await orchestrator.validateAll({ logger: console });
711
- await orchestrator.registerAll({ logger: console });
712
- } catch (err) {
713
- console.error(` Frontend plugin setup failed: ${err.message}`);
714
- process.exit(1);
715
- }
716
-
717
- if (orchestrator.plugins.size === 0) {
718
- console.error(" Frontend sites are configured, but no frontendPlugins are registered in config.");
719
- process.exit(1);
720
- }
721
-
722
- const results = [];
723
- const mountsBySite = {};
724
- let failures = 0;
725
-
726
- try {
727
- console.log(`\n ⚡ Building frontend sites (${frontendSites.length})\n`);
728
-
729
- for (const site of frontendSites) {
730
- const pluginName = site.frontend.plugin;
731
- try {
732
- console.log(` • ${site.siteId} (${pluginName})`);
733
- const result = await orchestrator.buildSite(site, { logger: console });
734
- const mounts = await orchestrator.staticMounts(site, { logger: console });
735
- mountsBySite[site.siteId] = mounts;
736
- results.push({
737
- siteId: site.siteId,
738
- plugin: pluginName,
739
- outDir: result?.outDir ?? site.frontend.outDir,
740
- });
741
- console.log(` ✓ ${result?.outDir ?? site.frontend.outDir}`);
742
- } catch (err) {
743
- failures++;
744
- console.error(` ✗ ${err.message}`);
745
- }
746
- }
747
- } finally {
748
- await orchestrator.disposeAll({ logger: console });
749
- }
750
-
751
- const outputDir = path.join(process.cwd(), ".threadforge");
752
- fs.mkdirSync(outputDir, { recursive: true });
753
- const manifestPath = path.join(outputDir, "frontend-manifest.json");
754
- fs.writeFileSync(
755
- manifestPath,
756
- JSON.stringify(
757
- {
758
- generatedAt: new Date().toISOString(),
759
- sites: results,
760
- mounts: mountsBySite,
761
- },
762
- null,
763
- 2,
764
- ),
765
- );
766
-
767
- console.log(`\n Wrote frontend manifest: ${manifestPath}`);
768
- if (failures > 0) {
769
- console.error(` Frontend build completed with ${failures} failure(s).`);
770
- process.exit(1);
771
- }
772
- }
773
-
774
- async function cmdDeploy() {
775
- const configPath = await findConfig("deploy");
776
- if (!configPath) {
777
- printNoConfigError("deploy");
778
- process.exit(1);
779
- }
780
-
781
- // Look for forge.deploy.js
782
- const deployPath = path.join(process.cwd(), "forge.deploy.js");
783
- if (!fs.existsSync(deployPath)) {
784
- console.log(` No forge.deploy.js found. Creating a template...`);
785
- const template = generateDeployTemplate();
786
- fs.writeFileSync(deployPath, template);
787
- console.log(` Created: ${deployPath}`);
788
- console.log(` Edit the manifest with your node addresses, then run 'forge deploy' again.`);
789
- return;
790
- }
791
-
792
- const { generateAll } = await import("../src/deploy/index.js");
793
-
794
- // Load base config
795
- let config;
796
- try {
797
- const configUrl = pathToFileURL(configPath).href;
798
- config = await loadConfig(configUrl);
799
- } catch (err) {
800
- console.error(`\n \u2717 Error loading ${path.basename(configPath)}:`);
801
- console.error(` ${err.message}`);
802
- if (err.code === "ERR_MODULE_NOT_FOUND") {
803
- console.error(" Hint: Check your import paths and run `npm install` to ensure dependencies are installed.");
804
- }
805
- process.exit(1);
806
- }
807
-
808
- // Load deploy manifest
809
- let deployMod;
810
- try {
811
- const deployUrl = pathToFileURL(deployPath).href;
812
- deployMod = await import(deployUrl);
813
- } catch (err) {
814
- console.error(`\n \u2717 Error loading ${path.basename(deployPath)}:`);
815
- console.error(` ${err.message}`);
816
- process.exit(1);
817
- }
818
- const manifest = deployMod.default ?? deployMod;
819
-
820
- const outputDir = path.join(process.cwd(), "deploy");
821
- const isDryRun = args.includes("--dry-run");
822
-
823
- try {
824
- if (isDryRun) {
825
- // M-CLI-3: Dry run — validate and show what would be generated without writing
826
- const { loadManifest, generateNodeConfigs } = await import("../src/deploy/index.js");
827
- const validated = loadManifest(manifest, config.services);
828
-
829
- console.log(`\n \u26A1 Dry run — no files will be written\n`);
830
- console.log(` Nodes:`);
831
- for (const [nodeName, nodeDef] of Object.entries(manifest.nodes)) {
832
- console.log(` ${nodeName.padEnd(16)} ${nodeDef.host.padEnd(16)} [${nodeDef.services.join(", ")}]`);
833
- }
834
-
835
- const nodeNames = Object.keys(manifest.nodes);
836
- const files = [
837
- ...nodeNames.map((n) => `${n}/forge.config.js`),
838
- "docker-compose.yml",
839
- "nginx.conf",
840
- ...nodeNames.map((n) => `forge-${n}.service`),
841
- ...nodeNames.map((n) => `${n}.env`),
842
- "deploy.sh",
843
- ".env.example",
844
- ".gitignore",
845
- ];
846
-
847
- console.log(`\n Files that WOULD be generated:`);
848
- for (const file of files) {
849
- console.log(` deploy/${file}`);
850
- }
851
- return;
852
- }
853
-
854
- const result = generateAll(manifest, config.services, outputDir);
855
-
856
- console.log(`\n \u26A1 Deployment artifacts generated\n`);
857
- console.log(` Nodes:`);
858
- for (const node of result.nodes) {
859
- const nodeDef = manifest.nodes[node];
860
- console.log(` ${node.padEnd(16)} ${nodeDef.host.padEnd(16)} [${nodeDef.services.join(", ")}]`);
861
- }
862
-
863
- console.log(`\n Files:`);
864
- for (const file of result.files) {
865
- console.log(` deploy/${file}`);
866
- }
867
-
868
- console.log(`\n Next steps:`);
869
- console.log(` 1. Review the generated configs in ./deploy/`);
870
- console.log(` 2. Test locally: docker compose -f deploy/docker-compose.yml up`);
871
- console.log(` 3. Deploy: ./deploy/deploy.sh`);
872
- console.log(` 4. Rolling deploy: ./deploy/deploy.sh --rolling`);
873
- } catch (err) {
874
- console.error(` Error: ${err.message}`);
875
- process.exit(1);
876
- }
877
- }
878
-
879
- function generateDeployTemplate() {
880
- return `// forge.deploy.js \u2014 Multi-machine deployment manifest
881
- // Edit this file with your node addresses, then run: forge deploy
882
-
883
- export default {
884
- cluster: 'my-saas',
885
-
886
- nodes: {
887
- 'web-1': {
888
- host: '10.0.1.10',
889
- services: ['gateway'],
890
- role: 'edge',
891
- },
892
- 'api-1': {
893
- host: '10.0.1.20',
894
- services: ['users'],
895
- role: 'api',
896
- },
897
- 'worker-1': {
898
- host: '10.0.1.30',
899
- services: ['notifications'],
900
- role: 'worker',
901
- },
902
- },
903
-
904
- registry: 'multicast',
905
- httpBasePort: 4000,
906
- metricsPort: 9090,
907
- };
908
- `;
909
- }
910
-
911
- async function cmdGenerate() {
912
- const configPath = await findConfig("generate");
913
- if (!configPath) {
914
- printNoConfigError("generate");
915
- process.exit(1);
916
- }
917
-
918
- const { getContract } = await import("../src/decorators/index.js");
919
- const { generateRouteManifest } = await import("../src/deploy/RouteManifestGenerator.js");
920
-
921
- let config;
922
- try {
923
- const configUrl = pathToFileURL(configPath).href;
924
- config = await loadConfig(configUrl);
925
- } catch (err) {
926
- console.error(`\n \u2717 Error loading configuration:`);
927
- console.error(` ${err.message}`);
928
- process.exit(1);
929
- }
930
-
931
- // Load all service classes to read their contracts
932
- const serviceClasses = new Map();
933
- for (const [name, svc] of Object.entries(config.services)) {
934
- if (!svc.entry) continue;
935
- try {
936
- const entryPath = path.resolve(process.cwd(), svc.entry);
937
- const entryUrl = pathToFileURL(entryPath).href;
938
- const mod = await import(entryUrl);
939
- serviceClasses.set(name, mod.default ?? mod);
940
- } catch (err) {
941
- console.warn(` \u26A0 Could not load ${name}: ${err.message}`);
942
- }
943
- }
944
-
945
- // Create output directory
946
- const routesDir = path.join(process.cwd(), "routes");
947
- fs.mkdirSync(routesDir, { recursive: true });
948
-
949
- let routeManifests = 0;
950
-
951
- for (const [name, ServiceClass] of serviceClasses) {
952
- const contract = getContract(ServiceClass);
953
- if (!contract || contract.methods.size === 0) continue;
954
-
955
- // Generate route manifest for ForgeProxy
956
- const svcConfig = config.services[name] ?? {};
957
- const manifest = generateRouteManifest(name, ServiceClass, svcConfig);
958
- if (manifest) {
959
- const manifestPath = path.join(routesDir, `${name}.yaml`);
960
- fs.writeFileSync(manifestPath, manifest);
961
- routeManifests++;
962
- }
963
-
964
- console.log(` \u2713 ${name}`);
965
- if (manifest) console.log(` routes/${name}.yaml`);
966
- }
967
-
968
- if (routeManifests === 0) {
969
- console.log(" No services with contracts found. Add static contract to your services.");
970
- } else {
971
- console.log(`\n Generated ${routeManifests} route manifest(s):`);
972
- console.log(` Routes: ./routes/ (${routeManifests} manifests for ForgeProxy)`);
973
- console.log(`\n ForgeProxy reads ./routes/*.yaml automatically on startup.`);
974
- }
975
- }
976
-
977
- // -- Main --
978
-
979
- if (args.includes('--version') || args.includes('-v')) {
980
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
981
- const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
982
- console.log(`threadforge v${pkg.version}`);
983
- process.exit(0);
984
- }
985
-
986
- switch (command) {
987
- case "dev":
988
- cmdStart(true);
989
- break;
990
-
991
- case "start":
992
- cmdStart(false);
993
- break;
994
-
995
- case "build":
996
- cmdBuild();
997
- break;
998
-
999
- case "init":
1000
- {
1001
- const { targetDir, source } = parseInitArgs(args.slice(1));
1002
- cmdInit(targetDir, { source });
1003
- }
1004
- break;
1005
-
1006
- case "status":
1007
- cmdStatus();
1008
- break;
1009
-
1010
- case "stop":
1011
- cmdStop();
1012
- break;
1013
-
1014
- case "generate":
1015
- cmdGenerate();
1016
- break;
1017
-
1018
- case "deploy":
1019
- cmdDeploy();
1020
- break;
1021
-
1022
- case "scale":
1023
- console.error(" `forge scale` is not yet implemented.");
1024
- console.error(" To scale a service, update the `threads` field in your config and restart.");
1025
- console.error(" Auto-scaling recommendations are available via ScaleAdvisor (see `forge status`).");
1026
- process.exit(1);
1027
- break;
1028
-
1029
- case "restart":
1030
- console.error(" `forge restart` is not yet implemented.");
1031
- console.error(" To restart, run `forge stop` followed by `forge start`.");
1032
- console.error(" For zero-downtime restarts, use a process manager (systemd, pm2) or rolling deploy.");
1033
- process.exit(1);
1034
- break;
1035
-
1036
- case "host": {
1037
- const { cmdHost } = await import("./host-commands.js");
1038
- cmdHost(args.slice(1));
1039
- break;
1040
- }
1041
-
1042
- case "platform": {
1043
- const { cmdPlatform } = await import("./platform-commands.js");
1044
- cmdPlatform(args.slice(1));
1045
- break;
1046
- }
1047
-
1048
- case "help":
1049
- case "--help":
1050
- case "-h":
1051
- case undefined:
1052
- printHelp();
1053
- break;
1054
-
1055
- default:
1056
- console.error(` Unknown command: ${command}`);
1057
- printHelp();
1058
- process.exit(1);
1059
- }
2
+ import { execute } from "@oclif/core";
3
+ await execute({ dir: import.meta.url });