@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.
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/dist/bin.mjs +2 -2
- package/dist/chunks/{cli-BgNif2D1.mjs → cli-B6JTojx_.mjs} +15 -2
- package/dist/client/assets/App-Dfm5F199.js +74 -0
- package/dist/client/assets/CreatePullRequestDialog-CAeykZ72.js +2 -0
- package/dist/client/assets/{DiffWorkspace-Ca2ZUlVn.js → DiffWorkspace-BRicWHvw.js} +13 -13
- package/dist/client/assets/{EditableFileViewer-85aEeMBK.js → EditableFileViewer-DWeSDnZn.js} +1 -1
- package/dist/client/assets/{File-DTtWrbYf.js → File-D65shwRR.js} +1 -1
- package/dist/client/assets/FilesView-EfoU7sF-.js +1 -0
- package/dist/client/assets/GitHubAvatar-C8R3iNoM.js +1 -0
- package/dist/client/assets/HistoryView-C6T2dsBC.js +1 -0
- package/dist/client/assets/{HtmlFileViewer-bL6T57PQ.js → HtmlFileViewer-JXLHTNPm.js} +1 -1
- package/dist/client/assets/{IconEdit-DePJMGIi.js → IconEdit-3fgBTCQ4.js} +1 -1
- package/dist/client/assets/IconFolderSymlink-C4ydD1eS.js +1 -0
- package/dist/client/assets/{ImageFileViewer-DJaHH3hJ.js → ImageFileViewer-YH2xhkkk.js} +1 -1
- package/dist/client/assets/LocalRefsDialog--1NiYKYj.js +1 -0
- package/dist/client/assets/{MarkdownFileViewer-BNPb9v8u.js → MarkdownFileViewer-BsB8WkIH.js} +2 -2
- package/dist/client/assets/PairingPage-DXRyAYHn.js +1 -0
- package/dist/client/assets/{PierreDiffViewer-8wTLYd5-.js → PierreDiffViewer-DoV3V0IW.js} +1 -1
- package/dist/client/assets/{PierreEditBoundary-KzU3HDc2.js → PierreEditBoundary-BnYZbHfi.js} +1 -1
- package/dist/client/assets/{PierreWorkerPool-DH2jp9oe.js → PierreWorkerPool-Cg9zdxiI.js} +1 -1
- package/dist/client/assets/PullRequestDiff-D6QG5Anx.js +1 -0
- package/dist/client/assets/PullRequestViewer-jC3LVmiO.js +2 -0
- package/dist/client/assets/PullRequestsView-GStKxG7k.js +1 -0
- package/dist/client/assets/QuickOpenDialog-HvslpGQl.js +1 -0
- package/dist/client/assets/RepositoryProjectsDialog-Biqv5GO_.js +1 -0
- package/dist/client/assets/SearchView-D5XB6wdu.js +1 -0
- package/dist/client/assets/SettingsDialog-DWAunOQB.js +72 -0
- package/dist/client/assets/Viewer-Gfy3Ejyo.js +3 -0
- package/dist/client/assets/{abnfDiagram-N423BO3Z-CkHOGdAl.js → abnfDiagram-N423BO3Z-DdAfHevw.js} +1 -1
- package/dist/client/assets/{arc-Dj8F8CO5.js → arc-C43uoeEi.js} +1 -1
- package/dist/client/assets/{architectureDiagram-T3A2C74G-g87Y5LXk.js → architectureDiagram-T3A2C74G-C7q0hhRO.js} +1 -1
- package/dist/client/assets/base-ui-CJ8ZIx3d.js +1 -0
- package/dist/client/assets/{blockDiagram-VBNYF7ZC-D6efB7jD.js → blockDiagram-VBNYF7ZC-C1x9Iap4.js} +1 -1
- package/dist/client/assets/{c4Diagram-5PPSVZJV-DoZEC2xg.js → c4Diagram-5PPSVZJV-BmKaVW6q.js} +1 -1
- package/dist/client/assets/channel-Diwpee6E.js +1 -0
- package/dist/client/assets/checkbox-XfIUFPxh.js +1 -0
- package/dist/client/assets/{chunk-2GRJ4B5K-Dr44hDNV.js → chunk-2GRJ4B5K-kzy_H3BQ.js} +1 -1
- package/dist/client/assets/{chunk-2Q5K7J3B-YIEyVOOL.js → chunk-2Q5K7J3B-BMj-FT4X.js} +1 -1
- package/dist/client/assets/{chunk-5RXB4S5H-gjmGBtkE.js → chunk-5RXB4S5H-DmPGiCCb.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-DCOQko4g.js → chunk-5VM5RSS4-DYFL8__L.js} +1 -1
- package/dist/client/assets/{chunk-6Q2QTUOP-DfGlvPVm.js → chunk-6Q2QTUOP-CI98oVdT.js} +1 -1
- package/dist/client/assets/{chunk-GF5L2VYU-CCWOLPVL.js → chunk-GF5L2VYU-D9JiiMlX.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-CFRTGh8r.js → chunk-JWPE2WC7-D0THYpZl.js} +1 -1
- package/dist/client/assets/{chunk-KBJHAD2P-By3wsJVD.js → chunk-KBJHAD2P-Z1tJfyAr.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-C0wmsHmy.js → chunk-RYQCIY6F-31aS4cD3.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-t3SnJvV_.js → chunk-XXDRQBXY-ClWDHmEp.js} +1 -1
- package/dist/client/assets/classDiagram-JCYQIIEL-BDuo9kiS.js +1 -0
- package/dist/client/assets/classDiagram-v2-OCEON4UE-BDuo9kiS.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-D3VltoVk.js → cose-bilkent-JH36ORCC-Btg2MYLh.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-DwLwbeoe.js → cynefin-VYW2F7L2-Cbh4Pk6j.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-MW4NZA55-B23kuaQF.js → cynefinDiagram-MW4NZA55-BBqorl0u.js} +1 -1
- package/dist/client/assets/{dagre-VZM6K2ZE-SgMrTr2p.js → dagre-VZM6K2ZE-DugEItZ0.js} +1 -1
- package/dist/client/assets/{diagram-7IWD3JNH-C7cQN4aD.js → diagram-7IWD3JNH-Biu7dmH8.js} +1 -1
- package/dist/client/assets/{diagram-B4RE2ZJO-CzeI3yOw.js → diagram-B4RE2ZJO-UDm4DXFS.js} +1 -1
- package/dist/client/assets/{diagram-LBJQPF4R-DhKm4hlI.js → diagram-LBJQPF4R-Bcku4gb-.js} +1 -1
- package/dist/client/assets/{diagram-Q27KOJAE-BXlU6Wp3.js → diagram-Q27KOJAE-DFRjglV-.js} +1 -1
- package/dist/client/assets/{diagram-UB23O5K3-BOwY_YwR.js → diagram-UB23O5K3-CgfMWtd0.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-BXEA7PRR-CuIQuYPa.js → ebnfDiagram-BXEA7PRR-CZzYOqs0.js} +1 -1
- package/dist/client/assets/{erDiagram-JOGREHBK-d45NhKfk.js → erDiagram-JOGREHBK-Cdlbxgs6.js} +1 -1
- package/dist/client/assets/{flowDiagram-UKHOOZJN-Dzq-6qo7.js → flowDiagram-UKHOOZJN-BgImDwpP.js} +1 -1
- package/dist/client/assets/{ganttDiagram-PKOTCBZU-RVlL9mX2.js → ganttDiagram-PKOTCBZU-SFhtiyL4.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-8kEJVUFH.js → gitGraphDiagram-DS77QQ5N-Bkm5ueys.js} +1 -1
- package/dist/client/assets/{index-CQcx9aRt.js → index-B5RC4THs.js} +4 -4
- package/dist/client/assets/index-Dc9xVluy.css +1 -0
- package/dist/client/assets/{infoDiagram-6WML65LV-Co21Ql9M.js → infoDiagram-6WML65LV-pCYc-VX2.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-BvPB_G97.js → ishikawaDiagram-WSZJBQD7-BtNbG3Y6.js} +1 -1
- package/dist/client/assets/{journeyDiagram-NVQOT4AX-k7x89IE9.js → journeyDiagram-NVQOT4AX-CPW2qT3y.js} +1 -1
- package/dist/client/assets/{kanban-definition-27J2QSJJ-BjrgN30t.js → kanban-definition-27J2QSJJ-wD6JXQFs.js} +1 -1
- package/dist/client/assets/{linear-m158kske.js → linear-sj8MOkmS.js} +1 -1
- package/dist/client/assets/{mermaid.core-C0FKucv-.js → mermaid.core-CGJtfmwb.js} +4 -4
- package/dist/client/assets/{mindmap-definition-FAOFIHXS-CF8noycK.js → mindmap-definition-FAOFIHXS-BQwLHBVs.js} +1 -1
- package/dist/client/assets/{pegDiagram-VL7TDLO6-_y7nfJFV.js → pegDiagram-VL7TDLO6-DtDsb-Z8.js} +1 -1
- package/dist/client/assets/{pieDiagram-7S7Q4E2Y-DhdWHi45.js → pieDiagram-7S7Q4E2Y-B3WhqNuS.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-BRvEIECQ.js → quadrantDiagram-CIZ2JOQS-CPi38pp_.js} +1 -1
- package/dist/client/assets/{railroadDiagram-AXF67PYL-BgLZ8XU1.js → railroadDiagram-AXF67PYL-Btac44Ux.js} +1 -1
- package/dist/client/assets/{render-markdown-2GjhuGA-.js → render-markdown-hAIWRfn0.js} +1 -1
- package/dist/client/assets/{requirementDiagram-LRYGKXZP-CX-gxyGF.js → requirementDiagram-LRYGKXZP-CHJgDNdg.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-W5VNT64P-CLSjSU_X.js → sankeyDiagram-W5VNT64P-OYJHfmtd.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-SI44F4Z6-dGlHyZRB.js → sequenceDiagram-SI44F4Z6-9ypa9W08.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-BH3qO9T3.js → sizeCapture-X5ZJPWSS-5ZDUTERI.js} +1 -1
- package/dist/client/assets/{stateDiagram-OKZ733FA-FXxo1iIM.js → stateDiagram-OKZ733FA-nGHE1hN7.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-UEYNNEHI-CkCpE1Yu.js +1 -0
- package/dist/client/assets/{swimlanes-SLNWSIFB-VJGIuM4S.js → swimlanes-SLNWSIFB-BZFv9Zp8.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-Dxlb7bju.js +8 -0
- package/dist/client/assets/tanstack-CcfAJoQ7.js +1 -0
- package/dist/client/assets/{timeline-definition-Z64GVDOM-BXNTAanY.js → timeline-definition-Z64GVDOM-pUIcaCIE.js} +1 -1
- package/dist/client/assets/{vennDiagram-T6HMQDX7-D9nXAts5.js → vennDiagram-T6HMQDX7-D6dEIJmP.js} +1 -1
- package/dist/client/assets/viewer-tabs-BjG3oKQ9.js +1 -0
- package/dist/client/assets/{wardleyDiagram-T6FBY63Y-D89EaEfa.js → wardleyDiagram-T6FBY63Y-DZ8ro_-y.js} +1 -1
- package/dist/client/assets/{xychartDiagram-ELKLHX3M-B_xFDiIh.js → xychartDiagram-ELKLHX3M-WJ2Zfmlk.js} +1 -1
- package/dist/client/index.html +4 -4
- package/dist/dev.mjs +2 -2
- package/dist/manifest.json +2 -2
- package/dist/server.mjs +2107 -565
- package/dist/utility.mjs +12 -6
- package/package.json +2 -2
- package/dist/client/assets/App-9EfSdZB8.js +0 -74
- package/dist/client/assets/CreatePullRequestDialog-BgVrWdkH.js +0 -2
- package/dist/client/assets/FilesView-rT5hzb7y.js +0 -1
- package/dist/client/assets/GitHubAvatar-EO1TD_1z.js +0 -1
- package/dist/client/assets/HistoryView-C4c0CqMC.js +0 -1
- package/dist/client/assets/IconFolderPlus-ByGyA0Di.js +0 -1
- package/dist/client/assets/LocalRefsDialog-D9wYZWBg.js +0 -1
- package/dist/client/assets/PairingPage-D9oguvKU.js +0 -1
- package/dist/client/assets/PullRequestDiff-DK-m07ZP.js +0 -1
- package/dist/client/assets/PullRequestViewer-CpLwq4o2.js +0 -2
- package/dist/client/assets/PullRequestsView-DnvXaJ4t.js +0 -1
- package/dist/client/assets/QuickOpenDialog-DIWjfUkt.js +0 -1
- package/dist/client/assets/RepositoryProjectsDialog-BsQn5Yjr.js +0 -1
- package/dist/client/assets/SearchView-D-91dtSv.js +0 -1
- package/dist/client/assets/SettingsDialog-dJUYk2cP.js +0 -9
- package/dist/client/assets/Viewer-GH-5bJW4.js +0 -3
- package/dist/client/assets/base-ui-DT3JsyND.js +0 -1
- package/dist/client/assets/channel-Dsy2VD23.js +0 -1
- package/dist/client/assets/checkbox-DYwmgDPS.js +0 -1
- package/dist/client/assets/classDiagram-JCYQIIEL-BBQJgoWd.js +0 -1
- package/dist/client/assets/classDiagram-v2-OCEON4UE-BBQJgoWd.js +0 -1
- package/dist/client/assets/gh-utils-LsOSydCg.js +0 -1
- package/dist/client/assets/index-DMbFAH51.css +0 -1
- package/dist/client/assets/stateDiagram-v2-UEYNNEHI-DqxQ14yP.js +0 -1
- package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-No0dCAkd.js +0 -8
- package/dist/client/assets/tanstack-CtfgE41X.js +0 -1
- 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,
|
|
4
|
-
import { access, mkdir, open, rename, chmod, rm, appendFile, readFile, mkdtemp, writeFile, lstat, readdir,
|
|
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
|
-
|
|
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
|
-
|
|
7012
|
-
|
|
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
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
return
|
|
7049
|
-
}
|
|
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
|
-
|
|
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(
|
|
7291
|
-
const currDef =
|
|
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(
|
|
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(
|
|
7559
|
+
return clone(schema2, def);
|
|
7314
7560
|
}
|
|
7315
|
-
function omit(
|
|
7316
|
-
const currDef =
|
|
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(
|
|
7568
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7323
7569
|
get shape() {
|
|
7324
|
-
const newShape = { ...
|
|
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(
|
|
7584
|
+
return clone(schema2, def);
|
|
7339
7585
|
}
|
|
7340
|
-
function extend(
|
|
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 =
|
|
7590
|
+
const checks = schema2._zod.def.checks;
|
|
7345
7591
|
const hasChecks = checks && checks.length > 0;
|
|
7346
7592
|
if (hasChecks) {
|
|
7347
|
-
const existingShape =
|
|
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(
|
|
7600
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7355
7601
|
get shape() {
|
|
7356
|
-
const _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(
|
|
7607
|
+
return clone(schema2, def);
|
|
7362
7608
|
}
|
|
7363
|
-
function safeExtend(
|
|
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(
|
|
7613
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7368
7614
|
get shape() {
|
|
7369
|
-
const _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(
|
|
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,
|
|
7394
|
-
const currDef =
|
|
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(
|
|
7646
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7401
7647
|
get shape() {
|
|
7402
|
-
const oldShape =
|
|
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(
|
|
7675
|
+
return clone(schema2, def);
|
|
7430
7676
|
}
|
|
7431
|
-
function required(Class,
|
|
7432
|
-
const def = mergeDefs(
|
|
7677
|
+
function required(Class, schema2, mask) {
|
|
7678
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7433
7679
|
get shape() {
|
|
7434
|
-
const oldShape =
|
|
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(
|
|
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) => (
|
|
7835
|
+
const _parse = (_Err) => (schema2, value, _ctx, _params) => {
|
|
7590
7836
|
const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
|
|
7591
|
-
const result =
|
|
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 (
|
|
7848
|
+
const _parseAsync = (_Err) => async (schema2, value, _ctx, params) => {
|
|
7603
7849
|
const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
|
|
7604
|
-
let result =
|
|
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) => (
|
|
7860
|
+
const _safeParse = (_Err) => (schema2, value, _ctx) => {
|
|
7615
7861
|
const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
|
|
7616
|
-
const result =
|
|
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 (
|
|
7872
|
+
const _safeParseAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7627
7873
|
const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
|
|
7628
|
-
let result =
|
|
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) => (
|
|
7883
|
+
const _encode = (_Err) => (schema2, value, _ctx) => {
|
|
7638
7884
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7639
|
-
return _parse(_Err)(
|
|
7885
|
+
return _parse(_Err)(schema2, value, ctx);
|
|
7640
7886
|
};
|
|
7641
|
-
const _decode = (_Err) => (
|
|
7642
|
-
return _parse(_Err)(
|
|
7887
|
+
const _decode = (_Err) => (schema2, value, _ctx) => {
|
|
7888
|
+
return _parse(_Err)(schema2, value, _ctx);
|
|
7643
7889
|
};
|
|
7644
|
-
const _encodeAsync = (_Err) => async (
|
|
7890
|
+
const _encodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7645
7891
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7646
|
-
return _parseAsync(_Err)(
|
|
7892
|
+
return _parseAsync(_Err)(schema2, value, ctx);
|
|
7647
7893
|
};
|
|
7648
|
-
const _decodeAsync = (_Err) => async (
|
|
7649
|
-
return _parseAsync(_Err)(
|
|
7894
|
+
const _decodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7895
|
+
return _parseAsync(_Err)(schema2, value, _ctx);
|
|
7650
7896
|
};
|
|
7651
|
-
const _safeEncode = (_Err) => (
|
|
7897
|
+
const _safeEncode = (_Err) => (schema2, value, _ctx) => {
|
|
7652
7898
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7653
|
-
return _safeParse(_Err)(
|
|
7899
|
+
return _safeParse(_Err)(schema2, value, ctx);
|
|
7654
7900
|
};
|
|
7655
|
-
const _safeDecode = (_Err) => (
|
|
7656
|
-
return _safeParse(_Err)(
|
|
7901
|
+
const _safeDecode = (_Err) => (schema2, value, _ctx) => {
|
|
7902
|
+
return _safeParse(_Err)(schema2, value, _ctx);
|
|
7657
7903
|
};
|
|
7658
|
-
const _safeEncodeAsync = (_Err) => async (
|
|
7904
|
+
const _safeEncodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7659
7905
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7660
|
-
return _safeParseAsync(_Err)(
|
|
7906
|
+
return _safeParseAsync(_Err)(schema2, value, ctx);
|
|
7661
7907
|
};
|
|
7662
|
-
const _safeDecodeAsync = (_Err) => async (
|
|
7663
|
-
return _safeParseAsync(_Err)(
|
|
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
|
|
8834
|
-
const isOptionalIn =
|
|
8835
|
-
const isOptionalOut =
|
|
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(
|
|
9857
|
+
add(schema2, ..._meta) {
|
|
9523
9858
|
const meta = _meta[0];
|
|
9524
|
-
this._map.set(
|
|
9859
|
+
this._map.set(schema2, meta);
|
|
9525
9860
|
if (meta && typeof meta === "object" && "id" in meta) {
|
|
9526
|
-
this._idmap.set(meta.id,
|
|
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(
|
|
9536
|
-
const meta = this._map.get(
|
|
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(
|
|
9875
|
+
this._map.delete(schema2);
|
|
9541
9876
|
return this;
|
|
9542
9877
|
}
|
|
9543
|
-
get(
|
|
9544
|
-
const p =
|
|
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(
|
|
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(
|
|
9886
|
+
return this._map.get(schema2);
|
|
9552
9887
|
}
|
|
9553
|
-
has(
|
|
9554
|
-
return this._map.has(
|
|
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
|
|
10366
|
+
const schema2 = new Class({
|
|
10032
10367
|
type: "custom",
|
|
10033
10368
|
check: "custom",
|
|
10034
10369
|
fn,
|
|
10035
10370
|
...normalizeParams(_params)
|
|
10036
10371
|
});
|
|
10037
|
-
return
|
|
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(
|
|
10425
|
+
function process$1(schema2, ctx, _params = { path: [], schemaPath: [] }) {
|
|
10091
10426
|
var _a2;
|
|
10092
|
-
const def =
|
|
10093
|
-
const seen = ctx.seen.get(
|
|
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(
|
|
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(
|
|
10104
|
-
const overrideSchema =
|
|
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,
|
|
10445
|
+
schemaPath: [..._params.schemaPath, schema2],
|
|
10111
10446
|
path: _params.path
|
|
10112
10447
|
};
|
|
10113
|
-
if (
|
|
10114
|
-
|
|
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(
|
|
10456
|
+
processor(schema2, ctx, _json, params);
|
|
10122
10457
|
}
|
|
10123
|
-
const 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(
|
|
10466
|
+
const meta = ctx.metadataRegistry.get(schema2);
|
|
10132
10467
|
if (meta)
|
|
10133
10468
|
Object.assign(result.schema, meta);
|
|
10134
|
-
if (ctx.io === "input" && isTransforming(
|
|
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(
|
|
10476
|
+
const _result = ctx.seen.get(schema2);
|
|
10142
10477
|
return _result.schema;
|
|
10143
10478
|
}
|
|
10144
|
-
function extractDefs(ctx,
|
|
10145
|
-
const root = ctx.seen.get(
|
|
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
|
|
10189
|
-
for (const key in
|
|
10190
|
-
delete
|
|
10523
|
+
const schema3 = seen.schema;
|
|
10524
|
+
for (const key in schema3) {
|
|
10525
|
+
delete schema3[key];
|
|
10191
10526
|
}
|
|
10192
|
-
|
|
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 (
|
|
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 (
|
|
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,
|
|
10235
|
-
const root = ctx.seen.get(
|
|
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
|
|
10243
|
-
const _cached = { ...
|
|
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
|
-
|
|
10252
|
-
|
|
10586
|
+
schema3.allOf = schema3.allOf ?? [];
|
|
10587
|
+
schema3.allOf.push(refSchema);
|
|
10253
10588
|
} else {
|
|
10254
|
-
Object.assign(
|
|
10589
|
+
Object.assign(schema3, refSchema);
|
|
10255
10590
|
}
|
|
10256
|
-
Object.assign(
|
|
10591
|
+
Object.assign(schema3, _cached);
|
|
10257
10592
|
const isParentRef = zodSchema._zod.parent === ref;
|
|
10258
10593
|
if (isParentRef) {
|
|
10259
|
-
for (const key in
|
|
10594
|
+
for (const key in schema3) {
|
|
10260
10595
|
if (key === "$ref" || key === "allOf")
|
|
10261
10596
|
continue;
|
|
10262
10597
|
if (!(key in _cached)) {
|
|
10263
|
-
delete
|
|
10598
|
+
delete schema3[key];
|
|
10264
10599
|
}
|
|
10265
10600
|
}
|
|
10266
10601
|
}
|
|
10267
10602
|
if (refSchema.$ref && refSeen.def) {
|
|
10268
|
-
for (const key in
|
|
10603
|
+
for (const key in schema3) {
|
|
10269
10604
|
if (key === "$ref" || key === "allOf")
|
|
10270
10605
|
continue;
|
|
10271
|
-
if (key in refSeen.def && JSON.stringify(
|
|
10272
|
-
delete
|
|
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
|
-
|
|
10617
|
+
schema3.$ref = parentSeen.schema.$ref;
|
|
10283
10618
|
if (parentSeen.def) {
|
|
10284
|
-
for (const key in
|
|
10619
|
+
for (const key in schema3) {
|
|
10285
10620
|
if (key === "$ref" || key === "allOf")
|
|
10286
10621
|
continue;
|
|
10287
|
-
if (key in parentSeen.def && JSON.stringify(
|
|
10288
|
-
delete
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
-
...
|
|
10680
|
+
...schema2["~standard"],
|
|
10346
10681
|
jsonSchema: {
|
|
10347
|
-
input: createStandardJSONSchemaMethod(
|
|
10348
|
-
output: createStandardJSONSchemaMethod(
|
|
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 = (
|
|
10747
|
+
const createToJSONSchemaMethod = (schema2, processors = {}) => (params) => {
|
|
10413
10748
|
const ctx = initializeContext({ ...params, processors });
|
|
10414
|
-
process$1(
|
|
10415
|
-
extractDefs(ctx,
|
|
10416
|
-
return finalize(ctx,
|
|
10749
|
+
process$1(schema2, ctx);
|
|
10750
|
+
extractDefs(ctx, schema2);
|
|
10751
|
+
return finalize(ctx, schema2);
|
|
10417
10752
|
};
|
|
10418
|
-
const createStandardJSONSchemaMethod = (
|
|
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(
|
|
10422
|
-
extractDefs(ctx,
|
|
10423
|
-
return finalize(ctx,
|
|
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 = (
|
|
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 } =
|
|
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 = (
|
|
10800
|
+
const numberProcessor = (schema2, ctx, _json, _params) => {
|
|
10466
10801
|
const json2 = _json;
|
|
10467
|
-
const { minimum, maximum, format: format2, multipleOf, exclusiveMaximum, exclusiveMinimum } =
|
|
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 = (
|
|
10543
|
-
const 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 = (
|
|
10552
|
-
const 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 = (
|
|
10930
|
+
const templateLiteralProcessor = (schema2, _ctx, json2, _params) => {
|
|
10596
10931
|
const _json = json2;
|
|
10597
|
-
const 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 = (
|
|
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 } =
|
|
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 = (
|
|
10990
|
+
const arrayProcessor = (schema2, ctx, _json, params) => {
|
|
10656
10991
|
const json2 = _json;
|
|
10657
|
-
const def =
|
|
10658
|
-
const { minimum, maximum } =
|
|
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 = (
|
|
11004
|
+
const objectProcessor = (schema2, ctx, _json, params) => {
|
|
10670
11005
|
const json2 = _json;
|
|
10671
|
-
const 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 = (
|
|
10706
|
-
const 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 = (
|
|
10719
|
-
const 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 = (
|
|
11070
|
+
const tupleProcessor = (schema2, ctx, _json, params) => {
|
|
10736
11071
|
const json2 = _json;
|
|
10737
|
-
const 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 } =
|
|
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 = (
|
|
11112
|
+
const recordProcessor = (schema2, ctx, _json, params) => {
|
|
10778
11113
|
const json2 = _json;
|
|
10779
|
-
const 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 = (
|
|
10814
|
-
const 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(
|
|
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 = (
|
|
10825
|
-
const 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(
|
|
11162
|
+
const seen = ctx.seen.get(schema2);
|
|
10828
11163
|
seen.ref = def.innerType;
|
|
10829
11164
|
};
|
|
10830
|
-
const defaultProcessor = (
|
|
10831
|
-
const 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(
|
|
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 = (
|
|
10838
|
-
const 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(
|
|
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 = (
|
|
10846
|
-
const 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(
|
|
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 = (
|
|
10859
|
-
const 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(
|
|
11198
|
+
const seen = ctx.seen.get(schema2);
|
|
10864
11199
|
seen.ref = innerType;
|
|
10865
11200
|
};
|
|
10866
|
-
const readonlyProcessor = (
|
|
10867
|
-
const 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(
|
|
11204
|
+
const seen = ctx.seen.get(schema2);
|
|
10870
11205
|
seen.ref = def.innerType;
|
|
10871
11206
|
json2.readOnly = true;
|
|
10872
11207
|
};
|
|
10873
|
-
const promiseProcessor = (
|
|
10874
|
-
const 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(
|
|
11211
|
+
const seen = ctx.seen.get(schema2);
|
|
10877
11212
|
seen.ref = def.innerType;
|
|
10878
11213
|
};
|
|
10879
|
-
const optionalProcessor = (
|
|
10880
|
-
const 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(
|
|
11217
|
+
const seen = ctx.seen.get(schema2);
|
|
10883
11218
|
seen.ref = def.innerType;
|
|
10884
11219
|
};
|
|
10885
|
-
const lazyProcessor = (
|
|
10886
|
-
const 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(
|
|
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 [_,
|
|
10939
|
-
process$1(
|
|
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,
|
|
10950
|
-
extractDefs(ctx2,
|
|
10951
|
-
schemas[key] = finalize(ctx2,
|
|
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
|
-
|
|
12232
|
-
|
|
12233
|
-
this.
|
|
12234
|
-
|
|
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(
|
|
12578
|
-
return strictify(structuredClone(
|
|
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
|
|
12678
|
-
|
|
12679
|
-
|
|
12680
|
-
const
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
12728
|
-
|
|
12729
|
-
|
|
12730
|
-
const
|
|
12731
|
-
|
|
12732
|
-
|
|
12733
|
-
|
|
12734
|
-
|
|
12735
|
-
|
|
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:
|
|
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.
|
|
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
|
|
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
|
|
12839
|
-
const
|
|
12840
|
-
|
|
12841
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
13225
|
-
|
|
13226
|
-
const
|
|
13227
|
-
|
|
13228
|
-
const
|
|
13229
|
-
|
|
13230
|
-
this.runner
|
|
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
|
-
|
|
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
|
-
|
|
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",
|
|
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
|
-
|
|
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
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
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
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
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
|
|
14855
|
-
|
|
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
|
|
14858
|
-
|
|
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
|
|
14861
|
-
this.assertKnownOid(repositoryId, oid);
|
|
15366
|
+
async read(repositoryId) {
|
|
14862
15367
|
const repository = this.repositories.get(repositoryId);
|
|
14863
|
-
const
|
|
14864
|
-
const [
|
|
14865
|
-
this.
|
|
14866
|
-
this.
|
|
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
|
-
|
|
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
|
|
14871
|
-
|
|
14872
|
-
|
|
14873
|
-
|
|
14874
|
-
|
|
14875
|
-
|
|
14876
|
-
|
|
14877
|
-
|
|
14878
|
-
|
|
14879
|
-
|
|
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
|
-
|
|
14882
|
-
|
|
14883
|
-
|
|
14884
|
-
|
|
14885
|
-
|
|
14886
|
-
|
|
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
|
-
|
|
14889
|
-
|
|
14890
|
-
|
|
14891
|
-
|
|
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
|
|
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
|
|
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
|
|
16230
|
+
const managed = await this.remoteBranches.snapshotBranches(repositoryId, branches);
|
|
15430
16231
|
return {
|
|
15431
|
-
branches:
|
|
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
|
|
16564
|
-
const name = typeof authorRecord?.name === "string" && authorRecord.name ? authorRecord.name :
|
|
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:
|
|
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(
|
|
16590
|
-
if (!
|
|
16591
|
-
if (
|
|
16592
|
-
const botName =
|
|
16593
|
-
const path2 = isBot ||
|
|
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
|
-
|
|
17819
|
+
accounts;
|
|
16627
17820
|
async status(forceRefresh = false) {
|
|
16628
|
-
if (
|
|
16629
|
-
const
|
|
16630
|
-
|
|
16631
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
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
|
|
16792
|
-
if (
|
|
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
|
-
|
|
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
|
|
18319
|
-
const
|
|
18320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18325
|
-
|
|
18326
|
-
|
|
18327
|
-
|
|
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
|
|
18337
|
-
if (options.empty !== true)
|
|
18338
|
-
if (options.controls === false) return
|
|
18339
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
19400
|
-
|
|
19401
|
-
|
|
19402
|
-
|
|
19403
|
-
|
|
19404
|
-
|
|
19405
|
-
|
|
19406
|
-
|
|
19407
|
-
|
|
19408
|
-
|
|
19409
|
-
|
|
19410
|
-
|
|
19411
|
-
|
|
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
|
-
|
|
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))
|
|
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) {
|