@dotobokuri/fleet-console 1.44.0 → 1.45.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 (67) hide show
  1. package/dist/cli.d.ts +1 -1
  2. package/dist/cli.mjs +3088 -3139
  3. package/dist/client/assets/{_baseUniq-DfRTNtWd.js → _baseUniq-DtJH2bh8.js} +1 -1
  4. package/dist/client/assets/{arc-Ds9vyD8n.js → arc-DHCKMqqF.js} +1 -1
  5. package/dist/client/assets/{architectureDiagram-Q4EWVU46-pZ_1tCpx.js → architectureDiagram-Q4EWVU46-BOuyINL8.js} +1 -1
  6. package/dist/client/assets/{blockDiagram-DXYQGD6D-nntXWaQ9.js → blockDiagram-DXYQGD6D-cIre3_5n.js} +1 -1
  7. package/dist/client/assets/{c4Diagram-AHTNJAMY-AHAd4NjZ.js → c4Diagram-AHTNJAMY-DTX-qjSW.js} +1 -1
  8. package/dist/client/assets/channel-BhciS8jR.js +1 -0
  9. package/dist/client/assets/{chunk-4BX2VUAB-BNfWXMb-.js → chunk-4BX2VUAB-DiBwpozK.js} +1 -1
  10. package/dist/client/assets/{chunk-4TB4RGXK-B5MiBj0N.js → chunk-4TB4RGXK-uCZ-B3-y.js} +1 -1
  11. package/dist/client/assets/{chunk-55IACEB6-9vJ9AfWn.js → chunk-55IACEB6-CmBu2sf0.js} +1 -1
  12. package/dist/client/assets/{chunk-EDXVE4YY-CpxmITfr.js → chunk-EDXVE4YY-BXpw2VyB.js} +1 -1
  13. package/dist/client/assets/{chunk-FMBD7UC4-BoUi1EcN.js → chunk-FMBD7UC4-Cn9sKTn5.js} +1 -1
  14. package/dist/client/assets/{chunk-OYMX7WX6-s7qMNTfq.js → chunk-OYMX7WX6-BDgH1zlW.js} +1 -1
  15. package/dist/client/assets/{chunk-QZHKN3VN-Bdgo-JE9.js → chunk-QZHKN3VN-BOdvRibu.js} +1 -1
  16. package/dist/client/assets/{chunk-YZCP3GAM-C9RCb0eu.js → chunk-YZCP3GAM-BDIEapcU.js} +1 -1
  17. package/dist/client/assets/classDiagram-6PBFFD2Q-BjRUnHce.js +1 -0
  18. package/dist/client/assets/classDiagram-v2-HSJHXN6E-BjRUnHce.js +1 -0
  19. package/dist/client/assets/clone-DygBt67n.js +1 -0
  20. package/dist/client/assets/{cose-bilkent-S5V4N54A-Mr2xbCPD.js → cose-bilkent-S5V4N54A-BVlc3Dad.js} +1 -1
  21. package/dist/client/assets/{dagre-KV5264BT-xIfrxsrb.js → dagre-KV5264BT-raV4BK6w.js} +1 -1
  22. package/dist/client/assets/{diagram-5BDNPKRD-BM49JRbq.js → diagram-5BDNPKRD-BMAh1LJt.js} +1 -1
  23. package/dist/client/assets/{diagram-G4DWMVQ6-uMduxuqy.js → diagram-G4DWMVQ6-AM5t2f3j.js} +1 -1
  24. package/dist/client/assets/{diagram-MMDJMWI5-BfCnnu9a.js → diagram-MMDJMWI5-DMw_UwWR.js} +1 -1
  25. package/dist/client/assets/{diagram-TYMM5635-DTVL_WEx.js → diagram-TYMM5635-CiDAD3e9.js} +1 -1
  26. package/dist/client/assets/{erDiagram-SMLLAGMA-BUQDjGU2.js → erDiagram-SMLLAGMA-D1IXSwFZ.js} +1 -1
  27. package/dist/client/assets/{flowDiagram-DWJPFMVM-RZ85tRp5.js → flowDiagram-DWJPFMVM-pEq1L6HP.js} +1 -1
  28. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-CenzFyys.js → ganttDiagram-T4ZO3ILL-BAXTU16v.js} +1 -1
  29. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-BpkFTm7f.js → gitGraphDiagram-UUTBAWPF-PkEvpSio.js} +1 -1
  30. package/dist/client/assets/{graph-BlyDgw2r.js → graph-OOLam9NE.js} +1 -1
  31. package/dist/client/assets/{index-BmGY-BNs.css → index-BgHFwZy9.css} +1 -1
  32. package/dist/client/assets/{index-BAMerP8r.js → index-CvXOp1di.js} +68 -68
  33. package/dist/client/assets/{infoDiagram-42DDH7IO-ZB4pKXQT.js → infoDiagram-42DDH7IO-CcLveXnw.js} +1 -1
  34. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-BfXl0ksQ.js → ishikawaDiagram-UXIWVN3A-D-p_eX0W.js} +1 -1
  35. package/dist/client/assets/{journeyDiagram-VCZTEJTY-DCSluqxm.js → journeyDiagram-VCZTEJTY-D81ht6ZY.js} +1 -1
  36. package/dist/client/assets/{kanban-definition-6JOO6SKY-5X3OxziR.js → kanban-definition-6JOO6SKY-D8SIIBWy.js} +1 -1
  37. package/dist/client/assets/{layout-C2Jxamqz.js → layout-CeMy87tl.js} +1 -1
  38. package/dist/client/assets/{linear-DgO5otCG.js → linear-C0Txkuhf.js} +1 -1
  39. package/dist/client/assets/{mermaid.core-CqKupZd5.js → mermaid.core-zdTcCzmD.js} +4 -4
  40. package/dist/client/assets/{min-B9jNVA7s.js → min-BPp_9Vnv.js} +1 -1
  41. package/dist/client/assets/{mindmap-definition-QFDTVHPH-B_Clh0rq.js → mindmap-definition-QFDTVHPH-DRJbMf8W.js} +1 -1
  42. package/dist/client/assets/{pieDiagram-DEJITSTG-D1rR7mwo.js → pieDiagram-DEJITSTG-CqPISwE-.js} +1 -1
  43. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-CGnU5k3k.js → quadrantDiagram-34T5L4WZ-BGLaujDD.js} +1 -1
  44. package/dist/client/assets/{requirementDiagram-MS252O5E-BDFOsklw.js → requirementDiagram-MS252O5E-DWF6sFQ_.js} +1 -1
  45. package/dist/client/assets/{sankeyDiagram-XADWPNL6-DT-8dpb6.js → sankeyDiagram-XADWPNL6-Dv2O23aj.js} +1 -1
  46. package/dist/client/assets/{sequenceDiagram-FGHM5R23-3IY8NxLM.js → sequenceDiagram-FGHM5R23-BXgYtweg.js} +1 -1
  47. package/dist/client/assets/{stateDiagram-FHFEXIEX-DqaGlW3D.js → stateDiagram-FHFEXIEX-COtDK2gC.js} +1 -1
  48. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BzCFtEfV.js +1 -0
  49. package/dist/client/assets/{timeline-definition-GMOUNBTQ-DmOlxq0r.js → timeline-definition-GMOUNBTQ-B_-QYLHK.js} +1 -1
  50. package/dist/client/assets/{vennDiagram-DHZGUBPP-UsUm6iWZ.js → vennDiagram-DHZGUBPP-BNVDRgAa.js} +1 -1
  51. package/dist/client/assets/{wardley-RL74JXVD-C0JK7zQx.js → wardley-RL74JXVD-B3XwoW1A.js} +1 -1
  52. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-pFHz6DzF.js → wardleyDiagram-NUSXRM2D-B6-OKLa-.js} +1 -1
  53. package/dist/client/assets/{xychartDiagram-5P7HB3ND-Cgy9Igjb.js → xychartDiagram-5P7HB3ND-DepfDqrZ.js} +1 -1
  54. package/dist/client/index.html +2 -2
  55. package/dist/fleet-plugins/file-explorer/routes.mjs +647 -648
  56. package/dist/fleet-plugins/ledger/routes.mjs +1 -5
  57. package/dist/fleet-plugins/quota/routes.mjs +169 -157
  58. package/dist/fleet-plugins/repository/routes.mjs +0 -2
  59. package/dist/fleet-plugins/scuttlebutt/routes.mjs +1 -5
  60. package/dist/fleet-plugins/skills/routes.mjs +1 -5
  61. package/dist/fleet-plugins/terminal/routes.mjs +5865 -6684
  62. package/package.json +1 -1
  63. package/dist/client/assets/channel-D_VyV3Fr.js +0 -1
  64. package/dist/client/assets/classDiagram-6PBFFD2Q-TBLL_03g.js +0 -1
  65. package/dist/client/assets/classDiagram-v2-HSJHXN6E-TBLL_03g.js +0 -1
  66. package/dist/client/assets/clone-CYDDwEhh.js +0 -1
  67. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-2tlPZvTx.js +0 -1
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
  import { execFile, spawn } from 'child_process';
3
- import fs3, { constants } from 'fs';
4
- import fs8 from 'fs/promises';
5
- import path4 from 'path';
3
+ import fs2, { constants } from 'fs';
4
+ import fsp from 'fs/promises';
5
+ import path5 from 'path';
6
6
 
7
7
  // sdk/plugin/node.ts
8
8
  function definePlugin(definition) {
9
9
  return definition;
10
10
  }
11
- function registerRouter(ctx, path10, handler) {
12
- ctx.registerRouter(normalizePath(path10), handler);
11
+ function registerRouter(ctx, path6, handler) {
12
+ ctx.registerRouter(normalizePath(path6), handler);
13
13
  }
14
- function normalizePath(path10) {
15
- if (path10.length === 0 || path10 === "/") return "/";
16
- return path10;
14
+ function normalizePath(path6) {
15
+ if (path6.length === 0 || path6 === "/") return "/";
16
+ return path6;
17
17
  }
18
18
  var PathActionError = class extends Error {
19
19
  code;
@@ -24,16 +24,16 @@ var PathActionError = class extends Error {
24
24
  }
25
25
  };
26
26
  async function resolveContainedActionPath(theaterPath, relativePath) {
27
- if (path4.isAbsolute(relativePath)) throw new PathActionError("path_outside_theater");
28
- const rootPath = path4.resolve(theaterPath);
29
- const candidatePath = path4.resolve(rootPath, relativePath);
27
+ if (path5.isAbsolute(relativePath)) throw new PathActionError("path_outside_theater");
28
+ const rootPath = path5.resolve(theaterPath);
29
+ const candidatePath = path5.resolve(rootPath, relativePath);
30
30
  if (!isPathContained(rootPath, candidatePath)) throw new PathActionError("path_outside_theater");
31
31
  let realRootPath;
32
32
  let realCandidatePath;
33
33
  try {
34
34
  [realRootPath, realCandidatePath] = await Promise.all([
35
- fs8.realpath(rootPath),
36
- fs8.realpath(candidatePath)
35
+ fsp.realpath(rootPath),
36
+ fsp.realpath(candidatePath)
37
37
  ]);
38
38
  } catch (error) {
39
39
  const code = error.code;
@@ -51,11 +51,61 @@ async function resolveContainedActionPath(theaterPath, relativePath) {
51
51
  };
52
52
  }
53
53
  function isPathContained(rootPath, candidatePath) {
54
- const relative = path4.relative(rootPath, candidatePath);
55
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
54
+ const relative = path5.relative(rootPath, candidatePath);
55
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
56
+ }
57
+ var FileActionUnavailableError = class extends Error {
58
+ constructor() {
59
+ super("action_unavailable");
60
+ this.name = "FileActionUnavailableError";
61
+ }
62
+ };
63
+ var DEFAULT_REVEAL_DEPENDENCIES = {
64
+ platform: process.platform,
65
+ launch: spawnDetached
66
+ };
67
+ async function revealPath(theaterPath, relativePath, mode, dependencies = {}) {
68
+ const deps = { ...DEFAULT_REVEAL_DEPENDENCIES, ...dependencies };
69
+ const { realPath } = await resolveContainedActionPath(theaterPath, relativePath);
70
+ const command = resolveRevealCommand(deps.platform, mode, realPath);
71
+ if (!command) throw new FileActionUnavailableError();
72
+ try {
73
+ await deps.launch(command.file, command.args);
74
+ } catch (error) {
75
+ if (error.code === "ENOENT") throw new FileActionUnavailableError();
76
+ throw error;
77
+ }
78
+ }
79
+ function resolveRevealCommand(platform, mode, absolutePath) {
80
+ if (platform === "darwin") {
81
+ return mode === "reveal" ? { file: "open", args: ["-R", absolutePath] } : { file: "open", args: [absolutePath] };
82
+ }
83
+ if (platform === "linux") {
84
+ return {
85
+ file: "xdg-open",
86
+ args: [mode === "reveal" ? path5.dirname(absolutePath) : absolutePath]
87
+ };
88
+ }
89
+ if (platform === "win32") {
90
+ return mode === "reveal" ? { file: "explorer", args: [`/select,${absolutePath}`] } : { file: "explorer", args: [absolutePath] };
91
+ }
92
+ return null;
93
+ }
94
+ function spawnDetached(file, args) {
95
+ return new Promise((resolve, reject) => {
96
+ const child = spawn(file, [...args], {
97
+ detached: true,
98
+ shell: false,
99
+ stdio: "ignore",
100
+ windowsHide: true
101
+ });
102
+ child.once("error", reject);
103
+ child.once("spawn", () => {
104
+ child.unref();
105
+ resolve();
106
+ });
107
+ });
56
108
  }
57
-
58
- // ../fleet-plugins/file-explorer/server/clipboard.ts
59
109
  var ClipboardUnavailableError = class extends Error {
60
110
  constructor() {
61
111
  super("clipboard_unavailable");
@@ -67,13 +117,13 @@ var LINUX_CLIPBOARD_TOOLS = [
67
117
  { name: "xclip", args: ["-selection", "clipboard"] },
68
118
  { name: "xsel", args: ["--clipboard", "--input"] }
69
119
  ];
70
- var DEFAULT_DEPENDENCIES = {
120
+ var DEFAULT_CLIPBOARD_ACTION_DEPENDENCIES = {
71
121
  platform: process.platform,
72
122
  findExecutable: findExecutableOnPath,
73
123
  runWithInput: execFileWithInput
74
124
  };
75
125
  async function copyPathToClipboard(theaterPath, relativePath, dependencies = {}) {
76
- const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
126
+ const deps = { ...DEFAULT_CLIPBOARD_ACTION_DEPENDENCIES, ...dependencies };
77
127
  const { logicalPath } = await resolveContainedActionPath(theaterPath, relativePath);
78
128
  const command = await resolveClipboardCommand(deps.platform, deps.findExecutable);
79
129
  if (!command) throw new ClipboardUnavailableError();
@@ -96,11 +146,11 @@ async function resolveClipboardCommand(platform, findExecutable = findExecutable
96
146
  }
97
147
  async function findExecutableOnPath(name) {
98
148
  const rawPath = process.env.PATH ?? "";
99
- for (const directory of rawPath.split(path4.delimiter)) {
149
+ for (const directory of rawPath.split(path5.delimiter)) {
100
150
  if (!directory) continue;
101
- const candidate = path4.join(directory, name);
151
+ const candidate = path5.join(directory, name);
102
152
  try {
103
- await fs8.access(candidate, constants.X_OK);
153
+ await fsp.access(candidate, constants.X_OK);
104
154
  return candidate;
105
155
  } catch {
106
156
  }
@@ -186,14 +236,14 @@ var EXT_LANG_MAP = {
186
236
  ".txt": "plaintext"
187
237
  };
188
238
  async function readFileForTheater(theaterPath, relativePath) {
189
- const resolved = path4.resolve(theaterPath, relativePath);
239
+ const resolved = path5.resolve(theaterPath, relativePath);
190
240
  if (!isWithinRoot(resolved, theaterPath)) throw new FileReadError("path_outside_theater");
191
241
  let realResolved;
192
242
  let realRoot;
193
243
  try {
194
244
  [realResolved, realRoot] = await Promise.all([
195
- fs3.promises.realpath(resolved),
196
- fs3.promises.realpath(theaterPath)
245
+ fs2.promises.realpath(resolved),
246
+ fs2.promises.realpath(theaterPath)
197
247
  ]);
198
248
  } catch (error) {
199
249
  const code = error.code;
@@ -203,7 +253,7 @@ async function readFileForTheater(theaterPath, relativePath) {
203
253
  if (!isWithinRoot(realResolved, realRoot)) throw new FileReadError("path_outside_theater");
204
254
  let stat;
205
255
  try {
206
- stat = await fs3.promises.stat(realResolved);
256
+ stat = await fs2.promises.stat(realResolved);
207
257
  } catch (error) {
208
258
  const code = error.code;
209
259
  if (code === "EACCES" || code === "EPERM") throw new FileReadError("forbidden");
@@ -211,29 +261,29 @@ async function readFileForTheater(theaterPath, relativePath) {
211
261
  }
212
262
  if (!stat.isFile()) throw new FileReadError("not_a_file");
213
263
  if (stat.size > FILE_SIZE_CAP) {
214
- const fd = await fs3.promises.open(realResolved, "r");
264
+ const fd = await fs2.promises.open(realResolved, "r");
215
265
  const buffer2 = Buffer.alloc(FILE_SIZE_CAP);
216
266
  const { bytesRead } = await fd.read(buffer2, 0, FILE_SIZE_CAP, 0);
217
267
  await fd.close();
218
268
  const chunk = buffer2.subarray(0, bytesRead);
219
269
  if (isBinaryBuffer(chunk)) throw new FileReadError("binary_file");
220
270
  return {
221
- relativePath: path4.relative(realRoot, realResolved),
271
+ relativePath: path5.relative(realRoot, realResolved),
222
272
  content: chunk.toString("utf8"),
223
273
  lang: detectLang(realResolved),
224
274
  truncated: true
225
275
  };
226
276
  }
227
- const buffer = await fs3.promises.readFile(realResolved);
277
+ const buffer = await fs2.promises.readFile(realResolved);
228
278
  if (isBinaryBuffer(buffer)) throw new FileReadError("binary_file");
229
279
  return {
230
- relativePath: path4.relative(realRoot, realResolved),
280
+ relativePath: path5.relative(realRoot, realResolved),
231
281
  content: buffer.toString("utf8"),
232
282
  lang: detectLang(realResolved)
233
283
  };
234
284
  }
235
285
  function isWithinRoot(resolved, root) {
236
- const normalizedRoot = root.endsWith(path4.sep) ? root : root + path4.sep;
286
+ const normalizedRoot = root.endsWith(path5.sep) ? root : root + path5.sep;
237
287
  return resolved === root || resolved.startsWith(normalizedRoot);
238
288
  }
239
289
  function isBinaryBuffer(buffer) {
@@ -246,605 +296,646 @@ function isBinaryBuffer(buffer) {
246
296
  return nulCount / checkLen > BINARY_NUL_THRESHOLD;
247
297
  }
248
298
  function detectLang(filePath) {
249
- const ext = path4.extname(filePath).toLowerCase();
299
+ const ext = path5.extname(filePath).toLowerCase();
250
300
  return EXT_LANG_MAP[ext] ?? "plaintext";
251
301
  }
252
- var FolderBrowserError = class extends Error {
302
+ var ImageServeError = class extends Error {
253
303
  code;
254
304
  constructor(code) {
255
305
  super(code);
256
- this.name = "FolderBrowserError";
306
+ this.name = "ImageServeError";
257
307
  this.code = code;
258
308
  }
259
309
  };
260
- var DIRECTORY_ENTRY_CAP = 500;
261
- async function listTheaterContents(theaterPath, relativePath, deps = {}) {
262
- const opendir = deps.opendir ?? fs3.promises.opendir;
263
- const stat = deps.stat ?? fs3.promises.stat;
264
- const targetAbs = path4.resolve(theaterPath, relativePath);
265
- const normalizedRoot = theaterPath.endsWith(path4.sep) ? theaterPath : theaterPath + path4.sep;
266
- if (targetAbs !== theaterPath && !targetAbs.startsWith(normalizedRoot)) {
267
- throw new FolderBrowserError("forbidden");
268
- }
310
+ var IMAGE_SIZE_CAP = 16 * 1024 * 1024;
311
+ var MIME_ALLOWLIST = {
312
+ ".png": "image/png",
313
+ ".jpg": "image/jpeg",
314
+ ".jpeg": "image/jpeg",
315
+ ".webp": "image/webp",
316
+ ".gif": "image/gif"
317
+ };
318
+ var IMAGE_SECURITY_HEADERS = {
319
+ "X-Content-Type-Options": "nosniff",
320
+ "Referrer-Policy": "no-referrer",
321
+ "Cache-Control": "private, no-store"
322
+ };
323
+ async function readImageForTheater(theaterPath, relativePath) {
324
+ const resolved = path5.resolve(theaterPath, relativePath);
325
+ if (!isWithinRoot2(resolved, theaterPath)) throw new ImageServeError("path_outside_theater");
326
+ const ext = path5.extname(resolved).toLowerCase();
327
+ const mimeType = MIME_ALLOWLIST[ext];
328
+ if (!mimeType) throw new ImageServeError("mime_not_allowed");
329
+ let realResolved;
269
330
  let realRoot;
270
- let realTargetAbs;
271
331
  try {
272
- [realRoot, realTargetAbs] = await Promise.all([
273
- fs3.promises.realpath(theaterPath),
274
- fs3.promises.realpath(targetAbs)
332
+ [realResolved, realRoot] = await Promise.all([
333
+ fs2.promises.realpath(resolved),
334
+ fs2.promises.realpath(theaterPath)
275
335
  ]);
276
336
  } catch (error) {
277
- throw mapFsError(error);
278
- }
279
- const realNormalizedRoot = realRoot.endsWith(path4.sep) ? realRoot : realRoot + path4.sep;
280
- if (realTargetAbs !== realRoot && !realTargetAbs.startsWith(realNormalizedRoot)) {
281
- throw new FolderBrowserError("forbidden");
337
+ const code = error.code;
338
+ if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
339
+ throw new ImageServeError("not_found");
282
340
  }
283
- const targetStat = await statDirectory(realTargetAbs, stat);
284
- if (!targetStat.isDirectory()) throw new FolderBrowserError("invalid_path");
285
- const entries = [];
286
- const truncated = await collectContentsEntries(realTargetAbs, realRoot, opendir, stat, entries);
287
- entries.sort((a, b) => {
288
- if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
289
- return a.name.localeCompare(b.name);
290
- });
291
- const rel = path4.relative(realRoot, realTargetAbs);
292
- const parentRel = rel === "" ? null : path4.relative(realRoot, path4.dirname(realTargetAbs));
293
- return {
294
- relativePath: rel,
295
- parentRelativePath: parentRel === "" ? null : parentRel ?? null,
296
- entries,
297
- ...truncated ? { truncated: true } : {}
298
- };
299
- }
300
- async function statDirectory(targetPath, stat) {
341
+ if (!isWithinRoot2(realResolved, realRoot)) throw new ImageServeError("path_outside_theater");
342
+ let stat;
301
343
  try {
302
- return await stat(targetPath);
344
+ stat = await fs2.promises.stat(realResolved);
303
345
  } catch (error) {
304
- throw mapFsError(error);
346
+ const code = error.code;
347
+ if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
348
+ throw new ImageServeError("not_found");
305
349
  }
350
+ if (!stat.isFile()) throw new ImageServeError("not_a_file");
351
+ if (stat.size > IMAGE_SIZE_CAP) throw new ImageServeError("size_exceeded");
352
+ const buffer = await fs2.promises.readFile(realResolved);
353
+ return { mimeType, buffer };
306
354
  }
307
- async function collectContentsEntries(targetPath, theaterPath, opendir, stat, entries) {
308
- const directory = await openDirectory(targetPath, opendir);
309
- try {
310
- let dirent = await directory.read();
311
- while (dirent !== null) {
312
- if (entries.length >= DIRECTORY_ENTRY_CAP) return true;
313
- const entry = await toContentsEntry(targetPath, theaterPath, dirent, stat);
314
- if (entry !== null) entries.push(entry);
315
- dirent = await directory.read();
316
- }
317
- return false;
318
- } catch (error) {
319
- throw mapFsError(error);
320
- } finally {
321
- await directory.close();
322
- }
355
+ function writeImageResponse(res, result) {
356
+ res.writeHead(200, {
357
+ ...IMAGE_SECURITY_HEADERS,
358
+ "Content-Type": result.mimeType,
359
+ "Content-Length": result.buffer.length
360
+ });
361
+ res.end(result.buffer);
323
362
  }
324
- async function openDirectory(targetPath, opendir) {
325
- try {
326
- return await opendir(targetPath, { bufferSize: DIRECTORY_ENTRY_CAP + 1 });
327
- } catch (error) {
328
- throw mapFsError(error);
329
- }
363
+ function isWithinRoot2(resolved, root) {
364
+ const normalizedRoot = root.endsWith(path5.sep) ? root : root + path5.sep;
365
+ return resolved === root || resolved.startsWith(normalizedRoot);
330
366
  }
331
- async function toContentsEntry(targetPath, theaterPath, dirent, stat) {
332
- const entryPath = path4.join(targetPath, dirent.name);
333
- const rel = path4.relative(theaterPath, entryPath);
334
- if (dirent.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
335
- if (dirent.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
336
- if (dirent.isSymbolicLink()) {
337
- try {
338
- const realEntryPath = await fs3.promises.realpath(entryPath);
339
- const realNormalizedRoot = theaterPath.endsWith(path4.sep) ? theaterPath : theaterPath + path4.sep;
340
- if (realEntryPath !== theaterPath && !realEntryPath.startsWith(realNormalizedRoot)) return null;
341
- const s = await stat(realEntryPath);
342
- if (s.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
343
- if (s.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
344
- } catch {
345
- return null;
367
+ var DEBOUNCE_MS = 200;
368
+ var LINUX_DIRECTORY_WATCH_CAP = 1024;
369
+ var watcherRegistry = createWatcherRegistry();
370
+ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs2.watch(p, opts, (ev, fn) => cb(ev, typeof fn === "string" ? fn : null)), debounceMs = DEBOUNCE_MS, platform = process.platform) {
371
+ const entries = /* @__PURE__ */ new Map();
372
+ function scheduleChange(entry, relDir) {
373
+ const pending = entry.debounceTimers.get(relDir);
374
+ if (pending) clearTimeout(pending);
375
+ entry.debounceTimers.set(
376
+ relDir,
377
+ setTimeout(() => {
378
+ entry.debounceTimers.delete(relDir);
379
+ for (const sub of entry.subscribers) sub(relDir);
380
+ }, debounceMs)
381
+ );
382
+ }
383
+ function closeEntry(entry) {
384
+ for (const timer of entry.debounceTimers.values()) clearTimeout(timer);
385
+ entry.debounceTimers.clear();
386
+ for (const watcher of entry.watchers.values()) {
387
+ try {
388
+ watcher.close();
389
+ } catch {
390
+ }
346
391
  }
392
+ entry.watchers.clear();
393
+ entry.identityChecks.clear();
394
+ entry.pendingDirectories.clear();
347
395
  }
348
- return null;
349
- }
350
- function mapFsError(error) {
351
- const code = error.code;
352
- if (code === "EACCES" || code === "EPERM") return new FolderBrowserError("forbidden");
353
- if (code === "ENOENT" || code === "ENOTDIR") return new FolderBrowserError("not_found");
354
- return new FolderBrowserError("invalid_path");
355
- }
356
- var GitStatusPathError = class extends Error {
357
- code;
358
- constructor(code) {
359
- super(code);
360
- this.name = "GitStatusPathError";
361
- this.code = code;
396
+ async function rearmPendingDirectories(theaterId, entry) {
397
+ entry.rearmRequested = true;
398
+ if (entry.rearmRunning) return;
399
+ entry.rearmRunning = true;
400
+ try {
401
+ while (entry.rearmRequested) {
402
+ entry.rearmRequested = false;
403
+ for (const relativePath of entry.pendingDirectories) {
404
+ const target = await resolveTrackedDirectory(entry.theaterPath, relativePath);
405
+ if (!target) continue;
406
+ if (entries.get(theaterId) !== entry || !entry.pendingDirectories.has(relativePath) || entry.watchers.has(relativePath) || entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) continue;
407
+ try {
408
+ startWatcher(theaterId, entry, relativePath, target.path, target.identity);
409
+ entry.pendingDirectories.delete(relativePath);
410
+ scheduleChange(entry, relativePath);
411
+ } catch {
412
+ for (const notify of entry.stateSubscribers) notify("degraded");
413
+ }
414
+ }
415
+ }
416
+ } finally {
417
+ entry.rearmRunning = false;
418
+ }
362
419
  }
363
- };
364
- var GIT_TIMEOUT_MS = 5e3;
365
- var GIT_MAX_BUFFER = 8 * 1024 * 1024;
366
- var GIT_STATUS_CAP = 1e4;
367
- var UNMERGED_STATUS_PAIRS = /* @__PURE__ */ new Set(["UU", "UD", "DU", "AA", "AU", "UA", "DD"]);
368
- async function readTheaterGitStatus(theaterPath, deps = {}) {
369
- const realpath = deps.realpath ?? ((target) => fs8.realpath(target));
370
- const execGit = deps.execGit ?? executeGit;
371
- let theaterRootAbs;
372
- try {
373
- theaterRootAbs = await realpath(path4.resolve(theaterPath));
374
- } catch (error) {
375
- throw mapFsError2(error);
420
+ function invalidateWatcherTree(entry, watchKey) {
421
+ const descendantPrefix = watchKey + path5.sep;
422
+ for (const [candidateKey, candidateWatcher] of entry.watchers) {
423
+ if (candidateKey !== watchKey && !candidateKey.startsWith(descendantPrefix)) continue;
424
+ entry.watchers.delete(candidateKey);
425
+ entry.identityChecks.delete(candidateKey);
426
+ entry.pendingDirectories.add(candidateKey);
427
+ try {
428
+ candidateWatcher.close();
429
+ } catch {
430
+ }
431
+ }
376
432
  }
377
- try {
378
- const options = {
379
- env: sanitizeGitEnvironment(deps.environment ?? process.env),
380
- killSignal: "SIGKILL",
381
- maxBuffer: GIT_MAX_BUFFER,
382
- timeout: GIT_TIMEOUT_MS
383
- };
384
- const gitArgs = (args) => [
385
- "-c",
386
- "core.fsmonitor=false",
387
- "--no-optional-locks",
388
- "-C",
389
- theaterRootAbs,
390
- ...args
391
- ];
392
- const [prefixOutput, statusOutput] = await Promise.all([
393
- execGit(gitArgs(["rev-parse", "--show-prefix"]), options),
394
- execGit(gitArgs(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]), options)
395
- ]);
396
- const prefix = stripTrailingLineBreak(prefixOutput);
397
- const statuses = scopeGitStatusesToTheater(parseGitStatusPorcelainV1Z(statusOutput), prefix);
398
- const truncated = statuses.length > GIT_STATUS_CAP;
399
- return {
400
- ok: true,
401
- gitAvailable: true,
402
- statuses: statuses.slice(0, GIT_STATUS_CAP),
403
- ...truncated ? { truncated: true } : {}
433
+ function startWatcher(theaterId, entry, watchKey, watchPath, watchIdentity) {
434
+ let identityCheckRunning = false;
435
+ let identityCheckRequested = false;
436
+ let activeWatcher = null;
437
+ const lexicalWatchPath = path5.resolve(entry.theaterPath, watchKey);
438
+ const checkDirectoryIdentity = async (expectedWatcher) => {
439
+ identityCheckRequested = true;
440
+ if (identityCheckRunning || !watchIdentity) return;
441
+ identityCheckRunning = true;
442
+ try {
443
+ while (identityCheckRequested) {
444
+ identityCheckRequested = false;
445
+ let replaced = false;
446
+ try {
447
+ const stats = await fs2.promises.stat(lexicalWatchPath);
448
+ replaced = stats.dev !== watchIdentity.device || stats.ino !== watchIdentity.inode;
449
+ } catch (error) {
450
+ replaced = error.code === "ENOENT";
451
+ }
452
+ if (entry.watchers.get(watchKey) !== expectedWatcher) return;
453
+ if (!replaced) continue;
454
+ invalidateWatcherTree(entry, watchKey);
455
+ await rearmPendingDirectories(theaterId, entry);
456
+ return;
457
+ }
458
+ } finally {
459
+ identityCheckRunning = false;
460
+ }
404
461
  };
405
- } catch {
406
- return { ok: true, gitAvailable: false, statuses: [] };
462
+ const watcher = watcherFactory(watchPath, { recursive: entry.recursive }, (event, filename) => {
463
+ const currentWatcher = activeWatcher;
464
+ if (!currentWatcher || entry.watchers.get(watchKey) !== currentWatcher) return;
465
+ scheduleChange(entry, entry.recursive ? computeRelDir(filename) : watchKey);
466
+ if (!entry.recursive && event === "rename") {
467
+ if (watchKey !== "" && watchIdentity) void checkDirectoryIdentity(currentWatcher);
468
+ if (filename) {
469
+ const changedChildKey = path5.join(watchKey, filename);
470
+ entry.identityChecks.get(changedChildKey)?.();
471
+ }
472
+ }
473
+ if (!entry.recursive && entry.pendingDirectories.size > 0) {
474
+ void rearmPendingDirectories(theaterId, entry);
475
+ }
476
+ });
477
+ activeWatcher = watcher;
478
+ entry.watchers.set(watchKey, watcher);
479
+ if (watchIdentity) {
480
+ entry.identityChecks.set(watchKey, () => {
481
+ const currentWatcher = activeWatcher;
482
+ if (currentWatcher) void checkDirectoryIdentity(currentWatcher);
483
+ });
484
+ }
485
+ watcher.on("error", () => {
486
+ if (entry.watchers.get(watchKey) !== watcher) return;
487
+ try {
488
+ watcher.close();
489
+ } catch {
490
+ }
491
+ entry.watchers.delete(watchKey);
492
+ entry.identityChecks.delete(watchKey);
493
+ if (watchKey === "") {
494
+ closeEntry(entry);
495
+ if (entries.get(theaterId) === entry) entries.delete(theaterId);
496
+ } else {
497
+ const pending = entry.debounceTimers.get(watchKey);
498
+ if (pending) clearTimeout(pending);
499
+ entry.debounceTimers.delete(watchKey);
500
+ }
501
+ for (const notify of entry.stateSubscribers) notify("degraded");
502
+ });
407
503
  }
408
- }
409
- function parseGitStatusPorcelainV1Z(output) {
410
- const records = output.split("\0");
411
- const statuses = [];
412
- for (let index = 0; index < records.length; index += 1) {
413
- const record = records[index];
414
- if (!record || record.length < 4 || record[2] !== " ") continue;
415
- const indexStatus = record[0] ?? " ";
416
- const worktreeStatus = record[1] ?? " ";
417
- const statusPair = `${indexStatus}${worktreeStatus}`;
418
- const gitPath = record.slice(3);
419
- const isRenameOrCopy = statusPair.includes("R") || statusPair.includes("C");
420
- const status = classifyGitStatus(statusPair);
421
- if (status && gitPath) statuses.push({ gitPath, status });
422
- if (isRenameOrCopy && index + 1 < records.length) index += 1;
504
+ function makeUnsubscribe(theaterId, onChange, onState, entry) {
505
+ let called = false;
506
+ return () => {
507
+ if (called) return;
508
+ called = true;
509
+ entry.subscribers.delete(onChange);
510
+ entry.stateSubscribers.delete(onState);
511
+ entry.subscriberCount--;
512
+ if (entry.subscriberCount <= 0) {
513
+ closeEntry(entry);
514
+ if (entries.get(theaterId) === entry) entries.delete(theaterId);
515
+ }
516
+ };
423
517
  }
424
- return statuses;
425
- }
426
- function scopeGitStatusesToTheater(entries, prefix, separator = path4.sep) {
427
- const statuses = [];
428
- for (const entry of entries) {
429
- if (prefix && !entry.gitPath.startsWith(prefix)) continue;
430
- const relativeGitPath = prefix ? entry.gitPath.slice(prefix.length) : entry.gitPath;
431
- const segments = relativeGitPath.split("/");
432
- if (relativeGitPath === "" || relativeGitPath.startsWith("/") || segments.some((segment) => segment === "" || segment === "." || segment === "..")) continue;
433
- const relativePath = segments.join(separator);
434
- if (path4.isAbsolute(relativePath)) continue;
435
- statuses.push({ path: relativePath, status: entry.status });
518
+ function subscribe(theaterId, theaterPath, onChange, onState) {
519
+ const existing = entries.get(theaterId);
520
+ if (existing) {
521
+ existing.subscribers.add(onChange);
522
+ existing.stateSubscribers.add(onState);
523
+ existing.subscriberCount++;
524
+ onState("watching");
525
+ return makeUnsubscribe(theaterId, onChange, onState, existing);
526
+ }
527
+ const entry = {
528
+ // Linux의 recursive fs.watch fallback은 구독 시 전체 트리를 동기 순회한다.
529
+ // 대신 root와 실제로 조회된 디렉터리만 개별 비재귀 watcher로 추적한다.
530
+ theaterPath,
531
+ recursive: platform !== "linux",
532
+ watchers: /* @__PURE__ */ new Map(),
533
+ identityChecks: /* @__PURE__ */ new Map(),
534
+ pendingDirectories: /* @__PURE__ */ new Set(),
535
+ subscribers: /* @__PURE__ */ new Set([onChange]),
536
+ stateSubscribers: /* @__PURE__ */ new Set([onState]),
537
+ subscriberCount: 1,
538
+ debounceTimers: /* @__PURE__ */ new Map(),
539
+ rearmRunning: false,
540
+ rearmRequested: false
541
+ };
542
+ try {
543
+ startWatcher(theaterId, entry, "", theaterPath);
544
+ } catch {
545
+ onState("degraded");
546
+ return () => {
547
+ };
548
+ }
549
+ entries.set(theaterId, entry);
550
+ onState("watching");
551
+ return makeUnsubscribe(theaterId, onChange, onState, entry);
436
552
  }
437
- return statuses;
438
- }
439
- function classifyGitStatus(statusPair) {
440
- if (statusPair === "??") return "untracked";
441
- if (UNMERGED_STATUS_PAIRS.has(statusPair)) return "modified";
442
- if (statusPair.includes("D")) return "deleted";
443
- if ([...statusPair].some((status) => status === "A" || status === "M" || status === "R" || status === "C" || status === "T")) {
444
- return "modified";
553
+ async function trackDirectory(theaterId, theaterPath, relativePath) {
554
+ const entry = entries.get(theaterId);
555
+ if (!entry || entry.recursive || relativePath === "" || entry.watchers.has(relativePath)) return;
556
+ if (entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) {
557
+ for (const notify of entry.stateSubscribers) notify("degraded");
558
+ return;
559
+ }
560
+ const target = await resolveTrackedDirectory(theaterPath, relativePath);
561
+ if (!target) return;
562
+ if (entries.get(theaterId) !== entry || entry.watchers.has(relativePath) || entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) return;
563
+ try {
564
+ startWatcher(theaterId, entry, relativePath, target.path, target.identity);
565
+ entry.pendingDirectories.delete(relativePath);
566
+ } catch {
567
+ for (const notify of entry.stateSubscribers) notify("degraded");
568
+ }
445
569
  }
446
- return null;
447
- }
448
- function executeGit(args, options) {
449
- return new Promise((resolve, reject) => {
450
- execFile(
451
- "git",
452
- [...args],
453
- {
454
- encoding: "utf8",
455
- env: options.env,
456
- killSignal: options.killSignal,
457
- maxBuffer: options.maxBuffer,
458
- shell: false,
459
- timeout: options.timeout,
460
- windowsHide: true
461
- },
462
- (error, stdout) => {
463
- if (error) {
464
- reject(error);
465
- return;
466
- }
467
- resolve(stdout);
468
- }
469
- );
470
- });
570
+ return { subscribe, trackDirectory };
471
571
  }
472
- function sanitizeGitEnvironment(environment) {
473
- const sanitized = {};
474
- for (const [key, value] of Object.entries(environment)) {
475
- if (key.toUpperCase().startsWith("GIT_")) continue;
476
- if (value !== void 0) sanitized[key] = value;
572
+ async function resolveTrackedDirectory(theaterPath, relativePath) {
573
+ const resolvedRoot = path5.resolve(theaterPath);
574
+ const targetPath = path5.resolve(resolvedRoot, relativePath);
575
+ if (!isWithinRoot3(targetPath, resolvedRoot)) return null;
576
+ try {
577
+ const [realRoot, realTarget] = await Promise.all([
578
+ fs2.promises.realpath(theaterPath),
579
+ fs2.promises.realpath(targetPath)
580
+ ]);
581
+ if (!isWithinRoot3(realTarget, realRoot)) return null;
582
+ const stats = await fs2.promises.stat(realTarget);
583
+ if (!stats.isDirectory()) return null;
584
+ return {
585
+ path: realTarget,
586
+ identity: { device: stats.dev, inode: stats.ino }
587
+ };
588
+ } catch {
589
+ return null;
477
590
  }
478
- sanitized.GIT_OPTIONAL_LOCKS = "0";
479
- return sanitized;
480
591
  }
481
- function stripTrailingLineBreak(output) {
482
- if (!output.endsWith("\n")) return output;
483
- const withoutLineFeed = output.slice(0, -1);
484
- return withoutLineFeed.endsWith("\r") ? withoutLineFeed.slice(0, -1) : withoutLineFeed;
592
+ function isWithinRoot3(candidate, root) {
593
+ const normalizedRoot = root.endsWith(path5.sep) ? root : root + path5.sep;
594
+ return candidate === root || candidate.startsWith(normalizedRoot);
485
595
  }
486
- function mapFsError2(error) {
487
- const code = error.code;
488
- if (code === "EACCES" || code === "EPERM") return new GitStatusPathError("forbidden");
489
- if (code === "ENOENT" || code === "ENOTDIR") return new GitStatusPathError("not_found");
490
- return new GitStatusPathError("invalid_path");
596
+ function computeRelDir(filename) {
597
+ if (!filename) return "";
598
+ const lastSep = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
599
+ if (lastSep <= 0) return "";
600
+ return filename.slice(0, lastSep);
491
601
  }
492
- var ImageServeError = class extends Error {
602
+
603
+ // ../fleet-plugins/file-explorer/server/tree-services.ts
604
+ var FolderBrowserError = class extends Error {
493
605
  code;
494
606
  constructor(code) {
495
607
  super(code);
496
- this.name = "ImageServeError";
608
+ this.name = "FolderBrowserError";
497
609
  this.code = code;
498
610
  }
499
611
  };
500
- var IMAGE_SIZE_CAP = 16 * 1024 * 1024;
501
- var MIME_ALLOWLIST = {
502
- ".png": "image/png",
503
- ".jpg": "image/jpeg",
504
- ".jpeg": "image/jpeg",
505
- ".webp": "image/webp",
506
- ".gif": "image/gif"
507
- };
508
- var IMAGE_SECURITY_HEADERS = {
509
- "X-Content-Type-Options": "nosniff",
510
- "Referrer-Policy": "no-referrer",
511
- "Cache-Control": "private, no-store"
512
- };
513
- async function readImageForTheater(theaterPath, relativePath) {
514
- const resolved = path4.resolve(theaterPath, relativePath);
515
- if (!isWithinRoot2(resolved, theaterPath)) throw new ImageServeError("path_outside_theater");
516
- const ext = path4.extname(resolved).toLowerCase();
517
- const mimeType = MIME_ALLOWLIST[ext];
518
- if (!mimeType) throw new ImageServeError("mime_not_allowed");
519
- let realResolved;
520
- let realRoot;
521
- try {
522
- [realResolved, realRoot] = await Promise.all([
523
- fs3.promises.realpath(resolved),
524
- fs3.promises.realpath(theaterPath)
525
- ]);
526
- } catch (error) {
527
- const code = error.code;
528
- if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
529
- throw new ImageServeError("not_found");
612
+ var DIRECTORY_ENTRY_CAP = 500;
613
+ async function listTheaterContents(theaterPath, relativePath, deps = {}) {
614
+ const opendir = deps.opendir ?? fs2.promises.opendir;
615
+ const stat = deps.stat ?? fs2.promises.stat;
616
+ const targetAbs = path5.resolve(theaterPath, relativePath);
617
+ const normalizedRoot = theaterPath.endsWith(path5.sep) ? theaterPath : theaterPath + path5.sep;
618
+ if (targetAbs !== theaterPath && !targetAbs.startsWith(normalizedRoot)) {
619
+ throw new FolderBrowserError("forbidden");
530
620
  }
531
- if (!isWithinRoot2(realResolved, realRoot)) throw new ImageServeError("path_outside_theater");
532
- let stat;
621
+ let realRoot;
622
+ let realTargetAbs;
533
623
  try {
534
- stat = await fs3.promises.stat(realResolved);
624
+ [realRoot, realTargetAbs] = await Promise.all([
625
+ fs2.promises.realpath(theaterPath),
626
+ fs2.promises.realpath(targetAbs)
627
+ ]);
535
628
  } catch (error) {
536
- const code = error.code;
537
- if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
538
- throw new ImageServeError("not_found");
629
+ throw mapFolderBrowserFsError(error);
539
630
  }
540
- if (!stat.isFile()) throw new ImageServeError("not_a_file");
541
- if (stat.size > IMAGE_SIZE_CAP) throw new ImageServeError("size_exceeded");
542
- const buffer = await fs3.promises.readFile(realResolved);
543
- return { mimeType, buffer };
544
- }
545
- function writeImageResponse(res, result) {
546
- res.writeHead(200, {
547
- ...IMAGE_SECURITY_HEADERS,
548
- "Content-Type": result.mimeType,
549
- "Content-Length": result.buffer.length
631
+ const realNormalizedRoot = realRoot.endsWith(path5.sep) ? realRoot : realRoot + path5.sep;
632
+ if (realTargetAbs !== realRoot && !realTargetAbs.startsWith(realNormalizedRoot)) {
633
+ throw new FolderBrowserError("forbidden");
634
+ }
635
+ const targetStat = await statDirectory(realTargetAbs, stat);
636
+ if (!targetStat.isDirectory()) throw new FolderBrowserError("invalid_path");
637
+ const entries = [];
638
+ const truncated = await collectContentsEntries(realTargetAbs, realRoot, opendir, stat, entries);
639
+ entries.sort((a, b) => {
640
+ if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
641
+ return a.name.localeCompare(b.name);
550
642
  });
551
- res.end(result.buffer);
552
- }
553
- function isWithinRoot2(resolved, root) {
554
- const normalizedRoot = root.endsWith(path4.sep) ? root : root + path4.sep;
555
- return resolved === root || resolved.startsWith(normalizedRoot);
643
+ const rel = path5.relative(realRoot, realTargetAbs);
644
+ const parentRel = rel === "" ? null : path5.relative(realRoot, path5.dirname(realTargetAbs));
645
+ return {
646
+ relativePath: rel,
647
+ parentRelativePath: parentRel === "" ? null : parentRel ?? null,
648
+ entries,
649
+ ...truncated ? { truncated: true } : {}
650
+ };
556
651
  }
557
- var FileActionUnavailableError = class extends Error {
558
- constructor() {
559
- super("action_unavailable");
560
- this.name = "FileActionUnavailableError";
561
- }
562
- };
563
- var DEFAULT_DEPENDENCIES2 = {
564
- platform: process.platform,
565
- launch: spawnDetached
566
- };
567
- async function revealPath(theaterPath, relativePath, mode, dependencies = {}) {
568
- const deps = { ...DEFAULT_DEPENDENCIES2, ...dependencies };
569
- const { realPath } = await resolveContainedActionPath(theaterPath, relativePath);
570
- const command = resolveRevealCommand(deps.platform, mode, realPath);
571
- if (!command) throw new FileActionUnavailableError();
652
+ async function statDirectory(targetPath, stat) {
572
653
  try {
573
- await deps.launch(command.file, command.args);
654
+ return await stat(targetPath);
574
655
  } catch (error) {
575
- if (error.code === "ENOENT") throw new FileActionUnavailableError();
576
- throw error;
656
+ throw mapFolderBrowserFsError(error);
577
657
  }
578
658
  }
579
- function resolveRevealCommand(platform, mode, absolutePath) {
580
- if (platform === "darwin") {
581
- return mode === "reveal" ? { file: "open", args: ["-R", absolutePath] } : { file: "open", args: [absolutePath] };
659
+ async function collectContentsEntries(targetPath, theaterPath, opendir, stat, entries) {
660
+ const directory = await openDirectory(targetPath, opendir);
661
+ try {
662
+ let dirent = await directory.read();
663
+ while (dirent !== null) {
664
+ if (entries.length >= DIRECTORY_ENTRY_CAP) return true;
665
+ const entry = await toContentsEntry(targetPath, theaterPath, dirent, stat);
666
+ if (entry !== null) entries.push(entry);
667
+ dirent = await directory.read();
668
+ }
669
+ return false;
670
+ } catch (error) {
671
+ throw mapFolderBrowserFsError(error);
672
+ } finally {
673
+ await directory.close();
582
674
  }
583
- if (platform === "linux") {
584
- return {
585
- file: "xdg-open",
586
- args: [mode === "reveal" ? path4.dirname(absolutePath) : absolutePath]
587
- };
675
+ }
676
+ async function openDirectory(targetPath, opendir) {
677
+ try {
678
+ return await opendir(targetPath, { bufferSize: DIRECTORY_ENTRY_CAP + 1 });
679
+ } catch (error) {
680
+ throw mapFolderBrowserFsError(error);
588
681
  }
589
- if (platform === "win32") {
590
- return mode === "reveal" ? { file: "explorer", args: [`/select,${absolutePath}`] } : { file: "explorer", args: [absolutePath] };
682
+ }
683
+ async function toContentsEntry(targetPath, theaterPath, dirent, stat) {
684
+ const entryPath = path5.join(targetPath, dirent.name);
685
+ const rel = path5.relative(theaterPath, entryPath);
686
+ if (dirent.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
687
+ if (dirent.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
688
+ if (dirent.isSymbolicLink()) {
689
+ try {
690
+ const realEntryPath = await fs2.promises.realpath(entryPath);
691
+ const realNormalizedRoot = theaterPath.endsWith(path5.sep) ? theaterPath : theaterPath + path5.sep;
692
+ if (realEntryPath !== theaterPath && !realEntryPath.startsWith(realNormalizedRoot)) return null;
693
+ const s = await stat(realEntryPath);
694
+ if (s.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
695
+ if (s.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
696
+ } catch {
697
+ return null;
698
+ }
591
699
  }
592
700
  return null;
593
701
  }
594
- function spawnDetached(file, args) {
595
- return new Promise((resolve, reject) => {
596
- const child = spawn(file, [...args], {
597
- detached: true,
598
- shell: false,
599
- stdio: "ignore",
600
- windowsHide: true
601
- });
602
- child.once("error", reject);
603
- child.once("spawn", () => {
604
- child.unref();
605
- resolve();
606
- });
607
- });
702
+ function mapFolderBrowserFsError(error) {
703
+ const code = error.code;
704
+ if (code === "EACCES" || code === "EPERM") return new FolderBrowserError("forbidden");
705
+ if (code === "ENOENT" || code === "ENOTDIR") return new FolderBrowserError("not_found");
706
+ return new FolderBrowserError("invalid_path");
608
707
  }
609
- var DEBOUNCE_MS = 200;
610
- var LINUX_DIRECTORY_WATCH_CAP = 1024;
611
- var watcherRegistry = createWatcherRegistry();
612
- function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs3.watch(p, opts, (ev, fn) => cb(ev, typeof fn === "string" ? fn : null)), debounceMs = DEBOUNCE_MS, platform = process.platform) {
613
- const entries = /* @__PURE__ */ new Map();
614
- function scheduleChange(entry, relDir) {
615
- const pending = entry.debounceTimers.get(relDir);
616
- if (pending) clearTimeout(pending);
617
- entry.debounceTimers.set(
618
- relDir,
619
- setTimeout(() => {
620
- entry.debounceTimers.delete(relDir);
621
- for (const sub of entry.subscribers) sub(relDir);
622
- }, debounceMs)
623
- );
708
+ var GitStatusPathError = class extends Error {
709
+ code;
710
+ constructor(code) {
711
+ super(code);
712
+ this.name = "GitStatusPathError";
713
+ this.code = code;
624
714
  }
625
- function closeEntry(entry) {
626
- for (const timer of entry.debounceTimers.values()) clearTimeout(timer);
627
- entry.debounceTimers.clear();
628
- for (const watcher of entry.watchers.values()) {
629
- try {
630
- watcher.close();
631
- } catch {
632
- }
633
- }
634
- entry.watchers.clear();
635
- entry.identityChecks.clear();
636
- entry.pendingDirectories.clear();
715
+ };
716
+ var GIT_TIMEOUT_MS = 5e3;
717
+ var GIT_MAX_BUFFER = 8 * 1024 * 1024;
718
+ var GIT_STATUS_CAP = 1e4;
719
+ var UNMERGED_STATUS_PAIRS = /* @__PURE__ */ new Set(["UU", "UD", "DU", "AA", "AU", "UA", "DD"]);
720
+ async function readTheaterGitStatus(theaterPath, deps = {}) {
721
+ const realpath = deps.realpath ?? ((target) => fsp.realpath(target));
722
+ const execGit = deps.execGit ?? executeGit;
723
+ let theaterRootAbs;
724
+ try {
725
+ theaterRootAbs = await realpath(path5.resolve(theaterPath));
726
+ } catch (error) {
727
+ throw mapGitStatusFsError(error);
637
728
  }
638
- async function rearmPendingDirectories(theaterId, entry) {
639
- entry.rearmRequested = true;
640
- if (entry.rearmRunning) return;
641
- entry.rearmRunning = true;
642
- try {
643
- while (entry.rearmRequested) {
644
- entry.rearmRequested = false;
645
- for (const relativePath of entry.pendingDirectories) {
646
- const target = await resolveTrackedDirectory(entry.theaterPath, relativePath);
647
- if (!target) continue;
648
- if (entries.get(theaterId) !== entry || !entry.pendingDirectories.has(relativePath) || entry.watchers.has(relativePath) || entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) continue;
649
- try {
650
- startWatcher(theaterId, entry, relativePath, target.path, target.identity);
651
- entry.pendingDirectories.delete(relativePath);
652
- scheduleChange(entry, relativePath);
653
- } catch {
654
- for (const notify of entry.stateSubscribers) notify("degraded");
655
- }
656
- }
657
- }
658
- } finally {
659
- entry.rearmRunning = false;
660
- }
729
+ try {
730
+ const options = {
731
+ env: sanitizeGitEnvironment(deps.environment ?? process.env),
732
+ killSignal: "SIGKILL",
733
+ maxBuffer: GIT_MAX_BUFFER,
734
+ timeout: GIT_TIMEOUT_MS
735
+ };
736
+ const gitArgs = (args) => [
737
+ "-c",
738
+ "core.fsmonitor=false",
739
+ "--no-optional-locks",
740
+ "-C",
741
+ theaterRootAbs,
742
+ ...args
743
+ ];
744
+ const [prefixOutput, statusOutput] = await Promise.all([
745
+ execGit(gitArgs(["rev-parse", "--show-prefix"]), options),
746
+ execGit(gitArgs(["status", "--porcelain=v1", "-z", "--untracked-files=all", "--", "."]), options)
747
+ ]);
748
+ const prefix = stripTrailingLineBreak(prefixOutput);
749
+ const statuses = scopeGitStatusesToTheater(parseGitStatusPorcelainV1Z(statusOutput), prefix);
750
+ const truncated = statuses.length > GIT_STATUS_CAP;
751
+ return {
752
+ ok: true,
753
+ gitAvailable: true,
754
+ statuses: statuses.slice(0, GIT_STATUS_CAP),
755
+ ...truncated ? { truncated: true } : {}
756
+ };
757
+ } catch {
758
+ return { ok: true, gitAvailable: false, statuses: [] };
661
759
  }
662
- function invalidateWatcherTree(entry, watchKey) {
663
- const descendantPrefix = watchKey + path4.sep;
664
- for (const [candidateKey, candidateWatcher] of entry.watchers) {
665
- if (candidateKey !== watchKey && !candidateKey.startsWith(descendantPrefix)) continue;
666
- entry.watchers.delete(candidateKey);
667
- entry.identityChecks.delete(candidateKey);
668
- entry.pendingDirectories.add(candidateKey);
669
- try {
670
- candidateWatcher.close();
671
- } catch {
672
- }
673
- }
760
+ }
761
+ function parseGitStatusPorcelainV1Z(output) {
762
+ const records = output.split("\0");
763
+ const statuses = [];
764
+ for (let index = 0; index < records.length; index += 1) {
765
+ const record = records[index];
766
+ if (!record || record.length < 4 || record[2] !== " ") continue;
767
+ const indexStatus = record[0] ?? " ";
768
+ const worktreeStatus = record[1] ?? " ";
769
+ const statusPair = `${indexStatus}${worktreeStatus}`;
770
+ const gitPath = record.slice(3);
771
+ const isRenameOrCopy = statusPair.includes("R") || statusPair.includes("C");
772
+ const status = classifyGitStatus(statusPair);
773
+ if (status && gitPath) statuses.push({ gitPath, status });
774
+ if (isRenameOrCopy && index + 1 < records.length) index += 1;
674
775
  }
675
- function startWatcher(theaterId, entry, watchKey, watchPath, watchIdentity) {
676
- let identityCheckRunning = false;
677
- let identityCheckRequested = false;
678
- let activeWatcher = null;
679
- const lexicalWatchPath = path4.resolve(entry.theaterPath, watchKey);
680
- const checkDirectoryIdentity = async (expectedWatcher) => {
681
- identityCheckRequested = true;
682
- if (identityCheckRunning || !watchIdentity) return;
683
- identityCheckRunning = true;
684
- try {
685
- while (identityCheckRequested) {
686
- identityCheckRequested = false;
687
- let replaced = false;
688
- try {
689
- const stats = await fs3.promises.stat(lexicalWatchPath);
690
- replaced = stats.dev !== watchIdentity.device || stats.ino !== watchIdentity.inode;
691
- } catch (error) {
692
- replaced = error.code === "ENOENT";
693
- }
694
- if (entry.watchers.get(watchKey) !== expectedWatcher) return;
695
- if (!replaced) continue;
696
- invalidateWatcherTree(entry, watchKey);
697
- await rearmPendingDirectories(theaterId, entry);
698
- return;
699
- }
700
- } finally {
701
- identityCheckRunning = false;
702
- }
703
- };
704
- const watcher = watcherFactory(watchPath, { recursive: entry.recursive }, (event, filename) => {
705
- const currentWatcher = activeWatcher;
706
- if (!currentWatcher || entry.watchers.get(watchKey) !== currentWatcher) return;
707
- scheduleChange(entry, entry.recursive ? computeRelDir(filename) : watchKey);
708
- if (!entry.recursive && event === "rename") {
709
- if (watchKey !== "" && watchIdentity) void checkDirectoryIdentity(currentWatcher);
710
- if (filename) {
711
- const changedChildKey = path4.join(watchKey, filename);
712
- entry.identityChecks.get(changedChildKey)?.();
713
- }
714
- }
715
- if (!entry.recursive && entry.pendingDirectories.size > 0) {
716
- void rearmPendingDirectories(theaterId, entry);
717
- }
718
- });
719
- activeWatcher = watcher;
720
- entry.watchers.set(watchKey, watcher);
721
- if (watchIdentity) {
722
- entry.identityChecks.set(watchKey, () => {
723
- const currentWatcher = activeWatcher;
724
- if (currentWatcher) void checkDirectoryIdentity(currentWatcher);
725
- });
726
- }
727
- watcher.on("error", () => {
728
- if (entry.watchers.get(watchKey) !== watcher) return;
729
- try {
730
- watcher.close();
731
- } catch {
732
- }
733
- entry.watchers.delete(watchKey);
734
- entry.identityChecks.delete(watchKey);
735
- if (watchKey === "") {
736
- closeEntry(entry);
737
- if (entries.get(theaterId) === entry) entries.delete(theaterId);
738
- } else {
739
- const pending = entry.debounceTimers.get(watchKey);
740
- if (pending) clearTimeout(pending);
741
- entry.debounceTimers.delete(watchKey);
742
- }
743
- for (const notify of entry.stateSubscribers) notify("degraded");
744
- });
776
+ return statuses;
777
+ }
778
+ function scopeGitStatusesToTheater(entries, prefix, separator = path5.sep) {
779
+ const statuses = [];
780
+ for (const entry of entries) {
781
+ if (prefix && !entry.gitPath.startsWith(prefix)) continue;
782
+ const relativeGitPath = prefix ? entry.gitPath.slice(prefix.length) : entry.gitPath;
783
+ const segments = relativeGitPath.split("/");
784
+ if (relativeGitPath === "" || relativeGitPath.startsWith("/") || segments.some((segment) => segment === "" || segment === "." || segment === "..")) continue;
785
+ const relativePath = segments.join(separator);
786
+ if (path5.isAbsolute(relativePath)) continue;
787
+ statuses.push({ path: relativePath, status: entry.status });
745
788
  }
746
- function makeUnsubscribe(theaterId, onChange, onState, entry) {
747
- let called = false;
748
- return () => {
749
- if (called) return;
750
- called = true;
751
- entry.subscribers.delete(onChange);
752
- entry.stateSubscribers.delete(onState);
753
- entry.subscriberCount--;
754
- if (entry.subscriberCount <= 0) {
755
- closeEntry(entry);
756
- if (entries.get(theaterId) === entry) entries.delete(theaterId);
789
+ return statuses;
790
+ }
791
+ function classifyGitStatus(statusPair) {
792
+ if (statusPair === "??") return "untracked";
793
+ if (UNMERGED_STATUS_PAIRS.has(statusPair)) return "modified";
794
+ if (statusPair.includes("D")) return "deleted";
795
+ if ([...statusPair].some((status) => status === "A" || status === "M" || status === "R" || status === "C" || status === "T")) {
796
+ return "modified";
797
+ }
798
+ return null;
799
+ }
800
+ function executeGit(args, options) {
801
+ return new Promise((resolve, reject) => {
802
+ execFile(
803
+ "git",
804
+ [...args],
805
+ {
806
+ encoding: "utf8",
807
+ env: options.env,
808
+ killSignal: options.killSignal,
809
+ maxBuffer: options.maxBuffer,
810
+ shell: false,
811
+ timeout: options.timeout,
812
+ windowsHide: true
813
+ },
814
+ (error, stdout) => {
815
+ if (error) {
816
+ reject(error);
817
+ return;
818
+ }
819
+ resolve(stdout);
757
820
  }
758
- };
821
+ );
822
+ });
823
+ }
824
+ function sanitizeGitEnvironment(environment) {
825
+ const sanitized = {};
826
+ for (const [key, value] of Object.entries(environment)) {
827
+ const normalizedKey = key.toUpperCase();
828
+ if (normalizedKey.startsWith("GIT_") || normalizedKey === "LC_ALL" || normalizedKey === "SSH_ASKPASS" || normalizedKey === "SSH_ASKPASS_REQUIRE") continue;
829
+ if (value !== void 0) sanitized[key] = value;
759
830
  }
760
- function subscribe(theaterId, theaterPath, onChange, onState) {
761
- const existing = entries.get(theaterId);
762
- if (existing) {
763
- existing.subscribers.add(onChange);
764
- existing.stateSubscribers.add(onState);
765
- existing.subscriberCount++;
766
- onState("watching");
767
- return makeUnsubscribe(theaterId, onChange, onState, existing);
768
- }
769
- const entry = {
770
- // Linux의 recursive fs.watch fallback은 구독 시 전체 트리를 동기 순회한다.
771
- // 대신 root와 실제로 조회된 디렉터리만 개별 비재귀 watcher로 추적한다.
772
- theaterPath,
773
- recursive: platform !== "linux",
774
- watchers: /* @__PURE__ */ new Map(),
775
- identityChecks: /* @__PURE__ */ new Map(),
776
- pendingDirectories: /* @__PURE__ */ new Set(),
777
- subscribers: /* @__PURE__ */ new Set([onChange]),
778
- stateSubscribers: /* @__PURE__ */ new Set([onState]),
779
- subscriberCount: 1,
780
- debounceTimers: /* @__PURE__ */ new Map(),
781
- rearmRunning: false,
782
- rearmRequested: false
783
- };
831
+ sanitized.GIT_OPTIONAL_LOCKS = "0";
832
+ sanitized.GIT_TERMINAL_PROMPT = "0";
833
+ sanitized.GIT_ALLOW_PROTOCOL = "ssh:git:http:https:file";
834
+ sanitized.LC_ALL = "C";
835
+ return sanitized;
836
+ }
837
+ function stripTrailingLineBreak(output) {
838
+ if (!output.endsWith("\n")) return output;
839
+ const withoutLineFeed = output.slice(0, -1);
840
+ return withoutLineFeed.endsWith("\r") ? withoutLineFeed.slice(0, -1) : withoutLineFeed;
841
+ }
842
+ function mapGitStatusFsError(error) {
843
+ const code = error.code;
844
+ if (code === "EACCES" || code === "EPERM") return new GitStatusPathError("forbidden");
845
+ if (code === "ENOENT" || code === "ENOTDIR") return new GitStatusPathError("not_found");
846
+ return new GitStatusPathError("invalid_path");
847
+ }
848
+ var SEARCH_DIRECTORY_CAP = 500;
849
+ var SEARCH_ENTRY_CAP = 25e3;
850
+ async function searchTheaterFiles(theaterPath, query, limit) {
851
+ const realRoot = await fsp.realpath(theaterPath);
852
+ const tokens = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
853
+ const pending = [{ absolutePath: realRoot, relativePath: "" }];
854
+ const visited = /* @__PURE__ */ new Set();
855
+ const matches = [];
856
+ let directoryCount = 0;
857
+ let entryCount = 0;
858
+ while (pending.length > 0 && directoryCount < SEARCH_DIRECTORY_CAP && entryCount < SEARCH_ENTRY_CAP) {
859
+ const directory = pending.shift();
860
+ if (!directory || visited.has(directory.absolutePath)) continue;
861
+ visited.add(directory.absolutePath);
862
+ directoryCount += 1;
863
+ let entries;
784
864
  try {
785
- startWatcher(theaterId, entry, "", theaterPath);
865
+ entries = await fsp.readdir(directory.absolutePath, { withFileTypes: true });
786
866
  } catch {
787
- onState("degraded");
788
- return () => {
789
- };
790
- }
791
- entries.set(theaterId, entry);
792
- onState("watching");
793
- return makeUnsubscribe(theaterId, onChange, onState, entry);
794
- }
795
- async function trackDirectory(theaterId, theaterPath, relativePath) {
796
- const entry = entries.get(theaterId);
797
- if (!entry || entry.recursive || relativePath === "" || entry.watchers.has(relativePath)) return;
798
- if (entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) {
799
- for (const notify of entry.stateSubscribers) notify("degraded");
800
- return;
867
+ continue;
801
868
  }
802
- const target = await resolveTrackedDirectory(theaterPath, relativePath);
803
- if (!target) return;
804
- if (entries.get(theaterId) !== entry || entry.watchers.has(relativePath) || entry.watchers.size >= LINUX_DIRECTORY_WATCH_CAP) return;
805
- try {
806
- startWatcher(theaterId, entry, relativePath, target.path, target.identity);
807
- entry.pendingDirectories.delete(relativePath);
808
- } catch {
809
- for (const notify of entry.stateSubscribers) notify("degraded");
869
+ entries.sort((left, right) => left.name.localeCompare(right.name));
870
+ for (const entry of entries) {
871
+ if (entryCount >= SEARCH_ENTRY_CAP) break;
872
+ entryCount += 1;
873
+ const relativePath = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
874
+ const absolutePath = path5.join(directory.absolutePath, entry.name);
875
+ let kind = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : null;
876
+ let realPath = absolutePath;
877
+ if (entry.isSymbolicLink()) {
878
+ try {
879
+ realPath = await fsp.realpath(absolutePath);
880
+ if (!isContained(realRoot, realPath)) continue;
881
+ const stat = await fsp.stat(realPath);
882
+ kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : null;
883
+ } catch {
884
+ continue;
885
+ }
886
+ }
887
+ if (kind === "dir") {
888
+ if (!visited.has(realPath)) pending.push({ absolutePath: realPath, relativePath });
889
+ continue;
890
+ }
891
+ if (kind !== "file") continue;
892
+ const low = relativePath.toLocaleLowerCase();
893
+ if (tokens.every((token) => low.includes(token))) matches.push({ relativePath });
810
894
  }
811
895
  }
812
- return { subscribe, trackDirectory };
896
+ return matches.sort((left, right) => compareFileSearchItem(left, right, query)).slice(0, limit);
813
897
  }
814
- async function resolveTrackedDirectory(theaterPath, relativePath) {
815
- const resolvedRoot = path4.resolve(theaterPath);
816
- const targetPath = path4.resolve(resolvedRoot, relativePath);
817
- if (!isWithinRoot3(targetPath, resolvedRoot)) return null;
898
+ async function handleFilesSearch(req, res, ctx) {
899
+ if (req.method !== "POST") {
900
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
901
+ return;
902
+ }
903
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
904
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
905
+ return;
906
+ }
907
+ const body = await ctx.host.http.readJsonBody(req);
908
+ if (!isPlainObject(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8) {
909
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
910
+ return;
911
+ }
912
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
913
+ if (!theaterPath) {
914
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
915
+ return;
916
+ }
818
917
  try {
819
- const [realRoot, realTarget] = await Promise.all([
820
- fs3.promises.realpath(theaterPath),
821
- fs3.promises.realpath(targetPath)
822
- ]);
823
- if (!isWithinRoot3(realTarget, realRoot)) return null;
824
- const stats = await fs3.promises.stat(realTarget);
825
- if (!stats.isDirectory()) return null;
826
- return {
827
- path: realTarget,
828
- identity: { device: stats.dev, inode: stats.ino }
829
- };
918
+ ctx.host.http.writeJson(res, 200, {
919
+ files: await searchTheaterFiles(theaterPath, body.query, body.limit)
920
+ });
830
921
  } catch {
831
- return null;
922
+ ctx.host.http.writeJson(res, 500, { error: "search_failed" });
832
923
  }
833
924
  }
834
- function isWithinRoot3(candidate, root) {
835
- const normalizedRoot = root.endsWith(path4.sep) ? root : root + path4.sep;
836
- return candidate === root || candidate.startsWith(normalizedRoot);
925
+ function compareFileSearchItem(left, right, query) {
926
+ const lowQuery = query.trim().toLocaleLowerCase();
927
+ const leftName = left.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
928
+ const rightName = right.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
929
+ const leftRank = leftName === lowQuery ? 0 : leftName.startsWith(lowQuery) ? 1 : 2;
930
+ const rightRank = rightName === lowQuery ? 0 : rightName.startsWith(lowQuery) ? 1 : 2;
931
+ return leftRank - rightRank || left.relativePath.localeCompare(right.relativePath);
837
932
  }
838
- function computeRelDir(filename) {
839
- if (!filename) return "";
840
- const lastSep = Math.max(filename.lastIndexOf("/"), filename.lastIndexOf("\\"));
841
- if (lastSep <= 0) return "";
842
- return filename.slice(0, lastSep);
933
+ function isContained(root, candidate) {
934
+ const relative = path5.relative(root, candidate);
935
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path5.sep}`) && !path5.isAbsolute(relative);
843
936
  }
844
-
845
- // ../fleet-plugins/file-explorer/server/handlers.ts
846
937
  var DEFAULT_CLIPBOARD_DEPENDENCIES = { copyPath: copyPathToClipboard };
847
- var DEFAULT_REVEAL_DEPENDENCIES = { revealPath };
938
+ var DEFAULT_REVEAL_DEPENDENCIES2 = { revealPath };
848
939
  function isPlainObject(value) {
849
940
  return typeof value === "object" && value !== null && !Array.isArray(value);
850
941
  }
@@ -1031,7 +1122,7 @@ async function handleFilesClipboard(req, res, ctx, dependencies = DEFAULT_CLIPBO
1031
1122
  ctx.host.http.writeJson(res, 500, { error: "clipboard_failed" });
1032
1123
  }
1033
1124
  }
1034
- async function handleFilesReveal(req, res, ctx, dependencies = DEFAULT_REVEAL_DEPENDENCIES) {
1125
+ async function handleFilesReveal(req, res, ctx, dependencies = DEFAULT_REVEAL_DEPENDENCIES2) {
1035
1126
  if (req.method !== "POST") {
1036
1127
  ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
1037
1128
  return;
@@ -1127,98 +1218,6 @@ function writePathActionError(res, ctx, error) {
1127
1218
  const status = error.code === "not_found" ? 404 : 403;
1128
1219
  ctx.host.http.writeJson(res, status, { error: error.code });
1129
1220
  }
1130
- var SEARCH_DIRECTORY_CAP = 500;
1131
- var SEARCH_ENTRY_CAP = 25e3;
1132
- async function searchTheaterFiles(theaterPath, query, limit) {
1133
- const realRoot = await fs8.realpath(theaterPath);
1134
- const tokens = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
1135
- const pending = [{ absolutePath: realRoot, relativePath: "" }];
1136
- const visited = /* @__PURE__ */ new Set();
1137
- const matches = [];
1138
- let directoryCount = 0;
1139
- let entryCount = 0;
1140
- while (pending.length > 0 && directoryCount < SEARCH_DIRECTORY_CAP && entryCount < SEARCH_ENTRY_CAP) {
1141
- const directory = pending.shift();
1142
- if (!directory || visited.has(directory.absolutePath)) continue;
1143
- visited.add(directory.absolutePath);
1144
- directoryCount += 1;
1145
- let entries;
1146
- try {
1147
- entries = await fs8.readdir(directory.absolutePath, { withFileTypes: true });
1148
- } catch {
1149
- continue;
1150
- }
1151
- entries.sort((left, right) => left.name.localeCompare(right.name));
1152
- for (const entry of entries) {
1153
- if (entryCount >= SEARCH_ENTRY_CAP) break;
1154
- entryCount += 1;
1155
- const relativePath = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
1156
- const absolutePath = path4.join(directory.absolutePath, entry.name);
1157
- let kind = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : null;
1158
- let realPath = absolutePath;
1159
- if (entry.isSymbolicLink()) {
1160
- try {
1161
- realPath = await fs8.realpath(absolutePath);
1162
- if (!isContained(realRoot, realPath)) continue;
1163
- const stat = await fs8.stat(realPath);
1164
- kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : null;
1165
- } catch {
1166
- continue;
1167
- }
1168
- }
1169
- if (kind === "dir") {
1170
- if (!visited.has(realPath)) pending.push({ absolutePath: realPath, relativePath });
1171
- continue;
1172
- }
1173
- if (kind !== "file") continue;
1174
- const low = relativePath.toLocaleLowerCase();
1175
- if (tokens.every((token) => low.includes(token))) matches.push({ relativePath });
1176
- }
1177
- }
1178
- return matches.sort((left, right) => compareFileSearchItem(left, right, query)).slice(0, limit);
1179
- }
1180
- async function handleFilesSearch(req, res, ctx) {
1181
- if (req.method !== "POST") {
1182
- ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
1183
- return;
1184
- }
1185
- if (!ctx.host.security.isTerminalAuthorized(req)) {
1186
- ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
1187
- return;
1188
- }
1189
- const body = await ctx.host.http.readJsonBody(req);
1190
- if (!isPlainObject2(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8) {
1191
- ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1192
- return;
1193
- }
1194
- const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
1195
- if (!theaterPath) {
1196
- ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
1197
- return;
1198
- }
1199
- try {
1200
- ctx.host.http.writeJson(res, 200, {
1201
- files: await searchTheaterFiles(theaterPath, body.query, body.limit)
1202
- });
1203
- } catch {
1204
- ctx.host.http.writeJson(res, 500, { error: "search_failed" });
1205
- }
1206
- }
1207
- function compareFileSearchItem(left, right, query) {
1208
- const lowQuery = query.trim().toLocaleLowerCase();
1209
- const leftName = left.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
1210
- const rightName = right.relativePath.split("/").at(-1)?.toLocaleLowerCase() ?? "";
1211
- const leftRank = leftName === lowQuery ? 0 : leftName.startsWith(lowQuery) ? 1 : 2;
1212
- const rightRank = rightName === lowQuery ? 0 : rightName.startsWith(lowQuery) ? 1 : 2;
1213
- return leftRank - rightRank || left.relativePath.localeCompare(right.relativePath);
1214
- }
1215
- function isContained(root, candidate) {
1216
- const relative = path4.relative(root, candidate);
1217
- return relative === "" || relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
1218
- }
1219
- function isPlainObject2(value) {
1220
- return typeof value === "object" && value !== null && !Array.isArray(value);
1221
- }
1222
1221
 
1223
1222
  // ../fleet-plugins/file-explorer/routes.ts
1224
1223
  var routes_default = definePlugin({