@jait/gateway 0.1.148 → 0.1.150

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 (144) hide show
  1. package/dist/providers/claude-code-provider.d.ts.map +1 -1
  2. package/dist/providers/claude-code-provider.js +22 -2
  3. package/dist/providers/claude-code-provider.js.map +1 -1
  4. package/dist/providers/codex-event-mapper.d.ts.map +1 -1
  5. package/dist/providers/codex-event-mapper.js +5 -2
  6. package/dist/providers/codex-event-mapper.js.map +1 -1
  7. package/dist/providers/copilot-provider.d.ts.map +1 -1
  8. package/dist/providers/copilot-provider.js +24 -5
  9. package/dist/providers/copilot-provider.js.map +1 -1
  10. package/dist/providers/gemini-provider.d.ts +6 -0
  11. package/dist/providers/gemini-provider.d.ts.map +1 -1
  12. package/dist/providers/gemini-provider.js +128 -16
  13. package/dist/providers/gemini-provider.js.map +1 -1
  14. package/dist/providers/jait-provider.d.ts +2 -1
  15. package/dist/providers/jait-provider.d.ts.map +1 -1
  16. package/dist/providers/jait-provider.js +16 -0
  17. package/dist/providers/jait-provider.js.map +1 -1
  18. package/dist/providers/opencode-provider.d.ts.map +1 -1
  19. package/dist/providers/opencode-provider.js +18 -2
  20. package/dist/providers/opencode-provider.js.map +1 -1
  21. package/dist/routes/chat.d.ts.map +1 -1
  22. package/dist/routes/chat.js +36 -3
  23. package/dist/routes/chat.js.map +1 -1
  24. package/dist/routes/git.d.ts.map +1 -1
  25. package/dist/routes/git.js +87 -1
  26. package/dist/routes/git.js.map +1 -1
  27. package/dist/routes/repositories.d.ts.map +1 -1
  28. package/dist/routes/repositories.js +9 -3
  29. package/dist/routes/repositories.js.map +1 -1
  30. package/dist/services/git-forge.d.ts +123 -0
  31. package/dist/services/git-forge.d.ts.map +1 -0
  32. package/dist/services/git-forge.js +686 -0
  33. package/dist/services/git-forge.js.map +1 -0
  34. package/dist/services/git.d.ts +1 -2
  35. package/dist/services/git.d.ts.map +1 -1
  36. package/package.json +1 -1
  37. package/web-dist/assets/{_basePickBy-CMcRAnqh.js → _basePickBy-LU8wIi8b.js} +1 -1
  38. package/web-dist/assets/{_baseUniq-DiG-gjzX.js → _baseUniq-cAoMJ-MY.js} +1 -1
  39. package/web-dist/{dist/assets/arc-8zSvdafL.js → assets/arc-C18JJSYg.js} +1 -1
  40. package/web-dist/{dist/assets/architectureDiagram-2XIMDMQ5-uV_qdZLp.js → assets/architectureDiagram-2XIMDMQ5-nz7zxpaB.js} +1 -1
  41. package/web-dist/assets/{blockDiagram-WCTKOSBZ-D_DoJaaX.js → blockDiagram-WCTKOSBZ-BGHtMvVK.js} +1 -1
  42. package/web-dist/assets/{c4Diagram-IC4MRINW-Ce0cQzk3.js → c4Diagram-IC4MRINW-rqsOXKFz.js} +1 -1
  43. package/web-dist/assets/channel-DKj1dAKt.js +1 -0
  44. package/web-dist/assets/{chunk-4BX2VUAB-D4AMg2Bs.js → chunk-4BX2VUAB-CpXKbpFf.js} +1 -1
  45. package/web-dist/assets/{chunk-55IACEB6-DoMMBL__.js → chunk-55IACEB6-CQdYUf0H.js} +1 -1
  46. package/web-dist/assets/{chunk-FMBD7UC4-CYf525fL.js → chunk-FMBD7UC4-B0vrPNyY.js} +1 -1
  47. package/web-dist/assets/{chunk-JSJVCQXG-Nss8Md0k.js → chunk-JSJVCQXG-BUBQoCR9.js} +1 -1
  48. package/web-dist/assets/{chunk-KX2RTZJC-_0pgSdsc.js → chunk-KX2RTZJC-CbzQLLM-.js} +1 -1
  49. package/web-dist/{dist/assets/chunk-NQ4KR5QH-DAIj1AYn.js → assets/chunk-NQ4KR5QH-CtEZ7vPf.js} +1 -1
  50. package/web-dist/assets/{chunk-QZHKN3VN-rSyB5-Hb.js → chunk-QZHKN3VN-TAvb5hte.js} +1 -1
  51. package/web-dist/{dist/assets/chunk-WL4C6EOR-CpTjmrFV.js → assets/chunk-WL4C6EOR-C_iTviJq.js} +1 -1
  52. package/web-dist/assets/classDiagram-VBA2DB6C-mY7WkylN.js +1 -0
  53. package/web-dist/assets/classDiagram-v2-RAHNMMFH-mY7WkylN.js +1 -0
  54. package/web-dist/assets/clone-uFhDaKsD.js +1 -0
  55. package/web-dist/{dist/assets/cose-bilkent-S5V4N54A-bW-AeIv0.js → assets/cose-bilkent-S5V4N54A-DIMZuEOJ.js} +1 -1
  56. package/web-dist/assets/{dagre-KLK3FWXG-C3LgqjWf.js → dagre-KLK3FWXG-C0tNDBnv.js} +1 -1
  57. package/web-dist/assets/{diagram-E7M64L7V-FSqHe-E-.js → diagram-E7M64L7V-TLjtBSKq.js} +1 -1
  58. package/web-dist/{dist/assets/diagram-IFDJBPK2-CVKla5Pl.js → assets/diagram-IFDJBPK2-CtO-hSG2.js} +1 -1
  59. package/web-dist/assets/{diagram-P4PSJMXO-DbdkNkqR.js → diagram-P4PSJMXO-Cv6QIRY-.js} +1 -1
  60. package/web-dist/{dist/assets/erDiagram-INFDFZHY-GDZX_pjq.js → assets/erDiagram-INFDFZHY-D_Yg-41-.js} +1 -1
  61. package/web-dist/assets/{flowDiagram-PKNHOUZH-Cg_Kg-6s.js → flowDiagram-PKNHOUZH-D6NH9QlA.js} +1 -1
  62. package/web-dist/assets/{ganttDiagram-A5KZAMGK-BNj6qaAZ.js → ganttDiagram-A5KZAMGK-B5gY6Skm.js} +1 -1
  63. package/web-dist/assets/{gitGraphDiagram-K3NZZRJ6-CYDlDHPF.js → gitGraphDiagram-K3NZZRJ6-Dj9SIGcr.js} +1 -1
  64. package/web-dist/assets/{graph-CgnxvBit.js → graph-CQZD-Od5.js} +1 -1
  65. package/web-dist/assets/{index-0BjDR9yA.js → index-B5YMcrIB.js} +277 -272
  66. package/web-dist/assets/{index-BVFq-LvQ.css → index-D4ARb74M.css} +1 -1
  67. package/web-dist/assets/{infoDiagram-LFFYTUFH-BuSCypq8.js → infoDiagram-LFFYTUFH-D361ALhC.js} +1 -1
  68. package/web-dist/{dist/assets/ishikawaDiagram-PHBUUO56-AjFXL8hp.js → assets/ishikawaDiagram-PHBUUO56-DphZk_Xd.js} +1 -1
  69. package/web-dist/{dist/assets/journeyDiagram-4ABVD52K-T24AykCT.js → assets/journeyDiagram-4ABVD52K-CGFCaXH2.js} +1 -1
  70. package/web-dist/assets/{kanban-definition-K7BYSVSG-BFmOJ3Za.js → kanban-definition-K7BYSVSG-wD7MbgLO.js} +1 -1
  71. package/web-dist/{dist/assets/layout-DBZbe3s5.js → assets/layout-uO5JcAM7.js} +1 -1
  72. package/web-dist/assets/{linear-DH_G-pjt.js → linear-BmyTkSil.js} +1 -1
  73. package/web-dist/{dist/assets/mindmap-definition-YRQLILUH-DmrFp1JV.js → assets/mindmap-definition-YRQLILUH-DOayLZ5O.js} +1 -1
  74. package/web-dist/assets/{pieDiagram-SKSYHLDU-DICPv0I1.js → pieDiagram-SKSYHLDU-BWhhu1eE.js} +1 -1
  75. package/web-dist/{dist/assets/quadrantDiagram-337W2JSQ-4ZQkWCL2.js → assets/quadrantDiagram-337W2JSQ-BPYgkTJB.js} +1 -1
  76. package/web-dist/{dist/assets/requirementDiagram-Z7DCOOCP-xuxQgvoc.js → assets/requirementDiagram-Z7DCOOCP-TrZHvSP6.js} +1 -1
  77. package/web-dist/{dist/assets/sankeyDiagram-WA2Y5GQK-fZCbCQr0.js → assets/sankeyDiagram-WA2Y5GQK-uwaqx1iU.js} +1 -1
  78. package/web-dist/assets/{sequenceDiagram-2WXFIKYE-BpvPwC4U.js → sequenceDiagram-2WXFIKYE-COm8M834.js} +1 -1
  79. package/web-dist/assets/{stateDiagram-RAJIS63D-CtazoD_k.js → stateDiagram-RAJIS63D-x4uR-tWs.js} +1 -1
  80. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-C00OpKKp.js +1 -0
  81. package/web-dist/{dist/assets/timeline-definition-YZTLITO2-TOninkTE.js → assets/timeline-definition-YZTLITO2-Bg5ngdqT.js} +1 -1
  82. package/web-dist/assets/{treemap-KZPCXAKY-DN7nCe4Y.js → treemap-KZPCXAKY-DWRt-GPG.js} +1 -1
  83. package/web-dist/{dist/assets/vennDiagram-LZ73GAT5-Cu5kB4rI.js → assets/vennDiagram-LZ73GAT5-Db_DI7RR.js} +1 -1
  84. package/web-dist/assets/{xychartDiagram-JWTSCODW-CKRZxJIX.js → xychartDiagram-JWTSCODW-DsOCdAG-.js} +1 -1
  85. package/web-dist/dist/assets/{_basePickBy-CMcRAnqh.js → _basePickBy-LU8wIi8b.js} +1 -1
  86. package/web-dist/dist/assets/{_baseUniq-DiG-gjzX.js → _baseUniq-cAoMJ-MY.js} +1 -1
  87. package/web-dist/{assets/arc-8zSvdafL.js → dist/assets/arc-C18JJSYg.js} +1 -1
  88. package/web-dist/{assets/architectureDiagram-2XIMDMQ5-uV_qdZLp.js → dist/assets/architectureDiagram-2XIMDMQ5-nz7zxpaB.js} +1 -1
  89. package/web-dist/dist/assets/{blockDiagram-WCTKOSBZ-D_DoJaaX.js → blockDiagram-WCTKOSBZ-BGHtMvVK.js} +1 -1
  90. package/web-dist/dist/assets/{c4Diagram-IC4MRINW-Ce0cQzk3.js → c4Diagram-IC4MRINW-rqsOXKFz.js} +1 -1
  91. package/web-dist/dist/assets/channel-DKj1dAKt.js +1 -0
  92. package/web-dist/dist/assets/{chunk-4BX2VUAB-D4AMg2Bs.js → chunk-4BX2VUAB-CpXKbpFf.js} +1 -1
  93. package/web-dist/dist/assets/{chunk-55IACEB6-DoMMBL__.js → chunk-55IACEB6-CQdYUf0H.js} +1 -1
  94. package/web-dist/dist/assets/{chunk-FMBD7UC4-CYf525fL.js → chunk-FMBD7UC4-B0vrPNyY.js} +1 -1
  95. package/web-dist/dist/assets/{chunk-JSJVCQXG-Nss8Md0k.js → chunk-JSJVCQXG-BUBQoCR9.js} +1 -1
  96. package/web-dist/dist/assets/{chunk-KX2RTZJC-_0pgSdsc.js → chunk-KX2RTZJC-CbzQLLM-.js} +1 -1
  97. package/web-dist/{assets/chunk-NQ4KR5QH-DAIj1AYn.js → dist/assets/chunk-NQ4KR5QH-CtEZ7vPf.js} +1 -1
  98. package/web-dist/dist/assets/{chunk-QZHKN3VN-rSyB5-Hb.js → chunk-QZHKN3VN-TAvb5hte.js} +1 -1
  99. package/web-dist/{assets/chunk-WL4C6EOR-CpTjmrFV.js → dist/assets/chunk-WL4C6EOR-C_iTviJq.js} +1 -1
  100. package/web-dist/dist/assets/classDiagram-VBA2DB6C-mY7WkylN.js +1 -0
  101. package/web-dist/dist/assets/classDiagram-v2-RAHNMMFH-mY7WkylN.js +1 -0
  102. package/web-dist/dist/assets/clone-uFhDaKsD.js +1 -0
  103. package/web-dist/{assets/cose-bilkent-S5V4N54A-bW-AeIv0.js → dist/assets/cose-bilkent-S5V4N54A-DIMZuEOJ.js} +1 -1
  104. package/web-dist/dist/assets/{dagre-KLK3FWXG-C3LgqjWf.js → dagre-KLK3FWXG-C0tNDBnv.js} +1 -1
  105. package/web-dist/dist/assets/{diagram-E7M64L7V-FSqHe-E-.js → diagram-E7M64L7V-TLjtBSKq.js} +1 -1
  106. package/web-dist/{assets/diagram-IFDJBPK2-CVKla5Pl.js → dist/assets/diagram-IFDJBPK2-CtO-hSG2.js} +1 -1
  107. package/web-dist/dist/assets/{diagram-P4PSJMXO-DbdkNkqR.js → diagram-P4PSJMXO-Cv6QIRY-.js} +1 -1
  108. package/web-dist/{assets/erDiagram-INFDFZHY-GDZX_pjq.js → dist/assets/erDiagram-INFDFZHY-D_Yg-41-.js} +1 -1
  109. package/web-dist/dist/assets/{flowDiagram-PKNHOUZH-Cg_Kg-6s.js → flowDiagram-PKNHOUZH-D6NH9QlA.js} +1 -1
  110. package/web-dist/dist/assets/{ganttDiagram-A5KZAMGK-BNj6qaAZ.js → ganttDiagram-A5KZAMGK-B5gY6Skm.js} +1 -1
  111. package/web-dist/dist/assets/{gitGraphDiagram-K3NZZRJ6-CYDlDHPF.js → gitGraphDiagram-K3NZZRJ6-Dj9SIGcr.js} +1 -1
  112. package/web-dist/dist/assets/{graph-CgnxvBit.js → graph-CQZD-Od5.js} +1 -1
  113. package/web-dist/dist/assets/{index-0BjDR9yA.js → index-B5YMcrIB.js} +277 -272
  114. package/web-dist/dist/assets/{index-BVFq-LvQ.css → index-D4ARb74M.css} +1 -1
  115. package/web-dist/dist/assets/{infoDiagram-LFFYTUFH-BuSCypq8.js → infoDiagram-LFFYTUFH-D361ALhC.js} +1 -1
  116. package/web-dist/{assets/ishikawaDiagram-PHBUUO56-AjFXL8hp.js → dist/assets/ishikawaDiagram-PHBUUO56-DphZk_Xd.js} +1 -1
  117. package/web-dist/{assets/journeyDiagram-4ABVD52K-T24AykCT.js → dist/assets/journeyDiagram-4ABVD52K-CGFCaXH2.js} +1 -1
  118. package/web-dist/dist/assets/{kanban-definition-K7BYSVSG-BFmOJ3Za.js → kanban-definition-K7BYSVSG-wD7MbgLO.js} +1 -1
  119. package/web-dist/{assets/layout-DBZbe3s5.js → dist/assets/layout-uO5JcAM7.js} +1 -1
  120. package/web-dist/dist/assets/{linear-DH_G-pjt.js → linear-BmyTkSil.js} +1 -1
  121. package/web-dist/{assets/mindmap-definition-YRQLILUH-DmrFp1JV.js → dist/assets/mindmap-definition-YRQLILUH-DOayLZ5O.js} +1 -1
  122. package/web-dist/dist/assets/{pieDiagram-SKSYHLDU-DICPv0I1.js → pieDiagram-SKSYHLDU-BWhhu1eE.js} +1 -1
  123. package/web-dist/{assets/quadrantDiagram-337W2JSQ-4ZQkWCL2.js → dist/assets/quadrantDiagram-337W2JSQ-BPYgkTJB.js} +1 -1
  124. package/web-dist/{assets/requirementDiagram-Z7DCOOCP-xuxQgvoc.js → dist/assets/requirementDiagram-Z7DCOOCP-TrZHvSP6.js} +1 -1
  125. package/web-dist/{assets/sankeyDiagram-WA2Y5GQK-fZCbCQr0.js → dist/assets/sankeyDiagram-WA2Y5GQK-uwaqx1iU.js} +1 -1
  126. package/web-dist/dist/assets/{sequenceDiagram-2WXFIKYE-BpvPwC4U.js → sequenceDiagram-2WXFIKYE-COm8M834.js} +1 -1
  127. package/web-dist/dist/assets/{stateDiagram-RAJIS63D-CtazoD_k.js → stateDiagram-RAJIS63D-x4uR-tWs.js} +1 -1
  128. package/web-dist/dist/assets/stateDiagram-v2-FVOUBMTO-C00OpKKp.js +1 -0
  129. package/web-dist/{assets/timeline-definition-YZTLITO2-TOninkTE.js → dist/assets/timeline-definition-YZTLITO2-Bg5ngdqT.js} +1 -1
  130. package/web-dist/dist/assets/{treemap-KZPCXAKY-DN7nCe4Y.js → treemap-KZPCXAKY-DWRt-GPG.js} +1 -1
  131. package/web-dist/{assets/vennDiagram-LZ73GAT5-Cu5kB4rI.js → dist/assets/vennDiagram-LZ73GAT5-Db_DI7RR.js} +1 -1
  132. package/web-dist/dist/assets/{xychartDiagram-JWTSCODW-CKRZxJIX.js → xychartDiagram-JWTSCODW-DsOCdAG-.js} +1 -1
  133. package/web-dist/dist/index.html +2 -2
  134. package/web-dist/index.html +2 -2
  135. package/web-dist/assets/channel-BHaRqHuw.js +0 -1
  136. package/web-dist/assets/classDiagram-VBA2DB6C-D-jlQbaz.js +0 -1
  137. package/web-dist/assets/classDiagram-v2-RAHNMMFH-D-jlQbaz.js +0 -1
  138. package/web-dist/assets/clone-xemW6BD5.js +0 -1
  139. package/web-dist/assets/stateDiagram-v2-FVOUBMTO-C8JhD5ad.js +0 -1
  140. package/web-dist/dist/assets/channel-BHaRqHuw.js +0 -1
  141. package/web-dist/dist/assets/classDiagram-VBA2DB6C-D-jlQbaz.js +0 -1
  142. package/web-dist/dist/assets/classDiagram-v2-RAHNMMFH-D-jlQbaz.js +0 -1
  143. package/web-dist/dist/assets/clone-xemW6BD5.js +0 -1
  144. package/web-dist/dist/assets/stateDiagram-v2-FVOUBMTO-C8JhD5ad.js +0 -1
@@ -0,0 +1,686 @@
1
+ /**
2
+ * Generic Git forge abstraction — GitHub, GitLab, Gitea, Azure DevOps, Bitbucket.
3
+ *
4
+ * Each forge implements the same interface so PR creation, auth checks,
5
+ * and status queries work uniformly regardless of hosting provider.
6
+ */
7
+ import { exec as execCb } from "node:child_process";
8
+ import { writeFile, unlink } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+ import { promisify } from "node:util";
12
+ import { parseGitRemote } from "./git.js";
13
+ const exec = promisify(execCb);
14
+ // ── Helpers ──────────────────────────────────────────────────────────
15
+ function cleanGhEnv() {
16
+ const { GH_TOKEN, GITHUB_TOKEN, ...rest } = process.env;
17
+ return rest;
18
+ }
19
+ async function cliExec(cmd, cwd, timeout = 30_000, env) {
20
+ const { stdout } = await exec(cmd, { cwd, timeout, env });
21
+ return stdout.trim();
22
+ }
23
+ async function cliAvailable(cmd, cwd) {
24
+ try {
25
+ await exec(`${cmd} --version`, { cwd, timeout: 5_000 });
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ function apiHeaders(token, extra) {
33
+ return {
34
+ Authorization: `Bearer ${token}`,
35
+ "User-Agent": "jait-gateway",
36
+ ...extra,
37
+ };
38
+ }
39
+ // ══════════════════════════════════════════════════════════════════════
40
+ // GitHub Forge
41
+ // ══════════════════════════════════════════════════════════════════════
42
+ export class GitHubForge {
43
+ provider = "github";
44
+ displayName = "GitHub";
45
+ async checkCliAvailable(cwd) {
46
+ return cliAvailable("gh", cwd);
47
+ }
48
+ async checkAuth(cwd) {
49
+ try {
50
+ const out = await cliExec("gh auth status", cwd, 10_000, cleanGhEnv());
51
+ const userMatch = out.match(/Logged in to .+ as (\S+)/);
52
+ return { authenticated: true, username: userMatch?.[1] };
53
+ }
54
+ catch (err) {
55
+ return { authenticated: false, error: err instanceof Error ? err.message : String(err) };
56
+ }
57
+ }
58
+ async loginWithToken(token, cwd) {
59
+ try {
60
+ const { spawn } = await import("node:child_process");
61
+ await new Promise((resolve, reject) => {
62
+ const child = spawn("gh", ["auth", "login", "--with-token"], {
63
+ cwd, stdio: "pipe", shell: true, env: cleanGhEnv(),
64
+ });
65
+ child.stdin.write(token);
66
+ child.stdin.end();
67
+ child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`gh auth failed (code ${code})`)));
68
+ child.on("error", reject);
69
+ });
70
+ const username = await cliExec('gh api user --jq ".login"', cwd, 10_000, cleanGhEnv()).catch(() => "");
71
+ return { authenticated: true, username: username || undefined };
72
+ }
73
+ catch (err) {
74
+ return { authenticated: false, error: err instanceof Error ? err.message : String(err) };
75
+ }
76
+ }
77
+ async resolveToken(_cwd) {
78
+ const quick = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? process.env.GITHUB_PAT ?? null;
79
+ if (quick)
80
+ return quick;
81
+ // Try git credential manager
82
+ try {
83
+ const { spawn } = await import("node:child_process");
84
+ return await new Promise((resolve) => {
85
+ const proc = spawn("git", ["credential", "fill"], { timeout: 5_000 });
86
+ let out = "";
87
+ proc.stdout.on("data", (d) => { out += d.toString(); });
88
+ proc.on("close", () => {
89
+ const match = out.match(/^password=(.+)$/m);
90
+ resolve(match?.[1]?.trim() ?? null);
91
+ });
92
+ proc.on("error", () => resolve(null));
93
+ proc.stdin.write("protocol=https\nhost=github.com\n\n");
94
+ proc.stdin.end();
95
+ });
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
101
+ async getDefaultBranch(cwd, _remote) {
102
+ try {
103
+ const json = await cliExec("gh repo view --json defaultBranchRef", cwd, 15_000, cleanGhEnv());
104
+ const parsed = JSON.parse(json);
105
+ const ref = parsed.defaultBranchRef;
106
+ if (ref?.name)
107
+ return String(ref.name);
108
+ }
109
+ catch { /* not available */ }
110
+ return null;
111
+ }
112
+ async findExistingPr(cwd, remote, headBranch, token) {
113
+ // Try gh CLI first
114
+ if (await this.checkCliAvailable(cwd)) {
115
+ try {
116
+ const raw = await cliExec(`gh pr view --head "${headBranch}" --json number,url,title,state,baseRefName,headRefName`, cwd, 15_000, cleanGhEnv());
117
+ const parsed = JSON.parse(raw);
118
+ if (parsed.number) {
119
+ return {
120
+ status: "opened_existing",
121
+ url: String(parsed.url ?? ""),
122
+ number: Number(parsed.number),
123
+ baseBranch: String(parsed.baseRefName ?? ""),
124
+ headBranch: String(parsed.headRefName ?? headBranch),
125
+ title: String(parsed.title ?? ""),
126
+ state: String(parsed.state ?? "OPEN").toLowerCase() === "open" ? "open" : "closed",
127
+ };
128
+ }
129
+ }
130
+ catch { /* no existing PR */ }
131
+ }
132
+ // Try API
133
+ const effectiveToken = token ?? await this.resolveToken(cwd);
134
+ if (effectiveToken && remote.owner) {
135
+ return this.findPrViaApi(remote, effectiveToken, headBranch);
136
+ }
137
+ return null;
138
+ }
139
+ async createPr(cwd, remote, input, token) {
140
+ // Try gh CLI first
141
+ if (await this.checkCliAvailable(cwd)) {
142
+ return this.createPrViaCli(cwd, input);
143
+ }
144
+ // Fall back to API
145
+ const effectiveToken = token ?? await this.resolveToken(cwd);
146
+ if (effectiveToken && remote.owner) {
147
+ return this.createPrViaApi(remote, effectiveToken, input);
148
+ }
149
+ return { status: "not_found" };
150
+ }
151
+ async getPrChecks(cwd, _headBranch) {
152
+ if (!await this.checkCliAvailable(cwd))
153
+ return [];
154
+ try {
155
+ const raw = await cliExec("gh pr checks --json name,state,conclusion,startedAt,completedAt,detailsUrl", cwd, 15_000, cleanGhEnv());
156
+ return JSON.parse(raw);
157
+ }
158
+ catch {
159
+ return [];
160
+ }
161
+ }
162
+ buildCreatePrUrl(remote, headBranch, _baseBranch) {
163
+ return `${remote.normalizedUrl}/compare/${encodeURIComponent(headBranch)}?expand=1`;
164
+ }
165
+ async createPrViaCli(cwd, input) {
166
+ const bodyFile = join(tmpdir(), `jait-pr-body-${Date.now()}.md`);
167
+ await writeFile(bodyFile, input.body, "utf-8");
168
+ try {
169
+ const baseFlag = input.baseBranch ? ` --base "${input.baseBranch}"` : "";
170
+ const prUrl = await cliExec(`gh pr create --title "${input.title.replace(/"/g, '\\"')}" --body-file "${bodyFile}"${baseFlag}`, cwd, 60_000, cleanGhEnv());
171
+ let prNumber = 0;
172
+ let baseBranch = input.baseBranch;
173
+ let headBranch = input.headBranch;
174
+ let title = input.title;
175
+ try {
176
+ const details = await cliExec(`gh pr view "${prUrl.trim()}" --json number,title,baseRefName,headRefName`, cwd, 15_000, cleanGhEnv());
177
+ const p = JSON.parse(details);
178
+ prNumber = Number(p.number ?? 0);
179
+ baseBranch = String(p.baseRefName ?? baseBranch);
180
+ headBranch = String(p.headRefName ?? headBranch);
181
+ title = String(p.title ?? title);
182
+ }
183
+ catch { /* best effort */ }
184
+ return { status: "created", url: prUrl.trim(), number: prNumber, baseBranch, headBranch, title };
185
+ }
186
+ finally {
187
+ await unlink(bodyFile).catch(() => { });
188
+ }
189
+ }
190
+ async createPrViaApi(remote, token, input) {
191
+ const apiBase = remote.host === "github.com" ? "https://api.github.com" : `https://${remote.host}/api/v3`;
192
+ const headers = { ...apiHeaders(token), Accept: "application/vnd.github+json", "Content-Type": "application/json" };
193
+ const headParam = `${remote.owner}:${input.headBranch}`;
194
+ const res = await fetch(`${apiBase}/repos/${remote.owner}/${remote.repo}/pulls`, {
195
+ method: "POST",
196
+ headers,
197
+ body: JSON.stringify({ title: input.title, head: headParam, base: input.baseBranch, body: input.body }),
198
+ });
199
+ if (!res.ok) {
200
+ const text = await res.text().catch(() => "");
201
+ throw new Error(`GitHub API PR create failed (${res.status}): ${text.slice(0, 400)}`);
202
+ }
203
+ const json = await res.json();
204
+ return {
205
+ status: "created",
206
+ url: String(json.html_url ?? ""),
207
+ number: Number(json.number ?? 0),
208
+ baseBranch: String(json.base?.ref ?? input.baseBranch),
209
+ headBranch: String(json.head?.ref ?? input.headBranch),
210
+ title: String(json.title ?? input.title),
211
+ };
212
+ }
213
+ async findPrViaApi(remote, token, headBranch) {
214
+ const apiBase = remote.host === "github.com" ? "https://api.github.com" : `https://${remote.host}/api/v3`;
215
+ const headers = apiHeaders(token, { Accept: "application/vnd.github+json" });
216
+ const headParam = `${remote.owner}:${headBranch}`;
217
+ try {
218
+ const res = await fetch(`${apiBase}/repos/${remote.owner}/${remote.repo}/pulls?head=${encodeURIComponent(headParam)}&state=all`, { headers });
219
+ if (!res.ok)
220
+ return null;
221
+ const list = await res.json();
222
+ const pr = (list.find((p) => p?.state === "open") ?? list[0]);
223
+ if (!pr?.html_url)
224
+ return null;
225
+ const merged = pr.merged_at != null;
226
+ return {
227
+ status: "opened_existing",
228
+ url: String(pr.html_url),
229
+ number: Number(pr.number ?? 0),
230
+ baseBranch: String(pr.base?.ref ?? ""),
231
+ headBranch: String(pr.head?.ref ?? headBranch),
232
+ title: String(pr.title ?? ""),
233
+ state: merged ? "merged" : String(pr.state ?? "open") === "closed" ? "closed" : "open",
234
+ };
235
+ }
236
+ catch {
237
+ return null;
238
+ }
239
+ }
240
+ }
241
+ // ══════════════════════════════════════════════════════════════════════
242
+ // GitLab Forge
243
+ // ══════════════════════════════════════════════════════════════════════
244
+ export class GitLabForge {
245
+ provider = "gitlab";
246
+ displayName = "GitLab";
247
+ async checkCliAvailable(cwd) {
248
+ return cliAvailable("glab", cwd);
249
+ }
250
+ async checkAuth(cwd) {
251
+ try {
252
+ const out = await cliExec("glab auth status", cwd, 10_000);
253
+ const userMatch = out.match(/Logged in .+ as (\S+)/i);
254
+ return { authenticated: true, username: userMatch?.[1] };
255
+ }
256
+ catch {
257
+ const token = await this.resolveToken(cwd);
258
+ return token ? { authenticated: true } : { authenticated: false, error: "glab CLI not authenticated and no GITLAB_TOKEN set" };
259
+ }
260
+ }
261
+ async loginWithToken(token, cwd) {
262
+ try {
263
+ await cliExec(`glab auth login --token "${token}"`, cwd, 15_000);
264
+ return { authenticated: true };
265
+ }
266
+ catch (err) {
267
+ return { authenticated: false, error: err instanceof Error ? err.message : String(err) };
268
+ }
269
+ }
270
+ async resolveToken(_cwd) {
271
+ return process.env.GITLAB_TOKEN ?? process.env.GITLAB_PAT ?? null;
272
+ }
273
+ async getDefaultBranch(_cwd, remote) {
274
+ const token = await this.resolveToken(_cwd);
275
+ if (!token)
276
+ return null;
277
+ const apiBase = `https://${remote.host}/api/v4`;
278
+ const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
279
+ try {
280
+ const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}`, {
281
+ headers: apiHeaders(token),
282
+ });
283
+ if (!res.ok)
284
+ return null;
285
+ const json = await res.json();
286
+ return String(json.default_branch ?? "main");
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ }
292
+ async findExistingPr(_cwd, remote, headBranch, token) {
293
+ const effectiveToken = token ?? await this.resolveToken(_cwd);
294
+ if (!effectiveToken)
295
+ return null;
296
+ const apiBase = `https://${remote.host}/api/v4`;
297
+ const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
298
+ try {
299
+ const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests?source_branch=${encodeURIComponent(headBranch)}&state=opened`, { headers: apiHeaders(effectiveToken) });
300
+ if (!res.ok)
301
+ return null;
302
+ const list = await res.json();
303
+ const mr = list[0];
304
+ if (!mr)
305
+ return null;
306
+ return {
307
+ status: "opened_existing",
308
+ url: String(mr.web_url ?? ""),
309
+ number: Number(mr.iid ?? 0),
310
+ baseBranch: String(mr.target_branch ?? ""),
311
+ headBranch: String(mr.source_branch ?? headBranch),
312
+ title: String(mr.title ?? ""),
313
+ state: "open",
314
+ };
315
+ }
316
+ catch {
317
+ return null;
318
+ }
319
+ }
320
+ async createPr(_cwd, remote, input, token) {
321
+ const effectiveToken = token ?? await this.resolveToken(_cwd);
322
+ if (!effectiveToken)
323
+ return { status: "not_found" };
324
+ const apiBase = `https://${remote.host}/api/v4`;
325
+ const projectPath = remote.owner ? `${remote.owner}/${remote.repo}` : remote.repo;
326
+ const res = await fetch(`${apiBase}/projects/${encodeURIComponent(projectPath)}/merge_requests`, {
327
+ method: "POST",
328
+ headers: { ...apiHeaders(effectiveToken), "Content-Type": "application/json" },
329
+ body: JSON.stringify({
330
+ title: input.title,
331
+ source_branch: input.headBranch,
332
+ target_branch: input.baseBranch,
333
+ description: input.body,
334
+ }),
335
+ });
336
+ if (!res.ok) {
337
+ const text = await res.text().catch(() => "");
338
+ throw new Error(`GitLab API MR create failed (${res.status}): ${text.slice(0, 400)}`);
339
+ }
340
+ const json = await res.json();
341
+ return {
342
+ status: "created",
343
+ url: String(json.web_url ?? ""),
344
+ number: Number(json.iid ?? 0),
345
+ baseBranch: String(json.target_branch ?? input.baseBranch),
346
+ headBranch: String(json.source_branch ?? input.headBranch),
347
+ title: String(json.title ?? input.title),
348
+ };
349
+ }
350
+ async getPrChecks(_cwd, _headBranch) {
351
+ return [];
352
+ }
353
+ buildCreatePrUrl(remote, headBranch, _baseBranch) {
354
+ return `${remote.normalizedUrl}/-/merge_requests/new?merge_request[source_branch]=${encodeURIComponent(headBranch)}`;
355
+ }
356
+ }
357
+ // ══════════════════════════════════════════════════════════════════════
358
+ // Gitea Forge (also covers Forgejo)
359
+ // ══════════════════════════════════════════════════════════════════════
360
+ export class GiteaForge {
361
+ provider = "gitea";
362
+ displayName = "Gitea";
363
+ async checkCliAvailable(cwd) {
364
+ return cliAvailable("tea", cwd);
365
+ }
366
+ async checkAuth(_cwd) {
367
+ const token = await this.resolveToken(_cwd);
368
+ return token
369
+ ? { authenticated: true }
370
+ : { authenticated: false, error: "No GITEA_TOKEN set. Configure it in Settings → API." };
371
+ }
372
+ async loginWithToken(_token, _cwd) {
373
+ return { authenticated: true };
374
+ }
375
+ async resolveToken(_cwd) {
376
+ return process.env.GITEA_TOKEN ?? process.env.GITEA_PAT ?? null;
377
+ }
378
+ async getDefaultBranch(_cwd, remote) {
379
+ const token = await this.resolveToken(_cwd);
380
+ if (!token || !remote.owner)
381
+ return null;
382
+ try {
383
+ const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}`, {
384
+ headers: apiHeaders(token),
385
+ });
386
+ if (!res.ok)
387
+ return null;
388
+ const json = await res.json();
389
+ return String(json.default_branch ?? "main");
390
+ }
391
+ catch {
392
+ return null;
393
+ }
394
+ }
395
+ async findExistingPr(_cwd, remote, headBranch, token) {
396
+ const effectiveToken = token ?? await this.resolveToken(_cwd);
397
+ if (!effectiveToken || !remote.owner)
398
+ return null;
399
+ try {
400
+ const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls?state=open&head=${encodeURIComponent(`${remote.owner}:${headBranch}`)}`, { headers: apiHeaders(effectiveToken) });
401
+ if (!res.ok)
402
+ return null;
403
+ const list = await res.json();
404
+ const pr = list[0];
405
+ if (!pr)
406
+ return null;
407
+ return {
408
+ status: "opened_existing",
409
+ url: String(pr.html_url ?? ""),
410
+ number: Number(pr.number ?? 0),
411
+ baseBranch: String(pr.base?.label ?? ""),
412
+ headBranch: String(pr.head?.label ?? headBranch),
413
+ title: String(pr.title ?? ""),
414
+ state: "open",
415
+ };
416
+ }
417
+ catch {
418
+ return null;
419
+ }
420
+ }
421
+ async createPr(_cwd, remote, input, token) {
422
+ const effectiveToken = token ?? await this.resolveToken(_cwd);
423
+ if (!effectiveToken || !remote.owner)
424
+ return { status: "not_found" };
425
+ const res = await fetch(`https://${remote.host}/api/v1/repos/${remote.owner}/${remote.repo}/pulls`, {
426
+ method: "POST",
427
+ headers: { ...apiHeaders(effectiveToken), "Content-Type": "application/json" },
428
+ body: JSON.stringify({
429
+ title: input.title,
430
+ head: input.headBranch,
431
+ base: input.baseBranch,
432
+ body: input.body,
433
+ }),
434
+ });
435
+ if (!res.ok) {
436
+ const text = await res.text().catch(() => "");
437
+ throw new Error(`Gitea API PR create failed (${res.status}): ${text.slice(0, 400)}`);
438
+ }
439
+ const json = await res.json();
440
+ return {
441
+ status: "created",
442
+ url: String(json.html_url ?? ""),
443
+ number: Number(json.number ?? 0),
444
+ baseBranch: String(json.base?.label ?? input.baseBranch),
445
+ headBranch: String(json.head?.label ?? input.headBranch),
446
+ title: String(json.title ?? input.title),
447
+ };
448
+ }
449
+ async getPrChecks(_cwd, _headBranch) {
450
+ return [];
451
+ }
452
+ buildCreatePrUrl(remote, headBranch, baseBranch) {
453
+ return `${remote.normalizedUrl}/compare/${encodeURIComponent(baseBranch ?? "main")}...${encodeURIComponent(headBranch)}`;
454
+ }
455
+ }
456
+ // ══════════════════════════════════════════════════════════════════════
457
+ // Azure DevOps Forge
458
+ // ══════════════════════════════════════════════════════════════════════
459
+ export class AzureDevOpsForge {
460
+ provider = "azure-devops";
461
+ displayName = "Azure DevOps";
462
+ async checkCliAvailable(cwd) {
463
+ return cliAvailable("az", cwd);
464
+ }
465
+ async checkAuth(cwd) {
466
+ try {
467
+ const out = await cliExec("az account show --query user.name -o tsv", cwd, 10_000);
468
+ return { authenticated: true, username: out || undefined };
469
+ }
470
+ catch {
471
+ const token = await this.resolveToken(cwd);
472
+ return token
473
+ ? { authenticated: true }
474
+ : { authenticated: false, error: "Azure CLI not authenticated and no AZURE_DEVOPS_PAT set" };
475
+ }
476
+ }
477
+ async loginWithToken(_token, _cwd) {
478
+ return { authenticated: true };
479
+ }
480
+ async resolveToken(_cwd) {
481
+ return process.env.AZURE_DEVOPS_PAT ?? process.env.AZURE_DEVOPS_TOKEN ?? null;
482
+ }
483
+ async getDefaultBranch(_cwd, _remote) {
484
+ return null;
485
+ }
486
+ async findExistingPr(cwd, remote, headBranch, _token) {
487
+ if (!await this.checkCliAvailable(cwd))
488
+ return null;
489
+ const orgUrl = this.buildOrgUrl(remote);
490
+ try {
491
+ const raw = await cliExec(`az repos pr list --organization "${orgUrl}" --project "${remote.project}" --repository "${remote.repo}" --source-branch "${headBranch}" --status active --output json`, cwd, 30_000);
492
+ const list = JSON.parse(raw);
493
+ const first = list[0];
494
+ if (!first)
495
+ return null;
496
+ const prId = Number(first.pullRequestId ?? 0);
497
+ return {
498
+ status: "opened_existing",
499
+ url: this.buildPrUrl(remote, prId),
500
+ number: prId,
501
+ baseBranch: String(first.targetRefName ?? "").replace(/^refs\/heads\//, ""),
502
+ headBranch: String(first.sourceRefName ?? headBranch).replace(/^refs\/heads\//, ""),
503
+ title: String(first.title ?? ""),
504
+ state: "open",
505
+ };
506
+ }
507
+ catch {
508
+ return null;
509
+ }
510
+ }
511
+ async createPr(cwd, remote, input, _token) {
512
+ if (!await this.checkCliAvailable(cwd))
513
+ return { status: "not_found" };
514
+ const orgUrl = this.buildOrgUrl(remote);
515
+ const bodyFile = join(tmpdir(), `jait-az-pr-body-${Date.now()}.md`);
516
+ await writeFile(bodyFile, input.body, "utf-8");
517
+ try {
518
+ const raw = await cliExec(`az repos pr create --organization "${orgUrl}" --project "${remote.project}" --repository "${remote.repo}" --source-branch "${input.headBranch}" --target-branch "${input.baseBranch}" --title "${input.title.replace(/"/g, '\\"')}" --description @"${bodyFile}" --output json`, cwd, 60_000);
519
+ const created = JSON.parse(raw);
520
+ const prId = Number(created.pullRequestId ?? 0);
521
+ return {
522
+ status: "created",
523
+ url: this.buildPrUrl(remote, prId),
524
+ number: prId,
525
+ baseBranch: String(created.targetRefName ?? input.baseBranch).replace(/^refs\/heads\//, ""),
526
+ headBranch: String(created.sourceRefName ?? input.headBranch).replace(/^refs\/heads\//, ""),
527
+ title: String(created.title ?? input.title),
528
+ };
529
+ }
530
+ finally {
531
+ await unlink(bodyFile).catch(() => { });
532
+ }
533
+ }
534
+ async getPrChecks(_cwd, _headBranch) {
535
+ return [];
536
+ }
537
+ buildCreatePrUrl(remote, headBranch, baseBranch) {
538
+ const base = this.buildRepoUrl(remote);
539
+ return `${base}/pullrequestcreate?sourceRef=${encodeURIComponent(`refs/heads/${headBranch}`)}${baseBranch ? `&targetRef=${encodeURIComponent(`refs/heads/${baseBranch}`)}` : ""}`;
540
+ }
541
+ buildOrgUrl(remote) {
542
+ return remote.host === "dev.azure.com"
543
+ ? `https://dev.azure.com/${remote.organization}`
544
+ : `https://${remote.host}`;
545
+ }
546
+ buildRepoUrl(remote) {
547
+ return remote.host === "dev.azure.com"
548
+ ? `https://dev.azure.com/${remote.organization}/${remote.project}/_git/${remote.repo}`
549
+ : `https://${remote.host}/${remote.project}/_git/${remote.repo}`;
550
+ }
551
+ buildPrUrl(remote, prId) {
552
+ return `${this.buildRepoUrl(remote)}/pullrequest/${prId}`;
553
+ }
554
+ }
555
+ // ══════════════════════════════════════════════════════════════════════
556
+ // Bitbucket Forge
557
+ // ══════════════════════════════════════════════════════════════════════
558
+ export class BitbucketForge {
559
+ provider = "bitbucket";
560
+ displayName = "Bitbucket";
561
+ async checkCliAvailable(_cwd) {
562
+ return false;
563
+ }
564
+ async checkAuth(_cwd) {
565
+ const token = await this.resolveToken(_cwd);
566
+ return token
567
+ ? { authenticated: true }
568
+ : { authenticated: false, error: "No BITBUCKET_TOKEN set. Configure it in Settings → API." };
569
+ }
570
+ async loginWithToken(_token, _cwd) {
571
+ return { authenticated: true };
572
+ }
573
+ async resolveToken(_cwd) {
574
+ return process.env.BITBUCKET_TOKEN ?? process.env.BITBUCKET_APP_PASSWORD ?? null;
575
+ }
576
+ async getDefaultBranch(_cwd, remote) {
577
+ const token = await this.resolveToken(_cwd);
578
+ if (!token || !remote.owner)
579
+ return null;
580
+ try {
581
+ const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}`, {
582
+ headers: apiHeaders(token),
583
+ });
584
+ if (!res.ok)
585
+ return null;
586
+ const json = await res.json();
587
+ return json.mainbranch?.name ?? null;
588
+ }
589
+ catch {
590
+ return null;
591
+ }
592
+ }
593
+ async findExistingPr(_cwd, remote, headBranch, token) {
594
+ const effectiveToken = token ?? await this.resolveToken(_cwd);
595
+ if (!effectiveToken || !remote.owner)
596
+ return null;
597
+ try {
598
+ const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests?q=source.branch.name="${encodeURIComponent(headBranch)}"&state=OPEN`, { headers: apiHeaders(effectiveToken) });
599
+ if (!res.ok)
600
+ return null;
601
+ const data = await res.json();
602
+ const pr = data.values?.[0];
603
+ if (!pr)
604
+ return null;
605
+ return {
606
+ status: "opened_existing",
607
+ url: String(pr.links?.html?.href ?? ""),
608
+ number: Number(pr.id ?? 0),
609
+ baseBranch: String(pr.destination?.branch?.name ?? ""),
610
+ headBranch: String(pr.source?.branch?.name ?? headBranch),
611
+ title: String(pr.title ?? ""),
612
+ state: "open",
613
+ };
614
+ }
615
+ catch {
616
+ return null;
617
+ }
618
+ }
619
+ async createPr(_cwd, remote, input, token) {
620
+ const effectiveToken = token ?? await this.resolveToken(_cwd);
621
+ if (!effectiveToken || !remote.owner)
622
+ return { status: "not_found" };
623
+ const res = await fetch(`https://api.bitbucket.org/2.0/repositories/${remote.owner}/${remote.repo}/pullrequests`, {
624
+ method: "POST",
625
+ headers: { ...apiHeaders(effectiveToken), "Content-Type": "application/json" },
626
+ body: JSON.stringify({
627
+ title: input.title,
628
+ source: { branch: { name: input.headBranch } },
629
+ destination: { branch: { name: input.baseBranch } },
630
+ description: input.body,
631
+ }),
632
+ });
633
+ if (!res.ok) {
634
+ const text = await res.text().catch(() => "");
635
+ throw new Error(`Bitbucket API PR create failed (${res.status}): ${text.slice(0, 400)}`);
636
+ }
637
+ const json = await res.json();
638
+ return {
639
+ status: "created",
640
+ url: String(json.links?.html?.href ?? ""),
641
+ number: Number(json.id ?? 0),
642
+ baseBranch: String(json.destination?.branch?.name ?? input.baseBranch),
643
+ headBranch: String(json.source?.branch?.name ?? input.headBranch),
644
+ title: String(json.title ?? input.title),
645
+ };
646
+ }
647
+ async getPrChecks(_cwd, _headBranch) {
648
+ return [];
649
+ }
650
+ buildCreatePrUrl(remote, headBranch, _baseBranch) {
651
+ return `${remote.normalizedUrl}/pull-requests/new?source=${encodeURIComponent(headBranch)}`;
652
+ }
653
+ }
654
+ // ══════════════════════════════════════════════════════════════════════
655
+ // Forge Factory
656
+ // ══════════════════════════════════════════════════════════════════════
657
+ const FORGE_MAP = {
658
+ github: () => new GitHubForge(),
659
+ gitlab: () => new GitLabForge(),
660
+ gitea: () => new GiteaForge(),
661
+ "azure-devops": () => new AzureDevOpsForge(),
662
+ bitbucket: () => new BitbucketForge(),
663
+ };
664
+ let _forgeCache = new Map();
665
+ export function getForge(provider) {
666
+ if (provider === "unknown" || provider === "none")
667
+ return null;
668
+ const cached = _forgeCache.get(provider);
669
+ if (cached)
670
+ return cached;
671
+ const factory = FORGE_MAP[provider];
672
+ if (!factory)
673
+ return null;
674
+ const forge = factory();
675
+ _forgeCache.set(provider, forge);
676
+ return forge;
677
+ }
678
+ export function getForgeForRemote(remoteUrl) {
679
+ if (!remoteUrl)
680
+ return null;
681
+ const parsed = parseGitRemote(remoteUrl);
682
+ if (!parsed)
683
+ return null;
684
+ return getForge(parsed.provider);
685
+ }
686
+ //# sourceMappingURL=git-forge.js.map