@opentig/cli 0.1.8 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) 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-Bi6ET5UT.mjs} +15 -2
  4. package/dist/client/assets/App-BXZLGT86.js +74 -0
  5. package/dist/client/assets/CreatePullRequestDialog-DFonMYEx.js +2 -0
  6. package/dist/client/assets/{DiffWorkspace-Ca2ZUlVn.js → DiffWorkspace-RDG_Y70N.js} +5 -5
  7. package/dist/client/assets/{EditableFileViewer-85aEeMBK.js → EditableFileViewer-CsbifqBO.js} +1 -1
  8. package/dist/client/assets/{File-DTtWrbYf.js → File-CHjvYpb2.js} +1 -1
  9. package/dist/client/assets/FilesView-Dl8QFbFA.js +1 -0
  10. package/dist/client/assets/GitHubAvatar-CnBaloV0.js +1 -0
  11. package/dist/client/assets/HistoryView-BgCZU2JO.js +1 -0
  12. package/dist/client/assets/{HtmlFileViewer-bL6T57PQ.js → HtmlFileViewer-h8ULU8Gy.js} +1 -1
  13. package/dist/client/assets/{IconEdit-DePJMGIi.js → IconEdit-Cg8S2wqW.js} +1 -1
  14. package/dist/client/assets/IconFolderSymlink-BZSLX1ez.js +1 -0
  15. package/dist/client/assets/{ImageFileViewer-DJaHH3hJ.js → ImageFileViewer-Cjw58ckv.js} +1 -1
  16. package/dist/client/assets/LocalRefsDialog-DNgrd7Qp.js +1 -0
  17. package/dist/client/assets/{MarkdownFileViewer-BNPb9v8u.js → MarkdownFileViewer-kLiduag6.js} +2 -2
  18. package/dist/client/assets/{PairingPage-D9oguvKU.js → PairingPage-C97sv-8s.js} +1 -1
  19. package/dist/client/assets/{PierreDiffViewer-8wTLYd5-.js → PierreDiffViewer-eAcugmoF.js} +1 -1
  20. package/dist/client/assets/{PierreEditBoundary-KzU3HDc2.js → PierreEditBoundary-DlGd1pqO.js} +1 -1
  21. package/dist/client/assets/{PierreWorkerPool-DH2jp9oe.js → PierreWorkerPool-Col4k7qz.js} +1 -1
  22. package/dist/client/assets/{PullRequestDiff-DK-m07ZP.js → PullRequestDiff-DhKTXyIF.js} +1 -1
  23. package/dist/client/assets/PullRequestViewer-BgI8HFMe.js +2 -0
  24. package/dist/client/assets/PullRequestsView-PYWLy8We.js +1 -0
  25. package/dist/client/assets/QuickOpenDialog-BFDDZVCL.js +1 -0
  26. package/dist/client/assets/RepositoryProjectsDialog-CvB8KcFp.js +1 -0
  27. package/dist/client/assets/SearchView-BwF_MsxK.js +1 -0
  28. package/dist/client/assets/SettingsDialog-DIcLmXSE.js +9 -0
  29. package/dist/client/assets/Viewer-o1hDU33O.js +3 -0
  30. package/dist/client/assets/{abnfDiagram-N423BO3Z-CkHOGdAl.js → abnfDiagram-N423BO3Z-BvrnK_R-.js} +1 -1
  31. package/dist/client/assets/{arc-Dj8F8CO5.js → arc-DLa3_7ua.js} +1 -1
  32. package/dist/client/assets/{architectureDiagram-T3A2C74G-g87Y5LXk.js → architectureDiagram-T3A2C74G-BexwhnyX.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-CoOttUUo.js} +1 -1
  35. package/dist/client/assets/{c4Diagram-5PPSVZJV-DoZEC2xg.js → c4Diagram-5PPSVZJV-Bgp6ppUh.js} +1 -1
  36. package/dist/client/assets/channel-1k1AYpyV.js +1 -0
  37. package/dist/client/assets/{checkbox-DYwmgDPS.js → checkbox-BRH0ZNIC.js} +1 -1
  38. package/dist/client/assets/{chunk-2GRJ4B5K-Dr44hDNV.js → chunk-2GRJ4B5K-C_kzp38w.js} +1 -1
  39. package/dist/client/assets/{chunk-2Q5K7J3B-YIEyVOOL.js → chunk-2Q5K7J3B-CysYnRkJ.js} +1 -1
  40. package/dist/client/assets/{chunk-5RXB4S5H-gjmGBtkE.js → chunk-5RXB4S5H-oMS6x_ag.js} +1 -1
  41. package/dist/client/assets/{chunk-5VM5RSS4-DCOQko4g.js → chunk-5VM5RSS4-C5-dW5wf.js} +1 -1
  42. package/dist/client/assets/{chunk-6Q2QTUOP-DfGlvPVm.js → chunk-6Q2QTUOP-DxahInnA.js} +1 -1
  43. package/dist/client/assets/{chunk-GF5L2VYU-CCWOLPVL.js → chunk-GF5L2VYU-D7V-3eRg.js} +1 -1
  44. package/dist/client/assets/{chunk-JWPE2WC7-CFRTGh8r.js → chunk-JWPE2WC7-BrLmjbVP.js} +1 -1
  45. package/dist/client/assets/{chunk-KBJHAD2P-By3wsJVD.js → chunk-KBJHAD2P-CyOvijwM.js} +1 -1
  46. package/dist/client/assets/{chunk-RYQCIY6F-C0wmsHmy.js → chunk-RYQCIY6F-BvEjVjQs.js} +1 -1
  47. package/dist/client/assets/{chunk-XXDRQBXY-t3SnJvV_.js → chunk-XXDRQBXY-C_IY9A64.js} +1 -1
  48. package/dist/client/assets/classDiagram-JCYQIIEL-ChyNFpay.js +1 -0
  49. package/dist/client/assets/classDiagram-v2-OCEON4UE-ChyNFpay.js +1 -0
  50. package/dist/client/assets/{cose-bilkent-JH36ORCC-D3VltoVk.js → cose-bilkent-JH36ORCC-Cu7qZi7h.js} +1 -1
  51. package/dist/client/assets/{cynefin-VYW2F7L2-DwLwbeoe.js → cynefin-VYW2F7L2-B4M1t9lL.js} +1 -1
  52. package/dist/client/assets/{cynefinDiagram-MW4NZA55-B23kuaQF.js → cynefinDiagram-MW4NZA55-CAosOLmT.js} +1 -1
  53. package/dist/client/assets/{dagre-VZM6K2ZE-SgMrTr2p.js → dagre-VZM6K2ZE-q354V538.js} +1 -1
  54. package/dist/client/assets/{diagram-7IWD3JNH-C7cQN4aD.js → diagram-7IWD3JNH-C_CzkBPt.js} +1 -1
  55. package/dist/client/assets/{diagram-B4RE2ZJO-CzeI3yOw.js → diagram-B4RE2ZJO-VvvYHE9c.js} +1 -1
  56. package/dist/client/assets/{diagram-LBJQPF4R-DhKm4hlI.js → diagram-LBJQPF4R-D8vRQwb5.js} +1 -1
  57. package/dist/client/assets/{diagram-Q27KOJAE-BXlU6Wp3.js → diagram-Q27KOJAE-Bul4aDKg.js} +1 -1
  58. package/dist/client/assets/{diagram-UB23O5K3-BOwY_YwR.js → diagram-UB23O5K3-DNadEE58.js} +1 -1
  59. package/dist/client/assets/{ebnfDiagram-BXEA7PRR-CuIQuYPa.js → ebnfDiagram-BXEA7PRR-Bf-68MMZ.js} +1 -1
  60. package/dist/client/assets/{erDiagram-JOGREHBK-d45NhKfk.js → erDiagram-JOGREHBK-CJ-jHFsf.js} +1 -1
  61. package/dist/client/assets/{flowDiagram-UKHOOZJN-Dzq-6qo7.js → flowDiagram-UKHOOZJN-DMOdinBU.js} +1 -1
  62. package/dist/client/assets/{ganttDiagram-PKOTCBZU-RVlL9mX2.js → ganttDiagram-PKOTCBZU-DzezS3Zy.js} +1 -1
  63. package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-8kEJVUFH.js → gitGraphDiagram-DS77QQ5N-Dftpvns8.js} +1 -1
  64. package/dist/client/assets/index-BfA4tAc1.css +1 -0
  65. package/dist/client/assets/{index-CQcx9aRt.js → index-DAUK3rnz.js} +4 -4
  66. package/dist/client/assets/{infoDiagram-6WML65LV-Co21Ql9M.js → infoDiagram-6WML65LV-DmgUBq17.js} +1 -1
  67. package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-BvPB_G97.js → ishikawaDiagram-WSZJBQD7-CL0PQhpQ.js} +1 -1
  68. package/dist/client/assets/{journeyDiagram-NVQOT4AX-k7x89IE9.js → journeyDiagram-NVQOT4AX-Ck4pQx_A.js} +1 -1
  69. package/dist/client/assets/{kanban-definition-27J2QSJJ-BjrgN30t.js → kanban-definition-27J2QSJJ-DuRZxBql.js} +1 -1
  70. package/dist/client/assets/{linear-m158kske.js → linear-C2JDCaz4.js} +1 -1
  71. package/dist/client/assets/{mermaid.core-C0FKucv-.js → mermaid.core-9dt2ugQA.js} +4 -4
  72. package/dist/client/assets/{mindmap-definition-FAOFIHXS-CF8noycK.js → mindmap-definition-FAOFIHXS-vy0fdDF0.js} +1 -1
  73. package/dist/client/assets/{pegDiagram-VL7TDLO6-_y7nfJFV.js → pegDiagram-VL7TDLO6-DjVJy-Gm.js} +1 -1
  74. package/dist/client/assets/{pieDiagram-7S7Q4E2Y-DhdWHi45.js → pieDiagram-7S7Q4E2Y-ATbd_8vw.js} +1 -1
  75. package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-BRvEIECQ.js → quadrantDiagram-CIZ2JOQS-C8o6ofcl.js} +1 -1
  76. package/dist/client/assets/{railroadDiagram-AXF67PYL-BgLZ8XU1.js → railroadDiagram-AXF67PYL-Qvij9Ubo.js} +1 -1
  77. package/dist/client/assets/{render-markdown-2GjhuGA-.js → render-markdown-BtCybfvQ.js} +6 -6
  78. package/dist/client/assets/{requirementDiagram-LRYGKXZP-CX-gxyGF.js → requirementDiagram-LRYGKXZP-yLfrba2h.js} +1 -1
  79. package/dist/client/assets/{sankeyDiagram-W5VNT64P-CLSjSU_X.js → sankeyDiagram-W5VNT64P-BTXRtTyG.js} +1 -1
  80. package/dist/client/assets/{sequenceDiagram-SI44F4Z6-dGlHyZRB.js → sequenceDiagram-SI44F4Z6-BN1CCZIX.js} +1 -1
  81. package/dist/client/assets/{sizeCapture-X5ZJPWSS-BH3qO9T3.js → sizeCapture-X5ZJPWSS-BfXjBBYo.js} +1 -1
  82. package/dist/client/assets/{stateDiagram-OKZ733FA-FXxo1iIM.js → stateDiagram-OKZ733FA-DMAWiBsP.js} +1 -1
  83. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-D_ofZeaa.js +1 -0
  84. package/dist/client/assets/{swimlanes-SLNWSIFB-VJGIuM4S.js → swimlanes-SLNWSIFB-BD20m5cw.js} +2 -2
  85. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-CF9m5JSB.js +8 -0
  86. package/dist/client/assets/{tanstack-CtfgE41X.js → tanstack-B0Sf7wNU.js} +1 -1
  87. package/dist/client/assets/{timeline-definition-Z64GVDOM-BXNTAanY.js → timeline-definition-Z64GVDOM-gtc0-AHB.js} +1 -1
  88. package/dist/client/assets/{vennDiagram-T6HMQDX7-D9nXAts5.js → vennDiagram-T6HMQDX7-Cdr4AXTp.js} +1 -1
  89. package/dist/client/assets/viewer-tabs-D-T0xQaq.js +1 -0
  90. package/dist/client/assets/{wardleyDiagram-T6FBY63Y-D89EaEfa.js → wardleyDiagram-T6FBY63Y-BcU4vxO8.js} +1 -1
  91. package/dist/client/assets/{xychartDiagram-ELKLHX3M-B_xFDiIh.js → xychartDiagram-ELKLHX3M-B3Zuadj8.js} +1 -1
  92. package/dist/client/index.html +4 -4
  93. package/dist/dev.mjs +1 -1
  94. package/dist/manifest.json +1 -1
  95. package/dist/server.mjs +526 -127
  96. package/dist/utility.mjs +1 -1
  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/PullRequestViewer-CpLwq4o2.js +0 -2
  106. package/dist/client/assets/PullRequestsView-DnvXaJ4t.js +0 -1
  107. package/dist/client/assets/QuickOpenDialog-DIWjfUkt.js +0 -1
  108. package/dist/client/assets/RepositoryProjectsDialog-BsQn5Yjr.js +0 -1
  109. package/dist/client/assets/SearchView-D-91dtSv.js +0 -1
  110. package/dist/client/assets/SettingsDialog-dJUYk2cP.js +0 -9
  111. package/dist/client/assets/Viewer-GH-5bJW4.js +0 -3
  112. package/dist/client/assets/base-ui-DT3JsyND.js +0 -1
  113. package/dist/client/assets/channel-Dsy2VD23.js +0 -1
  114. package/dist/client/assets/classDiagram-JCYQIIEL-BBQJgoWd.js +0 -1
  115. package/dist/client/assets/classDiagram-v2-OCEON4UE-BBQJgoWd.js +0 -1
  116. package/dist/client/assets/gh-utils-LsOSydCg.js +0 -1
  117. package/dist/client/assets/index-DMbFAH51.css +0 -1
  118. package/dist/client/assets/stateDiagram-v2-UEYNNEHI-DqxQ14yP.js +0 -1
  119. package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-No0dCAkd.js +0 -8
  120. 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
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";
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";
@@ -6872,6 +6872,153 @@ createExeca(() => ({ isSync: true }));
6872
6872
  createExeca(mapNode);
6873
6873
  createExeca(mapScriptAsync, {}, deepScriptOptions, setScriptSync);
6874
6874
  getIpcExport();
6875
+ 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"];
6876
+ function currentCliHost() {
6877
+ let shell;
6878
+ try {
6879
+ shell = os.userInfo().shell ?? void 0;
6880
+ } catch {
6881
+ }
6882
+ return { platform: process.platform, home: os.homedir(), env: cleanEnvironment(process.env), shell: process.env.SHELL || shell };
6883
+ }
6884
+ function cleanEnvironment(input, platform2 = process.platform) {
6885
+ const result = {};
6886
+ for (const [key, value] of Object.entries(input)) {
6887
+ if (value !== void 0) result[platform2 === "win32" && key.toUpperCase() === "PATH" ? "PATH" : key] = value;
6888
+ }
6889
+ return result;
6890
+ }
6891
+ function pathDirectories(value, platform2) {
6892
+ const p = platform2 === "win32" ? path.win32 : path.posix;
6893
+ const seen = /* @__PURE__ */ new Set();
6894
+ return (value ?? "").split(platform2 === "win32" ? ";" : ":").map((item) => item.replace(/^"(.*)"$/, "$1")).filter((item) => {
6895
+ if (!p.isAbsolute(item)) return false;
6896
+ const key = platform2 === "win32" ? p.normalize(item).toLowerCase() : p.normalize(item);
6897
+ if (seen.has(key)) return false;
6898
+ seen.add(key);
6899
+ return true;
6900
+ });
6901
+ }
6902
+ class CliEnvironment {
6903
+ constructor(host = currentCliHost(), read = readUserEnvironment) {
6904
+ this.host = host;
6905
+ this.read = read;
6906
+ }
6907
+ host;
6908
+ read;
6909
+ cached;
6910
+ pending;
6911
+ generation = 0;
6912
+ invalidate() {
6913
+ this.generation++;
6914
+ this.cached = void 0;
6915
+ this.pending = void 0;
6916
+ }
6917
+ async get(force = false) {
6918
+ const env = cleanEnvironment(this.host.env, this.host.platform);
6919
+ const inheritedPath = pathDirectories(env.PATH, this.host.platform);
6920
+ if (this.pending) return this.pending;
6921
+ if (!force && this.cached && Date.now() - this.cached.at < 3e4) return this.cached.value;
6922
+ const generation = this.generation;
6923
+ const pending = (async () => {
6924
+ let locations = {};
6925
+ let warning;
6926
+ try {
6927
+ locations = await this.read(this.host);
6928
+ } catch {
6929
+ warning = "Could not refresh the user environment. Using the inherited environment and known installation locations. Check your shell configuration or set an executable path.";
6930
+ }
6931
+ const refreshedPath = pathDirectories(locations.PATH, this.host.platform);
6932
+ const value = { env: { ...env, ...locations }, inheritedPath, refreshedPath, ...warning ? { warning } : {} };
6933
+ if (generation === this.generation) this.cached = { at: Date.now(), value };
6934
+ return value;
6935
+ })();
6936
+ this.pending = pending;
6937
+ try {
6938
+ return await pending;
6939
+ } finally {
6940
+ if (this.pending === pending) this.pending = void 0;
6941
+ }
6942
+ }
6943
+ }
6944
+ async function readUserEnvironment(host) {
6945
+ const options = {
6946
+ cwd: host.home,
6947
+ env: host.env,
6948
+ extendEnv: false,
6949
+ input: "",
6950
+ timeout: 5e3,
6951
+ maxBuffer: 128 * 1024,
6952
+ cleanup: true,
6953
+ killDescendants: true,
6954
+ windowsHide: true,
6955
+ reject: false,
6956
+ stripFinalNewline: false
6957
+ };
6958
+ if (host.platform === "win32") {
6959
+ const root = host.env.SystemRoot || host.env.SYSTEMROOT;
6960
+ if (!root || !path.win32.isAbsolute(root)) throw new Error("SystemRoot unavailable");
6961
+ const shell2 = path.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
6962
+ const script = '$ErrorActionPreference="Stop"; $r=@{}; foreach($scope in @("Machine","User")){ $r[$scope]=[Environment]::GetEnvironmentVariables($scope) }; ConvertTo-Json -InputObject $r -Compress';
6963
+ const result = await execa(shell2, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], options);
6964
+ if (result.exitCode !== 0 || result.timedOut || result.isMaxBuffer) throw new Error("Environment query failed");
6965
+ return windowsLocations(JSON.parse(result.stdout), host.env);
6966
+ }
6967
+ const shell = host.shell;
6968
+ if (!shell || !path.posix.isAbsolute(shell)) throw new Error("User shell unavailable");
6969
+ const kind = path.posix.basename(shell);
6970
+ if (!["bash", "zsh", "fish"].includes(kind)) throw new Error("Unsupported user shell");
6971
+ const started = Date.now();
6972
+ const modes = kind === "bash" ? ["-lic", "-ic"] : ["-lic"];
6973
+ let merged = {};
6974
+ let succeeded = false;
6975
+ for (const mode of modes) {
6976
+ const timeout = 5e3 - (Date.now() - started);
6977
+ if (timeout <= 0) break;
6978
+ 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("; ");
6979
+ const result = await execa(shell, [mode, script], { ...options, timeout });
6980
+ if (result.exitCode !== 0 || result.timedOut || result.isMaxBuffer) continue;
6981
+ const locations = parseShellLocations(result.stdout);
6982
+ if (!locations.PATH) continue;
6983
+ succeeded = true;
6984
+ merged = { ...merged, ...locations, PATH: [locations.PATH, merged.PATH].filter(Boolean).join(":") };
6985
+ }
6986
+ if (!succeeded) throw new Error("Shell environment query failed");
6987
+ return merged;
6988
+ }
6989
+ function parseShellLocations(output) {
6990
+ const locations = {};
6991
+ for (const key of LOCATION_KEYS) {
6992
+ const marker = `\0OPENTIG_ENV_${key}\0`;
6993
+ const start = output.lastIndexOf(marker);
6994
+ if (start < 0) continue;
6995
+ const end = output.indexOf("\0", start + marker.length);
6996
+ if (end < 0) continue;
6997
+ const value = output.slice(start + marker.length, end);
6998
+ if (value && !/[\r\n]/.test(value)) locations[key] = value;
6999
+ }
7000
+ return locations;
7001
+ }
7002
+ function windowsLocations(raw, inherited) {
7003
+ const data = raw;
7004
+ if (!data || typeof data !== "object" || !data.Machine || !data.User) throw new Error("Invalid environment response");
7005
+ const upper = (input) => Object.fromEntries(Object.entries(input).filter(([, v]) => typeof v === "string").map(([k, v]) => [k.toUpperCase(), v]));
7006
+ const machine = upper(data.Machine), user = upper(data.User);
7007
+ const variables = { ...upper(inherited), ...machine, ...user };
7008
+ const expand = (input) => {
7009
+ let value = input;
7010
+ for (let i2 = 0; i2 < 10; i2++) {
7011
+ const next = value.replace(/%([^%]+)%/g, (match, key) => variables[key.toUpperCase()] ?? match);
7012
+ if (next === value) break;
7013
+ value = next;
7014
+ }
7015
+ return value;
7016
+ };
7017
+ const result = {};
7018
+ for (const key of LOCATION_KEYS) if (key !== "PATH" && variables[key.toUpperCase()]) result[key] = expand(variables[key.toUpperCase()]);
7019
+ result.PATH = [machine.PATH, user.PATH].filter((value) => !!value).map(expand).join(";");
7020
+ return result;
7021
+ }
6875
7022
  class GitOperationError extends Error {
6876
7023
  detail;
6877
7024
  constructor(detail) {
@@ -6939,17 +7086,18 @@ class CliProcessRunner {
6939
7086
  async run(command2, args, options = {}) {
6940
7087
  const timeoutMs = options.timeoutMs ?? 15e3;
6941
7088
  const maxOutputBytes = options.maxOutputBytes ?? 2 * 1024 * 1024;
6942
- const environment = Object.fromEntries(
7089
+ const environment = cleanEnvironment(Object.fromEntries(
6943
7090
  Object.entries({ ...process.env, ...options.env }).filter((entry) => entry[1] !== void 0)
6944
- );
7091
+ ));
6945
7092
  for (const key of options.removeEnv ?? []) delete environment[key];
6946
7093
  if (this.closePromise || options.signal?.aborted) throw cancelled();
6947
7094
  const lifecycle = new AbortController();
6948
7095
  this.active.add(lifecycle);
6949
7096
  const signal = options.signal ? AbortSignal.any([options.signal, lifecycle.signal]) : lifecycle.signal;
6950
- const resolvedCommand = resolveProcessCommand(command2, options.cwd ?? process.cwd(), environment);
7097
+ let resolvedCommand;
6951
7098
  let result;
6952
7099
  try {
7100
+ resolvedCommand = resolveProcessCommand(command2, options.cwd ?? process.cwd(), environment);
6953
7101
  result = await execa(resolvedCommand.file, args, {
6954
7102
  ...options.cwd ? { cwd: options.cwd } : {},
6955
7103
  cancelSignal: signal,
@@ -6964,9 +7112,9 @@ class CliProcessRunner {
6964
7112
  input: options.stdin ?? "",
6965
7113
  stripFinalNewline: false,
6966
7114
  reject: false
6967
- }).catch(() => {
7115
+ }).catch((error) => {
6968
7116
  if (signal.aborted) throw cancelled();
6969
- throw processFailed();
7117
+ throw processFailed(error);
6970
7118
  });
6971
7119
  } finally {
6972
7120
  this.active.delete(lifecycle);
@@ -6983,7 +7131,7 @@ class CliProcessRunner {
6983
7131
  throw new AiOperationError({ code: "AI_CONTEXT_TOO_LARGE", operation: "ai-process", message: "The AI tool produced too much output." });
6984
7132
  }
6985
7133
  if (result.exitCode === void 0 || !resolvedCommand.found) {
6986
- throw processFailed();
7134
+ throw processFailed(result);
6987
7135
  }
6988
7136
  return { exitCode: result.exitCode, stdout: result.stdout, stderr: result.stderr };
6989
7137
  }
@@ -7008,49 +7156,143 @@ class CliProcessRunner {
7008
7156
  function cancelled() {
7009
7157
  return new AiOperationError({ code: "AI_CANCELLED", operation: "ai-process", message: "Generation canceled." });
7010
7158
  }
7011
- function processFailed() {
7012
- return new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "ai-process", message: "Could not start the AI tool." });
7159
+ class CliLaunchError extends AiOperationError {
7160
+ constructor(reason) {
7161
+ super({ code: "AI_PROCESS_FAILED", operation: "ai-process", message: "Could not start the AI tool." });
7162
+ this.reason = reason;
7163
+ }
7164
+ reason;
7165
+ }
7166
+ function processFailed(error) {
7167
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
7168
+ return new CliLaunchError(code === "ENOENT" ? "missing" : code === "EACCES" || code === "EPERM" ? "permission" : code === "ENOEXEC" ? "format" : "unknown");
7013
7169
  }
7014
- const ALLOWED = /* @__PURE__ */ new Set(["codex", "claude", "opencode", "grok", "gh"]);
7015
7170
  const ALIASES = {
7016
7171
  codex: ["codex"],
7017
7172
  claude: ["claude"],
7018
- // Current v2 uses opencode; older distributions also provide opencode2.
7019
7173
  opencode: ["opencode", "opencode2"],
7020
7174
  grok: ["grok"],
7021
7175
  gh: ["gh"]
7022
7176
  };
7023
7177
  class CliResolver {
7178
+ constructor(environment = new CliEnvironment(), preferences = () => ({ aiExecutablePaths: {} })) {
7179
+ this.environment = environment;
7180
+ this.preferences = preferences;
7181
+ }
7182
+ environment;
7183
+ preferences;
7024
7184
  cache = /* @__PURE__ */ new Map();
7185
+ settingsKey = "";
7186
+ generation = 0;
7187
+ invalidate() {
7188
+ this.generation++;
7189
+ this.cache.clear();
7190
+ this.environment.invalidate();
7191
+ }
7025
7192
  async resolve(name, forceRefresh = false) {
7026
7193
  return (await this.resolveAll(name, forceRefresh))[0] ?? null;
7027
7194
  }
7028
7195
  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 {
7196
+ return (await this.discover(name, forceRefresh)).filter((item) => !item.problem).map((item) => item.executable);
7197
+ }
7198
+ async discover(name, forceRefresh = false) {
7199
+ if (!Object.hasOwn(ALIASES, name)) return [];
7200
+ const preferences = this.preferences();
7201
+ const settingsKey = JSON.stringify(preferences.aiExecutablePaths);
7202
+ if (settingsKey !== this.settingsKey) {
7203
+ this.settingsKey = settingsKey;
7204
+ this.invalidate();
7205
+ }
7206
+ const key = name;
7207
+ if (forceRefresh) {
7208
+ this.generation++;
7209
+ this.cache.delete(key);
7210
+ }
7211
+ const generation = this.generation;
7212
+ const cached2 = this.cache.get(key);
7213
+ if (!forceRefresh && cached2 && Date.now() - cached2.at < 3e4) {
7214
+ if ((await Promise.all(cached2.candidates.map((item) => this.problem(item.executable)))).every((problem, index) => problem === cached2.candidates[index]?.problem)) {
7215
+ return structuredClone(cached2.candidates);
7216
+ }
7217
+ }
7218
+ const snapshot = await this.environment.get(forceRefresh);
7219
+ const host = this.environment.host;
7220
+ const p = host.platform === "win32" ? path.win32 : path.posix;
7221
+ const known = knownCliDirectories(name, host.home, host.platform, snapshot.env);
7222
+ const allKnown = [...new Set(Object.keys(ALIASES).flatMap((id) => knownCliDirectories(id, host.home, host.platform, snapshot.env)))];
7223
+ const runtimePath = host.platform === "win32" ? [...snapshot.inheritedPath, ...snapshot.refreshedPath] : [...snapshot.refreshedPath, ...snapshot.inheritedPath];
7224
+ const env = { ...snapshot.env, PATH: pathDirectories([...runtimePath, ...allKnown].join(p.delimiter), host.platform).join(p.delimiter) };
7225
+ if (host.env.XDG_DATA_HOME === void 0) delete env.XDG_DATA_HOME;
7226
+ else env.XDG_DATA_HOME = host.env.XDG_DATA_HOME;
7227
+ const configured = name === "gh" ? void 0 : preferences.aiExecutablePaths[name];
7228
+ const candidates = [];
7229
+ const seen = /* @__PURE__ */ new Set();
7230
+ const add = async (executable, alias, source) => {
7231
+ const problem = await this.problem(executable);
7232
+ if (problem === "not-found" && source !== "configured") return;
7233
+ const canonical = await realpath(executable).catch(() => executable);
7234
+ const identity2 = host.platform === "win32" ? canonical.toLowerCase() : canonical;
7235
+ if (seen.has(identity2)) return;
7236
+ seen.add(identity2);
7237
+ candidates.push({ executable, alias, source, env, ...problem ? { problem } : {}, ...snapshot.warning ? { warning: snapshot.warning } : {} });
7238
+ };
7239
+ if (configured) {
7240
+ await add(configured, p.basename(configured), "configured");
7241
+ } else {
7242
+ for (const [directories, source] of [[snapshot.inheritedPath, "process-path"], [snapshot.refreshedPath, "user-path"], [known, "known-location"]]) {
7243
+ for (const directory of directories) {
7244
+ for (const alias of ALIASES[name]) {
7245
+ for (const extension of executableExtensions(host.platform, snapshot.env)) await add(p.join(directory, `${alias}${extension}`), alias, source);
7246
+ }
7247
+ }
7050
7248
  }
7051
7249
  }
7250
+ if (generation === this.generation) this.cache.set(key, { at: Date.now(), candidates });
7251
+ return structuredClone(candidates);
7052
7252
  }
7053
- return null;
7253
+ async warning() {
7254
+ return (await this.environment.get()).warning;
7255
+ }
7256
+ async problem(executable) {
7257
+ try {
7258
+ if (!(await stat(executable)).isFile()) return "not-executable";
7259
+ if (this.environment.host.platform === "win32") {
7260
+ if (![".exe", ".com", ".cmd", ".bat"].includes(path.win32.extname(executable).toLowerCase())) return "not-executable";
7261
+ } else await access(executable, constants$1.X_OK);
7262
+ return void 0;
7263
+ } catch (error) {
7264
+ return error.code === "EACCES" ? "not-executable" : "not-found";
7265
+ }
7266
+ }
7267
+ }
7268
+ function executableExtensions(platform2, env) {
7269
+ if (platform2 !== "win32") return [""];
7270
+ const value = Object.entries(env).find(([key]) => key.toUpperCase() === "PATHEXT")?.[1] ?? ".COM;.EXE;.BAT;.CMD";
7271
+ return [...new Set(value.split(";").map((item) => item.toLowerCase()).filter((item) => [".exe", ".com", ".cmd", ".bat"].includes(item)))];
7272
+ }
7273
+ function knownCliDirectories(name, home, platform2, env) {
7274
+ const p = platform2 === "win32" ? path.win32 : path.posix;
7275
+ const dirs = [p.join(home, ".local", "bin")];
7276
+ if (name === "opencode") dirs.push(p.join(home, ".opencode", "bin"));
7277
+ if (name === "grok") dirs.push(env.GROK_BIN_DIR, p.join(home, ".grok", "bin"));
7278
+ if (name === "codex") dirs.push(env.CODEX_INSTALL_DIR);
7279
+ dirs.push(env.PNPM_HOME, env.NVM_BIN, p.join(env.BUN_INSTALL || p.join(home, ".bun"), "bin"));
7280
+ if (env.FNM_MULTISHELL_PATH) dirs.push(platform2 === "win32" ? env.FNM_MULTISHELL_PATH : p.join(env.FNM_MULTISHELL_PATH, "bin"));
7281
+ const prefix = env.npm_config_prefix || env.NPM_CONFIG_PREFIX;
7282
+ if (prefix) dirs.push(platform2 === "win32" ? prefix : p.join(prefix, "bin"));
7283
+ if (platform2 === "win32") {
7284
+ if (env.APPDATA) dirs.push(p.join(env.APPDATA, "npm"));
7285
+ if (env.LOCALAPPDATA) {
7286
+ dirs.push(p.join(env.LOCALAPPDATA, "Volta", "bin"), p.join(env.LOCALAPPDATA, "pnpm"), p.join(env.LOCALAPPDATA, "Microsoft", "WinGet", "Links"));
7287
+ if (name === "codex") dirs.push(p.join(env.LOCALAPPDATA, "Programs", "OpenAI", "Codex", "bin"));
7288
+ }
7289
+ if (env.VOLTA_HOME) dirs.push(p.join(env.VOLTA_HOME, "bin"));
7290
+ } else {
7291
+ dirs.push(p.join(env.VOLTA_HOME || p.join(home, ".volta"), "bin"), p.join(home, ".npm-global", "bin"), p.join(home, ".yarn", "bin"));
7292
+ dirs.push(p.join(env.XDG_DATA_HOME || p.join(home, ".local", "share"), "pnpm"));
7293
+ dirs.push("/usr/local/bin", "/usr/bin", "/bin", platform2 === "darwin" ? "/opt/homebrew/bin" : "/home/linuxbrew/.linuxbrew/bin");
7294
+ }
7295
+ return [...new Set(dirs.filter((item) => !!item && p.isAbsolute(item)))];
7054
7296
  }
7055
7297
  var _a$1;
7056
7298
  function $constructor(name, initializer2, params) {
@@ -12216,22 +12458,58 @@ function cleanText(value, maxLength) {
12216
12458
  const text2 = value.trim();
12217
12459
  return !text2 || text2.length > maxLength || hasControlCharacters$6(text2, true) ? null : text2;
12218
12460
  }
12461
+ const DEFAULT_MODEL$1 = { id: "default", label: "Default (CLI)" };
12219
12462
  class CommitMessageService {
12220
- constructor(operations, providers, log) {
12463
+ constructor(operations, providers, log, invalidateDiscovery) {
12221
12464
  this.operations = operations;
12222
12465
  this.log = log;
12466
+ this.invalidateDiscovery = invalidateDiscovery;
12223
12467
  for (const provider of providers) this.providers.set(provider.id, provider);
12224
12468
  }
12225
12469
  operations;
12226
12470
  log;
12471
+ invalidateDiscovery;
12227
12472
  providers = /* @__PURE__ */ new Map();
12228
12473
  active = /* @__PURE__ */ new Map();
12229
12474
  statusCache = null;
12475
+ statusGeneration = 0;
12476
+ pendingStatus;
12477
+ invalidateStatuses() {
12478
+ this.statusGeneration++;
12479
+ this.statusCache = null;
12480
+ this.pendingStatus = void 0;
12481
+ this.invalidateDiscovery?.();
12482
+ }
12230
12483
  async statuses(forceRefresh = false) {
12231
12484
  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;
12485
+ if (this.pendingStatus && (!forceRefresh || this.pendingStatus.force)) return this.pendingStatus.promise;
12486
+ const generation = ++this.statusGeneration;
12487
+ const promise = Promise.all([...this.providers.values()].map(async (provider) => {
12488
+ try {
12489
+ return await provider.status(forceRefresh);
12490
+ } catch {
12491
+ return {
12492
+ id: provider.id,
12493
+ label: { codex: "Codex", claude: "Claude Code", opencode: "OpenCode", grok: "Grok Build" }[provider.id],
12494
+ installed: false,
12495
+ availability: "error",
12496
+ authStatus: "unknown",
12497
+ installationStatus: "inspection-failed",
12498
+ message: "Could not inspect this CLI. Check again or review its executable path.",
12499
+ models: [DEFAULT_MODEL$1],
12500
+ checkedAt: (/* @__PURE__ */ new Date()).toISOString()
12501
+ };
12502
+ }
12503
+ })).then((value) => {
12504
+ if (generation === this.statusGeneration) this.statusCache = { at: Date.now(), value };
12505
+ return value;
12506
+ });
12507
+ this.pendingStatus = { force: forceRefresh, promise };
12508
+ try {
12509
+ return await promise;
12510
+ } finally {
12511
+ if (this.pendingStatus?.promise === promise) this.pendingStatus = void 0;
12512
+ }
12235
12513
  }
12236
12514
  async generate(input, externalSignal) {
12237
12515
  if (this.active.has(input.requestId) || [...this.active.values()].some((entry) => entry.repositoryId === input.repositoryId) || this.active.size >= 3) {
@@ -12468,9 +12746,53 @@ function throwIfCancelled(signal, harness) {
12468
12746
  if (!signal?.aborted) return;
12469
12747
  throw new AiOperationError({ code: "AI_CANCELLED", operation: "ai-pr-draft", harness, message: "Generation canceled." });
12470
12748
  }
12749
+ function runCandidate(runner, candidate, args, options = {}) {
12750
+ return runner.run(candidate.executable, args, { ...options, env: { ...candidate.env, ...options.env } });
12751
+ }
12752
+ async function selectCli(resolver, runner, name, options = {}) {
12753
+ let first;
12754
+ const candidates = await resolver.discover(name, options.forceRefresh);
12755
+ for (const candidate of candidates) {
12756
+ if (options.runOptions?.signal?.aborted) throw new AiOperationError({ code: "AI_CANCELLED", operation: "cli-discovery", message: "Generation canceled." });
12757
+ if (candidate.problem) {
12758
+ first ??= { candidate, state: candidate.problem };
12759
+ continue;
12760
+ }
12761
+ try {
12762
+ const result = await runCandidate(runner, candidate, ["--version"], options.runOptions);
12763
+ const raw = result.stdout.trim() || result.stderr.trim();
12764
+ const version2 = (raw.split(/\r?\n/)[0] ?? "").slice(0, 200);
12765
+ if (result.exitCode !== 0 || !version2) {
12766
+ first ??= { candidate, state: "not-executable" };
12767
+ continue;
12768
+ }
12769
+ if (options.compatible && !options.compatible(raw)) {
12770
+ first ??= { candidate, version: version2, state: "incompatible" };
12771
+ continue;
12772
+ }
12773
+ return { candidate, version: version2, state: "available", warning: candidate.warning };
12774
+ } catch (error) {
12775
+ if (options.runOptions?.signal?.aborted) throw error;
12776
+ first ??= { candidate, state: "not-executable" };
12777
+ }
12778
+ }
12779
+ const warning = await resolver.warning();
12780
+ return first ? { ...first, warning } : { state: warning ? "inspection-failed" : "not-found", warning };
12781
+ }
12782
+ function detectionFields(detected) {
12783
+ return { installationStatus: detected.state, ...detected.candidate ? { executablePath: detected.candidate.executable, executableSource: detected.candidate.source } : {}, ...detected.warning ? { discoveryWarning: detected.warning } : {} };
12784
+ }
12785
+ function detectionFailure(detected, base, unsupported) {
12786
+ if (detected.state === "available") return null;
12787
+ 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.";
12788
+ return { ...base, ...detectionFields(detected), installed: !!detected.candidate && detected.state !== "not-found", availability: "error", authStatus: "unknown", ...detected.version ? { version: detected.version } : {}, message };
12789
+ }
12790
+ function requireCandidate(detected, name) {
12791
+ 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.` });
12792
+ return detected.candidate;
12793
+ }
12471
12794
  const AI_PROVIDER_TIMEOUT_MS = 9 * 6e4;
12472
12795
  const AI_SERVER_TIMEOUT_MS = AI_PROVIDER_TIMEOUT_MS + 3e4;
12473
- const DEFAULT_MODEL$1 = { id: "default", label: "Default (CLI)" };
12474
12796
  function grokUsage(envelope) {
12475
12797
  if (!envelope || typeof envelope !== "object") return { ...EMPTY_AI_USAGE };
12476
12798
  const record2 = envelope;
@@ -12674,10 +12996,11 @@ class ClaudeProvider {
12674
12996
  id = "claude";
12675
12997
  async status(forceRefresh = false) {
12676
12998
  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"]);
12999
+ const detected = await selectCli(this.resolver, this.runner, "claude", { forceRefresh });
13000
+ const failure2 = detectionFailure(detected, { id: this.id, label: "Claude Code", models: MODELS, checkedAt });
13001
+ if (failure2) return failure2;
13002
+ const executable = requireCandidate(detected, this.id);
13003
+ const auth = await runCandidate(this.runner, executable, ["auth", "status", "--json"]);
12681
13004
  let loggedIn;
12682
13005
  try {
12683
13006
  loggedIn = auth.exitCode === 0 && JSON.parse(auth.stdout).loggedIn === true;
@@ -12685,23 +13008,23 @@ class ClaudeProvider {
12685
13008
  loggedIn = false;
12686
13009
  }
12687
13010
  return {
13011
+ ...detectionFields(detected),
12688
13012
  id: this.id,
12689
13013
  label: "Claude Code",
12690
13014
  availability: loggedIn ? "ready" : "error",
12691
13015
  installed: true,
12692
13016
  authStatus: loggedIn ? "authenticated" : "unauthenticated",
12693
- version: version2.stdout.trim() || version2.stderr.trim(),
13017
+ version: detected.version ?? "",
12694
13018
  ...!loggedIn ? { message: "Run claude auth login." } : {},
12695
13019
  models: MODELS,
12696
13020
  checkedAt
12697
13021
  };
12698
13022
  }
12699
13023
  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." });
13024
+ const executable = requireCandidate(await selectCli(this.resolver, this.runner, "claude", { runOptions: { signal: input.signal } }), this.id);
12702
13025
  const args = ["-p", "--output-format", "json", "--json-schema", JSON.stringify(input.schema), "--tools", "", "--no-session-persistence", "--safe-mode"];
12703
13026
  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"] });
13027
+ 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
13028
  requireSuccess(result, this.id, "claude-generate");
12706
13029
  try {
12707
13030
  const envelope = JSON.parse(result.stdout);
@@ -12724,31 +13047,32 @@ class CodexProvider {
12724
13047
  id = "codex";
12725
13048
  async status(forceRefresh = false) {
12726
13049
  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"]);
13050
+ const detected = await selectCli(this.resolver, this.runner, "codex", { forceRefresh });
13051
+ const failure2 = detectionFailure(detected, { id: this.id, label: "Codex", models: [DEFAULT_MODEL$1], checkedAt });
13052
+ if (failure2) return failure2;
13053
+ const executable = requireCandidate(detected, this.id);
13054
+ const login = await runCandidate(this.runner, executable, ["login", "status"]);
12731
13055
  if (login.exitCode !== 0 || !/logged in/i.test(`${login.stdout}
12732
13056
  ${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 };
13057
+ 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 };
12734
13058
  }
12735
- const catalog = await this.runner.run(executable, ["debug", "models", "--bundled"]);
13059
+ const catalog = await runCandidate(this.runner, executable, ["debug", "models", "--bundled"]);
12736
13060
  const models = catalog.exitCode === 0 ? parseCodexModels(catalog.stdout) : [DEFAULT_MODEL$1];
12737
13061
  return {
13062
+ ...detectionFields(detected),
12738
13063
  id: this.id,
12739
13064
  label: "Codex",
12740
13065
  availability: models.length > 1 ? "ready" : "warning",
12741
13066
  installed: true,
12742
13067
  authStatus: "authenticated",
12743
- version: version2.stdout.trim() || version2.stderr.trim(),
13068
+ version: detected.version ?? "",
12744
13069
  ...models.length > 1 ? {} : { message: "Available; the model catalog could not be verified." },
12745
13070
  models,
12746
13071
  checkedAt
12747
13072
  };
12748
13073
  }
12749
13074
  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." });
13075
+ const executable = requireCandidate(await selectCli(this.resolver, this.runner, "codex", { runOptions: { signal: input.signal } }), this.id);
12752
13076
  const temporary = await mkdtemp(path.join(os.tmpdir(), "opentig-codex-"));
12753
13077
  const schemaPath = path.join(temporary, "schema.json");
12754
13078
  const outputPath = path.join(temporary, "output.json");
@@ -12757,7 +13081,7 @@ ${login.stderr}`)) {
12757
13081
  const args = ["exec", "--json", "--ephemeral", "--skip-git-repo-check", "-s", "read-only"];
12758
13082
  if (input.model !== "default") args.push("--model", input.model);
12759
13083
  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"] });
13084
+ 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
13085
  requireSuccess(result, this.id, "codex-generate");
12762
13086
  return { output: parseJsonPayload(await readFile(outputPath, "utf8")), usage: codexUsage(result.stdout) };
12763
13087
  } finally {
@@ -12832,16 +13156,15 @@ class GrokProvider {
12832
13156
  async status(forceRefresh = false) {
12833
13157
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
12834
13158
  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
13159
  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 };
13160
+ const detected = await selectCli(this.resolver, this.runner, this.id, { forceRefresh, compatible: isSupportedGrokVersion, runOptions: options });
13161
+ const failure2 = detectionFailure(detected, base, UNSUPPORTED);
13162
+ if (failure2) return failure2;
13163
+ const executable = requireCandidate(detected, this.id);
13164
+ const installed = { ...base, ...detectionFields(detected), installed: true, version: detected.version ?? "" };
12842
13165
  try {
12843
13166
  await this.checkControls(executable, options);
12844
- const result = await this.runner.run(executable, ["models"], options);
13167
+ const result = await runCandidate(this.runner, executable, ["models"], options);
12845
13168
  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
13169
  const { authStatus, models } = parseGrokModels(result.stdout);
12847
13170
  return {
@@ -12857,15 +13180,12 @@ class GrokProvider {
12857
13180
  });
12858
13181
  }
12859
13182
  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
13183
  const timeout = AbortSignal.timeout(AI_PROVIDER_TIMEOUT_MS);
12863
13184
  const signal = AbortSignal.any([input.signal, timeout]);
12864
13185
  try {
12865
13186
  return await this.withEnvironment(async (options) => {
12866
13187
  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);
13188
+ const executable = requireCandidate(await selectCli(this.resolver, this.runner, this.id, { compatible: isSupportedGrokVersion, runOptions: controlled }), this.id);
12869
13189
  await this.checkControls(executable, controlled);
12870
13190
  const promptFile = path.join(options.cwd, "prompt.txt");
12871
13191
  await writeFile(promptFile, input.prompt, { encoding: "utf8", mode: 384 });
@@ -12891,7 +13211,7 @@ class GrokProvider {
12891
13211
  "--no-memory"
12892
13212
  ];
12893
13213
  if (input.model !== "default") args.push("--model", input.model);
12894
- const result = await this.runner.run(executable, args, { ...controlled, timeoutMs: AI_PROVIDER_TIMEOUT_MS });
13214
+ const result = await runCandidate(this.runner, executable, args, { ...controlled, timeoutMs: AI_PROVIDER_TIMEOUT_MS });
12895
13215
  requireGrokSuccess(result);
12896
13216
  const parsed = parseGrokOutput(result.stdout);
12897
13217
  return { output: parsed.output, usage: grokUsage(parsed.envelope) };
@@ -12903,9 +13223,9 @@ class GrokProvider {
12903
13223
  }
12904
13224
  }
12905
13225
  async checkControls(executable, options) {
12906
- const help = await this.runner.run(executable, ["--help"], options);
13226
+ const help = await runCandidate(this.runner, executable, ["--help"], options);
12907
13227
  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);
13228
+ const result = await runCandidate(this.runner, executable, ["inspect", "--json"], options);
12909
13229
  let report;
12910
13230
  try {
12911
13231
  report = JSON.parse(result.stdout);
@@ -13015,13 +13335,14 @@ async function startOpenCodeV2Server(options) {
13015
13335
  const timeoutMs = options.timeoutMs;
13016
13336
  const password = randomBytes(24).toString("base64url");
13017
13337
  const port = await freePort();
13018
- const environment = Object.fromEntries(
13338
+ const environment = cleanEnvironment(Object.fromEntries(
13019
13339
  Object.entries({
13020
13340
  ...process.env,
13341
+ ...options.env,
13021
13342
  OPENCODE_SERVER_USERNAME: SERVER_USERNAME,
13022
13343
  OPENCODE_SERVER_PASSWORD: password
13023
13344
  }).filter((entry) => entry[1] !== void 0)
13024
- );
13345
+ ));
13025
13346
  const resolved = resolveProcessCommand(options.executable, options.cwd ?? process.cwd(), environment);
13026
13347
  if (!resolved.found) {
13027
13348
  throw new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "opencode-server", harness: "opencode", message: "Could not start the local OpenCode server.", retryable: true });
@@ -13221,13 +13542,14 @@ class OpenCodeProvider {
13221
13542
  async status(forceRefresh = false) {
13222
13543
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
13223
13544
  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 };
13545
+ const failure2 = detectionFailure(detected, { id: this.id, label: "OpenCode", models: [DEFAULT_MODEL$1], checkedAt }, UNSUPPORTED_MESSAGE);
13546
+ if (failure2) return failure2;
13547
+ const executable = requireCandidate(detected, this.id);
13548
+ const version2 = detected.version ?? "";
13549
+ const cliName = openCodeCliName(executable.executable);
13228
13550
  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 })
13551
+ runCandidate(this.runner, executable, ["auth", "list", "--format", "json", "--standalone"]),
13552
+ runCandidate(this.runner, executable, ["models", "--standalone"], { timeoutMs: 3e4 })
13231
13553
  ]);
13232
13554
  const models = catalog.exitCode === 0 ? parseOpenCodeModels(catalog.stdout) : [DEFAULT_MODEL$1];
13233
13555
  let authStatus = parseOpenCodeAuthList(auth.stdout, auth.exitCode);
@@ -13237,6 +13559,7 @@ class OpenCodeProvider {
13237
13559
  id: this.id,
13238
13560
  label: "OpenCode",
13239
13561
  availability: models.length > 1 ? confirmed ? "ready" : "warning" : authStatus === "unauthenticated" ? "error" : "warning",
13562
+ ...detectionFields(detected),
13240
13563
  installed: true,
13241
13564
  authStatus,
13242
13565
  cliName,
@@ -13248,9 +13571,7 @@ class OpenCodeProvider {
13248
13571
  }
13249
13572
  async generate(input) {
13250
13573
  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;
13574
+ const executable = requireCandidate(detected, this.id);
13254
13575
  const timeoutSignal = AbortSignal.timeout(AI_PROVIDER_TIMEOUT_MS);
13255
13576
  const boundedInput = { ...input, signal: AbortSignal.any([input.signal, timeoutSignal]) };
13256
13577
  try {
@@ -13263,7 +13584,7 @@ class OpenCodeProvider {
13263
13584
  }
13264
13585
  }
13265
13586
  async generateV2(executable, input) {
13266
- const server = await startOpenCodeV2Server({ executable, cwd: input.repositoryPath, timeoutMs: 15e3, signal: input.signal }).catch((error) => {
13587
+ const server = await startOpenCodeV2Server({ executable: executable.executable, env: executable.env, cwd: input.repositoryPath, timeoutMs: 15e3, signal: input.signal }).catch((error) => {
13267
13588
  if (error instanceof AiOperationError) throw error;
13268
13589
  throw new AiOperationError({ code: "AI_PROCESS_FAILED", operation: "opencode-server", harness: this.id, message: "Could not start the local OpenCode server.", retryable: true });
13269
13590
  });
@@ -13286,21 +13607,7 @@ ${input.prompt}`, model: input.model, cwd: input.repositoryPath, signal: input.s
13286
13607
  }
13287
13608
  }
13288
13609
  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;
13610
+ return selectCli(this.resolver, this.runner, this.id, { forceRefresh, compatible: isOpenCodeV2Version, runOptions: signal ? { signal } : {} });
13304
13611
  }
13305
13612
  close() {
13306
13613
  for (const server of this.servers) server.close();
@@ -13589,7 +13896,7 @@ class FileService {
13589
13896
  // UI pulls its children through here instead of paying for them at startup.
13590
13897
  async listDirectory(repositoryId, relativePath) {
13591
13898
  const repository = this.repositories.get(repositoryId);
13592
- const directory = this.repositories.resolvePath(repositoryId, relativePath);
13899
+ const directory = relativePath === "" ? repository.path : this.repositories.resolvePath(repositoryId, relativePath);
13593
13900
  const parent = normalizeGitPath(relativePath).replace(/\/+$/, "");
13594
13901
  let dirents;
13595
13902
  try {
@@ -13599,13 +13906,12 @@ class FileService {
13599
13906
  if (code === "ENOENT" || code === "ENOTDIR") return [];
13600
13907
  throw error;
13601
13908
  }
13602
- const ignored = (await this.findIgnoredDirectories(repository.path, [parent])).has(parent);
13603
13909
  const entries = [];
13604
13910
  for (const dirent of dirents) {
13605
13911
  if (dirent.name === ".git") continue;
13606
13912
  const entryPath = parent ? `${parent}/${dirent.name}` : dirent.name;
13607
13913
  if (dirent.isDirectory() && !dirent.isSymbolicLink()) {
13608
- entries.push({ path: entryPath, name: dirent.name, type: "directory", ...ignored ? { ignored: true } : {}, children: [] });
13914
+ entries.push({ path: entryPath, name: dirent.name, type: "directory", children: [] });
13609
13915
  continue;
13610
13916
  }
13611
13917
  if (!dirent.isFile() && !dirent.isSymbolicLink()) continue;
@@ -13616,15 +13922,25 @@ class FileService {
13616
13922
  name: dirent.name,
13617
13923
  type: "file",
13618
13924
  size: metadata.size,
13619
- mtimeMs: metadata.mtimeMs,
13620
- ...ignored ? { ignored: true } : {}
13925
+ mtimeMs: metadata.mtimeMs
13621
13926
  });
13622
13927
  } catch (error) {
13623
13928
  const code = error.code;
13624
13929
  if (code !== "ENOENT" && code !== "ENOTDIR") throw error;
13625
13930
  }
13626
13931
  }
13627
- return entries.sort((a2, b) => a2.type === b.type ? a2.name.localeCompare(b.name) : a2.type === "directory" ? -1 : 1);
13932
+ const ignoredChildren = await this.findIgnoredDirectories(repository.path, entries.map((entry) => entry.path));
13933
+ const ignoredFolders = entries.filter((entry) => entry.type === "directory" && ignoredChildren.has(entry.path));
13934
+ if (ignoredFolders.length > 0) {
13935
+ const tracked = await this.git.run(repository.path, ["ls-files", "-z", "--", ...ignoredFolders.map((entry) => entry.path)], {
13936
+ operation: "list-tracked-folder-children",
13937
+ readOnly: true,
13938
+ maxOutputBytes: 64 * 1024 * 1024
13939
+ });
13940
+ const ancestors = directoryAncestors(tracked.stdout.toString("utf8").split("\0").filter(Boolean).map(normalizeGitPath));
13941
+ for (const folder of ignoredFolders) if (ancestors.has(folder.path)) ignoredChildren.delete(folder.path);
13942
+ }
13943
+ 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
13944
  }
13629
13945
  async findIgnoredDirectories(repositoryPath, directories) {
13630
13946
  const ignored = /* @__PURE__ */ new Set();
@@ -16652,6 +16968,12 @@ class GitHubService {
16652
16968
  const operation = "gh-pr-for-branch";
16653
16969
  const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
16654
16970
  const branch = validateRef(branchName, operation);
16971
+ const openResult = await this.runGh(
16972
+ ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "open", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
16973
+ { cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
16974
+ );
16975
+ const openPull = sortPullRequestsNewestFirst(parsePullRequestList(openResult.stdout)).find((pull) => pull.state === "OPEN");
16976
+ if (openPull) return openPull;
16655
16977
  const result = await this.runGh(
16656
16978
  ["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "all", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
16657
16979
  { cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
@@ -16764,12 +17086,13 @@ class GitHubService {
16764
17086
  }
16765
17087
  async checkStatus(forceRefresh) {
16766
17088
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
16767
- const executable = await this.resolver.resolve("gh", forceRefresh);
17089
+ const detected = await selectCli(this.resolver, this.runner, "gh", { forceRefresh, runOptions: { cwd: os.tmpdir(), env: { ...GH_ENV } } });
17090
+ const executable = detected.candidate;
16768
17091
  if (!executable) {
16769
17092
  return { installed: false, availability: "error", authStatus: "unknown", message: "Install GitHub CLI (cli.github.com) to work with pull requests.", checkedAt };
16770
17093
  }
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();
17094
+ if (detected.state !== "available") return { installed: true, availability: "error", authStatus: "unknown", message: "GitHub CLI was found but could not run. Check its installation and dependencies.", checkedAt };
17095
+ const versionLabel = detected.version ?? "";
16773
17096
  const auth = await this.run(executable, ["auth", "status", "--hostname", "github.com", "--active"], { cwd: os.tmpdir(), operation: "gh-auth-status" });
16774
17097
  if (auth.exitCode !== 0) {
16775
17098
  return { installed: true, availability: "error", authStatus: "unauthenticated", version: versionLabel, message: "Run gh auth login.", checkedAt };
@@ -16788,7 +17111,8 @@ class GitHubService {
16788
17111
  return { repository, nameWithOwner: info.nameWithOwner };
16789
17112
  }
16790
17113
  async runGh(args, options) {
16791
- const executable = await this.resolver.resolve("gh");
17114
+ const detected = await selectCli(this.resolver, this.runner, "gh", { runOptions: { cwd: options.cwd, env: { ...GH_ENV } } });
17115
+ const executable = detected.state === "available" ? detected.candidate : void 0;
16792
17116
  if (!executable) throw new GhOperationError({ code: "GH_CLI_NOT_FOUND", operation: options.operation, message: "GitHub CLI is not installed." });
16793
17117
  const result = await this.run(executable, args, options);
16794
17118
  if (result.exitCode !== 0) throw classifyFailure(result, options.operation);
@@ -16796,9 +17120,9 @@ class GitHubService {
16796
17120
  }
16797
17121
  async run(executable, args, options) {
16798
17122
  try {
16799
- return await this.runner.run(executable, args, {
17123
+ return await this.runner.run(executable.executable, args, {
16800
17124
  cwd: options.cwd,
16801
- env: { ...GH_ENV },
17125
+ env: { ...executable.env, ...GH_ENV },
16802
17126
  ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
16803
17127
  ...options.maxOutputBytes !== void 0 ? { maxOutputBytes: options.maxOutputBytes } : {},
16804
17128
  ...options.stdin !== void 0 ? { stdin: options.stdin } : {}
@@ -17263,6 +17587,7 @@ const defaults = {
17263
17587
  monoFont: "jetbrains-mono",
17264
17588
  commitMessageHarness: "codex",
17265
17589
  commitMessageModels: { codex: "default", claude: "default", opencode: "default", grok: "default" },
17590
+ aiExecutablePaths: {},
17266
17591
  shortcutOverrides: {},
17267
17592
  doubleControlShortcutEnabled: true,
17268
17593
  remoteFetchIntervalSeconds: DEFAULT_REMOTE_FETCH_INTERVAL_SECONDS
@@ -17342,7 +17667,7 @@ class SettingsStore {
17342
17667
  return this.data.activeRepositoryId;
17343
17668
  }
17344
17669
  get preferences() {
17345
- return { ...this.data.preferences, commitMessageModels: { ...this.data.preferences.commitMessageModels }, shortcutOverrides: { ...this.data.preferences.shortcutOverrides } };
17670
+ return { ...this.data.preferences, aiExecutablePaths: { ...this.data.preferences.aiExecutablePaths }, commitMessageModels: { ...this.data.preferences.commitMessageModels }, shortcutOverrides: { ...this.data.preferences.shortcutOverrides } };
17346
17671
  }
17347
17672
  get windowBounds() {
17348
17673
  return { ...this.data.windowBounds };
@@ -17471,6 +17796,8 @@ class SettingsStore {
17471
17796
  next.monoFont = isMonoFont(next.monoFont) ? next.monoFont : "jetbrains-mono";
17472
17797
  next.commitMessageHarness = isHarness(next.commitMessageHarness) ? next.commitMessageHarness : "codex";
17473
17798
  next.commitMessageModels = modelPreferences(next.commitMessageModels);
17799
+ next.aiExecutablePaths = executablePaths(next.aiExecutablePaths, true);
17800
+ delete next.aiShellEnvironment;
17474
17801
  next.shortcutOverrides = sanitizeShortcutOverrides(next.shortcutOverrides);
17475
17802
  next.doubleControlShortcutEnabled = typeof next.doubleControlShortcutEnabled === "boolean" ? next.doubleControlShortcutEnabled : this.defaultDoubleControlShortcutEnabled;
17476
17803
  next.remoteFetchIntervalSeconds = normalizeRemoteFetchIntervalSeconds(next.remoteFetchIntervalSeconds);
@@ -17599,6 +17926,7 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
17599
17926
  monoFont: isMonoFont(parsedPreferences.data.monoFont) ? parsedPreferences.data.monoFont : "jetbrains-mono",
17600
17927
  commitMessageHarness: isHarness(parsedPreferences.data.commitMessageHarness) ? parsedPreferences.data.commitMessageHarness : "codex",
17601
17928
  commitMessageModels: modelPreferences(parsedPreferences.data.commitMessageModels),
17929
+ aiExecutablePaths: executablePaths(parsedPreferences.data.aiExecutablePaths),
17602
17930
  shortcutOverrides: sanitizeShortcutOverrides(parsedPreferences.data.shortcutOverrides),
17603
17931
  doubleControlShortcutEnabled: typeof parsedPreferences.data.doubleControlShortcutEnabled === "boolean" ? parsedPreferences.data.doubleControlShortcutEnabled : defaultDoubleControlShortcutEnabled,
17604
17932
  remoteFetchIntervalSeconds: normalizeRemoteFetchIntervalSeconds(
@@ -17706,6 +18034,31 @@ function normalizeRecent(item) {
17706
18034
  repositoryName: typeof item.repositoryName === "string" && item.repositoryName ? item.repositoryName : path.basename(path.dirname(commonDir))
17707
18035
  };
17708
18036
  }
18037
+ function executablePaths(value, strict = false, platform2 = process.platform, home = os.homedir()) {
18038
+ const result = {};
18039
+ const invalid2 = () => {
18040
+ if (strict) throw projectError("CLI executable paths must be absolute paths on the backend host, without arguments or control characters.");
18041
+ };
18042
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
18043
+ invalid2();
18044
+ return result;
18045
+ }
18046
+ const p = platform2 === "win32" ? path.win32 : path.posix;
18047
+ for (const [key, raw] of Object.entries(value)) {
18048
+ if (!isHarness(key) || typeof raw !== "string") {
18049
+ invalid2();
18050
+ continue;
18051
+ }
18052
+ if (!raw.trim()) continue;
18053
+ const expanded = raw.startsWith("~/") ? p.join(home, raw.slice(2)) : raw;
18054
+ if (raw.length > 4096 || hasControlCharacters$1(raw) || !p.isAbsolute(expanded) || platform2 === "win32" && !/^(?:[a-z]:[\\/]|\\\\)/i.test(expanded)) {
18055
+ invalid2();
18056
+ continue;
18057
+ }
18058
+ result[key] = expanded;
18059
+ }
18060
+ return result;
18061
+ }
17709
18062
  class SystemTrash {
17710
18063
  available;
17711
18064
  operation;
@@ -17824,7 +18177,7 @@ async function createOpenTigRuntime(options) {
17824
18177
  const fileHistory = new FileOperationHistory(files, trash);
17825
18178
  const search = new SearchService(git, repositories, files, fileHistory);
17826
18179
  const operations = new GitRepositoryOperations(git, repositories, files);
17827
- const cliResolver = new CliResolver();
18180
+ const cliResolver = new CliResolver(void 0, () => settings.preferences);
17828
18181
  const cliRunner = new CliProcessRunner();
17829
18182
  const providers = [
17830
18183
  new CodexProvider(cliResolver, cliRunner),
@@ -17834,7 +18187,7 @@ async function createOpenTigRuntime(options) {
17834
18187
  ];
17835
18188
  const aiLog = new AiLogStore(options.aiLogPath);
17836
18189
  await aiLog.load();
17837
- const ai = new CommitMessageService(operations, providers, aiLog);
18190
+ const ai = new CommitMessageService(operations, providers, aiLog, () => cliResolver.invalidate());
17838
18191
  const prDrafts = new PullRequestDraftService(operations, providers, aiLog);
17839
18192
  const github = new GitHubService(cliResolver, cliRunner, git, repositories);
17840
18193
  let runtime = null;
@@ -18314,22 +18667,66 @@ function mimeTypeOf(relativePath, bytes) {
18314
18667
  if (fromPath === "image/svg+xml") return fromPath;
18315
18668
  return detectRasterImageMime(bytes) ?? fromPath;
18316
18669
  }
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.");
18670
+ async function browseServerDirectories(requestedPath, git = new GitProcess()) {
18671
+ const home = homedir();
18672
+ const input = requestedPath ?? home;
18673
+ const expanded = input === "~" ? home : /^~[/\\]/.test(input) ? join(home, input.slice(2)) : input;
18674
+ if (!isAbsolute(expanded)) throw new Error("Enter an absolute folder path.");
18321
18675
  const path2 = resolve(expanded);
18322
- const entries = await readdir(path2, { withFileTypes: true });
18676
+ let entries;
18677
+ try {
18678
+ entries = await readdir(path2, { withFileTypes: true });
18679
+ } catch (error) {
18680
+ const code = error.code;
18681
+ if (code === "EACCES" || code === "EPERM") throw new Error("You do not have permission to browse this folder.", { cause: error });
18682
+ if (code === "ENOENT") throw new Error("That folder was not found. Check the path and try again.", { cause: error });
18683
+ if (code === "ENOTDIR") throw new Error("This path points to a file. Choose a folder instead.", { cause: error });
18684
+ throw new Error("Could not read this folder. Try another location.", { cause: error });
18685
+ }
18323
18686
  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 });
18687
+ let index = 0;
18688
+ await Promise.all(Array.from({ length: Math.min(6, entries.length) }, async () => {
18689
+ while (index < entries.length) {
18690
+ const entry = entries[index++];
18691
+ const entryPath = join(path2, entry.name);
18692
+ if (entry.isDirectory() || entry.isSymbolicLink() && await stat(entryPath).then((value) => value.isDirectory(), () => false)) {
18693
+ directories.push({ name: entry.name, path: entryPath, repository: await inspectRepository(entryPath, git) });
18694
+ }
18328
18695
  }
18329
- }
18696
+ }));
18330
18697
  directories.sort((a2, b) => a2.name.localeCompare(b.name, void 0, { numeric: true, sensitivity: "base" }));
18698
+ const locations = [{ name: "Home", path: home, kind: "home" }];
18699
+ for (const [name, kind] of [["Documents", "documents"], ["Downloads", "downloads"]]) {
18700
+ const location = join(home, name);
18701
+ if (await stat(location).then((value) => value.isDirectory(), () => false)) locations.push({ name, path: location, kind });
18702
+ }
18331
18703
  const parentPath = dirname(path2);
18332
- return { path: path2, parentPath: parentPath === path2 ? null : parentPath, directories };
18704
+ return { path: path2, parentPath: parentPath === path2 ? null : parentPath, repository: await inspectRepository(path2, git), locations, directories };
18705
+ }
18706
+ async function inspectRepository(path2, git) {
18707
+ if (!await stat(join(path2, ".git")).then((value) => value.isDirectory() || value.isFile(), () => false)) return null;
18708
+ try {
18709
+ const result = await git.run(path2, ["rev-parse", "--is-inside-work-tree", "--absolute-git-dir"], {
18710
+ operation: "browse-repository",
18711
+ readOnly: true,
18712
+ timeoutMs: 2e3,
18713
+ maxOutputBytes: 32768
18714
+ });
18715
+ const [inside, gitDir] = result.stdout.toString("utf8").trim().split(/\r?\n/);
18716
+ if (inside !== "true" || !gitDir || !isAbsolute(gitDir)) return null;
18717
+ const headFile = await open(join(gitDir, "HEAD"), "r");
18718
+ let head;
18719
+ try {
18720
+ const buffer = Buffer.alloc(4096);
18721
+ const { bytesRead } = await headFile.read(buffer, 0, buffer.length, 0);
18722
+ head = buffer.subarray(0, bytesRead).toString("utf8").trim();
18723
+ } finally {
18724
+ await headFile.close();
18725
+ }
18726
+ return { branch: head.startsWith("ref: refs/heads/") ? head.slice("ref: refs/heads/".length) : null };
18727
+ } catch {
18728
+ return null;
18729
+ }
18333
18730
  }
18334
18731
  const requestRecordSchema = looseObject({});
18335
18732
  function boundedStringSchema(maxLength, options = {}) {
@@ -18673,6 +19070,7 @@ function registerServerCommands(registry2, services, host) {
18673
19070
  });
18674
19071
  handle(IPC.preferences, "preferences", async (partial2) => {
18675
19072
  const preferences = await services.settings.setPreferences(partial2 ?? {});
19073
+ if (partial2 && typeof partial2 === "object" && "aiExecutablePaths" in partial2) services.ai.invalidateStatuses();
18676
19074
  return preferences;
18677
19075
  });
18678
19076
  handle(IPC.filesTreeStateUpdate, "files-tree-state", (repositoryId, expandedPaths) => {
@@ -18692,7 +19090,8 @@ function registerServerCommands(registry2, services, host) {
18692
19090
  context
18693
19091
  ));
18694
19092
  handle(IPC.repositoryBrowseDirectories, "browse-directories", (path2) => browseServerDirectories(
18695
- path2 === void 0 ? void 0 : stringArg(path2, "browse-directories", 32768)
19093
+ path2 === void 0 ? void 0 : stringArg(path2, "browse-directories", 32768),
19094
+ services.git
18696
19095
  ));
18697
19096
  handleWithContext(IPC.repositoryOpenRecent, "open-recent", async (context, id) => {
18698
19097
  const repositoryId = stringArg(id, "open-recent", 64);
@@ -18730,7 +19129,7 @@ function registerServerCommands(registry2, services, host) {
18730
19129
  ));
18731
19130
  handle(IPC.repositoryDirectoryEntries, "directory-entries", (id, directoryPath) => services.files.listDirectory(
18732
19131
  stringArg(id, "directory-entries", 64),
18733
- stringArg(directoryPath, "directory-entries")
19132
+ directoryPath === "" ? "" : stringArg(directoryPath, "directory-entries")
18734
19133
  ));
18735
19134
  handle(IPC.repositoryReadFile, "read-file", (id, filePath, allowLarge) => services.files.read(stringArg(id, "read-file", 64), stringArg(filePath, "read-file"), allowLarge === true));
18736
19135
  handle(IPC.repositoryReadImage, "read-image", (id, filePath) => services.files.readImage(