@tekyzinc/gsd-t 2.70.16 → 2.71.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.
@@ -0,0 +1,388 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * GSD-T Design Review Server — Zero-dep proxy + review coordination
4
+ *
5
+ * Proxies the project dev server (same-origin for iframe DOM access),
6
+ * injects the inspect overlay script, and provides coordination APIs
7
+ * for the builder terminal ↔ human review loop.
8
+ *
9
+ * Usage:
10
+ * node gsd-t-design-review-server.js [--port 3456] [--target http://localhost:5173] [--project /path/to/project]
11
+ */
12
+ const http = require("http");
13
+ const fs = require("fs");
14
+ const path = require("path");
15
+ const url = require("url");
16
+
17
+ // ── CLI args ──────────────────────────────────────────────────────────
18
+ const args = process.argv.slice(2);
19
+ function getArg(name, fallback) {
20
+ const i = args.indexOf(`--${name}`);
21
+ return i >= 0 && args[i + 1] ? args[i + 1] : fallback;
22
+ }
23
+
24
+ const PORT = parseInt(getArg("port", "3456"), 10);
25
+ const TARGET = getArg("target", "http://localhost:5173");
26
+ const PROJECT_DIR = getArg("project", process.cwd());
27
+ const REVIEW_DIR = path.join(PROJECT_DIR, ".gsd-t", "design-review");
28
+
29
+ // ── Ensure coordination directory ─────────────────────────────────────
30
+ function ensureDir(dir) {
31
+ try { fs.mkdirSync(dir, { recursive: true }); } catch { /* exists */ }
32
+ }
33
+ ensureDir(REVIEW_DIR);
34
+ ensureDir(path.join(REVIEW_DIR, "queue"));
35
+ ensureDir(path.join(REVIEW_DIR, "feedback"));
36
+
37
+ // Init status if missing
38
+ const STATUS_FILE = path.join(REVIEW_DIR, "status.json");
39
+ if (!fs.existsSync(STATUS_FILE)) {
40
+ fs.writeFileSync(STATUS_FILE, JSON.stringify({
41
+ phase: "elements",
42
+ state: "waiting",
43
+ startedAt: new Date().toISOString(),
44
+ }, null, 2));
45
+ }
46
+
47
+ // ── SSE clients ───────────────────────────────────────────────────────
48
+ const sseClients = new Set();
49
+
50
+ function broadcast(event, data) {
51
+ const msg = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
52
+ for (const res of sseClients) {
53
+ try { res.write(msg); } catch { sseClients.delete(res); }
54
+ }
55
+ }
56
+
57
+ // Watch queue directory for changes — auto-reject items with CRITICAL measurement failures
58
+ let queueWatcher;
59
+ try {
60
+ queueWatcher = fs.watch(path.join(REVIEW_DIR, "queue"), () => {
61
+ autoRejectFailures();
62
+ broadcast("queue-update", readQueue());
63
+ });
64
+ } catch { /* dir may not exist yet */ }
65
+
66
+ function autoRejectFailures() {
67
+ const queueDir = path.join(REVIEW_DIR, "queue");
68
+ const fbDir = path.join(REVIEW_DIR, "feedback");
69
+ ensureDir(fbDir);
70
+ try {
71
+ const files = fs.readdirSync(queueDir).filter(f => f.endsWith(".json"));
72
+ for (const f of files) {
73
+ try {
74
+ const item = JSON.parse(fs.readFileSync(path.join(queueDir, f), "utf8"));
75
+ if (!item.measurements || !Array.isArray(item.measurements)) continue;
76
+ // Check for CRITICAL failures (auto-reject threshold)
77
+ const failures = item.measurements.filter(m => !m.pass);
78
+ const criticalFailures = failures.filter(m =>
79
+ m.severity === "critical" ||
80
+ m.property === "chart type" ||
81
+ m.property === "display" ||
82
+ m.property === "flexDirection"
83
+ );
84
+ if (criticalFailures.length > 0) {
85
+ // Auto-reject: write feedback and remove from queue
86
+ const feedback = {
87
+ id: item.id,
88
+ verdict: "rejected",
89
+ source: "auto-review",
90
+ comment: `Auto-rejected: ${criticalFailures.length} critical measurement failures: ${criticalFailures.map(m => `${m.property} (expected: ${m.expected}, got: ${m.actual})`).join("; ")}`,
91
+ changes: [],
92
+ rejectedAt: new Date().toISOString(),
93
+ };
94
+ fs.writeFileSync(path.join(fbDir, `${item.id}.json`), JSON.stringify(feedback, null, 2));
95
+ // Move item to rejected (don't delete, move to a rejected subfolder)
96
+ ensureDir(path.join(REVIEW_DIR, "rejected"));
97
+ fs.renameSync(path.join(queueDir, f), path.join(REVIEW_DIR, "rejected", f));
98
+ broadcast("auto-reject", { id: item.id, failures: criticalFailures });
99
+ console.log(` ✗ Auto-rejected: ${item.name} — ${criticalFailures.length} critical failures`);
100
+ }
101
+ } catch { /* skip malformed */ }
102
+ }
103
+ } catch { /* no queue dir yet */ }
104
+ }
105
+
106
+ // ── Coordination API ──────────────────────────────────────────────────
107
+
108
+ function readQueue() {
109
+ const queueDir = path.join(REVIEW_DIR, "queue");
110
+ try {
111
+ const items = fs.readdirSync(queueDir)
112
+ .filter(f => f.endsWith(".json") && !f.includes(".ai-review"))
113
+ .map(f => {
114
+ try { return JSON.parse(fs.readFileSync(path.join(queueDir, f), "utf8")); }
115
+ catch { return null; }
116
+ })
117
+ .filter(Boolean)
118
+ .sort((a, b) => (a.order || 0) - (b.order || 0));
119
+
120
+ // Attach AI review annotations if they exist
121
+ for (const item of items) {
122
+ const aiFile = path.join(queueDir, `${item.id}.ai-review.json`);
123
+ try {
124
+ if (fs.existsSync(aiFile)) {
125
+ item.aiReview = JSON.parse(fs.readFileSync(aiFile, "utf8"));
126
+ }
127
+ } catch { /* skip malformed */ }
128
+ }
129
+ return items;
130
+ } catch { return []; }
131
+ }
132
+
133
+ function readStatus() {
134
+ try { return JSON.parse(fs.readFileSync(STATUS_FILE, "utf8")); }
135
+ catch { return { phase: "elements", state: "waiting" }; }
136
+ }
137
+
138
+ function readFeedback() {
139
+ const fbDir = path.join(REVIEW_DIR, "feedback");
140
+ try {
141
+ return fs.readdirSync(fbDir)
142
+ .filter(f => f.endsWith(".json"))
143
+ .map(f => {
144
+ try { return JSON.parse(fs.readFileSync(path.join(fbDir, f), "utf8")); }
145
+ catch { return null; }
146
+ })
147
+ .filter(Boolean);
148
+ } catch { return []; }
149
+ }
150
+
151
+ function writeFeedback(items) {
152
+ const fbDir = path.join(REVIEW_DIR, "feedback");
153
+ ensureDir(fbDir);
154
+ for (const item of items) {
155
+ const fname = `${item.id}.json`;
156
+ fs.writeFileSync(path.join(fbDir, fname), JSON.stringify(item, null, 2));
157
+ }
158
+ // Write a summary signal file so the builder knows review is done
159
+ fs.writeFileSync(
160
+ path.join(REVIEW_DIR, "review-complete.json"),
161
+ JSON.stringify({
162
+ completedAt: new Date().toISOString(),
163
+ phase: readStatus().phase,
164
+ items: items.map(i => ({ id: i.id, verdict: i.verdict })),
165
+ }, null, 2)
166
+ );
167
+ }
168
+
169
+ // ── Proxy helper ──────────────────────────────────────────────────────
170
+ const targetUrl = new URL(TARGET);
171
+
172
+ function proxyRequest(req, res) {
173
+ const opts = {
174
+ hostname: targetUrl.hostname,
175
+ port: targetUrl.port,
176
+ path: req.url,
177
+ method: req.method,
178
+ headers: { ...req.headers, host: `${targetUrl.hostname}:${targetUrl.port}` },
179
+ };
180
+
181
+ const proxyReq = http.request(opts, (proxyRes) => {
182
+ const contentType = proxyRes.headers["content-type"] || "";
183
+ const isHtml = contentType.includes("text/html");
184
+
185
+ if (isHtml) {
186
+ // Buffer HTML to inject our overlay script
187
+ const chunks = [];
188
+ proxyRes.on("data", (chunk) => chunks.push(chunk));
189
+ proxyRes.on("end", () => {
190
+ let html = Buffer.concat(chunks).toString("utf8");
191
+ // Inject the review overlay script before </body>
192
+ const injectScript = `<script src="/review/inject.js"></script>`;
193
+ if (html.includes("</body>")) {
194
+ html = html.replace("</body>", `${injectScript}\n</body>`);
195
+ } else {
196
+ html += injectScript;
197
+ }
198
+ // Update content-length
199
+ const buf = Buffer.from(html, "utf8");
200
+ const headers = { ...proxyRes.headers };
201
+ headers["content-length"] = buf.length;
202
+ delete headers["content-encoding"]; // remove gzip if present
203
+ res.writeHead(proxyRes.statusCode, headers);
204
+ res.end(buf);
205
+ });
206
+ } else {
207
+ // Pass through non-HTML responses
208
+ res.writeHead(proxyRes.statusCode, proxyRes.headers);
209
+ proxyRes.pipe(res);
210
+ }
211
+ });
212
+
213
+ proxyReq.on("error", (err) => {
214
+ res.writeHead(502, { "Content-Type": "application/json" });
215
+ res.end(JSON.stringify({ error: "Dev server unreachable", details: err.message }));
216
+ });
217
+
218
+ req.pipe(proxyReq);
219
+ }
220
+
221
+ // ── Static files ──────────────────────────────────────────────────────
222
+ const SCRIPT_DIR = __dirname;
223
+
224
+ const MIME_TYPES = {
225
+ ".html": "text/html",
226
+ ".js": "application/javascript",
227
+ ".css": "text/css",
228
+ ".json": "application/json",
229
+ ".png": "image/png",
230
+ ".svg": "image/svg+xml",
231
+ };
232
+
233
+ function serveFile(filePath, res) {
234
+ try {
235
+ const ext = path.extname(filePath);
236
+ const mime = MIME_TYPES[ext] || "application/octet-stream";
237
+ const content = fs.readFileSync(filePath);
238
+ res.writeHead(200, { "Content-Type": mime, "Cache-Control": "no-cache" });
239
+ res.end(content);
240
+ } catch {
241
+ res.writeHead(404);
242
+ res.end("Not found");
243
+ }
244
+ }
245
+
246
+ // ── HTTP Server ───────────────────────────────────────────────────────
247
+ const server = http.createServer((req, res) => {
248
+ const parsed = url.parse(req.url, true);
249
+ const pathname = parsed.pathname;
250
+
251
+ // ── Review UI routes ────────────────────────────────────────────
252
+ if (pathname === "/review" || pathname === "/review/") {
253
+ serveFile(path.join(SCRIPT_DIR, "gsd-t-design-review.html"), res);
254
+ return;
255
+ }
256
+
257
+ if (pathname === "/review/inject.js") {
258
+ serveFile(path.join(SCRIPT_DIR, "gsd-t-design-review-inject.js"), res);
259
+ return;
260
+ }
261
+
262
+ // ── Review API ──────────────────────────────────────────────────
263
+ if (pathname === "/review/api/status") {
264
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
265
+ res.end(JSON.stringify(readStatus()));
266
+ return;
267
+ }
268
+
269
+ if (pathname === "/review/api/queue") {
270
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
271
+ res.end(JSON.stringify(readQueue()));
272
+ return;
273
+ }
274
+
275
+ if (pathname === "/review/api/feedback" && req.method === "GET") {
276
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
277
+ res.end(JSON.stringify(readFeedback()));
278
+ return;
279
+ }
280
+
281
+ if (pathname === "/review/api/feedback" && req.method === "POST") {
282
+ let body = "";
283
+ req.on("data", (chunk) => { body += chunk; });
284
+ req.on("end", () => {
285
+ try {
286
+ const items = JSON.parse(body);
287
+ writeFeedback(Array.isArray(items) ? items : [items]);
288
+ broadcast("feedback-submitted", { count: Array.isArray(items) ? items.length : 1 });
289
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
290
+ res.end(JSON.stringify({ ok: true }));
291
+ } catch (err) {
292
+ res.writeHead(400, { "Content-Type": "application/json" });
293
+ res.end(JSON.stringify({ error: err.message }));
294
+ }
295
+ });
296
+ return;
297
+ }
298
+
299
+ if (pathname === "/review/api/write-source" && req.method === "POST") {
300
+ // Apply CSS property changes back to source files
301
+ let body = "";
302
+ req.on("data", (chunk) => { body += chunk; });
303
+ req.on("end", () => {
304
+ try {
305
+ const { changes } = JSON.parse(body);
306
+ // Changes are stored for the builder to process
307
+ // (Claude will interpret CSS changes → Tailwind class changes)
308
+ const changesFile = path.join(REVIEW_DIR, "pending-changes.json");
309
+ fs.writeFileSync(changesFile, JSON.stringify(changes, null, 2));
310
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
311
+ res.end(JSON.stringify({ ok: true, count: changes.length }));
312
+ } catch (err) {
313
+ res.writeHead(400, { "Content-Type": "application/json" });
314
+ res.end(JSON.stringify({ error: err.message }));
315
+ }
316
+ });
317
+ return;
318
+ }
319
+
320
+ // CORS preflight
321
+ if (req.method === "OPTIONS") {
322
+ res.writeHead(204, {
323
+ "Access-Control-Allow-Origin": "*",
324
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
325
+ "Access-Control-Allow-Headers": "Content-Type",
326
+ });
327
+ res.end();
328
+ return;
329
+ }
330
+
331
+ // ── SSE stream ──────────────────────────────────────────────────
332
+ if (pathname === "/review/api/events") {
333
+ res.writeHead(200, {
334
+ "Content-Type": "text/event-stream",
335
+ "Cache-Control": "no-cache",
336
+ "Connection": "keep-alive",
337
+ "Access-Control-Allow-Origin": "*",
338
+ });
339
+ sseClients.add(res);
340
+ req.on("close", () => sseClients.delete(res));
341
+ // Send initial state
342
+ res.write(`event: init\ndata: ${JSON.stringify({ status: readStatus(), queue: readQueue() })}\n\n`);
343
+ return;
344
+ }
345
+
346
+ // ── Proxy everything else to dev server ─────────────────────────
347
+ proxyRequest(req, res);
348
+ });
349
+
350
+ // ── WebSocket upgrade for Vite HMR ───────────────────────────────────
351
+ server.on("upgrade", (req, socket, head) => {
352
+ const opts = {
353
+ hostname: targetUrl.hostname,
354
+ port: targetUrl.port,
355
+ path: req.url,
356
+ method: req.method,
357
+ headers: req.headers,
358
+ };
359
+
360
+ const proxyReq = http.request(opts);
361
+ proxyReq.on("upgrade", (proxyRes, proxySocket, proxyHead) => {
362
+ socket.write(
363
+ `HTTP/1.1 101 Switching Protocols\r\n` +
364
+ Object.entries(proxyRes.headers).map(([k, v]) => `${k}: ${v}`).join("\r\n") +
365
+ "\r\n\r\n"
366
+ );
367
+ if (proxyHead.length) socket.write(proxyHead);
368
+ proxySocket.pipe(socket);
369
+ socket.pipe(proxySocket);
370
+ });
371
+
372
+ proxyReq.on("error", () => socket.end());
373
+ proxyReq.end();
374
+ });
375
+
376
+ server.listen(PORT, () => {
377
+ const BOLD = "\x1b[1m";
378
+ const GREEN = "\x1b[32m";
379
+ const CYAN = "\x1b[36m";
380
+ const DIM = "\x1b[2m";
381
+ const RESET = "\x1b[0m";
382
+
383
+ console.log(`\n${BOLD}GSD-T Design Review Server${RESET}`);
384
+ console.log(`${GREEN} ✓${RESET} Review UI: ${CYAN}http://localhost:${PORT}/review${RESET}`);
385
+ console.log(`${GREEN} ✓${RESET} Proxying: ${DIM}${TARGET} → http://localhost:${PORT}/${RESET}`);
386
+ console.log(`${GREEN} ✓${RESET} Project: ${DIM}${PROJECT_DIR}${RESET}`);
387
+ console.log(`${DIM} Coordination: ${REVIEW_DIR}${RESET}\n`);
388
+ });