@opentig/cli 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.mjs +2 -1
- package/dist/chunks/{cli-Bi6ET5UT.mjs → cli-DNzGBqmL.mjs} +1 -1
- package/dist/client/assets/App-BbT5H97Z.js +138 -0
- package/dist/client/assets/CreatePullRequestDialog-DRLeMY4e.js +2 -0
- package/dist/client/assets/{DiffWorkspace-RDG_Y70N.js → DiffWorkspace-DJX00otP.js} +13 -13
- package/dist/client/assets/{EditableFileViewer-CsbifqBO.js → EditableFileViewer-Cmbomx5y.js} +1 -1
- package/dist/client/assets/{File-CHjvYpb2.js → File-4o4egqlz.js} +1 -1
- package/dist/client/assets/FilesView-TmUF-VLr.js +1 -0
- package/dist/client/assets/GitHubAvatar-azKPrTMv.js +1 -0
- package/dist/client/assets/HistoryView-Go6rXO0P.js +1 -0
- package/dist/client/assets/{HtmlFileViewer-h8ULU8Gy.js → HtmlFileViewer-WpYnLdMW.js} +1 -1
- package/dist/client/assets/{IconEdit-Cg8S2wqW.js → IconEdit-B8LZHB8l.js} +1 -1
- package/dist/client/assets/{IconFolderSymlink-BZSLX1ez.js → IconFolderSymlink-D29EhQPC.js} +1 -1
- package/dist/client/assets/{ImageFileViewer-Cjw58ckv.js → ImageFileViewer-1ObUacJu.js} +1 -1
- package/dist/client/assets/LocalRefsDialog-VcrlFgxR.js +1 -0
- package/dist/client/assets/{MarkdownFileViewer-kLiduag6.js → MarkdownFileViewer-CvsKPnvF.js} +2 -2
- package/dist/client/assets/PairingPage-cSoKZi8t.js +1 -0
- package/dist/client/assets/{PierreDiffViewer-eAcugmoF.js → PierreDiffViewer-DVTrOwX4.js} +1 -1
- package/dist/client/assets/{PierreEditBoundary-DlGd1pqO.js → PierreEditBoundary-DPtaTiUB.js} +1 -1
- package/dist/client/assets/{PierreWorkerPool-Col4k7qz.js → PierreWorkerPool-BxP-s6jD.js} +1 -1
- package/dist/client/assets/{PullRequestDiff-DhKTXyIF.js → PullRequestDiff-DeQznd9n.js} +1 -1
- package/dist/client/assets/PullRequestViewer-IhGZZs_A.js +2 -0
- package/dist/client/assets/PullRequestsView-BG0xrok4.js +1 -0
- package/dist/client/assets/QuickOpenDialog-DpmcAzTx.js +1 -0
- package/dist/client/assets/RepositoryProjectsDialog-DAzbUB3X.js +1 -0
- package/dist/client/assets/SearchView-HoiWKG8z.js +1 -0
- package/dist/client/assets/SettingsDialog-pmJKOaiK.js +9 -0
- package/dist/client/assets/{Viewer-o1hDU33O.js → Viewer-BCk8FHrc.js} +3 -3
- package/dist/client/assets/{abnfDiagram-N423BO3Z-BvrnK_R-.js → abnfDiagram-N423BO3Z-BvfKOanh.js} +1 -1
- package/dist/client/assets/{arc-DLa3_7ua.js → arc-DB2nIH2Y.js} +1 -1
- package/dist/client/assets/{architectureDiagram-T3A2C74G-BexwhnyX.js → architectureDiagram-T3A2C74G-Bqh4uY3R.js} +1 -1
- package/dist/client/assets/base-ui-DIP2rZxz.js +1 -0
- package/dist/client/assets/{blockDiagram-VBNYF7ZC-CoOttUUo.js → blockDiagram-VBNYF7ZC-DMW415Ms.js} +1 -1
- package/dist/client/assets/{c4Diagram-5PPSVZJV-Bgp6ppUh.js → c4Diagram-5PPSVZJV-BSNPes7l.js} +1 -1
- package/dist/client/assets/channel-VcDernYy.js +1 -0
- package/dist/client/assets/checkbox-Dta4gqy2.js +1 -0
- package/dist/client/assets/{chunk-2GRJ4B5K-C_kzp38w.js → chunk-2GRJ4B5K-D-86JbWu.js} +1 -1
- package/dist/client/assets/{chunk-2Q5K7J3B-CysYnRkJ.js → chunk-2Q5K7J3B-BNI-SD-k.js} +1 -1
- package/dist/client/assets/{chunk-5RXB4S5H-oMS6x_ag.js → chunk-5RXB4S5H-OFFYyML8.js} +1 -1
- package/dist/client/assets/{chunk-5VM5RSS4-C5-dW5wf.js → chunk-5VM5RSS4-DpE1dtnL.js} +1 -1
- package/dist/client/assets/{chunk-6Q2QTUOP-DxahInnA.js → chunk-6Q2QTUOP-DxAnzBXJ.js} +1 -1
- package/dist/client/assets/{chunk-GF5L2VYU-D7V-3eRg.js → chunk-GF5L2VYU-DNrTNRgr.js} +1 -1
- package/dist/client/assets/{chunk-JWPE2WC7-BrLmjbVP.js → chunk-JWPE2WC7-D-jfKyIh.js} +1 -1
- package/dist/client/assets/{chunk-KBJHAD2P-CyOvijwM.js → chunk-KBJHAD2P-CIyQl_ts.js} +1 -1
- package/dist/client/assets/{chunk-RYQCIY6F-BvEjVjQs.js → chunk-RYQCIY6F-CgCYQccM.js} +1 -1
- package/dist/client/assets/{chunk-XXDRQBXY-C_IY9A64.js → chunk-XXDRQBXY-B31oXtqU.js} +1 -1
- package/dist/client/assets/classDiagram-JCYQIIEL-CUuEGRCt.js +1 -0
- package/dist/client/assets/classDiagram-v2-OCEON4UE-CUuEGRCt.js +1 -0
- package/dist/client/assets/{cose-bilkent-JH36ORCC-Cu7qZi7h.js → cose-bilkent-JH36ORCC-CrX-LDNw.js} +1 -1
- package/dist/client/assets/{cynefin-VYW2F7L2-B4M1t9lL.js → cynefin-VYW2F7L2-DnkWlsqW.js} +1 -1
- package/dist/client/assets/{cynefinDiagram-MW4NZA55-CAosOLmT.js → cynefinDiagram-MW4NZA55-bUDQ9ElF.js} +1 -1
- package/dist/client/assets/{dagre-VZM6K2ZE-q354V538.js → dagre-VZM6K2ZE-BW5Kni4n.js} +1 -1
- package/dist/client/assets/{diagram-7IWD3JNH-C_CzkBPt.js → diagram-7IWD3JNH-1EXGwguC.js} +1 -1
- package/dist/client/assets/{diagram-B4RE2ZJO-VvvYHE9c.js → diagram-B4RE2ZJO-CzpH5ieN.js} +1 -1
- package/dist/client/assets/{diagram-LBJQPF4R-D8vRQwb5.js → diagram-LBJQPF4R-BnKszJ_B.js} +1 -1
- package/dist/client/assets/{diagram-Q27KOJAE-Bul4aDKg.js → diagram-Q27KOJAE-DbQlD3OM.js} +1 -1
- package/dist/client/assets/{diagram-UB23O5K3-DNadEE58.js → diagram-UB23O5K3-BqZ-RSlm.js} +1 -1
- package/dist/client/assets/{ebnfDiagram-BXEA7PRR-Bf-68MMZ.js → ebnfDiagram-BXEA7PRR-B2iPWDvr.js} +1 -1
- package/dist/client/assets/{erDiagram-JOGREHBK-CJ-jHFsf.js → erDiagram-JOGREHBK-aMqHrKCk.js} +1 -1
- package/dist/client/assets/{flowDiagram-UKHOOZJN-DMOdinBU.js → flowDiagram-UKHOOZJN-ivrTsn4D.js} +1 -1
- package/dist/client/assets/{ganttDiagram-PKOTCBZU-DzezS3Zy.js → ganttDiagram-PKOTCBZU-IMfIqkXw.js} +1 -1
- package/dist/client/assets/{gitGraphDiagram-DS77QQ5N-Dftpvns8.js → gitGraphDiagram-DS77QQ5N-BNURRh2N.js} +1 -1
- package/dist/client/assets/{index-DAUK3rnz.js → index-B3QJsvjR.js} +4 -4
- package/dist/client/assets/index-Cx9LaJW8.css +1 -0
- package/dist/client/assets/{infoDiagram-6WML65LV-DmgUBq17.js → infoDiagram-6WML65LV-BKEENkUQ.js} +1 -1
- package/dist/client/assets/{ishikawaDiagram-WSZJBQD7-CL0PQhpQ.js → ishikawaDiagram-WSZJBQD7-HrCSmiTr.js} +1 -1
- package/dist/client/assets/{journeyDiagram-NVQOT4AX-Ck4pQx_A.js → journeyDiagram-NVQOT4AX-CQIuWA6F.js} +1 -1
- package/dist/client/assets/{kanban-definition-27J2QSJJ-DuRZxBql.js → kanban-definition-27J2QSJJ-h4fSAkel.js} +1 -1
- package/dist/client/assets/{linear-C2JDCaz4.js → linear-BAdz-uC9.js} +1 -1
- package/dist/client/assets/{mermaid.core-9dt2ugQA.js → mermaid.core-vLn7mMZB.js} +4 -4
- package/dist/client/assets/{mindmap-definition-FAOFIHXS-vy0fdDF0.js → mindmap-definition-FAOFIHXS-DOrZl827.js} +1 -1
- package/dist/client/assets/{pegDiagram-VL7TDLO6-DjVJy-Gm.js → pegDiagram-VL7TDLO6-_Vs7p633.js} +1 -1
- package/dist/client/assets/{pieDiagram-7S7Q4E2Y-ATbd_8vw.js → pieDiagram-7S7Q4E2Y-7qbB_Z16.js} +1 -1
- package/dist/client/assets/{quadrantDiagram-CIZ2JOQS-C8o6ofcl.js → quadrantDiagram-CIZ2JOQS-DNH8SosA.js} +1 -1
- package/dist/client/assets/{railroadDiagram-AXF67PYL-Qvij9Ubo.js → railroadDiagram-AXF67PYL-Ctazrrpa.js} +1 -1
- package/dist/client/assets/{render-markdown-BtCybfvQ.js → render-markdown-dvHhHbC0.js} +6 -6
- package/dist/client/assets/{requirementDiagram-LRYGKXZP-yLfrba2h.js → requirementDiagram-LRYGKXZP-BAH8dYer.js} +1 -1
- package/dist/client/assets/{sankeyDiagram-W5VNT64P-BTXRtTyG.js → sankeyDiagram-W5VNT64P-CuffrpdF.js} +1 -1
- package/dist/client/assets/{sequenceDiagram-SI44F4Z6-BN1CCZIX.js → sequenceDiagram-SI44F4Z6-CaO4fJnS.js} +1 -1
- package/dist/client/assets/{sizeCapture-X5ZJPWSS-BfXjBBYo.js → sizeCapture-X5ZJPWSS-B_LramBU.js} +1 -1
- package/dist/client/assets/{stateDiagram-OKZ733FA-DMAWiBsP.js → stateDiagram-OKZ733FA-bc7zBYHn.js} +1 -1
- package/dist/client/assets/stateDiagram-v2-UEYNNEHI-C-UGmA2d.js +1 -0
- package/dist/client/assets/{swimlanes-SLNWSIFB-BD20m5cw.js → swimlanes-SLNWSIFB-DQzg1IPn.js} +2 -2
- package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-DnopIiWa.js +8 -0
- package/dist/client/assets/tanstack-BysN2Oew.js +1 -0
- package/dist/client/assets/{timeline-definition-Z64GVDOM-gtc0-AHB.js → timeline-definition-Z64GVDOM-CtbuIB6K.js} +1 -1
- package/dist/client/assets/{vennDiagram-T6HMQDX7-Cdr4AXTp.js → vennDiagram-T6HMQDX7-pYJ0ZfFS.js} +1 -1
- package/dist/client/assets/{viewer-tabs-D-T0xQaq.js → viewer-tabs-Cnfyd6rg.js} +1 -1
- package/dist/client/assets/{wardleyDiagram-T6FBY63Y-BcU4vxO8.js → wardleyDiagram-T6FBY63Y-DYl_Oi-Y.js} +1 -1
- package/dist/client/assets/{xychartDiagram-ELKLHX3M-B3Zuadj8.js → xychartDiagram-ELKLHX3M-Cj5YUfP5.js} +1 -1
- package/dist/client/index.html +4 -4
- package/dist/dev.mjs +3 -2
- package/dist/manifest.json +2 -2
- package/dist/server.mjs +2219 -508
- package/dist/utility.mjs +12 -5
- package/package.json +1 -1
- package/dist/client/assets/App-BXZLGT86.js +0 -74
- package/dist/client/assets/CreatePullRequestDialog-DFonMYEx.js +0 -2
- package/dist/client/assets/FilesView-Dl8QFbFA.js +0 -1
- package/dist/client/assets/GitHubAvatar-CnBaloV0.js +0 -1
- package/dist/client/assets/HistoryView-BgCZU2JO.js +0 -1
- package/dist/client/assets/LocalRefsDialog-DNgrd7Qp.js +0 -1
- package/dist/client/assets/PairingPage-C97sv-8s.js +0 -1
- package/dist/client/assets/PullRequestViewer-BgI8HFMe.js +0 -2
- package/dist/client/assets/PullRequestsView-PYWLy8We.js +0 -1
- package/dist/client/assets/QuickOpenDialog-BFDDZVCL.js +0 -1
- package/dist/client/assets/RepositoryProjectsDialog-CvB8KcFp.js +0 -1
- package/dist/client/assets/SearchView-BwF_MsxK.js +0 -1
- package/dist/client/assets/SettingsDialog-DIcLmXSE.js +0 -9
- package/dist/client/assets/base-ui-CJ8ZIx3d.js +0 -1
- package/dist/client/assets/channel-1k1AYpyV.js +0 -1
- package/dist/client/assets/checkbox-BRH0ZNIC.js +0 -1
- package/dist/client/assets/classDiagram-JCYQIIEL-ChyNFpay.js +0 -1
- package/dist/client/assets/classDiagram-v2-OCEON4UE-ChyNFpay.js +0 -1
- package/dist/client/assets/index-BfA4tAc1.css +0 -1
- package/dist/client/assets/stateDiagram-v2-UEYNNEHI-D_ofZeaa.js +0 -1
- package/dist/client/assets/swimlanesDiagram-ULZ7WXOC-CF9m5JSB.js +0 -8
- package/dist/client/assets/tanstack-B0Sf7wNU.js +0 -1
package/dist/server.mjs
CHANGED
|
@@ -17,8 +17,12 @@ import { Transform, getDefaultHighWaterMark, Duplex, Writable, Readable, PassThr
|
|
|
17
17
|
import { finished } from "node:stream/promises";
|
|
18
18
|
import path$1 from "node:path/win32";
|
|
19
19
|
import { createServer, isIP } from "node:net";
|
|
20
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
20
21
|
import { createServer as createServer$1 } from "node:http";
|
|
21
22
|
import WebSocket, { WebSocketServer } from "ws";
|
|
23
|
+
function aiExecutablePathsKey(paths) {
|
|
24
|
+
return JSON.stringify(["codex", "claude", "opencode", "grok"].map((id) => paths[id] ?? ""));
|
|
25
|
+
}
|
|
22
26
|
function isPlainObject$1(value) {
|
|
23
27
|
if (typeof value !== "object" || value === null) {
|
|
24
28
|
return false;
|
|
@@ -5709,7 +5713,7 @@ const createReadable = ({ subprocess, concurrentStreams, encoding }, { from, bin
|
|
|
5709
5713
|
const binary = binaryOption || BINARY_ENCODINGS.has(encoding);
|
|
5710
5714
|
const { subprocessStdout, waitReadableDestroy } = getSubprocessStdout(subprocess, from, concurrentStreams);
|
|
5711
5715
|
const { readableEncoding, readableObjectMode, readableHighWaterMark } = getReadableOptions(subprocessStdout, binary);
|
|
5712
|
-
const { read, onStdoutDataDone } = getReadableMethods({
|
|
5716
|
+
const { read: read2, onStdoutDataDone } = getReadableMethods({
|
|
5713
5717
|
subprocessStdout,
|
|
5714
5718
|
subprocess,
|
|
5715
5719
|
binary,
|
|
@@ -5717,7 +5721,7 @@ const createReadable = ({ subprocess, concurrentStreams, encoding }, { from, bin
|
|
|
5717
5721
|
preserveNewlines
|
|
5718
5722
|
});
|
|
5719
5723
|
const readable2 = new Readable({
|
|
5720
|
-
read,
|
|
5724
|
+
read: read2,
|
|
5721
5725
|
destroy: callbackify(onReadableDestroy.bind(void 0, { subprocessStdout, subprocess, waitReadableDestroy })),
|
|
5722
5726
|
highWaterMark: readableHighWaterMark,
|
|
5723
5727
|
objectMode: readableObjectMode,
|
|
@@ -6555,7 +6559,7 @@ const createDuplex = ({ subprocess, concurrentStreams, encoding }, { from, to, b
|
|
|
6555
6559
|
const { subprocessStdout, waitReadableDestroy } = getSubprocessStdout(subprocess, from, concurrentStreams);
|
|
6556
6560
|
const { subprocessStdin, waitWritableFinal, waitWritableDestroy } = getSubprocessStdin(subprocess, to, concurrentStreams);
|
|
6557
6561
|
const { readableEncoding, readableObjectMode, readableHighWaterMark } = getReadableOptions(subprocessStdout, binary);
|
|
6558
|
-
const { read, onStdoutDataDone } = getReadableMethods({
|
|
6562
|
+
const { read: read2, onStdoutDataDone } = getReadableMethods({
|
|
6559
6563
|
subprocessStdout,
|
|
6560
6564
|
subprocess,
|
|
6561
6565
|
binary,
|
|
@@ -6563,7 +6567,7 @@ const createDuplex = ({ subprocess, concurrentStreams, encoding }, { from, to, b
|
|
|
6563
6567
|
preserveNewlines
|
|
6564
6568
|
});
|
|
6565
6569
|
const duplex2 = new Duplex({
|
|
6566
|
-
read,
|
|
6570
|
+
read: read2,
|
|
6567
6571
|
...getWritableMethods(subprocessStdin, subprocess, waitWritableFinal),
|
|
6568
6572
|
destroy: callbackify(onDuplexDestroy.bind(void 0, {
|
|
6569
6573
|
subprocessStdout,
|
|
@@ -6900,9 +6904,9 @@ function pathDirectories(value, platform2) {
|
|
|
6900
6904
|
});
|
|
6901
6905
|
}
|
|
6902
6906
|
class CliEnvironment {
|
|
6903
|
-
constructor(host = currentCliHost(),
|
|
6907
|
+
constructor(host = currentCliHost(), read2 = readUserEnvironment) {
|
|
6904
6908
|
this.host = host;
|
|
6905
|
-
this.read =
|
|
6909
|
+
this.read = read2;
|
|
6906
6910
|
}
|
|
6907
6911
|
host;
|
|
6908
6912
|
read;
|
|
@@ -6947,7 +6951,7 @@ async function readUserEnvironment(host) {
|
|
|
6947
6951
|
env: host.env,
|
|
6948
6952
|
extendEnv: false,
|
|
6949
6953
|
input: "",
|
|
6950
|
-
timeout: 5e3,
|
|
6954
|
+
timeout: host.platform === "win32" ? 15e3 : 5e3,
|
|
6951
6955
|
maxBuffer: 128 * 1024,
|
|
6952
6956
|
cleanup: true,
|
|
6953
6957
|
killDescendants: true,
|
|
@@ -6961,7 +6965,8 @@ async function readUserEnvironment(host) {
|
|
|
6961
6965
|
const shell2 = path.win32.join(root, "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
6962
6966
|
const script = '$ErrorActionPreference="Stop"; $r=@{}; foreach($scope in @("Machine","User")){ $r[$scope]=[Environment]::GetEnvironmentVariables($scope) }; ConvertTo-Json -InputObject $r -Compress';
|
|
6963
6967
|
const result = await execa(shell2, ["-NoLogo", "-NoProfile", "-NonInteractive", "-Command", script], options);
|
|
6964
|
-
if (result.
|
|
6968
|
+
if (result.timedOut) throw new Error("Environment query timed out");
|
|
6969
|
+
if (result.exitCode !== 0 || result.isMaxBuffer) throw new Error("Environment query failed");
|
|
6965
6970
|
return windowsLocations(JSON.parse(result.stdout), host.env);
|
|
6966
6971
|
}
|
|
6967
6972
|
const shell = host.shell;
|
|
@@ -7529,14 +7534,14 @@ const NUMBER_FORMAT_RANGES = {
|
|
|
7529
7534
|
float32: [-34028234663852886e22, 34028234663852886e22],
|
|
7530
7535
|
float64: [-Number.MAX_VALUE, Number.MAX_VALUE]
|
|
7531
7536
|
};
|
|
7532
|
-
function pick(
|
|
7533
|
-
const currDef =
|
|
7537
|
+
function pick(schema2, mask) {
|
|
7538
|
+
const currDef = schema2._zod.def;
|
|
7534
7539
|
const checks = currDef.checks;
|
|
7535
7540
|
const hasChecks = checks && checks.length > 0;
|
|
7536
7541
|
if (hasChecks) {
|
|
7537
7542
|
throw new Error(".pick() cannot be used on object schemas containing refinements");
|
|
7538
7543
|
}
|
|
7539
|
-
const def = mergeDefs(
|
|
7544
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7540
7545
|
get shape() {
|
|
7541
7546
|
const newShape = {};
|
|
7542
7547
|
for (const key in mask) {
|
|
@@ -7552,18 +7557,18 @@ function pick(schema, mask) {
|
|
|
7552
7557
|
},
|
|
7553
7558
|
checks: []
|
|
7554
7559
|
});
|
|
7555
|
-
return clone(
|
|
7560
|
+
return clone(schema2, def);
|
|
7556
7561
|
}
|
|
7557
|
-
function omit(
|
|
7558
|
-
const currDef =
|
|
7562
|
+
function omit(schema2, mask) {
|
|
7563
|
+
const currDef = schema2._zod.def;
|
|
7559
7564
|
const checks = currDef.checks;
|
|
7560
7565
|
const hasChecks = checks && checks.length > 0;
|
|
7561
7566
|
if (hasChecks) {
|
|
7562
7567
|
throw new Error(".omit() cannot be used on object schemas containing refinements");
|
|
7563
7568
|
}
|
|
7564
|
-
const def = mergeDefs(
|
|
7569
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7565
7570
|
get shape() {
|
|
7566
|
-
const newShape = { ...
|
|
7571
|
+
const newShape = { ...schema2._zod.def.shape };
|
|
7567
7572
|
for (const key in mask) {
|
|
7568
7573
|
if (!(key in currDef.shape)) {
|
|
7569
7574
|
throw new Error(`Unrecognized key: "${key}"`);
|
|
@@ -7577,43 +7582,43 @@ function omit(schema, mask) {
|
|
|
7577
7582
|
},
|
|
7578
7583
|
checks: []
|
|
7579
7584
|
});
|
|
7580
|
-
return clone(
|
|
7585
|
+
return clone(schema2, def);
|
|
7581
7586
|
}
|
|
7582
|
-
function extend(
|
|
7587
|
+
function extend(schema2, shape) {
|
|
7583
7588
|
if (!isPlainObject(shape)) {
|
|
7584
7589
|
throw new Error("Invalid input to extend: expected a plain object");
|
|
7585
7590
|
}
|
|
7586
|
-
const checks =
|
|
7591
|
+
const checks = schema2._zod.def.checks;
|
|
7587
7592
|
const hasChecks = checks && checks.length > 0;
|
|
7588
7593
|
if (hasChecks) {
|
|
7589
|
-
const existingShape =
|
|
7594
|
+
const existingShape = schema2._zod.def.shape;
|
|
7590
7595
|
for (const key in shape) {
|
|
7591
7596
|
if (Object.getOwnPropertyDescriptor(existingShape, key) !== void 0) {
|
|
7592
7597
|
throw new Error("Cannot overwrite keys on object schemas containing refinements. Use `.safeExtend()` instead.");
|
|
7593
7598
|
}
|
|
7594
7599
|
}
|
|
7595
7600
|
}
|
|
7596
|
-
const def = mergeDefs(
|
|
7601
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7597
7602
|
get shape() {
|
|
7598
|
-
const _shape = { ...
|
|
7603
|
+
const _shape = { ...schema2._zod.def.shape, ...shape };
|
|
7599
7604
|
assignProp(this, "shape", _shape);
|
|
7600
7605
|
return _shape;
|
|
7601
7606
|
}
|
|
7602
7607
|
});
|
|
7603
|
-
return clone(
|
|
7608
|
+
return clone(schema2, def);
|
|
7604
7609
|
}
|
|
7605
|
-
function safeExtend(
|
|
7610
|
+
function safeExtend(schema2, shape) {
|
|
7606
7611
|
if (!isPlainObject(shape)) {
|
|
7607
7612
|
throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
7608
7613
|
}
|
|
7609
|
-
const def = mergeDefs(
|
|
7614
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7610
7615
|
get shape() {
|
|
7611
|
-
const _shape = { ...
|
|
7616
|
+
const _shape = { ...schema2._zod.def.shape, ...shape };
|
|
7612
7617
|
assignProp(this, "shape", _shape);
|
|
7613
7618
|
return _shape;
|
|
7614
7619
|
}
|
|
7615
7620
|
});
|
|
7616
|
-
return clone(
|
|
7621
|
+
return clone(schema2, def);
|
|
7617
7622
|
}
|
|
7618
7623
|
function merge(a2, b) {
|
|
7619
7624
|
if (a2._zod.def.checks?.length) {
|
|
@@ -7632,16 +7637,16 @@ function merge(a2, b) {
|
|
|
7632
7637
|
});
|
|
7633
7638
|
return clone(a2, def);
|
|
7634
7639
|
}
|
|
7635
|
-
function partial(Class,
|
|
7636
|
-
const currDef =
|
|
7640
|
+
function partial(Class, schema2, mask) {
|
|
7641
|
+
const currDef = schema2._zod.def;
|
|
7637
7642
|
const checks = currDef.checks;
|
|
7638
7643
|
const hasChecks = checks && checks.length > 0;
|
|
7639
7644
|
if (hasChecks) {
|
|
7640
7645
|
throw new Error(".partial() cannot be used on object schemas containing refinements");
|
|
7641
7646
|
}
|
|
7642
|
-
const def = mergeDefs(
|
|
7647
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7643
7648
|
get shape() {
|
|
7644
|
-
const oldShape =
|
|
7649
|
+
const oldShape = schema2._zod.def.shape;
|
|
7645
7650
|
const shape = { ...oldShape };
|
|
7646
7651
|
if (mask) {
|
|
7647
7652
|
for (const key in mask) {
|
|
@@ -7668,12 +7673,12 @@ function partial(Class, schema, mask) {
|
|
|
7668
7673
|
},
|
|
7669
7674
|
checks: []
|
|
7670
7675
|
});
|
|
7671
|
-
return clone(
|
|
7676
|
+
return clone(schema2, def);
|
|
7672
7677
|
}
|
|
7673
|
-
function required(Class,
|
|
7674
|
-
const def = mergeDefs(
|
|
7678
|
+
function required(Class, schema2, mask) {
|
|
7679
|
+
const def = mergeDefs(schema2._zod.def, {
|
|
7675
7680
|
get shape() {
|
|
7676
|
-
const oldShape =
|
|
7681
|
+
const oldShape = schema2._zod.def.shape;
|
|
7677
7682
|
const shape = { ...oldShape };
|
|
7678
7683
|
if (mask) {
|
|
7679
7684
|
for (const key in mask) {
|
|
@@ -7699,7 +7704,7 @@ function required(Class, schema, mask) {
|
|
|
7699
7704
|
return shape;
|
|
7700
7705
|
}
|
|
7701
7706
|
});
|
|
7702
|
-
return clone(
|
|
7707
|
+
return clone(schema2, def);
|
|
7703
7708
|
}
|
|
7704
7709
|
function aborted(x, startIndex = 0) {
|
|
7705
7710
|
if (x.aborted === true)
|
|
@@ -7828,9 +7833,9 @@ function formatError(error, mapper = (issue2) => issue2.message) {
|
|
|
7828
7833
|
processError(error);
|
|
7829
7834
|
return fieldErrors;
|
|
7830
7835
|
}
|
|
7831
|
-
const _parse = (_Err) => (
|
|
7836
|
+
const _parse = (_Err) => (schema2, value, _ctx, _params) => {
|
|
7832
7837
|
const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
|
|
7833
|
-
const result =
|
|
7838
|
+
const result = schema2._zod.run({ value, issues: [] }, ctx);
|
|
7834
7839
|
if (result instanceof Promise) {
|
|
7835
7840
|
throw new $ZodAsyncError();
|
|
7836
7841
|
}
|
|
@@ -7841,9 +7846,9 @@ const _parse = (_Err) => (schema, value, _ctx, _params) => {
|
|
|
7841
7846
|
}
|
|
7842
7847
|
return result.value;
|
|
7843
7848
|
};
|
|
7844
|
-
const _parseAsync = (_Err) => async (
|
|
7849
|
+
const _parseAsync = (_Err) => async (schema2, value, _ctx, params) => {
|
|
7845
7850
|
const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
|
|
7846
|
-
let result =
|
|
7851
|
+
let result = schema2._zod.run({ value, issues: [] }, ctx);
|
|
7847
7852
|
if (result instanceof Promise)
|
|
7848
7853
|
result = await result;
|
|
7849
7854
|
if (result.issues.length) {
|
|
@@ -7853,9 +7858,9 @@ const _parseAsync = (_Err) => async (schema, value, _ctx, params) => {
|
|
|
7853
7858
|
}
|
|
7854
7859
|
return result.value;
|
|
7855
7860
|
};
|
|
7856
|
-
const _safeParse = (_Err) => (
|
|
7861
|
+
const _safeParse = (_Err) => (schema2, value, _ctx) => {
|
|
7857
7862
|
const ctx = _ctx ? { ..._ctx, async: false } : { async: false };
|
|
7858
|
-
const result =
|
|
7863
|
+
const result = schema2._zod.run({ value, issues: [] }, ctx);
|
|
7859
7864
|
if (result instanceof Promise) {
|
|
7860
7865
|
throw new $ZodAsyncError();
|
|
7861
7866
|
}
|
|
@@ -7865,9 +7870,9 @@ const _safeParse = (_Err) => (schema, value, _ctx) => {
|
|
|
7865
7870
|
} : { success: true, data: result.value };
|
|
7866
7871
|
};
|
|
7867
7872
|
const safeParse$1 = /* @__PURE__ */ _safeParse($ZodRealError);
|
|
7868
|
-
const _safeParseAsync = (_Err) => async (
|
|
7873
|
+
const _safeParseAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7869
7874
|
const ctx = _ctx ? { ..._ctx, async: true } : { async: true };
|
|
7870
|
-
let result =
|
|
7875
|
+
let result = schema2._zod.run({ value, issues: [] }, ctx);
|
|
7871
7876
|
if (result instanceof Promise)
|
|
7872
7877
|
result = await result;
|
|
7873
7878
|
return result.issues.length ? {
|
|
@@ -7876,33 +7881,33 @@ const _safeParseAsync = (_Err) => async (schema, value, _ctx) => {
|
|
|
7876
7881
|
} : { success: true, data: result.value };
|
|
7877
7882
|
};
|
|
7878
7883
|
const safeParseAsync$1 = /* @__PURE__ */ _safeParseAsync($ZodRealError);
|
|
7879
|
-
const _encode = (_Err) => (
|
|
7884
|
+
const _encode = (_Err) => (schema2, value, _ctx) => {
|
|
7880
7885
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7881
|
-
return _parse(_Err)(
|
|
7886
|
+
return _parse(_Err)(schema2, value, ctx);
|
|
7882
7887
|
};
|
|
7883
|
-
const _decode = (_Err) => (
|
|
7884
|
-
return _parse(_Err)(
|
|
7888
|
+
const _decode = (_Err) => (schema2, value, _ctx) => {
|
|
7889
|
+
return _parse(_Err)(schema2, value, _ctx);
|
|
7885
7890
|
};
|
|
7886
|
-
const _encodeAsync = (_Err) => async (
|
|
7891
|
+
const _encodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7887
7892
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7888
|
-
return _parseAsync(_Err)(
|
|
7893
|
+
return _parseAsync(_Err)(schema2, value, ctx);
|
|
7889
7894
|
};
|
|
7890
|
-
const _decodeAsync = (_Err) => async (
|
|
7891
|
-
return _parseAsync(_Err)(
|
|
7895
|
+
const _decodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7896
|
+
return _parseAsync(_Err)(schema2, value, _ctx);
|
|
7892
7897
|
};
|
|
7893
|
-
const _safeEncode = (_Err) => (
|
|
7898
|
+
const _safeEncode = (_Err) => (schema2, value, _ctx) => {
|
|
7894
7899
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7895
|
-
return _safeParse(_Err)(
|
|
7900
|
+
return _safeParse(_Err)(schema2, value, ctx);
|
|
7896
7901
|
};
|
|
7897
|
-
const _safeDecode = (_Err) => (
|
|
7898
|
-
return _safeParse(_Err)(
|
|
7902
|
+
const _safeDecode = (_Err) => (schema2, value, _ctx) => {
|
|
7903
|
+
return _safeParse(_Err)(schema2, value, _ctx);
|
|
7899
7904
|
};
|
|
7900
|
-
const _safeEncodeAsync = (_Err) => async (
|
|
7905
|
+
const _safeEncodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7901
7906
|
const ctx = _ctx ? { ..._ctx, direction: "backward" } : { direction: "backward" };
|
|
7902
|
-
return _safeParseAsync(_Err)(
|
|
7907
|
+
return _safeParseAsync(_Err)(schema2, value, ctx);
|
|
7903
7908
|
};
|
|
7904
|
-
const _safeDecodeAsync = (_Err) => async (
|
|
7905
|
-
return _safeParseAsync(_Err)(
|
|
7909
|
+
const _safeDecodeAsync = (_Err) => async (schema2, value, _ctx) => {
|
|
7910
|
+
return _safeParseAsync(_Err)(schema2, value, _ctx);
|
|
7906
7911
|
};
|
|
7907
7912
|
const cuid = /^[cC][0-9a-z]{6,}$/;
|
|
7908
7913
|
const cuid2 = /^[0-9a-z]+$/;
|
|
@@ -9072,9 +9077,9 @@ const $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def)
|
|
|
9072
9077
|
for (const key of normalized.keys) {
|
|
9073
9078
|
const id = ids[key];
|
|
9074
9079
|
const k = esc(key);
|
|
9075
|
-
const
|
|
9076
|
-
const isOptionalIn =
|
|
9077
|
-
const isOptionalOut =
|
|
9080
|
+
const schema2 = shape[key];
|
|
9081
|
+
const isOptionalIn = schema2?._zod?.optin === "optional";
|
|
9082
|
+
const isOptionalOut = schema2?._zod?.optout === "optional";
|
|
9078
9083
|
doc.write(`const ${id} = ${parseStr(key)};`);
|
|
9079
9084
|
if (isOptionalIn && isOptionalOut) {
|
|
9080
9085
|
doc.write(`
|
|
@@ -9243,6 +9248,73 @@ const $ZodUnion = /* @__PURE__ */ $constructor("$ZodUnion", (inst, def) => {
|
|
|
9243
9248
|
});
|
|
9244
9249
|
};
|
|
9245
9250
|
});
|
|
9251
|
+
const $ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("$ZodDiscriminatedUnion", (inst, def) => {
|
|
9252
|
+
def.inclusive = false;
|
|
9253
|
+
$ZodUnion.init(inst, def);
|
|
9254
|
+
const _super = inst._zod.parse;
|
|
9255
|
+
defineLazy(inst._zod, "propValues", () => {
|
|
9256
|
+
const propValues = {};
|
|
9257
|
+
for (const option of def.options) {
|
|
9258
|
+
const pv = option._zod.propValues;
|
|
9259
|
+
if (!pv || Object.keys(pv).length === 0)
|
|
9260
|
+
throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(option)}"`);
|
|
9261
|
+
for (const [k, v] of Object.entries(pv)) {
|
|
9262
|
+
if (!propValues[k])
|
|
9263
|
+
propValues[k] = /* @__PURE__ */ new Set();
|
|
9264
|
+
for (const val of v) {
|
|
9265
|
+
propValues[k].add(val);
|
|
9266
|
+
}
|
|
9267
|
+
}
|
|
9268
|
+
}
|
|
9269
|
+
return propValues;
|
|
9270
|
+
});
|
|
9271
|
+
const disc = cached(() => {
|
|
9272
|
+
const opts = def.options;
|
|
9273
|
+
const map = /* @__PURE__ */ new Map();
|
|
9274
|
+
for (const o2 of opts) {
|
|
9275
|
+
const values = o2._zod.propValues?.[def.discriminator];
|
|
9276
|
+
if (!values || values.size === 0)
|
|
9277
|
+
throw new Error(`Invalid discriminated union option at index "${def.options.indexOf(o2)}"`);
|
|
9278
|
+
for (const v of values) {
|
|
9279
|
+
if (map.has(v)) {
|
|
9280
|
+
throw new Error(`Duplicate discriminator value "${String(v)}"`);
|
|
9281
|
+
}
|
|
9282
|
+
map.set(v, o2);
|
|
9283
|
+
}
|
|
9284
|
+
}
|
|
9285
|
+
return map;
|
|
9286
|
+
});
|
|
9287
|
+
inst._zod.parse = (payload, ctx) => {
|
|
9288
|
+
const input = payload.value;
|
|
9289
|
+
if (!isObject(input)) {
|
|
9290
|
+
payload.issues.push({
|
|
9291
|
+
code: "invalid_type",
|
|
9292
|
+
expected: "object",
|
|
9293
|
+
input,
|
|
9294
|
+
inst
|
|
9295
|
+
});
|
|
9296
|
+
return payload;
|
|
9297
|
+
}
|
|
9298
|
+
const opt = disc.value.get(input?.[def.discriminator]);
|
|
9299
|
+
if (opt) {
|
|
9300
|
+
return opt._zod.run(payload, ctx);
|
|
9301
|
+
}
|
|
9302
|
+
if (def.unionFallback || ctx.direction === "backward") {
|
|
9303
|
+
return _super(payload, ctx);
|
|
9304
|
+
}
|
|
9305
|
+
payload.issues.push({
|
|
9306
|
+
code: "invalid_union",
|
|
9307
|
+
errors: [],
|
|
9308
|
+
note: "No matching discriminator",
|
|
9309
|
+
discriminator: def.discriminator,
|
|
9310
|
+
options: Array.from(disc.value.keys()),
|
|
9311
|
+
input,
|
|
9312
|
+
path: [def.discriminator],
|
|
9313
|
+
inst
|
|
9314
|
+
});
|
|
9315
|
+
return payload;
|
|
9316
|
+
};
|
|
9317
|
+
});
|
|
9246
9318
|
const $ZodIntersection = /* @__PURE__ */ $constructor("$ZodIntersection", (inst, def) => {
|
|
9247
9319
|
$ZodType.init(inst, def);
|
|
9248
9320
|
inst._zod.parse = (payload, ctx) => {
|
|
@@ -9487,6 +9559,28 @@ const $ZodEnum = /* @__PURE__ */ $constructor("$ZodEnum", (inst, def) => {
|
|
|
9487
9559
|
return payload;
|
|
9488
9560
|
};
|
|
9489
9561
|
});
|
|
9562
|
+
const $ZodLiteral = /* @__PURE__ */ $constructor("$ZodLiteral", (inst, def) => {
|
|
9563
|
+
$ZodType.init(inst, def);
|
|
9564
|
+
if (def.values.length === 0) {
|
|
9565
|
+
throw new Error("Cannot create literal schema with no valid values");
|
|
9566
|
+
}
|
|
9567
|
+
const values = new Set(def.values);
|
|
9568
|
+
inst._zod.values = values;
|
|
9569
|
+
inst._zod.pattern = new RegExp(`^(${def.values.map((o2) => typeof o2 === "string" ? escapeRegex(o2) : o2 ? escapeRegex(o2.toString()) : String(o2)).join("|")})$`);
|
|
9570
|
+
inst._zod.parse = (payload, _ctx) => {
|
|
9571
|
+
const input = payload.value;
|
|
9572
|
+
if (values.has(input)) {
|
|
9573
|
+
return payload;
|
|
9574
|
+
}
|
|
9575
|
+
payload.issues.push({
|
|
9576
|
+
code: "invalid_value",
|
|
9577
|
+
values: def.values,
|
|
9578
|
+
input,
|
|
9579
|
+
inst
|
|
9580
|
+
});
|
|
9581
|
+
return payload;
|
|
9582
|
+
};
|
|
9583
|
+
});
|
|
9490
9584
|
const $ZodTransform = /* @__PURE__ */ $constructor("$ZodTransform", (inst, def) => {
|
|
9491
9585
|
$ZodType.init(inst, def);
|
|
9492
9586
|
inst._zod.optin = "optional";
|
|
@@ -9761,11 +9855,11 @@ class $ZodRegistry {
|
|
|
9761
9855
|
this._map = /* @__PURE__ */ new WeakMap();
|
|
9762
9856
|
this._idmap = /* @__PURE__ */ new Map();
|
|
9763
9857
|
}
|
|
9764
|
-
add(
|
|
9858
|
+
add(schema2, ..._meta) {
|
|
9765
9859
|
const meta = _meta[0];
|
|
9766
|
-
this._map.set(
|
|
9860
|
+
this._map.set(schema2, meta);
|
|
9767
9861
|
if (meta && typeof meta === "object" && "id" in meta) {
|
|
9768
|
-
this._idmap.set(meta.id,
|
|
9862
|
+
this._idmap.set(meta.id, schema2);
|
|
9769
9863
|
}
|
|
9770
9864
|
return this;
|
|
9771
9865
|
}
|
|
@@ -9774,26 +9868,26 @@ class $ZodRegistry {
|
|
|
9774
9868
|
this._idmap = /* @__PURE__ */ new Map();
|
|
9775
9869
|
return this;
|
|
9776
9870
|
}
|
|
9777
|
-
remove(
|
|
9778
|
-
const meta = this._map.get(
|
|
9871
|
+
remove(schema2) {
|
|
9872
|
+
const meta = this._map.get(schema2);
|
|
9779
9873
|
if (meta && typeof meta === "object" && "id" in meta) {
|
|
9780
9874
|
this._idmap.delete(meta.id);
|
|
9781
9875
|
}
|
|
9782
|
-
this._map.delete(
|
|
9876
|
+
this._map.delete(schema2);
|
|
9783
9877
|
return this;
|
|
9784
9878
|
}
|
|
9785
|
-
get(
|
|
9786
|
-
const p =
|
|
9879
|
+
get(schema2) {
|
|
9880
|
+
const p = schema2._zod.parent;
|
|
9787
9881
|
if (p) {
|
|
9788
9882
|
const pm = { ...this.get(p) ?? {} };
|
|
9789
9883
|
delete pm.id;
|
|
9790
|
-
const f = { ...pm, ...this._map.get(
|
|
9884
|
+
const f = { ...pm, ...this._map.get(schema2) };
|
|
9791
9885
|
return Object.keys(f).length ? f : void 0;
|
|
9792
9886
|
}
|
|
9793
|
-
return this._map.get(
|
|
9887
|
+
return this._map.get(schema2);
|
|
9794
9888
|
}
|
|
9795
|
-
has(
|
|
9796
|
-
return this._map.has(
|
|
9889
|
+
has(schema2) {
|
|
9890
|
+
return this._map.has(schema2);
|
|
9797
9891
|
}
|
|
9798
9892
|
}
|
|
9799
9893
|
function registry() {
|
|
@@ -10270,13 +10364,13 @@ function _array(Class, element, params) {
|
|
|
10270
10364
|
}
|
|
10271
10365
|
// @__NO_SIDE_EFFECTS__
|
|
10272
10366
|
function _refine(Class, fn, _params) {
|
|
10273
|
-
const
|
|
10367
|
+
const schema2 = new Class({
|
|
10274
10368
|
type: "custom",
|
|
10275
10369
|
check: "custom",
|
|
10276
10370
|
fn,
|
|
10277
10371
|
...normalizeParams(_params)
|
|
10278
10372
|
});
|
|
10279
|
-
return
|
|
10373
|
+
return schema2;
|
|
10280
10374
|
}
|
|
10281
10375
|
// @__NO_SIDE_EFFECTS__
|
|
10282
10376
|
function _superRefine(fn, params) {
|
|
@@ -10329,40 +10423,40 @@ function initializeContext(params) {
|
|
|
10329
10423
|
external: params?.external ?? void 0
|
|
10330
10424
|
};
|
|
10331
10425
|
}
|
|
10332
|
-
function process$1(
|
|
10426
|
+
function process$1(schema2, ctx, _params = { path: [], schemaPath: [] }) {
|
|
10333
10427
|
var _a2;
|
|
10334
|
-
const def =
|
|
10335
|
-
const seen = ctx.seen.get(
|
|
10428
|
+
const def = schema2._zod.def;
|
|
10429
|
+
const seen = ctx.seen.get(schema2);
|
|
10336
10430
|
if (seen) {
|
|
10337
10431
|
seen.count++;
|
|
10338
|
-
const isCycle = _params.schemaPath.includes(
|
|
10432
|
+
const isCycle = _params.schemaPath.includes(schema2);
|
|
10339
10433
|
if (isCycle) {
|
|
10340
10434
|
seen.cycle = _params.path;
|
|
10341
10435
|
}
|
|
10342
10436
|
return seen.schema;
|
|
10343
10437
|
}
|
|
10344
10438
|
const result = { schema: {}, count: 1, cycle: void 0, path: _params.path };
|
|
10345
|
-
ctx.seen.set(
|
|
10346
|
-
const overrideSchema =
|
|
10439
|
+
ctx.seen.set(schema2, result);
|
|
10440
|
+
const overrideSchema = schema2._zod.toJSONSchema?.();
|
|
10347
10441
|
if (overrideSchema) {
|
|
10348
10442
|
result.schema = overrideSchema;
|
|
10349
10443
|
} else {
|
|
10350
10444
|
const params = {
|
|
10351
10445
|
..._params,
|
|
10352
|
-
schemaPath: [..._params.schemaPath,
|
|
10446
|
+
schemaPath: [..._params.schemaPath, schema2],
|
|
10353
10447
|
path: _params.path
|
|
10354
10448
|
};
|
|
10355
|
-
if (
|
|
10356
|
-
|
|
10449
|
+
if (schema2._zod.processJSONSchema) {
|
|
10450
|
+
schema2._zod.processJSONSchema(ctx, result.schema, params);
|
|
10357
10451
|
} else {
|
|
10358
10452
|
const _json = result.schema;
|
|
10359
10453
|
const processor = ctx.processors[def.type];
|
|
10360
10454
|
if (!processor) {
|
|
10361
10455
|
throw new Error(`[toJSONSchema]: Non-representable type encountered: ${def.type}`);
|
|
10362
10456
|
}
|
|
10363
|
-
processor(
|
|
10457
|
+
processor(schema2, ctx, _json, params);
|
|
10364
10458
|
}
|
|
10365
|
-
const parent =
|
|
10459
|
+
const parent = schema2._zod.parent;
|
|
10366
10460
|
if (parent) {
|
|
10367
10461
|
if (!result.ref)
|
|
10368
10462
|
result.ref = parent;
|
|
@@ -10370,21 +10464,21 @@ function process$1(schema, ctx, _params = { path: [], schemaPath: [] }) {
|
|
|
10370
10464
|
ctx.seen.get(parent).isParent = true;
|
|
10371
10465
|
}
|
|
10372
10466
|
}
|
|
10373
|
-
const meta = ctx.metadataRegistry.get(
|
|
10467
|
+
const meta = ctx.metadataRegistry.get(schema2);
|
|
10374
10468
|
if (meta)
|
|
10375
10469
|
Object.assign(result.schema, meta);
|
|
10376
|
-
if (ctx.io === "input" && isTransforming(
|
|
10470
|
+
if (ctx.io === "input" && isTransforming(schema2)) {
|
|
10377
10471
|
delete result.schema.examples;
|
|
10378
10472
|
delete result.schema.default;
|
|
10379
10473
|
}
|
|
10380
10474
|
if (ctx.io === "input" && "_prefault" in result.schema)
|
|
10381
10475
|
(_a2 = result.schema).default ?? (_a2.default = result.schema._prefault);
|
|
10382
10476
|
delete result.schema._prefault;
|
|
10383
|
-
const _result = ctx.seen.get(
|
|
10477
|
+
const _result = ctx.seen.get(schema2);
|
|
10384
10478
|
return _result.schema;
|
|
10385
10479
|
}
|
|
10386
|
-
function extractDefs(ctx,
|
|
10387
|
-
const root = ctx.seen.get(
|
|
10480
|
+
function extractDefs(ctx, schema2) {
|
|
10481
|
+
const root = ctx.seen.get(schema2);
|
|
10388
10482
|
if (!root)
|
|
10389
10483
|
throw new Error("Unprocessed schema. This is a bug in Zod.");
|
|
10390
10484
|
const idToSchema = /* @__PURE__ */ new Map();
|
|
@@ -10427,11 +10521,11 @@ function extractDefs(ctx, schema) {
|
|
|
10427
10521
|
seen.def = { ...seen.schema };
|
|
10428
10522
|
if (defId)
|
|
10429
10523
|
seen.defId = defId;
|
|
10430
|
-
const
|
|
10431
|
-
for (const key in
|
|
10432
|
-
delete
|
|
10524
|
+
const schema3 = seen.schema;
|
|
10525
|
+
for (const key in schema3) {
|
|
10526
|
+
delete schema3[key];
|
|
10433
10527
|
}
|
|
10434
|
-
|
|
10528
|
+
schema3.$ref = ref;
|
|
10435
10529
|
};
|
|
10436
10530
|
if (ctx.cycles === "throw") {
|
|
10437
10531
|
for (const entry of ctx.seen.entries()) {
|
|
@@ -10445,13 +10539,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10445
10539
|
}
|
|
10446
10540
|
for (const entry of ctx.seen.entries()) {
|
|
10447
10541
|
const seen = entry[1];
|
|
10448
|
-
if (
|
|
10542
|
+
if (schema2 === entry[0]) {
|
|
10449
10543
|
extractToDef(entry);
|
|
10450
10544
|
continue;
|
|
10451
10545
|
}
|
|
10452
10546
|
if (ctx.external) {
|
|
10453
10547
|
const ext = ctx.external.registry.get(entry[0])?.id;
|
|
10454
|
-
if (
|
|
10548
|
+
if (schema2 !== entry[0] && ext) {
|
|
10455
10549
|
extractToDef(entry);
|
|
10456
10550
|
continue;
|
|
10457
10551
|
}
|
|
@@ -10473,16 +10567,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10473
10567
|
}
|
|
10474
10568
|
}
|
|
10475
10569
|
}
|
|
10476
|
-
function finalize(ctx,
|
|
10477
|
-
const root = ctx.seen.get(
|
|
10570
|
+
function finalize(ctx, schema2) {
|
|
10571
|
+
const root = ctx.seen.get(schema2);
|
|
10478
10572
|
if (!root)
|
|
10479
10573
|
throw new Error("Unprocessed schema. This is a bug in Zod.");
|
|
10480
10574
|
const flattenRef = (zodSchema) => {
|
|
10481
10575
|
const seen = ctx.seen.get(zodSchema);
|
|
10482
10576
|
if (seen.ref === null)
|
|
10483
10577
|
return;
|
|
10484
|
-
const
|
|
10485
|
-
const _cached = { ...
|
|
10578
|
+
const schema3 = seen.def ?? seen.schema;
|
|
10579
|
+
const _cached = { ...schema3 };
|
|
10486
10580
|
const ref = seen.ref;
|
|
10487
10581
|
seen.ref = null;
|
|
10488
10582
|
if (ref) {
|
|
@@ -10490,28 +10584,28 @@ function finalize(ctx, schema) {
|
|
|
10490
10584
|
const refSeen = ctx.seen.get(ref);
|
|
10491
10585
|
const refSchema = refSeen.schema;
|
|
10492
10586
|
if (refSchema.$ref && (ctx.target === "draft-07" || ctx.target === "draft-04" || ctx.target === "openapi-3.0")) {
|
|
10493
|
-
|
|
10494
|
-
|
|
10587
|
+
schema3.allOf = schema3.allOf ?? [];
|
|
10588
|
+
schema3.allOf.push(refSchema);
|
|
10495
10589
|
} else {
|
|
10496
|
-
Object.assign(
|
|
10590
|
+
Object.assign(schema3, refSchema);
|
|
10497
10591
|
}
|
|
10498
|
-
Object.assign(
|
|
10592
|
+
Object.assign(schema3, _cached);
|
|
10499
10593
|
const isParentRef = zodSchema._zod.parent === ref;
|
|
10500
10594
|
if (isParentRef) {
|
|
10501
|
-
for (const key in
|
|
10595
|
+
for (const key in schema3) {
|
|
10502
10596
|
if (key === "$ref" || key === "allOf")
|
|
10503
10597
|
continue;
|
|
10504
10598
|
if (!(key in _cached)) {
|
|
10505
|
-
delete
|
|
10599
|
+
delete schema3[key];
|
|
10506
10600
|
}
|
|
10507
10601
|
}
|
|
10508
10602
|
}
|
|
10509
10603
|
if (refSchema.$ref && refSeen.def) {
|
|
10510
|
-
for (const key in
|
|
10604
|
+
for (const key in schema3) {
|
|
10511
10605
|
if (key === "$ref" || key === "allOf")
|
|
10512
10606
|
continue;
|
|
10513
|
-
if (key in refSeen.def && JSON.stringify(
|
|
10514
|
-
delete
|
|
10607
|
+
if (key in refSeen.def && JSON.stringify(schema3[key]) === JSON.stringify(refSeen.def[key])) {
|
|
10608
|
+
delete schema3[key];
|
|
10515
10609
|
}
|
|
10516
10610
|
}
|
|
10517
10611
|
}
|
|
@@ -10521,13 +10615,13 @@ function finalize(ctx, schema) {
|
|
|
10521
10615
|
flattenRef(parent);
|
|
10522
10616
|
const parentSeen = ctx.seen.get(parent);
|
|
10523
10617
|
if (parentSeen?.schema.$ref) {
|
|
10524
|
-
|
|
10618
|
+
schema3.$ref = parentSeen.schema.$ref;
|
|
10525
10619
|
if (parentSeen.def) {
|
|
10526
|
-
for (const key in
|
|
10620
|
+
for (const key in schema3) {
|
|
10527
10621
|
if (key === "$ref" || key === "allOf")
|
|
10528
10622
|
continue;
|
|
10529
|
-
if (key in parentSeen.def && JSON.stringify(
|
|
10530
|
-
delete
|
|
10623
|
+
if (key in parentSeen.def && JSON.stringify(schema3[key]) === JSON.stringify(parentSeen.def[key])) {
|
|
10624
|
+
delete schema3[key];
|
|
10531
10625
|
}
|
|
10532
10626
|
}
|
|
10533
10627
|
}
|
|
@@ -10535,7 +10629,7 @@ function finalize(ctx, schema) {
|
|
|
10535
10629
|
}
|
|
10536
10630
|
ctx.override({
|
|
10537
10631
|
zodSchema,
|
|
10538
|
-
jsonSchema:
|
|
10632
|
+
jsonSchema: schema3,
|
|
10539
10633
|
path: seen.path ?? []
|
|
10540
10634
|
});
|
|
10541
10635
|
};
|
|
@@ -10552,13 +10646,13 @@ function finalize(ctx, schema) {
|
|
|
10552
10646
|
} else if (ctx.target === "openapi-3.0") ;
|
|
10553
10647
|
else ;
|
|
10554
10648
|
if (ctx.external?.uri) {
|
|
10555
|
-
const id = ctx.external.registry.get(
|
|
10649
|
+
const id = ctx.external.registry.get(schema2)?.id;
|
|
10556
10650
|
if (!id)
|
|
10557
10651
|
throw new Error("Schema is missing an `id` property");
|
|
10558
10652
|
result.$id = ctx.external.uri(id);
|
|
10559
10653
|
}
|
|
10560
10654
|
Object.assign(result, root.def ?? root.schema);
|
|
10561
|
-
const rootMetaId = ctx.metadataRegistry.get(
|
|
10655
|
+
const rootMetaId = ctx.metadataRegistry.get(schema2)?.id;
|
|
10562
10656
|
if (rootMetaId !== void 0 && result.id === rootMetaId)
|
|
10563
10657
|
delete result.id;
|
|
10564
10658
|
const defs = ctx.external?.defs ?? {};
|
|
@@ -10584,10 +10678,10 @@ function finalize(ctx, schema) {
|
|
|
10584
10678
|
const finalized = JSON.parse(JSON.stringify(result));
|
|
10585
10679
|
Object.defineProperty(finalized, "~standard", {
|
|
10586
10680
|
value: {
|
|
10587
|
-
...
|
|
10681
|
+
...schema2["~standard"],
|
|
10588
10682
|
jsonSchema: {
|
|
10589
|
-
input: createStandardJSONSchemaMethod(
|
|
10590
|
-
output: createStandardJSONSchemaMethod(
|
|
10683
|
+
input: createStandardJSONSchemaMethod(schema2, "input", ctx.processors),
|
|
10684
|
+
output: createStandardJSONSchemaMethod(schema2, "output", ctx.processors)
|
|
10591
10685
|
}
|
|
10592
10686
|
},
|
|
10593
10687
|
enumerable: false,
|
|
@@ -10651,18 +10745,18 @@ function isTransforming(_schema, _ctx) {
|
|
|
10651
10745
|
}
|
|
10652
10746
|
return false;
|
|
10653
10747
|
}
|
|
10654
|
-
const createToJSONSchemaMethod = (
|
|
10748
|
+
const createToJSONSchemaMethod = (schema2, processors = {}) => (params) => {
|
|
10655
10749
|
const ctx = initializeContext({ ...params, processors });
|
|
10656
|
-
process$1(
|
|
10657
|
-
extractDefs(ctx,
|
|
10658
|
-
return finalize(ctx,
|
|
10750
|
+
process$1(schema2, ctx);
|
|
10751
|
+
extractDefs(ctx, schema2);
|
|
10752
|
+
return finalize(ctx, schema2);
|
|
10659
10753
|
};
|
|
10660
|
-
const createStandardJSONSchemaMethod = (
|
|
10754
|
+
const createStandardJSONSchemaMethod = (schema2, io, processors = {}) => (params) => {
|
|
10661
10755
|
const { libraryOptions, target } = params ?? {};
|
|
10662
10756
|
const ctx = initializeContext({ ...libraryOptions ?? {}, target, io, processors });
|
|
10663
|
-
process$1(
|
|
10664
|
-
extractDefs(ctx,
|
|
10665
|
-
return finalize(ctx,
|
|
10757
|
+
process$1(schema2, ctx);
|
|
10758
|
+
extractDefs(ctx, schema2);
|
|
10759
|
+
return finalize(ctx, schema2);
|
|
10666
10760
|
};
|
|
10667
10761
|
const formatMap = {
|
|
10668
10762
|
guid: "uuid",
|
|
@@ -10672,10 +10766,10 @@ const formatMap = {
|
|
|
10672
10766
|
regex: ""
|
|
10673
10767
|
// do not set
|
|
10674
10768
|
};
|
|
10675
|
-
const stringProcessor = (
|
|
10769
|
+
const stringProcessor = (schema2, ctx, _json, _params) => {
|
|
10676
10770
|
const json2 = _json;
|
|
10677
10771
|
json2.type = "string";
|
|
10678
|
-
const { minimum, maximum, format: format2, patterns, contentEncoding } =
|
|
10772
|
+
const { minimum, maximum, format: format2, patterns, contentEncoding } = schema2._zod.bag;
|
|
10679
10773
|
if (typeof minimum === "number")
|
|
10680
10774
|
json2.minLength = minimum;
|
|
10681
10775
|
if (typeof maximum === "number")
|
|
@@ -10704,9 +10798,9 @@ const stringProcessor = (schema, ctx, _json, _params) => {
|
|
|
10704
10798
|
}
|
|
10705
10799
|
}
|
|
10706
10800
|
};
|
|
10707
|
-
const numberProcessor = (
|
|
10801
|
+
const numberProcessor = (schema2, ctx, _json, _params) => {
|
|
10708
10802
|
const json2 = _json;
|
|
10709
|
-
const { minimum, maximum, format: format2, multipleOf, exclusiveMaximum, exclusiveMinimum } =
|
|
10803
|
+
const { minimum, maximum, format: format2, multipleOf, exclusiveMaximum, exclusiveMinimum } = schema2._zod.bag;
|
|
10710
10804
|
if (typeof format2 === "string" && format2.includes("int"))
|
|
10711
10805
|
json2.type = "integer";
|
|
10712
10806
|
else
|
|
@@ -10781,8 +10875,8 @@ const dateProcessor = (_schema, ctx, _json, _params) => {
|
|
|
10781
10875
|
throw new Error("Date cannot be represented in JSON Schema");
|
|
10782
10876
|
}
|
|
10783
10877
|
};
|
|
10784
|
-
const enumProcessor = (
|
|
10785
|
-
const def =
|
|
10878
|
+
const enumProcessor = (schema2, _ctx, json2, _params) => {
|
|
10879
|
+
const def = schema2._zod.def;
|
|
10786
10880
|
const values = getEnumValues(def.entries);
|
|
10787
10881
|
if (values.every((v) => typeof v === "number"))
|
|
10788
10882
|
json2.type = "number";
|
|
@@ -10790,8 +10884,8 @@ const enumProcessor = (schema, _ctx, json2, _params) => {
|
|
|
10790
10884
|
json2.type = "string";
|
|
10791
10885
|
json2.enum = values;
|
|
10792
10886
|
};
|
|
10793
|
-
const literalProcessor = (
|
|
10794
|
-
const def =
|
|
10887
|
+
const literalProcessor = (schema2, ctx, json2, _params) => {
|
|
10888
|
+
const def = schema2._zod.def;
|
|
10795
10889
|
const vals = [];
|
|
10796
10890
|
for (const val of def.values) {
|
|
10797
10891
|
if (val === void 0) {
|
|
@@ -10834,22 +10928,22 @@ const nanProcessor = (_schema, ctx, _json, _params) => {
|
|
|
10834
10928
|
throw new Error("NaN cannot be represented in JSON Schema");
|
|
10835
10929
|
}
|
|
10836
10930
|
};
|
|
10837
|
-
const templateLiteralProcessor = (
|
|
10931
|
+
const templateLiteralProcessor = (schema2, _ctx, json2, _params) => {
|
|
10838
10932
|
const _json = json2;
|
|
10839
|
-
const pattern =
|
|
10933
|
+
const pattern = schema2._zod.pattern;
|
|
10840
10934
|
if (!pattern)
|
|
10841
10935
|
throw new Error("Pattern not found in template literal");
|
|
10842
10936
|
_json.type = "string";
|
|
10843
10937
|
_json.pattern = pattern.source;
|
|
10844
10938
|
};
|
|
10845
|
-
const fileProcessor = (
|
|
10939
|
+
const fileProcessor = (schema2, _ctx, json2, _params) => {
|
|
10846
10940
|
const _json = json2;
|
|
10847
10941
|
const file = {
|
|
10848
10942
|
type: "string",
|
|
10849
10943
|
format: "binary",
|
|
10850
10944
|
contentEncoding: "binary"
|
|
10851
10945
|
};
|
|
10852
|
-
const { minimum, maximum, mime } =
|
|
10946
|
+
const { minimum, maximum, mime } = schema2._zod.bag;
|
|
10853
10947
|
if (minimum !== void 0)
|
|
10854
10948
|
file.minLength = minimum;
|
|
10855
10949
|
if (maximum !== void 0)
|
|
@@ -10894,10 +10988,10 @@ const setProcessor = (_schema, ctx, _json, _params) => {
|
|
|
10894
10988
|
throw new Error("Set cannot be represented in JSON Schema");
|
|
10895
10989
|
}
|
|
10896
10990
|
};
|
|
10897
|
-
const arrayProcessor = (
|
|
10991
|
+
const arrayProcessor = (schema2, ctx, _json, params) => {
|
|
10898
10992
|
const json2 = _json;
|
|
10899
|
-
const def =
|
|
10900
|
-
const { minimum, maximum } =
|
|
10993
|
+
const def = schema2._zod.def;
|
|
10994
|
+
const { minimum, maximum } = schema2._zod.bag;
|
|
10901
10995
|
if (typeof minimum === "number")
|
|
10902
10996
|
json2.minItems = minimum;
|
|
10903
10997
|
if (typeof maximum === "number")
|
|
@@ -10908,9 +11002,9 @@ const arrayProcessor = (schema, ctx, _json, params) => {
|
|
|
10908
11002
|
path: [...params.path, "items"]
|
|
10909
11003
|
});
|
|
10910
11004
|
};
|
|
10911
|
-
const objectProcessor = (
|
|
11005
|
+
const objectProcessor = (schema2, ctx, _json, params) => {
|
|
10912
11006
|
const json2 = _json;
|
|
10913
|
-
const def =
|
|
11007
|
+
const def = schema2._zod.def;
|
|
10914
11008
|
json2.type = "object";
|
|
10915
11009
|
json2.properties = {};
|
|
10916
11010
|
const shape = def.shape;
|
|
@@ -10944,8 +11038,8 @@ const objectProcessor = (schema, ctx, _json, params) => {
|
|
|
10944
11038
|
});
|
|
10945
11039
|
}
|
|
10946
11040
|
};
|
|
10947
|
-
const unionProcessor = (
|
|
10948
|
-
const def =
|
|
11041
|
+
const unionProcessor = (schema2, ctx, json2, params) => {
|
|
11042
|
+
const def = schema2._zod.def;
|
|
10949
11043
|
const isExclusive = def.inclusive === false;
|
|
10950
11044
|
const options = def.options.map((x, i2) => process$1(x, ctx, {
|
|
10951
11045
|
...params,
|
|
@@ -10957,8 +11051,8 @@ const unionProcessor = (schema, ctx, json2, params) => {
|
|
|
10957
11051
|
json2.anyOf = options;
|
|
10958
11052
|
}
|
|
10959
11053
|
};
|
|
10960
|
-
const intersectionProcessor = (
|
|
10961
|
-
const def =
|
|
11054
|
+
const intersectionProcessor = (schema2, ctx, json2, params) => {
|
|
11055
|
+
const def = schema2._zod.def;
|
|
10962
11056
|
const a2 = process$1(def.left, ctx, {
|
|
10963
11057
|
...params,
|
|
10964
11058
|
path: [...params.path, "allOf", 0]
|
|
@@ -10974,9 +11068,9 @@ const intersectionProcessor = (schema, ctx, json2, params) => {
|
|
|
10974
11068
|
];
|
|
10975
11069
|
json2.allOf = allOf;
|
|
10976
11070
|
};
|
|
10977
|
-
const tupleProcessor = (
|
|
11071
|
+
const tupleProcessor = (schema2, ctx, _json, params) => {
|
|
10978
11072
|
const json2 = _json;
|
|
10979
|
-
const def =
|
|
11073
|
+
const def = schema2._zod.def;
|
|
10980
11074
|
json2.type = "array";
|
|
10981
11075
|
const prefixPath = ctx.target === "draft-2020-12" ? "prefixItems" : "items";
|
|
10982
11076
|
const restPath = ctx.target === "draft-2020-12" ? "items" : ctx.target === "openapi-3.0" ? "items" : "additionalItems";
|
|
@@ -11010,15 +11104,15 @@ const tupleProcessor = (schema, ctx, _json, params) => {
|
|
|
11010
11104
|
json2.additionalItems = rest;
|
|
11011
11105
|
}
|
|
11012
11106
|
}
|
|
11013
|
-
const { minimum, maximum } =
|
|
11107
|
+
const { minimum, maximum } = schema2._zod.bag;
|
|
11014
11108
|
if (typeof minimum === "number")
|
|
11015
11109
|
json2.minItems = minimum;
|
|
11016
11110
|
if (typeof maximum === "number")
|
|
11017
11111
|
json2.maxItems = maximum;
|
|
11018
11112
|
};
|
|
11019
|
-
const recordProcessor = (
|
|
11113
|
+
const recordProcessor = (schema2, ctx, _json, params) => {
|
|
11020
11114
|
const json2 = _json;
|
|
11021
|
-
const def =
|
|
11115
|
+
const def = schema2._zod.def;
|
|
11022
11116
|
json2.type = "object";
|
|
11023
11117
|
const keyType = def.keyType;
|
|
11024
11118
|
const keyBag = keyType._zod.bag;
|
|
@@ -11052,10 +11146,10 @@ const recordProcessor = (schema, ctx, _json, params) => {
|
|
|
11052
11146
|
}
|
|
11053
11147
|
}
|
|
11054
11148
|
};
|
|
11055
|
-
const nullableProcessor = (
|
|
11056
|
-
const def =
|
|
11149
|
+
const nullableProcessor = (schema2, ctx, json2, params) => {
|
|
11150
|
+
const def = schema2._zod.def;
|
|
11057
11151
|
const inner = process$1(def.innerType, ctx, params);
|
|
11058
|
-
const seen = ctx.seen.get(
|
|
11152
|
+
const seen = ctx.seen.get(schema2);
|
|
11059
11153
|
if (ctx.target === "openapi-3.0") {
|
|
11060
11154
|
seen.ref = def.innerType;
|
|
11061
11155
|
json2.nullable = true;
|
|
@@ -11063,31 +11157,31 @@ const nullableProcessor = (schema, ctx, json2, params) => {
|
|
|
11063
11157
|
json2.anyOf = [inner, { type: "null" }];
|
|
11064
11158
|
}
|
|
11065
11159
|
};
|
|
11066
|
-
const nonoptionalProcessor = (
|
|
11067
|
-
const def =
|
|
11160
|
+
const nonoptionalProcessor = (schema2, ctx, _json, params) => {
|
|
11161
|
+
const def = schema2._zod.def;
|
|
11068
11162
|
process$1(def.innerType, ctx, params);
|
|
11069
|
-
const seen = ctx.seen.get(
|
|
11163
|
+
const seen = ctx.seen.get(schema2);
|
|
11070
11164
|
seen.ref = def.innerType;
|
|
11071
11165
|
};
|
|
11072
|
-
const defaultProcessor = (
|
|
11073
|
-
const def =
|
|
11166
|
+
const defaultProcessor = (schema2, ctx, json2, params) => {
|
|
11167
|
+
const def = schema2._zod.def;
|
|
11074
11168
|
process$1(def.innerType, ctx, params);
|
|
11075
|
-
const seen = ctx.seen.get(
|
|
11169
|
+
const seen = ctx.seen.get(schema2);
|
|
11076
11170
|
seen.ref = def.innerType;
|
|
11077
11171
|
json2.default = JSON.parse(JSON.stringify(def.defaultValue));
|
|
11078
11172
|
};
|
|
11079
|
-
const prefaultProcessor = (
|
|
11080
|
-
const def =
|
|
11173
|
+
const prefaultProcessor = (schema2, ctx, json2, params) => {
|
|
11174
|
+
const def = schema2._zod.def;
|
|
11081
11175
|
process$1(def.innerType, ctx, params);
|
|
11082
|
-
const seen = ctx.seen.get(
|
|
11176
|
+
const seen = ctx.seen.get(schema2);
|
|
11083
11177
|
seen.ref = def.innerType;
|
|
11084
11178
|
if (ctx.io === "input")
|
|
11085
11179
|
json2._prefault = JSON.parse(JSON.stringify(def.defaultValue));
|
|
11086
11180
|
};
|
|
11087
|
-
const catchProcessor = (
|
|
11088
|
-
const def =
|
|
11181
|
+
const catchProcessor = (schema2, ctx, json2, params) => {
|
|
11182
|
+
const def = schema2._zod.def;
|
|
11089
11183
|
process$1(def.innerType, ctx, params);
|
|
11090
|
-
const seen = ctx.seen.get(
|
|
11184
|
+
const seen = ctx.seen.get(schema2);
|
|
11091
11185
|
seen.ref = def.innerType;
|
|
11092
11186
|
let catchValue;
|
|
11093
11187
|
try {
|
|
@@ -11097,37 +11191,37 @@ const catchProcessor = (schema, ctx, json2, params) => {
|
|
|
11097
11191
|
}
|
|
11098
11192
|
json2.default = catchValue;
|
|
11099
11193
|
};
|
|
11100
|
-
const pipeProcessor = (
|
|
11101
|
-
const def =
|
|
11194
|
+
const pipeProcessor = (schema2, ctx, _json, params) => {
|
|
11195
|
+
const def = schema2._zod.def;
|
|
11102
11196
|
const inIsTransform = def.in._zod.traits.has("$ZodTransform");
|
|
11103
11197
|
const innerType = ctx.io === "input" ? inIsTransform ? def.out : def.in : def.out;
|
|
11104
11198
|
process$1(innerType, ctx, params);
|
|
11105
|
-
const seen = ctx.seen.get(
|
|
11199
|
+
const seen = ctx.seen.get(schema2);
|
|
11106
11200
|
seen.ref = innerType;
|
|
11107
11201
|
};
|
|
11108
|
-
const readonlyProcessor = (
|
|
11109
|
-
const def =
|
|
11202
|
+
const readonlyProcessor = (schema2, ctx, json2, params) => {
|
|
11203
|
+
const def = schema2._zod.def;
|
|
11110
11204
|
process$1(def.innerType, ctx, params);
|
|
11111
|
-
const seen = ctx.seen.get(
|
|
11205
|
+
const seen = ctx.seen.get(schema2);
|
|
11112
11206
|
seen.ref = def.innerType;
|
|
11113
11207
|
json2.readOnly = true;
|
|
11114
11208
|
};
|
|
11115
|
-
const promiseProcessor = (
|
|
11116
|
-
const def =
|
|
11209
|
+
const promiseProcessor = (schema2, ctx, _json, params) => {
|
|
11210
|
+
const def = schema2._zod.def;
|
|
11117
11211
|
process$1(def.innerType, ctx, params);
|
|
11118
|
-
const seen = ctx.seen.get(
|
|
11212
|
+
const seen = ctx.seen.get(schema2);
|
|
11119
11213
|
seen.ref = def.innerType;
|
|
11120
11214
|
};
|
|
11121
|
-
const optionalProcessor = (
|
|
11122
|
-
const def =
|
|
11215
|
+
const optionalProcessor = (schema2, ctx, _json, params) => {
|
|
11216
|
+
const def = schema2._zod.def;
|
|
11123
11217
|
process$1(def.innerType, ctx, params);
|
|
11124
|
-
const seen = ctx.seen.get(
|
|
11218
|
+
const seen = ctx.seen.get(schema2);
|
|
11125
11219
|
seen.ref = def.innerType;
|
|
11126
11220
|
};
|
|
11127
|
-
const lazyProcessor = (
|
|
11128
|
-
const innerType =
|
|
11221
|
+
const lazyProcessor = (schema2, ctx, _json, params) => {
|
|
11222
|
+
const innerType = schema2._zod.innerType;
|
|
11129
11223
|
process$1(innerType, ctx, params);
|
|
11130
|
-
const seen = ctx.seen.get(
|
|
11224
|
+
const seen = ctx.seen.get(schema2);
|
|
11131
11225
|
seen.ref = innerType;
|
|
11132
11226
|
};
|
|
11133
11227
|
const allProcessors = {
|
|
@@ -11177,8 +11271,8 @@ function toJSONSchema(input, params) {
|
|
|
11177
11271
|
const ctx2 = initializeContext({ ...params, processors: allProcessors });
|
|
11178
11272
|
const defs = {};
|
|
11179
11273
|
for (const entry of registry2._idmap.entries()) {
|
|
11180
|
-
const [_,
|
|
11181
|
-
process$1(
|
|
11274
|
+
const [_, schema2] = entry;
|
|
11275
|
+
process$1(schema2, ctx2);
|
|
11182
11276
|
}
|
|
11183
11277
|
const schemas = {};
|
|
11184
11278
|
const external = {
|
|
@@ -11188,9 +11282,9 @@ function toJSONSchema(input, params) {
|
|
|
11188
11282
|
};
|
|
11189
11283
|
ctx2.external = external;
|
|
11190
11284
|
for (const entry of registry2._idmap.entries()) {
|
|
11191
|
-
const [key,
|
|
11192
|
-
extractDefs(ctx2,
|
|
11193
|
-
schemas[key] = finalize(ctx2,
|
|
11285
|
+
const [key, schema2] = entry;
|
|
11286
|
+
extractDefs(ctx2, schema2);
|
|
11287
|
+
schemas[key] = finalize(ctx2, schema2);
|
|
11194
11288
|
}
|
|
11195
11289
|
if (Object.keys(defs).length > 0) {
|
|
11196
11290
|
const defsSegment = ctx2.target === "draft-2020-12" ? "$defs" : "definitions";
|
|
@@ -11814,6 +11908,18 @@ function union(options, params) {
|
|
|
11814
11908
|
...normalizeParams(params)
|
|
11815
11909
|
});
|
|
11816
11910
|
}
|
|
11911
|
+
const ZodDiscriminatedUnion = /* @__PURE__ */ $constructor("ZodDiscriminatedUnion", (inst, def) => {
|
|
11912
|
+
ZodUnion.init(inst, def);
|
|
11913
|
+
$ZodDiscriminatedUnion.init(inst, def);
|
|
11914
|
+
});
|
|
11915
|
+
function discriminatedUnion(discriminator, options, params) {
|
|
11916
|
+
return new ZodDiscriminatedUnion({
|
|
11917
|
+
type: "union",
|
|
11918
|
+
options,
|
|
11919
|
+
discriminator,
|
|
11920
|
+
...normalizeParams(params)
|
|
11921
|
+
});
|
|
11922
|
+
}
|
|
11817
11923
|
const ZodIntersection = /* @__PURE__ */ $constructor("ZodIntersection", (inst, def) => {
|
|
11818
11924
|
$ZodIntersection.init(inst, def);
|
|
11819
11925
|
ZodType.init(inst, def);
|
|
@@ -11895,6 +12001,27 @@ function _enum(values, params) {
|
|
|
11895
12001
|
...normalizeParams(params)
|
|
11896
12002
|
});
|
|
11897
12003
|
}
|
|
12004
|
+
const ZodLiteral = /* @__PURE__ */ $constructor("ZodLiteral", (inst, def) => {
|
|
12005
|
+
$ZodLiteral.init(inst, def);
|
|
12006
|
+
ZodType.init(inst, def);
|
|
12007
|
+
inst._zod.processJSONSchema = (ctx, json2, params) => literalProcessor(inst, ctx, json2);
|
|
12008
|
+
inst.values = new Set(def.values);
|
|
12009
|
+
Object.defineProperty(inst, "value", {
|
|
12010
|
+
get() {
|
|
12011
|
+
if (def.values.length > 1) {
|
|
12012
|
+
throw new Error("This schema contains multiple valid literal values. Use `.values` instead.");
|
|
12013
|
+
}
|
|
12014
|
+
return def.values[0];
|
|
12015
|
+
}
|
|
12016
|
+
});
|
|
12017
|
+
});
|
|
12018
|
+
function literal(value, params) {
|
|
12019
|
+
return new ZodLiteral({
|
|
12020
|
+
type: "literal",
|
|
12021
|
+
values: Array.isArray(value) ? value : [value],
|
|
12022
|
+
...normalizeParams(params)
|
|
12023
|
+
});
|
|
12024
|
+
}
|
|
11898
12025
|
const ZodTransform = /* @__PURE__ */ $constructor("ZodTransform", (inst, def) => {
|
|
11899
12026
|
$ZodTransform.init(inst, def);
|
|
11900
12027
|
ZodType.init(inst, def);
|
|
@@ -12068,7 +12195,7 @@ function superRefine(fn, params) {
|
|
|
12068
12195
|
return /* @__PURE__ */ _superRefine(fn, params);
|
|
12069
12196
|
}
|
|
12070
12197
|
function redactSensitiveText(value) {
|
|
12071
|
-
const withoutHeaders = value.replace(/\b(authorization|cookie|set-cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\b(token|secret|password)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]");
|
|
12198
|
+
const withoutHeaders = value.replace(/\b(?:github_pat_|gh[opusr]_)[A-Za-z0-9_]{16,}/g, "[redacted]").replace(/\bauthorization\s*[:=]\s*(?:bearer|basic)\s+[^\s,;]+/gi, "authorization=[redacted]").replace(/\b(authorization|cookie|set-cookie)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]").replace(/\b(token|secret|password)\s*[:=]\s*[^\s,;]+/gi, "$1=[redacted]");
|
|
12072
12199
|
return withoutHeaders.replace(/https?:\/\/[^\s"'<>]+/gi, (candidate) => redactUrl(candidate));
|
|
12073
12200
|
}
|
|
12074
12201
|
function redactUrl(candidate) {
|
|
@@ -12460,51 +12587,71 @@ function cleanText(value, maxLength) {
|
|
|
12460
12587
|
}
|
|
12461
12588
|
const DEFAULT_MODEL$1 = { id: "default", label: "Default (CLI)" };
|
|
12462
12589
|
class CommitMessageService {
|
|
12463
|
-
constructor(operations, providers, log, invalidateDiscovery) {
|
|
12590
|
+
constructor(operations, providers, log, invalidateDiscovery, statusPersistence) {
|
|
12464
12591
|
this.operations = operations;
|
|
12465
12592
|
this.log = log;
|
|
12466
12593
|
this.invalidateDiscovery = invalidateDiscovery;
|
|
12594
|
+
this.statusPersistence = statusPersistence;
|
|
12467
12595
|
for (const provider of providers) this.providers.set(provider.id, provider);
|
|
12468
12596
|
}
|
|
12469
12597
|
operations;
|
|
12470
12598
|
log;
|
|
12471
12599
|
invalidateDiscovery;
|
|
12600
|
+
statusPersistence;
|
|
12472
12601
|
providers = /* @__PURE__ */ new Map();
|
|
12473
12602
|
active = /* @__PURE__ */ new Map();
|
|
12474
12603
|
statusCache = null;
|
|
12604
|
+
statusInvalidated = false;
|
|
12475
12605
|
statusGeneration = 0;
|
|
12476
12606
|
pendingStatus;
|
|
12477
12607
|
invalidateStatuses() {
|
|
12478
12608
|
this.statusGeneration++;
|
|
12479
12609
|
this.statusCache = null;
|
|
12610
|
+
this.statusInvalidated = true;
|
|
12480
12611
|
this.pendingStatus = void 0;
|
|
12481
12612
|
this.invalidateDiscovery?.();
|
|
12482
12613
|
}
|
|
12483
12614
|
async statuses(forceRefresh = false) {
|
|
12484
|
-
|
|
12485
|
-
if (
|
|
12615
|
+
const key = this.statusPersistence?.key() ?? "";
|
|
12616
|
+
if (!forceRefresh && this.statusCache?.key === key) return this.statusCache.value;
|
|
12617
|
+
if (this.pendingStatus?.key === key && (!forceRefresh || this.pendingStatus.force)) return this.pendingStatus.promise;
|
|
12486
12618
|
const generation = ++this.statusGeneration;
|
|
12487
|
-
const promise =
|
|
12488
|
-
|
|
12489
|
-
|
|
12490
|
-
|
|
12491
|
-
|
|
12492
|
-
|
|
12493
|
-
|
|
12494
|
-
|
|
12495
|
-
|
|
12496
|
-
|
|
12497
|
-
|
|
12498
|
-
|
|
12499
|
-
|
|
12500
|
-
|
|
12501
|
-
|
|
12619
|
+
const promise = (async () => {
|
|
12620
|
+
if (!forceRefresh && !this.statusInvalidated && this.statusPersistence) {
|
|
12621
|
+
const saved = await this.statusPersistence.store.load(key).catch(() => null);
|
|
12622
|
+
if (saved?.length === this.providers.size && saved.every((status) => this.providers.has(status.id))) {
|
|
12623
|
+
if (generation === this.statusGeneration) this.statusCache = { key, value: saved };
|
|
12624
|
+
return saved;
|
|
12625
|
+
}
|
|
12626
|
+
}
|
|
12627
|
+
const value = await Promise.all([...this.providers.values()].map(async (provider) => {
|
|
12628
|
+
try {
|
|
12629
|
+
return { ...await provider.status(forceRefresh), checkedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12630
|
+
} catch {
|
|
12631
|
+
return {
|
|
12632
|
+
id: provider.id,
|
|
12633
|
+
label: { codex: "Codex", claude: "Claude Code", opencode: "OpenCode", grok: "Grok Build" }[provider.id],
|
|
12634
|
+
installed: false,
|
|
12635
|
+
availability: "error",
|
|
12636
|
+
authStatus: "unknown",
|
|
12637
|
+
installationStatus: "inspection-failed",
|
|
12638
|
+
message: "Could not inspect this CLI. Check again or review its executable path.",
|
|
12639
|
+
models: [DEFAULT_MODEL$1],
|
|
12640
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
12641
|
+
};
|
|
12642
|
+
}
|
|
12643
|
+
}));
|
|
12644
|
+
if (generation === this.statusGeneration) {
|
|
12645
|
+
this.statusCache = { key, value };
|
|
12646
|
+
this.statusInvalidated = false;
|
|
12647
|
+
try {
|
|
12648
|
+
await this.statusPersistence?.store.save(key, value);
|
|
12649
|
+
} catch {
|
|
12650
|
+
}
|
|
12502
12651
|
}
|
|
12503
|
-
})).then((value) => {
|
|
12504
|
-
if (generation === this.statusGeneration) this.statusCache = { at: Date.now(), value };
|
|
12505
12652
|
return value;
|
|
12506
|
-
});
|
|
12507
|
-
this.pendingStatus = { force: forceRefresh, promise };
|
|
12653
|
+
})();
|
|
12654
|
+
this.pendingStatus = { force: forceRefresh, key, promise };
|
|
12508
12655
|
try {
|
|
12509
12656
|
return await promise;
|
|
12510
12657
|
} finally {
|
|
@@ -12896,8 +13043,8 @@ function label(harness) {
|
|
|
12896
13043
|
function stripAnsi(value) {
|
|
12897
13044
|
return value.replace(new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`, "g"), "");
|
|
12898
13045
|
}
|
|
12899
|
-
function toCodexOutputSchema(
|
|
12900
|
-
return strictify(structuredClone(
|
|
13046
|
+
function toCodexOutputSchema(schema2) {
|
|
13047
|
+
return strictify(structuredClone(schema2));
|
|
12901
13048
|
}
|
|
12902
13049
|
function strictify(value) {
|
|
12903
13050
|
if (!value || typeof value !== "object") return value;
|
|
@@ -13051,9 +13198,9 @@ class CodexProvider {
|
|
|
13051
13198
|
const failure2 = detectionFailure(detected, { id: this.id, label: "Codex", models: [DEFAULT_MODEL$1], checkedAt });
|
|
13052
13199
|
if (failure2) return failure2;
|
|
13053
13200
|
const executable = requireCandidate(detected, this.id);
|
|
13054
|
-
const
|
|
13055
|
-
if (
|
|
13056
|
-
${
|
|
13201
|
+
const login2 = await runCandidate(this.runner, executable, ["login", "status"]);
|
|
13202
|
+
if (login2.exitCode !== 0 || !/logged in/i.test(`${login2.stdout}
|
|
13203
|
+
${login2.stderr}`)) {
|
|
13057
13204
|
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 };
|
|
13058
13205
|
}
|
|
13059
13206
|
const catalog = await runCandidate(this.runner, executable, ["debug", "models", "--bundled"]);
|
|
@@ -13547,11 +13694,10 @@ class OpenCodeProvider {
|
|
|
13547
13694
|
const executable = requireCandidate(detected, this.id);
|
|
13548
13695
|
const version2 = detected.version ?? "";
|
|
13549
13696
|
const cliName = openCodeCliName(executable.executable);
|
|
13550
|
-
const [auth,
|
|
13697
|
+
const [auth, models] = await Promise.all([
|
|
13551
13698
|
runCandidate(this.runner, executable, ["auth", "list", "--format", "json", "--standalone"]),
|
|
13552
|
-
|
|
13699
|
+
this.loadModels(executable)
|
|
13553
13700
|
]);
|
|
13554
|
-
const models = catalog.exitCode === 0 ? parseOpenCodeModels(catalog.stdout) : [DEFAULT_MODEL$1];
|
|
13555
13701
|
let authStatus = parseOpenCodeAuthList(auth.stdout, auth.exitCode);
|
|
13556
13702
|
if (authStatus === "unauthenticated" && models.length > 1) authStatus = "unknown";
|
|
13557
13703
|
const confirmed = authStatus === "authenticated";
|
|
@@ -13566,9 +13712,20 @@ class OpenCodeProvider {
|
|
|
13566
13712
|
version: version2,
|
|
13567
13713
|
models,
|
|
13568
13714
|
checkedAt,
|
|
13569
|
-
...authStatus === "unauthenticated" ? { message: `Run ${openCodeLoginCommand(cliName)}.` } : !confirmed ? { message: "Could not confirm authentication for the selected provider." } : {}
|
|
13715
|
+
...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." } : {}
|
|
13570
13716
|
};
|
|
13571
13717
|
}
|
|
13718
|
+
async loadModels(executable) {
|
|
13719
|
+
for (const args of [["models", "--standalone"], ["models"]]) {
|
|
13720
|
+
try {
|
|
13721
|
+
const catalog = await runCandidate(this.runner, executable, args, { timeoutMs: 3e4 });
|
|
13722
|
+
const models = catalog.exitCode === 0 ? parseOpenCodeModels(catalog.stdout) : [DEFAULT_MODEL$1];
|
|
13723
|
+
if (models.length > 1) return models;
|
|
13724
|
+
} catch {
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13727
|
+
return [DEFAULT_MODEL$1];
|
|
13728
|
+
}
|
|
13572
13729
|
async generate(input) {
|
|
13573
13730
|
const detected = await this.detect(false, input.signal);
|
|
13574
13731
|
const executable = requireCandidate(detected, this.id);
|
|
@@ -14679,6 +14836,450 @@ class RepositoryWatcher {
|
|
|
14679
14836
|
this.onChange(this.active.id, scope);
|
|
14680
14837
|
}
|
|
14681
14838
|
}
|
|
14839
|
+
const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
14840
|
+
const REPO_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
14841
|
+
function parseGitHubRemote(url) {
|
|
14842
|
+
const trimmed = url.trim();
|
|
14843
|
+
if (!trimmed) return null;
|
|
14844
|
+
let rest = null;
|
|
14845
|
+
const https = /^https?:\/\/(?:[^@/\s]+@)?github\.com\/(.+)$/i.exec(trimmed);
|
|
14846
|
+
const ssh = /^ssh:\/\/(?:[^@/\s]+@)?github\.com(?::\d+)?\/(.+)$/i.exec(trimmed);
|
|
14847
|
+
const scp = /^(?:[^@\s]+)@github\.com:(.+)$/i.exec(trimmed);
|
|
14848
|
+
const git = /^git:\/\/github\.com\/(.+)$/i.exec(trimmed);
|
|
14849
|
+
if (https) rest = https[1] ?? null;
|
|
14850
|
+
else if (ssh) rest = ssh[1] ?? null;
|
|
14851
|
+
else if (scp) rest = scp[1] ?? null;
|
|
14852
|
+
else if (git) rest = git[1] ?? null;
|
|
14853
|
+
if (!rest) return null;
|
|
14854
|
+
const segments = rest.replace(/\/+$/, "").split("/");
|
|
14855
|
+
if (segments.length !== 2) return null;
|
|
14856
|
+
const owner = segments[0] ?? "";
|
|
14857
|
+
const repo = (segments[1] ?? "").replace(/\.git$/i, "");
|
|
14858
|
+
if (!OWNER_PATTERN.test(owner) || !repo || repo === "." || repo === ".." || !REPO_PATTERN.test(repo)) return null;
|
|
14859
|
+
return { nameWithOwner: `${owner}/${repo}` };
|
|
14860
|
+
}
|
|
14861
|
+
function parseSshRemote(url) {
|
|
14862
|
+
const trimmed = url.trim();
|
|
14863
|
+
const ssh = /^ssh:\/\/(?:(\w[\w.-]*)@)?([A-Za-z0-9][A-Za-z0-9._-]*)(?::(\d+))?\/(.+)$/i.exec(trimmed);
|
|
14864
|
+
const scp = /^(?:(\w[\w.-]*)@)?([A-Za-z0-9][A-Za-z0-9._-]*):(.+)$/.exec(trimmed);
|
|
14865
|
+
const match = ssh ?? scp;
|
|
14866
|
+
if (!match) return null;
|
|
14867
|
+
const parsed = parseGitHubRemote(`https://github.com/${ssh ? match[4] : match[3]}`);
|
|
14868
|
+
if (!parsed) return null;
|
|
14869
|
+
return { destination: `${match[1] ? `${match[1]}@` : ""}${match[2]}`, port: ssh ? match[3] ?? null : null, ...parsed };
|
|
14870
|
+
}
|
|
14871
|
+
const GITHUB_LOGIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
14872
|
+
function parseGitHubAuthStatus(raw) {
|
|
14873
|
+
const input = JSON.parse(raw);
|
|
14874
|
+
if (!input || typeof input !== "object" || !("hosts" in input) || !input.hosts || typeof input.hosts !== "object") throw new Error("Invalid gh status");
|
|
14875
|
+
const entries = input.hosts["github.com"] ?? [];
|
|
14876
|
+
if (!Array.isArray(entries) || entries.length > 100) throw new Error("Invalid gh accounts");
|
|
14877
|
+
const accounts = [];
|
|
14878
|
+
for (const entry of entries) {
|
|
14879
|
+
if (!entry || typeof entry !== "object" || typeof entry.login !== "string" || !GITHUB_LOGIN.test(entry.login)) continue;
|
|
14880
|
+
accounts.push({
|
|
14881
|
+
host: "github.com",
|
|
14882
|
+
login: entry.login,
|
|
14883
|
+
active: entry.active === true,
|
|
14884
|
+
state: entry.state === "success" ? "authenticated" : entry.state === "error" ? "invalid" : "unknown",
|
|
14885
|
+
storage: entry.tokenSource === "keyring" ? "keyring" : typeof entry.tokenSource === "string" && (entry.tokenSource === "oauth_token" || /(?:^|[/\\])hosts\.yml$/.test(entry.tokenSource)) ? "file" : "unknown",
|
|
14886
|
+
...typeof entry.scopes === "string" && /^[A-Za-z0-9_:, -]{0,500}$/.test(entry.scopes) ? { scopes: entry.scopes } : {}
|
|
14887
|
+
});
|
|
14888
|
+
}
|
|
14889
|
+
return { accounts, activeLogin: accounts.find((account) => account.active)?.login ?? null };
|
|
14890
|
+
}
|
|
14891
|
+
const GH_ENV = { GH_HOST: "github.com", GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", GH_PAGER: "cat", NO_COLOR: "1" };
|
|
14892
|
+
const GH_PRIVATE_ENV = ["GITHUB_TOKEN", "GH_DEBUG", "DEBUG"];
|
|
14893
|
+
async function identifySshAccount(remote, runner) {
|
|
14894
|
+
const ssh = parseSshRemote(remote);
|
|
14895
|
+
if (!ssh) return null;
|
|
14896
|
+
try {
|
|
14897
|
+
const config2 = await runner.run("ssh", ["-G", ...ssh.port ? ["-p", ssh.port] : [], "--", ssh.destination], {
|
|
14898
|
+
cwd: os.homedir(),
|
|
14899
|
+
timeoutMs: 5e3,
|
|
14900
|
+
maxOutputBytes: 64 * 1024
|
|
14901
|
+
});
|
|
14902
|
+
const hostname = config2.stdout.split(/\r?\n/).find((line) => /^hostname\s/i.test(line))?.trim().split(/\s+/)[1];
|
|
14903
|
+
if (config2.exitCode !== 0 || hostname?.toLowerCase() !== "github.com") return null;
|
|
14904
|
+
const result = await runner.run("ssh", [
|
|
14905
|
+
"-T",
|
|
14906
|
+
"-o",
|
|
14907
|
+
"BatchMode=yes",
|
|
14908
|
+
"-o",
|
|
14909
|
+
"StrictHostKeyChecking=yes",
|
|
14910
|
+
"-o",
|
|
14911
|
+
"ConnectTimeout=5",
|
|
14912
|
+
"-o",
|
|
14913
|
+
"ConnectionAttempts=1",
|
|
14914
|
+
"-o",
|
|
14915
|
+
"RemoteCommand=none",
|
|
14916
|
+
"-o",
|
|
14917
|
+
"UpdateHostKeys=no",
|
|
14918
|
+
...ssh.port ? ["-p", ssh.port] : [],
|
|
14919
|
+
"--",
|
|
14920
|
+
ssh.destination
|
|
14921
|
+
], { cwd: os.homedir(), timeoutMs: 1e4, maxOutputBytes: 64 * 1024 });
|
|
14922
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) return null;
|
|
14923
|
+
const login2 = /^Hi ([A-Za-z0-9-]+)! You've successfully authenticated, but GitHub does not provide shell access\.\s*$/m.exec(`${result.stdout}
|
|
14924
|
+
${result.stderr}`)?.[1];
|
|
14925
|
+
return login2 && GITHUB_LOGIN.test(login2) ? login2 : null;
|
|
14926
|
+
} catch {
|
|
14927
|
+
return null;
|
|
14928
|
+
}
|
|
14929
|
+
}
|
|
14930
|
+
async function accountAuthentication(login2, executable, runner, operation) {
|
|
14931
|
+
try {
|
|
14932
|
+
const result = await runner.run(executable.executable, ["auth", "token", "--hostname", "github.com", ...login2 ? ["--user", login2] : []], {
|
|
14933
|
+
cwd: os.homedir(),
|
|
14934
|
+
env: { ...executable.env, ...GH_ENV },
|
|
14935
|
+
removeEnv: login2 ? ["GH_TOKEN", ...GH_PRIVATE_ENV] : ["GH_DEBUG", "DEBUG"],
|
|
14936
|
+
timeoutMs: 1e4,
|
|
14937
|
+
maxOutputBytes: 8 * 1024
|
|
14938
|
+
});
|
|
14939
|
+
const token = result.stdout.trim();
|
|
14940
|
+
if (result.exitCode === 0 && /^[A-Za-z0-9_]{20,4096}$/.test(token)) return { env: { GH_TOKEN: token }, removeEnv: GH_PRIVATE_ENV };
|
|
14941
|
+
if (/unknown flag.*user/i.test(result.stderr)) throw new GhOperationError({
|
|
14942
|
+
code: "GH_CLI_INCOMPATIBLE",
|
|
14943
|
+
operation,
|
|
14944
|
+
message: "Update GitHub CLI to a version supporting multiple accounts and auth token --user."
|
|
14945
|
+
});
|
|
14946
|
+
} catch (error) {
|
|
14947
|
+
if (error instanceof GhOperationError) throw error;
|
|
14948
|
+
}
|
|
14949
|
+
throw new GhOperationError({
|
|
14950
|
+
code: login2 ? "GH_ACCOUNT_MISSING" : "GH_AUTH_REQUIRED",
|
|
14951
|
+
operation,
|
|
14952
|
+
message: login2 ? `GitHub CLI has no usable saved credentials for @${login2}. Sign in as ${login2} on the backend host and check Settings → GitHub again. SSH and GitHub CLI authentication are separate.` : "GitHub CLI has no usable credentials. Sign in on the backend host and check Settings → GitHub again."
|
|
14953
|
+
});
|
|
14954
|
+
}
|
|
14955
|
+
function redactCommandToken(value, auth) {
|
|
14956
|
+
const token = auth.env?.GH_TOKEN;
|
|
14957
|
+
return token ? value.split(token).join("[redacted]") : value;
|
|
14958
|
+
}
|
|
14959
|
+
function gitShellQuote(value) {
|
|
14960
|
+
return `'${value.replace(/\\/g, "/").replace(/'/g, `'\\''`)}'`;
|
|
14961
|
+
}
|
|
14962
|
+
function managedGitHubPath(value) {
|
|
14963
|
+
try {
|
|
14964
|
+
const url = new URL(value);
|
|
14965
|
+
if (!/^https:\/\/github\.com\//i.test(value) || url.protocol !== "https:" || url.hostname !== "github.com" || url.port || url.username || url.password || url.search || url.hash) return null;
|
|
14966
|
+
if (!/^\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(url.pathname)) return null;
|
|
14967
|
+
return url.pathname.slice(1);
|
|
14968
|
+
} catch {
|
|
14969
|
+
return null;
|
|
14970
|
+
}
|
|
14971
|
+
}
|
|
14972
|
+
async function createCredentialBroker(url, login2, token) {
|
|
14973
|
+
const allowedPath = managedGitHubPath(url);
|
|
14974
|
+
if (!allowedPath) throw new Error("Managed authentication requires one plain github.com HTTPS repository URL.");
|
|
14975
|
+
const directory = await mkdtemp(path.join(os.tmpdir(), "opentig-git-auth-"));
|
|
14976
|
+
const key = randomBytes(32).toString("hex");
|
|
14977
|
+
const server = createServer$1((request, response) => {
|
|
14978
|
+
response.setHeader("Connection", "close");
|
|
14979
|
+
if (request.method !== "POST" || request.url !== "/" || request.headers["x-opentig-key"] !== key) {
|
|
14980
|
+
response.writeHead(403).end();
|
|
14981
|
+
return;
|
|
14982
|
+
}
|
|
14983
|
+
let body = "";
|
|
14984
|
+
request.setTimeout(5e3, () => request.destroy());
|
|
14985
|
+
request.on("data", (data) => {
|
|
14986
|
+
body += data.toString("utf8");
|
|
14987
|
+
if (body.length > 8192) request.destroy();
|
|
14988
|
+
});
|
|
14989
|
+
request.on("end", () => {
|
|
14990
|
+
const fields = body.trim().split("\n").map((line) => line.replace(/\r$/, "").split(/=(.*)/s));
|
|
14991
|
+
const value = Object.fromEntries(fields.map(([name, field]) => [name, field]));
|
|
14992
|
+
const unique = ["protocol", "host", "path"].every((name) => fields.filter(([key2]) => key2 === name).length === 1);
|
|
14993
|
+
if (!unique || value.protocol !== "https" || value.host !== "github.com" || value.path !== allowedPath) {
|
|
14994
|
+
response.writeHead(403).end();
|
|
14995
|
+
return;
|
|
14996
|
+
}
|
|
14997
|
+
response.end(`username=${login2}
|
|
14998
|
+
password=${token}
|
|
14999
|
+
|
|
15000
|
+
`);
|
|
15001
|
+
});
|
|
15002
|
+
});
|
|
15003
|
+
const dispose = async () => {
|
|
15004
|
+
server.closeAllConnections();
|
|
15005
|
+
if (server.listening) await new Promise((resolve2) => server.close(() => resolve2()));
|
|
15006
|
+
await rm(directory, { recursive: true, force: true });
|
|
15007
|
+
};
|
|
15008
|
+
try {
|
|
15009
|
+
server.listen(0, "127.0.0.1");
|
|
15010
|
+
await once(server, "listening");
|
|
15011
|
+
const port = server.address().port;
|
|
15012
|
+
const headers = path.join(directory, "headers");
|
|
15013
|
+
const helper = path.join(directory, "helper.sh");
|
|
15014
|
+
await writeFile(headers, `X-OpenTig-Key: ${key}
|
|
15015
|
+
`, { mode: 384 });
|
|
15016
|
+
await writeFile(helper, `#!/bin/sh
|
|
15017
|
+
[ "$1" = get ] || exit 0
|
|
15018
|
+
curl --disable --silent --fail --noproxy '*' --max-time 10 --proto '=http' --header @${gitShellQuote(headers)} --data-binary @- http://127.0.0.1:${port}/ || exit 1
|
|
15019
|
+
`, { mode: 448 });
|
|
15020
|
+
return { helper: `!sh ${gitShellQuote(helper)}`, directory, dispose };
|
|
15021
|
+
} catch (error) {
|
|
15022
|
+
await dispose();
|
|
15023
|
+
throw error;
|
|
15024
|
+
}
|
|
15025
|
+
}
|
|
15026
|
+
const read$1 = { operation: "repository-access", readOnly: true, maxOutputBytes: 1024 * 1024 };
|
|
15027
|
+
const unchecked = () => ({ state: "unchecked" });
|
|
15028
|
+
const digest = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
15029
|
+
class RepositoryGitAccess {
|
|
15030
|
+
constructor(git, repositories, accounts, runner) {
|
|
15031
|
+
this.git = git;
|
|
15032
|
+
this.repositories = repositories;
|
|
15033
|
+
this.accounts = accounts;
|
|
15034
|
+
this.runner = runner;
|
|
15035
|
+
}
|
|
15036
|
+
git;
|
|
15037
|
+
repositories;
|
|
15038
|
+
accounts;
|
|
15039
|
+
runner;
|
|
15040
|
+
sessions = new AsyncLocalStorage();
|
|
15041
|
+
checks = /* @__PURE__ */ new Map();
|
|
15042
|
+
latest = /* @__PURE__ */ new Map();
|
|
15043
|
+
install() {
|
|
15044
|
+
this.git.network = (cwd, args, options, execute) => this.scope(cwd, () => this.execute(args, options, execute));
|
|
15045
|
+
this.git.authenticationKey = (cwd) => {
|
|
15046
|
+
const repository = this.repositories.recents().find((item) => item.path === cwd);
|
|
15047
|
+
return repository ? digest([this.accounts.selection(repository), this.accounts.revisionFor(repository)]) : "";
|
|
15048
|
+
};
|
|
15049
|
+
this.git.networkScope = (cwd, task) => this.scope(cwd, task);
|
|
15050
|
+
}
|
|
15051
|
+
async scope(cwd, task) {
|
|
15052
|
+
if (this.sessions.getStore()?.repository.path === cwd) return task();
|
|
15053
|
+
const repository = this.repositories.recents().find((item) => item.path === cwd);
|
|
15054
|
+
if (!repository) throw new Error("The repository must be open before remote operations.");
|
|
15055
|
+
return this.sessions.run({ repository, selection: this.accounts.selection(repository), revision: this.accounts.revisionFor(repository) }, task);
|
|
15056
|
+
}
|
|
15057
|
+
async config(repository, key, all = false) {
|
|
15058
|
+
try {
|
|
15059
|
+
return (await this.git.run(repository.path, ["config", all ? "--get-all" : "--get", key], read$1)).stdout.toString("utf8").trim();
|
|
15060
|
+
} catch (error) {
|
|
15061
|
+
if (error instanceof GitOperationError && error.detail.exitCode === 1) return "";
|
|
15062
|
+
throw error;
|
|
15063
|
+
}
|
|
15064
|
+
}
|
|
15065
|
+
async resolve(repository, push, selected) {
|
|
15066
|
+
const branch = (await this.git.run(repository.path, ["branch", "--show-current"], read$1)).stdout.toString("utf8").trim() || null;
|
|
15067
|
+
const remotes = (await this.git.run(repository.path, ["remote"], read$1)).stdout.toString("utf8").trim().split(/\r?\n/).filter(Boolean);
|
|
15068
|
+
let remote = selected;
|
|
15069
|
+
if (!remote) for (const key of [...push ? [`branch.${branch}.pushRemote`, "remote.pushDefault"] : [], `branch.${branch}.remote`]) {
|
|
15070
|
+
remote = await this.config(repository, key);
|
|
15071
|
+
if (remote) break;
|
|
15072
|
+
}
|
|
15073
|
+
if (!remote && !push && remotes.includes("origin")) remote = "origin";
|
|
15074
|
+
remote ||= remotes.length === 1 ? remotes[0] : void 0;
|
|
15075
|
+
let urls = [];
|
|
15076
|
+
if (remote === ".") urls = ["."];
|
|
15077
|
+
else if (remote && remotes.includes(remote)) urls = (await this.git.run(repository.path, ["remote", "get-url", ...push ? ["--push", "--all"] : [], "--", remote], read$1)).stdout.toString("utf8").trim().split(/\r?\n/).filter(Boolean);
|
|
15078
|
+
else if (selected && !remotes.includes(selected)) {
|
|
15079
|
+
urls = [(await this.git.run(repository.path, ["ls-remote", "--get-url", "--", selected], { ...read$1, operation: "resolve-git-url" })).stdout.toString("utf8").trim()];
|
|
15080
|
+
}
|
|
15081
|
+
return { branch, remotes, remote: remote ?? null, urls };
|
|
15082
|
+
}
|
|
15083
|
+
async publication(repositoryId, selected) {
|
|
15084
|
+
const repository = this.repositories.get(repositoryId);
|
|
15085
|
+
const selection = this.accounts.selection(repository);
|
|
15086
|
+
const revision2 = this.accounts.revisionFor(repository);
|
|
15087
|
+
const target = await this.resolve(repository, true, selected);
|
|
15088
|
+
let oid = null;
|
|
15089
|
+
try {
|
|
15090
|
+
oid = (await this.git.run(repository.path, ["rev-parse", "--verify", "HEAD"], read$1)).stdout.toString("utf8").trim();
|
|
15091
|
+
} catch {
|
|
15092
|
+
}
|
|
15093
|
+
const managed = selection.mode === "account" && selection.gitMode === "managed";
|
|
15094
|
+
const compatible = target.urls.length === 1 && !!managedGitHubPath(target.urls[0]);
|
|
15095
|
+
const configuration = (await this.git.run(repository.path, ["config", "--null", "--list"], read$1)).stdout.toString("utf8");
|
|
15096
|
+
const upstreamRemote = await this.config(repository, `branch.${target.branch}.remote`);
|
|
15097
|
+
const merge2 = await this.config(repository, `branch.${target.branch}.merge`, true);
|
|
15098
|
+
const pushDefault = await this.config(repository, "push.default") || "simple";
|
|
15099
|
+
const targetRef = target.branch ? target.remote === upstreamRemote && merge2 && ["simple", "upstream"].includes(pushDefault) ? merge2 : `refs/heads/${target.branch}` : null;
|
|
15100
|
+
const unsafe = merge2 && (merge2.includes("\n") || !["simple", "upstream", "current"].includes(pushDefault) || pushDefault === "simple" && target.remote === upstreamRemote && merge2 !== `refs/heads/${target.branch}` || pushDefault === "upstream" && target.remote !== upstreamRemote || await this.config(repository, `remote.${target.remote}.push`) || await this.config(repository, `remote.${target.remote}.mirror`) === "true");
|
|
15101
|
+
const blocked = selected && selected !== "." && !target.remotes.includes(selected) ? "The selected push remote is not configured. Review the repository remotes." : unsafe ? "The push refspec, mirror or push.default configuration requires review in Git before publishing." : target.urls.length > 1 ? "This remote has multiple push URLs. Choose a single destination in Git configuration before publishing." : target.remote && !target.urls.length ? "The configured push remote is unavailable." : managed && target.urls.some(isGitHubHttp) && !compatible ? "Managed Git requires a plain github.com HTTPS destination. Review the URL or use external authentication." : void 0;
|
|
15102
|
+
return {
|
|
15103
|
+
...target,
|
|
15104
|
+
urls: target.urls.map(redactSensitiveText),
|
|
15105
|
+
repositoryId,
|
|
15106
|
+
oid,
|
|
15107
|
+
targetRef,
|
|
15108
|
+
id: digest([repository.commonDir, selection, revision2, target, oid, configuration]),
|
|
15109
|
+
mode: managed && compatible ? "managed" : "external",
|
|
15110
|
+
login: managed && compatible && selection.mode === "account" ? selection.login : null,
|
|
15111
|
+
...blocked ? { blocked } : {}
|
|
15112
|
+
};
|
|
15113
|
+
}
|
|
15114
|
+
async assertPublication(repositoryId, expected, remote) {
|
|
15115
|
+
const current = await this.publication(repositoryId, remote);
|
|
15116
|
+
if (expected && expected !== current.id) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "push", message: "The account, branch or destination changed. Review the publication context and try again." });
|
|
15117
|
+
if (current.blocked) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "push", message: current.blocked });
|
|
15118
|
+
return current;
|
|
15119
|
+
}
|
|
15120
|
+
async status(repositoryId, force = false, apiContext) {
|
|
15121
|
+
const publication = await this.publication(repositoryId);
|
|
15122
|
+
const repository = this.repositories.get(repositoryId);
|
|
15123
|
+
const previous = this.checks.get(repositoryId);
|
|
15124
|
+
const value = previous?.publication.id === publication.id ? structuredClone(previous) : {
|
|
15125
|
+
publication,
|
|
15126
|
+
identity: unchecked(),
|
|
15127
|
+
api: unchecked(),
|
|
15128
|
+
read: publication.mode === "external" ? { state: "external" } : unchecked(),
|
|
15129
|
+
write: unchecked(),
|
|
15130
|
+
checkedAt: null
|
|
15131
|
+
};
|
|
15132
|
+
const last = this.latest.get(repositoryId);
|
|
15133
|
+
if (last) value.lastOperation = last;
|
|
15134
|
+
if (!force) {
|
|
15135
|
+
if (value.checkedAt && Date.now() - Date.parse(value.checkedAt) > 6e4) for (const key of ["identity", "api", "read", "write"]) {
|
|
15136
|
+
if (value[key].state === "ok") value[key] = { state: "stale", message: value[key].message ?? "Check access again for a current result." };
|
|
15137
|
+
}
|
|
15138
|
+
return value;
|
|
15139
|
+
}
|
|
15140
|
+
value.identity = unchecked();
|
|
15141
|
+
value.api = unchecked();
|
|
15142
|
+
value.write = unchecked();
|
|
15143
|
+
value.read = publication.mode === "external" ? { state: "external" } : unchecked();
|
|
15144
|
+
const selection = this.accounts.selection(repository);
|
|
15145
|
+
if (selection.mode !== "account") {
|
|
15146
|
+
value.identity = { state: "unchecked", message: "Choose a saved account to pin access for this repository." };
|
|
15147
|
+
} else {
|
|
15148
|
+
try {
|
|
15149
|
+
this.accounts.refreshAuthentication();
|
|
15150
|
+
const { auth, executable } = await this.accounts.authenticate(repository, apiContext?.remote ?? "", apiContext?.nameWithOwner ?? "", "repository-access");
|
|
15151
|
+
value.identity = { state: "ok" };
|
|
15152
|
+
const origin = await this.resolve(repository, false, "origin");
|
|
15153
|
+
const apiUrl = origin.urls[0] ?? "";
|
|
15154
|
+
const apiPath = apiContext?.nameWithOwner ?? managedGitHubPath(apiUrl) ?? /^git@github\.com:([\w.-]+\/[\w.-]+)$/.exec(apiUrl)?.[1];
|
|
15155
|
+
const checkApi = async (name) => {
|
|
15156
|
+
try {
|
|
15157
|
+
const result = await this.runner.run(executable.executable, ["api", `repos/${name.replace(/\.git$/, "")}`], {
|
|
15158
|
+
cwd: repository.path,
|
|
15159
|
+
env: { ...executable.env, ...GH_ENV, ...auth.env },
|
|
15160
|
+
...auth.removeEnv ? { removeEnv: auth.removeEnv } : {},
|
|
15161
|
+
timeoutMs: 15e3,
|
|
15162
|
+
maxOutputBytes: 128 * 1024
|
|
15163
|
+
});
|
|
15164
|
+
if (result.exitCode !== 0) {
|
|
15165
|
+
const check = accessError(redactCommandToken(result.stderr, auth));
|
|
15166
|
+
return { check, write: check };
|
|
15167
|
+
}
|
|
15168
|
+
const data = JSON.parse(result.stdout);
|
|
15169
|
+
return { check: { state: "ok" }, write: typeof data.permissions?.push === "boolean" ? { state: data.permissions.push ? "ok" : "inaccessible", message: data.permissions.push ? "Write permission declared by GitHub; branch policies still apply." : "GitHub does not declare write permission." } : unchecked() };
|
|
15170
|
+
} catch (error) {
|
|
15171
|
+
const check = accessError(redactCommandToken(messageOf(error), auth));
|
|
15172
|
+
return { check, write: check };
|
|
15173
|
+
}
|
|
15174
|
+
};
|
|
15175
|
+
if (apiPath) value.api = (await checkApi(apiPath)).check;
|
|
15176
|
+
const destination = publication.urls[0];
|
|
15177
|
+
const pushPath = destination && managedGitHubPath(destination);
|
|
15178
|
+
if (pushPath) value.write = (await checkApi(pushPath)).write;
|
|
15179
|
+
if (publication.mode === "managed" && destination && !publication.blocked) {
|
|
15180
|
+
try {
|
|
15181
|
+
await this.git.withNetworkContext(repository.path, () => this.git.run(repository.path, ["ls-remote", "--", destination, "HEAD"], { ...read$1, timeoutMs: 3e4 }));
|
|
15182
|
+
value.read = { state: "ok", message: "Read verified at the push destination. This does not guarantee a push." };
|
|
15183
|
+
} catch (error) {
|
|
15184
|
+
value.read = accessError(messageOf(error));
|
|
15185
|
+
}
|
|
15186
|
+
}
|
|
15187
|
+
} catch (error) {
|
|
15188
|
+
if (value.identity.state === "ok") value.api = accessError(messageOf(error));
|
|
15189
|
+
else value.identity = accessError(messageOf(error));
|
|
15190
|
+
}
|
|
15191
|
+
}
|
|
15192
|
+
if ((await this.publication(repositoryId)).id !== publication.id) return this.status(repositoryId, false, apiContext);
|
|
15193
|
+
value.checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15194
|
+
this.checks.set(repositoryId, value);
|
|
15195
|
+
return value;
|
|
15196
|
+
}
|
|
15197
|
+
async execute(args, options, execute) {
|
|
15198
|
+
const session = this.sessions.getStore();
|
|
15199
|
+
const { repository, selection } = session;
|
|
15200
|
+
let index = 0;
|
|
15201
|
+
while (args[index] === "-c") index += 2;
|
|
15202
|
+
const command2 = args[index];
|
|
15203
|
+
if (command2 === "ls-remote" && args.includes("--get-url")) return execute(args, options);
|
|
15204
|
+
const selected = args.slice(index + 1).find((arg) => !arg.startsWith("-"));
|
|
15205
|
+
const target = await this.resolve(repository, command2 === "push", selected);
|
|
15206
|
+
if (target.urls.length !== 1) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: options.operation, message: "The remote destination is ambiguous. Configure or choose a single remote URL." });
|
|
15207
|
+
const url = target.urls[0];
|
|
15208
|
+
if (selected?.includes("://") && selected !== url) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: options.operation, message: "URL rewriting changes the reviewed destination again. Resolve the chained rewrite before checking or publishing." });
|
|
15209
|
+
const managed = selection.mode === "account" && selection.gitMode === "managed" && isGitHubHttp(url);
|
|
15210
|
+
let broker;
|
|
15211
|
+
let token = "";
|
|
15212
|
+
try {
|
|
15213
|
+
if (managed) {
|
|
15214
|
+
if (!managedGitHubPath(url)) throw new Error("Managed Git cannot send credentials to this destination. Use external authentication for unsupported URLs.");
|
|
15215
|
+
if (!session.auth) {
|
|
15216
|
+
if (session.revision !== this.accounts.revisionFor(repository)) throw new Error("The repository account changed before the operation started. Review and retry.");
|
|
15217
|
+
session.auth = await this.accounts.authenticate(repository, url, managedGitHubPath(url), options.operation);
|
|
15218
|
+
if (session.revision !== this.accounts.revisionFor(repository) || digest(session.auth.context.selection) !== digest(selection)) throw new Error("The repository account changed before authentication completed. Review and retry.");
|
|
15219
|
+
}
|
|
15220
|
+
if (command2 === "fetch" && !selected && target.remote) args = [...args, "--", target.remote];
|
|
15221
|
+
token = session.auth.auth.env?.GH_TOKEN ?? "";
|
|
15222
|
+
broker = await createCredentialBroker(url, selection.login, token);
|
|
15223
|
+
const config2 = (await this.git.run(repository.path, ["config", "--name-only", "--list"], read$1)).stdout.toString("utf8").trim().split(/\r?\n/);
|
|
15224
|
+
const overrides = [...new Set(config2)].flatMap((key) => {
|
|
15225
|
+
if (/^credential\..*helper$/i.test(key)) return ["-c", `${key}=`];
|
|
15226
|
+
if (/^http\..*extraheader$/i.test(key)) return ["-c", `${key}=`];
|
|
15227
|
+
if (/^http\..*followredirects$/i.test(key)) return ["-c", `${key}=false`];
|
|
15228
|
+
if (/^http\..*sslverify$/i.test(key)) return ["-c", `${key}=true`];
|
|
15229
|
+
if (/^credential\..*usehttppath$/i.test(key)) return ["-c", `${key}=true`];
|
|
15230
|
+
return [];
|
|
15231
|
+
});
|
|
15232
|
+
args = [
|
|
15233
|
+
...overrides,
|
|
15234
|
+
"-c",
|
|
15235
|
+
"credential.helper=",
|
|
15236
|
+
"-c",
|
|
15237
|
+
`credential.helper=${broker.helper}`,
|
|
15238
|
+
"-c",
|
|
15239
|
+
"credential.useHttpPath=true",
|
|
15240
|
+
"-c",
|
|
15241
|
+
"core.askPass=",
|
|
15242
|
+
"-c",
|
|
15243
|
+
"http.extraHeader=",
|
|
15244
|
+
"-c",
|
|
15245
|
+
"http.followRedirects=false",
|
|
15246
|
+
"-c",
|
|
15247
|
+
"http.sslVerify=true",
|
|
15248
|
+
"-c",
|
|
15249
|
+
"fetch.recurseSubmodules=false",
|
|
15250
|
+
"-c",
|
|
15251
|
+
"push.recurseSubmodules=no",
|
|
15252
|
+
...args
|
|
15253
|
+
];
|
|
15254
|
+
}
|
|
15255
|
+
if (options.publication) await this.assertPublication(options.publication.repositoryId, options.publication.id, options.publication.remote);
|
|
15256
|
+
const result = await execute(args, { ...options, ...managed ? { managed: true, managedSecrets: [token] } : {} });
|
|
15257
|
+
this.latest.set(repository.id, { operation: options.operation, url: redactSensitiveText(url), login: managed && selection.mode === "account" ? selection.login : null, at: (/* @__PURE__ */ new Date()).toISOString(), ok: true });
|
|
15258
|
+
return result;
|
|
15259
|
+
} catch (error) {
|
|
15260
|
+
const message = redactSensitiveText(token ? messageOf(error).split(token).join("[redacted]") : messageOf(error));
|
|
15261
|
+
this.latest.set(repository.id, { operation: options.operation, url: redactSensitiveText(url), login: managed && selection.mode === "account" ? selection.login : null, at: (/* @__PURE__ */ new Date()).toISOString(), ok: false, message });
|
|
15262
|
+
if (error instanceof GitOperationError) throw error;
|
|
15263
|
+
throw new GitOperationError({ code: "UNKNOWN", operation: options.operation, message });
|
|
15264
|
+
} finally {
|
|
15265
|
+
await broker?.dispose();
|
|
15266
|
+
}
|
|
15267
|
+
}
|
|
15268
|
+
}
|
|
15269
|
+
function messageOf(error) {
|
|
15270
|
+
return error instanceof Error ? error.message : "Could not check remote access.";
|
|
15271
|
+
}
|
|
15272
|
+
function accessError(message) {
|
|
15273
|
+
return { state: /401|bad credentials|expired|revoked|no usable saved credentials/i.test(message) ? "expired" : /404|403|not found|denied|not accessible/i.test(message) ? "inaccessible" : "offline", message: redactSensitiveText(message).slice(0, 1500) };
|
|
15274
|
+
}
|
|
15275
|
+
function isGitHubHttp(value) {
|
|
15276
|
+
try {
|
|
15277
|
+
const url = new URL(value);
|
|
15278
|
+
return ["http:", "https:"].includes(url.protocol) && url.hostname === "github.com";
|
|
15279
|
+
} catch {
|
|
15280
|
+
return false;
|
|
15281
|
+
}
|
|
15282
|
+
}
|
|
14682
15283
|
function normalizeLockKey(value) {
|
|
14683
15284
|
const unified = value.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
14684
15285
|
return process.platform === "win32" ? unified.toLowerCase() : unified;
|
|
@@ -14689,7 +15290,21 @@ class GitProcess {
|
|
|
14689
15290
|
activeControllers = /* @__PURE__ */ new Set();
|
|
14690
15291
|
idleWaiters = /* @__PURE__ */ new Set();
|
|
14691
15292
|
closePromise = null;
|
|
15293
|
+
authenticationKey;
|
|
15294
|
+
network;
|
|
15295
|
+
networkScope;
|
|
15296
|
+
withNetworkContext(cwd, task) {
|
|
15297
|
+
return this.networkScope ? this.networkScope(cwd, task) : task();
|
|
15298
|
+
}
|
|
14692
15299
|
run(cwd, args, options) {
|
|
15300
|
+
let index = 0;
|
|
15301
|
+
while (args[index] === "-c") index += 2;
|
|
15302
|
+
if (this.network && ["fetch", "push", "ls-remote"].includes(args[index] ?? "")) {
|
|
15303
|
+
return this.network(cwd, args, options, (next, settings) => this.runDirect(cwd, next, settings));
|
|
15304
|
+
}
|
|
15305
|
+
return this.runDirect(cwd, args, options);
|
|
15306
|
+
}
|
|
15307
|
+
runDirect(cwd, args, options) {
|
|
14693
15308
|
const globalArgs = ["--no-pager"];
|
|
14694
15309
|
if (options.readOnly) globalArgs.push("--no-optional-locks");
|
|
14695
15310
|
return this.spawnGit(cwd, [...globalArgs, ...args], options);
|
|
@@ -14745,6 +15360,10 @@ class GitProcess {
|
|
|
14745
15360
|
LC_ALL: "C"
|
|
14746
15361
|
}).filter((entry) => entry[1] !== void 0)
|
|
14747
15362
|
);
|
|
15363
|
+
if (options.managed) for (const key of Object.keys(environment)) {
|
|
15364
|
+
if (/^(GH_TOKEN|GITHUB_TOKEN|GH_DEBUG|SSLKEYLOGFILE|GIT_SSL_NO_VERIFY|GIT_ASKPASS|SSH_ASKPASS|GIT_CONFIG.*|GIT_TRACE.*|GIT_CURL_VERBOSE)$/i.test(key)) delete environment[key];
|
|
15365
|
+
}
|
|
15366
|
+
const sanitize = (value) => redactSensitiveText((options.managedSecrets ?? []).reduce((text2, secret) => text2.split(secret).join("[redacted]"), value));
|
|
14748
15367
|
const resolvedCommand = resolveProcessCommand("git", cwd, environment);
|
|
14749
15368
|
const outputLimit = new AbortController();
|
|
14750
15369
|
const lifecycle = new AbortController();
|
|
@@ -14784,8 +15403,8 @@ class GitProcess {
|
|
|
14784
15403
|
childPid = subprocess.pid;
|
|
14785
15404
|
if (childPid) this.activeChildren.add(childPid);
|
|
14786
15405
|
const result = await subprocess;
|
|
14787
|
-
const stdout = Buffer.from(result.stdout);
|
|
14788
|
-
const stderr = Buffer.from(result.stderr);
|
|
15406
|
+
const stdout = options.managed ? Buffer.from(sanitize(Buffer.from(result.stdout).toString("utf8"))) : Buffer.from(result.stdout);
|
|
15407
|
+
const stderr = Buffer.from(sanitize(Buffer.from(result.stderr).toString("utf8")));
|
|
14789
15408
|
if (overLimit || result.isMaxBuffer) {
|
|
14790
15409
|
if (options.truncateOverflow) return { stdout, stderr, exitCode: 0, truncated: true };
|
|
14791
15410
|
throw new GitOperationError({ code: "OUTPUT_LIMIT", operation: options.operation, message: "Git output exceeded the safety limit." });
|
|
@@ -14796,11 +15415,11 @@ class GitProcess {
|
|
|
14796
15415
|
if (result.exitCode === void 0 || !resolvedCommand.found) {
|
|
14797
15416
|
throw this.mapError(options.operation, Object.assign(new Error("Git was not found in PATH."), { code: "ENOENT" }));
|
|
14798
15417
|
}
|
|
14799
|
-
if (result.exitCode !== 0) throw this.fromExit(options.operation, result.exitCode, stderr.toString("utf8"));
|
|
15418
|
+
if (result.exitCode !== 0) throw this.fromExit(options.operation, result.exitCode, sanitize(`${stderr.toString("utf8")}${options.operation.includes("push") || options.operation === "publish-branch" ? "\n" + stdout.toString("utf8") : ""}`));
|
|
14800
15419
|
return { stdout, stderr, exitCode: result.exitCode };
|
|
14801
15420
|
} catch (error) {
|
|
14802
15421
|
if (error instanceof GitOperationError) throw error;
|
|
14803
|
-
throw this.mapError(options.operation, error);
|
|
15422
|
+
throw this.mapError(options.operation, Object.assign(new Error(sanitize(error instanceof Error ? error.message : "Could not start Git.")), { code: error?.code }));
|
|
14804
15423
|
} finally {
|
|
14805
15424
|
if (childPid) this.activeChildren.delete(childPid);
|
|
14806
15425
|
this.activeControllers.delete(lifecycle);
|
|
@@ -15140,47 +15759,381 @@ function parseWorktrees(buffer) {
|
|
|
15140
15759
|
if (current) worktrees.push(current);
|
|
15141
15760
|
return worktrees;
|
|
15142
15761
|
}
|
|
15143
|
-
const
|
|
15144
|
-
|
|
15145
|
-
|
|
15146
|
-
|
|
15762
|
+
const commitAuthorshipInputSchema = object({
|
|
15763
|
+
name: string().trim().min(1).max(120).regex(/^[^<>]+$/).refine(noControls),
|
|
15764
|
+
email: string().trim().max(254).regex(/^[^\s<>@]+@[^\s<>@]+$/).refine(noControls),
|
|
15765
|
+
expectedRevision: string().regex(/^[a-f0-9]{64}$/)
|
|
15766
|
+
});
|
|
15767
|
+
function noControls(value) {
|
|
15768
|
+
return [...value].every((character) => character.charCodeAt(0) >= 32 && character.charCodeAt(0) !== 127);
|
|
15769
|
+
}
|
|
15770
|
+
const environmentKeys = ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GIT_COMMITTER_NAME", "GIT_COMMITTER_EMAIL", "EMAIL"];
|
|
15771
|
+
const identityPattern = "^(user|author|committer)\\.(name|email)$";
|
|
15772
|
+
class GitCommitAuthorship {
|
|
15773
|
+
constructor(git, repositories) {
|
|
15147
15774
|
this.git = git;
|
|
15148
15775
|
this.repositories = repositories;
|
|
15149
|
-
this.files = files;
|
|
15150
15776
|
}
|
|
15151
15777
|
git;
|
|
15152
15778
|
repositories;
|
|
15153
|
-
|
|
15154
|
-
|
|
15155
|
-
async diff(request) {
|
|
15156
|
-
const repository = this.repositories.get(request.repositoryId);
|
|
15157
|
-
const paths = this.repositories.validatePaths(request.repositoryId, [request.path]);
|
|
15158
|
-
const tracked = (await this.git.run(
|
|
15159
|
-
repository.path,
|
|
15160
|
-
["--literal-pathspecs", "ls-files", "-z", "--", ...paths],
|
|
15161
|
-
{ operation: "diff-tracked-check", readOnly: true, maxOutputBytes: 1024 * 1024 }
|
|
15162
|
-
)).stdout.length > 0;
|
|
15163
|
-
if (request.kind === "unstaged" && !tracked) return this.untrackedDiff(request.repositoryId, request.path);
|
|
15164
|
-
const args = ["--literal-pathspecs", "diff"];
|
|
15165
|
-
if (request.kind === "staged") args.push("--cached");
|
|
15166
|
-
args.push("--no-color", "--no-ext-diff", "--unified=3", "--", ...paths);
|
|
15167
|
-
const output = await this.git.run(repository.path, args, { operation: `diff-${request.kind}`, readOnly: true, maxOutputBytes: 32 * 1024 * 1024 });
|
|
15168
|
-
return makeDiffResult(request.path, output.stdout);
|
|
15169
|
-
}
|
|
15170
|
-
async commitDiff(repositoryId, oid) {
|
|
15171
|
-
this.assertKnownOid(repositoryId, oid);
|
|
15779
|
+
revisionKey = randomBytes(32);
|
|
15780
|
+
async get(repositoryId) {
|
|
15172
15781
|
const repository = this.repositories.get(repositoryId);
|
|
15173
|
-
|
|
15174
|
-
return makeDiffResult(oid, output.stdout);
|
|
15782
|
+
return this.git.runWriteTask(repository.path, () => this.read(repositoryId), repository.commonDir);
|
|
15175
15783
|
}
|
|
15176
|
-
async
|
|
15177
|
-
|
|
15784
|
+
async set(repositoryId, input) {
|
|
15785
|
+
const parsed = commitAuthorshipInputSchema.safeParse(input);
|
|
15786
|
+
if (!parsed.success) throw invalid$1("Enter a valid name, email and reviewed identity.");
|
|
15787
|
+
const { name, email: email2, expectedRevision } = parsed.data;
|
|
15178
15788
|
const repository = this.repositories.get(repositoryId);
|
|
15179
|
-
const
|
|
15180
|
-
const
|
|
15181
|
-
|
|
15182
|
-
|
|
15183
|
-
|
|
15789
|
+
const configPath = path.join(repository.commonDir, "config");
|
|
15790
|
+
const lockPath = `${configPath}.lock`;
|
|
15791
|
+
return this.git.runWriteTask(repository.path, async (run) => {
|
|
15792
|
+
const current = await this.read(repositoryId);
|
|
15793
|
+
if (!current.editable) throw invalid$1(current.blockers.join(" "));
|
|
15794
|
+
if (current.revision !== expectedRevision) throw invalid$1("Git identity or repository settings changed. Reopen the editor and review them again.");
|
|
15795
|
+
let locked = false;
|
|
15796
|
+
try {
|
|
15797
|
+
const file = await open(lockPath, "wx", 384);
|
|
15798
|
+
locked = true;
|
|
15799
|
+
try {
|
|
15800
|
+
await file.writeFile(await readFile(configPath));
|
|
15801
|
+
} finally {
|
|
15802
|
+
await file.close();
|
|
15803
|
+
}
|
|
15804
|
+
const reviewed = await this.read(repositoryId);
|
|
15805
|
+
if (!reviewed.editable || reviewed.revision !== expectedRevision) throw invalid$1("Git settings changed before saving. Reopen the editor and review them again.");
|
|
15806
|
+
const mode = (await lstat(configPath)).mode & 511;
|
|
15807
|
+
await run(["config", "--file", lockPath, "--replace-all", "--", "user.name", name], { operation: "commit-authorship-set" });
|
|
15808
|
+
await run(["config", "--file", lockPath, "--replace-all", "--", "user.email", email2], { operation: "commit-authorship-set" });
|
|
15809
|
+
const candidate = await this.identities(repository.path, ["-c", `include.path=${lockPath}`]);
|
|
15810
|
+
if (candidate.author?.name !== name || candidate.author.email !== email2 || candidate.committer?.name !== name || candidate.committer.email !== email2) {
|
|
15811
|
+
throw invalid$1("An included Git setting overrides this identity. Edit that setting outside OpenTig before applying a repository identity.");
|
|
15812
|
+
}
|
|
15813
|
+
const latest = await this.read(repositoryId);
|
|
15814
|
+
if (!latest.editable || latest.revision !== expectedRevision) throw invalid$1("Git settings changed while saving. Reopen the editor and review them again.");
|
|
15815
|
+
await chmod(lockPath, mode);
|
|
15816
|
+
await rename(lockPath, configPath);
|
|
15817
|
+
locked = false;
|
|
15818
|
+
} catch (error) {
|
|
15819
|
+
if (error.code === "EEXIST") {
|
|
15820
|
+
throw invalid$1("Another Git process is editing repository settings. Wait for it to finish and try again.");
|
|
15821
|
+
}
|
|
15822
|
+
throw error;
|
|
15823
|
+
} finally {
|
|
15824
|
+
if (locked) await rm(lockPath, { force: true });
|
|
15825
|
+
}
|
|
15826
|
+
return this.read(repositoryId);
|
|
15827
|
+
}, repository.commonDir);
|
|
15828
|
+
}
|
|
15829
|
+
async read(repositoryId) {
|
|
15830
|
+
const repository = this.repositories.get(repositoryId);
|
|
15831
|
+
const configPath = path.join(repository.commonDir, "config");
|
|
15832
|
+
const [entries, identities, config2, info] = await Promise.all([
|
|
15833
|
+
this.config(repository.path),
|
|
15834
|
+
this.identities(repository.path),
|
|
15835
|
+
readFile(configPath),
|
|
15836
|
+
lstat(configPath)
|
|
15837
|
+
]);
|
|
15838
|
+
const environment = environmentKeys.filter((key) => process.env[key] !== void 0);
|
|
15839
|
+
const custom = entries.some((entry) => entry.key.startsWith("author.") || entry.key.startsWith("committer."));
|
|
15840
|
+
const worktree = entries.some((entry) => entry.scope === "worktree");
|
|
15841
|
+
const command2 = entries.some((entry) => entry.scope === "command");
|
|
15842
|
+
const blockers = [];
|
|
15843
|
+
if (environment.length) blockers.push("Backend environment variables override Git identity. Manage them outside OpenTig before editing repository authorship.");
|
|
15844
|
+
if (custom) blockers.push("Git author.* or committer.* settings override user.name/email. Manage those settings outside OpenTig first.");
|
|
15845
|
+
if (worktree) blockers.push("This worktree has its own identity. Manage its worktree settings outside OpenTig first.");
|
|
15846
|
+
if (command2) blockers.push("Backend command configuration overrides Git identity. Manage it outside OpenTig first.");
|
|
15847
|
+
if (!info.isFile() || info.nlink !== 1) blockers.push("The repository config is linked or shared. Edit it outside OpenTig to avoid changing another repository.");
|
|
15848
|
+
const scopes = ["user.name", "user.email"].map((key) => entries.filter((entry) => entry.key === key).at(-1)?.scope ?? "inferred");
|
|
15849
|
+
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";
|
|
15850
|
+
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");
|
|
15851
|
+
return { ...identities, source, editable: blockers.length === 0, blockers, revision: revision2 };
|
|
15852
|
+
}
|
|
15853
|
+
async config(cwd) {
|
|
15854
|
+
try {
|
|
15855
|
+
const output = await this.git.run(cwd, ["config", "--null", "--show-scope", "--get-regexp", identityPattern], { operation: "commit-authorship-read", readOnly: true });
|
|
15856
|
+
const fields = output.stdout.toString("utf8").split("\0");
|
|
15857
|
+
const entries = [];
|
|
15858
|
+
for (let index = 0; index + 1 < fields.length; index += 2) {
|
|
15859
|
+
const field = fields[index + 1];
|
|
15860
|
+
const separator = field.indexOf("\n");
|
|
15861
|
+
if (separator >= 0) entries.push({ scope: fields[index], key: field.slice(0, separator), value: field.slice(separator + 1) });
|
|
15862
|
+
}
|
|
15863
|
+
return entries;
|
|
15864
|
+
} catch (error) {
|
|
15865
|
+
if (error instanceof GitOperationError && error.detail.exitCode === 1) return [];
|
|
15866
|
+
throw error;
|
|
15867
|
+
}
|
|
15868
|
+
}
|
|
15869
|
+
async identities(cwd, prefix = []) {
|
|
15870
|
+
const [author, committer] = await Promise.all(["GIT_AUTHOR_IDENT", "GIT_COMMITTER_IDENT"].map(async (variable) => {
|
|
15871
|
+
try {
|
|
15872
|
+
const output = await this.git.run(cwd, [...prefix, "var", variable], { operation: "commit-authorship-read", readOnly: true });
|
|
15873
|
+
const match = /^(.*?) <([^<>]*)> \d+ [+-]\d{4}$/.exec(output.stdout.toString("utf8").trim());
|
|
15874
|
+
if (!match) throw invalid$1("Git returned an unreadable commit identity.");
|
|
15875
|
+
return { name: match[1], email: match[2] };
|
|
15876
|
+
} catch (error) {
|
|
15877
|
+
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;
|
|
15878
|
+
throw error;
|
|
15879
|
+
}
|
|
15880
|
+
}));
|
|
15881
|
+
return { author, committer };
|
|
15882
|
+
}
|
|
15883
|
+
}
|
|
15884
|
+
function invalid$1(message) {
|
|
15885
|
+
return new GitOperationError({ code: "INVALID_ARGUMENT", operation: "commit-authorship-set", message });
|
|
15886
|
+
}
|
|
15887
|
+
const read = { operation: "remote-branches", readOnly: true, maxOutputBytes: 4 * 1024 * 1024 };
|
|
15888
|
+
const network = { ...read, timeoutMs: 6e4 };
|
|
15889
|
+
class GitRemoteBranches {
|
|
15890
|
+
constructor(git, repositories) {
|
|
15891
|
+
this.git = git;
|
|
15892
|
+
this.repositories = repositories;
|
|
15893
|
+
}
|
|
15894
|
+
git;
|
|
15895
|
+
repositories;
|
|
15896
|
+
reader(root) {
|
|
15897
|
+
return (args, options) => this.git.run(root, args, options);
|
|
15898
|
+
}
|
|
15899
|
+
async remotes(run) {
|
|
15900
|
+
const names = (await run(["remote"], read)).stdout.toString("utf8").trim().split(/\r?\n/).filter(Boolean);
|
|
15901
|
+
return Promise.all(names.map(async (name) => ({ name, refspecs: (await this.config(run, `remote.${name}.fetch`)).split(/\r?\n/).filter(Boolean) })));
|
|
15902
|
+
}
|
|
15903
|
+
async config(run, key) {
|
|
15904
|
+
try {
|
|
15905
|
+
return (await run(["config", "--get-all", key], read)).stdout.toString("utf8").trim();
|
|
15906
|
+
} catch (error) {
|
|
15907
|
+
if (error instanceof GitOperationError && error.detail.exitCode === 1) return "";
|
|
15908
|
+
throw error;
|
|
15909
|
+
}
|
|
15910
|
+
}
|
|
15911
|
+
mappings(fullName, remotes) {
|
|
15912
|
+
return remotes.flatMap((remote) => remote.refspecs.flatMap((spec) => {
|
|
15913
|
+
if (spec.startsWith("^")) return [];
|
|
15914
|
+
const [source, destination] = spec.replace(/^\+/, "").split(":");
|
|
15915
|
+
if (!source?.startsWith("refs/heads/") || !destination?.startsWith("refs/remotes/")) return [];
|
|
15916
|
+
const match = refspecMatch(destination, fullName);
|
|
15917
|
+
if (match === null) return [];
|
|
15918
|
+
const sourceRef = source.replace("*", match);
|
|
15919
|
+
if (remote.refspecs.some((excluded) => excluded.startsWith("^") && refspecMatch(excluded.slice(1), sourceRef) !== null)) return [];
|
|
15920
|
+
return [{ remote: remote.name, sourceRef }];
|
|
15921
|
+
}));
|
|
15922
|
+
}
|
|
15923
|
+
async snapshotBranches(repositoryId, branches) {
|
|
15924
|
+
const root = this.repositories.get(repositoryId).path;
|
|
15925
|
+
const run = this.reader(root);
|
|
15926
|
+
const remotes = await this.remotes(run);
|
|
15927
|
+
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]));
|
|
15928
|
+
return { remotes: remotes.map((remote) => remote.name), branches: branches.filter((branch) => !aliases.has(branch.fullName)).map((branch) => {
|
|
15929
|
+
if (!branch.remote) return branch;
|
|
15930
|
+
const mappings = this.mappings(branch.fullName, remotes);
|
|
15931
|
+
if (mappings.length !== 1) return branch;
|
|
15932
|
+
return { ...branch, remoteName: mappings[0].remote, remoteBranchName: mappings[0].sourceRef.slice("refs/heads/".length) };
|
|
15933
|
+
}) };
|
|
15934
|
+
}
|
|
15935
|
+
async target(fullName, run) {
|
|
15936
|
+
const branches = parseRefs((await run(["for-each-ref", `--format=${REF_FORMAT}`, "refs/remotes"], read)).stdout);
|
|
15937
|
+
const branch = branches.find((item) => item.fullName === fullName);
|
|
15938
|
+
if (!branch) return null;
|
|
15939
|
+
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]));
|
|
15940
|
+
const mappings = this.mappings(fullName, await this.remotes(run));
|
|
15941
|
+
if (aliases || mappings.length !== 1) throw new GitOperationError({
|
|
15942
|
+
code: "INVALID_ARGUMENT",
|
|
15943
|
+
operation: "remote-branch",
|
|
15944
|
+
message: "This reference does not map to one configured remote branch. Check the remote fetch settings."
|
|
15945
|
+
});
|
|
15946
|
+
return { branch, ...mappings[0], branchName: mappings[0].sourceRef.slice("refs/heads/".length) };
|
|
15947
|
+
}
|
|
15948
|
+
async destination(target, run, root) {
|
|
15949
|
+
const fetchUrls = (await run(["remote", "get-url", "--all", "--", target.remote], read)).stdout.toString("utf8").trim().split(/\r?\n/);
|
|
15950
|
+
const pushUrls = (await run(["remote", "get-url", "--push", "--all", "--", target.remote], read)).stdout.toString("utf8").trim().split(/\r?\n/);
|
|
15951
|
+
const id = createHash("sha256").update(JSON.stringify([target.remote, target.sourceRef, fetchUrls, pushUrls, this.git.authenticationKey?.(root)])).digest("hex");
|
|
15952
|
+
const blocked = fetchUrls.length !== 1 || pushUrls.length !== 1 || fetchUrls[0] !== pushUrls[0] ? "Fetch and push destinations differ or have multiple URLs. Manage remote deletion outside OpenTig." : null;
|
|
15953
|
+
return { id, blocked, url: fetchUrls[0] };
|
|
15954
|
+
}
|
|
15955
|
+
async advertised(target, run, url) {
|
|
15956
|
+
const lines = (await run(["ls-remote", "--symref", "--", url, "HEAD", target.sourceRef], network)).stdout.toString("utf8").split(/\r?\n/);
|
|
15957
|
+
const defaultRef = lines.find((line) => line.startsWith("ref: refs/heads/") && line.endsWith(" HEAD"))?.split(" ")[0]?.slice(5) ?? null;
|
|
15958
|
+
const oid = lines.find((line) => line.split(" ")[1] === target.sourceRef)?.split(" ")[0] ?? null;
|
|
15959
|
+
return { defaultRef, oid };
|
|
15960
|
+
}
|
|
15961
|
+
async details(repositoryId, fullName) {
|
|
15962
|
+
const run = this.reader(this.repositories.get(repositoryId).path);
|
|
15963
|
+
const target = await this.target(fullName, run);
|
|
15964
|
+
if (!target) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "remote-branch-details", message: "The remote reference no longer exists. Fetch branches again." });
|
|
15965
|
+
const destination = await this.destination(target, run, this.repositories.get(repositoryId).path);
|
|
15966
|
+
let blocked = destination.blocked;
|
|
15967
|
+
let defaultRef = null;
|
|
15968
|
+
let remoteState = "unchecked";
|
|
15969
|
+
if (!blocked) {
|
|
15970
|
+
try {
|
|
15971
|
+
const state = await this.advertised(target, run, destination.url);
|
|
15972
|
+
defaultRef = state.defaultRef;
|
|
15973
|
+
remoteState = !state.oid ? "missing" : state.oid === target.branch.oid ? "current" : "changed";
|
|
15974
|
+
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;
|
|
15975
|
+
} catch {
|
|
15976
|
+
blocked = "Could not check the remote. Fetch again or check your connection and Git permissions.";
|
|
15977
|
+
}
|
|
15978
|
+
}
|
|
15979
|
+
const upstreams = (await run(["for-each-ref", "--format=%(refname)%00%(refname:short)%00%(upstream)%00%(worktreepath)", "refs/heads"], read)).stdout.toString("utf8");
|
|
15980
|
+
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 }));
|
|
15981
|
+
return {
|
|
15982
|
+
...target.branch,
|
|
15983
|
+
remote: target.remote,
|
|
15984
|
+
branchName: target.branchName,
|
|
15985
|
+
localBranches,
|
|
15986
|
+
defaultBranch: defaultRef?.slice("refs/heads/".length) ?? null,
|
|
15987
|
+
remoteState,
|
|
15988
|
+
destinationId: destination.id,
|
|
15989
|
+
deletionBlockedReason: blocked
|
|
15990
|
+
};
|
|
15991
|
+
}
|
|
15992
|
+
async create(request) {
|
|
15993
|
+
const repository = this.repositories.get(request.repositoryId);
|
|
15994
|
+
return this.git.runWriteTask(repository.path, async (run) => {
|
|
15995
|
+
const target = await this.target(request.fullName, run);
|
|
15996
|
+
if (!target) return { status: "missing" };
|
|
15997
|
+
if (target.branch.oid !== request.expectedOid) return { status: "stale" };
|
|
15998
|
+
if (request.localName.startsWith("-")) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "create-tracking-branch", message: "Invalid local branch name." });
|
|
15999
|
+
await run(["check-ref-format", `refs/heads/${request.localName}`], read);
|
|
16000
|
+
const localRef = `refs/heads/${request.localName}`;
|
|
16001
|
+
const existing = (await run(["for-each-ref", "--format=%(refname)", "refs/heads"], read)).stdout.toString("utf8").trim().split(/\r?\n/);
|
|
16002
|
+
if (existing.includes(localRef)) return { status: "exists" };
|
|
16003
|
+
await run(["branch", "--track", "--", request.localName, target.branch.fullName], { operation: "create-tracking-branch" });
|
|
16004
|
+
return { status: "created", fullName: localRef };
|
|
16005
|
+
}, repository.commonDir);
|
|
16006
|
+
}
|
|
16007
|
+
async delete(request) {
|
|
16008
|
+
const repository = this.repositories.get(request.repositoryId);
|
|
16009
|
+
return this.git.runWriteTask(repository.path, async (run) => {
|
|
16010
|
+
const target = await this.target(request.fullName, run);
|
|
16011
|
+
if (!target) return { status: "missing" };
|
|
16012
|
+
if (target.branch.oid !== request.expectedOid) return { status: "stale" };
|
|
16013
|
+
const destination = await this.destination(target, run, repository.path);
|
|
16014
|
+
if (destination.id !== request.destinationId) return { status: "destination-changed" };
|
|
16015
|
+
if (destination.blocked) return { status: "rejected", message: destination.blocked };
|
|
16016
|
+
try {
|
|
16017
|
+
const state = await this.advertised(target, run, destination.url);
|
|
16018
|
+
if (!state.oid) return { status: "missing" };
|
|
16019
|
+
if (!state.defaultRef) return { status: "rejected", message: "Could not identify the remote default branch. Nothing was deleted." };
|
|
16020
|
+
if (state.defaultRef === target.sourceRef) return { status: "default" };
|
|
16021
|
+
if (state.oid !== request.expectedOid) return { status: "stale" };
|
|
16022
|
+
await run(
|
|
16023
|
+
[
|
|
16024
|
+
"-c",
|
|
16025
|
+
`remote.${target.remote}.mirror=false`,
|
|
16026
|
+
"push",
|
|
16027
|
+
"--porcelain",
|
|
16028
|
+
"--no-follow-tags",
|
|
16029
|
+
`--force-with-lease=${target.sourceRef}:${request.expectedOid}`,
|
|
16030
|
+
"--",
|
|
16031
|
+
destination.url,
|
|
16032
|
+
`:${target.sourceRef}`
|
|
16033
|
+
],
|
|
16034
|
+
{ operation: "delete-remote-branch", timeoutMs: 12e4, maxOutputBytes: 4 * 1024 * 1024 }
|
|
16035
|
+
);
|
|
16036
|
+
return { status: "deleted" };
|
|
16037
|
+
} catch {
|
|
16038
|
+
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." };
|
|
16039
|
+
}
|
|
16040
|
+
}, repository.commonDir);
|
|
16041
|
+
}
|
|
16042
|
+
async fetch(repositoryId) {
|
|
16043
|
+
const repository = this.repositories.get(repositoryId);
|
|
16044
|
+
try {
|
|
16045
|
+
await this.git.runWriteTask(repository.path, async (run) => {
|
|
16046
|
+
for (const remote of await this.remotes(run)) {
|
|
16047
|
+
const specs = remote.refspecs.filter((spec) => spec.startsWith("^refs/heads/") || /^\+?refs\/heads\/[^:]+:refs\/remotes\//.test(spec));
|
|
16048
|
+
if (!specs.some((spec) => !spec.startsWith("^"))) continue;
|
|
16049
|
+
await run(
|
|
16050
|
+
["-c", "fetch.pruneTags=false", "fetch", "--prune", "--no-prune-tags", "--no-tags", "--no-recurse-submodules", "--", remote.name, ...specs],
|
|
16051
|
+
{ operation: "fetch-branches", timeoutMs: 12e4, maxOutputBytes: 4 * 1024 * 1024 }
|
|
16052
|
+
);
|
|
16053
|
+
}
|
|
16054
|
+
}, repository.commonDir);
|
|
16055
|
+
} catch {
|
|
16056
|
+
return { status: "failed", message: "Could not fetch all remote branches. Check your connection and Git credentials, then fetch again." };
|
|
16057
|
+
}
|
|
16058
|
+
const status = await this.repositories.status(repositoryId, false);
|
|
16059
|
+
return { status: "success", ahead: status.ahead, behind: status.behind };
|
|
16060
|
+
}
|
|
16061
|
+
}
|
|
16062
|
+
function refspecMatch(pattern, ref) {
|
|
16063
|
+
const star = pattern.indexOf("*");
|
|
16064
|
+
if (star < 0) return pattern === ref ? "" : null;
|
|
16065
|
+
const prefix = pattern.slice(0, star);
|
|
16066
|
+
const suffix = pattern.slice(star + 1);
|
|
16067
|
+
return ref.startsWith(prefix) && ref.endsWith(suffix) && ref.length >= prefix.length + suffix.length ? ref.slice(prefix.length, suffix ? -suffix.length : void 0) : null;
|
|
16068
|
+
}
|
|
16069
|
+
const AI_PATCH_BUDGET = 4e5;
|
|
16070
|
+
const AI_SUMMARY_BUDGET = 24e3;
|
|
16071
|
+
class GitRepositoryOperations {
|
|
16072
|
+
constructor(git, repositories, files) {
|
|
16073
|
+
this.git = git;
|
|
16074
|
+
this.repositories = repositories;
|
|
16075
|
+
this.files = files;
|
|
16076
|
+
this.authorship = new GitCommitAuthorship(git, repositories);
|
|
16077
|
+
this.remoteBranches = new GitRemoteBranches(git, repositories);
|
|
16078
|
+
}
|
|
16079
|
+
git;
|
|
16080
|
+
repositories;
|
|
16081
|
+
files;
|
|
16082
|
+
access;
|
|
16083
|
+
publicationContext(repositoryId, remote) {
|
|
16084
|
+
if (!this.access) throw new Error("Repository access is unavailable.");
|
|
16085
|
+
return this.access.publication(repositoryId, remote);
|
|
16086
|
+
}
|
|
16087
|
+
knownOids = /* @__PURE__ */ new Map();
|
|
16088
|
+
authorship;
|
|
16089
|
+
remoteBranches;
|
|
16090
|
+
remoteBranchDetails(repositoryId, fullName) {
|
|
16091
|
+
return this.remoteBranches.details(repositoryId, fullName);
|
|
16092
|
+
}
|
|
16093
|
+
createTrackingBranch(request) {
|
|
16094
|
+
return this.remoteBranches.create(request);
|
|
16095
|
+
}
|
|
16096
|
+
deleteRemoteBranch(request) {
|
|
16097
|
+
return this.git.withNetworkContext(this.repositories.get(request.repositoryId).path, () => this.remoteBranches.delete(request));
|
|
16098
|
+
}
|
|
16099
|
+
fetchBranches(repositoryId) {
|
|
16100
|
+
return this.git.withNetworkContext(this.repositories.get(repositoryId).path, () => this.remoteBranches.fetch(repositoryId));
|
|
16101
|
+
}
|
|
16102
|
+
getCommitAuthorship(repositoryId) {
|
|
16103
|
+
return this.authorship.get(repositoryId);
|
|
16104
|
+
}
|
|
16105
|
+
setCommitAuthorship(repositoryId, input) {
|
|
16106
|
+
return this.authorship.set(repositoryId, input);
|
|
16107
|
+
}
|
|
16108
|
+
async diff(request) {
|
|
16109
|
+
const repository = this.repositories.get(request.repositoryId);
|
|
16110
|
+
const paths = this.repositories.validatePaths(request.repositoryId, [request.path]);
|
|
16111
|
+
const tracked = (await this.git.run(
|
|
16112
|
+
repository.path,
|
|
16113
|
+
["--literal-pathspecs", "ls-files", "-z", "--", ...paths],
|
|
16114
|
+
{ operation: "diff-tracked-check", readOnly: true, maxOutputBytes: 1024 * 1024 }
|
|
16115
|
+
)).stdout.length > 0;
|
|
16116
|
+
if (request.kind === "unstaged" && !tracked) return this.untrackedDiff(request.repositoryId, request.path);
|
|
16117
|
+
const args = ["--literal-pathspecs", "diff"];
|
|
16118
|
+
if (request.kind === "staged") args.push("--cached");
|
|
16119
|
+
args.push("--no-color", "--no-ext-diff", "--unified=3", "--", ...paths);
|
|
16120
|
+
const output = await this.git.run(repository.path, args, { operation: `diff-${request.kind}`, readOnly: true, maxOutputBytes: 32 * 1024 * 1024 });
|
|
16121
|
+
return makeDiffResult(request.path, output.stdout);
|
|
16122
|
+
}
|
|
16123
|
+
async commitDiff(repositoryId, oid) {
|
|
16124
|
+
this.assertKnownOid(repositoryId, oid);
|
|
16125
|
+
const repository = this.repositories.get(repositoryId);
|
|
16126
|
+
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 });
|
|
16127
|
+
return makeDiffResult(oid, output.stdout);
|
|
16128
|
+
}
|
|
16129
|
+
async commitFiles(repositoryId, oid) {
|
|
16130
|
+
this.assertKnownOid(repositoryId, oid);
|
|
16131
|
+
const repository = this.repositories.get(repositoryId);
|
|
16132
|
+
const common2 = ["show", "--format=", "--no-color", "--no-ext-diff", "-M", "-z"];
|
|
16133
|
+
const [nameStatus, numstat] = await Promise.all([
|
|
16134
|
+
this.git.run(repository.path, [...common2, "--name-status", oid, "--"], { operation: "commit-files", readOnly: true, maxOutputBytes: 8 * 1024 * 1024 }),
|
|
16135
|
+
this.git.run(repository.path, [...common2, "--numstat", oid, "--"], { operation: "commit-files", readOnly: true, maxOutputBytes: 8 * 1024 * 1024 })
|
|
16136
|
+
]);
|
|
15184
16137
|
return parseCommitFiles(nameStatus.stdout.toString("utf8"), numstat.stdout.toString("utf8"));
|
|
15185
16138
|
}
|
|
15186
16139
|
async commitFileDiff(repositoryId, oid, filePath, oldPath) {
|
|
@@ -15555,6 +16508,9 @@ ${base}
|
|
|
15555
16508
|
}, repository.commonDir);
|
|
15556
16509
|
}
|
|
15557
16510
|
async fetch(repositoryId) {
|
|
16511
|
+
return this.git.withNetworkContext(this.repositories.get(repositoryId).path, () => this.fetchInContext(repositoryId));
|
|
16512
|
+
}
|
|
16513
|
+
async fetchInContext(repositoryId) {
|
|
15558
16514
|
const repository = this.repositories.get(repositoryId);
|
|
15559
16515
|
try {
|
|
15560
16516
|
await this.runFetch(repository.path, "fetch");
|
|
@@ -15566,6 +16522,9 @@ ${base}
|
|
|
15566
16522
|
return { status: "success", ahead: status.ahead, behind: status.behind };
|
|
15567
16523
|
}
|
|
15568
16524
|
async pull(repositoryId) {
|
|
16525
|
+
return this.git.withNetworkContext(this.repositories.get(repositoryId).path, () => this.pullInContext(repositoryId));
|
|
16526
|
+
}
|
|
16527
|
+
async pullInContext(repositoryId) {
|
|
15569
16528
|
const repository = this.repositories.get(repositoryId);
|
|
15570
16529
|
let status = await this.repositories.status(repositoryId, false);
|
|
15571
16530
|
const existingConflicts = conflictPaths(status);
|
|
@@ -15642,7 +16601,47 @@ ${base}
|
|
|
15642
16601
|
const nextStatus = await this.repositories.status(repositoryId, false);
|
|
15643
16602
|
return { status: "success", commits, restoredLocalChanges: hasLocalChanges, rebased, localCommits: nextStatus.ahead };
|
|
15644
16603
|
}
|
|
15645
|
-
async push(repositoryId, publish) {
|
|
16604
|
+
async push(repositoryId, publish, expectedContext) {
|
|
16605
|
+
if (this.access) return this.pushWithContext(repositoryId, publish, expectedContext);
|
|
16606
|
+
return this.legacyPush(repositoryId, publish);
|
|
16607
|
+
}
|
|
16608
|
+
async pushWithContext(repositoryId, publish, expectedContext) {
|
|
16609
|
+
const repository = this.repositories.get(repositoryId);
|
|
16610
|
+
try {
|
|
16611
|
+
const reviewed = await this.access.assertPublication(repositoryId, expectedContext, publish?.remote);
|
|
16612
|
+
return await this.git.withNetworkContext(repository.path, () => this.git.runWriteTask(repository.path, async (run) => {
|
|
16613
|
+
const context = await this.access.assertPublication(repositoryId, reviewed.id, publish?.remote);
|
|
16614
|
+
const status = await this.repositories.status(repositoryId, false);
|
|
16615
|
+
const conflicts = conflictPaths(status);
|
|
16616
|
+
if (conflicts.length) return { status: "blocked-conflicts", files: conflicts };
|
|
16617
|
+
if (status.operation) return { status: "blocked-operation", operation: status.operation };
|
|
16618
|
+
if (!context.branch || !context.oid || !context.targetRef) return { status: "rejected", reason: "configuration", message: "Check out a branch with a commit before publishing." };
|
|
16619
|
+
if (publish && (publish.expectedBranch !== context.branch || publish.expectedOid !== context.oid)) return { status: "rejected", reason: "configuration", message: "The branch changed. Review and try again." };
|
|
16620
|
+
if (!context.remote) return context.remotes.length ? { status: "remote-required", branch: context.branch, oid: context.oid, remotes: context.remotes } : { status: "rejected", reason: "configuration", message: "No remote is configured." };
|
|
16621
|
+
const result = await run([
|
|
16622
|
+
"-c",
|
|
16623
|
+
`remote.${context.remote}.mirror=false`,
|
|
16624
|
+
"push",
|
|
16625
|
+
"--porcelain",
|
|
16626
|
+
"--no-follow-tags",
|
|
16627
|
+
...!status.upstream ? ["--set-upstream"] : [],
|
|
16628
|
+
"--",
|
|
16629
|
+
context.remote,
|
|
16630
|
+
`refs/heads/${context.branch}:${context.targetRef}`
|
|
16631
|
+
], {
|
|
16632
|
+
operation: "push",
|
|
16633
|
+
timeoutMs: 12e4,
|
|
16634
|
+
maxOutputBytes: 4 * 1024 * 1024,
|
|
16635
|
+
publication: { repositoryId, id: context.id, ...publish?.remote ? { remote: publish.remote } : {} }
|
|
16636
|
+
});
|
|
16637
|
+
if (!status.upstream) return { status: "published", branch: context.branch, remote: context.remote };
|
|
16638
|
+
return result.stdout.toString("utf8").includes("[up to date]") ? { status: "up-to-date" } : { status: "success", commits: status.ahead };
|
|
16639
|
+
}, repository.commonDir));
|
|
16640
|
+
} catch (error) {
|
|
16641
|
+
return pushFailure(error);
|
|
16642
|
+
}
|
|
16643
|
+
}
|
|
16644
|
+
async legacyPush(repositoryId, publish) {
|
|
15646
16645
|
const repository = this.repositories.get(repositoryId);
|
|
15647
16646
|
let status = await this.repositories.status(repositoryId, false);
|
|
15648
16647
|
const existingConflicts = conflictPaths(status);
|
|
@@ -15736,15 +16735,16 @@ ${base}
|
|
|
15736
16735
|
return this.repositories.openPath(worktree.path);
|
|
15737
16736
|
}
|
|
15738
16737
|
/**
|
|
15739
|
-
* Cheap enumeration for the
|
|
16738
|
+
* Cheap enumeration for the refs manager. Deliberately runs no status
|
|
15740
16739
|
* scan per worktree so ordinary refreshes stay as fast as the selectors.
|
|
15741
16740
|
*/
|
|
15742
16741
|
async localRefsSnapshot(repositoryId) {
|
|
15743
16742
|
const repository = this.repositories.get(repositoryId);
|
|
15744
16743
|
const [branches, worktrees] = await Promise.all([this.branches(repositoryId), this.worktrees(repositoryId)]);
|
|
15745
|
-
const
|
|
16744
|
+
const managed = await this.remoteBranches.snapshotBranches(repositoryId, branches);
|
|
15746
16745
|
return {
|
|
15747
|
-
branches:
|
|
16746
|
+
branches: managed.branches.sort(compareBranches),
|
|
16747
|
+
remotes: managed.remotes,
|
|
15748
16748
|
worktrees: await Promise.all(worktrees.map(async (worktree) => ({ ...worktree, current: await samePath(worktree.path, repository.path) })))
|
|
15749
16749
|
};
|
|
15750
16750
|
}
|
|
@@ -16139,24 +17139,20 @@ function bounded(value, limit) {
|
|
|
16139
17139
|
}
|
|
16140
17140
|
function pushFailure(error) {
|
|
16141
17141
|
const detail = error instanceof GitOperationError ? error.detail : null;
|
|
16142
|
-
const
|
|
16143
|
-
|
|
16144
|
-
|
|
16145
|
-
|
|
16146
|
-
|
|
16147
|
-
|
|
16148
|
-
|
|
16149
|
-
}
|
|
16150
|
-
if (/hook declined|pre-receive hook|
|
|
16151
|
-
|
|
16152
|
-
|
|
16153
|
-
if (/could not resolve host|unable to access|connection
|
|
16154
|
-
|
|
16155
|
-
}
|
|
16156
|
-
if (/upstream branch .* does not match|no upstream branch|push\.default/.test(raw)) {
|
|
16157
|
-
return { status: "rejected", reason: "configuration", message: "The local branch and its upstream do not allow a safe push with the current configuration." };
|
|
16158
|
-
}
|
|
16159
|
-
return { status: "rejected", reason: "unknown", message: "Git rejected the push. No force push was performed." };
|
|
17142
|
+
const message = redactSensitiveText(detail?.stderr || detail?.message || (error instanceof Error ? error.message : "Git rejected the push.")).trim().slice(0, 3e3);
|
|
17143
|
+
const raw = message.toLowerCase();
|
|
17144
|
+
let reason = "unknown";
|
|
17145
|
+
let explanation = "";
|
|
17146
|
+
if (/repository not found|repository .* not found|(?:http|error:)\s*404/.test(raw)) {
|
|
17147
|
+
reason = "inaccessible";
|
|
17148
|
+
explanation = "The repository is missing or inaccessible to this account. ";
|
|
17149
|
+
} else if (/authentication failed|could not read username|bad credentials|expired|revoked|no usable saved credentials|publickey|(?:http|error:)\s*401/.test(raw)) reason = "authentication";
|
|
17150
|
+
else if (/protected branch|hook declined|pre-receive hook|remote rejected|gh006|gh013|repository rule/.test(raw)) reason = "hook";
|
|
17151
|
+
else if (/permission.*denied|write access.*not granted|access denied|(?:http|error:)\s*403/.test(raw)) reason = "permission";
|
|
17152
|
+
else if (/non-fast-forward|fetch first|stale info/.test(raw)) reason = "remote-changed";
|
|
17153
|
+
else if (/could not resolve host|unable to access|connection.*(timed out|reset|refused)|network is unreachable|(?:http|error:)\s*50[0-4]/.test(raw)) reason = "network";
|
|
17154
|
+
else if (detail?.code === "INVALID_ARGUMENT" || /upstream branch .* does not match|no upstream branch|push\.default|context|account changed/.test(raw)) reason = "configuration";
|
|
17155
|
+
return { status: "rejected", reason, message: explanation + message };
|
|
16160
17156
|
}
|
|
16161
17157
|
function makeDiffResult(path2, buffer) {
|
|
16162
17158
|
const patch = buffer.toString("utf8");
|
|
@@ -16168,7 +17164,6 @@ const MAX_REPOSITORY_PROJECTS = 50;
|
|
|
16168
17164
|
const MAX_PROJECT_NAME_LENGTH = 60;
|
|
16169
17165
|
const MAX_REPOSITORIES_PER_PROJECT = 200;
|
|
16170
17166
|
const MAX_REPOSITORY_KEY_LENGTH = 2048;
|
|
16171
|
-
const UNASSIGNED_RECENT_LIMIT = 10;
|
|
16172
17167
|
function normalizeRepositoryKey(commonDir) {
|
|
16173
17168
|
return commonDir.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
16174
17169
|
}
|
|
@@ -16196,8 +17191,23 @@ class RepositoryService {
|
|
|
16196
17191
|
return this.openPath(recent.path);
|
|
16197
17192
|
}
|
|
16198
17193
|
async openPath(selectedPath) {
|
|
17194
|
+
const repository = await this.preparePath(selectedPath);
|
|
17195
|
+
await this.settings.touchRepository(repository);
|
|
17196
|
+
return repository;
|
|
17197
|
+
}
|
|
17198
|
+
/** Inspect/cache only: no recent entry, active repository, or persistence. */
|
|
17199
|
+
async preparePath(selectedPath) {
|
|
16199
17200
|
const repository = await this.inspectPath(selectedPath);
|
|
16200
17201
|
this.repositories.set(repository.id, repository);
|
|
17202
|
+
return repository;
|
|
17203
|
+
}
|
|
17204
|
+
async completeSetup(id, validate2 = () => void 0) {
|
|
17205
|
+
const prepared = this.get(id);
|
|
17206
|
+
const repository = await this.inspectPath(prepared.path);
|
|
17207
|
+
if (repository.id !== prepared.id || repository.commonDir !== prepared.commonDir) {
|
|
17208
|
+
throw new Error("The repository location changed. Select the folder again.");
|
|
17209
|
+
}
|
|
17210
|
+
validate2();
|
|
16201
17211
|
await this.settings.touchRepository(repository);
|
|
16202
17212
|
return repository;
|
|
16203
17213
|
}
|
|
@@ -16692,6 +17702,331 @@ function revision(bytes) {
|
|
|
16692
17702
|
function invalidReplacement(message) {
|
|
16693
17703
|
return new GitOperationError({ code: "INVALID_ARGUMENT", operation: "replace-search", message });
|
|
16694
17704
|
}
|
|
17705
|
+
const githubAccountSelectionSchema = discriminatedUnion("mode", [
|
|
17706
|
+
object({ mode: literal("auto") }),
|
|
17707
|
+
object({ mode: literal("account"), host: literal("github.com"), login: string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/), gitMode: _enum(["external", "managed"]).optional(), useGlobalDefault: boolean().optional() })
|
|
17708
|
+
]);
|
|
17709
|
+
const AUTOMATIC_GITHUB_ACCOUNT = { mode: "auto" };
|
|
17710
|
+
function githubRepositoryKey(commonDir, platform2 = process.platform) {
|
|
17711
|
+
const key = commonDir.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
17712
|
+
return platform2 === "win32" ? key.toLowerCase() : key;
|
|
17713
|
+
}
|
|
17714
|
+
const CACHE_MS = 6e4;
|
|
17715
|
+
const emptyStatus = () => ({
|
|
17716
|
+
installationStatus: "unchecked",
|
|
17717
|
+
accounts: [],
|
|
17718
|
+
activeLogin: null,
|
|
17719
|
+
environment: { present: false, login: null, state: "unknown" },
|
|
17720
|
+
checkedAt: null
|
|
17721
|
+
});
|
|
17722
|
+
class GitHubAccountsService {
|
|
17723
|
+
constructor(resolver, runner, git, settings, store, changed = () => void 0) {
|
|
17724
|
+
this.resolver = resolver;
|
|
17725
|
+
this.runner = runner;
|
|
17726
|
+
this.git = git;
|
|
17727
|
+
this.settings = settings;
|
|
17728
|
+
this.store = store;
|
|
17729
|
+
this.changed = changed;
|
|
17730
|
+
}
|
|
17731
|
+
resolver;
|
|
17732
|
+
runner;
|
|
17733
|
+
git;
|
|
17734
|
+
settings;
|
|
17735
|
+
store;
|
|
17736
|
+
changed;
|
|
17737
|
+
inventory = null;
|
|
17738
|
+
loading = null;
|
|
17739
|
+
checking = null;
|
|
17740
|
+
detection = null;
|
|
17741
|
+
revision = 0;
|
|
17742
|
+
get authRevision() {
|
|
17743
|
+
return this.revision;
|
|
17744
|
+
}
|
|
17745
|
+
revisions = /* @__PURE__ */ new Map();
|
|
17746
|
+
defaultRevision = 0;
|
|
17747
|
+
revisionFor(repository) {
|
|
17748
|
+
const selection = this.selection(repository);
|
|
17749
|
+
return Math.max(
|
|
17750
|
+
this.revisions.get(githubRepositoryKey(repository.commonDir)) ?? 0,
|
|
17751
|
+
selection.mode === "account" && selection.useGlobalDefault ? this.defaultRevision : 0
|
|
17752
|
+
);
|
|
17753
|
+
}
|
|
17754
|
+
contexts = /* @__PURE__ */ new Map();
|
|
17755
|
+
ssh = /* @__PURE__ */ new Map();
|
|
17756
|
+
identities = /* @__PURE__ */ new Map();
|
|
17757
|
+
status(force = false) {
|
|
17758
|
+
if (force) {
|
|
17759
|
+
this.checking ??= this.check().finally(() => {
|
|
17760
|
+
this.checking = null;
|
|
17761
|
+
});
|
|
17762
|
+
return this.checking.then((value) => ({ ...structuredClone(value), defaultLogin: this.settings?.githubDefaultLogin ?? null }));
|
|
17763
|
+
}
|
|
17764
|
+
this.loading ??= (async () => {
|
|
17765
|
+
this.inventory ??= await this.store?.load() ?? emptyStatus();
|
|
17766
|
+
return this.inventory;
|
|
17767
|
+
})();
|
|
17768
|
+
return this.loading.then(() => ({ ...structuredClone(this.inventory ?? emptyStatus()), defaultLogin: this.settings?.githubDefaultLogin ?? null }));
|
|
17769
|
+
}
|
|
17770
|
+
selection(repository) {
|
|
17771
|
+
return this.settings?.githubAccount(repository.commonDir) ?? { ...AUTOMATIC_GITHUB_ACCOUNT };
|
|
17772
|
+
}
|
|
17773
|
+
context(repository, remote, nameWithOwner) {
|
|
17774
|
+
const cached2 = this.contexts.get(repository.id);
|
|
17775
|
+
if (cached2?.remote === remote && cached2.value.revision === this.revisionFor(repository)) return structuredClone(cached2.value);
|
|
17776
|
+
const selection = this.selection(repository);
|
|
17777
|
+
return {
|
|
17778
|
+
selection,
|
|
17779
|
+
nameWithOwner,
|
|
17780
|
+
login: selection.mode === "account" ? selection.login : null,
|
|
17781
|
+
source: selection.mode === "account" ? "explicit" : null,
|
|
17782
|
+
state: "unchecked",
|
|
17783
|
+
checkedAt: null,
|
|
17784
|
+
revision: this.revisionFor(repository)
|
|
17785
|
+
};
|
|
17786
|
+
}
|
|
17787
|
+
async setSelection(repository, selection) {
|
|
17788
|
+
if (!this.settings) throw new Error("GitHub account settings are unavailable.");
|
|
17789
|
+
const saving = this.settings.setGitHubAccount(repository.commonDir, selection);
|
|
17790
|
+
this.invalidate(repository);
|
|
17791
|
+
try {
|
|
17792
|
+
await saving;
|
|
17793
|
+
} catch (error) {
|
|
17794
|
+
this.invalidate(repository);
|
|
17795
|
+
throw error;
|
|
17796
|
+
}
|
|
17797
|
+
}
|
|
17798
|
+
async setDefaultAccount(login2) {
|
|
17799
|
+
if (!this.settings) throw new Error("GitHub account settings are unavailable.");
|
|
17800
|
+
this.defaultRevision = ++this.revision;
|
|
17801
|
+
try {
|
|
17802
|
+
await this.settings.setGitHubDefaultLogin(login2);
|
|
17803
|
+
} finally {
|
|
17804
|
+
this.changed(void 0, true);
|
|
17805
|
+
}
|
|
17806
|
+
return this.status();
|
|
17807
|
+
}
|
|
17808
|
+
invalidate(repository) {
|
|
17809
|
+
this.revision++;
|
|
17810
|
+
if (repository) {
|
|
17811
|
+
const key = githubRepositoryKey(repository.commonDir);
|
|
17812
|
+
this.revisions.set(key, this.revision);
|
|
17813
|
+
} else {
|
|
17814
|
+
this.contexts.clear();
|
|
17815
|
+
}
|
|
17816
|
+
this.ssh.clear();
|
|
17817
|
+
this.identities.clear();
|
|
17818
|
+
this.changed(repository);
|
|
17819
|
+
}
|
|
17820
|
+
refreshAuthentication() {
|
|
17821
|
+
this.ssh.clear();
|
|
17822
|
+
this.identities.clear();
|
|
17823
|
+
}
|
|
17824
|
+
async detect(force = false) {
|
|
17825
|
+
if (force || !this.detection || Date.now() - this.detection.at > CACHE_MS) {
|
|
17826
|
+
this.detection = { at: Date.now(), promise: selectCli(this.resolver, this.runner, "gh", {
|
|
17827
|
+
forceRefresh: force,
|
|
17828
|
+
runOptions: { cwd: os.homedir(), env: { ...GH_ENV }, removeEnv: ["GH_DEBUG", "DEBUG"] }
|
|
17829
|
+
}) };
|
|
17830
|
+
}
|
|
17831
|
+
return this.detection.promise;
|
|
17832
|
+
}
|
|
17833
|
+
async authenticate(repository, remote, nameWithOwner, operation) {
|
|
17834
|
+
const revision2 = this.revisionFor(repository);
|
|
17835
|
+
const selection = this.selection(repository);
|
|
17836
|
+
let login2 = selection.mode === "account" ? selection.login : null;
|
|
17837
|
+
let source = selection.mode === "account" ? "explicit" : null;
|
|
17838
|
+
try {
|
|
17839
|
+
const detection = await this.detect();
|
|
17840
|
+
if (detection.state !== "available" || !detection.candidate) throw new GhOperationError({
|
|
17841
|
+
code: detection.state === "not-found" ? "GH_CLI_NOT_FOUND" : "GH_PROCESS_FAILED",
|
|
17842
|
+
operation,
|
|
17843
|
+
message: detection.state === "not-found" ? "GitHub CLI is not installed on the backend host." : "GitHub CLI could not run. Check Settings → GitHub."
|
|
17844
|
+
});
|
|
17845
|
+
const executable = detection.candidate;
|
|
17846
|
+
if (selection.mode === "auto" && parseSshRemote(remote)) {
|
|
17847
|
+
const override = executable.env.GIT_SSH_COMMAND || executable.env.GIT_SSH || process.env.GIT_SSH_COMMAND || process.env.GIT_SSH;
|
|
17848
|
+
const configured = await this.git.run(repository.path, ["config", "--default", "", "--get", "core.sshCommand"], {
|
|
17849
|
+
operation: "github-ssh-configuration",
|
|
17850
|
+
readOnly: true,
|
|
17851
|
+
maxOutputBytes: 8 * 1024
|
|
17852
|
+
});
|
|
17853
|
+
if (override || configured.stdout.toString("utf8").trim()) throw new GhOperationError({
|
|
17854
|
+
code: "GH_ACCOUNT_UNRESOLVED",
|
|
17855
|
+
operation,
|
|
17856
|
+
message: "This repository uses a custom SSH command. Choose an explicit account in Settings → GitHub."
|
|
17857
|
+
});
|
|
17858
|
+
const key = `${repository.path}\0${remote}`;
|
|
17859
|
+
let cached2 = this.ssh.get(key);
|
|
17860
|
+
if (!cached2 || Date.now() - cached2.at > CACHE_MS) {
|
|
17861
|
+
cached2 = { at: Date.now(), promise: identifySshAccount(remote, this.runner) };
|
|
17862
|
+
this.ssh.set(key, cached2);
|
|
17863
|
+
}
|
|
17864
|
+
login2 = await cached2.promise;
|
|
17865
|
+
source = "ssh";
|
|
17866
|
+
if (!login2) throw new GhOperationError({
|
|
17867
|
+
code: "GH_ACCOUNT_UNRESOLVED",
|
|
17868
|
+
operation,
|
|
17869
|
+
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."
|
|
17870
|
+
});
|
|
17871
|
+
}
|
|
17872
|
+
if (!source) source = executable.env.GH_TOKEN || executable.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN ? "environment" : "global";
|
|
17873
|
+
const auth = await accountAuthentication(login2, executable, this.runner, operation);
|
|
17874
|
+
const verified = await this.identity(executable, auth, operation);
|
|
17875
|
+
if (login2 && login2.toLowerCase() !== verified.toLowerCase()) throw new GhOperationError({
|
|
17876
|
+
code: "GH_AUTH_REQUIRED",
|
|
17877
|
+
operation,
|
|
17878
|
+
message: `The saved credentials for @${login2} authenticate a different account. Authenticate @${login2} again and check Settings → GitHub.`
|
|
17879
|
+
});
|
|
17880
|
+
login2 = verified;
|
|
17881
|
+
const context = { selection, nameWithOwner, login: login2, source, state: "ready", checkedAt: (/* @__PURE__ */ new Date()).toISOString(), revision: revision2 };
|
|
17882
|
+
if (revision2 === this.revisionFor(repository)) {
|
|
17883
|
+
const previous = this.contexts.get(repository.id)?.value;
|
|
17884
|
+
if (previous?.state === "ready" && previous.revision === revision2 && (previous.login?.toLowerCase() !== login2.toLowerCase() || previous.source !== source)) {
|
|
17885
|
+
this.invalidate(repository);
|
|
17886
|
+
context.revision = this.revisionFor(repository);
|
|
17887
|
+
}
|
|
17888
|
+
this.contexts.set(repository.id, { remote, value: context });
|
|
17889
|
+
}
|
|
17890
|
+
return { auth, executable, context };
|
|
17891
|
+
} catch (error) {
|
|
17892
|
+
const message = error instanceof GhOperationError ? error.message : "Could not inspect GitHub authentication. Check Settings → GitHub again.";
|
|
17893
|
+
if (revision2 === this.revisionFor(repository)) this.contexts.set(repository.id, { remote, value: {
|
|
17894
|
+
selection,
|
|
17895
|
+
nameWithOwner,
|
|
17896
|
+
login: login2,
|
|
17897
|
+
source,
|
|
17898
|
+
state: "error",
|
|
17899
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
17900
|
+
message,
|
|
17901
|
+
revision: revision2
|
|
17902
|
+
} });
|
|
17903
|
+
if (error instanceof GhOperationError) throw error;
|
|
17904
|
+
throw new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message, retryable: true });
|
|
17905
|
+
}
|
|
17906
|
+
}
|
|
17907
|
+
identity(executable, auth, operation) {
|
|
17908
|
+
const fingerprint = createHash("sha256").update(auth.env?.GH_TOKEN ?? "").digest("hex");
|
|
17909
|
+
let cached2 = this.identities.get(fingerprint);
|
|
17910
|
+
if (!cached2 || Date.now() - cached2.at > CACHE_MS) {
|
|
17911
|
+
const promise = (async () => {
|
|
17912
|
+
const result = await this.runner.run(executable.executable, ["api", "user", "--jq", ".login"], {
|
|
17913
|
+
cwd: os.homedir(),
|
|
17914
|
+
env: { ...executable.env, ...GH_ENV, ...auth.env },
|
|
17915
|
+
...auth.removeEnv ? { removeEnv: auth.removeEnv } : {},
|
|
17916
|
+
timeoutMs: 15e3,
|
|
17917
|
+
maxOutputBytes: 8 * 1024
|
|
17918
|
+
});
|
|
17919
|
+
const login2 = result.stdout.trim();
|
|
17920
|
+
if (result.exitCode === 0 && GITHUB_LOGIN.test(login2)) return login2;
|
|
17921
|
+
const invalid2 = /401|bad credentials|requires authentication/i.test(result.stderr);
|
|
17922
|
+
throw new GhOperationError({
|
|
17923
|
+
code: invalid2 ? "GH_AUTH_REQUIRED" : "GH_PROCESS_FAILED",
|
|
17924
|
+
operation,
|
|
17925
|
+
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.",
|
|
17926
|
+
retryable: !invalid2
|
|
17927
|
+
});
|
|
17928
|
+
})();
|
|
17929
|
+
cached2 = { at: Date.now(), promise };
|
|
17930
|
+
this.identities.set(fingerprint, cached2);
|
|
17931
|
+
void promise.catch(() => {
|
|
17932
|
+
if (this.identities.get(fingerprint)?.promise === promise) this.identities.delete(fingerprint);
|
|
17933
|
+
});
|
|
17934
|
+
}
|
|
17935
|
+
return cached2.promise;
|
|
17936
|
+
}
|
|
17937
|
+
async check() {
|
|
17938
|
+
await this.status();
|
|
17939
|
+
this.refreshAuthentication();
|
|
17940
|
+
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17941
|
+
const detection = await this.detect(true);
|
|
17942
|
+
const result = {
|
|
17943
|
+
...emptyStatus(),
|
|
17944
|
+
installationStatus: detection.state,
|
|
17945
|
+
checkedAt,
|
|
17946
|
+
...detection.version ? { version: detection.version } : {}
|
|
17947
|
+
};
|
|
17948
|
+
if (detection.state === "available" && detection.candidate) {
|
|
17949
|
+
const executable = detection.candidate;
|
|
17950
|
+
try {
|
|
17951
|
+
const status = await this.runner.run(executable.executable, ["auth", "status", "--hostname", "github.com", "--json", "hosts"], {
|
|
17952
|
+
cwd: os.homedir(),
|
|
17953
|
+
env: { ...executable.env, ...GH_ENV },
|
|
17954
|
+
removeEnv: ["GH_TOKEN", ...GH_PRIVATE_ENV],
|
|
17955
|
+
timeoutMs: 6e4,
|
|
17956
|
+
maxOutputBytes: 256 * 1024
|
|
17957
|
+
});
|
|
17958
|
+
if (status.exitCode !== 0) {
|
|
17959
|
+
if (/unknown flag.*json/i.test(status.stderr)) result.installationStatus = "incompatible";
|
|
17960
|
+
throw new Error("Status failed");
|
|
17961
|
+
}
|
|
17962
|
+
Object.assign(result, parseGitHubAuthStatus(status.stdout));
|
|
17963
|
+
const envToken = executable.env.GH_TOKEN || executable.env.GITHUB_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN;
|
|
17964
|
+
if (envToken) {
|
|
17965
|
+
result.environment.present = true;
|
|
17966
|
+
try {
|
|
17967
|
+
const auth = await accountAuthentication(null, executable, this.runner, "gh-auth-status");
|
|
17968
|
+
result.environment.login = await this.identity(executable, auth, "gh-auth-status");
|
|
17969
|
+
result.environment.state = "authenticated";
|
|
17970
|
+
} catch (error) {
|
|
17971
|
+
result.environment.state = error instanceof GhOperationError && error.detail.code === "GH_AUTH_REQUIRED" ? "invalid" : "unknown";
|
|
17972
|
+
}
|
|
17973
|
+
}
|
|
17974
|
+
} catch {
|
|
17975
|
+
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.";
|
|
17976
|
+
result.accounts = this.inventory?.accounts.map((account) => ({ ...account, state: "unknown" })) ?? [];
|
|
17977
|
+
result.activeLogin = this.inventory?.activeLogin ?? null;
|
|
17978
|
+
}
|
|
17979
|
+
} 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.";
|
|
17980
|
+
this.inventory = result;
|
|
17981
|
+
try {
|
|
17982
|
+
await this.store?.save(result);
|
|
17983
|
+
} finally {
|
|
17984
|
+
this.invalidate();
|
|
17985
|
+
}
|
|
17986
|
+
return result;
|
|
17987
|
+
}
|
|
17988
|
+
}
|
|
17989
|
+
const authState = _enum(["authenticated", "invalid", "unknown"]);
|
|
17990
|
+
const login = string().regex(/^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/);
|
|
17991
|
+
const schema = object({
|
|
17992
|
+
installationStatus: _enum(["unchecked", "available", "not-found", "not-executable", "incompatible", "inspection-failed"]),
|
|
17993
|
+
version: string().max(200).optional(),
|
|
17994
|
+
accounts: array(object({
|
|
17995
|
+
host: literal("github.com"),
|
|
17996
|
+
login,
|
|
17997
|
+
active: boolean(),
|
|
17998
|
+
state: authState,
|
|
17999
|
+
storage: _enum(["keyring", "file", "unknown"]),
|
|
18000
|
+
scopes: string().max(500).optional()
|
|
18001
|
+
})).max(100),
|
|
18002
|
+
activeLogin: login.nullable(),
|
|
18003
|
+
environment: object({ present: boolean(), login: login.nullable(), state: authState }),
|
|
18004
|
+
checkedAt: string().datetime().nullable(),
|
|
18005
|
+
message: string().max(500).optional()
|
|
18006
|
+
});
|
|
18007
|
+
class GitHubStatusStore {
|
|
18008
|
+
constructor(filePath) {
|
|
18009
|
+
this.filePath = filePath;
|
|
18010
|
+
}
|
|
18011
|
+
filePath;
|
|
18012
|
+
pending = Promise.resolve();
|
|
18013
|
+
async load() {
|
|
18014
|
+
await this.pending;
|
|
18015
|
+
try {
|
|
18016
|
+
if ((await stat(this.filePath)).size > 256 * 1024) return null;
|
|
18017
|
+
const parsed = schema.safeParse(JSON.parse(await readFile(this.filePath, "utf8")));
|
|
18018
|
+
return parsed.success ? parsed.data : null;
|
|
18019
|
+
} catch {
|
|
18020
|
+
return null;
|
|
18021
|
+
}
|
|
18022
|
+
}
|
|
18023
|
+
save(value) {
|
|
18024
|
+
const snapshot = JSON.stringify(schema.parse(value));
|
|
18025
|
+
const write = this.pending.then(() => writeFileAtomically(this.filePath, snapshot, { parseJson: true }));
|
|
18026
|
+
this.pending = write.catch(() => void 0);
|
|
18027
|
+
return write;
|
|
18028
|
+
}
|
|
18029
|
+
}
|
|
16695
18030
|
const positive = number().int().positive().safe();
|
|
16696
18031
|
const membershipSchema = object({
|
|
16697
18032
|
stack: object({ number: positive, size: positive, baseRefName: string().min(1) }).nullable(),
|
|
@@ -16759,28 +18094,6 @@ function parsePullRequestStack(raw, prNumber, listing = false) {
|
|
|
16759
18094
|
}))
|
|
16760
18095
|
};
|
|
16761
18096
|
}
|
|
16762
|
-
const OWNER_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?$/;
|
|
16763
|
-
const REPO_PATTERN = /^[A-Za-z0-9._-]+$/;
|
|
16764
|
-
function parseGitHubRemote(url) {
|
|
16765
|
-
const trimmed = url.trim();
|
|
16766
|
-
if (!trimmed) return null;
|
|
16767
|
-
let rest = null;
|
|
16768
|
-
const https = /^https?:\/\/(?:[^@/\s]+@)?github\.com\/(.+)$/i.exec(trimmed);
|
|
16769
|
-
const ssh = /^ssh:\/\/(?:[^@/\s]+@)?github\.com(?::\d+)?\/(.+)$/i.exec(trimmed);
|
|
16770
|
-
const scp = /^(?:[^@\s]+)@github\.com:(.+)$/i.exec(trimmed);
|
|
16771
|
-
const git = /^git:\/\/github\.com\/(.+)$/i.exec(trimmed);
|
|
16772
|
-
if (https) rest = https[1] ?? null;
|
|
16773
|
-
else if (ssh) rest = ssh[1] ?? null;
|
|
16774
|
-
else if (scp) rest = scp[1] ?? null;
|
|
16775
|
-
else if (git) rest = git[1] ?? null;
|
|
16776
|
-
if (!rest) return null;
|
|
16777
|
-
const segments = rest.replace(/\/+$/, "").split("/");
|
|
16778
|
-
if (segments.length !== 2) return null;
|
|
16779
|
-
const owner = segments[0] ?? "";
|
|
16780
|
-
const repo = (segments[1] ?? "").replace(/\.git$/i, "");
|
|
16781
|
-
if (!OWNER_PATTERN.test(owner) || !repo || repo === "." || repo === ".." || !REPO_PATTERN.test(repo)) return null;
|
|
16782
|
-
return { nameWithOwner: `${owner}/${repo}` };
|
|
16783
|
-
}
|
|
16784
18097
|
const PR_SUMMARY_FIELDS = "number,title,state,isDraft,author,headRefName,baseRefName,updatedAt,url,reviewDecision,additions,deletions,statusCheckRollup";
|
|
16785
18098
|
const PR_DETAIL_FIELDS = `${PR_SUMMARY_FIELDS},body,changedFiles,labels,commits`;
|
|
16786
18099
|
const tolerantRecordSchema = looseObject({});
|
|
@@ -16795,6 +18108,15 @@ function parsePullRequestList(raw) {
|
|
|
16795
18108
|
if (!list.success) throw invalidOutput("gh-pr-list");
|
|
16796
18109
|
return list.data.map((item) => parseSummary(item, "gh-pr-list"));
|
|
16797
18110
|
}
|
|
18111
|
+
function parseBranchPullRequestList(raw, nameWithOwner, branch) {
|
|
18112
|
+
const list = pullRequestArraySchema.safeParse(parseJson(raw, "gh-pr-for-branch"));
|
|
18113
|
+
if (!list.success) throw invalidOutput("gh-pr-for-branch");
|
|
18114
|
+
const headSchema = looseObject({ headRefName: string(), headRepository: looseObject({ nameWithOwner: string() }) });
|
|
18115
|
+
return list.data.filter((item) => {
|
|
18116
|
+
const head = headSchema.safeParse(item);
|
|
18117
|
+
return head.success && head.data.headRefName === branch && head.data.headRepository.nameWithOwner.toLowerCase() === nameWithOwner.toLowerCase();
|
|
18118
|
+
}).map((item) => parseSummary(item, "gh-pr-for-branch"));
|
|
18119
|
+
}
|
|
16798
18120
|
function parsePullRequestDetails(raw) {
|
|
16799
18121
|
const item = parseJson(raw, "gh-pr-view");
|
|
16800
18122
|
const summary = parseSummary(item, "gh-pr-view");
|
|
@@ -16876,14 +18198,14 @@ function parseCommits(value) {
|
|
|
16876
18198
|
if (typeof record2.oid !== "string" || !record2.oid) return [];
|
|
16877
18199
|
const authors = Array.isArray(record2.authors) ? record2.authors : [];
|
|
16878
18200
|
const authorRecord = authors.map((author) => tolerantRecordSchema.safeParse(author)).find((author) => author.success)?.data;
|
|
16879
|
-
const
|
|
16880
|
-
const name = typeof authorRecord?.name === "string" && authorRecord.name ? authorRecord.name :
|
|
18201
|
+
const login2 = typeof authorRecord?.login === "string" ? authorRecord.login : null;
|
|
18202
|
+
const name = typeof authorRecord?.name === "string" && authorRecord.name ? authorRecord.name : login2 ?? "unknown";
|
|
16881
18203
|
return [{
|
|
16882
18204
|
oid: record2.oid,
|
|
16883
18205
|
messageHeadline: typeof record2.messageHeadline === "string" ? record2.messageHeadline : "",
|
|
16884
18206
|
authoredAt: typeof record2.authoredDate === "string" ? record2.authoredDate : "",
|
|
16885
18207
|
author: name,
|
|
16886
|
-
authorAvatarUrl:
|
|
18208
|
+
authorAvatarUrl: login2 ? avatarUrl(login2, login2.endsWith("[bot]")) : null
|
|
16887
18209
|
}];
|
|
16888
18210
|
});
|
|
16889
18211
|
}
|
|
@@ -16902,11 +18224,11 @@ function parseChecksState(value) {
|
|
|
16902
18224
|
}
|
|
16903
18225
|
return pending ? "PENDING" : "PASSING";
|
|
16904
18226
|
}
|
|
16905
|
-
function avatarUrl(
|
|
16906
|
-
if (!
|
|
16907
|
-
if (
|
|
16908
|
-
const botName =
|
|
16909
|
-
const path2 = isBot ||
|
|
18227
|
+
function avatarUrl(login2, isBot) {
|
|
18228
|
+
if (!login2 || login2 === "unknown") return null;
|
|
18229
|
+
if (login2 === "app/dependabot" || login2 === "dependabot[bot]") return "https://avatars.githubusercontent.com/in/29110?s=40&v=4";
|
|
18230
|
+
const botName = login2.startsWith("app/") ? login2.slice(4) : login2.replace(/\[bot\]$/i, "");
|
|
18231
|
+
const path2 = isBot || login2.startsWith("app/") ? botName : login2;
|
|
16910
18232
|
return `https://github.com/${encodeURIComponent(path2)}.png?size=40`;
|
|
16911
18233
|
}
|
|
16912
18234
|
function parseJson(raw, operation) {
|
|
@@ -16926,70 +18248,113 @@ function validTimestamp(value) {
|
|
|
16926
18248
|
function invalidOutput(operation) {
|
|
16927
18249
|
return new GhOperationError({ code: "GH_INVALID_OUTPUT", operation, message: "GitHub CLI returned an unexpected response." });
|
|
16928
18250
|
}
|
|
16929
|
-
const GH_ENV = { GH_PROMPT_DISABLED: "1", GH_NO_UPDATE_NOTIFIER: "1", GH_PAGER: "cat", NO_COLOR: "1" };
|
|
16930
18251
|
const NETWORK_TIMEOUT_MS = 6e4;
|
|
16931
18252
|
class GitHubService {
|
|
16932
|
-
constructor(resolver, runner, git, repositories) {
|
|
16933
|
-
this.resolver = resolver;
|
|
18253
|
+
constructor(resolver, runner, git, repositories, accounts) {
|
|
16934
18254
|
this.runner = runner;
|
|
16935
18255
|
this.git = git;
|
|
16936
18256
|
this.repositories = repositories;
|
|
18257
|
+
this.accounts = accounts ?? new GitHubAccountsService(resolver, runner, git);
|
|
16937
18258
|
}
|
|
16938
|
-
resolver;
|
|
16939
18259
|
runner;
|
|
16940
18260
|
git;
|
|
16941
18261
|
repositories;
|
|
16942
|
-
|
|
18262
|
+
gitAccess;
|
|
18263
|
+
accounts;
|
|
16943
18264
|
async status(forceRefresh = false) {
|
|
16944
|
-
if (
|
|
16945
|
-
const
|
|
16946
|
-
|
|
16947
|
-
|
|
18265
|
+
if (forceRefresh) this.accounts.refreshAuthentication();
|
|
18266
|
+
const detected = await this.accounts.detect(forceRefresh);
|
|
18267
|
+
return {
|
|
18268
|
+
installed: !!detected.candidate,
|
|
18269
|
+
availability: detected.state === "available" ? "ready" : "error",
|
|
18270
|
+
authStatus: "unknown",
|
|
18271
|
+
...detected.version ? { version: detected.version } : {},
|
|
18272
|
+
checkedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
18273
|
+
};
|
|
16948
18274
|
}
|
|
16949
|
-
async
|
|
18275
|
+
async repositoryAccount(repositoryId, forceRefresh = false) {
|
|
18276
|
+
const repository = this.repositories.get(repositoryId);
|
|
18277
|
+
const info = await this.repositoryInfo(repositoryId);
|
|
18278
|
+
const remote = await this.remote(repositoryId);
|
|
18279
|
+
if (forceRefresh && info.nameWithOwner && !this.gitAccess) {
|
|
18280
|
+
this.accounts.refreshAuthentication();
|
|
18281
|
+
try {
|
|
18282
|
+
await this.accounts.authenticate(repository, remote, info.nameWithOwner, "gh-repository-account");
|
|
18283
|
+
} catch {
|
|
18284
|
+
}
|
|
18285
|
+
}
|
|
18286
|
+
const access2 = await this.gitAccess?.status(repositoryId, forceRefresh, { remote, nameWithOwner: info.nameWithOwner });
|
|
18287
|
+
return { ...this.accounts.context(repository, remote, info.nameWithOwner), ...access2 ? { access: access2 } : {} };
|
|
18288
|
+
}
|
|
18289
|
+
async setRepositoryAccount(repositoryId, selection, expectedRevision) {
|
|
18290
|
+
if (expectedRevision !== void 0 && expectedRevision !== this.accounts.revisionFor(this.repositories.get(repositoryId))) throw new Error("The repository account changed in another session. Reopen the account setup and review it.");
|
|
18291
|
+
await this.accounts.setSelection(this.repositories.get(repositoryId), selection);
|
|
18292
|
+
return this.repositoryAccount(repositoryId);
|
|
18293
|
+
}
|
|
18294
|
+
async remote(repositoryId) {
|
|
16950
18295
|
const repository = this.repositories.get(repositoryId);
|
|
16951
18296
|
try {
|
|
16952
|
-
const output = await this.git.run(repository.path, ["
|
|
16953
|
-
|
|
18297
|
+
const output = await this.git.run(repository.path, ["remote", "get-url", "--", "origin"], { operation: "github-remote-url", readOnly: true, maxOutputBytes: 64 * 1024 });
|
|
18298
|
+
return output.stdout.toString("utf8").trim();
|
|
18299
|
+
} catch {
|
|
18300
|
+
return "";
|
|
18301
|
+
}
|
|
18302
|
+
}
|
|
18303
|
+
async repositoryInfo(repositoryId) {
|
|
18304
|
+
try {
|
|
18305
|
+
const url = await this.remote(repositoryId);
|
|
18306
|
+
let parsed = parseGitHubRemote(url);
|
|
18307
|
+
if (!parsed) {
|
|
18308
|
+
const ssh = parseSshRemote(url);
|
|
18309
|
+
if (ssh) {
|
|
18310
|
+
const config2 = await this.runner.run("ssh", ["-G", ...ssh.port ? ["-p", ssh.port] : [], "--", ssh.destination], {
|
|
18311
|
+
cwd: os.homedir(),
|
|
18312
|
+
timeoutMs: 5e3,
|
|
18313
|
+
maxOutputBytes: 64 * 1024
|
|
18314
|
+
});
|
|
18315
|
+
const hostname = config2.stdout.split(/\r?\n/).find((line) => /^hostname\s/i.test(line))?.trim().split(/\s+/)[1];
|
|
18316
|
+
if (config2.exitCode === 0 && hostname?.toLowerCase() === "github.com") parsed = { nameWithOwner: ssh.nameWithOwner };
|
|
18317
|
+
}
|
|
18318
|
+
}
|
|
16954
18319
|
return parsed ? { isGitHub: true, nameWithOwner: parsed.nameWithOwner } : { isGitHub: false, nameWithOwner: null };
|
|
16955
18320
|
} catch {
|
|
16956
18321
|
return { isGitHub: false, nameWithOwner: null };
|
|
16957
18322
|
}
|
|
16958
18323
|
}
|
|
16959
18324
|
async listPullRequests(repositoryId, states) {
|
|
16960
|
-
const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, "gh-pr-list");
|
|
18325
|
+
const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-list");
|
|
16961
18326
|
const results = await Promise.all(states.map((state) => this.runGh(
|
|
16962
18327
|
["pr", "list", "-R", nameWithOwner, "--state", state.toLowerCase(), "--json", PR_SUMMARY_FIELDS, "--limit", "50"],
|
|
16963
|
-
{ cwd: repository.path, operation: "gh-pr-list", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
|
|
18328
|
+
{ auth, executable, cwd: repository.path, operation: "gh-pr-list", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
|
|
16964
18329
|
)));
|
|
16965
|
-
return this.withStackMembership(repository.path, nameWithOwner, selectPullRequestsNewestFirst(results.flatMap((result) => parsePullRequestList(result.stdout)), states));
|
|
18330
|
+
return this.withStackMembership(repository.path, nameWithOwner, auth, executable, selectPullRequestsNewestFirst(results.flatMap((result) => parsePullRequestList(result.stdout)), states));
|
|
16966
18331
|
}
|
|
16967
18332
|
async findPullRequestForBranch(repositoryId, branchName) {
|
|
16968
18333
|
const operation = "gh-pr-for-branch";
|
|
16969
|
-
const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
|
|
18334
|
+
const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
|
|
16970
18335
|
const branch = validateRef(branchName, operation);
|
|
16971
18336
|
const openResult = await this.runGh(
|
|
16972
|
-
["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "open", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
|
|
16973
|
-
{ cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
|
|
18337
|
+
["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "open", "--json", `${PR_SUMMARY_FIELDS},headRepository`, "--limit", "10"],
|
|
18338
|
+
{ auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
|
|
16974
18339
|
);
|
|
16975
|
-
const openPull = sortPullRequestsNewestFirst(
|
|
18340
|
+
const openPull = sortPullRequestsNewestFirst(parseBranchPullRequestList(openResult.stdout, nameWithOwner, branch)).find((pull) => pull.state === "OPEN");
|
|
16976
18341
|
if (openPull) return openPull;
|
|
16977
18342
|
const result = await this.runGh(
|
|
16978
|
-
["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "all", "--json", PR_SUMMARY_FIELDS, "--limit", "10"],
|
|
16979
|
-
{ cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
|
|
18343
|
+
["pr", "list", "-R", nameWithOwner, "--head", branch, "--state", "all", "--json", `${PR_SUMMARY_FIELDS},headRepository`, "--limit", "10"],
|
|
18344
|
+
{ auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 1024 * 1024 }
|
|
16980
18345
|
);
|
|
16981
|
-
return sortPullRequestsNewestFirst(
|
|
18346
|
+
return sortPullRequestsNewestFirst(parseBranchPullRequestList(result.stdout, nameWithOwner, branch))[0] ?? null;
|
|
16982
18347
|
}
|
|
16983
18348
|
async getPullRequest(repositoryId, prNumber) {
|
|
16984
|
-
const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, "gh-pr-view");
|
|
18349
|
+
const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-view");
|
|
16985
18350
|
const result = await this.runGh(
|
|
16986
18351
|
["pr", "view", String(prNumber), "-R", nameWithOwner, "--json", PR_DETAIL_FIELDS],
|
|
16987
|
-
{ cwd: repository.path, operation: "gh-pr-view", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
|
|
18352
|
+
{ auth, executable, cwd: repository.path, operation: "gh-pr-view", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 }
|
|
16988
18353
|
);
|
|
16989
18354
|
const details = parsePullRequestDetails(result.stdout);
|
|
16990
|
-
return (await this.withStackMembership(repository.path, nameWithOwner, [details]))[0];
|
|
18355
|
+
return (await this.withStackMembership(repository.path, nameWithOwner, auth, executable, [details]))[0];
|
|
16991
18356
|
}
|
|
16992
|
-
async withStackMembership(cwd, nameWithOwner, pulls) {
|
|
18357
|
+
async withStackMembership(cwd, nameWithOwner, auth, executable, pulls) {
|
|
16993
18358
|
const [owner, name] = nameWithOwner.split("/");
|
|
16994
18359
|
const enriched = [];
|
|
16995
18360
|
for (let start = 0; start < pulls.length; start += 25) {
|
|
@@ -16997,6 +18362,8 @@ class GitHubService {
|
|
|
16997
18362
|
try {
|
|
16998
18363
|
const numbers = batch.map((pr) => pr.number);
|
|
16999
18364
|
const result = await this.runGh(["api", "graphql", "-f", `owner=${owner}`, "-f", `name=${name}`, "-f", `query=${stackMembershipQuery(numbers)}`], {
|
|
18365
|
+
auth,
|
|
18366
|
+
executable,
|
|
17000
18367
|
cwd,
|
|
17001
18368
|
operation: "gh-pr-stack-membership",
|
|
17002
18369
|
timeoutMs: 1e4,
|
|
@@ -17015,28 +18382,28 @@ class GitHubService {
|
|
|
17015
18382
|
}
|
|
17016
18383
|
async getPullRequestStack(repositoryId, prNumber) {
|
|
17017
18384
|
const operation = "gh-pr-stack";
|
|
17018
|
-
const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
|
|
17019
|
-
const options = { cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 };
|
|
17020
|
-
const
|
|
18385
|
+
const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
|
|
18386
|
+
const options = { auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 4 * 1024 * 1024 };
|
|
18387
|
+
const read2 = async (endpoint) => this.runGh(["api", endpoint], options);
|
|
17021
18388
|
let listing;
|
|
17022
18389
|
try {
|
|
17023
|
-
listing = await
|
|
18390
|
+
listing = await read2(`repos/${nameWithOwner}/stacks?pull_request=${prNumber}`);
|
|
17024
18391
|
} catch (error) {
|
|
17025
18392
|
if (error instanceof GhOperationError && error.detail.code === "GH_PROCESS_FAILED" && /HTTP 404/.test(error.message)) return null;
|
|
17026
18393
|
throw error;
|
|
17027
18394
|
}
|
|
17028
18395
|
const stack = parsePullRequestStack(listing.stdout, prNumber, true);
|
|
17029
18396
|
if (!stack) return null;
|
|
17030
|
-
const details = await
|
|
18397
|
+
const details = await read2(`repos/${nameWithOwner}/stacks/${stack.number}`);
|
|
17031
18398
|
const complete = parsePullRequestStack(details.stdout, prNumber);
|
|
17032
18399
|
if (complete?.number !== stack.number) throw new GhOperationError({ code: "GH_INVALID_OUTPUT", operation, message: "The stack changed while loading. Refresh to try again." });
|
|
17033
18400
|
return complete;
|
|
17034
18401
|
}
|
|
17035
18402
|
async getPullRequestDiff(repositoryId, prNumber) {
|
|
17036
|
-
const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, "gh-pr-diff");
|
|
18403
|
+
const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, "gh-pr-diff");
|
|
17037
18404
|
const result = await this.runGh(
|
|
17038
18405
|
["pr", "diff", String(prNumber), "-R", nameWithOwner],
|
|
17039
|
-
{ cwd: repository.path, operation: "gh-pr-diff", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
|
|
18406
|
+
{ auth, executable, cwd: repository.path, operation: "gh-pr-diff", timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
|
|
17040
18407
|
);
|
|
17041
18408
|
const patch = result.stdout;
|
|
17042
18409
|
const binary = /Binary files .* differ|GIT binary patch/.test(patch);
|
|
@@ -17044,13 +18411,13 @@ class GitHubService {
|
|
|
17044
18411
|
}
|
|
17045
18412
|
async getPullRequestCommitDiff(repositoryId, oid) {
|
|
17046
18413
|
const operation = "gh-pr-commit-diff";
|
|
17047
|
-
const { repository, nameWithOwner } = await this.requireGitHub(repositoryId, operation);
|
|
18414
|
+
const { repository, nameWithOwner, auth, executable } = await this.requireGitHub(repositoryId, operation);
|
|
17048
18415
|
if (!/^[0-9a-f]{40,64}$/i.test(oid)) {
|
|
17049
18416
|
throw new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: "The commit is not valid." });
|
|
17050
18417
|
}
|
|
17051
18418
|
const result = await this.runGh(
|
|
17052
18419
|
["api", `repos/${nameWithOwner}/commits/${oid}`, "--header", "Accept: application/vnd.github.diff"],
|
|
17053
|
-
{ cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
|
|
18420
|
+
{ auth, executable, cwd: repository.path, operation, timeoutMs: NETWORK_TIMEOUT_MS, maxOutputBytes: 32 * 1024 * 1024 }
|
|
17054
18421
|
);
|
|
17055
18422
|
const patch = result.stdout;
|
|
17056
18423
|
const binary = /Binary files .* differ|GIT binary patch/.test(patch);
|
|
@@ -17058,7 +18425,13 @@ class GitHubService {
|
|
|
17058
18425
|
}
|
|
17059
18426
|
async createPullRequest(input) {
|
|
17060
18427
|
const operation = "gh-pr-create";
|
|
17061
|
-
const { repository, nameWithOwner } = await this.requireGitHub(input.repositoryId, operation);
|
|
18428
|
+
const { repository, nameWithOwner, auth, executable, context } = await this.requireGitHub(input.repositoryId, operation);
|
|
18429
|
+
const verifyAccount = () => {
|
|
18430
|
+
if (input.expectedAccount && (input.expectedAccount.revision !== this.accounts.revisionFor(repository) || input.expectedAccount.revision !== context.revision || input.expectedAccount.login.toLowerCase() !== context.login?.toLowerCase())) {
|
|
18431
|
+
throw new GhOperationError({ code: "GH_ACCOUNT_UNRESOLVED", operation, message: "The GitHub account changed. Review the current identity before creating the pull request." });
|
|
18432
|
+
}
|
|
18433
|
+
};
|
|
18434
|
+
verifyAccount();
|
|
17062
18435
|
const status = await this.repositories.status(input.repositoryId, false);
|
|
17063
18436
|
if (status.detached || status.unborn || !status.branch) {
|
|
17064
18437
|
throw new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: "Check out a branch before creating a pull request." });
|
|
@@ -17076,7 +18449,8 @@ class GitHubService {
|
|
|
17076
18449
|
await writeFile(bodyPath, input.body, { encoding: "utf8", mode: 384 });
|
|
17077
18450
|
const args = ["pr", "create", "-R", nameWithOwner, "--title", input.title, "--body-file", bodyPath, "--base", base, "--head", head];
|
|
17078
18451
|
if (input.draft) args.push("--draft");
|
|
17079
|
-
|
|
18452
|
+
verifyAccount();
|
|
18453
|
+
const result = await this.runGh(args, { auth, executable, cwd: repository.path, operation, timeoutMs: 12e4, maxOutputBytes: 1024 * 1024 });
|
|
17080
18454
|
const created = parseCreatedPullRequestUrl(result.stdout);
|
|
17081
18455
|
if (!created) throw new GhOperationError({ code: "GH_INVALID_OUTPUT", operation, message: "GitHub CLI did not return the URL of the new pull request." });
|
|
17082
18456
|
return created;
|
|
@@ -17084,51 +18458,32 @@ class GitHubService {
|
|
|
17084
18458
|
await rm(temporary, { recursive: true, force: true });
|
|
17085
18459
|
}
|
|
17086
18460
|
}
|
|
17087
|
-
async checkStatus(forceRefresh) {
|
|
17088
|
-
const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
17089
|
-
const detected = await selectCli(this.resolver, this.runner, "gh", { forceRefresh, runOptions: { cwd: os.tmpdir(), env: { ...GH_ENV } } });
|
|
17090
|
-
const executable = detected.candidate;
|
|
17091
|
-
if (!executable) {
|
|
17092
|
-
return { installed: false, availability: "error", authStatus: "unknown", message: "Install GitHub CLI (cli.github.com) to work with pull requests.", checkedAt };
|
|
17093
|
-
}
|
|
17094
|
-
if (detected.state !== "available") return { installed: true, availability: "error", authStatus: "unknown", message: "GitHub CLI was found but could not run. Check its installation and dependencies.", checkedAt };
|
|
17095
|
-
const versionLabel = detected.version ?? "";
|
|
17096
|
-
const auth = await this.run(executable, ["auth", "status", "--hostname", "github.com", "--active"], { cwd: os.tmpdir(), operation: "gh-auth-status" });
|
|
17097
|
-
if (auth.exitCode !== 0) {
|
|
17098
|
-
return { installed: true, availability: "error", authStatus: "unauthenticated", version: versionLabel, message: "Run gh auth login.", checkedAt };
|
|
17099
|
-
}
|
|
17100
|
-
return { installed: true, availability: "ready", authStatus: "authenticated", version: versionLabel, checkedAt };
|
|
17101
|
-
}
|
|
17102
18461
|
async requireGitHub(repositoryId, operation) {
|
|
17103
18462
|
const repository = this.repositories.get(repositoryId);
|
|
17104
18463
|
const info = await this.repositoryInfo(repositoryId);
|
|
17105
|
-
if (!info.isGitHub || !info.nameWithOwner) {
|
|
17106
|
-
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
if (!status.installed) throw new GhOperationError({ code: "GH_CLI_NOT_FOUND", operation, message: "GitHub CLI is not installed." });
|
|
17110
|
-
if (status.authStatus === "unauthenticated") throw new GhOperationError({ code: "GH_AUTH_REQUIRED", operation, message: "Sign in with gh auth login to continue." });
|
|
17111
|
-
return { repository, nameWithOwner: info.nameWithOwner };
|
|
18464
|
+
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." });
|
|
18465
|
+
const remote = await this.remote(repositoryId);
|
|
18466
|
+
const authenticated = await this.accounts.authenticate(repository, remote, info.nameWithOwner, operation);
|
|
18467
|
+
return { repository, nameWithOwner: info.nameWithOwner, ...authenticated };
|
|
17112
18468
|
}
|
|
17113
18469
|
async runGh(args, options) {
|
|
17114
|
-
const
|
|
17115
|
-
|
|
17116
|
-
if (!executable) throw new GhOperationError({ code: "GH_CLI_NOT_FOUND", operation: options.operation, message: "GitHub CLI is not installed." });
|
|
17117
|
-
const result = await this.run(executable, args, options);
|
|
17118
|
-
if (result.exitCode !== 0) throw classifyFailure(result, options.operation);
|
|
18470
|
+
const result = await this.run(options.executable, args, options);
|
|
18471
|
+
if (result.exitCode !== 0) throw classifyFailure({ ...result, stdout: redactCommandToken(result.stdout, options.auth), stderr: redactCommandToken(result.stderr, options.auth) }, options.operation);
|
|
17119
18472
|
return result;
|
|
17120
18473
|
}
|
|
17121
18474
|
async run(executable, args, options) {
|
|
17122
18475
|
try {
|
|
17123
18476
|
return await this.runner.run(executable.executable, args, {
|
|
17124
18477
|
cwd: options.cwd,
|
|
17125
|
-
env: { ...executable.env, ...GH_ENV },
|
|
18478
|
+
env: { ...executable.env, ...GH_ENV, ...options.auth?.env },
|
|
18479
|
+
...options.auth?.removeEnv ? { removeEnv: options.auth.removeEnv } : {},
|
|
17126
18480
|
...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
|
|
17127
18481
|
...options.maxOutputBytes !== void 0 ? { maxOutputBytes: options.maxOutputBytes } : {},
|
|
17128
18482
|
...options.stdin !== void 0 ? { stdin: options.stdin } : {}
|
|
17129
18483
|
});
|
|
17130
18484
|
} catch (error) {
|
|
17131
|
-
|
|
18485
|
+
const mapped = mapRunnerError(error, options.operation);
|
|
18486
|
+
throw new GhOperationError({ ...mapped.detail, message: redactCommandToken(mapped.message, options.auth ?? {}) });
|
|
17132
18487
|
}
|
|
17133
18488
|
}
|
|
17134
18489
|
}
|
|
@@ -17152,6 +18507,8 @@ ${result.stdout}`.toLowerCase();
|
|
|
17152
18507
|
if (/could not resolve|no such host|connection refused|connection reset|network|dial tcp|tls handshake/.test(raw)) {
|
|
17153
18508
|
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 });
|
|
17154
18509
|
}
|
|
18510
|
+
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 });
|
|
18511
|
+
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 });
|
|
17155
18512
|
const detail = firstLine(result.stderr) || firstLine(result.stdout);
|
|
17156
18513
|
return new GhOperationError({ code: "GH_PROCESS_FAILED", operation, message: detail || "GitHub CLI could not complete the operation.", exitCode: result.exitCode });
|
|
17157
18514
|
}
|
|
@@ -17167,6 +18524,50 @@ function mapRunnerError(error, operation) {
|
|
|
17167
18524
|
function firstLine(value) {
|
|
17168
18525
|
return (value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "").slice(0, 300);
|
|
17169
18526
|
}
|
|
18527
|
+
const statusSchema = object({
|
|
18528
|
+
id: _enum(["codex", "claude", "opencode", "grok"]),
|
|
18529
|
+
label: string(),
|
|
18530
|
+
availability: _enum(["ready", "warning", "error"]),
|
|
18531
|
+
installed: boolean(),
|
|
18532
|
+
authStatus: _enum(["authenticated", "unauthenticated", "unknown"]),
|
|
18533
|
+
installationStatus: _enum(["available", "not-found", "not-executable", "incompatible", "inspection-failed"]).optional(),
|
|
18534
|
+
executablePath: string().optional(),
|
|
18535
|
+
executableSource: _enum(["configured", "process-path", "user-path", "known-location"]).optional(),
|
|
18536
|
+
discoveryWarning: string().optional(),
|
|
18537
|
+
version: string().optional(),
|
|
18538
|
+
message: string().optional(),
|
|
18539
|
+
cliName: string().optional(),
|
|
18540
|
+
models: array(object({ id: string(), label: string(), description: string().optional() })),
|
|
18541
|
+
checkedAt: string().datetime()
|
|
18542
|
+
});
|
|
18543
|
+
const cacheSchema = object({
|
|
18544
|
+
version: literal(1),
|
|
18545
|
+
key: string(),
|
|
18546
|
+
statuses: array(statusSchema).min(1).max(4)
|
|
18547
|
+
}).refine(({ statuses }) => new Set(statuses.map((status) => status.id)).size === statuses.length);
|
|
18548
|
+
class AiStatusStore {
|
|
18549
|
+
constructor(filePath) {
|
|
18550
|
+
this.filePath = filePath;
|
|
18551
|
+
}
|
|
18552
|
+
filePath;
|
|
18553
|
+
pending = Promise.resolve();
|
|
18554
|
+
async load(key) {
|
|
18555
|
+
await this.pending;
|
|
18556
|
+
try {
|
|
18557
|
+
if ((await stat(this.filePath)).size > 4 * 1024 * 1024) return null;
|
|
18558
|
+
const parsed = cacheSchema.safeParse(JSON.parse(await readFile(this.filePath, "utf8")));
|
|
18559
|
+
return parsed.success && parsed.data.key === key ? parsed.data.statuses : null;
|
|
18560
|
+
} catch {
|
|
18561
|
+
return null;
|
|
18562
|
+
}
|
|
18563
|
+
}
|
|
18564
|
+
save(key, statuses) {
|
|
18565
|
+
const snapshot = cacheSchema.parse({ version: 1, key, statuses });
|
|
18566
|
+
const write = this.pending.then(() => writeFileAtomically(this.filePath, JSON.stringify(snapshot), { parseJson: true }));
|
|
18567
|
+
this.pending = write.catch(() => void 0);
|
|
18568
|
+
return write;
|
|
18569
|
+
}
|
|
18570
|
+
}
|
|
17170
18571
|
const MAX_PROBLEM_LOG_ENTRIES = 500;
|
|
17171
18572
|
const PROBLEM_LOG_COMPACTION_SLACK = 1.25;
|
|
17172
18573
|
const LEVELS = ["error", "warn"];
|
|
@@ -17567,9 +18968,11 @@ function normalizeRemoteFetchIntervalSeconds(value) {
|
|
|
17567
18968
|
Math.max(MIN_REMOTE_FETCH_INTERVAL_SECONDS, stepped)
|
|
17568
18969
|
);
|
|
17569
18970
|
}
|
|
17570
|
-
const SETTINGS_SCHEMA_VERSION =
|
|
18971
|
+
const SETTINGS_SCHEMA_VERSION = 2;
|
|
17571
18972
|
const defaults = {
|
|
17572
18973
|
version: SETTINGS_SCHEMA_VERSION,
|
|
18974
|
+
githubAccounts: {},
|
|
18975
|
+
githubDefaultLogin: null,
|
|
17573
18976
|
recentRepositories: [],
|
|
17574
18977
|
repositoryProjects: [],
|
|
17575
18978
|
filesTreeStates: [],
|
|
@@ -17596,6 +18999,7 @@ const defaults = {
|
|
|
17596
18999
|
};
|
|
17597
19000
|
const settingsRecordSchema = looseObject({
|
|
17598
19001
|
version: unknown().optional(),
|
|
19002
|
+
githubAccounts: unknown().optional(),
|
|
17599
19003
|
recentRepositories: unknown().optional(),
|
|
17600
19004
|
repositoryProjects: unknown().optional(),
|
|
17601
19005
|
filesTreeStates: unknown().optional(),
|
|
@@ -17672,11 +19076,53 @@ class SettingsStore {
|
|
|
17672
19076
|
get windowBounds() {
|
|
17673
19077
|
return { ...this.data.windowBounds };
|
|
17674
19078
|
}
|
|
19079
|
+
githubAccount(commonDir) {
|
|
19080
|
+
const selection = this.data.githubAccounts[githubRepositoryKey(commonDir)] ?? AUTOMATIC_GITHUB_ACCOUNT;
|
|
19081
|
+
if (selection.mode === "account" && selection.useGlobalDefault) {
|
|
19082
|
+
if (!this.data.githubDefaultLogin) throw new Error("Choose an OpenTig default account in Settings → GitHub.");
|
|
19083
|
+
return { ...selection, login: this.data.githubDefaultLogin };
|
|
19084
|
+
}
|
|
19085
|
+
return { ...selection };
|
|
19086
|
+
}
|
|
19087
|
+
get githubDefaultLogin() {
|
|
19088
|
+
return this.data.githubDefaultLogin;
|
|
19089
|
+
}
|
|
19090
|
+
async setGitHubDefaultLogin(login2) {
|
|
19091
|
+
const parsed = githubAccountSelectionSchema.parse({ mode: "account", host: "github.com", login: login2 });
|
|
19092
|
+
if (parsed.mode !== "account") throw new Error("Invalid default account.");
|
|
19093
|
+
const previous = this.data.githubDefaultLogin;
|
|
19094
|
+
this.data.githubDefaultLogin = parsed.login;
|
|
19095
|
+
try {
|
|
19096
|
+
await this.save();
|
|
19097
|
+
} catch (error) {
|
|
19098
|
+
if (this.data.githubDefaultLogin === parsed.login) this.data.githubDefaultLogin = previous;
|
|
19099
|
+
throw error;
|
|
19100
|
+
}
|
|
19101
|
+
}
|
|
19102
|
+
async setGitHubAccount(commonDir, selection) {
|
|
19103
|
+
const parsed = githubAccountSelectionSchema.parse(selection);
|
|
19104
|
+
if (parsed.mode === "account" && parsed.useGlobalDefault && parsed.login.toLowerCase() !== this.data.githubDefaultLogin?.toLowerCase()) {
|
|
19105
|
+
throw new Error("The OpenTig default account changed. Review the current account and try again.");
|
|
19106
|
+
}
|
|
19107
|
+
const key = githubRepositoryKey(commonDir);
|
|
19108
|
+
const previous = this.data.githubAccounts[key];
|
|
19109
|
+
if (parsed.mode === "auto") delete this.data.githubAccounts[key];
|
|
19110
|
+
else this.data.githubAccounts[key] = parsed;
|
|
19111
|
+
try {
|
|
19112
|
+
await this.save();
|
|
19113
|
+
} catch (error) {
|
|
19114
|
+
if (this.data.githubAccounts[key] === (parsed.mode === "auto" ? void 0 : parsed)) {
|
|
19115
|
+
if (previous) this.data.githubAccounts[key] = previous;
|
|
19116
|
+
else delete this.data.githubAccounts[key];
|
|
19117
|
+
}
|
|
19118
|
+
throw error;
|
|
19119
|
+
}
|
|
19120
|
+
}
|
|
17675
19121
|
async touchRepository(repository) {
|
|
17676
19122
|
const recent = { ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
17677
|
-
|
|
19123
|
+
const existing = this.data.recentRepositories.some((item) => item.id === repository.id);
|
|
19124
|
+
this.data.recentRepositories = existing ? this.data.recentRepositories.map((item) => item.id === repository.id ? recent : item) : [...this.data.recentRepositories, recent];
|
|
17678
19125
|
this.data.activeRepositoryId = repository.id;
|
|
17679
|
-
this.pruneRecentRepositories();
|
|
17680
19126
|
await this.save();
|
|
17681
19127
|
}
|
|
17682
19128
|
/**
|
|
@@ -17687,6 +19133,12 @@ class SettingsStore {
|
|
|
17687
19133
|
async relocateRepository(previousId, repository) {
|
|
17688
19134
|
const previous = this.data.recentRepositories.find((item) => item.id === previousId);
|
|
17689
19135
|
if (!previous) throw projectError("Unknown repository.");
|
|
19136
|
+
const previousAccountKey = githubRepositoryKey(previous.commonDir);
|
|
19137
|
+
const nextAccountKey = githubRepositoryKey(repository.commonDir);
|
|
19138
|
+
if (previousAccountKey !== nextAccountKey && this.data.githubAccounts[previousAccountKey]) {
|
|
19139
|
+
this.data.githubAccounts[nextAccountKey] ??= this.data.githubAccounts[previousAccountKey];
|
|
19140
|
+
delete this.data.githubAccounts[previousAccountKey];
|
|
19141
|
+
}
|
|
17690
19142
|
const previousKey = normalizeRepositoryKey(previous.commonDir);
|
|
17691
19143
|
const nextKey = normalizeRepositoryKey(repository.commonDir);
|
|
17692
19144
|
const destinationAlreadyAssigned = this.data.repositoryProjects.some((project) => project.repositoryKeys.some((key) => key === nextKey && key !== previousKey));
|
|
@@ -17699,18 +19151,17 @@ class SettingsStore {
|
|
|
17699
19151
|
});
|
|
17700
19152
|
this.data.filesTreeStates = normalizeFilesTreeStates(this.data.filesTreeStates.map((state) => state.repositoryId === previousId ? { ...state, repositoryId: repository.id } : state));
|
|
17701
19153
|
this.data.openFilesStates = normalizeOpenFilesStates(this.data.openFilesStates.map((state) => state.repositoryId === previousId ? { ...state, repositoryId: repository.id } : state));
|
|
17702
|
-
this.data.recentRepositories = [
|
|
17703
|
-
{ ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() },
|
|
17704
|
-
...this.data.recentRepositories.filter((item) => item.id !== previousId && item.id !== repository.id)
|
|
17705
|
-
];
|
|
19154
|
+
this.data.recentRepositories = this.data.recentRepositories.flatMap((item) => item.id === previousId ? [{ ...repository, lastOpenedAt: (/* @__PURE__ */ new Date()).toISOString() }] : item.id === repository.id ? [] : [item]);
|
|
17706
19155
|
this.data.activeRepositoryId = repository.id;
|
|
17707
|
-
this.pruneRecentRepositories();
|
|
17708
19156
|
await this.save();
|
|
17709
19157
|
}
|
|
17710
19158
|
/** Forget the whole repository group, never its directories or Git data. */
|
|
17711
19159
|
async forgetRepository(repositoryKey) {
|
|
17712
19160
|
const key = normalizeRepositoryKey(repositoryKey);
|
|
17713
19161
|
const removedIds = new Set(this.data.recentRepositories.filter((item) => normalizeRepositoryKey(item.commonDir) === key).map((item) => item.id));
|
|
19162
|
+
for (const item of this.data.recentRepositories) {
|
|
19163
|
+
if (removedIds.has(item.id)) delete this.data.githubAccounts[githubRepositoryKey(item.commonDir)];
|
|
19164
|
+
}
|
|
17714
19165
|
this.data.recentRepositories = this.data.recentRepositories.filter((item) => !removedIds.has(item.id));
|
|
17715
19166
|
this.data.repositoryProjects = this.data.repositoryProjects.map((project) => ({
|
|
17716
19167
|
...project,
|
|
@@ -17759,7 +19210,6 @@ class SettingsStore {
|
|
|
17759
19210
|
async removeRepositoryProject(projectId) {
|
|
17760
19211
|
this.getProject(projectId);
|
|
17761
19212
|
this.data.repositoryProjects = this.data.repositoryProjects.filter((project) => project.id !== projectId);
|
|
17762
|
-
this.pruneRecentRepositories();
|
|
17763
19213
|
await this.save();
|
|
17764
19214
|
return this.organization;
|
|
17765
19215
|
}
|
|
@@ -17767,19 +19217,44 @@ class SettingsStore {
|
|
|
17767
19217
|
const key = normalizeRepositoryKey(repositoryKey);
|
|
17768
19218
|
if (!key || key.length > MAX_REPOSITORY_KEY_LENGTH || hasControlCharacters$1(key)) throw projectError("Invalid repository.");
|
|
17769
19219
|
const target = projectId === null ? null : this.getProject(projectId);
|
|
17770
|
-
if (
|
|
19220
|
+
if (!this.data.recentRepositories.some((repository) => normalizeRepositoryKey(repository.commonDir) === key)) {
|
|
17771
19221
|
throw projectError("Unknown repository.");
|
|
17772
19222
|
}
|
|
19223
|
+
if (target?.repositoryKeys.includes(key)) return this.organization;
|
|
19224
|
+
if (target && target.repositoryKeys.length >= MAX_REPOSITORIES_PER_PROJECT) throw projectError("This project is full.");
|
|
17773
19225
|
this.data.repositoryProjects = this.data.repositoryProjects.map((project) => ({
|
|
17774
19226
|
...project,
|
|
17775
19227
|
repositoryKeys: project.repositoryKeys.filter((item) => item !== key)
|
|
17776
19228
|
}));
|
|
17777
19229
|
if (target) {
|
|
17778
19230
|
const refreshed = this.getProject(target.id);
|
|
17779
|
-
if (refreshed.repositoryKeys.length >= MAX_REPOSITORIES_PER_PROJECT) throw projectError("This project is full.");
|
|
17780
19231
|
refreshed.repositoryKeys.push(key);
|
|
17781
19232
|
}
|
|
17782
|
-
this.
|
|
19233
|
+
await this.save();
|
|
19234
|
+
return this.organization;
|
|
19235
|
+
}
|
|
19236
|
+
async moveRepositoryProject(projectId, toIndex) {
|
|
19237
|
+
this.data.repositoryProjects = moveToIndex(this.data.repositoryProjects, projectId, toIndex, (project) => project.id);
|
|
19238
|
+
await this.save();
|
|
19239
|
+
return this.organization;
|
|
19240
|
+
}
|
|
19241
|
+
/** Move within the assigned project, or within the unassigned repositories. */
|
|
19242
|
+
async moveRepository(repositoryKey, toIndex) {
|
|
19243
|
+
const key = normalizeRepositoryKey(repositoryKey);
|
|
19244
|
+
const keys = [...new Set(this.data.recentRepositories.map((item) => normalizeRepositoryKey(item.commonDir)))];
|
|
19245
|
+
const known = new Set(keys);
|
|
19246
|
+
const project = this.data.repositoryProjects.find((item) => item.repositoryKeys.includes(key));
|
|
19247
|
+
if (project) {
|
|
19248
|
+
const visible = project.repositoryKeys.filter((item) => known.has(item));
|
|
19249
|
+
project.repositoryKeys = [...moveToIndex(visible, key, toIndex, (item) => item), ...project.repositoryKeys.filter((item) => !known.has(item))];
|
|
19250
|
+
} else {
|
|
19251
|
+
const assigned = new Set(this.data.repositoryProjects.flatMap((item) => item.repositoryKeys));
|
|
19252
|
+
const unassigned = moveToIndex(keys.filter((item) => !assigned.has(item)), key, toIndex, (item) => item);
|
|
19253
|
+
let index = 0;
|
|
19254
|
+
const ordered = keys.map((item) => assigned.has(item) ? item : unassigned[index++]);
|
|
19255
|
+
const positions = new Map(ordered.map((item, position) => [item, position]));
|
|
19256
|
+
this.data.recentRepositories = [...this.data.recentRepositories].sort((a2, b) => positions.get(normalizeRepositoryKey(a2.commonDir)) - positions.get(normalizeRepositoryKey(b.commonDir)));
|
|
19257
|
+
}
|
|
17783
19258
|
await this.save();
|
|
17784
19259
|
return this.organization;
|
|
17785
19260
|
}
|
|
@@ -17891,15 +19366,6 @@ class SettingsStore {
|
|
|
17891
19366
|
throw projectError("A project with this name already exists.");
|
|
17892
19367
|
}
|
|
17893
19368
|
}
|
|
17894
|
-
pruneRecentRepositories() {
|
|
17895
|
-
const assigned = new Set(this.data.repositoryProjects.flatMap((project) => project.repositoryKeys));
|
|
17896
|
-
let unassigned = 0;
|
|
17897
|
-
this.data.recentRepositories = this.data.recentRepositories.filter((repository) => {
|
|
17898
|
-
if (repository.id === this.data.activeRepositoryId || assigned.has(normalizeRepositoryKey(repository.commonDir))) return true;
|
|
17899
|
-
unassigned += 1;
|
|
17900
|
-
return unassigned <= UNASSIGNED_RECENT_LIMIT;
|
|
17901
|
-
});
|
|
17902
|
-
}
|
|
17903
19369
|
}
|
|
17904
19370
|
function validate(value, defaultDoubleControlShortcutEnabled) {
|
|
17905
19371
|
const parsed = settingsRecordSchema.safeParse(value);
|
|
@@ -17909,8 +19375,16 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
|
|
|
17909
19375
|
return data;
|
|
17910
19376
|
}
|
|
17911
19377
|
const input = parsed.data;
|
|
17912
|
-
const recentRepositories = Array.isArray(input.recentRepositories) ? input.recentRepositories.filter(isRecent).map(normalizeRecent)
|
|
19378
|
+
const recentRepositories = Array.isArray(input.recentRepositories) ? input.recentRepositories.filter(isRecent).map(normalizeRecent) : [];
|
|
17913
19379
|
const repositoryProjects = normalizeProjects(input.repositoryProjects);
|
|
19380
|
+
if (input.version !== SETTINGS_SCHEMA_VERSION) {
|
|
19381
|
+
const keys = [...new Set(recentRepositories.map((item) => normalizeRepositoryKey(item.commonDir)))];
|
|
19382
|
+
const known = new Set(keys);
|
|
19383
|
+
for (const project of repositoryProjects) {
|
|
19384
|
+
const assigned = new Set(project.repositoryKeys);
|
|
19385
|
+
project.repositoryKeys = [...keys.filter((key) => assigned.has(key)), ...project.repositoryKeys.filter((key) => !known.has(key))];
|
|
19386
|
+
}
|
|
19387
|
+
}
|
|
17914
19388
|
const filesTreeStates = normalizeFilesTreeStates(input.filesTreeStates);
|
|
17915
19389
|
const openFilesStates = normalizeOpenFilesStates(input.openFilesStates);
|
|
17916
19390
|
const parsedPreferences = preferencesRecordSchema.safeParse(input.preferences);
|
|
@@ -17938,6 +19412,8 @@ function validate(value, defaultDoubleControlShortcutEnabled) {
|
|
|
17938
19412
|
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 };
|
|
17939
19413
|
return {
|
|
17940
19414
|
version: SETTINGS_SCHEMA_VERSION,
|
|
19415
|
+
githubAccounts: normalizeGitHubAccounts(input.githubAccounts),
|
|
19416
|
+
githubDefaultLogin: typeof input.githubDefaultLogin === "string" && githubAccountSelectionSchema.safeParse({ mode: "account", host: "github.com", login: input.githubDefaultLogin }).success ? input.githubDefaultLogin : null,
|
|
17941
19417
|
recentRepositories,
|
|
17942
19418
|
repositoryProjects,
|
|
17943
19419
|
filesTreeStates,
|
|
@@ -17990,6 +19466,15 @@ function normalizeWorktreePath(value) {
|
|
|
17990
19466
|
function cloneProject(project) {
|
|
17991
19467
|
return { ...project, repositoryKeys: [...project.repositoryKeys] };
|
|
17992
19468
|
}
|
|
19469
|
+
function moveToIndex(items, key, toIndex, keyOf) {
|
|
19470
|
+
const fromIndex = items.findIndex((item2) => keyOf(item2) === key);
|
|
19471
|
+
if (fromIndex < 0) throw projectError("Unknown project or repository.");
|
|
19472
|
+
if (!Number.isSafeInteger(toIndex) || toIndex < 0 || toIndex >= items.length) throw projectError("Invalid ordering position.");
|
|
19473
|
+
const next = [...items];
|
|
19474
|
+
const [item] = next.splice(fromIndex, 1);
|
|
19475
|
+
next.splice(toIndex, 0, item);
|
|
19476
|
+
return next;
|
|
19477
|
+
}
|
|
17993
19478
|
function cloneFilesTreeState(state) {
|
|
17994
19479
|
return { ...state, expandedPaths: [...state.expandedPaths] };
|
|
17995
19480
|
}
|
|
@@ -18059,6 +19544,16 @@ function executablePaths(value, strict = false, platform2 = process.platform, ho
|
|
|
18059
19544
|
}
|
|
18060
19545
|
return result;
|
|
18061
19546
|
}
|
|
19547
|
+
function normalizeGitHubAccounts(value) {
|
|
19548
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
19549
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return result;
|
|
19550
|
+
for (const [key, selection] of Object.entries(value).slice(0, 1e3)) {
|
|
19551
|
+
if (!key || key.length > MAX_REPOSITORY_KEY_LENGTH || hasControlCharacters$1(key) || !path.isAbsolute(key)) continue;
|
|
19552
|
+
const parsed = githubAccountSelectionSchema.safeParse(selection);
|
|
19553
|
+
if (parsed.success && parsed.data.mode === "account") result[githubRepositoryKey(key)] = parsed.data;
|
|
19554
|
+
}
|
|
19555
|
+
return result;
|
|
19556
|
+
}
|
|
18062
19557
|
class SystemTrash {
|
|
18063
19558
|
available;
|
|
18064
19559
|
operation;
|
|
@@ -18129,6 +19624,15 @@ class OpenTigRuntime {
|
|
|
18129
19624
|
this.lastNotifiedAt = Date.now();
|
|
18130
19625
|
this.onEvent({ type: "repository.changed", repositoryId, scope });
|
|
18131
19626
|
}
|
|
19627
|
+
publishGitHubAccountsChange(commonDir, defaultOnly = false) {
|
|
19628
|
+
if (this.closePromise) return;
|
|
19629
|
+
const repositories = this.services.repositories.recents();
|
|
19630
|
+
const repositoryIds = defaultOnly ? repositories.filter((repo) => {
|
|
19631
|
+
const selection = this.settings.githubAccount(repo.commonDir);
|
|
19632
|
+
return selection.mode === "account" && selection.useGlobalDefault;
|
|
19633
|
+
}).map((repo) => repo.id) : commonDir ? repositories.filter((repo) => githubRepositoryKey(repo.commonDir) === githubRepositoryKey(commonDir)).map((repo) => repo.id) : void 0;
|
|
19634
|
+
this.onEvent({ type: "github.accounts-changed", ...repositoryIds ? { repositoryIds } : {}, ...defaultOnly ? { inventoryChanged: true } : {} });
|
|
19635
|
+
}
|
|
18132
19636
|
publishActiveRepositoryChange(repository) {
|
|
18133
19637
|
if (!this.closePromise) this.onEvent({ type: "repository.active-changed", repository });
|
|
18134
19638
|
}
|
|
@@ -18187,10 +19691,25 @@ async function createOpenTigRuntime(options) {
|
|
|
18187
19691
|
];
|
|
18188
19692
|
const aiLog = new AiLogStore(options.aiLogPath);
|
|
18189
19693
|
await aiLog.load();
|
|
18190
|
-
const ai = new CommitMessageService(operations, providers, aiLog, () => cliResolver.invalidate()
|
|
19694
|
+
const ai = new CommitMessageService(operations, providers, aiLog, () => cliResolver.invalidate(), {
|
|
19695
|
+
store: new AiStatusStore(path.join(path.dirname(options.settingsPath), "ai-statuses.json")),
|
|
19696
|
+
key: () => aiExecutablePathsKey(settings.preferences.aiExecutablePaths)
|
|
19697
|
+
});
|
|
18191
19698
|
const prDrafts = new PullRequestDraftService(operations, providers, aiLog);
|
|
18192
|
-
const github = new GitHubService(cliResolver, cliRunner, git, repositories);
|
|
18193
19699
|
let runtime = null;
|
|
19700
|
+
const githubAccounts = new GitHubAccountsService(
|
|
19701
|
+
cliResolver,
|
|
19702
|
+
cliRunner,
|
|
19703
|
+
git,
|
|
19704
|
+
settings,
|
|
19705
|
+
new GitHubStatusStore(path.join(path.dirname(options.settingsPath), "github-status.json")),
|
|
19706
|
+
(repository, defaultOnly) => runtime?.publishGitHubAccountsChange(repository?.commonDir, defaultOnly)
|
|
19707
|
+
);
|
|
19708
|
+
const gitAccess = new RepositoryGitAccess(git, repositories, githubAccounts, cliRunner);
|
|
19709
|
+
gitAccess.install();
|
|
19710
|
+
operations.access = gitAccess;
|
|
19711
|
+
const github = new GitHubService(cliResolver, cliRunner, git, repositories, githubAccounts);
|
|
19712
|
+
github.gitAccess = gitAccess;
|
|
18194
19713
|
const events = {
|
|
18195
19714
|
repositoryChanged: (repositoryId, scope) => {
|
|
18196
19715
|
runtime?.publishRepositoryChange(repositoryId, scope);
|
|
@@ -18357,6 +19876,10 @@ const IPC = {
|
|
|
18357
19876
|
projectRename: "projects:rename",
|
|
18358
19877
|
projectRemove: "projects:remove",
|
|
18359
19878
|
projectAssign: "projects:assign",
|
|
19879
|
+
projectMove: "projects:move",
|
|
19880
|
+
projectMoveRepository: "projects:move-repository",
|
|
19881
|
+
repositoryPreparePath: "repository:prepare-path",
|
|
19882
|
+
repositoryCompleteSetup: "repository:complete-setup",
|
|
18360
19883
|
repositoryOpenPath: "repository:open-path",
|
|
18361
19884
|
repositoryBrowseDirectories: "repository:browse-directories",
|
|
18362
19885
|
repositoryOpenRecent: "repository:open-recent",
|
|
@@ -18399,18 +19922,25 @@ const IPC = {
|
|
|
18399
19922
|
commitUndoLatest: "commit:undo-latest",
|
|
18400
19923
|
commitsList: "commits:list",
|
|
18401
19924
|
commitsFiles: "commits:files",
|
|
19925
|
+
commitAuthorship: "commits:authorship",
|
|
19926
|
+
commitSetAuthorship: "commits:set-authorship",
|
|
18402
19927
|
branchesList: "refs:branches",
|
|
18403
19928
|
branchSwitch: "refs:switch",
|
|
18404
19929
|
worktreesList: "refs:worktrees",
|
|
18405
19930
|
worktreeSelect: "refs:select-worktree",
|
|
18406
19931
|
refsPull: "refs:pull",
|
|
18407
19932
|
refsPush: "refs:push",
|
|
19933
|
+
refsPushContext: "refs:push-context",
|
|
18408
19934
|
refsFetch: "refs:fetch",
|
|
18409
19935
|
localRefsSnapshot: "refs:local-snapshot",
|
|
18410
19936
|
branchDetails: "refs:branch-details",
|
|
18411
19937
|
worktreeDetails: "refs:worktree-details",
|
|
18412
19938
|
branchDelete: "refs:delete-branch",
|
|
18413
19939
|
worktreeRemove: "refs:remove-worktree",
|
|
19940
|
+
remoteBranchDetails: "refs:remote-branch-details",
|
|
19941
|
+
trackingBranchCreate: "refs:create-tracking-branch",
|
|
19942
|
+
remoteBranchDelete: "refs:delete-remote-branch",
|
|
19943
|
+
branchesFetch: "refs:fetch-branches",
|
|
18414
19944
|
aiStatuses: "ai:statuses",
|
|
18415
19945
|
aiGenerateCommitMessage: "ai:generate-commit-message",
|
|
18416
19946
|
aiCancelGeneration: "ai:cancel-generation",
|
|
@@ -18419,6 +19949,10 @@ const IPC = {
|
|
|
18419
19949
|
diagnosticsList: "diagnostics:list",
|
|
18420
19950
|
diagnosticsClear: "diagnostics:clear",
|
|
18421
19951
|
diagnosticsRecord: "diagnostics:record",
|
|
19952
|
+
githubAccountsStatus: "github:accounts-status",
|
|
19953
|
+
githubSetDefaultAccount: "github:set-default-account",
|
|
19954
|
+
githubRepositoryAccount: "github:repository-account",
|
|
19955
|
+
githubSetRepositoryAccount: "github:set-repository-account",
|
|
18422
19956
|
githubStatus: "github:status",
|
|
18423
19957
|
githubRepositoryInfo: "github:repository-info",
|
|
18424
19958
|
githubPrForBranch: "github:pr-for-branch",
|
|
@@ -18445,6 +19979,10 @@ const OPEN_TIG_SERVER_COMMANDS = {
|
|
|
18445
19979
|
"projects.rename": command(IPC.projectRename, "project-rename", true),
|
|
18446
19980
|
"projects.remove": command(IPC.projectRemove, "project-remove", true),
|
|
18447
19981
|
"projects.assign": command(IPC.projectAssign, "project-assign", true),
|
|
19982
|
+
"projects.moveProject": command(IPC.projectMove, "project-move", true),
|
|
19983
|
+
"projects.moveRepository": command(IPC.projectMoveRepository, "project-move-repository", true),
|
|
19984
|
+
"repository.preparePath": command(IPC.repositoryPreparePath, "prepare-path", false),
|
|
19985
|
+
"repository.completeSetup": command(IPC.repositoryCompleteSetup, "complete-setup", true),
|
|
18448
19986
|
"repository.openPath": command(IPC.repositoryOpenPath, "open-path", true),
|
|
18449
19987
|
"repository.browseDirectories": command(IPC.repositoryBrowseDirectories, "browse-directories", false),
|
|
18450
19988
|
"repository.openRecent": command(IPC.repositoryOpenRecent, "open-recent", true),
|
|
@@ -18484,6 +20022,8 @@ const OPEN_TIG_SERVER_COMMANDS = {
|
|
|
18484
20022
|
"index.updateConflict": command(IPC.indexUpdateConflict, "update-conflict", true, { maxRequestBytes: CONTENT_MAX_REQUEST_BYTES }),
|
|
18485
20023
|
"index.resolveConflict": command(IPC.indexResolveConflict, "resolve-conflict", true, { maxRequestBytes: CONTENT_MAX_REQUEST_BYTES }),
|
|
18486
20024
|
"commits.create": command(IPC.commitCreate, "commit", true),
|
|
20025
|
+
"commits.authorship": command(IPC.commitAuthorship, "commit-authorship-read", false),
|
|
20026
|
+
"commits.setAuthorship": command(IPC.commitSetAuthorship, "commit-authorship-set", true),
|
|
18487
20027
|
"commits.undoLatest": command(IPC.commitUndoLatest, "undo-latest-commit", true),
|
|
18488
20028
|
"commits.list": command(IPC.commitsList, "history", false),
|
|
18489
20029
|
"commits.files": command(IPC.commitsFiles, "commit-files", false),
|
|
@@ -18492,10 +20032,15 @@ const OPEN_TIG_SERVER_COMMANDS = {
|
|
|
18492
20032
|
"refs.listWorktrees": command(IPC.worktreesList, "worktrees", false),
|
|
18493
20033
|
"refs.selectWorktree": command(IPC.worktreeSelect, "select-worktree", true),
|
|
18494
20034
|
"refs.pull": command(IPC.refsPull, "pull", true),
|
|
20035
|
+
"refs.pushContext": command(IPC.refsPushContext, "push-context", false),
|
|
18495
20036
|
"refs.push": command(IPC.refsPush, "push", true),
|
|
18496
20037
|
"refs.fetch": command(IPC.refsFetch, "fetch", true),
|
|
18497
20038
|
"refs.localRefsSnapshot": command(IPC.localRefsSnapshot, "local-refs-snapshot", false),
|
|
18498
20039
|
"refs.branchDetails": command(IPC.branchDetails, "branch-details", false),
|
|
20040
|
+
"refs.remoteBranchDetails": command(IPC.remoteBranchDetails, "remote-branch-details", false, { timeoutMs: 12e4 }),
|
|
20041
|
+
"refs.createTrackingBranch": command(IPC.trackingBranchCreate, "create-tracking-branch", true),
|
|
20042
|
+
"refs.deleteRemoteBranch": command(IPC.remoteBranchDelete, "delete-remote-branch", true, { timeoutMs: 18e4 }),
|
|
20043
|
+
"refs.fetchBranches": command(IPC.branchesFetch, "fetch-branches", true, { timeoutMs: 18e4 }),
|
|
18499
20044
|
"refs.worktreeDetails": command(IPC.worktreeDetails, "worktree-details", false),
|
|
18500
20045
|
"refs.deleteBranch": command(IPC.branchDelete, "delete-branch", true),
|
|
18501
20046
|
"refs.removeWorktree": command(IPC.worktreeRemove, "remove-worktree", true),
|
|
@@ -18504,6 +20049,10 @@ const OPEN_TIG_SERVER_COMMANDS = {
|
|
|
18504
20049
|
"ai.log": command(IPC.aiLog, "ai-log", false),
|
|
18505
20050
|
"ai.clearLog": command(IPC.aiClearLog, "ai-clear-log", true),
|
|
18506
20051
|
"ai.cancelGeneration": command(IPC.aiCancelGeneration, "ai-cancel-generation", true),
|
|
20052
|
+
"github.accountsStatus": command(IPC.githubAccountsStatus, "gh-accounts-status", false, { timeoutMs: 9e4 }),
|
|
20053
|
+
"github.repositoryAccount": command(IPC.githubRepositoryAccount, "gh-repository-account", false, { timeoutMs: 6e4 }),
|
|
20054
|
+
"github.setDefaultAccount": command(IPC.githubSetDefaultAccount, "gh-set-default-account", true),
|
|
20055
|
+
"github.setRepositoryAccount": command(IPC.githubSetRepositoryAccount, "gh-set-repository-account", true),
|
|
18507
20056
|
"github.status": command(IPC.githubStatus, "gh-status", false),
|
|
18508
20057
|
"github.repositoryInfo": command(IPC.githubRepositoryInfo, "gh-repository-info", false),
|
|
18509
20058
|
"github.findPullRequestForBranch": command(IPC.githubPrForBranch, "gh-pr-for-branch", false),
|
|
@@ -18730,10 +20279,10 @@ async function inspectRepository(path2, git) {
|
|
|
18730
20279
|
}
|
|
18731
20280
|
const requestRecordSchema = looseObject({});
|
|
18732
20281
|
function boundedStringSchema(maxLength, options = {}) {
|
|
18733
|
-
let
|
|
18734
|
-
if (options.empty !== true)
|
|
18735
|
-
if (options.controls === false) return
|
|
18736
|
-
return
|
|
20282
|
+
let schema2 = string().max(maxLength).refine((value) => !value.includes("\0"));
|
|
20283
|
+
if (options.empty !== true) schema2 = schema2.min(1);
|
|
20284
|
+
if (options.controls === false) return schema2;
|
|
20285
|
+
return schema2.refine((value) => ![...value].some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127));
|
|
18737
20286
|
}
|
|
18738
20287
|
const booleanWithDefaultSchema = (fallback) => boolean().optional().default(fallback);
|
|
18739
20288
|
function parseRecord(value) {
|
|
@@ -18884,6 +20433,22 @@ function deleteBranchArg(value, operation) {
|
|
|
18884
20433
|
force: booleanArg(request.force, operation)
|
|
18885
20434
|
};
|
|
18886
20435
|
}
|
|
20436
|
+
function createTrackingBranchArg(value) {
|
|
20437
|
+
const operation = "create-tracking-branch";
|
|
20438
|
+
const request = requestObject(value, operation);
|
|
20439
|
+
return {
|
|
20440
|
+
...branchDetailsArg(request, operation),
|
|
20441
|
+
expectedOid: oidArg(request.expectedOid, operation),
|
|
20442
|
+
localName: refsString(request.localName, operation, MAX_BRANCH_REF_LENGTH)
|
|
20443
|
+
};
|
|
20444
|
+
}
|
|
20445
|
+
function deleteRemoteBranchArg(value) {
|
|
20446
|
+
const operation = "delete-remote-branch";
|
|
20447
|
+
const request = requestObject(value, operation);
|
|
20448
|
+
const destinationId = refsString(request.destinationId, operation, 64);
|
|
20449
|
+
if (!/^[0-9a-f]{64}$/.test(destinationId)) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation, message: "Invalid remote destination." });
|
|
20450
|
+
return { ...branchDetailsArg(request, operation), expectedOid: oidArg(request.expectedOid, operation), destinationId };
|
|
20451
|
+
}
|
|
18887
20452
|
function removeWorktreeArg(value, operation) {
|
|
18888
20453
|
const request = requestObject(value, operation);
|
|
18889
20454
|
return {
|
|
@@ -18981,7 +20546,16 @@ function createPullRequestArg(value) {
|
|
|
18981
20546
|
if (typeof input.base !== "string" || !input.base || input.base.length > 300 || input.base.includes("\0")) throw invalidGh(operation);
|
|
18982
20547
|
if (typeof input.draft !== "boolean") throw invalidGh(operation);
|
|
18983
20548
|
if (typeof input.repositoryId !== "string" || !input.repositoryId || input.repositoryId.length > 64) throw invalidGh(operation);
|
|
18984
|
-
|
|
20549
|
+
const expected = input.expectedAccount;
|
|
20550
|
+
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);
|
|
20551
|
+
return {
|
|
20552
|
+
repositoryId: input.repositoryId,
|
|
20553
|
+
title: input.title.trim(),
|
|
20554
|
+
body: input.body,
|
|
20555
|
+
base: input.base,
|
|
20556
|
+
draft: input.draft,
|
|
20557
|
+
...expected ? { expectedAccount: { login: expected.login, revision: expected.revision } } : {}
|
|
20558
|
+
};
|
|
18985
20559
|
}
|
|
18986
20560
|
function generatePullRequestDraftArg(value) {
|
|
18987
20561
|
const operation = "ai-pr-draft";
|
|
@@ -19023,6 +20597,10 @@ function invalidAi(operation) {
|
|
|
19023
20597
|
function invalidProject(operation) {
|
|
19024
20598
|
return new GitOperationError({ code: "INVALID_ARGUMENT", operation, message: "Invalid project data." });
|
|
19025
20599
|
}
|
|
20600
|
+
function orderingIndexArg(value, operation) {
|
|
20601
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw invalidProject(operation);
|
|
20602
|
+
return value;
|
|
20603
|
+
}
|
|
19026
20604
|
function registerServerCommands(registry2, services, host) {
|
|
19027
20605
|
const serverDefinitions = new Map(
|
|
19028
20606
|
Object.values(OPEN_TIG_SERVER_COMMANDS).map((definition) => [definition.command, definition])
|
|
@@ -19069,8 +20647,9 @@ function registerServerCommands(registry2, services, host) {
|
|
|
19069
20647
|
};
|
|
19070
20648
|
});
|
|
19071
20649
|
handle(IPC.preferences, "preferences", async (partial2) => {
|
|
20650
|
+
const previousPaths = aiExecutablePathsKey(services.settings.preferences.aiExecutablePaths);
|
|
19072
20651
|
const preferences = await services.settings.setPreferences(partial2 ?? {});
|
|
19073
|
-
if (
|
|
20652
|
+
if (previousPaths !== aiExecutablePathsKey(preferences.aiExecutablePaths)) services.ai.invalidateStatuses();
|
|
19074
20653
|
return preferences;
|
|
19075
20654
|
});
|
|
19076
20655
|
handle(IPC.filesTreeStateUpdate, "files-tree-state", (repositoryId, expandedPaths) => {
|
|
@@ -19085,6 +20664,22 @@ function registerServerCommands(registry2, services, host) {
|
|
|
19085
20664
|
handle(IPC.projectRename, "project-rename", (projectId, name) => services.settings.renameRepositoryProject(projectIdArg(projectId, "project-rename"), projectNameArg(name, "project-rename")));
|
|
19086
20665
|
handle(IPC.projectRemove, "project-remove", (projectId) => services.settings.removeRepositoryProject(projectIdArg(projectId, "project-remove")));
|
|
19087
20666
|
handle(IPC.projectAssign, "project-assign", (repositoryKey, projectId) => services.settings.assignRepositoryProject(repositoryKeyArg(repositoryKey, "project-assign"), nullableProjectIdArg(projectId, "project-assign")));
|
|
20667
|
+
handle(IPC.projectMove, "project-move", (projectId, toIndex) => services.settings.moveRepositoryProject(projectIdArg(projectId, "project-move"), orderingIndexArg(toIndex, "project-move")));
|
|
20668
|
+
handle(IPC.projectMoveRepository, "project-move-repository", (repositoryKey, toIndex) => services.settings.moveRepository(repositoryKeyArg(repositoryKey, "project-move-repository"), orderingIndexArg(toIndex, "project-move-repository")));
|
|
20669
|
+
handle(IPC.repositoryPreparePath, "prepare-path", (path2) => services.repositories.preparePath(stringArg(path2, "prepare-path", 32768)));
|
|
20670
|
+
handleWithContext(IPC.repositoryCompleteSetup, "complete-setup", async (context, id, revision2) => {
|
|
20671
|
+
const repositoryId = stringArg(id, "complete-setup", 64);
|
|
20672
|
+
if (typeof revision2 !== "number" || !Number.isSafeInteger(revision2) || revision2 < 0) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "complete-setup", message: "Invalid account revision." });
|
|
20673
|
+
const repository = await services.repositories.completeSetup(repositoryId, () => {
|
|
20674
|
+
if (revision2 !== services.github.accounts.revisionFor(services.repositories.get(repositoryId))) {
|
|
20675
|
+
throw new Error("The repository account changed. Reload setup and review it again.");
|
|
20676
|
+
}
|
|
20677
|
+
});
|
|
20678
|
+
fileClipboardState(context).pendingCut = null;
|
|
20679
|
+
services.watcher.start(repository);
|
|
20680
|
+
services.events.activeRepositoryChanged(repository);
|
|
20681
|
+
return repository;
|
|
20682
|
+
});
|
|
19088
20683
|
handleWithContext(IPC.repositoryOpenPath, "open-path", (context, selectedPath) => openRepositoryPath(
|
|
19089
20684
|
stringArg(selectedPath, "open-path", 32768),
|
|
19090
20685
|
context
|
|
@@ -19329,6 +20924,17 @@ function registerServerCommands(registry2, services, host) {
|
|
|
19329
20924
|
textArg(content, "update-conflict", 8 * 1024 * 1024)
|
|
19330
20925
|
));
|
|
19331
20926
|
handle(IPC.commitCreate, "commit", (id, message) => services.operations.createCommit(stringArg(id, "commit", 64), stringArg(message, "commit", 1e5)));
|
|
20927
|
+
handle(IPC.commitAuthorship, "commit-authorship-read", (id) => services.operations.getCommitAuthorship(stringArg(id, "commit-authorship-read", 64)));
|
|
20928
|
+
handle(IPC.commitSetAuthorship, "commit-authorship-set", async (id, input) => {
|
|
20929
|
+
const repositoryId = stringArg(id, "commit-authorship-set", 64);
|
|
20930
|
+
const parsed = commitAuthorshipInputSchema.safeParse(input);
|
|
20931
|
+
if (!parsed.success) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "commit-authorship-set", message: "Enter a valid name, email and reviewed identity." });
|
|
20932
|
+
const result = await services.operations.setCommitAuthorship(repositoryId, parsed.data);
|
|
20933
|
+
const repository = services.repositories.get(repositoryId);
|
|
20934
|
+
const ids = /* @__PURE__ */ new Set([repositoryId, ...services.repositories.recents().filter((item) => item.commonDir === repository.commonDir).map((item) => item.id)]);
|
|
20935
|
+
for (const siblingId of ids) services.events.repositoryChanged(siblingId, "unknown");
|
|
20936
|
+
return result;
|
|
20937
|
+
});
|
|
19332
20938
|
handle(IPC.commitUndoLatest, "undo-latest-commit", (id, expectedOid) => services.operations.undoLatestCommit(
|
|
19333
20939
|
stringArg(id, "undo-latest-commit", 64),
|
|
19334
20940
|
oidArg(expectedOid, "undo-latest-commit")
|
|
@@ -19353,6 +20959,13 @@ function registerServerCommands(registry2, services, host) {
|
|
|
19353
20959
|
const { repositoryId, fullName } = branchDetailsArg(request, "branch-details");
|
|
19354
20960
|
return services.operations.branchDetails(repositoryId, fullName);
|
|
19355
20961
|
});
|
|
20962
|
+
handle(IPC.remoteBranchDetails, "remote-branch-details", (request) => {
|
|
20963
|
+
const { repositoryId, fullName } = branchDetailsArg(request, "remote-branch-details");
|
|
20964
|
+
return services.operations.remoteBranchDetails(repositoryId, fullName);
|
|
20965
|
+
});
|
|
20966
|
+
handle(IPC.trackingBranchCreate, "create-tracking-branch", (request) => services.operations.createTrackingBranch(createTrackingBranchArg(request)));
|
|
20967
|
+
handle(IPC.remoteBranchDelete, "delete-remote-branch", (request) => services.operations.deleteRemoteBranch(deleteRemoteBranchArg(request)));
|
|
20968
|
+
handle(IPC.branchesFetch, "fetch-branches", (id) => services.operations.fetchBranches(stringArg(id, "fetch-branches", 64)));
|
|
19356
20969
|
handle(IPC.worktreeDetails, "worktree-details", (request) => {
|
|
19357
20970
|
const { repositoryId, path: path2 } = worktreeDetailsArg(request, "worktree-details");
|
|
19358
20971
|
return services.operations.worktreeDetails(repositoryId, path2);
|
|
@@ -19366,7 +20979,8 @@ function registerServerCommands(registry2, services, host) {
|
|
|
19366
20979
|
return services.operations.removeWorktree(repositoryId, path2, expectedOid, force, deleteBranch);
|
|
19367
20980
|
});
|
|
19368
20981
|
handle(IPC.refsPull, "pull", (id) => services.operations.pull(stringArg(id, "pull", 64)));
|
|
19369
|
-
handle(IPC.
|
|
20982
|
+
handle(IPC.refsPushContext, "push-context", (id, remote) => services.operations.publicationContext(stringArg(id, "push-context", 64), remote == null ? void 0 : stringArg(remote, "push-context", 512)));
|
|
20983
|
+
handle(IPC.refsPush, "push", (id, publish, expected) => services.operations.push(stringArg(id, "push", 64), publishBranchArg(publish), expected == null ? void 0 : stringArg(expected, "push", 64)));
|
|
19370
20984
|
handle(IPC.refsFetch, "fetch", (id) => services.operations.fetch(stringArg(id, "fetch", 64)));
|
|
19371
20985
|
handle(IPC.aiStatuses, "ai-statuses", (forceRefresh) => services.ai.statuses(booleanArg(forceRefresh, "ai-statuses")));
|
|
19372
20986
|
handleWithContext(IPC.aiGenerateCommitMessage, "ai-generate-commit-message", (context, input) => services.ai.generate(generateCommitMessageArg(input), context.signal));
|
|
@@ -19390,6 +21004,15 @@ function registerServerCommands(registry2, services, host) {
|
|
|
19390
21004
|
handle(IPC.aiCancelGeneration, "ai-cancel-generation", (requestId2) => {
|
|
19391
21005
|
services.ai.cancel(aiString(requestId2, "ai-cancel-generation", 100, true));
|
|
19392
21006
|
});
|
|
21007
|
+
handle(IPC.githubAccountsStatus, "gh-accounts-status", (forceRefresh) => services.github.accounts.status(booleanArg(forceRefresh, "gh-accounts-status")));
|
|
21008
|
+
handle(IPC.githubSetDefaultAccount, "gh-set-default-account", (login2) => services.github.accounts.setDefaultAccount(stringArg(login2, "gh-set-default-account", 39)));
|
|
21009
|
+
handle(IPC.githubRepositoryAccount, "gh-repository-account", (id, forceRefresh) => services.github.repositoryAccount(stringArg(id, "gh-repository-account", 64), booleanArg(forceRefresh, "gh-repository-account")));
|
|
21010
|
+
handle(IPC.githubSetRepositoryAccount, "gh-set-repository-account", (id, selection, expectedRevision) => {
|
|
21011
|
+
const parsed = githubAccountSelectionSchema.safeParse(selection);
|
|
21012
|
+
if (expectedRevision !== void 0 && (typeof expectedRevision !== "number" || !Number.isSafeInteger(expectedRevision) || expectedRevision < 0)) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "gh-set-repository-account", message: "Invalid account revision." });
|
|
21013
|
+
if (!parsed.success) throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "gh-set-repository-account", message: "Invalid GitHub account selection." });
|
|
21014
|
+
return services.github.setRepositoryAccount(stringArg(id, "gh-set-repository-account", 64), parsed.data, typeof expectedRevision === "number" ? expectedRevision : void 0);
|
|
21015
|
+
});
|
|
19393
21016
|
handle(IPC.githubStatus, "gh-status", (forceRefresh) => services.github.status(booleanArg(forceRefresh, "gh-status")));
|
|
19394
21017
|
handle(IPC.githubRepositoryInfo, "gh-repository-info", (id) => services.github.repositoryInfo(stringArg(id, "gh-repository-info", 64)));
|
|
19395
21018
|
handle(IPC.githubPrForBranch, "gh-pr-for-branch", (id, branchName) => services.github.findPullRequestForBranch(
|
|
@@ -19439,7 +21062,7 @@ function clipboardImageArg(value) {
|
|
|
19439
21062
|
if (value instanceof Uint8Array) return value;
|
|
19440
21063
|
throw new GitOperationError({ code: "INVALID_ARGUMENT", operation: "paste-entries", message: "Invalid clipboard image." });
|
|
19441
21064
|
}
|
|
19442
|
-
const protocolVersion =
|
|
21065
|
+
const protocolVersion = 4;
|
|
19443
21066
|
const protocolMetadata = {
|
|
19444
21067
|
protocolVersion
|
|
19445
21068
|
};
|
|
@@ -19455,6 +21078,85 @@ function preferredServerPort(profile) {
|
|
|
19455
21078
|
}
|
|
19456
21079
|
const AUTH_DATA_VERSION = 4;
|
|
19457
21080
|
const BROWSER_SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
|
|
21081
|
+
async function hasActiveBrowserSessions(directory, now = Date.now) {
|
|
21082
|
+
const { sessions } = await loadSessions(path.join(directory, "sessions.json"));
|
|
21083
|
+
return sessions.some((session) => session.kind !== "desktop" && session.expiresAt !== null && now() < Date.parse(session.expiresAt));
|
|
21084
|
+
}
|
|
21085
|
+
async function loadSessions(filePath) {
|
|
21086
|
+
try {
|
|
21087
|
+
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
21088
|
+
if (isLegacyAuthData(parsed)) {
|
|
21089
|
+
await chmod(filePath, 384);
|
|
21090
|
+
return {
|
|
21091
|
+
sessions: parsed.sessions.map((session) => ({
|
|
21092
|
+
...session,
|
|
21093
|
+
kind: "legacy",
|
|
21094
|
+
expiresAt: sessionExpiry("legacy", session.createdAt),
|
|
21095
|
+
clientName: "Legacy browser session",
|
|
21096
|
+
deviceType: "unknown",
|
|
21097
|
+
os: null,
|
|
21098
|
+
browser: null,
|
|
21099
|
+
remoteAddress: null,
|
|
21100
|
+
viaProxy: false,
|
|
21101
|
+
lastConnectedAt: null
|
|
21102
|
+
})),
|
|
21103
|
+
migrated: true
|
|
21104
|
+
};
|
|
21105
|
+
}
|
|
21106
|
+
if (isVersionTwoAuthData(parsed)) {
|
|
21107
|
+
await chmod(filePath, 384);
|
|
21108
|
+
return {
|
|
21109
|
+
sessions: parsed.sessions.map((session) => ({
|
|
21110
|
+
...session,
|
|
21111
|
+
expiresAt: sessionExpiry(session.kind, session.createdAt),
|
|
21112
|
+
clientName: session.kind === "legacy" ? "Legacy browser session" : session.clientName,
|
|
21113
|
+
deviceType: session.kind === "desktop" ? "desktop" : "unknown",
|
|
21114
|
+
os: null,
|
|
21115
|
+
browser: session.kind === "browser" ? session.clientName : null,
|
|
21116
|
+
viaProxy: false,
|
|
21117
|
+
lastConnectedAt: null
|
|
21118
|
+
})),
|
|
21119
|
+
migrated: true
|
|
21120
|
+
};
|
|
21121
|
+
}
|
|
21122
|
+
if (isVersionThreeAuthData(parsed)) {
|
|
21123
|
+
await chmod(filePath, 384);
|
|
21124
|
+
return {
|
|
21125
|
+
sessions: parsed.sessions.map((session) => ({ ...session, expiresAt: sessionExpiry(session.kind, session.createdAt) })),
|
|
21126
|
+
migrated: true
|
|
21127
|
+
};
|
|
21128
|
+
}
|
|
21129
|
+
if (!isAuthData(parsed)) throw corrupt$1("session data");
|
|
21130
|
+
await chmod(filePath, 384);
|
|
21131
|
+
return { sessions: parsed.sessions.map((session) => ({ ...session })), migrated: false };
|
|
21132
|
+
} catch (error) {
|
|
21133
|
+
if (error.code === "ENOENT") return { sessions: [], migrated: false };
|
|
21134
|
+
if (error instanceof SyntaxError) throw corrupt$1("session data");
|
|
21135
|
+
throw error;
|
|
21136
|
+
}
|
|
21137
|
+
}
|
|
21138
|
+
function isAuthData(value) {
|
|
21139
|
+
const data = value;
|
|
21140
|
+
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)));
|
|
21141
|
+
}
|
|
21142
|
+
function isVersionThreeAuthData(value) {
|
|
21143
|
+
const data = value;
|
|
21144
|
+
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)))));
|
|
21145
|
+
}
|
|
21146
|
+
function isVersionTwoAuthData(value) {
|
|
21147
|
+
const data = value;
|
|
21148
|
+
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))));
|
|
21149
|
+
}
|
|
21150
|
+
function isLegacyAuthData(value) {
|
|
21151
|
+
const data = value;
|
|
21152
|
+
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))));
|
|
21153
|
+
}
|
|
21154
|
+
function corrupt$1(name) {
|
|
21155
|
+
return new Error(`OpenTig authentication ${name} is corrupt.`);
|
|
21156
|
+
}
|
|
21157
|
+
function sessionExpiry(kind, createdAt) {
|
|
21158
|
+
return kind === "desktop" ? null : new Date(Date.parse(createdAt) + BROWSER_SESSION_MAX_AGE_SECONDS * 1e3).toISOString();
|
|
21159
|
+
}
|
|
19458
21160
|
const SECRET_BYTES = 32;
|
|
19459
21161
|
const SECRET_FILE = "server-secret";
|
|
19460
21162
|
const SESSIONS_FILE = "sessions.json";
|
|
@@ -19470,6 +21172,10 @@ class PersistentAuthStore {
|
|
|
19470
21172
|
now;
|
|
19471
21173
|
sessions;
|
|
19472
21174
|
mutationQueue = Promise.resolve();
|
|
21175
|
+
/** Inspect an existing installation without issuing, revoking, or writing sessions. */
|
|
21176
|
+
static async hasActiveBrowserSessions(directory, now = Date.now) {
|
|
21177
|
+
return hasActiveBrowserSessions(directory, now);
|
|
21178
|
+
}
|
|
19473
21179
|
static async open(directory, now = Date.now) {
|
|
19474
21180
|
const resolved = path.resolve(directory);
|
|
19475
21181
|
await mkdir(resolved, { recursive: true, mode: 448 });
|
|
@@ -19527,9 +21233,9 @@ class PersistentAuthStore {
|
|
|
19527
21233
|
});
|
|
19528
21234
|
}
|
|
19529
21235
|
renewBrowserSession(token) {
|
|
19530
|
-
const
|
|
21236
|
+
const digest2 = this.digestCredential(token);
|
|
19531
21237
|
return this.mutate((sessions) => {
|
|
19532
|
-
const index = sessions.findIndex((session2) => session2.kind !== "desktop" && constantTimeEqual(
|
|
21238
|
+
const index = sessions.findIndex((session2) => session2.kind !== "desktop" && constantTimeEqual(digest2, session2.digest) && this.isActive(session2));
|
|
19533
21239
|
if (index < 0) return { next: sessions, result: false };
|
|
19534
21240
|
const session = sessions[index];
|
|
19535
21241
|
const expiresAt = sessionExpiry(session.kind, new Date(this.now()).toISOString());
|
|
@@ -19659,75 +21365,6 @@ async function loadOrCreateSecret(filePath) {
|
|
|
19659
21365
|
throw error;
|
|
19660
21366
|
}
|
|
19661
21367
|
}
|
|
19662
|
-
async function loadSessions(filePath) {
|
|
19663
|
-
try {
|
|
19664
|
-
const parsed = JSON.parse(await readFile(filePath, "utf8"));
|
|
19665
|
-
if (isLegacyAuthData(parsed)) {
|
|
19666
|
-
await chmod(filePath, 384);
|
|
19667
|
-
return {
|
|
19668
|
-
sessions: parsed.sessions.map((session) => ({
|
|
19669
|
-
...session,
|
|
19670
|
-
kind: "legacy",
|
|
19671
|
-
expiresAt: sessionExpiry("legacy", session.createdAt),
|
|
19672
|
-
clientName: "Legacy browser session",
|
|
19673
|
-
deviceType: "unknown",
|
|
19674
|
-
os: null,
|
|
19675
|
-
browser: null,
|
|
19676
|
-
remoteAddress: null,
|
|
19677
|
-
viaProxy: false,
|
|
19678
|
-
lastConnectedAt: null
|
|
19679
|
-
})),
|
|
19680
|
-
migrated: true
|
|
19681
|
-
};
|
|
19682
|
-
}
|
|
19683
|
-
if (isVersionTwoAuthData(parsed)) {
|
|
19684
|
-
await chmod(filePath, 384);
|
|
19685
|
-
return {
|
|
19686
|
-
sessions: parsed.sessions.map((session) => ({
|
|
19687
|
-
...session,
|
|
19688
|
-
expiresAt: sessionExpiry(session.kind, session.createdAt),
|
|
19689
|
-
clientName: session.kind === "legacy" ? "Legacy browser session" : session.clientName,
|
|
19690
|
-
deviceType: session.kind === "desktop" ? "desktop" : "unknown",
|
|
19691
|
-
os: null,
|
|
19692
|
-
browser: session.kind === "browser" ? session.clientName : null,
|
|
19693
|
-
viaProxy: false,
|
|
19694
|
-
lastConnectedAt: null
|
|
19695
|
-
})),
|
|
19696
|
-
migrated: true
|
|
19697
|
-
};
|
|
19698
|
-
}
|
|
19699
|
-
if (isVersionThreeAuthData(parsed)) {
|
|
19700
|
-
await chmod(filePath, 384);
|
|
19701
|
-
return {
|
|
19702
|
-
sessions: parsed.sessions.map((session) => ({ ...session, expiresAt: sessionExpiry(session.kind, session.createdAt) })),
|
|
19703
|
-
migrated: true
|
|
19704
|
-
};
|
|
19705
|
-
}
|
|
19706
|
-
if (!isAuthData(parsed)) throw corrupt("session data");
|
|
19707
|
-
await chmod(filePath, 384);
|
|
19708
|
-
return { sessions: parsed.sessions.map((session) => ({ ...session })), migrated: false };
|
|
19709
|
-
} catch (error) {
|
|
19710
|
-
if (error.code === "ENOENT") return { sessions: [], migrated: false };
|
|
19711
|
-
if (error instanceof SyntaxError) throw corrupt("session data");
|
|
19712
|
-
throw error;
|
|
19713
|
-
}
|
|
19714
|
-
}
|
|
19715
|
-
function isAuthData(value) {
|
|
19716
|
-
const data = value;
|
|
19717
|
-
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)));
|
|
19718
|
-
}
|
|
19719
|
-
function isVersionThreeAuthData(value) {
|
|
19720
|
-
const data = value;
|
|
19721
|
-
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)))));
|
|
19722
|
-
}
|
|
19723
|
-
function isVersionTwoAuthData(value) {
|
|
19724
|
-
const data = value;
|
|
19725
|
-
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))));
|
|
19726
|
-
}
|
|
19727
|
-
function isLegacyAuthData(value) {
|
|
19728
|
-
const data = value;
|
|
19729
|
-
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))));
|
|
19730
|
-
}
|
|
19731
21368
|
function constantTimeEqual(left, right) {
|
|
19732
21369
|
const leftBytes = Buffer.from(left);
|
|
19733
21370
|
const rightBytes = Buffer.from(right);
|
|
@@ -19745,17 +21382,15 @@ async function exists(filePath) {
|
|
|
19745
21382
|
function corrupt(name) {
|
|
19746
21383
|
return new Error(`OpenTig authentication ${name} is corrupt.`);
|
|
19747
21384
|
}
|
|
19748
|
-
function sessionExpiry(kind, createdAt) {
|
|
19749
|
-
return kind === "desktop" ? null : new Date(Date.parse(createdAt) + BROWSER_SESSION_MAX_AGE_SECONDS * 1e3).toISOString();
|
|
19750
|
-
}
|
|
19751
21385
|
const DEFAULT_PAIRING_TTL_MS = 5 * 60 * 1e3;
|
|
19752
21386
|
class OpenTigSessionAuth {
|
|
19753
|
-
constructor(source, store, secureCookies, now, profile) {
|
|
21387
|
+
constructor(source, store, secureCookies, now, profile, browserAccessEnabled) {
|
|
19754
21388
|
this.source = source;
|
|
19755
21389
|
this.store = store;
|
|
19756
21390
|
this.secureCookies = secureCookies;
|
|
19757
21391
|
this.now = now;
|
|
19758
21392
|
this.profile = profile;
|
|
21393
|
+
this.browserAccessEnabled = browserAccessEnabled;
|
|
19759
21394
|
}
|
|
19760
21395
|
source;
|
|
19761
21396
|
store;
|
|
@@ -19763,15 +21398,30 @@ class OpenTigSessionAuth {
|
|
|
19763
21398
|
now;
|
|
19764
21399
|
profile;
|
|
19765
21400
|
pairing = null;
|
|
21401
|
+
browserAccessEnabled;
|
|
21402
|
+
browserGeneration = 0;
|
|
21403
|
+
browserQueue = Promise.resolve();
|
|
19766
21404
|
static async open(options) {
|
|
19767
21405
|
const store = await PersistentAuthStore.open(options.dataDirectory, options.now);
|
|
19768
|
-
return new OpenTigSessionAuth(options.source, store, options.secureCookies ?? false, options.now ?? Date.now, options.profile ?? "production");
|
|
21406
|
+
return new OpenTigSessionAuth(options.source, store, options.secureCookies ?? false, options.now ?? Date.now, options.profile ?? "production", options.browserAccessEnabled ?? true);
|
|
19769
21407
|
}
|
|
19770
21408
|
descriptor() {
|
|
19771
21409
|
this.dropExpiredPairing();
|
|
19772
|
-
return { authenticationRequired: true, pairingAvailable: this.pairing !== null };
|
|
21410
|
+
return { authenticationRequired: true, pairingAvailable: this.browserAccessEnabled && this.pairing !== null, browserAccessEnabled: this.browserAccessEnabled };
|
|
21411
|
+
}
|
|
21412
|
+
setBrowserAccessEnabled(enabled) {
|
|
21413
|
+
if (!enabled) {
|
|
21414
|
+
this.browserAccessEnabled = false;
|
|
21415
|
+
this.browserGeneration++;
|
|
21416
|
+
this.pairing = null;
|
|
21417
|
+
}
|
|
21418
|
+
const generation = this.browserGeneration;
|
|
21419
|
+
return this.queueBrowserMutation(async () => {
|
|
21420
|
+
this.browserAccessEnabled = enabled && generation === this.browserGeneration;
|
|
21421
|
+
});
|
|
19773
21422
|
}
|
|
19774
21423
|
createPairingToken(ttlMs = DEFAULT_PAIRING_TTL_MS) {
|
|
21424
|
+
if (!this.browserAccessEnabled) throw new Error("Web access is disabled.");
|
|
19775
21425
|
if (!Number.isSafeInteger(ttlMs) || ttlMs < 1e3 || ttlMs > 15 * 60 * 1e3) throw new Error("Invalid pairing token lifetime.");
|
|
19776
21426
|
const token = randomBytes(32).toString("base64url");
|
|
19777
21427
|
const expiresAt = this.now() + ttlMs;
|
|
@@ -19793,30 +21443,43 @@ class OpenTigSessionAuth {
|
|
|
19793
21443
|
async exchangePairingToken(token, metadata, secure = false) {
|
|
19794
21444
|
this.dropExpiredPairing();
|
|
19795
21445
|
const pairing = this.pairing;
|
|
19796
|
-
if (!pairing || !isCredential(token) || !this.store.matchesDigest(token, pairing.digest)) return null;
|
|
21446
|
+
if (!this.browserAccessEnabled || !pairing || !isCredential(token) || !this.store.matchesDigest(token, pairing.digest)) return null;
|
|
19797
21447
|
this.pairing = null;
|
|
19798
|
-
|
|
19799
|
-
|
|
19800
|
-
|
|
19801
|
-
|
|
19802
|
-
|
|
19803
|
-
|
|
19804
|
-
|
|
19805
|
-
|
|
19806
|
-
|
|
19807
|
-
|
|
19808
|
-
|
|
19809
|
-
|
|
19810
|
-
|
|
19811
|
-
|
|
21448
|
+
const generation = this.browserGeneration;
|
|
21449
|
+
return this.queueBrowserMutation(async () => {
|
|
21450
|
+
if (!this.browserAccessEnabled || generation !== this.browserGeneration) return null;
|
|
21451
|
+
try {
|
|
21452
|
+
const cookie = await this.issueCookie({
|
|
21453
|
+
kind: "browser",
|
|
21454
|
+
clientName: metadata?.clientName ?? "Browser",
|
|
21455
|
+
deviceType: metadata?.deviceType ?? "unknown",
|
|
21456
|
+
os: metadata?.os ?? null,
|
|
21457
|
+
browser: metadata?.browser ?? null,
|
|
21458
|
+
remoteAddress: metadata?.remoteAddress ?? null,
|
|
21459
|
+
viaProxy: metadata?.viaProxy ?? false
|
|
21460
|
+
}, secure);
|
|
21461
|
+
if (!this.browserAccessEnabled || generation !== this.browserGeneration) {
|
|
21462
|
+
await this.revoke({ cookie });
|
|
21463
|
+
return null;
|
|
21464
|
+
}
|
|
21465
|
+
return cookie;
|
|
21466
|
+
} catch (error) {
|
|
21467
|
+
if (this.browserAccessEnabled && generation === this.browserGeneration && this.now() < pairing.expiresAt && !this.pairing) this.pairing = pairing;
|
|
21468
|
+
throw error;
|
|
21469
|
+
}
|
|
21470
|
+
});
|
|
19812
21471
|
}
|
|
19813
21472
|
authenticate(headers) {
|
|
19814
21473
|
const token = readCookie(headers.cookie, sessionCookieName(this.profile));
|
|
19815
|
-
|
|
21474
|
+
const sessionId = token ? this.store.authenticate(token) : null;
|
|
21475
|
+
return sessionId && this.hasSession(sessionId) ? sessionId : null;
|
|
19816
21476
|
}
|
|
19817
21477
|
async renewBrowserCookie(headers, secure = false) {
|
|
21478
|
+
if (!this.browserAccessEnabled) return null;
|
|
21479
|
+
const generation = this.browserGeneration;
|
|
19818
21480
|
const token = readCookie(headers.cookie, sessionCookieName(this.profile));
|
|
19819
21481
|
if (!token || !await this.store.renewBrowserSession(token)) return null;
|
|
21482
|
+
if (!this.browserAccessEnabled || generation !== this.browserGeneration) return null;
|
|
19820
21483
|
return this.sessionCookie(token, true, secure);
|
|
19821
21484
|
}
|
|
19822
21485
|
async revoke(headers) {
|
|
@@ -19861,11 +21524,18 @@ class OpenTigSessionAuth {
|
|
|
19861
21524
|
};
|
|
19862
21525
|
}
|
|
19863
21526
|
hasSession(sessionId) {
|
|
19864
|
-
return this.store.hasSession(sessionId);
|
|
21527
|
+
return this.store.hasSession(sessionId) && (this.browserAccessEnabled || this.sessions().some((session) => session.id === sessionId && session.kind === "desktop"));
|
|
19865
21528
|
}
|
|
19866
21529
|
close() {
|
|
21530
|
+
this.browserAccessEnabled = false;
|
|
21531
|
+
this.browserGeneration++;
|
|
19867
21532
|
this.pairing = null;
|
|
19868
|
-
return this.store.close();
|
|
21533
|
+
return this.browserQueue.then(() => this.store.close());
|
|
21534
|
+
}
|
|
21535
|
+
queueBrowserMutation(operation) {
|
|
21536
|
+
const result = this.browserQueue.then(operation);
|
|
21537
|
+
this.browserQueue = result.then(() => void 0, () => void 0);
|
|
21538
|
+
return result;
|
|
19869
21539
|
}
|
|
19870
21540
|
expiredCookie(secure = false) {
|
|
19871
21541
|
return `${sessionCookieName(this.profile)}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0${this.secureCookies || secure ? "; Secure" : ""}`;
|
|
@@ -20128,6 +21798,19 @@ async function handleRequest(context, request, response) {
|
|
|
20128
21798
|
}
|
|
20129
21799
|
return sendJson(response, 404, { error: "Not found." });
|
|
20130
21800
|
}
|
|
21801
|
+
if (method === "GET" && rawPath === "/api/auth/web-access") {
|
|
21802
|
+
if (!context.auth.authenticate(request.headers)) return sendJson(response, 401, { error: "Authentication required." });
|
|
21803
|
+
const listener = context.listenerAddress();
|
|
21804
|
+
const status = {
|
|
21805
|
+
webAccessEnabled: context.auth.descriptor().browserAccessEnabled,
|
|
21806
|
+
pairingAvailable: context.auth.descriptor().pairingAvailable,
|
|
21807
|
+
listeningOnLan: !isLoopbackAddress(listener.host),
|
|
21808
|
+
listenerHost: listener.host,
|
|
21809
|
+
actualPort: listener.port,
|
|
21810
|
+
ready: context.isReady()
|
|
21811
|
+
};
|
|
21812
|
+
return sendJson(response, 200, status);
|
|
21813
|
+
}
|
|
20131
21814
|
if (method === "GET" && rawPath === "/api/auth/sessions") {
|
|
20132
21815
|
const currentSessionId = context.auth.authenticate(request.headers);
|
|
20133
21816
|
if (!currentSessionId) return sendJson(response, 401, { error: "Authentication required." });
|
|
@@ -20147,12 +21830,22 @@ async function handleRequest(context, request, response) {
|
|
|
20147
21830
|
}
|
|
20148
21831
|
if (method === "POST" && rawPath.startsWith("/api/")) {
|
|
20149
21832
|
if (!isAllowedOrigin(request)) return sendJson(response, 403, { error: "Forbidden origin." });
|
|
21833
|
+
if (rawPath === "/api/auth/pairing-link") {
|
|
21834
|
+
if (!context.auth.authenticate(request.headers)) return sendJson(response, 401, { error: "Authentication required." });
|
|
21835
|
+
if (!context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
|
|
21836
|
+
if (!context.isReady()) return sendJson(response, 503, { error: "Server is not ready." });
|
|
21837
|
+
const url = new URL("/pair", request.headers.origin);
|
|
21838
|
+
const pairing = context.auth.createPairingToken();
|
|
21839
|
+
url.hash = new URLSearchParams({ token: pairing.token }).toString();
|
|
21840
|
+
return sendJson(response, 200, { url: url.href, expiresAt: pairing.expiresAt });
|
|
21841
|
+
}
|
|
20150
21842
|
if (rawPath === "/api/auth/renew") {
|
|
20151
21843
|
const cookie = await context.auth.renewBrowserCookie(request.headers, requestOriginIsSecure(request));
|
|
20152
21844
|
if (!cookie) return sendJson(response, 401, { error: "Authentication required." });
|
|
20153
21845
|
return sendJson(response, 204, null, { "Set-Cookie": cookie });
|
|
20154
21846
|
}
|
|
20155
21847
|
if (rawPath === "/api/auth/pair" || rawPath === "/api/auth/desktop") {
|
|
21848
|
+
if (rawPath === "/api/auth/pair" && !context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
|
|
20156
21849
|
const body = await readJsonObject(request, response);
|
|
20157
21850
|
if (!body) return;
|
|
20158
21851
|
const secure = requestOriginIsSecure(request);
|
|
@@ -20164,6 +21857,7 @@ async function handleRequest(context, request, response) {
|
|
|
20164
21857
|
} else {
|
|
20165
21858
|
cookie = await context.auth.exchangeDesktopSecret(body.secret);
|
|
20166
21859
|
}
|
|
21860
|
+
if (!cookie && rawPath === "/api/auth/pair" && !context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
|
|
20167
21861
|
if (!cookie) return sendJson(response, 401, { error: "Authentication failed." });
|
|
20168
21862
|
return sendJson(response, 204, null, { "Set-Cookie": cookie });
|
|
20169
21863
|
}
|
|
@@ -20324,6 +22018,7 @@ async function handleLocalAdminPair(context, request, response) {
|
|
|
20324
22018
|
}
|
|
20325
22019
|
const origin = normalizePublicOrigin(body.publicOrigin);
|
|
20326
22020
|
if (!origin) return sendJson(response, 400, { error: "Invalid public origin." });
|
|
22021
|
+
if (!context.auth.descriptor().browserAccessEnabled) return sendJson(response, 403, { error: "Web access is disabled.", code: "WEB_ACCESS_DISABLED" });
|
|
20327
22022
|
const pairing = admin.createPairingToken();
|
|
20328
22023
|
const url = new URL("/pair", origin);
|
|
20329
22024
|
url.hash = new URLSearchParams({ token: pairing.token }).toString();
|
|
@@ -20456,7 +22151,10 @@ class OpenTigWebSocketTransport {
|
|
|
20456
22151
|
revokeSessions(sessionIds) {
|
|
20457
22152
|
const revoked = new Set(sessionIds);
|
|
20458
22153
|
for (const [socket, state] of this.clients) {
|
|
20459
|
-
if (revoked.has(state.sessionId))
|
|
22154
|
+
if (revoked.has(state.sessionId)) {
|
|
22155
|
+
for (const controller of state.requests.values()) controller.abort();
|
|
22156
|
+
socket.close(1008, "Session revoked");
|
|
22157
|
+
}
|
|
20460
22158
|
}
|
|
20461
22159
|
}
|
|
20462
22160
|
close() {
|
|
@@ -20499,6 +22197,7 @@ class OpenTigWebSocketTransport {
|
|
|
20499
22197
|
socket.on("error", () => this.options.logger("warn", "WebSocket connection failed."));
|
|
20500
22198
|
}
|
|
20501
22199
|
async message(socket, state, data, isBinary) {
|
|
22200
|
+
if (!this.options.auth.hasSession(state.sessionId)) return socket.close(1008, "Session expired or revoked");
|
|
20502
22201
|
if (isBinary) return socket.close(1003, "Text messages only");
|
|
20503
22202
|
const bytes = rawBytes(data);
|
|
20504
22203
|
if (bytes.byteLength > FILE_WRITE_FRAME_LIMIT) return socket.close(1009, "Message too large");
|
|
@@ -20653,6 +22352,10 @@ class OpenTigServer {
|
|
|
20653
22352
|
identity: options.identity,
|
|
20654
22353
|
mode: options.mode ?? "desktop",
|
|
20655
22354
|
isReady: () => this.ready,
|
|
22355
|
+
listenerAddress: () => {
|
|
22356
|
+
const address = this.httpServer.address();
|
|
22357
|
+
return { host: address.address, port: address.port };
|
|
22358
|
+
},
|
|
20656
22359
|
sessionConnectionCount: (sessionId) => this.webSockets.connectionCount(sessionId),
|
|
20657
22360
|
onSessionsRevoked: (sessionIds) => this.disconnectSessions(sessionIds),
|
|
20658
22361
|
logger: this.logger,
|
|
@@ -20696,7 +22399,13 @@ class OpenTigServer {
|
|
|
20696
22399
|
return this.options.auth.createPairingToken();
|
|
20697
22400
|
}
|
|
20698
22401
|
getStatus() {
|
|
20699
|
-
return { connectedSessionCount: this.webSockets.connectedSessionCount };
|
|
22402
|
+
return { connectedSessionCount: this.webSockets.connectedSessionCount, browserAccessEnabled: this.options.auth.descriptor().browserAccessEnabled };
|
|
22403
|
+
}
|
|
22404
|
+
async setBrowserAccessEnabled(enabled) {
|
|
22405
|
+
if (!this.ready) throw new Error("Server is not ready.");
|
|
22406
|
+
const pending = this.options.auth.setBrowserAccessEnabled(enabled);
|
|
22407
|
+
if (!enabled) this.disconnectSessions(this.options.auth.sessions().filter((session) => session.kind !== "desktop").map((session) => session.id));
|
|
22408
|
+
await pending;
|
|
20700
22409
|
}
|
|
20701
22410
|
async revokeAllSessions() {
|
|
20702
22411
|
if (!this.ready) throw new Error("Server is not ready.");
|
|
@@ -20754,6 +22463,7 @@ async function runOpenTigServer(config2) {
|
|
|
20754
22463
|
let transport = null;
|
|
20755
22464
|
const auth = await OpenTigSessionAuth.open({
|
|
20756
22465
|
source: config2.auth,
|
|
22466
|
+
browserAccessEnabled: config2.browserAccessEnabled ?? config2.mode === "web-access",
|
|
20757
22467
|
...config2.profile ? { profile: config2.profile } : {},
|
|
20758
22468
|
dataDirectory: config2.serverDataPath ?? path.join(path.dirname(config2.settingsPath), "server"),
|
|
20759
22469
|
...config2.secureCookies === void 0 ? {} : { secureCookies: config2.secureCookies }
|
|
@@ -20807,6 +22517,7 @@ async function runOpenTigServer(config2) {
|
|
|
20807
22517
|
createPairingLink: () => pairingLink(address.origin, transport.createPairingToken()),
|
|
20808
22518
|
getStatus: () => transport.getStatus(),
|
|
20809
22519
|
revokeAllSessions: () => transport.revokeAllSessions(),
|
|
22520
|
+
setBrowserAccessEnabled: (enabled) => transport.setBrowserAccessEnabled(enabled),
|
|
20810
22521
|
close: () => transport.stop()
|
|
20811
22522
|
};
|
|
20812
22523
|
} catch (error) {
|