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.
- package/LICENSE +21 -0
- package/README.md +152 -0
- package/bin/forge.js +1050 -0
- package/bin/host-commands.js +344 -0
- package/bin/platform-commands.js +570 -0
- package/package.json +71 -0
- package/shared/auth.js +475 -0
- package/src/core/DirectMessageBus.js +364 -0
- package/src/core/EndpointResolver.js +247 -0
- package/src/core/ForgeContext.js +2227 -0
- package/src/core/ForgeHost.js +122 -0
- package/src/core/ForgePlatform.js +145 -0
- package/src/core/Ingress.js +768 -0
- package/src/core/Interceptors.js +420 -0
- package/src/core/MessageBus.js +310 -0
- package/src/core/Prometheus.js +305 -0
- package/src/core/RequestContext.js +413 -0
- package/src/core/RoutingStrategy.js +316 -0
- package/src/core/Supervisor.js +1306 -0
- package/src/core/ThreadAllocator.js +196 -0
- package/src/core/WorkerChannelManager.js +879 -0
- package/src/core/config.js +624 -0
- package/src/core/host-config.js +311 -0
- package/src/core/network-utils.js +166 -0
- package/src/core/platform-config.js +308 -0
- package/src/decorators/ServiceProxy.js +899 -0
- package/src/decorators/index.js +571 -0
- package/src/deploy/NginxGenerator.js +865 -0
- package/src/deploy/PlatformManifestGenerator.js +96 -0
- package/src/deploy/RouteManifestGenerator.js +112 -0
- package/src/deploy/index.js +984 -0
- package/src/frontend/FrontendDevLifecycle.js +65 -0
- package/src/frontend/FrontendPluginOrchestrator.js +187 -0
- package/src/frontend/SiteResolver.js +63 -0
- package/src/frontend/StaticMountRegistry.js +90 -0
- package/src/frontend/index.js +5 -0
- package/src/frontend/plugins/index.js +2 -0
- package/src/frontend/plugins/viteFrontend.js +79 -0
- package/src/frontend/types.js +35 -0
- package/src/index.js +56 -0
- package/src/internals.js +31 -0
- package/src/plugins/PluginManager.js +537 -0
- package/src/plugins/ScopedPostgres.js +192 -0
- package/src/plugins/ScopedRedis.js +142 -0
- package/src/plugins/index.js +1729 -0
- package/src/registry/ServiceRegistry.js +796 -0
- package/src/scaling/ScaleAdvisor.js +442 -0
- package/src/services/Service.js +195 -0
- package/src/services/worker-bootstrap.js +676 -0
- package/src/templates/auth-service.js +65 -0
- 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
|
+
}
|