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,570 @@
1
+ /**
2
+ * ForgePlatform CLI Commands
3
+ *
4
+ * Subcommands for `forge platform`:
5
+ * forge platform init Scaffold forge.platform.js + shared services + apps/
6
+ * forge platform start Start the platform
7
+ * forge platform add <name> Scaffold a new app
8
+ * forge platform remove <id> Print removal instructions
9
+ * forge platform status Show per-app status table
10
+ * forge platform generate Generate nginx + platform.yaml + route manifests
11
+ */
12
+
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { fileURLToPath, pathToFileURL } from "node:url";
16
+
17
+ const PLATFORM_CONFIG_NAMES = ["forge.platform.js", "forge.platform.mjs"];
18
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
19
+ const LOCAL_PKG_PATH = path.resolve(__dirname, "..", "package.json");
20
+
21
+ async function findPlatformConfig() {
22
+ const cwd = process.cwd();
23
+ for (const name of PLATFORM_CONFIG_NAMES) {
24
+ const fullPath = path.join(cwd, name);
25
+ try {
26
+ await fs.promises.access(fullPath);
27
+ return fullPath;
28
+ } catch {}
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Compute the import path for threadforge from a given source directory.
35
+ * Returns a relative path when running from a local clone, or "threadforge"
36
+ * when installed as a dependency.
37
+ *
38
+ * @param {string} fromDir - The directory the import will appear in
39
+ * @returns {string}
40
+ */
41
+ function computeImportPath(fromDir) {
42
+ try {
43
+ if (fs.existsSync(LOCAL_PKG_PATH)) {
44
+ const pkg = JSON.parse(fs.readFileSync(LOCAL_PKG_PATH, "utf8"));
45
+ if (pkg.name === "threadforge") {
46
+ const indexAbsolute = fs.realpathSync(path.resolve(__dirname, "..", "src", "index.js"));
47
+ const realFrom = fs.realpathSync(fromDir);
48
+ let rel = path.relative(realFrom, indexAbsolute);
49
+ if (!rel.startsWith(".")) rel = `./${rel}`;
50
+ return rel;
51
+ }
52
+ }
53
+ } catch {}
54
+ return "threadforge";
55
+ }
56
+
57
+ async function loadPlatformConfig() {
58
+ const configPath = await findPlatformConfig();
59
+ if (!configPath) {
60
+ console.error(" Error: No forge.platform.js found in current directory.");
61
+ console.error(" Run `forge platform init` to create one.");
62
+ process.exit(1);
63
+ }
64
+ const configUrl = pathToFileURL(configPath).href;
65
+ const mod = await import(configUrl);
66
+ return mod.default ?? mod;
67
+ }
68
+
69
+ function loadFrontendBuildManifest(cwd) {
70
+ const manifestPath = path.join(cwd, ".threadforge", "frontend-manifest.json");
71
+ if (!fs.existsSync(manifestPath)) return null;
72
+ try {
73
+ const json = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
74
+ return { path: manifestPath, data: json };
75
+ } catch (err) {
76
+ console.warn(` Warning: Could not parse ${manifestPath}: ${err.message}`);
77
+ return null;
78
+ }
79
+ }
80
+
81
+ // ── forge platform init ──────────────────────────────────────────────────
82
+
83
+ async function cmdPlatformInit() {
84
+ const cwd = process.cwd();
85
+ const configPath = path.join(cwd, "forge.platform.js");
86
+
87
+ if (fs.existsSync(configPath)) {
88
+ console.error(" forge.platform.js already exists in this directory.");
89
+ process.exit(1);
90
+ }
91
+
92
+ const importPath = computeImportPath(cwd);
93
+ const serviceImportPath = computeImportPath(path.join(cwd, "shared"));
94
+
95
+ // Create directories
96
+ for (const dir of ["shared", "apps"]) {
97
+ fs.mkdirSync(path.join(cwd, dir), { recursive: true });
98
+ }
99
+
100
+ // Write platform config
101
+ fs.writeFileSync(
102
+ configPath,
103
+ `import { definePlatform } from '${importPath}';
104
+
105
+ export default definePlatform({
106
+ platform: {
107
+ globalAuth: true,
108
+ sessionSharing: true,
109
+
110
+ apps: {
111
+ // Add apps here:
112
+ // myapp: {
113
+ // domains: ['myapp.example.com'],
114
+ // services: ['myapp-api'],
115
+ // schema: 'myapp',
116
+ // },
117
+ },
118
+ },
119
+
120
+ // Plugins (shared across all apps)
121
+ // plugins: [postgres(), redis()],
122
+
123
+ // Shared services (run once, serve all apps)
124
+ identity: {
125
+ entry: './shared/identity.js',
126
+ type: 'edge',
127
+ port: 3001,
128
+ },
129
+ auth: {
130
+ entry: './shared/auth.js',
131
+ type: 'edge',
132
+ port: 3002,
133
+ prefix: '/auth',
134
+ },
135
+
136
+ // Per-app services go here:
137
+ // 'myapp-api': {
138
+ // entry: './apps/myapp/api.js',
139
+ // type: 'edge',
140
+ // port: 4001,
141
+ // connects: ['auth'],
142
+ // },
143
+ });
144
+ `,
145
+ );
146
+
147
+ // Write identity service stub
148
+ const identityPath = path.join(cwd, "shared", "identity.js");
149
+ if (!fs.existsSync(identityPath)) {
150
+ fs.writeFileSync(
151
+ identityPath,
152
+ `import { Service } from '${serviceImportPath}';
153
+
154
+ export default class IdentityService extends Service {
155
+ static contract = {
156
+ expose: ['getUser', 'getUserByEmail', 'createUser', 'listMembers'],
157
+ routes: [
158
+ { method: 'POST', path: '/login', handler: 'login' },
159
+ { method: 'POST', path: '/register', handler: 'register' },
160
+ { method: 'GET', path: '/users/:id', handler: 'getUserRoute' },
161
+ { method: 'GET', path: '/users', handler: 'listUsersRoute' },
162
+ ],
163
+ };
164
+
165
+ async onStart(ctx) {
166
+ ctx.logger.info('Identity service ready');
167
+ }
168
+
169
+ async login(_body) { return { error: 'Not implemented' }; }
170
+ async register(_body) { return { error: 'Not implemented' }; }
171
+ async getUserRoute(_body, params) { return this.getUser(params.id); }
172
+ async listUsersRoute() { return { error: 'Not implemented' }; }
173
+ async getUser(_userId) { return { error: 'Not implemented' }; }
174
+ async getUserByEmail(_email) { return { error: 'Not implemented' }; }
175
+ async createUser(_data) { return { error: 'Not implemented' }; }
176
+ async listMembers(_appId) { return { error: 'Not implemented' }; }
177
+ }
178
+ `,
179
+ );
180
+ }
181
+
182
+ // Write auth service stub
183
+ const authPath = path.join(cwd, "shared", "auth.js");
184
+ if (!fs.existsSync(authPath)) {
185
+ fs.writeFileSync(
186
+ authPath,
187
+ `import { Service } from '${serviceImportPath}';
188
+
189
+ export default class AuthService extends Service {
190
+ static contract = {
191
+ expose: ['validateToken', 'issueToken', 'revokeToken'],
192
+ routes: [
193
+ { method: 'POST', path: '/token', handler: 'issueTokenRoute' },
194
+ { method: 'POST', path: '/token/validate', handler: 'validateTokenRoute' },
195
+ { method: 'POST', path: '/token/revoke', handler: 'revokeTokenRoute' },
196
+ { method: 'POST', path: '/token/refresh', handler: 'refreshTokenRoute' },
197
+ ],
198
+ };
199
+
200
+ async onStart(ctx) {
201
+ ctx.logger.info('Auth service ready');
202
+ }
203
+
204
+ async issueTokenRoute(_body) { return { error: 'Not implemented' }; }
205
+ async validateTokenRoute(_body) { return { error: 'Not implemented' }; }
206
+ async revokeTokenRoute(_body) { return { error: 'Not implemented' }; }
207
+ async refreshTokenRoute(_body) { return { error: 'Not implemented' }; }
208
+ async validateToken(_token) { return { error: 'Not implemented' }; }
209
+ async issueToken(_payload) { return { error: 'Not implemented' }; }
210
+ async revokeToken(_token) { return { error: 'Not implemented' }; }
211
+ }
212
+ `,
213
+ );
214
+ }
215
+
216
+ console.log(`
217
+ ⚡ ForgePlatform initialized!
218
+
219
+ Created:
220
+ forge.platform.js - Platform configuration
221
+ shared/identity.js - Identity service (stub)
222
+ shared/auth.js - Auth service (stub)
223
+ apps/ - App directory
224
+
225
+ Next steps:
226
+ 1. Edit forge.platform.js to configure your apps
227
+ 2. forge platform add myapp
228
+ 3. forge platform start
229
+ `);
230
+ }
231
+
232
+ // ── forge platform start ─────────────────────────────────────────────────
233
+
234
+ async function cmdPlatformStart() {
235
+ const platformConfig = await loadPlatformConfig();
236
+ const { ForgePlatform } = await import("../src/core/ForgePlatform.js");
237
+
238
+ const platform = new ForgePlatform(platformConfig);
239
+ await platform.start();
240
+ }
241
+
242
+ // ── forge platform add <name> ────────────────────────────────────────────
243
+
244
+ async function cmdPlatformAdd(args) {
245
+ const appName = args[0];
246
+ if (!appName) {
247
+ console.error(" Usage: forge platform add <app-name>");
248
+ process.exit(1);
249
+ }
250
+
251
+ if (!/^[a-z][a-z0-9_-]*$/.test(appName)) {
252
+ console.error(` Invalid app name "${appName}": must start with a lowercase letter and contain only lowercase letters, digits, hyphens, and underscores.`);
253
+ process.exit(1);
254
+ }
255
+
256
+ const cwd = process.cwd();
257
+ const appDir = path.join(cwd, "apps", appName);
258
+
259
+ if (fs.existsSync(appDir)) {
260
+ console.error(` App directory already exists: apps/${appName}/`);
261
+ process.exit(1);
262
+ }
263
+
264
+ const serviceImportPath = computeImportPath(path.join(cwd, "apps", appName));
265
+
266
+ // Create app directory + migrations
267
+ fs.mkdirSync(path.join(appDir, "migrations"), { recursive: true });
268
+
269
+ // Write service file
270
+ fs.writeFileSync(
271
+ path.join(appDir, "api.js"),
272
+ `import { Service } from '${serviceImportPath}';
273
+
274
+ export default class ${capitalize(appName)}ApiService extends Service {
275
+ static contract = {
276
+ expose: ['getStatus'],
277
+ routes: [
278
+ { method: 'GET', path: '/health', handler: 'healthCheck' },
279
+ { method: 'GET', path: '/hello/:name', handler: 'hello' },
280
+ ],
281
+ };
282
+
283
+ async onStart(ctx) {
284
+ ctx.logger.info('${appName} API service ready');
285
+ }
286
+
287
+ async healthCheck() {
288
+ return { status: 'ok', app: '${appName}', worker: this.ctx.workerId };
289
+ }
290
+
291
+ async hello(_body, params) {
292
+ return { message: \`Hello from ${appName}, \${params.name}!\` };
293
+ }
294
+
295
+ async getStatus() {
296
+ return { app: '${appName}', status: 'running' };
297
+ }
298
+ }
299
+ `,
300
+ );
301
+
302
+ // Write migration template
303
+ const safeName = appName.replace(/"/g, '""');
304
+ fs.writeFileSync(
305
+ path.join(appDir, "migrations", "001_init.sql"),
306
+ `-- Migration: ${appName} initial schema
307
+ -- This runs in the app's isolated schema (SET search_path = ${appName})
308
+
309
+ CREATE TABLE IF NOT EXISTS "${safeName}_items" (
310
+ id SERIAL PRIMARY KEY,
311
+ name TEXT NOT NULL,
312
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
313
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
314
+ );
315
+
316
+ CREATE INDEX IF NOT EXISTS "idx_${safeName}_items_created"
317
+ ON "${safeName}_items" (created_at);
318
+ `,
319
+ );
320
+
321
+ console.log(`
322
+ ⚡ App "${appName}" scaffolded!
323
+
324
+ Created:
325
+ apps/${appName}/api.js - API service
326
+ apps/${appName}/migrations/001_init.sql - Initial migration
327
+
328
+ Add this to your forge.platform.js:
329
+
330
+ // In platform.apps:
331
+ ${appName}: {
332
+ domains: ['${appName}.example.com'],
333
+ services: ['${appName}-api'],
334
+ schema: '${appName}',
335
+ },
336
+
337
+ // As a top-level service:
338
+ '${appName}-api': {
339
+ entry: './apps/${appName}/api.js',
340
+ type: 'edge',
341
+ port: 4001,
342
+ connects: ['auth'],
343
+ },
344
+ `);
345
+ }
346
+
347
+ function capitalize(str) {
348
+ return str.charAt(0).toUpperCase() + str.slice(1).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
349
+ }
350
+
351
+ // ── forge platform remove <id> ───────────────────────────────────────────
352
+
353
+ async function cmdPlatformRemove(args) {
354
+ const appId = args[0];
355
+ if (!appId) {
356
+ console.error(" Usage: forge platform remove <app-id>");
357
+ process.exit(1);
358
+ }
359
+
360
+ const dropData = args.includes("--drop-data");
361
+
362
+ console.log(`
363
+ To remove app "${appId}":
364
+ 1. Remove its entry from platform.apps in forge.platform.js
365
+ 2. Remove its service definitions from forge.platform.js
366
+ 3. Restart: forge platform start
367
+ ${dropData ? `4. To drop data, manually run this SQL:\n DROP SCHEMA "${appId}" CASCADE;` : `4. Data preserved in schema "${appId}"`}
368
+ 5. Optionally delete: rm -rf apps/${appId}/
369
+ `);
370
+ }
371
+
372
+ // ── forge platform status ────────────────────────────────────────────────
373
+
374
+ async function cmdPlatformStatus() {
375
+ const metricsPort = parseInt(process.env.FORGE_METRICS_PORT || '9090', 10);
376
+ try {
377
+ const res = await fetch(`http://localhost:${metricsPort}/status`);
378
+ const data = await res.json();
379
+
380
+ console.log("");
381
+ console.log(" ForgePlatform Status");
382
+ console.log(" ──────────────────────────────────────");
383
+
384
+ if (data.projects) {
385
+ console.log("");
386
+ console.log(" ┌──────────────────┬─────────────────────────┬──────────┬─────────┬────────────────┐");
387
+ console.log(" │ App │ Domain │ Services │ Workers │ Schema │");
388
+ console.log(" ├──────────────────┼─────────────────────────┼──────────┼─────────┼────────────────┤");
389
+
390
+ for (const [id, proj] of Object.entries(data.projects)) {
391
+ const name = id.padEnd(16);
392
+ const domain = (proj.domain ?? "—").substring(0, 23).padEnd(23);
393
+ const services = String(proj.services).padEnd(8);
394
+ const workers = String(proj.workers).padEnd(7);
395
+ const schema = (proj.schema ?? "—").substring(0, 14).padEnd(14);
396
+ console.log(` │ ${name} │ ${domain} │ ${services} │ ${workers} │ ${schema} │`);
397
+ }
398
+
399
+ console.log(" └──────────────────┴─────────────────────────┴──────────┴─────────┴────────────────┘");
400
+ } else {
401
+ console.log(" Not running in platform mode.");
402
+ }
403
+
404
+ console.log(` Uptime: ${Math.floor(data.uptime)}s`);
405
+ console.log("");
406
+ } catch {
407
+ console.error(" Could not connect to ForgePlatform runtime.");
408
+ console.error(" Is it running? (forge platform start)");
409
+ process.exit(1);
410
+ }
411
+ }
412
+
413
+ // ── forge platform generate ──────────────────────────────────────────────
414
+
415
+ async function cmdPlatformGenerate() {
416
+ const platformConfig = await loadPlatformConfig();
417
+ const { resolveAppDomains, transformToHostConfig } = await import("../src/core/platform-config.js");
418
+ const { resolveHostConfig } = await import("../src/core/host-config.js");
419
+ const { generatePlatformNginxConfig } = await import("../src/deploy/NginxGenerator.js");
420
+ const { generatePlatformManifest } = await import("../src/deploy/PlatformManifestGenerator.js");
421
+
422
+ const raw = platformConfig._isPlatformConfig ? platformConfig._raw : platformConfig;
423
+ const hostConfig = transformToHostConfig(raw);
424
+ const resolved = await resolveHostConfig(hostConfig);
425
+
426
+ const cwd = process.cwd();
427
+ const routesDir = path.join(cwd, "routes");
428
+ const deployDir = path.join(cwd, "deploy");
429
+ fs.mkdirSync(routesDir, { recursive: true });
430
+ fs.mkdirSync(deployDir, { recursive: true });
431
+ const frontendManifest = loadFrontendBuildManifest(cwd);
432
+
433
+ /** @type {Record<string, string>} */
434
+ const builtOutDirBySite = {};
435
+ if (frontendManifest?.data?.sites && Array.isArray(frontendManifest.data.sites)) {
436
+ for (const site of frontendManifest.data.sites) {
437
+ if (site?.siteId && site?.outDir) {
438
+ builtOutDirBySite[site.siteId] = site.outDir;
439
+ }
440
+ }
441
+ }
442
+
443
+ /** @type {Record<string, any[]>} */
444
+ const mountsFromManifest = {};
445
+ if (frontendManifest?.data?.mounts && typeof frontendManifest.data.mounts === "object") {
446
+ for (const [siteId, mounts] of Object.entries(frontendManifest.data.mounts)) {
447
+ if (Array.isArray(mounts)) mountsFromManifest[siteId] = mounts;
448
+ }
449
+ }
450
+
451
+ const apps = {};
452
+ for (const [appId, app] of Object.entries(raw.platform.apps)) {
453
+ const site = resolved.sites?.[appId];
454
+ const frontend = site?.frontend ? { ...site.frontend } : (app.frontend ? { ...app.frontend } : null);
455
+ if (frontend && builtOutDirBySite[appId]) {
456
+ frontend.outDir = builtOutDirBySite[appId];
457
+ }
458
+
459
+ const staticMounts = mountsFromManifest[appId]
460
+ ? mountsFromManifest[appId]
461
+ : (() => {
462
+ if (frontend) {
463
+ return [{
464
+ siteId: appId,
465
+ domains: site?.domains ?? resolveAppDomains(app),
466
+ basePath: frontend.basePath ?? "/",
467
+ dir: frontend.outDir,
468
+ spaFallback: frontend.spaFallback ?? true,
469
+ cachePolicy: "short",
470
+ }];
471
+ }
472
+ const legacyStatic = site?.staticDir ?? app.static ?? null;
473
+ if (!legacyStatic) return [];
474
+ return [{
475
+ siteId: appId,
476
+ domains: site?.domains ?? resolveAppDomains(app),
477
+ basePath: "/static",
478
+ dir: legacyStatic,
479
+ spaFallback: false,
480
+ cachePolicy: "short",
481
+ }];
482
+ })();
483
+
484
+ apps[appId] = {
485
+ domains: resolveAppDomains(app),
486
+ ssl: app.ssl ?? null,
487
+ static: site?.staticDir ?? app.static ?? null,
488
+ frontend,
489
+ staticMounts,
490
+ services: app.services ?? [],
491
+ };
492
+ }
493
+
494
+ // Generate nginx config
495
+ const nginxConfig = generatePlatformNginxConfig({
496
+ apps,
497
+ services: resolved.services,
498
+ hostMeta: resolved.hostMeta,
499
+ defaultSsl: raw.platform.ssl ?? null,
500
+ maxBodySize: raw.platform.maxBodySize ?? 10,
501
+ });
502
+ fs.writeFileSync(path.join(deployDir, "nginx.conf"), nginxConfig);
503
+
504
+ // Generate platform.yaml
505
+ const manifestWithSites = generatePlatformManifest(raw.platform, resolved.hostMeta, resolved.sites);
506
+ fs.writeFileSync(path.join(routesDir, "platform.yaml"), manifestWithSites);
507
+
508
+ // Generate per-service route manifests
509
+ let manifestCount = 0;
510
+ for (const [name, svc] of Object.entries(resolved.services)) {
511
+ if (svc.type !== "edge") continue;
512
+ const safeName = name.replace(/:/g, "_");
513
+ let yaml = `# Auto-generated for ForgePlatform\n`;
514
+ yaml += `service: ${name}\n`;
515
+ yaml += `prefix: ${svc.prefix ?? "/"}\n`;
516
+ if (svc._projectId) yaml += `project_id: ${svc._projectId}\n`;
517
+ const domain = svc._projectId ? resolved.hostMeta[svc._projectId]?.domain : null;
518
+ if (domain) yaml += `host: ${domain}\n`;
519
+ yaml += `\nroutes:\n`;
520
+ yaml += ` - method: GET\n`;
521
+ yaml += ` path: ""\n`;
522
+ yaml += ` handler: index\n`;
523
+ fs.writeFileSync(path.join(routesDir, `${safeName}.yaml`), yaml);
524
+ manifestCount++;
525
+ }
526
+
527
+ console.log(`
528
+ ForgePlatform artifacts generated:
529
+
530
+ deploy/nginx.conf Platform nginx config
531
+ routes/platform.yaml Platform manifest for ForgeProxy
532
+ routes/ ${manifestCount} service route manifest(s)
533
+ ${frontendManifest ? `.threadforge/frontend-manifest.json Frontend build manifest (source)` : "(no frontend manifest found)"}
534
+
535
+ ForgeProxy reads ./routes/*.yaml automatically.
536
+ `);
537
+ }
538
+
539
+ // ── Main dispatch ────────────────────────────────────────────────────────
540
+
541
+ export async function cmdPlatform(args) {
542
+ const subcommand = args[0];
543
+ const subArgs = args.slice(1);
544
+
545
+ switch (subcommand) {
546
+ case "init":
547
+ return cmdPlatformInit();
548
+ case "start":
549
+ return cmdPlatformStart();
550
+ case "add":
551
+ return cmdPlatformAdd(subArgs);
552
+ case "remove":
553
+ return cmdPlatformRemove(subArgs);
554
+ case "status":
555
+ return cmdPlatformStatus();
556
+ case "generate":
557
+ return cmdPlatformGenerate();
558
+ default:
559
+ console.log(`
560
+ ForgePlatform Commands:
561
+
562
+ forge platform init Scaffold a platform project
563
+ forge platform start Start the platform
564
+ forge platform add <name> Scaffold a new app
565
+ forge platform remove <id> Remove an app
566
+ forge platform status Show per-app status
567
+ forge platform generate Generate nginx + route manifests
568
+ `);
569
+ }
570
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "threadforge",
3
+ "version": "0.1.0",
4
+ "description": "Multi-threaded Node.js service runtime framework",
5
+ "type": "module",
6
+ "bin": {
7
+ "forge": "./bin/forge.js"
8
+ },
9
+ "main": "./src/index.js",
10
+ "exports": {
11
+ ".": "./src/index.js",
12
+ "./service": "./src/services/Service.js",
13
+ "./config": "./src/core/config.js",
14
+ "./decorators": "./src/decorators/index.js",
15
+ "./plugins": "./src/plugins/index.js",
16
+ "./plugins/redis": "./src/plugins/redis.js",
17
+ "./plugins/postgres": "./src/plugins/postgres.js",
18
+ "./ingress": "./src/core/Ingress.js",
19
+ "./frontend": "./src/frontend/index.js",
20
+ "./frontend/plugins": "./src/frontend/plugins/index.js",
21
+ "./internals": "./src/internals.js"
22
+ },
23
+ "scripts": {
24
+ "dev": "node bin/forge.js dev",
25
+ "start": "node bin/forge.js start",
26
+ "test": "node --test tests/*.test.js",
27
+ "test:watch": "node --test --watch tests/*.test.js",
28
+ "test:coverage": "node --experimental-test-coverage --test tests/*.test.js",
29
+ "lint": "biome check ."
30
+ },
31
+ "keywords": [
32
+ "microservices",
33
+ "multi-threaded",
34
+ "cluster",
35
+ "multi-process",
36
+ "ipc",
37
+ "service-mesh",
38
+ "service-runtime"
39
+ ],
40
+ "author": "ThreadForge Contributors",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "https://github.com/ChrisBland/threadforge.git"
45
+ },
46
+ "homepage": "https://github.com/ChrisBland/threadforge#readme",
47
+ "bugs": {
48
+ "url": "https://github.com/ChrisBland/threadforge/issues"
49
+ },
50
+ "files": [
51
+ "src/",
52
+ "bin/",
53
+ "shared/",
54
+ "README.md",
55
+ "LICENSE"
56
+ ],
57
+ "engines": {
58
+ "node": ">=20.0.0"
59
+ },
60
+ "peerDependencies": {
61
+ "ioredis": "^5.3.0",
62
+ "pg": "^8.11.0"
63
+ },
64
+ "peerDependenciesMeta": {
65
+ "ioredis": { "optional": true },
66
+ "pg": { "optional": true }
67
+ },
68
+ "devDependencies": {
69
+ "@biomejs/biome": "^2.3.15"
70
+ }
71
+ }