@dotobokuri/fleet-console 1.40.0 → 1.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/cli.d.ts +2 -1
- package/dist/cli.mjs +151 -134
- package/dist/client/assets/{_baseUniq-Bao5GOIa.js → _baseUniq-CImzMrtU.js} +1 -1
- package/dist/client/assets/{arc-DnhHMLy9.js → arc-CKb0rHjN.js} +1 -1
- package/dist/client/assets/{architectureDiagram-Q4EWVU46-DAbURr0a.js → architectureDiagram-Q4EWVU46-BaZipXSe.js} +1 -1
- package/dist/client/assets/{blockDiagram-DXYQGD6D-DE1raZQ6.js → blockDiagram-DXYQGD6D-D4sS9TjR.js} +1 -1
- package/dist/client/assets/{c4Diagram-AHTNJAMY-beLZlUB3.js → c4Diagram-AHTNJAMY-DvjpMsKx.js} +1 -1
- package/dist/client/assets/channel-dSuGhtYS.js +1 -0
- package/dist/client/assets/{chunk-4BX2VUAB-DEqdunZS.js → chunk-4BX2VUAB-B47BryhY.js} +1 -1
- package/dist/client/assets/{chunk-4TB4RGXK-D8i5nx4L.js → chunk-4TB4RGXK-DKIhSC52.js} +1 -1
- package/dist/client/assets/{chunk-55IACEB6-eM3-fH_e.js → chunk-55IACEB6-B3Gvpef7.js} +1 -1
- package/dist/client/assets/{chunk-EDXVE4YY-nQoyt_Pd.js → chunk-EDXVE4YY-C4gFtvU8.js} +1 -1
- package/dist/client/assets/{chunk-FMBD7UC4-D8Wq8CYj.js → chunk-FMBD7UC4-BADPieOa.js} +1 -1
- package/dist/client/assets/{chunk-OYMX7WX6-Cx4Wr2ZT.js → chunk-OYMX7WX6-BUdtWGUY.js} +1 -1
- package/dist/client/assets/{chunk-QZHKN3VN-BM2v7Krr.js → chunk-QZHKN3VN-CMxkeGnM.js} +1 -1
- package/dist/client/assets/{chunk-YZCP3GAM-zTniPxm7.js → chunk-YZCP3GAM-Br6N7t8d.js} +1 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-DEN4mh7x.js +1 -0
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-DEN4mh7x.js +1 -0
- package/dist/client/assets/clone-BbTnmCoq.js +1 -0
- package/dist/client/assets/{cose-bilkent-S5V4N54A-B5pl2A_p.js → cose-bilkent-S5V4N54A-D1Uw7i5_.js} +1 -1
- package/dist/client/assets/{dagre-KV5264BT-KHPgW9C2.js → dagre-KV5264BT-kop0bHkM.js} +1 -1
- package/dist/client/assets/{diagram-5BDNPKRD-DCv28c7S.js → diagram-5BDNPKRD-B9e41qNA.js} +1 -1
- package/dist/client/assets/{diagram-G4DWMVQ6-mT5WQ415.js → diagram-G4DWMVQ6-Wew8X03k.js} +1 -1
- package/dist/client/assets/{diagram-MMDJMWI5-iKYfjiRC.js → diagram-MMDJMWI5-DGBb5Oqa.js} +1 -1
- package/dist/client/assets/{diagram-TYMM5635-_uqBP7vH.js → diagram-TYMM5635-CiVNsAz6.js} +1 -1
- package/dist/client/assets/{erDiagram-SMLLAGMA-DxNN1WhN.js → erDiagram-SMLLAGMA-BdemYTgC.js} +1 -1
- package/dist/client/assets/{flowDiagram-DWJPFMVM-DGTsrBvM.js → flowDiagram-DWJPFMVM-CXC6yxhU.js} +1 -1
- package/dist/client/assets/{ganttDiagram-T4ZO3ILL-C17hg4lr.js → ganttDiagram-T4ZO3ILL-SMUpaiUg.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-CeJ25hAd.js → gitGraphDiagram-UUTBAWPF-UaFgTHEh.js} +1 -1
- package/dist/client/assets/{graph-BQvb-BfT.js → graph-Ch8F0gMI.js} +1 -1
- package/dist/client/assets/index-D38r12i5.css +1 -0
- package/dist/client/assets/index-DkTcJcbj.js +452 -0
- package/dist/client/assets/{infoDiagram-42DDH7IO-BctsofOZ.js → infoDiagram-42DDH7IO-BHBzw0bu.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-PiPmxcg9.js → ishikawaDiagram-UXIWVN3A-CnDea8bw.js} +1 -1
- package/dist/client/assets/{journeyDiagram-VCZTEJTY-BwkjYq3n.js → journeyDiagram-VCZTEJTY-CI91m8BP.js} +1 -1
- package/dist/client/assets/{kanban-definition-6JOO6SKY-CSV4dYfF.js → kanban-definition-6JOO6SKY-D-rn-rG2.js} +1 -1
- package/dist/client/assets/{layout-DdReLJnP.js → layout-Cd9zpG94.js} +1 -1
- package/dist/client/assets/{linear-B54euy_k.js → linear-Cucr1ojd.js} +1 -1
- package/dist/client/assets/{mermaid.core-CF92I5mT.js → mermaid.core-G36P12JZ.js} +4 -4
- package/dist/client/assets/{min-BcETQ2qJ.js → min-CzfCLG7v.js} +1 -1
- package/dist/client/assets/{mindmap-definition-QFDTVHPH-CrmVPycO.js → mindmap-definition-QFDTVHPH-4e1VjBV7.js} +1 -1
- package/dist/client/assets/{pieDiagram-DEJITSTG-B5d4jR0H.js → pieDiagram-DEJITSTG-m2CGbU51.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-34T5L4WZ-DlgCNExa.js → quadrantDiagram-34T5L4WZ-BKNPGqMc.js} +1 -1
- package/dist/client/assets/{requirementDiagram-MS252O5E-CeuVib_X.js → requirementDiagram-MS252O5E-D2BMwnpz.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-XADWPNL6-Bzxv73ik.js → sankeyDiagram-XADWPNL6-Bm6hTqEh.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-FGHM5R23-B0Ondg0u.js → sequenceDiagram-FGHM5R23-Ds2FEGPj.js} +1 -1
- package/dist/client/assets/{stateDiagram-FHFEXIEX-CuNHg5DJ.js → stateDiagram-FHFEXIEX-CrXn2Tb7.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BbzpXIlW.js +1 -0
- package/dist/client/assets/{timeline-definition-GMOUNBTQ-DXcye6z7.js → timeline-definition-GMOUNBTQ-mw053fb5.js} +1 -1
- package/dist/client/assets/{vennDiagram-DHZGUBPP-CL4rbpzL.js → vennDiagram-DHZGUBPP-CAws0Vt2.js} +1 -1
- package/dist/client/assets/{wardley-RL74JXVD-rVZBTA_R.js → wardley-RL74JXVD-DGad2cmz.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-NUSXRM2D-Cb8Arj1F.js → wardleyDiagram-NUSXRM2D-BlIyft8I.js} +1 -1
- package/dist/client/assets/{xychartDiagram-5P7HB3ND-C-qp4Say.js → xychartDiagram-5P7HB3ND-DHmSULAI.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/fleet-plugins/file-explorer/routes.mjs +484 -56
- package/dist/fleet-plugins/ledger/routes.mjs +56 -47
- package/dist/fleet-plugins/quota/routes.mjs +19252 -118
- package/dist/fleet-plugins/repository/routes.mjs +274 -17
- package/dist/fleet-plugins/scuttlebutt/routes.mjs +6 -81
- package/dist/fleet-plugins/skills/routes.mjs +1 -46
- package/dist/fleet-plugins/terminal/routes.mjs +12090 -1060
- package/package.json +1 -1
- package/dist/client/assets/channel-Bn1wyUhs.js +0 -1
- package/dist/client/assets/classDiagram-6PBFFD2Q-DBE3Ix3c.js +0 -1
- package/dist/client/assets/classDiagram-v2-HSJHXN6E-DBE3Ix3c.js +0 -1
- package/dist/client/assets/clone-COL47Wy_.js +0 -1
- package/dist/client/assets/index-Ca6HkGPQ.css +0 -1
- package/dist/client/assets/index-Cr3Xq85X.js +0 -452
- package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-BVPbUfrG.js +0 -1
|
@@ -1,18 +1,135 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
2
|
+
import { execFile, spawn } from 'child_process';
|
|
3
|
+
import fs3, { constants } from 'fs';
|
|
4
|
+
import fs8 from 'fs/promises';
|
|
5
|
+
import path4 from 'path';
|
|
5
6
|
|
|
6
7
|
// sdk/plugin/node.ts
|
|
7
8
|
function definePlugin(definition) {
|
|
8
9
|
return definition;
|
|
9
10
|
}
|
|
10
|
-
function registerRouter(ctx,
|
|
11
|
-
ctx.registerRouter(normalizePath(
|
|
11
|
+
function registerRouter(ctx, path10, handler) {
|
|
12
|
+
ctx.registerRouter(normalizePath(path10), handler);
|
|
12
13
|
}
|
|
13
|
-
function normalizePath(
|
|
14
|
-
if (
|
|
15
|
-
return
|
|
14
|
+
function normalizePath(path10) {
|
|
15
|
+
if (path10.length === 0 || path10 === "/") return "/";
|
|
16
|
+
return path10;
|
|
17
|
+
}
|
|
18
|
+
var PathActionError = class extends Error {
|
|
19
|
+
code;
|
|
20
|
+
constructor(code) {
|
|
21
|
+
super(code);
|
|
22
|
+
this.name = "PathActionError";
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
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);
|
|
30
|
+
if (!isPathContained(rootPath, candidatePath)) throw new PathActionError("path_outside_theater");
|
|
31
|
+
let realRootPath;
|
|
32
|
+
let realCandidatePath;
|
|
33
|
+
try {
|
|
34
|
+
[realRootPath, realCandidatePath] = await Promise.all([
|
|
35
|
+
fs8.realpath(rootPath),
|
|
36
|
+
fs8.realpath(candidatePath)
|
|
37
|
+
]);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const code = error.code;
|
|
40
|
+
if (code === "EACCES" || code === "EPERM") throw new PathActionError("forbidden");
|
|
41
|
+
throw new PathActionError("not_found");
|
|
42
|
+
}
|
|
43
|
+
if (!isPathContained(realRootPath, realCandidatePath)) {
|
|
44
|
+
throw new PathActionError("path_outside_theater");
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
// Preserve the row the user acted on for display-only actions such as copying path text.
|
|
48
|
+
logicalPath: candidatePath,
|
|
49
|
+
// Process-launch actions must use the same resolved path that passed containment.
|
|
50
|
+
realPath: realCandidatePath
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function isPathContained(rootPath, candidatePath) {
|
|
54
|
+
const relative = path4.relative(rootPath, candidatePath);
|
|
55
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ../fleet-plugins/file-explorer/server/clipboard.ts
|
|
59
|
+
var ClipboardUnavailableError = class extends Error {
|
|
60
|
+
constructor() {
|
|
61
|
+
super("clipboard_unavailable");
|
|
62
|
+
this.name = "ClipboardUnavailableError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var LINUX_CLIPBOARD_TOOLS = [
|
|
66
|
+
{ name: "wl-copy", args: [] },
|
|
67
|
+
{ name: "xclip", args: ["-selection", "clipboard"] },
|
|
68
|
+
{ name: "xsel", args: ["--clipboard", "--input"] }
|
|
69
|
+
];
|
|
70
|
+
var DEFAULT_DEPENDENCIES = {
|
|
71
|
+
platform: process.platform,
|
|
72
|
+
findExecutable: findExecutableOnPath,
|
|
73
|
+
runWithInput: execFileWithInput
|
|
74
|
+
};
|
|
75
|
+
async function copyPathToClipboard(theaterPath, relativePath, dependencies = {}) {
|
|
76
|
+
const deps = { ...DEFAULT_DEPENDENCIES, ...dependencies };
|
|
77
|
+
const { logicalPath } = await resolveContainedActionPath(theaterPath, relativePath);
|
|
78
|
+
const command = await resolveClipboardCommand(deps.platform, deps.findExecutable);
|
|
79
|
+
if (!command) throw new ClipboardUnavailableError();
|
|
80
|
+
try {
|
|
81
|
+
await deps.runWithInput(command.file, command.args, logicalPath);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (error.code === "ENOENT") throw new ClipboardUnavailableError();
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function resolveClipboardCommand(platform, findExecutable = findExecutableOnPath) {
|
|
88
|
+
if (platform === "darwin") return { file: "pbcopy", args: [] };
|
|
89
|
+
if (platform === "win32") return { file: "clip.exe", args: [] };
|
|
90
|
+
if (platform !== "linux") return null;
|
|
91
|
+
for (const candidate of LINUX_CLIPBOARD_TOOLS) {
|
|
92
|
+
const executable = await findExecutable(candidate.name);
|
|
93
|
+
if (executable) return { file: executable, args: candidate.args };
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
async function findExecutableOnPath(name) {
|
|
98
|
+
const rawPath = process.env.PATH ?? "";
|
|
99
|
+
for (const directory of rawPath.split(path4.delimiter)) {
|
|
100
|
+
if (!directory) continue;
|
|
101
|
+
const candidate = path4.join(directory, name);
|
|
102
|
+
try {
|
|
103
|
+
await fs8.access(candidate, constants.X_OK);
|
|
104
|
+
return candidate;
|
|
105
|
+
} catch {
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
function execFileWithInput(file, args, input) {
|
|
111
|
+
return new Promise((resolve, reject) => {
|
|
112
|
+
let settled = false;
|
|
113
|
+
const finish = (error) => {
|
|
114
|
+
if (settled) return;
|
|
115
|
+
settled = true;
|
|
116
|
+
if (error) reject(error);
|
|
117
|
+
else resolve();
|
|
118
|
+
};
|
|
119
|
+
const child = execFile(
|
|
120
|
+
file,
|
|
121
|
+
[...args],
|
|
122
|
+
{ shell: false, timeout: 5e3, windowsHide: true },
|
|
123
|
+
(error) => finish(error)
|
|
124
|
+
);
|
|
125
|
+
child.once("error", finish);
|
|
126
|
+
if (!child.stdin) {
|
|
127
|
+
finish(new Error("clipboard stdin unavailable"));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
child.stdin.once("error", finish);
|
|
131
|
+
child.stdin.end(input);
|
|
132
|
+
});
|
|
16
133
|
}
|
|
17
134
|
var FileReadError = class extends Error {
|
|
18
135
|
code;
|
|
@@ -69,14 +186,14 @@ var EXT_LANG_MAP = {
|
|
|
69
186
|
".txt": "plaintext"
|
|
70
187
|
};
|
|
71
188
|
async function readFileForTheater(theaterPath, relativePath) {
|
|
72
|
-
const resolved =
|
|
189
|
+
const resolved = path4.resolve(theaterPath, relativePath);
|
|
73
190
|
if (!isWithinRoot(resolved, theaterPath)) throw new FileReadError("path_outside_theater");
|
|
74
191
|
let realResolved;
|
|
75
192
|
let realRoot;
|
|
76
193
|
try {
|
|
77
194
|
[realResolved, realRoot] = await Promise.all([
|
|
78
|
-
|
|
79
|
-
|
|
195
|
+
fs3.promises.realpath(resolved),
|
|
196
|
+
fs3.promises.realpath(theaterPath)
|
|
80
197
|
]);
|
|
81
198
|
} catch (error) {
|
|
82
199
|
const code = error.code;
|
|
@@ -86,7 +203,7 @@ async function readFileForTheater(theaterPath, relativePath) {
|
|
|
86
203
|
if (!isWithinRoot(realResolved, realRoot)) throw new FileReadError("path_outside_theater");
|
|
87
204
|
let stat;
|
|
88
205
|
try {
|
|
89
|
-
stat = await
|
|
206
|
+
stat = await fs3.promises.stat(realResolved);
|
|
90
207
|
} catch (error) {
|
|
91
208
|
const code = error.code;
|
|
92
209
|
if (code === "EACCES" || code === "EPERM") throw new FileReadError("forbidden");
|
|
@@ -94,29 +211,29 @@ async function readFileForTheater(theaterPath, relativePath) {
|
|
|
94
211
|
}
|
|
95
212
|
if (!stat.isFile()) throw new FileReadError("not_a_file");
|
|
96
213
|
if (stat.size > FILE_SIZE_CAP) {
|
|
97
|
-
const fd = await
|
|
214
|
+
const fd = await fs3.promises.open(realResolved, "r");
|
|
98
215
|
const buffer2 = Buffer.alloc(FILE_SIZE_CAP);
|
|
99
216
|
const { bytesRead } = await fd.read(buffer2, 0, FILE_SIZE_CAP, 0);
|
|
100
217
|
await fd.close();
|
|
101
218
|
const chunk = buffer2.subarray(0, bytesRead);
|
|
102
219
|
if (isBinaryBuffer(chunk)) throw new FileReadError("binary_file");
|
|
103
220
|
return {
|
|
104
|
-
relativePath:
|
|
221
|
+
relativePath: path4.relative(realRoot, realResolved),
|
|
105
222
|
content: chunk.toString("utf8"),
|
|
106
223
|
lang: detectLang(realResolved),
|
|
107
224
|
truncated: true
|
|
108
225
|
};
|
|
109
226
|
}
|
|
110
|
-
const buffer = await
|
|
227
|
+
const buffer = await fs3.promises.readFile(realResolved);
|
|
111
228
|
if (isBinaryBuffer(buffer)) throw new FileReadError("binary_file");
|
|
112
229
|
return {
|
|
113
|
-
relativePath:
|
|
230
|
+
relativePath: path4.relative(realRoot, realResolved),
|
|
114
231
|
content: buffer.toString("utf8"),
|
|
115
232
|
lang: detectLang(realResolved)
|
|
116
233
|
};
|
|
117
234
|
}
|
|
118
235
|
function isWithinRoot(resolved, root) {
|
|
119
|
-
const normalizedRoot = root.endsWith(
|
|
236
|
+
const normalizedRoot = root.endsWith(path4.sep) ? root : root + path4.sep;
|
|
120
237
|
return resolved === root || resolved.startsWith(normalizedRoot);
|
|
121
238
|
}
|
|
122
239
|
function isBinaryBuffer(buffer) {
|
|
@@ -129,7 +246,7 @@ function isBinaryBuffer(buffer) {
|
|
|
129
246
|
return nulCount / checkLen > BINARY_NUL_THRESHOLD;
|
|
130
247
|
}
|
|
131
248
|
function detectLang(filePath) {
|
|
132
|
-
const ext =
|
|
249
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
133
250
|
return EXT_LANG_MAP[ext] ?? "plaintext";
|
|
134
251
|
}
|
|
135
252
|
var FolderBrowserError = class extends Error {
|
|
@@ -142,10 +259,10 @@ var FolderBrowserError = class extends Error {
|
|
|
142
259
|
};
|
|
143
260
|
var DIRECTORY_ENTRY_CAP = 500;
|
|
144
261
|
async function listTheaterContents(theaterPath, relativePath, deps = {}) {
|
|
145
|
-
const opendir = deps.opendir ??
|
|
146
|
-
const stat = deps.stat ??
|
|
147
|
-
const targetAbs =
|
|
148
|
-
const normalizedRoot = theaterPath.endsWith(
|
|
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;
|
|
149
266
|
if (targetAbs !== theaterPath && !targetAbs.startsWith(normalizedRoot)) {
|
|
150
267
|
throw new FolderBrowserError("forbidden");
|
|
151
268
|
}
|
|
@@ -153,13 +270,13 @@ async function listTheaterContents(theaterPath, relativePath, deps = {}) {
|
|
|
153
270
|
let realTargetAbs;
|
|
154
271
|
try {
|
|
155
272
|
[realRoot, realTargetAbs] = await Promise.all([
|
|
156
|
-
|
|
157
|
-
|
|
273
|
+
fs3.promises.realpath(theaterPath),
|
|
274
|
+
fs3.promises.realpath(targetAbs)
|
|
158
275
|
]);
|
|
159
276
|
} catch (error) {
|
|
160
277
|
throw mapFsError(error);
|
|
161
278
|
}
|
|
162
|
-
const realNormalizedRoot = realRoot.endsWith(
|
|
279
|
+
const realNormalizedRoot = realRoot.endsWith(path4.sep) ? realRoot : realRoot + path4.sep;
|
|
163
280
|
if (realTargetAbs !== realRoot && !realTargetAbs.startsWith(realNormalizedRoot)) {
|
|
164
281
|
throw new FolderBrowserError("forbidden");
|
|
165
282
|
}
|
|
@@ -171,8 +288,8 @@ async function listTheaterContents(theaterPath, relativePath, deps = {}) {
|
|
|
171
288
|
if (a.kind !== b.kind) return a.kind === "dir" ? -1 : 1;
|
|
172
289
|
return a.name.localeCompare(b.name);
|
|
173
290
|
});
|
|
174
|
-
const rel =
|
|
175
|
-
const parentRel = rel === "" ? null :
|
|
291
|
+
const rel = path4.relative(realRoot, realTargetAbs);
|
|
292
|
+
const parentRel = rel === "" ? null : path4.relative(realRoot, path4.dirname(realTargetAbs));
|
|
176
293
|
return {
|
|
177
294
|
relativePath: rel,
|
|
178
295
|
parentRelativePath: parentRel === "" ? null : parentRel ?? null,
|
|
@@ -212,14 +329,14 @@ async function openDirectory(targetPath, opendir) {
|
|
|
212
329
|
}
|
|
213
330
|
}
|
|
214
331
|
async function toContentsEntry(targetPath, theaterPath, dirent, stat) {
|
|
215
|
-
const entryPath =
|
|
216
|
-
const rel =
|
|
332
|
+
const entryPath = path4.join(targetPath, dirent.name);
|
|
333
|
+
const rel = path4.relative(theaterPath, entryPath);
|
|
217
334
|
if (dirent.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
|
|
218
335
|
if (dirent.isFile()) return { name: dirent.name, relativePath: rel, kind: "file" };
|
|
219
336
|
if (dirent.isSymbolicLink()) {
|
|
220
337
|
try {
|
|
221
|
-
const realEntryPath = await
|
|
222
|
-
const realNormalizedRoot = theaterPath.endsWith(
|
|
338
|
+
const realEntryPath = await fs3.promises.realpath(entryPath);
|
|
339
|
+
const realNormalizedRoot = theaterPath.endsWith(path4.sep) ? theaterPath : theaterPath + path4.sep;
|
|
223
340
|
if (realEntryPath !== theaterPath && !realEntryPath.startsWith(realNormalizedRoot)) return null;
|
|
224
341
|
const s = await stat(realEntryPath);
|
|
225
342
|
if (s.isDirectory()) return { name: dirent.name, relativePath: rel, kind: "dir" };
|
|
@@ -236,6 +353,142 @@ function mapFsError(error) {
|
|
|
236
353
|
if (code === "ENOENT" || code === "ENOTDIR") return new FolderBrowserError("not_found");
|
|
237
354
|
return new FolderBrowserError("invalid_path");
|
|
238
355
|
}
|
|
356
|
+
var GitStatusPathError = class extends Error {
|
|
357
|
+
code;
|
|
358
|
+
constructor(code) {
|
|
359
|
+
super(code);
|
|
360
|
+
this.name = "GitStatusPathError";
|
|
361
|
+
this.code = code;
|
|
362
|
+
}
|
|
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);
|
|
376
|
+
}
|
|
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 } : {}
|
|
404
|
+
};
|
|
405
|
+
} catch {
|
|
406
|
+
return { ok: true, gitAvailable: false, statuses: [] };
|
|
407
|
+
}
|
|
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;
|
|
423
|
+
}
|
|
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 });
|
|
436
|
+
}
|
|
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";
|
|
445
|
+
}
|
|
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
|
+
});
|
|
471
|
+
}
|
|
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;
|
|
477
|
+
}
|
|
478
|
+
sanitized.GIT_OPTIONAL_LOCKS = "0";
|
|
479
|
+
return sanitized;
|
|
480
|
+
}
|
|
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;
|
|
485
|
+
}
|
|
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");
|
|
491
|
+
}
|
|
239
492
|
var ImageServeError = class extends Error {
|
|
240
493
|
code;
|
|
241
494
|
constructor(code) {
|
|
@@ -258,17 +511,17 @@ var IMAGE_SECURITY_HEADERS = {
|
|
|
258
511
|
"Cache-Control": "private, no-store"
|
|
259
512
|
};
|
|
260
513
|
async function readImageForTheater(theaterPath, relativePath) {
|
|
261
|
-
const resolved =
|
|
514
|
+
const resolved = path4.resolve(theaterPath, relativePath);
|
|
262
515
|
if (!isWithinRoot2(resolved, theaterPath)) throw new ImageServeError("path_outside_theater");
|
|
263
|
-
const ext =
|
|
516
|
+
const ext = path4.extname(resolved).toLowerCase();
|
|
264
517
|
const mimeType = MIME_ALLOWLIST[ext];
|
|
265
518
|
if (!mimeType) throw new ImageServeError("mime_not_allowed");
|
|
266
519
|
let realResolved;
|
|
267
520
|
let realRoot;
|
|
268
521
|
try {
|
|
269
522
|
[realResolved, realRoot] = await Promise.all([
|
|
270
|
-
|
|
271
|
-
|
|
523
|
+
fs3.promises.realpath(resolved),
|
|
524
|
+
fs3.promises.realpath(theaterPath)
|
|
272
525
|
]);
|
|
273
526
|
} catch (error) {
|
|
274
527
|
const code = error.code;
|
|
@@ -278,7 +531,7 @@ async function readImageForTheater(theaterPath, relativePath) {
|
|
|
278
531
|
if (!isWithinRoot2(realResolved, realRoot)) throw new ImageServeError("path_outside_theater");
|
|
279
532
|
let stat;
|
|
280
533
|
try {
|
|
281
|
-
stat = await
|
|
534
|
+
stat = await fs3.promises.stat(realResolved);
|
|
282
535
|
} catch (error) {
|
|
283
536
|
const code = error.code;
|
|
284
537
|
if (code === "EACCES" || code === "EPERM") throw new ImageServeError("forbidden");
|
|
@@ -286,7 +539,7 @@ async function readImageForTheater(theaterPath, relativePath) {
|
|
|
286
539
|
}
|
|
287
540
|
if (!stat.isFile()) throw new ImageServeError("not_a_file");
|
|
288
541
|
if (stat.size > IMAGE_SIZE_CAP) throw new ImageServeError("size_exceeded");
|
|
289
|
-
const buffer = await
|
|
542
|
+
const buffer = await fs3.promises.readFile(realResolved);
|
|
290
543
|
return { mimeType, buffer };
|
|
291
544
|
}
|
|
292
545
|
function writeImageResponse(res, result) {
|
|
@@ -298,13 +551,65 @@ function writeImageResponse(res, result) {
|
|
|
298
551
|
res.end(result.buffer);
|
|
299
552
|
}
|
|
300
553
|
function isWithinRoot2(resolved, root) {
|
|
301
|
-
const normalizedRoot = root.endsWith(
|
|
554
|
+
const normalizedRoot = root.endsWith(path4.sep) ? root : root + path4.sep;
|
|
302
555
|
return resolved === root || resolved.startsWith(normalizedRoot);
|
|
303
556
|
}
|
|
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();
|
|
572
|
+
try {
|
|
573
|
+
await deps.launch(command.file, command.args);
|
|
574
|
+
} catch (error) {
|
|
575
|
+
if (error.code === "ENOENT") throw new FileActionUnavailableError();
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function resolveRevealCommand(platform, mode, absolutePath) {
|
|
580
|
+
if (platform === "darwin") {
|
|
581
|
+
return mode === "reveal" ? { file: "open", args: ["-R", absolutePath] } : { file: "open", args: [absolutePath] };
|
|
582
|
+
}
|
|
583
|
+
if (platform === "linux") {
|
|
584
|
+
return {
|
|
585
|
+
file: "xdg-open",
|
|
586
|
+
args: [mode === "reveal" ? path4.dirname(absolutePath) : absolutePath]
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
if (platform === "win32") {
|
|
590
|
+
return mode === "reveal" ? { file: "explorer", args: [`/select,${absolutePath}`] } : { file: "explorer", args: [absolutePath] };
|
|
591
|
+
}
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
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
|
+
});
|
|
608
|
+
}
|
|
304
609
|
var DEBOUNCE_MS = 200;
|
|
305
610
|
var LINUX_DIRECTORY_WATCH_CAP = 1024;
|
|
306
611
|
var watcherRegistry = createWatcherRegistry();
|
|
307
|
-
function createWatcherRegistry(watcherFactory = (p, opts, cb) =>
|
|
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) {
|
|
308
613
|
const entries = /* @__PURE__ */ new Map();
|
|
309
614
|
function scheduleChange(entry, relDir) {
|
|
310
615
|
const pending = entry.debounceTimers.get(relDir);
|
|
@@ -355,7 +660,7 @@ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs.watch(p, opt
|
|
|
355
660
|
}
|
|
356
661
|
}
|
|
357
662
|
function invalidateWatcherTree(entry, watchKey) {
|
|
358
|
-
const descendantPrefix = watchKey +
|
|
663
|
+
const descendantPrefix = watchKey + path4.sep;
|
|
359
664
|
for (const [candidateKey, candidateWatcher] of entry.watchers) {
|
|
360
665
|
if (candidateKey !== watchKey && !candidateKey.startsWith(descendantPrefix)) continue;
|
|
361
666
|
entry.watchers.delete(candidateKey);
|
|
@@ -371,7 +676,7 @@ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs.watch(p, opt
|
|
|
371
676
|
let identityCheckRunning = false;
|
|
372
677
|
let identityCheckRequested = false;
|
|
373
678
|
let activeWatcher = null;
|
|
374
|
-
const lexicalWatchPath =
|
|
679
|
+
const lexicalWatchPath = path4.resolve(entry.theaterPath, watchKey);
|
|
375
680
|
const checkDirectoryIdentity = async (expectedWatcher) => {
|
|
376
681
|
identityCheckRequested = true;
|
|
377
682
|
if (identityCheckRunning || !watchIdentity) return;
|
|
@@ -381,7 +686,7 @@ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs.watch(p, opt
|
|
|
381
686
|
identityCheckRequested = false;
|
|
382
687
|
let replaced = false;
|
|
383
688
|
try {
|
|
384
|
-
const stats = await
|
|
689
|
+
const stats = await fs3.promises.stat(lexicalWatchPath);
|
|
385
690
|
replaced = stats.dev !== watchIdentity.device || stats.ino !== watchIdentity.inode;
|
|
386
691
|
} catch (error) {
|
|
387
692
|
replaced = error.code === "ENOENT";
|
|
@@ -403,7 +708,7 @@ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs.watch(p, opt
|
|
|
403
708
|
if (!entry.recursive && event === "rename") {
|
|
404
709
|
if (watchKey !== "" && watchIdentity) void checkDirectoryIdentity(currentWatcher);
|
|
405
710
|
if (filename) {
|
|
406
|
-
const changedChildKey =
|
|
711
|
+
const changedChildKey = path4.join(watchKey, filename);
|
|
407
712
|
entry.identityChecks.get(changedChildKey)?.();
|
|
408
713
|
}
|
|
409
714
|
}
|
|
@@ -507,16 +812,16 @@ function createWatcherRegistry(watcherFactory = (p, opts, cb) => fs.watch(p, opt
|
|
|
507
812
|
return { subscribe, trackDirectory };
|
|
508
813
|
}
|
|
509
814
|
async function resolveTrackedDirectory(theaterPath, relativePath) {
|
|
510
|
-
const resolvedRoot =
|
|
511
|
-
const targetPath =
|
|
815
|
+
const resolvedRoot = path4.resolve(theaterPath);
|
|
816
|
+
const targetPath = path4.resolve(resolvedRoot, relativePath);
|
|
512
817
|
if (!isWithinRoot3(targetPath, resolvedRoot)) return null;
|
|
513
818
|
try {
|
|
514
819
|
const [realRoot, realTarget] = await Promise.all([
|
|
515
|
-
|
|
516
|
-
|
|
820
|
+
fs3.promises.realpath(theaterPath),
|
|
821
|
+
fs3.promises.realpath(targetPath)
|
|
517
822
|
]);
|
|
518
823
|
if (!isWithinRoot3(realTarget, realRoot)) return null;
|
|
519
|
-
const stats = await
|
|
824
|
+
const stats = await fs3.promises.stat(realTarget);
|
|
520
825
|
if (!stats.isDirectory()) return null;
|
|
521
826
|
return {
|
|
522
827
|
path: realTarget,
|
|
@@ -527,7 +832,7 @@ async function resolveTrackedDirectory(theaterPath, relativePath) {
|
|
|
527
832
|
}
|
|
528
833
|
}
|
|
529
834
|
function isWithinRoot3(candidate, root) {
|
|
530
|
-
const normalizedRoot = root.endsWith(
|
|
835
|
+
const normalizedRoot = root.endsWith(path4.sep) ? root : root + path4.sep;
|
|
531
836
|
return candidate === root || candidate.startsWith(normalizedRoot);
|
|
532
837
|
}
|
|
533
838
|
function computeRelDir(filename) {
|
|
@@ -538,6 +843,8 @@ function computeRelDir(filename) {
|
|
|
538
843
|
}
|
|
539
844
|
|
|
540
845
|
// ../fleet-plugins/file-explorer/server/handlers.ts
|
|
846
|
+
var DEFAULT_CLIPBOARD_DEPENDENCIES = { copyPath: copyPathToClipboard };
|
|
847
|
+
var DEFAULT_REVEAL_DEPENDENCIES = { revealPath };
|
|
541
848
|
function isPlainObject(value) {
|
|
542
849
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
543
850
|
}
|
|
@@ -587,6 +894,36 @@ async function handleFilesList(req, res, ctx) {
|
|
|
587
894
|
throw error;
|
|
588
895
|
}
|
|
589
896
|
}
|
|
897
|
+
async function handleFilesGitStatus(req, res, ctx) {
|
|
898
|
+
if (req.method !== "POST") {
|
|
899
|
+
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
if (!ctx.host.security.isTerminalAuthorized(req)) {
|
|
903
|
+
ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const body = await ctx.host.http.readJsonBody(req);
|
|
907
|
+
if (!isPlainObject(body) || typeof body.theaterId !== "string") {
|
|
908
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
|
|
912
|
+
if (!theaterPath) {
|
|
913
|
+
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
try {
|
|
917
|
+
ctx.host.http.writeJson(res, 200, await readTheaterGitStatus(theaterPath));
|
|
918
|
+
} catch (error) {
|
|
919
|
+
if (error instanceof GitStatusPathError) {
|
|
920
|
+
const httpStatus = error.code === "forbidden" ? 403 : error.code === "not_found" ? 404 : 400;
|
|
921
|
+
ctx.host.http.writeJson(res, httpStatus, { error: error.code });
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
throw error;
|
|
925
|
+
}
|
|
926
|
+
}
|
|
590
927
|
async function handleFilesRead(req, res, ctx) {
|
|
591
928
|
if (req.method !== "POST") {
|
|
592
929
|
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
@@ -660,6 +997,74 @@ async function handleFilesImage(req, res, ctx) {
|
|
|
660
997
|
throw error;
|
|
661
998
|
}
|
|
662
999
|
}
|
|
1000
|
+
async function handleFilesClipboard(req, res, ctx, dependencies = DEFAULT_CLIPBOARD_DEPENDENCIES) {
|
|
1001
|
+
if (req.method !== "POST") {
|
|
1002
|
+
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
if (!ctx.host.security.isTerminalAuthorized(req)) {
|
|
1006
|
+
ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
const body = await ctx.host.http.readJsonBody(req);
|
|
1010
|
+
if (!isPlainObject(body) || typeof body.theaterId !== "string" || typeof body.relativePath !== "string") {
|
|
1011
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
|
|
1015
|
+
if (!theaterPath) {
|
|
1016
|
+
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
try {
|
|
1020
|
+
await dependencies.copyPath(theaterPath, body.relativePath);
|
|
1021
|
+
writeNoContent(res);
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
if (error instanceof PathActionError) {
|
|
1024
|
+
writePathActionError(res, ctx, error);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
if (error instanceof ClipboardUnavailableError) {
|
|
1028
|
+
ctx.host.http.writeJson(res, 501, { error: "clipboard_unavailable" });
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
ctx.host.http.writeJson(res, 500, { error: "clipboard_failed" });
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async function handleFilesReveal(req, res, ctx, dependencies = DEFAULT_REVEAL_DEPENDENCIES) {
|
|
1035
|
+
if (req.method !== "POST") {
|
|
1036
|
+
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
if (!ctx.host.security.isTerminalAuthorized(req)) {
|
|
1040
|
+
ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const body = await ctx.host.http.readJsonBody(req);
|
|
1044
|
+
if (!isPlainObject(body) || typeof body.theaterId !== "string" || typeof body.relativePath !== "string" || !isFileRevealMode(body.mode)) {
|
|
1045
|
+
ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
|
|
1049
|
+
if (!theaterPath) {
|
|
1050
|
+
ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
|
|
1051
|
+
return;
|
|
1052
|
+
}
|
|
1053
|
+
try {
|
|
1054
|
+
await dependencies.revealPath(theaterPath, body.relativePath, body.mode);
|
|
1055
|
+
writeNoContent(res);
|
|
1056
|
+
} catch (error) {
|
|
1057
|
+
if (error instanceof PathActionError) {
|
|
1058
|
+
writePathActionError(res, ctx, error);
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
if (error instanceof FileActionUnavailableError) {
|
|
1062
|
+
ctx.host.http.writeJson(res, 501, { error: "action_unavailable" });
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
ctx.host.http.writeJson(res, 500, { error: "action_failed" });
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
663
1068
|
function handleFilesWatch(req, res, ctx) {
|
|
664
1069
|
if (req.method !== "GET") {
|
|
665
1070
|
ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
|
|
@@ -711,10 +1116,21 @@ data: ${data}
|
|
|
711
1116
|
}
|
|
712
1117
|
});
|
|
713
1118
|
}
|
|
1119
|
+
function isFileRevealMode(value) {
|
|
1120
|
+
return value === "reveal" || value === "open";
|
|
1121
|
+
}
|
|
1122
|
+
function writeNoContent(res) {
|
|
1123
|
+
res.statusCode = 204;
|
|
1124
|
+
res.end();
|
|
1125
|
+
}
|
|
1126
|
+
function writePathActionError(res, ctx, error) {
|
|
1127
|
+
const status = error.code === "not_found" ? 404 : 403;
|
|
1128
|
+
ctx.host.http.writeJson(res, status, { error: error.code });
|
|
1129
|
+
}
|
|
714
1130
|
var SEARCH_DIRECTORY_CAP = 500;
|
|
715
1131
|
var SEARCH_ENTRY_CAP = 25e3;
|
|
716
1132
|
async function searchTheaterFiles(theaterPath, query, limit) {
|
|
717
|
-
const realRoot = await
|
|
1133
|
+
const realRoot = await fs8.realpath(theaterPath);
|
|
718
1134
|
const tokens = query.trim().toLocaleLowerCase().split(/\s+/).filter(Boolean);
|
|
719
1135
|
const pending = [{ absolutePath: realRoot, relativePath: "" }];
|
|
720
1136
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -728,7 +1144,7 @@ async function searchTheaterFiles(theaterPath, query, limit) {
|
|
|
728
1144
|
directoryCount += 1;
|
|
729
1145
|
let entries;
|
|
730
1146
|
try {
|
|
731
|
-
entries = await
|
|
1147
|
+
entries = await fs8.readdir(directory.absolutePath, { withFileTypes: true });
|
|
732
1148
|
} catch {
|
|
733
1149
|
continue;
|
|
734
1150
|
}
|
|
@@ -737,14 +1153,14 @@ async function searchTheaterFiles(theaterPath, query, limit) {
|
|
|
737
1153
|
if (entryCount >= SEARCH_ENTRY_CAP) break;
|
|
738
1154
|
entryCount += 1;
|
|
739
1155
|
const relativePath = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
|
|
740
|
-
const absolutePath =
|
|
1156
|
+
const absolutePath = path4.join(directory.absolutePath, entry.name);
|
|
741
1157
|
let kind = entry.isDirectory() ? "dir" : entry.isFile() ? "file" : null;
|
|
742
1158
|
let realPath = absolutePath;
|
|
743
1159
|
if (entry.isSymbolicLink()) {
|
|
744
1160
|
try {
|
|
745
|
-
realPath = await
|
|
1161
|
+
realPath = await fs8.realpath(absolutePath);
|
|
746
1162
|
if (!isContained(realRoot, realPath)) continue;
|
|
747
|
-
const stat = await
|
|
1163
|
+
const stat = await fs8.stat(realPath);
|
|
748
1164
|
kind = stat.isDirectory() ? "dir" : stat.isFile() ? "file" : null;
|
|
749
1165
|
} catch {
|
|
750
1166
|
continue;
|
|
@@ -797,8 +1213,8 @@ function compareFileSearchItem(left, right, query) {
|
|
|
797
1213
|
return leftRank - rightRank || left.relativePath.localeCompare(right.relativePath);
|
|
798
1214
|
}
|
|
799
1215
|
function isContained(root, candidate) {
|
|
800
|
-
const relative =
|
|
801
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
1216
|
+
const relative = path4.relative(root, candidate);
|
|
1217
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path4.sep}`) && !path4.isAbsolute(relative);
|
|
802
1218
|
}
|
|
803
1219
|
function isPlainObject2(value) {
|
|
804
1220
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -812,6 +1228,10 @@ var routes_default = definePlugin({
|
|
|
812
1228
|
await handleFilesList(req, res, ctx);
|
|
813
1229
|
return true;
|
|
814
1230
|
});
|
|
1231
|
+
registerRouter(ctx, "files/git-status", async ({ req, res }) => {
|
|
1232
|
+
await handleFilesGitStatus(req, res, ctx);
|
|
1233
|
+
return true;
|
|
1234
|
+
});
|
|
815
1235
|
registerRouter(ctx, "files/read", async ({ req, res }) => {
|
|
816
1236
|
await handleFilesRead(req, res, ctx);
|
|
817
1237
|
return true;
|
|
@@ -820,6 +1240,14 @@ var routes_default = definePlugin({
|
|
|
820
1240
|
await handleFilesImage(req, res, ctx);
|
|
821
1241
|
return true;
|
|
822
1242
|
});
|
|
1243
|
+
registerRouter(ctx, "files/clipboard", async ({ req, res }) => {
|
|
1244
|
+
await handleFilesClipboard(req, res, ctx);
|
|
1245
|
+
return true;
|
|
1246
|
+
});
|
|
1247
|
+
registerRouter(ctx, "files/reveal", async ({ req, res }) => {
|
|
1248
|
+
await handleFilesReveal(req, res, ctx);
|
|
1249
|
+
return true;
|
|
1250
|
+
});
|
|
823
1251
|
registerRouter(ctx, "files/watch", ({ req, res }) => {
|
|
824
1252
|
handleFilesWatch(req, res, ctx);
|
|
825
1253
|
return true;
|