@tekyzinc/gsd-t 2.73.25 → 2.74.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.
@@ -10,6 +10,7 @@
10
10
  * node gsd-t-design-review-server.js [--port 3456] [--target http://localhost:5173] [--project /path/to/project]
11
11
  */
12
12
  const http = require("http");
13
+ const { spawn } = require("child_process");
13
14
  const fs = require("fs");
14
15
  const path = require("path");
15
16
  const url = require("url");
@@ -90,6 +91,15 @@ function extractFixtureFromContract(componentPath) {
90
91
  } catch { return null; }
91
92
  }
92
93
 
94
+ function readContractForComponent(componentPath) {
95
+ const match = componentPath.match(/src\/components\/(\w+)\/(\w+)\.\w+$/);
96
+ if (!match) return null;
97
+ const [, tier, name] = match;
98
+ const kebab = name.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
99
+ const contractPath = path.join(PROJECT_DIR, ".gsd-t", "contracts", "design", tier, `${kebab}.contract.md`);
100
+ try { return fs.readFileSync(contractPath, "utf8"); } catch { return null; }
101
+ }
102
+
93
103
  function generatePreviewHtml(componentPath) {
94
104
  const linkTags = GLOBAL_STYLES.map(s => ` <link rel="stylesheet" href="/${s}">`).join("\n");
95
105
  const fixture = extractFixtureFromContract(componentPath);
@@ -372,10 +382,39 @@ function readFeedback() {
372
382
  } catch { return []; }
373
383
  }
374
384
 
385
+ function persistAttachments(item) {
386
+ if (!Array.isArray(item.attachments) || item.attachments.length === 0) return item;
387
+ const attDir = path.join(REVIEW_DIR, "feedback", "attachments");
388
+ ensureDir(attDir);
389
+ const persisted = [];
390
+ item.attachments.forEach((att, idx) => {
391
+ if (!att || typeof att.dataUrl !== "string") return;
392
+ const m = att.dataUrl.match(/^data:(image\/[a-zA-Z0-9+.-]+);base64,(.+)$/);
393
+ if (!m) return;
394
+ const mime = m[1];
395
+ const ext = mime.split("/")[1].replace("+xml", "").replace("jpeg", "jpg");
396
+ const ts = Date.now();
397
+ const safeId = String(item.id).replace(/[^a-zA-Z0-9_-]/g, "_");
398
+ const filename = `${safeId}-${ts}-${idx}.${ext}`;
399
+ const absPath = path.join(attDir, filename);
400
+ try {
401
+ fs.writeFileSync(absPath, Buffer.from(m[2], "base64"));
402
+ persisted.push({
403
+ name: att.name || filename,
404
+ path: path.relative(REVIEW_DIR, absPath),
405
+ mime,
406
+ size: Buffer.byteLength(m[2], "base64"),
407
+ });
408
+ } catch {}
409
+ });
410
+ return { ...item, attachments: persisted };
411
+ }
412
+
375
413
  function writeFeedback(items) {
376
414
  const fbDir = path.join(REVIEW_DIR, "feedback");
377
415
  ensureDir(fbDir);
378
- for (const item of items) {
416
+ for (const rawItem of items) {
417
+ const item = persistAttachments(rawItem);
379
418
  const fname = `${item.id}.json`;
380
419
  fs.writeFileSync(path.join(fbDir, fname), JSON.stringify(item, null, 2));
381
420
  }
@@ -596,6 +635,116 @@ const server = http.createServer((req, res) => {
596
635
  return;
597
636
  }
598
637
 
638
+ if (pathname === "/review/api/contract") {
639
+ const component = parsed.query.component;
640
+ const content = component ? readContractForComponent(component) : null;
641
+ res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
642
+ res.end(JSON.stringify({ content: content || "" }));
643
+ return;
644
+ }
645
+
646
+ if (pathname === "/review/api/ai-assist" && req.method === "POST") {
647
+ let body = "";
648
+ req.on("data", (chunk) => { body += chunk; });
649
+ req.on("end", () => {
650
+ try {
651
+ const { messages, componentContext } = JSON.parse(body);
652
+
653
+ // Build a single prompt from system context + conversation history
654
+ const systemLines = [
655
+ "You are a design review assistant. A human is reviewing UI components against design contracts in the GSD-T Design Review panel.",
656
+ "Help them:",
657
+ "1. Answer questions about the component (properties, styles, measurements, data)",
658
+ "2. Translate vague corrections into precise, actionable contract language",
659
+ "3. Suggest specific property changes in the format: property: current → target",
660
+ "",
661
+ "Be concise. When suggesting corrections, format them so they can be pasted directly as a review comment.",
662
+ "",
663
+ "=== Component Context ===",
664
+ componentContext || "(no component selected)",
665
+ "=== End Context ===",
666
+ ];
667
+
668
+ // Include conversation history for multi-turn
669
+ if (messages && messages.length > 1) {
670
+ systemLines.push("", "=== Conversation History ===");
671
+ for (const msg of messages.slice(0, -1)) {
672
+ systemLines.push(`${msg.role === "user" ? "Human" : "Assistant"}: ${msg.content}`);
673
+ }
674
+ systemLines.push("=== End History ===");
675
+ }
676
+
677
+ const lastMessage = messages && messages.length > 0 ? messages[messages.length - 1].content : "";
678
+ const fullPrompt = systemLines.join("\n") + "\n\nHuman: " + lastMessage;
679
+ const model = process.env.GSD_AI_ASSIST_MODEL || "opus";
680
+
681
+ res.writeHead(200, {
682
+ "Content-Type": "text/event-stream",
683
+ "Cache-Control": "no-cache",
684
+ "Connection": "keep-alive",
685
+ "Access-Control-Allow-Origin": "*",
686
+ });
687
+
688
+ const claude = spawn("claude", [
689
+ "-p", fullPrompt,
690
+ "--model", model,
691
+ "--output-format", "stream-json",
692
+ "--verbose",
693
+ ], { env: { ...process.env, NO_COLOR: "1" } });
694
+
695
+ let buf = "";
696
+ let textSent = 0;
697
+
698
+ claude.stdout.on("data", (chunk) => {
699
+ buf += chunk.toString();
700
+ const lines = buf.split("\n");
701
+ buf = lines.pop();
702
+ for (const line of lines) {
703
+ if (!line.trim()) continue;
704
+ try {
705
+ const evt = JSON.parse(line);
706
+ if (evt.type === "assistant" && evt.message?.content) {
707
+ for (const block of evt.message.content) {
708
+ if (block.type === "text" && block.text) {
709
+ const newText = block.text.slice(textSent);
710
+ if (newText) {
711
+ res.write(`data: ${JSON.stringify({ text: newText })}\n\n`);
712
+ textSent = block.text.length;
713
+ }
714
+ }
715
+ }
716
+ }
717
+ } catch { /* skip non-JSON lines */ }
718
+ }
719
+ });
720
+
721
+ claude.stderr.on("data", () => { /* suppress stderr */ });
722
+
723
+ claude.on("close", (code) => {
724
+ if (!res.writableEnded) {
725
+ if (code !== 0 && textSent === 0) {
726
+ res.write(`event: error\ndata: ${JSON.stringify({ error: "Claude CLI exited with code " + code + ". Is claude installed and authenticated?" })}\n\n`);
727
+ }
728
+ res.write("event: done\ndata: {}\n\n");
729
+ res.end();
730
+ }
731
+ });
732
+
733
+ claude.on("error", (err) => {
734
+ if (!res.writableEnded) {
735
+ res.write(`event: error\ndata: ${JSON.stringify({ error: "Failed to spawn claude: " + err.message + ". Install Claude Code CLI to enable AI assist." })}\n\n`);
736
+ res.write("event: done\ndata: {}\n\n");
737
+ res.end();
738
+ }
739
+ });
740
+ } catch (err) {
741
+ res.writeHead(400, { "Content-Type": "application/json" });
742
+ res.end(JSON.stringify({ error: err.message }));
743
+ }
744
+ });
745
+ return;
746
+ }
747
+
599
748
  if (pathname === "/review/api/feedback" && req.method === "GET") {
600
749
  res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });
601
750
  res.end(JSON.stringify(readFeedback()));
@@ -647,9 +796,14 @@ const server = http.createServer((req, res) => {
647
796
  removed.push({ id, source: srcPath });
648
797
  }
649
798
  }
650
- // Remove from queue
799
+ // Remove from queue (memory + disk)
651
800
  for (let i = reviewQueue.length - 1; i >= 0; i--) {
652
- if (excludedIds.includes(reviewQueue[i].id)) reviewQueue.splice(i, 1);
801
+ if (excludedIds.includes(reviewQueue[i].id)) {
802
+ // Delete queue JSON file from disk
803
+ const queueFile = path.join(REVIEW_DIR, "queue", `${reviewQueue[i].id}.json`);
804
+ try { if (fs.existsSync(queueFile)) fs.unlinkSync(queueFile); } catch {}
805
+ reviewQueue.splice(i, 1);
806
+ }
653
807
  }
654
808
  broadcast("queue-update", reviewQueue);
655
809
  res.writeHead(200, { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" });