getgloss 0.1.0

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,427 @@
1
+ // src/server/daemon.ts
2
+ import { serve } from "@hono/node-server";
3
+
4
+ // src/cli/lifecycle.ts
5
+ import { spawn } from "child_process";
6
+ import { existsSync, openSync } from "fs";
7
+ import { readFile, rm, writeFile } from "fs/promises";
8
+ import { fileURLToPath } from "url";
9
+ import getPort from "get-port";
10
+
11
+ // src/shared/paths.ts
12
+ import { mkdir } from "fs/promises";
13
+ import { homedir } from "os";
14
+ import path from "path";
15
+ var packageVersion = "0.1.0";
16
+ function expandHome(input) {
17
+ if (input === "~") {
18
+ return homedir();
19
+ }
20
+ if (input.startsWith("~/")) {
21
+ return path.join(homedir(), input.slice(2));
22
+ }
23
+ return input;
24
+ }
25
+ function globalStateDir() {
26
+ return expandHome(process.env.GLOSS_STATE_DIR ?? "~/.gloss");
27
+ }
28
+ function globalServerFile() {
29
+ return path.join(globalStateDir(), "server.json");
30
+ }
31
+ function repoGlossDir(cwd) {
32
+ return path.join(cwd, ".gloss");
33
+ }
34
+ function reviewsDir(cwd) {
35
+ return path.join(repoGlossDir(cwd), "reviews");
36
+ }
37
+ function reviewDir(cwd, reviewId) {
38
+ return path.join(reviewsDir(cwd), reviewId);
39
+ }
40
+ async function ensureDir(dir) {
41
+ await mkdir(dir, { recursive: true });
42
+ }
43
+
44
+ // src/cli/lifecycle.ts
45
+ async function writeServerInfo(info) {
46
+ await ensureDir(globalStateDir());
47
+ await writeFile(globalServerFile(), `${JSON.stringify(info, null, 2)}
48
+ `);
49
+ }
50
+
51
+ // src/server/index.ts
52
+ import { readFile as readFile2 } from "fs/promises";
53
+ import path3 from "path";
54
+ import { fileURLToPath as fileURLToPath2 } from "url";
55
+ import { Hono } from "hono";
56
+
57
+ // src/server/store.ts
58
+ import { mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
59
+ import path2 from "path";
60
+ import { ulid } from "ulid";
61
+
62
+ // src/shared/markdown.ts
63
+ function formatLineRange(comment) {
64
+ const prefix = comment.side;
65
+ if (comment.startLine === comment.endLine) {
66
+ return `${prefix}${comment.startLine}`;
67
+ }
68
+ return `${prefix}${comment.startLine}-${prefix}${comment.endLine}`;
69
+ }
70
+ function fenceFor(snippet) {
71
+ let fence = "```";
72
+ while (snippet.includes(fence)) {
73
+ fence += "`";
74
+ }
75
+ return fence;
76
+ }
77
+ function languageForPath(filePath) {
78
+ const ext = filePath.split(".").pop()?.toLowerCase();
79
+ const map = {
80
+ cjs: "js",
81
+ css: "css",
82
+ go: "go",
83
+ html: "html",
84
+ js: "js",
85
+ json: "json",
86
+ jsx: "jsx",
87
+ md: "markdown",
88
+ mjs: "js",
89
+ py: "python",
90
+ rb: "ruby",
91
+ rs: "rust",
92
+ sh: "bash",
93
+ swift: "swift",
94
+ ts: "ts",
95
+ tsx: "tsx",
96
+ yaml: "yaml",
97
+ yml: "yaml"
98
+ };
99
+ return ext ? map[ext] ?? ext : "";
100
+ }
101
+ function byFileThenLine(a, b) {
102
+ return a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side);
103
+ }
104
+ function serializeFeedbackMarkdown(bundle) {
105
+ const comments = [...bundle.comments].sort(byFileThenLine);
106
+ const files = [...new Set(comments.map((comment) => comment.filePath))];
107
+ const lines = [
108
+ `# Gloss feedback - ${bundle.timestamp}`,
109
+ `Review: ${bundle.reviewId}`,
110
+ `Base: ${bundle.base.ref} (${bundle.base.sha.slice(0, 7)}) Branch: ${bundle.branch ?? "(detached)"}`,
111
+ `Files: ${files.length} Comments: ${comments.length}`,
112
+ ""
113
+ ];
114
+ for (const filePath of files) {
115
+ lines.push(`## ${filePath}`, "");
116
+ for (const comment of comments.filter((item) => item.filePath === filePath)) {
117
+ const snippet = comment.originalSnippet.trimEnd();
118
+ const firstSnippetLine = snippet.split("\n").find((line) => line.trim().length > 0);
119
+ const heading = comment.startLine === comment.endLine && firstSnippetLine ? `### ${formatLineRange(comment)} - \`${firstSnippetLine.trim().slice(0, 80)}\`` : `### ${formatLineRange(comment)}`;
120
+ lines.push(heading, comment.body.trim(), "");
121
+ if (snippet) {
122
+ const fence = fenceFor(snippet);
123
+ lines.push(`${fence}${languageForPath(comment.filePath)}`, snippet, fence, "");
124
+ }
125
+ }
126
+ }
127
+ return `${lines.join("\n").trimEnd()}
128
+ `;
129
+ }
130
+
131
+ // src/server/store.ts
132
+ var ReviewStore = class {
133
+ reviews = /* @__PURE__ */ new Map();
134
+ listeners = /* @__PURE__ */ new Map();
135
+ async create(diff) {
136
+ const id = ulid();
137
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
138
+ const meta = {
139
+ id,
140
+ cwd: diff.cwd,
141
+ base: diff.base,
142
+ branch: diff.branch,
143
+ status: "pending",
144
+ createdAt
145
+ };
146
+ const record = { meta, diff };
147
+ this.reviews.set(id, record);
148
+ await this.persistInitial(record);
149
+ this.emit({ type: "review.opened", reviewId: id });
150
+ return record;
151
+ }
152
+ list() {
153
+ return [...this.reviews.values()].map((record) => record.meta).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
154
+ }
155
+ async get(id) {
156
+ return this.reviews.get(id) ?? await this.loadKnownReview(id);
157
+ }
158
+ async submit(id, comments) {
159
+ const record = await this.get(id);
160
+ if (!record) {
161
+ throw new Error(`Review ${id} not found`);
162
+ }
163
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
164
+ const feedback = {
165
+ version: 1,
166
+ reviewId: id,
167
+ timestamp,
168
+ base: record.diff.base,
169
+ branch: record.diff.branch,
170
+ comments: [...comments].sort(
171
+ (a, b) => a.filePath.localeCompare(b.filePath) || a.startLine - b.startLine || a.endLine - b.endLine || a.side.localeCompare(b.side)
172
+ )
173
+ };
174
+ record.feedback = feedback;
175
+ record.meta = { ...record.meta, status: "completed", completedAt: timestamp };
176
+ this.reviews.set(id, record);
177
+ const dir = reviewDir(record.meta.cwd, id);
178
+ const feedbackPath = path2.join(dir, "feedback.json");
179
+ const markdownPath = path2.join(dir, "feedback.md");
180
+ await ensureDir(dir);
181
+ await Promise.all([
182
+ writeFile2(path2.join(dir, "meta.json"), `${JSON.stringify(record.meta, null, 2)}
183
+ `),
184
+ writeFile2(feedbackPath, `${JSON.stringify(feedback, null, 2)}
185
+ `),
186
+ writeFile2(markdownPath, serializeFeedbackMarkdown(feedback))
187
+ ]);
188
+ this.emit({
189
+ type: "review.completed",
190
+ reviewId: id,
191
+ counts: {
192
+ files: new Set(feedback.comments.map((comment) => comment.filePath)).size,
193
+ comments: feedback.comments.length
194
+ }
195
+ });
196
+ return { record, feedbackPath, markdownPath };
197
+ }
198
+ async feedback(id) {
199
+ const record = await this.get(id);
200
+ return record?.feedback ?? null;
201
+ }
202
+ async markResolved(id, summary) {
203
+ const record = await this.get(id);
204
+ if (!record) {
205
+ throw new Error(`Review ${id} not found`);
206
+ }
207
+ const resolvedPath = path2.join(reviewDir(record.meta.cwd, id), "resolved.json");
208
+ await writeFile2(
209
+ resolvedPath,
210
+ `${JSON.stringify({ reviewId: id, summary: summary ?? null, resolvedAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)}
211
+ `
212
+ );
213
+ return resolvedPath;
214
+ }
215
+ subscribe(reviewId, listener) {
216
+ const listeners = this.listeners.get(reviewId) ?? /* @__PURE__ */ new Set();
217
+ listeners.add(listener);
218
+ this.listeners.set(reviewId, listeners);
219
+ return () => {
220
+ listeners.delete(listener);
221
+ if (listeners.size === 0) {
222
+ this.listeners.delete(reviewId);
223
+ }
224
+ };
225
+ }
226
+ emit(event) {
227
+ for (const listener of this.listeners.get(event.reviewId) ?? []) {
228
+ listener(event);
229
+ }
230
+ }
231
+ async persistInitial(record) {
232
+ await ensureDir(repoGlossDir(record.meta.cwd));
233
+ await writeFile2(path2.join(repoGlossDir(record.meta.cwd), ".gitignore"), "*\n!.gitignore\n");
234
+ const dir = reviewDir(record.meta.cwd, record.meta.id);
235
+ await ensureDir(dir);
236
+ await mkdir2(path2.join(dir, "original"), { recursive: true });
237
+ await Promise.all([
238
+ writeFile2(path2.join(dir, "meta.json"), `${JSON.stringify(record.meta, null, 2)}
239
+ `),
240
+ writeFile2(path2.join(dir, "diff.json"), `${JSON.stringify(record.diff, null, 2)}
241
+ `)
242
+ ]);
243
+ }
244
+ async loadKnownReview(id) {
245
+ for (const meta of this.reviews.values()) {
246
+ if (meta.meta.id === id) {
247
+ return meta;
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+ };
253
+ var reviewStore = new ReviewStore();
254
+
255
+ // src/server/index.ts
256
+ var webRoot = fileURLToPath2(new URL("../web", import.meta.url));
257
+ var mimeTypes = {
258
+ ".css": "text/css; charset=utf-8",
259
+ ".html": "text/html; charset=utf-8",
260
+ ".js": "application/javascript; charset=utf-8",
261
+ ".json": "application/json; charset=utf-8",
262
+ ".map": "application/json; charset=utf-8",
263
+ ".svg": "image/svg+xml"
264
+ };
265
+ function createApp(origin2) {
266
+ const app = new Hono();
267
+ app.get(
268
+ "/api/health",
269
+ (c) => c.json({
270
+ ok: true,
271
+ version: packageVersion,
272
+ activeReviews: reviewStore.list().length
273
+ })
274
+ );
275
+ app.get("/api/reviews", (c) => c.json({ reviews: reviewStore.list() }));
276
+ app.post("/api/reviews", async (c) => {
277
+ const diff = await c.req.json();
278
+ const record = await reviewStore.create(diff);
279
+ return c.json({ meta: record.meta, url: `${origin2}/review/${record.meta.id}` }, 201);
280
+ });
281
+ app.get("/api/reviews/:id", async (c) => {
282
+ const record = await reviewStore.get(c.req.param("id"));
283
+ if (!record) {
284
+ return c.json({ error: "review not found" }, 404);
285
+ }
286
+ return c.json(record);
287
+ });
288
+ app.get("/api/reviews/:id/feedback", async (c) => {
289
+ const feedback = await reviewStore.feedback(c.req.param("id"));
290
+ if (!feedback) {
291
+ return c.json({ error: "feedback not found" }, 404);
292
+ }
293
+ return c.json(feedback);
294
+ });
295
+ app.get("/api/reviews/:id/events", async (c) => {
296
+ const id = c.req.param("id");
297
+ const record = await reviewStore.get(id);
298
+ if (!record) {
299
+ return c.json({ error: "review not found" }, 404);
300
+ }
301
+ const encoder = new TextEncoder();
302
+ let cleanup = null;
303
+ const stream = new ReadableStream({
304
+ start(controller) {
305
+ const send = (event) => {
306
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}
307
+
308
+ `));
309
+ if (event.type === "review.completed" || event.type === "review.cancelled") {
310
+ cleanup?.();
311
+ controller.close();
312
+ }
313
+ };
314
+ cleanup = reviewStore.subscribe(id, send);
315
+ send({ type: "review.opened", reviewId: id });
316
+ if (record.meta.status === "completed" && record.feedback) {
317
+ send({
318
+ type: "review.completed",
319
+ reviewId: id,
320
+ counts: {
321
+ files: new Set(record.feedback.comments.map((comment) => comment.filePath)).size,
322
+ comments: record.feedback.comments.length
323
+ }
324
+ });
325
+ }
326
+ },
327
+ cancel() {
328
+ cleanup?.();
329
+ }
330
+ });
331
+ return new Response(stream, {
332
+ headers: {
333
+ "cache-control": "no-cache",
334
+ connection: "keep-alive",
335
+ "content-type": "text/event-stream"
336
+ }
337
+ });
338
+ });
339
+ app.post("/api/reviews/:id/submit", async (c) => {
340
+ const id = c.req.param("id");
341
+ const body = await c.req.json();
342
+ const { record, feedbackPath, markdownPath } = await reviewStore.submit(
343
+ id,
344
+ body.comments ?? []
345
+ );
346
+ return c.json({
347
+ reviewId: id,
348
+ url: `${origin2}/review/${id}`,
349
+ files: record.diff.files.length,
350
+ comments: body.comments?.length ?? 0,
351
+ feedbackPath,
352
+ markdownPath
353
+ });
354
+ });
355
+ app.post("/api/reviews/:id/resolved", async (c) => {
356
+ const body = await c.req.json().catch(() => ({}));
357
+ const resolvedPath = await reviewStore.markResolved(c.req.param("id"), body.summary);
358
+ return c.json({ ok: true, path: resolvedPath });
359
+ });
360
+ app.get("/setup.md", serveRootFile("setup.md", "text/markdown; charset=utf-8"));
361
+ app.get("/prompt.md", serveRootFile("prompt.md", "text/markdown; charset=utf-8"));
362
+ app.get("/assets/*", serveAsset);
363
+ app.get("/review/:id", serveIndex);
364
+ app.get("/", serveIndex);
365
+ return app;
366
+ }
367
+ async function serveAsset(c) {
368
+ const requestPath = new URL(c.req.url).pathname.replace(/^\/assets\//, "");
369
+ const normalized = path3.normalize(requestPath).replace(/^(\.\.(\/|\\|$))+/, "");
370
+ const assetPath = path3.join(webRoot, "assets", normalized);
371
+ try {
372
+ const body = await readFile2(assetPath);
373
+ return new Response(body, {
374
+ headers: {
375
+ "content-type": mimeTypes[path3.extname(assetPath)] ?? "application/octet-stream"
376
+ }
377
+ });
378
+ } catch {
379
+ return new Response("Not found", { status: 404 });
380
+ }
381
+ }
382
+ async function serveIndex() {
383
+ try {
384
+ const body = await readFile2(path3.join(webRoot, "index.html"));
385
+ return new Response(body, {
386
+ headers: { "content-type": "text/html; charset=utf-8" }
387
+ });
388
+ } catch {
389
+ return new Response("Gloss web assets are missing. Run pnpm build.", { status: 500 });
390
+ }
391
+ }
392
+ function serveRootFile(fileName, contentType) {
393
+ return async () => {
394
+ try {
395
+ const body = await readFile2(path3.join(webRoot, fileName));
396
+ return new Response(body, {
397
+ headers: { "content-type": contentType }
398
+ });
399
+ } catch {
400
+ return new Response(`${fileName} is missing. Run pnpm build.`, { status: 404 });
401
+ }
402
+ };
403
+ }
404
+
405
+ // src/server/daemon.ts
406
+ var port = Number(process.env.GLOSS_PORT ?? "0");
407
+ if (!port) {
408
+ throw new Error("GLOSS_PORT is required");
409
+ }
410
+ var origin = `http://localhost:${port}`;
411
+ var server = serve({
412
+ fetch: createApp(origin).fetch,
413
+ port
414
+ });
415
+ await writeServerInfo({
416
+ pid: process.pid,
417
+ port,
418
+ version: packageVersion,
419
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
420
+ stateDir: globalStateDir()
421
+ });
422
+ process.on("SIGTERM", () => {
423
+ server.close(() => {
424
+ process.exit(0);
425
+ });
426
+ });
427
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/server/daemon.ts","../../src/cli/lifecycle.ts","../../src/shared/paths.ts","../../src/server/index.ts","../../src/server/store.ts","../../src/shared/markdown.ts"],"sourcesContent":["import { serve } from '@hono/node-server';\nimport { writeServerInfo } from '../cli/lifecycle';\nimport { globalStateDir, packageVersion } from '../shared/paths';\nimport { createApp } from './index';\n\nconst port = Number(process.env.GLOSS_PORT ?? '0');\n\nif (!port) {\n throw new Error('GLOSS_PORT is required');\n}\n\nconst origin = `http://localhost:${port}`;\nconst server = serve({\n fetch: createApp(origin).fetch,\n port\n});\n\nawait writeServerInfo({\n pid: process.pid,\n port,\n version: packageVersion,\n startedAt: new Date().toISOString(),\n stateDir: globalStateDir()\n});\n\nprocess.on('SIGTERM', () => {\n server.close(() => {\n process.exit(0);\n });\n});\n","import { spawn } from 'node:child_process';\nimport { existsSync, openSync } from 'node:fs';\nimport { readFile, rm, writeFile } from 'node:fs/promises';\nimport { fileURLToPath } from 'node:url';\nimport getPort from 'get-port';\nimport {\n ensureDir,\n globalLogDir,\n globalServerFile,\n globalServerLogFile,\n globalStateDir,\n packageVersion\n} from '../shared/paths';\nimport type { ServerInfo } from '../shared/types';\nimport { ServerClient } from './server-client';\n\nexport async function readServerInfo(): Promise<ServerInfo | null> {\n try {\n return JSON.parse(await readFile(globalServerFile(), 'utf8')) as ServerInfo;\n } catch {\n return null;\n }\n}\n\nexport function serverUrl(info: Pick<ServerInfo, 'port'>): string {\n return `http://localhost:${info.port}`;\n}\n\nexport async function isServerResponsive(info: ServerInfo): Promise<boolean> {\n if (!isPidAlive(info.pid)) {\n return false;\n }\n try {\n const health = await new ServerClient(serverUrl(info)).health();\n return health.ok === true;\n } catch {\n return false;\n }\n}\n\nexport async function ensureServer(options: { port?: number } = {}): Promise<ServerInfo> {\n const existing = await readServerInfo();\n if (existing && (await isServerResponsive(existing))) {\n return existing;\n }\n return startServer(options);\n}\n\nexport async function startServer(options: { port?: number } = {}): Promise<ServerInfo> {\n const existing = await readServerInfo();\n if (existing && (await isServerResponsive(existing))) {\n return existing;\n }\n\n await ensureDir(globalStateDir());\n await ensureDir(globalLogDir());\n const port = options.port ?? (await getPort());\n const daemonPath = fileURLToPath(new URL('../server/daemon.js', import.meta.url));\n if (!existsSync(daemonPath)) {\n throw new Error(`Cannot find server daemon at ${daemonPath}. Run pnpm build first.`);\n }\n\n const logFd = openSync(globalServerLogFile(), 'a');\n const child = spawn(process.execPath, [daemonPath], {\n detached: true,\n env: {\n ...process.env,\n GLOSS_PORT: String(port),\n GLOSS_STATE_DIR: globalStateDir()\n },\n stdio: ['ignore', logFd, logFd]\n });\n child.unref();\n\n const info: ServerInfo = {\n pid: child.pid ?? -1,\n port,\n version: packageVersion,\n startedAt: new Date().toISOString(),\n stateDir: globalStateDir()\n };\n await writeFile(globalServerFile(), `${JSON.stringify(info, null, 2)}\\n`);\n\n const deadline = Date.now() + 8000;\n while (Date.now() < deadline) {\n if (await isServerResponsive(info)) {\n return info;\n }\n await new Promise((resolve) => setTimeout(resolve, 150));\n }\n\n throw new Error(`Server did not become responsive. See ${globalServerLogFile()}`);\n}\n\nexport async function stopServer(): Promise<{ stopped: boolean; info: ServerInfo | null }> {\n const info = await readServerInfo();\n if (!info) {\n return { stopped: false, info: null };\n }\n\n if (isPidAlive(info.pid)) {\n process.kill(info.pid, 'SIGTERM');\n }\n await rm(globalServerFile(), { force: true });\n return { stopped: true, info };\n}\n\nexport async function writeServerInfo(info: ServerInfo): Promise<void> {\n await ensureDir(globalStateDir());\n await writeFile(globalServerFile(), `${JSON.stringify(info, null, 2)}\\n`);\n}\n\nexport function isPidAlive(pid: number): boolean {\n if (pid <= 0) {\n return false;\n }\n try {\n process.kill(pid, 0);\n return true;\n } catch {\n return false;\n }\n}\n","import { mkdir } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport path from 'node:path';\n\nexport const packageVersion = '0.1.0';\n\nexport function expandHome(input: string): string {\n if (input === '~') {\n return homedir();\n }\n if (input.startsWith('~/')) {\n return path.join(homedir(), input.slice(2));\n }\n return input;\n}\n\nexport function globalStateDir(): string {\n return expandHome(process.env.GLOSS_STATE_DIR ?? '~/.gloss');\n}\n\nexport function globalServerFile(): string {\n return path.join(globalStateDir(), 'server.json');\n}\n\nexport function globalLogDir(): string {\n return path.join(globalStateDir(), 'logs');\n}\n\nexport function globalServerLogFile(): string {\n return path.join(globalLogDir(), 'server.log');\n}\n\nexport function repoGlossDir(cwd: string): string {\n return path.join(cwd, '.gloss');\n}\n\nexport function reviewsDir(cwd: string): string {\n return path.join(repoGlossDir(cwd), 'reviews');\n}\n\nexport function reviewDir(cwd: string, reviewId: string): string {\n return path.join(reviewsDir(cwd), reviewId);\n}\n\nexport async function ensureDir(dir: string): Promise<void> {\n await mkdir(dir, { recursive: true });\n}\n","import { readFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { Context } from 'hono';\nimport { Hono } from 'hono';\nimport { packageVersion, reviewDir } from '../shared/paths';\nimport type { Comment, DiffPayload, ReviewEvent } from '../shared/types';\nimport { reviewStore } from './store';\n\nconst webRoot = fileURLToPath(new URL('../web', import.meta.url));\n\nconst mimeTypes: Record<string, string> = {\n '.css': 'text/css; charset=utf-8',\n '.html': 'text/html; charset=utf-8',\n '.js': 'application/javascript; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml'\n};\n\nexport function createApp(origin: string): Hono {\n const app = new Hono();\n\n app.get('/api/health', (c) =>\n c.json({\n ok: true,\n version: packageVersion,\n activeReviews: reviewStore.list().length\n })\n );\n\n app.get('/api/reviews', (c) => c.json({ reviews: reviewStore.list() }));\n\n app.post('/api/reviews', async (c) => {\n const diff = (await c.req.json()) as DiffPayload;\n const record = await reviewStore.create(diff);\n return c.json({ meta: record.meta, url: `${origin}/review/${record.meta.id}` }, 201);\n });\n\n app.get('/api/reviews/:id', async (c) => {\n const record = await reviewStore.get(c.req.param('id'));\n if (!record) {\n return c.json({ error: 'review not found' }, 404);\n }\n return c.json(record);\n });\n\n app.get('/api/reviews/:id/feedback', async (c) => {\n const feedback = await reviewStore.feedback(c.req.param('id'));\n if (!feedback) {\n return c.json({ error: 'feedback not found' }, 404);\n }\n return c.json(feedback);\n });\n\n app.get('/api/reviews/:id/events', async (c) => {\n const id = c.req.param('id');\n const record = await reviewStore.get(id);\n if (!record) {\n return c.json({ error: 'review not found' }, 404);\n }\n\n const encoder = new TextEncoder();\n let cleanup: (() => void) | null = null;\n const stream = new ReadableStream<Uint8Array>({\n start(controller) {\n const send = (event: ReviewEvent) => {\n controller.enqueue(encoder.encode(`data: ${JSON.stringify(event)}\\n\\n`));\n if (event.type === 'review.completed' || event.type === 'review.cancelled') {\n cleanup?.();\n controller.close();\n }\n };\n cleanup = reviewStore.subscribe(id, send);\n send({ type: 'review.opened', reviewId: id });\n if (record.meta.status === 'completed' && record.feedback) {\n send({\n type: 'review.completed',\n reviewId: id,\n counts: {\n files: new Set(record.feedback.comments.map((comment) => comment.filePath)).size,\n comments: record.feedback.comments.length\n }\n });\n }\n },\n cancel() {\n cleanup?.();\n }\n });\n\n return new Response(stream, {\n headers: {\n 'cache-control': 'no-cache',\n connection: 'keep-alive',\n 'content-type': 'text/event-stream'\n }\n });\n });\n\n app.post('/api/reviews/:id/submit', async (c) => {\n const id = c.req.param('id');\n const body = (await c.req.json()) as { comments: Comment[] };\n const { record, feedbackPath, markdownPath } = await reviewStore.submit(\n id,\n body.comments ?? []\n );\n return c.json({\n reviewId: id,\n url: `${origin}/review/${id}`,\n files: record.diff.files.length,\n comments: body.comments?.length ?? 0,\n feedbackPath,\n markdownPath\n });\n });\n\n app.post('/api/reviews/:id/resolved', async (c) => {\n const body = (await c.req.json().catch(() => ({}))) as { summary?: string };\n const resolvedPath = await reviewStore.markResolved(c.req.param('id'), body.summary);\n return c.json({ ok: true, path: resolvedPath });\n });\n\n app.get('/setup.md', serveRootFile('setup.md', 'text/markdown; charset=utf-8'));\n app.get('/prompt.md', serveRootFile('prompt.md', 'text/markdown; charset=utf-8'));\n app.get('/assets/*', serveAsset);\n app.get('/review/:id', serveIndex);\n app.get('/', serveIndex);\n\n return app;\n}\n\nasync function serveAsset(c: Context) {\n const requestPath = new URL(c.req.url).pathname.replace(/^\\/assets\\//, '');\n const normalized = path.normalize(requestPath).replace(/^(\\.\\.(\\/|\\\\|$))+/, '');\n const assetPath = path.join(webRoot, 'assets', normalized);\n try {\n const body = await readFile(assetPath);\n return new Response(body, {\n headers: {\n 'content-type': mimeTypes[path.extname(assetPath)] ?? 'application/octet-stream'\n }\n });\n } catch {\n return new Response('Not found', { status: 404 });\n }\n}\n\nasync function serveIndex() {\n try {\n const body = await readFile(path.join(webRoot, 'index.html'));\n return new Response(body, {\n headers: { 'content-type': 'text/html; charset=utf-8' }\n });\n } catch {\n return new Response('Gloss web assets are missing. Run pnpm build.', { status: 500 });\n }\n}\n\nfunction serveRootFile(fileName: string, contentType: string) {\n return async () => {\n try {\n const body = await readFile(path.join(webRoot, fileName));\n return new Response(body, {\n headers: { 'content-type': contentType }\n });\n } catch {\n return new Response(`${fileName} is missing. Run pnpm build.`, { status: 404 });\n }\n };\n}\n\nexport function getReviewArtifactDir(cwd: string, reviewId: string): string {\n return reviewDir(cwd, reviewId);\n}\n","import { mkdir, rm, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport { ulid } from 'ulid';\nimport { serializeFeedbackMarkdown } from '../shared/markdown';\nimport { ensureDir, repoGlossDir, reviewDir } from '../shared/paths';\nimport type {\n Comment,\n DiffPayload,\n FeedbackBundle,\n ReviewEvent,\n ReviewMeta,\n ReviewRecord\n} from '../shared/types';\n\ntype Listener = (event: ReviewEvent) => void;\n\nexport class ReviewStore {\n private readonly reviews = new Map<string, ReviewRecord>();\n private readonly listeners = new Map<string, Set<Listener>>();\n\n async create(diff: DiffPayload): Promise<ReviewRecord> {\n const id = ulid();\n const createdAt = new Date().toISOString();\n const meta: ReviewMeta = {\n id,\n cwd: diff.cwd,\n base: diff.base,\n branch: diff.branch,\n status: 'pending',\n createdAt\n };\n const record: ReviewRecord = { meta, diff };\n this.reviews.set(id, record);\n await this.persistInitial(record);\n this.emit({ type: 'review.opened', reviewId: id });\n return record;\n }\n\n list(): ReviewMeta[] {\n return [...this.reviews.values()]\n .map((record) => record.meta)\n .sort((a, b) => a.createdAt.localeCompare(b.createdAt));\n }\n\n async get(id: string): Promise<ReviewRecord | null> {\n return this.reviews.get(id) ?? (await this.loadKnownReview(id));\n }\n\n async submit(\n id: string,\n comments: Comment[]\n ): Promise<{ record: ReviewRecord; feedbackPath: string; markdownPath: string }> {\n const record = await this.get(id);\n if (!record) {\n throw new Error(`Review ${id} not found`);\n }\n const timestamp = new Date().toISOString();\n const feedback: FeedbackBundle = {\n version: 1,\n reviewId: id,\n timestamp,\n base: record.diff.base,\n branch: record.diff.branch,\n comments: [...comments].sort(\n (a, b) =>\n a.filePath.localeCompare(b.filePath) ||\n a.startLine - b.startLine ||\n a.endLine - b.endLine ||\n a.side.localeCompare(b.side)\n )\n };\n record.feedback = feedback;\n record.meta = { ...record.meta, status: 'completed', completedAt: timestamp };\n this.reviews.set(id, record);\n\n const dir = reviewDir(record.meta.cwd, id);\n const feedbackPath = path.join(dir, 'feedback.json');\n const markdownPath = path.join(dir, 'feedback.md');\n await ensureDir(dir);\n await Promise.all([\n writeFile(path.join(dir, 'meta.json'), `${JSON.stringify(record.meta, null, 2)}\\n`),\n writeFile(feedbackPath, `${JSON.stringify(feedback, null, 2)}\\n`),\n writeFile(markdownPath, serializeFeedbackMarkdown(feedback))\n ]);\n\n this.emit({\n type: 'review.completed',\n reviewId: id,\n counts: {\n files: new Set(feedback.comments.map((comment) => comment.filePath)).size,\n comments: feedback.comments.length\n }\n });\n return { record, feedbackPath, markdownPath };\n }\n\n async feedback(id: string): Promise<FeedbackBundle | null> {\n const record = await this.get(id);\n return record?.feedback ?? null;\n }\n\n async markResolved(id: string, summary?: string): Promise<string> {\n const record = await this.get(id);\n if (!record) {\n throw new Error(`Review ${id} not found`);\n }\n const resolvedPath = path.join(reviewDir(record.meta.cwd, id), 'resolved.json');\n await writeFile(\n resolvedPath,\n `${JSON.stringify({ reviewId: id, summary: summary ?? null, resolvedAt: new Date().toISOString() }, null, 2)}\\n`\n );\n return resolvedPath;\n }\n\n subscribe(reviewId: string, listener: Listener): () => void {\n const listeners = this.listeners.get(reviewId) ?? new Set<Listener>();\n listeners.add(listener);\n this.listeners.set(reviewId, listeners);\n return () => {\n listeners.delete(listener);\n if (listeners.size === 0) {\n this.listeners.delete(reviewId);\n }\n };\n }\n\n private emit(event: ReviewEvent): void {\n for (const listener of this.listeners.get(event.reviewId) ?? []) {\n listener(event);\n }\n }\n\n private async persistInitial(record: ReviewRecord): Promise<void> {\n await ensureDir(repoGlossDir(record.meta.cwd));\n await writeFile(path.join(repoGlossDir(record.meta.cwd), '.gitignore'), '*\\n!.gitignore\\n');\n const dir = reviewDir(record.meta.cwd, record.meta.id);\n await ensureDir(dir);\n await mkdir(path.join(dir, 'original'), { recursive: true });\n await Promise.all([\n writeFile(path.join(dir, 'meta.json'), `${JSON.stringify(record.meta, null, 2)}\\n`),\n writeFile(path.join(dir, 'diff.json'), `${JSON.stringify(record.diff, null, 2)}\\n`)\n ]);\n }\n\n private async loadKnownReview(id: string): Promise<ReviewRecord | null> {\n for (const meta of this.reviews.values()) {\n if (meta.meta.id === id) {\n return meta;\n }\n }\n return null;\n }\n}\n\nexport const reviewStore = new ReviewStore();\n\nexport async function removeReviewArtifacts(cwd: string, id: string): Promise<void> {\n await rm(reviewDir(cwd, id), { force: true, recursive: true });\n}\n","import type { Comment, FeedbackBundle } from './types';\n\nfunction formatLineRange(comment: Comment): string {\n const prefix = comment.side;\n if (comment.startLine === comment.endLine) {\n return `${prefix}${comment.startLine}`;\n }\n return `${prefix}${comment.startLine}-${prefix}${comment.endLine}`;\n}\n\nfunction fenceFor(snippet: string): string {\n let fence = '```';\n while (snippet.includes(fence)) {\n fence += '`';\n }\n return fence;\n}\n\nfunction languageForPath(filePath: string): string {\n const ext = filePath.split('.').pop()?.toLowerCase();\n const map: Record<string, string> = {\n cjs: 'js',\n css: 'css',\n go: 'go',\n html: 'html',\n js: 'js',\n json: 'json',\n jsx: 'jsx',\n md: 'markdown',\n mjs: 'js',\n py: 'python',\n rb: 'ruby',\n rs: 'rust',\n sh: 'bash',\n swift: 'swift',\n ts: 'ts',\n tsx: 'tsx',\n yaml: 'yaml',\n yml: 'yaml'\n };\n return ext ? (map[ext] ?? ext) : '';\n}\n\nfunction byFileThenLine(a: Comment, b: Comment): number {\n return (\n a.filePath.localeCompare(b.filePath) ||\n a.startLine - b.startLine ||\n a.endLine - b.endLine ||\n a.side.localeCompare(b.side)\n );\n}\n\nexport function serializeFeedbackMarkdown(bundle: FeedbackBundle): string {\n const comments = [...bundle.comments].sort(byFileThenLine);\n const files = [...new Set(comments.map((comment) => comment.filePath))];\n const lines: string[] = [\n `# Gloss feedback - ${bundle.timestamp}`,\n `Review: ${bundle.reviewId}`,\n `Base: ${bundle.base.ref} (${bundle.base.sha.slice(0, 7)}) Branch: ${bundle.branch ?? '(detached)'}`,\n `Files: ${files.length} Comments: ${comments.length}`,\n ''\n ];\n\n for (const filePath of files) {\n lines.push(`## ${filePath}`, '');\n for (const comment of comments.filter((item) => item.filePath === filePath)) {\n const snippet = comment.originalSnippet.trimEnd();\n const firstSnippetLine = snippet.split('\\n').find((line) => line.trim().length > 0);\n const heading =\n comment.startLine === comment.endLine && firstSnippetLine\n ? `### ${formatLineRange(comment)} - \\`${firstSnippetLine.trim().slice(0, 80)}\\``\n : `### ${formatLineRange(comment)}`;\n lines.push(heading, comment.body.trim(), '');\n if (snippet) {\n const fence = fenceFor(snippet);\n lines.push(`${fence}${languageForPath(comment.filePath)}`, snippet, fence, '');\n }\n }\n }\n\n return `${lines.join('\\n').trimEnd()}\\n`;\n}\n"],"mappings":"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","names":["readFile","path","fileURLToPath","mkdir","rm","writeFile","path","path","writeFile","mkdir","fileURLToPath","origin","path","readFile"]}
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;-moz-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;-moz-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.block{display:block}.contents{display:contents}.hidden{display:none}}:root{color-scheme:dark;color:#f4f1ea;background:#101113;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif}*{box-sizing:border-box}body{background:linear-gradient(#ffffff06,#0000 280px),#101113;min-width:320px;margin:0}button,textarea{font:inherit}button{color:inherit}.review-shell{width:min(1320px,100vw - 32px);margin:0 auto;padding:18px 0 110px}.topbar{z-index:5;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);background:#101113eb;justify-content:space-between;align-items:center;gap:16px;padding:14px 0 16px;display:flex;position:sticky;top:0}.brand-row{align-items:center;gap:10px;display:flex}.brand-row h1{letter-spacing:0;margin:0;font-size:20px;font-weight:700}.brand-mark{color:#d8f28f;background:#1b1d21;border:1px solid #3d4147;border-radius:7px;place-items:center;width:28px;height:28px;font-weight:800;display:grid}.muted{color:#9da3ad;margin:4px 0 0;font-size:13px}.branch-pill{color:#cbd1da;background:#181a1e;border:1px solid #30343a;border-radius:7px;align-items:center;gap:8px;min-width:0;max-width:46vw;padding:7px 10px;font-size:13px;display:inline-flex}.branch-pill span{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.diff-stack{gap:12px;display:grid}.file-card{background:#15171a;border:1px solid #30343a;border-radius:8px;overflow:hidden}.file-header{cursor:pointer;text-align:left;background:#1b1d21;border:0;border-bottom:1px solid #30343a;grid-template-columns:auto auto minmax(0,1fr) auto auto auto;align-items:center;gap:9px;width:100%;padding:10px 12px;display:grid}.file-path,.rename-path{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.file-path{color:#f4f1ea;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:13px}.rename-path{color:#8e96a3;max-width:240px;font-size:12px}.stat{font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12px;font-weight:700}.stat.add{color:#74d59a}.stat.del{color:#ef8a8c}.diff-table{border:0;width:100%;margin:0;padding:6px 0;overflow-x:auto}.sr-only{clip:rect(0,0,0,0);white-space:nowrap;border:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.hunk+.hunk{border-top:1px solid #24282e}.hunk-header,.hidden-lines,.binary-note{color:#8f98a6;text-align:left;background:#20242a;border:0;width:100%;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12px}.hunk-header{padding:8px 14px}.hidden-lines{cursor:pointer;padding:7px 14px;display:block}.binary-note{padding:14px}.diff-row{color:#d8dde5;cursor:cell;text-align:left;background:0 0;border:0;grid-template-columns:58px 58px 24px minmax(680px,1fr);align-items:stretch;width:100%;min-height:26px;display:grid}.diff-row:hover,.diff-row.selected{background:#2a2f36}.diff-row.add{background:#43a05f24}.diff-row.delete{background:#c94c5229}.diff-row.add:hover,.diff-row.delete:hover{filter:brightness(1.18)}.line-number{-webkit-user-select:none;user-select:none;color:#727b88;text-align:right;border-right:1px solid #252a31;padding-right:10px;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12px;line-height:26px}.marker{-webkit-user-select:none;user-select:none;color:#8e96a3;text-align:center;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12px;line-height:26px}.diff-row code{white-space:pre;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12.5px;line-height:26px;overflow:visible}.inline-comment{color:#f4e5b0;background:#252116;border:1px solid #45412b;border-radius:7px;align-items:flex-start;gap:8px;margin:4px 12px 8px 140px;padding:8px 10px;font-size:13px;display:flex}.popover{z-index:20;background:#202329;border:1px solid #4b5059;border-radius:8px;width:min(360px,100vw - 24px);padding:12px;position:fixed;box-shadow:0 24px 80px #0000006b}.popover-title{color:#f4f1ea;justify-content:space-between;align-items:center;font-size:13px;font-weight:700;display:flex}.popover-subtitle{color:#9da3ad;margin-top:2px;font-size:12px}.popover textarea{resize:vertical;color:#f4f1ea;background:#121417;border:1px solid #3b4048;border-radius:7px;outline:none;width:100%;min-height:96px;margin-top:10px;padding:9px}.popover textarea:focus{border-color:#d8f28f}.popover-actions,.submit-actions{justify-content:flex-end;align-items:center;gap:10px;display:flex}.popover-actions{margin-top:10px}.primary-button,.icon-button{cursor:pointer;border:1px solid #0000;border-radius:7px;justify-content:center;align-items:center;display:inline-flex}.primary-button{color:#171912;background:#d8f28f;gap:8px;min-height:34px;padding:0 12px;font-size:13px;font-weight:800}.primary-button:disabled{cursor:default;opacity:.58}.icon-button{color:#aab1bc;background:0 0;width:28px;height:28px}.icon-button:hover{color:#f4f1ea;background:#30343a}.submit-bar{z-index:10;-webkit-backdrop-filter:blur(16px);backdrop-filter:blur(16px);background:#181a1ef0;border:1px solid #343941;border-radius:8px;justify-content:space-between;align-items:center;gap:18px;width:min(1320px,100vw - 36px);margin:0 auto;padding:12px;display:flex;position:fixed;bottom:18px;left:18px;right:18px;box-shadow:0 18px 70px #00000061}.comment-list{flex:1;gap:8px;min-width:0;display:flex;overflow-x:auto}.comment-chip{color:#f4e5b0;background:#242116;border:1px solid #45412b;border-radius:7px;align-items:center;gap:8px;min-width:max-content;padding:5px 7px 5px 10px;font-family:SFMono-Regular,Consolas,Liberation Mono,monospace;font-size:12px;display:inline-flex}.submit-message{color:#9da3ad;text-overflow:ellipsis;white-space:nowrap;max-width:36vw;font-size:13px;overflow:hidden}.submit-message.error{color:#ef8a8c}.submit-message.done{color:#74d59a}.empty-shell{place-items:center;min-height:100vh;padding:24px;display:grid}.empty-panel{text-align:center;background:#181a1e;border:1px solid #30343a;border-radius:8px;width:min(420px,100%);padding:22px}.empty-panel h1{margin:0 0 8px}.empty-diff{color:#9da3ad;background:#181a1e;border:1px solid #30343a;border-radius:8px;padding:22px}.spin{animation:1s linear infinite spin}@keyframes spin{to{transform:rotate(360deg)}}@media(max-width:720px){.review-shell{width:calc(100vw - 18px)}.topbar{flex-direction:column;align-items:flex-start}.branch-pill{max-width:100%}.file-header{grid-template-columns:auto auto minmax(0,1fr) auto auto}.rename-path{display:none}.diff-row{grid-template-columns:46px 46px 22px minmax(520px,1fr)}.inline-comment{margin-left:108px}.submit-bar{flex-direction:column;align-items:stretch}.submit-actions{justify-content:space-between}}