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