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