threadforge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/bin/forge.js +1050 -0
  4. package/bin/host-commands.js +344 -0
  5. package/bin/platform-commands.js +570 -0
  6. package/package.json +71 -0
  7. package/shared/auth.js +475 -0
  8. package/src/core/DirectMessageBus.js +364 -0
  9. package/src/core/EndpointResolver.js +247 -0
  10. package/src/core/ForgeContext.js +2227 -0
  11. package/src/core/ForgeHost.js +122 -0
  12. package/src/core/ForgePlatform.js +145 -0
  13. package/src/core/Ingress.js +768 -0
  14. package/src/core/Interceptors.js +420 -0
  15. package/src/core/MessageBus.js +310 -0
  16. package/src/core/Prometheus.js +305 -0
  17. package/src/core/RequestContext.js +413 -0
  18. package/src/core/RoutingStrategy.js +316 -0
  19. package/src/core/Supervisor.js +1306 -0
  20. package/src/core/ThreadAllocator.js +196 -0
  21. package/src/core/WorkerChannelManager.js +879 -0
  22. package/src/core/config.js +624 -0
  23. package/src/core/host-config.js +311 -0
  24. package/src/core/network-utils.js +166 -0
  25. package/src/core/platform-config.js +308 -0
  26. package/src/decorators/ServiceProxy.js +899 -0
  27. package/src/decorators/index.js +571 -0
  28. package/src/deploy/NginxGenerator.js +865 -0
  29. package/src/deploy/PlatformManifestGenerator.js +96 -0
  30. package/src/deploy/RouteManifestGenerator.js +112 -0
  31. package/src/deploy/index.js +984 -0
  32. package/src/frontend/FrontendDevLifecycle.js +65 -0
  33. package/src/frontend/FrontendPluginOrchestrator.js +187 -0
  34. package/src/frontend/SiteResolver.js +63 -0
  35. package/src/frontend/StaticMountRegistry.js +90 -0
  36. package/src/frontend/index.js +5 -0
  37. package/src/frontend/plugins/index.js +2 -0
  38. package/src/frontend/plugins/viteFrontend.js +79 -0
  39. package/src/frontend/types.js +35 -0
  40. package/src/index.js +56 -0
  41. package/src/internals.js +31 -0
  42. package/src/plugins/PluginManager.js +537 -0
  43. package/src/plugins/ScopedPostgres.js +192 -0
  44. package/src/plugins/ScopedRedis.js +142 -0
  45. package/src/plugins/index.js +1729 -0
  46. package/src/registry/ServiceRegistry.js +796 -0
  47. package/src/scaling/ScaleAdvisor.js +442 -0
  48. package/src/services/Service.js +195 -0
  49. package/src/services/worker-bootstrap.js +676 -0
  50. package/src/templates/auth-service.js +65 -0
  51. package/src/templates/identity-service.js +75 -0
@@ -0,0 +1,344 @@
1
+ /**
2
+ * ForgeHost CLI Commands
3
+ *
4
+ * Subcommands for `forge host`:
5
+ * forge host init - Scaffold forge.host.js, shared/auth.js, projects/
6
+ * forge host start - Start the multi-project host
7
+ * forge host add <path> - Add a project to forge.host.js
8
+ * forge host remove <id> - Remove a project (keep data unless --drop-data)
9
+ * forge host status - Show per-project status table
10
+ * forge host restart <id> - Restart a specific project's workers
11
+ * forge host generate - Generate nginx + route manifests
12
+ */
13
+
14
+ import fs from "node:fs";
15
+ import path from "node:path";
16
+ import { pathToFileURL } from "node:url";
17
+
18
+ const HOST_CONFIG_NAMES = ["forge.host.js", "forge.host.mjs"];
19
+
20
+ async function findHostConfig() {
21
+ const cwd = process.cwd();
22
+ for (const name of HOST_CONFIG_NAMES) {
23
+ const fullPath = path.join(cwd, name);
24
+ try {
25
+ await fs.promises.access(fullPath);
26
+ return fullPath;
27
+ } catch {}
28
+ }
29
+ return null;
30
+ }
31
+
32
+ async function loadHostConfig() {
33
+ const configPath = await findHostConfig();
34
+ if (!configPath) {
35
+ console.error(" Error: No forge.host.js found in current directory.");
36
+ console.error(" Run `forge host init` to create one.");
37
+ process.exit(1);
38
+ }
39
+ const configUrl = pathToFileURL(configPath).href;
40
+ const mod = await import(configUrl);
41
+ return mod.default ?? mod;
42
+ }
43
+
44
+ async function cmdHostInit() {
45
+ const cwd = process.cwd();
46
+ const configPath = path.join(cwd, "forge.host.js");
47
+
48
+ if (fs.existsSync(configPath)) {
49
+ console.error(" forge.host.js already exists in this directory.");
50
+ process.exit(1);
51
+ }
52
+
53
+ // Create directories
54
+ for (const dir of ["shared", "projects"]) {
55
+ fs.mkdirSync(path.join(cwd, dir), { recursive: true });
56
+ }
57
+
58
+ // Write host config
59
+ fs.writeFileSync(
60
+ configPath,
61
+ `import { defineHost, postgres, redis } from 'threadforge';
62
+
63
+ export default defineHost({
64
+ domain: 'myhost.dev',
65
+ basePort: 3200,
66
+
67
+ shared: {
68
+ auth: {
69
+ entry: './shared/auth.js',
70
+ type: 'edge',
71
+ port: 3100,
72
+ prefix: '/auth',
73
+ },
74
+ },
75
+
76
+ projects: {
77
+ // Add projects here:
78
+ // blog: {
79
+ // domain: 'blog.myhost.dev',
80
+ // config: './projects/blog/forge.config.js',
81
+ // },
82
+ },
83
+
84
+ plugins: [
85
+ postgres({ url: process.env.DATABASE_URL }),
86
+ redis(),
87
+ ],
88
+ });
89
+ `,
90
+ );
91
+
92
+ // Copy auth template if not exists
93
+ const authDst = path.join(cwd, "shared", "auth.js");
94
+ if (!fs.existsSync(authDst)) {
95
+ const authSrc = path.join(cwd, "node_modules", "threadforge", "shared", "auth.js");
96
+ if (fs.existsSync(authSrc)) {
97
+ fs.copyFileSync(authSrc, authDst);
98
+ } else {
99
+ fs.writeFileSync(
100
+ authDst,
101
+ `// Shared auth service — customize for your deployment
102
+ // See: https://github.com/threadforge/threadforge#forgehost-auth
103
+ import { Service } from 'threadforge';
104
+
105
+ export default class AuthService extends Service {
106
+ static contract = {
107
+ routes: [
108
+ { method: 'POST', path: '/login', handler: 'login' },
109
+ { method: 'POST', path: '/register', handler: 'register' },
110
+ ],
111
+ };
112
+
113
+ async login(body) {
114
+ // TODO: implement login
115
+ return { error: 'Not implemented' };
116
+ }
117
+
118
+ async register(body) {
119
+ // TODO: implement registration
120
+ return { error: 'Not implemented' };
121
+ }
122
+ }
123
+ `,
124
+ );
125
+ }
126
+ }
127
+
128
+ console.log(`
129
+ ForgeHost initialized!
130
+
131
+ Created:
132
+ forge.host.js - Host configuration
133
+ shared/auth.js - Shared auth service
134
+ projects/ - Project directory
135
+
136
+ Next steps:
137
+ 1. Edit forge.host.js to add your projects
138
+ 2. forge host start
139
+ `);
140
+ }
141
+
142
+ async function cmdHostStart() {
143
+ const hostConfig = await loadHostConfig();
144
+ const { ForgeHost } = await import("../src/core/ForgeHost.js");
145
+
146
+ const host = new ForgeHost(hostConfig);
147
+ await host.start();
148
+ }
149
+
150
+ async function cmdHostAdd(args) {
151
+ const projectPath = args[0];
152
+ if (!projectPath) {
153
+ console.error(" Usage: forge host add <path-to-project>");
154
+ process.exit(1);
155
+ }
156
+
157
+ const absPath = path.resolve(process.cwd(), projectPath);
158
+ const configFile = ["forge.config.js", "forge.config.mjs"].find((name) =>
159
+ fs.existsSync(path.join(absPath, name)),
160
+ );
161
+
162
+ if (!configFile) {
163
+ console.error(` No forge.config.js found at ${absPath}`);
164
+ process.exit(1);
165
+ }
166
+
167
+ const projectId = path.basename(absPath);
168
+ const relPath = path.relative(process.cwd(), path.join(absPath, configFile));
169
+
170
+ console.log(`
171
+ Add this to your forge.host.js projects:
172
+
173
+ ${projectId}: {
174
+ domain: '${projectId}.myhost.dev',
175
+ config: './${relPath}',
176
+ },
177
+ `);
178
+ }
179
+
180
+ async function cmdHostRemove(args) {
181
+ const projectId = args[0];
182
+ if (!projectId) {
183
+ console.error(" Usage: forge host remove <project-id> [--drop-data]");
184
+ process.exit(1);
185
+ }
186
+
187
+ const dropData = args.includes("--drop-data");
188
+
189
+ console.log(`
190
+ To remove project "${projectId}":
191
+ 1. Remove its entry from forge.host.js
192
+ 2. Restart: forge host start
193
+ ${dropData ? `3. Data will be dropped: DROP SCHEMA project_${projectId} CASCADE` : `3. Data preserved in schema project_${projectId}`}
194
+ `);
195
+ }
196
+
197
+ async function cmdHostStatus() {
198
+ const metricsPort = 9090;
199
+ try {
200
+ const res = await fetch(`http://localhost:${metricsPort}/status`);
201
+ const data = await res.json();
202
+
203
+ console.log("");
204
+ console.log(" ForgeHost Status");
205
+ console.log(" ────────────────────────────────────");
206
+
207
+ if (data.projects) {
208
+ console.log("");
209
+ console.log(" ┌──────────────────┬─────────────────────────┬──────────┬─────────┐");
210
+ console.log(" │ Project │ Domain │ Services │ Workers │");
211
+ console.log(" ├──────────────────┼─────────────────────────┼──────────┼─────────┤");
212
+
213
+ for (const [id, proj] of Object.entries(data.projects)) {
214
+ const name = id.padEnd(16);
215
+ const domain = (proj.domain ?? "—").substring(0, 23).padEnd(23);
216
+ const services = String(proj.services).padEnd(8);
217
+ const workers = String(proj.workers).padEnd(7);
218
+ console.log(` │ ${name} │ ${domain} │ ${services} │ ${workers} │`);
219
+ }
220
+
221
+ console.log(" └──────────────────┴─────────────────────────┴──────────┴─────────┘");
222
+ } else {
223
+ console.log(" Not running in host mode.");
224
+ }
225
+
226
+ console.log(` Uptime: ${Math.floor(data.uptime)}s`);
227
+ console.log("");
228
+ } catch {
229
+ console.error(" Could not connect to ForgeHost runtime.");
230
+ console.error(" Is it running? (forge host start)");
231
+ process.exit(1);
232
+ }
233
+ }
234
+
235
+ async function cmdHostRestart(args) {
236
+ const projectId = args[0];
237
+ if (!projectId) {
238
+ console.error(" Usage: forge host restart <project-id>");
239
+ process.exit(1);
240
+ }
241
+
242
+ console.error(` Error: Per-project restart is not yet implemented.`);
243
+ console.error(` Individual project restart requires the ForgeHost supervisor to support`);
244
+ console.error(` graceful worker replacement without downtime.`);
245
+ console.error(``);
246
+ console.error(` Workarounds:`);
247
+ console.error(` 1. Restart the entire host: forge host start`);
248
+ console.error(` 2. Use a process manager (systemd, pm2) for zero-downtime restarts`);
249
+ console.error(` 3. For rolling deploys, use: forge deploy --rolling`);
250
+ process.exit(1);
251
+ }
252
+
253
+ async function cmdHostGenerate() {
254
+ const hostConfig = await loadHostConfig();
255
+ const { resolveHostConfig } = await import("../src/core/host-config.js");
256
+ const { generateHostNginxConfig } = await import("../src/deploy/NginxGenerator.js");
257
+
258
+ const raw = hostConfig._isHostConfig ? hostConfig._raw : hostConfig;
259
+ const resolved = await resolveHostConfig(raw);
260
+ const routesDir = path.join(process.cwd(), "routes");
261
+ fs.mkdirSync(routesDir, { recursive: true });
262
+
263
+ // Generate route manifests
264
+ let manifestCount = 0;
265
+ for (const [name, svc] of Object.entries(resolved.services)) {
266
+ if (svc.type !== "edge") continue;
267
+ // Generate a minimal manifest for the route
268
+ const yaml = generateBasicManifest(name, svc, resolved.hostMeta);
269
+ if (yaml) {
270
+ const safeName = name.replace(/:/g, "_");
271
+ fs.writeFileSync(path.join(routesDir, `${safeName}.yaml`), yaml);
272
+ manifestCount++;
273
+ }
274
+ }
275
+
276
+ // Generate nginx config
277
+ const nginxConfig = generateHostNginxConfig({
278
+ hostMeta: resolved.hostMeta,
279
+ services: resolved.services,
280
+ });
281
+ const deployDir = path.join(process.cwd(), "deploy");
282
+ fs.mkdirSync(deployDir, { recursive: true });
283
+ fs.writeFileSync(path.join(deployDir, "nginx.conf"), nginxConfig);
284
+
285
+ console.log(`
286
+ ForgeHost artifacts generated:
287
+
288
+ routes/ ${manifestCount} route manifest(s)
289
+ deploy/ nginx.conf
290
+
291
+ ForgeProxy reads ./routes/*.yaml automatically.
292
+ `);
293
+ }
294
+
295
+ function generateBasicManifest(serviceName, svc, hostMeta) {
296
+ const projectId = svc._projectId;
297
+ const domain = projectId ? hostMeta[projectId]?.domain : null;
298
+
299
+ let yaml = `# Auto-generated for ForgeHost\n`;
300
+ yaml += `service: ${serviceName}\n`;
301
+ yaml += `prefix: ${svc.prefix ?? "/"}\n`;
302
+ yaml += `auth: ${svc.auth ?? "required"}\n`;
303
+ if (projectId) yaml += `project_id: ${projectId}\n`;
304
+ if (domain) yaml += `host: ${domain}\n`;
305
+ yaml += `\nroutes:\n`;
306
+ yaml += ` - method: GET\n`;
307
+ yaml += ` path: ""\n`;
308
+ yaml += ` handler: index\n`;
309
+ return yaml;
310
+ }
311
+
312
+ export async function cmdHost(args) {
313
+ const subcommand = args[0];
314
+ const subArgs = args.slice(1);
315
+
316
+ switch (subcommand) {
317
+ case "init":
318
+ return cmdHostInit();
319
+ case "start":
320
+ return cmdHostStart();
321
+ case "add":
322
+ return cmdHostAdd(subArgs);
323
+ case "remove":
324
+ return cmdHostRemove(subArgs);
325
+ case "status":
326
+ return cmdHostStatus();
327
+ case "restart":
328
+ return cmdHostRestart(subArgs);
329
+ case "generate":
330
+ return cmdHostGenerate();
331
+ default:
332
+ console.log(`
333
+ ForgeHost Commands:
334
+
335
+ forge host init Scaffold a ForgeHost project
336
+ forge host start Start the multi-project host
337
+ forge host add <path> Add a project
338
+ forge host remove <id> Remove a project
339
+ forge host status Show per-project status
340
+ forge host restart <id> Restart a project's workers
341
+ forge host generate Generate nginx + route manifests
342
+ `);
343
+ }
344
+ }