@dotobokuri/fleet-console 1.29.0 → 1.31.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 (65) hide show
  1. package/dist/cli.mjs +116 -73
  2. package/dist/client/assets/{_baseUniq-DEvADUqz.js → _baseUniq-6rANA-_q.js} +1 -1
  3. package/dist/client/assets/{arc-pB9oYw5d.js → arc-tzTBQuJE.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-Q4EWVU46-D0gUBsh1.js → architectureDiagram-Q4EWVU46-B4MzTga_.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-DXYQGD6D-9NSt9xrC.js → blockDiagram-DXYQGD6D-BSkil4Kq.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AHTNJAMY-BS1EhMAN.js → c4Diagram-AHTNJAMY-Dwq5nIR_.js} +1 -1
  7. package/dist/client/assets/channel-DGrgEBwr.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-BKkeF7mF.js → chunk-4BX2VUAB-BGefUfmP.js} +1 -1
  9. package/dist/client/assets/{chunk-4TB4RGXK-DRp3Dsap.js → chunk-4TB4RGXK-DBUtBigu.js} +1 -1
  10. package/dist/client/assets/{chunk-55IACEB6-De-sLT-H.js → chunk-55IACEB6-BLF4uUa8.js} +1 -1
  11. package/dist/client/assets/{chunk-EDXVE4YY-ByYWvw6c.js → chunk-EDXVE4YY-Cc41I6y_.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-BDpIWnNm.js → chunk-FMBD7UC4-C8PUZ57p.js} +1 -1
  13. package/dist/client/assets/{chunk-OYMX7WX6-qxbtvneQ.js → chunk-OYMX7WX6-CYos7ODp.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-BnkLN840.js → chunk-QZHKN3VN-I4gCcbsX.js} +1 -1
  15. package/dist/client/assets/{chunk-YZCP3GAM-HfizEXdz.js → chunk-YZCP3GAM-CO0vuwK9.js} +1 -1
  16. package/dist/client/assets/classDiagram-6PBFFD2Q-CKhx9oUH.js +1 -0
  17. package/dist/client/assets/classDiagram-v2-HSJHXN6E-CKhx9oUH.js +1 -0
  18. package/dist/client/assets/clone-BODjA13N.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-S5V4N54A-B4U_tiL9.js → cose-bilkent-S5V4N54A-BjXxkeBn.js} +1 -1
  20. package/dist/client/assets/{dagre-KV5264BT-De3dSv-R.js → dagre-KV5264BT-DKVbTDmI.js} +1 -1
  21. package/dist/client/assets/{diagram-5BDNPKRD-CZAF0Kro.js → diagram-5BDNPKRD-C1as30xl.js} +1 -1
  22. package/dist/client/assets/{diagram-G4DWMVQ6-BHyDlHAq.js → diagram-G4DWMVQ6-B_G7-yDL.js} +1 -1
  23. package/dist/client/assets/{diagram-MMDJMWI5-CV93x0hu.js → diagram-MMDJMWI5-B2srczqP.js} +1 -1
  24. package/dist/client/assets/{diagram-TYMM5635-CTRzVEQp.js → diagram-TYMM5635-pYQydnR9.js} +1 -1
  25. package/dist/client/assets/{erDiagram-SMLLAGMA-xW-UvBlm.js → erDiagram-SMLLAGMA-AcmG-sIC.js} +1 -1
  26. package/dist/client/assets/{flowDiagram-DWJPFMVM-C21IECUy.js → flowDiagram-DWJPFMVM-BRvh-u54.js} +1 -1
  27. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-DVzOf8Xv.js → ganttDiagram-T4ZO3ILL-CuyxaGr-.js} +1 -1
  28. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-B-NQC3Ae.js → gitGraphDiagram-UUTBAWPF-BWFXSUkW.js} +1 -1
  29. package/dist/client/assets/{graph-BDzdWbuy.js → graph-BFw4pMat.js} +1 -1
  30. package/dist/client/assets/index-DD3oRP6t.css +1 -0
  31. package/dist/client/assets/index-qutkoA_F.js +451 -0
  32. package/dist/client/assets/{infoDiagram-42DDH7IO-2KCXFMZg.js → infoDiagram-42DDH7IO-CgrEFkpl.js} +1 -1
  33. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-D4Fvy3ms.js → ishikawaDiagram-UXIWVN3A-C1PYBJVn.js} +1 -1
  34. package/dist/client/assets/{journeyDiagram-VCZTEJTY-DkXZkeED.js → journeyDiagram-VCZTEJTY-BHC_QZyQ.js} +1 -1
  35. package/dist/client/assets/{kanban-definition-6JOO6SKY-CTrIp4rv.js → kanban-definition-6JOO6SKY-BTQZjl-H.js} +1 -1
  36. package/dist/client/assets/{layout-DKdT-A2u.js → layout-LyiAOink.js} +1 -1
  37. package/dist/client/assets/{linear-Dn21C3ab.js → linear-BWduiQJJ.js} +1 -1
  38. package/dist/client/assets/{mermaid.core-ho7gOyUr.js → mermaid.core-B2Lf_dRe.js} +4 -4
  39. package/dist/client/assets/{min-DV-zxfLU.js → min-BS4WshjZ.js} +1 -1
  40. package/dist/client/assets/{mindmap-definition-QFDTVHPH-By8nfvJ0.js → mindmap-definition-QFDTVHPH-uBQw-Rcf.js} +1 -1
  41. package/dist/client/assets/{pieDiagram-DEJITSTG-B4ndzH1J.js → pieDiagram-DEJITSTG-Daco6XeA.js} +1 -1
  42. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CiDb6b3-.js → quadrantDiagram-34T5L4WZ-phmU55Tk.js} +1 -1
  43. package/dist/client/assets/{requirementDiagram-MS252O5E-TTr1SzvX.js → requirementDiagram-MS252O5E-D7WmHJB1.js} +1 -1
  44. package/dist/client/assets/{sankeyDiagram-XADWPNL6-CUyxk-WA.js → sankeyDiagram-XADWPNL6-QmItdHVw.js} +1 -1
  45. package/dist/client/assets/{sequenceDiagram-FGHM5R23-CyBxBNAZ.js → sequenceDiagram-FGHM5R23-B-Wgp5TP.js} +1 -1
  46. package/dist/client/assets/{stateDiagram-FHFEXIEX-Cj4j5FFK.js → stateDiagram-FHFEXIEX-J7H1J8u1.js} +1 -1
  47. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-D0qZOg43.js +1 -0
  48. package/dist/client/assets/{timeline-definition-GMOUNBTQ-MQzKGbxg.js → timeline-definition-GMOUNBTQ-BI_kLbxz.js} +1 -1
  49. package/dist/client/assets/{vennDiagram-DHZGUBPP-sG3K7NAZ.js → vennDiagram-DHZGUBPP-Do5nm7Eq.js} +1 -1
  50. package/dist/client/assets/{wardley-RL74JXVD-ID5rQjqH.js → wardley-RL74JXVD-DvA1fdMU.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-AMajCCI5.js → wardleyDiagram-NUSXRM2D-BrigyBQU.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-5P7HB3ND-DUb36W9E.js → xychartDiagram-5P7HB3ND-B0Inv_Uf.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/fleet-plugins/file-explorer/routes.mjs +199 -38
  55. package/dist/fleet-plugins/repository/routes.mjs +612 -33
  56. package/dist/fleet-plugins/skills/routes.mjs +43 -16
  57. package/dist/fleet-plugins/terminal/routes.mjs +308 -148
  58. package/package.json +1 -1
  59. package/dist/client/assets/channel-BSgCfWXO.js +0 -1
  60. package/dist/client/assets/classDiagram-6PBFFD2Q-C3WhxxfZ.js +0 -1
  61. package/dist/client/assets/classDiagram-v2-HSJHXN6E-C3WhxxfZ.js +0 -1
  62. package/dist/client/assets/clone-DjOK69th.js +0 -1
  63. package/dist/client/assets/index-BDJVPQd1.css +0 -1
  64. package/dist/client/assets/index-CjBu8lNT.js +0 -451
  65. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-CWhJmDZj.js +0 -1
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawn } from 'child_process';
3
3
  import 'fs';
4
- import path2 from 'path';
5
- import fs from 'fs/promises';
4
+ import path4 from 'path';
5
+ import fs2 from 'fs/promises';
6
6
 
7
7
  // sdk/plugin/node.ts
8
8
  function definePlugin(definition) {
9
9
  return definition;
10
10
  }
11
- function registerRouter(ctx, path4, handler) {
12
- ctx.registerRouter(normalizePath(path4), handler);
11
+ function registerRouter(ctx, path9, handler) {
12
+ ctx.registerRouter(normalizePath(path9), handler);
13
13
  }
14
- function normalizePath(path4) {
15
- if (path4.length === 0 || path4 === "/") return "/";
16
- return path4;
14
+ function normalizePath(path9) {
15
+ if (path9.length === 0 || path9 === "/") return "/";
16
+ return path9;
17
17
  }
18
18
 
19
19
  // ../../packages/core-process/src/hidden.ts
@@ -97,6 +97,72 @@ function runGit(args, opts) {
97
97
  });
98
98
  });
99
99
  }
100
+ function isPathContained(root, candidate) {
101
+ const relative = path4.relative(root, candidate);
102
+ return relative === "" || !path4.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path4.sep}`);
103
+ }
104
+ function isSelectableRepoRel(repoRel) {
105
+ if (repoRel === "") return true;
106
+ if (path4.isAbsolute(repoRel)) return false;
107
+ const normalized = path4.normalize(repoRel);
108
+ return !normalized.startsWith("-") && normalized !== ".." && !normalized.startsWith(`..${path4.sep}`);
109
+ }
110
+
111
+ // ../fleet-plugins/repository/server/git-marker.ts
112
+ var GITDIR_PREFIX = "gitdir:";
113
+ async function resolveContainedGitDir(repoDir, realTheaterPath) {
114
+ const markerPath = path4.join(repoDir, ".git");
115
+ let marker;
116
+ try {
117
+ marker = await fs2.stat(markerPath);
118
+ } catch {
119
+ return null;
120
+ }
121
+ let gitDir;
122
+ if (marker.isFile()) {
123
+ let content;
124
+ try {
125
+ content = await fs2.readFile(markerPath, "utf8");
126
+ } catch {
127
+ return null;
128
+ }
129
+ const line = content.split("\n").find((entry) => entry.trim().startsWith(GITDIR_PREFIX));
130
+ if (!line) return null;
131
+ const target = line.trim().slice(GITDIR_PREFIX.length).trim();
132
+ if (!target) return null;
133
+ gitDir = path4.resolve(repoDir, target);
134
+ } else if (marker.isDirectory()) {
135
+ gitDir = markerPath;
136
+ } else {
137
+ return null;
138
+ }
139
+ let realGitDir;
140
+ try {
141
+ realGitDir = await fs2.realpath(gitDir);
142
+ } catch {
143
+ return null;
144
+ }
145
+ if (!isPathContained(realTheaterPath, realGitDir)) return null;
146
+ let commonRaw = null;
147
+ try {
148
+ commonRaw = await fs2.readFile(path4.join(realGitDir, "commondir"), "utf8");
149
+ } catch {
150
+ }
151
+ if (commonRaw !== null) {
152
+ const commonTarget = commonRaw.trim();
153
+ if (!commonTarget) return null;
154
+ let realCommonDir;
155
+ try {
156
+ realCommonDir = await fs2.realpath(path4.resolve(realGitDir, commonTarget));
157
+ } catch {
158
+ return null;
159
+ }
160
+ if (!isPathContained(realTheaterPath, realCommonDir)) return null;
161
+ }
162
+ return realGitDir;
163
+ }
164
+
165
+ // ../fleet-plugins/repository/server/diff.ts
100
166
  function isPlainObject(value) {
101
167
  return typeof value === "object" && value !== null && !Array.isArray(value);
102
168
  }
@@ -154,8 +220,29 @@ function isNoHeadError(error) {
154
220
  if (error.code !== "non_zero_exit") return false;
155
221
  return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision");
156
222
  }
157
- function resolveGitCwd(theaterPath) {
158
- return { gitCwd: theaterPath };
223
+ var InvalidRepoError = class extends Error {
224
+ code = "invalid_repo";
225
+ };
226
+ async function resolveGitCwd(theaterPath, repoRel = "") {
227
+ if (repoRel === void 0 || repoRel === "") return { gitCwd: theaterPath };
228
+ if (typeof repoRel !== "string" || path4.isAbsolute(repoRel)) throw new InvalidRepoError("Repository path must be relative");
229
+ if (!isSelectableRepoRel(repoRel)) throw new InvalidRepoError("Repository path is invalid");
230
+ const resolved = path4.resolve(theaterPath, path4.normalize(repoRel));
231
+ if (!isPathContained(theaterPath, resolved)) throw new InvalidRepoError("Repository path escapes Theater");
232
+ let realTheater;
233
+ let realRepo;
234
+ try {
235
+ [realTheater, realRepo] = await Promise.all([fs2.realpath(theaterPath), fs2.realpath(resolved)]);
236
+ } catch {
237
+ throw new InvalidRepoError("Repository path does not exist");
238
+ }
239
+ if (!isPathContained(realTheater, realRepo)) {
240
+ throw new InvalidRepoError("Repository path escapes Theater");
241
+ }
242
+ if (await resolveContainedGitDir(realRepo, realTheater) === null) {
243
+ throw new InvalidRepoError("Repository marker does not exist");
244
+ }
245
+ return { gitCwd: realRepo };
159
246
  }
160
247
  async function handleRepositoryChanged(req, res, ctx) {
161
248
  if (req.method !== "POST") {
@@ -181,7 +268,16 @@ async function handleRepositoryChanged(req, res, ctx) {
181
268
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
182
269
  return;
183
270
  }
184
- const cwdResult = resolveGitCwd(theaterPath);
271
+ let cwdResult;
272
+ try {
273
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
274
+ } catch (error) {
275
+ if (error instanceof InvalidRepoError) {
276
+ ctx.host.http.writeJson(res, 400, { error: error.code });
277
+ return;
278
+ }
279
+ throw error;
280
+ }
185
281
  const { gitCwd } = cwdResult;
186
282
  try {
187
283
  await ensureGitRepository(gitCwd);
@@ -250,15 +346,24 @@ async function handleRepositoryFile(req, res, ctx) {
250
346
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
251
347
  return;
252
348
  }
253
- const cwdResult = resolveGitCwd(theaterPath);
349
+ let cwdResult;
350
+ try {
351
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
352
+ } catch (error) {
353
+ if (error instanceof InvalidRepoError) {
354
+ ctx.host.http.writeJson(res, 400, { error: error.code });
355
+ return;
356
+ }
357
+ throw error;
358
+ }
254
359
  const { gitCwd } = cwdResult;
255
360
  const rawFilePath = body.filePath;
256
- const resolvedFilePath = path2.resolve(gitCwd, rawFilePath);
257
- if (!resolvedFilePath.startsWith(gitCwd + path2.sep) && resolvedFilePath !== gitCwd) {
361
+ const resolvedFilePath = path4.resolve(gitCwd, rawFilePath);
362
+ if (!resolvedFilePath.startsWith(gitCwd + path4.sep) && resolvedFilePath !== gitCwd) {
258
363
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
259
364
  return;
260
365
  }
261
- const relativePath = path2.normalize(rawFilePath);
366
+ const relativePath = path4.normalize(rawFilePath);
262
367
  if (relativePath.startsWith("..")) {
263
368
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
264
369
  return;
@@ -269,14 +374,14 @@ async function handleRepositoryFile(req, res, ctx) {
269
374
  let realFile;
270
375
  try {
271
376
  [realGitCwd, realFile] = await Promise.all([
272
- fs.realpath(gitCwd),
273
- fs.realpath(resolvedFilePath)
377
+ fs2.realpath(gitCwd),
378
+ fs2.realpath(resolvedFilePath)
274
379
  ]);
275
380
  } catch {
276
381
  ctx.host.http.writeJson(res, 404, { error: "file_not_found" });
277
382
  return;
278
383
  }
279
- if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path2.sep)) {
384
+ if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path4.sep)) {
280
385
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
281
386
  return;
282
387
  }
@@ -289,10 +394,10 @@ async function handleRepositoryFile(req, res, ctx) {
289
394
  }
290
395
  try {
291
396
  const [realGitCwd, realFile] = await Promise.all([
292
- fs.realpath(gitCwd),
293
- fs.realpath(resolvedFilePath)
397
+ fs2.realpath(gitCwd),
398
+ fs2.realpath(resolvedFilePath)
294
399
  ]);
295
- if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path2.sep)) {
400
+ if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path4.sep)) {
296
401
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
297
402
  return;
298
403
  }
@@ -369,7 +474,16 @@ async function handleRepositoryCommit(req, res, ctx) {
369
474
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
370
475
  return;
371
476
  }
372
- const cwdResult = resolveGitCwd(theaterPath);
477
+ let cwdResult;
478
+ try {
479
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
480
+ } catch (error) {
481
+ if (error instanceof InvalidRepoError) {
482
+ ctx.host.http.writeJson(res, 400, { error: error.code });
483
+ return;
484
+ }
485
+ throw error;
486
+ }
373
487
  const { gitCwd } = cwdResult;
374
488
  try {
375
489
  const [metaResult, nameStatusResult, numstatResult] = await Promise.all([
@@ -403,9 +517,9 @@ function isPlainObject3(value) {
403
517
  return typeof value === "object" && value !== null && !Array.isArray(value);
404
518
  }
405
519
  function resolveLiteralFilePath(gitCwd, rawPath) {
406
- const relativePath = path2.normalize(rawPath);
407
- const resolved = path2.resolve(gitCwd, relativePath);
408
- if (relativePath === ".." || relativePath.startsWith(".." + path2.sep) || !resolved.startsWith(gitCwd + path2.sep)) return null;
520
+ const relativePath = path4.normalize(rawPath);
521
+ const resolved = path4.resolve(gitCwd, relativePath);
522
+ if (relativePath === ".." || relativePath.startsWith(".." + path4.sep) || !resolved.startsWith(gitCwd + path4.sep)) return null;
409
523
  return relativePath;
410
524
  }
411
525
  function literalPathspec2(relativePath) {
@@ -442,7 +556,16 @@ async function handleRepositoryCommitFile(req, res, ctx) {
442
556
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
443
557
  return;
444
558
  }
445
- const cwdResult = resolveGitCwd(theaterPath);
559
+ let cwdResult;
560
+ try {
561
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
562
+ } catch (error) {
563
+ if (error instanceof InvalidRepoError) {
564
+ ctx.host.http.writeJson(res, 400, { error: error.code });
565
+ return;
566
+ }
567
+ throw error;
568
+ }
446
569
  const relativePath = resolveLiteralFilePath(cwdResult.gitCwd, body.filePath);
447
570
  const oldPath = typeof body.oldPath === "string" ? resolveLiteralFilePath(cwdResult.gitCwd, body.oldPath) : void 0;
448
571
  if (!relativePath || body.oldPath !== void 0 && !oldPath) {
@@ -464,10 +587,207 @@ async function handleRepositoryCommitFile(req, res, ctx) {
464
587
  throw error;
465
588
  }
466
589
  }
590
+
591
+ // ../fleet-plugins/repository/server/compare.ts
592
+ function isPlainObject4(value) {
593
+ return typeof value === "object" && value !== null && !Array.isArray(value);
594
+ }
595
+ var FULL_REF_PREFIXES = ["refs/heads/", "refs/remotes/", "refs/tags/"];
596
+ var FORBIDDEN_REF_CHARS = /[\\~^:?*\[]/;
597
+ function hasWhitespaceOrControlChar(value) {
598
+ for (const ch of value) {
599
+ const code = ch.codePointAt(0);
600
+ if (code === void 0 || code <= 32 || code === 127) return true;
601
+ }
602
+ return /\s/.test(value);
603
+ }
604
+ function isSafeCompareRef(value) {
605
+ if (value === "HEAD") return true;
606
+ if (REF_RE.test(value)) return true;
607
+ if (!FULL_REF_PREFIXES.some((prefix) => value.startsWith(prefix))) return false;
608
+ if (hasWhitespaceOrControlChar(value) || FORBIDDEN_REF_CHARS.test(value)) return false;
609
+ if (value.includes("..") || value.includes("@{") || value.includes("//")) return false;
610
+ if (value.endsWith("/") || value.endsWith(".")) return false;
611
+ for (const component of value.split("/")) {
612
+ if (component.startsWith(".") || component.endsWith(".lock")) return false;
613
+ }
614
+ return true;
615
+ }
616
+ function isUnknownRevisionError(error) {
617
+ if (!(error instanceof GitExecutorError)) return false;
618
+ if (error.code !== "non_zero_exit") return false;
619
+ return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision");
620
+ }
621
+ function isNoMergeBaseError(error) {
622
+ if (!(error instanceof GitExecutorError)) return false;
623
+ if (error.code !== "non_zero_exit") return false;
624
+ return error.stderr.includes("no merge base");
625
+ }
626
+ async function handleRepositoryCompare(req, res, ctx) {
627
+ if (req.method !== "POST") {
628
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
629
+ return;
630
+ }
631
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
632
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
633
+ return;
634
+ }
635
+ const body = await ctx.host.http.readJsonBody(req);
636
+ if (!isPlainObject4(body) || "subPath" in body) {
637
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
638
+ return;
639
+ }
640
+ const theaterId = body.theaterId;
641
+ if (typeof theaterId !== "string") {
642
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
643
+ return;
644
+ }
645
+ const { base, head } = body;
646
+ if (typeof base !== "string" || typeof head !== "string" || !isSafeCompareRef(base) || !isSafeCompareRef(head)) {
647
+ ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
648
+ return;
649
+ }
650
+ if (base === head) {
651
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
652
+ return;
653
+ }
654
+ const theaterPath = ctx.host.paths.resolveTheaterPath(theaterId);
655
+ if (!theaterPath) {
656
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
657
+ return;
658
+ }
659
+ let cwdResult;
660
+ try {
661
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
662
+ } catch (error) {
663
+ if (error instanceof InvalidRepoError) {
664
+ ctx.host.http.writeJson(res, 400, { error: error.code });
665
+ return;
666
+ }
667
+ throw error;
668
+ }
669
+ const { gitCwd } = cwdResult;
670
+ const range = `${base}...${head}`;
671
+ try {
672
+ const [mergeBaseResult, nameStatusResult, numstatResult] = await Promise.all([
673
+ // 비교 컨텍스트 표기용 — 실패하면 응답에서 생략한다
674
+ runGit(["merge-base", "--end-of-options", base, head], { cwd: gitCwd }).catch(() => null),
675
+ runGit(["diff", "--relative", "--name-status", "--diff-filter=MADRT", "-M", "--end-of-options", range, "--", "."], { cwd: gitCwd }),
676
+ runGit(["diff", "--relative", "--numstat", "--diff-filter=MADRT", "-M", "--end-of-options", range, "--", "."], { cwd: gitCwd })
677
+ ]);
678
+ const mergeBase = mergeBaseResult?.stdout.trim().slice(0, 9);
679
+ ctx.host.http.writeJson(res, 200, {
680
+ files: parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout),
681
+ ...mergeBase ? { mergeBase } : {},
682
+ ...nameStatusResult.truncated || numstatResult.truncated ? { truncated: true } : {}
683
+ });
684
+ } catch (error) {
685
+ if (error instanceof GitExecutorError) {
686
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
687
+ ctx.host.http.writeJson(res, 422, { error: error.code });
688
+ return;
689
+ }
690
+ if (isNoMergeBaseError(error)) {
691
+ ctx.host.http.writeJson(res, 400, { error: "no_merge_base" });
692
+ return;
693
+ }
694
+ if (isUnknownRevisionError(error)) {
695
+ ctx.host.http.writeJson(res, 400, { error: "unknown_ref" });
696
+ return;
697
+ }
698
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
699
+ return;
700
+ }
701
+ throw error;
702
+ }
703
+ }
704
+ function isPlainObject5(value) {
705
+ return typeof value === "object" && value !== null && !Array.isArray(value);
706
+ }
707
+ function resolveLiteralFilePath2(gitCwd, rawPath) {
708
+ const relativePath = path4.normalize(rawPath);
709
+ const resolved = path4.resolve(gitCwd, relativePath);
710
+ if (relativePath === ".." || relativePath.startsWith(".." + path4.sep) || !resolved.startsWith(gitCwd + path4.sep)) return null;
711
+ return relativePath;
712
+ }
713
+ function literalPathspec3(relativePath) {
714
+ return `:(literal)${relativePath}`;
715
+ }
716
+ async function handleRepositoryCompareFile(req, res, ctx) {
717
+ if (req.method !== "POST") {
718
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
719
+ return;
720
+ }
721
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
722
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
723
+ return;
724
+ }
725
+ const body = await ctx.host.http.readJsonBody(req);
726
+ if (!isPlainObject5(body) || "subPath" in body || typeof body.theaterId !== "string" || typeof body.base !== "string" || typeof body.head !== "string" || typeof body.filePath !== "string") {
727
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
728
+ return;
729
+ }
730
+ if (!isSafeCompareRef(body.base) || !isSafeCompareRef(body.head)) {
731
+ ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
732
+ return;
733
+ }
734
+ if (!body.filePath) {
735
+ ctx.host.http.writeJson(res, 400, { error: "invalid_file_path" });
736
+ return;
737
+ }
738
+ if (body.oldPath !== void 0 && (typeof body.oldPath !== "string" || !body.oldPath)) {
739
+ ctx.host.http.writeJson(res, 400, { error: "invalid_file_path" });
740
+ return;
741
+ }
742
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
743
+ if (!theaterPath) {
744
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
745
+ return;
746
+ }
747
+ let cwdResult;
748
+ try {
749
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
750
+ } catch (error) {
751
+ if (error instanceof InvalidRepoError) {
752
+ ctx.host.http.writeJson(res, 400, { error: error.code });
753
+ return;
754
+ }
755
+ throw error;
756
+ }
757
+ const relativePath = resolveLiteralFilePath2(cwdResult.gitCwd, body.filePath);
758
+ const oldPath = typeof body.oldPath === "string" ? resolveLiteralFilePath2(cwdResult.gitCwd, body.oldPath) : void 0;
759
+ if (!relativePath || body.oldPath !== void 0 && !oldPath) {
760
+ ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
761
+ return;
762
+ }
763
+ const range = `${body.base}...${body.head}`;
764
+ try {
765
+ const result = await runGit(oldPath ? ["diff", "--unified=3", "-M", "--end-of-options", range, "--", literalPathspec3(oldPath), literalPathspec3(relativePath)] : ["diff", "--unified=3", "-M", "--end-of-options", range, "--", literalPathspec3(relativePath)], { cwd: cwdResult.gitCwd });
766
+ ctx.host.http.writeJson(res, 200, { content: result.stdout, ...result.truncated ? { truncated: true } : {} });
767
+ } catch (error) {
768
+ if (error instanceof GitExecutorError) {
769
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
770
+ ctx.host.http.writeJson(res, 422, { error: error.code });
771
+ return;
772
+ }
773
+ if (isNoMergeBaseError(error)) {
774
+ ctx.host.http.writeJson(res, 400, { error: "no_merge_base" });
775
+ return;
776
+ }
777
+ if (isUnknownRevisionError(error)) {
778
+ ctx.host.http.writeJson(res, 400, { error: "unknown_ref" });
779
+ return;
780
+ }
781
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
782
+ return;
783
+ }
784
+ throw error;
785
+ }
786
+ }
467
787
  var WORKTREE_SHA_RE = /^[0-9a-f]{40}$/;
468
788
  var CANONICAL_REF_RE = /^refs\/(?:heads|remotes|tags)\//;
469
789
  var REF_METACHAR_RE = /[~^:?*\[\\\s\x00-\x1f\x7f]/;
470
- function isPlainObject4(value) {
790
+ function isPlainObject6(value) {
471
791
  return typeof value === "object" && value !== null && !Array.isArray(value);
472
792
  }
473
793
  function isCanonicalRepositoryRef(ref) {
@@ -507,7 +827,7 @@ function parseLogOutput(stdout) {
507
827
  }
508
828
  return commits;
509
829
  }
510
- async function parseWorktreePorcelain(stdout, currentWorktreePath) {
830
+ function parseWorktreePorcelainEntries(stdout) {
511
831
  const worktrees = [];
512
832
  let worktreePath = null;
513
833
  let sha = "";
@@ -538,10 +858,14 @@ async function parseWorktreePorcelain(stdout, currentWorktreePath) {
538
858
  }
539
859
  }
540
860
  pushCurrent();
861
+ return worktrees;
862
+ }
863
+ async function parseWorktreePorcelain(stdout, currentWorktreePath) {
864
+ const worktrees = parseWorktreePorcelainEntries(stdout);
541
865
  const normalizedCurrentWorktreePath = await normalizeWorktreePath(currentWorktreePath);
542
- return Promise.all(worktrees.map(async ({ worktreePath: worktreePath2, ...checkout }) => ({
866
+ return Promise.all(worktrees.map(async ({ worktreePath, ...checkout }) => ({
543
867
  ...checkout,
544
- isCurrent: normalizedCurrentWorktreePath !== "" && await normalizeWorktreePath(worktreePath2) === normalizedCurrentWorktreePath
868
+ isCurrent: normalizedCurrentWorktreePath !== "" && await normalizeWorktreePath(worktreePath) === normalizedCurrentWorktreePath
545
869
  })));
546
870
  }
547
871
  function annotateHeadReachability(commits, revListStdout) {
@@ -557,7 +881,7 @@ function isNoHeadError2(error) {
557
881
  async function normalizeWorktreePath(worktreePath) {
558
882
  if (!worktreePath) return "";
559
883
  try {
560
- return await fs.realpath(worktreePath);
884
+ return await fs2.realpath(worktreePath);
561
885
  } catch {
562
886
  return worktreePath;
563
887
  }
@@ -588,7 +912,7 @@ async function handleRepositoryLog(req, res, ctx) {
588
912
  return;
589
913
  }
590
914
  const body = await ctx.host.http.readJsonBody(req);
591
- if (!isPlainObject4(body) || "subPath" in body) {
915
+ if (!isPlainObject6(body) || "subPath" in body) {
592
916
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
593
917
  return;
594
918
  }
@@ -602,7 +926,16 @@ async function handleRepositoryLog(req, res, ctx) {
602
926
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
603
927
  return;
604
928
  }
605
- const cwdResult = resolveGitCwd(theaterPath);
929
+ let cwdResult;
930
+ try {
931
+ cwdResult = await resolveGitCwd(theaterPath, body.repoRel);
932
+ } catch (error) {
933
+ if (error instanceof InvalidRepoError) {
934
+ ctx.host.http.writeJson(res, 400, { error: error.code });
935
+ return;
936
+ }
937
+ throw error;
938
+ }
606
939
  const { gitCwd } = cwdResult;
607
940
  const requestedRef = body.ref;
608
941
  if (requestedRef !== void 0 && (typeof requestedRef !== "string" || !isCanonicalRepositoryRef(requestedRef))) {
@@ -691,7 +1024,16 @@ async function handleRepositoryRefs(req, res, ctx) {
691
1024
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
692
1025
  return;
693
1026
  }
694
- const resolved = resolveGitCwd(theaterPath);
1027
+ let resolved;
1028
+ try {
1029
+ resolved = await resolveGitCwd(theaterPath, body.repoRel);
1030
+ } catch (error) {
1031
+ if (error instanceof InvalidRepoError) {
1032
+ ctx.host.http.writeJson(res, 400, { error: error.code });
1033
+ return;
1034
+ }
1035
+ throw error;
1036
+ }
695
1037
  try {
696
1038
  const [head, local, remote, tags, stashes] = await Promise.all([
697
1039
  runGit(["symbolic-ref", "--quiet", "HEAD"], { cwd: resolved.gitCwd, allowExitCodes: [1] }),
@@ -722,11 +1064,240 @@ async function handleRepositoryRefs(req, res, ctx) {
722
1064
  ctx.host.http.writeJson(res, 500, { error: "git_failed" });
723
1065
  }
724
1066
  }
1067
+ var REPOS_CAP = 200;
1068
+ var HARD_CAP_DEPTH = 8;
1069
+ var NESTED_BRANCH_CAP = 64;
1070
+ var SCAN_VISIT_BUDGET = 1e4;
1071
+ var DEFAULT_DEPTH = 3;
1072
+ function isPlainObject7(value) {
1073
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1074
+ }
1075
+ async function resolveRepoBranch(repoDir) {
1076
+ try {
1077
+ const result = await runGit(["status", "--porcelain=v2", "--branch", "--untracked-files=no"], { cwd: repoDir });
1078
+ const branch = result.stdout.split("\n").find((line) => line.startsWith("# branch.head "))?.slice(14).trim() ?? "";
1079
+ const oid = result.stdout.split("\n").find((line) => line.startsWith("# branch.oid "))?.slice(13).trim() ?? "";
1080
+ return branch === "(detached)" ? oid.slice(0, 7) : branch;
1081
+ } catch {
1082
+ return "";
1083
+ }
1084
+ }
1085
+ async function scanRepos(theaterPath, realTheaterPath, dir, currentDepth, maxDepth, repos, cap = REPOS_CAP, visits = { remaining: SCAN_VISIT_BUDGET }) {
1086
+ if (repos.length >= cap) return true;
1087
+ if (visits.remaining <= 0) return true;
1088
+ visits.remaining -= 1;
1089
+ const relPath = path4.relative(realTheaterPath, dir);
1090
+ const gitDir = isSelectableRepoRel(relPath) ? await resolveContainedGitDir(dir, realTheaterPath) : null;
1091
+ let linkedWorktree = false;
1092
+ if (gitDir !== null) {
1093
+ try {
1094
+ await fs2.access(path4.join(gitDir, "commondir"));
1095
+ linkedWorktree = true;
1096
+ } catch {
1097
+ }
1098
+ }
1099
+ if (gitDir !== null && !linkedWorktree) {
1100
+ repos.push({ relPath, name: relPath === "" ? path4.basename(realTheaterPath) : path4.basename(dir), repoDir: dir });
1101
+ if (repos.length >= cap) return true;
1102
+ }
1103
+ if (currentDepth >= maxDepth) return false;
1104
+ let dirHandle = null;
1105
+ try {
1106
+ dirHandle = await fs2.opendir(dir);
1107
+ let dirent = await dirHandle.read();
1108
+ while (dirent !== null) {
1109
+ if (repos.length >= cap || visits.remaining <= 0) return true;
1110
+ if (dirent.isDirectory() && dirent.name !== "node_modules" && dirent.name !== ".git") {
1111
+ const childPath = path4.join(dir, dirent.name);
1112
+ let realChildPath;
1113
+ try {
1114
+ realChildPath = await fs2.realpath(childPath);
1115
+ } catch {
1116
+ dirent = await dirHandle.read();
1117
+ continue;
1118
+ }
1119
+ if (isPathContained(realTheaterPath, realChildPath)) {
1120
+ const truncated = await scanRepos(realTheaterPath, realTheaterPath, realChildPath, currentDepth + 1, maxDepth, repos, cap, visits);
1121
+ if (truncated) return true;
1122
+ }
1123
+ }
1124
+ dirent = await dirHandle.read();
1125
+ }
1126
+ return false;
1127
+ } catch {
1128
+ return false;
1129
+ } finally {
1130
+ if (dirHandle) {
1131
+ try {
1132
+ await dirHandle.close();
1133
+ } catch {
1134
+ }
1135
+ }
1136
+ }
1137
+ }
1138
+ async function resolveNestedRepoCandidates(candidates, availableSlots, resolver = resolveRepoBranch) {
1139
+ const resolved = [];
1140
+ const selected = candidates.slice(0, Math.max(0, availableSlots));
1141
+ for (const [index, candidate] of selected.entries()) {
1142
+ resolved.push({
1143
+ relPath: candidate.relPath,
1144
+ name: candidate.name,
1145
+ branch: index < NESTED_BRANCH_CAP ? await resolver(candidate.repoDir) : "",
1146
+ kind: "nested"
1147
+ });
1148
+ }
1149
+ return resolved;
1150
+ }
1151
+ async function handleRepositoryRepos(req, res, ctx) {
1152
+ if (req.method !== "POST") {
1153
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
1154
+ return;
1155
+ }
1156
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
1157
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
1158
+ return;
1159
+ }
1160
+ const body = await ctx.host.http.readJsonBody(req);
1161
+ if (!isPlainObject7(body) || typeof body.theaterId !== "string") {
1162
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1163
+ return;
1164
+ }
1165
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
1166
+ if (!theaterPath) {
1167
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
1168
+ return;
1169
+ }
1170
+ let realTheaterPath;
1171
+ try {
1172
+ realTheaterPath = await fs2.realpath(theaterPath);
1173
+ } catch {
1174
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
1175
+ return;
1176
+ }
1177
+ const maxDepth = typeof body.maxDepth === "number" && Number.isFinite(body.maxDepth) ? Math.max(1, Math.min(HARD_CAP_DEPTH, Math.floor(body.maxDepth))) : DEFAULT_DEPTH;
1178
+ const repos = [];
1179
+ const seen = /* @__PURE__ */ new Set();
1180
+ let truncated = false;
1181
+ let rootIsRepo = await resolveContainedGitDir(realTheaterPath, realTheaterPath) !== null;
1182
+ if (!rootIsRepo) {
1183
+ try {
1184
+ const inside = await runGit(["rev-parse", "--is-inside-work-tree"], { cwd: theaterPath });
1185
+ rootIsRepo = inside.stdout.trim() === "true";
1186
+ } catch {
1187
+ }
1188
+ }
1189
+ if (rootIsRepo) {
1190
+ repos.push({ relPath: "", name: path4.basename(theaterPath), branch: await resolveRepoBranch(theaterPath), kind: "root" });
1191
+ seen.add("");
1192
+ }
1193
+ const scanned = [];
1194
+ const scanCap = Math.max(0, REPOS_CAP - repos.length);
1195
+ truncated = await scanRepos(realTheaterPath, realTheaterPath, realTheaterPath, 0, maxDepth, scanned, scanCap) || truncated;
1196
+ const unseenNested = scanned.filter((candidate) => !seen.has(candidate.relPath));
1197
+ const remainingSlots = Math.max(0, REPOS_CAP - repos.length);
1198
+ if (unseenNested.length > remainingSlots) truncated = true;
1199
+ const nestedCandidates = await resolveNestedRepoCandidates(unseenNested, remainingSlots);
1200
+ for (const candidate of nestedCandidates) {
1201
+ if (repos.length >= REPOS_CAP) {
1202
+ truncated = true;
1203
+ break;
1204
+ }
1205
+ repos.push(candidate);
1206
+ seen.add(candidate.relPath);
1207
+ }
1208
+ const result = { repos, ...truncated ? { truncated: true } : {} };
1209
+ ctx.host.http.writeJson(res, 200, result);
1210
+ }
1211
+ function isPlainObject8(value) {
1212
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1213
+ }
1214
+ async function handleRepositoryWorktrees(req, res, ctx) {
1215
+ if (req.method !== "POST") {
1216
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
1217
+ return;
1218
+ }
1219
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
1220
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
1221
+ return;
1222
+ }
1223
+ const body = await ctx.host.http.readJsonBody(req);
1224
+ if (!isPlainObject8(body) || "subPath" in body || typeof body.theaterId !== "string") {
1225
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1226
+ return;
1227
+ }
1228
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
1229
+ if (!theaterPath) {
1230
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
1231
+ return;
1232
+ }
1233
+ let gitCwd;
1234
+ try {
1235
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
1236
+ } catch (error) {
1237
+ if (error instanceof InvalidRepoError) {
1238
+ ctx.host.http.writeJson(res, 400, { error: error.code });
1239
+ return;
1240
+ }
1241
+ throw error;
1242
+ }
1243
+ let realTheaterPath;
1244
+ let realGitCwd;
1245
+ try {
1246
+ [realTheaterPath, realGitCwd] = await Promise.all([fs2.realpath(theaterPath), fs2.realpath(gitCwd)]);
1247
+ } catch {
1248
+ ctx.host.http.writeJson(res, 400, { error: "invalid_repo" });
1249
+ return;
1250
+ }
1251
+ try {
1252
+ const result = await runGit(["worktree", "list", "--porcelain"], { cwd: gitCwd });
1253
+ const worktrees = [];
1254
+ const seen = /* @__PURE__ */ new Set();
1255
+ for (const worktree of parseWorktreePorcelainEntries(result.stdout)) {
1256
+ let realWorktree;
1257
+ try {
1258
+ realWorktree = await fs2.realpath(worktree.worktreePath);
1259
+ } catch {
1260
+ continue;
1261
+ }
1262
+ if (!isPathContained(realTheaterPath, realWorktree)) continue;
1263
+ const relPath = path4.relative(realTheaterPath, realWorktree);
1264
+ if (!isSelectableRepoRel(relPath) || seen.has(relPath)) continue;
1265
+ if (await resolveContainedGitDir(realWorktree, realTheaterPath) === null) continue;
1266
+ worktrees.push({
1267
+ relPath,
1268
+ name: path4.basename(realWorktree),
1269
+ branch: worktree.branch ?? worktree.sha.slice(0, 7),
1270
+ current: realWorktree === realGitCwd
1271
+ });
1272
+ seen.add(relPath);
1273
+ }
1274
+ const payload = { worktrees };
1275
+ ctx.host.http.writeJson(res, 200, payload);
1276
+ } catch (error) {
1277
+ if (error instanceof GitExecutorError) {
1278
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
1279
+ ctx.host.http.writeJson(res, 422, { error: error.code });
1280
+ return;
1281
+ }
1282
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
1283
+ return;
1284
+ }
1285
+ throw error;
1286
+ }
1287
+ }
725
1288
 
726
1289
  // ../fleet-plugins/repository/routes.ts
727
1290
  var routes_default = definePlugin({
728
1291
  id: "repository",
729
1292
  register(ctx) {
1293
+ registerRouter(ctx, "repos", async ({ req, res }) => {
1294
+ await handleRepositoryRepos(req, res, ctx);
1295
+ return true;
1296
+ });
1297
+ registerRouter(ctx, "worktrees", async ({ req, res }) => {
1298
+ await handleRepositoryWorktrees(req, res, ctx);
1299
+ return true;
1300
+ });
730
1301
  registerRouter(ctx, "changed", async ({ req, res }) => {
731
1302
  await handleRepositoryChanged(req, res, ctx);
732
1303
  return true;
@@ -751,6 +1322,14 @@ var routes_default = definePlugin({
751
1322
  await handleRepositoryCommitFile(req, res, ctx);
752
1323
  return true;
753
1324
  });
1325
+ registerRouter(ctx, "compare", async ({ req, res }) => {
1326
+ await handleRepositoryCompare(req, res, ctx);
1327
+ return true;
1328
+ });
1329
+ registerRouter(ctx, "compare-file", async ({ req, res }) => {
1330
+ await handleRepositoryCompareFile(req, res, ctx);
1331
+ return true;
1332
+ });
754
1333
  }
755
1334
  });
756
1335