@dotobokuri/fleet-console 1.16.1 → 1.17.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/AGENTS.md +5 -2
  2. package/dist/cli.mjs +499 -299
  3. package/dist/client/assets/{_baseUniq-Bvbp_jlP.js → _baseUniq-24vRkv8i.js} +1 -1
  4. package/dist/client/assets/{arc-BIE5Buf8.js → arc-BTiCiQ57.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-Q4EWVU46-D5XMG93D.js → architectureDiagram-Q4EWVU46-B24oDJvB.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-DXYQGD6D-DF6L79td.js → blockDiagram-DXYQGD6D-BOAE3FdW.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AHTNJAMY-DhF1h0OW.js → c4Diagram-AHTNJAMY-C0W9raQx.js} +1 -1
  8. package/dist/client/assets/channel-yb1CkB_S.js +1 -0
  9. package/dist/client/assets/{chunk-4BX2VUAB-HIDhmpsY.js → chunk-4BX2VUAB-DvyhIkzo.js} +1 -1
  10. package/dist/client/assets/{chunk-4TB4RGXK-WM3UvlRe.js → chunk-4TB4RGXK-CMV1UZLH.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-D9kCAM0-.js → chunk-55IACEB6-CG75wbs4.js} +1 -1
  12. package/dist/client/assets/{chunk-EDXVE4YY-CHGSf6YA.js → chunk-EDXVE4YY-9CjdVkNM.js} +1 -1
  13. package/dist/client/assets/{chunk-FMBD7UC4-saa1UCmo.js → chunk-FMBD7UC4-i496V3zS.js} +1 -1
  14. package/dist/client/assets/{chunk-OYMX7WX6-BrlxxClZ.js → chunk-OYMX7WX6-CeRfBoEk.js} +1 -1
  15. package/dist/client/assets/{chunk-QZHKN3VN-CVnd5MX8.js → chunk-QZHKN3VN-D1mXQVL_.js} +1 -1
  16. package/dist/client/assets/{chunk-YZCP3GAM-BpsFNlbM.js → chunk-YZCP3GAM-DUXK6jpB.js} +1 -1
  17. package/dist/client/assets/classDiagram-6PBFFD2Q-DgNTT8Lb.js +1 -0
  18. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DgNTT8Lb.js +1 -0
  19. package/dist/client/assets/clone-BLvG1gy6.js +1 -0
  20. package/dist/client/assets/{cose-bilkent-S5V4N54A-AdFIrmD1.js → cose-bilkent-S5V4N54A-Cw97IMZH.js} +1 -1
  21. package/dist/client/assets/{dagre-KV5264BT-YPE_DPjG.js → dagre-KV5264BT-CyY9URRi.js} +1 -1
  22. package/dist/client/assets/{diagram-5BDNPKRD-sW7JSe_o.js → diagram-5BDNPKRD-BEVj5KaK.js} +1 -1
  23. package/dist/client/assets/{diagram-G4DWMVQ6-CdoUA7HM.js → diagram-G4DWMVQ6-IXOT_4VL.js} +1 -1
  24. package/dist/client/assets/{diagram-MMDJMWI5-Dr_Q44k2.js → diagram-MMDJMWI5-CO2p0poL.js} +1 -1
  25. package/dist/client/assets/{diagram-TYMM5635-CgWLDdO4.js → diagram-TYMM5635-DvRDFMNM.js} +1 -1
  26. package/dist/client/assets/{erDiagram-SMLLAGMA-BS5jKQII.js → erDiagram-SMLLAGMA-C5uteguB.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-DWJPFMVM-DM_ZIfsi.js → flowDiagram-DWJPFMVM-JekEx60D.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-D1ASi2pt.js → ganttDiagram-T4ZO3ILL-DRKjnKDn.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-DTS8qu5f.js → gitGraphDiagram-UUTBAWPF-Dhnk3n7N.js} +1 -1
  30. package/dist/client/assets/{graph-BSP3V12O.js → graph-DE-kEMug.js} +1 -1
  31. package/dist/client/assets/index-BY_CeGCl.js +402 -0
  32. package/dist/client/assets/{index-DNtncJ2Y.css → index-CfNSyAUJ.css} +1 -1
  33. package/dist/client/assets/{infoDiagram-42DDH7IO-C6rS44Vv.js → infoDiagram-42DDH7IO-DG9M_7T9.js} +1 -1
  34. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-BysMV0Yj.js → ishikawaDiagram-UXIWVN3A-CiP4wnqc.js} +1 -1
  35. package/dist/client/assets/{journeyDiagram-VCZTEJTY-Cq1tDTjK.js → journeyDiagram-VCZTEJTY-DklSog3d.js} +1 -1
  36. package/dist/client/assets/{kanban-definition-6JOO6SKY-CV7fuoIo.js → kanban-definition-6JOO6SKY-BVB77h52.js} +1 -1
  37. package/dist/client/assets/{layout-mBwNdhT9.js → layout-CAevigwv.js} +1 -1
  38. package/dist/client/assets/{linear-B8Mqzhqu.js → linear-CB_shmWV.js} +1 -1
  39. package/dist/client/assets/{mermaid.core-mTVIRAe-.js → mermaid.core-DPLCTlKT.js} +4 -4
  40. package/dist/client/assets/{min-LkS4GBrL.js → min-Cimc7NFp.js} +1 -1
  41. package/dist/client/assets/{mindmap-definition-QFDTVHPH-CuK4DHsN.js → mindmap-definition-QFDTVHPH-DnyowBeF.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-DEJITSTG-yKtYS0aG.js → pieDiagram-DEJITSTG-B1mr5ZWu.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CQYBuUXw.js → quadrantDiagram-34T5L4WZ-CJla4J4E.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-MS252O5E-DExCsGb_.js → requirementDiagram-MS252O5E-Bduy03Er.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-XADWPNL6-kqoN-VmQ.js → sankeyDiagram-XADWPNL6-G7I_cbR7.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-FGHM5R23-Dsk6zp0F.js → sequenceDiagram-FGHM5R23-D4Uf0SLt.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-FHFEXIEX-Bg-kY2ci.js → stateDiagram-FHFEXIEX-Fr0IevW-.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-DXt_OYu8.js +1 -0
  49. package/dist/client/assets/{timeline-definition-GMOUNBTQ-DkeszYPW.js → timeline-definition-GMOUNBTQ-CzR6FMOJ.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-DHZGUBPP-a9aq_CaS.js → vennDiagram-DHZGUBPP-CCzsRm3-.js} +1 -1
  51. package/dist/client/assets/{wardley-RL74JXVD-BioIIFLl.js → wardley-RL74JXVD-C9U_HC7W.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-Bpy2nx7x.js → wardleyDiagram-NUSXRM2D-BkwkDUKo.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-5P7HB3ND-N7vhpanC.js → xychartDiagram-5P7HB3ND-CZ45hysw.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/fleet-plugins/diff/routes.mjs +224 -57
  56. package/dist/fleet-plugins/file-explorer/routes.mjs +130 -0
  57. package/dist/fleet-plugins/skills/routes.mjs +17052 -0
  58. package/dist/fleet-plugins/terminal/routes.mjs +164 -67
  59. package/package.json +1 -1
  60. package/dist/client/assets/channel-BSjWiXu9.js +0 -1
  61. package/dist/client/assets/classDiagram-6PBFFD2Q-DIEVc5gp.js +0 -1
  62. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DIEVc5gp.js +0 -1
  63. package/dist/client/assets/clone-VuQnnBA0.js +0 -1
  64. package/dist/client/assets/index-D3a51i1X.js +0 -402
  65. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-Dlqv8bJ9.js +0 -1
@@ -7,12 +7,12 @@ import { spawn } from 'child_process';
7
7
  function definePlugin(definition) {
8
8
  return definition;
9
9
  }
10
- function registerRouter(ctx, path2, handler) {
11
- ctx.registerRouter(normalizePath(path2), handler);
10
+ function registerRouter(ctx, path3, handler) {
11
+ ctx.registerRouter(normalizePath(path3), handler);
12
12
  }
13
- function normalizePath(path2) {
14
- if (path2.length === 0 || path2 === "/") return "/";
15
- return path2;
13
+ function normalizePath(path3) {
14
+ if (path3.length === 0 || path3 === "/") return "/";
15
+ return path3;
16
16
  }
17
17
  var GitExecutorError = class extends Error {
18
18
  code;
@@ -91,14 +91,13 @@ function runGit(args, opts) {
91
91
  }
92
92
 
93
93
  // ../fleet-plugins/diff/server/diff.ts
94
- var REF_SAFE_RE = /^(?:[a-f0-9]{7,40}|[A-Za-z0-9][A-Za-z0-9/_.~^-]{0,250})$/;
95
- function isSafeGitRef(ref) {
96
- return REF_SAFE_RE.test(ref);
94
+ function isPlainObject(value) {
95
+ return typeof value === "object" && value !== null && !Array.isArray(value);
97
96
  }
98
- function buildDiffArgs(mode, ref) {
99
- if (mode === "staged") return ["diff", "--cached"];
100
- if (mode === "commit" && ref) return ["diff", `${ref}^`, ref];
101
- return ["diff"];
97
+ function normalizeNumstatPath(p) {
98
+ const match = /^(.*?)\{[^}]* => ([^}]*)\}(.*)$/.exec(p);
99
+ if (!match) return p;
100
+ return (match[1] ?? "") + (match[2] ?? "") + (match[3] ?? "");
102
101
  }
103
102
  function parseDiffFileList(nameStatusOutput, numstatOutput) {
104
103
  const numstatMap = /* @__PURE__ */ new Map();
@@ -107,7 +106,7 @@ function parseDiffFileList(nameStatusOutput, numstatOutput) {
107
106
  if (parts.length < 3) continue;
108
107
  const [adds, dels, filePath] = parts;
109
108
  if (!filePath) continue;
110
- numstatMap.set(filePath, {
109
+ numstatMap.set(normalizeNumstatPath(filePath), {
111
110
  additions: parseInt(adds ?? "0", 10) || 0,
112
111
  deletions: parseInt(dels ?? "0", 10) || 0
113
112
  });
@@ -135,8 +134,29 @@ async function fetchUntrackedFiles(cwd) {
135
134
  deletions: 0
136
135
  }));
137
136
  }
138
- function isPlainObject(value) {
139
- return typeof value === "object" && value !== null && !Array.isArray(value);
137
+ function isNoHeadError(error) {
138
+ if (!(error instanceof GitExecutorError)) return false;
139
+ if (error.code !== "non_zero_exit") return false;
140
+ return error.stderr.includes("unknown revision") || error.stderr.includes("bad revision");
141
+ }
142
+ async function resolveGitCwd(theaterPath, rawSubPath) {
143
+ if (rawSubPath === "") return { gitCwd: theaterPath };
144
+ const resolvedSub = path.resolve(theaterPath, rawSubPath);
145
+ if (!resolvedSub.startsWith(theaterPath + path.sep) && resolvedSub !== theaterPath) return null;
146
+ if (path.normalize(rawSubPath).startsWith("..")) return null;
147
+ let realTheater;
148
+ let realSub;
149
+ try {
150
+ [realTheater, realSub] = await Promise.all([
151
+ fs.realpath(theaterPath),
152
+ fs.realpath(resolvedSub)
153
+ ]);
154
+ } catch {
155
+ return null;
156
+ }
157
+ const norm = realTheater.endsWith(path.sep) ? realTheater : realTheater + path.sep;
158
+ if (realSub !== realTheater && !realSub.startsWith(norm)) return null;
159
+ return { gitCwd: resolvedSub };
140
160
  }
141
161
  async function handleDiffChanged(req, res, ctx) {
142
162
  if (req.method !== "POST") {
@@ -152,43 +172,48 @@ async function handleDiffChanged(req, res, ctx) {
152
172
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
153
173
  return;
154
174
  }
155
- const mode = body.mode;
156
- if (mode !== "workdir" && mode !== "staged" && mode !== "commit") {
157
- ctx.host.http.writeJson(res, 400, { error: "invalid_mode" });
158
- return;
159
- }
160
175
  const theaterId = body.theaterId;
161
176
  if (typeof theaterId !== "string") {
162
177
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
163
178
  return;
164
179
  }
165
- const rawRef = typeof body.ref === "string" ? body.ref : void 0;
166
- if (rawRef !== void 0 && !isSafeGitRef(rawRef)) {
167
- ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
168
- return;
169
- }
170
180
  const theaterPath = ctx.host.paths.resolveTheaterPath(theaterId);
171
181
  if (!theaterPath) {
172
182
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
173
183
  return;
174
184
  }
185
+ const rawSubPath = typeof body.subPath === "string" ? body.subPath : "";
186
+ const cwdResult = await resolveGitCwd(theaterPath, rawSubPath);
187
+ if (!cwdResult) {
188
+ ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
189
+ return;
190
+ }
191
+ const { gitCwd } = cwdResult;
175
192
  try {
176
- const diffArgs = buildDiffArgs(mode, rawRef);
177
- const nameStatusArgs = [...diffArgs, "--name-status", "--diff-filter=MADR"];
178
- const numstatArgs = [...diffArgs, "--numstat", "--diff-filter=MADR"];
179
- const [nameStatusResult, numstatResult] = await Promise.all([
180
- runGit(nameStatusArgs, { cwd: theaterPath }),
181
- runGit(numstatArgs, { cwd: theaterPath })
182
- ]);
183
- const files = parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout);
184
- if (mode === "workdir") {
185
- try {
186
- const untrackedFiles = await fetchUntrackedFiles(theaterPath);
187
- files.push(...untrackedFiles);
188
- } catch {
189
- }
193
+ let files;
194
+ let truncated = false;
195
+ try {
196
+ const [nameStatusResult, numstatResult] = await Promise.all([
197
+ runGit(["diff", "HEAD", "--name-status", "--diff-filter=MADR"], { cwd: gitCwd }),
198
+ runGit(["diff", "HEAD", "--numstat", "--diff-filter=MADR"], { cwd: gitCwd })
199
+ ]);
200
+ files = parseDiffFileList(nameStatusResult.stdout, numstatResult.stdout);
201
+ truncated = nameStatusResult.truncated || numstatResult.truncated;
202
+ } catch (err) {
203
+ if (!isNoHeadError(err)) throw err;
204
+ const [nsResult, nsNumstat] = await Promise.all([
205
+ runGit(["diff", "--cached", "--name-status", "--diff-filter=MADR"], { cwd: gitCwd }),
206
+ runGit(["diff", "--cached", "--numstat", "--diff-filter=MADR"], { cwd: gitCwd })
207
+ ]);
208
+ files = parseDiffFileList(nsResult.stdout, nsNumstat.stdout);
209
+ truncated = nsResult.truncated || nsNumstat.truncated;
210
+ }
211
+ try {
212
+ const untrackedFiles = await fetchUntrackedFiles(gitCwd);
213
+ files.push(...untrackedFiles);
214
+ } catch {
190
215
  }
191
- ctx.host.http.writeJson(res, 200, { files, truncated: nameStatusResult.truncated || numstatResult.truncated });
216
+ ctx.host.http.writeJson(res, 200, { files, truncated });
192
217
  } catch (error) {
193
218
  if (error instanceof GitExecutorError) {
194
219
  if (error.code === "no_git_repo" || error.code === "git_unavailable") {
@@ -216,7 +241,7 @@ async function handleDiffFile(req, res, ctx) {
216
241
  return;
217
242
  }
218
243
  const mode = body.mode;
219
- if (mode !== "workdir" && mode !== "staged" && mode !== "commit" && mode !== "untracked") {
244
+ if (mode !== "unified" && mode !== "untracked") {
220
245
  ctx.host.http.writeJson(res, 400, { error: "invalid_mode" });
221
246
  return;
222
247
  }
@@ -225,53 +250,71 @@ async function handleDiffFile(req, res, ctx) {
225
250
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
226
251
  return;
227
252
  }
228
- const rawFilePath = body.filePath;
229
253
  const theaterPath = ctx.host.paths.resolveTheaterPath(theaterId);
230
254
  if (!theaterPath) {
231
255
  ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
232
256
  return;
233
257
  }
234
- const resolvedPath = path.resolve(theaterPath, rawFilePath);
235
- if (!resolvedPath.startsWith(theaterPath + path.sep) && resolvedPath !== theaterPath) {
258
+ const rawSubPath = typeof body.subPath === "string" ? body.subPath : "";
259
+ const cwdResult = await resolveGitCwd(theaterPath, rawSubPath);
260
+ if (!cwdResult) {
236
261
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
237
262
  return;
238
263
  }
239
- const relativePath = path.normalize(rawFilePath);
240
- if (relativePath.startsWith("..")) {
264
+ const { gitCwd } = cwdResult;
265
+ const rawFilePath = body.filePath;
266
+ const resolvedFilePath = path.resolve(gitCwd, rawFilePath);
267
+ if (!resolvedFilePath.startsWith(gitCwd + path.sep) && resolvedFilePath !== gitCwd) {
241
268
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
242
269
  return;
243
270
  }
244
- const rawRef = typeof body.ref === "string" ? body.ref : void 0;
245
- if (rawRef !== void 0 && !isSafeGitRef(rawRef)) {
246
- ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
271
+ const relativePath = path.normalize(rawFilePath);
272
+ if (relativePath.startsWith("..")) {
273
+ ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
247
274
  return;
248
275
  }
249
276
  try {
250
277
  if (mode === "untracked") {
251
- let realTheater;
278
+ let realGitCwd;
252
279
  let realFile;
253
280
  try {
254
- [realTheater, realFile] = await Promise.all([
255
- fs.realpath(theaterPath),
256
- fs.realpath(resolvedPath)
281
+ [realGitCwd, realFile] = await Promise.all([
282
+ fs.realpath(gitCwd),
283
+ fs.realpath(resolvedFilePath)
257
284
  ]);
258
285
  } catch {
259
286
  ctx.host.http.writeJson(res, 404, { error: "file_not_found" });
260
287
  return;
261
288
  }
262
- if (realFile !== realTheater && !realFile.startsWith(realTheater + path.sep)) {
289
+ if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path.sep)) {
263
290
  ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
264
291
  return;
265
292
  }
266
293
  const result2 = await runGit(
267
294
  ["diff", "--no-index", "--unified=3", "--", "/dev/null", relativePath],
268
- { cwd: theaterPath, allowExitCodes: [1] }
295
+ { cwd: gitCwd, allowExitCodes: [1] }
269
296
  );
270
297
  ctx.host.http.writeJson(res, 200, { content: result2.stdout, truncated: result2.truncated });
271
298
  return;
272
299
  }
273
- const diffArgs = [...buildDiffArgs(mode, rawRef), "--unified=3", "--", relativePath];
274
- const result = await runGit(diffArgs, { cwd: theaterPath });
300
+ try {
301
+ const [realGitCwd, realFile] = await Promise.all([
302
+ fs.realpath(gitCwd),
303
+ fs.realpath(resolvedFilePath)
304
+ ]);
305
+ if (realFile !== realGitCwd && !realFile.startsWith(realGitCwd + path.sep)) {
306
+ ctx.host.http.writeJson(res, 403, { error: "path_outside_theater" });
307
+ return;
308
+ }
309
+ } catch {
310
+ }
311
+ let result;
312
+ try {
313
+ result = await runGit(["diff", "HEAD", "--unified=3", "--", relativePath], { cwd: gitCwd });
314
+ } catch (err) {
315
+ if (!isNoHeadError(err)) throw err;
316
+ result = await runGit(["diff", "--cached", "--unified=3", "--", relativePath], { cwd: gitCwd });
317
+ }
275
318
  ctx.host.http.writeJson(res, 200, { content: result.stdout, truncated: result.truncated });
276
319
  } catch (error) {
277
320
  if (error instanceof GitExecutorError) {
@@ -285,11 +328,135 @@ async function handleDiffFile(req, res, ctx) {
285
328
  throw error;
286
329
  }
287
330
  }
331
+ var REPOS_CAP = 200;
332
+ var HARD_CAP_DEPTH = 8;
333
+ var DEFAULT_DEPTH = 3;
334
+ function isPlainObject2(value) {
335
+ return typeof value === "object" && value !== null && !Array.isArray(value);
336
+ }
337
+ async function resolveRepoBranch(repoDir) {
338
+ try {
339
+ const result = await runGit(["rev-parse", "--abbrev-ref", "HEAD"], { cwd: repoDir });
340
+ const branch = result.stdout.trim();
341
+ if (branch === "HEAD") {
342
+ const sha = await runGit(["rev-parse", "--short", "HEAD"], { cwd: repoDir });
343
+ return sha.stdout.trim() || "HEAD";
344
+ }
345
+ return branch || "HEAD";
346
+ } catch {
347
+ return "unknown";
348
+ }
349
+ }
350
+ async function scanRepos(theaterPath, realTheaterPath, dir, currentDepth, maxDepth, repos, cap = REPOS_CAP) {
351
+ if (repos.length >= cap) return true;
352
+ const gitPath = path.join(dir, ".git");
353
+ try {
354
+ await fs.stat(gitPath);
355
+ const relPath = path.relative(theaterPath, dir);
356
+ const name = relPath === "" ? path.basename(theaterPath) : path.basename(dir);
357
+ const branch = await resolveRepoBranch(dir);
358
+ repos.push({ relPath, name, branch });
359
+ if (repos.length >= cap) return true;
360
+ } catch {
361
+ }
362
+ if (currentDepth >= maxDepth) return false;
363
+ let dirHandle = null;
364
+ try {
365
+ dirHandle = await fs.opendir(dir);
366
+ try {
367
+ let dirent = await dirHandle.read();
368
+ while (dirent !== null) {
369
+ if (repos.length >= cap) return true;
370
+ if (dirent.isDirectory()) {
371
+ const childName = dirent.name;
372
+ if (childName === "node_modules" || childName === ".git") {
373
+ dirent = await dirHandle.read();
374
+ continue;
375
+ }
376
+ const childPath = path.join(dir, childName);
377
+ let realChildPath;
378
+ try {
379
+ realChildPath = await fs.realpath(childPath);
380
+ } catch {
381
+ dirent = await dirHandle.read();
382
+ continue;
383
+ }
384
+ const normalizedReal = realTheaterPath.endsWith(path.sep) ? realTheaterPath : realTheaterPath + path.sep;
385
+ if (realChildPath !== realTheaterPath && !realChildPath.startsWith(normalizedReal)) {
386
+ dirent = await dirHandle.read();
387
+ continue;
388
+ }
389
+ const truncated = await scanRepos(theaterPath, realTheaterPath, childPath, currentDepth + 1, maxDepth, repos, cap);
390
+ if (truncated) return true;
391
+ }
392
+ dirent = await dirHandle.read();
393
+ }
394
+ return false;
395
+ } finally {
396
+ await dirHandle.close();
397
+ }
398
+ } catch {
399
+ if (dirHandle) {
400
+ try {
401
+ await dirHandle.close();
402
+ } catch {
403
+ }
404
+ }
405
+ return false;
406
+ }
407
+ }
408
+ async function handleDiffRepos(req, res, ctx) {
409
+ if (req.method !== "POST") {
410
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
411
+ return;
412
+ }
413
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
414
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
415
+ return;
416
+ }
417
+ const body = await ctx.host.http.readJsonBody(req);
418
+ if (!isPlainObject2(body)) {
419
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
420
+ return;
421
+ }
422
+ const theaterId = body.theaterId;
423
+ if (typeof theaterId !== "string") {
424
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
425
+ return;
426
+ }
427
+ const theaterPath = ctx.host.paths.resolveTheaterPath(theaterId);
428
+ if (!theaterPath) {
429
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
430
+ return;
431
+ }
432
+ const rawDepth = body.maxDepth;
433
+ const maxDepth = typeof rawDepth === "number" && Number.isFinite(rawDepth) && rawDepth >= 1 ? Math.min(HARD_CAP_DEPTH, Math.floor(rawDepth)) : DEFAULT_DEPTH;
434
+ let realTheaterPath;
435
+ try {
436
+ realTheaterPath = await fs.realpath(theaterPath);
437
+ } catch {
438
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
439
+ return;
440
+ }
441
+ const repos = [];
442
+ const truncated = await scanRepos(theaterPath, realTheaterPath, theaterPath, 0, maxDepth, repos);
443
+ repos.sort((a, b) => {
444
+ if (a.relPath === "") return -1;
445
+ if (b.relPath === "") return 1;
446
+ return a.relPath.localeCompare(b.relPath);
447
+ });
448
+ const result = { repos, ...truncated ? { truncated: true } : {} };
449
+ ctx.host.http.writeJson(res, 200, result);
450
+ }
288
451
 
289
452
  // ../fleet-plugins/diff/routes.ts
290
453
  var routes_default = definePlugin({
291
454
  id: "diff",
292
455
  register(ctx) {
456
+ registerRouter(ctx, "repos", async ({ req, res }) => {
457
+ await handleDiffRepos(req, res, ctx);
458
+ return true;
459
+ });
293
460
  registerRouter(ctx, "changed", async ({ req, res }) => {
294
461
  await handleDiffChanged(req, res, ctx);
295
462
  return true;
@@ -300,6 +300,81 @@ function isWithinRoot2(resolved, root) {
300
300
  const normalizedRoot = root.endsWith(path2.sep) ? root : root + path2.sep;
301
301
  return resolved === root || resolved.startsWith(normalizedRoot);
302
302
  }
303
+ var DEBOUNCE_MS = 200;
304
+ var watcherRegistry = createWatcherRegistry();
305
+ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs.watch(p, opts, (ev, fn) => cb(ev, typeof fn === "string" ? fn : null)), debounceMs = DEBOUNCE_MS) {
306
+ const entries = /* @__PURE__ */ new Map();
307
+ function makeUnsubscribe(theaterId, onChange, onState, entry) {
308
+ let called = false;
309
+ return () => {
310
+ if (called) return;
311
+ called = true;
312
+ entry.subscribers.delete(onChange);
313
+ entry.stateSubscribers.delete(onState);
314
+ entry.subscriberCount--;
315
+ if (entry.subscriberCount <= 0) {
316
+ for (const t of entry.debounceTimers.values()) clearTimeout(t);
317
+ entry.watcher.close();
318
+ if (entries.get(theaterId) === entry) entries.delete(theaterId);
319
+ }
320
+ };
321
+ }
322
+ function subscribe(theaterId, theaterPath, onChange, onState) {
323
+ const existing = entries.get(theaterId);
324
+ if (existing) {
325
+ existing.subscribers.add(onChange);
326
+ existing.stateSubscribers.add(onState);
327
+ existing.subscriberCount++;
328
+ onState("watching");
329
+ return makeUnsubscribe(theaterId, onChange, onState, existing);
330
+ }
331
+ const debounceTimers = /* @__PURE__ */ new Map();
332
+ const subscribers = /* @__PURE__ */ new Set();
333
+ const stateSubscribers = /* @__PURE__ */ new Set();
334
+ let watcher;
335
+ try {
336
+ watcher = watcherFactory(theaterPath, { recursive: true }, (_, filename) => {
337
+ const relDir = computeRelDir(filename);
338
+ const pending = debounceTimers.get(relDir);
339
+ if (pending) clearTimeout(pending);
340
+ debounceTimers.set(
341
+ relDir,
342
+ setTimeout(() => {
343
+ debounceTimers.delete(relDir);
344
+ for (const sub of subscribers) sub(relDir);
345
+ }, debounceMs)
346
+ );
347
+ });
348
+ } catch {
349
+ onState("degraded");
350
+ return () => {
351
+ };
352
+ }
353
+ const entry = { watcher, subscribers, stateSubscribers, subscriberCount: 1, debounceTimers };
354
+ watcher.on("error", () => {
355
+ for (const t of debounceTimers.values()) clearTimeout(t);
356
+ debounceTimers.clear();
357
+ try {
358
+ watcher.close();
359
+ } catch {
360
+ }
361
+ if (entries.get(theaterId) === entry) entries.delete(theaterId);
362
+ for (const notify of stateSubscribers) notify("degraded");
363
+ });
364
+ entries.set(theaterId, entry);
365
+ subscribers.add(onChange);
366
+ stateSubscribers.add(onState);
367
+ onState("watching");
368
+ return makeUnsubscribe(theaterId, onChange, onState, entry);
369
+ }
370
+ return { subscribe };
371
+ }
372
+ function computeRelDir(filename) {
373
+ if (!filename) return "";
374
+ const lastSep = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
375
+ if (lastSep <= 0) return "";
376
+ return filename.slice(0, lastSep);
377
+ }
303
378
 
304
379
  // ../fleet-plugins/file-explorer/server/handlers.ts
305
380
  function isPlainObject(value) {
@@ -423,6 +498,57 @@ async function handleFilesImage(req, res, ctx) {
423
498
  throw error;
424
499
  }
425
500
  }
501
+ function handleFilesWatch(req, res, ctx) {
502
+ if (req.method !== "GET") {
503
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
504
+ return;
505
+ }
506
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
507
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
508
+ return;
509
+ }
510
+ const url = readUrl(req);
511
+ const theaterId = url.searchParams.get("theaterId");
512
+ if (!theaterId) {
513
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
514
+ return;
515
+ }
516
+ const theaterPath = ctx.host.paths.resolveTheaterPath(theaterId);
517
+ if (!theaterPath) {
518
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
519
+ return;
520
+ }
521
+ res.writeHead(200, {
522
+ "Content-Type": "text/event-stream",
523
+ "Cache-Control": "no-cache",
524
+ "Connection": "keep-alive",
525
+ "X-Accel-Buffering": "no"
526
+ });
527
+ res.flushHeaders();
528
+ function sendEvent(name, data) {
529
+ try {
530
+ res.write(`event: ${name}
531
+ data: ${data}
532
+
533
+ `);
534
+ } catch {
535
+ }
536
+ }
537
+ const unsubscribe = watcherRegistry.subscribe(
538
+ theaterId,
539
+ theaterPath,
540
+ // 개행 포함 파일명이 SSE 필드 경계를 깨지 않도록 JSON으로 프레이밍한다
541
+ (relDir) => sendEvent("change", JSON.stringify(relDir)),
542
+ (state) => sendEvent("state", state)
543
+ );
544
+ req.on("close", () => {
545
+ unsubscribe();
546
+ try {
547
+ res.end();
548
+ } catch {
549
+ }
550
+ });
551
+ }
426
552
 
427
553
  // ../fleet-plugins/file-explorer/routes.ts
428
554
  var routes_default = definePlugin({
@@ -440,6 +566,10 @@ var routes_default = definePlugin({
440
566
  await handleFilesImage(req, res, ctx);
441
567
  return true;
442
568
  });
569
+ registerRouter(ctx, "files/watch", ({ req, res }) => {
570
+ handleFilesWatch(req, res, ctx);
571
+ return true;
572
+ });
443
573
  }
444
574
  });
445
575