thurview 0.1.4

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.
Files changed (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +185 -0
  3. package/bin/thurview.js +8 -0
  4. package/dist/cli.js +1101 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/diff.js +65 -0
  7. package/dist/diff.js.map +1 -0
  8. package/dist/document/compile.js +418 -0
  9. package/dist/document/compile.js.map +1 -0
  10. package/dist/document/parse.js +139 -0
  11. package/dist/document/parse.js.map +1 -0
  12. package/dist/document/schema.js +129 -0
  13. package/dist/document/schema.js.map +1 -0
  14. package/dist/flags.js +66 -0
  15. package/dist/flags.js.map +1 -0
  16. package/dist/git.js +206 -0
  17. package/dist/git.js.map +1 -0
  18. package/dist/highlight-theme.js +93 -0
  19. package/dist/highlight-theme.js.map +1 -0
  20. package/dist/highlight.js +163 -0
  21. package/dist/highlight.js.map +1 -0
  22. package/dist/main.js +3 -0
  23. package/dist/main.js.map +1 -0
  24. package/dist/server/server.js +387 -0
  25. package/dist/server/server.js.map +1 -0
  26. package/dist/store.js +100 -0
  27. package/dist/store.js.map +1 -0
  28. package/dist/symbols.js +192 -0
  29. package/dist/symbols.js.map +1 -0
  30. package/dist/theme.js +301 -0
  31. package/dist/theme.js.map +1 -0
  32. package/dist/threads.js +87 -0
  33. package/dist/threads.js.map +1 -0
  34. package/dist/ui/app.css +1415 -0
  35. package/dist/ui/app.js +2303 -0
  36. package/dist/ui/app.js.map +7 -0
  37. package/dist/ui/assets/fonts/inter-400.woff2 +0 -0
  38. package/dist/ui/assets/fonts/jetbrains-mono-400.woff2 +0 -0
  39. package/dist/ui/assets/fonts/press-start-2p-400.woff2 +0 -0
  40. package/dist/ui/assets/inter-400.woff2 +0 -0
  41. package/dist/ui/assets/jetbrains-mono-400.woff2 +0 -0
  42. package/dist/ui/assets/press-start-2p-400.woff2 +0 -0
  43. package/dist/ui/index.html +13 -0
  44. package/dist/version.js +6 -0
  45. package/dist/version.js.map +1 -0
  46. package/package.json +70 -0
  47. package/skills/thurview/SKILL.md +219 -0
  48. package/skills/thurview/references/components.md +120 -0
  49. package/skills/thurview/references/document-authoring.md +107 -0
  50. package/skills/thurview/references/lifecycle.md +88 -0
  51. package/skills/thurview/references/software-map.md +42 -0
  52. package/skills/thurview/references/theme.md +90 -0
@@ -0,0 +1,93 @@
1
+ /** Code palette matching the UI theme: warm charcoal ground, red accent, green strings, yellow names. */
2
+ export const theme = {
3
+ name: "thurview",
4
+ type: "dark",
5
+ colors: { "editor.background": "#0d0b09", "editor.foreground": "#e0e0e0" },
6
+ tokenColors: [
7
+ {
8
+ scope: ["comment", "punctuation.definition.comment", "string.comment"],
9
+ settings: { foreground: "#948a7d", fontStyle: "italic" },
10
+ },
11
+ {
12
+ scope: ["punctuation", "meta.brace", "punctuation.separator", "punctuation.terminator"],
13
+ settings: { foreground: "#948a7d" },
14
+ },
15
+ {
16
+ scope: [
17
+ "keyword",
18
+ "storage",
19
+ "storage.type",
20
+ "storage.modifier",
21
+ "keyword.control",
22
+ "constant.language",
23
+ "variable.language",
24
+ ],
25
+ settings: { foreground: "#ff8c8c" },
26
+ },
27
+ {
28
+ scope: ["keyword.operator", "keyword.operator.assignment", "keyword.operator.arrow"],
29
+ settings: { foreground: "#ff5c54" },
30
+ },
31
+ {
32
+ scope: [
33
+ "string",
34
+ "string.quoted",
35
+ "string.template",
36
+ "punctuation.definition.string",
37
+ "markup.inserted",
38
+ ],
39
+ settings: { foreground: "#6eff6e" },
40
+ },
41
+ {
42
+ scope: [
43
+ "entity.name.function",
44
+ "support.function",
45
+ "meta.function-call entity.name.function",
46
+ "entity.name.type",
47
+ "entity.name.class",
48
+ "support.class",
49
+ "support.type",
50
+ "entity.other.inherited-class",
51
+ "entity.name.namespace",
52
+ ],
53
+ settings: { foreground: "#ffb627" },
54
+ },
55
+ {
56
+ scope: [
57
+ "constant.numeric",
58
+ "constant.character",
59
+ "constant.other",
60
+ "constant.language.boolean",
61
+ ],
62
+ settings: { foreground: "#ffb627" },
63
+ },
64
+ {
65
+ scope: [
66
+ "entity.name.tag",
67
+ "support.type.property-name",
68
+ "meta.object-literal.key",
69
+ "entity.other.attribute-name",
70
+ "meta.property-name",
71
+ ],
72
+ settings: { foreground: "#ff5c54" },
73
+ },
74
+ {
75
+ scope: [
76
+ "variable",
77
+ "variable.parameter",
78
+ "variable.other",
79
+ "meta.definition.variable",
80
+ "entity.name.variable",
81
+ ],
82
+ settings: { foreground: "#00d9ff" },
83
+ },
84
+ { scope: ["markup.deleted"], settings: { foreground: "#ff3b30" } },
85
+ {
86
+ scope: ["markup.heading", "entity.name.section"],
87
+ settings: { foreground: "#ff5c54", fontStyle: "bold" },
88
+ },
89
+ { scope: ["markup.bold"], settings: { fontStyle: "bold" } },
90
+ { scope: ["markup.italic"], settings: { fontStyle: "italic" } },
91
+ ],
92
+ };
93
+ //# sourceMappingURL=highlight-theme.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight-theme.js","sourceRoot":"","sources":["../src/highlight-theme.ts"],"names":[],"mappings":"AAEA,yGAAyG;AACzG,MAAM,CAAC,MAAM,KAAK,GAAsB;IACtC,IAAI,EAAE,UAAU;IAChB,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,EAAE,mBAAmB,EAAE,SAAS,EAAE,mBAAmB,EAAE,SAAS,EAAE;IAC1E,WAAW,EAAE;QACX;YACE,KAAK,EAAE,CAAC,SAAS,EAAE,gCAAgC,EAAE,gBAAgB,CAAC;YACtE,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE;SACzD;QACD;YACE,KAAK,EAAE,CAAC,aAAa,EAAE,YAAY,EAAE,uBAAuB,EAAE,wBAAwB,CAAC;YACvF,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE;gBACL,SAAS;gBACT,SAAS;gBACT,cAAc;gBACd,kBAAkB;gBAClB,iBAAiB;gBACjB,mBAAmB;gBACnB,mBAAmB;aACpB;YACD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE,CAAC,kBAAkB,EAAE,6BAA6B,EAAE,wBAAwB,CAAC;YACpF,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE;gBACL,QAAQ;gBACR,eAAe;gBACf,iBAAiB;gBACjB,+BAA+B;gBAC/B,iBAAiB;aAClB;YACD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE;gBACL,sBAAsB;gBACtB,kBAAkB;gBAClB,yCAAyC;gBACzC,kBAAkB;gBAClB,mBAAmB;gBACnB,eAAe;gBACf,cAAc;gBACd,8BAA8B;gBAC9B,uBAAuB;aACxB;YACD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE;gBACL,kBAAkB;gBAClB,oBAAoB;gBACpB,gBAAgB;gBAChB,2BAA2B;aAC5B;YACD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE;gBACL,iBAAiB;gBACjB,4BAA4B;gBAC5B,yBAAyB;gBACzB,6BAA6B;gBAC7B,oBAAoB;aACrB;YACD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD;YACE,KAAK,EAAE;gBACL,UAAU;gBACV,oBAAoB;gBACpB,gBAAgB;gBAChB,0BAA0B;gBAC1B,sBAAsB;aACvB;YACD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE;SACpC;QACD,EAAE,KAAK,EAAE,CAAC,gBAAgB,CAAC,EAAE,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,EAAE;QAClE;YACE,KAAK,EAAE,CAAC,gBAAgB,EAAE,qBAAqB,CAAC;YAChD,QAAQ,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE;SACvD;QACD,EAAE,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE;QAC3D,EAAE,KAAK,EAAE,CAAC,eAAe,CAAC,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE;KAChE;CACF,CAAC"}
@@ -0,0 +1,163 @@
1
+ import { createHighlighter, bundledLanguages, } from "shiki";
2
+ import { theme } from "./highlight-theme.js";
3
+ const EXT = {
4
+ ts: "typescript",
5
+ tsx: "tsx",
6
+ mts: "typescript",
7
+ cts: "typescript",
8
+ js: "javascript",
9
+ jsx: "jsx",
10
+ mjs: "javascript",
11
+ cjs: "javascript",
12
+ py: "python",
13
+ rb: "ruby",
14
+ go: "go",
15
+ rs: "rust",
16
+ java: "java",
17
+ kt: "kotlin",
18
+ kts: "kotlin",
19
+ c: "c",
20
+ h: "c",
21
+ cc: "cpp",
22
+ cpp: "cpp",
23
+ hpp: "cpp",
24
+ cs: "csharp",
25
+ swift: "swift",
26
+ m: "objective-c",
27
+ php: "php",
28
+ lua: "lua",
29
+ sh: "bash",
30
+ bash: "bash",
31
+ zsh: "bash",
32
+ fish: "fish",
33
+ ps1: "powershell",
34
+ json: "json",
35
+ jsonc: "jsonc",
36
+ yaml: "yaml",
37
+ yml: "yaml",
38
+ toml: "toml",
39
+ xml: "xml",
40
+ html: "html",
41
+ css: "css",
42
+ scss: "scss",
43
+ less: "less",
44
+ md: "markdown",
45
+ mdx: "mdx",
46
+ sql: "sql",
47
+ graphql: "graphql",
48
+ nix: "nix",
49
+ tf: "hcl",
50
+ hcl: "hcl",
51
+ dockerfile: "dockerfile",
52
+ vue: "vue",
53
+ svelte: "svelte",
54
+ ex: "elixir",
55
+ exs: "elixir",
56
+ erl: "erlang",
57
+ hs: "haskell",
58
+ scala: "scala",
59
+ clj: "clojure",
60
+ dart: "dart",
61
+ zig: "zig",
62
+ proto: "proto",
63
+ cmake: "cmake",
64
+ make: "makefile",
65
+ mk: "makefile",
66
+ ini: "ini",
67
+ cfg: "ini",
68
+ diff: "diff",
69
+ patch: "diff",
70
+ txt: "text",
71
+ csv: "csv",
72
+ svg: "xml",
73
+ };
74
+ const NAME = {
75
+ Dockerfile: "dockerfile",
76
+ Makefile: "makefile",
77
+ CMakeLists: "cmake",
78
+ };
79
+ export function languageFor(path) {
80
+ const name = path.split("/").pop() ?? "";
81
+ if (NAME[name])
82
+ return NAME[name];
83
+ const ext = name.includes(".") ? name.split(".").pop().toLowerCase() : name.toLowerCase();
84
+ return EXT[ext] ?? "text";
85
+ }
86
+ let hl = null;
87
+ const registered = new Set(["thurview"]);
88
+ async function highlighter() {
89
+ if (!hl)
90
+ hl = createHighlighter({ themes: [theme], langs: [] });
91
+ return hl;
92
+ }
93
+ /** Register a per-review highlighter theme; repeated registrations are no-ops. */
94
+ export async function registerTheme(reg) {
95
+ const name = reg.name ?? "thurview";
96
+ if (!registered.has(name)) {
97
+ await (await highlighter()).loadTheme(reg);
98
+ registered.add(name);
99
+ }
100
+ return name;
101
+ }
102
+ function escapeHtml(s) {
103
+ return s
104
+ .replace(/&/g, "&")
105
+ .replace(/</g, "&lt;")
106
+ .replace(/>/g, "&gt;")
107
+ .replace(/"/g, "&quot;");
108
+ }
109
+ const cache = new Map();
110
+ /** Highlight a whole file; returns one HTML fragment per line (no trailing newline line). */
111
+ export async function highlightLines(code, lang, cacheKey, themeName = "thurview") {
112
+ if (cacheKey)
113
+ cacheKey = `${themeName}|${cacheKey}`;
114
+ if (cacheKey && cache.has(cacheKey))
115
+ return cache.get(cacheKey);
116
+ const lines = code.endsWith("\n") ? code.slice(0, -1).split("\n") : code.split("\n");
117
+ let out;
118
+ if (lang === "text" || !(lang in bundledLanguages) || code.length > 1_500_000) {
119
+ out = lines.map(escapeHtml);
120
+ }
121
+ else {
122
+ const h = await highlighter();
123
+ if (!h.getLoadedLanguages().includes(lang)) {
124
+ try {
125
+ await h.loadLanguage(lang);
126
+ }
127
+ catch {
128
+ out = lines.map(escapeHtml);
129
+ return remember(cacheKey, out);
130
+ }
131
+ }
132
+ const result = h.codeToTokens(code, { lang: lang, theme: themeName });
133
+ out = result.tokens.map((toks) => toks
134
+ .map((t) => {
135
+ const style = [
136
+ t.color ? `color:${t.color}` : "",
137
+ t.fontStyle === 2 ? "font-style:italic" : t.fontStyle === 1 ? "font-weight:700" : "",
138
+ ]
139
+ .filter(Boolean)
140
+ .join(";");
141
+ return style
142
+ ? `<span style="${style}">${escapeHtml(t.content)}</span>`
143
+ : escapeHtml(t.content);
144
+ })
145
+ .join(""));
146
+ // keep counts aligned with `lines`: shiki may add or drop a trailing empty line
147
+ while (out.length < lines.length)
148
+ out.push("");
149
+ if (out.length > lines.length)
150
+ out.length = lines.length;
151
+ }
152
+ return remember(cacheKey, out);
153
+ }
154
+ function remember(key, v) {
155
+ if (key) {
156
+ if (cache.size > 200)
157
+ cache.delete(cache.keys().next().value);
158
+ cache.set(key, v);
159
+ }
160
+ return v;
161
+ }
162
+ export { escapeHtml };
163
+ //# sourceMappingURL=highlight.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"highlight.js","sourceRoot":"","sources":["../src/highlight.ts"],"names":[],"mappings":"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"}
package/dist/main.js ADDED
@@ -0,0 +1,3 @@
1
+ import { main } from "./cli.js";
2
+ await main();
3
+ //# sourceMappingURL=main.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,MAAM,IAAI,EAAE,CAAC"}
@@ -0,0 +1,387 @@
1
+ import http from "node:http";
2
+ import { readFile } from "node:fs/promises";
3
+ import { watch, existsSync } from "node:fs";
4
+ import { networkInterfaces } from "node:os";
5
+ import { join, dirname } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { buildFileDiff } from "../diff.js";
8
+ import { highlightLines, languageFor } from "../highlight.js";
9
+ import { changedFiles, log, showFile, git } from "../git.js";
10
+ import { symbolIndex } from "../symbols.js";
11
+ import { registerTheme } from "../highlight.js";
12
+ import { listReviews, readReview, writeReview, readThreads, readJson, reviewDir, revisionDir, deleteReview, writeJson, serverStateFile, } from "../store.js";
13
+ import { createThread, replyThread, setThreadStatus, submitReview, deleteThread, } from "../threads.js";
14
+ const UI_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "ui");
15
+ class HttpError extends Error {
16
+ status;
17
+ constructor(status, message) {
18
+ super(message);
19
+ this.status = status;
20
+ }
21
+ }
22
+ function send(res, status, body, type = "application/json") {
23
+ const data = typeof body === "string" ? body : JSON.stringify(body);
24
+ res.writeHead(status, {
25
+ "content-type": type + (type.startsWith("text") || type.includes("json") ? "; charset=utf-8" : ""),
26
+ "cache-control": "no-store",
27
+ });
28
+ res.end(data);
29
+ }
30
+ async function readBody(req) {
31
+ const chunks = [];
32
+ for await (const c of req)
33
+ chunks.push(c);
34
+ const text = Buffer.concat(chunks).toString("utf8");
35
+ if (!text)
36
+ return {};
37
+ try {
38
+ return JSON.parse(text);
39
+ }
40
+ catch {
41
+ throw new HttpError(400, "invalid JSON body");
42
+ }
43
+ }
44
+ async function findReview(idOrPrefix) {
45
+ const r = await readReview(idOrPrefix);
46
+ if (r)
47
+ return r;
48
+ const all = (await listReviews()).filter((x) => x.id.startsWith(idOrPrefix));
49
+ if (all.length === 1)
50
+ return all[0];
51
+ throw new HttpError(404, all.length ? "ambiguous review id" : "review not found");
52
+ }
53
+ async function revisionData(id, n) {
54
+ const dir = revisionDir(id, n);
55
+ const [document, map, changes, meta, theme] = await Promise.all([
56
+ readJson(join(dir, "document.json")),
57
+ readJson(join(dir, "map.json")),
58
+ readJson(join(dir, "changes.json")),
59
+ readJson(join(dir, "meta.json")),
60
+ readJson(join(dir, "theme.json")),
61
+ ]);
62
+ return {
63
+ document,
64
+ map,
65
+ changes: changes ?? [],
66
+ meta,
67
+ theme: theme ? { name: theme.name, source: theme.source, css: theme.css } : null,
68
+ };
69
+ }
70
+ /** Highlighter theme name for a review's presented revision (default skin when none). */
71
+ async function themeFor(id, revision) {
72
+ if (!revision)
73
+ return undefined;
74
+ const t = await readJson(join(revisionDir(id, revision), "theme.json"));
75
+ return t ? registerTheme(t.shiki) : undefined;
76
+ }
77
+ const BLOB_TYPES = {
78
+ woff2: "font/woff2",
79
+ woff: "font/woff",
80
+ ttf: "font/ttf",
81
+ otf: "font/otf",
82
+ svg: "image/svg+xml",
83
+ png: "image/png",
84
+ jpg: "image/jpeg",
85
+ jpeg: "image/jpeg",
86
+ webp: "image/webp",
87
+ css: "text/css",
88
+ };
89
+ export function tailscaleAddresses() {
90
+ const out = [];
91
+ for (const [name, addrs] of Object.entries(networkInterfaces())) {
92
+ for (const a of addrs ?? []) {
93
+ if (a.family !== "IPv4")
94
+ continue;
95
+ const [o1, o2] = a.address.split(".").map(Number);
96
+ if (name.startsWith("tailscale") || (o1 === 100 && (o2 ?? 0) >= 64 && (o2 ?? 0) <= 127))
97
+ out.push(a.address);
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+ export async function startServer(opts = {}) {
103
+ const clients = new Map();
104
+ const watchers = new Map();
105
+ function subscribe(id, res) {
106
+ let set = clients.get(id);
107
+ if (!set) {
108
+ set = new Set();
109
+ clients.set(id, set);
110
+ }
111
+ set.add(res);
112
+ if (!watchers.has(id) && existsSync(reviewDir(id))) {
113
+ let timer = null;
114
+ const w = watch(reviewDir(id), () => {
115
+ if (timer)
116
+ clearTimeout(timer);
117
+ timer = setTimeout(() => {
118
+ for (const c of clients.get(id) ?? [])
119
+ c.write(`data: ${JSON.stringify({ type: "change" })}\n\n`);
120
+ }, 150);
121
+ });
122
+ watchers.set(id, w);
123
+ }
124
+ res.on("close", () => {
125
+ set.delete(res);
126
+ if (set.size === 0) {
127
+ watchers.get(id)?.close();
128
+ watchers.delete(id);
129
+ clients.delete(id);
130
+ }
131
+ });
132
+ }
133
+ async function api(req, url) {
134
+ const parts = url.pathname.split("/").filter(Boolean); // ["api", ...]
135
+ const method = req.method ?? "GET";
136
+ if (parts[1] === "health")
137
+ return { ok: true, pid: process.pid };
138
+ if (parts[1] !== "reviews")
139
+ throw new HttpError(404, "not found");
140
+ if (!parts[2]) {
141
+ const reviews = await listReviews();
142
+ const out = [];
143
+ for (const r of reviews) {
144
+ const t = await readThreads(r.id);
145
+ out.push({ ...r, openThreads: t.threads.filter((x) => x.status === "open").length });
146
+ }
147
+ return out;
148
+ }
149
+ const review = await findReview(parts[2]);
150
+ const id = review.id;
151
+ const sub = parts[3];
152
+ if (!sub) {
153
+ if (method === "DELETE") {
154
+ await deleteReview(id);
155
+ return { ok: true };
156
+ }
157
+ const n = Number(url.searchParams.get("revision") ?? review.revision);
158
+ const data = review.revision
159
+ ? await revisionData(id, n)
160
+ : { document: null, map: null, changes: [], meta: null };
161
+ const threads = await readThreads(id);
162
+ return {
163
+ review,
164
+ revision: n,
165
+ ...data,
166
+ threads: threads.threads,
167
+ decisions: threads.decisions,
168
+ };
169
+ }
170
+ if (sub === "events") {
171
+ throw new HttpError(500, "handled elsewhere");
172
+ }
173
+ if (sub === "revisions") {
174
+ const out = [];
175
+ for (let n = 1; n <= review.revision; n++)
176
+ out.push(await readJson(join(revisionDir(id, n), "meta.json")));
177
+ return out.filter(Boolean);
178
+ }
179
+ if (sub === "changes") {
180
+ return await changedFiles(review.worktree, review.pins.base, review.pins.head);
181
+ }
182
+ if (sub === "commits") {
183
+ return await log(review.worktree, review.pins.base, review.pins.head);
184
+ }
185
+ if (sub === "diff") {
186
+ const path = url.searchParams.get("path") ?? "";
187
+ if (!path)
188
+ throw new HttpError(400, "path required");
189
+ const changes = await changedFiles(review.worktree, review.pins.base, review.pins.head);
190
+ const entry = changes.find((c) => c.path === path);
191
+ const oldPath = entry?.oldPath ?? path;
192
+ const [oldText, newText] = await Promise.all([
193
+ entry?.status === "A"
194
+ ? Promise.resolve(null)
195
+ : showFile(review.worktree, review.pins.base, oldPath),
196
+ entry?.status === "D"
197
+ ? Promise.resolve(null)
198
+ : showFile(review.worktree, review.pins.head, path),
199
+ ]);
200
+ return (await buildFileDiff(path, oldText, newText, { old: `${review.pins.base}:${oldPath}`, new: `${review.pins.head}:${path}` }, entry?.oldPath, await themeFor(id, review.revision)));
201
+ }
202
+ if (sub === "file") {
203
+ const path = url.searchParams.get("path") ?? "";
204
+ const graph = url.searchParams.get("graph") === "base" ? "base" : "head";
205
+ const commit = graph === "base" ? review.pins.base : review.pins.head;
206
+ const text = await showFile(review.worktree, commit, path);
207
+ if (text === null)
208
+ throw new HttpError(404, `${path} not found at ${graph}`);
209
+ const lang = languageFor(path);
210
+ const all = await highlightLines(text, lang, `${commit}:${path}`, await themeFor(id, review.revision));
211
+ const from = Math.max(1, Number(url.searchParams.get("from") ?? 1));
212
+ const to = Math.min(all.length, Number(url.searchParams.get("to") ?? all.length));
213
+ return { path, graph, lang, total: all.length, from, to, lines: all.slice(from - 1, to) };
214
+ }
215
+ if (sub === "symbols") {
216
+ const name = url.searchParams.get("name") ?? "";
217
+ const graph = url.searchParams.get("graph") === "base" ? "base" : "head";
218
+ if (!/^[A-Za-z_$][\w$]*$/.test(name))
219
+ return [];
220
+ const idx = symbolIndex(review.worktree, graph === "base" ? review.pins.base : review.pins.head);
221
+ return (await idx.lookup(name)).slice(0, 20);
222
+ }
223
+ if (sub === "threads") {
224
+ const tid = parts[4];
225
+ if (method === "GET")
226
+ return (await readThreads(id)).threads;
227
+ if (method === "POST" && !tid) {
228
+ const b = await readBody(req);
229
+ const body = String(b["body"] ?? "").trim();
230
+ if (!body)
231
+ throw new HttpError(400, "body required");
232
+ return createThread(id, {
233
+ kind: b["kind"] === "question" ? "question" : "comment",
234
+ mode: b["mode"] === "ask" ? "ask" : "review",
235
+ target: b["target"] ?? { type: "review" },
236
+ body,
237
+ });
238
+ }
239
+ if (method === "POST" && tid) {
240
+ const action = parts[5];
241
+ const b = await readBody(req);
242
+ if (action === "reply")
243
+ return replyThread(id, tid, b["role"] === "agent" ? "agent" : "reviewer", String(b["body"] ?? ""));
244
+ if (action === "resolve")
245
+ return setThreadStatus(id, tid, "resolved");
246
+ if (action === "reopen")
247
+ return setThreadStatus(id, tid, "open");
248
+ if (action === "delete") {
249
+ await deleteThread(id, tid);
250
+ return { ok: true };
251
+ }
252
+ }
253
+ throw new HttpError(404, "not found");
254
+ }
255
+ if (sub === "submit" && method === "POST") {
256
+ const b = await readBody(req);
257
+ const decision = b["decision"] === "approve" ? "approve" : "request-changes";
258
+ const body = String(b["body"] ?? "").trim();
259
+ const t = await submitReview(id, decision, body || undefined);
260
+ return { review: await readReview(id), decisions: t.decisions };
261
+ }
262
+ if (sub === "dismiss" && method === "POST") {
263
+ const b = await readBody(req);
264
+ review.dismissed = b["dismissed"] !== false;
265
+ await writeReview(review);
266
+ return review;
267
+ }
268
+ throw new HttpError(404, "not found");
269
+ }
270
+ const server = http.createServer(async (req, res) => {
271
+ const url = new URL(req.url ?? "/", "http://localhost");
272
+ try {
273
+ if (url.pathname.startsWith("/api/")) {
274
+ const parts = url.pathname.split("/").filter(Boolean);
275
+ if (parts[1] === "reviews" && parts[3] === "events" && parts[2]) {
276
+ const review = await findReview(parts[2]);
277
+ res.writeHead(200, {
278
+ "content-type": "text/event-stream",
279
+ "cache-control": "no-store",
280
+ connection: "keep-alive",
281
+ });
282
+ res.write(`data: ${JSON.stringify({ type: "hello" })}\n\n`);
283
+ subscribe(review.id, res);
284
+ const ping = setInterval(() => res.write(": ping\n\n"), 25000);
285
+ res.on("close", () => clearInterval(ping));
286
+ return;
287
+ }
288
+ if (parts[1] === "reviews" && parts[3] === "blob" && parts[2]) {
289
+ // raw file at the head commit, for theme fonts and images the reviewed project ships
290
+ const review = await findReview(parts[2]);
291
+ const path = url.searchParams.get("path") ?? "";
292
+ const ext = path.split(".").pop()?.toLowerCase() ?? "";
293
+ const type = BLOB_TYPES[ext];
294
+ if (!path || !type)
295
+ throw new HttpError(400, "path must name a font, image or css file");
296
+ const text = await git(review.worktree, ["show", `${review.pins.head}:${path}`], {
297
+ encoding: "buffer",
298
+ });
299
+ res.writeHead(200, {
300
+ "content-type": type,
301
+ "cache-control": "public, max-age=31536000, immutable",
302
+ });
303
+ res.end(text);
304
+ return;
305
+ }
306
+ const body = await api(req, url);
307
+ send(res, 200, body);
308
+ return;
309
+ }
310
+ if (url.pathname.startsWith("/assets/")) {
311
+ const rel = url.pathname
312
+ .slice(1)
313
+ .split("/")
314
+ .filter((p) => p && p !== "..")
315
+ .join("/");
316
+ const type = rel.endsWith(".woff2")
317
+ ? "font/woff2"
318
+ : rel.endsWith(".svg")
319
+ ? "image/svg+xml"
320
+ : "application/octet-stream";
321
+ let data;
322
+ try {
323
+ data = await readFile(join(UI_DIR, rel));
324
+ }
325
+ catch {
326
+ throw new HttpError(404, "asset not found");
327
+ }
328
+ res.writeHead(200, {
329
+ "content-type": type,
330
+ "cache-control": "public, max-age=31536000, immutable",
331
+ });
332
+ res.end(data);
333
+ return;
334
+ }
335
+ const file = url.pathname === "/app.js"
336
+ ? "app.js"
337
+ : url.pathname === "/app.css"
338
+ ? "app.css"
339
+ : "index.html";
340
+ const type = file.endsWith(".js")
341
+ ? "text/javascript"
342
+ : file.endsWith(".css")
343
+ ? "text/css"
344
+ : "text/html";
345
+ send(res, 200, await readFile(join(UI_DIR, file), "utf8"), type);
346
+ }
347
+ catch (e) {
348
+ const status = e instanceof HttpError ? e.status : 500;
349
+ send(res, status, { error: e.message });
350
+ }
351
+ });
352
+ // Warm the syntax highlighter so the first diff does not pay the grammar load.
353
+ void highlightLines("const warm = 1;\n", "typescript").catch(() => { });
354
+ const hosts = opts.hosts ?? ["127.0.0.1", ...tailscaleAddresses()];
355
+ const port = await new Promise((resolve, reject) => {
356
+ server.once("error", reject);
357
+ server.listen(opts.port ?? 0, hosts[0], () => resolve(server.address().port));
358
+ });
359
+ const extra = [];
360
+ for (const h of hosts.slice(1)) {
361
+ const s = http.createServer(server.listeners("request")[0]);
362
+ await new Promise((resolve) => {
363
+ s.once("error", () => resolve());
364
+ s.listen(port, h, () => resolve());
365
+ });
366
+ extra.push(s);
367
+ }
368
+ await writeJson(serverStateFile(), {
369
+ pid: process.pid,
370
+ port,
371
+ hosts,
372
+ startedAt: new Date().toISOString(),
373
+ });
374
+ return {
375
+ port,
376
+ hosts,
377
+ close: async () => {
378
+ for (const s of [server, ...extra]) {
379
+ s.closeAllConnections();
380
+ await new Promise((r) => s.close(() => r()));
381
+ }
382
+ for (const w of watchers.values())
383
+ w.close();
384
+ },
385
+ };
386
+ }
387
+ //# sourceMappingURL=server.js.map