@dotobokuri/fleet-console 1.39.0 → 1.41.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 (71) hide show
  1. package/README.md +2 -3
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.mjs +152 -1182
  4. package/dist/client/assets/{_baseUniq-BDfju2rW.js → _baseUniq-CImzMrtU.js} +1 -1
  5. package/dist/client/assets/{arc-DWcMv-ui.js → arc-CKb0rHjN.js} +1 -1
  6. package/dist/client/assets/{architectureDiagram-Q4EWVU46-DhPv_Uba.js → architectureDiagram-Q4EWVU46-BaZipXSe.js} +1 -1
  7. package/dist/client/assets/{blockDiagram-DXYQGD6D-D7RYcNDP.js → blockDiagram-DXYQGD6D-D4sS9TjR.js} +1 -1
  8. package/dist/client/assets/{c4Diagram-AHTNJAMY-9_4dQBo3.js → c4Diagram-AHTNJAMY-DvjpMsKx.js} +1 -1
  9. package/dist/client/assets/channel-dSuGhtYS.js +1 -0
  10. package/dist/client/assets/{chunk-4BX2VUAB-CQf5x2mT.js → chunk-4BX2VUAB-B47BryhY.js} +1 -1
  11. package/dist/client/assets/{chunk-4TB4RGXK-eeG8BKQB.js → chunk-4TB4RGXK-DKIhSC52.js} +1 -1
  12. package/dist/client/assets/{chunk-55IACEB6-1dkBlsDt.js → chunk-55IACEB6-B3Gvpef7.js} +1 -1
  13. package/dist/client/assets/{chunk-EDXVE4YY-C5vcF-lT.js → chunk-EDXVE4YY-C4gFtvU8.js} +1 -1
  14. package/dist/client/assets/{chunk-FMBD7UC4-CGelAuwP.js → chunk-FMBD7UC4-BADPieOa.js} +1 -1
  15. package/dist/client/assets/{chunk-OYMX7WX6-CbKgVyop.js → chunk-OYMX7WX6-BUdtWGUY.js} +1 -1
  16. package/dist/client/assets/{chunk-QZHKN3VN-CT0ppG8R.js → chunk-QZHKN3VN-CMxkeGnM.js} +1 -1
  17. package/dist/client/assets/{chunk-YZCP3GAM-BEf8DhxB.js → chunk-YZCP3GAM-Br6N7t8d.js} +1 -1
  18. package/dist/client/assets/classDiagram-6PBFFD2Q-DEN4mh7x.js +1 -0
  19. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DEN4mh7x.js +1 -0
  20. package/dist/client/assets/clone-BbTnmCoq.js +1 -0
  21. package/dist/client/assets/{cose-bilkent-S5V4N54A-C6GtvpYi.js → cose-bilkent-S5V4N54A-D1Uw7i5_.js} +1 -1
  22. package/dist/client/assets/{dagre-KV5264BT-DvrU2BgH.js → dagre-KV5264BT-kop0bHkM.js} +1 -1
  23. package/dist/client/assets/{diagram-5BDNPKRD-Cy1qKKqt.js → diagram-5BDNPKRD-B9e41qNA.js} +1 -1
  24. package/dist/client/assets/{diagram-G4DWMVQ6-C4GGKnnD.js → diagram-G4DWMVQ6-Wew8X03k.js} +1 -1
  25. package/dist/client/assets/{diagram-MMDJMWI5-By7JFsIw.js → diagram-MMDJMWI5-DGBb5Oqa.js} +1 -1
  26. package/dist/client/assets/{diagram-TYMM5635-YwzVWpID.js → diagram-TYMM5635-CiVNsAz6.js} +1 -1
  27. package/dist/client/assets/{erDiagram-SMLLAGMA-CDsNlmLk.js → erDiagram-SMLLAGMA-BdemYTgC.js} +1 -1
  28. package/dist/client/assets/{flowDiagram-DWJPFMVM-BuUpIzTa.js → flowDiagram-DWJPFMVM-CXC6yxhU.js} +1 -1
  29. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-Dqv9t2x8.js → ganttDiagram-T4ZO3ILL-SMUpaiUg.js} +1 -1
  30. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-DApQn2c7.js → gitGraphDiagram-UUTBAWPF-UaFgTHEh.js} +1 -1
  31. package/dist/client/assets/{graph-CyX-IC4n.js → graph-Ch8F0gMI.js} +1 -1
  32. package/dist/client/assets/index-D38r12i5.css +1 -0
  33. package/dist/client/assets/index-DkTcJcbj.js +452 -0
  34. package/dist/client/assets/{infoDiagram-42DDH7IO-DIf3qelw.js → infoDiagram-42DDH7IO-BHBzw0bu.js} +1 -1
  35. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-CU3QtNdx.js → ishikawaDiagram-UXIWVN3A-CnDea8bw.js} +1 -1
  36. package/dist/client/assets/{journeyDiagram-VCZTEJTY-Dfq4OBhW.js → journeyDiagram-VCZTEJTY-CI91m8BP.js} +1 -1
  37. package/dist/client/assets/{kanban-definition-6JOO6SKY-CWSe3eh7.js → kanban-definition-6JOO6SKY-D-rn-rG2.js} +1 -1
  38. package/dist/client/assets/{layout-CfdpDpEB.js → layout-Cd9zpG94.js} +1 -1
  39. package/dist/client/assets/{linear-BQA5wLDW.js → linear-Cucr1ojd.js} +1 -1
  40. package/dist/client/assets/{mermaid.core-BwJS5O4y.js → mermaid.core-G36P12JZ.js} +4 -4
  41. package/dist/client/assets/{min-Cg-Q6TB5.js → min-CzfCLG7v.js} +1 -1
  42. package/dist/client/assets/{mindmap-definition-QFDTVHPH-BNXT0q7L.js → mindmap-definition-QFDTVHPH-4e1VjBV7.js} +1 -1
  43. package/dist/client/assets/{pieDiagram-DEJITSTG-Cot4WNe7.js → pieDiagram-DEJITSTG-m2CGbU51.js} +1 -1
  44. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-_YW3nZwc.js → quadrantDiagram-34T5L4WZ-BKNPGqMc.js} +1 -1
  45. package/dist/client/assets/{requirementDiagram-MS252O5E-didEilrX.js → requirementDiagram-MS252O5E-D2BMwnpz.js} +1 -1
  46. package/dist/client/assets/{sankeyDiagram-XADWPNL6-CIpzp5Ep.js → sankeyDiagram-XADWPNL6-Bm6hTqEh.js} +1 -1
  47. package/dist/client/assets/{sequenceDiagram-FGHM5R23-CqvwD_CI.js → sequenceDiagram-FGHM5R23-Ds2FEGPj.js} +1 -1
  48. package/dist/client/assets/{stateDiagram-FHFEXIEX-D492KgCG.js → stateDiagram-FHFEXIEX-CrXn2Tb7.js} +1 -1
  49. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BbzpXIlW.js +1 -0
  50. package/dist/client/assets/{timeline-definition-GMOUNBTQ-BpAwqH0H.js → timeline-definition-GMOUNBTQ-mw053fb5.js} +1 -1
  51. package/dist/client/assets/{vennDiagram-DHZGUBPP-DDMPO_HG.js → vennDiagram-DHZGUBPP-CAws0Vt2.js} +1 -1
  52. package/dist/client/assets/{wardley-RL74JXVD-TKEj0wFU.js → wardley-RL74JXVD-DGad2cmz.js} +1 -1
  53. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-CLjBh2KA.js → wardleyDiagram-NUSXRM2D-BlIyft8I.js} +1 -1
  54. package/dist/client/assets/{xychartDiagram-5P7HB3ND-DEn2Z9lG.js → xychartDiagram-5P7HB3ND-DHmSULAI.js} +1 -1
  55. package/dist/client/index.html +2 -2
  56. package/dist/client/theme-boot.js +5 -4
  57. package/dist/fleet-plugins/file-explorer/routes.mjs +484 -56
  58. package/dist/fleet-plugins/ledger/routes.mjs +56 -80
  59. package/dist/fleet-plugins/quota/routes.mjs +19806 -0
  60. package/dist/fleet-plugins/repository/routes.mjs +274 -17
  61. package/dist/fleet-plugins/scuttlebutt/routes.mjs +6 -431
  62. package/dist/fleet-plugins/skills/routes.mjs +1 -79
  63. package/dist/fleet-plugins/terminal/routes.mjs +12449 -2974
  64. package/package.json +1 -1
  65. package/dist/client/assets/channel-Cjiuj1tM.js +0 -1
  66. package/dist/client/assets/classDiagram-6PBFFD2Q-B-_6FtAZ.js +0 -1
  67. package/dist/client/assets/classDiagram-v2-HSJHXN6E-B-_6FtAZ.js +0 -1
  68. package/dist/client/assets/clone-BF2H_qSP.js +0 -1
  69. package/dist/client/assets/index-RzoF0XVt.js +0 -452
  70. package/dist/client/assets/index-bog2t8Rz.css +0 -1
  71. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DUe9CEL6.js +0 -1
@@ -8,12 +8,12 @@ import fs2 from 'fs/promises';
8
8
  function definePlugin(definition) {
9
9
  return definition;
10
10
  }
11
- function registerRouter(ctx, path9, handler) {
12
- ctx.registerRouter(normalizePath(path9), handler);
11
+ function registerRouter(ctx, path10, handler) {
12
+ ctx.registerRouter(normalizePath(path10), handler);
13
13
  }
14
- function normalizePath(path9) {
15
- if (path9.length === 0 || path9 === "/") return "/";
16
- return path9;
14
+ function normalizePath(path10) {
15
+ if (path10.length === 0 || path10 === "/") return "/";
16
+ return path10;
17
17
  }
18
18
 
19
19
  // ../../packages/core-process/src/hidden.ts
@@ -36,9 +36,25 @@ var GitExecutorError = class extends Error {
36
36
  };
37
37
  var DEFAULT_TIMEOUT_MS = 3e4;
38
38
  var DEFAULT_MAX_BUFFER = 8 * 1024 * 1024;
39
+ var STDERR_MAX_BUFFER = 1024 * 1024;
40
+ var GIT_HARDENING_PREFIX = ["-c", "core.fsmonitor=false", "--no-optional-locks"];
41
+ function sanitizeGitEnvironment(environment) {
42
+ const sanitized = {};
43
+ for (const [key, value] of Object.entries(environment)) {
44
+ const normalizedKey = key.toUpperCase();
45
+ if (normalizedKey.startsWith("GIT_") || normalizedKey === "LC_ALL" || normalizedKey === "SSH_ASKPASS" || normalizedKey === "SSH_ASKPASS_REQUIRE") continue;
46
+ if (value !== void 0) sanitized[key] = value;
47
+ }
48
+ sanitized.GIT_OPTIONAL_LOCKS = "0";
49
+ sanitized.GIT_TERMINAL_PROMPT = "0";
50
+ sanitized.GIT_ALLOW_PROTOCOL = "ssh:git:http:https:file";
51
+ sanitized.LC_ALL = "C";
52
+ return sanitized;
53
+ }
39
54
  function runGit(args, opts) {
40
55
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
41
56
  const maxBuffer = opts.maxBuffer ?? DEFAULT_MAX_BUFFER;
57
+ const hardenedArgs = [...GIT_HARDENING_PREFIX, ...args];
42
58
  return new Promise((resolve, reject) => {
43
59
  if (opts.signal?.aborted) {
44
60
  reject(new GitExecutorError("timeout"));
@@ -46,7 +62,11 @@ function runGit(args, opts) {
46
62
  }
47
63
  let child;
48
64
  try {
49
- child = spawn("git", args, withHidden({ cwd: opts.cwd, shell: false }));
65
+ child = spawn("git", hardenedArgs, withHidden({
66
+ cwd: opts.cwd,
67
+ env: sanitizeGitEnvironment(process.env),
68
+ shell: false
69
+ }));
50
70
  } catch (error) {
51
71
  const code = error.code === "ENOENT" ? "git_unavailable" : "spawn_failed";
52
72
  reject(new GitExecutorError(code, String(error)));
@@ -55,7 +75,9 @@ function runGit(args, opts) {
55
75
  const stdoutChunks = [];
56
76
  const stderrChunks = [];
57
77
  let stdoutLen = 0;
78
+ let stderrLen = 0;
58
79
  let truncated = false;
80
+ let stderrTruncated = false;
59
81
  let timedOut = false;
60
82
  const abort = () => {
61
83
  timedOut = true;
@@ -83,7 +105,16 @@ function runGit(args, opts) {
83
105
  }
84
106
  });
85
107
  child.stderr.on("data", (chunk) => {
86
- stderrChunks.push(chunk);
108
+ if (stderrTruncated) return;
109
+ const remaining = STDERR_MAX_BUFFER - stderrLen;
110
+ if (chunk.length >= remaining) {
111
+ stderrChunks.push(chunk.subarray(0, remaining));
112
+ stderrLen += remaining;
113
+ stderrTruncated = true;
114
+ } else {
115
+ stderrChunks.push(chunk);
116
+ stderrLen += chunk.length;
117
+ }
87
118
  });
88
119
  child.on("error", (error) => {
89
120
  cleanup();
@@ -97,7 +128,7 @@ function runGit(args, opts) {
97
128
  if (code !== 0) {
98
129
  if (code !== null && (opts.allowExitCodes?.includes(code) ?? false)) {
99
130
  const stdout2 = Buffer.concat(stdoutChunks).toString("utf8");
100
- resolve({ stdout: stdout2, truncated });
131
+ resolve({ stdout: stdout2, stderr, truncated, stderrTruncated });
101
132
  return;
102
133
  }
103
134
  const isNoRepo = stderr.toLowerCase().includes("not a git repository");
@@ -105,7 +136,7 @@ function runGit(args, opts) {
105
136
  return;
106
137
  }
107
138
  const stdout = Buffer.concat(stdoutChunks).toString("utf8");
108
- resolve({ stdout, truncated });
139
+ resolve({ stdout, stderr, truncated, stderrTruncated });
109
140
  });
110
141
  });
111
142
  }
@@ -796,10 +827,232 @@ async function handleRepositoryCompareFile(req, res, ctx) {
796
827
  throw error;
797
828
  }
798
829
  }
830
+ var AUTO_FETCH_THROTTLE_MS = 3e5;
831
+ var autoFetchInFlight = /* @__PURE__ */ new Map();
832
+ var lastSuccessfulFetchAt = /* @__PURE__ */ new Map();
833
+ function isPlainObject6(value) {
834
+ return typeof value === "object" && value !== null && !Array.isArray(value);
835
+ }
836
+ function classifyFetchError(stderr) {
837
+ if (/permission denied|authentication failed|could not read username|terminal prompts disabled|repository not found/i.test(stderr)) {
838
+ return "auth_failed";
839
+ }
840
+ if (/could not resolve host|failed to connect|connection (refused|reset)|operation timed out|network is unreachable/i.test(stderr)) {
841
+ return "network";
842
+ }
843
+ if (/no remote repository specified|does not appear to be a git repository/i.test(stderr)) {
844
+ return "no_remote";
845
+ }
846
+ return null;
847
+ }
848
+ async function resolveGitIdentity(gitCwd) {
849
+ const [gitDirResult, commonDirResult] = await Promise.all([
850
+ runGit(["rev-parse", "--absolute-git-dir"], { cwd: gitCwd }),
851
+ runGit(["rev-parse", "--git-common-dir"], { cwd: gitCwd })
852
+ ]);
853
+ const [gitDir, commonDir] = await Promise.all([
854
+ fs2.realpath(path4.resolve(gitCwd, gitDirResult.stdout.trim())),
855
+ fs2.realpath(path4.resolve(gitCwd, commonDirResult.stdout.trim()))
856
+ ]);
857
+ return { gitDir, commonDir };
858
+ }
859
+ async function readLastFetchAt(gitDirs) {
860
+ let newest = null;
861
+ for (const gitDir of gitDirs) {
862
+ try {
863
+ const stats = await fs2.stat(path4.join(gitDir, "FETCH_HEAD"));
864
+ if (stats.size === 0) continue;
865
+ if (!newest || stats.mtime.getTime() > newest.getTime()) newest = stats.mtime;
866
+ } catch (error) {
867
+ if (error.code !== "ENOENT") throw error;
868
+ }
869
+ }
870
+ return newest;
871
+ }
872
+ function countFetchProgress(stderr, marker) {
873
+ return stderr.split(/\r?\n/).filter((line) => line.includes(marker)).length;
874
+ }
875
+ function countFetchUpdatedRefs(stderr) {
876
+ return stderr.split(/\r?\n/).filter(
877
+ (line) => line.includes(" -> ") && /\S+\.\.\S+/.test(line) && !line.includes("[deleted]") && !line.includes("[new ")
878
+ ).length;
879
+ }
880
+ var NoRemoteError = class extends Error {
881
+ };
882
+ var SAFE_CREDENTIAL_HELPERS = /* @__PURE__ */ new Set([
883
+ "cache",
884
+ "store",
885
+ "osxkeychain",
886
+ "wincred",
887
+ "libsecret",
888
+ "gnome-libsecret",
889
+ "manager",
890
+ "manager-core"
891
+ ]);
892
+ async function resolveCredentialHelperArgs(gitCwd) {
893
+ const raw = (await runGit(["config", "--get-all", "credential.helper"], { cwd: gitCwd, allowExitCodes: [1] })).stdout;
894
+ const helpers = raw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
895
+ const kept = helpers.filter((helper) => {
896
+ if (!/^[A-Za-z0-9-]+$/.test(helper)) return false;
897
+ return SAFE_CREDENTIAL_HELPERS.has(helper) || SAFE_CREDENTIAL_HELPERS.has(helper.replace(/^git-credential-/, ""));
898
+ });
899
+ return ["-c", "credential.helper=", ...kept.flatMap((helper) => ["-c", `credential.helper=${helper}`]), "-c", "core.askPass="];
900
+ }
901
+ async function resolveDefaultRemote(gitCwd) {
902
+ const branch = (await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitCwd })).stdout.trim();
903
+ if (branch && branch !== "HEAD") {
904
+ const upstream = (await runGit(["config", "--get", `branch.${branch}.remote`], { cwd: gitCwd, allowExitCodes: [1] })).stdout.trim();
905
+ if (upstream && upstream !== ".") return upstream;
906
+ }
907
+ const remotes = (await runGit(["remote"], { cwd: gitCwd })).stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
908
+ if (remotes.includes("origin")) return "origin";
909
+ return remotes[0] ?? null;
910
+ }
911
+ async function readThrottleResult(gitDirs, commonDir) {
912
+ const fsNewest = await readLastFetchAt(gitDirs);
913
+ const recorded = lastSuccessfulFetchAt.get(commonDir);
914
+ let newest = fsNewest;
915
+ if (recorded !== void 0 && (!newest || recorded > newest.getTime())) newest = new Date(recorded);
916
+ if (!newest || Date.now() - newest.getTime() >= AUTO_FETCH_THROTTLE_MS) return null;
917
+ return { ok: true, skipped: "throttled", lastFetchAt: newest.toISOString() };
918
+ }
919
+ async function fetchRepository(gitCwd) {
920
+ const remote = await resolveDefaultRemote(gitCwd);
921
+ if (!remote) throw new NoRemoteError();
922
+ const credentialArgs = await resolveCredentialHelperArgs(gitCwd);
923
+ const result = await runGit([
924
+ "-c",
925
+ "core.sshCommand=ssh",
926
+ "-c",
927
+ // git 예약어 none만 프록시 우회다 — 다른 값은 프록시 "명령"으로 실행된다.
928
+ "core.gitProxy=none",
929
+ "-c",
930
+ // protocol.allow=user는 ext를 never에서 허용으로 뒤집는다(GIT_PROTOCOL_FROM_USER unset) —
931
+ // 기본 정책을 유지한 채 실행형 transport만 명시 차단한다.
932
+ "protocol.ext.allow=never",
933
+ "-c",
934
+ // ref transaction 훅(reference-transaction 등)의 zero-click 실행을 플랫폼 널 장치로 차단한다.
935
+ `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
936
+ ...credentialArgs,
937
+ "fetch",
938
+ remote,
939
+ // 명시 refspec이 configured refspec을 대체한다 — prune의 파괴 범위를
940
+ // refs/remotes/<remote>/ 아래로 고정해 로컬 refs를 보호한다.
941
+ `+refs/heads/*:refs/remotes/${remote}/*`,
942
+ // repo config의 remote.<name>.uploadpack이 로컬 transport에서 명령 실행되므로 표준 명령을 강제한다.
943
+ "--upload-pack=git-upload-pack",
944
+ // 하드닝된 상위 fetch를 빠져나가는 재귀(중첩 네트워크+하위 upload-pack)를 끊는다.
945
+ "--no-recurse-submodules",
946
+ "--prune",
947
+ "--no-tags",
948
+ // fetch.pruneTags=true인 저장소가 --prune과 결합해 로컬 전용 태그를 지우는 것을 막는다.
949
+ "--no-prune-tags"
950
+ ], { cwd: gitCwd });
951
+ const fetchedAt = (/* @__PURE__ */ new Date()).toISOString();
952
+ return {
953
+ ok: true,
954
+ fetchedAt,
955
+ lastFetchAt: fetchedAt,
956
+ pruned: countFetchProgress(result.stderr, " - [deleted] "),
957
+ newRefs: countFetchProgress(result.stderr, " * [new branch] "),
958
+ updatedRefs: countFetchUpdatedRefs(result.stderr)
959
+ };
960
+ }
961
+ async function fetchHeadCandidates(identity) {
962
+ const candidates = /* @__PURE__ */ new Set([identity.gitDir, identity.commonDir]);
963
+ try {
964
+ const worktreesDir = path4.join(identity.commonDir, "worktrees");
965
+ for (const entry of await fs2.readdir(worktreesDir, { withFileTypes: true })) {
966
+ if (entry.isDirectory()) candidates.add(path4.join(worktreesDir, entry.name));
967
+ }
968
+ } catch (error) {
969
+ if (error.code !== "ENOENT") throw error;
970
+ }
971
+ return [...candidates];
972
+ }
973
+ async function runAutoFetch(gitCwd, identity) {
974
+ const throttled = await readThrottleResult(await fetchHeadCandidates(identity), identity.commonDir);
975
+ return throttled ?? fetchRepository(gitCwd);
976
+ }
977
+ async function fetchAutoSingleFlight(gitCwd, identity) {
978
+ const key = identity.commonDir;
979
+ const existing = autoFetchInFlight.get(key);
980
+ if (existing) {
981
+ await existing;
982
+ const throttled = await readThrottleResult(await fetchHeadCandidates(identity), identity.commonDir);
983
+ if (throttled) return throttled;
984
+ }
985
+ const operation = runAutoFetch(gitCwd, identity);
986
+ autoFetchInFlight.set(key, operation);
987
+ try {
988
+ return await operation;
989
+ } finally {
990
+ if (autoFetchInFlight.get(key) === operation) autoFetchInFlight.delete(key);
991
+ }
992
+ }
993
+ async function handleRepositoryFetch(req, res, ctx) {
994
+ if (req.method !== "POST") {
995
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
996
+ return;
997
+ }
998
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
999
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
1000
+ return;
1001
+ }
1002
+ const body = await ctx.host.http.readJsonBody(req);
1003
+ if (!isPlainObject6(body) || "subPath" in body || typeof body.theaterId !== "string" || body.mode !== void 0 && body.mode !== "auto") {
1004
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1005
+ return;
1006
+ }
1007
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
1008
+ if (!theaterPath) {
1009
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
1010
+ return;
1011
+ }
1012
+ let gitCwd;
1013
+ try {
1014
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
1015
+ } catch (error) {
1016
+ if (error instanceof InvalidRepoError) {
1017
+ ctx.host.http.writeJson(res, 400, { error: error.code });
1018
+ return;
1019
+ }
1020
+ throw error;
1021
+ }
1022
+ try {
1023
+ const identity = await resolveGitIdentity(gitCwd);
1024
+ const result = body.mode === "auto" ? await fetchAutoSingleFlight(gitCwd, identity) : await (autoFetchInFlight.get(identity.commonDir) ?? fetchRepository(gitCwd));
1025
+ if ("fetchedAt" in result) lastSuccessfulFetchAt.set(identity.commonDir, Date.parse(result.fetchedAt));
1026
+ ctx.host.http.writeJson(res, 200, result);
1027
+ } catch (error) {
1028
+ if (error instanceof NoRemoteError) {
1029
+ ctx.host.http.writeJson(res, 422, { error: "no_remote" });
1030
+ return;
1031
+ }
1032
+ if (error instanceof GitExecutorError) {
1033
+ if (error.code === "timeout") {
1034
+ ctx.host.http.writeJson(res, 422, { error: "timeout" });
1035
+ return;
1036
+ }
1037
+ const classified = classifyFetchError(error.stderr);
1038
+ if (classified) {
1039
+ ctx.host.http.writeJson(res, 422, { error: classified });
1040
+ return;
1041
+ }
1042
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
1043
+ ctx.host.http.writeJson(res, 422, { error: error.code });
1044
+ return;
1045
+ }
1046
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
1047
+ return;
1048
+ }
1049
+ throw error;
1050
+ }
1051
+ }
799
1052
  var WORKTREE_SHA_RE = /^[0-9a-f]{40}$/;
800
1053
  var CANONICAL_REF_RE = /^refs\/(?:heads|remotes|tags)\//;
801
1054
  var REF_METACHAR_RE = /[~^:?*\[\\\s\x00-\x1f\x7f]/;
802
- function isPlainObject6(value) {
1055
+ function isPlainObject7(value) {
803
1056
  return typeof value === "object" && value !== null && !Array.isArray(value);
804
1057
  }
805
1058
  function isCanonicalRepositoryRef(ref) {
@@ -924,7 +1177,7 @@ async function handleRepositoryLog(req, res, ctx) {
924
1177
  return;
925
1178
  }
926
1179
  const body = await ctx.host.http.readJsonBody(req);
927
- if (!isPlainObject6(body) || "subPath" in body) {
1180
+ if (!isPlainObject7(body) || "subPath" in body) {
928
1181
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
929
1182
  return;
930
1183
  }
@@ -1092,7 +1345,7 @@ var HARD_CAP_DEPTH = 8;
1092
1345
  var NESTED_BRANCH_CAP = 64;
1093
1346
  var SCAN_VISIT_BUDGET = 1e4;
1094
1347
  var DEFAULT_DEPTH = 3;
1095
- function isPlainObject7(value) {
1348
+ function isPlainObject8(value) {
1096
1349
  return typeof value === "object" && value !== null && !Array.isArray(value);
1097
1350
  }
1098
1351
  async function resolveRepoBranch(repoDir) {
@@ -1181,7 +1434,7 @@ async function handleRepositoryRepos(req, res, ctx) {
1181
1434
  return;
1182
1435
  }
1183
1436
  const body = await ctx.host.http.readJsonBody(req);
1184
- if (!isPlainObject7(body) || typeof body.theaterId !== "string") {
1437
+ if (!isPlainObject8(body) || typeof body.theaterId !== "string") {
1185
1438
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1186
1439
  return;
1187
1440
  }
@@ -1260,7 +1513,7 @@ async function handleRepositorySearch(req, res, ctx) {
1260
1513
  return;
1261
1514
  }
1262
1515
  const body = await ctx.host.http.readJsonBody(req);
1263
- if (!isPlainObject8(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8 || "subPath" in body || "ref" in body) {
1516
+ if (!isPlainObject9(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8 || "subPath" in body || "ref" in body) {
1264
1517
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1265
1518
  return;
1266
1519
  }
@@ -1314,10 +1567,10 @@ async function handleRepositorySearch(req, res, ctx) {
1314
1567
  throw error;
1315
1568
  }
1316
1569
  }
1317
- function isPlainObject8(value) {
1570
+ function isPlainObject9(value) {
1318
1571
  return typeof value === "object" && value !== null && !Array.isArray(value);
1319
1572
  }
1320
- function isPlainObject9(value) {
1573
+ function isPlainObject10(value) {
1321
1574
  return typeof value === "object" && value !== null && !Array.isArray(value);
1322
1575
  }
1323
1576
  async function handleRepositoryWorktrees(req, res, ctx) {
@@ -1330,7 +1583,7 @@ async function handleRepositoryWorktrees(req, res, ctx) {
1330
1583
  return;
1331
1584
  }
1332
1585
  const body = await ctx.host.http.readJsonBody(req);
1333
- if (!isPlainObject9(body) || "subPath" in body || typeof body.theaterId !== "string") {
1586
+ if (!isPlainObject10(body) || "subPath" in body || typeof body.theaterId !== "string") {
1334
1587
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1335
1588
  return;
1336
1589
  }
@@ -1411,6 +1664,10 @@ var routes_default = definePlugin({
1411
1664
  await handleRepositoryChanged(req, res, ctx);
1412
1665
  return true;
1413
1666
  });
1667
+ registerRouter(ctx, "fetch", async ({ req, res }) => {
1668
+ await handleRepositoryFetch(req, res, ctx);
1669
+ return true;
1670
+ });
1414
1671
  registerRouter(ctx, "file", async ({ req, res }) => {
1415
1672
  await handleRepositoryFile(req, res, ctx);
1416
1673
  return true;