snatching amp's walkthrough for my own purposes mwhahaha traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c

feat: allow multiple clients

dunkirk.sh 2f238620 c4e2a084

verified
+187 -94
+142 -93
src/index.ts
··· 3 3 import { z } from "zod/v4"; 4 4 import { generateViewerHTML } from "./template.ts"; 5 5 import type { WalkthroughDiagram } from "./types.ts"; 6 - import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId } from "./storage.ts"; 6 + import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId, getSharedUrl, saveSharedUrl } from "./storage.ts"; 7 7 import { loadConfig } from "./config.ts"; 8 8 9 9 const PORT = parseInt(process.env.TRAVERSE_PORT || "4173", 10); ··· 18 18 const diagrams = loadAllDiagrams(); 19 19 20 20 // --- Web server for serving interactive diagrams --- 21 - Bun.serve({ 22 - port: PORT, 23 - async fetch(req) { 24 - const url = new URL(req.url); 25 - const diagramMatch = url.pathname.match(/^\/diagram\/([\w-]+)$/); 21 + let isClient = false; 22 + 23 + try { 24 + Bun.serve({ 25 + port: PORT, 26 + async fetch(req) { 27 + const url = new URL(req.url); 28 + const diagramMatch = url.pathname.match(/^\/diagram\/([\w-]+)$/); 26 29 27 - if (diagramMatch) { 28 - const id = diagramMatch[1]!; 29 - const diagram = diagrams.get(id); 30 - if (!diagram) { 31 - return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), { 32 - status: 404, 30 + if (diagramMatch) { 31 + const id = diagramMatch[1]!; 32 + const diagram = diagrams.get(id); 33 + if (!diagram) { 34 + return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), { 35 + status: 404, 36 + headers: { "Content-Type": "text/html; charset=utf-8" }, 37 + }); 38 + } 39 + const existingShareUrl = getSharedUrl(id); 40 + return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd(), { 41 + mode: MODE, 42 + shareServerUrl: config.shareServerUrl, 43 + diagramId: id, 44 + existingShareUrl: existingShareUrl ?? undefined, 45 + }), { 33 46 headers: { "Content-Type": "text/html; charset=utf-8" }, 34 47 }); 35 48 } 36 - return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd(), { 37 - mode: MODE, 38 - shareServerUrl: config.shareServerUrl, 39 - diagramId: id, 40 - }), { 41 - headers: { "Content-Type": "text/html; charset=utf-8" }, 42 - }); 43 - } 44 49 45 - // DELETE /api/diagrams/:id 46 - const apiMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)$/); 47 - if (apiMatch && req.method === "DELETE") { 48 - const id = apiMatch[1]!; 49 - if (!diagrams.has(id)) { 50 - return Response.json({ error: "not found" }, { status: 404 }); 50 + // DELETE /api/diagrams/:id 51 + const apiMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)$/); 52 + if (apiMatch && req.method === "DELETE") { 53 + const id = apiMatch[1]!; 54 + if (!diagrams.has(id)) { 55 + return Response.json({ error: "not found" }, { status: 404 }); 56 + } 57 + diagrams.delete(id); 58 + deleteDiagramFromDb(id); 59 + return Response.json({ ok: true, id }); 51 60 } 52 - diagrams.delete(id); 53 - deleteDiagramFromDb(id); 54 - return Response.json({ ok: true, id }); 55 - } 56 61 57 - // POST /api/diagrams (server mode: accept diagrams from remote) 58 - if (url.pathname === "/api/diagrams" && req.method === "POST") { 59 - if (MODE !== "server") { 60 - return Response.json({ error: "POST only available in server mode" }, { status: 403 }); 62 + // POST /api/diagrams/:id/shared-url — save a shared URL for a local diagram 63 + const sharedUrlMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)\/shared-url$/); 64 + if (sharedUrlMatch && req.method === "POST") { 65 + const id = sharedUrlMatch[1]!; 66 + try { 67 + const body = await req.json() as { url: string }; 68 + if (!body.url) { 69 + return Response.json({ error: "missing required field: url" }, { status: 400 }); 70 + } 71 + saveSharedUrl(id, body.url); 72 + return Response.json({ ok: true, id, url: body.url }); 73 + } catch { 74 + return Response.json({ error: "invalid JSON body" }, { status: 400 }); 75 + } 76 + } 77 + 78 + // GET /api/diagrams/:id/shared-url — retrieve a stored shared URL 79 + if (sharedUrlMatch && req.method === "GET") { 80 + const id = sharedUrlMatch[1]!; 81 + const sharedUrl = getSharedUrl(id); 82 + if (!sharedUrl) { 83 + return Response.json({ url: null }); 84 + } 85 + return Response.json({ url: sharedUrl }); 61 86 } 62 - try { 63 - const body = await req.json() as WalkthroughDiagram; 64 - if (!body.code || !body.summary || !body.nodes) { 65 - return Response.json({ error: "missing required fields: code, summary, nodes" }, { status: 400 }); 87 + 88 + // POST /api/diagrams — accept diagrams from remote or sibling instances 89 + if (url.pathname === "/api/diagrams" && req.method === "POST") { 90 + try { 91 + const body = await req.json() as WalkthroughDiagram; 92 + if (!body.code || !body.summary || !body.nodes) { 93 + return Response.json({ error: "missing required fields: code, summary, nodes" }, { status: 400 }); 94 + } 95 + const id = generateId(); 96 + const diagram: WalkthroughDiagram = { 97 + code: body.code, 98 + summary: body.summary, 99 + nodes: body.nodes, 100 + createdAt: new Date().toISOString(), 101 + }; 102 + diagrams.set(id, diagram); 103 + saveDiagram(id, diagram); 104 + const diagramUrl = `${url.origin}/diagram/${id}`; 105 + return Response.json({ id, url: diagramUrl }, { 106 + status: 201, 107 + headers: { 108 + "Access-Control-Allow-Origin": "*", 109 + }, 110 + }); 111 + } catch { 112 + return Response.json({ error: "invalid JSON body" }, { status: 400 }); 66 113 } 67 - const id = generateId(); 68 - const diagram: WalkthroughDiagram = { 69 - code: body.code, 70 - summary: body.summary, 71 - nodes: body.nodes, 72 - createdAt: new Date().toISOString(), 73 - }; 74 - diagrams.set(id, diagram); 75 - saveDiagram(id, diagram); 76 - const diagramUrl = `${url.origin}/diagram/${id}`; 77 - return Response.json({ id, url: diagramUrl }, { 78 - status: 201, 114 + } 115 + 116 + // OPTIONS /api/diagrams — CORS preflight 117 + if (url.pathname === "/api/diagrams" && req.method === "OPTIONS") { 118 + return new Response(null, { 119 + status: 204, 79 120 headers: { 80 121 "Access-Control-Allow-Origin": "*", 122 + "Access-Control-Allow-Methods": "POST, OPTIONS", 123 + "Access-Control-Allow-Headers": "Content-Type", 81 124 }, 82 125 }); 83 - } catch { 84 - return Response.json({ error: "invalid JSON body" }, { status: 400 }); 85 126 } 86 - } 87 127 88 - // OPTIONS /api/diagrams — CORS preflight 89 - if (url.pathname === "/api/diagrams" && req.method === "OPTIONS") { 90 - return new Response(null, { 91 - status: 204, 92 - headers: { 93 - "Access-Control-Allow-Origin": "*", 94 - "Access-Control-Allow-Methods": "POST, OPTIONS", 95 - "Access-Control-Allow-Headers": "Content-Type", 96 - }, 97 - }); 98 - } 128 + if (url.pathname === "/icon.svg") { 129 + return new Response(Bun.file(import.meta.dir + "/../icon.svg"), { 130 + headers: { "Content-Type": "image/svg+xml" }, 131 + }); 132 + } 99 133 100 - if (url.pathname === "/icon.svg") { 101 - return new Response(Bun.file(import.meta.dir + "/../icon.svg"), { 102 - headers: { "Content-Type": "image/svg+xml" }, 103 - }); 104 - } 134 + // List available diagrams 135 + if (url.pathname === "/") { 136 + const html = MODE === "server" 137 + ? generateServerIndexHTML(diagrams.size, GIT_HASH) 138 + : generateLocalIndexHTML(diagrams, GIT_HASH); 139 + return new Response(html, { 140 + headers: { "Content-Type": "text/html; charset=utf-8" }, 141 + }); 142 + } 105 143 106 - // List available diagrams 107 - if (url.pathname === "/") { 108 - const html = MODE === "server" 109 - ? generateServerIndexHTML(diagrams.size, GIT_HASH) 110 - : generateLocalIndexHTML(diagrams, GIT_HASH); 111 - return new Response(html, { 144 + return new Response(generate404HTML("Page not found", "There's nothing at this URL."), { 145 + status: 404, 112 146 headers: { "Content-Type": "text/html; charset=utf-8" }, 113 147 }); 114 - } 115 - 116 - return new Response(generate404HTML("Page not found", "There's nothing at this URL."), { 117 - status: 404, 118 - headers: { "Content-Type": "text/html; charset=utf-8" }, 119 - }); 120 - }, 121 - }); 148 + }, 149 + }); 150 + } catch { 151 + isClient = true; 152 + console.error(`Web server already running on port ${PORT}, running in client mode`); 153 + } 122 154 123 155 // --- MCP Server (local mode only) --- 124 156 if (MODE === "local") { ··· 158 190 nodes: z.record(z.string(), nodeMetadataSchema), 159 191 }), 160 192 }, async ({ code, summary, nodes }) => { 161 - const id = generateId(); 162 - const diagram: WalkthroughDiagram = { 163 - code, 164 - summary, 165 - nodes, 166 - createdAt: new Date().toISOString(), 167 - }; 168 - diagrams.set(id, diagram); 169 - saveDiagram(id, diagram); 193 + let diagramUrl: string; 170 194 171 - const diagramUrl = `http://localhost:${PORT}/diagram/${id}`; 195 + if (isClient) { 196 + // POST diagram to the existing web server instance 197 + const res = await fetch(`http://localhost:${PORT}/api/diagrams`, { 198 + method: "POST", 199 + headers: { "Content-Type": "application/json" }, 200 + body: JSON.stringify({ code, summary, nodes }), 201 + }); 202 + if (!res.ok) { 203 + return { 204 + content: [{ type: "text", text: `Failed to send diagram to server: ${res.statusText}` }], 205 + }; 206 + } 207 + const data = await res.json() as { id: string; url: string }; 208 + diagramUrl = data.url; 209 + } else { 210 + const id = generateId(); 211 + const diagram: WalkthroughDiagram = { 212 + code, 213 + summary, 214 + nodes, 215 + createdAt: new Date().toISOString(), 216 + }; 217 + diagrams.set(id, diagram); 218 + saveDiagram(id, diagram); 219 + diagramUrl = `http://localhost:${PORT}/diagram/${id}`; 220 + } 172 221 173 222 return { 174 223 content: [
+19
src/storage.ts
··· 34 34 created_at TEXT 35 35 ) 36 36 `); 37 + db.run(` 38 + CREATE TABLE IF NOT EXISTS shared_urls ( 39 + local_id TEXT PRIMARY KEY, 40 + remote_url TEXT, 41 + shared_at TEXT 42 + ) 43 + `); 37 44 return db; 38 45 } 39 46 ··· 55 62 56 63 export function deleteDiagramFromDb(id: string): void { 57 64 db.run("DELETE FROM diagrams WHERE id = ?", [id]); 65 + } 66 + 67 + export function getSharedUrl(localId: string): string | null { 68 + const row = db.query("SELECT remote_url FROM shared_urls WHERE local_id = ?").get(localId) as { remote_url: string } | null; 69 + return row?.remote_url ?? null; 70 + } 71 + 72 + export function saveSharedUrl(localId: string, remoteUrl: string): void { 73 + db.run( 74 + "INSERT OR REPLACE INTO shared_urls (local_id, remote_url, shared_at) VALUES (?, ?, ?)", 75 + [localId, remoteUrl, new Date().toISOString()] 76 + ); 58 77 } 59 78 60 79 export function generateId(): string {
+26 -1
src/template.ts
··· 4 4 mode?: "local" | "server"; 5 5 shareServerUrl?: string; 6 6 diagramId?: string; 7 + existingShareUrl?: string; 7 8 } 8 9 9 10 export function generateViewerHTML(diagram: WalkthroughDiagram, gitHash: string = "dev", projectRoot: string = "", options: ViewerOptions = {}): string { 10 11 const diagramJSON = JSON.stringify(diagram).replace(/<\//g, "<\\/"); 11 - const { mode = "local", shareServerUrl = "", diagramId = "" } = options; 12 + const { mode = "local", shareServerUrl = "", diagramId = "", existingShareUrl = "" } = options; 12 13 13 14 return `<!DOCTYPE html> 14 15 <html lang="en"> ··· 648 649 const SHARE_SERVER_URL = ${JSON.stringify(shareServerUrl)}; 649 650 const DIAGRAM_ID = ${JSON.stringify(diagramId)}; 650 651 const VIEWER_MODE = ${JSON.stringify(mode)}; 652 + let EXISTING_SHARE_URL = ${JSON.stringify(existingShareUrl)}; 651 653 652 654 const COPY_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="5" y="5" width="8" height="8" rx="1.5"/><path d="M3 11V3a1.5 1.5 0 011.5-1.5H11"/></svg>'; 653 655 const CHECK_ICON = '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 8.5l3.5 3.5 6.5-7"/></svg>'; ··· 935 937 shareBtn.addEventListener("click", async () => { 936 938 shareBtn.disabled = true; 937 939 try { 940 + // If we already have a shared URL, just copy it 941 + if (EXISTING_SHARE_URL) { 942 + await navigator.clipboard.writeText(EXISTING_SHARE_URL); 943 + shareBtn.querySelector("span").textContent = "Copied link!"; 944 + shareBtn.classList.add("shared"); 945 + setTimeout(() => { 946 + shareBtn.querySelector("span").textContent = "Share"; 947 + shareBtn.classList.remove("shared"); 948 + }, 2000); 949 + return; 950 + } 951 + 938 952 const res = await fetch(SHARE_SERVER_URL + "/api/diagrams", { 939 953 method: "POST", 940 954 headers: { "Content-Type": "application/json" }, ··· 943 957 if (!res.ok) throw new Error("Share failed"); 944 958 const data = await res.json(); 945 959 await navigator.clipboard.writeText(data.url); 960 + 961 + // Save the shared URL locally so we don't re-upload next time 962 + if (DIAGRAM_ID) { 963 + fetch("/api/diagrams/" + DIAGRAM_ID + "/shared-url", { 964 + method: "POST", 965 + headers: { "Content-Type": "application/json" }, 966 + body: JSON.stringify({ url: data.url }), 967 + }).catch(() => {}); // best-effort 968 + EXISTING_SHARE_URL = data.url; 969 + } 970 + 946 971 shareBtn.querySelector("span").textContent = "Copied link!"; 947 972 shareBtn.classList.add("shared"); 948 973 setTimeout(() => {