@dotobokuri/fleet-console 1.23.0 → 1.25.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 (69) hide show
  1. package/AGENTS.md +43 -257
  2. package/README.md +8 -0
  3. package/dist/cli.d.ts +11 -1
  4. package/dist/cli.mjs +4464 -3241
  5. package/dist/client/assets/{_baseUniq-BjOe5yYF.js → _baseUniq-CftO_qma.js} +1 -1
  6. package/dist/client/assets/{arc-bFUuO3jf.js → arc-CIUNumHO.js} +1 -1
  7. package/dist/client/assets/{architectureDiagram-Q4EWVU46-Ba7nimQq.js → architectureDiagram-Q4EWVU46-TpVISJv_.js} +1 -1
  8. package/dist/client/assets/{blockDiagram-DXYQGD6D-CQGf47hl.js → blockDiagram-DXYQGD6D-iHi0B0P6.js} +1 -1
  9. package/dist/client/assets/{c4Diagram-AHTNJAMY-DZF_cAK4.js → c4Diagram-AHTNJAMY-CYAcFDrF.js} +1 -1
  10. package/dist/client/assets/channel-B-2UBgQ_.js +1 -0
  11. package/dist/client/assets/{chunk-4BX2VUAB-DjUUHSQ5.js → chunk-4BX2VUAB-B3Nh-xBT.js} +1 -1
  12. package/dist/client/assets/{chunk-4TB4RGXK-Dvqti8bd.js → chunk-4TB4RGXK-CDbmqVYQ.js} +1 -1
  13. package/dist/client/assets/{chunk-55IACEB6-2BxiBvkp.js → chunk-55IACEB6-DRXIVd03.js} +1 -1
  14. package/dist/client/assets/{chunk-EDXVE4YY-CqCjol5O.js → chunk-EDXVE4YY-b05C0dVN.js} +1 -1
  15. package/dist/client/assets/{chunk-FMBD7UC4-eoCfIbly.js → chunk-FMBD7UC4-ByrEd9CG.js} +1 -1
  16. package/dist/client/assets/{chunk-OYMX7WX6-CoC86V_1.js → chunk-OYMX7WX6-BpEVT-V-.js} +1 -1
  17. package/dist/client/assets/{chunk-QZHKN3VN-CJQA5mlQ.js → chunk-QZHKN3VN-CfdhAAA9.js} +1 -1
  18. package/dist/client/assets/{chunk-YZCP3GAM-C5C4kbLu.js → chunk-YZCP3GAM-Fdx8QRTs.js} +1 -1
  19. package/dist/client/assets/classDiagram-6PBFFD2Q-DiaCSZq-.js +1 -0
  20. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DiaCSZq-.js +1 -0
  21. package/dist/client/assets/clone-Dq4F-r1q.js +1 -0
  22. package/dist/client/assets/{cose-bilkent-S5V4N54A-CahuunpL.js → cose-bilkent-S5V4N54A-D8w9KovE.js} +1 -1
  23. package/dist/client/assets/{dagre-KV5264BT-Cbv5kSi5.js → dagre-KV5264BT-D4fw7rTF.js} +1 -1
  24. package/dist/client/assets/{diagram-5BDNPKRD-CNm40ZGB.js → diagram-5BDNPKRD-BRFcBwDM.js} +1 -1
  25. package/dist/client/assets/{diagram-G4DWMVQ6-DrlGNT3i.js → diagram-G4DWMVQ6-BKzuEau8.js} +1 -1
  26. package/dist/client/assets/{diagram-MMDJMWI5-r8Y2oqmw.js → diagram-MMDJMWI5-DkqP7aZ4.js} +1 -1
  27. package/dist/client/assets/{diagram-TYMM5635-CMzEhXup.js → diagram-TYMM5635-NelXGAN3.js} +1 -1
  28. package/dist/client/assets/{erDiagram-SMLLAGMA-UFjUUMPz.js → erDiagram-SMLLAGMA-DHH5btR9.js} +1 -1
  29. package/dist/client/assets/{flowDiagram-DWJPFMVM-Dd1STmYc.js → flowDiagram-DWJPFMVM-BmqHUJoi.js} +1 -1
  30. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-CHHU0hVd.js → ganttDiagram-T4ZO3ILL-BtmWEvNP.js} +1 -1
  31. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-vj2HukAF.js → gitGraphDiagram-UUTBAWPF-B64kk9Nb.js} +1 -1
  32. package/dist/client/assets/{graph-DhjfEbEb.js → graph-CEjP0hvY.js} +1 -1
  33. package/dist/client/assets/index-B3gs6ohi.js +380 -0
  34. package/dist/client/assets/index-DDfl_vPx.css +1 -0
  35. package/dist/client/assets/{infoDiagram-42DDH7IO-CC7AEgDm.js → infoDiagram-42DDH7IO-CaTXrw5R.js} +1 -1
  36. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-DlRw2B9G.js → ishikawaDiagram-UXIWVN3A-D9-Ds_71.js} +1 -1
  37. package/dist/client/assets/{journeyDiagram-VCZTEJTY-BbRXANKQ.js → journeyDiagram-VCZTEJTY-Cv4qWOhs.js} +1 -1
  38. package/dist/client/assets/{kanban-definition-6JOO6SKY-DtvdCxGN.js → kanban-definition-6JOO6SKY-CWBLcIpD.js} +1 -1
  39. package/dist/client/assets/{layout-Dg5cvw5v.js → layout-D8eNRDnf.js} +1 -1
  40. package/dist/client/assets/{linear-BvVlQLqF.js → linear-lDifS_HV.js} +1 -1
  41. package/dist/client/assets/{mermaid.core-DzrV8sC-.js → mermaid.core-DcGkHMSR.js} +4 -4
  42. package/dist/client/assets/{min-DNGQ1Ciz.js → min-Dz_jFvYt.js} +1 -1
  43. package/dist/client/assets/{mindmap-definition-QFDTVHPH-DxJCNZbg.js → mindmap-definition-QFDTVHPH-F3RmDOG-.js} +1 -1
  44. package/dist/client/assets/{pieDiagram-DEJITSTG-B75XHG-s.js → pieDiagram-DEJITSTG-B1fDKBcl.js} +1 -1
  45. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CL4yoF0m.js → quadrantDiagram-34T5L4WZ-D93hlO3O.js} +1 -1
  46. package/dist/client/assets/{requirementDiagram-MS252O5E-kGV4vcr1.js → requirementDiagram-MS252O5E-B4wj4AQL.js} +1 -1
  47. package/dist/client/assets/{sankeyDiagram-XADWPNL6-DoA4u7wI.js → sankeyDiagram-XADWPNL6-ZRwx77Xd.js} +1 -1
  48. package/dist/client/assets/{sequenceDiagram-FGHM5R23-ZpNDf1bS.js → sequenceDiagram-FGHM5R23-COMMRNyM.js} +1 -1
  49. package/dist/client/assets/{stateDiagram-FHFEXIEX-BzgKcVAG.js → stateDiagram-FHFEXIEX-CpBccMyi.js} +1 -1
  50. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-UCYrbaKP.js +1 -0
  51. package/dist/client/assets/{timeline-definition-GMOUNBTQ-B_DkUDsD.js → timeline-definition-GMOUNBTQ-C44EI_-o.js} +1 -1
  52. package/dist/client/assets/{vennDiagram-DHZGUBPP-MCUnnsKb.js → vennDiagram-DHZGUBPP-BiBJbCdD.js} +1 -1
  53. package/dist/client/assets/{wardley-RL74JXVD-D_JAMSxj.js → wardley-RL74JXVD-Dm5-kWNd.js} +1 -1
  54. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-BHCwZGLq.js → wardleyDiagram-NUSXRM2D-BjbesihY.js} +1 -1
  55. package/dist/client/assets/{xychartDiagram-5P7HB3ND-FbGwbD3V.js → xychartDiagram-5P7HB3ND-5P6xhAMr.js} +1 -1
  56. package/dist/client/index.html +3 -3
  57. package/dist/desktop-protocol.d.ts +50 -0
  58. package/dist/desktop-protocol.mjs +107 -0
  59. package/dist/fleet-plugins/diff/routes.mjs +233 -226
  60. package/dist/fleet-plugins/skills/routes.mjs +218 -82
  61. package/dist/fleet-plugins/terminal/routes.mjs +3541 -3499
  62. package/package.json +7 -2
  63. package/dist/client/assets/channel-D2BahVp1.js +0 -1
  64. package/dist/client/assets/classDiagram-6PBFFD2Q-DsDAVLhS.js +0 -1
  65. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DsDAVLhS.js +0 -1
  66. package/dist/client/assets/clone-CdZy2Xzf.js +0 -1
  67. package/dist/client/assets/index-Dq9EKm2G.css +0 -1
  68. package/dist/client/assets/index-DtzbOdd7.js +0 -380
  69. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-yRK5hzZ4.js +0 -1
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
3
  import 'fs';
4
- import path3 from 'path';
4
+ import path2 from 'path';
5
5
  import fs from 'fs/promises';
6
6
 
7
7
  // sdk/plugin/node.ts
@@ -100,17 +100,22 @@ function runGit(args, opts) {
100
100
  function isPlainObject(value) {
101
101
  return typeof value === "object" && value !== null && !Array.isArray(value);
102
102
  }
103
+ function literalPathspec(relativePath) {
104
+ return `:(literal)${relativePath}`;
105
+ }
103
106
  function normalizeNumstatPath(p) {
104
107
  const match = /^(.*?)\{[^}]* => ([^}]*)\}(.*)$/.exec(p);
105
- if (!match) return p;
106
- return (match[1] ?? "") + (match[2] ?? "") + (match[3] ?? "");
108
+ if (match) return (match[1] ?? "") + (match[2] ?? "") + (match[3] ?? "");
109
+ const plainRename = /^.+ => (.+)$/.exec(p);
110
+ return plainRename?.[1] ?? p;
107
111
  }
108
112
  function parseDiffFileList(nameStatusOutput, numstatOutput) {
109
113
  const numstatMap = /* @__PURE__ */ new Map();
110
114
  for (const line of numstatOutput.split("\n")) {
111
115
  const parts = line.split(" ");
112
116
  if (parts.length < 3) continue;
113
- const [adds, dels, filePath] = parts;
117
+ const [adds, dels] = parts;
118
+ const filePath = parts.length > 3 ? parts[parts.length - 1] : parts[2];
114
119
  if (!filePath) continue;
115
120
  numstatMap.set(normalizeNumstatPath(filePath), {
116
121
  additions: parseInt(adds ?? "0", 10) || 0,
@@ -123,16 +128,17 @@ function parseDiffFileList(nameStatusOutput, numstatOutput) {
123
128
  const [rawStatus, ...pathParts] = line.split(" ");
124
129
  if (!rawStatus || pathParts.length === 0) continue;
125
130
  const statusChar = rawStatus.charAt(0).toUpperCase();
126
- if (statusChar !== "M" && statusChar !== "A" && statusChar !== "D" && statusChar !== "R") continue;
131
+ if (statusChar !== "M" && statusChar !== "A" && statusChar !== "D" && statusChar !== "R" && statusChar !== "T") continue;
132
+ const oldPath = statusChar === "R" ? pathParts[0] : void 0;
127
133
  const filePath = statusChar === "R" ? pathParts[1] ?? pathParts[0] ?? "" : pathParts[0] ?? "";
128
134
  if (!filePath) continue;
129
135
  const nums = numstatMap.get(filePath) ?? { additions: 0, deletions: 0 };
130
- files.push({ path: filePath, status: statusChar, ...nums });
136
+ files.push({ path: filePath, ...oldPath ? { oldPath } : {}, status: statusChar, ...nums });
131
137
  }
132
138
  return files;
133
139
  }
134
140
  async function fetchUntrackedFiles(cwd) {
135
- const result = await runGit(["ls-files", "--others", "--exclude-standard"], { cwd });
141
+ const result = await runGit(["ls-files", "--others", "--exclude-standard", "--", "."], { cwd });
136
142
  return result.stdout.split("\n").filter((p) => p.trim()).map((p) => ({
137
143
  path: p,
138
144
  status: "U",
@@ -140,6 +146,9 @@ async function fetchUntrackedFiles(cwd) {
140
146
  deletions: 0
141
147
  }));
142
148
  }
149
+ async function ensureGitRepository(cwd) {
150
+ await runGit(["rev-parse", "--is-inside-work-tree"], { cwd });
151
+ }
143
152
  function isNoHeadError(error) {
144
153
  if (!(error instanceof GitExecutorError)) return false;
145
154
  if (error.code !== "non_zero_exit") return false;
@@ -147,9 +156,9 @@ function isNoHeadError(error) {
147
156
  }
148
157
  async function resolveGitCwd(theaterPath, rawSubPath) {
149
158
  if (rawSubPath === "") return { gitCwd: theaterPath };
150
- const resolvedSub = path3.resolve(theaterPath, rawSubPath);
151
- if (!resolvedSub.startsWith(theaterPath + path3.sep) && resolvedSub !== theaterPath) return null;
152
- if (path3.normalize(rawSubPath).startsWith("..")) return null;
159
+ const resolvedSub = path2.resolve(theaterPath, rawSubPath);
160
+ if (!resolvedSub.startsWith(theaterPath + path2.sep) && resolvedSub !== theaterPath) return null;
161
+ if (path2.normalize(rawSubPath).startsWith("..")) return null;
153
162
  let realTheater;
154
163
  let realSub;
155
164
  try {
@@ -160,7 +169,7 @@ async function resolveGitCwd(theaterPath, rawSubPath) {
160
169
  } catch {
161
170
  return null;
162
171
  }
163
- const norm = realTheater.endsWith(path3.sep) ? realTheater : realTheater + path3.sep;
172
+ const norm = realTheater.endsWith(path2.sep) ? realTheater : realTheater + path2.sep;
164
173
  if (realSub !== realTheater && !realSub.startsWith(norm)) return null;
165
174
  return { gitCwd: resolvedSub };
166
175
  }
@@ -196,20 +205,21 @@ async function handleDiffChanged(req, res, ctx) {
196
205
  }
197
206
  const { gitCwd } = cwdResult;
198
207
  try {
208
+ await ensureGitRepository(gitCwd);
199
209
  let files;
200
210
  let truncated = false;
201
211
  try {
202
212
  const [nameStatusResult, numstatResult] = await Promise.all([
203
- runGit(["diff", "HEAD", "--name-status", "--diff-filter=MADR"], { cwd: gitCwd }),
204
- runGit(["diff", "HEAD", "--numstat", "--diff-filter=MADR"], { cwd: gitCwd })
213
+ runGit(["diff", "HEAD", "--relative", "--name-status", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd }),
214
+ runGit(["diff", "HEAD", "--relative", "--numstat", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd })
205
215
  ]);
206
216
  files = parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout);
207
217
  truncated = nameStatusResult.truncated || numstatResult.truncated;
208
218
  } catch (err) {
209
219
  if (!isNoHeadError(err)) throw err;
210
220
  const [nsResult, nsNumstat] = await Promise.all([
211
- runGit(["diff", "--cached", "--name-status", "--diff-filter=MADR"], { cwd: gitCwd }),
212
- runGit(["diff", "--cached", "--numstat", "--diff-filter=MADR"], { cwd: gitCwd })
221
+ runGit(["diff", "--cached", "--relative", "--name-status", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd }),
222
+ runGit(["diff", "--cached", "--relative", "--numstat", "--diff-filter=MADRT", "--", "."], { cwd: gitCwd })
213
223
  ]);
214
224
  files = parseDiffFileList(nsResult.stdout, nsNumstat.stdout);
215
225
  truncated = nsResult.truncated || nsNumstat.truncated;
@@ -269,12 +279,12 @@ async function handleDiffFile(req, res, ctx) {
269
279
  }
270
280
  const { gitCwd } = cwdResult;
271
281
  const rawFilePath = body.filePath;
272
- const resolvedFilePath = path3.resolve(gitCwd, rawFilePath);
273
- if (!resolvedFilePath.startsWith(gitCwd + path3.sep) && resolvedFilePath !== gitCwd) {
282
+ const resolvedFilePath = path2.resolve(gitCwd, rawFilePath);
283
+ if (!resolvedFilePath.startsWith(gitCwd + path2.sep) && resolvedFilePath !== gitCwd) {
274
284
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
275
285
  return;
276
286
  }
277
- const relativePath = path3.normalize(rawFilePath);
287
+ const relativePath = path2.normalize(rawFilePath);
278
288
  if (relativePath.startsWith("..")) {
279
289
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
280
290
  return;
@@ -292,12 +302,12 @@ async function handleDiffFile(req, res, ctx) {
292
302
  ctx.host.http.writeJson(res, 404, { error: "file_not_found" });
293
303
  return;
294
304
  }
295
- if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path3.sep)) {
305
+ if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path2.sep)) {
296
306
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
297
307
  return;
298
308
  }
299
309
  const result2 = await runGit(
300
- ["diff", "--no-index", "--unified=3", "--", "/dev/null", relativePath],
310
+ ["diff", "--no-index", "--relative", "--unified=3", "--", "/dev/null", relativePath],
301
311
  { cwd: gitCwd, allowExitCodes: [1] }
302
312
  );
303
313
  ctx.host.http.writeJson(res, 200, { content: result2.stdout, truncated: result2.truncated });
@@ -308,7 +318,7 @@ async function handleDiffFile(req, res, ctx) {
308
318
  fs.realpath(gitCwd),
309
319
  fs.realpath(resolvedFilePath)
310
320
  ]);
311
- if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path3.sep)) {
321
+ if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path2.sep)) {
312
322
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
313
323
  return;
314
324
  }
@@ -316,10 +326,10 @@ async function handleDiffFile(req, res, ctx) {
316
326
  }
317
327
  let result;
318
328
  try {
319
- result = await runGit(["diff", "HEAD", "--unified=3", "--", relativePath], { cwd: gitCwd });
329
+ result = await runGit(["diff", "HEAD", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
320
330
  } catch (err) {
321
331
  if (!isNoHeadError(err)) throw err;
322
- result = await runGit(["diff", "--cached", "--unified=3", "--", relativePath], { cwd: gitCwd });
332
+ result = await runGit(["diff", "--cached", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
323
333
  }
324
334
  ctx.host.http.writeJson(res, 200, { content: result.stdout, truncated: result.truncated });
325
335
  } catch (error) {
@@ -340,6 +350,22 @@ var REF_RE = /^[0-9a-f]{7,40}$/i;
340
350
  function isPlainObject2(value) {
341
351
  return typeof value === "object" && value !== null && !Array.isArray(value);
342
352
  }
353
+ function parseCommitMeta(output) {
354
+ const fields = output.split("\0");
355
+ if (fields.length < 6) return null;
356
+ const [authorName, authorEmail, rawAuthorAt, subject, rawParents, ...bodyParts] = fields;
357
+ if (authorName === void 0 || authorEmail === void 0 || rawAuthorAt === void 0 || subject === void 0 || rawParents === void 0) return null;
358
+ const authorAt = Number.parseInt(rawAuthorAt, 10);
359
+ if (!Number.isFinite(authorAt)) return null;
360
+ return {
361
+ authorName,
362
+ authorEmail,
363
+ authorAt,
364
+ subject,
365
+ body: bodyParts.join("\0").trimEnd(),
366
+ parents: rawParents.split(" ").filter(Boolean).map((full) => ({ full, short: full.slice(0, 9) }))
367
+ };
368
+ }
343
369
  async function handleDiffCommit(req, res, ctx) {
344
370
  if (req.method !== "POST") {
345
371
  ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
@@ -377,8 +403,21 @@ async function handleDiffCommit(req, res, ctx) {
377
403
  }
378
404
  const { gitCwd } = cwdResult;
379
405
  try {
380
- const result = await runGit(["show", ref, "--unified=3", "--format="], { cwd: gitCwd });
381
- ctx.host.http.writeJson(res, 200, { content: result.stdout, ...result.truncated ? { truncated: true } : {} });
406
+ const [metaResult, nameStatusResult, numstatResult] = await Promise.all([
407
+ runGit(["show", "-s", "--format=%an%x00%ae%x00%at%x00%s%x00%P%x00%b", ref], { cwd: gitCwd }),
408
+ runGit(["show", "--first-parent", "--format=", "--relative", "--name-status", "--diff-filter=MADRT", ref, "--", "."], { cwd: gitCwd }),
409
+ runGit(["show", "--first-parent", "--format=", "--relative", "--numstat", "--diff-filter=MADRT", ref, "--", "."], { cwd: gitCwd })
410
+ ]);
411
+ const meta = parseCommitMeta(metaResult.stdout);
412
+ if (!meta) {
413
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
414
+ return;
415
+ }
416
+ ctx.host.http.writeJson(res, 200, {
417
+ meta,
418
+ files: parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout),
419
+ ...metaResult.truncated || nameStatusResult.truncated || numstatResult.truncated ? { truncated: true } : {}
420
+ });
382
421
  } catch (error) {
383
422
  if (error instanceof GitExecutorError) {
384
423
  if (error.code === "no_git_repo" || error.code === "git_unavailable") {
@@ -391,51 +430,19 @@ async function handleDiffCommit(req, res, ctx) {
391
430
  throw error;
392
431
  }
393
432
  }
394
-
395
- // ../fleet-plugins/diff/server/log.ts
396
433
  function isPlainObject3(value) {
397
434
  return typeof value === "object" && value !== null && !Array.isArray(value);
398
435
  }
399
- function parseLogOutput(stdout) {
400
- if (!stdout.trim()) return [];
401
- const commits = [];
402
- for (const chunk of stdout.split("")) {
403
- const trimmed = chunk.trimStart();
404
- if (!trimmed) continue;
405
- const lines = trimmed.split("\n");
406
- const firstLine = lines[0] ?? "";
407
- const fields = firstLine.split("\0");
408
- const fullHash = fields[0] ?? "";
409
- const shortHash = fields[1] ?? "";
410
- const subject = fields[2] ?? "";
411
- const authorName = fields[3] ?? "";
412
- const relTime = fields[4] ?? "";
413
- const refsRaw = fields[5] ?? "";
414
- const parentsRaw = fields[6] ?? "";
415
- if (!fullHash) continue;
416
- const refs = refsRaw.split(",").map((r) => r.trim()).filter(Boolean);
417
- const parents = parentsRaw.split(" ").map((p) => p.trim()).filter(Boolean);
418
- let additions = 0;
419
- let deletions = 0;
420
- for (const line of lines.slice(1)) {
421
- if (!line.trim()) continue;
422
- const parts = line.split(" ");
423
- if (parts.length < 2) continue;
424
- const a = parseInt(parts[0] ?? "0", 10);
425
- const d = parseInt(parts[1] ?? "0", 10);
426
- if (!isNaN(a)) additions += a;
427
- if (!isNaN(d)) deletions += d;
428
- }
429
- commits.push({ shortHash, fullHash, subject, authorName, relTime, refs, parents, additions, deletions });
430
- }
431
- return commits;
436
+ function resolveLiteralFilePath(gitCwd, rawPath) {
437
+ const relativePath = path2.normalize(rawPath);
438
+ const resolved = path2.resolve(gitCwd, relativePath);
439
+ if (relativePath === ".." || relativePath.startsWith(".." + path2.sep) || !resolved.startsWith(gitCwd + path2.sep)) return null;
440
+ return relativePath;
432
441
  }
433
- function isNoHeadError2(error) {
434
- if (!(error instanceof GitExecutorError)) return false;
435
- if (error.code !== "non_zero_exit") return false;
436
- return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision");
442
+ function literalPathspec2(relativePath) {
443
+ return `:(literal)${relativePath}`;
437
444
  }
438
- async function handleDiffLog(req, res, ctx) {
445
+ async function handleDiffCommitFile(req, res, ctx) {
439
446
  if (req.method !== "POST") {
440
447
  ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
441
448
  return;
@@ -445,186 +452,163 @@ async function handleDiffLog(req, res, ctx) {
445
452
  return;
446
453
  }
447
454
  const body = await ctx.host.http.readJsonBody(req);
448
- if (!isPlainObject3(body)) {
455
+ if (!isPlainObject3(body) || typeof body.theaterId !== "string" || typeof body.ref !== "string" || typeof body.filePath !== "string") {
449
456
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
450
457
  return;
451
458
  }
452
- const theaterId = body.theaterId;
453
- if (typeof theaterId !== "string") {
454
- ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
459
+ if (!REF_RE.test(body.ref)) {
460
+ ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
455
461
  return;
456
462
  }
457
- const theaterPath = ctx.host.paths.resolveTheaterPath(theaterId);
463
+ if (!body.filePath) {
464
+ ctx.host.http.writeJson(res, 400, { error: "invalid_file_path" });
465
+ return;
466
+ }
467
+ if (body.oldPath !== void 0 && (typeof body.oldPath !== "string" || !body.oldPath)) {
468
+ ctx.host.http.writeJson(res, 400, { error: "invalid_file_path" });
469
+ return;
470
+ }
471
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
458
472
  if (!theaterPath) {
459
473
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
460
474
  return;
461
475
  }
462
- const rawSubPath = typeof body.subPath === "string" ? body.subPath : "";
463
- const cwdResult = await resolveGitCwd(theaterPath, rawSubPath);
476
+ const cwdResult = await resolveGitCwd(theaterPath, typeof body.subPath === "string" ? body.subPath : "");
464
477
  if (!cwdResult) {
465
478
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
466
479
  return;
467
480
  }
468
- const { gitCwd } = cwdResult;
481
+ const relativePath = resolveLiteralFilePath(cwdResult.gitCwd, body.filePath);
482
+ const oldPath = typeof body.oldPath === "string" ? resolveLiteralFilePath(cwdResult.gitCwd, body.oldPath) : void 0;
483
+ if (!relativePath || body.oldPath !== void 0 && !oldPath) {
484
+ ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
485
+ return;
486
+ }
469
487
  try {
470
- const result = await runGit(
471
- ["log", "-n", "50", "--numstat", "--pretty=format:%x1e%H%x00%h%x00%s%x00%an%x00%ar%x00%D%x00%P"],
472
- { cwd: gitCwd }
473
- );
474
- const commits = parseLogOutput(result.stdout);
475
- ctx.host.http.writeJson(res, 200, { commits, ...result.truncated ? { truncated: true } : {} });
488
+ const result = await runGit(oldPath ? ["show", "--first-parent", body.ref, "-M", "--format=", "--unified=3", "--", literalPathspec2(oldPath), literalPathspec2(relativePath)] : ["show", "--first-parent", body.ref, "--format=", "--unified=3", "--", literalPathspec2(relativePath)], { cwd: cwdResult.gitCwd });
489
+ ctx.host.http.writeJson(res, 200, { content: result.stdout, ...result.truncated ? { truncated: true } : {} });
476
490
  } catch (error) {
477
491
  if (error instanceof GitExecutorError) {
478
- if (error.code === "no_git_repo") {
479
- ctx.host.http.writeJson(res, 200, { commits: [] });
480
- return;
481
- }
482
- if (error.code === "git_unavailable") {
492
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
483
493
  ctx.host.http.writeJson(res, 422, { error: error.code });
484
494
  return;
485
495
  }
486
- if (isNoHeadError2(error)) {
487
- ctx.host.http.writeJson(res, 200, { commits: [] });
488
- return;
489
- }
490
496
  ctx.host.http.writeJson(res, 500, { error: "git_failed" });
491
497
  return;
492
498
  }
493
499
  throw error;
494
500
  }
495
501
  }
496
- var REPOS_CAP = 200;
497
- var HARD_CAP_DEPTH = 8;
498
- var DEFAULT_DEPTH = 3;
502
+ var WORKTREE_SHA_RE = /^[0-9a-f]{40}$/;
499
503
  function isPlainObject4(value) {
500
504
  return typeof value === "object" && value !== null && !Array.isArray(value);
501
505
  }
502
- async function resolveRepoBranch(repoDir) {
503
- try {
504
- const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoDir });
505
- const branch = result.stdout.trim();
506
- if (branch === "HEAD") {
507
- const sha = await runGit(["rev-parse", "--short", "HEAD"], { cwd: repoDir });
508
- return sha.stdout.trim() || "HEAD";
509
- }
510
- return branch || "HEAD";
511
- } catch {
512
- return "unknown";
506
+ function parseLogOutput(stdout) {
507
+ if (!stdout.trim()) return [];
508
+ const commits = [];
509
+ for (const chunk of stdout.split("")) {
510
+ const trimmed = chunk.trimStart();
511
+ if (!trimmed) continue;
512
+ const lines = trimmed.split("\n");
513
+ const firstLine = lines[0] ?? "";
514
+ const fields = firstLine.split("\0");
515
+ const fullHash = fields[0] ?? "";
516
+ const shortHash = fields[1] ?? "";
517
+ const subject = fields[2] ?? "";
518
+ const authorName = fields[3] ?? "";
519
+ const relTime = fields[4] ?? "";
520
+ const authorAt = Number.parseInt(fields[5] ?? "0", 10);
521
+ const refsRaw = fields[6] ?? "";
522
+ const parentsRaw = fields[7] ?? "";
523
+ if (!fullHash) continue;
524
+ const refs = refsRaw.split(",").map((r) => r.trim()).filter(Boolean);
525
+ const parents = parentsRaw.split(" ").map((p) => p.trim()).filter(Boolean);
526
+ commits.push({
527
+ shortHash,
528
+ fullHash,
529
+ subject,
530
+ authorName,
531
+ relTime,
532
+ authorAt: Number.isFinite(authorAt) ? authorAt : 0,
533
+ refs,
534
+ parents,
535
+ onHead: true
536
+ });
513
537
  }
538
+ return commits;
514
539
  }
515
- async function detectWorktreeParent(dir, gitFileContent) {
516
- const match = gitFileContent.trim().match(/^gitdir:\s*(.+)$/m);
517
- if (!match) return null;
518
- const rawPointer = match[1].trim();
519
- const resolvedGitdir = path3.normalize(
520
- path3.isAbsolute(rawPointer) ? rawPointer : path3.resolve(dir, rawPointer)
521
- );
522
- if (resolvedGitdir.includes(`${path3.sep}modules${path3.sep}`)) {
523
- return null;
524
- }
525
- const worktreesSegment = `${path3.sep}.git${path3.sep}worktrees${path3.sep}`;
526
- const wtIdx = resolvedGitdir.indexOf(worktreesSegment);
527
- if (wtIdx === -1) {
528
- return null;
540
+ async function parseWorktreePorcelain(stdout, currentWorktreePath) {
541
+ const worktrees = [];
542
+ let worktreePath = null;
543
+ let sha = "";
544
+ let branch = null;
545
+ let prunable = false;
546
+ const pushCurrent = () => {
547
+ if (!worktreePath || !sha || prunable || /^0+$/.test(sha)) return;
548
+ worktrees.push({
549
+ sha,
550
+ branch,
551
+ worktreePath
552
+ });
553
+ };
554
+ for (const line of stdout.split("\n")) {
555
+ if (line.startsWith("worktree ")) {
556
+ pushCurrent();
557
+ worktreePath = line.slice(9);
558
+ sha = "";
559
+ branch = null;
560
+ prunable = false;
561
+ } else if (line.startsWith("HEAD ")) {
562
+ sha = line.slice(5);
563
+ } else if (line.startsWith("branch ")) {
564
+ const ref = line.slice(7);
565
+ branch = ref.startsWith("refs/heads/") ? ref.slice(11) : ref;
566
+ } else if (line === "prunable" || line.startsWith("prunable ")) {
567
+ prunable = true;
568
+ }
529
569
  }
530
- const parentAbsRaw = resolvedGitdir.slice(0, wtIdx);
531
- let parentAbs;
570
+ pushCurrent();
571
+ const normalizedCurrentWorktreePath = await normalizeWorktreePath(currentWorktreePath);
572
+ return Promise.all(worktrees.map(async ({ worktreePath: worktreePath2, ...checkout }) => ({
573
+ ...checkout,
574
+ isCurrent: normalizedCurrentWorktreePath !== "" && await normalizeWorktreePath(worktreePath2) === normalizedCurrentWorktreePath
575
+ })));
576
+ }
577
+ function annotateHeadReachability(commits, revListStdout) {
578
+ const reachable = new Set(revListStdout.split("\n").map((line) => line.trim()).filter(Boolean));
579
+ if (reachable.size === 0) return commits;
580
+ return commits.map((commit) => ({ ...commit, onHead: reachable.has(commit.fullHash) }));
581
+ }
582
+ function isNoHeadError2(error) {
583
+ if (!(error instanceof GitExecutorError)) return false;
584
+ if (error.code !== "non_zero_exit") return false;
585
+ return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision") || error.stderr.includes("does not have any commits");
586
+ }
587
+ async function normalizeWorktreePath(worktreePath) {
588
+ if (!worktreePath) return "";
532
589
  try {
533
- parentAbs = await fs.realpath(parentAbsRaw);
590
+ return await fs.realpath(worktreePath);
534
591
  } catch {
535
- parentAbs = path3.normalize(parentAbsRaw);
592
+ return worktreePath;
536
593
  }
537
- return { isWorktree: true, parentAbs };
538
594
  }
539
- async function scanRepos(theaterPath, realTheaterPath, dir, currentDepth, maxDepth, repos, cap = REPOS_CAP) {
540
- if (repos.length >= cap) return true;
541
- const gitPath = path3.join(dir, ".git");
595
+ async function readHeadRevList(gitCwd) {
542
596
  try {
543
- const gitStat = await fs.stat(gitPath);
544
- const relPath = path3.relative(theaterPath, dir);
545
- const name = relPath === "" ? path3.basename(theaterPath) : path3.basename(dir);
546
- const branch = await resolveRepoBranch(dir);
547
- if (gitStat.isFile()) {
548
- const content = await fs.readFile(gitPath, "utf8");
549
- const worktreeResult = await detectWorktreeParent(dir, content);
550
- if (worktreeResult) {
551
- repos.push({ relPath, name, branch, isWorktree: true, _wtParentAbs: worktreeResult.parentAbs });
552
- } else {
553
- repos.push({ relPath, name, branch });
554
- }
555
- } else {
556
- repos.push({ relPath, name, branch });
557
- }
558
- if (repos.length >= cap) return true;
559
- } catch {
597
+ return (await runGit(["rev-list", "-n", "1000", "HEAD"], { cwd: gitCwd })).stdout;
598
+ } catch (error) {
599
+ if (error instanceof GitExecutorError) return "";
600
+ throw error;
560
601
  }
561
- if (currentDepth >= maxDepth) return false;
562
- let dirHandle = null;
602
+ }
603
+ async function readCurrentWorktreePath(gitCwd) {
563
604
  try {
564
- dirHandle = await fs.opendir(dir);
565
- try {
566
- let dirent = await dirHandle.read();
567
- while (dirent !== null) {
568
- if (repos.length >= cap) return true;
569
- if (dirent.isDirectory()) {
570
- const childName = dirent.name;
571
- if (childName === "node_modules" || childName === ".git") {
572
- dirent = await dirHandle.read();
573
- continue;
574
- }
575
- const childPath = path3.join(dir, childName);
576
- let realChildPath;
577
- try {
578
- realChildPath = await fs.realpath(childPath);
579
- } catch {
580
- dirent = await dirHandle.read();
581
- continue;
582
- }
583
- const normalizedReal = realTheaterPath.endsWith(path3.sep) ? realTheaterPath : realTheaterPath + path3.sep;
584
- if (realChildPath !== realTheaterPath && !realChildPath.startsWith(normalizedReal)) {
585
- dirent = await dirHandle.read();
586
- continue;
587
- }
588
- const truncated = await scanRepos(theaterPath, realTheaterPath, childPath, currentDepth + 1, maxDepth, repos, cap);
589
- if (truncated) return true;
590
- }
591
- dirent = await dirHandle.read();
592
- }
593
- return false;
594
- } finally {
595
- await dirHandle.close();
596
- }
597
- } catch {
598
- if (dirHandle) {
599
- try {
600
- await dirHandle.close();
601
- } catch {
602
- }
603
- }
604
- return false;
605
+ return (await runGit(["rev-parse", "--show-toplevel"], { cwd: gitCwd })).stdout.trim();
606
+ } catch (error) {
607
+ if (error instanceof GitExecutorError) return "";
608
+ throw error;
605
609
  }
606
610
  }
607
- function resolveWorktreeParents(raw, realTheaterPath) {
608
- const absToRelPath = /* @__PURE__ */ new Map();
609
- for (const entry of raw) {
610
- const absDir = path3.normalize(
611
- entry.relPath === "" ? realTheaterPath : path3.join(realTheaterPath, entry.relPath)
612
- );
613
- absToRelPath.set(absDir, entry.relPath);
614
- }
615
- return raw.map((entry) => {
616
- const { _wtParentAbs, ...rest } = entry;
617
- if (_wtParentAbs && entry.isWorktree) {
618
- const normalizedParent = path3.normalize(_wtParentAbs);
619
- const parentRelPath = absToRelPath.get(normalizedParent);
620
- if (parentRelPath !== void 0) {
621
- return { ...rest, worktreeOf: parentRelPath };
622
- }
623
- }
624
- return rest;
625
- });
626
- }
627
- async function handleDiffRepos(req, res, ctx) {
611
+ async function handleDiffLog(req, res, ctx) {
628
612
  if (req.method !== "POST") {
629
613
  ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
630
614
  return;
@@ -648,35 +632,54 @@ async function handleDiffRepos(req, res, ctx) {
648
632
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
649
633
  return;
650
634
  }
651
- const rawDepth = body.maxDepth;
652
- const maxDepth = typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 1 ? Math.min(HARD_CAP_DEPTH, Math.floor(rawDepth)) : DEFAULT_DEPTH;
653
- let realTheaterPath;
654
- try {
655
- realTheaterPath = await fs.realpath(theaterPath);
656
- } catch {
657
- ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
635
+ const rawSubPath = typeof body.subPath === "string" ? body.subPath : "";
636
+ const cwdResult = await resolveGitCwd(theaterPath, rawSubPath);
637
+ if (!cwdResult) {
638
+ ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
658
639
  return;
659
640
  }
660
- const raw = [];
661
- const truncated = await scanRepos(theaterPath, realTheaterPath, theaterPath, 0, maxDepth, raw);
662
- const resolved = resolveWorktreeParents(raw, realTheaterPath);
663
- resolved.sort((a, b) => {
664
- if (a.relPath === "") return -1;
665
- if (b.relPath === "") return 1;
666
- return a.relPath.localeCompare(b.relPath);
667
- });
668
- const result = { repos: resolved, ...truncated ? { truncated: true } : {} };
669
- ctx.host.http.writeJson(res, 200, result);
641
+ const { gitCwd } = cwdResult;
642
+ try {
643
+ const [worktrees, currentWorktreePath, headRevList] = await Promise.all([
644
+ runGit(["worktree", "list", "--porcelain"], { cwd: gitCwd }),
645
+ readCurrentWorktreePath(gitCwd),
646
+ readHeadRevList(gitCwd)
647
+ ]);
648
+ const checkouts = await parseWorktreePorcelain(worktrees.stdout, currentWorktreePath);
649
+ const worktreeRevs = [...new Set(checkouts.map((checkout) => checkout.sha))].filter((sha) => WORKTREE_SHA_RE.test(sha) && !/^0+$/.test(sha));
650
+ const headRevs = headRevList ? ["HEAD"] : [];
651
+ const result = await runGit(
652
+ // --all은 refs/stash·refs/notes까지 그래프에 유입시키므로 브랜치/태그/원격 + 현재 HEAD + 워크트리 HEAD로 한정한다
653
+ ["log", "--branches", "--tags", "--remotes", "--date-order", "-n", "200", "--relative", "--decorate=full", "--pretty=format:%x1e%H%x00%h%x00%s%x00%an%x00%ar%x00%at%x00%D%x00%P", ...headRevs, ...worktreeRevs, "--", "."],
654
+ { cwd: gitCwd }
655
+ );
656
+ const commits = annotateHeadReachability(parseLogOutput(result.stdout), headRevList);
657
+ ctx.host.http.writeJson(res, 200, { commits, checkouts, ...result.truncated ? { truncated: true } : {} });
658
+ } catch (error) {
659
+ if (error instanceof GitExecutorError) {
660
+ if (error.code === "no_git_repo") {
661
+ ctx.host.http.writeJson(res, 200, { commits: [], checkouts: [] });
662
+ return;
663
+ }
664
+ if (error.code === "git_unavailable") {
665
+ ctx.host.http.writeJson(res, 422, { error: error.code });
666
+ return;
667
+ }
668
+ if (isNoHeadError2(error)) {
669
+ ctx.host.http.writeJson(res, 200, { commits: [], checkouts: [] });
670
+ return;
671
+ }
672
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
673
+ return;
674
+ }
675
+ throw error;
676
+ }
670
677
  }
671
678
 
672
679
  // ../fleet-plugins/diff/routes.ts
673
680
  var routes_default = definePlugin({
674
681
  id: "diff",
675
682
  register(ctx) {
676
- registerRouter(ctx, "repos", async ({ req, res }) => {
677
- await handleDiffRepos(req, res, ctx);
678
- return true;
679
- });
680
683
  registerRouter(ctx, "changed", async ({ req, res }) => {
681
684
  await handleDiffChanged(req, res, ctx);
682
685
  return true;
@@ -693,6 +696,10 @@ var routes_default = definePlugin({
693
696
  await handleDiffCommit(req, res, ctx);
694
697
  return true;
695
698
  });
699
+ registerRouter(ctx, "commit-file", async ({ req, res }) => {
700
+ await handleDiffCommitFile(req, res, ctx);
701
+ return true;
702
+ });
696
703
  }
697
704
  });
698
705