@dotobokuri/fleet-console 1.67.0 → 1.68.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 (68) hide show
  1. package/dist/cli.mjs +379 -48
  2. package/dist/client/assets/{_baseUniq-DG32CEpb.js → _baseUniq-BPBd-J9P.js} +1 -1
  3. package/dist/client/assets/{arc-CoHkSCMF.js → arc-spHqy4X3.js} +1 -1
  4. package/dist/client/assets/{architectureDiagram-Q4EWVU46-f0RKHqJg.js → architectureDiagram-Q4EWVU46-7CHqKfxu.js} +1 -1
  5. package/dist/client/assets/{blockDiagram-DXYQGD6D-Dxb3hLAV.js → blockDiagram-DXYQGD6D-YasFDgl_.js} +1 -1
  6. package/dist/client/assets/{c4Diagram-AHTNJAMY-C5q_ahSd.js → c4Diagram-AHTNJAMY-CcoPJq56.js} +1 -1
  7. package/dist/client/assets/channel-DfEjadrw.js +1 -0
  8. package/dist/client/assets/{chunk-4BX2VUAB-BS0W4Fpt.js → chunk-4BX2VUAB-MYBJn9BE.js} +1 -1
  9. package/dist/client/assets/{chunk-4TB4RGXK-p03eWKTO.js → chunk-4TB4RGXK-C1wg6GR_.js} +1 -1
  10. package/dist/client/assets/{chunk-55IACEB6-BB1ALVX2.js → chunk-55IACEB6-BoufMT1O.js} +1 -1
  11. package/dist/client/assets/{chunk-EDXVE4YY-DV4N-Npm.js → chunk-EDXVE4YY-BzAhAIl3.js} +1 -1
  12. package/dist/client/assets/{chunk-FMBD7UC4-Da8VspnD.js → chunk-FMBD7UC4-yVrLsKzP.js} +1 -1
  13. package/dist/client/assets/{chunk-OYMX7WX6-CWTmAQhy.js → chunk-OYMX7WX6-YiiKaqRQ.js} +1 -1
  14. package/dist/client/assets/{chunk-QZHKN3VN-DE9xUsy3.js → chunk-QZHKN3VN-CqWQlGHT.js} +1 -1
  15. package/dist/client/assets/{chunk-YZCP3GAM-BxriNNdB.js → chunk-YZCP3GAM-9yLOmU5k.js} +1 -1
  16. package/dist/client/assets/classDiagram-6PBFFD2Q-D9vQdv0q.js +1 -0
  17. package/dist/client/assets/classDiagram-v2-HSJHXN6E-D9vQdv0q.js +1 -0
  18. package/dist/client/assets/clone-Ck2x5JX2.js +1 -0
  19. package/dist/client/assets/{cose-bilkent-S5V4N54A-B4sjskIn.js → cose-bilkent-S5V4N54A-XkTuSq7v.js} +1 -1
  20. package/dist/client/assets/{dagre-KV5264BT-oJms5aS3.js → dagre-KV5264BT-DyzXIEAe.js} +1 -1
  21. package/dist/client/assets/{diagram-5BDNPKRD-BAbLGddM.js → diagram-5BDNPKRD-DF24YlCa.js} +1 -1
  22. package/dist/client/assets/{diagram-G4DWMVQ6-BrhRQwe-.js → diagram-G4DWMVQ6-CVhcj9RK.js} +1 -1
  23. package/dist/client/assets/{diagram-MMDJMWI5-Buxu7Ng3.js → diagram-MMDJMWI5-C-kGKk4K.js} +1 -1
  24. package/dist/client/assets/{diagram-TYMM5635-Cnp__7LJ.js → diagram-TYMM5635-mr285WbR.js} +1 -1
  25. package/dist/client/assets/{erDiagram-SMLLAGMA-ctmZHrVO.js → erDiagram-SMLLAGMA-BtEx-XlU.js} +1 -1
  26. package/dist/client/assets/{flowDiagram-DWJPFMVM-DsQXJqF_.js → flowDiagram-DWJPFMVM-DuWy3xzN.js} +1 -1
  27. package/dist/client/assets/{ganttDiagram-T4ZO3ILL-SAFJNY9M.js → ganttDiagram-T4ZO3ILL-B2-xEikU.js} +1 -1
  28. package/dist/client/assets/{gitGraphDiagram-UUTBAWPF-Bd5Dq0vQ.js → gitGraphDiagram-UUTBAWPF-EM17u_t7.js} +1 -1
  29. package/dist/client/assets/{graph-DzMOPhFp.js → graph-xuE12FIq.js} +1 -1
  30. package/dist/client/assets/index-C5EOZGlC.js +466 -0
  31. package/dist/client/assets/index-CHHE3nPw.css +1 -0
  32. package/dist/client/assets/{infoDiagram-42DDH7IO-BrEfG9P4.js → infoDiagram-42DDH7IO-B3MF4XtW.js} +1 -1
  33. package/dist/client/assets/{ishikawaDiagram-UXIWVN3A-Dg22orvH.js → ishikawaDiagram-UXIWVN3A-DoQ2mGyh.js} +1 -1
  34. package/dist/client/assets/{journeyDiagram-VCZTEJTY-DEcjEc6c.js → journeyDiagram-VCZTEJTY-GeR9sFH-.js} +1 -1
  35. package/dist/client/assets/{kanban-definition-6JOO6SKY-C2Xa4znN.js → kanban-definition-6JOO6SKY-DrrYMuvL.js} +1 -1
  36. package/dist/client/assets/{layout-B7E_-0ev.js → layout-DH63A-3u.js} +1 -1
  37. package/dist/client/assets/{linear-BXJsh3FL.js → linear-BIjE6zB1.js} +1 -1
  38. package/dist/client/assets/{mermaid.core-CdZpLnyY.js → mermaid.core-yrrC2Lqo.js} +4 -4
  39. package/dist/client/assets/{min-CEAFFcJY.js → min-CR-H8E6U.js} +1 -1
  40. package/dist/client/assets/{mindmap-definition-QFDTVHPH-iXzcnu1C.js → mindmap-definition-QFDTVHPH-BzSC43J-.js} +1 -1
  41. package/dist/client/assets/{pieDiagram-DEJITSTG-CdRN75ad.js → pieDiagram-DEJITSTG-BdghNtCc.js} +1 -1
  42. package/dist/client/assets/{quadrantDiagram-34T5L4WZ-Dscl0EFH.js → quadrantDiagram-34T5L4WZ-s_zN2epw.js} +1 -1
  43. package/dist/client/assets/{requirementDiagram-MS252O5E-CLmSCaQs.js → requirementDiagram-MS252O5E-DuiGG6Tx.js} +1 -1
  44. package/dist/client/assets/{sankeyDiagram-XADWPNL6-BfPk87jf.js → sankeyDiagram-XADWPNL6-Bp0JBk2g.js} +1 -1
  45. package/dist/client/assets/{sequenceDiagram-FGHM5R23-DE8sU8ej.js → sequenceDiagram-FGHM5R23-AzQ3M31V.js} +1 -1
  46. package/dist/client/assets/{stateDiagram-FHFEXIEX-BmaQc1Nm.js → stateDiagram-FHFEXIEX-BGgm44ev.js} +1 -1
  47. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-CZDbnuN-.js +1 -0
  48. package/dist/client/assets/{timeline-definition-GMOUNBTQ-C4Z0umzx.js → timeline-definition-GMOUNBTQ-UqfsiBJY.js} +1 -1
  49. package/dist/client/assets/{vennDiagram-DHZGUBPP-BKlcRXlE.js → vennDiagram-DHZGUBPP-PsCGF9nR.js} +1 -1
  50. package/dist/client/assets/{wardley-RL74JXVD-B0RT7bAW.js → wardley-RL74JXVD-BDQbnh-d.js} +1 -1
  51. package/dist/client/assets/{wardleyDiagram-NUSXRM2D-mx28kvam.js → wardleyDiagram-NUSXRM2D-CtVA1JMs.js} +1 -1
  52. package/dist/client/assets/{xychartDiagram-5P7HB3ND-DU6qIPI0.js → xychartDiagram-5P7HB3ND-BsLlZWbc.js} +1 -1
  53. package/dist/client/index.html +2 -2
  54. package/dist/fleet-plugins/file-explorer/routes.mjs +21 -2
  55. package/dist/fleet-plugins/ledger/routes.mjs +9 -2
  56. package/dist/fleet-plugins/quota/routes.mjs +9 -2
  57. package/dist/fleet-plugins/repository/routes.mjs +910 -26
  58. package/dist/fleet-plugins/scuttlebutt/routes.mjs +11 -3
  59. package/dist/fleet-plugins/terminal/routes.mjs +626 -934
  60. package/dist/fleet.mjs +832 -806
  61. package/package.json +1 -1
  62. package/dist/client/assets/channel-C-Qx-Oi-.js +0 -1
  63. package/dist/client/assets/classDiagram-6PBFFD2Q-DA7e8Mft.js +0 -1
  64. package/dist/client/assets/classDiagram-v2-HSJHXN6E-DA7e8Mft.js +0 -1
  65. package/dist/client/assets/clone-Rpb38n4i.js +0 -1
  66. package/dist/client/assets/index-BLA_qnPk.css +0 -1
  67. package/dist/client/assets/index-CGE8u6Es.js +0 -465
  68. package/dist/client/assets/stateDiagram-v2-QKLJ7IA2-TcoFpvuT.js +0 -1
@@ -7,16 +7,16 @@ import path3 from 'node:path';
7
7
  function definePlugin(definition) {
8
8
  return definition;
9
9
  }
10
- function registerRouter(ctx, path9, handler, catalog) {
10
+ function registerRouter(ctx, path12, handler, catalog) {
11
11
  if (catalog) {
12
- ctx.registerRouter(normalizePath(path9), handler, catalog);
12
+ ctx.registerRouter(normalizePath(path12), handler, catalog);
13
13
  return;
14
14
  }
15
- ctx.registerRouter(normalizePath(path9), handler);
15
+ ctx.registerRouter(normalizePath(path12), handler);
16
16
  }
17
- function normalizePath(path9) {
18
- if (path9.length === 0 || path9 === "/") return "/";
19
- return path9;
17
+ function normalizePath(path12) {
18
+ if (path12.length === 0 || path12 === "/") return "/";
19
+ return path12;
20
20
  }
21
21
 
22
22
  // ../../packages/core-process/src/bin-resolver.ts
@@ -418,7 +418,7 @@ async function handleRepositoryFile(req, res, ctx) {
418
418
  return;
419
419
  }
420
420
  const mode = body.mode;
421
- if (mode !== "unified" && mode !== "untracked") {
421
+ if (mode !== "unified" && mode !== "untracked" && mode !== "staged" && mode !== "worktree") {
422
422
  ctx.host.http.writeJson(res, 400, { error: "invalid_mode" });
423
423
  return;
424
424
  }
@@ -490,11 +490,17 @@ async function handleRepositoryFile(req, res, ctx) {
490
490
  } catch {
491
491
  }
492
492
  let result;
493
- try {
494
- result = await runGit(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
495
- } catch (err) {
496
- if (!isNoHeadError(err)) throw err;
493
+ if (mode === "worktree") {
494
+ result = await runGit(["diff", "--no-ext-diff", "--no-textconv", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
495
+ } else if (mode === "staged") {
497
496
  result = await runGit(["diff", "--no-ext-diff", "--no-textconv", "--cached", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
497
+ } else {
498
+ try {
499
+ result = await runGit(["diff", "--no-ext-diff", "--no-textconv", "HEAD", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
500
+ } catch (err) {
501
+ if (!isNoHeadError(err)) throw err;
502
+ result = await runGit(["diff", "--no-ext-diff", "--no-textconv", "--cached", "--relative", "--unified=3", "--", literalPathspec(relativePath)], { cwd: gitCwd });
503
+ }
498
504
  }
499
505
  ctx.host.http.writeJson(res, 200, { content: result.stdout, truncated: result.truncated });
500
506
  } catch (error) {
@@ -602,6 +608,254 @@ async function handleRepositoryCommit(req, res, ctx) {
602
608
  function isPlainObject3(value) {
603
609
  return typeof value === "object" && value !== null && !Array.isArray(value);
604
610
  }
611
+ var MAX_PATHS_PER_REQUEST = 1e3;
612
+ function readStagePaths(value) {
613
+ if (!Array.isArray(value) || value.length === 0 || value.length > MAX_PATHS_PER_REQUEST) return null;
614
+ const paths = [];
615
+ for (const raw of value) {
616
+ if (typeof raw !== "string" || raw === "" || raw.includes("\0")) return null;
617
+ if (path3.isAbsolute(raw) || path3.posix.isAbsolute(raw)) return null;
618
+ const normalized = path3.posix.normalize(raw);
619
+ if (normalized === ".." || normalized.startsWith("../")) return null;
620
+ paths.push(raw);
621
+ }
622
+ return paths;
623
+ }
624
+ function literalPathspecs(paths) {
625
+ return paths.map((entry) => `:(literal)${entry}`);
626
+ }
627
+ function classifyWriteError(stderr) {
628
+ return /index\.lock|Unable to create .* File exists/i.test(stderr) ? "index_locked" : null;
629
+ }
630
+ async function resolveWriteContext(req, res, ctx) {
631
+ if (req.method !== "POST") {
632
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
633
+ return null;
634
+ }
635
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
636
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
637
+ return null;
638
+ }
639
+ const body = await ctx.host.http.readJsonBody(req);
640
+ if (!isPlainObject3(body) || "subPath" in body || typeof body.theaterId !== "string") {
641
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
642
+ return null;
643
+ }
644
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
645
+ if (!theaterPath) {
646
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
647
+ return null;
648
+ }
649
+ try {
650
+ const { gitCwd } = await resolveGitCwd(theaterPath, body.repoRel);
651
+ return { gitCwd, body };
652
+ } catch (error) {
653
+ if (error instanceof InvalidRepoError) {
654
+ ctx.host.http.writeJson(res, 400, { error: error.code });
655
+ return null;
656
+ }
657
+ throw error;
658
+ }
659
+ }
660
+ function writeGitFailure(res, ctx, error) {
661
+ if (error instanceof GitExecutorError) {
662
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
663
+ ctx.host.http.writeJson(res, 422, { error: error.code });
664
+ return;
665
+ }
666
+ if (error.code === "timeout") {
667
+ ctx.host.http.writeJson(res, 422, { error: "timeout" });
668
+ return;
669
+ }
670
+ const locked = classifyWriteError(error.stderr);
671
+ if (locked) {
672
+ ctx.host.http.writeJson(res, 409, { error: locked });
673
+ return;
674
+ }
675
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
676
+ return;
677
+ }
678
+ throw error;
679
+ }
680
+ async function handleRepositoryStage(req, res, ctx) {
681
+ const resolved = await resolveWriteContext(req, res, ctx);
682
+ if (!resolved) return;
683
+ if (resolved.body.all === true) {
684
+ try {
685
+ await runGit(["add", "-A", "--", "."], { cwd: resolved.gitCwd });
686
+ ctx.host.http.writeJson(res, 200, { ok: true });
687
+ } catch (error) {
688
+ writeGitFailure(res, ctx, error);
689
+ }
690
+ return;
691
+ }
692
+ const paths = readStagePaths(resolved.body.paths);
693
+ if (!paths) {
694
+ ctx.host.http.writeJson(res, 400, { error: "invalid_paths" });
695
+ return;
696
+ }
697
+ try {
698
+ await runGit(["add", "--", ...literalPathspecs(paths)], { cwd: resolved.gitCwd });
699
+ ctx.host.http.writeJson(res, 200, { ok: true });
700
+ } catch (error) {
701
+ writeGitFailure(res, ctx, error);
702
+ }
703
+ }
704
+ async function handleRepositoryUnstage(req, res, ctx) {
705
+ const resolved = await resolveWriteContext(req, res, ctx);
706
+ if (!resolved) return;
707
+ const all = resolved.body.all === true;
708
+ const paths = all ? [] : readStagePaths(resolved.body.paths);
709
+ if (!paths) {
710
+ ctx.host.http.writeJson(res, 400, { error: "invalid_paths" });
711
+ return;
712
+ }
713
+ const pathspecs = all ? ["."] : literalPathspecs(paths);
714
+ try {
715
+ try {
716
+ await runGit(["restore", "--staged", "--", ...pathspecs], { cwd: resolved.gitCwd });
717
+ } catch (error) {
718
+ if (!(error instanceof GitExecutorError) || !/could not resolve HEAD|unknown revision|bad revision/i.test(error.stderr)) throw error;
719
+ await runGit(["rm", "-r", "--cached", "-q", "--", ...pathspecs], { cwd: resolved.gitCwd });
720
+ }
721
+ ctx.host.http.writeJson(res, 200, { ok: true });
722
+ } catch (error) {
723
+ writeGitFailure(res, ctx, error);
724
+ }
725
+ }
726
+ async function handleRepositoryDiscard(req, res, ctx) {
727
+ const resolved = await resolveWriteContext(req, res, ctx);
728
+ if (!resolved) return;
729
+ const hasTracked = resolved.body.paths !== void 0;
730
+ const hasUntracked = resolved.body.untrackedPaths !== void 0;
731
+ if (!hasTracked && !hasUntracked) {
732
+ ctx.host.http.writeJson(res, 400, { error: "invalid_paths" });
733
+ return;
734
+ }
735
+ const tracked = hasTracked ? readStagePaths(resolved.body.paths) : [];
736
+ const untracked = hasUntracked ? readStagePaths(resolved.body.untrackedPaths) : [];
737
+ if (tracked === null || untracked === null) {
738
+ ctx.host.http.writeJson(res, 400, { error: "invalid_paths" });
739
+ return;
740
+ }
741
+ try {
742
+ if (tracked.length > 0) {
743
+ await runGit(["restore", "--worktree", "--", ...literalPathspecs(tracked)], { cwd: resolved.gitCwd });
744
+ }
745
+ if (untracked.length > 0) {
746
+ await runGit(["clean", "-f", "-d", "--", ...literalPathspecs(untracked)], { cwd: resolved.gitCwd });
747
+ }
748
+ ctx.host.http.writeJson(res, 200, { ok: true });
749
+ } catch (error) {
750
+ writeGitFailure(res, ctx, error);
751
+ }
752
+ }
753
+
754
+ // ../fleet-plugins/repository/server/commit-create.ts
755
+ function isPlainObject4(value) {
756
+ return typeof value === "object" && value !== null && !Array.isArray(value);
757
+ }
758
+ var MAX_SUBJECT_LENGTH = 500;
759
+ var MAX_BODY_LENGTH = 1e5;
760
+ var COMMIT_HARDENING_ARGS = [
761
+ "-c",
762
+ `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`,
763
+ "-c",
764
+ "commit.gpgSign=false"
765
+ ];
766
+ async function hasStagedChanges(gitCwd) {
767
+ try {
768
+ return (await runGit(["diff", "--cached", "--name-only", "--", "."], { cwd: gitCwd })).stdout.trim() !== "";
769
+ } catch (error) {
770
+ if (error instanceof GitExecutorError && /unknown revision|bad revision|ambiguous argument/i.test(error.stderr)) {
771
+ return (await runGit(["ls-files", "--cached"], { cwd: gitCwd })).stdout.trim() !== "";
772
+ }
773
+ throw error;
774
+ }
775
+ }
776
+ function classifyCommitError(stderr) {
777
+ if (/Please tell me who you are|unable to auto-detect email address|empty ident name/i.test(stderr)) return "identity_missing";
778
+ if (/nothing to commit|no changes added to commit|nothing added to commit/i.test(stderr)) return "nothing_to_commit";
779
+ return null;
780
+ }
781
+ async function handleRepositoryCommitCreate(req, res, ctx) {
782
+ if (req.method !== "POST") {
783
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
784
+ return;
785
+ }
786
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
787
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
788
+ return;
789
+ }
790
+ const body = await ctx.host.http.readJsonBody(req);
791
+ if (!isPlainObject4(body) || "subPath" in body || typeof body.theaterId !== "string") {
792
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
793
+ return;
794
+ }
795
+ const subject = typeof body.subject === "string" ? body.subject.trim() : "";
796
+ const message = body.message === void 0 ? "" : body.message;
797
+ const amend = body.amend === true;
798
+ if (subject === "" || subject.length > MAX_SUBJECT_LENGTH || typeof message !== "string" || message.length > MAX_BODY_LENGTH || subject.includes("\0") || message.includes("\0")) {
799
+ ctx.host.http.writeJson(res, 400, { error: "invalid_message" });
800
+ return;
801
+ }
802
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
803
+ if (!theaterPath) {
804
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
805
+ return;
806
+ }
807
+ let gitCwd;
808
+ try {
809
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
810
+ } catch (error) {
811
+ if (error instanceof InvalidRepoError) {
812
+ ctx.host.http.writeJson(res, 400, { error: error.code });
813
+ return;
814
+ }
815
+ throw error;
816
+ }
817
+ try {
818
+ if (!amend && !await hasStagedChanges(gitCwd)) {
819
+ ctx.host.http.writeJson(res, 422, { error: "nothing_to_commit" });
820
+ return;
821
+ }
822
+ const trimmedMessage = message.trim();
823
+ const result = await runGit([
824
+ ...COMMIT_HARDENING_ARGS,
825
+ "commit",
826
+ "--no-verify",
827
+ ...amend ? ["--amend"] : [],
828
+ "-m",
829
+ subject,
830
+ ...trimmedMessage ? ["-m", trimmedMessage] : []
831
+ ], { cwd: gitCwd });
832
+ const sha = (await runGit(["rev-parse", "HEAD"], { cwd: gitCwd })).stdout.trim();
833
+ ctx.host.http.writeJson(res, 200, { ok: true, sha, summary: result.stdout.split("\n")[0] ?? "" });
834
+ } catch (error) {
835
+ if (error instanceof GitExecutorError) {
836
+ const classified = classifyCommitError(error.stderr) ?? classifyCommitError(error.message);
837
+ if (classified) {
838
+ ctx.host.http.writeJson(res, 422, { error: classified });
839
+ return;
840
+ }
841
+ const locked = classifyWriteError(error.stderr);
842
+ if (locked) {
843
+ ctx.host.http.writeJson(res, 409, { error: locked });
844
+ return;
845
+ }
846
+ if (error.code === "no_git_repo" || error.code === "git_unavailable" || error.code === "timeout") {
847
+ ctx.host.http.writeJson(res, 422, { error: error.code });
848
+ return;
849
+ }
850
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
851
+ return;
852
+ }
853
+ throw error;
854
+ }
855
+ }
856
+ function isPlainObject5(value) {
857
+ return typeof value === "object" && value !== null && !Array.isArray(value);
858
+ }
605
859
  function resolveLiteralFilePath(gitCwd, rawPath) {
606
860
  const relativePath = path3.normalize(rawPath);
607
861
  const resolved = path3.resolve(gitCwd, relativePath);
@@ -621,7 +875,7 @@ async function handleRepositoryCommitFile(req, res, ctx) {
621
875
  return;
622
876
  }
623
877
  const body = await ctx.host.http.readJsonBody(req);
624
- if (!isPlainObject3(body) || "subPath" in body || typeof body.theaterId !== "string" || typeof body.ref !== "string" || typeof body.filePath !== "string") {
878
+ if (!isPlainObject5(body) || "subPath" in body || typeof body.theaterId !== "string" || typeof body.ref !== "string" || typeof body.filePath !== "string") {
625
879
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
626
880
  return;
627
881
  }
@@ -675,7 +929,7 @@ async function handleRepositoryCommitFile(req, res, ctx) {
675
929
  }
676
930
 
677
931
  // ../fleet-plugins/repository/server/compare.ts
678
- function isPlainObject4(value) {
932
+ function isPlainObject6(value) {
679
933
  return typeof value === "object" && value !== null && !Array.isArray(value);
680
934
  }
681
935
  var FULL_REF_PREFIXES = ["refs/heads/", "refs/remotes/", "refs/tags/"];
@@ -719,7 +973,7 @@ async function handleRepositoryCompare(req, res, ctx) {
719
973
  return;
720
974
  }
721
975
  const body = await ctx.host.http.readJsonBody(req);
722
- if (!isPlainObject4(body) || "subPath" in body) {
976
+ if (!isPlainObject6(body) || "subPath" in body) {
723
977
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
724
978
  return;
725
979
  }
@@ -787,7 +1041,7 @@ async function handleRepositoryCompare(req, res, ctx) {
787
1041
  throw error;
788
1042
  }
789
1043
  }
790
- function isPlainObject5(value) {
1044
+ function isPlainObject7(value) {
791
1045
  return typeof value === "object" && value !== null && !Array.isArray(value);
792
1046
  }
793
1047
  function resolveLiteralFilePath2(gitCwd, rawPath) {
@@ -809,7 +1063,7 @@ async function handleRepositoryCompareFile(req, res, ctx) {
809
1063
  return;
810
1064
  }
811
1065
  const body = await ctx.host.http.readJsonBody(req);
812
- if (!isPlainObject5(body) || "subPath" in body || typeof body.theaterId !== "string" || typeof body.base !== "string" || typeof body.head !== "string" || typeof body.filePath !== "string") {
1066
+ if (!isPlainObject7(body) || "subPath" in body || typeof body.theaterId !== "string" || typeof body.base !== "string" || typeof body.head !== "string" || typeof body.filePath !== "string") {
813
1067
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
814
1068
  return;
815
1069
  }
@@ -873,7 +1127,7 @@ async function handleRepositoryCompareFile(req, res, ctx) {
873
1127
  var AUTO_FETCH_THROTTLE_MS = 3e5;
874
1128
  var autoFetchInFlight = /* @__PURE__ */ new Map();
875
1129
  var lastSuccessfulFetchAt = /* @__PURE__ */ new Map();
876
- function isPlainObject6(value) {
1130
+ function isPlainObject8(value) {
877
1131
  return typeof value === "object" && value !== null && !Array.isArray(value);
878
1132
  }
879
1133
  function classifyFetchError(stderr) {
@@ -922,6 +1176,16 @@ function countFetchUpdatedRefs(stderr) {
922
1176
  }
923
1177
  var NoRemoteError = class extends Error {
924
1178
  };
1179
+ var NETWORK_HARDENING_ARGS = [
1180
+ "-c",
1181
+ "core.sshCommand=ssh",
1182
+ "-c",
1183
+ "core.gitProxy=none",
1184
+ "-c",
1185
+ "protocol.ext.allow=never",
1186
+ "-c",
1187
+ `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`
1188
+ ];
925
1189
  var SAFE_CREDENTIAL_HELPERS = /* @__PURE__ */ new Set([
926
1190
  "cache",
927
1191
  "store",
@@ -1043,7 +1307,7 @@ async function handleRepositoryFetch(req, res, ctx) {
1043
1307
  return;
1044
1308
  }
1045
1309
  const body = await ctx.host.http.readJsonBody(req);
1046
- if (!isPlainObject6(body) || "subPath" in body || typeof body.theaterId !== "string" || body.mode !== void 0 && body.mode !== "auto") {
1310
+ if (!isPlainObject8(body) || "subPath" in body || typeof body.theaterId !== "string" || body.mode !== void 0 && body.mode !== "auto") {
1047
1311
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1048
1312
  return;
1049
1313
  }
@@ -1103,7 +1367,7 @@ function resolveLogOrder(requested) {
1103
1367
  if (requested === void 0) return "topo";
1104
1368
  return requested === "topo" || requested === "date" ? requested : null;
1105
1369
  }
1106
- function isPlainObject7(value) {
1370
+ function isPlainObject9(value) {
1107
1371
  return typeof value === "object" && value !== null && !Array.isArray(value);
1108
1372
  }
1109
1373
  function isCanonicalRepositoryRef(ref) {
@@ -1230,7 +1494,7 @@ async function handleRepositoryLog(req, res, ctx) {
1230
1494
  return;
1231
1495
  }
1232
1496
  const body = await ctx.host.http.readJsonBody(req);
1233
- if (!isPlainObject7(body) || "subPath" in body) {
1497
+ if (!isPlainObject9(body) || "subPath" in body) {
1234
1498
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1235
1499
  return;
1236
1500
  }
@@ -1319,6 +1583,145 @@ async function handleRepositoryLog(req, res, ctx) {
1319
1583
  }
1320
1584
  }
1321
1585
 
1586
+ // ../fleet-plugins/repository/server/remote.ts
1587
+ function isPlainObject10(value) {
1588
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1589
+ }
1590
+ var REMOTE_TIMEOUT_MS = 12e4;
1591
+ function classifyPushError(stderr) {
1592
+ return /non-fast-forward|fetch first|updates were rejected/i.test(stderr) ? "non_fast_forward" : null;
1593
+ }
1594
+ function classifyPullError(stderr) {
1595
+ if (/not possible to fast-forward|need to specify how to reconcile|diverg/i.test(stderr)) return "non_fast_forward";
1596
+ if (/overwritten by merge|Please commit your changes or stash/i.test(stderr)) return "dirty_worktree";
1597
+ return null;
1598
+ }
1599
+ async function resolveRemoteContext(req, res, ctx) {
1600
+ if (req.method !== "POST") {
1601
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
1602
+ return null;
1603
+ }
1604
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
1605
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
1606
+ return null;
1607
+ }
1608
+ const body = await ctx.host.http.readJsonBody(req);
1609
+ if (!isPlainObject10(body) || "subPath" in body || typeof body.theaterId !== "string") {
1610
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1611
+ return null;
1612
+ }
1613
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
1614
+ if (!theaterPath) {
1615
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
1616
+ return null;
1617
+ }
1618
+ let gitCwd;
1619
+ try {
1620
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
1621
+ } catch (error) {
1622
+ if (error instanceof InvalidRepoError) {
1623
+ ctx.host.http.writeJson(res, 400, { error: error.code });
1624
+ return null;
1625
+ }
1626
+ throw error;
1627
+ }
1628
+ try {
1629
+ const branch = (await runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: gitCwd, allowExitCodes: [1] })).stdout.trim();
1630
+ if (!branch) {
1631
+ ctx.host.http.writeJson(res, 422, { error: "detached_head" });
1632
+ return null;
1633
+ }
1634
+ return { gitCwd, branch };
1635
+ } catch (error) {
1636
+ writeRemoteFailure(res, ctx, error, () => null);
1637
+ return null;
1638
+ }
1639
+ }
1640
+ function writeRemoteFailure(res, ctx, error, classify) {
1641
+ if (error instanceof NoRemoteError) {
1642
+ ctx.host.http.writeJson(res, 422, { error: "no_remote" });
1643
+ return;
1644
+ }
1645
+ if (error instanceof GitExecutorError) {
1646
+ if (error.code === "timeout") {
1647
+ ctx.host.http.writeJson(res, 422, { error: "timeout" });
1648
+ return;
1649
+ }
1650
+ const specific = classify(error.stderr);
1651
+ if (specific) {
1652
+ ctx.host.http.writeJson(res, 409, { error: specific });
1653
+ return;
1654
+ }
1655
+ const network = classifyFetchError(error.stderr);
1656
+ if (network) {
1657
+ ctx.host.http.writeJson(res, 422, { error: network });
1658
+ return;
1659
+ }
1660
+ const locked = classifyWriteError(error.stderr);
1661
+ if (locked) {
1662
+ ctx.host.http.writeJson(res, 409, { error: locked });
1663
+ return;
1664
+ }
1665
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
1666
+ ctx.host.http.writeJson(res, 422, { error: error.code });
1667
+ return;
1668
+ }
1669
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
1670
+ return;
1671
+ }
1672
+ throw error;
1673
+ }
1674
+ async function handleRepositoryPush(req, res, ctx) {
1675
+ const remoteCtx = await resolveRemoteContext(req, res, ctx);
1676
+ if (!remoteCtx) return;
1677
+ const { gitCwd, branch } = remoteCtx;
1678
+ try {
1679
+ const upstreamRemote = (await runGit(["config", "--get", `branch.${branch}.remote`], { cwd: gitCwd, allowExitCodes: [1] })).stdout.trim();
1680
+ const remote = upstreamRemote && upstreamRemote !== "." ? upstreamRemote : await resolveDefaultRemote(gitCwd);
1681
+ if (!remote) throw new NoRemoteError();
1682
+ const credentialArgs = await resolveCredentialHelperArgs(gitCwd);
1683
+ await runGit([
1684
+ ...NETWORK_HARDENING_ARGS,
1685
+ ...credentialArgs,
1686
+ "push",
1687
+ // repo config의 remote.<name>.receivepack이 로컬 transport에서 명령 실행되므로 표준 명령을 강제한다.
1688
+ "--receive-pack=git-receive-pack",
1689
+ "--no-recurse-submodules",
1690
+ ...upstreamRemote ? [] : ["--set-upstream"],
1691
+ remote,
1692
+ `refs/heads/${branch}:refs/heads/${branch}`
1693
+ ], { cwd: gitCwd, timeoutMs: REMOTE_TIMEOUT_MS });
1694
+ ctx.host.http.writeJson(res, 200, { ok: true, remote, branch });
1695
+ } catch (error) {
1696
+ writeRemoteFailure(res, ctx, error, classifyPushError);
1697
+ }
1698
+ }
1699
+ async function handleRepositoryPull(req, res, ctx) {
1700
+ const remoteCtx = await resolveRemoteContext(req, res, ctx);
1701
+ if (!remoteCtx) return;
1702
+ const { gitCwd, branch } = remoteCtx;
1703
+ try {
1704
+ const upstreamRemote = (await runGit(["config", "--get", `branch.${branch}.remote`], { cwd: gitCwd, allowExitCodes: [1] })).stdout.trim();
1705
+ if (!upstreamRemote || upstreamRemote === ".") {
1706
+ ctx.host.http.writeJson(res, 422, { error: "no_upstream" });
1707
+ return;
1708
+ }
1709
+ const credentialArgs = await resolveCredentialHelperArgs(gitCwd);
1710
+ await runGit([
1711
+ ...NETWORK_HARDENING_ARGS,
1712
+ ...credentialArgs,
1713
+ "pull",
1714
+ "--ff-only",
1715
+ "--no-rebase",
1716
+ "--upload-pack=git-upload-pack",
1717
+ "--no-recurse-submodules"
1718
+ ], { cwd: gitCwd, timeoutMs: REMOTE_TIMEOUT_MS });
1719
+ ctx.host.http.writeJson(res, 200, { ok: true, remote: upstreamRemote, branch });
1720
+ } catch (error) {
1721
+ writeRemoteFailure(res, ctx, error, classifyPullError);
1722
+ }
1723
+ }
1724
+
1322
1725
  // ../fleet-plugins/repository/server/refs.ts
1323
1726
  function isObject(value) {
1324
1727
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1421,7 +1824,7 @@ var HARD_CAP_DEPTH = 8;
1421
1824
  var NESTED_BRANCH_CAP = 64;
1422
1825
  var SCAN_VISIT_BUDGET = 1e4;
1423
1826
  var DEFAULT_DEPTH = 3;
1424
- function isPlainObject8(value) {
1827
+ function isPlainObject11(value) {
1425
1828
  return typeof value === "object" && value !== null && !Array.isArray(value);
1426
1829
  }
1427
1830
  async function resolveRepoBranch(repoDir) {
@@ -1510,7 +1913,7 @@ async function handleRepositoryRepos(req, res, ctx) {
1510
1913
  return;
1511
1914
  }
1512
1915
  const body = await ctx.host.http.readJsonBody(req);
1513
- if (!isPlainObject8(body) || typeof body.theaterId !== "string") {
1916
+ if (!isPlainObject11(body) || typeof body.theaterId !== "string") {
1514
1917
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1515
1918
  return;
1516
1919
  }
@@ -1589,7 +1992,7 @@ async function handleRepositorySearch(req, res, ctx) {
1589
1992
  return;
1590
1993
  }
1591
1994
  const body = await ctx.host.http.readJsonBody(req);
1592
- if (!isPlainObject9(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8 || "subPath" in body || "ref" in body) {
1995
+ if (!isPlainObject12(body) || typeof body.theaterId !== "string" || typeof body.query !== "string" || body.query.trim() === "" || !Number.isInteger(body.limit) || body.limit < 1 || body.limit > 8 || "subPath" in body || "ref" in body) {
1593
1996
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1594
1997
  return;
1595
1998
  }
@@ -1643,10 +2046,451 @@ async function handleRepositorySearch(req, res, ctx) {
1643
2046
  throw error;
1644
2047
  }
1645
2048
  }
1646
- function isPlainObject9(value) {
2049
+ function isPlainObject12(value) {
1647
2050
  return typeof value === "object" && value !== null && !Array.isArray(value);
1648
2051
  }
1649
- function isPlainObject10(value) {
2052
+
2053
+ // ../fleet-plugins/repository/server/stash.ts
2054
+ function isPlainObject13(value) {
2055
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2056
+ }
2057
+ var STASH_NAME_RE = /^stash@\{\d{1,4}\}$/;
2058
+ var MAX_STASH_MESSAGE_LENGTH = 500;
2059
+ var STASH_HARDENING_ARGS = [
2060
+ "-c",
2061
+ `core.hooksPath=${process.platform === "win32" ? "NUL" : "/dev/null"}`
2062
+ ];
2063
+ function readStashAction(value) {
2064
+ return value === "save" || value === "apply" || value === "pop" || value === "drop" ? value : null;
2065
+ }
2066
+ function classifyStashError(stderr) {
2067
+ if (/conflict|could not restore untracked files|overwritten by merge/i.test(stderr)) return "stash_conflict";
2068
+ if (/No local changes to save/i.test(stderr)) return "nothing_to_stash";
2069
+ return null;
2070
+ }
2071
+ async function handleRepositoryStash(req, res, ctx) {
2072
+ if (req.method !== "POST") {
2073
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
2074
+ return;
2075
+ }
2076
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
2077
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
2078
+ return;
2079
+ }
2080
+ const body = await ctx.host.http.readJsonBody(req);
2081
+ if (!isPlainObject13(body) || "subPath" in body || typeof body.theaterId !== "string") {
2082
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2083
+ return;
2084
+ }
2085
+ const action = readStashAction(body.action);
2086
+ if (!action) {
2087
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2088
+ return;
2089
+ }
2090
+ const name = body.name;
2091
+ if (action !== "save" && (typeof name !== "string" || !STASH_NAME_RE.test(name))) {
2092
+ ctx.host.http.writeJson(res, 400, { error: "invalid_stash" });
2093
+ return;
2094
+ }
2095
+ const sha = body.sha;
2096
+ if (action !== "save" && (typeof sha !== "string" || !/^[0-9a-f]{7,40}$/i.test(sha))) {
2097
+ ctx.host.http.writeJson(res, 400, { error: "invalid_stash" });
2098
+ return;
2099
+ }
2100
+ const message = body.message === void 0 ? "" : body.message;
2101
+ if (typeof message !== "string" || message.length > MAX_STASH_MESSAGE_LENGTH || message.includes("\0") || message.trimStart().startsWith("-")) {
2102
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2103
+ return;
2104
+ }
2105
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
2106
+ if (!theaterPath) {
2107
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
2108
+ return;
2109
+ }
2110
+ let gitCwd;
2111
+ try {
2112
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
2113
+ } catch (error) {
2114
+ if (error instanceof InvalidRepoError) {
2115
+ ctx.host.http.writeJson(res, 400, { error: error.code });
2116
+ return;
2117
+ }
2118
+ throw error;
2119
+ }
2120
+ try {
2121
+ if (action === "save") {
2122
+ const trimmed = message.trim();
2123
+ const result = await runGit([
2124
+ ...STASH_HARDENING_ARGS,
2125
+ "stash",
2126
+ "push",
2127
+ "-u",
2128
+ ...trimmed ? ["-m", trimmed] : []
2129
+ ], { cwd: gitCwd });
2130
+ if (/No local changes to save/i.test(result.stdout)) {
2131
+ ctx.host.http.writeJson(res, 422, { error: "nothing_to_stash" });
2132
+ return;
2133
+ }
2134
+ ctx.host.http.writeJson(res, 200, { ok: true });
2135
+ return;
2136
+ }
2137
+ let resolved;
2138
+ try {
2139
+ resolved = (await runGit(["rev-parse", "--verify", `${name}`], { cwd: gitCwd })).stdout.trim();
2140
+ } catch (error) {
2141
+ if (error instanceof GitExecutorError && error.code === "non_zero_exit") {
2142
+ ctx.host.http.writeJson(res, 409, { error: "stash_moved" });
2143
+ return;
2144
+ }
2145
+ throw error;
2146
+ }
2147
+ if (!resolved.toLowerCase().startsWith(sha.toLowerCase())) {
2148
+ ctx.host.http.writeJson(res, 409, { error: "stash_moved" });
2149
+ return;
2150
+ }
2151
+ await runGit([...STASH_HARDENING_ARGS, "stash", action, name], { cwd: gitCwd });
2152
+ ctx.host.http.writeJson(res, 200, { ok: true });
2153
+ } catch (error) {
2154
+ if (error instanceof GitExecutorError) {
2155
+ const classified = classifyStashError(error.stderr);
2156
+ if (classified) {
2157
+ ctx.host.http.writeJson(res, 409, { error: classified });
2158
+ return;
2159
+ }
2160
+ const locked = classifyWriteError(error.stderr);
2161
+ if (locked) {
2162
+ ctx.host.http.writeJson(res, 409, { error: locked });
2163
+ return;
2164
+ }
2165
+ if (error.code === "no_git_repo" || error.code === "git_unavailable" || error.code === "timeout") {
2166
+ ctx.host.http.writeJson(res, 422, { error: error.code });
2167
+ return;
2168
+ }
2169
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
2170
+ return;
2171
+ }
2172
+ throw error;
2173
+ }
2174
+ }
2175
+
2176
+ // ../fleet-plugins/repository/server/status.ts
2177
+ function isPlainObject14(value) {
2178
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2179
+ }
2180
+ function parseNumstat(stdout) {
2181
+ const map = /* @__PURE__ */ new Map();
2182
+ const records = stdout.split("\0");
2183
+ for (let index = 0; index < records.length; index += 1) {
2184
+ const record = records[index];
2185
+ const parts = record.split(" ");
2186
+ if (parts.length < 3) continue;
2187
+ let filePath = parts[2];
2188
+ if (filePath === "" && index + 2 < records.length) {
2189
+ filePath = records[index + 2];
2190
+ index += 2;
2191
+ }
2192
+ if (!filePath) continue;
2193
+ map.set(filePath, {
2194
+ additions: Number.parseInt(parts[0] ?? "0", 10) || 0,
2195
+ deletions: Number.parseInt(parts[1] ?? "0", 10) || 0
2196
+ });
2197
+ }
2198
+ return map;
2199
+ }
2200
+ var STATUS_CHARS = /* @__PURE__ */ new Set(["M", "A", "D", "R", "T"]);
2201
+ function toEntry(statusChar, filePath, oldPath, nums) {
2202
+ if (!STATUS_CHARS.has(statusChar)) return null;
2203
+ const stat = nums.get(filePath) ?? { additions: 0, deletions: 0 };
2204
+ return {
2205
+ path: filePath,
2206
+ ...oldPath ? { oldPath } : {},
2207
+ status: statusChar,
2208
+ ...stat
2209
+ };
2210
+ }
2211
+ function parseStatusV2(stdout, stagedNums, unstagedNums) {
2212
+ const staged = [];
2213
+ const unstaged = [];
2214
+ const records = stdout.split("\0");
2215
+ for (let index = 0; index < records.length; index += 1) {
2216
+ const record = records[index];
2217
+ if (!record) continue;
2218
+ const kind = record.charAt(0);
2219
+ if (kind === "1" || kind === "2") {
2220
+ const fields = record.split(" ");
2221
+ const xy = fields[1] ?? "..";
2222
+ const filePath = fields.slice(kind === "1" ? 8 : 9).join(" ");
2223
+ if (!filePath) continue;
2224
+ let oldPath;
2225
+ if (kind === "2" && index + 1 < records.length) {
2226
+ oldPath = records[index + 1] || void 0;
2227
+ index += 1;
2228
+ }
2229
+ const stagedChar = xy.charAt(0);
2230
+ const unstagedChar = xy.charAt(1);
2231
+ if (stagedChar !== ".") {
2232
+ const entry = toEntry(stagedChar, filePath, oldPath, stagedNums);
2233
+ if (entry) staged.push(entry);
2234
+ }
2235
+ if (unstagedChar !== ".") {
2236
+ const entry = toEntry(unstagedChar === "R" ? "R" : unstagedChar, filePath, void 0, unstagedNums);
2237
+ if (entry) unstaged.push(entry);
2238
+ }
2239
+ } else if (kind === "?") {
2240
+ const filePath = record.slice(2);
2241
+ if (filePath) unstaged.push({ path: filePath, status: "U", additions: 0, deletions: 0 });
2242
+ } else if (kind === "u") {
2243
+ const filePath = record.split(" ").slice(10).join(" ");
2244
+ if (filePath) unstaged.push({ path: filePath, status: "U", conflicted: true, additions: 0, deletions: 0 });
2245
+ }
2246
+ }
2247
+ return { staged, unstaged };
2248
+ }
2249
+ async function handleRepositoryStatus(req, res, ctx) {
2250
+ if (req.method !== "POST") {
2251
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
2252
+ return;
2253
+ }
2254
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
2255
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
2256
+ return;
2257
+ }
2258
+ const body = await ctx.host.http.readJsonBody(req);
2259
+ if (!isPlainObject14(body) || "subPath" in body || typeof body.theaterId !== "string") {
2260
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2261
+ return;
2262
+ }
2263
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
2264
+ if (!theaterPath) {
2265
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
2266
+ return;
2267
+ }
2268
+ let gitCwd;
2269
+ try {
2270
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
2271
+ } catch (error) {
2272
+ if (error instanceof InvalidRepoError) {
2273
+ ctx.host.http.writeJson(res, 400, { error: error.code });
2274
+ return;
2275
+ }
2276
+ throw error;
2277
+ }
2278
+ try {
2279
+ const [statusResult, stagedNumsResult, unstagedNumsResult] = await Promise.all([
2280
+ runGit(["status", "--porcelain=v2", "--untracked-files=all", "-z"], { cwd: gitCwd }),
2281
+ // 스테이지 열의 +/− — unborn HEAD에서는 비교 대상이 없어 실패하므로 빈 목록으로 강등한다.
2282
+ runGit(["diff", "--cached", "--numstat", "-z", "--", "."], { cwd: gitCwd }).catch(() => ({ stdout: "", truncated: false })),
2283
+ runGit(["diff", "--numstat", "-z", "--", "."], { cwd: gitCwd }).catch(() => ({ stdout: "", truncated: false }))
2284
+ ]);
2285
+ const parsed = parseStatusV2(statusResult.stdout, parseNumstat(stagedNumsResult.stdout), parseNumstat(unstagedNumsResult.stdout));
2286
+ const payload = { staged: parsed.staged, unstaged: parsed.unstaged, truncated: statusResult.truncated };
2287
+ ctx.host.http.writeJson(res, 200, payload);
2288
+ } catch (error) {
2289
+ if (error instanceof GitExecutorError) {
2290
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
2291
+ ctx.host.http.writeJson(res, 422, { error: error.code });
2292
+ return;
2293
+ }
2294
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
2295
+ return;
2296
+ }
2297
+ throw error;
2298
+ }
2299
+ }
2300
+ function isPlainObject15(value) {
2301
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2302
+ }
2303
+ function parseTreeEntries(stdout) {
2304
+ const entries = [];
2305
+ for (const record of stdout.split("\0")) {
2306
+ if (!record) continue;
2307
+ const tab = record.indexOf(" ");
2308
+ if (tab < 0) continue;
2309
+ const meta = record.slice(0, tab).split(" ");
2310
+ const entryPath = record.slice(tab + 1);
2311
+ const kind = meta[1];
2312
+ if (!entryPath || kind !== "tree" && kind !== "blob") continue;
2313
+ const trimmed = entryPath.endsWith("/") ? entryPath.slice(0, -1) : entryPath;
2314
+ entries.push({ path: entryPath, name: trimmed.slice(trimmed.lastIndexOf("/") + 1), kind });
2315
+ }
2316
+ return entries.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "tree" ? -1 : 1);
2317
+ }
2318
+ function isSafeTreeDirPath(dirPath) {
2319
+ if (dirPath === "") return true;
2320
+ if (path3.posix.isAbsolute(dirPath) || path3.isAbsolute(dirPath)) return false;
2321
+ if (dirPath.includes("\0") || dirPath.startsWith("-") || dirPath.startsWith(":")) return false;
2322
+ const normalized = path3.posix.normalize(dirPath);
2323
+ return normalized !== ".." && !normalized.startsWith("../");
2324
+ }
2325
+ async function handleRepositoryTree(req, res, ctx) {
2326
+ if (req.method !== "POST") {
2327
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
2328
+ return;
2329
+ }
2330
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
2331
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
2332
+ return;
2333
+ }
2334
+ const body = await ctx.host.http.readJsonBody(req);
2335
+ if (!isPlainObject15(body) || "subPath" in body || typeof body.theaterId !== "string") {
2336
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2337
+ return;
2338
+ }
2339
+ const ref = body.ref;
2340
+ if (typeof ref !== "string" || !REF_RE.test(ref)) {
2341
+ ctx.host.http.writeJson(res, 400, { error: "invalid_ref" });
2342
+ return;
2343
+ }
2344
+ const dirPath = body.dirPath === void 0 ? "" : body.dirPath;
2345
+ if (typeof dirPath !== "string" || !isSafeTreeDirPath(dirPath)) {
2346
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2347
+ return;
2348
+ }
2349
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
2350
+ if (!theaterPath) {
2351
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
2352
+ return;
2353
+ }
2354
+ let gitCwd;
2355
+ try {
2356
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
2357
+ } catch (error) {
2358
+ if (error instanceof InvalidRepoError) {
2359
+ ctx.host.http.writeJson(res, 400, { error: error.code });
2360
+ return;
2361
+ }
2362
+ throw error;
2363
+ }
2364
+ try {
2365
+ const args = ["ls-tree", "-z", "--full-tree", ref, "--", dirPath === "" ? "." : `${dirPath}/`];
2366
+ const result = await runGit(args, { cwd: gitCwd });
2367
+ const payload = { entries: parseTreeEntries(result.stdout), truncated: result.truncated };
2368
+ ctx.host.http.writeJson(res, 200, payload);
2369
+ } catch (error) {
2370
+ if (error instanceof GitExecutorError) {
2371
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
2372
+ ctx.host.http.writeJson(res, 422, { error: error.code });
2373
+ return;
2374
+ }
2375
+ if (error.code === "non_zero_exit" && /not a tree object|Not a valid object name|bad object/i.test(error.stderr)) {
2376
+ ctx.host.http.writeJson(res, 404, { error: "ref_not_found" });
2377
+ return;
2378
+ }
2379
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
2380
+ return;
2381
+ }
2382
+ throw error;
2383
+ }
2384
+ }
2385
+ function isPlainObject16(value) {
2386
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2387
+ }
2388
+ async function fileExists(candidate) {
2389
+ try {
2390
+ await fs2.stat(candidate);
2391
+ return true;
2392
+ } catch {
2393
+ return false;
2394
+ }
2395
+ }
2396
+ function parseAheadBehind(stdout) {
2397
+ const match = /^(\d+)\s+(\d+)$/.exec(stdout.trim());
2398
+ if (!match) return null;
2399
+ return { behind: Number.parseInt(match[1], 10), ahead: Number.parseInt(match[2], 10) };
2400
+ }
2401
+ async function handleRepositoryWorkstate(req, res, ctx) {
2402
+ if (req.method !== "POST") {
2403
+ ctx.host.http.writeJson(res, 405, { error: "Method not allowed" });
2404
+ return;
2405
+ }
2406
+ if (!ctx.host.security.isTerminalAuthorized(req)) {
2407
+ ctx.host.http.writeJson(res, 401, { error: "unauthorized" });
2408
+ return;
2409
+ }
2410
+ const body = await ctx.host.http.readJsonBody(req);
2411
+ if (!isPlainObject16(body) || "subPath" in body || typeof body.theaterId !== "string") {
2412
+ ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
2413
+ return;
2414
+ }
2415
+ const theaterPath = ctx.host.paths.resolveTheaterPath(body.theaterId);
2416
+ if (!theaterPath) {
2417
+ ctx.host.http.writeJson(res, 404, { error: "theater_not_found" });
2418
+ return;
2419
+ }
2420
+ let gitCwd;
2421
+ try {
2422
+ ({ gitCwd } = await resolveGitCwd(theaterPath, body.repoRel));
2423
+ } catch (error) {
2424
+ if (error instanceof InvalidRepoError) {
2425
+ ctx.host.http.writeJson(res, 400, { error: error.code });
2426
+ return;
2427
+ }
2428
+ throw error;
2429
+ }
2430
+ try {
2431
+ const [gitDirResult, headResult, headShaResult] = await Promise.all([
2432
+ runGit(["rev-parse", "--absolute-git-dir"], { cwd: gitCwd }),
2433
+ runGit(["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: gitCwd, allowExitCodes: [1] }),
2434
+ runGit(["rev-parse", "--quiet", "--verify", "HEAD"], { cwd: gitCwd, allowExitCodes: [1] })
2435
+ ]);
2436
+ const gitDir = path3.resolve(gitCwd, gitDirResult.stdout.trim());
2437
+ const headBranch = headResult.stdout.trim() || null;
2438
+ const headSha = headShaResult.stdout.trim() || null;
2439
+ const [indexLock, merge, rebaseMerge, rebaseApply, cherryPick] = await Promise.all([
2440
+ fileExists(path3.join(gitDir, "index.lock")),
2441
+ fileExists(path3.join(gitDir, "MERGE_HEAD")),
2442
+ fileExists(path3.join(gitDir, "rebase-merge")),
2443
+ fileExists(path3.join(gitDir, "rebase-apply")),
2444
+ fileExists(path3.join(gitDir, "CHERRY_PICK_HEAD"))
2445
+ ]);
2446
+ const inProgress = merge ? "merge" : rebaseMerge || rebaseApply ? "rebase" : cherryPick ? "cherry-pick" : null;
2447
+ let upstream = null;
2448
+ let ahead = null;
2449
+ let behind = null;
2450
+ if (headBranch) {
2451
+ const upstreamResult = await runGit(
2452
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"],
2453
+ { cwd: gitCwd, allowExitCodes: [128] }
2454
+ );
2455
+ upstream = upstreamResult.stdout.trim() || null;
2456
+ if (upstream) {
2457
+ const counts = parseAheadBehind((await runGit(["rev-list", "--left-right", "--count", "@{upstream}...HEAD"], { cwd: gitCwd })).stdout);
2458
+ ahead = counts?.ahead ?? null;
2459
+ behind = counts?.behind ?? null;
2460
+ }
2461
+ }
2462
+ let realGitCwd = gitCwd;
2463
+ try {
2464
+ realGitCwd = await fs2.realpath(gitCwd);
2465
+ } catch {
2466
+ }
2467
+ const stationedOperations = [];
2468
+ for (const operation of ctx.host.operations.list()) {
2469
+ const rawCwd = operation.payload?.cwd;
2470
+ const cwd = typeof rawCwd === "string" ? rawCwd : theaterPath;
2471
+ let realCwd;
2472
+ try {
2473
+ realCwd = await fs2.realpath(cwd);
2474
+ } catch {
2475
+ continue;
2476
+ }
2477
+ if (isPathContained(realGitCwd, realCwd)) stationedOperations.push({ id: operation.id, title: operation.title });
2478
+ }
2479
+ const payload = { indexLock, inProgress, headBranch, headSha, upstream, ahead, behind, stationedOperations };
2480
+ ctx.host.http.writeJson(res, 200, payload);
2481
+ } catch (error) {
2482
+ if (error instanceof GitExecutorError) {
2483
+ if (error.code === "no_git_repo" || error.code === "git_unavailable") {
2484
+ ctx.host.http.writeJson(res, 422, { error: error.code });
2485
+ return;
2486
+ }
2487
+ ctx.host.http.writeJson(res, 500, { error: "git_failed" });
2488
+ return;
2489
+ }
2490
+ throw error;
2491
+ }
2492
+ }
2493
+ function isPlainObject17(value) {
1650
2494
  return typeof value === "object" && value !== null && !Array.isArray(value);
1651
2495
  }
1652
2496
  async function handleRepositoryWorktrees(req, res, ctx) {
@@ -1659,7 +2503,7 @@ async function handleRepositoryWorktrees(req, res, ctx) {
1659
2503
  return;
1660
2504
  }
1661
2505
  const body = await ctx.host.http.readJsonBody(req);
1662
- if (!isPlainObject10(body) || "subPath" in body || typeof body.theaterId !== "string") {
2506
+ if (!isPlainObject17(body) || "subPath" in body || typeof body.theaterId !== "string") {
1663
2507
  ctx.host.http.writeJson(res, 400, { error: "invalid_request" });
1664
2508
  return;
1665
2509
  }
@@ -1776,6 +2620,46 @@ var routes_default = definePlugin({
1776
2620
  await handleRepositoryCompareFile(req, res, ctx);
1777
2621
  return true;
1778
2622
  }, { method: "POST", path: "", summary: "Compare repository files.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2623
+ registerRouter(ctx, "tree", async ({ req, res }) => {
2624
+ await handleRepositoryTree(req, res, ctx);
2625
+ return true;
2626
+ }, { method: "POST", path: "", summary: "List one folder of a commit tree.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2627
+ registerRouter(ctx, "workstate", async ({ req, res }) => {
2628
+ await handleRepositoryWorkstate(req, res, ctx);
2629
+ return true;
2630
+ }, { method: "POST", path: "", summary: "Report write-safety state for a checkout.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2631
+ registerRouter(ctx, "status", async ({ req, res }) => {
2632
+ await handleRepositoryStatus(req, res, ctx);
2633
+ return true;
2634
+ }, { method: "POST", path: "", summary: "List staged and unstaged repository files.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2635
+ registerRouter(ctx, "stage", async ({ req, res }) => {
2636
+ await handleRepositoryStage(req, res, ctx);
2637
+ return true;
2638
+ }, { method: "POST", path: "", summary: "Stage repository files.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2639
+ registerRouter(ctx, "unstage", async ({ req, res }) => {
2640
+ await handleRepositoryUnstage(req, res, ctx);
2641
+ return true;
2642
+ }, { method: "POST", path: "", summary: "Unstage repository files.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2643
+ registerRouter(ctx, "discard", async ({ req, res }) => {
2644
+ await handleRepositoryDiscard(req, res, ctx);
2645
+ return true;
2646
+ }, { method: "POST", path: "", summary: "Discard working-tree changes for chosen files.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2647
+ registerRouter(ctx, "commit-create", async ({ req, res }) => {
2648
+ await handleRepositoryCommitCreate(req, res, ctx);
2649
+ return true;
2650
+ }, { method: "POST", path: "", summary: "Create or amend a commit from the staged files.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2651
+ registerRouter(ctx, "stash", async ({ req, res }) => {
2652
+ await handleRepositoryStash(req, res, ctx);
2653
+ return true;
2654
+ }, { method: "POST", path: "", summary: "Save, apply, pop, or drop a stash.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2655
+ registerRouter(ctx, "push", async ({ req, res }) => {
2656
+ await handleRepositoryPush(req, res, ctx);
2657
+ return true;
2658
+ }, { method: "POST", path: "", summary: "Push the current branch to its upstream.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
2659
+ registerRouter(ctx, "pull", async ({ req, res }) => {
2660
+ await handleRepositoryPull(req, res, ctx);
2661
+ return true;
2662
+ }, { method: "POST", path: "", summary: "Fast-forward the current branch from its upstream.", category: "Repository Plugin", gate: "origin-write", transport: "http" });
1779
2663
  }
1780
2664
  });
1781
2665