tangled
alpha
login
or
join now
dunkirk.sh
/
traverse
1
fork
atom
snatching amp's walkthrough for my own purposes mwhahaha
traverse.dunkirk.sh/diagram/6121f05c-a5ef-4ecf-8ffc-02534c5e767c
1
fork
atom
overview
issues
pulls
pipelines
feat: allow multiple clients
dunkirk.sh
1 month ago
2f238620
c4e2a084
verified
This commit was signed with the committer's
known signature
.
dunkirk.sh
SSH Key Fingerprint:
SHA256:DqcG0RXYExE26KiWo3VxJnsxswN1QNfTBvB+bdSpk80=
+187
-94
3 changed files
expand all
collapse all
unified
split
src
index.ts
storage.ts
template.ts
+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
6
-
import { initDb, loadAllDiagrams, saveDiagram, deleteDiagramFromDb, generateId } from "./storage.ts";
6
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
21
-
Bun.serve({
22
22
-
port: PORT,
23
23
-
async fetch(req) {
24
24
-
const url = new URL(req.url);
25
25
-
const diagramMatch = url.pathname.match(/^\/diagram\/([\w-]+)$/);
21
21
+
let isClient = false;
22
22
+
23
23
+
try {
24
24
+
Bun.serve({
25
25
+
port: PORT,
26
26
+
async fetch(req) {
27
27
+
const url = new URL(req.url);
28
28
+
const diagramMatch = url.pathname.match(/^\/diagram\/([\w-]+)$/);
26
29
27
27
-
if (diagramMatch) {
28
28
-
const id = diagramMatch[1]!;
29
29
-
const diagram = diagrams.get(id);
30
30
-
if (!diagram) {
31
31
-
return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), {
32
32
-
status: 404,
30
30
+
if (diagramMatch) {
31
31
+
const id = diagramMatch[1]!;
32
32
+
const diagram = diagrams.get(id);
33
33
+
if (!diagram) {
34
34
+
return new Response(generate404HTML("Diagram not found", "This diagram doesn't exist or may have expired."), {
35
35
+
status: 404,
36
36
+
headers: { "Content-Type": "text/html; charset=utf-8" },
37
37
+
});
38
38
+
}
39
39
+
const existingShareUrl = getSharedUrl(id);
40
40
+
return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd(), {
41
41
+
mode: MODE,
42
42
+
shareServerUrl: config.shareServerUrl,
43
43
+
diagramId: id,
44
44
+
existingShareUrl: existingShareUrl ?? undefined,
45
45
+
}), {
33
46
headers: { "Content-Type": "text/html; charset=utf-8" },
34
47
});
35
48
}
36
36
-
return new Response(generateViewerHTML(diagram, GIT_HASH, process.cwd(), {
37
37
-
mode: MODE,
38
38
-
shareServerUrl: config.shareServerUrl,
39
39
-
diagramId: id,
40
40
-
}), {
41
41
-
headers: { "Content-Type": "text/html; charset=utf-8" },
42
42
-
});
43
43
-
}
44
49
45
45
-
// DELETE /api/diagrams/:id
46
46
-
const apiMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)$/);
47
47
-
if (apiMatch && req.method === "DELETE") {
48
48
-
const id = apiMatch[1]!;
49
49
-
if (!diagrams.has(id)) {
50
50
-
return Response.json({ error: "not found" }, { status: 404 });
50
50
+
// DELETE /api/diagrams/:id
51
51
+
const apiMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)$/);
52
52
+
if (apiMatch && req.method === "DELETE") {
53
53
+
const id = apiMatch[1]!;
54
54
+
if (!diagrams.has(id)) {
55
55
+
return Response.json({ error: "not found" }, { status: 404 });
56
56
+
}
57
57
+
diagrams.delete(id);
58
58
+
deleteDiagramFromDb(id);
59
59
+
return Response.json({ ok: true, id });
51
60
}
52
52
-
diagrams.delete(id);
53
53
-
deleteDiagramFromDb(id);
54
54
-
return Response.json({ ok: true, id });
55
55
-
}
56
61
57
57
-
// POST /api/diagrams (server mode: accept diagrams from remote)
58
58
-
if (url.pathname === "/api/diagrams" && req.method === "POST") {
59
59
-
if (MODE !== "server") {
60
60
-
return Response.json({ error: "POST only available in server mode" }, { status: 403 });
62
62
+
// POST /api/diagrams/:id/shared-url — save a shared URL for a local diagram
63
63
+
const sharedUrlMatch = url.pathname.match(/^\/api\/diagrams\/([\w-]+)\/shared-url$/);
64
64
+
if (sharedUrlMatch && req.method === "POST") {
65
65
+
const id = sharedUrlMatch[1]!;
66
66
+
try {
67
67
+
const body = await req.json() as { url: string };
68
68
+
if (!body.url) {
69
69
+
return Response.json({ error: "missing required field: url" }, { status: 400 });
70
70
+
}
71
71
+
saveSharedUrl(id, body.url);
72
72
+
return Response.json({ ok: true, id, url: body.url });
73
73
+
} catch {
74
74
+
return Response.json({ error: "invalid JSON body" }, { status: 400 });
75
75
+
}
76
76
+
}
77
77
+
78
78
+
// GET /api/diagrams/:id/shared-url — retrieve a stored shared URL
79
79
+
if (sharedUrlMatch && req.method === "GET") {
80
80
+
const id = sharedUrlMatch[1]!;
81
81
+
const sharedUrl = getSharedUrl(id);
82
82
+
if (!sharedUrl) {
83
83
+
return Response.json({ url: null });
84
84
+
}
85
85
+
return Response.json({ url: sharedUrl });
61
86
}
62
62
-
try {
63
63
-
const body = await req.json() as WalkthroughDiagram;
64
64
-
if (!body.code || !body.summary || !body.nodes) {
65
65
-
return Response.json({ error: "missing required fields: code, summary, nodes" }, { status: 400 });
87
87
+
88
88
+
// POST /api/diagrams — accept diagrams from remote or sibling instances
89
89
+
if (url.pathname === "/api/diagrams" && req.method === "POST") {
90
90
+
try {
91
91
+
const body = await req.json() as WalkthroughDiagram;
92
92
+
if (!body.code || !body.summary || !body.nodes) {
93
93
+
return Response.json({ error: "missing required fields: code, summary, nodes" }, { status: 400 });
94
94
+
}
95
95
+
const id = generateId();
96
96
+
const diagram: WalkthroughDiagram = {
97
97
+
code: body.code,
98
98
+
summary: body.summary,
99
99
+
nodes: body.nodes,
100
100
+
createdAt: new Date().toISOString(),
101
101
+
};
102
102
+
diagrams.set(id, diagram);
103
103
+
saveDiagram(id, diagram);
104
104
+
const diagramUrl = `${url.origin}/diagram/${id}`;
105
105
+
return Response.json({ id, url: diagramUrl }, {
106
106
+
status: 201,
107
107
+
headers: {
108
108
+
"Access-Control-Allow-Origin": "*",
109
109
+
},
110
110
+
});
111
111
+
} catch {
112
112
+
return Response.json({ error: "invalid JSON body" }, { status: 400 });
66
113
}
67
67
-
const id = generateId();
68
68
-
const diagram: WalkthroughDiagram = {
69
69
-
code: body.code,
70
70
-
summary: body.summary,
71
71
-
nodes: body.nodes,
72
72
-
createdAt: new Date().toISOString(),
73
73
-
};
74
74
-
diagrams.set(id, diagram);
75
75
-
saveDiagram(id, diagram);
76
76
-
const diagramUrl = `${url.origin}/diagram/${id}`;
77
77
-
return Response.json({ id, url: diagramUrl }, {
78
78
-
status: 201,
114
114
+
}
115
115
+
116
116
+
// OPTIONS /api/diagrams — CORS preflight
117
117
+
if (url.pathname === "/api/diagrams" && req.method === "OPTIONS") {
118
118
+
return new Response(null, {
119
119
+
status: 204,
79
120
headers: {
80
121
"Access-Control-Allow-Origin": "*",
122
122
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
123
123
+
"Access-Control-Allow-Headers": "Content-Type",
81
124
},
82
125
});
83
83
-
} catch {
84
84
-
return Response.json({ error: "invalid JSON body" }, { status: 400 });
85
126
}
86
86
-
}
87
127
88
88
-
// OPTIONS /api/diagrams — CORS preflight
89
89
-
if (url.pathname === "/api/diagrams" && req.method === "OPTIONS") {
90
90
-
return new Response(null, {
91
91
-
status: 204,
92
92
-
headers: {
93
93
-
"Access-Control-Allow-Origin": "*",
94
94
-
"Access-Control-Allow-Methods": "POST, OPTIONS",
95
95
-
"Access-Control-Allow-Headers": "Content-Type",
96
96
-
},
97
97
-
});
98
98
-
}
128
128
+
if (url.pathname === "/icon.svg") {
129
129
+
return new Response(Bun.file(import.meta.dir + "/../icon.svg"), {
130
130
+
headers: { "Content-Type": "image/svg+xml" },
131
131
+
});
132
132
+
}
99
133
100
100
-
if (url.pathname === "/icon.svg") {
101
101
-
return new Response(Bun.file(import.meta.dir + "/../icon.svg"), {
102
102
-
headers: { "Content-Type": "image/svg+xml" },
103
103
-
});
104
104
-
}
134
134
+
// List available diagrams
135
135
+
if (url.pathname === "/") {
136
136
+
const html = MODE === "server"
137
137
+
? generateServerIndexHTML(diagrams.size, GIT_HASH)
138
138
+
: generateLocalIndexHTML(diagrams, GIT_HASH);
139
139
+
return new Response(html, {
140
140
+
headers: { "Content-Type": "text/html; charset=utf-8" },
141
141
+
});
142
142
+
}
105
143
106
106
-
// List available diagrams
107
107
-
if (url.pathname === "/") {
108
108
-
const html = MODE === "server"
109
109
-
? generateServerIndexHTML(diagrams.size, GIT_HASH)
110
110
-
: generateLocalIndexHTML(diagrams, GIT_HASH);
111
111
-
return new Response(html, {
144
144
+
return new Response(generate404HTML("Page not found", "There's nothing at this URL."), {
145
145
+
status: 404,
112
146
headers: { "Content-Type": "text/html; charset=utf-8" },
113
147
});
114
114
-
}
115
115
-
116
116
-
return new Response(generate404HTML("Page not found", "There's nothing at this URL."), {
117
117
-
status: 404,
118
118
-
headers: { "Content-Type": "text/html; charset=utf-8" },
119
119
-
});
120
120
-
},
121
121
-
});
148
148
+
},
149
149
+
});
150
150
+
} catch {
151
151
+
isClient = true;
152
152
+
console.error(`Web server already running on port ${PORT}, running in client mode`);
153
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
161
-
const id = generateId();
162
162
-
const diagram: WalkthroughDiagram = {
163
163
-
code,
164
164
-
summary,
165
165
-
nodes,
166
166
-
createdAt: new Date().toISOString(),
167
167
-
};
168
168
-
diagrams.set(id, diagram);
169
169
-
saveDiagram(id, diagram);
193
193
+
let diagramUrl: string;
170
194
171
171
-
const diagramUrl = `http://localhost:${PORT}/diagram/${id}`;
195
195
+
if (isClient) {
196
196
+
// POST diagram to the existing web server instance
197
197
+
const res = await fetch(`http://localhost:${PORT}/api/diagrams`, {
198
198
+
method: "POST",
199
199
+
headers: { "Content-Type": "application/json" },
200
200
+
body: JSON.stringify({ code, summary, nodes }),
201
201
+
});
202
202
+
if (!res.ok) {
203
203
+
return {
204
204
+
content: [{ type: "text", text: `Failed to send diagram to server: ${res.statusText}` }],
205
205
+
};
206
206
+
}
207
207
+
const data = await res.json() as { id: string; url: string };
208
208
+
diagramUrl = data.url;
209
209
+
} else {
210
210
+
const id = generateId();
211
211
+
const diagram: WalkthroughDiagram = {
212
212
+
code,
213
213
+
summary,
214
214
+
nodes,
215
215
+
createdAt: new Date().toISOString(),
216
216
+
};
217
217
+
diagrams.set(id, diagram);
218
218
+
saveDiagram(id, diagram);
219
219
+
diagramUrl = `http://localhost:${PORT}/diagram/${id}`;
220
220
+
}
172
221
173
222
return {
174
223
content: [
+19
src/storage.ts
···
34
34
created_at TEXT
35
35
)
36
36
`);
37
37
+
db.run(`
38
38
+
CREATE TABLE IF NOT EXISTS shared_urls (
39
39
+
local_id TEXT PRIMARY KEY,
40
40
+
remote_url TEXT,
41
41
+
shared_at TEXT
42
42
+
)
43
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
65
+
}
66
66
+
67
67
+
export function getSharedUrl(localId: string): string | null {
68
68
+
const row = db.query("SELECT remote_url FROM shared_urls WHERE local_id = ?").get(localId) as { remote_url: string } | null;
69
69
+
return row?.remote_url ?? null;
70
70
+
}
71
71
+
72
72
+
export function saveSharedUrl(localId: string, remoteUrl: string): void {
73
73
+
db.run(
74
74
+
"INSERT OR REPLACE INTO shared_urls (local_id, remote_url, shared_at) VALUES (?, ?, ?)",
75
75
+
[localId, remoteUrl, new Date().toISOString()]
76
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
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
11
-
const { mode = "local", shareServerUrl = "", diagramId = "" } = options;
12
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
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
940
+
// If we already have a shared URL, just copy it
941
941
+
if (EXISTING_SHARE_URL) {
942
942
+
await navigator.clipboard.writeText(EXISTING_SHARE_URL);
943
943
+
shareBtn.querySelector("span").textContent = "Copied link!";
944
944
+
shareBtn.classList.add("shared");
945
945
+
setTimeout(() => {
946
946
+
shareBtn.querySelector("span").textContent = "Share";
947
947
+
shareBtn.classList.remove("shared");
948
948
+
}, 2000);
949
949
+
return;
950
950
+
}
951
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
960
+
961
961
+
// Save the shared URL locally so we don't re-upload next time
962
962
+
if (DIAGRAM_ID) {
963
963
+
fetch("/api/diagrams/" + DIAGRAM_ID + "/shared-url", {
964
964
+
method: "POST",
965
965
+
headers: { "Content-Type": "application/json" },
966
966
+
body: JSON.stringify({ url: data.url }),
967
967
+
}).catch(() => {}); // best-effort
968
968
+
EXISTING_SHARE_URL = data.url;
969
969
+
}
970
970
+
946
971
shareBtn.querySelector("span").textContent = "Copied link!";
947
972
shareBtn.classList.add("shared");
948
973
setTimeout(() => {