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.
- package/LICENSE +22 -0
- package/README.md +109 -0
- package/dist/cli/index.js +638 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/mcp/index.js +293 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/server/daemon.js +427 -0
- package/dist/server/daemon.js.map +1 -0
- package/dist/web/assets/index-BiGi3rBS.css +1 -0
- package/dist/web/assets/index-GpOF1p41.js +149 -0
- package/dist/web/index.html +14 -0
- package/dist/web/prompt.md +58 -0
- package/dist/web/setup.md +133 -0
- package/package.json +80 -0
- package/skill/SKILL.md +23 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import openBrowser from "open";
|
|
6
|
+
|
|
7
|
+
// src/mcp/index.ts
|
|
8
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
10
|
+
import { z } from "zod/v4";
|
|
11
|
+
|
|
12
|
+
// src/cli/lifecycle.ts
|
|
13
|
+
import { spawn } from "child_process";
|
|
14
|
+
import { existsSync, openSync } from "fs";
|
|
15
|
+
import { readFile, rm, writeFile } from "fs/promises";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import getPort from "get-port";
|
|
18
|
+
|
|
19
|
+
// src/shared/paths.ts
|
|
20
|
+
import { mkdir } from "fs/promises";
|
|
21
|
+
import { homedir } from "os";
|
|
22
|
+
import path from "path";
|
|
23
|
+
var packageVersion = "0.1.0";
|
|
24
|
+
function expandHome(input) {
|
|
25
|
+
if (input === "~") {
|
|
26
|
+
return homedir();
|
|
27
|
+
}
|
|
28
|
+
if (input.startsWith("~/")) {
|
|
29
|
+
return path.join(homedir(), input.slice(2));
|
|
30
|
+
}
|
|
31
|
+
return input;
|
|
32
|
+
}
|
|
33
|
+
function globalStateDir() {
|
|
34
|
+
return expandHome(process.env.GLOSS_STATE_DIR ?? "~/.gloss");
|
|
35
|
+
}
|
|
36
|
+
function globalServerFile() {
|
|
37
|
+
return path.join(globalStateDir(), "server.json");
|
|
38
|
+
}
|
|
39
|
+
function globalLogDir() {
|
|
40
|
+
return path.join(globalStateDir(), "logs");
|
|
41
|
+
}
|
|
42
|
+
function globalServerLogFile() {
|
|
43
|
+
return path.join(globalLogDir(), "server.log");
|
|
44
|
+
}
|
|
45
|
+
async function ensureDir(dir) {
|
|
46
|
+
await mkdir(dir, { recursive: true });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// src/cli/server-client.ts
|
|
50
|
+
var ServerClient = class {
|
|
51
|
+
constructor(baseUrl) {
|
|
52
|
+
this.baseUrl = baseUrl;
|
|
53
|
+
}
|
|
54
|
+
baseUrl;
|
|
55
|
+
async health() {
|
|
56
|
+
return this.get("/api/health");
|
|
57
|
+
}
|
|
58
|
+
async createReview(diff) {
|
|
59
|
+
return this.post("/api/reviews", diff);
|
|
60
|
+
}
|
|
61
|
+
async getReview(reviewId) {
|
|
62
|
+
return this.get(`/api/reviews/${reviewId}`);
|
|
63
|
+
}
|
|
64
|
+
async listReviews() {
|
|
65
|
+
return this.get("/api/reviews");
|
|
66
|
+
}
|
|
67
|
+
async getFeedback(reviewId) {
|
|
68
|
+
return this.get(`/api/reviews/${reviewId}/feedback`);
|
|
69
|
+
}
|
|
70
|
+
async markResolved(reviewId, summary) {
|
|
71
|
+
return this.post(`/api/reviews/${reviewId}/resolved`, { summary });
|
|
72
|
+
}
|
|
73
|
+
async submitReview(reviewId, comments) {
|
|
74
|
+
return this.post(`/api/reviews/${reviewId}/submit`, { comments });
|
|
75
|
+
}
|
|
76
|
+
async watchReview(reviewId, timeoutSeconds) {
|
|
77
|
+
const controller = new AbortController();
|
|
78
|
+
const timeout = timeoutSeconds && timeoutSeconds > 0 ? setTimeout(() => controller.abort(), timeoutSeconds * 1e3) : null;
|
|
79
|
+
try {
|
|
80
|
+
const response = await fetch(`${this.baseUrl}/api/reviews/${reviewId}/events`, {
|
|
81
|
+
signal: controller.signal
|
|
82
|
+
});
|
|
83
|
+
if (!response.ok || !response.body) {
|
|
84
|
+
throw new Error(`watch failed: ${response.status} ${await response.text()}`);
|
|
85
|
+
}
|
|
86
|
+
const reader = response.body.getReader();
|
|
87
|
+
const decoder = new TextDecoder();
|
|
88
|
+
let buffer = "";
|
|
89
|
+
while (true) {
|
|
90
|
+
const { value, done } = await reader.read();
|
|
91
|
+
if (done) {
|
|
92
|
+
throw new Error("watch stream ended before completion");
|
|
93
|
+
}
|
|
94
|
+
buffer += decoder.decode(value, { stream: true });
|
|
95
|
+
const events = buffer.split("\n\n");
|
|
96
|
+
buffer = events.pop() ?? "";
|
|
97
|
+
for (const eventChunk of events) {
|
|
98
|
+
const dataLine = eventChunk.split("\n").find((line) => line.startsWith("data:"));
|
|
99
|
+
if (!dataLine) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const event = JSON.parse(dataLine.slice(5).trim());
|
|
103
|
+
if (event.type === "review.completed" || event.type === "review.cancelled") {
|
|
104
|
+
return event;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} finally {
|
|
109
|
+
if (timeout) {
|
|
110
|
+
clearTimeout(timeout);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
async get(path3) {
|
|
115
|
+
const response = await fetch(`${this.baseUrl}${path3}`);
|
|
116
|
+
return parseResponse(response);
|
|
117
|
+
}
|
|
118
|
+
async post(path3, body) {
|
|
119
|
+
const response = await fetch(`${this.baseUrl}${path3}`, {
|
|
120
|
+
method: "POST",
|
|
121
|
+
headers: { "content-type": "application/json" },
|
|
122
|
+
body: JSON.stringify(body)
|
|
123
|
+
});
|
|
124
|
+
return parseResponse(response);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
async function parseResponse(response) {
|
|
128
|
+
if (!response.ok) {
|
|
129
|
+
throw new Error(`${response.status} ${response.statusText}: ${await response.text()}`);
|
|
130
|
+
}
|
|
131
|
+
return await response.json();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/cli/lifecycle.ts
|
|
135
|
+
async function readServerInfo() {
|
|
136
|
+
try {
|
|
137
|
+
return JSON.parse(await readFile(globalServerFile(), "utf8"));
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function serverUrl(info) {
|
|
143
|
+
return `http://localhost:${info.port}`;
|
|
144
|
+
}
|
|
145
|
+
async function isServerResponsive(info) {
|
|
146
|
+
if (!isPidAlive(info.pid)) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
const health = await new ServerClient(serverUrl(info)).health();
|
|
151
|
+
return health.ok === true;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function ensureServer(options = {}) {
|
|
157
|
+
const existing = await readServerInfo();
|
|
158
|
+
if (existing && await isServerResponsive(existing)) {
|
|
159
|
+
return existing;
|
|
160
|
+
}
|
|
161
|
+
return startServer(options);
|
|
162
|
+
}
|
|
163
|
+
async function startServer(options = {}) {
|
|
164
|
+
const existing = await readServerInfo();
|
|
165
|
+
if (existing && await isServerResponsive(existing)) {
|
|
166
|
+
return existing;
|
|
167
|
+
}
|
|
168
|
+
await ensureDir(globalStateDir());
|
|
169
|
+
await ensureDir(globalLogDir());
|
|
170
|
+
const port = options.port ?? await getPort();
|
|
171
|
+
const daemonPath = fileURLToPath(new URL("../server/daemon.js", import.meta.url));
|
|
172
|
+
if (!existsSync(daemonPath)) {
|
|
173
|
+
throw new Error(`Cannot find server daemon at ${daemonPath}. Run pnpm build first.`);
|
|
174
|
+
}
|
|
175
|
+
const logFd = openSync(globalServerLogFile(), "a");
|
|
176
|
+
const child = spawn(process.execPath, [daemonPath], {
|
|
177
|
+
detached: true,
|
|
178
|
+
env: {
|
|
179
|
+
...process.env,
|
|
180
|
+
GLOSS_PORT: String(port),
|
|
181
|
+
GLOSS_STATE_DIR: globalStateDir()
|
|
182
|
+
},
|
|
183
|
+
stdio: ["ignore", logFd, logFd]
|
|
184
|
+
});
|
|
185
|
+
child.unref();
|
|
186
|
+
const info = {
|
|
187
|
+
pid: child.pid ?? -1,
|
|
188
|
+
port,
|
|
189
|
+
version: packageVersion,
|
|
190
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
191
|
+
stateDir: globalStateDir()
|
|
192
|
+
};
|
|
193
|
+
await writeFile(globalServerFile(), `${JSON.stringify(info, null, 2)}
|
|
194
|
+
`);
|
|
195
|
+
const deadline = Date.now() + 8e3;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
if (await isServerResponsive(info)) {
|
|
198
|
+
return info;
|
|
199
|
+
}
|
|
200
|
+
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
201
|
+
}
|
|
202
|
+
throw new Error(`Server did not become responsive. See ${globalServerLogFile()}`);
|
|
203
|
+
}
|
|
204
|
+
async function stopServer() {
|
|
205
|
+
const info = await readServerInfo();
|
|
206
|
+
if (!info) {
|
|
207
|
+
return { stopped: false, info: null };
|
|
208
|
+
}
|
|
209
|
+
if (isPidAlive(info.pid)) {
|
|
210
|
+
process.kill(info.pid, "SIGTERM");
|
|
211
|
+
}
|
|
212
|
+
await rm(globalServerFile(), { force: true });
|
|
213
|
+
return { stopped: true, info };
|
|
214
|
+
}
|
|
215
|
+
function isPidAlive(pid) {
|
|
216
|
+
if (pid <= 0) {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
process.kill(pid, 0);
|
|
221
|
+
return true;
|
|
222
|
+
} catch {
|
|
223
|
+
return false;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// src/mcp/index.ts
|
|
228
|
+
function textResult(value) {
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{
|
|
232
|
+
type: "text",
|
|
233
|
+
text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
async function client() {
|
|
239
|
+
const info = await ensureServer();
|
|
240
|
+
return new ServerClient(serverUrl(info));
|
|
241
|
+
}
|
|
242
|
+
async function startMcpServer() {
|
|
243
|
+
const server = new McpServer({
|
|
244
|
+
name: "gloss",
|
|
245
|
+
version: packageVersion
|
|
246
|
+
});
|
|
247
|
+
server.registerTool(
|
|
248
|
+
"list_pending_reviews",
|
|
249
|
+
{
|
|
250
|
+
title: "List pending Gloss reviews",
|
|
251
|
+
description: "List pending local Gloss review sessions."
|
|
252
|
+
},
|
|
253
|
+
async () => {
|
|
254
|
+
const api = await client();
|
|
255
|
+
const { reviews } = await api.listReviews();
|
|
256
|
+
return textResult({ reviews: reviews.filter((review) => review.status === "pending") });
|
|
257
|
+
}
|
|
258
|
+
);
|
|
259
|
+
server.registerTool(
|
|
260
|
+
"get_review",
|
|
261
|
+
{
|
|
262
|
+
title: "Get Gloss review",
|
|
263
|
+
description: "Fetch review metadata and diff payload.",
|
|
264
|
+
inputSchema: { id: z.string() }
|
|
265
|
+
},
|
|
266
|
+
async ({ id }) => textResult(await (await client()).getReview(id))
|
|
267
|
+
);
|
|
268
|
+
server.registerTool(
|
|
269
|
+
"watch_review",
|
|
270
|
+
{
|
|
271
|
+
title: "Watch Gloss review",
|
|
272
|
+
description: "Block until a review completes, then return feedback.",
|
|
273
|
+
inputSchema: {
|
|
274
|
+
id: z.string(),
|
|
275
|
+
timeout: z.number().optional()
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
async ({ id, timeout }) => {
|
|
279
|
+
const api = await client();
|
|
280
|
+
await api.watchReview(id, timeout);
|
|
281
|
+
return textResult(await api.getFeedback(id));
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
server.registerTool(
|
|
285
|
+
"get_review_feedback",
|
|
286
|
+
{
|
|
287
|
+
title: "Get Gloss review feedback",
|
|
288
|
+
description: "Fetch completed review feedback.",
|
|
289
|
+
inputSchema: { id: z.string() }
|
|
290
|
+
},
|
|
291
|
+
async ({ id }) => textResult(await (await client()).getFeedback(id))
|
|
292
|
+
);
|
|
293
|
+
server.registerTool(
|
|
294
|
+
"mark_review_resolved",
|
|
295
|
+
{
|
|
296
|
+
title: "Mark Gloss review resolved",
|
|
297
|
+
description: "Write a resolved marker for a completed review.",
|
|
298
|
+
inputSchema: {
|
|
299
|
+
id: z.string(),
|
|
300
|
+
summary: z.string().optional()
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
async ({ id, summary }) => textResult(await (await client()).markResolved(id, summary))
|
|
304
|
+
);
|
|
305
|
+
await server.connect(new StdioServerTransport());
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// src/cli/git.ts
|
|
309
|
+
import { execa } from "execa";
|
|
310
|
+
|
|
311
|
+
// src/cli/diff-parser.ts
|
|
312
|
+
import path2 from "path";
|
|
313
|
+
var hunkHeaderPattern = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
|
|
314
|
+
function stripGitPath(input) {
|
|
315
|
+
return input.replace(/^[ab]\//, "");
|
|
316
|
+
}
|
|
317
|
+
function languageForPath(filePath) {
|
|
318
|
+
const ext = path2.extname(filePath).slice(1).toLowerCase();
|
|
319
|
+
if (!ext) {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
const map = {
|
|
323
|
+
cjs: "js",
|
|
324
|
+
mjs: "js",
|
|
325
|
+
js: "js",
|
|
326
|
+
jsx: "jsx",
|
|
327
|
+
ts: "ts",
|
|
328
|
+
tsx: "tsx",
|
|
329
|
+
py: "python",
|
|
330
|
+
rb: "ruby",
|
|
331
|
+
sh: "bash",
|
|
332
|
+
md: "markdown",
|
|
333
|
+
yml: "yaml",
|
|
334
|
+
yaml: "yaml"
|
|
335
|
+
};
|
|
336
|
+
return map[ext] ?? ext;
|
|
337
|
+
}
|
|
338
|
+
function emptyFile() {
|
|
339
|
+
return {
|
|
340
|
+
path: "",
|
|
341
|
+
oldPath: null,
|
|
342
|
+
additions: 0,
|
|
343
|
+
deletions: 0,
|
|
344
|
+
isBinary: false,
|
|
345
|
+
isDeleted: false,
|
|
346
|
+
isNew: false,
|
|
347
|
+
isRenamed: false,
|
|
348
|
+
language: null,
|
|
349
|
+
hunks: []
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function parseUnifiedDiff(diffText) {
|
|
353
|
+
const files = [];
|
|
354
|
+
let current = null;
|
|
355
|
+
let currentHunk = null;
|
|
356
|
+
let oldCursor = 0;
|
|
357
|
+
let newCursor = 0;
|
|
358
|
+
const finalizeFile = () => {
|
|
359
|
+
if (current?.path) {
|
|
360
|
+
current.language = languageForPath(current.path);
|
|
361
|
+
files.push(current);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
for (const line of diffText.split("\n")) {
|
|
365
|
+
if (line.startsWith("diff --git ")) {
|
|
366
|
+
finalizeFile();
|
|
367
|
+
current = emptyFile();
|
|
368
|
+
currentHunk = null;
|
|
369
|
+
oldCursor = 0;
|
|
370
|
+
newCursor = 0;
|
|
371
|
+
const match = /^diff --git a\/(.+) b\/(.+)$/.exec(line);
|
|
372
|
+
if (match) {
|
|
373
|
+
current.oldPath = match[1];
|
|
374
|
+
current.path = match[2];
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!current) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (line.startsWith("new file mode")) {
|
|
382
|
+
current.isNew = true;
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
if (line.startsWith("deleted file mode")) {
|
|
386
|
+
current.isDeleted = true;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (line.startsWith("rename from ")) {
|
|
390
|
+
current.oldPath = line.slice("rename from ".length);
|
|
391
|
+
current.isRenamed = true;
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
if (line.startsWith("rename to ")) {
|
|
395
|
+
current.path = line.slice("rename to ".length);
|
|
396
|
+
current.isRenamed = true;
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (line.startsWith("Binary files ") || line.startsWith("GIT binary patch")) {
|
|
400
|
+
current.isBinary = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (line.startsWith("--- ")) {
|
|
404
|
+
const oldPath = line.slice(4).trim();
|
|
405
|
+
current.oldPath = oldPath === "/dev/null" ? null : stripGitPath(oldPath);
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (line.startsWith("+++ ")) {
|
|
409
|
+
const newPath = line.slice(4).trim();
|
|
410
|
+
current.path = newPath === "/dev/null" ? current.oldPath ?? current.path : stripGitPath(newPath);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
const hunkMatch = hunkHeaderPattern.exec(line);
|
|
414
|
+
if (hunkMatch) {
|
|
415
|
+
const oldStart = Number(hunkMatch[1]);
|
|
416
|
+
const oldLines = Number(hunkMatch[2] ?? "1");
|
|
417
|
+
const newStart = Number(hunkMatch[3]);
|
|
418
|
+
const newLines = Number(hunkMatch[4] ?? "1");
|
|
419
|
+
currentHunk = {
|
|
420
|
+
oldStart,
|
|
421
|
+
oldLines,
|
|
422
|
+
newStart,
|
|
423
|
+
newLines,
|
|
424
|
+
header: hunkMatch[5]?.trim() ?? "",
|
|
425
|
+
lines: []
|
|
426
|
+
};
|
|
427
|
+
current.hunks.push(currentHunk);
|
|
428
|
+
oldCursor = oldStart;
|
|
429
|
+
newCursor = newStart;
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (!currentHunk) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
const marker = line[0];
|
|
436
|
+
const content = line.slice(1);
|
|
437
|
+
let diffLine = null;
|
|
438
|
+
if (marker === "+") {
|
|
439
|
+
diffLine = { type: "add", oldLine: null, newLine: newCursor, content };
|
|
440
|
+
current.additions += 1;
|
|
441
|
+
newCursor += 1;
|
|
442
|
+
} else if (marker === "-") {
|
|
443
|
+
diffLine = { type: "delete", oldLine: oldCursor, newLine: null, content };
|
|
444
|
+
current.deletions += 1;
|
|
445
|
+
oldCursor += 1;
|
|
446
|
+
} else if (marker === " ") {
|
|
447
|
+
diffLine = { type: "context", oldLine: oldCursor, newLine: newCursor, content };
|
|
448
|
+
oldCursor += 1;
|
|
449
|
+
newCursor += 1;
|
|
450
|
+
} else if (line.startsWith("\")) {
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
if (diffLine) {
|
|
454
|
+
currentHunk.lines.push(diffLine);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
finalizeFile();
|
|
458
|
+
return files;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// src/cli/git.ts
|
|
462
|
+
async function git(args, cwd = process.cwd()) {
|
|
463
|
+
const result = await execa("git", args, { cwd });
|
|
464
|
+
return result.stdout.trimEnd();
|
|
465
|
+
}
|
|
466
|
+
async function gitLenient(args, cwd) {
|
|
467
|
+
const result = await execa("git", args, { cwd, reject: false });
|
|
468
|
+
if (result.exitCode !== 0 && result.stdout.length === 0) {
|
|
469
|
+
throw new Error(result.stderr || `git ${args.join(" ")} failed`);
|
|
470
|
+
}
|
|
471
|
+
return result.stdout.trimEnd();
|
|
472
|
+
}
|
|
473
|
+
async function getRepoRoot(cwd = process.cwd()) {
|
|
474
|
+
return git(["rev-parse", "--show-toplevel"], cwd);
|
|
475
|
+
}
|
|
476
|
+
async function captureDiff(baseRef = "HEAD", cwd = process.cwd()) {
|
|
477
|
+
const repoRoot = await getRepoRoot(cwd);
|
|
478
|
+
const [baseSha, branchResult, trackedDiff, untrackedFilesRaw] = await Promise.all([
|
|
479
|
+
git(["rev-parse", baseRef], repoRoot),
|
|
480
|
+
execa("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoRoot, reject: false }),
|
|
481
|
+
git(["diff", "--no-color", "--find-renames", "--find-copies", baseRef, "--"], repoRoot),
|
|
482
|
+
git(["ls-files", "--others", "--exclude-standard", "-z"], repoRoot)
|
|
483
|
+
]);
|
|
484
|
+
const untrackedFiles = untrackedFilesRaw.split("\0").filter(Boolean);
|
|
485
|
+
const untrackedDiffs = await Promise.all(
|
|
486
|
+
untrackedFiles.map(
|
|
487
|
+
(filePath) => gitLenient(["diff", "--no-color", "--no-index", "--", "/dev/null", filePath], repoRoot)
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
const rawDiff = [trackedDiff, ...untrackedDiffs].filter(Boolean).join("\n");
|
|
491
|
+
const branch = branchResult.exitCode === 0 ? branchResult.stdout.trim() : null;
|
|
492
|
+
return {
|
|
493
|
+
base: { ref: baseRef, sha: baseSha },
|
|
494
|
+
branch: branch && branch !== "HEAD" ? branch : null,
|
|
495
|
+
cwd: repoRoot,
|
|
496
|
+
rawDiff,
|
|
497
|
+
files: parseUnifiedDiff(rawDiff),
|
|
498
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
async function assertGitAvailable() {
|
|
502
|
+
await execa("git", ["--version"]);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// src/cli/index.ts
|
|
506
|
+
function printJson(value) {
|
|
507
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
508
|
+
`);
|
|
509
|
+
}
|
|
510
|
+
function printPlain(value) {
|
|
511
|
+
process.stdout.write(`${value}
|
|
512
|
+
`);
|
|
513
|
+
}
|
|
514
|
+
var program = new Command();
|
|
515
|
+
program.name("gloss").description("Local browser-based diff review for coding-agent loops.").version(packageVersion).option("--json", "print JSON for supported commands").option("--no-color", "disable color output");
|
|
516
|
+
program.command("open").description("Capture diff vs. base and open it for review").option("--base <ref>", "base git ref", "HEAD").option("--print-url", "print review URL").option("--no-open", "do not open a browser").option("--no-watch", "return immediately after registering the review").option("--timeout <seconds>", "watch timeout in seconds", Number).action(
|
|
517
|
+
async (options) => {
|
|
518
|
+
const globals = program.opts();
|
|
519
|
+
const info = await ensureServer();
|
|
520
|
+
const client2 = new ServerClient(serverUrl(info));
|
|
521
|
+
const diff = await captureDiff(options.base);
|
|
522
|
+
const { meta, url } = await client2.createReview(diff);
|
|
523
|
+
if (options.printUrl) {
|
|
524
|
+
printPlain(url);
|
|
525
|
+
}
|
|
526
|
+
if (options.open !== false) {
|
|
527
|
+
await openBrowser(url);
|
|
528
|
+
}
|
|
529
|
+
if (options.watch === false) {
|
|
530
|
+
const result2 = { reviewId: meta.id, url, files: diff.files.length };
|
|
531
|
+
globals.json ? printJson(result2) : printPlain(`Review ${meta.id}: ${url}`);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
const event = await client2.watchReview(meta.id, options.timeout);
|
|
535
|
+
if (event.type === "review.cancelled") {
|
|
536
|
+
process.exitCode = 2;
|
|
537
|
+
globals.json ? printJson(event) : printPlain(`Review ${meta.id} cancelled`);
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (event.type !== "review.completed") {
|
|
541
|
+
throw new Error(`Unexpected review event ${event.type}`);
|
|
542
|
+
}
|
|
543
|
+
const feedback = await client2.getFeedback(meta.id);
|
|
544
|
+
const result = {
|
|
545
|
+
reviewId: meta.id,
|
|
546
|
+
url,
|
|
547
|
+
files: event.counts.files,
|
|
548
|
+
comments: event.counts.comments,
|
|
549
|
+
feedbackPath: `${diff.cwd}/.gloss/reviews/${meta.id}/feedback.json`,
|
|
550
|
+
markdownPath: `${diff.cwd}/.gloss/reviews/${meta.id}/feedback.md`,
|
|
551
|
+
feedback
|
|
552
|
+
};
|
|
553
|
+
globals.json ? printJson(result) : printPlain(`Review ${meta.id} completed with ${event.counts.comments} comments`);
|
|
554
|
+
}
|
|
555
|
+
);
|
|
556
|
+
program.command("watch").argument("<reviewId>", "review id").description("Wait for review.completed for an existing review").option("--timeout <seconds>", "watch timeout in seconds", Number).action(async (reviewId, options) => {
|
|
557
|
+
const globals = program.opts();
|
|
558
|
+
const info = await ensureServer();
|
|
559
|
+
const client2 = new ServerClient(serverUrl(info));
|
|
560
|
+
const event = await client2.watchReview(reviewId, options.timeout);
|
|
561
|
+
globals.json ? printJson(event) : printPlain(`${event.type} ${event.reviewId}`);
|
|
562
|
+
});
|
|
563
|
+
program.command("start").description("Start or reuse the background server").option("--port <port>", "port to bind", Number).action(async (options) => {
|
|
564
|
+
const globals = program.opts();
|
|
565
|
+
const info = await startServer({ port: options.port });
|
|
566
|
+
globals.json ? printJson(info) : printPlain(`Gloss server running at ${serverUrl(info)} (pid ${info.pid})`);
|
|
567
|
+
});
|
|
568
|
+
program.command("status").description("Show server and active reviews").action(async () => {
|
|
569
|
+
const globals = program.opts();
|
|
570
|
+
const info = await readServerInfo();
|
|
571
|
+
const responsive = info ? await isServerResponsive(info) : false;
|
|
572
|
+
let reviews = [];
|
|
573
|
+
if (info && responsive) {
|
|
574
|
+
reviews = (await new ServerClient(serverUrl(info)).listReviews()).reviews;
|
|
575
|
+
}
|
|
576
|
+
const status = { running: responsive, server: info, reviews };
|
|
577
|
+
globals.json ? printJson(status) : printPlain(
|
|
578
|
+
responsive && info ? `Gloss server running at ${serverUrl(info)} with ${reviews.length} active review(s)` : "Gloss server is not running"
|
|
579
|
+
);
|
|
580
|
+
});
|
|
581
|
+
program.command("stop").description("Stop the managed background server").option("--all", "reserved for future multi-server cleanup").action(async () => {
|
|
582
|
+
const globals = program.opts();
|
|
583
|
+
const result = await stopServer();
|
|
584
|
+
globals.json ? printJson(result) : printPlain(result.stopped ? "Gloss server stopped" : "Gloss server was not running");
|
|
585
|
+
});
|
|
586
|
+
program.command("mcp").description("Start the experimental stdio MCP server").action(async () => {
|
|
587
|
+
await startMcpServer();
|
|
588
|
+
});
|
|
589
|
+
program.command("doctor").description("Diagnose setup and validate git/state").action(async () => {
|
|
590
|
+
const globals = program.opts();
|
|
591
|
+
const checks = [];
|
|
592
|
+
try {
|
|
593
|
+
await assertGitAvailable();
|
|
594
|
+
checks.push({ name: "git", ok: true });
|
|
595
|
+
} catch (error) {
|
|
596
|
+
checks.push({
|
|
597
|
+
name: "git",
|
|
598
|
+
ok: false,
|
|
599
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
try {
|
|
603
|
+
const root = await getRepoRoot();
|
|
604
|
+
checks.push({ name: "repo", ok: true, detail: root });
|
|
605
|
+
} catch (error) {
|
|
606
|
+
checks.push({
|
|
607
|
+
name: "repo",
|
|
608
|
+
ok: false,
|
|
609
|
+
detail: error instanceof Error ? error.message : String(error)
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
const info = await readServerInfo();
|
|
613
|
+
checks.push({
|
|
614
|
+
name: "server",
|
|
615
|
+
ok: info ? await isServerResponsive(info) : false,
|
|
616
|
+
detail: info ? serverUrl(info) : "not started"
|
|
617
|
+
});
|
|
618
|
+
checks.push({
|
|
619
|
+
name: "@pierre/diffs license",
|
|
620
|
+
ok: true,
|
|
621
|
+
detail: "apache-2.0 dependency present"
|
|
622
|
+
});
|
|
623
|
+
if (globals.json) {
|
|
624
|
+
printJson({ checks });
|
|
625
|
+
} else {
|
|
626
|
+
for (const check of checks) {
|
|
627
|
+
printPlain(
|
|
628
|
+
`${check.ok ? "ok" : "fail"} ${check.name}${check.detail ? ` - ${check.detail}` : ""}`
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
program.parseAsync(process.argv).catch((error) => {
|
|
634
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
635
|
+
`);
|
|
636
|
+
process.exitCode = 1;
|
|
637
|
+
});
|
|
638
|
+
//# sourceMappingURL=index.js.map
|