@opentig/cli 0.1.8 → 0.2.1

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 (124) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -1
  2. package/dist/bin.mjs +2 -2
  3. package/dist/chunks/{cli-BgNif2D1.mjs → cli-B6JTojx_.mjs} +15 -2
  4. package/dist/client/assets/App-Dfm5F199.js +74 -0
  5. package/dist/client/assets/CreatePullRequestDialog-CAeykZ72.js +2 -0
  6. package/dist/client/assets/{DiffWorkspace-Ca2ZUlVn.js → DiffWorkspace-BRicWHvw.js} +13 -13
  7. package/dist/client/assets/{EditableFileViewer-85aEeMBK.js → EditableFileViewer-DWeSDnZn.js} +1 -1
  8. package/dist/client/assets/{File-DTtWrbYf.js → File-D65shwRR.js} +1 -1
  9. package/dist/client/assets/FilesView-EfoU7sF-.js +1 -0
  10. package/dist/client/assets/GitHubAvatar-C8R3iNoM.js +1 -0
  11. package/dist/client/assets/HistoryView-C6T2dsBC.js +1 -0
  12. package/dist/client/assets/{HtmlFileViewer-bL6T57PQ.js → HtmlFileViewer-JXLHTNPm.js} +1 -1
  13. package/dist/client/assets/{IconEdit-DePJMGIi.js → IconEdit-3fgBTCQ4.js} +1 -1
  14. package/dist/client/assets/IconFolderSymlink-C4ydD1eS.js +1 -0
  15. package/dist/client/assets/{ImageFileViewer-DJaHH3hJ.js → ImageFileViewer-YH2xhkkk.js} +1 -1
  16. package/dist/client/assets/LocalRefsDialog--1NiYKYj.js +1 -0
  17. package/dist/client/assets/{MarkdownFileViewer-BNPb9v8u.js → MarkdownFileViewer-BsB8WkIH.js} +2 -2
  18. package/dist/client/assets/PairingPage-DXRyAYHn.js +1 -0
  19. package/dist/client/assets/{PierreDiffViewer-8wTLYd5-.js → PierreDiffViewer-DoV3V0IW.js} +1 -1
  20. package/dist/client/assets/{PierreEditBoundary-KzU3HDc2.js → PierreEditBoundary-BnYZbHfi.js} +1 -1
  21. package/dist/client/assets/{PierreWorkerPool-DH2jp9oe.js → PierreWorkerPool-Cg9zdxiI.js} +1 -1
  22. package/dist/client/assets/PullRequestDiff-D6QG5Anx.js +1 -0
  23. package/dist/client/assets/PullRequestViewer-jC3LVmiO.js +2 -0
  24. package/dist/client/assets/PullRequestsView-GStKxG7k.js +1 -0
  25. package/dist/client/assets/QuickOpenDialog-HvslpGQl.js +1 -0
  26. package/dist/client/assets/RepositoryProjectsDialog-Biqv5GO_.js +1 -0
  27. package/dist/client/assets/SearchView-D5XB6wdu.js +1 -0
  28. package/dist/client/assets/SettingsDialog-DWAunOQB.js +72 -0
  29. package/dist/client/assets/Viewer-Gfy3Ejyo.js +3 -0
  30. package/dist/client/assets/{abnfDiagram-N423BO3Z-CkHOGdAl.js → abnfDiagram-N423BO3Z-DdAfHevw.js} +1 -1
  31. package/dist/client/assets/{arc-Dj8F8CO5.js → arc-C43uoeEi.js} +1 -1
  32. package/dist/client/assets/{architectureDiagram-T3A2C74G-g87Y5LXk.js → architectureDiagram-T3A2C74G-C7q0hhRO.js} +1 -1
  33. package/dist/client/assets/base-ui-CJ8ZIx3d.js +1 -0
  34. package/dist/client/assets/{blockDiagram-VBNYF7ZC-D6efB7jD.js → blockDiagram-VBNYF7ZC-C1x9Iap4.js} +1 -1
  35. package/dist/client/assets/{c4Diagram-5PPSVZJV-DoZEC2xg.js → c4Diagram-5PPSVZJV-BmKaVW6q.js} +1 -1
  36. package/dist/client/assets/channel-Diwpee6E.js +1 -0
  37. package/dist/client/assets/checkbox-XfIUFPxh.js +1 -0
  38. package/dist/client/assets/{chunk-2GRJ4B5K-Dr44hDNV.js → chunk-2GRJ4B5K-kzy_H3BQ.js} +1 -1
  39. package/dist/client/assets/{chunk-2Q5K7J3B-YIEyVOOL.js → chunk-2Q5K7J3B-BMj-FT4X.js} +1 -1
  40. package/dist/client/assets/{chunk-5RXB4S5H-gjmGBtkE.js → chunk-5RXB4S5H-DmPGiCCb.js} +1 -1
  41. package/dist/client/assets/{chunk-5VM5RSS4-DCOQko4g.js → chunk-5VM5RSS4-DYFL8__L.js} +1 -1
  42. package/dist/client/assets/{chunk-6Q2QTUOP-DfGlvPVm.js → chunk-6Q2QTUOP-CI98oVdT.js} +1 -1
  43. package/dist/client/assets/{chunk-GF5L2VYU-CCWOLPVL.js → chunk-GF5L2VYU-D9JiiMlX.js} +1 -1
  44. package/dist/client/assets/{chunk-JWPE2WC7-CFRTGh8r.js → chunk-JWPE2WC7-D0THYpZl.js} +1 -1
  45. package/dist/client/assets/{chunk-KBJHAD2P-By3wsJVD.js → chunk-KBJHAD2P-Z1tJfyAr.js} +1 -1
  46. package/dist/client/assets/{chunk-RYQCIY6F-C0wmsHmy.js → chunk-RYQCIY6F-31aS4cD3.js} +1 -1
  47. package/dist/client/assets/{chunk-XXDRQBXY-t3SnJvV_.js → chunk-XXDRQBXY-ClWDHmEp.js} +1 -1
  48. package/dist/client/assets/classDiagram-JCYQIIEL-BDuo9kiS.js +1 -0
  49. package/dist/client/assets/classDiagram-v2-OCEON4UE-BDuo9kiS.js +1 -0
  50. package/dist/client/assets/{cose-bilkent-JH36ORCC-D3VltoVk.js → cose-bilkent-JH36ORCC-Btg2MYLh.js} +1 -1
  51. package/dist/client/assets/{cynefin-VYW2F7L2-DwLwbeoe.js → cynefin-VYW2F7L2-Cbh4Pk6j.js} +1 -1
  52. package/dist/client/assets/{cynefinDiagram-MW4NZA55-B23kuaQF.js → cynefinDiagram-MW4NZA55-BBqorl0u.js} +1 -1
  53. package/dist/client/assets/{dagre-VZM6K2ZE-SgMrTr2p.js → dagre-VZM6K2ZE-DugEItZ0.js} +1 -1
  54. package/dist/client/assets/{diagram-7IWD3JNH-C7cQN4aD.js → diagram-7IWD3JNH-Biu7dmH8.js} +1 -1
  55. package/dist/client/assets/{diagram-B4RE2ZJO-CzeI3yOw.js → diagram-B4RE2ZJO-UDm4DXFS.js} +1 -1
  56. package/dist/client/assets/{diagram-LBJQPF4R-DhKm4hlI.js → diagram-LBJQPF4R-Bcku4gb-.js} +1 -1
  57. package/dist/client/assets/{diagram-Q27KOJAE-BXlU6Wp3.js → diagram-Q27KOJAE-DFRjglV-.js} +1 -1
  58. package/dist/client/assets/{diagram-UB23O5K3-BOwY_YwR.js → diagram-UB23O5K3-CgfMWtd0.js} +1 -1
  59. package/dist/client/assets/{ebnfDiagram-BXEA7PRR-CuIQuYPa.js → ebnfDiagram-BXEA7PRR-CZzYOqs0.js} +1 -1
  60. package/dist/client/assets/{erDiagram-JOGREHBK-d45NhKfk.js → erDiagram-JOGREHBK-Cdlbxgs6.js} +1 -1
  61. package/dist/client/assets/{flowDiagram-UKHOOZJN-Dzq-6qo7.js → flowDiagram-UKHOOZJN-BgImDwpP.js} +1 -1
  62. package/dist/client/assets/{ganttDiagram-PKOTCBZU-RVlL9mX2.js → ganttDiagram-PKOTCBZU-SFhtiyL4.js} +1 -1
  63. package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-8kEJVUFH.js → gitGraphDiagram-DS77QQ5N-Bkm5ueys.js} +1 -1
  64. package/dist/client/assets/{index-CQcx9aRt.js → index-B5RC4THs.js} +4 -4
  65. package/dist/client/assets/index-Dc9xVluy.css +1 -0
  66. package/dist/client/assets/{infoDiagram-6WML65LV-Co21Ql9M.js → infoDiagram-6WML65LV-pCYc-VX2.js} +1 -1
  67. package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-BvPB_G97.js → ishikawaDiagram-WSZJBQD7-BtNbG3Y6.js} +1 -1
  68. package/dist/client/assets/{journeyDiagram-NVQOT4AX-k7x89IE9.js → journeyDiagram-NVQOT4AX-CPW2qT3y.js} +1 -1
  69. package/dist/client/assets/{kanban-definition-27J2QSJJ-BjrgN30t.js → kanban-definition-27J2QSJJ-wD6JXQFs.js} +1 -1
  70. package/dist/client/assets/{linear-m158kske.js → linear-sj8MOkmS.js} +1 -1
  71. package/dist/client/assets/{mermaid.core-C0FKucv-.js → mermaid.core-CGJtfmwb.js} +4 -4
  72. package/dist/client/assets/{mindmap-definition-FAOFIHXS-CF8noycK.js → mindmap-definition-FAOFIHXS-BQwLHBVs.js} +1 -1
  73. package/dist/client/assets/{pegDiagram-VL7TDLO6-_y7nfJFV.js → pegDiagram-VL7TDLO6-DtDsb-Z8.js} +1 -1
  74. package/dist/client/assets/{pieDiagram-7S7Q4E2Y-DhdWHi45.js → pieDiagram-7S7Q4E2Y-B3WhqNuS.js} +1 -1
  75. package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-BRvEIECQ.js → quadrantDiagram-CIZ2JOQS-CPi38pp_.js} +1 -1
  76. package/dist/client/assets/{railroadDiagram-AXF67PYL-BgLZ8XU1.js → railroadDiagram-AXF67PYL-Btac44Ux.js} +1 -1
  77. package/dist/client/assets/{render-markdown-2GjhuGA-.js → render-markdown-hAIWRfn0.js} +1 -1
  78. package/dist/client/assets/{requirementDiagram-LRYGKXZP-CX-gxyGF.js → requirementDiagram-LRYGKXZP-CHJgDNdg.js} +1 -1
  79. package/dist/client/assets/{sankeyDiagram-W5VNT64P-CLSjSU_X.js → sankeyDiagram-W5VNT64P-OYJHfmtd.js} +1 -1
  80. package/dist/client/assets/{sequenceDiagram-SI44F4Z6-dGlHyZRB.js → sequenceDiagram-SI44F4Z6-9ypa9W08.js} +1 -1
  81. package/dist/client/assets/{sizeCapture-X5ZJPWSS-BH3qO9T3.js → sizeCapture-X5ZJPWSS-5ZDUTERI.js} +1 -1
  82. package/dist/client/assets/{stateDiagram-OKZ733FA-FXxo1iIM.js → stateDiagram-OKZ733FA-nGHE1hN7.js} +1 -1
  83. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-CkCpE1Yu.js +1 -0
  84. package/dist/client/assets/{swimlanes-SLNWSIFB-VJGIuM4S.js → swimlanes-SLNWSIFB-BZFv9Zp8.js} +2 -2
  85. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-Dxlb7bju.js +8 -0
  86. package/dist/client/assets/tanstack-CcfAJoQ7.js +1 -0
  87. package/dist/client/assets/{timeline-definition-Z64GVDOM-BXNTAanY.js → timeline-definition-Z64GVDOM-pUIcaCIE.js} +1 -1
  88. package/dist/client/assets/{vennDiagram-T6HMQDX7-D9nXAts5.js → vennDiagram-T6HMQDX7-D6dEIJmP.js} +1 -1
  89. package/dist/client/assets/viewer-tabs-BjG3oKQ9.js +1 -0
  90. package/dist/client/assets/{wardleyDiagram-T6FBY63Y-D89EaEfa.js → wardleyDiagram-T6FBY63Y-DZ8ro_-y.js} +1 -1
  91. package/dist/client/assets/{xychartDiagram-ELKLHX3M-B_xFDiIh.js → xychartDiagram-ELKLHX3M-WJ2Zfmlk.js} +1 -1
  92. package/dist/client/index.html +4 -4
  93. package/dist/dev.mjs +2 -2
  94. package/dist/manifest.json +2 -2
  95. package/dist/server.mjs +2107 -565
  96. package/dist/utility.mjs +12 -6
  97. package/package.json +2 -2
  98. package/dist/client/assets/App-9EfSdZB8.js +0 -74
  99. package/dist/client/assets/CreatePullRequestDialog-BgVrWdkH.js +0 -2
  100. package/dist/client/assets/FilesView-rT5hzb7y.js +0 -1
  101. package/dist/client/assets/GitHubAvatar-EO1TD_1z.js +0 -1
  102. package/dist/client/assets/HistoryView-C4c0CqMC.js +0 -1
  103. package/dist/client/assets/IconFolderPlus-ByGyA0Di.js +0 -1
  104. package/dist/client/assets/LocalRefsDialog-D9wYZWBg.js +0 -1
  105. package/dist/client/assets/PairingPage-D9oguvKU.js +0 -1
  106. package/dist/client/assets/PullRequestDiff-DK-m07ZP.js +0 -1
  107. package/dist/client/assets/PullRequestViewer-CpLwq4o2.js +0 -2
  108. package/dist/client/assets/PullRequestsView-DnvXaJ4t.js +0 -1
  109. package/dist/client/assets/QuickOpenDialog-DIWjfUkt.js +0 -1
  110. package/dist/client/assets/RepositoryProjectsDialog-BsQn5Yjr.js +0 -1
  111. package/dist/client/assets/SearchView-D-91dtSv.js +0 -1
  112. package/dist/client/assets/SettingsDialog-dJUYk2cP.js +0 -9
  113. package/dist/client/assets/Viewer-GH-5bJW4.js +0 -3
  114. package/dist/client/assets/base-ui-DT3JsyND.js +0 -1
  115. package/dist/client/assets/channel-Dsy2VD23.js +0 -1
  116. package/dist/client/assets/checkbox-DYwmgDPS.js +0 -1
  117. package/dist/client/assets/classDiagram-JCYQIIEL-BBQJgoWd.js +0 -1
  118. package/dist/client/assets/classDiagram-v2-OCEON4UE-BBQJgoWd.js +0 -1
  119. package/dist/client/assets/gh-utils-LsOSydCg.js +0 -1
  120. package/dist/client/assets/index-DMbFAH51.css +0 -1
  121. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-DqxQ14yP.js +0 -1
  122. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-No0dCAkd.js +0 -8
  123. package/dist/client/assets/tanstack-CtfgE41X.js +0 -1
  124. package/dist/client/assets/viewer-tabs-DkLmDzFg.js +0 -1
package/dist/server.mjs CHANGED
@@ -1,17 +1,17 @@
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, createHash, createHmac, timingSafeEqual } from "node:crypto";
4
- import { access, mkdir, open, rename, chmod, rm, appendFile, readFile, mkdtemp, writeFile, lstat, readdir, realpath, stat, cp } from "node:fs/promises";
3
+ import { randomUUID, randomBytes, createHmac, createHash, timingSafeEqual } from "node:crypto";
4
+ import { stat, access, realpath, mkdir, open, rename, chmod, rm, appendFile, readFile, mkdtemp, writeFile, lstat, readdir, cp } from "node:fs/promises";
5
+ import os, { constants, homedir, networkInterfaces } from "node:os";
5
6
  import { ChildProcess, execFile, spawnSync, spawn } from "node:child_process";
6
7
  import { StringDecoder } from "node:string_decoder";
7
8
  import { debuglog, stripVTControlCharacters, inspect, promisify, callbackify, aborted as aborted$1 } from "node:util";
8
9
  import process$3, { platform, hrtime, execPath, execArgv } from "node:process";
9
10
  import tty from "node:tty";
10
11
  import { setTimeout as setTimeout$1, scheduler, setImmediate } from "node:timers/promises";
11
- import os, { constants, homedir, networkInterfaces } from "node:os";
12
12
  import { once, addAbortListener, EventEmitter, on, setMaxListeners } from "node:events";
13
13
  import { serialize } from "node:v8";
14
- import fs, { openSync, readSync, closeSync, statSync, readFileSync, appendFileSync, writeFileSync, createWriteStream, createReadStream, watch } from "node:fs";
14
+ import fs, { openSync, readSync, closeSync, statSync, readFileSync, appendFileSync, writeFileSync, createWriteStream, createReadStream, constants as constants$1, watch } from "node:fs";
15
15
  import { Buffer as Buffer$1 } from "node:buffer";
16
16
  import { Transform, getDefaultHighWaterMark, Duplex, Writable, Readable, PassThrough } from "node:stream";
17
17
  import { finished } from "node:stream/promises";
@@ -19,6 +19,9 @@ import path$1 from "node:path/win32";
19
19
  import { createServer, isIP } from "node:net";
20
20
  import { createServer as createServer$1 } from "node:http";
21
21
  import WebSocket, { WebSocketServer } from "ws";
22
+ function aiExecutablePathsKey(paths) {
23
+ return JSON.stringify(["codex", "claude", "opencode", "grok"].map((id) => paths[id] ?? ""));
24
+ }
22
25
  function isPlainObject$1(value) {
23
26
  if (typeof value !== "object" || value === null) {
24
27
  return false;
@@ -5709,7 +5712,7 @@ const createReadable = ({ subprocess, concurrentStreams, encoding }, { from, bin
5709
5712
  const binary = binaryOption || BINARY_ENCODINGS.has(encoding);
5710
5713
  const { subprocessStdout, waitReadableDestroy } = getSubprocessStdout(subprocess, from, concurrentStreams);
5711
5714
  const { readableEncoding, readableObjectMode, readableHighWaterMark } = getReadableOptions(subprocessStdout, binary);
5712
- const { read, onStdoutDataDone } = getReadableMethods({
5715
+ const { read: read2, onStdoutDataDone } = getReadableMethods({
5713
5716
  subprocessStdout,
5714
5717
  subprocess,
5715
5718
  binary,
@@ -5717,7 +5720,7 @@ const createReadable = ({ subprocess, concurrentStreams, encoding }, { from, bin
5717
5720
  preserveNewlines
5718
5721
  });
5719
5722
  const readable2 = new Readable({
5720
- read,
5723
+ read: read2,
5721
5724
  destroy: callbackify(onReadableDestroy.bind(void 0, { subprocessStdout, subprocess, waitReadableDestroy })),
5722
5725
  highWaterMark: readableHighWaterMark,
5723
5726
  objectMode: readableObjectMode,
@@ -6555,7 +6558,7 @@ const createDuplex = ({ subprocess, concurrentStreams, encoding }, { from, to, b
6555
6558
  const { subprocessStdout, waitReadableDestroy } = getSubprocessStdout(subprocess, from, concurrentStreams);
6556
6559
  const { subprocessStdin, waitWritableFinal, waitWritableDestroy } = getSubprocessStdin(subprocess, to, concurrentStreams);
6557
6560
  const { readableEncoding, readableObjectMode, readableHighWaterMark } = getReadableOptions(subprocessStdout, binary);
6558
- const { read, onStdoutDataDone } = getReadableMethods({
6561
+ const { read: read2, onStdoutDataDone } = getReadableMethods({
6559
6562
  subprocessStdout,
6560
6563
  subprocess,
6561
6564
  binary,
@@ -6563,7 +6566,7 @@ const createDuplex = ({ subprocess, concurrentStreams, encoding }, { from, to, b
6563
6566
  preserveNewlines
6564
6567
  });
6565
6568
  const duplex2 = new Duplex({
6566
- read,
6569
+ read: read2,
6567
6570
  ...getWritableMethods(subprocessStdin, subprocess, waitWritableFinal),
6568
6571
  destroy: callbackify(onDuplexDestroy.bind(void 0, {
6569
6572
  subprocessStdout,
@@ -6872,6 +6875,154 @@ createExeca(() => ({ isSync: true }));
6872
6875
  createExeca(mapNode);
6873
6876
  createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
6874
6877
  getIpcExport();
6878
+ const LOCATION_KEYS = ["PATH", "CODEX_INSTALL_DIR", "GROK_BIN_DIR", "BUN_INSTALL", "PNPM_HOME", "VOLTA_HOME", "NPM_CONFIG_PREFIX", "npm_config_prefix", "NVM_BIN", "FNM_MULTISHELL_PATH", "ASDF_DATA_DIR", "MISE_DATA_DIR", "XDG_DATA_HOME", "APPDATA", "LOCALAPPDATA"];
6879
+ function currentCliHost() {
6880
+ let shell;
6881
+ try {
6882
+ shell = os.userInfo().shell ?? void 0;
6883
+ } catch {
6884
+ }
6885
+ return { platform: process.platform, home: os.homedir(), env: cleanEnvironment(process.env), shell: process.env.SHELL || shell };
6886
+ }
6887
+ function cleanEnvironment(input, platform2 = process.platform) {
6888
+ const result = {};
6889
+ for (const [key, value] of Object.entries(input)) {
6890
+ if (value !== void 0) result[platform2 === "win32" && key.toUpperCase() === "PATH" ? "PATH" : key] = value;
6891
+ }
6892
+ return result;
6893
+ }
6894
+ function pathDirectories(value, platform2) {
6895
+ const p = platform2 === "win32" ? path.win32 : path.posix;
6896
+ const seen = /* @__PURE__ */ new Set();
6897
+ return (value ?? "").split(platform2 === "win32" ? ";" : ":").map((item) => item.replace(/^"(.*)"$/, "$1")).filter((item) => {
6898
+ if (!p.isAbsolute(item)) return false;
6899
+ const key = platform2 === "win32" ? p.normalize(item).toLowerCase() : p.normalize(item);
6900
+ if (seen.has(key)) return false;
6901
+ seen.add(key);
6902
+ return true;
6903
+ });
6904
+ }
6905
+ class CliEnvironment {
6906
+ constructor(host = currentCliHost(), read2 = readUserEnvironment) {
6907
+ this.host = host;
6908
+ this.read = read2;
6909
+ }
6910
+ host;
6911
+ read;
6912
+ cached;
6913
+ pending;
6914
+ generation = 0;
6915
+ invalidate() {
6916
+ this.generation++;
6917
+ this.cached = void 0;
6918
+ this.pending = void 0;
6919
+ }
6920
+ async get(force = false) {
6921
+ const env = cleanEnvironment(this.host.env, this.host.platform);
6922
+ const inheritedPath = pathDirectories(env.PATH, this.host.platform);
6923
+ if (this.pending) return this.pending;
6924
+ if (!force && this.cached && Date.now() - this.cached.at < 3e4) return this.cached.value;
6925
+ const generation = this.generation;
6926
+ const pending = (async () => {
6927
+ let locations = {};
6928
+ let warning;
6929
+ try {
6930
+ locations = await this.read(this.host);
6931
+ } catch {
6932
+ warning = "Could not refresh the user environment. Using the inherited environment and known installation locations. Check your shell configuration or set an executable path.";
6933
+ }
6934
+ const refreshedPath = pathDirectories(locations.PATH, this.host.platform);
6935
+ const value = { env: { ...env, ...locations }, inheritedPath, refreshedPath, ...warning ? { warning } : {} };
6936
+ if (generation === this.generation) this.cached = { at: Date.now(), value };
6937
+ return value;
6938
+ })();
6939
+ this.pending = pending;
6940
+ try {
6941
+ return await pending;
6942
+ } finally {
6943
+ if (this.pending === pending) this.pending = void 0;
6944
+ }
6945
+ }
6946
+ }
6947
+ async function readUserEnvironment(host) {
6948
+ const options = {
6949
+ cwd: host.home,
6950
+ env: host.env,
6951
+ extendEnv: false,
6952
+ input: "",
6953
+ timeout: host.platform === "win32" ? 15e3 : 5e3,
6954
+ maxBuffer: 128 * 1024,
6955
+ cleanup: true,
6956
+ killDescendants: true,
6957
+ windowsHide: true,
6958
+ reject: false,
6959
+ stripFinalNewline: false
6960
+ };
6961
+ if (host.platform === "win32") {
6962
+ const root = host.env.SystemRoot || host.env.SYSTEMROOT;
6963
+ if (!root || !path.win32.isAbsolute(root)) throw new Error("SystemRoot unavailable");
6964
+ const shell2 = path.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
6965
+ const script = '$ErrorActionPreference="Stop"; $r=@{}; foreach($scope in @("Machine","User")){ $r[$scope]=[Environment]::GetEnvironmentVariables($scope) }; ConvertTo-Json -InputObject $r -Compress';
6966
+ const result = await execa(shell2, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], options);
6967
+ if (result.timedOut) throw new Error("Environment query timed out");
6968
+ if (result.exitCode !== 0 || result.isMaxBuffer) throw new Error("Environment query failed");
6969
+ return windowsLocations(JSON.parse(result.stdout), host.env);
6970
+ }
6971
+ const shell = host.shell;
6972
+ if (!shell || !path.posix.isAbsolute(shell)) throw new Error("User shell unavailable");
6973
+ const kind = path.posix.basename(shell);
6974
+ if (!["bash", "zsh", "fish"].includes(kind)) throw new Error("Unsupported user shell");
6975
+ const started = Date.now();
6976
+ const modes = kind === "bash" ? ["-lic", "-ic"] : ["-lic"];
6977
+ let merged = {};
6978
+ let succeeded = false;
6979
+ for (const mode of modes) {
6980
+ const timeout = 5e3 - (Date.now() - started);
6981
+ if (timeout <= 0) break;
6982
+ const script = LOCATION_KEYS.map((key) => kind === "fish" ? `printf '\\0OPENTIG_ENV_${key}\\0%s\\0' (string join ':' -- $${key})` : `printf '\\0OPENTIG_ENV_${key}\\0%s\\0' "$${key}"`).join("; ");
6983
+ const result = await execa(shell, [mode, script], { ...options, timeout });
6984
+ if (result.exitCode !== 0 || result.timedOut || result.isMaxBuffer) continue;
6985
+ const locations = parseShellLocations(result.stdout);
6986
+ if (!locations.PATH) continue;
6987
+ succeeded = true;
6988
+ merged = { ...merged, ...locations, PATH: [locations.PATH, merged.PATH].filter(Boolean).join(":") };
6989
+ }
6990
+ if (!succeeded) throw new Error("Shell environment query failed");
6991
+ return merged;
6992
+ }
6993
+ function parseShellLocations(output) {
6994
+ const locations = {};
6995
+ for (const key of LOCATION_KEYS) {
6996
+ const marker = `\0OPENTIG_ENV_${key}\0`;
6997
+ const start = output.lastIndexOf(marker);
6998
+ if (start < 0) continue;
6999
+ const end = output.indexOf("\0", start + marker.length);
7000
+ if (end < 0) continue;
7001
+ const value = output.slice(start + marker.length, end);
7002
+ if (value && !/[\r\n]/.test(value)) locations[key] = value;
7003
+ }
7004
+ return locations;
7005
+ }
7006
+ function windowsLocations(raw, inherited) {
7007
+ const data = raw;
7008
+ if (!data || typeof data !== "object" || !data.Machine || !data.User) throw new Error("Invalid environment response");
7009
+ const upper = (input) => Object.fromEntries(Object.entries(input).filter(([, v]) => typeof v === "string").map(([k, v]) => [k.toUpperCase(), v]));
7010
+ const machine = upper(data.Machine), user = upper(data.User);
7011
+ const variables = { ...upper(inherited), ...machine, ...user };
7012
+ const expand = (input) => {
7013
+ let value = input;
7014
+ for (let i2 = 0; i2 < 10; i2++) {
7015
+ const next = value.replace(/%([^%]+)%/g, (match, key) => variables[key.toUpperCase()] ?? match);
7016
+ if (next === value) break;
7017
+ value = next;
7018
+ }
7019
+ return value;
7020
+ };
7021
+ const result = {};
7022
+ for (const key of LOCATION_KEYS) if (key !== "PATH" && variables[key.toUpperCase()]) result[key] = expand(variables[key.toUpperCase()]);
7023
+ result.PATH = [machine.PATH, user.PATH].filter((value) => !!value).map(expand).join(";");
7024
+ return result;
7025
+ }
6875
7026
  class GitOperationError extends Error {
6876
7027
  detail;
6877
7028
  constructor(detail) {
@@ -6939,17 +7090,18 @@ class CliProcessRunner {
6939
7090
  async run(command2, args, options = {}) {
6940
7091
  const timeoutMs = options.timeoutMs ?? 15e3;
6941
7092
  const maxOutputBytes = options.maxOutputBytes ?? 2 * 1024 * 1024;
6942
- const environment = Object.fromEntries(
7093
+ const environment = cleanEnvironment(Object.fromEntries(
6943
7094
  Object.entries({ ...process.env, ...options.env }).filter((entry) => entry[1] !== void 0)
6944
- );
7095
+ ));
6945
7096
  for (const key of options.removeEnv ?? []) delete environment[key];
6946
7097
  if (this.closePromise || options.signal?.aborted) throw cancelled();
6947
7098
  const lifecycle = new AbortController();
6948
7099
  this.active.add(lifecycle);
6949
7100
  const signal = options.signal ? AbortSignal.any([options.signal, lifecycle.signal]) : lifecycle.signal;
6950
- const resolvedCommand = resolveProcessCommand(command2, options.cwd ?? process.cwd(), environment);
7101
+ let resolvedCommand;
6951
7102
  let result;
6952
7103
  try {
7104
+ resolvedCommand = resolveProcessCommand(command2, options.cwd ?? process.cwd(), environment);
6953
7105
  result = await execa(resolvedCommand.file, args, {
6954
7106
  ...options.cwd ? { cwd: options.cwd } : {},
6955
7107
  cancelSignal: signal,
@@ -6964,9 +7116,9 @@ class CliProcessRunner {
6964
7116
  input: options.stdin ?? "",
6965
7117
  stripFinalNewline: false,
6966
7118
  reject: false
6967
- }).catch(() => {
7119
+ }).catch((error) => {
6968
7120
  if (signal.aborted) throw cancelled();
6969
- throw processFailed();
7121
+ throw processFailed(error);
6970
7122
  });
6971
7123
  } finally {
6972
7124
  this.active.delete(lifecycle);
@@ -6983,7 +7135,7 @@ class CliProcessRunner {
6983
7135
  throw new AiOperationError({ code: "AI_CONTEXT_TOO_LARGE", operation: "ai-process", message: "The AI tool produced too much output." });
6984
7136
  }
6985
7137
  if (result.exitCode === void 0 || !resolvedCommand.found) {
6986
- throw processFailed();
7138
+ throw processFailed(result);
6987
7139
  }
6988
7140
  return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
6989
7141
  }
@@ -7008,49 +7160,143 @@ class CliProcessRunner {
7008
7160
  function cancelled() {
7009
7161
  return new AiOperationError({ code: "AI_CANCELLED", operation: "ai-process", message: "Generation canceled." });
7010
7162
  }
7011
- function processFailed() {
7012
- return new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "ai-process", message: "Could not start the AI tool." });
7163
+ class CliLaunchError extends AiOperationError {
7164
+ constructor(reason) {
7165
+ super({ code: "AI_PROCESS_FAILED", operation: "ai-process", message: "Could not start the AI tool." });
7166
+ this.reason = reason;
7167
+ }
7168
+ reason;
7169
+ }
7170
+ function processFailed(error) {
7171
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
7172
+ return new CliLaunchError(code === "ENOENT" ? "missing" : code === "EACCES" || code === "EPERM" ? "permission" : code === "ENOEXEC" ? "format" : "unknown");
7013
7173
  }
7014
- const ALLOWED = /* @__PURE__ */ new Set(["codex", "claude", "opencode", "grok", "gh"]);
7015
7174
  const ALIASES = {
7016
7175
  codex: ["codex"],
7017
7176
  claude: ["claude"],
7018
- // Current v2 uses opencode; older distributions also provide opencode2.
7019
7177
  opencode: ["opencode", "opencode2"],
7020
7178
  grok: ["grok"],
7021
7179
  gh: ["gh"]
7022
7180
  };
7023
7181
  class CliResolver {
7182
+ constructor(environment = new CliEnvironment(), preferences = () => ({ aiExecutablePaths: {} })) {
7183
+ this.environment = environment;
7184
+ this.preferences = preferences;
7185
+ }
7186
+ environment;
7187
+ preferences;
7024
7188
  cache = /* @__PURE__ */ new Map();
7189
+ settingsKey = "";
7190
+ generation = 0;
7191
+ invalidate() {
7192
+ this.generation++;
7193
+ this.cache.clear();
7194
+ this.environment.invalidate();
7195
+ }
7025
7196
  async resolve(name, forceRefresh = false) {
7026
7197
  return (await this.resolveAll(name, forceRefresh))[0] ?? null;
7027
7198
  }
7028
7199
  async resolveAll(name, forceRefresh = false) {
7029
- if (!ALLOWED.has(name)) return [];
7030
- if (!forceRefresh && this.cache.has(name)) return [...this.cache.get(name)];
7031
- const resolved = [];
7032
- for (const alias of ALIASES[name] ?? [name]) {
7033
- const executable = await findOnPath(alias);
7034
- if (executable && !resolved.includes(executable)) resolved.push(executable);
7035
- }
7036
- this.cache.set(name, resolved);
7037
- return [...resolved];
7038
- }
7039
- }
7040
- async function findOnPath(name) {
7041
- const directories = (process.env.PATH ?? "").split(path.delimiter).filter(Boolean);
7042
- const extensions = process.platform === "win32" ? [".exe", ".cmd", ".bat"] : [""];
7043
- for (const extension of extensions) {
7044
- for (const directory of directories) {
7045
- const candidate = path.resolve(directory.replace(/^"|"$/g, ""), `${name}${extension}`);
7046
- try {
7047
- await access(candidate);
7048
- return candidate;
7049
- } catch {
7200
+ return (await this.discover(name, forceRefresh)).filter((item) => !item.problem).map((item) => item.executable);
7201
+ }
7202
+ async discover(name, forceRefresh = false) {
7203
+ if (!Object.hasOwn(ALIASES, name)) return [];
7204
+ const preferences = this.preferences();
7205
+ const settingsKey = JSON.stringify(preferences.aiExecutablePaths);
7206
+ if (settingsKey !== this.settingsKey) {
7207
+ this.settingsKey = settingsKey;
7208
+ this.invalidate();
7209
+ }
7210
+ const key = name;
7211
+ if (forceRefresh) {
7212
+ this.generation++;
7213
+ this.cache.delete(key);
7214
+ }
7215
+ const generation = this.generation;
7216
+ const cached2 = this.cache.get(key);
7217
+ if (!forceRefresh && cached2 && Date.now() - cached2.at < 3e4) {
7218
+ if ((await Promise.all(cached2.candidates.map((item) => this.problem(item.executable)))).every((problem, index) => problem === cached2.candidates[index]?.problem)) {
7219
+ return structuredClone(cached2.candidates);
7220
+ }
7221
+ }
7222
+ const snapshot = await this.environment.get(forceRefresh);
7223
+ const host = this.environment.host;
7224
+ const p = host.platform === "win32" ? path.win32 : path.posix;
7225
+ const known = knownCliDirectories(name, host.home, host.platform, snapshot.env);
7226
+ const allKnown = [...new Set(Object.keys(ALIASES).flatMap((id) => knownCliDirectories(id, host.home, host.platform, snapshot.env)))];
7227
+ const runtimePath = host.platform === "win32" ? [...snapshot.inheritedPath, ...snapshot.refreshedPath] : [...snapshot.refreshedPath, ...snapshot.inheritedPath];
7228
+ const env = { ...snapshot.env, PATH: pathDirectories([...runtimePath, ...allKnown].join(p.delimiter), host.platform).join(p.delimiter) };
7229
+ if (host.env.XDG_DATA_HOME === void 0) delete env.XDG_DATA_HOME;
7230
+ else env.XDG_DATA_HOME = host.env.XDG_DATA_HOME;
7231
+ const configured = name === "gh" ? void 0 : preferences.aiExecutablePaths[name];
7232
+ const candidates = [];
7233
+ const seen = /* @__PURE__ */ new Set();
7234
+ const add = async (executable, alias, source) => {
7235
+ const problem = await this.problem(executable);
7236
+ if (problem === "not-found" && source !== "configured") return;
7237
+ const canonical = await realpath(executable).catch(() => executable);
7238
+ const identity2 = host.platform === "win32" ? canonical.toLowerCase() : canonical;
7239
+ if (seen.has(identity2)) return;
7240
+ seen.add(identity2);
7241
+ candidates.push({ executable, alias, source, env, ...problem ? { problem } : {}, ...snapshot.warning ? { warning: snapshot.warning } : {} });
7242
+ };
7243
+ if (configured) {
7244
+ await add(configured, p.basename(configured), "configured");
7245
+ } else {
7246
+ for (const [directories, source] of [[snapshot.inheritedPath, "process-path"], [snapshot.refreshedPath, "user-path"], [known, "known-location"]]) {
7247
+ for (const directory of directories) {
7248
+ for (const alias of ALIASES[name]) {
7249
+ for (const extension of executableExtensions(host.platform, snapshot.env)) await add(p.join(directory, `${alias}${extension}`), alias, source);
7250
+ }
7251
+ }
7050
7252
  }
7051
7253
  }
7254
+ if (generation === this.generation) this.cache.set(key, { at: Date.now(), candidates });
7255
+ return structuredClone(candidates);
7052
7256
  }
7053
- return null;
7257
+ async warning() {
7258
+ return (await this.environment.get()).warning;
7259
+ }
7260
+ async problem(executable) {
7261
+ try {
7262
+ if (!(await stat(executable)).isFile()) return "not-executable";
7263
+ if (this.environment.host.platform === "win32") {
7264
+ if (![".exe", ".com", ".cmd", ".bat"].includes(path.win32.extname(executable).toLowerCase())) return "not-executable";
7265
+ } else await access(executable, constants$1.X_OK);
7266
+ return void 0;
7267
+ } catch (error) {
7268
+ return error.code === "EACCES" ? "not-executable" : "not-found";
7269
+ }
7270
+ }
7271
+ }
7272
+ function executableExtensions(platform2, env) {
7273
+ if (platform2 !== "win32") return [""];
7274
+ const value = Object.entries(env).find(([key]) => key.toUpperCase() === "PATHEXT")?.[1] ?? ".COM;.EXE;.BAT;.CMD";
7275
+ return [...new Set(value.split(";").map((item) => item.toLowerCase()).filter((item) => [".exe", ".com", ".cmd", ".bat"].includes(item)))];
7276
+ }
7277
+ function knownCliDirectories(name, home, platform2, env) {
7278
+ const p = platform2 === "win32" ? path.win32 : path.posix;
7279
+ const dirs = [p.join(home, ".local", "bin")];
7280
+ if (name === "opencode") dirs.push(p.join(home, ".opencode", "bin"));
7281
+ if (name === "grok") dirs.push(env.GROK_BIN_DIR, p.join(home, ".grok", "bin"));
7282
+ if (name === "codex") dirs.push(env.CODEX_INSTALL_DIR);
7283
+ dirs.push(env.PNPM_HOME, env.NVM_BIN, p.join(env.BUN_INSTALL || p.join(home, ".bun"), "bin"));
7284
+ if (env.FNM_MULTISHELL_PATH) dirs.push(platform2 === "win32" ? env.FNM_MULTISHELL_PATH : p.join(env.FNM_MULTISHELL_PATH, "bin"));
7285
+ const prefix = env.npm_config_prefix || env.NPM_CONFIG_PREFIX;
7286
+ if (prefix) dirs.push(platform2 === "win32" ? prefix : p.join(prefix, "bin"));
7287
+ if (platform2 === "win32") {
7288
+ if (env.APPDATA) dirs.push(p.join(env.APPDATA, "npm"));
7289
+ if (env.LOCALAPPDATA) {
7290
+ dirs.push(p.join(env.LOCALAPPDATA, "Volta", "bin"), p.join(env.LOCALAPPDATA, "pnpm"), p.join(env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"));
7291
+ if (name === "codex") dirs.push(p.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin"));
7292
+ }
7293
+ if (env.VOLTA_HOME) dirs.push(p.join(env.VOLTA_HOME, "bin"));
7294
+ } else {
7295
+ dirs.push(p.join(env.VOLTA_HOME || p.join(home, ".volta"), "bin"), p.join(home, ".npm-global", "bin"), p.join(home, ".yarn", "bin"));
7296
+ dirs.push(p.join(env.XDG_DATA_HOME || p.join(home, ".local", "share"), "pnpm"));
7297
+ dirs.push("/usr/local/bin", "/usr/bin", "/bin", platform2 === "darwin" ? "/opt/homebrew/bin" : "/home/linuxbrew/.linuxbrew/bin");
7298
+ }
7299
+ return [...new Set(dirs.filter((item) => !!item && p.isAbsolute(item)))];
7054
7300
  }
7055
7301
  var _a$1;
7056
7302
  function $constructor(name, initializer2, params) {
@@ -7287,14 +7533,14 @@ const NUMBER_FORMAT_RANGES = {
7287
7533
  float32: [-34028234663852886e22, 34028234663852886e22],
7288
7534
  float64: [-Number.MAX_VALUE, Number.MAX_VALUE]
7289
7535
  };
7290
- function pick(schema, mask) {
7291
- const currDef = schema._zod.def;
7536
+ function pick(schema2, mask) {
7537
+ const currDef = schema2._zod.def;
7292
7538
  const checks = currDef.checks;
7293
7539
  const hasChecks = checks && checks.length > 0;
7294
7540
  if (hasChecks) {
7295
7541
  throw new Error(".pick() cannot be used on object schemas containing refinements");
7296
7542
  }
7297
- const def = mergeDefs(schema._zod.def, {
7543
+ const def = mergeDefs(schema2._zod.def, {
7298
7544
  get shape() {
7299
7545
  const newShape = {};
7300
7546
  for (const key in mask) {
@@ -7310,18 +7556,18 @@ function pick(schema, mask) {
7310
7556
  },
7311
7557
  checks: []
7312
7558
  });
7313
- return clone(schema, def);
7559
+ return clone(schema2, def);
7314
7560
  }
7315
- function omit(schema, mask) {
7316
- const currDef = schema._zod.def;
7561
+ function omit(schema2, mask) {
7562
+ const currDef = schema2._zod.def;
7317
7563
  const checks = currDef.checks;
7318
7564
  const hasChecks = checks && checks.length > 0;
7319
7565
  if (hasChecks) {
7320
7566
  throw new Error(".omit() cannot be used on object schemas containing refinements");
7321
7567
  }
7322
- const def = mergeDefs(schema._zod.def, {
7568
+ const def = mergeDefs(schema2._zod.def, {
7323
7569
  get shape() {
7324
- const newShape = { ...schema._zod.def.shape };
7570
+ const newShape = { ...schema2._zod.def.shape };
7325
7571
  for (const key in mask) {
7326
7572
  if (!(key in currDef.shape)) {
7327
7573
  throw new Error(`Unrecognized key: "${key}"`);
@@ -7335,43 +7581,43 @@ function omit(schema, mask) {
7335
7581
  },
7336
7582
  checks: []
7337
7583
  });
7338
- return clone(schema, def);
7584
+ return clone(schema2, def);
7339
7585
  }
7340
- function extend(schema, shape) {
7586
+ function extend(schema2, shape) {
7341
7587
  if (!isPlainObject(shape)) {
7342
7588
  throw new Error("Invalid input to extend: expected a plain object");
7343
7589
  }
7344
- const checks = schema._zod.def.checks;
7590
+ const checks = schema2._zod.def.checks;
7345
7591
  const hasChecks = checks && checks.length > 0;
7346
7592
  if (hasChecks) {
7347
- const existingShape = schema._zod.def.shape;
7593
+ const existingShape = schema2._zod.def.shape;
7348
7594
  for (const key in shape) {
7349
7595
  if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) {
7350
7596
  throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
7351
7597
  }
7352
7598
  }
7353
7599
  }
7354
- const def = mergeDefs(schema._zod.def, {
7600
+ const def = mergeDefs(schema2._zod.def, {
7355
7601
  get shape() {
7356
- const _shape = { ...schema._zod.def.shape, ...shape };
7602
+ const _shape = { ...schema2._zod.def.shape, ...shape };
7357
7603
  assignProp(this, "shape", _shape);
7358
7604
  return _shape;
7359
7605
  }
7360
7606
  });
7361
- return clone(schema, def);
7607
+ return clone(schema2, def);
7362
7608
  }
7363
- function safeExtend(schema, shape) {
7609
+ function safeExtend(schema2, shape) {
7364
7610
  if (!isPlainObject(shape)) {
7365
7611
  throw new Error("Invalid input to safeExtend: expected a plain object");
7366
7612
  }
7367
- const def = mergeDefs(schema._zod.def, {
7613
+ const def = mergeDefs(schema2._zod.def, {
7368
7614
  get shape() {
7369
- const _shape = { ...schema._zod.def.shape, ...shape };
7615
+ const _shape = { ...schema2._zod.def.shape, ...shape };
7370
7616
  assignProp(this, "shape", _shape);
7371
7617
  return _shape;
7372
7618
  }
7373
7619
  });
7374
- return clone(schema, def);
7620
+ return clone(schema2, def);
7375
7621
  }
7376
7622
  function merge(a2, b) {
7377
7623
  if (a2._zod.def.checks?.length) {
@@ -7390,16 +7636,16 @@ function merge(a2, b) {
7390
7636
  });
7391
7637
  return clone(a2, def);
7392
7638
  }
7393
- function partial(Class, schema, mask) {
7394
- const currDef = schema._zod.def;
7639
+ function partial(Class, schema2, mask) {
7640
+ const currDef = schema2._zod.def;
7395
7641
  const checks = currDef.checks;
7396
7642
  const hasChecks = checks && checks.length > 0;
7397
7643
  if (hasChecks) {
7398
7644
  throw new Error(".partial() cannot be used on object schemas containing refinements");
7399
7645
  }
7400
- const def = mergeDefs(schema._zod.def, {
7646
+ const def = mergeDefs(schema2._zod.def, {
7401
7647
  get shape() {
7402
- const oldShape = schema._zod.def.shape;
7648
+ const oldShape = schema2._zod.def.shape;
7403
7649
  const shape = { ...oldShape };
7404
7650
  if (mask) {
7405
7651
  for (const key in mask) {
@@ -7426,12 +7672,12 @@ function partial(Class, schema, mask) {
7426
7672
  },
7427
7673
  checks: []
7428
7674
  });
7429
- return clone(schema, def);
7675
+ return clone(schema2, def);
7430
7676
  }
7431
- function required(Class, schema, mask) {
7432
- const def = mergeDefs(schema._zod.def, {
7677
+ function required(Class, schema2, mask) {
7678
+ const def = mergeDefs(schema2._zod.def, {
7433
7679
  get shape() {
7434
- const oldShape = schema._zod.def.shape;
7680
+ const oldShape = schema2._zod.def.shape;
7435
7681
  const shape = { ...oldShape };
7436
7682
  if (mask) {
7437
7683
  for (const key in mask) {
@@ -7457,7 +7703,7 @@ function required(Class, schema, mask) {
7457
7703
  return shape;
7458
7704
  }
7459
7705
  });
7460
- return clone(schema, def);
7706
+ return clone(schema2, def);
7461
7707
  }
7462
7708
  function aborted(x, startIndex = 0) {
7463
7709
  if (x.aborted === true)
@@ -7586,9 +7832,9 @@ function formatError(error, mapper = (issue2) => issue2.message) {
7586
7832
  processError(error);
7587
7833
  return fieldErrors;
7588
7834
  }
7589
- const _parse = (_Err) => (schema, value, _ctx, _params) => {
7835
+ const _parse = (_Err) => (schema2, value, _ctx, _params) => {
7590
7836
  const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
7591
- const result = schema._zod.run({ value, issues: [] }, ctx);
7837
+ const result = schema2._zod.run({ value, issues: [] }, ctx);
7592
7838
  if (result instanceof Promise) {
7593
7839
  throw new $ZodAsyncError();
7594
7840
  }
@@ -7599,9 +7845,9 @@ const _parse = (_Err) => (schema, value, _ctx, _params) => {
7599
7845
  }
7600
7846
  return result.value;
7601
7847
  };
7602
- const _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
7848
+ const _parseAsync = (_Err) => async (schema2, value, _ctx, params) => {
7603
7849
  const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
7604
- let result = schema._zod.run({ value, issues: [] }, ctx);
7850
+ let result = schema2._zod.run({ value, issues: [] }, ctx);
7605
7851
  if (result instanceof Promise)
7606
7852
  result = await result;
7607
7853
  if (result.issues.length) {
@@ -7611,9 +7857,9 @@ const _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
7611
7857
  }
7612
7858
  return result.value;
7613
7859
  };
7614
- const _safeParse = (_Err) => (schema, value, _ctx) => {
7860
+ const _safeParse = (_Err) => (schema2, value, _ctx) => {
7615
7861
  const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
7616
- const result = schema._zod.run({ value, issues: [] }, ctx);
7862
+ const result = schema2._zod.run({ value, issues: [] }, ctx);
7617
7863
  if (result instanceof Promise) {
7618
7864
  throw new $ZodAsyncError();
7619
7865
  }
@@ -7623,9 +7869,9 @@ const _safeParse = (_Err) => (schema, value, _ctx) => {
7623
7869
  } : { success: true, data: result.value };
7624
7870
  };
7625
7871
  const safeParse$1 = /* @__PURE__ */ _safeParse($ZodRealError);
7626
- const _safeParseAsync = (_Err) => async (schema, value, _ctx) => {
7872
+ const _safeParseAsync = (_Err) => async (schema2, value, _ctx) => {
7627
7873
  const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
7628
- let result = schema._zod.run({ value, issues: [] }, ctx);
7874
+ let result = schema2._zod.run({ value, issues: [] }, ctx);
7629
7875
  if (result instanceof Promise)
7630
7876
  result = await result;
7631
7877
  return result.issues.length ? {
@@ -7634,33 +7880,33 @@ const _safeParseAsync = (_Err) => async (schema, value, _ctx) => {
7634
7880
  } : { success: true, data: result.value };
7635
7881
  };
7636
7882
  const safeParseAsync$1 = /* @__PURE__ */ _safeParseAsync($ZodRealError);
7637
- const _encode = (_Err) => (schema, value, _ctx) => {
7883
+ const _encode = (_Err) => (schema2, value, _ctx) => {
7638
7884
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
7639
- return _parse(_Err)(schema, value, ctx);
7885
+ return _parse(_Err)(schema2, value, ctx);
7640
7886
  };
7641
- const _decode = (_Err) => (schema, value, _ctx) => {
7642
- return _parse(_Err)(schema, value, _ctx);
7887
+ const _decode = (_Err) => (schema2, value, _ctx) => {
7888
+ return _parse(_Err)(schema2, value, _ctx);
7643
7889
  };
7644
- const _encodeAsync = (_Err) => async (schema, value, _ctx) => {
7890
+ const _encodeAsync = (_Err) => async (schema2, value, _ctx) => {
7645
7891
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
7646
- return _parseAsync(_Err)(schema, value, ctx);
7892
+ return _parseAsync(_Err)(schema2, value, ctx);
7647
7893
  };
7648
- const _decodeAsync = (_Err) => async (schema, value, _ctx) => {
7649
- return _parseAsync(_Err)(schema, value, _ctx);
7894
+ const _decodeAsync = (_Err) => async (schema2, value, _ctx) => {
7895
+ return _parseAsync(_Err)(schema2, value, _ctx);
7650
7896
  };
7651
- const _safeEncode = (_Err) => (schema, value, _ctx) => {
7897
+ const _safeEncode = (_Err) => (schema2, value, _ctx) => {
7652
7898
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
7653
- return _safeParse(_Err)(schema, value, ctx);
7899
+ return _safeParse(_Err)(schema2, value, ctx);
7654
7900
  };
7655
- const _safeDecode = (_Err) => (schema, value, _ctx) => {
7656
- return _safeParse(_Err)(schema, value, _ctx);
7901
+ const _safeDecode = (_Err) => (schema2, value, _ctx) => {
7902
+ return _safeParse(_Err)(schema2, value, _ctx);
7657
7903
  };
7658
- const _safeEncodeAsync = (_Err) => async (schema, value, _ctx) => {
7904
+ const _safeEncodeAsync = (_Err) => async (schema2, value, _ctx) => {
7659
7905
  const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
7660
- return _safeParseAsync(_Err)(schema, value, ctx);
7906
+ return _safeParseAsync(_Err)(schema2, value, ctx);
7661
7907
  };
7662
- const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
7663
- return _safeParseAsync(_Err)(schema, value, _ctx);
7908
+ const _safeDecodeAsync = (_Err) => async (schema2, value, _ctx) => {
7909
+ return _safeParseAsync(_Err)(schema2, value, _ctx);
7664
7910
  };
7665
7911
  const cuid = /^[cC][0-9a-z]{6,}$/;
7666
7912
  const cuid2 = /^[0-9a-z]+$/;
@@ -8830,9 +9076,9 @@ const $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def)
8830
9076
  for (const key of normalized.keys) {
8831
9077
  const id = ids[key];
8832
9078
  const k = esc(key);
8833
- const schema = shape[key];
8834
- const isOptionalIn = schema?._zod?.optin === "optional";
8835
- const isOptionalOut = schema?._zod?.optout === "optional";
9079
+ const schema2 = shape[key];
9080
+ const isOptionalIn = schema2?._zod?.optin === "optional";
9081
+ const isOptionalOut = schema2?._zod?.optout === "optional";
8836
9082
  doc.write(`const ${id} = ${parseStr(key)};`);
8837
9083
  if (isOptionalIn && isOptionalOut) {
8838
9084
  doc.write(`
@@ -9001,6 +9247,73 @@ const $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
9001
9247
  });
9002
9248
  };
9003
9249
  });
9250
+ const $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
9251
+ def.inclusive = false;
9252
+ $ZodUnion.init(inst, def);
9253
+ const _super = inst._zod.parse;
9254
+ defineLazy(inst._zod, "propValues", () => {
9255
+ const propValues = {};
9256
+ for (const option of def.options) {
9257
+ const pv = option._zod.propValues;
9258
+ if (!pv || Object.keys(pv).length === 0)
9259
+ throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
9260
+ for (const [k, v] of Object.entries(pv)) {
9261
+ if (!propValues[k])
9262
+ propValues[k] = /* @__PURE__ */ new Set();
9263
+ for (const val of v) {
9264
+ propValues[k].add(val);
9265
+ }
9266
+ }
9267
+ }
9268
+ return propValues;
9269
+ });
9270
+ const disc = cached(() => {
9271
+ const opts = def.options;
9272
+ const map = /* @__PURE__ */ new Map();
9273
+ for (const o2 of opts) {
9274
+ const values = o2._zod.propValues?.[def.discriminator];
9275
+ if (!values || values.size === 0)
9276
+ throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o2)}"`);
9277
+ for (const v of values) {
9278
+ if (map.has(v)) {
9279
+ throw new Error(`Duplicate discriminator value "${String(v)}"`);
9280
+ }
9281
+ map.set(v, o2);
9282
+ }
9283
+ }
9284
+ return map;
9285
+ });
9286
+ inst._zod.parse = (payload, ctx) => {
9287
+ const input = payload.value;
9288
+ if (!isObject(input)) {
9289
+ payload.issues.push({
9290
+ code: "invalid_type",
9291
+ expected: "object",
9292
+ input,
9293
+ inst
9294
+ });
9295
+ return payload;
9296
+ }
9297
+ const opt = disc.value.get(input?.[def.discriminator]);
9298
+ if (opt) {
9299
+ return opt._zod.run(payload, ctx);
9300
+ }
9301
+ if (def.unionFallback || ctx.direction === "backward") {
9302
+ return _super(payload, ctx);
9303
+ }
9304
+ payload.issues.push({
9305
+ code: "invalid_union",
9306
+ errors: [],
9307
+ note: "No matching discriminator",
9308
+ discriminator: def.discriminator,
9309
+ options: Array.from(disc.value.keys()),
9310
+ input,
9311
+ path: [def.discriminator],
9312
+ inst
9313
+ });
9314
+ return payload;
9315
+ };
9316
+ });
9004
9317
  const $ZodIntersection = /* @__PURE__ */ $constructor("$ZodIntersection", (inst, def) => {
9005
9318
  $ZodType.init(inst, def);
9006
9319
  inst._zod.parse = (payload, ctx) => {
@@ -9245,6 +9558,28 @@ const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
9245
9558
  return payload;
9246
9559
  };
9247
9560
  });
9561
+ const $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
9562
+ $ZodType.init(inst, def);
9563
+ if (def.values.length === 0) {
9564
+ throw new Error("Cannot create literal schema with no valid values");
9565
+ }
9566
+ const values = new Set(def.values);
9567
+ inst._zod.values = values;
9568
+ inst._zod.pattern = new RegExp(`^(${def.values.map((o2) => typeof o2 === "string" ? escapeRegex(o2) : o2 ? escapeRegex(o2.toString()) : String(o2)).join("|")})$`);
9569
+ inst._zod.parse = (payload, _ctx) => {
9570
+ const input = payload.value;
9571
+ if (values.has(input)) {
9572
+ return payload;
9573
+ }
9574
+ payload.issues.push({
9575
+ code: "invalid_value",
9576
+ values: def.values,
9577
+ input,
9578
+ inst
9579
+ });
9580
+ return payload;
9581
+ };
9582
+ });
9248
9583
  const $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => {
9249
9584
  $ZodType.init(inst, def);
9250
9585
  inst._zod.optin = "optional";
@@ -9519,11 +9854,11 @@ class $ZodRegistry {
9519
9854
  this._map = /* @__PURE__ */ new WeakMap();
9520
9855
  this._idmap = /* @__PURE__ */ new Map();
9521
9856
  }
9522
- add(schema, ..._meta) {
9857
+ add(schema2, ..._meta) {
9523
9858
  const meta = _meta[0];
9524
- this._map.set(schema, meta);
9859
+ this._map.set(schema2, meta);
9525
9860
  if (meta && typeof meta === "object" && "id" in meta) {
9526
- this._idmap.set(meta.id, schema);
9861
+ this._idmap.set(meta.id, schema2);
9527
9862
  }
9528
9863
  return this;
9529
9864
  }
@@ -9532,26 +9867,26 @@ class $ZodRegistry {
9532
9867
  this._idmap = /* @__PURE__ */ new Map();
9533
9868
  return this;
9534
9869
  }
9535
- remove(schema) {
9536
- const meta = this._map.get(schema);
9870
+ remove(schema2) {
9871
+ const meta = this._map.get(schema2);
9537
9872
  if (meta && typeof meta === "object" && "id" in meta) {
9538
9873
  this._idmap.delete(meta.id);
9539
9874
  }
9540
- this._map.delete(schema);
9875
+ this._map.delete(schema2);
9541
9876
  return this;
9542
9877
  }
9543
- get(schema) {
9544
- const p = schema._zod.parent;
9878
+ get(schema2) {
9879
+ const p = schema2._zod.parent;
9545
9880
  if (p) {
9546
9881
  const pm = { ...this.get(p) ?? {} };
9547
9882
  delete pm.id;
9548
- const f = { ...pm, ...this._map.get(schema) };
9883
+ const f = { ...pm, ...this._map.get(schema2) };
9549
9884
  return Object.keys(f).length ? f : void 0;
9550
9885
  }
9551
- return this._map.get(schema);
9886
+ return this._map.get(schema2);
9552
9887
  }
9553
- has(schema) {
9554
- return this._map.has(schema);
9888
+ has(schema2) {
9889
+ return this._map.has(schema2);
9555
9890
  }
9556
9891
  }
9557
9892
  function registry() {
@@ -10028,13 +10363,13 @@ function _array(Class, element, params) {
10028
10363
  }
10029
10364
  // @__NO_SIDE_EFFECTS__
10030
10365
  function _refine(Class, fn, _params) {
10031
- const schema = new Class({
10366
+ const schema2 = new Class({
10032
10367
  type: "custom",
10033
10368
  check: "custom",
10034
10369
  fn,
10035
10370
  ...normalizeParams(_params)
10036
10371
  });
10037
- return schema;
10372
+ return schema2;
10038
10373
  }
10039
10374
  // @__NO_SIDE_EFFECTS__
10040
10375
  function _superRefine(fn, params) {
@@ -10087,40 +10422,40 @@ function initializeContext(params) {
10087
10422
  external: params?.external ?? void 0
10088
10423
  };
10089
10424
  }
10090
- function process$1(schema, ctx, _params = { path: [], schemaPath: [] }) {
10425
+ function process$1(schema2, ctx, _params = { path: [], schemaPath: [] }) {
10091
10426
  var _a2;
10092
- const def = schema._zod.def;
10093
- const seen = ctx.seen.get(schema);
10427
+ const def = schema2._zod.def;
10428
+ const seen = ctx.seen.get(schema2);
10094
10429
  if (seen) {
10095
10430
  seen.count++;
10096
- const isCycle = _params.schemaPath.includes(schema);
10431
+ const isCycle = _params.schemaPath.includes(schema2);
10097
10432
  if (isCycle) {
10098
10433
  seen.cycle = _params.path;
10099
10434
  }
10100
10435
  return seen.schema;
10101
10436
  }
10102
10437
  const result = { schema: {}, count: 1, cycle: void 0, path: _params.path };
10103
- ctx.seen.set(schema, result);
10104
- const overrideSchema = schema._zod.toJSONSchema?.();
10438
+ ctx.seen.set(schema2, result);
10439
+ const overrideSchema = schema2._zod.toJSONSchema?.();
10105
10440
  if (overrideSchema) {
10106
10441
  result.schema = overrideSchema;
10107
10442
  } else {
10108
10443
  const params = {
10109
10444
  ..._params,
10110
- schemaPath: [..._params.schemaPath, schema],
10445
+ schemaPath: [..._params.schemaPath, schema2],
10111
10446
  path: _params.path
10112
10447
  };
10113
- if (schema._zod.processJSONSchema) {
10114
- schema._zod.processJSONSchema(ctx, result.schema, params);
10448
+ if (schema2._zod.processJSONSchema) {
10449
+ schema2._zod.processJSONSchema(ctx, result.schema, params);
10115
10450
  } else {
10116
10451
  const _json = result.schema;
10117
10452
  const processor = ctx.processors[def.type];
10118
10453
  if (!processor) {
10119
10454
  throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);
10120
10455
  }
10121
- processor(schema, ctx, _json, params);
10456
+ processor(schema2, ctx, _json, params);
10122
10457
  }
10123
- const parent = schema._zod.parent;
10458
+ const parent = schema2._zod.parent;
10124
10459
  if (parent) {
10125
10460
  if (!result.ref)
10126
10461
  result.ref = parent;
@@ -10128,21 +10463,21 @@ function process$1(schema, ctx, _params = { path: [], schemaPath: [] }) {
10128
10463
  ctx.seen.get(parent).isParent = true;
10129
10464
  }
10130
10465
  }
10131
- const meta = ctx.metadataRegistry.get(schema);
10466
+ const meta = ctx.metadataRegistry.get(schema2);
10132
10467
  if (meta)
10133
10468
  Object.assign(result.schema, meta);
10134
- if (ctx.io === "input" && isTransforming(schema)) {
10469
+ if (ctx.io === "input" && isTransforming(schema2)) {
10135
10470
  delete result.schema.examples;
10136
10471
  delete result.schema.default;
10137
10472
  }
10138
10473
  if (ctx.io === "input" && "_prefault" in result.schema)
10139
10474
  (_a2 = result.schema).default ?? (_a2.default = result.schema._prefault);
10140
10475
  delete result.schema._prefault;
10141
- const _result = ctx.seen.get(schema);
10476
+ const _result = ctx.seen.get(schema2);
10142
10477
  return _result.schema;
10143
10478
  }
10144
- function extractDefs(ctx, schema) {
10145
- const root = ctx.seen.get(schema);
10479
+ function extractDefs(ctx, schema2) {
10480
+ const root = ctx.seen.get(schema2);
10146
10481
  if (!root)
10147
10482
  throw new Error("Unprocessed schema. This is a bug in Zod.");
10148
10483
  const idToSchema = /* @__PURE__ */ new Map();
@@ -10185,11 +10520,11 @@ function extractDefs(ctx, schema) {
10185
10520
  seen.def = { ...seen.schema };
10186
10521
  if (defId)
10187
10522
  seen.defId = defId;
10188
- const schema2 = seen.schema;
10189
- for (const key in schema2) {
10190
- delete schema2[key];
10523
+ const schema3 = seen.schema;
10524
+ for (const key in schema3) {
10525
+ delete schema3[key];
10191
10526
  }
10192
- schema2.$ref = ref;
10527
+ schema3.$ref = ref;
10193
10528
  };
10194
10529
  if (ctx.cycles === "throw") {
10195
10530
  for (const entry of ctx.seen.entries()) {
@@ -10203,13 +10538,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10203
10538
  }
10204
10539
  for (const entry of ctx.seen.entries()) {
10205
10540
  const seen = entry[1];
10206
- if (schema === entry[0]) {
10541
+ if (schema2 === entry[0]) {
10207
10542
  extractToDef(entry);
10208
10543
  continue;
10209
10544
  }
10210
10545
  if (ctx.external) {
10211
10546
  const ext = ctx.external.registry.get(entry[0])?.id;
10212
- if (schema !== entry[0] && ext) {
10547
+ if (schema2 !== entry[0] && ext) {
10213
10548
  extractToDef(entry);
10214
10549
  continue;
10215
10550
  }
@@ -10231,16 +10566,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
10231
10566
  }
10232
10567
  }
10233
10568
  }
10234
- function finalize(ctx, schema) {
10235
- const root = ctx.seen.get(schema);
10569
+ function finalize(ctx, schema2) {
10570
+ const root = ctx.seen.get(schema2);
10236
10571
  if (!root)
10237
10572
  throw new Error("Unprocessed schema. This is a bug in Zod.");
10238
10573
  const flattenRef = (zodSchema) => {
10239
10574
  const seen = ctx.seen.get(zodSchema);
10240
10575
  if (seen.ref === null)
10241
10576
  return;
10242
- const schema2 = seen.def ?? seen.schema;
10243
- const _cached = { ...schema2 };
10577
+ const schema3 = seen.def ?? seen.schema;
10578
+ const _cached = { ...schema3 };
10244
10579
  const ref = seen.ref;
10245
10580
  seen.ref = null;
10246
10581
  if (ref) {
@@ -10248,28 +10583,28 @@ function finalize(ctx, schema) {
10248
10583
  const refSeen = ctx.seen.get(ref);
10249
10584
  const refSchema = refSeen.schema;
10250
10585
  if (refSchema.$ref && (ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0")) {
10251
- schema2.allOf = schema2.allOf ?? [];
10252
- schema2.allOf.push(refSchema);
10586
+ schema3.allOf = schema3.allOf ?? [];
10587
+ schema3.allOf.push(refSchema);
10253
10588
  } else {
10254
- Object.assign(schema2, refSchema);
10589
+ Object.assign(schema3, refSchema);
10255
10590
  }
10256
- Object.assign(schema2, _cached);
10591
+ Object.assign(schema3, _cached);
10257
10592
  const isParentRef = zodSchema._zod.parent === ref;
10258
10593
  if (isParentRef) {
10259
- for (const key in schema2) {
10594
+ for (const key in schema3) {
10260
10595
  if (key === "$ref" || key === "allOf")
10261
10596
  continue;
10262
10597
  if (!(key in _cached)) {
10263
- delete schema2[key];
10598
+ delete schema3[key];
10264
10599
  }
10265
10600
  }
10266
10601
  }
10267
10602
  if (refSchema.$ref && refSeen.def) {
10268
- for (const key in schema2) {
10603
+ for (const key in schema3) {
10269
10604
  if (key === "$ref" || key === "allOf")
10270
10605
  continue;
10271
- if (key in refSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(refSeen.def[key])) {
10272
- delete schema2[key];
10606
+ if (key in refSeen.def && JSON.stringify(schema3[key]) === JSON.stringify(refSeen.def[key])) {
10607
+ delete schema3[key];
10273
10608
  }
10274
10609
  }
10275
10610
  }
@@ -10279,13 +10614,13 @@ function finalize(ctx, schema) {
10279
10614
  flattenRef(parent);
10280
10615
  const parentSeen = ctx.seen.get(parent);
10281
10616
  if (parentSeen?.schema.$ref) {
10282
- schema2.$ref = parentSeen.schema.$ref;
10617
+ schema3.$ref = parentSeen.schema.$ref;
10283
10618
  if (parentSeen.def) {
10284
- for (const key in schema2) {
10619
+ for (const key in schema3) {
10285
10620
  if (key === "$ref" || key === "allOf")
10286
10621
  continue;
10287
- if (key in parentSeen.def && JSON.stringify(schema2[key]) === JSON.stringify(parentSeen.def[key])) {
10288
- delete schema2[key];
10622
+ if (key in parentSeen.def && JSON.stringify(schema3[key]) === JSON.stringify(parentSeen.def[key])) {
10623
+ delete schema3[key];
10289
10624
  }
10290
10625
  }
10291
10626
  }
@@ -10293,7 +10628,7 @@ function finalize(ctx, schema) {
10293
10628
  }
10294
10629
  ctx.override({
10295
10630
  zodSchema,
10296
- jsonSchema: schema2,
10631
+ jsonSchema: schema3,
10297
10632
  path: seen.path ?? []
10298
10633
  });
10299
10634
  };
@@ -10310,13 +10645,13 @@ function finalize(ctx, schema) {
10310
10645
  } else if (ctx.target === "openapi-3.0") ;
10311
10646
  else ;
10312
10647
  if (ctx.external?.uri) {
10313
- const id = ctx.external.registry.get(schema)?.id;
10648
+ const id = ctx.external.registry.get(schema2)?.id;
10314
10649
  if (!id)
10315
10650
  throw new Error("Schema is missing an `id` property");
10316
10651
  result.$id = ctx.external.uri(id);
10317
10652
  }
10318
10653
  Object.assign(result, root.def ?? root.schema);
10319
- const rootMetaId = ctx.metadataRegistry.get(schema)?.id;
10654
+ const rootMetaId = ctx.metadataRegistry.get(schema2)?.id;
10320
10655
  if (rootMetaId !== void 0 && result.id === rootMetaId)
10321
10656
  delete result.id;
10322
10657
  const defs = ctx.external?.defs ?? {};
@@ -10342,10 +10677,10 @@ function finalize(ctx, schema) {
10342
10677
  const finalized = JSON.parse(JSON.stringify(result));
10343
10678
  Object.defineProperty(finalized, "~standard", {
10344
10679
  value: {
10345
- ...schema["~standard"],
10680
+ ...schema2["~standard"],
10346
10681
  jsonSchema: {
10347
- input: createStandardJSONSchemaMethod(schema, "input", ctx.processors),
10348
- output: createStandardJSONSchemaMethod(schema, "output", ctx.processors)
10682
+ input: createStandardJSONSchemaMethod(schema2, "input", ctx.processors),
10683
+ output: createStandardJSONSchemaMethod(schema2, "output", ctx.processors)
10349
10684
  }
10350
10685
  },
10351
10686
  enumerable: false,
@@ -10409,18 +10744,18 @@ function isTransforming(_schema, _ctx) {
10409
10744
  }
10410
10745
  return false;
10411
10746
  }
10412
- const createToJSONSchemaMethod = (schema, processors = {}) => (params) => {
10747
+ const createToJSONSchemaMethod = (schema2, processors = {}) => (params) => {
10413
10748
  const ctx = initializeContext({ ...params, processors });
10414
- process$1(schema, ctx);
10415
- extractDefs(ctx, schema);
10416
- return finalize(ctx, schema);
10749
+ process$1(schema2, ctx);
10750
+ extractDefs(ctx, schema2);
10751
+ return finalize(ctx, schema2);
10417
10752
  };
10418
- const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params) => {
10753
+ const createStandardJSONSchemaMethod = (schema2, io, processors = {}) => (params) => {
10419
10754
  const { libraryOptions, target } = params ?? {};
10420
10755
  const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
10421
- process$1(schema, ctx);
10422
- extractDefs(ctx, schema);
10423
- return finalize(ctx, schema);
10756
+ process$1(schema2, ctx);
10757
+ extractDefs(ctx, schema2);
10758
+ return finalize(ctx, schema2);
10424
10759
  };
10425
10760
  const formatMap = {
10426
10761
  guid: "uuid",
@@ -10430,10 +10765,10 @@ const formatMap = {
10430
10765
  regex: ""
10431
10766
  // do not set
10432
10767
  };
10433
- const stringProcessor = (schema, ctx, _json, _params) => {
10768
+ const stringProcessor = (schema2, ctx, _json, _params) => {
10434
10769
  const json2 = _json;
10435
10770
  json2.type = "string";
10436
- const { minimum, maximum, format: format2, patterns, contentEncoding } = schema._zod.bag;
10771
+ const { minimum, maximum, format: format2, patterns, contentEncoding } = schema2._zod.bag;
10437
10772
  if (typeof minimum === "number")
10438
10773
  json2.minLength = minimum;
10439
10774
  if (typeof maximum === "number")
@@ -10462,9 +10797,9 @@ const stringProcessor = (schema, ctx, _json, _params) => {
10462
10797
  }
10463
10798
  }
10464
10799
  };
10465
- const numberProcessor = (schema, ctx, _json, _params) => {
10800
+ const numberProcessor = (schema2, ctx, _json, _params) => {
10466
10801
  const json2 = _json;
10467
- const { minimum, maximum, format: format2, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema._zod.bag;
10802
+ const { minimum, maximum, format: format2, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema2._zod.bag;
10468
10803
  if (typeof format2 === "string" && format2.includes("int"))
10469
10804
  json2.type = "integer";
10470
10805
  else
@@ -10539,8 +10874,8 @@ const dateProcessor = (_schema, ctx, _json, _params) => {
10539
10874
  throw new Error("Date cannot be represented in JSON Schema");
10540
10875
  }
10541
10876
  };
10542
- const enumProcessor = (schema, _ctx, json2, _params) => {
10543
- const def = schema._zod.def;
10877
+ const enumProcessor = (schema2, _ctx, json2, _params) => {
10878
+ const def = schema2._zod.def;
10544
10879
  const values = getEnumValues(def.entries);
10545
10880
  if (values.every((v) => typeof v === "number"))
10546
10881
  json2.type = "number";
@@ -10548,8 +10883,8 @@ const enumProcessor = (schema, _ctx, json2, _params) => {
10548
10883
  json2.type = "string";
10549
10884
  json2.enum = values;
10550
10885
  };
10551
- const literalProcessor = (schema, ctx, json2, _params) => {
10552
- const def = schema._zod.def;
10886
+ const literalProcessor = (schema2, ctx, json2, _params) => {
10887
+ const def = schema2._zod.def;
10553
10888
  const vals = [];
10554
10889
  for (const val of def.values) {
10555
10890
  if (val === void 0) {
@@ -10592,22 +10927,22 @@ const nanProcessor = (_schema, ctx, _json, _params) => {
10592
10927
  throw new Error("NaN cannot be represented in JSON Schema");
10593
10928
  }
10594
10929
  };
10595
- const templateLiteralProcessor = (schema, _ctx, json2, _params) => {
10930
+ const templateLiteralProcessor = (schema2, _ctx, json2, _params) => {
10596
10931
  const _json = json2;
10597
- const pattern = schema._zod.pattern;
10932
+ const pattern = schema2._zod.pattern;
10598
10933
  if (!pattern)
10599
10934
  throw new Error("Pattern not found in template literal");
10600
10935
  _json.type = "string";
10601
10936
  _json.pattern = pattern.source;
10602
10937
  };
10603
- const fileProcessor = (schema, _ctx, json2, _params) => {
10938
+ const fileProcessor = (schema2, _ctx, json2, _params) => {
10604
10939
  const _json = json2;
10605
10940
  const file = {
10606
10941
  type: "string",
10607
10942
  format: "binary",
10608
10943
  contentEncoding: "binary"
10609
10944
  };
10610
- const { minimum, maximum, mime } = schema._zod.bag;
10945
+ const { minimum, maximum, mime } = schema2._zod.bag;
10611
10946
  if (minimum !== void 0)
10612
10947
  file.minLength = minimum;
10613
10948
  if (maximum !== void 0)
@@ -10652,10 +10987,10 @@ const setProcessor = (_schema, ctx, _json, _params) => {
10652
10987
  throw new Error("Set cannot be represented in JSON Schema");
10653
10988
  }
10654
10989
  };
10655
- const arrayProcessor = (schema, ctx, _json, params) => {
10990
+ const arrayProcessor = (schema2, ctx, _json, params) => {
10656
10991
  const json2 = _json;
10657
- const def = schema._zod.def;
10658
- const { minimum, maximum } = schema._zod.bag;
10992
+ const def = schema2._zod.def;
10993
+ const { minimum, maximum } = schema2._zod.bag;
10659
10994
  if (typeof minimum === "number")
10660
10995
  json2.minItems = minimum;
10661
10996
  if (typeof maximum === "number")
@@ -10666,9 +11001,9 @@ const arrayProcessor = (schema, ctx, _json, params) => {
10666
11001
  path: [...params.path, "items"]
10667
11002
  });
10668
11003
  };
10669
- const objectProcessor = (schema, ctx, _json, params) => {
11004
+ const objectProcessor = (schema2, ctx, _json, params) => {
10670
11005
  const json2 = _json;
10671
- const def = schema._zod.def;
11006
+ const def = schema2._zod.def;
10672
11007
  json2.type = "object";
10673
11008
  json2.properties = {};
10674
11009
  const shape = def.shape;
@@ -10702,8 +11037,8 @@ const objectProcessor = (schema, ctx, _json, params) => {
10702
11037
  });
10703
11038
  }
10704
11039
  };
10705
- const unionProcessor = (schema, ctx, json2, params) => {
10706
- const def = schema._zod.def;
11040
+ const unionProcessor = (schema2, ctx, json2, params) => {
11041
+ const def = schema2._zod.def;
10707
11042
  const isExclusive = def.inclusive === false;
10708
11043
  const options = def.options.map((x, i2) => process$1(x, ctx, {
10709
11044
  ...params,
@@ -10715,8 +11050,8 @@ const unionProcessor = (schema, ctx, json2, params) => {
10715
11050
  json2.anyOf = options;
10716
11051
  }
10717
11052
  };
10718
- const intersectionProcessor = (schema, ctx, json2, params) => {
10719
- const def = schema._zod.def;
11053
+ const intersectionProcessor = (schema2, ctx, json2, params) => {
11054
+ const def = schema2._zod.def;
10720
11055
  const a2 = process$1(def.left, ctx, {
10721
11056
  ...params,
10722
11057
  path: [...params.path, "allOf", 0]
@@ -10732,9 +11067,9 @@ const intersectionProcessor = (schema, ctx, json2, params) => {
10732
11067
  ];
10733
11068
  json2.allOf = allOf;
10734
11069
  };
10735
- const tupleProcessor = (schema, ctx, _json, params) => {
11070
+ const tupleProcessor = (schema2, ctx, _json, params) => {
10736
11071
  const json2 = _json;
10737
- const def = schema._zod.def;
11072
+ const def = schema2._zod.def;
10738
11073
  json2.type = "array";
10739
11074
  const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
10740
11075
  const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
@@ -10768,15 +11103,15 @@ const tupleProcessor = (schema, ctx, _json, params) => {
10768
11103
  json2.additionalItems = rest;
10769
11104
  }
10770
11105
  }
10771
- const { minimum, maximum } = schema._zod.bag;
11106
+ const { minimum, maximum } = schema2._zod.bag;
10772
11107
  if (typeof minimum === "number")
10773
11108
  json2.minItems = minimum;
10774
11109
  if (typeof maximum === "number")
10775
11110
  json2.maxItems = maximum;
10776
11111
  };
10777
- const recordProcessor = (schema, ctx, _json, params) => {
11112
+ const recordProcessor = (schema2, ctx, _json, params) => {
10778
11113
  const json2 = _json;
10779
- const def = schema._zod.def;
11114
+ const def = schema2._zod.def;
10780
11115
  json2.type = "object";
10781
11116
  const keyType = def.keyType;
10782
11117
  const keyBag = keyType._zod.bag;
@@ -10810,10 +11145,10 @@ const recordProcessor = (schema, ctx, _json, params) => {
10810
11145
  }
10811
11146
  }
10812
11147
  };
10813
- const nullableProcessor = (schema, ctx, json2, params) => {
10814
- const def = schema._zod.def;
11148
+ const nullableProcessor = (schema2, ctx, json2, params) => {
11149
+ const def = schema2._zod.def;
10815
11150
  const inner = process$1(def.innerType, ctx, params);
10816
- const seen = ctx.seen.get(schema);
11151
+ const seen = ctx.seen.get(schema2);
10817
11152
  if (ctx.target === "openapi-3.0") {
10818
11153
  seen.ref = def.innerType;
10819
11154
  json2.nullable = true;
@@ -10821,31 +11156,31 @@ const nullableProcessor = (schema, ctx, json2, params) => {
10821
11156
  json2.anyOf = [inner, { type: "null" }];
10822
11157
  }
10823
11158
  };
10824
- const nonoptionalProcessor = (schema, ctx, _json, params) => {
10825
- const def = schema._zod.def;
11159
+ const nonoptionalProcessor = (schema2, ctx, _json, params) => {
11160
+ const def = schema2._zod.def;
10826
11161
  process$1(def.innerType, ctx, params);
10827
- const seen = ctx.seen.get(schema);
11162
+ const seen = ctx.seen.get(schema2);
10828
11163
  seen.ref = def.innerType;
10829
11164
  };
10830
- const defaultProcessor = (schema, ctx, json2, params) => {
10831
- const def = schema._zod.def;
11165
+ const defaultProcessor = (schema2, ctx, json2, params) => {
11166
+ const def = schema2._zod.def;
10832
11167
  process$1(def.innerType, ctx, params);
10833
- const seen = ctx.seen.get(schema);
11168
+ const seen = ctx.seen.get(schema2);
10834
11169
  seen.ref = def.innerType;
10835
11170
  json2.default = JSON.parse(JSON.stringify(def.defaultValue));
10836
11171
  };
10837
- const prefaultProcessor = (schema, ctx, json2, params) => {
10838
- const def = schema._zod.def;
11172
+ const prefaultProcessor = (schema2, ctx, json2, params) => {
11173
+ const def = schema2._zod.def;
10839
11174
  process$1(def.innerType, ctx, params);
10840
- const seen = ctx.seen.get(schema);
11175
+ const seen = ctx.seen.get(schema2);
10841
11176
  seen.ref = def.innerType;
10842
11177
  if (ctx.io === "input")
10843
11178
  json2._prefault = JSON.parse(JSON.stringify(def.defaultValue));
10844
11179
  };
10845
- const catchProcessor = (schema, ctx, json2, params) => {
10846
- const def = schema._zod.def;
11180
+ const catchProcessor = (schema2, ctx, json2, params) => {
11181
+ const def = schema2._zod.def;
10847
11182
  process$1(def.innerType, ctx, params);
10848
- const seen = ctx.seen.get(schema);
11183
+ const seen = ctx.seen.get(schema2);
10849
11184
  seen.ref = def.innerType;
10850
11185
  let catchValue;
10851
11186
  try {
@@ -10855,37 +11190,37 @@ const catchProcessor = (schema, ctx, json2, params) => {
10855
11190
  }
10856
11191
  json2.default = catchValue;
10857
11192
  };
10858
- const pipeProcessor = (schema, ctx, _json, params) => {
10859
- const def = schema._zod.def;
11193
+ const pipeProcessor = (schema2, ctx, _json, params) => {
11194
+ const def = schema2._zod.def;
10860
11195
  const inIsTransform = def.in._zod.traits.has("$ZodTransform");
10861
11196
  const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
10862
11197
  process$1(innerType, ctx, params);
10863
- const seen = ctx.seen.get(schema);
11198
+ const seen = ctx.seen.get(schema2);
10864
11199
  seen.ref = innerType;
10865
11200
  };
10866
- const readonlyProcessor = (schema, ctx, json2, params) => {
10867
- const def = schema._zod.def;
11201
+ const readonlyProcessor = (schema2, ctx, json2, params) => {
11202
+ const def = schema2._zod.def;
10868
11203
  process$1(def.innerType, ctx, params);
10869
- const seen = ctx.seen.get(schema);
11204
+ const seen = ctx.seen.get(schema2);
10870
11205
  seen.ref = def.innerType;
10871
11206
  json2.readOnly = true;
10872
11207
  };
10873
- const promiseProcessor = (schema, ctx, _json, params) => {
10874
- const def = schema._zod.def;
11208
+ const promiseProcessor = (schema2, ctx, _json, params) => {
11209
+ const def = schema2._zod.def;
10875
11210
  process$1(def.innerType, ctx, params);
10876
- const seen = ctx.seen.get(schema);
11211
+ const seen = ctx.seen.get(schema2);
10877
11212
  seen.ref = def.innerType;
10878
11213
  };
10879
- const optionalProcessor = (schema, ctx, _json, params) => {
10880
- const def = schema._zod.def;
11214
+ const optionalProcessor = (schema2, ctx, _json, params) => {
11215
+ const def = schema2._zod.def;
10881
11216
  process$1(def.innerType, ctx, params);
10882
- const seen = ctx.seen.get(schema);
11217
+ const seen = ctx.seen.get(schema2);
10883
11218
  seen.ref = def.innerType;
10884
11219
  };
10885
- const lazyProcessor = (schema, ctx, _json, params) => {
10886
- const innerType = schema._zod.innerType;
11220
+ const lazyProcessor = (schema2, ctx, _json, params) => {
11221
+ const innerType = schema2._zod.innerType;
10887
11222
  process$1(innerType, ctx, params);
10888
- const seen = ctx.seen.get(schema);
11223
+ const seen = ctx.seen.get(schema2);
10889
11224
  seen.ref = innerType;
10890
11225
  };
10891
11226
  const allProcessors = {
@@ -10935,8 +11270,8 @@ function toJSONSchema(input, params) {
10935
11270
  const ctx2 = initializeContext({ ...params, processors: allProcessors });
10936
11271
  const defs = {};
10937
11272
  for (const entry of registry2._idmap.entries()) {
10938
- const [_, schema] = entry;
10939
- process$1(schema, ctx2);
11273
+ const [_, schema2] = entry;
11274
+ process$1(schema2, ctx2);
10940
11275
  }
10941
11276
  const schemas = {};
10942
11277
  const external = {
@@ -10946,9 +11281,9 @@ function toJSONSchema(input, params) {
10946
11281
  };
10947
11282
  ctx2.external = external;
10948
11283
  for (const entry of registry2._idmap.entries()) {
10949
- const [key, schema] = entry;
10950
- extractDefs(ctx2, schema);
10951
- schemas[key] = finalize(ctx2, schema);
11284
+ const [key, schema2] = entry;
11285
+ extractDefs(ctx2, schema2);
11286
+ schemas[key] = finalize(ctx2, schema2);
10952
11287
  }
10953
11288
  if (Object.keys(defs).length > 0) {
10954
11289
  const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
@@ -11572,6 +11907,18 @@ function union(options, params) {
11572
11907
  ...normalizeParams(params)
11573
11908
  });
11574
11909
  }
11910
+ const ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("ZodDiscriminatedUnion", (inst, def) => {
11911
+ ZodUnion.init(inst, def);
11912
+ $ZodDiscriminatedUnion.init(inst, def);
11913
+ });
11914
+ function discriminatedUnion(discriminator, options, params) {
11915
+ return new ZodDiscriminatedUnion({
11916
+ type: "union",
11917
+ options,
11918
+ discriminator,
11919
+ ...normalizeParams(params)
11920
+ });
11921
+ }
11575
11922
  const ZodIntersection = /* @__PURE__ */ $constructor("ZodIntersection", (inst, def) => {
11576
11923
  $ZodIntersection.init(inst, def);
11577
11924
  ZodType.init(inst, def);
@@ -11653,6 +12000,27 @@ function _enum(values, params) {
11653
12000
  ...normalizeParams(params)
11654
12001
  });
11655
12002
  }
12003
+ const ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => {
12004
+ $ZodLiteral.init(inst, def);
12005
+ ZodType.init(inst, def);
12006
+ inst._zod.processJSONSchema = (ctx, json2, params) => literalProcessor(inst, ctx, json2);
12007
+ inst.values = new Set(def.values);
12008
+ Object.defineProperty(inst, "value", {
12009
+ get() {
12010
+ if (def.values.length > 1) {
12011
+ throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
12012
+ }
12013
+ return def.values[0];
12014
+ }
12015
+ });
12016
+ });
12017
+ function literal(value, params) {
12018
+ return new ZodLiteral({
12019
+ type: "literal",
12020
+ values: Array.isArray(value) ? value : [value],
12021
+ ...normalizeParams(params)
12022
+ });
12023
+ }
11656
12024
  const ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
11657
12025
  $ZodTransform.init(inst, def);
11658
12026
  ZodType.init(inst, def);
@@ -12216,22 +12584,78 @@ function cleanText(value, maxLength) {
12216
12584
  const text2 = value.trim();
12217
12585
  return !text2 || text2.length > maxLength || hasControlCharacters$6(text2, true) ? null : text2;
12218
12586
  }
12587
+ const DEFAULT_MODEL$1 = { id: "default", label: "Default (CLI)" };
12219
12588
  class CommitMessageService {
12220
- constructor(operations, providers, log) {
12589
+ constructor(operations, providers, log, invalidateDiscovery, statusPersistence) {
12221
12590
  this.operations = operations;
12222
12591
  this.log = log;
12592
+ this.invalidateDiscovery = invalidateDiscovery;
12593
+ this.statusPersistence = statusPersistence;
12223
12594
  for (const provider of providers) this.providers.set(provider.id, provider);
12224
12595
  }
12225
12596
  operations;
12226
12597
  log;
12598
+ invalidateDiscovery;
12599
+ statusPersistence;
12227
12600
  providers = /* @__PURE__ */ new Map();
12228
12601
  active = /* @__PURE__ */ new Map();
12229
12602
  statusCache = null;
12603
+ statusInvalidated = false;
12604
+ statusGeneration = 0;
12605
+ pendingStatus;
12606
+ invalidateStatuses() {
12607
+ this.statusGeneration++;
12608
+ this.statusCache = null;
12609
+ this.statusInvalidated = true;
12610
+ this.pendingStatus = void 0;
12611
+ this.invalidateDiscovery?.();
12612
+ }
12230
12613
  async statuses(forceRefresh = false) {
12231
- if (!forceRefresh && this.statusCache && Date.now() - this.statusCache.at < 3e4) return this.statusCache.value;
12232
- const value = await Promise.all([...this.providers.values()].map((provider) => provider.status(forceRefresh)));
12233
- this.statusCache = { at: Date.now(), value };
12234
- return value;
12614
+ const key = this.statusPersistence?.key() ?? "";
12615
+ if (!forceRefresh && this.statusCache?.key === key) return this.statusCache.value;
12616
+ if (this.pendingStatus?.key === key && (!forceRefresh || this.pendingStatus.force)) return this.pendingStatus.promise;
12617
+ const generation = ++this.statusGeneration;
12618
+ const promise = (async () => {
12619
+ if (!forceRefresh && !this.statusInvalidated && this.statusPersistence) {
12620
+ const saved = await this.statusPersistence.store.load(key).catch(() => null);
12621
+ if (saved?.length === this.providers.size && saved.every((status) => this.providers.has(status.id))) {
12622
+ if (generation === this.statusGeneration) this.statusCache = { key, value: saved };
12623
+ return saved;
12624
+ }
12625
+ }
12626
+ const value = await Promise.all([...this.providers.values()].map(async (provider) => {
12627
+ try {
12628
+ return { ...await provider.status(forceRefresh), checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
12629
+ } catch {
12630
+ return {
12631
+ id: provider.id,
12632
+ label: { codex: "Codex", claude: "Claude Code", opencode: "OpenCode", grok: "Grok Build" }[provider.id],
12633
+ installed: false,
12634
+ availability: "error",
12635
+ authStatus: "unknown",
12636
+ installationStatus: "inspection-failed",
12637
+ message: "Could not inspect this CLI. Check again or review its executable path.",
12638
+ models: [DEFAULT_MODEL$1],
12639
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
12640
+ };
12641
+ }
12642
+ }));
12643
+ if (generation === this.statusGeneration) {
12644
+ this.statusCache = { key, value };
12645
+ this.statusInvalidated = false;
12646
+ try {
12647
+ await this.statusPersistence?.store.save(key, value);
12648
+ } catch {
12649
+ }
12650
+ }
12651
+ return value;
12652
+ })();
12653
+ this.pendingStatus = { force: forceRefresh, key, promise };
12654
+ try {
12655
+ return await promise;
12656
+ } finally {
12657
+ if (this.pendingStatus?.promise === promise) this.pendingStatus = void 0;
12658
+ }
12235
12659
  }
12236
12660
  async generate(input, externalSignal) {
12237
12661
  if (this.active.has(input.requestId) || [...this.active.values()].some((entry) => entry.repositoryId === input.repositoryId) || this.active.size >= 3) {
@@ -12468,9 +12892,53 @@ function throwIfCancelled(signal, harness) {
12468
12892
  if (!signal?.aborted) return;
12469
12893
  throw new AiOperationError({ code: "AI_CANCELLED", operation: "ai-pr-draft", harness, message: "Generation canceled." });
12470
12894
  }
12895
+ function runCandidate(runner, candidate, args, options = {}) {
12896
+ return runner.run(candidate.executable, args, { ...options, env: { ...candidate.env, ...options.env } });
12897
+ }
12898
+ async function selectCli(resolver, runner, name, options = {}) {
12899
+ let first;
12900
+ const candidates = await resolver.discover(name, options.forceRefresh);
12901
+ for (const candidate of candidates) {
12902
+ if (options.runOptions?.signal?.aborted) throw new AiOperationError({ code: "AI_CANCELLED", operation: "cli-discovery", message: "Generation canceled." });
12903
+ if (candidate.problem) {
12904
+ first ??= { candidate, state: candidate.problem };
12905
+ continue;
12906
+ }
12907
+ try {
12908
+ const result = await runCandidate(runner, candidate, ["--version"], options.runOptions);
12909
+ const raw = result.stdout.trim() || result.stderr.trim();
12910
+ const version2 = (raw.split(/\r?\n/)[0] ?? "").slice(0, 200);
12911
+ if (result.exitCode !== 0 || !version2) {
12912
+ first ??= { candidate, state: "not-executable" };
12913
+ continue;
12914
+ }
12915
+ if (options.compatible && !options.compatible(raw)) {
12916
+ first ??= { candidate, version: version2, state: "incompatible" };
12917
+ continue;
12918
+ }
12919
+ return { candidate, version: version2, state: "available", warning: candidate.warning };
12920
+ } catch (error) {
12921
+ if (options.runOptions?.signal?.aborted) throw error;
12922
+ first ??= { candidate, state: "not-executable" };
12923
+ }
12924
+ }
12925
+ const warning = await resolver.warning();
12926
+ return first ? { ...first, warning } : { state: warning ? "inspection-failed" : "not-found", warning };
12927
+ }
12928
+ function detectionFields(detected) {
12929
+ return { installationStatus: detected.state, ...detected.candidate ? { executablePath: detected.candidate.executable, executableSource: detected.candidate.source } : {}, ...detected.warning ? { discoveryWarning: detected.warning } : {} };
12930
+ }
12931
+ function detectionFailure(detected, base, unsupported) {
12932
+ if (detected.state === "available") return null;
12933
+ const message = detected.state === "not-found" ? detected.candidate?.source === "configured" ? "The configured executable was not found. Correct the path or reset it to Automatic." : `Install ${base.label} and check again, or set its executable path.` : detected.state === "incompatible" ? unsupported ?? "The installed CLI version is not supported." : detected.state === "inspection-failed" ? "Could not inspect the CLI installation. Check again or set its executable path." : "The CLI was found but could not run. Check executable permissions, runtime dependencies, and system compatibility.";
12934
+ return { ...base, ...detectionFields(detected), installed: !!detected.candidate && detected.state !== "not-found", availability: "error", authStatus: "unknown", ...detected.version ? { version: detected.version } : {}, message };
12935
+ }
12936
+ function requireCandidate(detected, name) {
12937
+ if (detected.state !== "available" || !detected.candidate) throw new AiOperationError({ code: detected.state === "not-found" ? "AI_CLI_NOT_FOUND" : "AI_PROCESS_FAILED", operation: "cli-discovery", message: `${name} could not be started. Check its status in Settings → AI assistance.` });
12938
+ return detected.candidate;
12939
+ }
12471
12940
  const AI_PROVIDER_TIMEOUT_MS = 9 * 6e4;
12472
12941
  const AI_SERVER_TIMEOUT_MS = AI_PROVIDER_TIMEOUT_MS + 3e4;
12473
- const DEFAULT_MODEL$1 = { id: "default", label: "Default (CLI)" };
12474
12942
  function grokUsage(envelope) {
12475
12943
  if (!envelope || typeof envelope !== "object") return { ...EMPTY_AI_USAGE };
12476
12944
  const record2 = envelope;
@@ -12574,8 +13042,8 @@ function label(harness) {
12574
13042
  function stripAnsi(value) {
12575
13043
  return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "");
12576
13044
  }
12577
- function toCodexOutputSchema(schema) {
12578
- return strictify(structuredClone(schema));
13045
+ function toCodexOutputSchema(schema2) {
13046
+ return strictify(structuredClone(schema2));
12579
13047
  }
12580
13048
  function strictify(value) {
12581
13049
  if (!value || typeof value !== "object") return value;
@@ -12674,10 +13142,11 @@ class ClaudeProvider {
12674
13142
  id = "claude";
12675
13143
  async status(forceRefresh = false) {
12676
13144
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
12677
- const executable = await this.resolver.resolve("claude", forceRefresh);
12678
- if (!executable) return { id: this.id, label: "Claude Code", availability: "error", installed: false, authStatus: "unknown", message: "Install Claude Code to use this harness.", models: MODELS, checkedAt };
12679
- const version2 = await this.runner.run(executable, ["--version"]);
12680
- const auth = await this.runner.run(executable, ["auth", "status", "--json"]);
13145
+ const detected = await selectCli(this.resolver, this.runner, "claude", { forceRefresh });
13146
+ const failure2 = detectionFailure(detected, { id: this.id, label: "Claude Code", models: MODELS, checkedAt });
13147
+ if (failure2) return failure2;
13148
+ const executable = requireCandidate(detected, this.id);
13149
+ const auth = await runCandidate(this.runner, executable, ["auth", "status", "--json"]);
12681
13150
  let loggedIn;
12682
13151
  try {
12683
13152
  loggedIn = auth.exitCode === 0 && JSON.parse(auth.stdout).loggedIn === true;
@@ -12685,23 +13154,23 @@ class ClaudeProvider {
12685
13154
  loggedIn = false;
12686
13155
  }
12687
13156
  return {
13157
+ ...detectionFields(detected),
12688
13158
  id: this.id,
12689
13159
  label: "Claude Code",
12690
13160
  availability: loggedIn ? "ready" : "error",
12691
13161
  installed: true,
12692
13162
  authStatus: loggedIn ? "authenticated" : "unauthenticated",
12693
- version: version2.stdout.trim() || version2.stderr.trim(),
13163
+ version: detected.version ?? "",
12694
13164
  ...!loggedIn ? { message: "Run claude auth login." } : {},
12695
13165
  models: MODELS,
12696
13166
  checkedAt
12697
13167
  };
12698
13168
  }
12699
13169
  async generate(input) {
12700
- const executable = await this.resolver.resolve("claude");
12701
- if (!executable) throw new AiOperationError({ code: "AI_CLI_NOT_FOUND", operation: "claude-generate", harness: this.id, message: "Claude Code is not installed." });
13170
+ const executable = requireCandidate(await selectCli(this.resolver, this.runner, "claude", { runOptions: { signal: input.signal } }), this.id);
12702
13171
  const args = ["-p", "--output-format", "json", "--json-schema", JSON.stringify(input.schema), "--tools", "", "--no-session-persistence", "--safe-mode"];
12703
13172
  if (input.model !== "default") args.push("--model", input.model);
12704
- const result = await this.runner.run(executable, args, { cwd: input.repositoryPath, stdin: input.prompt, timeoutMs: AI_PROVIDER_TIMEOUT_MS, signal: input.signal, removeEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"] });
13173
+ const result = await runCandidate(this.runner, executable, args, { cwd: input.repositoryPath, stdin: input.prompt, timeoutMs: AI_PROVIDER_TIMEOUT_MS, signal: input.signal, removeEnv: ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"] });
12705
13174
  requireSuccess(result, this.id, "claude-generate");
12706
13175
  try {
12707
13176
  const envelope = JSON.parse(result.stdout);
@@ -12724,31 +13193,32 @@ class CodexProvider {
12724
13193
  id = "codex";
12725
13194
  async status(forceRefresh = false) {
12726
13195
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
12727
- const executable = await this.resolver.resolve("codex", forceRefresh);
12728
- if (!executable) return { id: this.id, label: "Codex", availability: "error", installed: false, authStatus: "unknown", message: "Install Codex CLI to use this harness.", models: [DEFAULT_MODEL$1], checkedAt };
12729
- const version2 = await this.runner.run(executable, ["--version"]);
12730
- const login = await this.runner.run(executable, ["login", "status"]);
12731
- if (login.exitCode !== 0 || !/logged in/i.test(`${login.stdout}
12732
- ${login.stderr}`)) {
12733
- return { id: this.id, label: "Codex", availability: "error", installed: true, authStatus: "unauthenticated", version: version2.stdout.trim() || version2.stderr.trim(), message: "Run codex login.", models: [DEFAULT_MODEL$1], checkedAt };
12734
- }
12735
- const catalog = await this.runner.run(executable, ["debug", "models", "--bundled"]);
13196
+ const detected = await selectCli(this.resolver, this.runner, "codex", { forceRefresh });
13197
+ const failure2 = detectionFailure(detected, { id: this.id, label: "Codex", models: [DEFAULT_MODEL$1], checkedAt });
13198
+ if (failure2) return failure2;
13199
+ const executable = requireCandidate(detected, this.id);
13200
+ const login2 = await runCandidate(this.runner, executable, ["login", "status"]);
13201
+ if (login2.exitCode !== 0 || !/logged in/i.test(`${login2.stdout}
13202
+ ${login2.stderr}`)) {
13203
+ return { ...detectionFields(detected), id: this.id, label: "Codex", availability: "error", installed: true, authStatus: "unauthenticated", version: detected.version ?? "", message: "Run codex login.", models: [DEFAULT_MODEL$1], checkedAt };
13204
+ }
13205
+ const catalog = await runCandidate(this.runner, executable, ["debug", "models", "--bundled"]);
12736
13206
  const models = catalog.exitCode === 0 ? parseCodexModels(catalog.stdout) : [DEFAULT_MODEL$1];
12737
13207
  return {
13208
+ ...detectionFields(detected),
12738
13209
  id: this.id,
12739
13210
  label: "Codex",
12740
13211
  availability: models.length > 1 ? "ready" : "warning",
12741
13212
  installed: true,
12742
13213
  authStatus: "authenticated",
12743
- version: version2.stdout.trim() || version2.stderr.trim(),
13214
+ version: detected.version ?? "",
12744
13215
  ...models.length > 1 ? {} : { message: "Available; the model catalog could not be verified." },
12745
13216
  models,
12746
13217
  checkedAt
12747
13218
  };
12748
13219
  }
12749
13220
  async generate(input) {
12750
- const executable = await this.resolver.resolve("codex");
12751
- if (!executable) throw new AiOperationError({ code: "AI_CLI_NOT_FOUND", operation: "codex-generate", harness: this.id, message: "Codex is not installed." });
13221
+ const executable = requireCandidate(await selectCli(this.resolver, this.runner, "codex", { runOptions: { signal: input.signal } }), this.id);
12752
13222
  const temporary = await mkdtemp(path.join(os.tmpdir(), "opentig-codex-"));
12753
13223
  const schemaPath = path.join(temporary, "schema.json");
12754
13224
  const outputPath = path.join(temporary, "output.json");
@@ -12757,7 +13227,7 @@ ${login.stderr}`)) {
12757
13227
  const args = ["exec", "--json", "--ephemeral", "--skip-git-repo-check", "-s", "read-only"];
12758
13228
  if (input.model !== "default") args.push("--model", input.model);
12759
13229
  args.push("--config", 'model_reasoning_effort="low"', "--output-schema", schemaPath, "--output-last-message", outputPath, "-");
12760
- const result = await this.runner.run(executable, args, { cwd: input.repositoryPath, stdin: input.prompt, timeoutMs: AI_PROVIDER_TIMEOUT_MS, signal: input.signal, removeEnv: ["OPENAI_API_KEY"] });
13230
+ const result = await runCandidate(this.runner, executable, args, { cwd: input.repositoryPath, stdin: input.prompt, timeoutMs: AI_PROVIDER_TIMEOUT_MS, signal: input.signal, removeEnv: ["OPENAI_API_KEY"] });
12761
13231
  requireSuccess(result, this.id, "codex-generate");
12762
13232
  return { output: parseJsonPayload(await readFile(outputPath, "utf8")), usage: codexUsage(result.stdout) };
12763
13233
  } finally {
@@ -12832,16 +13302,15 @@ class GrokProvider {
12832
13302
  async status(forceRefresh = false) {
12833
13303
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
12834
13304
  const base = { id: this.id, label: "Grok Build", checkedAt, models: [DEFAULT_MODEL] };
12835
- const executable = await this.resolver.resolve("grok", forceRefresh);
12836
- if (!executable) return { ...base, installed: false, availability: "error", authStatus: "unknown", message: "Install the official Grok Build CLI and run grok login." };
12837
13305
  return this.withEnvironment(async (options) => {
12838
- const version2 = await this.runner.run(executable, ["--version"], options);
12839
- const versionText = version2.stdout.trim();
12840
- const installed = { ...base, installed: true, version: versionText };
12841
- if (version2.exitCode !== 0 || !isSupportedGrokVersion(versionText)) return { ...installed, availability: "error", authStatus: "unknown", message: UNSUPPORTED };
13306
+ const detected = await selectCli(this.resolver, this.runner, this.id, { forceRefresh, compatible: isSupportedGrokVersion, runOptions: options });
13307
+ const failure2 = detectionFailure(detected, base, UNSUPPORTED);
13308
+ if (failure2) return failure2;
13309
+ const executable = requireCandidate(detected, this.id);
13310
+ const installed = { ...base, ...detectionFields(detected), installed: true, version: detected.version ?? "" };
12842
13311
  try {
12843
13312
  await this.checkControls(executable, options);
12844
- const result = await this.runner.run(executable, ["models"], options);
13313
+ const result = await runCandidate(this.runner, executable, ["models"], options);
12845
13314
  if (result.exitCode !== 0) return { ...installed, availability: "warning", authStatus: "unknown", message: "Could not check Grok authentication or models. Check again or run grok models in a terminal." };
12846
13315
  const { authStatus, models } = parseGrokModels(result.stdout);
12847
13316
  return {
@@ -12857,15 +13326,12 @@ class GrokProvider {
12857
13326
  });
12858
13327
  }
12859
13328
  async generate(input) {
12860
- const executable = await this.resolver.resolve("grok");
12861
- if (!executable) throw failure("AI_CLI_NOT_FOUND", "Grok Build is not installed.");
12862
13329
  const timeout = AbortSignal.timeout(AI_PROVIDER_TIMEOUT_MS);
12863
13330
  const signal = AbortSignal.any([input.signal, timeout]);
12864
13331
  try {
12865
13332
  return await this.withEnvironment(async (options) => {
12866
13333
  const controlled = { ...options, signal };
12867
- const version2 = await this.runner.run(executable, ["--version"], controlled);
12868
- if (version2.exitCode !== 0 || !isSupportedGrokVersion(version2.stdout)) throw failure("AI_PROCESS_FAILED", UNSUPPORTED);
13334
+ const executable = requireCandidate(await selectCli(this.resolver, this.runner, this.id, { compatible: isSupportedGrokVersion, runOptions: controlled }), this.id);
12869
13335
  await this.checkControls(executable, controlled);
12870
13336
  const promptFile = path.join(options.cwd, "prompt.txt");
12871
13337
  await writeFile(promptFile, input.prompt, { encoding: "utf8", mode: 384 });
@@ -12891,7 +13357,7 @@ class GrokProvider {
12891
13357
  "--no-memory"
12892
13358
  ];
12893
13359
  if (input.model !== "default") args.push("--model", input.model);
12894
- const result = await this.runner.run(executable, args, { ...controlled, timeoutMs: AI_PROVIDER_TIMEOUT_MS });
13360
+ const result = await runCandidate(this.runner, executable, args, { ...controlled, timeoutMs: AI_PROVIDER_TIMEOUT_MS });
12895
13361
  requireGrokSuccess(result);
12896
13362
  const parsed = parseGrokOutput(result.stdout);
12897
13363
  return { output: parsed.output, usage: grokUsage(parsed.envelope) };
@@ -12903,9 +13369,9 @@ class GrokProvider {
12903
13369
  }
12904
13370
  }
12905
13371
  async checkControls(executable, options) {
12906
- const help = await this.runner.run(executable, ["--help"], options);
13372
+ const help = await runCandidate(this.runner, executable, ["--help"], options);
12907
13373
  if (help.exitCode !== 0 || REQUIRED_FLAGS.some((flag2) => !help.stdout.includes(flag2))) throw failure("AI_PROCESS_FAILED", UNSUPPORTED);
12908
- const result = await this.runner.run(executable, ["inspect", "--json"], options);
13374
+ const result = await runCandidate(this.runner, executable, ["inspect", "--json"], options);
12909
13375
  let report;
12910
13376
  try {
12911
13377
  report = JSON.parse(result.stdout);
@@ -13015,13 +13481,14 @@ async function startOpenCodeV2Server(options) {
13015
13481
  const timeoutMs = options.timeoutMs;
13016
13482
  const password = randomBytes(24).toString("base64url");
13017
13483
  const port = await freePort();
13018
- const environment = Object.fromEntries(
13484
+ const environment = cleanEnvironment(Object.fromEntries(
13019
13485
  Object.entries({
13020
13486
  ...process.env,
13487
+ ...options.env,
13021
13488
  OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
13022
13489
  OPENCODE_SERVER_PASSWORD: password
13023
13490
  }).filter((entry) => entry[1] !== void 0)
13024
- );
13491
+ ));
13025
13492
  const resolved = resolveProcessCommand(options.executable, options.cwd ?? process.cwd(), environment);
13026
13493
  if (!resolved.found) {
13027
13494
  throw new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "opencode-server", harness: "opencode", message: "Could not start the local OpenCode server.", retryable: true });
@@ -13221,15 +13688,15 @@ class OpenCodeProvider {
13221
13688
  async status(forceRefresh = false) {
13222
13689
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
13223
13690
  const detected = await this.detect(forceRefresh);
13224
- if (!detected) return { id: this.id, label: "OpenCode", availability: "error", installed: false, authStatus: "unknown", message: "Install OpenCode 2 to use this harness.", models: [DEFAULT_MODEL$1], checkedAt };
13225
- const { executable, version: version2, compatible } = detected;
13226
- const cliName = openCodeCliName(executable);
13227
- if (!compatible) return { id: this.id, label: "OpenCode", availability: "error", installed: true, authStatus: "unknown", message: UNSUPPORTED_MESSAGE, cliName, version: version2, models: [DEFAULT_MODEL$1], checkedAt };
13228
- const [auth, catalog] = await Promise.all([
13229
- this.runner.run(executable, ["auth", "list", "--format", "json", "--standalone"]),
13230
- this.runner.run(executable, ["models", "--standalone"], { timeoutMs: 3e4 })
13691
+ const failure2 = detectionFailure(detected, { id: this.id, label: "OpenCode", models: [DEFAULT_MODEL$1], checkedAt }, UNSUPPORTED_MESSAGE);
13692
+ if (failure2) return failure2;
13693
+ const executable = requireCandidate(detected, this.id);
13694
+ const version2 = detected.version ?? "";
13695
+ const cliName = openCodeCliName(executable.executable);
13696
+ const [auth, models] = await Promise.all([
13697
+ runCandidate(this.runner, executable, ["auth", "list", "--format", "json", "--standalone"]),
13698
+ this.loadModels(executable)
13231
13699
  ]);
13232
- const models = catalog.exitCode === 0 ? parseOpenCodeModels(catalog.stdout) : [DEFAULT_MODEL$1];
13233
13700
  let authStatus = parseOpenCodeAuthList(auth.stdout, auth.exitCode);
13234
13701
  if (authStatus === "unauthenticated" && models.length > 1) authStatus = "unknown";
13235
13702
  const confirmed = authStatus === "authenticated";
@@ -13237,20 +13704,30 @@ class OpenCodeProvider {
13237
13704
  id: this.id,
13238
13705
  label: "OpenCode",
13239
13706
  availability: models.length > 1 ? confirmed ? "ready" : "warning" : authStatus === "unauthenticated" ? "error" : "warning",
13707
+ ...detectionFields(detected),
13240
13708
  installed: true,
13241
13709
  authStatus,
13242
13710
  cliName,
13243
13711
  version: version2,
13244
13712
  models,
13245
13713
  checkedAt,
13246
- ...authStatus === "unauthenticated" ? { message: `Run ${openCodeLoginCommand(cliName)}.` } : !confirmed ? { message: "Could not confirm authentication for the selected provider." } : {}
13714
+ ...authStatus === "unauthenticated" ? { message: `Run ${openCodeLoginCommand(cliName)}.` } : models.length === 1 ? { message: `Could not load the OpenCode model catalog. Check again or verify that ${cliName} models lists your models in a terminal.` } : !confirmed ? { message: "Could not confirm authentication for the selected provider." } : {}
13247
13715
  };
13248
13716
  }
13717
+ async loadModels(executable) {
13718
+ for (const args of [["models", "--standalone"], ["models"]]) {
13719
+ try {
13720
+ const catalog = await runCandidate(this.runner, executable, args, { timeoutMs: 3e4 });
13721
+ const models = catalog.exitCode === 0 ? parseOpenCodeModels(catalog.stdout) : [DEFAULT_MODEL$1];
13722
+ if (models.length > 1) return models;
13723
+ } catch {
13724
+ }
13725
+ }
13726
+ return [DEFAULT_MODEL$1];
13727
+ }
13249
13728
  async generate(input) {
13250
13729
  const detected = await this.detect(false, input.signal);
13251
- if (!detected) throw new AiOperationError({ code: "AI_CLI_NOT_FOUND", operation: "opencode-generate", harness: this.id, message: "OpenCode is not installed." });
13252
- if (!detected.compatible) throw new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "opencode-generate", harness: this.id, message: UNSUPPORTED_MESSAGE });
13253
- const executable = detected.executable;
13730
+ const executable = requireCandidate(detected, this.id);
13254
13731
  const timeoutSignal = AbortSignal.timeout(AI_PROVIDER_TIMEOUT_MS);
13255
13732
  const boundedInput = { ...input, signal: AbortSignal.any([input.signal, timeoutSignal]) };
13256
13733
  try {
@@ -13263,7 +13740,7 @@ class OpenCodeProvider {
13263
13740
  }
13264
13741
  }
13265
13742
  async generateV2(executable, input) {
13266
- const server = await startOpenCodeV2Server({ executable, cwd: input.repositoryPath, timeoutMs: 15e3, signal: input.signal }).catch((error) => {
13743
+ const server = await startOpenCodeV2Server({ executable: executable.executable, env: executable.env, cwd: input.repositoryPath, timeoutMs: 15e3, signal: input.signal }).catch((error) => {
13267
13744
  if (error instanceof AiOperationError) throw error;
13268
13745
  throw new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "opencode-server", harness: this.id, message: "Could not start the local OpenCode server.", retryable: true });
13269
13746
  });
@@ -13286,21 +13763,7 @@ ${input.prompt}`, model: input.model, cwd: input.repositoryPath, signal: input.s
13286
13763
  }
13287
13764
  }
13288
13765
  async detect(forceRefresh = false, signal) {
13289
- const candidates = await this.resolver.resolveAll("opencode", forceRefresh);
13290
- let first = null;
13291
- for (const executable of candidates) {
13292
- try {
13293
- const result = await this.runner.run(executable, ["--version"], signal ? { signal } : {});
13294
- const version2 = result.stdout.trim() || result.stderr.trim();
13295
- const detected = { executable, version: version2, compatible: result.exitCode === 0 && isOpenCodeV2Version(version2) };
13296
- if (detected.compatible) return detected;
13297
- first ??= detected;
13298
- } catch (error) {
13299
- if (signal?.aborted) throw error;
13300
- first ??= { executable, version: "", compatible: false };
13301
- }
13302
- }
13303
- return first;
13766
+ return selectCli(this.resolver, this.runner, this.id, { forceRefresh, compatible: isOpenCodeV2Version, runOptions: signal ? { signal } : {} });
13304
13767
  }
13305
13768
  close() {
13306
13769
  for (const server of this.servers) server.close();
@@ -13589,7 +14052,7 @@ class FileService {
13589
14052
  // UI pulls its children through here instead of paying for them at startup.
13590
14053
  async listDirectory(repositoryId, relativePath) {
13591
14054
  const repository = this.repositories.get(repositoryId);
13592
- const directory = this.repositories.resolvePath(repositoryId, relativePath);
14055
+ const directory = relativePath === "" ? repository.path : this.repositories.resolvePath(repositoryId, relativePath);
13593
14056
  const parent = normalizeGitPath(relativePath).replace(/\/+$/, "");
13594
14057
  let dirents;
13595
14058
  try {
@@ -13599,13 +14062,12 @@ class FileService {
13599
14062
  if (code === "ENOENT" || code === "ENOTDIR") return [];
13600
14063
  throw error;
13601
14064
  }
13602
- const ignored = (await this.findIgnoredDirectories(repository.path, [parent])).has(parent);
13603
14065
  const entries = [];
13604
14066
  for (const dirent of dirents) {
13605
14067
  if (dirent.name === ".git") continue;
13606
14068
  const entryPath = parent ? `${parent}/${dirent.name}` : dirent.name;
13607
14069
  if (dirent.isDirectory() && !dirent.isSymbolicLink()) {
13608
- entries.push({ path: entryPath, name: dirent.name, type: "directory", ...ignored ? { ignored: true } : {}, children: [] });
14070
+ entries.push({ path: entryPath, name: dirent.name, type: "directory", children: [] });
13609
14071
  continue;
13610
14072
  }
13611
14073
  if (!dirent.isFile() && !dirent.isSymbolicLink()) continue;
@@ -13616,15 +14078,25 @@ class FileService {
13616
14078
  name: dirent.name,
13617
14079
  type: "file",
13618
14080
  size: metadata.size,
13619
- mtimeMs: metadata.mtimeMs,
13620
- ...ignored ? { ignored: true } : {}
14081
+ mtimeMs: metadata.mtimeMs
13621
14082
  });
13622
14083
  } catch (error) {
13623
14084
  const code = error.code;
13624
14085
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
13625
14086
  }
13626
14087
  }
13627
- return entries.sort((a2, b) => a2.type === b.type ? a2.name.localeCompare(b.name) : a2.type === "directory" ? -1 : 1);
14088
+ const ignoredChildren = await this.findIgnoredDirectories(repository.path, entries.map((entry) => entry.path));
14089
+ const ignoredFolders = entries.filter((entry) => entry.type === "directory" && ignoredChildren.has(entry.path));
14090
+ if (ignoredFolders.length > 0) {
14091
+ const tracked = await this.git.run(repository.path, ["ls-files", "-z", "--", ...ignoredFolders.map((entry) => entry.path)], {
14092
+ operation: "list-tracked-folder-children",
14093
+ readOnly: true,
14094
+ maxOutputBytes: 64 * 1024 * 1024
14095
+ });
14096
+ const ancestors = directoryAncestors(tracked.stdout.toString("utf8").split("\0").filter(Boolean).map(normalizeGitPath));
14097
+ for (const folder of ignoredFolders) if (ancestors.has(folder.path)) ignoredChildren.delete(folder.path);
14098
+ }
14099
+ return entries.map((entry) => ignoredChildren.has(entry.path) ? { ...entry, ignored: true } : entry).sort((a2, b) => a2.type === b.type ? a2.name.localeCompare(b.name) : a2.type === "directory" ? -1 : 1);
13628
14100
  }
13629
14101
  async findIgnoredDirectories(repositoryPath, directories) {
13630
14102
  const ignored = /* @__PURE__ */ new Set();
@@ -14824,74 +15296,403 @@ function parseWorktrees(buffer) {
14824
15296
  if (current) worktrees.push(current);
14825
15297
  return worktrees;
14826
15298
  }
14827
- const AI_PATCH_BUDGET = 4e5;
14828
- const AI_SUMMARY_BUDGET = 24e3;
14829
- class GitRepositoryOperations {
14830
- constructor(git, repositories, files) {
15299
+ const commitAuthorshipInputSchema = object({
15300
+ name: string().trim().min(1).max(120).regex(/^[^<>]+$/).refine(noControls),
15301
+ email: string().trim().max(254).regex(/^[^\s<>@]+@[^\s<>@]+$/).refine(noControls),
15302
+ expectedRevision: string().regex(/^[a-f0-9]{64}$/)
15303
+ });
15304
+ function noControls(value) {
15305
+ return [...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127);
15306
+ }
15307
+ const environmentKeys = ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "EMAIL"];
15308
+ const identityPattern = "^(user|author|committer)\\.(name|email)$";
15309
+ class GitCommitAuthorship {
15310
+ constructor(git, repositories) {
14831
15311
  this.git = git;
14832
15312
  this.repositories = repositories;
14833
- this.files = files;
14834
15313
  }
14835
15314
  git;
14836
15315
  repositories;
14837
- files;
14838
- knownOids = /* @__PURE__ */ new Map();
14839
- async diff(request) {
14840
- const repository = this.repositories.get(request.repositoryId);
14841
- const paths = this.repositories.validatePaths(request.repositoryId, [request.path]);
14842
- const tracked = (await this.git.run(
14843
- repository.path,
14844
- ["--literal-pathspecs", "ls-files", "-z", "--", ...paths],
14845
- { operation: "diff-tracked-check", readOnly: true, maxOutputBytes: 1024 * 1024 }
14846
- )).stdout.length > 0;
14847
- if (request.kind === "unstaged" && !tracked) return this.untrackedDiff(request.repositoryId, request.path);
14848
- const args = ["--literal-pathspecs", "diff"];
14849
- if (request.kind === "staged") args.push("--cached");
14850
- args.push("--no-color", "--no-ext-diff", "--unified=3", "--", ...paths);
14851
- const output = await this.git.run(repository.path, args, { operation: `diff-${request.kind}`, readOnly: true, maxOutputBytes: 32 * 1024 * 1024 });
14852
- return makeDiffResult(request.path, output.stdout);
15316
+ revisionKey = randomBytes(32);
15317
+ async get(repositoryId) {
15318
+ const repository = this.repositories.get(repositoryId);
15319
+ return this.git.runWriteTask(repository.path, () => this.read(repositoryId), repository.commonDir);
14853
15320
  }
14854
- async commitDiff(repositoryId, oid) {
14855
- this.assertKnownOid(repositoryId, oid);
15321
+ async set(repositoryId, input) {
15322
+ const parsed = commitAuthorshipInputSchema.safeParse(input);
15323
+ if (!parsed.success) throw invalid$1("Enter a valid name, email and reviewed identity.");
15324
+ const { name, email: email2, expectedRevision } = parsed.data;
14856
15325
  const repository = this.repositories.get(repositoryId);
14857
- const output = await this.git.run(repository.path, ["show", "--format=", "--no-color", "--no-ext-diff", oid, "--"], { operation: "commit-diff", readOnly: true, maxOutputBytes: 32 * 1024 * 1024 });
14858
- return makeDiffResult(oid, output.stdout);
15326
+ const configPath = path.join(repository.commonDir, "config");
15327
+ const lockPath = `${configPath}.lock`;
15328
+ return this.git.runWriteTask(repository.path, async (run) => {
15329
+ const current = await this.read(repositoryId);
15330
+ if (!current.editable) throw invalid$1(current.blockers.join(" "));
15331
+ if (current.revision !== expectedRevision) throw invalid$1("Git identity or repository settings changed. Reopen the editor and review them again.");
15332
+ let locked = false;
15333
+ try {
15334
+ const file = await open(lockPath, "wx", 384);
15335
+ locked = true;
15336
+ try {
15337
+ await file.writeFile(await readFile(configPath));
15338
+ } finally {
15339
+ await file.close();
15340
+ }
15341
+ const reviewed = await this.read(repositoryId);
15342
+ if (!reviewed.editable || reviewed.revision !== expectedRevision) throw invalid$1("Git settings changed before saving. Reopen the editor and review them again.");
15343
+ const mode = (await lstat(configPath)).mode & 511;
15344
+ await run(["config", "--file", lockPath, "--replace-all", "--", "user.name", name], { operation: "commit-authorship-set" });
15345
+ await run(["config", "--file", lockPath, "--replace-all", "--", "user.email", email2], { operation: "commit-authorship-set" });
15346
+ const candidate = await this.identities(repository.path, ["-c", `include.path=${lockPath}`]);
15347
+ if (candidate.author?.name !== name || candidate.author.email !== email2 || candidate.committer?.name !== name || candidate.committer.email !== email2) {
15348
+ throw invalid$1("An included Git setting overrides this identity. Edit that setting outside OpenTig before applying a repository identity.");
15349
+ }
15350
+ const latest = await this.read(repositoryId);
15351
+ if (!latest.editable || latest.revision !== expectedRevision) throw invalid$1("Git settings changed while saving. Reopen the editor and review them again.");
15352
+ await chmod(lockPath, mode);
15353
+ await rename(lockPath, configPath);
15354
+ locked = false;
15355
+ } catch (error) {
15356
+ if (error.code === "EEXIST") {
15357
+ throw invalid$1("Another Git process is editing repository settings. Wait for it to finish and try again.");
15358
+ }
15359
+ throw error;
15360
+ } finally {
15361
+ if (locked) await rm(lockPath, { force: true });
15362
+ }
15363
+ return this.read(repositoryId);
15364
+ }, repository.commonDir);
14859
15365
  }
14860
- async commitFiles(repositoryId, oid) {
14861
- this.assertKnownOid(repositoryId, oid);
15366
+ async read(repositoryId) {
14862
15367
  const repository = this.repositories.get(repositoryId);
14863
- const common2 = ["show", "--format=", "--no-color", "--no-ext-diff", "-M", "-z"];
14864
- const [nameStatus, numstat] = await Promise.all([
14865
- this.git.run(repository.path, [...common2, "--name-status", oid, "--"], { operation: "commit-files", readOnly: true, maxOutputBytes: 8 * 1024 * 1024 }),
14866
- this.git.run(repository.path, [...common2, "--numstat", oid, "--"], { operation: "commit-files", readOnly: true, maxOutputBytes: 8 * 1024 * 1024 })
15368
+ const configPath = path.join(repository.commonDir, "config");
15369
+ const [entries, identities, config2, info] = await Promise.all([
15370
+ this.config(repository.path),
15371
+ this.identities(repository.path),
15372
+ readFile(configPath),
15373
+ lstat(configPath)
14867
15374
  ]);
14868
- return parseCommitFiles(nameStatus.stdout.toString("utf8"), numstat.stdout.toString("utf8"));
15375
+ const environment = environmentKeys.filter((key) => process.env[key] !== void 0);
15376
+ const custom = entries.some((entry) => entry.key.startsWith("author.") || entry.key.startsWith("committer."));
15377
+ const worktree = entries.some((entry) => entry.scope === "worktree");
15378
+ const command2 = entries.some((entry) => entry.scope === "command");
15379
+ const blockers = [];
15380
+ if (environment.length) blockers.push("Backend environment variables override Git identity. Manage them outside OpenTig before editing repository authorship.");
15381
+ if (custom) blockers.push("Git author.* or committer.* settings override user.name/email. Manage those settings outside OpenTig first.");
15382
+ if (worktree) blockers.push("This worktree has its own identity. Manage its worktree settings outside OpenTig first.");
15383
+ if (command2) blockers.push("Backend command configuration overrides Git identity. Manage it outside OpenTig first.");
15384
+ if (!info.isFile() || info.nlink !== 1) blockers.push("The repository config is linked or shared. Edit it outside OpenTig to avoid changing another repository.");
15385
+ const scopes = ["user.name", "user.email"].map((key) => entries.filter((entry) => entry.key === key).at(-1)?.scope ?? "inferred");
15386
+ const source = environment.length || command2 ? "environment" : custom ? "custom" : worktree ? "worktree" : !identities.author ? "unset" : scopes[0] !== scopes[1] ? "mixed" : scopes[0] === "local" || scopes[0] === "global" || scopes[0] === "system" ? scopes[0] : "inferred";
15387
+ const revision2 = createHmac("sha256", this.revisionKey).update(repository.path).update(config2).update(JSON.stringify({ entries, identities, environment, mode: info.mode, linked: info.nlink, blockers })).digest("hex");
15388
+ return { ...identities, source, editable: blockers.length === 0, blockers, revision: revision2 };
15389
+ }
15390
+ async config(cwd) {
15391
+ try {
15392
+ const output = await this.git.run(cwd, ["config", "--null", "--show-scope", "--get-regexp", identityPattern], { operation: "commit-authorship-read", readOnly: true });
15393
+ const fields = output.stdout.toString("utf8").split("\0");
15394
+ const entries = [];
15395
+ for (let index = 0; index + 1 < fields.length; index += 2) {
15396
+ const field = fields[index + 1];
15397
+ const separator = field.indexOf("\n");
15398
+ if (separator >= 0) entries.push({ scope: fields[index], key: field.slice(0, separator), value: field.slice(separator + 1) });
15399
+ }
15400
+ return entries;
15401
+ } catch (error) {
15402
+ if (error instanceof GitOperationError && error.detail.exitCode === 1) return [];
15403
+ throw error;
15404
+ }
14869
15405
  }
14870
- async commitFileDiff(repositoryId, oid, filePath, oldPath) {
14871
- this.assertKnownOid(repositoryId, oid);
14872
- const repository = this.repositories.get(repositoryId);
14873
- const paths = this.repositories.validatePaths(repositoryId, oldPath ? [filePath, oldPath] : [filePath]);
14874
- const output = await this.git.run(
14875
- repository.path,
14876
- ["--literal-pathspecs", "show", "--format=", "--no-color", "--no-ext-diff", "-M", oid, "--", ...paths],
14877
- { operation: "commit-file-diff", readOnly: true, maxOutputBytes: 32 * 1024 * 1024 }
14878
- );
14879
- return makeDiffResult(filePath, output.stdout);
15406
+ async identities(cwd, prefix = []) {
15407
+ const [author, committer] = await Promise.all(["GIT_AUTHOR_IDENT", "GIT_COMMITTER_IDENT"].map(async (variable) => {
15408
+ try {
15409
+ const output = await this.git.run(cwd, [...prefix, "var", variable], { operation: "commit-authorship-read", readOnly: true });
15410
+ const match = /^(.*?) <([^<>]*)> \d+ [+-]\d{4}$/.exec(output.stdout.toString("utf8").trim());
15411
+ if (!match) throw invalid$1("Git returned an unreadable commit identity.");
15412
+ return { name: match[1], email: match[2] };
15413
+ } catch (error) {
15414
+ if (error instanceof GitOperationError && error.detail.exitCode === 128 && /identity unknown|unable to auto-detect|empty ident|no email was given|no name was given/i.test(error.detail.stderr ?? "")) return null;
15415
+ throw error;
15416
+ }
15417
+ }));
15418
+ return { author, committer };
14880
15419
  }
14881
- async stage(repositoryId, paths) {
14882
- await this.ensureWritable(repositoryId);
14883
- const repository = this.repositories.get(repositoryId);
14884
- const valid = this.repositories.validatePaths(repositoryId, paths);
14885
- await this.git.runWrite(repository.path, ["--literal-pathspecs", "add", "-A", "--", ...valid], { operation: "stage" });
14886
- return { ok: true };
15420
+ }
15421
+ function invalid$1(message) {
15422
+ return new GitOperationError({ code: "INVALID_ARGUMENT", operation: "commit-authorship-set", message });
15423
+ }
15424
+ const read = { operation: "remote-branches", readOnly: true, maxOutputBytes: 4 * 1024 * 1024 };
15425
+ const network = { ...read, timeoutMs: 6e4 };
15426
+ class GitRemoteBranches {
15427
+ constructor(git, repositories) {
15428
+ this.git = git;
15429
+ this.repositories = repositories;
14887
15430
  }
14888
- async stageAll(repositoryId) {
14889
- await this.ensureWritable(repositoryId);
14890
- const repository = this.repositories.get(repositoryId);
14891
- await this.git.runWrite(repository.path, ["add", "-A", "--", "."], { operation: "stage-all" });
14892
- return { ok: true };
15431
+ git;
15432
+ repositories;
15433
+ reader(root) {
15434
+ return (args, options) => this.git.run(root, args, options);
14893
15435
  }
14894
- async unstage(repositoryId, paths) {
15436
+ async remotes(run) {
15437
+ const names = (await run(["remote"], read)).stdout.toString("utf8").trim().split(/\r?\n/).filter(Boolean);
15438
+ return Promise.all(names.map(async (name) => ({ name, refspecs: (await this.config(run, `remote.${name}.fetch`)).split(/\r?\n/).filter(Boolean) })));
15439
+ }
15440
+ async config(run, key) {
15441
+ try {
15442
+ return (await run(["config", "--get-all", key], read)).stdout.toString("utf8").trim();
15443
+ } catch (error) {
15444
+ if (error instanceof GitOperationError && error.detail.exitCode === 1) return "";
15445
+ throw error;
15446
+ }
15447
+ }
15448
+ mappings(fullName, remotes) {
15449
+ return remotes.flatMap((remote) => remote.refspecs.flatMap((spec) => {
15450
+ if (spec.startsWith("^")) return [];
15451
+ const [source, destination] = spec.replace(/^\+/, "").split(":");
15452
+ if (!source?.startsWith("refs/heads/") || !destination?.startsWith("refs/remotes/")) return [];
15453
+ const match = refspecMatch(destination, fullName);
15454
+ if (match === null) return [];
15455
+ const sourceRef = source.replace("*", match);
15456
+ if (remote.refspecs.some((excluded) => excluded.startsWith("^") && refspecMatch(excluded.slice(1), sourceRef) !== null)) return [];
15457
+ return [{ remote: remote.name, sourceRef }];
15458
+ }));
15459
+ }
15460
+ async snapshotBranches(repositoryId, branches) {
15461
+ const root = this.repositories.get(repositoryId).path;
15462
+ const run = this.reader(root);
15463
+ const remotes = await this.remotes(run);
15464
+ const aliases = new Set((await run(["for-each-ref", "--format=%(refname)%09%(symref)", "refs/remotes"], read)).stdout.toString("utf8").split(/\r?\n/).filter((line) => line.split(" ")[1]).map((line) => line.split(" ")[0]));
15465
+ return { remotes: remotes.map((remote) => remote.name), branches: branches.filter((branch) => !aliases.has(branch.fullName)).map((branch) => {
15466
+ if (!branch.remote) return branch;
15467
+ const mappings = this.mappings(branch.fullName, remotes);
15468
+ if (mappings.length !== 1) return branch;
15469
+ return { ...branch, remoteName: mappings[0].remote, remoteBranchName: mappings[0].sourceRef.slice("refs/heads/".length) };
15470
+ }) };
15471
+ }
15472
+ async target(fullName, run) {
15473
+ const branches = parseRefs((await run(["for-each-ref", `--format=${REF_FORMAT}`, "refs/remotes"], read)).stdout);
15474
+ const branch = branches.find((item) => item.fullName === fullName);
15475
+ if (!branch) return null;
15476
+ const aliases = (await run(["for-each-ref", "--format=%(refname)%09%(symref)", fullName], read)).stdout.toString("utf8").split(/\r?\n/).some((line) => line.split(" ")[0] === fullName && Boolean(line.split(" ")[1]));
15477
+ const mappings = this.mappings(fullName, await this.remotes(run));
15478
+ if (aliases || mappings.length !== 1) throw new GitOperationError({
15479
+ code: "INVALID_ARGUMENT",
15480
+ operation: "remote-branch",
15481
+ message: "This reference does not map to one configured remote branch. Check the remote fetch settings."
15482
+ });
15483
+ return { branch, ...mappings[0], branchName: mappings[0].sourceRef.slice("refs/heads/".length) };
15484
+ }
15485
+ async destination(target, run) {
15486
+ const fetchUrls = (await run(["remote", "get-url", "--all", "--", target.remote], read)).stdout.toString("utf8").trim().split(/\r?\n/);
15487
+ 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");
15489
+ 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
+ return { id, blocked, url: fetchUrls[0] };
15491
+ }
15492
+ async advertised(target, run, url) {
15493
+ const lines = (await run(["ls-remote", "--symref", "--", url, "HEAD", target.sourceRef], network)).stdout.toString("utf8").split(/\r?\n/);
15494
+ const defaultRef = lines.find((line) => line.startsWith("ref: refs/heads/") && line.endsWith(" HEAD"))?.split(" ")[0]?.slice(5) ?? null;
15495
+ const oid = lines.find((line) => line.split(" ")[1] === target.sourceRef)?.split(" ")[0] ?? null;
15496
+ return { defaultRef, oid };
15497
+ }
15498
+ async details(repositoryId, fullName) {
15499
+ const run = this.reader(this.repositories.get(repositoryId).path);
15500
+ const target = await this.target(fullName, run);
15501
+ 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);
15503
+ let blocked = destination.blocked;
15504
+ let defaultRef = null;
15505
+ let remoteState = "unchecked";
15506
+ if (!blocked) {
15507
+ try {
15508
+ const state = await this.advertised(target, run, destination.url);
15509
+ defaultRef = state.defaultRef;
15510
+ remoteState = !state.oid ? "missing" : state.oid === target.branch.oid ? "current" : "changed";
15511
+ blocked = !state.oid ? "The branch no longer exists on the remote. Fetch to remove the obsolete reference." : state.oid !== target.branch.oid ? "The remote branch changed since the last fetch. Fetch before deleting." : !state.defaultRef ? "The remote does not identify its default branch. Deletion is unavailable here." : state.defaultRef === target.sourceRef ? "This is the remote’s default branch and cannot be deleted here." : null;
15512
+ } catch {
15513
+ blocked = "Could not check the remote. Fetch again or check your connection and Git permissions.";
15514
+ }
15515
+ }
15516
+ const upstreams = (await run(["for-each-ref", "--format=%(refname)%00%(refname:short)%00%(upstream)%00%(worktreepath)", "refs/heads"], read)).stdout.toString("utf8");
15517
+ const localBranches = upstreams.split(/\r?\n/).map((line) => line.split("\0")).filter((fields) => fields[2] === fullName).map(([ref, name, , worktreePath]) => ({ fullName: ref, name, worktreePath: worktreePath || null }));
15518
+ return {
15519
+ ...target.branch,
15520
+ remote: target.remote,
15521
+ branchName: target.branchName,
15522
+ localBranches,
15523
+ defaultBranch: defaultRef?.slice("refs/heads/".length) ?? null,
15524
+ remoteState,
15525
+ destinationId: destination.id,
15526
+ deletionBlockedReason: blocked
15527
+ };
15528
+ }
15529
+ async create(request) {
15530
+ const repository = this.repositories.get(request.repositoryId);
15531
+ return this.git.runWriteTask(repository.path, async (run) => {
15532
+ const target = await this.target(request.fullName, run);
15533
+ if (!target) return { status: "missing" };
15534
+ if (target.branch.oid !== request.expectedOid) return { status: "stale" };
15535
+ if (request.localName.startsWith("-")) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "create-tracking-branch", message: "Invalid local branch name." });
15536
+ await run(["check-ref-format", `refs/heads/${request.localName}`], read);
15537
+ const localRef = `refs/heads/${request.localName}`;
15538
+ const existing = (await run(["for-each-ref", "--format=%(refname)", "refs/heads"], read)).stdout.toString("utf8").trim().split(/\r?\n/);
15539
+ if (existing.includes(localRef)) return { status: "exists" };
15540
+ await run(["branch", "--track", "--", request.localName, target.branch.fullName], { operation: "create-tracking-branch" });
15541
+ return { status: "created", fullName: localRef };
15542
+ }, repository.commonDir);
15543
+ }
15544
+ async delete(request) {
15545
+ const repository = this.repositories.get(request.repositoryId);
15546
+ return this.git.runWriteTask(repository.path, async (run) => {
15547
+ const target = await this.target(request.fullName, run);
15548
+ if (!target) return { status: "missing" };
15549
+ if (target.branch.oid !== request.expectedOid) return { status: "stale" };
15550
+ const destination = await this.destination(target, run);
15551
+ if (destination.id !== request.destinationId) return { status: "destination-changed" };
15552
+ if (destination.blocked) return { status: "rejected", message: destination.blocked };
15553
+ try {
15554
+ const state = await this.advertised(target, run, destination.url);
15555
+ if (!state.oid) return { status: "missing" };
15556
+ if (!state.defaultRef) return { status: "rejected", message: "Could not identify the remote default branch. Nothing was deleted." };
15557
+ if (state.defaultRef === target.sourceRef) return { status: "default" };
15558
+ if (state.oid !== request.expectedOid) return { status: "stale" };
15559
+ await run(
15560
+ [
15561
+ "-c",
15562
+ `remote.${target.remote}.mirror=false`,
15563
+ "push",
15564
+ "--porcelain",
15565
+ "--no-follow-tags",
15566
+ `--force-with-lease=${target.sourceRef}:${request.expectedOid}`,
15567
+ "--",
15568
+ destination.url,
15569
+ `:${target.sourceRef}`
15570
+ ],
15571
+ { operation: "delete-remote-branch", timeoutMs: 12e4, maxOutputBytes: 4 * 1024 * 1024 }
15572
+ );
15573
+ return { status: "deleted" };
15574
+ } catch {
15575
+ return { status: "rejected", message: "Git refused the remote deletion. Check permissions or branch protection, fetch, and try again. No local branch or worktree was removed." };
15576
+ }
15577
+ }, repository.commonDir);
15578
+ }
15579
+ async fetch(repositoryId) {
15580
+ const repository = this.repositories.get(repositoryId);
15581
+ try {
15582
+ await this.git.runWriteTask(repository.path, async (run) => {
15583
+ for (const remote of await this.remotes(run)) {
15584
+ const specs = remote.refspecs.filter((spec) => spec.startsWith("^refs/heads/") || /^\+?refs\/heads\/[^:]+:refs\/remotes\//.test(spec));
15585
+ if (!specs.some((spec) => !spec.startsWith("^"))) continue;
15586
+ await run(
15587
+ ["-c", "fetch.pruneTags=false", "fetch", "--prune", "--no-prune-tags", "--no-tags", "--no-recurse-submodules", "--", remote.name, ...specs],
15588
+ { operation: "fetch-branches", timeoutMs: 12e4, maxOutputBytes: 4 * 1024 * 1024 }
15589
+ );
15590
+ }
15591
+ }, repository.commonDir);
15592
+ } catch {
15593
+ return { status: "failed", message: "Could not fetch all remote branches. Check your connection and Git credentials, then fetch again." };
15594
+ }
15595
+ const status = await this.repositories.status(repositoryId, false);
15596
+ return { status: "success", ahead: status.ahead, behind: status.behind };
15597
+ }
15598
+ }
15599
+ function refspecMatch(pattern, ref) {
15600
+ const star = pattern.indexOf("*");
15601
+ if (star < 0) return pattern === ref ? "" : null;
15602
+ const prefix = pattern.slice(0, star);
15603
+ const suffix = pattern.slice(star + 1);
15604
+ return ref.startsWith(prefix) && ref.endsWith(suffix) && ref.length >= prefix.length + suffix.length ? ref.slice(prefix.length, suffix ? -suffix.length : void 0) : null;
15605
+ }
15606
+ const AI_PATCH_BUDGET = 4e5;
15607
+ const AI_SUMMARY_BUDGET = 24e3;
15608
+ class GitRepositoryOperations {
15609
+ constructor(git, repositories, files) {
15610
+ this.git = git;
15611
+ this.repositories = repositories;
15612
+ this.files = files;
15613
+ this.authorship = new GitCommitAuthorship(git, repositories);
15614
+ this.remoteBranches = new GitRemoteBranches(git, repositories);
15615
+ }
15616
+ git;
15617
+ repositories;
15618
+ files;
15619
+ knownOids = /* @__PURE__ */ new Map();
15620
+ authorship;
15621
+ remoteBranches;
15622
+ remoteBranchDetails(repositoryId, fullName) {
15623
+ return this.remoteBranches.details(repositoryId, fullName);
15624
+ }
15625
+ createTrackingBranch(request) {
15626
+ return this.remoteBranches.create(request);
15627
+ }
15628
+ deleteRemoteBranch(request) {
15629
+ return this.remoteBranches.delete(request);
15630
+ }
15631
+ fetchBranches(repositoryId) {
15632
+ return this.remoteBranches.fetch(repositoryId);
15633
+ }
15634
+ getCommitAuthorship(repositoryId) {
15635
+ return this.authorship.get(repositoryId);
15636
+ }
15637
+ setCommitAuthorship(repositoryId, input) {
15638
+ return this.authorship.set(repositoryId, input);
15639
+ }
15640
+ async diff(request) {
15641
+ const repository = this.repositories.get(request.repositoryId);
15642
+ const paths = this.repositories.validatePaths(request.repositoryId, [request.path]);
15643
+ const tracked = (await this.git.run(
15644
+ repository.path,
15645
+ ["--literal-pathspecs", "ls-files", "-z", "--", ...paths],
15646
+ { operation: "diff-tracked-check", readOnly: true, maxOutputBytes: 1024 * 1024 }
15647
+ )).stdout.length > 0;
15648
+ if (request.kind === "unstaged" && !tracked) return this.untrackedDiff(request.repositoryId, request.path);
15649
+ const args = ["--literal-pathspecs", "diff"];
15650
+ if (request.kind === "staged") args.push("--cached");
15651
+ args.push("--no-color", "--no-ext-diff", "--unified=3", "--", ...paths);
15652
+ const output = await this.git.run(repository.path, args, { operation: `diff-${request.kind}`, readOnly: true, maxOutputBytes: 32 * 1024 * 1024 });
15653
+ return makeDiffResult(request.path, output.stdout);
15654
+ }
15655
+ async commitDiff(repositoryId, oid) {
15656
+ this.assertKnownOid(repositoryId, oid);
15657
+ const repository = this.repositories.get(repositoryId);
15658
+ const output = await this.git.run(repository.path, ["show", "--format=", "--no-color", "--no-ext-diff", oid, "--"], { operation: "commit-diff", readOnly: true, maxOutputBytes: 32 * 1024 * 1024 });
15659
+ return makeDiffResult(oid, output.stdout);
15660
+ }
15661
+ async commitFiles(repositoryId, oid) {
15662
+ this.assertKnownOid(repositoryId, oid);
15663
+ const repository = this.repositories.get(repositoryId);
15664
+ const common2 = ["show", "--format=", "--no-color", "--no-ext-diff", "-M", "-z"];
15665
+ const [nameStatus, numstat] = await Promise.all([
15666
+ this.git.run(repository.path, [...common2, "--name-status", oid, "--"], { operation: "commit-files", readOnly: true, maxOutputBytes: 8 * 1024 * 1024 }),
15667
+ this.git.run(repository.path, [...common2, "--numstat", oid, "--"], { operation: "commit-files", readOnly: true, maxOutputBytes: 8 * 1024 * 1024 })
15668
+ ]);
15669
+ return parseCommitFiles(nameStatus.stdout.toString("utf8"), numstat.stdout.toString("utf8"));
15670
+ }
15671
+ async commitFileDiff(repositoryId, oid, filePath, oldPath) {
15672
+ this.assertKnownOid(repositoryId, oid);
15673
+ const repository = this.repositories.get(repositoryId);
15674
+ const paths = this.repositories.validatePaths(repositoryId, oldPath ? [filePath, oldPath] : [filePath]);
15675
+ const output = await this.git.run(
15676
+ repository.path,
15677
+ ["--literal-pathspecs", "show", "--format=", "--no-color", "--no-ext-diff", "-M", oid, "--", ...paths],
15678
+ { operation: "commit-file-diff", readOnly: true, maxOutputBytes: 32 * 1024 * 1024 }
15679
+ );
15680
+ return makeDiffResult(filePath, output.stdout);
15681
+ }
15682
+ async stage(repositoryId, paths) {
15683
+ await this.ensureWritable(repositoryId);
15684
+ const repository = this.repositories.get(repositoryId);
15685
+ const valid = this.repositories.validatePaths(repositoryId, paths);
15686
+ await this.git.runWrite(repository.path, ["--literal-pathspecs", "add", "-A", "--", ...valid], { operation: "stage" });
15687
+ return { ok: true };
15688
+ }
15689
+ async stageAll(repositoryId) {
15690
+ await this.ensureWritable(repositoryId);
15691
+ const repository = this.repositories.get(repositoryId);
15692
+ await this.git.runWrite(repository.path, ["add", "-A", "--", "."], { operation: "stage-all" });
15693
+ return { ok: true };
15694
+ }
15695
+ async unstage(repositoryId, paths) {
14895
15696
  await this.ensureWritable(repositoryId);
14896
15697
  const repository = this.repositories.get(repositoryId);
14897
15698
  const valid = this.repositories.validatePaths(repositoryId, paths);
@@ -15420,15 +16221,16 @@ ${base}
15420
16221
  return this.repositories.openPath(worktree.path);
15421
16222
  }
15422
16223
  /**
15423
- * Cheap enumeration for the local refs manager. Deliberately runs no status
16224
+ * Cheap enumeration for the refs manager. Deliberately runs no status
15424
16225
  * scan per worktree so ordinary refreshes stay as fast as the selectors.
15425
16226
  */
15426
16227
  async localRefsSnapshot(repositoryId) {
15427
16228
  const repository = this.repositories.get(repositoryId);
15428
16229
  const [branches, worktrees] = await Promise.all([this.branches(repositoryId), this.worktrees(repositoryId)]);
15429
- const locals = branches.filter((branch) => !branch.remote).sort(compareBranches);
16230
+ const managed = await this.remoteBranches.snapshotBranches(repositoryId, branches);
15430
16231
  return {
15431
- branches: locals,
16232
+ branches: managed.branches.sort(compareBranches),
16233
+ remotes: managed.remotes,
15432
16234
  worktrees: await Promise.all(worktrees.map(async (worktree) => ({ ...worktree, current: await samePath(worktree.path, repository.path) })))
15433
16235
  };
15434
16236
  }
@@ -16376,6 +17178,421 @@ function revision(bytes) {
16376
17178
  function invalidReplacement(message) {
16377
17179
  return new GitOperationError({ code: "INVALID_ARGUMENT", operation: "replace-search", message });
16378
17180
  }
17181
+ const githubAccountSelectionSchema = discriminatedUnion("mode", [
17182
+ 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])?$/) })
17184
+ ]);
17185
+ const AUTOMATIC_GITHUB_ACCOUNT = { mode: "auto" };
17186
+ function githubRepositoryKey(commonDir, platform2 = process.platform) {
17187
+ const key = commonDir.replace(/\\/g, "/").replace(/\/+$/, "");
17188
+ return platform2 === "win32" ? key.toLowerCase() : key;
17189
+ }
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
+ const CACHE_MS = 6e4;
17311
+ const emptyStatus = () => ({
17312
+ installationStatus: "unchecked",
17313
+ accounts: [],
17314
+ activeLogin: null,
17315
+ environment: { present: false, login: null, state: "unknown" },
17316
+ checkedAt: null
17317
+ });
17318
+ class GitHubAccountsService {
17319
+ constructor(resolver, runner, git, settings, store, changed = () => void 0) {
17320
+ this.resolver = resolver;
17321
+ this.runner = runner;
17322
+ this.git = git;
17323
+ this.settings = settings;
17324
+ this.store = store;
17325
+ this.changed = changed;
17326
+ }
17327
+ resolver;
17328
+ runner;
17329
+ git;
17330
+ settings;
17331
+ store;
17332
+ changed;
17333
+ inventory = null;
17334
+ loading = null;
17335
+ checking = null;
17336
+ detection = null;
17337
+ revision = 0;
17338
+ get authRevision() {
17339
+ return this.revision;
17340
+ }
17341
+ contexts = /* @__PURE__ */ new Map();
17342
+ ssh = /* @__PURE__ */ new Map();
17343
+ identities = /* @__PURE__ */ new Map();
17344
+ status(force = false) {
17345
+ if (force) {
17346
+ this.checking ??= this.check().finally(() => {
17347
+ this.checking = null;
17348
+ });
17349
+ return this.checking.then((value) => structuredClone(value));
17350
+ }
17351
+ this.loading ??= (async () => {
17352
+ this.inventory ??= await this.store?.load() ?? emptyStatus();
17353
+ return this.inventory;
17354
+ })();
17355
+ return this.loading.then(() => structuredClone(this.inventory ?? emptyStatus()));
17356
+ }
17357
+ selection(repository) {
17358
+ return this.settings?.githubAccount(repository.commonDir) ?? { ...AUTOMATIC_GITHUB_ACCOUNT };
17359
+ }
17360
+ context(repository, remote, nameWithOwner) {
17361
+ const cached2 = this.contexts.get(repository.id);
17362
+ if (cached2?.remote === remote && cached2.value.revision === this.revision) return structuredClone(cached2.value);
17363
+ const selection = this.selection(repository);
17364
+ return {
17365
+ selection,
17366
+ nameWithOwner,
17367
+ login: selection.mode === "account" ? selection.login : null,
17368
+ source: selection.mode === "account" ? "explicit" : null,
17369
+ state: "unchecked",
17370
+ checkedAt: null,
17371
+ revision: this.revision
17372
+ };
17373
+ }
17374
+ async setSelection(repository, selection) {
17375
+ if (!this.settings) throw new Error("GitHub account settings are unavailable.");
17376
+ await this.settings.setGitHubAccount(repository.commonDir, selection);
17377
+ this.invalidate();
17378
+ }
17379
+ invalidate() {
17380
+ this.revision++;
17381
+ this.contexts.clear();
17382
+ this.ssh.clear();
17383
+ this.identities.clear();
17384
+ this.changed();
17385
+ }
17386
+ refreshAuthentication() {
17387
+ this.ssh.clear();
17388
+ this.identities.clear();
17389
+ }
17390
+ async detect(force = false) {
17391
+ if (force || !this.detection || Date.now() - this.detection.at > CACHE_MS) {
17392
+ this.detection = { at: Date.now(), promise: selectCli(this.resolver, this.runner, "gh", {
17393
+ forceRefresh: force,
17394
+ runOptions: { cwd: os.homedir(), env: { ...GH_ENV }, removeEnv: ["GH_DEBUG", "DEBUG"] }
17395
+ }) };
17396
+ }
17397
+ return this.detection.promise;
17398
+ }
17399
+ async authenticate(repository, remote, nameWithOwner, operation) {
17400
+ const revision2 = this.revision;
17401
+ const selection = this.selection(repository);
17402
+ let login2 = selection.mode === "account" ? selection.login : null;
17403
+ let source = selection.mode === "account" ? "explicit" : null;
17404
+ try {
17405
+ const detection = await this.detect();
17406
+ if (detection.state !== "available" || !detection.candidate) throw new GhOperationError({
17407
+ code: detection.state === "not-found" ? "GH_CLI_NOT_FOUND" : "GH_PROCESS_FAILED",
17408
+ operation,
17409
+ message: detection.state === "not-found" ? "GitHub CLI is not installed on the backend host." : "GitHub CLI could not run. Check Settings → GitHub."
17410
+ });
17411
+ const executable = detection.candidate;
17412
+ if (selection.mode === "auto" && parseSshRemote(remote)) {
17413
+ const override = executable.env.GIT_SSH_COMMAND || executable.env.GIT_SSH || process.env.GIT_SSH_COMMAND || process.env.GIT_SSH;
17414
+ const configured = await this.git.run(repository.path, ["config", "--default", "", "--get", "core.sshCommand"], {
17415
+ operation: "github-ssh-configuration",
17416
+ readOnly: true,
17417
+ maxOutputBytes: 8 * 1024
17418
+ });
17419
+ if (override || configured.stdout.toString("utf8").trim()) throw new GhOperationError({
17420
+ code: "GH_ACCOUNT_UNRESOLVED",
17421
+ operation,
17422
+ message: "This repository uses a custom SSH command. Choose an explicit account in Settings → GitHub."
17423
+ });
17424
+ const key = `${repository.path}\0${remote}`;
17425
+ let cached2 = this.ssh.get(key);
17426
+ if (!cached2 || Date.now() - cached2.at > CACHE_MS) {
17427
+ cached2 = { at: Date.now(), promise: identifySshAccount(remote, this.runner) };
17428
+ this.ssh.set(key, cached2);
17429
+ }
17430
+ login2 = await cached2.promise;
17431
+ source = "ssh";
17432
+ if (!login2) throw new GhOperationError({
17433
+ code: "GH_ACCOUNT_UNRESOLVED",
17434
+ operation,
17435
+ message: "SSH could not verify a GitHub user account. Check again or choose an explicit account in Settings → GitHub. Deploy keys do not identify user accounts."
17436
+ });
17437
+ }
17438
+ if (!source) source = executable.env.GH_TOKEN || executable.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN ? "environment" : "global";
17439
+ const auth = await accountAuthentication(login2, executable, this.runner, operation);
17440
+ const verified = await this.identity(executable, auth, operation);
17441
+ if (login2 && login2.toLowerCase() !== verified.toLowerCase()) throw new GhOperationError({
17442
+ code: "GH_AUTH_REQUIRED",
17443
+ operation,
17444
+ message: `The saved credentials for @${login2} authenticate a different account. Authenticate @${login2} again and check Settings → GitHub.`
17445
+ });
17446
+ login2 = verified;
17447
+ const context = { selection, nameWithOwner, login: login2, source, state: "ready", checkedAt: (/* @__PURE__ */ new Date()).toISOString(), revision: revision2 };
17448
+ if (revision2 === this.revision) {
17449
+ 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;
17453
+ }
17454
+ this.contexts.set(repository.id, { remote, value: context });
17455
+ }
17456
+ return { auth, executable, context };
17457
+ } catch (error) {
17458
+ 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: {
17460
+ selection,
17461
+ nameWithOwner,
17462
+ login: login2,
17463
+ source,
17464
+ state: "error",
17465
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
17466
+ message,
17467
+ revision: revision2
17468
+ } });
17469
+ if (error instanceof GhOperationError) throw error;
17470
+ throw new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message, retryable: true });
17471
+ }
17472
+ }
17473
+ identity(executable, auth, operation) {
17474
+ const fingerprint = createHash("sha256").update(auth.env?.GH_TOKEN ?? "").digest("hex");
17475
+ let cached2 = this.identities.get(fingerprint);
17476
+ if (!cached2 || Date.now() - cached2.at > CACHE_MS) {
17477
+ const promise = (async () => {
17478
+ const result = await this.runner.run(executable.executable, ["api", "user", "--jq", ".login"], {
17479
+ cwd: os.homedir(),
17480
+ env: { ...executable.env, ...GH_ENV, ...auth.env },
17481
+ ...auth.removeEnv ? { removeEnv: auth.removeEnv } : {},
17482
+ timeoutMs: 15e3,
17483
+ maxOutputBytes: 8 * 1024
17484
+ });
17485
+ const login2 = result.stdout.trim();
17486
+ if (result.exitCode === 0 && GITHUB_LOGIN.test(login2)) return login2;
17487
+ const invalid2 = /401|bad credentials|requires authentication/i.test(result.stderr);
17488
+ throw new GhOperationError({
17489
+ code: invalid2 ? "GH_AUTH_REQUIRED" : "GH_PROCESS_FAILED",
17490
+ operation,
17491
+ message: invalid2 ? "GitHub rejected these credentials. They may be expired or revoked. Authenticate this account again and check Settings → GitHub." : "Could not verify this GitHub account. Check your connection and try again.",
17492
+ retryable: !invalid2
17493
+ });
17494
+ })();
17495
+ cached2 = { at: Date.now(), promise };
17496
+ this.identities.set(fingerprint, cached2);
17497
+ void promise.catch(() => {
17498
+ if (this.identities.get(fingerprint)?.promise === promise) this.identities.delete(fingerprint);
17499
+ });
17500
+ }
17501
+ return cached2.promise;
17502
+ }
17503
+ async check() {
17504
+ await this.status();
17505
+ this.refreshAuthentication();
17506
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
17507
+ const detection = await this.detect(true);
17508
+ const result = {
17509
+ ...emptyStatus(),
17510
+ installationStatus: detection.state,
17511
+ checkedAt,
17512
+ ...detection.version ? { version: detection.version } : {}
17513
+ };
17514
+ if (detection.state === "available" && detection.candidate) {
17515
+ const executable = detection.candidate;
17516
+ try {
17517
+ const status = await this.runner.run(executable.executable, ["auth", "status", "--hostname", "github.com", "--json", "hosts"], {
17518
+ cwd: os.homedir(),
17519
+ env: { ...executable.env, ...GH_ENV },
17520
+ removeEnv: ["GH_TOKEN", ...GH_PRIVATE_ENV],
17521
+ timeoutMs: 6e4,
17522
+ maxOutputBytes: 256 * 1024
17523
+ });
17524
+ if (status.exitCode !== 0) {
17525
+ if (/unknown flag.*json/i.test(status.stderr)) result.installationStatus = "incompatible";
17526
+ throw new Error("Status failed");
17527
+ }
17528
+ Object.assign(result, parseGitHubAuthStatus(status.stdout));
17529
+ const envToken = executable.env.GH_TOKEN || executable.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
17530
+ if (envToken) {
17531
+ result.environment.present = true;
17532
+ try {
17533
+ const auth = await accountAuthentication(null, executable, this.runner, "gh-auth-status");
17534
+ result.environment.login = await this.identity(executable, auth, "gh-auth-status");
17535
+ result.environment.state = "authenticated";
17536
+ } catch (error) {
17537
+ result.environment.state = error instanceof GhOperationError && error.detail.code === "GH_AUTH_REQUIRED" ? "invalid" : "unknown";
17538
+ }
17539
+ }
17540
+ } catch {
17541
+ result.message = result.installationStatus === "incompatible" ? "Update GitHub CLI to a version supporting auth status --json and multiple accounts." : "Could not check the saved GitHub accounts. Check your connection and CLI configuration, then try again.";
17542
+ result.accounts = this.inventory?.accounts.map((account) => ({ ...account, state: "unknown" })) ?? [];
17543
+ result.activeLogin = this.inventory?.activeLogin ?? null;
17544
+ }
17545
+ } else result.message = detection.state === "not-found" ? "Install GitHub CLI on the backend host." : "GitHub CLI was found but could not run. Check its installation and dependencies.";
17546
+ this.inventory = result;
17547
+ try {
17548
+ await this.store?.save(result);
17549
+ } finally {
17550
+ this.invalidate();
17551
+ }
17552
+ return result;
17553
+ }
17554
+ }
17555
+ const authState = _enum(["authenticated", "invalid", "unknown"]);
17556
+ const login = string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/);
17557
+ const schema = object({
17558
+ installationStatus: _enum(["unchecked", "available", "not-found", "not-executable", "incompatible", "inspection-failed"]),
17559
+ version: string().max(200).optional(),
17560
+ accounts: array(object({
17561
+ host: literal("github.com"),
17562
+ login,
17563
+ active: boolean(),
17564
+ state: authState,
17565
+ storage: _enum(["keyring", "file", "unknown"]),
17566
+ scopes: string().max(500).optional()
17567
+ })).max(100),
17568
+ activeLogin: login.nullable(),
17569
+ environment: object({ present: boolean(), login: login.nullable(), state: authState }),
17570
+ checkedAt: string().datetime().nullable(),
17571
+ message: string().max(500).optional()
17572
+ });
17573
+ class GitHubStatusStore {
17574
+ constructor(filePath) {
17575
+ this.filePath = filePath;
17576
+ }
17577
+ filePath;
17578
+ pending = Promise.resolve();
17579
+ async load() {
17580
+ await this.pending;
17581
+ try {
17582
+ if ((await stat(this.filePath)).size > 256 * 1024) return null;
17583
+ const parsed = schema.safeParse(JSON.parse(await readFile(this.filePath, "utf8")));
17584
+ return parsed.success ? parsed.data : null;
17585
+ } catch {
17586
+ return null;
17587
+ }
17588
+ }
17589
+ save(value) {
17590
+ const snapshot = JSON.stringify(schema.parse(value));
17591
+ const write = this.pending.then(() => writeFileAtomically(this.filePath, snapshot, { parseJson: true }));
17592
+ this.pending = write.catch(() => void 0);
17593
+ return write;
17594
+ }
17595
+ }
16379
17596
  const positive = number().int().positive().safe();
16380
17597
  const membershipSchema = object({
16381
17598
  stack: object({ number: positive, size: positive, baseRefName: string().min(1) }).nullable(),
@@ -16443,28 +17660,6 @@ function parsePullRequestStack(raw, prNumber, listing = false) {
16443
17660
  }))
16444
17661
  };
16445
17662
  }
16446
- const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
16447
- const REPO_PATTERN = /^[A-Za-z0-9._-]+$/;
16448
- function parseGitHubRemote(url) {
16449
- const trimmed = url.trim();
16450
- if (!trimmed) return null;
16451
- let rest = null;
16452
- const https = /^https?:\/\/(?:[^@/\s]+@)?github\.com\/(.+)$/i.exec(trimmed);
16453
- const ssh = /^ssh:\/\/(?:[^@/\s]+@)?github\.com(?::\d+)?\/(.+)$/i.exec(trimmed);
16454
- const scp = /^(?:[^@\s]+)@github\.com:(.+)$/i.exec(trimmed);
16455
- const git = /^git:\/\/github\.com\/(.+)$/i.exec(trimmed);
16456
- if (https) rest = https[1] ?? null;
16457
- else if (ssh) rest = ssh[1] ?? null;
16458
- else if (scp) rest = scp[1] ?? null;
16459
- else if (git) rest = git[1] ?? null;
16460
- if (!rest) return null;
16461
- const segments = rest.replace(/\/+$/, "").split("/");
16462
- if (segments.length !== 2) return null;
16463
- const owner = segments[0] ?? "";
16464
- const repo = (segments[1] ?? "").replace(/\.git$/i, "");
16465
- if (!OWNER_PATTERN.test(owner) || !repo || repo === "." || repo === ".." || !REPO_PATTERN.test(repo)) return null;
16466
- return { nameWithOwner: `${owner}/${repo}` };
16467
- }
16468
17663
  const PR_SUMMARY_FIELDS = "number,title,state,isDraft,author,headRefName,baseRefName,updatedAt,url,reviewDecision,additions,deletions,statusCheckRollup";
16469
17664
  const PR_DETAIL_FIELDS = `${PR_SUMMARY_FIELDS},body,changedFiles,labels,commits`;
16470
17665
  const tolerantRecordSchema = looseObject({});
@@ -16560,14 +17755,14 @@ function parseCommits(value) {
16560
17755
  if (typeof record2.oid !== "string" || !record2.oid) return [];
16561
17756
  const authors = Array.isArray(record2.authors) ? record2.authors : [];
16562
17757
  const authorRecord = authors.map((author) => tolerantRecordSchema.safeParse(author)).find((author) => author.success)?.data;
16563
- const login = typeof authorRecord?.login === "string" ? authorRecord.login : null;
16564
- const name = typeof authorRecord?.name === "string" && authorRecord.name ? authorRecord.name : login ?? "unknown";
17758
+ const login2 = typeof authorRecord?.login === "string" ? authorRecord.login : null;
17759
+ const name = typeof authorRecord?.name === "string" && authorRecord.name ? authorRecord.name : login2 ?? "unknown";
16565
17760
  return [{
16566
17761
  oid: record2.oid,
16567
17762
  messageHeadline: typeof record2.messageHeadline === "string" ? record2.messageHeadline : "",
16568
17763
  authoredAt: typeof record2.authoredDate === "string" ? record2.authoredDate : "",
16569
17764
  author: name,
16570
- authorAvatarUrl: login ? avatarUrl(login, login.endsWith("[bot]")) : null
17765
+ authorAvatarUrl: login2 ? avatarUrl(login2, login2.endsWith("[bot]")) : null
16571
17766
  }];
16572
17767
  });
16573
17768
  }
@@ -16586,11 +17781,11 @@ function parseChecksState(value) {
16586
17781
  }
16587
17782
  return pending ? "PENDING" : "PASSING";
16588
17783
  }
16589
- function avatarUrl(login, isBot) {
16590
- if (!login || login === "unknown") return null;
16591
- if (login === "app/dependabot" || login === "dependabot[bot]") return "https://avatars.githubusercontent.com/in/29110?s=40&v=4";
16592
- const botName = login.startsWith("app/") ? login.slice(4) : login.replace(/\[bot\]$/i, "");
16593
- const path2 = isBot || login.startsWith("app/") ? botName : login;
17784
+ function avatarUrl(login2, isBot) {
17785
+ if (!login2 || login2 === "unknown") return null;
17786
+ if (login2 === "app/dependabot" || login2 === "dependabot[bot]") return "https://avatars.githubusercontent.com/in/29110?s=40&v=4";
17787
+ const botName = login2.startsWith("app/") ? login2.slice(4) : login2.replace(/\[bot\]$/i, "");
17788
+ const path2 = isBot || login2.startsWith("app/") ? botName : login2;
16594
17789
  return `https://github.com/${encodeURIComponent(path2)}.png?size=40`;
16595
17790
  }
16596
17791
  function parseJson(raw, operation) {
@@ -16610,64 +17805,112 @@ function validTimestamp(value) {
16610
17805
  function invalidOutput(operation) {
16611
17806
  return new GhOperationError({ code: "GH_INVALID_OUTPUT", operation, message: "GitHub CLI returned an unexpected response." });
16612
17807
  }
16613
- const GH_ENV = { GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", GH_PAGER: "cat", NO_COLOR: "1" };
16614
17808
  const NETWORK_TIMEOUT_MS = 6e4;
16615
17809
  class GitHubService {
16616
- constructor(resolver, runner, git, repositories) {
16617
- this.resolver = resolver;
17810
+ constructor(resolver, runner, git, repositories, accounts) {
16618
17811
  this.runner = runner;
16619
17812
  this.git = git;
16620
17813
  this.repositories = repositories;
17814
+ this.accounts = accounts ?? new GitHubAccountsService(resolver, runner, git);
16621
17815
  }
16622
- resolver;
16623
17816
  runner;
16624
17817
  git;
16625
17818
  repositories;
16626
- statusCache = null;
17819
+ accounts;
16627
17820
  async status(forceRefresh = false) {
16628
- if (!forceRefresh && this.statusCache && Date.now() - this.statusCache.at < 3e4) return this.statusCache.value;
16629
- const value = await this.checkStatus(forceRefresh);
16630
- this.statusCache = { at: Date.now(), value };
16631
- return value;
17821
+ if (forceRefresh) this.accounts.refreshAuthentication();
17822
+ const detected = await this.accounts.detect(forceRefresh);
17823
+ return {
17824
+ installed: !!detected.candidate,
17825
+ availability: detected.state === "available" ? "ready" : "error",
17826
+ authStatus: "unknown",
17827
+ ...detected.version ? { version: detected.version } : {},
17828
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
17829
+ };
17830
+ }
17831
+ async repositoryAccount(repositoryId, forceRefresh = false) {
17832
+ const repository = this.repositories.get(repositoryId);
17833
+ const info = await this.repositoryInfo(repositoryId);
17834
+ const remote = await this.remote(repositoryId);
17835
+ if (forceRefresh && info.nameWithOwner) {
17836
+ this.accounts.refreshAuthentication();
17837
+ try {
17838
+ await this.accounts.authenticate(repository, remote, info.nameWithOwner, "gh-repository-account");
17839
+ } catch {
17840
+ }
17841
+ }
17842
+ return this.accounts.context(repository, remote, info.nameWithOwner);
17843
+ }
17844
+ async setRepositoryAccount(repositoryId, selection) {
17845
+ await this.accounts.setSelection(this.repositories.get(repositoryId), selection);
17846
+ return this.repositoryAccount(repositoryId);
17847
+ }
17848
+ async remote(repositoryId) {
17849
+ const repository = this.repositories.get(repositoryId);
17850
+ try {
17851
+ const output = await this.git.run(repository.path, ["config", "--get", "remote.origin.url"], { operation: "github-remote-url", readOnly: true, maxOutputBytes: 64 * 1024 });
17852
+ return output.stdout.toString("utf8").trim();
17853
+ } catch {
17854
+ return "";
17855
+ }
16632
17856
  }
16633
17857
  async repositoryInfo(repositoryId) {
16634
17858
  const repository = this.repositories.get(repositoryId);
16635
17859
  try {
16636
17860
  const output = await this.git.run(repository.path, ["config", "--get", "remote.origin.url"], { operation: "github-remote-url", readOnly: true, maxOutputBytes: 64 * 1024 });
16637
- const parsed = parseGitHubRemote(output.stdout.toString("utf8"));
17861
+ const url = output.stdout.toString("utf8");
17862
+ let parsed = parseGitHubRemote(url);
17863
+ if (!parsed) {
17864
+ const ssh = parseSshRemote(url);
17865
+ if (ssh) {
17866
+ const config2 = await this.runner.run("ssh", ["-G", ...ssh.port ? ["-p", ssh.port] : [], "--", ssh.destination], {
17867
+ cwd: os.homedir(),
17868
+ timeoutMs: 5e3,
17869
+ maxOutputBytes: 64 * 1024
17870
+ });
17871
+ const hostname = config2.stdout.split(/\r?\n/).find((line) => /^hostname\s/i.test(line))?.trim().split(/\s+/)[1];
17872
+ if (config2.exitCode === 0 && hostname?.toLowerCase() === "github.com") parsed = { nameWithOwner: ssh.nameWithOwner };
17873
+ }
17874
+ }
16638
17875
  return parsed ? { isGitHub: true, nameWithOwner: parsed.nameWithOwner } : { isGitHub: false, nameWithOwner: null };
16639
17876
  } catch {
16640
17877
  return { isGitHub: false, nameWithOwner: null };
16641
17878
  }
16642
17879
  }
16643
17880
  async listPullRequests(repositoryId, states) {
16644
- const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, "gh-pr-list");
17881
+ const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-list");
16645
17882
  const results = await Promise.all(states.map((state) => this.runGh(
16646
17883
  ["pr", "list", "-R", nameWithOwner, "--state", state.toLowerCase(), "--json", PR_SUMMARY_FIELDS, "--limit", "50"],
16647
- { cwd: repository.path, operation: "gh-pr-list", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
17884
+ { auth, executable, cwd: repository.path, operation: "gh-pr-list", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
16648
17885
  )));
16649
- return this.withStackMembership(repository.path, nameWithOwner, selectPullRequestsNewestFirst(results.flatMap((result) => parsePullRequestList(result.stdout)), states));
17886
+ return this.withStackMembership(repository.path, nameWithOwner, auth, executable, selectPullRequestsNewestFirst(results.flatMap((result) => parsePullRequestList(result.stdout)), states));
16650
17887
  }
16651
17888
  async findPullRequestForBranch(repositoryId, branchName) {
16652
17889
  const operation = "gh-pr-for-branch";
16653
- const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
17890
+ const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
16654
17891
  const branch = validateRef(branchName, operation);
17892
+ const openResult = await this.runGh(
17893
+ ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "open", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
17894
+ { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
17895
+ );
17896
+ const openPull = sortPullRequestsNewestFirst(parsePullRequestList(openResult.stdout)).find((pull) => pull.state === "OPEN");
17897
+ if (openPull) return openPull;
16655
17898
  const result = await this.runGh(
16656
17899
  ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "all", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
16657
- { cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
17900
+ { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
16658
17901
  );
16659
17902
  return sortPullRequestsNewestFirst(parsePullRequestList(result.stdout))[0] ?? null;
16660
17903
  }
16661
17904
  async getPullRequest(repositoryId, prNumber) {
16662
- const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, "gh-pr-view");
17905
+ const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-view");
16663
17906
  const result = await this.runGh(
16664
17907
  ["pr", "view", String(prNumber), "-R", nameWithOwner, "--json", PR_DETAIL_FIELDS],
16665
- { cwd: repository.path, operation: "gh-pr-view", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
17908
+ { auth, executable, cwd: repository.path, operation: "gh-pr-view", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
16666
17909
  );
16667
17910
  const details = parsePullRequestDetails(result.stdout);
16668
- return (await this.withStackMembership(repository.path, nameWithOwner, [details]))[0];
17911
+ return (await this.withStackMembership(repository.path, nameWithOwner, auth, executable, [details]))[0];
16669
17912
  }
16670
- async withStackMembership(cwd, nameWithOwner, pulls) {
17913
+ async withStackMembership(cwd, nameWithOwner, auth, executable, pulls) {
16671
17914
  const [owner, name] = nameWithOwner.split("/");
16672
17915
  const enriched = [];
16673
17916
  for (let start = 0; start < pulls.length; start += 25) {
@@ -16675,6 +17918,8 @@ class GitHubService {
16675
17918
  try {
16676
17919
  const numbers = batch.map((pr) => pr.number);
16677
17920
  const result = await this.runGh(["api", "graphql", "-f", `owner=${owner}`, "-f", `name=${name}`, "-f", `query=${stackMembershipQuery(numbers)}`], {
17921
+ auth,
17922
+ executable,
16678
17923
  cwd,
16679
17924
  operation: "gh-pr-stack-membership",
16680
17925
  timeoutMs: 1e4,
@@ -16693,28 +17938,28 @@ class GitHubService {
16693
17938
  }
16694
17939
  async getPullRequestStack(repositoryId, prNumber) {
16695
17940
  const operation = "gh-pr-stack";
16696
- const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
16697
- const options = { cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 };
16698
- const read = async (endpoint) => this.runGh(["api", endpoint], options);
17941
+ const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
17942
+ const options = { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 };
17943
+ const read2 = async (endpoint) => this.runGh(["api", endpoint], options);
16699
17944
  let listing;
16700
17945
  try {
16701
- listing = await read(`repos/${nameWithOwner}/stacks?pull_request=${prNumber}`);
17946
+ listing = await read2(`repos/${nameWithOwner}/stacks?pull_request=${prNumber}`);
16702
17947
  } catch (error) {
16703
17948
  if (error instanceof GhOperationError && error.detail.code === "GH_PROCESS_FAILED" && /HTTP 404/.test(error.message)) return null;
16704
17949
  throw error;
16705
17950
  }
16706
17951
  const stack = parsePullRequestStack(listing.stdout, prNumber, true);
16707
17952
  if (!stack) return null;
16708
- const details = await read(`repos/${nameWithOwner}/stacks/${stack.number}`);
17953
+ const details = await read2(`repos/${nameWithOwner}/stacks/${stack.number}`);
16709
17954
  const complete = parsePullRequestStack(details.stdout, prNumber);
16710
17955
  if (complete?.number !== stack.number) throw new GhOperationError({ code: "GH_INVALID_OUTPUT", operation, message: "The stack changed while loading. Refresh to try again." });
16711
17956
  return complete;
16712
17957
  }
16713
17958
  async getPullRequestDiff(repositoryId, prNumber) {
16714
- const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, "gh-pr-diff");
17959
+ const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-diff");
16715
17960
  const result = await this.runGh(
16716
17961
  ["pr", "diff", String(prNumber), "-R", nameWithOwner],
16717
- { cwd: repository.path, operation: "gh-pr-diff", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
17962
+ { auth, executable, cwd: repository.path, operation: "gh-pr-diff", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
16718
17963
  );
16719
17964
  const patch = result.stdout;
16720
17965
  const binary = /Binary files .* differ|GIT binary patch/.test(patch);
@@ -16722,13 +17967,13 @@ class GitHubService {
16722
17967
  }
16723
17968
  async getPullRequestCommitDiff(repositoryId, oid) {
16724
17969
  const operation = "gh-pr-commit-diff";
16725
- const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
17970
+ const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
16726
17971
  if (!/^[0-9a-f]{40,64}$/i.test(oid)) {
16727
17972
  throw new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: "The commit is not valid." });
16728
17973
  }
16729
17974
  const result = await this.runGh(
16730
17975
  ["api", `repos/${nameWithOwner}/commits/${oid}`, "--header", "Accept: application/vnd.github.diff"],
16731
- { cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
17976
+ { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
16732
17977
  );
16733
17978
  const patch = result.stdout;
16734
17979
  const binary = /Binary files .* differ|GIT binary patch/.test(patch);
@@ -16736,7 +17981,13 @@ class GitHubService {
16736
17981
  }
16737
17982
  async createPullRequest(input) {
16738
17983
  const operation = "gh-pr-create";
16739
- const { repository, nameWithOwner } = await this.requireGitHub(input.repositoryId, operation);
17984
+ const { repository, nameWithOwner, auth, executable, context } = await this.requireGitHub(input.repositoryId, operation);
17985
+ const verifyAccount = () => {
17986
+ if (input.expectedAccount && (input.expectedAccount.revision !== this.accounts.authRevision || input.expectedAccount.revision !== context.revision || input.expectedAccount.login.toLowerCase() !== context.login?.toLowerCase())) {
17987
+ throw new GhOperationError({ code: "GH_ACCOUNT_UNRESOLVED", operation, message: "The GitHub account changed. Review the current identity before creating the pull request." });
17988
+ }
17989
+ };
17990
+ verifyAccount();
16740
17991
  const status = await this.repositories.status(input.repositoryId, false);
16741
17992
  if (status.detached || status.unborn || !status.branch) {
16742
17993
  throw new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: "Check out a branch before creating a pull request." });
@@ -16754,7 +18005,8 @@ class GitHubService {
16754
18005
  await writeFile(bodyPath, input.body, { encoding: "utf8", mode: 384 });
16755
18006
  const args = ["pr", "create", "-R", nameWithOwner, "--title", input.title, "--body-file", bodyPath, "--base", base, "--head", head];
16756
18007
  if (input.draft) args.push("--draft");
16757
- const result = await this.runGh(args, { cwd: repository.path, operation, timeoutMs: 12e4, maxOutputBytes: 1024 * 1024 });
18008
+ verifyAccount();
18009
+ const result = await this.runGh(args, { auth, executable, cwd: repository.path, operation, timeoutMs: 12e4, maxOutputBytes: 1024 * 1024 });
16758
18010
  const created = parseCreatedPullRequestUrl(result.stdout);
16759
18011
  if (!created) throw new GhOperationError({ code: "GH_INVALID_OUTPUT", operation, message: "GitHub CLI did not return the URL of the new pull request." });
16760
18012
  return created;
@@ -16762,49 +18014,32 @@ class GitHubService {
16762
18014
  await rm(temporary, { recursive: true, force: true });
16763
18015
  }
16764
18016
  }
16765
- async checkStatus(forceRefresh) {
16766
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
16767
- const executable = await this.resolver.resolve("gh", forceRefresh);
16768
- if (!executable) {
16769
- return { installed: false, availability: "error", authStatus: "unknown", message: "Install GitHub CLI (cli.github.com) to work with pull requests.", checkedAt };
16770
- }
16771
- const version2 = await this.run(executable, ["--version"], { cwd: os.tmpdir(), operation: "gh-version" });
16772
- const versionLabel = (version2.stdout.split(/\r?\n/, 1)[0] ?? "").trim() || version2.stderr.trim();
16773
- const auth = await this.run(executable, ["auth", "status", "--hostname", "github.com", "--active"], { cwd: os.tmpdir(), operation: "gh-auth-status" });
16774
- if (auth.exitCode !== 0) {
16775
- return { installed: true, availability: "error", authStatus: "unauthenticated", version: versionLabel, message: "Run gh auth login.", checkedAt };
16776
- }
16777
- return { installed: true, availability: "ready", authStatus: "authenticated", version: versionLabel, checkedAt };
16778
- }
16779
18017
  async requireGitHub(repositoryId, operation) {
16780
18018
  const repository = this.repositories.get(repositoryId);
16781
18019
  const info = await this.repositoryInfo(repositoryId);
16782
- if (!info.isGitHub || !info.nameWithOwner) {
16783
- throw new GhOperationError({ code: "GH_NOT_GITHUB_REPO", operation, message: "The origin remote does not point to a GitHub repository." });
16784
- }
16785
- const status = await this.status();
16786
- if (!status.installed) throw new GhOperationError({ code: "GH_CLI_NOT_FOUND", operation, message: "GitHub CLI is not installed." });
16787
- if (status.authStatus === "unauthenticated") throw new GhOperationError({ code: "GH_AUTH_REQUIRED", operation, message: "Sign in with gh auth login to continue." });
16788
- return { repository, nameWithOwner: info.nameWithOwner };
18020
+ if (!info.isGitHub || !info.nameWithOwner) throw new GhOperationError({ code: "GH_NOT_GITHUB_REPO", operation, message: "The origin remote does not point to a GitHub repository." });
18021
+ const remote = await this.remote(repositoryId);
18022
+ const authenticated = await this.accounts.authenticate(repository, remote, info.nameWithOwner, operation);
18023
+ return { repository, nameWithOwner: info.nameWithOwner, ...authenticated };
16789
18024
  }
16790
18025
  async runGh(args, options) {
16791
- const executable = await this.resolver.resolve("gh");
16792
- if (!executable) throw new GhOperationError({ code: "GH_CLI_NOT_FOUND", operation: options.operation, message: "GitHub CLI is not installed." });
16793
- const result = await this.run(executable, args, options);
16794
- if (result.exitCode !== 0) throw classifyFailure(result, options.operation);
18026
+ const result = await this.run(options.executable, args, options);
18027
+ if (result.exitCode !== 0) throw classifyFailure({ ...result, stdout: redactCommandToken(result.stdout, options.auth), stderr: redactCommandToken(result.stderr, options.auth) }, options.operation);
16795
18028
  return result;
16796
18029
  }
16797
18030
  async run(executable, args, options) {
16798
18031
  try {
16799
- return await this.runner.run(executable, args, {
18032
+ return await this.runner.run(executable.executable, args, {
16800
18033
  cwd: options.cwd,
16801
- env: { ...GH_ENV },
18034
+ env: { ...executable.env, ...GH_ENV, ...options.auth?.env },
18035
+ ...options.auth?.removeEnv ? { removeEnv: options.auth.removeEnv } : {},
16802
18036
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
16803
18037
  ...options.maxOutputBytes !== void 0 ? { maxOutputBytes: options.maxOutputBytes } : {},
16804
18038
  ...options.stdin !== void 0 ? { stdin: options.stdin } : {}
16805
18039
  });
16806
18040
  } catch (error) {
16807
- throw mapRunnerError(error, options.operation);
18041
+ const mapped = mapRunnerError(error, options.operation);
18042
+ throw new GhOperationError({ ...mapped.detail, message: redactCommandToken(mapped.message, options.auth ?? {}) });
16808
18043
  }
16809
18044
  }
16810
18045
  }
@@ -16828,6 +18063,8 @@ ${result.stdout}`.toLowerCase();
16828
18063
  if (/could not resolve|no such host|connection refused|connection reset|network|dial tcp|tls handshake/.test(raw)) {
16829
18064
  return new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: "Could not connect to GitHub. Check your network and try again.", exitCode: result.exitCode, retryable: true });
16830
18065
  }
18066
+ if (/403|resource not accessible|insufficient.*scope|saml|sso/.test(raw)) return new GhOperationError({ code: "GH_ACCESS_DENIED", operation, message: "This GitHub account cannot perform the operation. Check repository permissions, token permissions and organisation SSO policies.", exitCode: result.exitCode });
18067
+ if (/404|could not resolve to a repository/.test(raw) && operation !== "gh-pr-stack") return new GhOperationError({ code: "GH_ACCESS_DENIED", operation, message: "The repository was not found or is not accessible with this GitHub account.", exitCode: result.exitCode });
16831
18068
  const detail = firstLine(result.stderr) || firstLine(result.stdout);
16832
18069
  return new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: detail || "GitHub CLI could not complete the operation.", exitCode: result.exitCode });
16833
18070
  }
@@ -16843,6 +18080,50 @@ function mapRunnerError(error, operation) {
16843
18080
  function firstLine(value) {
16844
18081
  return (value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "").slice(0, 300);
16845
18082
  }
18083
+ const statusSchema = object({
18084
+ id: _enum(["codex", "claude", "opencode", "grok"]),
18085
+ label: string(),
18086
+ availability: _enum(["ready", "warning", "error"]),
18087
+ installed: boolean(),
18088
+ authStatus: _enum(["authenticated", "unauthenticated", "unknown"]),
18089
+ installationStatus: _enum(["available", "not-found", "not-executable", "incompatible", "inspection-failed"]).optional(),
18090
+ executablePath: string().optional(),
18091
+ executableSource: _enum(["configured", "process-path", "user-path", "known-location"]).optional(),
18092
+ discoveryWarning: string().optional(),
18093
+ version: string().optional(),
18094
+ message: string().optional(),
18095
+ cliName: string().optional(),
18096
+ models: array(object({ id: string(), label: string(), description: string().optional() })),
18097
+ checkedAt: string().datetime()
18098
+ });
18099
+ const cacheSchema = object({
18100
+ version: literal(1),
18101
+ key: string(),
18102
+ statuses: array(statusSchema).min(1).max(4)
18103
+ }).refine(({ statuses }) => new Set(statuses.map((status) => status.id)).size === statuses.length);
18104
+ class AiStatusStore {
18105
+ constructor(filePath) {
18106
+ this.filePath = filePath;
18107
+ }
18108
+ filePath;
18109
+ pending = Promise.resolve();
18110
+ async load(key) {
18111
+ await this.pending;
18112
+ try {
18113
+ if ((await stat(this.filePath)).size > 4 * 1024 * 1024) return null;
18114
+ const parsed = cacheSchema.safeParse(JSON.parse(await readFile(this.filePath, "utf8")));
18115
+ return parsed.success && parsed.data.key === key ? parsed.data.statuses : null;
18116
+ } catch {
18117
+ return null;
18118
+ }
18119
+ }
18120
+ save(key, statuses) {
18121
+ const snapshot = cacheSchema.parse({ version: 1, key, statuses });
18122
+ const write = this.pending.then(() => writeFileAtomically(this.filePath, JSON.stringify(snapshot), { parseJson: true }));
18123
+ this.pending = write.catch(() => void 0);
18124
+ return write;
18125
+ }
18126
+ }
16846
18127
  const MAX_PROBLEM_LOG_ENTRIES = 500;
16847
18128
  const PROBLEM_LOG_COMPACTION_SLACK = 1.25;
16848
18129
  const LEVELS = ["error", "warn"];
@@ -17246,6 +18527,7 @@ function normalizeRemoteFetchIntervalSeconds(value) {
17246
18527
  const SETTINGS_SCHEMA_VERSION = 1;
17247
18528
  const defaults = {
17248
18529
  version: SETTINGS_SCHEMA_VERSION,
18530
+ githubAccounts: {},
17249
18531
  recentRepositories: [],
17250
18532
  repositoryProjects: [],
17251
18533
  filesTreeStates: [],
@@ -17263,6 +18545,7 @@ const defaults = {
17263
18545
  monoFont: "jetbrains-mono",
17264
18546
  commitMessageHarness: "codex",
17265
18547
  commitMessageModels: { codex: "default", claude: "default", opencode: "default", grok: "default" },
18548
+ aiExecutablePaths: {},
17266
18549
  shortcutOverrides: {},
17267
18550
  doubleControlShortcutEnabled: true,
17268
18551
  remoteFetchIntervalSeconds: DEFAULT_REMOTE_FETCH_INTERVAL_SECONDS
@@ -17271,6 +18554,7 @@ const defaults = {
17271
18554
  };
17272
18555
  const settingsRecordSchema = looseObject({
17273
18556
  version: unknown().optional(),
18557
+ githubAccounts: unknown().optional(),
17274
18558
  recentRepositories: unknown().optional(),
17275
18559
  repositoryProjects: unknown().optional(),
17276
18560
  filesTreeStates: unknown().optional(),
@@ -17342,11 +18626,30 @@ class SettingsStore {
17342
18626
  return this.data.activeRepositoryId;
17343
18627
  }
17344
18628
  get preferences() {
17345
- return { ...this.data.preferences, commitMessageModels: { ...this.data.preferences.commitMessageModels }, shortcutOverrides: { ...this.data.preferences.shortcutOverrides } };
18629
+ return { ...this.data.preferences, aiExecutablePaths: { ...this.data.preferences.aiExecutablePaths }, commitMessageModels: { ...this.data.preferences.commitMessageModels }, shortcutOverrides: { ...this.data.preferences.shortcutOverrides } };
17346
18630
  }
17347
18631
  get windowBounds() {
17348
18632
  return { ...this.data.windowBounds };
17349
18633
  }
18634
+ githubAccount(commonDir) {
18635
+ return { ...this.data.githubAccounts[githubRepositoryKey(commonDir)] ?? AUTOMATIC_GITHUB_ACCOUNT };
18636
+ }
18637
+ async setGitHubAccount(commonDir, selection) {
18638
+ const parsed = githubAccountSelectionSchema.parse(selection);
18639
+ const key = githubRepositoryKey(commonDir);
18640
+ const previous = this.data.githubAccounts[key];
18641
+ if (parsed.mode === "auto") delete this.data.githubAccounts[key];
18642
+ else this.data.githubAccounts[key] = parsed;
18643
+ try {
18644
+ await this.save();
18645
+ } catch (error) {
18646
+ if (this.data.githubAccounts[key] === (parsed.mode === "auto" ? void 0 : parsed)) {
18647
+ if (previous) this.data.githubAccounts[key] = previous;
18648
+ else delete this.data.githubAccounts[key];
18649
+ }
18650
+ throw error;
18651
+ }
18652
+ }
17350
18653
  async touchRepository(repository) {
17351
18654
  const recent = { ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() };
17352
18655
  this.data.recentRepositories = [recent, ...this.data.recentRepositories.filter((item) => item.id !== repository.id)];
@@ -17362,6 +18665,12 @@ class SettingsStore {
17362
18665
  async relocateRepository(previousId, repository) {
17363
18666
  const previous = this.data.recentRepositories.find((item) => item.id === previousId);
17364
18667
  if (!previous) throw projectError("Unknown repository.");
18668
+ const previousAccountKey = githubRepositoryKey(previous.commonDir);
18669
+ const nextAccountKey = githubRepositoryKey(repository.commonDir);
18670
+ if (previousAccountKey !== nextAccountKey && this.data.githubAccounts[previousAccountKey]) {
18671
+ this.data.githubAccounts[nextAccountKey] ??= this.data.githubAccounts[previousAccountKey];
18672
+ delete this.data.githubAccounts[previousAccountKey];
18673
+ }
17365
18674
  const previousKey = normalizeRepositoryKey(previous.commonDir);
17366
18675
  const nextKey = normalizeRepositoryKey(repository.commonDir);
17367
18676
  const destinationAlreadyAssigned = this.data.repositoryProjects.some((project) => project.repositoryKeys.some((key) => key === nextKey && key !== previousKey));
@@ -17386,6 +18695,9 @@ class SettingsStore {
17386
18695
  async forgetRepository(repositoryKey) {
17387
18696
  const key = normalizeRepositoryKey(repositoryKey);
17388
18697
  const removedIds = new Set(this.data.recentRepositories.filter((item) => normalizeRepositoryKey(item.commonDir) === key).map((item) => item.id));
18698
+ for (const item of this.data.recentRepositories) {
18699
+ if (removedIds.has(item.id)) delete this.data.githubAccounts[githubRepositoryKey(item.commonDir)];
18700
+ }
17389
18701
  this.data.recentRepositories = this.data.recentRepositories.filter((item) => !removedIds.has(item.id));
17390
18702
  this.data.repositoryProjects = this.data.repositoryProjects.map((project) => ({
17391
18703
  ...project,
@@ -17471,6 +18783,8 @@ class SettingsStore {
17471
18783
  next.monoFont = isMonoFont(next.monoFont) ? next.monoFont : "jetbrains-mono";
17472
18784
  next.commitMessageHarness = isHarness(next.commitMessageHarness) ? next.commitMessageHarness : "codex";
17473
18785
  next.commitMessageModels = modelPreferences(next.commitMessageModels);
18786
+ next.aiExecutablePaths = executablePaths(next.aiExecutablePaths, true);
18787
+ delete next.aiShellEnvironment;
17474
18788
  next.shortcutOverrides = sanitizeShortcutOverrides(next.shortcutOverrides);
17475
18789
  next.doubleControlShortcutEnabled = typeof next.doubleControlShortcutEnabled === "boolean" ? next.doubleControlShortcutEnabled : this.defaultDoubleControlShortcutEnabled;
17476
18790
  next.remoteFetchIntervalSeconds = normalizeRemoteFetchIntervalSeconds(next.remoteFetchIntervalSeconds);
@@ -17599,6 +18913,7 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
17599
18913
  monoFont: isMonoFont(parsedPreferences.data.monoFont) ? parsedPreferences.data.monoFont : "jetbrains-mono",
17600
18914
  commitMessageHarness: isHarness(parsedPreferences.data.commitMessageHarness) ? parsedPreferences.data.commitMessageHarness : "codex",
17601
18915
  commitMessageModels: modelPreferences(parsedPreferences.data.commitMessageModels),
18916
+ aiExecutablePaths: executablePaths(parsedPreferences.data.aiExecutablePaths),
17602
18917
  shortcutOverrides: sanitizeShortcutOverrides(parsedPreferences.data.shortcutOverrides),
17603
18918
  doubleControlShortcutEnabled: typeof parsedPreferences.data.doubleControlShortcutEnabled === "boolean" ? parsedPreferences.data.doubleControlShortcutEnabled : defaultDoubleControlShortcutEnabled,
17604
18919
  remoteFetchIntervalSeconds: normalizeRemoteFetchIntervalSeconds(
@@ -17610,6 +18925,7 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
17610
18925
  const windowBounds = bounds && typeof bounds.width === "number" && Number.isFinite(bounds.width) && typeof bounds.height === "number" && Number.isFinite(bounds.height) ? { width: Math.max(900, bounds.width), height: Math.max(600, bounds.height), ...typeof bounds.x === "number" && Number.isFinite(bounds.x) ? { x: bounds.x } : {}, ...typeof bounds.y === "number" && Number.isFinite(bounds.y) ? { y: bounds.y } : {} } : { ...defaults.windowBounds };
17611
18926
  return {
17612
18927
  version: SETTINGS_SCHEMA_VERSION,
18928
+ githubAccounts: normalizeGitHubAccounts(input.githubAccounts),
17613
18929
  recentRepositories,
17614
18930
  repositoryProjects,
17615
18931
  filesTreeStates,
@@ -17706,6 +19022,41 @@ function normalizeRecent(item) {
17706
19022
  repositoryName: typeof item.repositoryName === "string" && item.repositoryName ? item.repositoryName : path.basename(path.dirname(commonDir))
17707
19023
  };
17708
19024
  }
19025
+ function executablePaths(value, strict = false, platform2 = process.platform, home = os.homedir()) {
19026
+ const result = {};
19027
+ const invalid2 = () => {
19028
+ if (strict) throw projectError("CLI executable paths must be absolute paths on the backend host, without arguments or control characters.");
19029
+ };
19030
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
19031
+ invalid2();
19032
+ return result;
19033
+ }
19034
+ const p = platform2 === "win32" ? path.win32 : path.posix;
19035
+ for (const [key, raw] of Object.entries(value)) {
19036
+ if (!isHarness(key) || typeof raw !== "string") {
19037
+ invalid2();
19038
+ continue;
19039
+ }
19040
+ if (!raw.trim()) continue;
19041
+ const expanded = raw.startsWith("~/") ? p.join(home, raw.slice(2)) : raw;
19042
+ if (raw.length > 4096 || hasControlCharacters$1(raw) || !p.isAbsolute(expanded) || platform2 === "win32" && !/^(?:[a-z]:[\\/]|\\\\)/i.test(expanded)) {
19043
+ invalid2();
19044
+ continue;
19045
+ }
19046
+ result[key] = expanded;
19047
+ }
19048
+ return result;
19049
+ }
19050
+ function normalizeGitHubAccounts(value) {
19051
+ const result = /* @__PURE__ */ Object.create(null);
19052
+ if (!value || typeof value !== "object" || Array.isArray(value)) return result;
19053
+ for (const [key, selection] of Object.entries(value).slice(0, 1e3)) {
19054
+ if (!key || key.length > MAX_REPOSITORY_KEY_LENGTH || hasControlCharacters$1(key) || !path.isAbsolute(key)) continue;
19055
+ const parsed = githubAccountSelectionSchema.safeParse(selection);
19056
+ if (parsed.success && parsed.data.mode === "account") result[githubRepositoryKey(key)] = parsed.data;
19057
+ }
19058
+ return result;
19059
+ }
17709
19060
  class SystemTrash {
17710
19061
  available;
17711
19062
  operation;
@@ -17776,6 +19127,9 @@ class OpenTigRuntime {
17776
19127
  this.lastNotifiedAt = Date.now();
17777
19128
  this.onEvent({ type: "repository.changed", repositoryId, scope });
17778
19129
  }
19130
+ publishGitHubAccountsChange() {
19131
+ if (!this.closePromise) this.onEvent({ type: "github.accounts-changed" });
19132
+ }
17779
19133
  publishActiveRepositoryChange(repository) {
17780
19134
  if (!this.closePromise) this.onEvent({ type: "repository.active-changed", repository });
17781
19135
  }
@@ -17824,7 +19178,7 @@ async function createOpenTigRuntime(options) {
17824
19178
  const fileHistory = new FileOperationHistory(files, trash);
17825
19179
  const search = new SearchService(git, repositories, files, fileHistory);
17826
19180
  const operations = new GitRepositoryOperations(git, repositories, files);
17827
- const cliResolver = new CliResolver();
19181
+ const cliResolver = new CliResolver(void 0, () => settings.preferences);
17828
19182
  const cliRunner = new CliProcessRunner();
17829
19183
  const providers = [
17830
19184
  new CodexProvider(cliResolver, cliRunner),
@@ -17834,10 +19188,21 @@ async function createOpenTigRuntime(options) {
17834
19188
  ];
17835
19189
  const aiLog = new AiLogStore(options.aiLogPath);
17836
19190
  await aiLog.load();
17837
- const ai = new CommitMessageService(operations, providers, aiLog);
19191
+ const ai = new CommitMessageService(operations, providers, aiLog, () => cliResolver.invalidate(), {
19192
+ store: new AiStatusStore(path.join(path.dirname(options.settingsPath), "ai-statuses.json")),
19193
+ key: () => aiExecutablePathsKey(settings.preferences.aiExecutablePaths)
19194
+ });
17838
19195
  const prDrafts = new PullRequestDraftService(operations, providers, aiLog);
17839
- const github = new GitHubService(cliResolver, cliRunner, git, repositories);
17840
19196
  let runtime = null;
19197
+ const githubAccounts = new GitHubAccountsService(
19198
+ cliResolver,
19199
+ cliRunner,
19200
+ git,
19201
+ settings,
19202
+ new GitHubStatusStore(path.join(path.dirname(options.settingsPath), "github-status.json")),
19203
+ () => runtime?.publishGitHubAccountsChange()
19204
+ );
19205
+ const github = new GitHubService(cliResolver, cliRunner, git, repositories, githubAccounts);
17841
19206
  const events = {
17842
19207
  repositoryChanged: (repositoryId, scope) => {
17843
19208
  runtime?.publishRepositoryChange(repositoryId, scope);
@@ -18046,6 +19411,8 @@ const IPC = {
18046
19411
  commitUndoLatest: "commit:undo-latest",
18047
19412
  commitsList: "commits:list",
18048
19413
  commitsFiles: "commits:files",
19414
+ commitAuthorship: "commits:authorship",
19415
+ commitSetAuthorship: "commits:set-authorship",
18049
19416
  branchesList: "refs:branches",
18050
19417
  branchSwitch: "refs:switch",
18051
19418
  worktreesList: "refs:worktrees",
@@ -18058,6 +19425,10 @@ const IPC = {
18058
19425
  worktreeDetails: "refs:worktree-details",
18059
19426
  branchDelete: "refs:delete-branch",
18060
19427
  worktreeRemove: "refs:remove-worktree",
19428
+ remoteBranchDetails: "refs:remote-branch-details",
19429
+ trackingBranchCreate: "refs:create-tracking-branch",
19430
+ remoteBranchDelete: "refs:delete-remote-branch",
19431
+ branchesFetch: "refs:fetch-branches",
18061
19432
  aiStatuses: "ai:statuses",
18062
19433
  aiGenerateCommitMessage: "ai:generate-commit-message",
18063
19434
  aiCancelGeneration: "ai:cancel-generation",
@@ -18066,6 +19437,9 @@ const IPC = {
18066
19437
  diagnosticsList: "diagnostics:list",
18067
19438
  diagnosticsClear: "diagnostics:clear",
18068
19439
  diagnosticsRecord: "diagnostics:record",
19440
+ githubAccountsStatus: "github:accounts-status",
19441
+ githubRepositoryAccount: "github:repository-account",
19442
+ githubSetRepositoryAccount: "github:set-repository-account",
18069
19443
  githubStatus: "github:status",
18070
19444
  githubRepositoryInfo: "github:repository-info",
18071
19445
  githubPrForBranch: "github:pr-for-branch",
@@ -18131,6 +19505,8 @@ const OPEN_TIG_SERVER_COMMANDS = {
18131
19505
  "index.updateConflict": command(IPC.indexUpdateConflict, "update-conflict", true, { maxRequestBytes: CONTENT_MAX_REQUEST_BYTES }),
18132
19506
  "index.resolveConflict": command(IPC.indexResolveConflict, "resolve-conflict", true, { maxRequestBytes: CONTENT_MAX_REQUEST_BYTES }),
18133
19507
  "commits.create": command(IPC.commitCreate, "commit", true),
19508
+ "commits.authorship": command(IPC.commitAuthorship, "commit-authorship-read", false),
19509
+ "commits.setAuthorship": command(IPC.commitSetAuthorship, "commit-authorship-set", true),
18134
19510
  "commits.undoLatest": command(IPC.commitUndoLatest, "undo-latest-commit", true),
18135
19511
  "commits.list": command(IPC.commitsList, "history", false),
18136
19512
  "commits.files": command(IPC.commitsFiles, "commit-files", false),
@@ -18143,6 +19519,10 @@ const OPEN_TIG_SERVER_COMMANDS = {
18143
19519
  "refs.fetch": command(IPC.refsFetch, "fetch", true),
18144
19520
  "refs.localRefsSnapshot": command(IPC.localRefsSnapshot, "local-refs-snapshot", false),
18145
19521
  "refs.branchDetails": command(IPC.branchDetails, "branch-details", false),
19522
+ "refs.remoteBranchDetails": command(IPC.remoteBranchDetails, "remote-branch-details", false, { timeoutMs: 12e4 }),
19523
+ "refs.createTrackingBranch": command(IPC.trackingBranchCreate, "create-tracking-branch", true),
19524
+ "refs.deleteRemoteBranch": command(IPC.remoteBranchDelete, "delete-remote-branch", true, { timeoutMs: 18e4 }),
19525
+ "refs.fetchBranches": command(IPC.branchesFetch, "fetch-branches", true, { timeoutMs: 18e4 }),
18146
19526
  "refs.worktreeDetails": command(IPC.worktreeDetails, "worktree-details", false),
18147
19527
  "refs.deleteBranch": command(IPC.branchDelete, "delete-branch", true),
18148
19528
  "refs.removeWorktree": command(IPC.worktreeRemove, "remove-worktree", true),
@@ -18151,6 +19531,9 @@ const OPEN_TIG_SERVER_COMMANDS = {
18151
19531
  "ai.log": command(IPC.aiLog, "ai-log", false),
18152
19532
  "ai.clearLog": command(IPC.aiClearLog, "ai-clear-log", true),
18153
19533
  "ai.cancelGeneration": command(IPC.aiCancelGeneration, "ai-cancel-generation", true),
19534
+ "github.accountsStatus": command(IPC.githubAccountsStatus, "gh-accounts-status", false, { timeoutMs: 9e4 }),
19535
+ "github.repositoryAccount": command(IPC.githubRepositoryAccount, "gh-repository-account", false, { timeoutMs: 6e4 }),
19536
+ "github.setRepositoryAccount": command(IPC.githubSetRepositoryAccount, "gh-set-repository-account", true),
18154
19537
  "github.status": command(IPC.githubStatus, "gh-status", false),
18155
19538
  "github.repositoryInfo": command(IPC.githubRepositoryInfo, "gh-repository-info", false),
18156
19539
  "github.findPullRequestForBranch": command(IPC.githubPrForBranch, "gh-pr-for-branch", false),
@@ -18314,29 +19697,73 @@ function mimeTypeOf(relativePath, bytes) {
18314
19697
  if (fromPath === "image/svg+xml") return fromPath;
18315
19698
  return detectRasterImageMime(bytes) ?? fromPath;
18316
19699
  }
18317
- async function browseServerDirectories(requestedPath) {
18318
- const input = requestedPath ?? homedir();
18319
- const expanded = input === "~" ? homedir() : /^~[/\\]/.test(input) ? join(homedir(), input.slice(2)) : input;
18320
- if (!isAbsolute(expanded)) throw new Error("Enter an absolute folder path on the server.");
19700
+ async function browseServerDirectories(requestedPath, git = new GitProcess()) {
19701
+ const home = homedir();
19702
+ const input = requestedPath ?? home;
19703
+ const expanded = input === "~" ? home : /^~[/\\]/.test(input) ? join(home, input.slice(2)) : input;
19704
+ if (!isAbsolute(expanded)) throw new Error("Enter an absolute folder path.");
18321
19705
  const path2 = resolve(expanded);
18322
- const entries = await readdir(path2, { withFileTypes: true });
19706
+ let entries;
19707
+ try {
19708
+ entries = await readdir(path2, { withFileTypes: true });
19709
+ } catch (error) {
19710
+ const code = error.code;
19711
+ if (code === "EACCES" || code === "EPERM") throw new Error("You do not have permission to browse this folder.", { cause: error });
19712
+ if (code === "ENOENT") throw new Error("That folder was not found. Check the path and try again.", { cause: error });
19713
+ if (code === "ENOTDIR") throw new Error("This path points to a file. Choose a folder instead.", { cause: error });
19714
+ throw new Error("Could not read this folder. Try another location.", { cause: error });
19715
+ }
18323
19716
  const directories = [];
18324
- for (const entry of entries) {
18325
- const entryPath = join(path2, entry.name);
18326
- if (entry.isDirectory() || entry.isSymbolicLink() && await stat(entryPath).then((value) => value.isDirectory(), () => false)) {
18327
- directories.push({ name: entry.name, path: entryPath });
19717
+ let index = 0;
19718
+ await Promise.all(Array.from({ length: Math.min(6, entries.length) }, async () => {
19719
+ while (index < entries.length) {
19720
+ const entry = entries[index++];
19721
+ const entryPath = join(path2, entry.name);
19722
+ if (entry.isDirectory() || entry.isSymbolicLink() && await stat(entryPath).then((value) => value.isDirectory(), () => false)) {
19723
+ directories.push({ name: entry.name, path: entryPath, repository: await inspectRepository(entryPath, git) });
19724
+ }
18328
19725
  }
18329
- }
19726
+ }));
18330
19727
  directories.sort((a2, b) => a2.name.localeCompare(b.name, void 0, { numeric: true, sensitivity: "base" }));
19728
+ const locations = [{ name: "Home", path: home, kind: "home" }];
19729
+ for (const [name, kind] of [["Documents", "documents"], ["Downloads", "downloads"]]) {
19730
+ const location = join(home, name);
19731
+ if (await stat(location).then((value) => value.isDirectory(), () => false)) locations.push({ name, path: location, kind });
19732
+ }
18331
19733
  const parentPath = dirname(path2);
18332
- return { path: path2, parentPath: parentPath === path2 ? null : parentPath, directories };
19734
+ return { path: path2, parentPath: parentPath === path2 ? null : parentPath, repository: await inspectRepository(path2, git), locations, directories };
19735
+ }
19736
+ async function inspectRepository(path2, git) {
19737
+ if (!await stat(join(path2, ".git")).then((value) => value.isDirectory() || value.isFile(), () => false)) return null;
19738
+ try {
19739
+ const result = await git.run(path2, ["rev-parse", "--is-inside-work-tree", "--absolute-git-dir"], {
19740
+ operation: "browse-repository",
19741
+ readOnly: true,
19742
+ timeoutMs: 2e3,
19743
+ maxOutputBytes: 32768
19744
+ });
19745
+ const [inside, gitDir] = result.stdout.toString("utf8").trim().split(/\r?\n/);
19746
+ if (inside !== "true" || !gitDir || !isAbsolute(gitDir)) return null;
19747
+ const headFile = await open(join(gitDir, "HEAD"), "r");
19748
+ let head;
19749
+ try {
19750
+ const buffer = Buffer.alloc(4096);
19751
+ const { bytesRead } = await headFile.read(buffer, 0, buffer.length, 0);
19752
+ head = buffer.subarray(0, bytesRead).toString("utf8").trim();
19753
+ } finally {
19754
+ await headFile.close();
19755
+ }
19756
+ return { branch: head.startsWith("ref: refs/heads/") ? head.slice("ref: refs/heads/".length) : null };
19757
+ } catch {
19758
+ return null;
19759
+ }
18333
19760
  }
18334
19761
  const requestRecordSchema = looseObject({});
18335
19762
  function boundedStringSchema(maxLength, options = {}) {
18336
- let schema = string().max(maxLength).refine((value) => !value.includes("\0"));
18337
- if (options.empty !== true) schema = schema.min(1);
18338
- if (options.controls === false) return schema;
18339
- return schema.refine((value) => ![...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127));
19763
+ let schema2 = string().max(maxLength).refine((value) => !value.includes("\0"));
19764
+ if (options.empty !== true) schema2 = schema2.min(1);
19765
+ if (options.controls === false) return schema2;
19766
+ return schema2.refine((value) => ![...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127));
18340
19767
  }
18341
19768
  const booleanWithDefaultSchema = (fallback) => boolean().optional().default(fallback);
18342
19769
  function parseRecord(value) {
@@ -18487,6 +19914,22 @@ function deleteBranchArg(value, operation) {
18487
19914
  force: booleanArg(request.force, operation)
18488
19915
  };
18489
19916
  }
19917
+ function createTrackingBranchArg(value) {
19918
+ const operation = "create-tracking-branch";
19919
+ const request = requestObject(value, operation);
19920
+ return {
19921
+ ...branchDetailsArg(request, operation),
19922
+ expectedOid: oidArg(request.expectedOid, operation),
19923
+ localName: refsString(request.localName, operation, MAX_BRANCH_REF_LENGTH)
19924
+ };
19925
+ }
19926
+ function deleteRemoteBranchArg(value) {
19927
+ const operation = "delete-remote-branch";
19928
+ const request = requestObject(value, operation);
19929
+ const destinationId = refsString(request.destinationId, operation, 64);
19930
+ if (!/^[0-9a-f]{64}$/.test(destinationId)) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation, message: "Invalid remote destination." });
19931
+ return { ...branchDetailsArg(request, operation), expectedOid: oidArg(request.expectedOid, operation), destinationId };
19932
+ }
18490
19933
  function removeWorktreeArg(value, operation) {
18491
19934
  const request = requestObject(value, operation);
18492
19935
  return {
@@ -18584,7 +20027,16 @@ function createPullRequestArg(value) {
18584
20027
  if (typeof input.base !== "string" || !input.base || input.base.length > 300 || input.base.includes("\0")) throw invalidGh(operation);
18585
20028
  if (typeof input.draft !== "boolean") throw invalidGh(operation);
18586
20029
  if (typeof input.repositoryId !== "string" || !input.repositoryId || input.repositoryId.length > 64) throw invalidGh(operation);
18587
- return { repositoryId: input.repositoryId, title: input.title.trim(), body: input.body, base: input.base, draft: input.draft };
20030
+ const expected = input.expectedAccount;
20031
+ if (expected !== void 0 && (!expected || typeof expected.login !== "string" || !/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/.test(expected.login) || !Number.isSafeInteger(expected.revision) || expected.revision < 0)) throw invalidGh(operation);
20032
+ return {
20033
+ repositoryId: input.repositoryId,
20034
+ title: input.title.trim(),
20035
+ body: input.body,
20036
+ base: input.base,
20037
+ draft: input.draft,
20038
+ ...expected ? { expectedAccount: { login: expected.login, revision: expected.revision } } : {}
20039
+ };
18588
20040
  }
18589
20041
  function generatePullRequestDraftArg(value) {
18590
20042
  const operation = "ai-pr-draft";
@@ -18672,7 +20124,9 @@ function registerServerCommands(registry2, services, host) {
18672
20124
  };
18673
20125
  });
18674
20126
  handle(IPC.preferences, "preferences", async (partial2) => {
20127
+ const previousPaths = aiExecutablePathsKey(services.settings.preferences.aiExecutablePaths);
18675
20128
  const preferences = await services.settings.setPreferences(partial2 ?? {});
20129
+ if (previousPaths !== aiExecutablePathsKey(preferences.aiExecutablePaths)) services.ai.invalidateStatuses();
18676
20130
  return preferences;
18677
20131
  });
18678
20132
  handle(IPC.filesTreeStateUpdate, "files-tree-state", (repositoryId, expandedPaths) => {
@@ -18692,7 +20146,8 @@ function registerServerCommands(registry2, services, host) {
18692
20146
  context
18693
20147
  ));
18694
20148
  handle(IPC.repositoryBrowseDirectories, "browse-directories", (path2) => browseServerDirectories(
18695
- path2 === void 0 ? void 0 : stringArg(path2, "browse-directories", 32768)
20149
+ path2 === void 0 ? void 0 : stringArg(path2, "browse-directories", 32768),
20150
+ services.git
18696
20151
  ));
18697
20152
  handleWithContext(IPC.repositoryOpenRecent, "open-recent", async (context, id) => {
18698
20153
  const repositoryId = stringArg(id, "open-recent", 64);
@@ -18730,7 +20185,7 @@ function registerServerCommands(registry2, services, host) {
18730
20185
  ));
18731
20186
  handle(IPC.repositoryDirectoryEntries, "directory-entries", (id, directoryPath) => services.files.listDirectory(
18732
20187
  stringArg(id, "directory-entries", 64),
18733
- stringArg(directoryPath, "directory-entries")
20188
+ directoryPath === "" ? "" : stringArg(directoryPath, "directory-entries")
18734
20189
  ));
18735
20190
  handle(IPC.repositoryReadFile, "read-file", (id, filePath, allowLarge) => services.files.read(stringArg(id, "read-file", 64), stringArg(filePath, "read-file"), allowLarge === true));
18736
20191
  handle(IPC.repositoryReadImage, "read-image", (id, filePath) => services.files.readImage(
@@ -18930,6 +20385,17 @@ function registerServerCommands(registry2, services, host) {
18930
20385
  textArg(content, "update-conflict", 8 * 1024 * 1024)
18931
20386
  ));
18932
20387
  handle(IPC.commitCreate, "commit", (id, message) => services.operations.createCommit(stringArg(id, "commit", 64), stringArg(message, "commit", 1e5)));
20388
+ handle(IPC.commitAuthorship, "commit-authorship-read", (id) => services.operations.getCommitAuthorship(stringArg(id, "commit-authorship-read", 64)));
20389
+ handle(IPC.commitSetAuthorship, "commit-authorship-set", async (id, input) => {
20390
+ const repositoryId = stringArg(id, "commit-authorship-set", 64);
20391
+ const parsed = commitAuthorshipInputSchema.safeParse(input);
20392
+ if (!parsed.success) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "commit-authorship-set", message: "Enter a valid name, email and reviewed identity." });
20393
+ const result = await services.operations.setCommitAuthorship(repositoryId, parsed.data);
20394
+ const repository = services.repositories.get(repositoryId);
20395
+ const ids = /* @__PURE__ */ new Set([repositoryId, ...services.repositories.recents().filter((item) => item.commonDir === repository.commonDir).map((item) => item.id)]);
20396
+ for (const siblingId of ids) services.events.repositoryChanged(siblingId, "unknown");
20397
+ return result;
20398
+ });
18933
20399
  handle(IPC.commitUndoLatest, "undo-latest-commit", (id, expectedOid) => services.operations.undoLatestCommit(
18934
20400
  stringArg(id, "undo-latest-commit", 64),
18935
20401
  oidArg(expectedOid, "undo-latest-commit")
@@ -18954,6 +20420,13 @@ function registerServerCommands(registry2, services, host) {
18954
20420
  const { repositoryId, fullName } = branchDetailsArg(request, "branch-details");
18955
20421
  return services.operations.branchDetails(repositoryId, fullName);
18956
20422
  });
20423
+ handle(IPC.remoteBranchDetails, "remote-branch-details", (request) => {
20424
+ const { repositoryId, fullName } = branchDetailsArg(request, "remote-branch-details");
20425
+ return services.operations.remoteBranchDetails(repositoryId, fullName);
20426
+ });
20427
+ handle(IPC.trackingBranchCreate, "create-tracking-branch", (request) => services.operations.createTrackingBranch(createTrackingBranchArg(request)));
20428
+ handle(IPC.remoteBranchDelete, "delete-remote-branch", (request) => services.operations.deleteRemoteBranch(deleteRemoteBranchArg(request)));
20429
+ handle(IPC.branchesFetch, "fetch-branches", (id) => services.operations.fetchBranches(stringArg(id, "fetch-branches", 64)));
18957
20430
  handle(IPC.worktreeDetails, "worktree-details", (request) => {
18958
20431
  const { repositoryId, path: path2 } = worktreeDetailsArg(request, "worktree-details");
18959
20432
  return services.operations.worktreeDetails(repositoryId, path2);
@@ -18991,6 +20464,13 @@ function registerServerCommands(registry2, services, host) {
18991
20464
  handle(IPC.aiCancelGeneration, "ai-cancel-generation", (requestId2) => {
18992
20465
  services.ai.cancel(aiString(requestId2, "ai-cancel-generation", 100, true));
18993
20466
  });
20467
+ handle(IPC.githubAccountsStatus, "gh-accounts-status", (forceRefresh) => services.github.accounts.status(booleanArg(forceRefresh, "gh-accounts-status")));
20468
+ 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) => {
20470
+ const parsed = githubAccountSelectionSchema.safeParse(selection);
20471
+ 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);
20473
+ });
18994
20474
  handle(IPC.githubStatus, "gh-status", (forceRefresh) => services.github.status(booleanArg(forceRefresh, "gh-status")));
18995
20475
  handle(IPC.githubRepositoryInfo, "gh-repository-info", (id) => services.github.repositoryInfo(stringArg(id, "gh-repository-info", 64)));
18996
20476
  handle(IPC.githubPrForBranch, "gh-pr-for-branch", (id, branchName) => services.github.findPullRequestForBranch(
@@ -19040,7 +20520,7 @@ function clipboardImageArg(value) {
19040
20520
  if (value instanceof Uint8Array) return value;
19041
20521
  throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "paste-entries", message: "Invalid clipboard image." });
19042
20522
  }
19043
- const protocolVersion = 1;
20523
+ const protocolVersion = 4;
19044
20524
  const protocolMetadata = {
19045
20525
  protocolVersion
19046
20526
  };
@@ -19056,6 +20536,85 @@ function preferredServerPort(profile) {
19056
20536
  }
19057
20537
  const AUTH_DATA_VERSION = 4;
19058
20538
  const BROWSER_SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
20539
+ async function hasActiveBrowserSessions(directory, now = Date.now) {
20540
+ const { sessions } = await loadSessions(path.join(directory, "sessions.json"));
20541
+ return sessions.some((session) => session.kind !== "desktop" && session.expiresAt !== null && now() < Date.parse(session.expiresAt));
20542
+ }
20543
+ async function loadSessions(filePath) {
20544
+ try {
20545
+ const parsed = JSON.parse(await readFile(filePath, "utf8"));
20546
+ if (isLegacyAuthData(parsed)) {
20547
+ await chmod(filePath, 384);
20548
+ return {
20549
+ sessions: parsed.sessions.map((session) => ({
20550
+ ...session,
20551
+ kind: "legacy",
20552
+ expiresAt: sessionExpiry("legacy", session.createdAt),
20553
+ clientName: "Legacy browser session",
20554
+ deviceType: "unknown",
20555
+ os: null,
20556
+ browser: null,
20557
+ remoteAddress: null,
20558
+ viaProxy: false,
20559
+ lastConnectedAt: null
20560
+ })),
20561
+ migrated: true
20562
+ };
20563
+ }
20564
+ if (isVersionTwoAuthData(parsed)) {
20565
+ await chmod(filePath, 384);
20566
+ return {
20567
+ sessions: parsed.sessions.map((session) => ({
20568
+ ...session,
20569
+ expiresAt: sessionExpiry(session.kind, session.createdAt),
20570
+ clientName: session.kind === "legacy" ? "Legacy browser session" : session.clientName,
20571
+ deviceType: session.kind === "desktop" ? "desktop" : "unknown",
20572
+ os: null,
20573
+ browser: session.kind === "browser" ? session.clientName : null,
20574
+ viaProxy: false,
20575
+ lastConnectedAt: null
20576
+ })),
20577
+ migrated: true
20578
+ };
20579
+ }
20580
+ if (isVersionThreeAuthData(parsed)) {
20581
+ await chmod(filePath, 384);
20582
+ return {
20583
+ sessions: parsed.sessions.map((session) => ({ ...session, expiresAt: sessionExpiry(session.kind, session.createdAt) })),
20584
+ migrated: true
20585
+ };
20586
+ }
20587
+ if (!isAuthData(parsed)) throw corrupt$1("session data");
20588
+ await chmod(filePath, 384);
20589
+ return { sessions: parsed.sessions.map((session) => ({ ...session })), migrated: false };
20590
+ } catch (error) {
20591
+ if (error.code === "ENOENT") return { sessions: [], migrated: false };
20592
+ if (error instanceof SyntaxError) throw corrupt$1("session data");
20593
+ throw error;
20594
+ }
20595
+ }
20596
+ function isAuthData(value) {
20597
+ const data = value;
20598
+ return Boolean(data && data.version === AUTH_DATA_VERSION && isVersionThreeAuthData({ ...data, version: 3 }) && data.sessions?.every((session) => session.kind === "desktop" ? session.expiresAt === null : typeof session.expiresAt === "string" && Number.isFinite(Date.parse(session.expiresAt)) && Date.parse(session.expiresAt) > Date.parse(session.createdAt)));
20599
+ }
20600
+ function isVersionThreeAuthData(value) {
20601
+ const data = value;
20602
+ return Boolean(data && data.version === 3 && Array.isArray(data.sessions) && data.sessions.every((session) => session && typeof session.id === "string" && /^[A-Za-z0-9_-]{24}$/.test(session.id) && typeof session.digest === "string" && /^[A-Za-z0-9_-]{43}$/.test(session.digest) && (session.kind === "desktop" || session.kind === "browser" || session.kind === "legacy") && typeof session.clientName === "string" && session.clientName.length > 0 && session.clientName.length <= 160 && (session.deviceType === "desktop" || session.deviceType === "mobile" || session.deviceType === "tablet" || session.deviceType === "bot" || session.deviceType === "unknown") && (session.os === null || typeof session.os === "string" && session.os.length <= 80) && (session.browser === null || typeof session.browser === "string" && session.browser.length <= 80) && (session.remoteAddress === null || typeof session.remoteAddress === "string" && session.remoteAddress.length <= 128) && typeof session.viaProxy === "boolean" && typeof session.createdAt === "string" && !Number.isNaN(Date.parse(session.createdAt)) && (session.lastConnectedAt === null || typeof session.lastConnectedAt === "string" && !Number.isNaN(Date.parse(session.lastConnectedAt)))));
20603
+ }
20604
+ function isVersionTwoAuthData(value) {
20605
+ const data = value;
20606
+ return Boolean(data && data.version === 2 && Array.isArray(data.sessions) && data.sessions.every((session) => session && typeof session.id === "string" && /^[A-Za-z0-9_-]{24}$/.test(session.id) && typeof session.digest === "string" && /^[A-Za-z0-9_-]{43}$/.test(session.digest) && (session.kind === "desktop" || session.kind === "browser" || session.kind === "legacy") && typeof session.clientName === "string" && session.clientName.length > 0 && session.clientName.length <= 160 && (session.remoteAddress === null || typeof session.remoteAddress === "string" && session.remoteAddress.length <= 128) && typeof session.createdAt === "string" && !Number.isNaN(Date.parse(session.createdAt))));
20607
+ }
20608
+ function isLegacyAuthData(value) {
20609
+ const data = value;
20610
+ return Boolean(data && data.version === 1 && Array.isArray(data.sessions) && data.sessions.every((session) => session && typeof session.id === "string" && /^[A-Za-z0-9_-]{24}$/.test(session.id) && typeof session.digest === "string" && /^[A-Za-z0-9_-]{43}$/.test(session.digest) && typeof session.createdAt === "string" && !Number.isNaN(Date.parse(session.createdAt))));
20611
+ }
20612
+ function corrupt$1(name) {
20613
+ return new Error(`OpenTig authentication ${name} is corrupt.`);
20614
+ }
20615
+ function sessionExpiry(kind, createdAt) {
20616
+ return kind === "desktop" ? null : new Date(Date.parse(createdAt) + BROWSER_SESSION_MAX_AGE_SECONDS * 1e3).toISOString();
20617
+ }
19059
20618
  const SECRET_BYTES = 32;
19060
20619
  const SECRET_FILE = "server-secret";
19061
20620
  const SESSIONS_FILE = "sessions.json";
@@ -19071,6 +20630,10 @@ class PersistentAuthStore {
19071
20630
  now;
19072
20631
  sessions;
19073
20632
  mutationQueue = Promise.resolve();
20633
+ /** Inspect an existing installation without issuing, revoking, or writing sessions. */
20634
+ static async hasActiveBrowserSessions(directory, now = Date.now) {
20635
+ return hasActiveBrowserSessions(directory, now);
20636
+ }
19074
20637
  static async open(directory, now = Date.now) {
19075
20638
  const resolved = path.resolve(directory);
19076
20639
  await mkdir(resolved, { recursive: true, mode: 448 });
@@ -19260,75 +20823,6 @@ async function loadOrCreateSecret(filePath) {
19260
20823
  throw error;
19261
20824
  }
19262
20825
  }
19263
- async function loadSessions(filePath) {
19264
- try {
19265
- const parsed = JSON.parse(await readFile(filePath, "utf8"));
19266
- if (isLegacyAuthData(parsed)) {
19267
- await chmod(filePath, 384);
19268
- return {
19269
- sessions: parsed.sessions.map((session) => ({
19270
- ...session,
19271
- kind: "legacy",
19272
- expiresAt: sessionExpiry("legacy", session.createdAt),
19273
- clientName: "Legacy browser session",
19274
- deviceType: "unknown",
19275
- os: null,
19276
- browser: null,
19277
- remoteAddress: null,
19278
- viaProxy: false,
19279
- lastConnectedAt: null
19280
- })),
19281
- migrated: true
19282
- };
19283
- }
19284
- if (isVersionTwoAuthData(parsed)) {
19285
- await chmod(filePath, 384);
19286
- return {
19287
- sessions: parsed.sessions.map((session) => ({
19288
- ...session,
19289
- expiresAt: sessionExpiry(session.kind, session.createdAt),
19290
- clientName: session.kind === "legacy" ? "Legacy browser session" : session.clientName,
19291
- deviceType: session.kind === "desktop" ? "desktop" : "unknown",
19292
- os: null,
19293
- browser: session.kind === "browser" ? session.clientName : null,
19294
- viaProxy: false,
19295
- lastConnectedAt: null
19296
- })),
19297
- migrated: true
19298
- };
19299
- }
19300
- if (isVersionThreeAuthData(parsed)) {
19301
- await chmod(filePath, 384);
19302
- return {
19303
- sessions: parsed.sessions.map((session) => ({ ...session, expiresAt: sessionExpiry(session.kind, session.createdAt) })),
19304
- migrated: true
19305
- };
19306
- }
19307
- if (!isAuthData(parsed)) throw corrupt("session data");
19308
- await chmod(filePath, 384);
19309
- return { sessions: parsed.sessions.map((session) => ({ ...session })), migrated: false };
19310
- } catch (error) {
19311
- if (error.code === "ENOENT") return { sessions: [], migrated: false };
19312
- if (error instanceof SyntaxError) throw corrupt("session data");
19313
- throw error;
19314
- }
19315
- }
19316
- function isAuthData(value) {
19317
- const data = value;
19318
- return Boolean(data && data.version === AUTH_DATA_VERSION && isVersionThreeAuthData({ ...data, version: 3 }) && data.sessions?.every((session) => session.kind === "desktop" ? session.expiresAt === null : typeof session.expiresAt === "string" && Number.isFinite(Date.parse(session.expiresAt)) && Date.parse(session.expiresAt) > Date.parse(session.createdAt)));
19319
- }
19320
- function isVersionThreeAuthData(value) {
19321
- const data = value;
19322
- return Boolean(data && data.version === 3 && Array.isArray(data.sessions) && data.sessions.every((session) => session && typeof session.id === "string" && /^[A-Za-z0-9_-]{24}$/.test(session.id) && typeof session.digest === "string" && /^[A-Za-z0-9_-]{43}$/.test(session.digest) && (session.kind === "desktop" || session.kind === "browser" || session.kind === "legacy") && typeof session.clientName === "string" && session.clientName.length > 0 && session.clientName.length <= 160 && (session.deviceType === "desktop" || session.deviceType === "mobile" || session.deviceType === "tablet" || session.deviceType === "bot" || session.deviceType === "unknown") && (session.os === null || typeof session.os === "string" && session.os.length <= 80) && (session.browser === null || typeof session.browser === "string" && session.browser.length <= 80) && (session.remoteAddress === null || typeof session.remoteAddress === "string" && session.remoteAddress.length <= 128) && typeof session.viaProxy === "boolean" && typeof session.createdAt === "string" && !Number.isNaN(Date.parse(session.createdAt)) && (session.lastConnectedAt === null || typeof session.lastConnectedAt === "string" && !Number.isNaN(Date.parse(session.lastConnectedAt)))));
19323
- }
19324
- function isVersionTwoAuthData(value) {
19325
- const data = value;
19326
- return Boolean(data && data.version === 2 && Array.isArray(data.sessions) && data.sessions.every((session) => session && typeof session.id === "string" && /^[A-Za-z0-9_-]{24}$/.test(session.id) && typeof session.digest === "string" && /^[A-Za-z0-9_-]{43}$/.test(session.digest) && (session.kind === "desktop" || session.kind === "browser" || session.kind === "legacy") && typeof session.clientName === "string" && session.clientName.length > 0 && session.clientName.length <= 160 && (session.remoteAddress === null || typeof session.remoteAddress === "string" && session.remoteAddress.length <= 128) && typeof session.createdAt === "string" && !Number.isNaN(Date.parse(session.createdAt))));
19327
- }
19328
- function isLegacyAuthData(value) {
19329
- const data = value;
19330
- return Boolean(data && data.version === 1 && Array.isArray(data.sessions) && data.sessions.every((session) => session && typeof session.id === "string" && /^[A-Za-z0-9_-]{24}$/.test(session.id) && typeof session.digest === "string" && /^[A-Za-z0-9_-]{43}$/.test(session.digest) && typeof session.createdAt === "string" && !Number.isNaN(Date.parse(session.createdAt))));
19331
- }
19332
20826
  function constantTimeEqual(left, right) {
19333
20827
  const leftBytes = Buffer.from(left);
19334
20828
  const rightBytes = Buffer.from(right);
@@ -19346,17 +20840,15 @@ async function exists(filePath) {
19346
20840
  function corrupt(name) {
19347
20841
  return new Error(`OpenTig authentication ${name} is corrupt.`);
19348
20842
  }
19349
- function sessionExpiry(kind, createdAt) {
19350
- return kind === "desktop" ? null : new Date(Date.parse(createdAt) + BROWSER_SESSION_MAX_AGE_SECONDS * 1e3).toISOString();
19351
- }
19352
20843
  const DEFAULT_PAIRING_TTL_MS = 5 * 60 * 1e3;
19353
20844
  class OpenTigSessionAuth {
19354
- constructor(source, store, secureCookies, now, profile) {
20845
+ constructor(source, store, secureCookies, now, profile, browserAccessEnabled) {
19355
20846
  this.source = source;
19356
20847
  this.store = store;
19357
20848
  this.secureCookies = secureCookies;
19358
20849
  this.now = now;
19359
20850
  this.profile = profile;
20851
+ this.browserAccessEnabled = browserAccessEnabled;
19360
20852
  }
19361
20853
  source;
19362
20854
  store;
@@ -19364,15 +20856,30 @@ class OpenTigSessionAuth {
19364
20856
  now;
19365
20857
  profile;
19366
20858
  pairing = null;
20859
+ browserAccessEnabled;
20860
+ browserGeneration = 0;
20861
+ browserQueue = Promise.resolve();
19367
20862
  static async open(options) {
19368
20863
  const store = await PersistentAuthStore.open(options.dataDirectory, options.now);
19369
- return new OpenTigSessionAuth(options.source, store, options.secureCookies ?? false, options.now ?? Date.now, options.profile ?? "production");
20864
+ return new OpenTigSessionAuth(options.source, store, options.secureCookies ?? false, options.now ?? Date.now, options.profile ?? "production", options.browserAccessEnabled ?? true);
19370
20865
  }
19371
20866
  descriptor() {
19372
20867
  this.dropExpiredPairing();
19373
- return { authenticationRequired: true, pairingAvailable: this.pairing !== null };
20868
+ return { authenticationRequired: true, pairingAvailable: this.browserAccessEnabled && this.pairing !== null, browserAccessEnabled: this.browserAccessEnabled };
20869
+ }
20870
+ setBrowserAccessEnabled(enabled) {
20871
+ if (!enabled) {
20872
+ this.browserAccessEnabled = false;
20873
+ this.browserGeneration++;
20874
+ this.pairing = null;
20875
+ }
20876
+ const generation = this.browserGeneration;
20877
+ return this.queueBrowserMutation(async () => {
20878
+ this.browserAccessEnabled = enabled && generation === this.browserGeneration;
20879
+ });
19374
20880
  }
19375
20881
  createPairingToken(ttlMs = DEFAULT_PAIRING_TTL_MS) {
20882
+ if (!this.browserAccessEnabled) throw new Error("Web access is disabled.");
19376
20883
  if (!Number.isSafeInteger(ttlMs) || ttlMs < 1e3 || ttlMs > 15 * 60 * 1e3) throw new Error("Invalid pairing token lifetime.");
19377
20884
  const token = randomBytes(32).toString("base64url");
19378
20885
  const expiresAt = this.now() + ttlMs;
@@ -19394,30 +20901,43 @@ class OpenTigSessionAuth {
19394
20901
  async exchangePairingToken(token, metadata, secure = false) {
19395
20902
  this.dropExpiredPairing();
19396
20903
  const pairing = this.pairing;
19397
- if (!pairing || !isCredential(token) || !this.store.matchesDigest(token, pairing.digest)) return null;
20904
+ if (!this.browserAccessEnabled || !pairing || !isCredential(token) || !this.store.matchesDigest(token, pairing.digest)) return null;
19398
20905
  this.pairing = null;
19399
- try {
19400
- return await this.issueCookie({
19401
- kind: "browser",
19402
- clientName: metadata?.clientName ?? "Browser",
19403
- deviceType: metadata?.deviceType ?? "unknown",
19404
- os: metadata?.os ?? null,
19405
- browser: metadata?.browser ?? null,
19406
- remoteAddress: metadata?.remoteAddress ?? null,
19407
- viaProxy: metadata?.viaProxy ?? false
19408
- }, secure);
19409
- } catch (error) {
19410
- if (this.now() < pairing.expiresAt && !this.pairing) this.pairing = pairing;
19411
- throw error;
19412
- }
20906
+ const generation = this.browserGeneration;
20907
+ return this.queueBrowserMutation(async () => {
20908
+ if (!this.browserAccessEnabled || generation !== this.browserGeneration) return null;
20909
+ try {
20910
+ const cookie = await this.issueCookie({
20911
+ kind: "browser",
20912
+ clientName: metadata?.clientName ?? "Browser",
20913
+ deviceType: metadata?.deviceType ?? "unknown",
20914
+ os: metadata?.os ?? null,
20915
+ browser: metadata?.browser ?? null,
20916
+ remoteAddress: metadata?.remoteAddress ?? null,
20917
+ viaProxy: metadata?.viaProxy ?? false
20918
+ }, secure);
20919
+ if (!this.browserAccessEnabled || generation !== this.browserGeneration) {
20920
+ await this.revoke({ cookie });
20921
+ return null;
20922
+ }
20923
+ return cookie;
20924
+ } catch (error) {
20925
+ if (this.browserAccessEnabled && generation === this.browserGeneration && this.now() < pairing.expiresAt && !this.pairing) this.pairing = pairing;
20926
+ throw error;
20927
+ }
20928
+ });
19413
20929
  }
19414
20930
  authenticate(headers) {
19415
20931
  const token = readCookie(headers.cookie, sessionCookieName(this.profile));
19416
- return token ? this.store.authenticate(token) : null;
20932
+ const sessionId = token ? this.store.authenticate(token) : null;
20933
+ return sessionId && this.hasSession(sessionId) ? sessionId : null;
19417
20934
  }
19418
20935
  async renewBrowserCookie(headers, secure = false) {
20936
+ if (!this.browserAccessEnabled) return null;
20937
+ const generation = this.browserGeneration;
19419
20938
  const token = readCookie(headers.cookie, sessionCookieName(this.profile));
19420
20939
  if (!token || !await this.store.renewBrowserSession(token)) return null;
20940
+ if (!this.browserAccessEnabled || generation !== this.browserGeneration) return null;
19421
20941
  return this.sessionCookie(token, true, secure);
19422
20942
  }
19423
20943
  async revoke(headers) {
@@ -19462,11 +20982,18 @@ class OpenTigSessionAuth {
19462
20982
  };
19463
20983
  }
19464
20984
  hasSession(sessionId) {
19465
- return this.store.hasSession(sessionId);
20985
+ return this.store.hasSession(sessionId) && (this.browserAccessEnabled || this.sessions().some((session) => session.id === sessionId && session.kind === "desktop"));
19466
20986
  }
19467
20987
  close() {
20988
+ this.browserAccessEnabled = false;
20989
+ this.browserGeneration++;
19468
20990
  this.pairing = null;
19469
- return this.store.close();
20991
+ return this.browserQueue.then(() => this.store.close());
20992
+ }
20993
+ queueBrowserMutation(operation) {
20994
+ const result = this.browserQueue.then(operation);
20995
+ this.browserQueue = result.then(() => void 0, () => void 0);
20996
+ return result;
19470
20997
  }
19471
20998
  expiredCookie(secure = false) {
19472
20999
  return `${sessionCookieName(this.profile)}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0${this.secureCookies || secure ? "; Secure" : ""}`;
@@ -19754,6 +21281,7 @@ async function handleRequest(context, request, response) {
19754
21281
  return sendJson(response, 204, null, { "Set-Cookie": cookie });
19755
21282
  }
19756
21283
  if (rawPath === "/api/auth/pair" || rawPath === "/api/auth/desktop") {
21284
+ if (rawPath === "/api/auth/pair" && !context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
19757
21285
  const body = await readJsonObject(request, response);
19758
21286
  if (!body) return;
19759
21287
  const secure = requestOriginIsSecure(request);
@@ -19765,6 +21293,7 @@ async function handleRequest(context, request, response) {
19765
21293
  } else {
19766
21294
  cookie = await context.auth.exchangeDesktopSecret(body.secret);
19767
21295
  }
21296
+ if (!cookie && rawPath === "/api/auth/pair" && !context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
19768
21297
  if (!cookie) return sendJson(response, 401, { error: "Authentication failed." });
19769
21298
  return sendJson(response, 204, null, { "Set-Cookie": cookie });
19770
21299
  }
@@ -19925,6 +21454,7 @@ async function handleLocalAdminPair(context, request, response) {
19925
21454
  }
19926
21455
  const origin = normalizePublicOrigin(body.publicOrigin);
19927
21456
  if (!origin) return sendJson(response, 400, { error: "Invalid public origin." });
21457
+ if (!context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
19928
21458
  const pairing = admin.createPairingToken();
19929
21459
  const url = new URL("/pair", origin);
19930
21460
  url.hash = new URLSearchParams({ token: pairing.token }).toString();
@@ -20057,7 +21587,10 @@ class OpenTigWebSocketTransport {
20057
21587
  revokeSessions(sessionIds) {
20058
21588
  const revoked = new Set(sessionIds);
20059
21589
  for (const [socket, state] of this.clients) {
20060
- if (revoked.has(state.sessionId)) socket.close(1008, "Session revoked");
21590
+ if (revoked.has(state.sessionId)) {
21591
+ for (const controller of state.requests.values()) controller.abort();
21592
+ socket.close(1008, "Session revoked");
21593
+ }
20061
21594
  }
20062
21595
  }
20063
21596
  close() {
@@ -20100,6 +21633,7 @@ class OpenTigWebSocketTransport {
20100
21633
  socket.on("error", () => this.options.logger("warn", "WebSocket connection failed."));
20101
21634
  }
20102
21635
  async message(socket, state, data, isBinary) {
21636
+ if (!this.options.auth.hasSession(state.sessionId)) return socket.close(1008, "Session expired or revoked");
20103
21637
  if (isBinary) return socket.close(1003, "Text messages only");
20104
21638
  const bytes = rawBytes(data);
20105
21639
  if (bytes.byteLength > FILE_WRITE_FRAME_LIMIT) return socket.close(1009, "Message too large");
@@ -20297,7 +21831,13 @@ class OpenTigServer {
20297
21831
  return this.options.auth.createPairingToken();
20298
21832
  }
20299
21833
  getStatus() {
20300
- return { connectedSessionCount: this.webSockets.connectedSessionCount };
21834
+ return { connectedSessionCount: this.webSockets.connectedSessionCount, browserAccessEnabled: this.options.auth.descriptor().browserAccessEnabled };
21835
+ }
21836
+ async setBrowserAccessEnabled(enabled) {
21837
+ if (!this.ready) throw new Error("Server is not ready.");
21838
+ const pending = this.options.auth.setBrowserAccessEnabled(enabled);
21839
+ if (!enabled) this.disconnectSessions(this.options.auth.sessions().filter((session) => session.kind !== "desktop").map((session) => session.id));
21840
+ await pending;
20301
21841
  }
20302
21842
  async revokeAllSessions() {
20303
21843
  if (!this.ready) throw new Error("Server is not ready.");
@@ -20355,6 +21895,7 @@ async function runOpenTigServer(config2) {
20355
21895
  let transport = null;
20356
21896
  const auth = await OpenTigSessionAuth.open({
20357
21897
  source: config2.auth,
21898
+ browserAccessEnabled: config2.browserAccessEnabled ?? config2.mode === "web-access",
20358
21899
  ...config2.profile ? { profile: config2.profile } : {},
20359
21900
  dataDirectory: config2.serverDataPath ?? path.join(path.dirname(config2.settingsPath), "server"),
20360
21901
  ...config2.secureCookies === void 0 ? {} : { secureCookies: config2.secureCookies }
@@ -20408,6 +21949,7 @@ async function runOpenTigServer(config2) {
20408
21949
  createPairingLink: () => pairingLink(address.origin, transport.createPairingToken()),
20409
21950
  getStatus: () => transport.getStatus(),
20410
21951
  revokeAllSessions: () => transport.revokeAllSessions(),
21952
+ setBrowserAccessEnabled: (enabled) => transport.setBrowserAccessEnabled(enabled),
20411
21953
  close: () => transport.stop()
20412
21954
  };
20413
21955
  } catch (error) {