@tekyzinc/gsd-t 2.50.12 → 2.53.10
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/CHANGELOG.md +24 -0
- package/README.md +379 -372
- package/bin/component-registry.js +250 -0
- package/bin/graph-cgc.js +510 -510
- package/bin/graph-indexer.js +147 -147
- package/bin/graph-overlay.js +195 -195
- package/bin/graph-parsers.js +327 -327
- package/bin/graph-query.js +453 -452
- package/bin/graph-store.js +154 -154
- package/bin/qa-calibrator.js +194 -0
- package/bin/scan-data-collector.js +153 -153
- package/bin/scan-diagrams-generators.js +187 -187
- package/bin/scan-diagrams.js +79 -79
- package/bin/scan-renderer.js +92 -92
- package/bin/scan-report-sections.js +121 -121
- package/bin/scan-report.js +184 -184
- package/bin/scan-schema-parsers.js +199 -199
- package/bin/scan-schema.js +103 -103
- package/bin/token-budget.js +246 -0
- package/commands/Claude-md.md +10 -10
- package/commands/branch.md +15 -15
- package/commands/checkin.md +45 -45
- package/commands/global-change.md +209 -209
- package/commands/gsd-t-audit.md +199 -0
- package/commands/gsd-t-backlog-add.md +94 -94
- package/commands/gsd-t-backlog-edit.md +111 -111
- package/commands/gsd-t-backlog-list.md +63 -63
- package/commands/gsd-t-backlog-move.md +94 -94
- package/commands/gsd-t-backlog-promote.md +123 -123
- package/commands/gsd-t-backlog-remove.md +86 -86
- package/commands/gsd-t-backlog-settings.md +158 -158
- package/commands/gsd-t-complete-milestone.md +528 -515
- package/commands/gsd-t-debug.md +506 -399
- package/commands/gsd-t-discuss.md +174 -174
- package/commands/gsd-t-execute.md +758 -634
- package/commands/gsd-t-feature.md +276 -276
- package/commands/gsd-t-health.md +142 -142
- package/commands/gsd-t-help.md +465 -457
- package/commands/gsd-t-impact.md +302 -302
- package/commands/gsd-t-init.md +320 -280
- package/commands/gsd-t-integrate.md +365 -249
- package/commands/gsd-t-milestone.md +87 -87
- package/commands/gsd-t-partition.md +442 -361
- package/commands/gsd-t-pause.md +82 -82
- package/commands/gsd-t-plan.md +345 -344
- package/commands/gsd-t-populate.md +111 -111
- package/commands/gsd-t-prd.md +326 -326
- package/commands/gsd-t-project.md +211 -211
- package/commands/gsd-t-promote-debt.md +123 -123
- package/commands/gsd-t-prompt.md +137 -137
- package/commands/gsd-t-qa.md +266 -266
- package/commands/gsd-t-quick.md +357 -234
- package/commands/gsd-t-reflect.md +134 -134
- package/commands/gsd-t-resume.md +72 -72
- package/commands/gsd-t-scan.md +615 -615
- package/commands/gsd-t-setup.md +76 -0
- package/commands/gsd-t-status.md +192 -166
- package/commands/gsd-t-test-sync.md +381 -381
- package/commands/gsd-t-triage-and-merge.md +171 -171
- package/commands/gsd-t-verify.md +382 -382
- package/commands/gsd-t-visualize.md +118 -118
- package/commands/gsd-t-wave.md +401 -378
- package/docs/GSD-T-README.md +425 -422
- package/docs/architecture.md +385 -369
- package/docs/harness-design-analysis.md +371 -0
- package/docs/infrastructure.md +205 -205
- package/docs/prd-graph-engine.md +398 -398
- package/docs/prd-gsd2-hybrid.md +559 -559
- package/docs/prd-harness-evolution.md +583 -0
- package/docs/requirements.md +14 -0
- package/docs/workflows.md +226 -226
- package/examples/.gsd-t/domains/example-domain/scope.md +13 -13
- package/package.json +40 -40
- package/scripts/gsd-t-auto-route.js +39 -39
- package/scripts/gsd-t-dashboard-mockup.html +1143 -1143
- package/scripts/gsd-t-dashboard-server.js +171 -171
- package/scripts/gsd-t-dashboard.html +262 -262
- package/scripts/gsd-t-event-writer.js +128 -128
- package/scripts/gsd-t-statusline.js +94 -94
- package/scripts/gsd-t-tools.js +175 -175
- package/templates/CLAUDE-global.md +639 -614
- package/templates/CLAUDE-project.md +24 -0
- package/templates/backlog-settings.md +18 -18
- package/templates/backlog.md +1 -1
- package/templates/progress.md +40 -40
- package/templates/shared-services-contract.md +60 -60
- package/templates/stacks/desktop.ini +2 -2
- package/bin/desktop.ini +0 -2
- package/commands/desktop.ini +0 -2
- package/docs/ci-examples/desktop.ini +0 -2
- package/docs/desktop.ini +0 -2
- package/examples/.gsd-t/contracts/desktop.ini +0 -2
- package/examples/.gsd-t/desktop.ini +0 -2
- package/examples/.gsd-t/domains/desktop.ini +0 -2
- package/examples/.gsd-t/domains/example-domain/desktop.ini +0 -2
- package/examples/desktop.ini +0 -2
- package/examples/rules/desktop.ini +0 -2
- package/scripts/desktop.ini +0 -2
- package/templates/desktop.ini +0 -2
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* GSD-T Dashboard Server — Zero-dep SSE server for .gsd-t/events/*.jsonl
|
|
4
|
-
* Serves gsd-t-dashboard.html and streams events to browser clients.
|
|
5
|
-
*/
|
|
6
|
-
const http = require("http");
|
|
7
|
-
const fs = require("fs");
|
|
8
|
-
const path = require("path");
|
|
9
|
-
const { spawn } = require("child_process");
|
|
10
|
-
|
|
11
|
-
const DEFAULT_PORT = 7433;
|
|
12
|
-
const MAX_EVENTS = 500;
|
|
13
|
-
const KEEPALIVE_MS = 15000;
|
|
14
|
-
const SSE_HEADERS = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*" };
|
|
15
|
-
|
|
16
|
-
function parseEventLine(line) {
|
|
17
|
-
if (!line || !line.trim()) return null;
|
|
18
|
-
try { return JSON.parse(line.trim()); } catch { return null; }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function findEventsDir(projectDir) {
|
|
22
|
-
return path.join(projectDir || process.cwd(), ".gsd-t", "events");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function safeReadJsonl(filePath) {
|
|
26
|
-
try { if (fs.lstatSync(filePath).isSymbolicLink()) return []; } catch { /* safe */ }
|
|
27
|
-
try { return fs.readFileSync(filePath, "utf8").split("\n").map(parseEventLine).filter(Boolean); } catch { return []; }
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function readExistingEvents(eventsDir, maxEvents) {
|
|
31
|
-
const limit = maxEvents || MAX_EVENTS;
|
|
32
|
-
if (!eventsDir) return [];
|
|
33
|
-
try { fs.accessSync(eventsDir); } catch { return []; }
|
|
34
|
-
let files;
|
|
35
|
-
try { files = fs.readdirSync(eventsDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); } catch { return []; }
|
|
36
|
-
const results = [];
|
|
37
|
-
for (const f of files) {
|
|
38
|
-
if (results.length >= limit) break;
|
|
39
|
-
safeReadJsonl(path.join(eventsDir, f)).forEach((e) => { if (results.length < limit) results.push(e); });
|
|
40
|
-
}
|
|
41
|
-
return results;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function tailEventsFile(filePath, callback) {
|
|
45
|
-
let offset = 0;
|
|
46
|
-
try { offset = fs.statSync(filePath).size; } catch { /* new file */ }
|
|
47
|
-
function processNewData() {
|
|
48
|
-
try { if (fs.lstatSync(filePath).isSymbolicLink()) return; } catch { return; }
|
|
49
|
-
let stat;
|
|
50
|
-
try { stat = fs.statSync(filePath); } catch { return; }
|
|
51
|
-
if (stat.size <= offset) return;
|
|
52
|
-
const fd = fs.openSync(filePath, "r");
|
|
53
|
-
let chunk;
|
|
54
|
-
try {
|
|
55
|
-
const buf = Buffer.alloc(stat.size - offset);
|
|
56
|
-
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
57
|
-
chunk = buf.toString("utf8");
|
|
58
|
-
offset = stat.size;
|
|
59
|
-
} finally { fs.closeSync(fd); }
|
|
60
|
-
chunk.split("\n").forEach((line) => { const obj = parseEventLine(line); if (obj) callback(obj); });
|
|
61
|
-
}
|
|
62
|
-
fs.watchFile(filePath, { interval: 500, persistent: true }, processNewData);
|
|
63
|
-
return () => fs.unwatchFile(filePath, processNewData);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function handleRoot(req, res, htmlPath) {
|
|
67
|
-
fs.readFile(htmlPath, (err, data) => {
|
|
68
|
-
if (err) { res.writeHead(404); res.end("Not found"); return; }
|
|
69
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
70
|
-
res.end(data);
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function handlePing(req, res, port) {
|
|
75
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
76
|
-
res.end(JSON.stringify({ status: "ok", port }));
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function handleStop(req, res, server) {
|
|
80
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
-
res.end(JSON.stringify({ status: "stopping" }));
|
|
82
|
-
setImmediate(() => server.close());
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function getNewestJsonl(eventsDir) {
|
|
86
|
-
try { const files = fs.readdirSync(eventsDir).filter((f) => f.endsWith(".jsonl")).sort(); return files.length ? path.join(eventsDir, files[files.length - 1]) : null; } catch { return null; }
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function handleEvents(req, res, eventsDir) {
|
|
90
|
-
res.writeHead(200, SSE_HEADERS);
|
|
91
|
-
readExistingEvents(eventsDir, MAX_EVENTS).forEach((e) => { try { res.write("data: " + JSON.stringify(e) + "\n\n"); } catch { /* gone */ } });
|
|
92
|
-
const sendEvent = (obj) => { try { res.write("data: " + JSON.stringify(obj) + "\n\n"); } catch { /* gone */ } };
|
|
93
|
-
let watchedFile = getNewestJsonl(eventsDir);
|
|
94
|
-
let unwatchFile = watchedFile ? tailEventsFile(watchedFile, sendEvent) : null;
|
|
95
|
-
// Watch events directory for new JSONL files (e.g., after midnight date rollover)
|
|
96
|
-
let dirWatcher = null;
|
|
97
|
-
try {
|
|
98
|
-
dirWatcher = fs.watch(eventsDir, (eventType, filename) => {
|
|
99
|
-
if (!filename || !filename.endsWith(".jsonl")) return;
|
|
100
|
-
const newFile = getNewestJsonl(eventsDir);
|
|
101
|
-
if (newFile && newFile !== watchedFile) {
|
|
102
|
-
if (unwatchFile) unwatchFile();
|
|
103
|
-
watchedFile = newFile;
|
|
104
|
-
unwatchFile = tailEventsFile(watchedFile, sendEvent);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
} catch { /* eventsDir may not exist yet */ }
|
|
108
|
-
const timer = setInterval(() => { try { res.write(": keepalive\n\n"); } catch { clearInterval(timer); } }, KEEPALIVE_MS);
|
|
109
|
-
req.on("close", () => { clearInterval(timer); if (unwatchFile) unwatchFile(); if (dirWatcher) dirWatcher.close(); });
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
function readMetricsData(metricsDir) {
|
|
113
|
-
const taskFile = path.join(metricsDir, "task-metrics.jsonl");
|
|
114
|
-
const rollupFile = path.join(metricsDir, "rollup.jsonl");
|
|
115
|
-
const taskMetrics = fs.existsSync(taskFile) ? safeReadJsonl(taskFile) : [];
|
|
116
|
-
const rollups = fs.existsSync(rollupFile) ? safeReadJsonl(rollupFile) : [];
|
|
117
|
-
return { taskMetrics, rollups };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function handleMetrics(req, res, projectDir) {
|
|
121
|
-
const metricsDir = path.join(projectDir, ".gsd-t", "metrics");
|
|
122
|
-
const data = readMetricsData(metricsDir);
|
|
123
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
124
|
-
res.end(JSON.stringify(data));
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function startServer(port, eventsDir, htmlPath, projectDir) {
|
|
128
|
-
const projDir = projectDir || path.resolve(eventsDir, "..", "..");
|
|
129
|
-
const server = http.createServer((req, res) => {
|
|
130
|
-
const url = req.url.split("?")[0];
|
|
131
|
-
if (url === "/" || url === "") return handleRoot(req, res, htmlPath);
|
|
132
|
-
if (url === "/events") return handleEvents(req, res, eventsDir);
|
|
133
|
-
if (url === "/metrics") return handleMetrics(req, res, projDir);
|
|
134
|
-
if (url === "/ping") return handlePing(req, res, port);
|
|
135
|
-
if (url === "/stop") return handleStop(req, res, server);
|
|
136
|
-
res.writeHead(404); res.end("Not found");
|
|
137
|
-
});
|
|
138
|
-
server.listen(port);
|
|
139
|
-
return { server, url: `http://localhost:${port}` };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
module.exports = { startServer, tailEventsFile, readExistingEvents, parseEventLine, findEventsDir, readMetricsData };
|
|
143
|
-
|
|
144
|
-
if (require.main === module) {
|
|
145
|
-
const argv = process.argv.slice(2);
|
|
146
|
-
const getArg = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
|
|
147
|
-
const hasFlag = (f) => argv.includes(f);
|
|
148
|
-
const port = parseInt(getArg("--port") || DEFAULT_PORT, 10);
|
|
149
|
-
const projectDir = process.env.GSD_T_PROJECT_DIR || process.cwd();
|
|
150
|
-
const eventsDir = getArg("--events") || findEventsDir(projectDir);
|
|
151
|
-
const pidFile = path.join(projectDir, ".gsd-t", "dashboard.pid");
|
|
152
|
-
const htmlPath = path.join(__dirname, "gsd-t-dashboard.html");
|
|
153
|
-
|
|
154
|
-
if (hasFlag("--stop")) {
|
|
155
|
-
try { const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); process.kill(pid); fs.unlinkSync(pidFile); }
|
|
156
|
-
catch (e) { process.stderr.write("No running server: " + e.message + "\n"); }
|
|
157
|
-
process.exit(0);
|
|
158
|
-
}
|
|
159
|
-
if (hasFlag("--detach")) {
|
|
160
|
-
const child = spawn(process.execPath, [__filename, ...argv.filter((a) => a !== "--detach")], { detached: true, stdio: "ignore" });
|
|
161
|
-
child.unref();
|
|
162
|
-
try { fs.mkdirSync(path.dirname(pidFile), { recursive: true }); } catch { /* exists */ }
|
|
163
|
-
fs.writeFileSync(pidFile, String(child.pid));
|
|
164
|
-
process.exit(0);
|
|
165
|
-
}
|
|
166
|
-
const { server, url } = startServer(port, eventsDir, htmlPath);
|
|
167
|
-
process.stdout.write("GSD-T Dashboard: " + url + "\n");
|
|
168
|
-
function cleanup() { try { fs.unlinkSync(pidFile); } catch { /* ok */ } server.close(() => process.exit(0)); }
|
|
169
|
-
process.on("SIGTERM", cleanup);
|
|
170
|
-
process.on("SIGINT", cleanup);
|
|
171
|
-
}
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GSD-T Dashboard Server — Zero-dep SSE server for .gsd-t/events/*.jsonl
|
|
4
|
+
* Serves gsd-t-dashboard.html and streams events to browser clients.
|
|
5
|
+
*/
|
|
6
|
+
const http = require("http");
|
|
7
|
+
const fs = require("fs");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawn } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const DEFAULT_PORT = 7433;
|
|
12
|
+
const MAX_EVENTS = 500;
|
|
13
|
+
const KEEPALIVE_MS = 15000;
|
|
14
|
+
const SSE_HEADERS = { "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", "Access-Control-Allow-Origin": "*" };
|
|
15
|
+
|
|
16
|
+
function parseEventLine(line) {
|
|
17
|
+
if (!line || !line.trim()) return null;
|
|
18
|
+
try { return JSON.parse(line.trim()); } catch { return null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function findEventsDir(projectDir) {
|
|
22
|
+
return path.join(projectDir || process.cwd(), ".gsd-t", "events");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function safeReadJsonl(filePath) {
|
|
26
|
+
try { if (fs.lstatSync(filePath).isSymbolicLink()) return []; } catch { /* safe */ }
|
|
27
|
+
try { return fs.readFileSync(filePath, "utf8").split("\n").map(parseEventLine).filter(Boolean); } catch { return []; }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function readExistingEvents(eventsDir, maxEvents) {
|
|
31
|
+
const limit = maxEvents || MAX_EVENTS;
|
|
32
|
+
if (!eventsDir) return [];
|
|
33
|
+
try { fs.accessSync(eventsDir); } catch { return []; }
|
|
34
|
+
let files;
|
|
35
|
+
try { files = fs.readdirSync(eventsDir).filter((f) => f.endsWith(".jsonl")).sort().reverse(); } catch { return []; }
|
|
36
|
+
const results = [];
|
|
37
|
+
for (const f of files) {
|
|
38
|
+
if (results.length >= limit) break;
|
|
39
|
+
safeReadJsonl(path.join(eventsDir, f)).forEach((e) => { if (results.length < limit) results.push(e); });
|
|
40
|
+
}
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function tailEventsFile(filePath, callback) {
|
|
45
|
+
let offset = 0;
|
|
46
|
+
try { offset = fs.statSync(filePath).size; } catch { /* new file */ }
|
|
47
|
+
function processNewData() {
|
|
48
|
+
try { if (fs.lstatSync(filePath).isSymbolicLink()) return; } catch { return; }
|
|
49
|
+
let stat;
|
|
50
|
+
try { stat = fs.statSync(filePath); } catch { return; }
|
|
51
|
+
if (stat.size <= offset) return;
|
|
52
|
+
const fd = fs.openSync(filePath, "r");
|
|
53
|
+
let chunk;
|
|
54
|
+
try {
|
|
55
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
56
|
+
fs.readSync(fd, buf, 0, buf.length, offset);
|
|
57
|
+
chunk = buf.toString("utf8");
|
|
58
|
+
offset = stat.size;
|
|
59
|
+
} finally { fs.closeSync(fd); }
|
|
60
|
+
chunk.split("\n").forEach((line) => { const obj = parseEventLine(line); if (obj) callback(obj); });
|
|
61
|
+
}
|
|
62
|
+
fs.watchFile(filePath, { interval: 500, persistent: true }, processNewData);
|
|
63
|
+
return () => fs.unwatchFile(filePath, processNewData);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function handleRoot(req, res, htmlPath) {
|
|
67
|
+
fs.readFile(htmlPath, (err, data) => {
|
|
68
|
+
if (err) { res.writeHead(404); res.end("Not found"); return; }
|
|
69
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
70
|
+
res.end(data);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handlePing(req, res, port) {
|
|
75
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
76
|
+
res.end(JSON.stringify({ status: "ok", port }));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleStop(req, res, server) {
|
|
80
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
81
|
+
res.end(JSON.stringify({ status: "stopping" }));
|
|
82
|
+
setImmediate(() => server.close());
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getNewestJsonl(eventsDir) {
|
|
86
|
+
try { const files = fs.readdirSync(eventsDir).filter((f) => f.endsWith(".jsonl")).sort(); return files.length ? path.join(eventsDir, files[files.length - 1]) : null; } catch { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleEvents(req, res, eventsDir) {
|
|
90
|
+
res.writeHead(200, SSE_HEADERS);
|
|
91
|
+
readExistingEvents(eventsDir, MAX_EVENTS).forEach((e) => { try { res.write("data: " + JSON.stringify(e) + "\n\n"); } catch { /* gone */ } });
|
|
92
|
+
const sendEvent = (obj) => { try { res.write("data: " + JSON.stringify(obj) + "\n\n"); } catch { /* gone */ } };
|
|
93
|
+
let watchedFile = getNewestJsonl(eventsDir);
|
|
94
|
+
let unwatchFile = watchedFile ? tailEventsFile(watchedFile, sendEvent) : null;
|
|
95
|
+
// Watch events directory for new JSONL files (e.g., after midnight date rollover)
|
|
96
|
+
let dirWatcher = null;
|
|
97
|
+
try {
|
|
98
|
+
dirWatcher = fs.watch(eventsDir, (eventType, filename) => {
|
|
99
|
+
if (!filename || !filename.endsWith(".jsonl")) return;
|
|
100
|
+
const newFile = getNewestJsonl(eventsDir);
|
|
101
|
+
if (newFile && newFile !== watchedFile) {
|
|
102
|
+
if (unwatchFile) unwatchFile();
|
|
103
|
+
watchedFile = newFile;
|
|
104
|
+
unwatchFile = tailEventsFile(watchedFile, sendEvent);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
} catch { /* eventsDir may not exist yet */ }
|
|
108
|
+
const timer = setInterval(() => { try { res.write(": keepalive\n\n"); } catch { clearInterval(timer); } }, KEEPALIVE_MS);
|
|
109
|
+
req.on("close", () => { clearInterval(timer); if (unwatchFile) unwatchFile(); if (dirWatcher) dirWatcher.close(); });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function readMetricsData(metricsDir) {
|
|
113
|
+
const taskFile = path.join(metricsDir, "task-metrics.jsonl");
|
|
114
|
+
const rollupFile = path.join(metricsDir, "rollup.jsonl");
|
|
115
|
+
const taskMetrics = fs.existsSync(taskFile) ? safeReadJsonl(taskFile) : [];
|
|
116
|
+
const rollups = fs.existsSync(rollupFile) ? safeReadJsonl(rollupFile) : [];
|
|
117
|
+
return { taskMetrics, rollups };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function handleMetrics(req, res, projectDir) {
|
|
121
|
+
const metricsDir = path.join(projectDir, ".gsd-t", "metrics");
|
|
122
|
+
const data = readMetricsData(metricsDir);
|
|
123
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
124
|
+
res.end(JSON.stringify(data));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function startServer(port, eventsDir, htmlPath, projectDir) {
|
|
128
|
+
const projDir = projectDir || path.resolve(eventsDir, "..", "..");
|
|
129
|
+
const server = http.createServer((req, res) => {
|
|
130
|
+
const url = req.url.split("?")[0];
|
|
131
|
+
if (url === "/" || url === "") return handleRoot(req, res, htmlPath);
|
|
132
|
+
if (url === "/events") return handleEvents(req, res, eventsDir);
|
|
133
|
+
if (url === "/metrics") return handleMetrics(req, res, projDir);
|
|
134
|
+
if (url === "/ping") return handlePing(req, res, port);
|
|
135
|
+
if (url === "/stop") return handleStop(req, res, server);
|
|
136
|
+
res.writeHead(404); res.end("Not found");
|
|
137
|
+
});
|
|
138
|
+
server.listen(port);
|
|
139
|
+
return { server, url: `http://localhost:${port}` };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { startServer, tailEventsFile, readExistingEvents, parseEventLine, findEventsDir, readMetricsData };
|
|
143
|
+
|
|
144
|
+
if (require.main === module) {
|
|
145
|
+
const argv = process.argv.slice(2);
|
|
146
|
+
const getArg = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
|
|
147
|
+
const hasFlag = (f) => argv.includes(f);
|
|
148
|
+
const port = parseInt(getArg("--port") || DEFAULT_PORT, 10);
|
|
149
|
+
const projectDir = process.env.GSD_T_PROJECT_DIR || process.cwd();
|
|
150
|
+
const eventsDir = getArg("--events") || findEventsDir(projectDir);
|
|
151
|
+
const pidFile = path.join(projectDir, ".gsd-t", "dashboard.pid");
|
|
152
|
+
const htmlPath = path.join(__dirname, "gsd-t-dashboard.html");
|
|
153
|
+
|
|
154
|
+
if (hasFlag("--stop")) {
|
|
155
|
+
try { const pid = parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10); process.kill(pid); fs.unlinkSync(pidFile); }
|
|
156
|
+
catch (e) { process.stderr.write("No running server: " + e.message + "\n"); }
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
if (hasFlag("--detach")) {
|
|
160
|
+
const child = spawn(process.execPath, [__filename, ...argv.filter((a) => a !== "--detach")], { detached: true, stdio: "ignore" });
|
|
161
|
+
child.unref();
|
|
162
|
+
try { fs.mkdirSync(path.dirname(pidFile), { recursive: true }); } catch { /* exists */ }
|
|
163
|
+
fs.writeFileSync(pidFile, String(child.pid));
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
const { server, url } = startServer(port, eventsDir, htmlPath);
|
|
167
|
+
process.stdout.write("GSD-T Dashboard: " + url + "\n");
|
|
168
|
+
function cleanup() { try { fs.unlinkSync(pidFile); } catch { /* ok */ } server.close(() => process.exit(0)); }
|
|
169
|
+
process.on("SIGTERM", cleanup);
|
|
170
|
+
process.on("SIGINT", cleanup);
|
|
171
|
+
}
|