@way_marks/server 3.0.0 → 4.0.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.
package/dist/api/server.js
CHANGED
|
@@ -60,6 +60,42 @@ function getRegistryProjects() {
|
|
|
60
60
|
return [];
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
function readRegistry() {
|
|
64
|
+
if (!fs.existsSync(registryPath))
|
|
65
|
+
return { projects: {}, releasedPorts: [] };
|
|
66
|
+
try {
|
|
67
|
+
const r = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
68
|
+
return { projects: r.projects || {}, releasedPorts: r.releasedPorts || [], lastUpdated: r.lastUpdated };
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { projects: {}, releasedPorts: [] };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function writeRegistry(reg) {
|
|
75
|
+
reg.lastUpdated = new Date().toISOString();
|
|
76
|
+
fs.writeFileSync(registryPath, JSON.stringify({ version: 1, ...reg }, null, 2) + '\n');
|
|
77
|
+
}
|
|
78
|
+
function mutateRegistryEntry(id, mutator) {
|
|
79
|
+
const reg = readRegistry();
|
|
80
|
+
const entry = reg.projects[id];
|
|
81
|
+
if (!entry)
|
|
82
|
+
return null;
|
|
83
|
+
mutator(entry);
|
|
84
|
+
reg.projects[id] = entry;
|
|
85
|
+
writeRegistry(reg);
|
|
86
|
+
return entry;
|
|
87
|
+
}
|
|
88
|
+
function tryKill(pid) {
|
|
89
|
+
if (!pid)
|
|
90
|
+
return false;
|
|
91
|
+
try {
|
|
92
|
+
process.kill(pid, 'SIGTERM');
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
63
99
|
// Phase 4: Garbage collection for registry
|
|
64
100
|
function garbageCollectRegistryFile() {
|
|
65
101
|
try {
|
|
@@ -94,9 +130,26 @@ function garbageCollectRegistryFile() {
|
|
|
94
130
|
}
|
|
95
131
|
}
|
|
96
132
|
const app = (0, express_1.default)();
|
|
97
|
-
|
|
133
|
+
// Fallback only — `waymark start` always passes WAYMARK_PORT explicitly.
|
|
134
|
+
// 47000 is the new default range (47000-47999); 3001 was the legacy default.
|
|
135
|
+
const PORT = parseInt(process.env.WAYMARK_PORT || '47000', 10);
|
|
98
136
|
app.use(express_1.default.json());
|
|
99
137
|
app.use(express_1.default.urlencoded({ extended: true }));
|
|
138
|
+
// Same-machine peer CORS for the Hub view: another Waymark dashboard on a
|
|
139
|
+
// different localhost port may probe this server's /api/* (e.g. /api/stats,
|
|
140
|
+
// /api/hub/*). Allow it without opening up arbitrary remote origins.
|
|
141
|
+
app.use((req, res, next) => {
|
|
142
|
+
const origin = req.headers.origin;
|
|
143
|
+
if (typeof origin === 'string' && /^http:\/\/(localhost|127\.0\.0\.1):\d+$/.test(origin)) {
|
|
144
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
145
|
+
res.setHeader('Vary', 'Origin');
|
|
146
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
|
|
147
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
148
|
+
if (req.method === 'OPTIONS')
|
|
149
|
+
return res.sendStatus(204);
|
|
150
|
+
}
|
|
151
|
+
next();
|
|
152
|
+
});
|
|
100
153
|
// Serve UI — path works for both ts-node (src/api/) and compiled (dist/api/).
|
|
101
154
|
const UI_DIR = path.resolve(__dirname, '../../src/ui-dist');
|
|
102
155
|
const UI_INDEX = path.join(UI_DIR, 'index.html');
|
|
@@ -447,15 +500,22 @@ app.get('/api/config', (req, res) => {
|
|
|
447
500
|
res.status(500).json({ error: err.message });
|
|
448
501
|
}
|
|
449
502
|
});
|
|
450
|
-
// GET /api/project — returns project metadata
|
|
451
|
-
|
|
503
|
+
// GET /api/project — returns live project metadata for the running server.
|
|
504
|
+
// Source of truth: .waymark/config.json (written by `waymark start`).
|
|
505
|
+
// Falls back to env-only state when running stand-alone (no .waymark/ yet).
|
|
506
|
+
app.get('/api/project', (_req, res) => {
|
|
452
507
|
try {
|
|
453
|
-
const
|
|
508
|
+
const projectRoot = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
509
|
+
const configPath = path.join(projectRoot, '.waymark', 'config.json');
|
|
454
510
|
if (!fs.existsSync(configPath)) {
|
|
455
|
-
return res.json({ projectName: null, port: PORT });
|
|
511
|
+
return res.json({ projectName: null, port: PORT, projectRoot });
|
|
456
512
|
}
|
|
457
513
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
458
|
-
res.json({
|
|
514
|
+
res.json({
|
|
515
|
+
projectName: cfg.projectName || null,
|
|
516
|
+
port: cfg.port || PORT,
|
|
517
|
+
projectRoot: cfg.projectRoot || projectRoot,
|
|
518
|
+
});
|
|
459
519
|
}
|
|
460
520
|
catch (err) {
|
|
461
521
|
res.status(500).json({ error: err.message });
|
|
@@ -482,6 +542,86 @@ app.post('/api/registry/cleanup', (req, res) => {
|
|
|
482
542
|
}
|
|
483
543
|
});
|
|
484
544
|
// ============================================================================
|
|
545
|
+
// Hub: cross-project mutations driven from any peer's dashboard.
|
|
546
|
+
// All write to ~/.waymark/registry.json the same way the CLI does.
|
|
547
|
+
// ============================================================================
|
|
548
|
+
// POST /api/hub/projects/:id/pause — flip status to paused (port retained)
|
|
549
|
+
app.post('/api/hub/projects/:id/pause', (req, res) => {
|
|
550
|
+
try {
|
|
551
|
+
const { id } = req.params;
|
|
552
|
+
const updated = mutateRegistryEntry(id, (e) => {
|
|
553
|
+
e.status = 'paused';
|
|
554
|
+
e.pausedAt = new Date().toISOString();
|
|
555
|
+
});
|
|
556
|
+
if (!updated)
|
|
557
|
+
return res.status(404).json({ error: `Project not found: ${id}` });
|
|
558
|
+
res.json({ success: true, project: updated });
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
res.status(500).json({ error: err.message });
|
|
562
|
+
}
|
|
563
|
+
});
|
|
564
|
+
// POST /api/hub/projects/:id/resume — flip status back to running
|
|
565
|
+
app.post('/api/hub/projects/:id/resume', (req, res) => {
|
|
566
|
+
try {
|
|
567
|
+
const { id } = req.params;
|
|
568
|
+
const updated = mutateRegistryEntry(id, (e) => {
|
|
569
|
+
e.status = 'running';
|
|
570
|
+
delete e.pausedAt;
|
|
571
|
+
});
|
|
572
|
+
if (!updated)
|
|
573
|
+
return res.status(404).json({ error: `Project not found: ${id}` });
|
|
574
|
+
res.json({ success: true, project: updated });
|
|
575
|
+
}
|
|
576
|
+
catch (err) {
|
|
577
|
+
res.status(500).json({ error: err.message });
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
// POST /api/hub/projects/:id/stop — SIGTERM the peer's mcp+api, mark stopped,
|
|
581
|
+
// release the port for reuse. Mirrors `waymark stop` behaviour without needing
|
|
582
|
+
// the user to cd into the other project.
|
|
583
|
+
app.post('/api/hub/projects/:id/stop', (req, res) => {
|
|
584
|
+
try {
|
|
585
|
+
const { id } = req.params;
|
|
586
|
+
const reg = readRegistry();
|
|
587
|
+
const entry = reg.projects[id];
|
|
588
|
+
if (!entry)
|
|
589
|
+
return res.status(404).json({ error: `Project not found: ${id}` });
|
|
590
|
+
const killedApi = tryKill(entry.api_pid);
|
|
591
|
+
const killedMcp = tryKill(entry.mcp_pid);
|
|
592
|
+
entry.status = 'stopped';
|
|
593
|
+
entry.stoppedAt = new Date().toISOString();
|
|
594
|
+
reg.projects[id] = entry;
|
|
595
|
+
// Release port for reuse (mirrors registry.releasePort behaviour).
|
|
596
|
+
if (entry.port && Array.isArray(reg.releasedPorts)) {
|
|
597
|
+
reg.releasedPorts.push(entry.port);
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
reg.releasedPorts = [entry.port];
|
|
601
|
+
}
|
|
602
|
+
writeRegistry(reg);
|
|
603
|
+
res.json({
|
|
604
|
+
success: true,
|
|
605
|
+
project: entry,
|
|
606
|
+
killed: { api: killedApi, mcp: killedMcp },
|
|
607
|
+
message: killedApi || killedMcp ? `Stopped ${id}.` : `${id} was not running (registry cleaned).`,
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
res.status(500).json({ error: err.message });
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
// POST /api/hub/gc — alias of /api/registry/cleanup; convenient from the Hub UI.
|
|
615
|
+
app.post('/api/hub/gc', (_req, res) => {
|
|
616
|
+
try {
|
|
617
|
+
const removed = garbageCollectRegistryFile();
|
|
618
|
+
res.json({ success: true, removed, message: `Garbage collected ${removed} stale entries` });
|
|
619
|
+
}
|
|
620
|
+
catch (err) {
|
|
621
|
+
res.status(500).json({ error: err.message });
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
// ============================================================================
|
|
485
625
|
// PHASE 2: Team Approval Routing Endpoints
|
|
486
626
|
// ============================================================================
|
|
487
627
|
// GET /api/team/members — list all team members
|