@opentig/cli 0.2.1 → 0.2.2

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 (112) hide show
  1. package/dist/bin.mjs +2 -1
  2. package/dist/chunks/{cli-B6JTojx_.mjs → cli-DNzGBqmL.mjs} +1 -1
  3. package/dist/client/assets/App-BbT5H97Z.js +138 -0
  4. package/dist/client/assets/{CreatePullRequestDialog-CAeykZ72.js → CreatePullRequestDialog-DRLeMY4e.js} +2 -2
  5. package/dist/client/assets/{DiffWorkspace-BRicWHvw.js → DiffWorkspace-DJX00otP.js} +1 -1
  6. package/dist/client/assets/{EditableFileViewer-DWeSDnZn.js → EditableFileViewer-Cmbomx5y.js} +1 -1
  7. package/dist/client/assets/{File-D65shwRR.js → File-4o4egqlz.js} +1 -1
  8. package/dist/client/assets/FilesView-TmUF-VLr.js +1 -0
  9. package/dist/client/assets/{GitHubAvatar-C8R3iNoM.js → GitHubAvatar-azKPrTMv.js} +1 -1
  10. package/dist/client/assets/HistoryView-Go6rXO0P.js +1 -0
  11. package/dist/client/assets/{HtmlFileViewer-JXLHTNPm.js → HtmlFileViewer-WpYnLdMW.js} +1 -1
  12. package/dist/client/assets/{IconEdit-3fgBTCQ4.js → IconEdit-B8LZHB8l.js} +1 -1
  13. package/dist/client/assets/{IconFolderSymlink-C4ydD1eS.js → IconFolderSymlink-D29EhQPC.js} +1 -1
  14. package/dist/client/assets/{ImageFileViewer-YH2xhkkk.js → ImageFileViewer-1ObUacJu.js} +1 -1
  15. package/dist/client/assets/LocalRefsDialog-VcrlFgxR.js +1 -0
  16. package/dist/client/assets/{MarkdownFileViewer-BsB8WkIH.js → MarkdownFileViewer-CvsKPnvF.js} +2 -2
  17. package/dist/client/assets/{PairingPage-DXRyAYHn.js → PairingPage-cSoKZi8t.js} +1 -1
  18. package/dist/client/assets/{PierreDiffViewer-DoV3V0IW.js → PierreDiffViewer-DVTrOwX4.js} +1 -1
  19. package/dist/client/assets/{PierreEditBoundary-BnYZbHfi.js → PierreEditBoundary-DPtaTiUB.js} +1 -1
  20. package/dist/client/assets/{PierreWorkerPool-Cg9zdxiI.js → PierreWorkerPool-BxP-s6jD.js} +1 -1
  21. package/dist/client/assets/PullRequestDiff-DeQznd9n.js +1 -0
  22. package/dist/client/assets/PullRequestViewer-IhGZZs_A.js +2 -0
  23. package/dist/client/assets/{PullRequestsView-GStKxG7k.js → PullRequestsView-BG0xrok4.js} +1 -1
  24. package/dist/client/assets/QuickOpenDialog-DpmcAzTx.js +1 -0
  25. package/dist/client/assets/RepositoryProjectsDialog-DAzbUB3X.js +1 -0
  26. package/dist/client/assets/{SearchView-D5XB6wdu.js → SearchView-HoiWKG8z.js} +1 -1
  27. package/dist/client/assets/SettingsDialog-pmJKOaiK.js +9 -0
  28. package/dist/client/assets/{Viewer-Gfy3Ejyo.js → Viewer-BCk8FHrc.js} +3 -3
  29. package/dist/client/assets/{abnfDiagram-N423BO3Z-DdAfHevw.js → abnfDiagram-N423BO3Z-BvfKOanh.js} +1 -1
  30. package/dist/client/assets/{arc-C43uoeEi.js → arc-DB2nIH2Y.js} +1 -1
  31. package/dist/client/assets/{architectureDiagram-T3A2C74G-C7q0hhRO.js → architectureDiagram-T3A2C74G-Bqh4uY3R.js} +1 -1
  32. package/dist/client/assets/base-ui-DIP2rZxz.js +1 -0
  33. package/dist/client/assets/{blockDiagram-VBNYF7ZC-C1x9Iap4.js → blockDiagram-VBNYF7ZC-DMW415Ms.js} +1 -1
  34. package/dist/client/assets/{c4Diagram-5PPSVZJV-BmKaVW6q.js → c4Diagram-5PPSVZJV-BSNPes7l.js} +1 -1
  35. package/dist/client/assets/channel-VcDernYy.js +1 -0
  36. package/dist/client/assets/{checkbox-XfIUFPxh.js → checkbox-Dta4gqy2.js} +1 -1
  37. package/dist/client/assets/{chunk-2GRJ4B5K-kzy_H3BQ.js → chunk-2GRJ4B5K-D-86JbWu.js} +1 -1
  38. package/dist/client/assets/{chunk-2Q5K7J3B-BMj-FT4X.js → chunk-2Q5K7J3B-BNI-SD-k.js} +1 -1
  39. package/dist/client/assets/{chunk-5RXB4S5H-DmPGiCCb.js → chunk-5RXB4S5H-OFFYyML8.js} +1 -1
  40. package/dist/client/assets/{chunk-5VM5RSS4-DYFL8__L.js → chunk-5VM5RSS4-DpE1dtnL.js} +1 -1
  41. package/dist/client/assets/{chunk-6Q2QTUOP-CI98oVdT.js → chunk-6Q2QTUOP-DxAnzBXJ.js} +1 -1
  42. package/dist/client/assets/{chunk-GF5L2VYU-D9JiiMlX.js → chunk-GF5L2VYU-DNrTNRgr.js} +1 -1
  43. package/dist/client/assets/{chunk-JWPE2WC7-D0THYpZl.js → chunk-JWPE2WC7-D-jfKyIh.js} +1 -1
  44. package/dist/client/assets/{chunk-KBJHAD2P-Z1tJfyAr.js → chunk-KBJHAD2P-CIyQl_ts.js} +1 -1
  45. package/dist/client/assets/{chunk-RYQCIY6F-31aS4cD3.js → chunk-RYQCIY6F-CgCYQccM.js} +1 -1
  46. package/dist/client/assets/{chunk-XXDRQBXY-ClWDHmEp.js → chunk-XXDRQBXY-B31oXtqU.js} +1 -1
  47. package/dist/client/assets/classDiagram-JCYQIIEL-CUuEGRCt.js +1 -0
  48. package/dist/client/assets/classDiagram-v2-OCEON4UE-CUuEGRCt.js +1 -0
  49. package/dist/client/assets/{cose-bilkent-JH36ORCC-Btg2MYLh.js → cose-bilkent-JH36ORCC-CrX-LDNw.js} +1 -1
  50. package/dist/client/assets/{cynefin-VYW2F7L2-Cbh4Pk6j.js → cynefin-VYW2F7L2-DnkWlsqW.js} +1 -1
  51. package/dist/client/assets/{cynefinDiagram-MW4NZA55-BBqorl0u.js → cynefinDiagram-MW4NZA55-bUDQ9ElF.js} +1 -1
  52. package/dist/client/assets/{dagre-VZM6K2ZE-DugEItZ0.js → dagre-VZM6K2ZE-BW5Kni4n.js} +1 -1
  53. package/dist/client/assets/{diagram-7IWD3JNH-Biu7dmH8.js → diagram-7IWD3JNH-1EXGwguC.js} +1 -1
  54. package/dist/client/assets/{diagram-B4RE2ZJO-UDm4DXFS.js → diagram-B4RE2ZJO-CzpH5ieN.js} +1 -1
  55. package/dist/client/assets/{diagram-LBJQPF4R-Bcku4gb-.js → diagram-LBJQPF4R-BnKszJ_B.js} +1 -1
  56. package/dist/client/assets/{diagram-Q27KOJAE-DFRjglV-.js → diagram-Q27KOJAE-DbQlD3OM.js} +1 -1
  57. package/dist/client/assets/{diagram-UB23O5K3-CgfMWtd0.js → diagram-UB23O5K3-BqZ-RSlm.js} +1 -1
  58. package/dist/client/assets/{ebnfDiagram-BXEA7PRR-CZzYOqs0.js → ebnfDiagram-BXEA7PRR-B2iPWDvr.js} +1 -1
  59. package/dist/client/assets/{erDiagram-JOGREHBK-Cdlbxgs6.js → erDiagram-JOGREHBK-aMqHrKCk.js} +1 -1
  60. package/dist/client/assets/{flowDiagram-UKHOOZJN-BgImDwpP.js → flowDiagram-UKHOOZJN-ivrTsn4D.js} +1 -1
  61. package/dist/client/assets/{ganttDiagram-PKOTCBZU-SFhtiyL4.js → ganttDiagram-PKOTCBZU-IMfIqkXw.js} +1 -1
  62. package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-Bkm5ueys.js → gitGraphDiagram-DS77QQ5N-BNURRh2N.js} +1 -1
  63. package/dist/client/assets/{index-B5RC4THs.js → index-B3QJsvjR.js} +4 -4
  64. package/dist/client/assets/index-Cx9LaJW8.css +1 -0
  65. package/dist/client/assets/{infoDiagram-6WML65LV-pCYc-VX2.js → infoDiagram-6WML65LV-BKEENkUQ.js} +1 -1
  66. package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-BtNbG3Y6.js → ishikawaDiagram-WSZJBQD7-HrCSmiTr.js} +1 -1
  67. package/dist/client/assets/{journeyDiagram-NVQOT4AX-CPW2qT3y.js → journeyDiagram-NVQOT4AX-CQIuWA6F.js} +1 -1
  68. package/dist/client/assets/{kanban-definition-27J2QSJJ-wD6JXQFs.js → kanban-definition-27J2QSJJ-h4fSAkel.js} +1 -1
  69. package/dist/client/assets/{linear-sj8MOkmS.js → linear-BAdz-uC9.js} +1 -1
  70. package/dist/client/assets/{mermaid.core-CGJtfmwb.js → mermaid.core-vLn7mMZB.js} +4 -4
  71. package/dist/client/assets/{mindmap-definition-FAOFIHXS-BQwLHBVs.js → mindmap-definition-FAOFIHXS-DOrZl827.js} +1 -1
  72. package/dist/client/assets/{pegDiagram-VL7TDLO6-DtDsb-Z8.js → pegDiagram-VL7TDLO6-_Vs7p633.js} +1 -1
  73. package/dist/client/assets/{pieDiagram-7S7Q4E2Y-B3WhqNuS.js → pieDiagram-7S7Q4E2Y-7qbB_Z16.js} +1 -1
  74. package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-CPi38pp_.js → quadrantDiagram-CIZ2JOQS-DNH8SosA.js} +1 -1
  75. package/dist/client/assets/{railroadDiagram-AXF67PYL-Btac44Ux.js → railroadDiagram-AXF67PYL-Ctazrrpa.js} +1 -1
  76. package/dist/client/assets/{render-markdown-hAIWRfn0.js → render-markdown-dvHhHbC0.js} +1 -1
  77. package/dist/client/assets/{requirementDiagram-LRYGKXZP-CHJgDNdg.js → requirementDiagram-LRYGKXZP-BAH8dYer.js} +1 -1
  78. package/dist/client/assets/{sankeyDiagram-W5VNT64P-OYJHfmtd.js → sankeyDiagram-W5VNT64P-CuffrpdF.js} +1 -1
  79. package/dist/client/assets/{sequenceDiagram-SI44F4Z6-9ypa9W08.js → sequenceDiagram-SI44F4Z6-CaO4fJnS.js} +1 -1
  80. package/dist/client/assets/{sizeCapture-X5ZJPWSS-5ZDUTERI.js → sizeCapture-X5ZJPWSS-B_LramBU.js} +1 -1
  81. package/dist/client/assets/{stateDiagram-OKZ733FA-nGHE1hN7.js → stateDiagram-OKZ733FA-bc7zBYHn.js} +1 -1
  82. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-C-UGmA2d.js +1 -0
  83. package/dist/client/assets/{swimlanes-SLNWSIFB-BZFv9Zp8.js → swimlanes-SLNWSIFB-DQzg1IPn.js} +2 -2
  84. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-DnopIiWa.js +8 -0
  85. package/dist/client/assets/{tanstack-CcfAJoQ7.js → tanstack-BysN2Oew.js} +1 -1
  86. package/dist/client/assets/{timeline-definition-Z64GVDOM-pUIcaCIE.js → timeline-definition-Z64GVDOM-CtbuIB6K.js} +1 -1
  87. package/dist/client/assets/{vennDiagram-T6HMQDX7-D6dEIJmP.js → vennDiagram-T6HMQDX7-pYJ0ZfFS.js} +1 -1
  88. package/dist/client/assets/{viewer-tabs-BjG3oKQ9.js → viewer-tabs-Cnfyd6rg.js} +1 -1
  89. package/dist/client/assets/{wardleyDiagram-T6FBY63Y-DZ8ro_-y.js → wardleyDiagram-T6FBY63Y-DYl_Oi-Y.js} +1 -1
  90. package/dist/client/assets/{xychartDiagram-ELKLHX3M-WJ2Zfmlk.js → xychartDiagram-ELKLHX3M-Cj5YUfP5.js} +1 -1
  91. package/dist/client/index.html +4 -4
  92. package/dist/dev.mjs +2 -1
  93. package/dist/manifest.json +1 -1
  94. package/dist/server.mjs +779 -211
  95. package/dist/utility.mjs +1 -0
  96. package/package.json +1 -1
  97. package/dist/client/assets/App-Dfm5F199.js +0 -74
  98. package/dist/client/assets/FilesView-EfoU7sF-.js +0 -1
  99. package/dist/client/assets/HistoryView-C6T2dsBC.js +0 -1
  100. package/dist/client/assets/LocalRefsDialog--1NiYKYj.js +0 -1
  101. package/dist/client/assets/PullRequestDiff-D6QG5Anx.js +0 -1
  102. package/dist/client/assets/PullRequestViewer-jC3LVmiO.js +0 -2
  103. package/dist/client/assets/QuickOpenDialog-HvslpGQl.js +0 -1
  104. package/dist/client/assets/RepositoryProjectsDialog-Biqv5GO_.js +0 -1
  105. package/dist/client/assets/SettingsDialog-DWAunOQB.js +0 -72
  106. package/dist/client/assets/base-ui-CJ8ZIx3d.js +0 -1
  107. package/dist/client/assets/channel-Diwpee6E.js +0 -1
  108. package/dist/client/assets/classDiagram-JCYQIIEL-BDuo9kiS.js +0 -1
  109. package/dist/client/assets/classDiagram-v2-OCEON4UE-BDuo9kiS.js +0 -1
  110. package/dist/client/assets/index-Dc9xVluy.css +0 -1
  111. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-CkCpE1Yu.js +0 -1
  112. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-Dxlb7bju.js +0 -8
package/dist/server.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { fileURLToPath, pathToFileURL } from "node:url";
2
2
  import path, { extname, delimiter, resolve, join, isAbsolute, dirname } from "node:path";
3
- import { randomUUID, randomBytes, createHmac, createHash, timingSafeEqual } from "node:crypto";
3
+ import { randomUUID, randomBytes, createHash, createHmac, timingSafeEqual } from "node:crypto";
4
4
  import { stat, access, realpath, mkdir, open, rename, chmod, rm, appendFile, readFile, mkdtemp, writeFile, lstat, readdir, cp } from "node:fs/promises";
5
5
  import os, { constants, homedir, networkInterfaces } from "node:os";
6
6
  import { ChildProcess, execFile, spawnSync, spawn } from "node:child_process";
@@ -17,6 +17,7 @@ import { Transform, getDefaultHighWaterMark, Duplex, Writable, Readable, PassThr
17
17
  import { finished } from "node:stream/promises";
18
18
  import path$1 from "node:path/win32";
19
19
  import { createServer, isIP } from "node:net";
20
+ import { AsyncLocalStorage } from "node:async_hooks";
20
21
  import { createServer as createServer$1 } from "node:http";
21
22
  import WebSocket, { WebSocketServer } from "ws";
22
23
  function aiExecutablePathsKey(paths) {
@@ -12194,7 +12195,7 @@ function superRefine(fn, params) {
12194
12195
  return /* @__PURE__ */ _superRefine(fn, params);
12195
12196
  }
12196
12197
  function redactSensitiveText(value) {
12197
- const withoutHeaders = value.replace(/\b(authorization|cookie|set-cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\b(token|secret|password)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]");
12198
+ const withoutHeaders = value.replace(/\b(?:github_pat_|gh[opusr]_)[A-Za-z0-9_]{16,}/g, "[redacted]").replace(/\bauthorization\s*[:=]\s*(?:bearer|basic)\s+[^\s,;]+/gi, "authorization=[redacted]").replace(/\b(authorization|cookie|set-cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\b(token|secret|password)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]");
12198
12199
  return withoutHeaders.replace(/https?:\/\/[^\s"'<>]+/gi, (candidate) => redactUrl(candidate));
12199
12200
  }
12200
12201
  function redactUrl(candidate) {
@@ -14835,6 +14836,450 @@ class RepositoryWatcher {
14835
14836
  this.onChange(this.active.id, scope);
14836
14837
  }
14837
14838
  }
14839
+ const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
14840
+ const REPO_PATTERN = /^[A-Za-z0-9._-]+$/;
14841
+ function parseGitHubRemote(url) {
14842
+ const trimmed = url.trim();
14843
+ if (!trimmed) return null;
14844
+ let rest = null;
14845
+ const https = /^https?:\/\/(?:[^@/\s]+@)?github\.com\/(.+)$/i.exec(trimmed);
14846
+ const ssh = /^ssh:\/\/(?:[^@/\s]+@)?github\.com(?::\d+)?\/(.+)$/i.exec(trimmed);
14847
+ const scp = /^(?:[^@\s]+)@github\.com:(.+)$/i.exec(trimmed);
14848
+ const git = /^git:\/\/github\.com\/(.+)$/i.exec(trimmed);
14849
+ if (https) rest = https[1] ?? null;
14850
+ else if (ssh) rest = ssh[1] ?? null;
14851
+ else if (scp) rest = scp[1] ?? null;
14852
+ else if (git) rest = git[1] ?? null;
14853
+ if (!rest) return null;
14854
+ const segments = rest.replace(/\/+$/, "").split("/");
14855
+ if (segments.length !== 2) return null;
14856
+ const owner = segments[0] ?? "";
14857
+ const repo = (segments[1] ?? "").replace(/\.git$/i, "");
14858
+ if (!OWNER_PATTERN.test(owner) || !repo || repo === "." || repo === ".." || !REPO_PATTERN.test(repo)) return null;
14859
+ return { nameWithOwner: `${owner}/${repo}` };
14860
+ }
14861
+ function parseSshRemote(url) {
14862
+ const trimmed = url.trim();
14863
+ const ssh = /^ssh:\/\/(?:(\w[\w.-]*)@)?([A-Za-z0-9][A-Za-z0-9._-]*)(?::(\d+))?\/(.+)$/i.exec(trimmed);
14864
+ const scp = /^(?:(\w[\w.-]*)@)?([A-Za-z0-9][A-Za-z0-9._-]*):(.+)$/.exec(trimmed);
14865
+ const match = ssh ?? scp;
14866
+ if (!match) return null;
14867
+ const parsed = parseGitHubRemote(`https://github.com/${ssh ? match[4] : match[3]}`);
14868
+ if (!parsed) return null;
14869
+ return { destination: `${match[1] ? `${match[1]}@` : ""}${match[2]}`, port: ssh ? match[3] ?? null : null, ...parsed };
14870
+ }
14871
+ const GITHUB_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
14872
+ function parseGitHubAuthStatus(raw) {
14873
+ const input = JSON.parse(raw);
14874
+ if (!input || typeof input !== "object" || !("hosts" in input) || !input.hosts || typeof input.hosts !== "object") throw new Error("Invalid gh status");
14875
+ const entries = input.hosts["github.com"] ?? [];
14876
+ if (!Array.isArray(entries) || entries.length > 100) throw new Error("Invalid gh accounts");
14877
+ const accounts = [];
14878
+ for (const entry of entries) {
14879
+ if (!entry || typeof entry !== "object" || typeof entry.login !== "string" || !GITHUB_LOGIN.test(entry.login)) continue;
14880
+ accounts.push({
14881
+ host: "github.com",
14882
+ login: entry.login,
14883
+ active: entry.active === true,
14884
+ state: entry.state === "success" ? "authenticated" : entry.state === "error" ? "invalid" : "unknown",
14885
+ storage: entry.tokenSource === "keyring" ? "keyring" : typeof entry.tokenSource === "string" && (entry.tokenSource === "oauth_token" || /(?:^|[/\\])hosts\.yml$/.test(entry.tokenSource)) ? "file" : "unknown",
14886
+ ...typeof entry.scopes === "string" && /^[A-Za-z0-9_:, -]{0,500}$/.test(entry.scopes) ? { scopes: entry.scopes } : {}
14887
+ });
14888
+ }
14889
+ return { accounts, activeLogin: accounts.find((account) => account.active)?.login ?? null };
14890
+ }
14891
+ const GH_ENV = { GH_HOST: "github.com", GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", GH_PAGER: "cat", NO_COLOR: "1" };
14892
+ const GH_PRIVATE_ENV = ["GITHUB_TOKEN", "GH_DEBUG", "DEBUG"];
14893
+ async function identifySshAccount(remote, runner) {
14894
+ const ssh = parseSshRemote(remote);
14895
+ if (!ssh) return null;
14896
+ try {
14897
+ const config2 = await runner.run("ssh", ["-G", ...ssh.port ? ["-p", ssh.port] : [], "--", ssh.destination], {
14898
+ cwd: os.homedir(),
14899
+ timeoutMs: 5e3,
14900
+ maxOutputBytes: 64 * 1024
14901
+ });
14902
+ const hostname = config2.stdout.split(/\r?\n/).find((line) => /^hostname\s/i.test(line))?.trim().split(/\s+/)[1];
14903
+ if (config2.exitCode !== 0 || hostname?.toLowerCase() !== "github.com") return null;
14904
+ const result = await runner.run("ssh", [
14905
+ "-T",
14906
+ "-o",
14907
+ "BatchMode=yes",
14908
+ "-o",
14909
+ "StrictHostKeyChecking=yes",
14910
+ "-o",
14911
+ "ConnectTimeout=5",
14912
+ "-o",
14913
+ "ConnectionAttempts=1",
14914
+ "-o",
14915
+ "RemoteCommand=none",
14916
+ "-o",
14917
+ "UpdateHostKeys=no",
14918
+ ...ssh.port ? ["-p", ssh.port] : [],
14919
+ "--",
14920
+ ssh.destination
14921
+ ], { cwd: os.homedir(), timeoutMs: 1e4, maxOutputBytes: 64 * 1024 });
14922
+ if (result.exitCode !== 0 && result.exitCode !== 1) return null;
14923
+ const login2 = /^Hi ([A-Za-z0-9-]+)! You've successfully authenticated, but GitHub does not provide shell access\.\s*$/m.exec(`${result.stdout}
14924
+ ${result.stderr}`)?.[1];
14925
+ return login2 && GITHUB_LOGIN.test(login2) ? login2 : null;
14926
+ } catch {
14927
+ return null;
14928
+ }
14929
+ }
14930
+ async function accountAuthentication(login2, executable, runner, operation) {
14931
+ try {
14932
+ const result = await runner.run(executable.executable, ["auth", "token", "--hostname", "github.com", ...login2 ? ["--user", login2] : []], {
14933
+ cwd: os.homedir(),
14934
+ env: { ...executable.env, ...GH_ENV },
14935
+ removeEnv: login2 ? ["GH_TOKEN", ...GH_PRIVATE_ENV] : ["GH_DEBUG", "DEBUG"],
14936
+ timeoutMs: 1e4,
14937
+ maxOutputBytes: 8 * 1024
14938
+ });
14939
+ const token = result.stdout.trim();
14940
+ if (result.exitCode === 0 && /^[A-Za-z0-9_]{20,4096}$/.test(token)) return { env: { GH_TOKEN: token }, removeEnv: GH_PRIVATE_ENV };
14941
+ if (/unknown flag.*user/i.test(result.stderr)) throw new GhOperationError({
14942
+ code: "GH_CLI_INCOMPATIBLE",
14943
+ operation,
14944
+ message: "Update GitHub CLI to a version supporting multiple accounts and auth token --user."
14945
+ });
14946
+ } catch (error) {
14947
+ if (error instanceof GhOperationError) throw error;
14948
+ }
14949
+ throw new GhOperationError({
14950
+ code: login2 ? "GH_ACCOUNT_MISSING" : "GH_AUTH_REQUIRED",
14951
+ operation,
14952
+ message: login2 ? `GitHub CLI has no usable saved credentials for @${login2}. Sign in as ${login2} on the backend host and check Settings → GitHub again. SSH and GitHub CLI authentication are separate.` : "GitHub CLI has no usable credentials. Sign in on the backend host and check Settings → GitHub again."
14953
+ });
14954
+ }
14955
+ function redactCommandToken(value, auth) {
14956
+ const token = auth.env?.GH_TOKEN;
14957
+ return token ? value.split(token).join("[redacted]") : value;
14958
+ }
14959
+ function gitShellQuote(value) {
14960
+ return `'${value.replace(/\\/g, "/").replace(/'/g, `'\\''`)}'`;
14961
+ }
14962
+ function managedGitHubPath(value) {
14963
+ try {
14964
+ const url = new URL(value);
14965
+ if (!/^https:\/\/github\.com\//i.test(value) || url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password || url.search || url.hash) return null;
14966
+ if (!/^\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(url.pathname)) return null;
14967
+ return url.pathname.slice(1);
14968
+ } catch {
14969
+ return null;
14970
+ }
14971
+ }
14972
+ async function createCredentialBroker(url, login2, token) {
14973
+ const allowedPath = managedGitHubPath(url);
14974
+ if (!allowedPath) throw new Error("Managed authentication requires one plain github.com HTTPS repository URL.");
14975
+ const directory = await mkdtemp(path.join(os.tmpdir(), "opentig-git-auth-"));
14976
+ const key = randomBytes(32).toString("hex");
14977
+ const server = createServer$1((request, response) => {
14978
+ response.setHeader("Connection", "close");
14979
+ if (request.method !== "POST" || request.url !== "/" || request.headers["x-opentig-key"] !== key) {
14980
+ response.writeHead(403).end();
14981
+ return;
14982
+ }
14983
+ let body = "";
14984
+ request.setTimeout(5e3, () => request.destroy());
14985
+ request.on("data", (data) => {
14986
+ body += data.toString("utf8");
14987
+ if (body.length > 8192) request.destroy();
14988
+ });
14989
+ request.on("end", () => {
14990
+ const fields = body.trim().split("\n").map((line) => line.replace(/\r$/, "").split(/=(.*)/s));
14991
+ const value = Object.fromEntries(fields.map(([name, field]) => [name, field]));
14992
+ const unique = ["protocol", "host", "path"].every((name) => fields.filter(([key2]) => key2 === name).length === 1);
14993
+ if (!unique || value.protocol !== "https" || value.host !== "github.com" || value.path !== allowedPath) {
14994
+ response.writeHead(403).end();
14995
+ return;
14996
+ }
14997
+ response.end(`username=${login2}
14998
+ password=${token}
14999
+
15000
+ `);
15001
+ });
15002
+ });
15003
+ const dispose = async () => {
15004
+ server.closeAllConnections();
15005
+ if (server.listening) await new Promise((resolve2) => server.close(() => resolve2()));
15006
+ await rm(directory, { recursive: true, force: true });
15007
+ };
15008
+ try {
15009
+ server.listen(0, "127.0.0.1");
15010
+ await once(server, "listening");
15011
+ const port = server.address().port;
15012
+ const headers = path.join(directory, "headers");
15013
+ const helper = path.join(directory, "helper.sh");
15014
+ await writeFile(headers, `X-OpenTig-Key: ${key}
15015
+ `, { mode: 384 });
15016
+ await writeFile(helper, `#!/bin/sh
15017
+ [ "$1" = get ] || exit 0
15018
+ curl --disable --silent --fail --noproxy '*' --max-time 10 --proto '=http' --header @${gitShellQuote(headers)} --data-binary @- http://127.0.0.1:${port}/ || exit 1
15019
+ `, { mode: 448 });
15020
+ return { helper: `!sh ${gitShellQuote(helper)}`, directory, dispose };
15021
+ } catch (error) {
15022
+ await dispose();
15023
+ throw error;
15024
+ }
15025
+ }
15026
+ const read$1 = { operation: "repository-access", readOnly: true, maxOutputBytes: 1024 * 1024 };
15027
+ const unchecked = () => ({ state: "unchecked" });
15028
+ const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
15029
+ class RepositoryGitAccess {
15030
+ constructor(git, repositories, accounts, runner) {
15031
+ this.git = git;
15032
+ this.repositories = repositories;
15033
+ this.accounts = accounts;
15034
+ this.runner = runner;
15035
+ }
15036
+ git;
15037
+ repositories;
15038
+ accounts;
15039
+ runner;
15040
+ sessions = new AsyncLocalStorage();
15041
+ checks = /* @__PURE__ */ new Map();
15042
+ latest = /* @__PURE__ */ new Map();
15043
+ install() {
15044
+ this.git.network = (cwd, args, options, execute) => this.scope(cwd, () => this.execute(args, options, execute));
15045
+ this.git.authenticationKey = (cwd) => {
15046
+ const repository = this.repositories.recents().find((item) => item.path === cwd);
15047
+ return repository ? digest([this.accounts.selection(repository), this.accounts.revisionFor(repository)]) : "";
15048
+ };
15049
+ this.git.networkScope = (cwd, task) => this.scope(cwd, task);
15050
+ }
15051
+ async scope(cwd, task) {
15052
+ if (this.sessions.getStore()?.repository.path === cwd) return task();
15053
+ const repository = this.repositories.recents().find((item) => item.path === cwd);
15054
+ if (!repository) throw new Error("The repository must be open before remote operations.");
15055
+ return this.sessions.run({ repository, selection: this.accounts.selection(repository), revision: this.accounts.revisionFor(repository) }, task);
15056
+ }
15057
+ async config(repository, key, all = false) {
15058
+ try {
15059
+ return (await this.git.run(repository.path, ["config", all ? "--get-all" : "--get", key], read$1)).stdout.toString("utf8").trim();
15060
+ } catch (error) {
15061
+ if (error instanceof GitOperationError && error.detail.exitCode === 1) return "";
15062
+ throw error;
15063
+ }
15064
+ }
15065
+ async resolve(repository, push, selected) {
15066
+ const branch = (await this.git.run(repository.path, ["branch", "--show-current"], read$1)).stdout.toString("utf8").trim() || null;
15067
+ const remotes = (await this.git.run(repository.path, ["remote"], read$1)).stdout.toString("utf8").trim().split(/\r?\n/).filter(Boolean);
15068
+ let remote = selected;
15069
+ if (!remote) for (const key of [...push ? [`branch.${branch}.pushRemote`, "remote.pushDefault"] : [], `branch.${branch}.remote`]) {
15070
+ remote = await this.config(repository, key);
15071
+ if (remote) break;
15072
+ }
15073
+ if (!remote && !push && remotes.includes("origin")) remote = "origin";
15074
+ remote ||= remotes.length === 1 ? remotes[0] : void 0;
15075
+ let urls = [];
15076
+ if (remote === ".") urls = ["."];
15077
+ else if (remote && remotes.includes(remote)) urls = (await this.git.run(repository.path, ["remote", "get-url", ...push ? ["--push", "--all"] : [], "--", remote], read$1)).stdout.toString("utf8").trim().split(/\r?\n/).filter(Boolean);
15078
+ else if (selected && !remotes.includes(selected)) {
15079
+ urls = [(await this.git.run(repository.path, ["ls-remote", "--get-url", "--", selected], { ...read$1, operation: "resolve-git-url" })).stdout.toString("utf8").trim()];
15080
+ }
15081
+ return { branch, remotes, remote: remote ?? null, urls };
15082
+ }
15083
+ async publication(repositoryId, selected) {
15084
+ const repository = this.repositories.get(repositoryId);
15085
+ const selection = this.accounts.selection(repository);
15086
+ const revision2 = this.accounts.revisionFor(repository);
15087
+ const target = await this.resolve(repository, true, selected);
15088
+ let oid = null;
15089
+ try {
15090
+ oid = (await this.git.run(repository.path, ["rev-parse", "--verify", "HEAD"], read$1)).stdout.toString("utf8").trim();
15091
+ } catch {
15092
+ }
15093
+ const managed = selection.mode === "account" && selection.gitMode === "managed";
15094
+ const compatible = target.urls.length === 1 && !!managedGitHubPath(target.urls[0]);
15095
+ const configuration = (await this.git.run(repository.path, ["config", "--null", "--list"], read$1)).stdout.toString("utf8");
15096
+ const upstreamRemote = await this.config(repository, `branch.${target.branch}.remote`);
15097
+ const merge2 = await this.config(repository, `branch.${target.branch}.merge`, true);
15098
+ const pushDefault = await this.config(repository, "push.default") || "simple";
15099
+ const targetRef = target.branch ? target.remote === upstreamRemote && merge2 && ["simple", "upstream"].includes(pushDefault) ? merge2 : `refs/heads/${target.branch}` : null;
15100
+ const unsafe = merge2 && (merge2.includes("\n") || !["simple", "upstream", "current"].includes(pushDefault) || pushDefault === "simple" && target.remote === upstreamRemote && merge2 !== `refs/heads/${target.branch}` || pushDefault === "upstream" && target.remote !== upstreamRemote || await this.config(repository, `remote.${target.remote}.push`) || await this.config(repository, `remote.${target.remote}.mirror`) === "true");
15101
+ const blocked = selected && selected !== "." && !target.remotes.includes(selected) ? "The selected push remote is not configured. Review the repository remotes." : unsafe ? "The push refspec, mirror or push.default configuration requires review in Git before publishing." : target.urls.length > 1 ? "This remote has multiple push URLs. Choose a single destination in Git configuration before publishing." : target.remote && !target.urls.length ? "The configured push remote is unavailable." : managed && target.urls.some(isGitHubHttp) && !compatible ? "Managed Git requires a plain github.com HTTPS destination. Review the URL or use external authentication." : void 0;
15102
+ return {
15103
+ ...target,
15104
+ urls: target.urls.map(redactSensitiveText),
15105
+ repositoryId,
15106
+ oid,
15107
+ targetRef,
15108
+ id: digest([repository.commonDir, selection, revision2, target, oid, configuration]),
15109
+ mode: managed && compatible ? "managed" : "external",
15110
+ login: managed && compatible && selection.mode === "account" ? selection.login : null,
15111
+ ...blocked ? { blocked } : {}
15112
+ };
15113
+ }
15114
+ async assertPublication(repositoryId, expected, remote) {
15115
+ const current = await this.publication(repositoryId, remote);
15116
+ if (expected && expected !== current.id) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "push", message: "The account, branch or destination changed. Review the publication context and try again." });
15117
+ if (current.blocked) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "push", message: current.blocked });
15118
+ return current;
15119
+ }
15120
+ async status(repositoryId, force = false, apiContext) {
15121
+ const publication = await this.publication(repositoryId);
15122
+ const repository = this.repositories.get(repositoryId);
15123
+ const previous = this.checks.get(repositoryId);
15124
+ const value = previous?.publication.id === publication.id ? structuredClone(previous) : {
15125
+ publication,
15126
+ identity: unchecked(),
15127
+ api: unchecked(),
15128
+ read: publication.mode === "external" ? { state: "external" } : unchecked(),
15129
+ write: unchecked(),
15130
+ checkedAt: null
15131
+ };
15132
+ const last = this.latest.get(repositoryId);
15133
+ if (last) value.lastOperation = last;
15134
+ if (!force) {
15135
+ if (value.checkedAt && Date.now() - Date.parse(value.checkedAt) > 6e4) for (const key of ["identity", "api", "read", "write"]) {
15136
+ if (value[key].state === "ok") value[key] = { state: "stale", message: value[key].message ?? "Check access again for a current result." };
15137
+ }
15138
+ return value;
15139
+ }
15140
+ value.identity = unchecked();
15141
+ value.api = unchecked();
15142
+ value.write = unchecked();
15143
+ value.read = publication.mode === "external" ? { state: "external" } : unchecked();
15144
+ const selection = this.accounts.selection(repository);
15145
+ if (selection.mode !== "account") {
15146
+ value.identity = { state: "unchecked", message: "Choose a saved account to pin access for this repository." };
15147
+ } else {
15148
+ try {
15149
+ this.accounts.refreshAuthentication();
15150
+ const { auth, executable } = await this.accounts.authenticate(repository, apiContext?.remote ?? "", apiContext?.nameWithOwner ?? "", "repository-access");
15151
+ value.identity = { state: "ok" };
15152
+ const origin = await this.resolve(repository, false, "origin");
15153
+ const apiUrl = origin.urls[0] ?? "";
15154
+ const apiPath = apiContext?.nameWithOwner ?? managedGitHubPath(apiUrl) ?? /^git@github\.com:([\w.-]+\/[\w.-]+)$/.exec(apiUrl)?.[1];
15155
+ const checkApi = async (name) => {
15156
+ try {
15157
+ const result = await this.runner.run(executable.executable, ["api", `repos/${name.replace(/\.git$/, "")}`], {
15158
+ cwd: repository.path,
15159
+ env: { ...executable.env, ...GH_ENV, ...auth.env },
15160
+ ...auth.removeEnv ? { removeEnv: auth.removeEnv } : {},
15161
+ timeoutMs: 15e3,
15162
+ maxOutputBytes: 128 * 1024
15163
+ });
15164
+ if (result.exitCode !== 0) {
15165
+ const check = accessError(redactCommandToken(result.stderr, auth));
15166
+ return { check, write: check };
15167
+ }
15168
+ const data = JSON.parse(result.stdout);
15169
+ return { check: { state: "ok" }, write: typeof data.permissions?.push === "boolean" ? { state: data.permissions.push ? "ok" : "inaccessible", message: data.permissions.push ? "Write permission declared by GitHub; branch policies still apply." : "GitHub does not declare write permission." } : unchecked() };
15170
+ } catch (error) {
15171
+ const check = accessError(redactCommandToken(messageOf(error), auth));
15172
+ return { check, write: check };
15173
+ }
15174
+ };
15175
+ if (apiPath) value.api = (await checkApi(apiPath)).check;
15176
+ const destination = publication.urls[0];
15177
+ const pushPath = destination && managedGitHubPath(destination);
15178
+ if (pushPath) value.write = (await checkApi(pushPath)).write;
15179
+ if (publication.mode === "managed" && destination && !publication.blocked) {
15180
+ try {
15181
+ await this.git.withNetworkContext(repository.path, () => this.git.run(repository.path, ["ls-remote", "--", destination, "HEAD"], { ...read$1, timeoutMs: 3e4 }));
15182
+ value.read = { state: "ok", message: "Read verified at the push destination. This does not guarantee a push." };
15183
+ } catch (error) {
15184
+ value.read = accessError(messageOf(error));
15185
+ }
15186
+ }
15187
+ } catch (error) {
15188
+ if (value.identity.state === "ok") value.api = accessError(messageOf(error));
15189
+ else value.identity = accessError(messageOf(error));
15190
+ }
15191
+ }
15192
+ if ((await this.publication(repositoryId)).id !== publication.id) return this.status(repositoryId, false, apiContext);
15193
+ value.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
15194
+ this.checks.set(repositoryId, value);
15195
+ return value;
15196
+ }
15197
+ async execute(args, options, execute) {
15198
+ const session = this.sessions.getStore();
15199
+ const { repository, selection } = session;
15200
+ let index = 0;
15201
+ while (args[index] === "-c") index += 2;
15202
+ const command2 = args[index];
15203
+ if (command2 === "ls-remote" && args.includes("--get-url")) return execute(args, options);
15204
+ const selected = args.slice(index + 1).find((arg) => !arg.startsWith("-"));
15205
+ const target = await this.resolve(repository, command2 === "push", selected);
15206
+ if (target.urls.length !== 1) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: options.operation, message: "The remote destination is ambiguous. Configure or choose a single remote URL." });
15207
+ const url = target.urls[0];
15208
+ if (selected?.includes("://") && selected !== url) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: options.operation, message: "URL rewriting changes the reviewed destination again. Resolve the chained rewrite before checking or publishing." });
15209
+ const managed = selection.mode === "account" && selection.gitMode === "managed" && isGitHubHttp(url);
15210
+ let broker;
15211
+ let token = "";
15212
+ try {
15213
+ if (managed) {
15214
+ if (!managedGitHubPath(url)) throw new Error("Managed Git cannot send credentials to this destination. Use external authentication for unsupported URLs.");
15215
+ if (!session.auth) {
15216
+ if (session.revision !== this.accounts.revisionFor(repository)) throw new Error("The repository account changed before the operation started. Review and retry.");
15217
+ session.auth = await this.accounts.authenticate(repository, url, managedGitHubPath(url), options.operation);
15218
+ if (session.revision !== this.accounts.revisionFor(repository) || digest(session.auth.context.selection) !== digest(selection)) throw new Error("The repository account changed before authentication completed. Review and retry.");
15219
+ }
15220
+ if (command2 === "fetch" && !selected && target.remote) args = [...args, "--", target.remote];
15221
+ token = session.auth.auth.env?.GH_TOKEN ?? "";
15222
+ broker = await createCredentialBroker(url, selection.login, token);
15223
+ const config2 = (await this.git.run(repository.path, ["config", "--name-only", "--list"], read$1)).stdout.toString("utf8").trim().split(/\r?\n/);
15224
+ const overrides = [...new Set(config2)].flatMap((key) => {
15225
+ if (/^credential\..*helper$/i.test(key)) return ["-c", `${key}=`];
15226
+ if (/^http\..*extraheader$/i.test(key)) return ["-c", `${key}=`];
15227
+ if (/^http\..*followredirects$/i.test(key)) return ["-c", `${key}=false`];
15228
+ if (/^http\..*sslverify$/i.test(key)) return ["-c", `${key}=true`];
15229
+ if (/^credential\..*usehttppath$/i.test(key)) return ["-c", `${key}=true`];
15230
+ return [];
15231
+ });
15232
+ args = [
15233
+ ...overrides,
15234
+ "-c",
15235
+ "credential.helper=",
15236
+ "-c",
15237
+ `credential.helper=${broker.helper}`,
15238
+ "-c",
15239
+ "credential.useHttpPath=true",
15240
+ "-c",
15241
+ "core.askPass=",
15242
+ "-c",
15243
+ "http.extraHeader=",
15244
+ "-c",
15245
+ "http.followRedirects=false",
15246
+ "-c",
15247
+ "http.sslVerify=true",
15248
+ "-c",
15249
+ "fetch.recurseSubmodules=false",
15250
+ "-c",
15251
+ "push.recurseSubmodules=no",
15252
+ ...args
15253
+ ];
15254
+ }
15255
+ if (options.publication) await this.assertPublication(options.publication.repositoryId, options.publication.id, options.publication.remote);
15256
+ const result = await execute(args, { ...options, ...managed ? { managed: true, managedSecrets: [token] } : {} });
15257
+ this.latest.set(repository.id, { operation: options.operation, url: redactSensitiveText(url), login: managed && selection.mode === "account" ? selection.login : null, at: (/* @__PURE__ */ new Date()).toISOString(), ok: true });
15258
+ return result;
15259
+ } catch (error) {
15260
+ const message = redactSensitiveText(token ? messageOf(error).split(token).join("[redacted]") : messageOf(error));
15261
+ this.latest.set(repository.id, { operation: options.operation, url: redactSensitiveText(url), login: managed && selection.mode === "account" ? selection.login : null, at: (/* @__PURE__ */ new Date()).toISOString(), ok: false, message });
15262
+ if (error instanceof GitOperationError) throw error;
15263
+ throw new GitOperationError({ code: "UNKNOWN", operation: options.operation, message });
15264
+ } finally {
15265
+ await broker?.dispose();
15266
+ }
15267
+ }
15268
+ }
15269
+ function messageOf(error) {
15270
+ return error instanceof Error ? error.message : "Could not check remote access.";
15271
+ }
15272
+ function accessError(message) {
15273
+ return { state: /401|bad credentials|expired|revoked|no usable saved credentials/i.test(message) ? "expired" : /404|403|not found|denied|not accessible/i.test(message) ? "inaccessible" : "offline", message: redactSensitiveText(message).slice(0, 1500) };
15274
+ }
15275
+ function isGitHubHttp(value) {
15276
+ try {
15277
+ const url = new URL(value);
15278
+ return ["http:", "https:"].includes(url.protocol) && url.hostname === "github.com";
15279
+ } catch {
15280
+ return false;
15281
+ }
15282
+ }
14838
15283
  function normalizeLockKey(value) {
14839
15284
  const unified = value.replace(/\\/g, "/").replace(/\/+$/, "");
14840
15285
  return process.platform === "win32" ? unified.toLowerCase() : unified;
@@ -14845,7 +15290,21 @@ class GitProcess {
14845
15290
  activeControllers = /* @__PURE__ */ new Set();
14846
15291
  idleWaiters = /* @__PURE__ */ new Set();
14847
15292
  closePromise = null;
15293
+ authenticationKey;
15294
+ network;
15295
+ networkScope;
15296
+ withNetworkContext(cwd, task) {
15297
+ return this.networkScope ? this.networkScope(cwd, task) : task();
15298
+ }
14848
15299
  run(cwd, args, options) {
15300
+ let index = 0;
15301
+ while (args[index] === "-c") index += 2;
15302
+ if (this.network && ["fetch", "push", "ls-remote"].includes(args[index] ?? "")) {
15303
+ return this.network(cwd, args, options, (next, settings) => this.runDirect(cwd, next, settings));
15304
+ }
15305
+ return this.runDirect(cwd, args, options);
15306
+ }
15307
+ runDirect(cwd, args, options) {
14849
15308
  const globalArgs = ["--no-pager"];
14850
15309
  if (options.readOnly) globalArgs.push("--no-optional-locks");
14851
15310
  return this.spawnGit(cwd, [...globalArgs, ...args], options);
@@ -14901,6 +15360,10 @@ class GitProcess {
14901
15360
  LC_ALL: "C"
14902
15361
  }).filter((entry) => entry[1] !== void 0)
14903
15362
  );
15363
+ if (options.managed) for (const key of Object.keys(environment)) {
15364
+ if (/^(GH_TOKEN|GITHUB_TOKEN|GH_DEBUG|SSLKEYLOGFILE|GIT_SSL_NO_VERIFY|GIT_ASKPASS|SSH_ASKPASS|GIT_CONFIG.*|GIT_TRACE.*|GIT_CURL_VERBOSE)$/i.test(key)) delete environment[key];
15365
+ }
15366
+ const sanitize = (value) => redactSensitiveText((options.managedSecrets ?? []).reduce((text2, secret) => text2.split(secret).join("[redacted]"), value));
14904
15367
  const resolvedCommand = resolveProcessCommand("git", cwd, environment);
14905
15368
  const outputLimit = new AbortController();
14906
15369
  const lifecycle = new AbortController();
@@ -14940,8 +15403,8 @@ class GitProcess {
14940
15403
  childPid = subprocess.pid;
14941
15404
  if (childPid) this.activeChildren.add(childPid);
14942
15405
  const result = await subprocess;
14943
- const stdout = Buffer.from(result.stdout);
14944
- const stderr = Buffer.from(result.stderr);
15406
+ const stdout = options.managed ? Buffer.from(sanitize(Buffer.from(result.stdout).toString("utf8"))) : Buffer.from(result.stdout);
15407
+ const stderr = Buffer.from(sanitize(Buffer.from(result.stderr).toString("utf8")));
14945
15408
  if (overLimit || result.isMaxBuffer) {
14946
15409
  if (options.truncateOverflow) return { stdout, stderr, exitCode: 0, truncated: true };
14947
15410
  throw new GitOperationError({ code: "OUTPUT_LIMIT", operation: options.operation, message: "Git output exceeded the safety limit." });
@@ -14952,11 +15415,11 @@ class GitProcess {
14952
15415
  if (result.exitCode === void 0 || !resolvedCommand.found) {
14953
15416
  throw this.mapError(options.operation, Object.assign(new Error("Git was not found in PATH."), { code: "ENOENT" }));
14954
15417
  }
14955
- if (result.exitCode !== 0) throw this.fromExit(options.operation, result.exitCode, stderr.toString("utf8"));
15418
+ if (result.exitCode !== 0) throw this.fromExit(options.operation, result.exitCode, sanitize(`${stderr.toString("utf8")}${options.operation.includes("push") || options.operation === "publish-branch" ? "\n" + stdout.toString("utf8") : ""}`));
14956
15419
  return { stdout, stderr, exitCode: result.exitCode };
14957
15420
  } catch (error) {
14958
15421
  if (error instanceof GitOperationError) throw error;
14959
- throw this.mapError(options.operation, error);
15422
+ throw this.mapError(options.operation, Object.assign(new Error(sanitize(error instanceof Error ? error.message : "Could not start Git.")), { code: error?.code }));
14960
15423
  } finally {
14961
15424
  if (childPid) this.activeChildren.delete(childPid);
14962
15425
  this.activeControllers.delete(lifecycle);
@@ -15482,10 +15945,10 @@ class GitRemoteBranches {
15482
15945
  });
15483
15946
  return { branch, ...mappings[0], branchName: mappings[0].sourceRef.slice("refs/heads/".length) };
15484
15947
  }
15485
- async destination(target, run) {
15948
+ async destination(target, run, root) {
15486
15949
  const fetchUrls = (await run(["remote", "get-url", "--all", "--", target.remote], read)).stdout.toString("utf8").trim().split(/\r?\n/);
15487
15950
  const pushUrls = (await run(["remote", "get-url", "--push", "--all", "--", target.remote], read)).stdout.toString("utf8").trim().split(/\r?\n/);
15488
- const id = createHash("sha256").update(JSON.stringify([target.remote, target.sourceRef, fetchUrls, pushUrls])).digest("hex");
15951
+ const id = createHash("sha256").update(JSON.stringify([target.remote, target.sourceRef, fetchUrls, pushUrls, this.git.authenticationKey?.(root)])).digest("hex");
15489
15952
  const blocked = fetchUrls.length !== 1 || pushUrls.length !== 1 || fetchUrls[0] !== pushUrls[0] ? "Fetch and push destinations differ or have multiple URLs. Manage remote deletion outside OpenTig." : null;
15490
15953
  return { id, blocked, url: fetchUrls[0] };
15491
15954
  }
@@ -15499,7 +15962,7 @@ class GitRemoteBranches {
15499
15962
  const run = this.reader(this.repositories.get(repositoryId).path);
15500
15963
  const target = await this.target(fullName, run);
15501
15964
  if (!target) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "remote-branch-details", message: "The remote reference no longer exists. Fetch branches again." });
15502
- const destination = await this.destination(target, run);
15965
+ const destination = await this.destination(target, run, this.repositories.get(repositoryId).path);
15503
15966
  let blocked = destination.blocked;
15504
15967
  let defaultRef = null;
15505
15968
  let remoteState = "unchecked";
@@ -15547,7 +16010,7 @@ class GitRemoteBranches {
15547
16010
  const target = await this.target(request.fullName, run);
15548
16011
  if (!target) return { status: "missing" };
15549
16012
  if (target.branch.oid !== request.expectedOid) return { status: "stale" };
15550
- const destination = await this.destination(target, run);
16013
+ const destination = await this.destination(target, run, repository.path);
15551
16014
  if (destination.id !== request.destinationId) return { status: "destination-changed" };
15552
16015
  if (destination.blocked) return { status: "rejected", message: destination.blocked };
15553
16016
  try {
@@ -15616,6 +16079,11 @@ class GitRepositoryOperations {
15616
16079
  git;
15617
16080
  repositories;
15618
16081
  files;
16082
+ access;
16083
+ publicationContext(repositoryId, remote) {
16084
+ if (!this.access) throw new Error("Repository access is unavailable.");
16085
+ return this.access.publication(repositoryId, remote);
16086
+ }
15619
16087
  knownOids = /* @__PURE__ */ new Map();
15620
16088
  authorship;
15621
16089
  remoteBranches;
@@ -15626,10 +16094,10 @@ class GitRepositoryOperations {
15626
16094
  return this.remoteBranches.create(request);
15627
16095
  }
15628
16096
  deleteRemoteBranch(request) {
15629
- return this.remoteBranches.delete(request);
16097
+ return this.git.withNetworkContext(this.repositories.get(request.repositoryId).path, () => this.remoteBranches.delete(request));
15630
16098
  }
15631
16099
  fetchBranches(repositoryId) {
15632
- return this.remoteBranches.fetch(repositoryId);
16100
+ return this.git.withNetworkContext(this.repositories.get(repositoryId).path, () => this.remoteBranches.fetch(repositoryId));
15633
16101
  }
15634
16102
  getCommitAuthorship(repositoryId) {
15635
16103
  return this.authorship.get(repositoryId);
@@ -16040,6 +16508,9 @@ ${base}
16040
16508
  }, repository.commonDir);
16041
16509
  }
16042
16510
  async fetch(repositoryId) {
16511
+ return this.git.withNetworkContext(this.repositories.get(repositoryId).path, () => this.fetchInContext(repositoryId));
16512
+ }
16513
+ async fetchInContext(repositoryId) {
16043
16514
  const repository = this.repositories.get(repositoryId);
16044
16515
  try {
16045
16516
  await this.runFetch(repository.path, "fetch");
@@ -16051,6 +16522,9 @@ ${base}
16051
16522
  return { status: "success", ahead: status.ahead, behind: status.behind };
16052
16523
  }
16053
16524
  async pull(repositoryId) {
16525
+ return this.git.withNetworkContext(this.repositories.get(repositoryId).path, () => this.pullInContext(repositoryId));
16526
+ }
16527
+ async pullInContext(repositoryId) {
16054
16528
  const repository = this.repositories.get(repositoryId);
16055
16529
  let status = await this.repositories.status(repositoryId, false);
16056
16530
  const existingConflicts = conflictPaths(status);
@@ -16127,7 +16601,47 @@ ${base}
16127
16601
  const nextStatus = await this.repositories.status(repositoryId, false);
16128
16602
  return { status: "success", commits, restoredLocalChanges: hasLocalChanges, rebased, localCommits: nextStatus.ahead };
16129
16603
  }
16130
- async push(repositoryId, publish) {
16604
+ async push(repositoryId, publish, expectedContext) {
16605
+ if (this.access) return this.pushWithContext(repositoryId, publish, expectedContext);
16606
+ return this.legacyPush(repositoryId, publish);
16607
+ }
16608
+ async pushWithContext(repositoryId, publish, expectedContext) {
16609
+ const repository = this.repositories.get(repositoryId);
16610
+ try {
16611
+ const reviewed = await this.access.assertPublication(repositoryId, expectedContext, publish?.remote);
16612
+ return await this.git.withNetworkContext(repository.path, () => this.git.runWriteTask(repository.path, async (run) => {
16613
+ const context = await this.access.assertPublication(repositoryId, reviewed.id, publish?.remote);
16614
+ const status = await this.repositories.status(repositoryId, false);
16615
+ const conflicts = conflictPaths(status);
16616
+ if (conflicts.length) return { status: "blocked-conflicts", files: conflicts };
16617
+ if (status.operation) return { status: "blocked-operation", operation: status.operation };
16618
+ if (!context.branch || !context.oid || !context.targetRef) return { status: "rejected", reason: "configuration", message: "Check out a branch with a commit before publishing." };
16619
+ if (publish && (publish.expectedBranch !== context.branch || publish.expectedOid !== context.oid)) return { status: "rejected", reason: "configuration", message: "The branch changed. Review and try again." };
16620
+ if (!context.remote) return context.remotes.length ? { status: "remote-required", branch: context.branch, oid: context.oid, remotes: context.remotes } : { status: "rejected", reason: "configuration", message: "No remote is configured." };
16621
+ const result = await run([
16622
+ "-c",
16623
+ `remote.${context.remote}.mirror=false`,
16624
+ "push",
16625
+ "--porcelain",
16626
+ "--no-follow-tags",
16627
+ ...!status.upstream ? ["--set-upstream"] : [],
16628
+ "--",
16629
+ context.remote,
16630
+ `refs/heads/${context.branch}:${context.targetRef}`
16631
+ ], {
16632
+ operation: "push",
16633
+ timeoutMs: 12e4,
16634
+ maxOutputBytes: 4 * 1024 * 1024,
16635
+ publication: { repositoryId, id: context.id, ...publish?.remote ? { remote: publish.remote } : {} }
16636
+ });
16637
+ if (!status.upstream) return { status: "published", branch: context.branch, remote: context.remote };
16638
+ return result.stdout.toString("utf8").includes("[up to date]") ? { status: "up-to-date" } : { status: "success", commits: status.ahead };
16639
+ }, repository.commonDir));
16640
+ } catch (error) {
16641
+ return pushFailure(error);
16642
+ }
16643
+ }
16644
+ async legacyPush(repositoryId, publish) {
16131
16645
  const repository = this.repositories.get(repositoryId);
16132
16646
  let status = await this.repositories.status(repositoryId, false);
16133
16647
  const existingConflicts = conflictPaths(status);
@@ -16625,24 +17139,20 @@ function bounded(value, limit) {
16625
17139
  }
16626
17140
  function pushFailure(error) {
16627
17141
  const detail = error instanceof GitOperationError ? error.detail : null;
16628
- const raw = `${detail?.stderr ?? ""}
16629
- ${detail?.message ?? (error instanceof Error ? error.message : "")}`.toLowerCase();
16630
- if (/authentication failed|permission denied|could not read username|access denied|publickey/.test(raw)) {
16631
- return { status: "rejected", reason: "authentication", message: "The remote rejected the credentials or you do not have permission to push." };
16632
- }
16633
- if (/non-fast-forward|fetch first|stale info|failed to push some refs/.test(raw)) {
16634
- return { status: "rejected", reason: "remote-changed", message: "The remote contains new changes. Update the branch and try again." };
16635
- }
16636
- if (/hook declined|pre-receive hook|protected branch|remote rejected/.test(raw)) {
16637
- return { status: "rejected", reason: "hook", message: "The server rejected the push because of a rule or branch protection." };
16638
- }
16639
- if (/could not resolve host|unable to access|connection timed out|connection reset|network is unreachable/.test(raw)) {
16640
- return { status: "rejected", reason: "network", message: "Could not connect to the remote. Check your network and try again." };
16641
- }
16642
- if (/upstream branch .* does not match|no upstream branch|push\.default/.test(raw)) {
16643
- return { status: "rejected", reason: "configuration", message: "The local branch and its upstream do not allow a safe push with the current configuration." };
16644
- }
16645
- return { status: "rejected", reason: "unknown", message: "Git rejected the push. No force push was performed." };
17142
+ const message = redactSensitiveText(detail?.stderr || detail?.message || (error instanceof Error ? error.message : "Git rejected the push.")).trim().slice(0, 3e3);
17143
+ const raw = message.toLowerCase();
17144
+ let reason = "unknown";
17145
+ let explanation = "";
17146
+ if (/repository not found|repository .* not found|(?:http|error:)\s*404/.test(raw)) {
17147
+ reason = "inaccessible";
17148
+ explanation = "The repository is missing or inaccessible to this account. ";
17149
+ } else if (/authentication failed|could not read username|bad credentials|expired|revoked|no usable saved credentials|publickey|(?:http|error:)\s*401/.test(raw)) reason = "authentication";
17150
+ else if (/protected branch|hook declined|pre-receive hook|remote rejected|gh006|gh013|repository rule/.test(raw)) reason = "hook";
17151
+ else if (/permission.*denied|write access.*not granted|access denied|(?:http|error:)\s*403/.test(raw)) reason = "permission";
17152
+ else if (/non-fast-forward|fetch first|stale info/.test(raw)) reason = "remote-changed";
17153
+ else if (/could not resolve host|unable to access|connection.*(timed out|reset|refused)|network is unreachable|(?:http|error:)\s*50[0-4]/.test(raw)) reason = "network";
17154
+ else if (detail?.code === "INVALID_ARGUMENT" || /upstream branch .* does not match|no upstream branch|push\.default|context|account changed/.test(raw)) reason = "configuration";
17155
+ return { status: "rejected", reason, message: explanation + message };
16646
17156
  }
16647
17157
  function makeDiffResult(path2, buffer) {
16648
17158
  const patch = buffer.toString("utf8");
@@ -16654,7 +17164,6 @@ const MAX_REPOSITORY_PROJECTS = 50;
16654
17164
  const MAX_PROJECT_NAME_LENGTH = 60;
16655
17165
  const MAX_REPOSITORIES_PER_PROJECT = 200;
16656
17166
  const MAX_REPOSITORY_KEY_LENGTH = 2048;
16657
- const UNASSIGNED_RECENT_LIMIT = 10;
16658
17167
  function normalizeRepositoryKey(commonDir) {
16659
17168
  return commonDir.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
16660
17169
  }
@@ -16682,8 +17191,23 @@ class RepositoryService {
16682
17191
  return this.openPath(recent.path);
16683
17192
  }
16684
17193
  async openPath(selectedPath) {
17194
+ const repository = await this.preparePath(selectedPath);
17195
+ await this.settings.touchRepository(repository);
17196
+ return repository;
17197
+ }
17198
+ /** Inspect/cache only: no recent entry, active repository, or persistence. */
17199
+ async preparePath(selectedPath) {
16685
17200
  const repository = await this.inspectPath(selectedPath);
16686
17201
  this.repositories.set(repository.id, repository);
17202
+ return repository;
17203
+ }
17204
+ async completeSetup(id, validate2 = () => void 0) {
17205
+ const prepared = this.get(id);
17206
+ const repository = await this.inspectPath(prepared.path);
17207
+ if (repository.id !== prepared.id || repository.commonDir !== prepared.commonDir) {
17208
+ throw new Error("The repository location changed. Select the folder again.");
17209
+ }
17210
+ validate2();
16687
17211
  await this.settings.touchRepository(repository);
16688
17212
  return repository;
16689
17213
  }
@@ -17180,133 +17704,13 @@ function invalidReplacement(message) {
17180
17704
  }
17181
17705
  const githubAccountSelectionSchema = discriminatedUnion("mode", [
17182
17706
  object({ mode: literal("auto") }),
17183
- object({ mode: literal("account"), host: literal("github.com"), login: string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/) })
17707
+ object({ mode: literal("account"), host: literal("github.com"), login: string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/), gitMode: _enum(["external", "managed"]).optional(), useGlobalDefault: boolean().optional() })
17184
17708
  ]);
17185
17709
  const AUTOMATIC_GITHUB_ACCOUNT = { mode: "auto" };
17186
17710
  function githubRepositoryKey(commonDir, platform2 = process.platform) {
17187
17711
  const key = commonDir.replace(/\\/g, "/").replace(/\/+$/, "");
17188
17712
  return platform2 === "win32" ? key.toLowerCase() : key;
17189
17713
  }
17190
- const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
17191
- const REPO_PATTERN = /^[A-Za-z0-9._-]+$/;
17192
- function parseGitHubRemote(url) {
17193
- const trimmed = url.trim();
17194
- if (!trimmed) return null;
17195
- let rest = null;
17196
- const https = /^https?:\/\/(?:[^@/\s]+@)?github\.com\/(.+)$/i.exec(trimmed);
17197
- const ssh = /^ssh:\/\/(?:[^@/\s]+@)?github\.com(?::\d+)?\/(.+)$/i.exec(trimmed);
17198
- const scp = /^(?:[^@\s]+)@github\.com:(.+)$/i.exec(trimmed);
17199
- const git = /^git:\/\/github\.com\/(.+)$/i.exec(trimmed);
17200
- if (https) rest = https[1] ?? null;
17201
- else if (ssh) rest = ssh[1] ?? null;
17202
- else if (scp) rest = scp[1] ?? null;
17203
- else if (git) rest = git[1] ?? null;
17204
- if (!rest) return null;
17205
- const segments = rest.replace(/\/+$/, "").split("/");
17206
- if (segments.length !== 2) return null;
17207
- const owner = segments[0] ?? "";
17208
- const repo = (segments[1] ?? "").replace(/\.git$/i, "");
17209
- if (!OWNER_PATTERN.test(owner) || !repo || repo === "." || repo === ".." || !REPO_PATTERN.test(repo)) return null;
17210
- return { nameWithOwner: `${owner}/${repo}` };
17211
- }
17212
- function parseSshRemote(url) {
17213
- const trimmed = url.trim();
17214
- const ssh = /^ssh:\/\/(?:(\w[\w.-]*)@)?([A-Za-z0-9][A-Za-z0-9._-]*)(?::(\d+))?\/(.+)$/i.exec(trimmed);
17215
- const scp = /^(?:(\w[\w.-]*)@)?([A-Za-z0-9][A-Za-z0-9._-]*):(.+)$/.exec(trimmed);
17216
- const match = ssh ?? scp;
17217
- if (!match) return null;
17218
- const parsed = parseGitHubRemote(`https://github.com/${ssh ? match[4] : match[3]}`);
17219
- if (!parsed) return null;
17220
- return { destination: `${match[1] ? `${match[1]}@` : ""}${match[2]}`, port: ssh ? match[3] ?? null : null, ...parsed };
17221
- }
17222
- const GITHUB_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
17223
- function parseGitHubAuthStatus(raw) {
17224
- const input = JSON.parse(raw);
17225
- if (!input || typeof input !== "object" || !("hosts" in input) || !input.hosts || typeof input.hosts !== "object") throw new Error("Invalid gh status");
17226
- const entries = input.hosts["github.com"] ?? [];
17227
- if (!Array.isArray(entries) || entries.length > 100) throw new Error("Invalid gh accounts");
17228
- const accounts = [];
17229
- for (const entry of entries) {
17230
- if (!entry || typeof entry !== "object" || typeof entry.login !== "string" || !GITHUB_LOGIN.test(entry.login)) continue;
17231
- accounts.push({
17232
- host: "github.com",
17233
- login: entry.login,
17234
- active: entry.active === true,
17235
- state: entry.state === "success" ? "authenticated" : entry.state === "error" ? "invalid" : "unknown",
17236
- storage: entry.tokenSource === "keyring" ? "keyring" : typeof entry.tokenSource === "string" && (entry.tokenSource === "oauth_token" || /(?:^|[/\\])hosts\.yml$/.test(entry.tokenSource)) ? "file" : "unknown",
17237
- ...typeof entry.scopes === "string" && /^[A-Za-z0-9_:, -]{0,500}$/.test(entry.scopes) ? { scopes: entry.scopes } : {}
17238
- });
17239
- }
17240
- return { accounts, activeLogin: accounts.find((account) => account.active)?.login ?? null };
17241
- }
17242
- const GH_ENV = { GH_HOST: "github.com", GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", GH_PAGER: "cat", NO_COLOR: "1" };
17243
- const GH_PRIVATE_ENV = ["GITHUB_TOKEN", "GH_DEBUG", "DEBUG"];
17244
- async function identifySshAccount(remote, runner) {
17245
- const ssh = parseSshRemote(remote);
17246
- if (!ssh) return null;
17247
- try {
17248
- const config2 = await runner.run("ssh", ["-G", ...ssh.port ? ["-p", ssh.port] : [], "--", ssh.destination], {
17249
- cwd: os.homedir(),
17250
- timeoutMs: 5e3,
17251
- maxOutputBytes: 64 * 1024
17252
- });
17253
- const hostname = config2.stdout.split(/\r?\n/).find((line) => /^hostname\s/i.test(line))?.trim().split(/\s+/)[1];
17254
- if (config2.exitCode !== 0 || hostname?.toLowerCase() !== "github.com") return null;
17255
- const result = await runner.run("ssh", [
17256
- "-T",
17257
- "-o",
17258
- "BatchMode=yes",
17259
- "-o",
17260
- "StrictHostKeyChecking=yes",
17261
- "-o",
17262
- "ConnectTimeout=5",
17263
- "-o",
17264
- "ConnectionAttempts=1",
17265
- "-o",
17266
- "RemoteCommand=none",
17267
- "-o",
17268
- "UpdateHostKeys=no",
17269
- ...ssh.port ? ["-p", ssh.port] : [],
17270
- "--",
17271
- ssh.destination
17272
- ], { cwd: os.homedir(), timeoutMs: 1e4, maxOutputBytes: 64 * 1024 });
17273
- if (result.exitCode !== 0 && result.exitCode !== 1) return null;
17274
- const login2 = /^Hi ([A-Za-z0-9-]+)! You've successfully authenticated, but GitHub does not provide shell access\.\s*$/m.exec(`${result.stdout}
17275
- ${result.stderr}`)?.[1];
17276
- return login2 && GITHUB_LOGIN.test(login2) ? login2 : null;
17277
- } catch {
17278
- return null;
17279
- }
17280
- }
17281
- async function accountAuthentication(login2, executable, runner, operation) {
17282
- try {
17283
- const result = await runner.run(executable.executable, ["auth", "token", "--hostname", "github.com", ...login2 ? ["--user", login2] : []], {
17284
- cwd: os.homedir(),
17285
- env: { ...executable.env, ...GH_ENV },
17286
- removeEnv: login2 ? ["GH_TOKEN", ...GH_PRIVATE_ENV] : ["GH_DEBUG", "DEBUG"],
17287
- timeoutMs: 1e4,
17288
- maxOutputBytes: 8 * 1024
17289
- });
17290
- const token = result.stdout.trim();
17291
- if (result.exitCode === 0 && /^[A-Za-z0-9_]{20,4096}$/.test(token)) return { env: { GH_TOKEN: token }, removeEnv: GH_PRIVATE_ENV };
17292
- if (/unknown flag.*user/i.test(result.stderr)) throw new GhOperationError({
17293
- code: "GH_CLI_INCOMPATIBLE",
17294
- operation,
17295
- message: "Update GitHub CLI to a version supporting multiple accounts and auth token --user."
17296
- });
17297
- } catch (error) {
17298
- if (error instanceof GhOperationError) throw error;
17299
- }
17300
- throw new GhOperationError({
17301
- code: login2 ? "GH_ACCOUNT_MISSING" : "GH_AUTH_REQUIRED",
17302
- operation,
17303
- message: login2 ? `GitHub CLI has no usable saved credentials for @${login2}. Sign in as ${login2} on the backend host and check Settings → GitHub again. SSH and GitHub CLI authentication are separate.` : "GitHub CLI has no usable credentials. Sign in on the backend host and check Settings → GitHub again."
17304
- });
17305
- }
17306
- function redactCommandToken(value, auth) {
17307
- const token = auth.env?.GH_TOKEN;
17308
- return token ? value.split(token).join("[redacted]") : value;
17309
- }
17310
17714
  const CACHE_MS = 6e4;
17311
17715
  const emptyStatus = () => ({
17312
17716
  installationStatus: "unchecked",
@@ -17338,6 +17742,15 @@ class GitHubAccountsService {
17338
17742
  get authRevision() {
17339
17743
  return this.revision;
17340
17744
  }
17745
+ revisions = /* @__PURE__ */ new Map();
17746
+ defaultRevision = 0;
17747
+ revisionFor(repository) {
17748
+ const selection = this.selection(repository);
17749
+ return Math.max(
17750
+ this.revisions.get(githubRepositoryKey(repository.commonDir)) ?? 0,
17751
+ selection.mode === "account" && selection.useGlobalDefault ? this.defaultRevision : 0
17752
+ );
17753
+ }
17341
17754
  contexts = /* @__PURE__ */ new Map();
17342
17755
  ssh = /* @__PURE__ */ new Map();
17343
17756
  identities = /* @__PURE__ */ new Map();
@@ -17346,20 +17759,20 @@ class GitHubAccountsService {
17346
17759
  this.checking ??= this.check().finally(() => {
17347
17760
  this.checking = null;
17348
17761
  });
17349
- return this.checking.then((value) => structuredClone(value));
17762
+ return this.checking.then((value) => ({ ...structuredClone(value), defaultLogin: this.settings?.githubDefaultLogin ?? null }));
17350
17763
  }
17351
17764
  this.loading ??= (async () => {
17352
17765
  this.inventory ??= await this.store?.load() ?? emptyStatus();
17353
17766
  return this.inventory;
17354
17767
  })();
17355
- return this.loading.then(() => structuredClone(this.inventory ?? emptyStatus()));
17768
+ return this.loading.then(() => ({ ...structuredClone(this.inventory ?? emptyStatus()), defaultLogin: this.settings?.githubDefaultLogin ?? null }));
17356
17769
  }
17357
17770
  selection(repository) {
17358
17771
  return this.settings?.githubAccount(repository.commonDir) ?? { ...AUTOMATIC_GITHUB_ACCOUNT };
17359
17772
  }
17360
17773
  context(repository, remote, nameWithOwner) {
17361
17774
  const cached2 = this.contexts.get(repository.id);
17362
- if (cached2?.remote === remote && cached2.value.revision === this.revision) return structuredClone(cached2.value);
17775
+ if (cached2?.remote === remote && cached2.value.revision === this.revisionFor(repository)) return structuredClone(cached2.value);
17363
17776
  const selection = this.selection(repository);
17364
17777
  return {
17365
17778
  selection,
@@ -17368,20 +17781,41 @@ class GitHubAccountsService {
17368
17781
  source: selection.mode === "account" ? "explicit" : null,
17369
17782
  state: "unchecked",
17370
17783
  checkedAt: null,
17371
- revision: this.revision
17784
+ revision: this.revisionFor(repository)
17372
17785
  };
17373
17786
  }
17374
17787
  async setSelection(repository, selection) {
17375
17788
  if (!this.settings) throw new Error("GitHub account settings are unavailable.");
17376
- await this.settings.setGitHubAccount(repository.commonDir, selection);
17377
- this.invalidate();
17789
+ const saving = this.settings.setGitHubAccount(repository.commonDir, selection);
17790
+ this.invalidate(repository);
17791
+ try {
17792
+ await saving;
17793
+ } catch (error) {
17794
+ this.invalidate(repository);
17795
+ throw error;
17796
+ }
17378
17797
  }
17379
- invalidate() {
17798
+ async setDefaultAccount(login2) {
17799
+ if (!this.settings) throw new Error("GitHub account settings are unavailable.");
17800
+ this.defaultRevision = ++this.revision;
17801
+ try {
17802
+ await this.settings.setGitHubDefaultLogin(login2);
17803
+ } finally {
17804
+ this.changed(void 0, true);
17805
+ }
17806
+ return this.status();
17807
+ }
17808
+ invalidate(repository) {
17380
17809
  this.revision++;
17381
- this.contexts.clear();
17810
+ if (repository) {
17811
+ const key = githubRepositoryKey(repository.commonDir);
17812
+ this.revisions.set(key, this.revision);
17813
+ } else {
17814
+ this.contexts.clear();
17815
+ }
17382
17816
  this.ssh.clear();
17383
17817
  this.identities.clear();
17384
- this.changed();
17818
+ this.changed(repository);
17385
17819
  }
17386
17820
  refreshAuthentication() {
17387
17821
  this.ssh.clear();
@@ -17397,7 +17831,7 @@ class GitHubAccountsService {
17397
17831
  return this.detection.promise;
17398
17832
  }
17399
17833
  async authenticate(repository, remote, nameWithOwner, operation) {
17400
- const revision2 = this.revision;
17834
+ const revision2 = this.revisionFor(repository);
17401
17835
  const selection = this.selection(repository);
17402
17836
  let login2 = selection.mode === "account" ? selection.login : null;
17403
17837
  let source = selection.mode === "account" ? "explicit" : null;
@@ -17445,18 +17879,18 @@ class GitHubAccountsService {
17445
17879
  });
17446
17880
  login2 = verified;
17447
17881
  const context = { selection, nameWithOwner, login: login2, source, state: "ready", checkedAt: (/* @__PURE__ */ new Date()).toISOString(), revision: revision2 };
17448
- if (revision2 === this.revision) {
17882
+ if (revision2 === this.revisionFor(repository)) {
17449
17883
  const previous = this.contexts.get(repository.id)?.value;
17450
- if (previous?.state === "ready" && (previous.login?.toLowerCase() !== login2.toLowerCase() || previous.source !== source)) {
17451
- this.invalidate();
17452
- context.revision = this.revision;
17884
+ if (previous?.state === "ready" && previous.revision === revision2 && (previous.login?.toLowerCase() !== login2.toLowerCase() || previous.source !== source)) {
17885
+ this.invalidate(repository);
17886
+ context.revision = this.revisionFor(repository);
17453
17887
  }
17454
17888
  this.contexts.set(repository.id, { remote, value: context });
17455
17889
  }
17456
17890
  return { auth, executable, context };
17457
17891
  } catch (error) {
17458
17892
  const message = error instanceof GhOperationError ? error.message : "Could not inspect GitHub authentication. Check Settings → GitHub again.";
17459
- if (revision2 === this.revision) this.contexts.set(repository.id, { remote, value: {
17893
+ if (revision2 === this.revisionFor(repository)) this.contexts.set(repository.id, { remote, value: {
17460
17894
  selection,
17461
17895
  nameWithOwner,
17462
17896
  login: login2,
@@ -17674,6 +18108,15 @@ function parsePullRequestList(raw) {
17674
18108
  if (!list.success) throw invalidOutput("gh-pr-list");
17675
18109
  return list.data.map((item) => parseSummary(item, "gh-pr-list"));
17676
18110
  }
18111
+ function parseBranchPullRequestList(raw, nameWithOwner, branch) {
18112
+ const list = pullRequestArraySchema.safeParse(parseJson(raw, "gh-pr-for-branch"));
18113
+ if (!list.success) throw invalidOutput("gh-pr-for-branch");
18114
+ const headSchema = looseObject({ headRefName: string(), headRepository: looseObject({ nameWithOwner: string() }) });
18115
+ return list.data.filter((item) => {
18116
+ const head = headSchema.safeParse(item);
18117
+ return head.success && head.data.headRefName === branch && head.data.headRepository.nameWithOwner.toLowerCase() === nameWithOwner.toLowerCase();
18118
+ }).map((item) => parseSummary(item, "gh-pr-for-branch"));
18119
+ }
17677
18120
  function parsePullRequestDetails(raw) {
17678
18121
  const item = parseJson(raw, "gh-pr-view");
17679
18122
  const summary = parseSummary(item, "gh-pr-view");
@@ -17816,6 +18259,7 @@ class GitHubService {
17816
18259
  runner;
17817
18260
  git;
17818
18261
  repositories;
18262
+ gitAccess;
17819
18263
  accounts;
17820
18264
  async status(forceRefresh = false) {
17821
18265
  if (forceRefresh) this.accounts.refreshAuthentication();
@@ -17832,33 +18276,33 @@ class GitHubService {
17832
18276
  const repository = this.repositories.get(repositoryId);
17833
18277
  const info = await this.repositoryInfo(repositoryId);
17834
18278
  const remote = await this.remote(repositoryId);
17835
- if (forceRefresh && info.nameWithOwner) {
18279
+ if (forceRefresh && info.nameWithOwner && !this.gitAccess) {
17836
18280
  this.accounts.refreshAuthentication();
17837
18281
  try {
17838
18282
  await this.accounts.authenticate(repository, remote, info.nameWithOwner, "gh-repository-account");
17839
18283
  } catch {
17840
18284
  }
17841
18285
  }
17842
- return this.accounts.context(repository, remote, info.nameWithOwner);
18286
+ const access2 = await this.gitAccess?.status(repositoryId, forceRefresh, { remote, nameWithOwner: info.nameWithOwner });
18287
+ return { ...this.accounts.context(repository, remote, info.nameWithOwner), ...access2 ? { access: access2 } : {} };
17843
18288
  }
17844
- async setRepositoryAccount(repositoryId, selection) {
18289
+ async setRepositoryAccount(repositoryId, selection, expectedRevision) {
18290
+ if (expectedRevision !== void 0 && expectedRevision !== this.accounts.revisionFor(this.repositories.get(repositoryId))) throw new Error("The repository account changed in another session. Reopen the account setup and review it.");
17845
18291
  await this.accounts.setSelection(this.repositories.get(repositoryId), selection);
17846
18292
  return this.repositoryAccount(repositoryId);
17847
18293
  }
17848
18294
  async remote(repositoryId) {
17849
18295
  const repository = this.repositories.get(repositoryId);
17850
18296
  try {
17851
- const output = await this.git.run(repository.path, ["config", "--get", "remote.origin.url"], { operation: "github-remote-url", readOnly: true, maxOutputBytes: 64 * 1024 });
18297
+ const output = await this.git.run(repository.path, ["remote", "get-url", "--", "origin"], { operation: "github-remote-url", readOnly: true, maxOutputBytes: 64 * 1024 });
17852
18298
  return output.stdout.toString("utf8").trim();
17853
18299
  } catch {
17854
18300
  return "";
17855
18301
  }
17856
18302
  }
17857
18303
  async repositoryInfo(repositoryId) {
17858
- const repository = this.repositories.get(repositoryId);
17859
18304
  try {
17860
- const output = await this.git.run(repository.path, ["config", "--get", "remote.origin.url"], { operation: "github-remote-url", readOnly: true, maxOutputBytes: 64 * 1024 });
17861
- const url = output.stdout.toString("utf8");
18305
+ const url = await this.remote(repositoryId);
17862
18306
  let parsed = parseGitHubRemote(url);
17863
18307
  if (!parsed) {
17864
18308
  const ssh = parseSshRemote(url);
@@ -17890,16 +18334,16 @@ class GitHubService {
17890
18334
  const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
17891
18335
  const branch = validateRef(branchName, operation);
17892
18336
  const openResult = await this.runGh(
17893
- ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "open", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
18337
+ ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "open", "--json", `${PR_SUMMARY_FIELDS},headRepository`, "--limit", "10"],
17894
18338
  { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
17895
18339
  );
17896
- const openPull = sortPullRequestsNewestFirst(parsePullRequestList(openResult.stdout)).find((pull) => pull.state === "OPEN");
18340
+ const openPull = sortPullRequestsNewestFirst(parseBranchPullRequestList(openResult.stdout, nameWithOwner, branch)).find((pull) => pull.state === "OPEN");
17897
18341
  if (openPull) return openPull;
17898
18342
  const result = await this.runGh(
17899
- ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "all", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
18343
+ ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "all", "--json", `${PR_SUMMARY_FIELDS},headRepository`, "--limit", "10"],
17900
18344
  { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
17901
18345
  );
17902
- return sortPullRequestsNewestFirst(parsePullRequestList(result.stdout))[0] ?? null;
18346
+ return sortPullRequestsNewestFirst(parseBranchPullRequestList(result.stdout, nameWithOwner, branch))[0] ?? null;
17903
18347
  }
17904
18348
  async getPullRequest(repositoryId, prNumber) {
17905
18349
  const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-view");
@@ -17983,7 +18427,7 @@ class GitHubService {
17983
18427
  const operation = "gh-pr-create";
17984
18428
  const { repository, nameWithOwner, auth, executable, context } = await this.requireGitHub(input.repositoryId, operation);
17985
18429
  const verifyAccount = () => {
17986
- if (input.expectedAccount && (input.expectedAccount.revision !== this.accounts.authRevision || input.expectedAccount.revision !== context.revision || input.expectedAccount.login.toLowerCase() !== context.login?.toLowerCase())) {
18430
+ if (input.expectedAccount && (input.expectedAccount.revision !== this.accounts.revisionFor(repository) || input.expectedAccount.revision !== context.revision || input.expectedAccount.login.toLowerCase() !== context.login?.toLowerCase())) {
17987
18431
  throw new GhOperationError({ code: "GH_ACCOUNT_UNRESOLVED", operation, message: "The GitHub account changed. Review the current identity before creating the pull request." });
17988
18432
  }
17989
18433
  };
@@ -18524,10 +18968,11 @@ function normalizeRemoteFetchIntervalSeconds(value) {
18524
18968
  Math.max(MIN_REMOTE_FETCH_INTERVAL_SECONDS, stepped)
18525
18969
  );
18526
18970
  }
18527
- const SETTINGS_SCHEMA_VERSION = 1;
18971
+ const SETTINGS_SCHEMA_VERSION = 2;
18528
18972
  const defaults = {
18529
18973
  version: SETTINGS_SCHEMA_VERSION,
18530
18974
  githubAccounts: {},
18975
+ githubDefaultLogin: null,
18531
18976
  recentRepositories: [],
18532
18977
  repositoryProjects: [],
18533
18978
  filesTreeStates: [],
@@ -18632,10 +19077,33 @@ class SettingsStore {
18632
19077
  return { ...this.data.windowBounds };
18633
19078
  }
18634
19079
  githubAccount(commonDir) {
18635
- return { ...this.data.githubAccounts[githubRepositoryKey(commonDir)] ?? AUTOMATIC_GITHUB_ACCOUNT };
19080
+ const selection = this.data.githubAccounts[githubRepositoryKey(commonDir)] ?? AUTOMATIC_GITHUB_ACCOUNT;
19081
+ if (selection.mode === "account" && selection.useGlobalDefault) {
19082
+ if (!this.data.githubDefaultLogin) throw new Error("Choose an OpenTig default account in Settings → GitHub.");
19083
+ return { ...selection, login: this.data.githubDefaultLogin };
19084
+ }
19085
+ return { ...selection };
19086
+ }
19087
+ get githubDefaultLogin() {
19088
+ return this.data.githubDefaultLogin;
19089
+ }
19090
+ async setGitHubDefaultLogin(login2) {
19091
+ const parsed = githubAccountSelectionSchema.parse({ mode: "account", host: "github.com", login: login2 });
19092
+ if (parsed.mode !== "account") throw new Error("Invalid default account.");
19093
+ const previous = this.data.githubDefaultLogin;
19094
+ this.data.githubDefaultLogin = parsed.login;
19095
+ try {
19096
+ await this.save();
19097
+ } catch (error) {
19098
+ if (this.data.githubDefaultLogin === parsed.login) this.data.githubDefaultLogin = previous;
19099
+ throw error;
19100
+ }
18636
19101
  }
18637
19102
  async setGitHubAccount(commonDir, selection) {
18638
19103
  const parsed = githubAccountSelectionSchema.parse(selection);
19104
+ if (parsed.mode === "account" && parsed.useGlobalDefault && parsed.login.toLowerCase() !== this.data.githubDefaultLogin?.toLowerCase()) {
19105
+ throw new Error("The OpenTig default account changed. Review the current account and try again.");
19106
+ }
18639
19107
  const key = githubRepositoryKey(commonDir);
18640
19108
  const previous = this.data.githubAccounts[key];
18641
19109
  if (parsed.mode === "auto") delete this.data.githubAccounts[key];
@@ -18652,9 +19120,9 @@ class SettingsStore {
18652
19120
  }
18653
19121
  async touchRepository(repository) {
18654
19122
  const recent = { ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() };
18655
- this.data.recentRepositories = [recent, ...this.data.recentRepositories.filter((item) => item.id !== repository.id)];
19123
+ const existing = this.data.recentRepositories.some((item) => item.id === repository.id);
19124
+ this.data.recentRepositories = existing ? this.data.recentRepositories.map((item) => item.id === repository.id ? recent : item) : [...this.data.recentRepositories, recent];
18656
19125
  this.data.activeRepositoryId = repository.id;
18657
- this.pruneRecentRepositories();
18658
19126
  await this.save();
18659
19127
  }
18660
19128
  /**
@@ -18683,12 +19151,8 @@ class SettingsStore {
18683
19151
  });
18684
19152
  this.data.filesTreeStates = normalizeFilesTreeStates(this.data.filesTreeStates.map((state) => state.repositoryId === previousId ? { ...state, repositoryId: repository.id } : state));
18685
19153
  this.data.openFilesStates = normalizeOpenFilesStates(this.data.openFilesStates.map((state) => state.repositoryId === previousId ? { ...state, repositoryId: repository.id } : state));
18686
- this.data.recentRepositories = [
18687
- { ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
18688
- ...this.data.recentRepositories.filter((item) => item.id !== previousId && item.id !== repository.id)
18689
- ];
19154
+ this.data.recentRepositories = this.data.recentRepositories.flatMap((item) => item.id === previousId ? [{ ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() }] : item.id === repository.id ? [] : [item]);
18690
19155
  this.data.activeRepositoryId = repository.id;
18691
- this.pruneRecentRepositories();
18692
19156
  await this.save();
18693
19157
  }
18694
19158
  /** Forget the whole repository group, never its directories or Git data. */
@@ -18746,7 +19210,6 @@ class SettingsStore {
18746
19210
  async removeRepositoryProject(projectId) {
18747
19211
  this.getProject(projectId);
18748
19212
  this.data.repositoryProjects = this.data.repositoryProjects.filter((project) => project.id !== projectId);
18749
- this.pruneRecentRepositories();
18750
19213
  await this.save();
18751
19214
  return this.organization;
18752
19215
  }
@@ -18754,19 +19217,44 @@ class SettingsStore {
18754
19217
  const key = normalizeRepositoryKey(repositoryKey);
18755
19218
  if (!key || key.length > MAX_REPOSITORY_KEY_LENGTH || hasControlCharacters$1(key)) throw projectError("Invalid repository.");
18756
19219
  const target = projectId === null ? null : this.getProject(projectId);
18757
- if (target && !this.data.recentRepositories.some((repository) => normalizeRepositoryKey(repository.commonDir) === key)) {
19220
+ if (!this.data.recentRepositories.some((repository) => normalizeRepositoryKey(repository.commonDir) === key)) {
18758
19221
  throw projectError("Unknown repository.");
18759
19222
  }
19223
+ if (target?.repositoryKeys.includes(key)) return this.organization;
19224
+ if (target && target.repositoryKeys.length >= MAX_REPOSITORIES_PER_PROJECT) throw projectError("This project is full.");
18760
19225
  this.data.repositoryProjects = this.data.repositoryProjects.map((project) => ({
18761
19226
  ...project,
18762
19227
  repositoryKeys: project.repositoryKeys.filter((item) => item !== key)
18763
19228
  }));
18764
19229
  if (target) {
18765
19230
  const refreshed = this.getProject(target.id);
18766
- if (refreshed.repositoryKeys.length >= MAX_REPOSITORIES_PER_PROJECT) throw projectError("This project is full.");
18767
19231
  refreshed.repositoryKeys.push(key);
18768
19232
  }
18769
- this.pruneRecentRepositories();
19233
+ await this.save();
19234
+ return this.organization;
19235
+ }
19236
+ async moveRepositoryProject(projectId, toIndex) {
19237
+ this.data.repositoryProjects = moveToIndex(this.data.repositoryProjects, projectId, toIndex, (project) => project.id);
19238
+ await this.save();
19239
+ return this.organization;
19240
+ }
19241
+ /** Move within the assigned project, or within the unassigned repositories. */
19242
+ async moveRepository(repositoryKey, toIndex) {
19243
+ const key = normalizeRepositoryKey(repositoryKey);
19244
+ const keys = [...new Set(this.data.recentRepositories.map((item) => normalizeRepositoryKey(item.commonDir)))];
19245
+ const known = new Set(keys);
19246
+ const project = this.data.repositoryProjects.find((item) => item.repositoryKeys.includes(key));
19247
+ if (project) {
19248
+ const visible = project.repositoryKeys.filter((item) => known.has(item));
19249
+ project.repositoryKeys = [...moveToIndex(visible, key, toIndex, (item) => item), ...project.repositoryKeys.filter((item) => !known.has(item))];
19250
+ } else {
19251
+ const assigned = new Set(this.data.repositoryProjects.flatMap((item) => item.repositoryKeys));
19252
+ const unassigned = moveToIndex(keys.filter((item) => !assigned.has(item)), key, toIndex, (item) => item);
19253
+ let index = 0;
19254
+ const ordered = keys.map((item) => assigned.has(item) ? item : unassigned[index++]);
19255
+ const positions = new Map(ordered.map((item, position) => [item, position]));
19256
+ this.data.recentRepositories = [...this.data.recentRepositories].sort((a2, b) => positions.get(normalizeRepositoryKey(a2.commonDir)) - positions.get(normalizeRepositoryKey(b.commonDir)));
19257
+ }
18770
19258
  await this.save();
18771
19259
  return this.organization;
18772
19260
  }
@@ -18878,15 +19366,6 @@ class SettingsStore {
18878
19366
  throw projectError("A project with this name already exists.");
18879
19367
  }
18880
19368
  }
18881
- pruneRecentRepositories() {
18882
- const assigned = new Set(this.data.repositoryProjects.flatMap((project) => project.repositoryKeys));
18883
- let unassigned = 0;
18884
- this.data.recentRepositories = this.data.recentRepositories.filter((repository) => {
18885
- if (repository.id === this.data.activeRepositoryId || assigned.has(normalizeRepositoryKey(repository.commonDir))) return true;
18886
- unassigned += 1;
18887
- return unassigned <= UNASSIGNED_RECENT_LIMIT;
18888
- });
18889
- }
18890
19369
  }
18891
19370
  function validate(value, defaultDoubleControlShortcutEnabled) {
18892
19371
  const parsed = settingsRecordSchema.safeParse(value);
@@ -18896,8 +19375,16 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
18896
19375
  return data;
18897
19376
  }
18898
19377
  const input = parsed.data;
18899
- const recentRepositories = Array.isArray(input.recentRepositories) ? input.recentRepositories.filter(isRecent).map(normalizeRecent).slice(0, 10) : [];
19378
+ const recentRepositories = Array.isArray(input.recentRepositories) ? input.recentRepositories.filter(isRecent).map(normalizeRecent) : [];
18900
19379
  const repositoryProjects = normalizeProjects(input.repositoryProjects);
19380
+ if (input.version !== SETTINGS_SCHEMA_VERSION) {
19381
+ const keys = [...new Set(recentRepositories.map((item) => normalizeRepositoryKey(item.commonDir)))];
19382
+ const known = new Set(keys);
19383
+ for (const project of repositoryProjects) {
19384
+ const assigned = new Set(project.repositoryKeys);
19385
+ project.repositoryKeys = [...keys.filter((key) => assigned.has(key)), ...project.repositoryKeys.filter((key) => !known.has(key))];
19386
+ }
19387
+ }
18901
19388
  const filesTreeStates = normalizeFilesTreeStates(input.filesTreeStates);
18902
19389
  const openFilesStates = normalizeOpenFilesStates(input.openFilesStates);
18903
19390
  const parsedPreferences = preferencesRecordSchema.safeParse(input.preferences);
@@ -18926,6 +19413,7 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
18926
19413
  return {
18927
19414
  version: SETTINGS_SCHEMA_VERSION,
18928
19415
  githubAccounts: normalizeGitHubAccounts(input.githubAccounts),
19416
+ githubDefaultLogin: typeof input.githubDefaultLogin === "string" && githubAccountSelectionSchema.safeParse({ mode: "account", host: "github.com", login: input.githubDefaultLogin }).success ? input.githubDefaultLogin : null,
18929
19417
  recentRepositories,
18930
19418
  repositoryProjects,
18931
19419
  filesTreeStates,
@@ -18978,6 +19466,15 @@ function normalizeWorktreePath(value) {
18978
19466
  function cloneProject(project) {
18979
19467
  return { ...project, repositoryKeys: [...project.repositoryKeys] };
18980
19468
  }
19469
+ function moveToIndex(items, key, toIndex, keyOf) {
19470
+ const fromIndex = items.findIndex((item2) => keyOf(item2) === key);
19471
+ if (fromIndex < 0) throw projectError("Unknown project or repository.");
19472
+ if (!Number.isSafeInteger(toIndex) || toIndex < 0 || toIndex >= items.length) throw projectError("Invalid ordering position.");
19473
+ const next = [...items];
19474
+ const [item] = next.splice(fromIndex, 1);
19475
+ next.splice(toIndex, 0, item);
19476
+ return next;
19477
+ }
18981
19478
  function cloneFilesTreeState(state) {
18982
19479
  return { ...state, expandedPaths: [...state.expandedPaths] };
18983
19480
  }
@@ -19127,8 +19624,14 @@ class OpenTigRuntime {
19127
19624
  this.lastNotifiedAt = Date.now();
19128
19625
  this.onEvent({ type: "repository.changed", repositoryId, scope });
19129
19626
  }
19130
- publishGitHubAccountsChange() {
19131
- if (!this.closePromise) this.onEvent({ type: "github.accounts-changed" });
19627
+ publishGitHubAccountsChange(commonDir, defaultOnly = false) {
19628
+ if (this.closePromise) return;
19629
+ const repositories = this.services.repositories.recents();
19630
+ const repositoryIds = defaultOnly ? repositories.filter((repo) => {
19631
+ const selection = this.settings.githubAccount(repo.commonDir);
19632
+ return selection.mode === "account" && selection.useGlobalDefault;
19633
+ }).map((repo) => repo.id) : commonDir ? repositories.filter((repo) => githubRepositoryKey(repo.commonDir) === githubRepositoryKey(commonDir)).map((repo) => repo.id) : void 0;
19634
+ this.onEvent({ type: "github.accounts-changed", ...repositoryIds ? { repositoryIds } : {}, ...defaultOnly ? { inventoryChanged: true } : {} });
19132
19635
  }
19133
19636
  publishActiveRepositoryChange(repository) {
19134
19637
  if (!this.closePromise) this.onEvent({ type: "repository.active-changed", repository });
@@ -19200,9 +19703,13 @@ async function createOpenTigRuntime(options) {
19200
19703
  git,
19201
19704
  settings,
19202
19705
  new GitHubStatusStore(path.join(path.dirname(options.settingsPath), "github-status.json")),
19203
- () => runtime?.publishGitHubAccountsChange()
19706
+ (repository, defaultOnly) => runtime?.publishGitHubAccountsChange(repository?.commonDir, defaultOnly)
19204
19707
  );
19708
+ const gitAccess = new RepositoryGitAccess(git, repositories, githubAccounts, cliRunner);
19709
+ gitAccess.install();
19710
+ operations.access = gitAccess;
19205
19711
  const github = new GitHubService(cliResolver, cliRunner, git, repositories, githubAccounts);
19712
+ github.gitAccess = gitAccess;
19206
19713
  const events = {
19207
19714
  repositoryChanged: (repositoryId, scope) => {
19208
19715
  runtime?.publishRepositoryChange(repositoryId, scope);
@@ -19369,6 +19876,10 @@ const IPC = {
19369
19876
  projectRename: "projects:rename",
19370
19877
  projectRemove: "projects:remove",
19371
19878
  projectAssign: "projects:assign",
19879
+ projectMove: "projects:move",
19880
+ projectMoveRepository: "projects:move-repository",
19881
+ repositoryPreparePath: "repository:prepare-path",
19882
+ repositoryCompleteSetup: "repository:complete-setup",
19372
19883
  repositoryOpenPath: "repository:open-path",
19373
19884
  repositoryBrowseDirectories: "repository:browse-directories",
19374
19885
  repositoryOpenRecent: "repository:open-recent",
@@ -19419,6 +19930,7 @@ const IPC = {
19419
19930
  worktreeSelect: "refs:select-worktree",
19420
19931
  refsPull: "refs:pull",
19421
19932
  refsPush: "refs:push",
19933
+ refsPushContext: "refs:push-context",
19422
19934
  refsFetch: "refs:fetch",
19423
19935
  localRefsSnapshot: "refs:local-snapshot",
19424
19936
  branchDetails: "refs:branch-details",
@@ -19438,6 +19950,7 @@ const IPC = {
19438
19950
  diagnosticsClear: "diagnostics:clear",
19439
19951
  diagnosticsRecord: "diagnostics:record",
19440
19952
  githubAccountsStatus: "github:accounts-status",
19953
+ githubSetDefaultAccount: "github:set-default-account",
19441
19954
  githubRepositoryAccount: "github:repository-account",
19442
19955
  githubSetRepositoryAccount: "github:set-repository-account",
19443
19956
  githubStatus: "github:status",
@@ -19466,6 +19979,10 @@ const OPEN_TIG_SERVER_COMMANDS = {
19466
19979
  "projects.rename": command(IPC.projectRename, "project-rename", true),
19467
19980
  "projects.remove": command(IPC.projectRemove, "project-remove", true),
19468
19981
  "projects.assign": command(IPC.projectAssign, "project-assign", true),
19982
+ "projects.moveProject": command(IPC.projectMove, "project-move", true),
19983
+ "projects.moveRepository": command(IPC.projectMoveRepository, "project-move-repository", true),
19984
+ "repository.preparePath": command(IPC.repositoryPreparePath, "prepare-path", false),
19985
+ "repository.completeSetup": command(IPC.repositoryCompleteSetup, "complete-setup", true),
19469
19986
  "repository.openPath": command(IPC.repositoryOpenPath, "open-path", true),
19470
19987
  "repository.browseDirectories": command(IPC.repositoryBrowseDirectories, "browse-directories", false),
19471
19988
  "repository.openRecent": command(IPC.repositoryOpenRecent, "open-recent", true),
@@ -19515,6 +20032,7 @@ const OPEN_TIG_SERVER_COMMANDS = {
19515
20032
  "refs.listWorktrees": command(IPC.worktreesList, "worktrees", false),
19516
20033
  "refs.selectWorktree": command(IPC.worktreeSelect, "select-worktree", true),
19517
20034
  "refs.pull": command(IPC.refsPull, "pull", true),
20035
+ "refs.pushContext": command(IPC.refsPushContext, "push-context", false),
19518
20036
  "refs.push": command(IPC.refsPush, "push", true),
19519
20037
  "refs.fetch": command(IPC.refsFetch, "fetch", true),
19520
20038
  "refs.localRefsSnapshot": command(IPC.localRefsSnapshot, "local-refs-snapshot", false),
@@ -19533,6 +20051,7 @@ const OPEN_TIG_SERVER_COMMANDS = {
19533
20051
  "ai.cancelGeneration": command(IPC.aiCancelGeneration, "ai-cancel-generation", true),
19534
20052
  "github.accountsStatus": command(IPC.githubAccountsStatus, "gh-accounts-status", false, { timeoutMs: 9e4 }),
19535
20053
  "github.repositoryAccount": command(IPC.githubRepositoryAccount, "gh-repository-account", false, { timeoutMs: 6e4 }),
20054
+ "github.setDefaultAccount": command(IPC.githubSetDefaultAccount, "gh-set-default-account", true),
19536
20055
  "github.setRepositoryAccount": command(IPC.githubSetRepositoryAccount, "gh-set-repository-account", true),
19537
20056
  "github.status": command(IPC.githubStatus, "gh-status", false),
19538
20057
  "github.repositoryInfo": command(IPC.githubRepositoryInfo, "gh-repository-info", false),
@@ -20078,6 +20597,10 @@ function invalidAi(operation) {
20078
20597
  function invalidProject(operation) {
20079
20598
  return new GitOperationError({ code: "INVALID_ARGUMENT", operation, message: "Invalid project data." });
20080
20599
  }
20600
+ function orderingIndexArg(value, operation) {
20601
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalidProject(operation);
20602
+ return value;
20603
+ }
20081
20604
  function registerServerCommands(registry2, services, host) {
20082
20605
  const serverDefinitions = new Map(
20083
20606
  Object.values(OPEN_TIG_SERVER_COMMANDS).map((definition) => [definition.command, definition])
@@ -20141,6 +20664,22 @@ function registerServerCommands(registry2, services, host) {
20141
20664
  handle(IPC.projectRename, "project-rename", (projectId, name) => services.settings.renameRepositoryProject(projectIdArg(projectId, "project-rename"), projectNameArg(name, "project-rename")));
20142
20665
  handle(IPC.projectRemove, "project-remove", (projectId) => services.settings.removeRepositoryProject(projectIdArg(projectId, "project-remove")));
20143
20666
  handle(IPC.projectAssign, "project-assign", (repositoryKey, projectId) => services.settings.assignRepositoryProject(repositoryKeyArg(repositoryKey, "project-assign"), nullableProjectIdArg(projectId, "project-assign")));
20667
+ handle(IPC.projectMove, "project-move", (projectId, toIndex) => services.settings.moveRepositoryProject(projectIdArg(projectId, "project-move"), orderingIndexArg(toIndex, "project-move")));
20668
+ handle(IPC.projectMoveRepository, "project-move-repository", (repositoryKey, toIndex) => services.settings.moveRepository(repositoryKeyArg(repositoryKey, "project-move-repository"), orderingIndexArg(toIndex, "project-move-repository")));
20669
+ handle(IPC.repositoryPreparePath, "prepare-path", (path2) => services.repositories.preparePath(stringArg(path2, "prepare-path", 32768)));
20670
+ handleWithContext(IPC.repositoryCompleteSetup, "complete-setup", async (context, id, revision2) => {
20671
+ const repositoryId = stringArg(id, "complete-setup", 64);
20672
+ if (typeof revision2 !== "number" || !Number.isSafeInteger(revision2) || revision2 < 0) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "complete-setup", message: "Invalid account revision." });
20673
+ const repository = await services.repositories.completeSetup(repositoryId, () => {
20674
+ if (revision2 !== services.github.accounts.revisionFor(services.repositories.get(repositoryId))) {
20675
+ throw new Error("The repository account changed. Reload setup and review it again.");
20676
+ }
20677
+ });
20678
+ fileClipboardState(context).pendingCut = null;
20679
+ services.watcher.start(repository);
20680
+ services.events.activeRepositoryChanged(repository);
20681
+ return repository;
20682
+ });
20144
20683
  handleWithContext(IPC.repositoryOpenPath, "open-path", (context, selectedPath) => openRepositoryPath(
20145
20684
  stringArg(selectedPath, "open-path", 32768),
20146
20685
  context
@@ -20440,7 +20979,8 @@ function registerServerCommands(registry2, services, host) {
20440
20979
  return services.operations.removeWorktree(repositoryId, path2, expectedOid, force, deleteBranch);
20441
20980
  });
20442
20981
  handle(IPC.refsPull, "pull", (id) => services.operations.pull(stringArg(id, "pull", 64)));
20443
- handle(IPC.refsPush, "push", (id, publish) => services.operations.push(stringArg(id, "push", 64), publishBranchArg(publish)));
20982
+ handle(IPC.refsPushContext, "push-context", (id, remote) => services.operations.publicationContext(stringArg(id, "push-context", 64), remote == null ? void 0 : stringArg(remote, "push-context", 512)));
20983
+ handle(IPC.refsPush, "push", (id, publish, expected) => services.operations.push(stringArg(id, "push", 64), publishBranchArg(publish), expected == null ? void 0 : stringArg(expected, "push", 64)));
20444
20984
  handle(IPC.refsFetch, "fetch", (id) => services.operations.fetch(stringArg(id, "fetch", 64)));
20445
20985
  handle(IPC.aiStatuses, "ai-statuses", (forceRefresh) => services.ai.statuses(booleanArg(forceRefresh, "ai-statuses")));
20446
20986
  handleWithContext(IPC.aiGenerateCommitMessage, "ai-generate-commit-message", (context, input) => services.ai.generate(generateCommitMessageArg(input), context.signal));
@@ -20465,11 +21005,13 @@ function registerServerCommands(registry2, services, host) {
20465
21005
  services.ai.cancel(aiString(requestId2, "ai-cancel-generation", 100, true));
20466
21006
  });
20467
21007
  handle(IPC.githubAccountsStatus, "gh-accounts-status", (forceRefresh) => services.github.accounts.status(booleanArg(forceRefresh, "gh-accounts-status")));
21008
+ handle(IPC.githubSetDefaultAccount, "gh-set-default-account", (login2) => services.github.accounts.setDefaultAccount(stringArg(login2, "gh-set-default-account", 39)));
20468
21009
  handle(IPC.githubRepositoryAccount, "gh-repository-account", (id, forceRefresh) => services.github.repositoryAccount(stringArg(id, "gh-repository-account", 64), booleanArg(forceRefresh, "gh-repository-account")));
20469
- handle(IPC.githubSetRepositoryAccount, "gh-set-repository-account", (id, selection) => {
21010
+ handle(IPC.githubSetRepositoryAccount, "gh-set-repository-account", (id, selection, expectedRevision) => {
20470
21011
  const parsed = githubAccountSelectionSchema.safeParse(selection);
21012
+ if (expectedRevision !== void 0 && (typeof expectedRevision !== "number" || !Number.isSafeInteger(expectedRevision) || expectedRevision < 0)) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "gh-set-repository-account", message: "Invalid account revision." });
20471
21013
  if (!parsed.success) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "gh-set-repository-account", message: "Invalid GitHub account selection." });
20472
- return services.github.setRepositoryAccount(stringArg(id, "gh-set-repository-account", 64), parsed.data);
21014
+ return services.github.setRepositoryAccount(stringArg(id, "gh-set-repository-account", 64), parsed.data, typeof expectedRevision === "number" ? expectedRevision : void 0);
20473
21015
  });
20474
21016
  handle(IPC.githubStatus, "gh-status", (forceRefresh) => services.github.status(booleanArg(forceRefresh, "gh-status")));
20475
21017
  handle(IPC.githubRepositoryInfo, "gh-repository-info", (id) => services.github.repositoryInfo(stringArg(id, "gh-repository-info", 64)));
@@ -20691,9 +21233,9 @@ class PersistentAuthStore {
20691
21233
  });
20692
21234
  }
20693
21235
  renewBrowserSession(token) {
20694
- const digest = this.digestCredential(token);
21236
+ const digest2 = this.digestCredential(token);
20695
21237
  return this.mutate((sessions) => {
20696
- const index = sessions.findIndex((session2) => session2.kind !== "desktop" && constantTimeEqual(digest, session2.digest) && this.isActive(session2));
21238
+ const index = sessions.findIndex((session2) => session2.kind !== "desktop" && constantTimeEqual(digest2, session2.digest) && this.isActive(session2));
20697
21239
  if (index < 0) return { next: sessions, result: false };
20698
21240
  const session = sessions[index];
20699
21241
  const expiresAt = sessionExpiry(session.kind, new Date(this.now()).toISOString());
@@ -21256,6 +21798,19 @@ async function handleRequest(context, request, response) {
21256
21798
  }
21257
21799
  return sendJson(response, 404, { error: "Not found." });
21258
21800
  }
21801
+ if (method === "GET" && rawPath === "/api/auth/web-access") {
21802
+ if (!context.auth.authenticate(request.headers)) return sendJson(response, 401, { error: "Authentication required." });
21803
+ const listener = context.listenerAddress();
21804
+ const status = {
21805
+ webAccessEnabled: context.auth.descriptor().browserAccessEnabled,
21806
+ pairingAvailable: context.auth.descriptor().pairingAvailable,
21807
+ listeningOnLan: !isLoopbackAddress(listener.host),
21808
+ listenerHost: listener.host,
21809
+ actualPort: listener.port,
21810
+ ready: context.isReady()
21811
+ };
21812
+ return sendJson(response, 200, status);
21813
+ }
21259
21814
  if (method === "GET" && rawPath === "/api/auth/sessions") {
21260
21815
  const currentSessionId = context.auth.authenticate(request.headers);
21261
21816
  if (!currentSessionId) return sendJson(response, 401, { error: "Authentication required." });
@@ -21275,6 +21830,15 @@ async function handleRequest(context, request, response) {
21275
21830
  }
21276
21831
  if (method === "POST" && rawPath.startsWith("/api/")) {
21277
21832
  if (!isAllowedOrigin(request)) return sendJson(response, 403, { error: "Forbidden origin." });
21833
+ if (rawPath === "/api/auth/pairing-link") {
21834
+ if (!context.auth.authenticate(request.headers)) return sendJson(response, 401, { error: "Authentication required." });
21835
+ if (!context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
21836
+ if (!context.isReady()) return sendJson(response, 503, { error: "Server is not ready." });
21837
+ const url = new URL("/pair", request.headers.origin);
21838
+ const pairing = context.auth.createPairingToken();
21839
+ url.hash = new URLSearchParams({ token: pairing.token }).toString();
21840
+ return sendJson(response, 200, { url: url.href, expiresAt: pairing.expiresAt });
21841
+ }
21278
21842
  if (rawPath === "/api/auth/renew") {
21279
21843
  const cookie = await context.auth.renewBrowserCookie(request.headers, requestOriginIsSecure(request));
21280
21844
  if (!cookie) return sendJson(response, 401, { error: "Authentication required." });
@@ -21788,6 +22352,10 @@ class OpenTigServer {
21788
22352
  identity: options.identity,
21789
22353
  mode: options.mode ?? "desktop",
21790
22354
  isReady: () => this.ready,
22355
+ listenerAddress: () => {
22356
+ const address = this.httpServer.address();
22357
+ return { host: address.address, port: address.port };
22358
+ },
21791
22359
  sessionConnectionCount: (sessionId) => this.webSockets.connectionCount(sessionId),
21792
22360
  onSessionsRevoked: (sessionIds) => this.disconnectSessions(sessionIds),
21793
22361
  logger: this.logger,