diffact 0.1.0 → 0.2.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.
Files changed (52) hide show
  1. package/dist/cli.mjs +2 -0
  2. package/dist/index-node-bKTmbwGt.mjs +1 -0
  3. package/dist/src-CPKE75x0.mjs +11 -0
  4. package/dist/src-Ceryd8j5.mjs +1 -0
  5. package/package.json +4 -5
  6. package/web/dist/assets/{code-block-37QAKDTI-C97XC0lL.js → code-block-37QAKDTI-yDNOZoY4.js} +1 -1
  7. package/web/dist/assets/{index-DWCfDth4.js → index-BlaXWu6U.js} +30 -30
  8. package/web/dist/assets/index-DMEToi1s.css +1 -0
  9. package/web/dist/index.html +2 -2
  10. package/dist/agent-manager.d.ts +0 -32
  11. package/dist/agent-manager.js +0 -502
  12. package/dist/app-server-client.d.ts +0 -38
  13. package/dist/app-server-client.js +0 -249
  14. package/dist/capabilities.d.ts +0 -2
  15. package/dist/capabilities.js +0 -27
  16. package/dist/cli.d.ts +0 -6
  17. package/dist/cli.js +0 -13
  18. package/dist/command-runner.d.ts +0 -83
  19. package/dist/command-runner.js +0 -427
  20. package/dist/editors.d.ts +0 -26
  21. package/dist/editors.js +0 -144
  22. package/dist/gh.d.ts +0 -61
  23. package/dist/gh.js +0 -185
  24. package/dist/git.d.ts +0 -57
  25. package/dist/git.js +0 -482
  26. package/dist/http.d.ts +0 -7
  27. package/dist/http.js +0 -98
  28. package/dist/index-node.d.ts +0 -5
  29. package/dist/index-node.js +0 -51
  30. package/dist/index.d.ts +0 -6
  31. package/dist/index.js +0 -1011
  32. package/dist/list-directories.d.ts +0 -8
  33. package/dist/list-directories.js +0 -32
  34. package/dist/log.d.ts +0 -2
  35. package/dist/log.js +0 -2
  36. package/dist/open-browser.d.ts +0 -5
  37. package/dist/open-browser.js +0 -23
  38. package/dist/project-commands.d.ts +0 -17
  39. package/dist/project-commands.js +0 -152
  40. package/dist/project-path.d.ts +0 -5
  41. package/dist/project-path.js +0 -33
  42. package/dist/runtime.d.ts +0 -65
  43. package/dist/runtime.js +0 -235
  44. package/dist/static.d.ts +0 -10
  45. package/dist/static.js +0 -127
  46. package/dist/types.d.ts +0 -17
  47. package/dist/types.js +0 -1
  48. package/dist/utils.d.ts +0 -3
  49. package/dist/utils.js +0 -26
  50. package/dist/ws-hub.d.ts +0 -20
  51. package/dist/ws-hub.js +0 -123
  52. package/web/dist/assets/index-CRDz04kv.css +0 -1
package/dist/index.js DELETED
@@ -1,1011 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { Hono } from "hono";
5
- import { logger } from "hono/logger";
6
- import { AgentManager } from "./agent-manager.js";
7
- import { computeCapabilities } from "./capabilities.js";
8
- import { getCommandRun, getCommandRunOutput, listCommandRuns, startCommandRun, stopCommandRun, } from "./command-runner.js";
9
- import { listAvailableEditors, openInEditor, } from "./editors.js";
10
- import { ghGetIssueDetail, ghListIssues, ghListPullRequests } from "./gh.js";
11
- import { gitCheckoutBranch, gitCommitDiff, gitCommitDiffLineEstimate, gitCommitDiffStream, gitCommitFiles, gitCreateBranch, gitDiff, gitListBranchesPaginated, gitListFileTree, gitListStagedFiles, gitLog, gitShowFile, gitStagePaths, gitUnstagePaths, } from "./git.js";
12
- import { jsonResponse, mapErrorResponse, optionsResponse, readJson, streamResponse, textResponse, withCors, } from "./http.js";
13
- import { listDirectories } from "./list-directories.js";
14
- import { log } from "./log.js";
15
- import { openBrowser } from "./open-browser.js";
16
- import { listProjectCommands, resolveCommandPlan, } from "./project-commands.js";
17
- import { resolveProjectRoot } from "./project-path.js";
18
- import { runtime } from "./runtime.js";
19
- import { getStaticDir, isStaticEnabled, serveStatic } from "./static.js";
20
- import { WsHub } from "./ws-hub.js";
21
- const PROJECT_ROOT = process.env.DIFFACT_PROJECT_ROOT
22
- ? path.resolve(process.env.DIFFACT_PROJECT_ROOT)
23
- : path.join(os.homedir(), "workspace");
24
- const PORT = Number.parseInt(process.env.PORT || "", 10) || 4312;
25
- const INLINE_DIFF_LINE_LIMIT = Number.parseInt(process.env.DIFFACT_INLINE_DIFF_LINES || "", 10) || 4000;
26
- const stripAnsi = (value) => value.replace(/\u001b\[[0-9;]*m/g, "");
27
- const decodePercents = (value) => value.replace(/(?:%[0-9A-Fa-f]{2})+/g, (match) => {
28
- try {
29
- return decodeURIComponent(match);
30
- }
31
- catch {
32
- return match;
33
- }
34
- });
35
- const readStringField = (record, key) => {
36
- const value = record[key];
37
- return typeof value === "string" ? value.trim() : "";
38
- };
39
- const readOptionalNumberField = (record, key) => {
40
- const value = record[key];
41
- if (typeof value !== "number" || !Number.isFinite(value)) {
42
- return null;
43
- }
44
- return value;
45
- };
46
- const readOptionalStringField = (record, key) => {
47
- const value = record[key];
48
- if (typeof value !== "string") {
49
- return undefined;
50
- }
51
- const trimmed = value.trim();
52
- return trimmed ? trimmed : undefined;
53
- };
54
- const readStringArrayField = (record, key) => {
55
- const value = record[key];
56
- if (!Array.isArray(value)) {
57
- return [];
58
- }
59
- return value
60
- .filter((entry) => typeof entry === "string")
61
- .map((entry) => entry.trim())
62
- .filter(Boolean);
63
- };
64
- const normalizeProjectPaths = (rootPath, paths) => {
65
- const unique = new Set();
66
- for (const raw of paths) {
67
- const trimmed = raw.trim();
68
- if (!trimmed) {
69
- continue;
70
- }
71
- const resolved = path.resolve(rootPath, trimmed);
72
- const relative = path.relative(rootPath, resolved);
73
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
74
- return null;
75
- }
76
- unique.add(relative.replace(/\\/g, "/"));
77
- }
78
- return Array.from(unique.values());
79
- };
80
- const isCommandSource = (value) => value === "package.json" || value === "makefile" || value === "custom";
81
- const toCommandDescriptor = (command) => ({
82
- id: command.id,
83
- name: command.name,
84
- label: command.label,
85
- source: command.source,
86
- });
87
- const toCommandRunSummary = (record, includeOutput) => ({
88
- runId: record.id,
89
- projectPath: record.projectRoot,
90
- terminalId: record.terminalId,
91
- commandId: record.command.id,
92
- commandName: record.command.name,
93
- commandLabel: record.command.label,
94
- commandSource: record.command.source,
95
- status: record.status,
96
- exitCode: record.exitCode,
97
- signal: record.signal,
98
- startedAt: record.startedAt,
99
- completedAt: record.completedAt ?? null,
100
- stdout: includeOutput ? record.output.stdout : undefined,
101
- stderr: includeOutput ? record.output.stderr : undefined,
102
- });
103
- const shouldIncludeOutput = (record) => record.status !== "running";
104
- const buildShellPlan = (cwd, commandLine) => ({
105
- command: commandLine,
106
- args: [],
107
- cwd,
108
- commandLine,
109
- });
110
- const CONTENT_TYPES = {
111
- avif: "image/avif",
112
- bmp: "image/bmp",
113
- gif: "image/gif",
114
- ico: "image/x-icon",
115
- jpeg: "image/jpeg",
116
- jpg: "image/jpeg",
117
- png: "image/png",
118
- svg: "image/svg+xml",
119
- webp: "image/webp",
120
- mp4: "video/mp4",
121
- mov: "video/quicktime",
122
- ogv: "video/ogg",
123
- webm: "video/webm",
124
- mp3: "audio/mpeg",
125
- m4a: "audio/mp4",
126
- ogg: "audio/ogg",
127
- wav: "audio/wav",
128
- flac: "audio/flac",
129
- pdf: "application/pdf",
130
- };
131
- const guessContentType = (filePath) => {
132
- const ext = path.extname(filePath).slice(1).toLowerCase();
133
- return CONTENT_TYPES[ext] || "application/octet-stream";
134
- };
135
- const hub = new WsHub();
136
- const agentManager = new AgentManager({ hub });
137
- hub.setCommandHandler(async ({ clientId, message }) => {
138
- const { type, payload, requestId } = message;
139
- if (!type) {
140
- return;
141
- }
142
- const sendAck = (result) => {
143
- if (!requestId) {
144
- return;
145
- }
146
- hub.sendToClient(clientId, "command_ack", { requestId, result });
147
- };
148
- const sendError = (error) => {
149
- if (!requestId) {
150
- return;
151
- }
152
- hub.sendToClient(clientId, "command_error", { requestId, error });
153
- };
154
- try {
155
- if (type === "agent_start") {
156
- if (!payload || typeof payload !== "object") {
157
- throw new Error("invalid_payload");
158
- }
159
- const record = payload;
160
- const projectPath = readStringField(record, "projectPath");
161
- const prompt = readStringField(record, "prompt");
162
- const label = readOptionalStringField(record, "label");
163
- if (!projectPath || !prompt) {
164
- throw new Error("invalid_payload");
165
- }
166
- const agent = await agentManager.startAgent({
167
- projectPath,
168
- prompt,
169
- label,
170
- });
171
- sendAck({ agent });
172
- return;
173
- }
174
- if (type === "thread_message") {
175
- if (!payload || typeof payload !== "object") {
176
- throw new Error("invalid_payload");
177
- }
178
- const record = payload;
179
- const threadId = readStringField(record, "threadId");
180
- const projectPath = readStringField(record, "projectPath");
181
- const text = readStringField(record, "text");
182
- const label = readOptionalStringField(record, "label");
183
- if (!threadId || !projectPath || !text) {
184
- throw new Error("invalid_payload");
185
- }
186
- const agent = await agentManager.resumeThread({
187
- threadId,
188
- projectPath,
189
- label,
190
- });
191
- const sent = await agentManager.sendMessage(agent.id, text);
192
- if (!sent) {
193
- throw new Error("agent_not_found");
194
- }
195
- sendAck({ agentId: agent.id, threadId: agent.threadId ?? threadId });
196
- return;
197
- }
198
- if (type === "agent_stop") {
199
- if (!payload || typeof payload !== "object") {
200
- throw new Error("invalid_payload");
201
- }
202
- const record = payload;
203
- const agentId = readOptionalStringField(record, "agentId");
204
- const threadId = readOptionalStringField(record, "threadId");
205
- let stopped = false;
206
- if (agentId) {
207
- stopped = agentManager.stopAgent(agentId);
208
- }
209
- else if (threadId) {
210
- stopped = agentManager.stopThread(threadId);
211
- }
212
- if (!stopped) {
213
- throw new Error("agent_not_found");
214
- }
215
- sendAck({ agentId: agentId ?? null, threadId: threadId ?? null });
216
- return;
217
- }
218
- if (type === "terminal_start") {
219
- if (!payload || typeof payload !== "object") {
220
- throw new Error("invalid_payload");
221
- }
222
- const record = payload;
223
- const projectPath = readStringField(record, "projectPath");
224
- const source = readStringField(record, "source");
225
- const name = readStringField(record, "name");
226
- const cols = readOptionalNumberField(record, "cols");
227
- const rows = readOptionalNumberField(record, "rows");
228
- if (!projectPath || !source || !name || !isCommandSource(source)) {
229
- throw new Error("invalid_payload");
230
- }
231
- const project = await resolveProjectRoot(projectPath);
232
- const commands = await listProjectCommands(project.rootPath);
233
- const command = commands.find((item) => item.source === source && item.name === name);
234
- if (!command) {
235
- throw new Error("command_not_found");
236
- }
237
- const plan = resolveCommandPlan(project.rootPath, source, name);
238
- if (!plan) {
239
- throw new Error("command_unsupported");
240
- }
241
- const result = await startCommandRun({
242
- command: toCommandDescriptor(command),
243
- projectRoot: project.rootPath,
244
- plan,
245
- terminal: cols && rows
246
- ? {
247
- cols,
248
- rows,
249
- }
250
- : undefined,
251
- listener: {
252
- onOutput: (event) => {
253
- const run = getCommandRun(event.runId);
254
- hub.send("terminal_output", {
255
- projectPath: project.rootPath,
256
- runId: event.runId,
257
- terminalId: run?.terminalId,
258
- stream: event.stream,
259
- data: event.data,
260
- });
261
- },
262
- onStatus: (run) => {
263
- hub.send("terminal_status", {
264
- projectPath: project.rootPath,
265
- run: toCommandRunSummary(run, shouldIncludeOutput(run)),
266
- });
267
- },
268
- },
269
- });
270
- if (!result.runId) {
271
- throw new Error(result.error || "command_failed");
272
- }
273
- const run = getCommandRun(result.runId);
274
- if (!run) {
275
- throw new Error("command_not_found");
276
- }
277
- sendAck({ run: toCommandRunSummary(run, shouldIncludeOutput(run)) });
278
- return;
279
- }
280
- if (type === "terminal_exec") {
281
- if (!payload || typeof payload !== "object") {
282
- throw new Error("invalid_payload");
283
- }
284
- const record = payload;
285
- const projectPath = readStringField(record, "projectPath");
286
- const commandLine = readStringField(record, "command");
287
- const label = readOptionalStringField(record, "label");
288
- const name = readOptionalStringField(record, "name");
289
- const cols = readOptionalNumberField(record, "cols");
290
- const rows = readOptionalNumberField(record, "rows");
291
- if (!projectPath || !commandLine) {
292
- throw new Error("invalid_payload");
293
- }
294
- const project = await resolveProjectRoot(projectPath);
295
- const plan = buildShellPlan(project.rootPath, commandLine);
296
- const commandLabel = label || name || "Custom Command";
297
- const commandName = name || label || "custom";
298
- const descriptor = {
299
- id: `custom:${commandName}`,
300
- name: commandName,
301
- label: commandLabel,
302
- source: "custom",
303
- };
304
- const result = await startCommandRun({
305
- command: descriptor,
306
- projectRoot: project.rootPath,
307
- plan,
308
- terminal: cols && rows
309
- ? {
310
- cols,
311
- rows,
312
- }
313
- : undefined,
314
- listener: {
315
- onOutput: (event) => {
316
- const run = getCommandRun(event.runId);
317
- hub.send("terminal_output", {
318
- projectPath: project.rootPath,
319
- runId: event.runId,
320
- terminalId: run?.terminalId,
321
- stream: event.stream,
322
- data: event.data,
323
- });
324
- },
325
- onStatus: (run) => {
326
- hub.send("terminal_status", {
327
- projectPath: project.rootPath,
328
- run: toCommandRunSummary(run, shouldIncludeOutput(run)),
329
- });
330
- },
331
- },
332
- });
333
- if (!result.runId) {
334
- throw new Error(result.error || "command_failed");
335
- }
336
- const run = getCommandRun(result.runId);
337
- if (!run) {
338
- throw new Error("command_not_found");
339
- }
340
- sendAck({ run: toCommandRunSummary(run, shouldIncludeOutput(run)) });
341
- return;
342
- }
343
- if (type === "terminal_stop") {
344
- if (!payload || typeof payload !== "object") {
345
- throw new Error("invalid_payload");
346
- }
347
- const record = payload;
348
- const projectPath = readStringField(record, "projectPath");
349
- const runId = readStringField(record, "runId");
350
- if (!projectPath || !runId) {
351
- throw new Error("invalid_payload");
352
- }
353
- const project = await resolveProjectRoot(projectPath);
354
- const existing = getCommandRun(runId);
355
- if (!existing || existing.projectRoot !== project.rootPath) {
356
- throw new Error("command_not_found");
357
- }
358
- const run = stopCommandRun(runId);
359
- if (!run) {
360
- throw new Error("command_not_found");
361
- }
362
- sendAck({ run: toCommandRunSummary(run, shouldIncludeOutput(run)) });
363
- return;
364
- }
365
- if (type === "terminal_list") {
366
- if (!payload || typeof payload !== "object") {
367
- throw new Error("invalid_payload");
368
- }
369
- const record = payload;
370
- const projectPath = readStringField(record, "projectPath");
371
- if (!projectPath) {
372
- throw new Error("invalid_payload");
373
- }
374
- const project = await resolveProjectRoot(projectPath);
375
- const runs = listCommandRuns(project.rootPath)
376
- .sort((a, b) => b.startedAt.localeCompare(a.startedAt))
377
- .map((run) => toCommandRunSummary(run, true));
378
- sendAck({ runs });
379
- return;
380
- }
381
- if (type === "terminal_output") {
382
- if (!payload || typeof payload !== "object") {
383
- throw new Error("invalid_payload");
384
- }
385
- const record = payload;
386
- const projectPath = readStringField(record, "projectPath");
387
- const runId = readStringField(record, "runId");
388
- if (!projectPath || !runId) {
389
- throw new Error("invalid_payload");
390
- }
391
- const project = await resolveProjectRoot(projectPath);
392
- const existing = getCommandRun(runId);
393
- if (!existing || existing.projectRoot !== project.rootPath) {
394
- throw new Error("command_not_found");
395
- }
396
- const output = getCommandRunOutput(runId);
397
- if (!output) {
398
- throw new Error("command_not_found");
399
- }
400
- sendAck({ stdout: output.stdout, stderr: output.stderr });
401
- return;
402
- }
403
- log.warn({ type }, "ws unknown command");
404
- sendError("unknown_command");
405
- }
406
- catch (error) {
407
- const messageText = error instanceof Error ? error.message : "command_failed";
408
- sendError(messageText);
409
- }
410
- });
411
- const app = new Hono();
412
- app.use("*", logger((message, ...rest) => {
413
- log.info({ rest }, decodePercents(stripAnsi(message)));
414
- }));
415
- app.onError((error, c) => {
416
- log.error({ err: error, path: c.req.path, method: c.req.method }, "error");
417
- return mapErrorResponse(error);
418
- });
419
- app.options("*", () => optionsResponse());
420
- app.get("/health", () => jsonResponse(200, {
421
- status: "ok",
422
- time: new Date().toISOString(),
423
- }));
424
- // WebSocket route is registered dynamically based on runtime (see bottom of file)
425
- app.get("/api/capabilities", async (c) => {
426
- const url = new URL(c.req.url);
427
- const projectPath = url.searchParams.get("project");
428
- const capabilities = await computeCapabilities(projectPath);
429
- return jsonResponse(200, capabilities);
430
- });
431
- app.get("/api/editors", async () => {
432
- const editors = await listAvailableEditors();
433
- return jsonResponse(200, { editors });
434
- });
435
- app.post("/api/editors/open", async (c) => {
436
- const body = (await readJson(c.req.raw));
437
- if (!body || !body.editor || !body.project) {
438
- return jsonResponse(400, { error: "invalid_payload" });
439
- }
440
- const editorId = body.editor;
441
- if (!["vscode", "cursor", "zed"].includes(editorId)) {
442
- return jsonResponse(400, { error: "invalid_editor" });
443
- }
444
- const target = body.target ?? { type: "project" };
445
- const result = await openInEditor(editorId, body.project, target);
446
- if (!result.success) {
447
- return jsonResponse(400, { error: result.error ?? "open_failed" });
448
- }
449
- return jsonResponse(200, { success: true });
450
- });
451
- app.get("/api/projects/browse", async (c) => {
452
- const url = new URL(c.req.url);
453
- const targetPath = url.searchParams.get("path")?.trim();
454
- const listing = await listDirectories(targetPath || PROJECT_ROOT);
455
- return jsonResponse(200, listing);
456
- });
457
- app.post("/api/projects/open", async (c) => {
458
- const body = (await readJson(c.req.raw));
459
- if (!body || typeof body.path !== "string") {
460
- return jsonResponse(400, { error: "invalid_path" });
461
- }
462
- const project = await resolveProjectRoot(body.path);
463
- return jsonResponse(200, project);
464
- });
465
- app.get("/api/projects/commands", async (c) => {
466
- const url = new URL(c.req.url);
467
- const projectPath = url.searchParams.get("project")?.trim();
468
- if (!projectPath) {
469
- return jsonResponse(400, { error: "invalid_path" });
470
- }
471
- const project = await resolveProjectRoot(projectPath);
472
- const commands = await listProjectCommands(project.rootPath);
473
- return jsonResponse(200, { commands });
474
- });
475
- app.post("/api/projects/commands/run", async (c) => {
476
- const url = new URL(c.req.url);
477
- const projectPath = url.searchParams.get("project")?.trim();
478
- if (!projectPath) {
479
- return jsonResponse(400, { error: "invalid_path" });
480
- }
481
- const body = (await readJson(c.req.raw));
482
- if (!body) {
483
- return jsonResponse(400, { error: "invalid_command" });
484
- }
485
- const source = readStringField(body, "source");
486
- const name = readStringField(body, "name");
487
- if (!source || !name || !isCommandSource(source)) {
488
- return jsonResponse(400, { error: "invalid_command" });
489
- }
490
- const project = await resolveProjectRoot(projectPath);
491
- const commands = await listProjectCommands(project.rootPath);
492
- const command = commands.find((item) => item.source === source && item.name === name);
493
- if (!command) {
494
- return jsonResponse(404, { error: "command_not_found" });
495
- }
496
- const plan = resolveCommandPlan(project.rootPath, source, name);
497
- if (!plan) {
498
- return jsonResponse(400, { error: "command_unsupported" });
499
- }
500
- const result = await startCommandRun({
501
- command: toCommandDescriptor(command),
502
- projectRoot: project.rootPath,
503
- plan,
504
- });
505
- return jsonResponse(200, result);
506
- });
507
- app.get("/api/projects/commands/runs/:id", async (c) => {
508
- const url = new URL(c.req.url);
509
- const projectPath = url.searchParams.get("project")?.trim();
510
- const runId = c.req.param("id")?.trim();
511
- if (!projectPath || !runId) {
512
- return jsonResponse(400, { error: "invalid_command" });
513
- }
514
- const project = await resolveProjectRoot(projectPath);
515
- const record = getCommandRun(runId);
516
- if (!record || record.projectRoot !== project.rootPath) {
517
- return jsonResponse(404, { error: "command_not_found" });
518
- }
519
- return jsonResponse(200, {
520
- status: record.status,
521
- exitCode: record.exitCode,
522
- signal: record.signal,
523
- });
524
- });
525
- app.post("/api/projects/commands/runs/:id/stop", async (c) => {
526
- const url = new URL(c.req.url);
527
- const projectPath = url.searchParams.get("project")?.trim();
528
- const runId = c.req.param("id")?.trim();
529
- if (!projectPath || !runId) {
530
- return jsonResponse(400, { error: "invalid_command" });
531
- }
532
- const project = await resolveProjectRoot(projectPath);
533
- const existing = getCommandRun(runId);
534
- if (!existing || existing.projectRoot !== project.rootPath) {
535
- return jsonResponse(404, { error: "command_not_found" });
536
- }
537
- const record = stopCommandRun(runId);
538
- if (!record) {
539
- return jsonResponse(404, { error: "command_not_found" });
540
- }
541
- return jsonResponse(200, {
542
- status: record.status,
543
- exitCode: record.exitCode,
544
- signal: record.signal,
545
- });
546
- });
547
- app.get("/api/projects/diff", async (c) => {
548
- const url = new URL(c.req.url);
549
- const projectPath = url.searchParams.get("project")?.trim();
550
- if (!projectPath) {
551
- return jsonResponse(400, { error: "invalid_path" });
552
- }
553
- const project = await resolveProjectRoot(projectPath);
554
- if (!project.isGit) {
555
- return textResponse(200, "");
556
- }
557
- const args = url.searchParams.getAll("arg").filter(Boolean);
558
- const paths = url.searchParams.getAll("path").filter(Boolean);
559
- const diff = await gitDiff(project.rootPath, args, paths);
560
- return textResponse(200, diff);
561
- });
562
- app.get("/api/projects/staged", async (c) => {
563
- const url = new URL(c.req.url);
564
- const projectPath = url.searchParams.get("project")?.trim();
565
- if (!projectPath) {
566
- return jsonResponse(400, { error: "invalid_path" });
567
- }
568
- const project = await resolveProjectRoot(projectPath);
569
- if (!project.isGit) {
570
- return jsonResponse(200, { paths: [] });
571
- }
572
- const paths = await gitListStagedFiles(project.rootPath);
573
- return jsonResponse(200, { paths });
574
- });
575
- app.post("/api/projects/stage", async (c) => {
576
- const url = new URL(c.req.url);
577
- const projectPath = url.searchParams.get("project")?.trim();
578
- if (!projectPath) {
579
- return jsonResponse(400, { error: "invalid_path" });
580
- }
581
- const body = (await readJson(c.req.raw));
582
- if (!body) {
583
- return jsonResponse(400, { error: "invalid_payload" });
584
- }
585
- const project = await resolveProjectRoot(projectPath);
586
- if (!project.isGit) {
587
- return jsonResponse(400, { error: "project_not_git" });
588
- }
589
- const stagedValue = body.staged;
590
- if (typeof stagedValue !== "boolean") {
591
- return jsonResponse(400, { error: "invalid_payload" });
592
- }
593
- const pathValue = readStringField(body, "path");
594
- const pathsValue = readStringArrayField(body, "paths");
595
- const requestedPaths = pathsValue.length
596
- ? pathsValue
597
- : pathValue
598
- ? [pathValue]
599
- : [];
600
- if (!requestedPaths.length) {
601
- return jsonResponse(400, { error: "invalid_path" });
602
- }
603
- const normalized = normalizeProjectPaths(project.rootPath, requestedPaths);
604
- if (!normalized || !normalized.length) {
605
- return jsonResponse(400, { error: "invalid_path" });
606
- }
607
- if (stagedValue) {
608
- await gitStagePaths(project.rootPath, normalized);
609
- }
610
- else {
611
- await gitUnstagePaths(project.rootPath, normalized);
612
- }
613
- return jsonResponse(200, { paths: normalized, staged: stagedValue });
614
- });
615
- app.get("/api/projects/tree", async (c) => {
616
- const url = new URL(c.req.url);
617
- const projectPath = url.searchParams.get("project")?.trim();
618
- if (!projectPath) {
619
- return jsonResponse(400, { error: "invalid_path" });
620
- }
621
- const project = await resolveProjectRoot(projectPath);
622
- if (!project.isGit) {
623
- return jsonResponse(200, {
624
- available: false,
625
- rootPath: project.rootPath,
626
- entries: [],
627
- });
628
- }
629
- const entries = await gitListFileTree(project.rootPath);
630
- return jsonResponse(200, {
631
- available: true,
632
- rootPath: project.rootPath,
633
- entries,
634
- });
635
- });
636
- app.get("/api/projects/file", async (c) => {
637
- const url = new URL(c.req.url);
638
- const projectPath = url.searchParams.get("project")?.trim();
639
- const filePath = url.searchParams.get("path")?.trim();
640
- const ref = url.searchParams.get("ref")?.trim();
641
- if (!projectPath || !filePath) {
642
- return jsonResponse(400, { error: "invalid_path" });
643
- }
644
- const project = await resolveProjectRoot(projectPath);
645
- if (!project.isGit) {
646
- return jsonResponse(400, { error: "project_not_git" });
647
- }
648
- const resolved = path.resolve(project.rootPath, filePath);
649
- const relative = path.relative(project.rootPath, resolved);
650
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
651
- return jsonResponse(400, { error: "invalid_path" });
652
- }
653
- const normalizedPath = relative.replace(/\\/g, "/");
654
- if (ref) {
655
- try {
656
- const contents = await gitShowFile(project.rootPath, ref, normalizedPath);
657
- return jsonResponse(200, { path: filePath, contents });
658
- }
659
- catch (error) {
660
- if (error && typeof error === "object" && "code" in error) {
661
- const { code } = error;
662
- if (code === 128) {
663
- return jsonResponse(404, { error: "invalid_path" });
664
- }
665
- }
666
- throw error;
667
- }
668
- }
669
- let stat;
670
- try {
671
- stat = await fs.stat(resolved);
672
- }
673
- catch (error) {
674
- if (error && typeof error === "object" && "code" in error) {
675
- if (error.code === "ENOENT") {
676
- return jsonResponse(404, { error: "invalid_path" });
677
- }
678
- }
679
- throw error;
680
- }
681
- if (!stat.isFile()) {
682
- return jsonResponse(400, { error: "invalid_path" });
683
- }
684
- const contents = await fs.readFile(resolved, "utf8");
685
- return jsonResponse(200, { path: filePath, contents });
686
- });
687
- app.get("/api/projects/file/raw", async (c) => {
688
- const url = new URL(c.req.url);
689
- const projectPath = url.searchParams.get("project")?.trim();
690
- const filePath = url.searchParams.get("path")?.trim();
691
- if (!projectPath || !filePath) {
692
- return jsonResponse(400, { error: "invalid_path" });
693
- }
694
- const project = await resolveProjectRoot(projectPath);
695
- if (!project.isGit) {
696
- return jsonResponse(400, { error: "project_not_git" });
697
- }
698
- const resolved = path.resolve(project.rootPath, filePath);
699
- const relative = path.relative(project.rootPath, resolved);
700
- if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {
701
- return jsonResponse(400, { error: "invalid_path" });
702
- }
703
- let stat;
704
- try {
705
- stat = await fs.stat(resolved);
706
- }
707
- catch (error) {
708
- if (error && typeof error === "object" && "code" in error) {
709
- if (error.code === "ENOENT") {
710
- return jsonResponse(404, { error: "invalid_path" });
711
- }
712
- }
713
- throw error;
714
- }
715
- if (!stat.isFile()) {
716
- return jsonResponse(400, { error: "invalid_path" });
717
- }
718
- const file = runtime.file(resolved);
719
- const response = new Response(file.stream(), {
720
- status: 200,
721
- headers: {
722
- "Content-Type": guessContentType(resolved),
723
- "Content-Disposition": "inline",
724
- },
725
- });
726
- return withCors(response);
727
- });
728
- app.get("/api/projects/history", async (c) => {
729
- const url = new URL(c.req.url);
730
- const projectPath = url.searchParams.get("project")?.trim();
731
- if (!projectPath) {
732
- return jsonResponse(400, { error: "invalid_path" });
733
- }
734
- const project = await resolveProjectRoot(projectPath);
735
- if (!project.isGit) {
736
- return jsonResponse(400, { error: "project_not_git" });
737
- }
738
- const limitParam = url.searchParams.get("limit");
739
- const skipParam = url.searchParams.get("skip");
740
- const limit = limitParam ? Number.parseInt(limitParam, 10) : 30;
741
- const skip = skipParam ? Number.parseInt(skipParam, 10) : 0;
742
- const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(200, limit) : 30;
743
- const safeSkip = Number.isFinite(skip) && skip > 0 ? skip : 0;
744
- const page = await gitLog(project.rootPath, safeSkip, safeLimit);
745
- return jsonResponse(200, page);
746
- });
747
- app.get("/api/projects/commits/:sha/diff", async (c) => {
748
- const url = new URL(c.req.url);
749
- const projectPath = url.searchParams.get("project")?.trim();
750
- const sha = c.req.param("sha")?.trim();
751
- if (!projectPath) {
752
- return jsonResponse(400, { error: "invalid_path" });
753
- }
754
- if (!sha) {
755
- return jsonResponse(400, { error: "invalid_commit" });
756
- }
757
- const project = await resolveProjectRoot(projectPath);
758
- if (!project.isGit) {
759
- return jsonResponse(400, { error: "project_not_git" });
760
- }
761
- let estimate = null;
762
- try {
763
- estimate = await gitCommitDiffLineEstimate(project.rootPath, sha);
764
- }
765
- catch (error) {
766
- if (error && typeof error === "object" && "code" in error) {
767
- const { code } = error;
768
- if (code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
769
- estimate = null;
770
- }
771
- else {
772
- throw error;
773
- }
774
- }
775
- else {
776
- throw error;
777
- }
778
- }
779
- const shouldStream = estimate === null || estimate > INLINE_DIFF_LINE_LIMIT;
780
- if (!shouldStream) {
781
- const diff = await gitCommitDiff(project.rootPath, sha);
782
- return textResponse(200, diff);
783
- }
784
- const stream = gitCommitDiffStream(project.rootPath, sha);
785
- return streamResponse(200, stream);
786
- });
787
- app.get("/api/projects/commits/:sha/files", async (c) => {
788
- const url = new URL(c.req.url);
789
- const projectPath = url.searchParams.get("project")?.trim();
790
- const sha = c.req.param("sha")?.trim();
791
- if (!projectPath) {
792
- return jsonResponse(400, { error: "invalid_path" });
793
- }
794
- if (!sha) {
795
- return jsonResponse(400, { error: "invalid_commit" });
796
- }
797
- const project = await resolveProjectRoot(projectPath);
798
- if (!project.isGit) {
799
- return jsonResponse(400, { error: "project_not_git" });
800
- }
801
- const files = await gitCommitFiles(project.rootPath, sha);
802
- return jsonResponse(200, files);
803
- });
804
- app.get("/api/projects/branches", async (c) => {
805
- const url = new URL(c.req.url);
806
- const projectPath = url.searchParams.get("project")?.trim();
807
- const cursor = url.searchParams.get("cursor")?.trim() || null;
808
- const filter = url.searchParams.get("filter")?.trim() || null;
809
- const limitParam = url.searchParams.get("limit");
810
- const limit = limitParam ? Number.parseInt(limitParam, 10) : undefined;
811
- if (!projectPath) {
812
- return jsonResponse(400, { error: "invalid_path" });
813
- }
814
- const project = await resolveProjectRoot(projectPath);
815
- if (!project.isGit) {
816
- return jsonResponse(400, { error: "project_not_git" });
817
- }
818
- const listing = await gitListBranchesPaginated(project.rootPath, {
819
- limit,
820
- cursor,
821
- filter,
822
- });
823
- return jsonResponse(200, listing);
824
- });
825
- app.post("/api/projects/branches", async (c) => {
826
- const url = new URL(c.req.url);
827
- const projectPath = url.searchParams.get("project")?.trim();
828
- const body = (await readJson(c.req.raw));
829
- const name = body?.name?.trim() ?? "";
830
- if (!name) {
831
- return jsonResponse(400, { error: "invalid_branch" });
832
- }
833
- if (!projectPath) {
834
- return jsonResponse(400, { error: "invalid_path" });
835
- }
836
- const project = await resolveProjectRoot(projectPath);
837
- if (!project.isGit) {
838
- return jsonResponse(400, { error: "project_not_git" });
839
- }
840
- await gitCreateBranch(project.rootPath, name);
841
- return jsonResponse(201, { name });
842
- });
843
- app.post("/api/projects/branches/checkout", async (c) => {
844
- const url = new URL(c.req.url);
845
- const projectPath = url.searchParams.get("project")?.trim();
846
- const body = (await readJson(c.req.raw));
847
- const name = body?.name?.trim() ?? "";
848
- if (!name) {
849
- return jsonResponse(400, { error: "invalid_branch" });
850
- }
851
- if (!projectPath) {
852
- return jsonResponse(400, { error: "invalid_path" });
853
- }
854
- const project = await resolveProjectRoot(projectPath);
855
- if (!project.isGit) {
856
- return jsonResponse(400, { error: "project_not_git" });
857
- }
858
- await gitCheckoutBranch(project.rootPath, name);
859
- return jsonResponse(200, { name });
860
- });
861
- app.get("/api/projects/pull-requests", async (c) => {
862
- const url = new URL(c.req.url);
863
- const projectPath = url.searchParams.get("project")?.trim();
864
- const cursor = url.searchParams.get("cursor")?.trim() || null;
865
- const search = url.searchParams.get("search")?.trim() || null;
866
- const limitParam = url.searchParams.get("limit");
867
- const limit = limitParam ? Number.parseInt(limitParam, 10) : undefined;
868
- if (!projectPath) {
869
- return jsonResponse(400, { error: "invalid_path" });
870
- }
871
- const project = await resolveProjectRoot(projectPath);
872
- if (!project.isGit) {
873
- return jsonResponse(400, { error: "project_not_git" });
874
- }
875
- const listing = await ghListPullRequests(project.rootPath, {
876
- limit,
877
- cursor,
878
- search,
879
- });
880
- return jsonResponse(200, listing);
881
- });
882
- app.get("/api/projects/issues", async (c) => {
883
- const url = new URL(c.req.url);
884
- const projectPath = url.searchParams.get("project")?.trim();
885
- const cursor = url.searchParams.get("cursor")?.trim() || null;
886
- const search = url.searchParams.get("search")?.trim() || null;
887
- const state = url.searchParams.get("state")?.trim();
888
- const limitParam = url.searchParams.get("limit");
889
- const limit = limitParam ? Number.parseInt(limitParam, 10) : undefined;
890
- if (!projectPath) {
891
- return jsonResponse(400, { error: "invalid_path" });
892
- }
893
- const project = await resolveProjectRoot(projectPath);
894
- if (!project.isGit) {
895
- return jsonResponse(400, { error: "project_not_git" });
896
- }
897
- const listing = await ghListIssues(project.rootPath, {
898
- limit,
899
- cursor,
900
- search,
901
- state: state || "open",
902
- });
903
- return jsonResponse(200, listing);
904
- });
905
- app.get("/api/projects/issues/:number", async (c) => {
906
- const url = new URL(c.req.url);
907
- const projectPath = url.searchParams.get("project")?.trim();
908
- const issueNumber = Number.parseInt(c.req.param("number") || "", 10);
909
- if (!projectPath) {
910
- return jsonResponse(400, { error: "invalid_path" });
911
- }
912
- if (Number.isNaN(issueNumber)) {
913
- return jsonResponse(400, { error: "invalid_issue_number" });
914
- }
915
- const project = await resolveProjectRoot(projectPath);
916
- if (!project.isGit) {
917
- return jsonResponse(400, { error: "project_not_git" });
918
- }
919
- const detail = await ghGetIssueDetail(project.rootPath, issueNumber);
920
- if (!detail) {
921
- return jsonResponse(404, { error: "issue_not_found" });
922
- }
923
- return jsonResponse(200, detail);
924
- });
925
- app.get("/api/agents", () => jsonResponse(200, agentManager.listAgents()));
926
- app.get("/api/agents/:id", (c) => {
927
- const agentId = c.req.param("id");
928
- const agent = agentManager.getAgent(agentId);
929
- if (!agent) {
930
- return jsonResponse(404, { error: "agent_not_found" });
931
- }
932
- return jsonResponse(200, agent);
933
- });
934
- app.get("/api/threads", async (c) => {
935
- const url = new URL(c.req.url);
936
- const cursor = url.searchParams.get("cursor");
937
- const limitParam = url.searchParams.get("limit");
938
- const projectParam = url.searchParams.get("project")?.trim();
939
- const limit = limitParam ? Number.parseInt(limitParam, 10) : null;
940
- const projectPath = projectParam
941
- ? (await resolveProjectRoot(projectParam)).rootPath
942
- : null;
943
- const result = await agentManager.listThreads({
944
- cursor: cursor || null,
945
- limit: Number.isNaN(limit) ? null : limit,
946
- projectPath,
947
- });
948
- return jsonResponse(200, result);
949
- });
950
- app.get("/api/threads/history", async (c) => {
951
- const url = new URL(c.req.url);
952
- const filePath = url.searchParams.get("path")?.trim();
953
- if (!filePath) {
954
- return jsonResponse(400, { error: "invalid_path" });
955
- }
956
- const resolved = path.resolve(filePath);
957
- if (path.extname(resolved).toLowerCase() !== ".jsonl") {
958
- return jsonResponse(400, { error: "invalid_path" });
959
- }
960
- let stat;
961
- try {
962
- stat = await fs.stat(resolved);
963
- }
964
- catch (error) {
965
- if (error && typeof error === "object" && "code" in error) {
966
- if (error.code === "ENOENT") {
967
- return jsonResponse(404, { error: "invalid_path" });
968
- }
969
- }
970
- throw error;
971
- }
972
- if (!stat.isFile()) {
973
- return jsonResponse(400, { error: "invalid_path" });
974
- }
975
- const contents = await fs.readFile(resolved, "utf8");
976
- return textResponse(200, contents);
977
- });
978
- // Static file serving (SPA fallback)
979
- app.get("*", async (c) => {
980
- const reqPath = new URL(c.req.url).pathname;
981
- const response = await serveStatic(reqPath);
982
- if (response)
983
- return response;
984
- return textResponse(404, "Not found");
985
- });
986
- // Export app and hub for external server adapters
987
- export { app, hub, PORT };
988
- // CLI flags
989
- const NO_OPEN = process.argv.includes("--no-open");
990
- // Start server based on runtime
991
- if (runtime.isBun) {
992
- const { upgradeWebSocket, websocket } = await import("hono/bun");
993
- // Re-register WebSocket route with Bun adapter
994
- app.get("/api/events", upgradeWebSocket(() => {
995
- log.info("ws open");
996
- return hub.open();
997
- }));
998
- Bun.serve({
999
- port: PORT,
1000
- fetch: app.fetch,
1001
- websocket,
1002
- });
1003
- const url = `http://localhost:${PORT}`;
1004
- log.info(`diffact server listening on ${url}`);
1005
- if (isStaticEnabled()) {
1006
- log.info(`serving static files from ${getStaticDir()}`);
1007
- }
1008
- if (!NO_OPEN && isStaticEnabled()) {
1009
- openBrowser(url);
1010
- }
1011
- }