@grainulation/wheat 1.0.3 → 1.0.5
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 +1 -1
- package/README.md +32 -31
- package/bin/wheat.js +63 -40
- package/compiler/detect-sprints.js +108 -66
- package/compiler/generate-manifest.js +116 -69
- package/compiler/wheat-compiler.js +763 -471
- package/lib/compiler.js +11 -6
- package/lib/connect.js +273 -134
- package/lib/defaults.js +32 -0
- package/lib/disconnect.js +61 -40
- package/lib/guard.js +20 -17
- package/lib/index.js +8 -8
- package/lib/init.js +260 -142
- package/lib/install-prompt.js +26 -26
- package/lib/load-claims.js +88 -0
- package/lib/quickstart.js +225 -111
- package/lib/serve-mcp.js +495 -180
- package/lib/server.js +198 -111
- package/lib/stats.js +65 -39
- package/lib/status.js +65 -34
- package/lib/update.js +13 -11
- package/package.json +8 -4
- package/templates/claude.md +31 -17
- package/templates/commands/blind-spot.md +9 -2
- package/templates/commands/brief.md +11 -1
- package/templates/commands/calibrate.md +3 -1
- package/templates/commands/challenge.md +4 -1
- package/templates/commands/connect.md +12 -1
- package/templates/commands/evaluate.md +4 -0
- package/templates/commands/feedback.md +3 -1
- package/templates/commands/handoff.md +11 -7
- package/templates/commands/init.md +4 -1
- package/templates/commands/merge.md +4 -1
- package/templates/commands/next.md +1 -0
- package/templates/commands/present.md +3 -0
- package/templates/commands/prototype.md +2 -0
- package/templates/commands/pull.md +103 -0
- package/templates/commands/replay.md +8 -0
- package/templates/commands/research.md +1 -0
- package/templates/commands/resolve.md +4 -1
- package/templates/commands/status.md +4 -0
- package/templates/commands/sync.md +94 -0
- package/templates/commands/witness.md +6 -2
package/lib/server.js
CHANGED
|
@@ -9,45 +9,78 @@
|
|
|
9
9
|
* wheat serve [--port 9092] [--dir /path/to/sprint]
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import http from
|
|
13
|
-
import fs from
|
|
14
|
-
import path from
|
|
15
|
-
import { execFileSync } from
|
|
16
|
-
import { fileURLToPath } from
|
|
12
|
+
import http from "node:http";
|
|
13
|
+
import fs from "node:fs";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
17
|
|
|
18
18
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
19
|
const __dirname = path.dirname(__filename);
|
|
20
20
|
|
|
21
21
|
// ── Crash handlers ──
|
|
22
|
-
process.on(
|
|
23
|
-
process.stderr.write(
|
|
22
|
+
process.on("uncaughtException", (err) => {
|
|
23
|
+
process.stderr.write(
|
|
24
|
+
`[${new Date().toISOString()}] FATAL: ${err.stack || err}\n`
|
|
25
|
+
);
|
|
24
26
|
process.exit(1);
|
|
25
27
|
});
|
|
26
|
-
process.on(
|
|
27
|
-
process.stderr.write(
|
|
28
|
+
process.on("unhandledRejection", (reason) => {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`[${new Date().toISOString()}] WARN unhandledRejection: ${reason}\n`
|
|
31
|
+
);
|
|
28
32
|
});
|
|
29
33
|
|
|
30
|
-
const PUBLIC_DIR = path.join(__dirname,
|
|
34
|
+
const PUBLIC_DIR = path.join(__dirname, "..", "public");
|
|
31
35
|
|
|
32
36
|
// ── Verbose logging ──────────────────────────────────────────────────────────
|
|
33
37
|
|
|
34
|
-
const verbose = process.argv.includes(
|
|
38
|
+
const verbose = process.argv.includes("--verbose");
|
|
35
39
|
function vlog(...a) {
|
|
36
40
|
if (!verbose) return;
|
|
37
41
|
const ts = new Date().toISOString();
|
|
38
|
-
process.stderr.write(`[${ts}] wheat: ${a.join(
|
|
42
|
+
process.stderr.write(`[${ts}] wheat: ${a.join(" ")}\n`);
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
// ── Routes manifest ──────────────────────────────────────────────────────────
|
|
42
46
|
|
|
43
47
|
const ROUTES = [
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
|
|
48
|
+
{
|
|
49
|
+
method: "GET",
|
|
50
|
+
path: "/events",
|
|
51
|
+
description: "SSE event stream for live sprint updates",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
method: "GET",
|
|
55
|
+
path: "/api/state",
|
|
56
|
+
description: "Current sprint state (claims, compilation, sprints)",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
method: "GET",
|
|
60
|
+
path: "/api/claims",
|
|
61
|
+
description:
|
|
62
|
+
"Claims list with optional ?topic, ?type, ?evidence, ?status filters",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
method: "GET",
|
|
66
|
+
path: "/api/coverage",
|
|
67
|
+
description: "Compilation coverage data",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
method: "GET",
|
|
71
|
+
path: "/api/compilation",
|
|
72
|
+
description: "Full compilation result",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
method: "POST",
|
|
76
|
+
path: "/api/compile",
|
|
77
|
+
description: "Trigger recompilation of claims",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
method: "GET",
|
|
81
|
+
path: "/api/docs",
|
|
82
|
+
description: "This API documentation page",
|
|
83
|
+
},
|
|
51
84
|
];
|
|
52
85
|
|
|
53
86
|
// ── State ─────────────────────────────────────────────────────────────────────
|
|
@@ -65,18 +98,22 @@ const sseClients = new Set();
|
|
|
65
98
|
function broadcast(event) {
|
|
66
99
|
const data = `data: ${JSON.stringify(event)}\n\n`;
|
|
67
100
|
for (const res of sseClients) {
|
|
68
|
-
try {
|
|
101
|
+
try {
|
|
102
|
+
res.write(data);
|
|
103
|
+
} catch {
|
|
104
|
+
sseClients.delete(res);
|
|
105
|
+
}
|
|
69
106
|
}
|
|
70
107
|
}
|
|
71
108
|
|
|
72
109
|
// ── Data loading ──────────────────────────────────────────────────────────────
|
|
73
110
|
|
|
74
111
|
function loadClaims(root) {
|
|
75
|
-
const claimsPath = path.join(root,
|
|
76
|
-
vlog(
|
|
112
|
+
const claimsPath = path.join(root, "claims.json");
|
|
113
|
+
vlog("read", claimsPath);
|
|
77
114
|
if (!fs.existsSync(claimsPath)) return { meta: null, claims: [] };
|
|
78
115
|
try {
|
|
79
|
-
const data = JSON.parse(fs.readFileSync(claimsPath,
|
|
116
|
+
const data = JSON.parse(fs.readFileSync(claimsPath, "utf8"));
|
|
80
117
|
return { meta: data.meta || null, claims: data.claims || [] };
|
|
81
118
|
} catch {
|
|
82
119
|
return { meta: null, claims: [] };
|
|
@@ -84,11 +121,11 @@ function loadClaims(root) {
|
|
|
84
121
|
}
|
|
85
122
|
|
|
86
123
|
function loadCompilation(root) {
|
|
87
|
-
const compilationPath = path.join(root,
|
|
88
|
-
vlog(
|
|
124
|
+
const compilationPath = path.join(root, "compilation.json");
|
|
125
|
+
vlog("read", compilationPath);
|
|
89
126
|
if (!fs.existsSync(compilationPath)) return null;
|
|
90
127
|
try {
|
|
91
|
-
return JSON.parse(fs.readFileSync(compilationPath,
|
|
128
|
+
return JSON.parse(fs.readFileSync(compilationPath, "utf8"));
|
|
92
129
|
} catch {
|
|
93
130
|
return null;
|
|
94
131
|
}
|
|
@@ -96,17 +133,18 @@ function loadCompilation(root) {
|
|
|
96
133
|
|
|
97
134
|
function loadSprints(root) {
|
|
98
135
|
try {
|
|
99
|
-
const compilerDir = path.join(__dirname,
|
|
100
|
-
const mod = path.join(compilerDir,
|
|
136
|
+
const compilerDir = path.join(__dirname, "..", "compiler");
|
|
137
|
+
const mod = path.join(compilerDir, "detect-sprints.js");
|
|
101
138
|
if (!fs.existsSync(mod)) return { sprints: [], active: null };
|
|
102
139
|
|
|
103
|
-
const result = execFileSync(
|
|
104
|
-
timeout: 10000,
|
|
140
|
+
const result = execFileSync("node", [mod, "--json", "--root", root], {
|
|
141
|
+
timeout: 10000,
|
|
142
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
105
143
|
});
|
|
106
144
|
const data = JSON.parse(result.toString());
|
|
107
145
|
return {
|
|
108
146
|
sprints: data.sprints || [],
|
|
109
|
-
active: (data.sprints || []).find(s => s.status ===
|
|
147
|
+
active: (data.sprints || []).find((s) => s.status === "active") || null,
|
|
110
148
|
};
|
|
111
149
|
} catch {
|
|
112
150
|
return { sprints: [], active: null };
|
|
@@ -115,10 +153,16 @@ function loadSprints(root) {
|
|
|
115
153
|
|
|
116
154
|
function runCompile(root) {
|
|
117
155
|
try {
|
|
118
|
-
const compiler = path.join(
|
|
156
|
+
const compiler = path.join(
|
|
157
|
+
__dirname,
|
|
158
|
+
"..",
|
|
159
|
+
"compiler",
|
|
160
|
+
"wheat-compiler.js"
|
|
161
|
+
);
|
|
119
162
|
if (!fs.existsSync(compiler)) return null;
|
|
120
|
-
execFileSync(
|
|
121
|
-
timeout: 30000,
|
|
163
|
+
execFileSync("node", [compiler, "--root", root], {
|
|
164
|
+
timeout: 30000,
|
|
165
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
122
166
|
cwd: root,
|
|
123
167
|
});
|
|
124
168
|
return loadCompilation(root);
|
|
@@ -133,14 +177,14 @@ function refreshState(viewRoot, scanRoot) {
|
|
|
133
177
|
state.sprints = sprintData.sprints;
|
|
134
178
|
state.activeSprint = sprintData.active;
|
|
135
179
|
|
|
136
|
-
if (viewRoot ===
|
|
180
|
+
if (viewRoot === "__all") {
|
|
137
181
|
// Merge claims from all sprints
|
|
138
182
|
let allClaims = [];
|
|
139
183
|
let meta = null;
|
|
140
184
|
for (const s of sprintData.sprints) {
|
|
141
185
|
const sprintRoot = path.resolve(sr, s.path);
|
|
142
186
|
const d = loadClaims(sprintRoot);
|
|
143
|
-
if (s.status ===
|
|
187
|
+
if (s.status === "active" && !meta) meta = d.meta;
|
|
144
188
|
for (const c of d.claims) {
|
|
145
189
|
c._sprint = s.name;
|
|
146
190
|
allClaims.push(c);
|
|
@@ -155,18 +199,18 @@ function refreshState(viewRoot, scanRoot) {
|
|
|
155
199
|
state.claims = claimsData.claims;
|
|
156
200
|
state.compilation = loadCompilation(viewRoot);
|
|
157
201
|
}
|
|
158
|
-
broadcast({ type:
|
|
202
|
+
broadcast({ type: "state", data: state });
|
|
159
203
|
}
|
|
160
204
|
|
|
161
205
|
// ── MIME types ────────────────────────────────────────────────────────────────
|
|
162
206
|
|
|
163
207
|
const MIME = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
208
|
+
".html": "text/html; charset=utf-8",
|
|
209
|
+
".css": "text/css; charset=utf-8",
|
|
210
|
+
".js": "application/javascript; charset=utf-8",
|
|
211
|
+
".json": "application/json; charset=utf-8",
|
|
212
|
+
".svg": "image/svg+xml",
|
|
213
|
+
".png": "image/png",
|
|
170
214
|
};
|
|
171
215
|
|
|
172
216
|
// ── HTTP server ───────────────────────────────────────────────────────────────
|
|
@@ -178,110 +222,129 @@ function createWheatServer(root, port, corsOrigin) {
|
|
|
178
222
|
|
|
179
223
|
// CORS (only when --cors is passed)
|
|
180
224
|
if (corsOrigin) {
|
|
181
|
-
res.setHeader(
|
|
182
|
-
res.setHeader(
|
|
183
|
-
res.setHeader(
|
|
225
|
+
res.setHeader("Access-Control-Allow-Origin", corsOrigin);
|
|
226
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
227
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
184
228
|
}
|
|
185
229
|
|
|
186
|
-
if (req.method ===
|
|
187
|
-
res.writeHead(204);
|
|
230
|
+
if (req.method === "OPTIONS" && corsOrigin) {
|
|
231
|
+
res.writeHead(204);
|
|
232
|
+
res.end();
|
|
233
|
+
return;
|
|
188
234
|
}
|
|
189
235
|
|
|
190
|
-
vlog(
|
|
236
|
+
vlog("request", req.method, url.pathname);
|
|
191
237
|
|
|
192
238
|
// ── API: docs ──
|
|
193
|
-
if (req.method ===
|
|
239
|
+
if (req.method === "GET" && url.pathname === "/api/docs") {
|
|
194
240
|
const html = `<!DOCTYPE html><html><head><title>wheat API</title>
|
|
195
241
|
<style>body{font-family:system-ui;background:#0a0e1a;color:#e8ecf1;max-width:800px;margin:40px auto;padding:0 20px}
|
|
196
242
|
table{width:100%;border-collapse:collapse}th,td{padding:8px 12px;border-bottom:1px solid #1e293b;text-align:left}
|
|
197
243
|
th{color:#9ca3af}code{background:#1e293b;padding:2px 6px;border-radius:4px;font-size:13px}</style></head>
|
|
198
244
|
<body><h1>wheat API</h1><p>${ROUTES.length} endpoints</p>
|
|
199
245
|
<table><tr><th>Method</th><th>Path</th><th>Description</th></tr>
|
|
200
|
-
${ROUTES.map(
|
|
246
|
+
${ROUTES.map(
|
|
247
|
+
(r) =>
|
|
248
|
+
"<tr><td><code>" +
|
|
249
|
+
r.method +
|
|
250
|
+
"</code></td><td><code>" +
|
|
251
|
+
r.path +
|
|
252
|
+
"</code></td><td>" +
|
|
253
|
+
r.description +
|
|
254
|
+
"</td></tr>"
|
|
255
|
+
).join("")}
|
|
201
256
|
</table></body></html>`;
|
|
202
|
-
res.writeHead(200, {
|
|
257
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
203
258
|
res.end(html);
|
|
204
259
|
return;
|
|
205
260
|
}
|
|
206
261
|
|
|
207
262
|
// ── SSE ──
|
|
208
|
-
if (req.method ===
|
|
263
|
+
if (req.method === "GET" && url.pathname === "/events") {
|
|
209
264
|
res.writeHead(200, {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
265
|
+
"Content-Type": "text/event-stream",
|
|
266
|
+
"Cache-Control": "no-cache",
|
|
267
|
+
Connection: "keep-alive",
|
|
213
268
|
});
|
|
214
|
-
res.write(`data: ${JSON.stringify({ type:
|
|
269
|
+
res.write(`data: ${JSON.stringify({ type: "state", data: state })}\n\n`);
|
|
215
270
|
const heartbeat = setInterval(() => {
|
|
216
|
-
try {
|
|
271
|
+
try {
|
|
272
|
+
res.write(": heartbeat\n\n");
|
|
273
|
+
} catch {
|
|
274
|
+
clearInterval(heartbeat);
|
|
275
|
+
}
|
|
217
276
|
}, 15000);
|
|
218
277
|
sseClients.add(res);
|
|
219
|
-
vlog(
|
|
220
|
-
req.on(
|
|
278
|
+
vlog("sse", `client connected (${sseClients.size} total)`);
|
|
279
|
+
req.on("close", () => {
|
|
280
|
+
clearInterval(heartbeat);
|
|
281
|
+
sseClients.delete(res);
|
|
282
|
+
vlog("sse", `client disconnected (${sseClients.size} total)`);
|
|
283
|
+
});
|
|
221
284
|
return;
|
|
222
285
|
}
|
|
223
286
|
|
|
224
287
|
// ── API: state ──
|
|
225
|
-
if (req.method ===
|
|
226
|
-
res.writeHead(200, {
|
|
288
|
+
if (req.method === "GET" && url.pathname === "/api/state") {
|
|
289
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
227
290
|
res.end(JSON.stringify(state));
|
|
228
291
|
return;
|
|
229
292
|
}
|
|
230
293
|
|
|
231
294
|
// ── API: claims (with optional filters) ──
|
|
232
|
-
if (req.method ===
|
|
295
|
+
if (req.method === "GET" && url.pathname === "/api/claims") {
|
|
233
296
|
let claims = state.claims;
|
|
234
|
-
const topic = url.searchParams.get(
|
|
235
|
-
const evidence = url.searchParams.get(
|
|
236
|
-
const type = url.searchParams.get(
|
|
237
|
-
const status = url.searchParams.get(
|
|
238
|
-
if (topic) claims = claims.filter(c => c.topic === topic);
|
|
239
|
-
if (evidence) claims = claims.filter(c => c.evidence === evidence);
|
|
240
|
-
if (type) claims = claims.filter(c => c.type === type);
|
|
241
|
-
if (status) claims = claims.filter(c => c.status === status);
|
|
242
|
-
res.writeHead(200, {
|
|
297
|
+
const topic = url.searchParams.get("topic");
|
|
298
|
+
const evidence = url.searchParams.get("evidence");
|
|
299
|
+
const type = url.searchParams.get("type");
|
|
300
|
+
const status = url.searchParams.get("status");
|
|
301
|
+
if (topic) claims = claims.filter((c) => c.topic === topic);
|
|
302
|
+
if (evidence) claims = claims.filter((c) => c.evidence === evidence);
|
|
303
|
+
if (type) claims = claims.filter((c) => c.type === type);
|
|
304
|
+
if (status) claims = claims.filter((c) => c.status === status);
|
|
305
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
243
306
|
res.end(JSON.stringify(claims));
|
|
244
307
|
return;
|
|
245
308
|
}
|
|
246
309
|
|
|
247
310
|
// ── API: coverage ──
|
|
248
|
-
if (req.method ===
|
|
311
|
+
if (req.method === "GET" && url.pathname === "/api/coverage") {
|
|
249
312
|
const coverage = state.compilation?.coverage || {};
|
|
250
|
-
res.writeHead(200, {
|
|
313
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
251
314
|
res.end(JSON.stringify(coverage));
|
|
252
315
|
return;
|
|
253
316
|
}
|
|
254
317
|
|
|
255
318
|
// ── API: compilation ──
|
|
256
|
-
if (req.method ===
|
|
257
|
-
res.writeHead(200, {
|
|
319
|
+
if (req.method === "GET" && url.pathname === "/api/compilation") {
|
|
320
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
258
321
|
res.end(JSON.stringify(state.compilation));
|
|
259
322
|
return;
|
|
260
323
|
}
|
|
261
324
|
|
|
262
325
|
// ── API: compile (trigger recompilation) ──
|
|
263
|
-
if (req.method ===
|
|
264
|
-
const compileRoot = activeRoot ===
|
|
326
|
+
if (req.method === "POST" && url.pathname === "/api/compile") {
|
|
327
|
+
const compileRoot = activeRoot === "__all" ? root : activeRoot;
|
|
265
328
|
state.compilation = runCompile(compileRoot);
|
|
266
329
|
refreshState(activeRoot, root);
|
|
267
|
-
res.writeHead(200, {
|
|
330
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
268
331
|
res.end(JSON.stringify(state));
|
|
269
332
|
return;
|
|
270
333
|
}
|
|
271
334
|
|
|
272
335
|
// ── API: switch sprint ──
|
|
273
|
-
if (req.method ===
|
|
274
|
-
let body =
|
|
275
|
-
req.on(
|
|
276
|
-
req.on(
|
|
336
|
+
if (req.method === "POST" && url.pathname === "/api/switch") {
|
|
337
|
+
let body = "";
|
|
338
|
+
req.on("data", (chunk) => (body += chunk));
|
|
339
|
+
req.on("end", () => {
|
|
277
340
|
try {
|
|
278
341
|
const { sprint } = JSON.parse(body);
|
|
279
|
-
if (sprint ===
|
|
280
|
-
activeRoot =
|
|
342
|
+
if (sprint === "__all") {
|
|
343
|
+
activeRoot = "__all";
|
|
281
344
|
} else if (!sprint) {
|
|
282
345
|
activeRoot = root;
|
|
283
346
|
} else {
|
|
284
|
-
const s = state.sprints.find(sp => sp.name === sprint);
|
|
347
|
+
const s = state.sprints.find((sp) => sp.name === sprint);
|
|
285
348
|
if (s) {
|
|
286
349
|
activeRoot = path.resolve(root, s.path);
|
|
287
350
|
} else {
|
|
@@ -289,40 +352,50 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
289
352
|
}
|
|
290
353
|
}
|
|
291
354
|
refreshState(activeRoot, root);
|
|
292
|
-
res.writeHead(200, {
|
|
355
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
293
356
|
res.end(JSON.stringify(state));
|
|
294
357
|
} catch {
|
|
295
|
-
res.writeHead(400);
|
|
358
|
+
res.writeHead(400);
|
|
359
|
+
res.end("bad request");
|
|
296
360
|
}
|
|
297
361
|
});
|
|
298
362
|
return;
|
|
299
363
|
}
|
|
300
364
|
|
|
301
365
|
// ── Static files ──
|
|
302
|
-
let filePath = url.pathname ===
|
|
303
|
-
const resolved = path.resolve(PUBLIC_DIR,
|
|
366
|
+
let filePath = url.pathname === "/" ? "/index.html" : url.pathname;
|
|
367
|
+
const resolved = path.resolve(PUBLIC_DIR, "." + filePath);
|
|
304
368
|
if (!resolved.startsWith(PUBLIC_DIR)) {
|
|
305
|
-
res.writeHead(403);
|
|
369
|
+
res.writeHead(403);
|
|
370
|
+
res.end("forbidden");
|
|
371
|
+
return;
|
|
306
372
|
}
|
|
307
373
|
|
|
308
374
|
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) {
|
|
309
375
|
const ext = path.extname(resolved);
|
|
310
|
-
res.writeHead(200, {
|
|
376
|
+
res.writeHead(200, {
|
|
377
|
+
"Content-Type": MIME[ext] || "application/octet-stream",
|
|
378
|
+
});
|
|
311
379
|
res.end(fs.readFileSync(resolved));
|
|
312
380
|
return;
|
|
313
381
|
}
|
|
314
382
|
|
|
315
|
-
res.writeHead(404);
|
|
383
|
+
res.writeHead(404);
|
|
384
|
+
res.end("not found");
|
|
316
385
|
});
|
|
317
386
|
|
|
318
387
|
// ── File watching ──
|
|
319
|
-
const claimsPath = path.join(root,
|
|
320
|
-
const compilationPath = path.join(root,
|
|
388
|
+
const claimsPath = path.join(root, "claims.json");
|
|
389
|
+
const compilationPath = path.join(root, "compilation.json");
|
|
321
390
|
if (fs.existsSync(claimsPath)) {
|
|
322
|
-
fs.watchFile(claimsPath, { interval: 2000 }, () =>
|
|
391
|
+
fs.watchFile(claimsPath, { interval: 2000 }, () =>
|
|
392
|
+
refreshState(activeRoot, root)
|
|
393
|
+
);
|
|
323
394
|
}
|
|
324
395
|
if (fs.existsSync(compilationPath)) {
|
|
325
|
-
fs.watchFile(compilationPath, { interval: 2000 }, () =>
|
|
396
|
+
fs.watchFile(compilationPath, { interval: 2000 }, () =>
|
|
397
|
+
refreshState(activeRoot, root)
|
|
398
|
+
);
|
|
326
399
|
}
|
|
327
400
|
|
|
328
401
|
// ── Start ──
|
|
@@ -331,16 +404,20 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
331
404
|
// ── Graceful shutdown ──
|
|
332
405
|
const shutdown = (signal) => {
|
|
333
406
|
console.log(`\nwheat: ${signal} received, shutting down...`);
|
|
334
|
-
for (const res of sseClients) {
|
|
407
|
+
for (const res of sseClients) {
|
|
408
|
+
try {
|
|
409
|
+
res.end();
|
|
410
|
+
} catch {}
|
|
411
|
+
}
|
|
335
412
|
sseClients.clear();
|
|
336
413
|
server.close(() => process.exit(0));
|
|
337
414
|
setTimeout(() => process.exit(1), 5000);
|
|
338
415
|
};
|
|
339
|
-
process.on(
|
|
340
|
-
process.on(
|
|
416
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
417
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
341
418
|
|
|
342
|
-
server.on(
|
|
343
|
-
if (err.code ===
|
|
419
|
+
server.on("error", (err) => {
|
|
420
|
+
if (err.code === "EADDRINUSE") {
|
|
344
421
|
console.error(`\nwheat: port ${port} is already in use.`);
|
|
345
422
|
console.error(` Try: wheat serve --port ${Number(port) + 1}`);
|
|
346
423
|
console.error(` Or stop the process using port ${port}.\n`);
|
|
@@ -349,14 +426,20 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
349
426
|
throw err;
|
|
350
427
|
});
|
|
351
428
|
|
|
352
|
-
server.listen(port,
|
|
353
|
-
vlog(
|
|
429
|
+
server.listen(port, "127.0.0.1", () => {
|
|
430
|
+
vlog("listen", `port=${port}`, `root=${root}`);
|
|
354
431
|
console.log(`wheat: serving on http://localhost:${port}`);
|
|
355
432
|
console.log(` claims: ${state.claims.length} loaded`);
|
|
356
|
-
console.log(
|
|
433
|
+
console.log(
|
|
434
|
+
` compilation: ${
|
|
435
|
+
state.compilation ? state.compilation.status : "not found"
|
|
436
|
+
}`
|
|
437
|
+
);
|
|
357
438
|
console.log(` sprints: ${state.sprints.length} detected`);
|
|
358
439
|
if (state.activeSprint) {
|
|
359
|
-
console.log(
|
|
440
|
+
console.log(
|
|
441
|
+
` active: ${state.activeSprint.name} (${state.activeSprint.phase})`
|
|
442
|
+
);
|
|
360
443
|
}
|
|
361
444
|
console.log(` root: ${root}`);
|
|
362
445
|
});
|
|
@@ -368,22 +451,26 @@ ${ROUTES.map(r => '<tr><td><code>'+r.method+'</code></td><td><code>'+r.path+'</c
|
|
|
368
451
|
|
|
369
452
|
export async function run(targetDir, subArgs) {
|
|
370
453
|
let port = 9092;
|
|
371
|
-
const portIdx = subArgs.indexOf(
|
|
454
|
+
const portIdx = subArgs.indexOf("--port");
|
|
372
455
|
if (portIdx !== -1 && subArgs[portIdx + 1]) {
|
|
373
456
|
port = parseInt(subArgs[portIdx + 1], 10);
|
|
374
457
|
}
|
|
375
|
-
const corsIdx = subArgs.indexOf(
|
|
376
|
-
const corsOrigin =
|
|
458
|
+
const corsIdx = subArgs.indexOf("--cors");
|
|
459
|
+
const corsOrigin =
|
|
460
|
+
corsIdx !== -1 && subArgs[corsIdx + 1] ? subArgs[corsIdx + 1] : null;
|
|
377
461
|
|
|
378
462
|
// Walk up to find project root if no claims.json in targetDir
|
|
379
463
|
let root = targetDir;
|
|
380
|
-
if (!fs.existsSync(path.join(root,
|
|
464
|
+
if (!fs.existsSync(path.join(root, "claims.json"))) {
|
|
381
465
|
let dir = path.resolve(root);
|
|
382
466
|
for (let i = 0; i < 5; i++) {
|
|
383
467
|
const parent = path.dirname(dir);
|
|
384
468
|
if (parent === dir) break;
|
|
385
469
|
dir = parent;
|
|
386
|
-
if (fs.existsSync(path.join(dir,
|
|
470
|
+
if (fs.existsSync(path.join(dir, "claims.json"))) {
|
|
471
|
+
root = dir;
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
387
474
|
}
|
|
388
475
|
}
|
|
389
476
|
|