@skaile/workspaces 4.24.0 → 4.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/{asset-feeds-65J4N2TB.js → asset-feeds-72RJOE4E.js} +4 -4
  3. package/dist/{asset-feeds-65J4N2TB.js.map → asset-feeds-72RJOE4E.js.map} +1 -1
  4. package/dist/asset-manager/index.js +1 -1
  5. package/dist/{chunk-SMXHEEJP.js → chunk-253427M3.js} +3 -3
  6. package/dist/{chunk-SMXHEEJP.js.map → chunk-253427M3.js.map} +1 -1
  7. package/dist/{chunk-4FFUB4ZB.js → chunk-2JGUFDKZ.js} +3 -3
  8. package/dist/{chunk-4FFUB4ZB.js.map → chunk-2JGUFDKZ.js.map} +1 -1
  9. package/dist/{chunk-VFGEIFRT.js → chunk-2N6NQUVB.js} +5 -5
  10. package/dist/{chunk-VFGEIFRT.js.map → chunk-2N6NQUVB.js.map} +1 -1
  11. package/dist/{chunk-3SZLZ56M.js → chunk-EZOFQW3G.js} +761 -234
  12. package/dist/chunk-EZOFQW3G.js.map +1 -0
  13. package/dist/{chunk-OTRHFPUF.js → chunk-H4XNX2FG.js} +2 -2
  14. package/dist/{chunk-OTRHFPUF.js.map → chunk-H4XNX2FG.js.map} +1 -1
  15. package/dist/{chunk-SWXUBDTV.js → chunk-IHFQTKUP.js} +2 -2
  16. package/dist/{chunk-SWXUBDTV.js.map → chunk-IHFQTKUP.js.map} +1 -1
  17. package/dist/{chunk-VTTBDFXM.js → chunk-OQRSSFTU.js} +3 -3
  18. package/dist/{chunk-VTTBDFXM.js.map → chunk-OQRSSFTU.js.map} +1 -1
  19. package/dist/{chunk-MTNIVJWB.js → chunk-SB4U7HLP.js} +6 -6
  20. package/dist/{chunk-MTNIVJWB.js.map → chunk-SB4U7HLP.js.map} +1 -1
  21. package/dist/{chunk-QD4ZCIPQ.js → chunk-SSODNBSC.js} +3 -3
  22. package/dist/{chunk-QD4ZCIPQ.js.map → chunk-SSODNBSC.js.map} +1 -1
  23. package/dist/{chunk-JCT4UMGQ.js → chunk-XZCRIPWT.js} +3 -3
  24. package/dist/{chunk-JCT4UMGQ.js.map → chunk-XZCRIPWT.js.map} +1 -1
  25. package/dist/{chunk-4U3NSLGV.js → chunk-YR3CLEQG.js} +6 -6
  26. package/dist/{chunk-4U3NSLGV.js.map → chunk-YR3CLEQG.js.map} +1 -1
  27. package/dist/cli/index.js +28 -28
  28. package/dist/connectors/index.js +1 -1
  29. package/dist/connectors/src/connector-prompt.d.ts.map +1 -1
  30. package/dist/{ensure-sources-TKY7PTWF.js → ensure-sources-HEOQJ4CS.js} +4 -4
  31. package/dist/{ensure-sources-TKY7PTWF.js.map → ensure-sources-HEOQJ4CS.js.map} +1 -1
  32. package/dist/factory-assets/connectors/box.js +1 -1
  33. package/dist/factory-assets/connectors/deploy.js +1 -1
  34. package/dist/factory-assets/connectors/devserver.js +1 -1
  35. package/dist/factory-assets/connectors/flow/adapter.js +1 -1
  36. package/dist/factory-assets/connectors/flow/run-flow.js +2 -2
  37. package/dist/factory-assets/connectors/flow.js +1 -1
  38. package/dist/factory-assets/connectors/git/CONNECTOR.md +63 -6
  39. package/dist/factory-assets/connectors/git/driver.d.ts +111 -27
  40. package/dist/factory-assets/connectors/git/driver.d.ts.map +1 -1
  41. package/dist/factory-assets/connectors/git.js +1 -1
  42. package/dist/factory-assets/connectors/gmail.js +1 -1
  43. package/dist/factory-assets/connectors/googledrive.js +1 -1
  44. package/dist/factory-assets/connectors/local.js +1 -1
  45. package/dist/factory-assets/connectors/mattermost.js +1 -1
  46. package/dist/factory-assets/connectors/memory.js +1 -1
  47. package/dist/factory-assets/connectors/minio.js +1 -1
  48. package/dist/factory-assets/connectors/postgres.js +1 -1
  49. package/dist/factory-assets/connectors/s3.js +1 -1
  50. package/dist/factory-assets/connectors/sharepoint.js +1 -1
  51. package/dist/factory-assets/connectors/sqlite.js +1 -1
  52. package/dist/factory-assets/connectors/static-server.js +1 -1
  53. package/dist/factory-assets/connectors/tunnel.js +1 -1
  54. package/dist/factory-assets/connectors/webdav.js +1 -1
  55. package/dist/factory-assets/connectors/xstate-store.js +1 -1
  56. package/dist/factory-assets/connectors/xstate.js +1 -1
  57. package/dist/{helpers-5GMEJWMX.js → helpers-VZJCQSLY.js} +4 -4
  58. package/dist/{helpers-5GMEJWMX.js.map → helpers-VZJCQSLY.js.map} +1 -1
  59. package/dist/{library-publish-FLQICZF4.js → library-publish-VBLSTO7S.js} +6 -6
  60. package/dist/{library-publish-FLQICZF4.js.map → library-publish-VBLSTO7S.js.map} +1 -1
  61. package/dist/{open-library-OOUZG24B.js → open-library-SJ54ZBGJ.js} +5 -5
  62. package/dist/{open-library-OOUZG24B.js.map → open-library-SJ54ZBGJ.js.map} +1 -1
  63. package/dist/{project-ZUYKJX3V.js → project-GLZR4SLT.js} +5 -5
  64. package/dist/{project-ZUYKJX3V.js.map → project-GLZR4SLT.js.map} +1 -1
  65. package/dist/runner/index.js +4 -4
  66. package/dist/sdk/asset-manager.js +1 -1
  67. package/dist/sdk/index.js +4 -4
  68. package/dist/sdk/runner.js +4 -4
  69. package/dist/{setup-RERVKTPI.js → setup-X73AWRMD.js} +5 -5
  70. package/dist/{setup-RERVKTPI.js.map → setup-X73AWRMD.js.map} +1 -1
  71. package/dist/{source-SUHVAXK6.js → source-HYXJO2IE.js} +6 -6
  72. package/dist/{source-SUHVAXK6.js.map → source-HYXJO2IE.js.map} +1 -1
  73. package/dist/{source-manifest-publish-VMW6ZHW6.js → source-manifest-publish-V5LCVOYZ.js} +7 -7
  74. package/dist/{source-manifest-publish-VMW6ZHW6.js.map → source-manifest-publish-V5LCVOYZ.js.map} +1 -1
  75. package/dist/tui/index.js +4 -4
  76. package/dist/workspace-plugin/index.js +1 -1
  77. package/package.json +1 -1
  78. package/dist/chunk-3SZLZ56M.js.map +0 -1
@@ -1568,6 +1568,69 @@ async function commitIdentityArgs(cwd) {
1568
1568
  function gitCommitting(args, cwd, timeout = 3e4) {
1569
1569
  return commitIdentityArgs(cwd).then((identity) => git([...identity, ...args], cwd, timeout));
1570
1570
  }
1571
+ async function pullByMerge(branch, cwd, auth, timeout = 3e4) {
1572
+ const pending = await unfinishedGitOperation(cwd);
1573
+ if (pending !== null) {
1574
+ throw new Error(
1575
+ `the checkout has an unfinished merge or rebase (${pending}); nothing was pulled. Finish or abort it first`
1576
+ );
1577
+ }
1578
+ await runRemoteGit(["fetch", "origin", branch], auth, {
1579
+ cwd,
1580
+ safeDir: cwd,
1581
+ timeout,
1582
+ env: cLocaleEnv()
1583
+ });
1584
+ try {
1585
+ await gitCommitting(["merge", "--ff", "--no-edit", "FETCH_HEAD"], cwd, timeout);
1586
+ } catch (err) {
1587
+ const unmerged = await gitOrNull(["diff", "--name-only", "--diff-filter=U"], cwd) ?? "";
1588
+ if (await gitOrNull(["rev-parse", "--quiet", "--verify", "MERGE_HEAD"], cwd) !== null) {
1589
+ await gitOrNull(["merge", "--abort"], cwd);
1590
+ }
1591
+ const files = unmerged.split("\n").filter(Boolean);
1592
+ if (files.length > 0) {
1593
+ throw new Error(
1594
+ `origin/${branch} and the local branch both have new commits that conflict in ${files.join(", ")}; the merge was aborted and the checkout is unchanged`
1595
+ );
1596
+ }
1597
+ throw err;
1598
+ }
1599
+ }
1600
+ var UNFINISHED_OPERATION_PATHS = [
1601
+ "MERGE_HEAD",
1602
+ "CHERRY_PICK_HEAD",
1603
+ "REVERT_HEAD",
1604
+ "rebase-merge",
1605
+ "rebase-apply"
1606
+ ];
1607
+ async function unfinishedGitOperation(cwd) {
1608
+ const paths = await gitOrNull(
1609
+ ["rev-parse", ...UNFINISHED_OPERATION_PATHS.flatMap((p) => ["--git-path", p])],
1610
+ cwd
1611
+ );
1612
+ if (paths === null) return "the git dir could not be resolved";
1613
+ const found = await Promise.all(
1614
+ paths.split("\n").map(async (p) => {
1615
+ try {
1616
+ return await existsOrThrow(resolve(cwd, p)) ? "is present" : null;
1617
+ } catch (err) {
1618
+ return `could not be checked (${err.code ?? "deadline or error"})`;
1619
+ }
1620
+ })
1621
+ );
1622
+ const index = found.findIndex((state) => state !== null);
1623
+ if (index >= 0) return `${UNFINISHED_OPERATION_PATHS[index]} ${found[index]}`;
1624
+ const unmerged = await gitOrNull(["diff", "--name-only", "--diff-filter=U"], cwd);
1625
+ if (unmerged) return `unmerged paths: ${unmerged.split("\n").join(", ")}`;
1626
+ return null;
1627
+ }
1628
+ async function refuseUnfinishedOperation(cwd, log) {
1629
+ const reason = await unfinishedGitOperation(cwd);
1630
+ if (reason === null) return false;
1631
+ log.warn("not committing: the checkout has an unfinished merge or rebase", { reason });
1632
+ return true;
1633
+ }
1571
1634
  async function ensureSkaileExcluded(cwd, log) {
1572
1635
  try {
1573
1636
  const commonDir = await git(["rev-parse", "--git-common-dir"], cwd, 5e3);
@@ -1638,6 +1701,208 @@ async function runGit(args, options) {
1638
1701
  function git(args, cwd, timeout = 3e4) {
1639
1702
  return runGit([...gitPrefix(cwd), ...args], { cwd, timeout });
1640
1703
  }
1704
+ var TOKEN_USERNAME = "x-access-token";
1705
+ var NETWORK_GIT_ARGS = [
1706
+ "-c",
1707
+ "credential.helper=",
1708
+ "-c",
1709
+ "core.hooksPath=/dev/null",
1710
+ "-c",
1711
+ "core.askPass=",
1712
+ "-c",
1713
+ "core.fsmonitor=false",
1714
+ "-c",
1715
+ "core.alternateRefsCommand=",
1716
+ "-c",
1717
+ "push.gpgSign=false",
1718
+ "-c",
1719
+ "gc.auto=0",
1720
+ "-c",
1721
+ "maintenance.auto=false"
1722
+ ];
1723
+ function plainRemoteUrl(auth) {
1724
+ if (!auth.token || !auth.url.startsWith("https://")) return auth.url;
1725
+ const u = new URL(auth.url);
1726
+ u.username = "";
1727
+ u.password = "";
1728
+ return u.toString();
1729
+ }
1730
+ function networkGitEnv(auth, base = process.env) {
1731
+ const env = { ...base, GIT_TERMINAL_PROMPT: "0", GIT_ASKPASS: "" };
1732
+ if (!auth?.token || !auth.url.startsWith("https://")) return env;
1733
+ const { protocol, host } = new URL(auth.url);
1734
+ const basic = Buffer.from(`${TOKEN_USERNAME}:${auth.token}`).toString("base64");
1735
+ const existing = Number.parseInt(base.GIT_CONFIG_COUNT ?? "", 10);
1736
+ const index = Number.isInteger(existing) && existing > 0 ? existing : 0;
1737
+ const scope = auth.repoScoped ? comparableRemoteUrl(auth.url) : `${protocol}//${host}/`;
1738
+ env[`GIT_CONFIG_KEY_${index}`] = `http.${scope}.extraHeader`;
1739
+ env[`GIT_CONFIG_VALUE_${index}`] = `Authorization: Basic ${basic}`;
1740
+ env.GIT_CONFIG_COUNT = String(index + 1);
1741
+ env.GIT_ALLOW_PROTOCOL = "https";
1742
+ return env;
1743
+ }
1744
+ async function runRemoteGit(args, auth, options) {
1745
+ const keys = subcommandOf(args) === "submodule" ? CHECKOUT_UNSAFE_KEY_RE : TLS_TRUST_KEY_RE;
1746
+ const withheld = auth?.token ? await tlsTrustOverride(options, keys) : null;
1747
+ if (withheld !== null) {
1748
+ authLog.warn(
1749
+ "not sending the git credential: the checkout's config changes TLS trust or names a filter",
1750
+ {
1751
+ key: withheld
1752
+ }
1753
+ );
1754
+ }
1755
+ const recursion = RECURSING_SUBCOMMANDS.has(subcommandOf(args)) ? NO_SUBMODULE_RECURSION : [];
1756
+ return runGit([...gitPrefix(options.safeDir), ...NETWORK_GIT_ARGS, ...recursion, ...args], {
1757
+ ...options.cwd === void 0 ? {} : { cwd: options.cwd },
1758
+ timeout: options.timeout,
1759
+ env: networkGitEnv(withheld === null ? auth : void 0, options.env)
1760
+ });
1761
+ }
1762
+ var RECURSING_SUBCOMMANDS = /* @__PURE__ */ new Set(["fetch", "pull", "push", "submodule", "clone"]);
1763
+ var NO_SUBMODULE_RECURSION = [
1764
+ "-c",
1765
+ "submodule.recurse=false",
1766
+ "-c",
1767
+ "fetch.recurseSubmodules=false",
1768
+ "-c",
1769
+ "push.recurseSubmodules=no"
1770
+ ];
1771
+ function subcommandOf(args) {
1772
+ let i = 0;
1773
+ while (args[i] === "-c") i += 2;
1774
+ return args[i] ?? "";
1775
+ }
1776
+ var authLog = createLogger({ kind: "connector", subkind: "git" });
1777
+ var TLS_TRUST_KEY_RE = "^http\\.(.*\\.)?(sslverify|sslcainfo|sslcapath)$";
1778
+ var CHECKOUT_UNSAFE_KEY_RE = `(${TLS_TRUST_KEY_RE})|(^filter\\..*\\.(smudge|clean|process)$)|(^lfs\\.(customtransfer\\..*\\.path|standalonetransferagent)$)`;
1779
+ async function tlsTrustOverride(options, keyRe = TLS_TRUST_KEY_RE) {
1780
+ try {
1781
+ const own = await configMatch(
1782
+ gitPrefix(options.safeDir),
1783
+ options.cwd,
1784
+ [...options.cwd === void 0 ? ["--global"] : [], "--show-scope"],
1785
+ keyRe
1786
+ );
1787
+ const line = own.find(isUnsafeConfigLine);
1788
+ if (line !== void 0) return line.split(/\s+/)[1] ?? "unknown";
1789
+ if (options.cwd === void 0) return null;
1790
+ for (const dir of await submoduleGitDirs(options.cwd)) {
1791
+ const hits = await configMatch(
1792
+ ["-c", "safe.directory=*", `--git-dir=${dir}`],
1793
+ options.cwd,
1794
+ ["--show-scope"],
1795
+ keyRe
1796
+ );
1797
+ const hit = hits.find(isUnsafeConfigLine);
1798
+ if (hit !== void 0) return `${hit.split(/\s+/)[1] ?? "unknown"} (${dir})`;
1799
+ }
1800
+ return null;
1801
+ } catch {
1802
+ return "unreadable config";
1803
+ }
1804
+ }
1805
+ function isUnsafeConfigLine(line) {
1806
+ return !/^(system|command)\s/.test(line);
1807
+ }
1808
+ async function configMatch(globalArgs, cwd, selector, keyRe) {
1809
+ try {
1810
+ const out = await runGit(
1811
+ [...globalArgs, "config", ...selector, "--includes", "--get-regexp", keyRe],
1812
+ { ...cwd === void 0 ? {} : { cwd }, timeout: 5e3 }
1813
+ );
1814
+ return out.split("\n").filter((l) => l !== "");
1815
+ } catch (err) {
1816
+ if (err.code === 1) return [];
1817
+ throw err;
1818
+ }
1819
+ }
1820
+ var MAX_SUBMODULE_GIT_DIRS = 256;
1821
+ var MAX_SUBMODULE_SCAN_ENTRIES = 4096;
1822
+ var MAX_SUBMODULE_DEPTH = 8;
1823
+ async function submoduleGitDirs(cwd) {
1824
+ const found = /* @__PURE__ */ new Set();
1825
+ let scanned = 0;
1826
+ const add = (dir) => {
1827
+ found.add(dir);
1828
+ if (found.size > MAX_SUBMODULE_GIT_DIRS) throw new Error("too many submodule git dirs");
1829
+ };
1830
+ const walkModules = async (dir, depth) => {
1831
+ if (depth > MAX_SUBMODULE_DEPTH) throw new Error("submodule git dirs nested too deep");
1832
+ for (const name of await listDirOrEmpty(dir)) {
1833
+ scanned += 1;
1834
+ if (scanned > MAX_SUBMODULE_SCAN_ENTRIES) throw new Error("submodule scan too large");
1835
+ const child = join(dir, name);
1836
+ const st = await withFsDeadline(`stat ${child}`, () => stat(child));
1837
+ if (!st.isDirectory()) continue;
1838
+ if (await existsOrThrow(join(child, "HEAD"))) {
1839
+ add(child);
1840
+ await walkModules(join(child, "modules"), depth + 1);
1841
+ } else {
1842
+ await walkModules(child, depth + 1);
1843
+ }
1844
+ }
1845
+ };
1846
+ const walkWorktree = async (worktree, depth) => {
1847
+ if (depth > MAX_SUBMODULE_DEPTH) throw new Error("submodules nested too deep");
1848
+ const gitmodules = join(worktree, ".gitmodules");
1849
+ if (!await existsOrThrow(gitmodules)) return;
1850
+ let listed = "";
1851
+ try {
1852
+ listed = await runGit(
1853
+ ["config", "--file", gitmodules, "--get-regexp", "^submodule\\..*\\.path$"],
1854
+ { cwd: worktree, timeout: 5e3 }
1855
+ );
1856
+ } catch (err) {
1857
+ if (err.code !== 1) throw err;
1858
+ }
1859
+ for (const line of listed.split("\n").filter((l) => l !== "")) {
1860
+ const path = line.slice(line.indexOf(" ") + 1);
1861
+ const subWorktree = resolve(worktree, path);
1862
+ const dotGit = join(subWorktree, ".git");
1863
+ let st;
1864
+ try {
1865
+ st = await withFsDeadline(`stat ${dotGit}`, () => stat(dotGit));
1866
+ } catch (err) {
1867
+ if (err.code === "ENOENT") continue;
1868
+ throw err;
1869
+ }
1870
+ if (st.isDirectory()) {
1871
+ add(dotGit);
1872
+ } else {
1873
+ const content = await withFsDeadline(`read ${dotGit}`, () => readFile(dotGit, "utf-8"));
1874
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(content);
1875
+ if (!m?.[1]) throw new Error(`unrecognised gitfile ${dotGit}`);
1876
+ add(resolve(dirname(dotGit), m[1]));
1877
+ }
1878
+ await walkWorktree(subWorktree, depth + 1);
1879
+ }
1880
+ };
1881
+ const commonDir = resolve(cwd, await git(["rev-parse", "--git-common-dir"], cwd, 5e3));
1882
+ await walkModules(join(commonDir, "modules"), 0);
1883
+ await walkWorktree(cwd, 0);
1884
+ return [...found];
1885
+ }
1886
+ async function listDirOrEmpty(dir) {
1887
+ try {
1888
+ return await withFsDeadline(`readdir ${dir}`, () => readdir(dir));
1889
+ } catch (err) {
1890
+ if (err.code === "ENOENT") return [];
1891
+ throw err;
1892
+ }
1893
+ }
1894
+ async function existsOrThrow(path) {
1895
+ try {
1896
+ await withFsDeadline(`stat ${path}`, () => stat(path));
1897
+ return true;
1898
+ } catch (err) {
1899
+ if (err.code === "ENOENT") return false;
1900
+ throw err;
1901
+ }
1902
+ }
1903
+ function gitRemote(args, cwd, auth, timeout = 3e4) {
1904
+ return runRemoteGit(args, auth, { cwd, safeDir: cwd, timeout });
1905
+ }
1641
1906
  function pathExists(path) {
1642
1907
  return tryWithFsDeadline(`stat ${path}`, () => stat(path).then(() => true), false);
1643
1908
  }
@@ -1682,16 +1947,27 @@ function gitLocalCheckout(args, cwd) {
1682
1947
  throw typeof err?.code === "number" ? withConnectErrorKind(err, "repo-state") : err;
1683
1948
  });
1684
1949
  }
1685
- async function syncSubmodules(cwd, log) {
1950
+ async function syncSubmodules(cwd, log, auth) {
1686
1951
  if (!await pathExists(join(cwd, ".gitmodules"))) return;
1687
1952
  try {
1688
- await git(["submodule", "update", "--init", "--recursive"], cwd, 12e4);
1953
+ await gitRemote(
1954
+ ["submodule", "update", "--init", "--recursive", "--checkout"],
1955
+ cwd,
1956
+ auth,
1957
+ 12e4
1958
+ );
1689
1959
  } catch (err) {
1690
1960
  log.warn("git submodule update failed \u2014 checkout may be incomplete", {
1691
1961
  error: redactCredentials(err instanceof Error ? err.message : String(err))
1692
1962
  });
1693
1963
  }
1694
1964
  }
1965
+ async function checkoutSubmodulesOffline(cwd) {
1966
+ if (!await pathExists(join(cwd, ".gitmodules"))) return [];
1967
+ await gitOrNull(["submodule", "update", "--recursive", "--checkout", "--no-fetch"], cwd);
1968
+ const status = await gitOrNull(["submodule", "status", "--recursive"], cwd) ?? "";
1969
+ return status.split("\n").map((l) => /^[+-][0-9a-f]+ (.+?)(?: \([^)]*\))?$/.exec(l)?.[1]).filter((p) => p !== void 0);
1970
+ }
1695
1971
  function isValidGitRefOrSha(ref) {
1696
1972
  if (ref.length === 0 || ref.length > 255) return false;
1697
1973
  if (/[\s\\~^:?*[\x00-\x1f\x7f`$;&|'"<>()]/.test(ref)) return false;
@@ -1736,6 +2012,40 @@ async function resolveUpstreamRef(cwd, branch) {
1736
2012
  const ref = `refs/remotes/${remote}/${name}`;
1737
2013
  return await gitOrNull(["rev-parse", "--verify", "--quiet", ref], cwd) !== null ? ref : null;
1738
2014
  }
2015
+ async function assertOriginIsMount(s, cwd, op) {
2016
+ const expected = comparableRemoteUrl(s.url);
2017
+ for (const args of [
2018
+ ["remote", "get-url", "origin"],
2019
+ ["remote", "get-url", "--push", "origin"]
2020
+ ]) {
2021
+ const actual = await gitOrNull(args, cwd);
2022
+ if (actual === null || comparableRemoteUrl(actual) !== expected) {
2023
+ throw new Error(
2024
+ `git ${op}: origin${args.includes("--push") ? " (push)" : ""} resolves to ${actual === null ? "nothing" : redactCredentials(actual)}, not this mount's remote ${expected}. Remove the pushurl / insteadOf / url override from the git config first.`
2025
+ );
2026
+ }
2027
+ }
2028
+ }
2029
+ function comparableRemoteUrl(url) {
2030
+ if (!url.startsWith("https://")) return url;
2031
+ try {
2032
+ const u = new URL(url);
2033
+ return `${u.protocol}//${u.host}${u.pathname}`;
2034
+ } catch {
2035
+ return url;
2036
+ }
2037
+ }
2038
+ function repoScopedAuth(s) {
2039
+ return {
2040
+ get url() {
2041
+ return s.url;
2042
+ },
2043
+ get token() {
2044
+ return s.token;
2045
+ },
2046
+ repoScoped: true
2047
+ };
2048
+ }
1739
2049
  function trackingRefspec(branch) {
1740
2050
  return `+refs/heads/${branch}:refs/remotes/origin/${branch}`;
1741
2051
  }
@@ -1762,51 +2072,15 @@ async function dropTrackingRefspec(branch, cwd) {
1762
2072
  async function isAncestor(ancestor, descendant, cwd) {
1763
2073
  return await gitOrNull(["merge-base", "--is-ancestor", ancestor, descendant], cwd) !== null;
1764
2074
  }
1765
- async function integrateOriginBaseBranch(branch, cwd) {
2075
+ async function integrateOriginBaseBranch(branch, cwd, auth) {
1766
2076
  if (await gitOrNull(["symbolic-ref", "-q", "HEAD"], cwd) !== `refs/heads/${branch}`) return;
1767
- if (await hasUnfinishedOperation(cwd)) return;
2077
+ if (await unfinishedGitOperation(cwd) !== null) return;
1768
2078
  try {
1769
- await runGit([...gitPrefix(cwd), "fetch", "origin", branch], {
1770
- cwd,
1771
- timeout: 6e4,
1772
- env: cLocaleEnv()
1773
- });
2079
+ await pullByMerge(branch, cwd, auth, 6e4);
1774
2080
  } catch (err) {
1775
2081
  if (/couldn't find remote ref/.test(err instanceof Error ? err.message : String(err))) return;
1776
2082
  throw err;
1777
2083
  }
1778
- if (await isAncestor("FETCH_HEAD", "HEAD", cwd)) return;
1779
- try {
1780
- await gitCommitting(["merge", "--ff", "--no-edit", "FETCH_HEAD"], cwd, 6e4);
1781
- } catch (err) {
1782
- const unmerged = await gitOrNull(["diff", "--name-only", "--diff-filter=U"], cwd) ?? "";
1783
- if (await gitOrNull(["rev-parse", "--quiet", "--verify", "MERGE_HEAD"], cwd) !== null) {
1784
- await gitOrNull(["merge", "--abort"], cwd);
1785
- }
1786
- const files = unmerged.split("\n").filter(Boolean);
1787
- if (files.length > 0) {
1788
- throw new Error(
1789
- `origin/${branch} and the local branch both have new commits that conflict in ${files.join(", ")}; the merge was aborted and nothing was pushed`
1790
- );
1791
- }
1792
- throw err;
1793
- }
1794
- }
1795
- async function hasUnfinishedOperation(cwd) {
1796
- for (const ref of ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
1797
- if (await gitOrNull(["rev-parse", "--quiet", "--verify", ref], cwd) !== null) return true;
1798
- }
1799
- for (const dir of ["rebase-merge", "rebase-apply"]) {
1800
- const path = await gitOrNull(["rev-parse", "--git-path", dir], cwd);
1801
- if (path === null) continue;
1802
- const found = await tryWithFsDeadline(
1803
- `stat ${dir}`,
1804
- () => stat(resolve(cwd, path)).then(() => true),
1805
- false
1806
- );
1807
- if (found) return true;
1808
- }
1809
- return false;
1810
2084
  }
1811
2085
  async function originTrackingRef(branch, cwd) {
1812
2086
  const ref = `refs/remotes/origin/${branch}`;
@@ -1852,11 +2126,34 @@ async function isDirEmpty(dir) {
1852
2126
  const entries = await tryWithFsDeadline(`readdir ${dir}`, () => readdir(dir), []);
1853
2127
  return entries.length === 0;
1854
2128
  }
1855
- async function commitAll(cwd, message) {
2129
+ async function commitAll(cwd, message, log) {
2130
+ if (await refuseUnfinishedOperation(cwd, log)) return null;
1856
2131
  if (!await hasChanges(cwd)) return null;
1857
- await git(["add", "-A"], cwd);
2132
+ await stageAll(cwd, log);
2133
+ if (await gitOrNull(["diff", "--cached", "--quiet"], cwd) !== null) return null;
1858
2134
  return gitCommitting(["commit", "-m", message], cwd);
1859
2135
  }
2136
+ async function stageAll(cwd, log) {
2137
+ await git(["add", "-A"], cwd);
2138
+ const raw = await gitOrNull(["diff", "--cached", "--raw", "--no-abbrev", "--no-renames", "-z"], cwd) ?? "";
2139
+ const fields = raw.split("\0");
2140
+ for (let i = 0; i + 1 < fields.length; i += 2) {
2141
+ const m = /^:160000 160000 ([0-9a-f]+) ([0-9a-f]+) M$/.exec(fields[i] ?? "");
2142
+ const path = fields[i + 1];
2143
+ if (m === null || !path) continue;
2144
+ const pinned = m[1];
2145
+ const staged = m[2];
2146
+ const dir = join(cwd, path);
2147
+ const known = await gitOrNull(["cat-file", "-e", `${pinned}^{commit}`], dir) !== null;
2148
+ if (!known || await isAncestor(staged, pinned, dir)) {
2149
+ await gitOrNull(["reset", "-q", "--", path], cwd);
2150
+ log.info("left a submodule pin out of the commit", {
2151
+ path,
2152
+ reason: known ? "would move the pin backwards" : "pinned commit is not local"
2153
+ });
2154
+ }
2155
+ }
2156
+ }
1860
2157
  var TokenMediatorError = class extends Error {
1861
2158
  /** Every mediator refusal is fixed where the credential lives, whatever its code. */
1862
2159
  skaileConnectErrorKind = "credential";
@@ -1886,6 +2183,27 @@ var GitConnector = class extends AbstractConnector {
1886
2183
  accessLevel: "read",
1887
2184
  args: [],
1888
2185
  examples: ["sync_status"]
2186
+ },
2187
+ {
2188
+ name: "push",
2189
+ description: "Push this mount's branch (the session branch on a branching mount) to origin, authenticated by the mount. Pushes commits only: commit with the git CLI first. Never forces; a push origin refuses comes back as status 'rejected' (run 'pull', then push again). The checkout must be on the mount's branch.",
2190
+ accessLevel: "write",
2191
+ args: [],
2192
+ examples: ["push"]
2193
+ },
2194
+ {
2195
+ name: "pull",
2196
+ description: "Fetch origin and merge origin's copy of this mount's branch into it (a merge, never a rebase). On a conflict the merge is aborted, the checkout is left as it was, and the conflicting files are listed (status 'conflict'); the fetched commits stay at origin/<branch> for a local `git merge`. Local changes that the merge would overwrite, or a merge or rebase already in progress, give status 'blocked'.",
2197
+ accessLevel: "read",
2198
+ args: [],
2199
+ examples: ["pull"]
2200
+ },
2201
+ {
2202
+ name: "fetch",
2203
+ description: "Fetch origin, authenticated by the mount, updating origin/<branch> refs only. The working tree is not touched.",
2204
+ accessLevel: "read",
2205
+ args: [],
2206
+ examples: ["fetch"]
1889
2207
  }
1890
2208
  ];
1891
2209
  }
@@ -1899,10 +2217,163 @@ var GitConnector = class extends AbstractConnector {
1899
2217
  const state = handle.state;
1900
2218
  return JSON.stringify(await readGitSyncStatus(cwd, state.branch));
1901
2219
  }
2220
+ case "push":
2221
+ case "pull":
2222
+ case "fetch":
2223
+ return JSON.stringify(await this.agentRemoteOp(handle, operation));
1902
2224
  default:
1903
2225
  throw new Error(`GitConnector has no operation: ${operation}`);
1904
2226
  }
1905
2227
  }
2228
+ /**
2229
+ * Run `fn` after every earlier job on this mount has settled. Network git and
2230
+ * index-writing git on one checkout must not overlap: two of them contend for
2231
+ * `.git/index.lock`, and a push racing a pull can push a half-merged branch.
2232
+ * Not re-entrant — a job must never wait on the lock it runs under.
2233
+ */
2234
+ withMountLock(s, fn) {
2235
+ const run = (s.mountLock ?? Promise.resolve()).then(fn);
2236
+ s.mountLock = run.then(
2237
+ () => void 0,
2238
+ () => void 0
2239
+ );
2240
+ return run;
2241
+ }
2242
+ /**
2243
+ * The agent's network git: the driver runs it with the mount's credential, so
2244
+ * an agent on a mount without `exposeAccessToken` can push and pull without
2245
+ * ever holding the token. Acts only on the mount's own branch, never forces.
2246
+ */
2247
+ async agentRemoteOp(handle, op) {
2248
+ const cwd = handle.mountPath;
2249
+ if (!cwd || handle.status !== "connected") {
2250
+ throw new Error(`git ${op}: the mount is not connected (${handle.status})`);
2251
+ }
2252
+ const s = handle.state;
2253
+ if (s.fleetManaged) {
2254
+ throw new Error(`git ${op}: the host syncs this mount; there is nothing to run here`);
2255
+ }
2256
+ if (op === "push" && handle.access === "read-only") {
2257
+ throw new Error("git push: this mount is read-only");
2258
+ }
2259
+ return this.withMountLock(s, async () => {
2260
+ if (s.disposed) throw new Error(`git ${op}: the mount is disconnected`);
2261
+ await assertOriginIsMount(s, cwd, op);
2262
+ if (op !== "fetch") {
2263
+ const head = await gitOrNull(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd);
2264
+ if (head !== s.activeBranch) {
2265
+ throw new Error(
2266
+ `git ${op}: the checkout is on ${head === null ? "a detached HEAD" : `"${head}"`}, but this mount pushes and pulls only "${s.activeBranch}". Check it out first.`
2267
+ );
2268
+ }
2269
+ }
2270
+ let outcome;
2271
+ try {
2272
+ outcome = op === "push" ? await this.agentPush(s, cwd) : op === "pull" ? await this.agentPull(s, cwd) : await this.agentFetch(s, cwd);
2273
+ } catch (err) {
2274
+ throw new Error(
2275
+ `git ${op} failed: ${redactCredentials(err instanceof Error ? err.message : String(err))}`
2276
+ );
2277
+ }
2278
+ this.log.info("agent git op", { op, branch: s.activeBranch, status: outcome.status });
2279
+ return {
2280
+ op,
2281
+ branch: s.activeBranch,
2282
+ ...outcome,
2283
+ sync: await readGitSyncStatus(cwd, s.branch)
2284
+ };
2285
+ });
2286
+ }
2287
+ /** Fetch origin's base branch and, on a branching mount, the session branch. */
2288
+ async fetchActiveBranch(s, cwd) {
2289
+ const auth = repoScopedAuth(s);
2290
+ await this.fetchOrigin(s, cwd, auth);
2291
+ if (s.activeBranch === s.branch) return;
2292
+ if (await this.trackSessionBranch(s.activeBranch, cwd, auth) === "failed") {
2293
+ throw new Error(`could not fetch origin/${s.activeBranch}`);
2294
+ }
2295
+ }
2296
+ async agentFetch(s, cwd) {
2297
+ await this.fetchActiveBranch(s, cwd);
2298
+ return { status: "fetched" };
2299
+ }
2300
+ async agentPush(s, cwd) {
2301
+ const b = s.activeBranch;
2302
+ const args = ["push", "origin", `refs/heads/${b}:refs/heads/${b}`];
2303
+ try {
2304
+ await runRemoteGit(args, repoScopedAuth(s), {
2305
+ cwd,
2306
+ safeDir: cwd,
2307
+ timeout: 6e4,
2308
+ env: cLocaleEnv()
2309
+ });
2310
+ } catch (err) {
2311
+ const message = redactCredentials(err instanceof Error ? err.message : String(err));
2312
+ if (/\[rejected\]|non-fast-forward|fetch first/.test(message)) {
2313
+ return {
2314
+ status: "rejected",
2315
+ message: `origin has commits on "${b}" that this checkout lacks, so the push was refused (no force). Run the "pull" op, then push again.`
2316
+ };
2317
+ }
2318
+ const remote = /\[remote rejected\][^\n]*/.exec(message)?.[0];
2319
+ if (remote !== void 0) {
2320
+ return { status: "rejected", message: `origin refused the push: ${remote.trim()}` };
2321
+ }
2322
+ throw err;
2323
+ }
2324
+ if (b === s.branch) {
2325
+ await gitOrNull(["update-ref", `refs/remotes/origin/${b}`, `refs/heads/${b}`], cwd);
2326
+ } else {
2327
+ await setSessionUpstream(b, cwd);
2328
+ await this.recordPushedSessionBranch(b, cwd);
2329
+ }
2330
+ return { status: "pushed" };
2331
+ }
2332
+ async agentPull(s, cwd) {
2333
+ const b = s.activeBranch;
2334
+ await this.fetchActiveBranch(s, cwd);
2335
+ const pending = await unfinishedGitOperation(cwd);
2336
+ if (pending !== null) {
2337
+ return {
2338
+ status: "blocked",
2339
+ message: `the checkout has an unfinished merge or rebase (${pending}), so nothing was merged. Finish it (resolve, \`git add\`, \`git commit\`) or abort it, then pull again.`
2340
+ };
2341
+ }
2342
+ const upstream = await originTrackingRef(b, cwd);
2343
+ if (upstream === null) {
2344
+ return {
2345
+ status: "no-upstream",
2346
+ message: `origin has no branch "${b}" yet; nothing to pull. "push" publishes it.`
2347
+ };
2348
+ }
2349
+ if (await isAncestor(upstream, "HEAD", cwd)) return { status: "up-to-date" };
2350
+ try {
2351
+ await gitCommitting(["merge", "--ff", "--no-edit", upstream], cwd, 6e4);
2352
+ } catch (err) {
2353
+ const unmerged = await gitOrNull(["diff", "--name-only", "--diff-filter=U"], cwd);
2354
+ const conflicts = (unmerged ?? "").split("\n").filter(Boolean);
2355
+ if (await gitOrNull(["rev-parse", "--quiet", "--verify", "MERGE_HEAD"], cwd) !== null) {
2356
+ await git(["merge", "--abort"], cwd);
2357
+ }
2358
+ if (conflicts.length > 0) {
2359
+ return {
2360
+ status: "conflict",
2361
+ conflicts,
2362
+ message: `merging origin/${b} conflicts, so the merge was aborted and the checkout is as it was. The fetched commits are at origin/${b}: run \`git merge origin/${b}\`, resolve, commit, then "push".`
2363
+ };
2364
+ }
2365
+ return {
2366
+ status: "blocked",
2367
+ message: `the merge did not start: ${redactCredentials(err instanceof Error ? err.message : String(err))}`
2368
+ };
2369
+ }
2370
+ const stale = await checkoutSubmodulesOffline(cwd);
2371
+ if (stale.length === 0) return { status: "merged" };
2372
+ return {
2373
+ status: "merged",
2374
+ message: `submodules ${stale.join(", ")} were not updated: their new commits need a fetch, which happens at the next platform sync. Do not stage or commit them until then.`
2375
+ };
2376
+ }
1906
2377
  async connect(declaration, ctx) {
1907
2378
  const targetDir = ctx.mountTarget;
1908
2379
  if (!targetDir) {
@@ -1945,11 +2416,10 @@ var GitConnector = class extends AbstractConnector {
1945
2416
  targetDir,
1946
2417
  tokenMediator: initial.authRef?.kind === "backend" ? tokenMediator : void 0
1947
2418
  };
1948
- const authUrl = this.authUrl(state);
1949
- await this.prepareCheckout(targetDir, url, branch, authUrl, declaration.access, started);
2419
+ await this.prepareCheckout(targetDir, url, branch, state, declaration.access, started);
1950
2420
  if (session.enabled && !session.isMain && session.slug) {
1951
2421
  if (declaration.mount?.exposeAccessToken !== true) {
1952
- await this.rebakeRemoteAuth(targetDir, state);
2422
+ await this.reconcileRemoteUrl(targetDir, state);
1953
2423
  }
1954
2424
  await this.checkoutSessionBranch(state, session, targetDir);
1955
2425
  }
@@ -1969,7 +2439,7 @@ var GitConnector = class extends AbstractConnector {
1969
2439
  }
1970
2440
  }
1971
2441
  if (!state.managedGitconfig) {
1972
- await this.rebakeRemoteAuth(targetDir, state);
2442
+ await this.reconcileRemoteUrl(targetDir, state);
1973
2443
  if (initial.token) {
1974
2444
  const providerKind = initial.authRef?.kind ?? "pat";
1975
2445
  const skipReason = providerKind !== "backend" ? "non-backend-credential" : !state.tokenMediator ? "no-token-mediator" : !initial.expiresAt ? "no-expiry" : null;
@@ -1989,13 +2459,17 @@ var GitConnector = class extends AbstractConnector {
1989
2459
  }
1990
2460
  }
1991
2461
  if (state.sessionBranchFetch === "failed") {
1992
- state.sessionBranchFetch = await this.trackSessionBranch(state.activeBranch, targetDir);
2462
+ state.sessionBranchFetch = await this.trackSessionBranch(
2463
+ state.activeBranch,
2464
+ targetDir,
2465
+ state
2466
+ );
1993
2467
  if (state.sessionBranchFetch === "fetched") {
1994
2468
  if (state.sessionBranchCreated) {
1995
- await this.adoptOriginSessionBranch(state.activeBranch, targetDir);
2469
+ await this.adoptOriginSessionBranch(state, state.activeBranch, targetDir);
1996
2470
  } else {
1997
2471
  await this.reconcileLocalSessionBranch(state, state.activeBranch, targetDir);
1998
- await syncSubmodules(targetDir, this.log);
2472
+ await syncSubmodules(targetDir, this.log, state);
1999
2473
  }
2000
2474
  }
2001
2475
  }
@@ -2041,55 +2515,57 @@ var GitConnector = class extends AbstractConnector {
2041
2515
  * reset, no clean. Existing files become uncommitted-on-init; the
2042
2516
  * agent or `pushAfterCommit` reconciles with remote later.
2043
2517
  */
2044
- async prepareCheckout(targetDir, url, branch, authUrl, access, started) {
2518
+ async prepareCheckout(targetDir, url, branch, auth, access, started) {
2045
2519
  try {
2046
2520
  if (await pathExists(join(targetDir, ".git"))) {
2047
2521
  await clearStaleIndexLock(targetDir, this.log);
2048
2522
  if (await this.isInitialisedCheckout(targetDir)) {
2049
2523
  this.log.info("found existing checkout \u2014 using as-is", { branch, targetDir });
2050
2524
  } else {
2051
- if (await this.countRemoteHeads(targetDir, authUrl) === 0) {
2525
+ if (await this.countRemoteHeads(targetDir, auth) === 0) {
2052
2526
  this.log.info("empty .git against a ref-less remote \u2014 bootstrapping branch", {
2053
2527
  branch,
2054
2528
  access
2055
2529
  });
2056
- await this.bootstrapEmptyRemote(targetDir, branch, authUrl, access);
2530
+ await this.bootstrapEmptyRemote(targetDir, branch, auth, access);
2057
2531
  } else {
2058
2532
  this.log.info("found empty .git \u2014 bootstrapping from origin", { branch });
2059
- await this.bootstrapFromOrigin(targetDir, branch, authUrl);
2533
+ await this.bootstrapFromOrigin(targetDir, branch, auth);
2060
2534
  }
2061
2535
  this.log.info("bootstrap ok", { branch, durationMs: Date.now() - started });
2062
2536
  }
2063
2537
  } else {
2064
2538
  await withFsDeadline(`mkdir ${targetDir}`, () => mkdir(targetDir, { recursive: true }));
2065
2539
  if (await isDirEmpty(targetDir)) {
2066
- if (await this.countRemoteHeads(targetDir, authUrl) === 0) {
2540
+ if (await this.countRemoteHeads(targetDir, auth) === 0) {
2067
2541
  this.log.info("remote has no refs \u2014 bootstrapping branch", { url, branch, access });
2068
- await this.bootstrapEmptyRemote(targetDir, branch, authUrl, access);
2542
+ await this.bootstrapEmptyRemote(targetDir, branch, auth, access);
2069
2543
  this.log.info("empty-remote bootstrap ok", {
2070
2544
  branch,
2071
2545
  durationMs: Date.now() - started
2072
2546
  });
2073
2547
  } else {
2074
2548
  this.log.info("clone start", { url, branch });
2075
- await runGit(
2549
+ await runRemoteGit(
2076
2550
  [
2077
- ...gitPrefix(targetDir),
2078
2551
  "clone",
2079
2552
  "--branch",
2080
2553
  branch,
2081
2554
  "--single-branch",
2082
- "--recurse-submodules",
2083
- authUrl,
2555
+ "--no-checkout",
2556
+ plainRemoteUrl(auth),
2084
2557
  targetDir
2085
2558
  ],
2086
- { timeout: 12e4 }
2559
+ auth,
2560
+ { safeDir: targetDir, timeout: 12e4 }
2087
2561
  );
2562
+ await git(["checkout", "-f", branch], targetDir, 12e4);
2563
+ await syncSubmodules(targetDir, this.log, auth);
2088
2564
  this.log.info("clone ok", { branch, durationMs: Date.now() - started });
2089
2565
  }
2090
2566
  } else {
2091
2567
  this.log.warn("target non-empty without .git \u2014 initialising in place", { targetDir });
2092
- await this.initInPlace(targetDir, authUrl);
2568
+ await this.initInPlace(targetDir, plainRemoteUrl(auth));
2093
2569
  this.log.info("init in place ok", { durationMs: Date.now() - started });
2094
2570
  }
2095
2571
  }
@@ -2113,7 +2589,7 @@ var GitConnector = class extends AbstractConnector {
2113
2589
  "config"
2114
2590
  );
2115
2591
  }
2116
- state.sessionBranchFetch = await this.trackSessionBranch(sessionBranch, targetDir);
2592
+ state.sessionBranchFetch = await this.trackSessionBranch(sessionBranch, targetDir, state);
2117
2593
  let created = false;
2118
2594
  if (await localBranchExists(sessionBranch, targetDir)) {
2119
2595
  await gitLocalCheckout([sessionBranch], targetDir);
@@ -2130,20 +2606,20 @@ var GitConnector = class extends AbstractConnector {
2130
2606
  }
2131
2607
  if (originHasBranch) {
2132
2608
  await gitLocalCheckout(["-B", sessionBranch, `origin/${sessionBranch}`], targetDir);
2133
- } else if (!await this.branchFromRef(session, sessionBranch, targetDir)) {
2609
+ } else if (!await this.branchFromRef(state, session, sessionBranch, targetDir)) {
2134
2610
  await gitLocalCheckout(["-b", sessionBranch], targetDir);
2135
2611
  created = true;
2136
2612
  } else {
2137
2613
  created = true;
2138
2614
  }
2139
2615
  }
2140
- await syncSubmodules(targetDir, this.log);
2616
+ await syncSubmodules(targetDir, this.log, state);
2141
2617
  state.activeBranch = sessionBranch;
2142
2618
  state.sessionBranchCreated = created;
2143
2619
  this.log.info("session branch checked out", { sessionBranch });
2144
2620
  if (created && state.lifecycle.publishOnCreate) {
2145
2621
  try {
2146
- await git(["push", "-u", "origin", sessionBranch], targetDir, 6e4);
2622
+ await gitRemote(["push", "-u", "origin", sessionBranch], targetDir, state, 6e4);
2147
2623
  await this.recordPushedSessionBranch(sessionBranch, targetDir);
2148
2624
  this.log.info("published new session branch", { sessionBranch });
2149
2625
  } catch (err) {
@@ -2165,11 +2641,12 @@ var GitConnector = class extends AbstractConnector {
2165
2641
  * every plain `git fetch origin` fail. Runs on every connect, so it also
2166
2642
  * repairs a checkout cloned before the refspec existed.
2167
2643
  */
2168
- async trackSessionBranch(sessionBranch, targetDir) {
2644
+ async trackSessionBranch(sessionBranch, targetDir, auth) {
2169
2645
  let outcome;
2170
2646
  try {
2171
- await runGit([...gitPrefix(targetDir), "fetch", "origin", trackingRefspec(sessionBranch)], {
2647
+ await runRemoteGit(["fetch", "origin", trackingRefspec(sessionBranch)], auth, {
2172
2648
  cwd: targetDir,
2649
+ safeDir: targetDir,
2173
2650
  timeout: 6e4,
2174
2651
  env: cLocaleEnv()
2175
2652
  });
@@ -2204,10 +2681,10 @@ var GitConnector = class extends AbstractConnector {
2204
2681
  * checkout is refused (for example local changes in the way), the branch
2205
2682
  * stays as it is and the push later fails as non-fast-forward.
2206
2683
  */
2207
- async adoptOriginSessionBranch(sessionBranch, targetDir) {
2684
+ async adoptOriginSessionBranch(state, sessionBranch, targetDir) {
2208
2685
  try {
2209
2686
  await gitLocalCheckout(["-B", sessionBranch, `origin/${sessionBranch}`], targetDir);
2210
- await syncSubmodules(targetDir, this.log);
2687
+ await syncSubmodules(targetDir, this.log, state);
2211
2688
  this.log.info("session branch re-pointed at origin after auth retry", { sessionBranch });
2212
2689
  } catch (err) {
2213
2690
  this.log.warn("could not re-point session branch at origin", {
@@ -2266,6 +2743,7 @@ var GitConnector = class extends AbstractConnector {
2266
2743
  async integrateOriginSessionBranch(s, cwd, branch = s.activeBranch) {
2267
2744
  const originRef = await originTrackingRef(branch, cwd);
2268
2745
  if (originRef === null || await isAncestor(originRef, "HEAD", cwd)) return;
2746
+ if (await this.skipOverUnfinishedOperation(cwd, "merge of origin session branch")) return;
2269
2747
  try {
2270
2748
  await gitCommitting(["merge", "--no-edit", originRef], cwd, 6e4);
2271
2749
  this.log.info("merged origin session branch", { branch });
@@ -2307,12 +2785,12 @@ var GitConnector = class extends AbstractConnector {
2307
2785
  */
2308
2786
  async pushActiveBranch(s, cwd) {
2309
2787
  if (s.activeBranch === s.branch) {
2310
- await integrateOriginBaseBranch(s.branch, cwd);
2311
- await git(["push", "origin", s.activeBranch], cwd, 6e4);
2788
+ await integrateOriginBaseBranch(s.branch, cwd, s);
2789
+ await gitRemote(["push", "origin", s.activeBranch], cwd, s, 6e4);
2312
2790
  return;
2313
2791
  }
2314
2792
  await this.integrateOriginSessionBranch(s, cwd);
2315
- await git(["push", "-u", "origin", s.activeBranch], cwd, 6e4);
2793
+ await gitRemote(["push", "-u", "origin", s.activeBranch], cwd, s, 6e4);
2316
2794
  await this.recordPushedSessionBranch(s.activeBranch, cwd);
2317
2795
  }
2318
2796
  /**
@@ -2320,7 +2798,7 @@ var GitConnector = class extends AbstractConnector {
2320
2798
  * true when the branch was created from the anchor; false when no valid
2321
2799
  * `fromRef` was supplied or the fetch failed (caller falls back to base).
2322
2800
  */
2323
- async branchFromRef(session, sessionBranch, targetDir) {
2801
+ async branchFromRef(auth, session, sessionBranch, targetDir) {
2324
2802
  if (!session.fromRef) return false;
2325
2803
  if (!isValidGitRefOrSha(session.fromRef)) {
2326
2804
  this.log.warn("fromRef rejected by refname validation, falling back to base branch", {
@@ -2330,7 +2808,7 @@ var GitConnector = class extends AbstractConnector {
2330
2808
  return false;
2331
2809
  }
2332
2810
  try {
2333
- await git(["fetch", "origin", session.fromRef], targetDir);
2811
+ await gitRemote(["fetch", "origin", session.fromRef], targetDir, auth);
2334
2812
  await git(["checkout", "-b", sessionBranch, "FETCH_HEAD"], targetDir);
2335
2813
  this.log.info("session branch created from fromRef", {
2336
2814
  sessionBranch,
@@ -2462,16 +2940,18 @@ var GitConnector = class extends AbstractConnector {
2462
2940
  this.log.debug("sync skipped \u2014 fleet-managed");
2463
2941
  return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
2464
2942
  }
2465
- this.log.debug("sync (pull) start", { branch: s.activeBranch });
2466
- try {
2467
- await gitCommitting(["pull", "origin", s.activeBranch], handle.mountPath, 6e4);
2468
- await syncSubmodules(handle.mountPath, this.log);
2469
- this.log.debug("sync ok");
2470
- } catch (err) {
2471
- this.log.error("sync failed", err, { branch: s.activeBranch });
2472
- throw err;
2473
- }
2474
- return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
2943
+ return this.withMountLock(s, async () => {
2944
+ this.log.debug("sync (pull) start", { branch: s.activeBranch });
2945
+ try {
2946
+ await pullByMerge(s.activeBranch, handle.mountPath, s, 6e4);
2947
+ await syncSubmodules(handle.mountPath, this.log, s);
2948
+ this.log.debug("sync ok");
2949
+ } catch (err) {
2950
+ this.log.error("sync failed", err, { branch: s.activeBranch });
2951
+ throw err;
2952
+ }
2953
+ return { filesWritten: 0, filesDeleted: 0, bytesTransferred: 0 };
2954
+ });
2475
2955
  }
2476
2956
  // ── Watch ──────────────────────────────────────────────────────────────────
2477
2957
  watch(handle, callback, options) {
@@ -2516,19 +2996,21 @@ var GitConnector = class extends AbstractConnector {
2516
2996
  }
2517
2997
  const lc = s.lifecycle;
2518
2998
  if (!lc.commitOnHibernate) return;
2519
- const result = await commitAll(handle.mountPath, `skaile: hibernate snapshot`);
2520
- if (!result) return this.pushUnpushedOnHibernate(s, handle.mountPath);
2521
- const sha = await git(["rev-parse", "--short", "HEAD"], handle.mountPath);
2522
- if (lc.pushOnHibernate) {
2523
- try {
2524
- await this.pushActiveBranch(s, handle.mountPath);
2525
- this.log.info("push ok", { branch: s.activeBranch });
2526
- } catch (err) {
2527
- this.log.error("push failed", err, { branch: s.activeBranch });
2528
- throw err;
2999
+ return this.withMountLock(s, async () => {
3000
+ const result = await commitAll(handle.mountPath, `skaile: hibernate snapshot`, this.log);
3001
+ if (!result) return this.pushUnpushedOnHibernate(s, handle.mountPath);
3002
+ const sha = await git(["rev-parse", "--short", "HEAD"], handle.mountPath);
3003
+ if (lc.pushOnHibernate) {
3004
+ try {
3005
+ await this.pushActiveBranch(s, handle.mountPath);
3006
+ this.log.info("push ok", { branch: s.activeBranch });
3007
+ } catch (err) {
3008
+ this.log.error("push failed", err, { branch: s.activeBranch });
3009
+ throw err;
3010
+ }
2529
3011
  }
2530
- }
2531
- return `Committed ${sha} on ${s.activeBranch}${lc.pushOnHibernate ? " (pushed)" : ""}`;
3012
+ return `Committed ${sha} on ${s.activeBranch}${lc.pushOnHibernate ? " (pushed)" : ""}`;
3013
+ });
2532
3014
  }
2533
3015
  /**
2534
3016
  * Hibernate with nothing new to commit: push commits that are still only
@@ -2560,46 +3042,54 @@ var GitConnector = class extends AbstractConnector {
2560
3042
  }
2561
3043
  const lc = s.lifecycle;
2562
3044
  const details = [];
2563
- try {
2564
- const commitResult = await commitAll(handle.mountPath, `skaile: session close`);
2565
- if (commitResult) {
2566
- const sha = await git(["rev-parse", "--short", "HEAD"], handle.mountPath);
2567
- details.push(`Committed ${sha}`);
2568
- }
2569
- if (lc.pushOnClose || lc.mergeOnClose) {
2570
- await this.pushActiveBranch(s, handle.mountPath);
2571
- details.push(`Pushed ${s.activeBranch}`);
2572
- }
2573
- if (lc.mergeOnClose && s.activeBranch !== s.branch) {
2574
- await git(["checkout", s.branch], handle.mountPath);
2575
- await gitCommitting(["pull", "origin", s.branch], handle.mountPath, 6e4);
2576
- await syncSubmodules(handle.mountPath, this.log);
2577
- let merged = true;
2578
- try {
2579
- await gitCommitting(["merge", s.activeBranch], handle.mountPath);
2580
- } catch (err) {
2581
- this.log.warn("merge failed", {
2582
- branch: s.activeBranch,
2583
- error: redactCredentials(err instanceof Error ? err.message : String(err))
3045
+ return this.withMountLock(s, async () => {
3046
+ try {
3047
+ const commitResult = await commitAll(handle.mountPath, `skaile: session close`, this.log);
3048
+ if (commitResult) {
3049
+ const sha = await git(["rev-parse", "--short", "HEAD"], handle.mountPath);
3050
+ details.push(`Committed ${sha}`);
3051
+ }
3052
+ if (lc.pushOnClose || lc.mergeOnClose) {
3053
+ await this.pushActiveBranch(s, handle.mountPath);
3054
+ details.push(`Pushed ${s.activeBranch}`);
3055
+ }
3056
+ const closePending = lc.mergeOnClose && s.activeBranch !== s.branch ? await unfinishedGitOperation(handle.mountPath) : null;
3057
+ if (closePending !== null) {
3058
+ this.log.warn("merge on close skipped: the checkout has an unfinished merge or rebase", {
3059
+ reason: closePending
2584
3060
  });
2585
- const resolved = await this.resolveConflictsOrAbort(s, handle.mountPath, "merge");
2586
- if (resolved) {
2587
- details.push("Resolved merge conflicts (created .conflict files)");
2588
- } else {
2589
- merged = false;
3061
+ details.push(`Merge of ${s.activeBranch} into ${s.branch} skipped: ${closePending}`);
3062
+ } else if (lc.mergeOnClose && s.activeBranch !== s.branch) {
3063
+ await git(["checkout", s.branch], handle.mountPath);
3064
+ await pullByMerge(s.branch, handle.mountPath, s, 6e4);
3065
+ await syncSubmodules(handle.mountPath, this.log, s);
3066
+ let merged = true;
3067
+ try {
3068
+ await gitCommitting(["merge", s.activeBranch], handle.mountPath);
3069
+ } catch (err) {
3070
+ this.log.warn("merge failed", {
3071
+ branch: s.activeBranch,
3072
+ error: redactCredentials(err instanceof Error ? err.message : String(err))
3073
+ });
3074
+ const resolved = await this.resolveConflictsOrAbort(s, handle.mountPath, "merge");
3075
+ if (resolved) {
3076
+ details.push("Resolved merge conflicts (created .conflict files)");
3077
+ } else {
3078
+ merged = false;
3079
+ }
2590
3080
  }
3081
+ if (lc.pushOnClose) {
3082
+ await gitRemote(["push", "origin", s.branch], handle.mountPath, s, 6e4);
3083
+ }
3084
+ details.push(
3085
+ merged ? `Merged ${s.activeBranch} into ${s.branch}` : `Merge of ${s.activeBranch} into ${s.branch} FAILED`
3086
+ );
2591
3087
  }
2592
- if (lc.pushOnClose) {
2593
- await git(["push", "origin", s.branch], handle.mountPath, 6e4);
2594
- }
2595
- details.push(
2596
- merged ? `Merged ${s.activeBranch} into ${s.branch}` : `Merge of ${s.activeBranch} into ${s.branch} FAILED`
2597
- );
3088
+ return details.join("; ") || void 0;
3089
+ } finally {
3090
+ await this.teardownManagedCredential(handle.id, s);
2598
3091
  }
2599
- return details.join("; ") || void 0;
2600
- } finally {
2601
- await this.teardownManagedCredential(handle.id, s);
2602
- }
3092
+ });
2603
3093
  }
2604
3094
  // ── Private: managed gitconfig (Tier-2 credential exposure) ──────────────
2605
3095
  /**
@@ -2926,15 +3416,6 @@ var GitConnector = class extends AbstractConnector {
2926
3416
  token: result.token,
2927
3417
  credentialsPath: state.managedGitconfig.credentialsPath
2928
3418
  });
2929
- } else if (state.targetDir) {
2930
- const rebake = this.rebakeRemoteAuth(state.targetDir, state);
2931
- state.rebakeInFlight = rebake;
2932
- try {
2933
- await rebake;
2934
- } finally {
2935
- if (state.rebakeInFlight === rebake) state.rebakeInFlight = void 0;
2936
- }
2937
- if (state.tokenMediator !== mediator) return;
2938
3419
  }
2939
3420
  this.log.info("refresh", {
2940
3421
  event: "refresh",
@@ -2954,11 +3435,7 @@ var GitConnector = class extends AbstractConnector {
2954
3435
  state.refreshTimer = void 0;
2955
3436
  }
2956
3437
  state.tokenMediator = void 0;
2957
- if (state.rebakeInFlight) await state.rebakeInFlight;
2958
- if (!state.managedGitconfig) {
2959
- await this.stripRemoteAuth(state);
2960
- return;
2961
- }
3438
+ if (!state.managedGitconfig) return;
2962
3439
  try {
2963
3440
  removeMountBlock({
2964
3441
  mountId: connectorId,
@@ -2991,7 +3468,7 @@ var GitConnector = class extends AbstractConnector {
2991
3468
  * The intent is to preserve whatever the agent last had on disk. We
2992
3469
  * therefore:
2993
3470
  * - `git init` — creates `.git/`; does not modify any existing file.
2994
- * - `git remote add origin <authUrl>` — wires up the remote so the
3471
+ * - `git remote add origin <remoteUrl>` — wires up the remote so the
2995
3472
  * normal lifecycle hooks (`pushOnHibernate`, `pushAfterCommit`)
2996
3473
  * have a target.
2997
3474
  *
@@ -3003,32 +3480,32 @@ var GitConnector = class extends AbstractConnector {
3003
3480
  * user's last on-disk state. The agent or autocommit reconciles
3004
3481
  * with the remote later.
3005
3482
  *
3006
- * Token persistence: the auth URL gets written into `.git/config`
3007
- * exactly like the `git clone <authUrl>` path does — token-rotation
3008
- * handling is external to this method (managed-gitconfig credential
3009
- * helper refresh in Tier-2 connectors).
3483
+ * `remoteUrl` carries no credential; network commands authenticate per
3484
+ * command (see `runRemoteGit`).
3010
3485
  */
3011
- async initInPlace(targetDir, authUrl) {
3486
+ async initInPlace(targetDir, remoteUrl) {
3012
3487
  await git(["init"], targetDir, 1e4);
3013
3488
  try {
3014
3489
  await git(["remote", "remove", "origin"], targetDir, 5e3);
3015
3490
  } catch {
3016
3491
  }
3017
- await git(["remote", "add", "origin", authUrl], targetDir, 5e3);
3492
+ await git(["remote", "add", "origin", remoteUrl], targetDir, 5e3);
3018
3493
  }
3019
3494
  /**
3020
- * Count `refs/heads/*` on `authUrl`, optionally narrowed to one `branch`.
3495
+ * Count `refs/heads/*` on the remote, optionally narrowed to one `branch`.
3021
3496
  *
3022
3497
  * Returns `null` when the probe itself fails (network, auth, bad URL).
3023
3498
  * "Cannot tell" must never collapse into "no refs": the empty-remote
3024
3499
  * fallback WRITES to origin, and every caller therefore treats `null` as
3025
3500
  * "assume populated" and falls through to the path that fails loudly.
3026
3501
  */
3027
- async countRemoteHeads(targetDir, authUrl, branch) {
3502
+ async countRemoteHeads(targetDir, auth, branch) {
3503
+ const remoteUrl = plainRemoteUrl(auth);
3028
3504
  try {
3029
- const out = await runGit(
3030
- branch === void 0 ? [...gitPrefix(targetDir), "ls-remote", "--heads", authUrl] : [...gitPrefix(targetDir), "ls-remote", "--heads", authUrl, `refs/heads/${branch}`],
3031
- { timeout: 3e4 }
3505
+ const out = await runRemoteGit(
3506
+ branch === void 0 ? ["ls-remote", "--heads", remoteUrl] : ["ls-remote", "--heads", remoteUrl, `refs/heads/${branch}`],
3507
+ auth,
3508
+ { safeDir: targetDir, timeout: 3e4 }
3032
3509
  );
3033
3510
  return out.length === 0 ? 0 : out.split("\n").length;
3034
3511
  } catch (err) {
@@ -3062,8 +3539,8 @@ var GitConnector = class extends AbstractConnector {
3062
3539
  * come up with a usable checkout even when origin is unreachable — the
3063
3540
  * lifecycle hooks (`pushAfterCommit`, hibernate/close push) publish later.
3064
3541
  */
3065
- async bootstrapEmptyRemote(targetDir, branch, authUrl, access) {
3066
- await this.initInPlace(targetDir, authUrl);
3542
+ async bootstrapEmptyRemote(targetDir, branch, auth, access) {
3543
+ await this.initInPlace(targetDir, plainRemoteUrl(auth));
3067
3544
  await git(["symbolic-ref", "HEAD", `refs/heads/${branch}`], targetDir, 5e3);
3068
3545
  await git(
3069
3546
  [
@@ -3086,7 +3563,7 @@ var GitConnector = class extends AbstractConnector {
3086
3563
  return;
3087
3564
  }
3088
3565
  try {
3089
- await git(["push", "-u", "origin", branch], targetDir, 6e4);
3566
+ await gitRemote(["push", "-u", "origin", branch], targetDir, auth, 6e4);
3090
3567
  this.log.info("empty remote initialised on origin", { branch });
3091
3568
  return;
3092
3569
  } catch (err) {
@@ -3095,11 +3572,11 @@ var GitConnector = class extends AbstractConnector {
3095
3572
  error: redactCredentials(err instanceof Error ? err.message : String(err))
3096
3573
  });
3097
3574
  }
3098
- const publishedElsewhere = await this.countRemoteHeads(targetDir, authUrl, branch);
3575
+ const publishedElsewhere = await this.countRemoteHeads(targetDir, auth, branch);
3099
3576
  if (publishedElsewhere === null || publishedElsewhere === 0) return;
3100
3577
  try {
3101
3578
  this.log.info("origin gained the branch concurrently \u2014 adopting it", { branch });
3102
- await this.bootstrapFromOrigin(targetDir, branch, authUrl);
3579
+ await this.bootstrapFromOrigin(targetDir, branch, auth);
3103
3580
  } catch (err) {
3104
3581
  this.log.warn("adopting the concurrently-created branch failed; keeping local branch", {
3105
3582
  branch,
@@ -3138,22 +3615,19 @@ var GitConnector = class extends AbstractConnector {
3138
3615
  * `reset --hard`. Files that do not conflict with the remote stay
3139
3616
  * exactly where they are.
3140
3617
  *
3141
- * Auth-URL handling matches `initInPlace`: a pre-existing `origin`
3142
- * remote is replaced with the authed URL so the fetch works even
3143
- * after token rotation. This persists the token in `.git/config`
3144
- * the same way `git clone <authUrl>` does — token-rotation handling
3145
- * is external to this connector method.
3618
+ * A pre-existing `origin` is replaced with the declared URL, which also
3619
+ * drops a token an older driver wrote into it.
3146
3620
  */
3147
- async bootstrapFromOrigin(targetDir, branch, authUrl) {
3621
+ async bootstrapFromOrigin(targetDir, branch, auth) {
3148
3622
  try {
3149
3623
  await git(["remote", "remove", "origin"], targetDir, 5e3);
3150
3624
  } catch {
3151
3625
  }
3152
- await git(["remote", "add", "origin", authUrl], targetDir, 5e3);
3153
- await git(["fetch", "--depth=1", "origin", branch], targetDir, 12e4);
3626
+ await git(["remote", "add", "origin", plainRemoteUrl(auth)], targetDir, 5e3);
3627
+ await gitRemote(["fetch", "--depth=1", "origin", branch], targetDir, auth, 12e4);
3154
3628
  await this.preserveConflictingUntracked(targetDir, branch);
3155
3629
  await git(["checkout", "-B", branch, `origin/${branch}`], targetDir, 3e4);
3156
- await syncSubmodules(targetDir, this.log);
3630
+ await syncSubmodules(targetDir, this.log, auth);
3157
3631
  try {
3158
3632
  await git(["branch", `--set-upstream-to=origin/${branch}`, branch], targetDir, 5e3);
3159
3633
  } catch {
@@ -3203,16 +3677,7 @@ var GitConnector = class extends AbstractConnector {
3203
3677
  }
3204
3678
  }
3205
3679
  }
3206
- // ── Private: auth URL ─────────────────────────────────────────────────────
3207
- authUrl(s) {
3208
- if (s.token && s.url.startsWith("https://")) {
3209
- const urlObj = new URL(s.url);
3210
- urlObj.username = "x-access-token";
3211
- urlObj.password = s.token;
3212
- return urlObj.toString();
3213
- }
3214
- return s.url;
3215
- }
3680
+ // ── Private: origin URL ───────────────────────────────────────────────────
3216
3681
  /**
3217
3682
  * Align `origin` with the exact sanitized URL used by the Tier-2 helper.
3218
3683
  * Git does not normalize query/fragment data, path case, or `.git` aliases
@@ -3230,43 +3695,37 @@ var GitConnector = class extends AbstractConnector {
3230
3695
  return credentialScopeUrl;
3231
3696
  }
3232
3697
  /**
3233
- * Tier-1 only: rewrite `origin` so it carries the token freshly minted on
3234
- * this connect. The "use as-is" wake path leaves whatever token the first
3235
- * connect baked in — long-dead for short-lived (GitHub App) tokens. Without
3236
- * a Tier-2 helper this URL token is the sole credential, so it must be
3237
- * refreshed here. No-op for anonymous (no token) or non-https remotes.
3238
- */
3239
- async rebakeRemoteAuth(targetDir, state) {
3240
- if (!state.token || !state.url.startsWith("https://")) return;
3241
- try {
3242
- await git(["remote", "set-url", "origin", this.authUrl(state)], targetDir, 5e3);
3243
- } catch (err) {
3244
- this.log.warn("failed to refresh inline token on origin", {
3245
- error: err instanceof Error ? err.message : String(err)
3246
- });
3247
- }
3248
- }
3249
- /**
3250
- * Tier-1: remove the userinfo from `origin`, so the token baked by
3251
- * connect/refresh does not stay in `.git/config` after teardown. No-op when
3252
- * `origin` carries none, or nothing was baked (no token, non-https remote).
3698
+ * Point `origin` at the declared URL, without a credential in it.
3699
+ *
3700
+ * Clones made before the driver authenticated per command hold
3701
+ * `https://x-access-token:<token>@…` in `.git/config`, where the agent can
3702
+ * read it; this is what removes it, on the first connect after an upgrade.
3703
+ * With a token in hand `origin` is set to the declared URL, as the old
3704
+ * re-bake did. Without one only a token-bearing origin is rewritten: a URL
3705
+ * the user declared with their own userinfo is theirs to keep.
3253
3706
  */
3254
- async stripRemoteAuth(state) {
3255
- if (!state.token || !state.targetDir || !state.url.startsWith("https://")) return;
3707
+ async reconcileRemoteUrl(targetDir, state) {
3256
3708
  try {
3257
- const current = await git(["remote", "get-url", "origin"], state.targetDir, 5e3);
3258
- const bare = new URL(current);
3259
- if (!bare.username && !bare.password) return;
3260
- bare.username = "";
3261
- bare.password = "";
3262
- await git(["remote", "set-url", "origin", bare.toString()], state.targetDir, 5e3);
3263
- this.log.info("teardown", {
3264
- event: "teardown",
3265
- tier: 1,
3266
- scope: redactCredentials(state.url)
3267
- });
3709
+ const current = await gitOrNull(["remote", "get-url", "origin"], targetDir);
3710
+ let wanted = null;
3711
+ if (state.token && state.url.startsWith("https://")) {
3712
+ wanted = plainRemoteUrl(state);
3713
+ } else if (current?.startsWith("https://")) {
3714
+ const u = new URL(current);
3715
+ if (u.username === TOKEN_USERNAME) {
3716
+ u.username = "";
3717
+ u.password = "";
3718
+ wanted = u.toString();
3719
+ }
3720
+ }
3721
+ if (wanted === null || wanted === current) return;
3722
+ await git(
3723
+ current === null ? ["remote", "add", "origin", wanted] : ["remote", "set-url", "origin", wanted],
3724
+ targetDir,
3725
+ 5e3
3726
+ );
3268
3727
  } catch (err) {
3269
- this.log.warn("failed to strip inline token from origin", {
3728
+ this.log.warn("failed to reconcile origin URL", {
3270
3729
  error: redactCredentials(err instanceof Error ? err.message : String(err))
3271
3730
  });
3272
3731
  }
@@ -3285,7 +3744,9 @@ var GitConnector = class extends AbstractConnector {
3285
3744
  }
3286
3745
  if (s.disposed) return;
3287
3746
  s.fetchInFlight = true;
3288
- void this.runPeriodicFetch(s, mountPath).catch((err) => {
3747
+ void this.withMountLock(s, async () => {
3748
+ if (!s.disposed) await this.runPeriodicFetch(s, mountPath);
3749
+ }).catch((err) => {
3289
3750
  this.log.debug("periodic fetch failed", {
3290
3751
  error: redactCredentials(err instanceof Error ? err.message : String(err))
3291
3752
  });
@@ -3299,14 +3760,15 @@ var GitConnector = class extends AbstractConnector {
3299
3760
  await this.fetchOrigin(s, mountPath);
3300
3761
  if (s.disposed) return;
3301
3762
  if (s.sync.autoRebase && s.activeBranch !== s.branch) {
3302
- const onOrigin = await this.trackSessionBranch(s.activeBranch, mountPath);
3763
+ const onOrigin = await this.trackSessionBranch(s.activeBranch, mountPath, s);
3303
3764
  if (s.disposed) return;
3304
3765
  if (onOrigin !== "absent") {
3305
3766
  await this.mergeBaseIntoSessionBranch(s, mountPath);
3306
3767
  if (s.disposed) return;
3307
- await syncSubmodules(mountPath, this.log);
3768
+ await syncSubmodules(mountPath, this.log, s);
3308
3769
  return;
3309
3770
  }
3771
+ if (await this.skipOverUnfinishedOperation(mountPath, "rebase onto base")) return;
3310
3772
  try {
3311
3773
  await gitCommitting(["rebase", `origin/${s.branch}`], mountPath, 6e4);
3312
3774
  } catch (err) {
@@ -3317,11 +3779,11 @@ var GitConnector = class extends AbstractConnector {
3317
3779
  await this.resolveConflictsOrAbort(s, mountPath, "rebase");
3318
3780
  }
3319
3781
  if (s.disposed) return;
3320
- await syncSubmodules(mountPath, this.log);
3782
+ await syncSubmodules(mountPath, this.log, s);
3321
3783
  } else if (s.sync.autoPull) {
3322
- await gitCommitting(["pull", "origin", s.activeBranch], mountPath, 6e4);
3784
+ await pullByMerge(s.activeBranch, mountPath, s, 6e4);
3323
3785
  if (s.disposed) return;
3324
- await syncSubmodules(mountPath, this.log);
3786
+ await syncSubmodules(mountPath, this.log, s);
3325
3787
  }
3326
3788
  }
3327
3789
  /**
@@ -3331,11 +3793,12 @@ var GitConnector = class extends AbstractConnector {
3331
3793
  * refspec fails the whole fetch. Drop it and fetch again. The base branch
3332
3794
  * refspec is never dropped.
3333
3795
  */
3334
- async fetchOrigin(s, mountPath) {
3796
+ async fetchOrigin(s, mountPath, auth = s) {
3335
3797
  for (let attempt = 0; ; attempt++) {
3336
3798
  try {
3337
- await runGit([...gitPrefix(mountPath), "fetch", "origin"], {
3799
+ await runRemoteGit(["fetch", "origin"], auth, {
3338
3800
  cwd: mountPath,
3801
+ safeDir: mountPath,
3339
3802
  timeout: 6e4,
3340
3803
  env: cLocaleEnv()
3341
3804
  });
@@ -3362,6 +3825,8 @@ var GitConnector = class extends AbstractConnector {
3362
3825
  async mergeBaseIntoSessionBranch(s, mountPath) {
3363
3826
  const baseRef = `origin/${s.branch}`;
3364
3827
  if (await isAncestor(baseRef, "HEAD", mountPath)) return;
3828
+ if (await this.skipOverUnfinishedOperation(mountPath, "merge of base into session branch"))
3829
+ return;
3365
3830
  try {
3366
3831
  await gitCommitting(["merge", "--no-edit", baseRef], mountPath, 6e4);
3367
3832
  this.log.info("merged base into published session branch", {
@@ -3378,6 +3843,17 @@ var GitConnector = class extends AbstractConnector {
3378
3843
  }
3379
3844
  }
3380
3845
  // ── Conflict resolution ──────────────────────────────────────────────────
3846
+ /**
3847
+ * True (and logged) when a merge or rebase is already open in the checkout,
3848
+ * so a driver merge must not start: its failure path would abort or resolve
3849
+ * the agent's own operation.
3850
+ */
3851
+ async skipOverUnfinishedOperation(cwd, what) {
3852
+ const reason = await unfinishedGitOperation(cwd);
3853
+ if (reason === null) return false;
3854
+ this.log.warn(`${what} skipped: the checkout has an unfinished merge or rebase`, { reason });
3855
+ return true;
3856
+ }
3381
3857
  /**
3382
3858
  * Attempt to resolve merge/rebase conflicts by keeping "ours" and copying
3383
3859
  * "theirs" to a .conflict file. If resolution fails entirely, abort.
@@ -3386,7 +3862,7 @@ var GitConnector = class extends AbstractConnector {
3386
3862
  async resolveConflictsOrAbort(s, mountPath, mode) {
3387
3863
  try {
3388
3864
  const conflicted = (await git(["diff", "--name-only", "--diff-filter=U"], mountPath)).split("\n").filter(Boolean);
3389
- if (conflicted.length === 0) return false;
3865
+ if (conflicted.length === 0) throw new Error(`${mode} failed without conflicts`);
3390
3866
  for (const file of conflicted) {
3391
3867
  await this.resolveFileConflict(mountPath, file);
3392
3868
  }
@@ -3482,7 +3958,9 @@ var GitConnector = class extends AbstractConnector {
3482
3958
  return;
3483
3959
  }
3484
3960
  s.autoCommitInFlight = true;
3485
- void this.performAutoCommit(s, mountPath).catch((err) => {
3961
+ void this.withMountLock(s, async () => {
3962
+ if (!s.disposed) await this.performAutoCommit(s, mountPath);
3963
+ }).catch((err) => {
3486
3964
  this.log.debug("auto-commit failed", {
3487
3965
  error: err instanceof Error ? err.message : String(err)
3488
3966
  });
@@ -3493,9 +3971,10 @@ var GitConnector = class extends AbstractConnector {
3493
3971
  }
3494
3972
  async performAutoCommit(s, mountPath) {
3495
3973
  try {
3974
+ if (await refuseUnfinishedOperation(mountPath, this.log)) return;
3496
3975
  const message = await this.generateCommitMessage(mountPath);
3497
3976
  if (s.disposed) return;
3498
- const result = await commitAll(mountPath, message);
3977
+ const result = await commitAll(mountPath, message, this.log);
3499
3978
  if (!result || s.disposed) return;
3500
3979
  if (s.lifecycle.pushAfterCommit) {
3501
3980
  try {
@@ -3508,7 +3987,7 @@ var GitConnector = class extends AbstractConnector {
3508
3987
  }
3509
3988
  async generateCommitMessage(mountPath) {
3510
3989
  try {
3511
- await git(["add", "-A"], mountPath);
3990
+ await stageAll(mountPath, this.log);
3512
3991
  const stat5 = await git(["diff", "--cached", "--stat"], mountPath);
3513
3992
  const lines = stat5.split("\n").filter((l) => l.includes("|"));
3514
3993
  if (lines.length === 0) return "skaile: auto-save";
@@ -3523,7 +4002,13 @@ var GitConnector = class extends AbstractConnector {
3523
4002
  function createConnector5() {
3524
4003
  return new GitConnector();
3525
4004
  }
3526
- var __test = { resolveGitConfig };
4005
+ var __test = {
4006
+ resolveGitConfig,
4007
+ networkGitEnv,
4008
+ plainRemoteUrl,
4009
+ runRemoteGit,
4010
+ comparableRemoteUrl
4011
+ };
3527
4012
 
3528
4013
  // factory-assets/connectors/gmail/adapter.ts
3529
4014
  var GmailConnector = class extends AbstractConnector {
@@ -8264,6 +8749,20 @@ async function buildSdkConnectorTools(manager, options) {
8264
8749
  tools: withActivityLabelDefs(buildConnectorToolDefs(manager, options), options?.z ?? zStatic)
8265
8750
  });
8266
8751
  }
8752
+ var GIT_REMOTE_TOOLS = [
8753
+ [
8754
+ "push",
8755
+ "Push a git mount's branch to origin with the mount's credential. Commit with the git CLI first; only commits are pushed. Never forces: a refused push returns status 'rejected' \u2014 run git_pull, then push again. Returns JSON {status, message, sync}."
8756
+ ],
8757
+ [
8758
+ "pull",
8759
+ "Fetch origin and merge it into a git mount's branch with the mount's credential (a merge, never a rebase). On a conflict the merge is aborted, the checkout is left as it was, and status 'conflict' lists the files; merge origin/<branch> locally to resolve. Submodules are not fetched: a moved submodule updates at the next platform sync, and the message names it. Returns JSON {status, message, conflicts?, sync}."
8760
+ ],
8761
+ [
8762
+ "fetch",
8763
+ "Fetch origin for a git mount with the mount's credential. Updates origin/<branch> refs only; the working tree is not touched. Returns JSON {status, sync}."
8764
+ ]
8765
+ ];
8267
8766
  function buildConnectorToolDefs(manager, options) {
8268
8767
  const shellAccess = options?.shellAccess ?? true;
8269
8768
  const z5 = options?.z ?? zStatic;
@@ -8412,6 +8911,31 @@ function buildConnectorToolDefs(manager, options) {
8412
8911
  }
8413
8912
  }
8414
8913
  });
8914
+ if (manager.listGitConnectors().length > 0) {
8915
+ for (const [op, what] of GIT_REMOTE_TOOLS) {
8916
+ tools.push({
8917
+ name: `git_${op}`,
8918
+ description: what,
8919
+ inputSchema: {
8920
+ connector_id: z5.string().describe("ID of the git mount (see the Mounts table)")
8921
+ },
8922
+ handler: async ({ connector_id }) => {
8923
+ try {
8924
+ if (manager.get(connector_id).connector.name !== "git") {
8925
+ throw new Error(`connector "${connector_id}" is not a git mount`);
8926
+ }
8927
+ const result = await manager.executeOp(connector_id, op, {});
8928
+ return { content: [{ type: "text", text: result }] };
8929
+ } catch (err) {
8930
+ return {
8931
+ content: [{ type: "text", text: `Error: ${err.message}` }],
8932
+ isError: true
8933
+ };
8934
+ }
8935
+ }
8936
+ });
8937
+ }
8938
+ }
8415
8939
  if (!shellAccess) {
8416
8940
  tools.push({
8417
8941
  name: "connector_exec",
@@ -8656,13 +9180,13 @@ function appendGitAuthNotes(lines, manager, fsConnectors) {
8656
9180
  const hosts = [...new Set(tier2.map((m) => safeHost(m.source)))].filter(Boolean);
8657
9181
  lines.push("");
8658
9182
  lines.push(
8659
- `> **Git auth (CLI):** the git CLI is authenticated for the duration of this session for ${hosts.map((h) => `\`${h}\``).join(", ")}. Use \`git push\` / \`git pull\` directly.`
9183
+ `> **Git auth (CLI):** the git CLI is authenticated for the duration of this session for ${hosts.map((h) => `\`${h}\``).join(", ")} (mounts ${idList(tier2)}). Use \`git push\` / \`git pull\` directly, or the \`git_push\` / \`git_pull\` / \`git_fetch\` tools.`
8660
9184
  );
8661
9185
  }
8662
9186
  if (tier1.length > 0) {
8663
9187
  lines.push("");
8664
9188
  lines.push(
8665
- '> **Git auth (mount-managed):** `git push` / `git pull` against mount remotes go through the workspace mount, not the agent. If a network git command fails with "could not read Username", do not retry \u2014 the credential is intentionally not exposed to the CLI. Use the workspace\'s autoPush / autoPull or ask the user to enable "Allow agent to use git CLI directly" on the mount.'
9189
+ `> **Git auth (mount-managed):** on mounts ${idList(tier1)} the git CLI has no credential, so a network git command (\`git push\`, \`git pull\`, \`git fetch\`) fails with "could not read Username" \u2014 do not retry it. Commit with the git CLI as usual, then push and pull with the \`git_push\`, \`git_pull\` and \`git_fetch\` tools (\`connector_id\` = the mount id; where only \`connector_exec\` is offered, its \`push\` / \`pull\` / \`fetch\` operations), which run with the mount's credential. They act on the mount's own branch only and never force; a rejected push means \`git_pull\` first. \`git_pull\` does not fetch submodules; they update at the next platform sync. \`gh\` and pull requests are not available on these mounts: they need the user to enable "Allow agent to use git CLI directly" on the mount.`
8666
9190
  );
8667
9191
  }
8668
9192
  }
@@ -8711,6 +9235,9 @@ function buildToolSection(manager, toolConnectors, shellAccess) {
8711
9235
  }
8712
9236
  return lines.join("\n");
8713
9237
  }
9238
+ function idList(mounts) {
9239
+ return mounts.map((m) => `\`${m.id}\``).join(", ");
9240
+ }
8714
9241
  function safeHost(source) {
8715
9242
  try {
8716
9243
  return new URL(source).host;
@@ -12043,5 +12570,5 @@ function describeFlowOperations() {
12043
12570
  }
12044
12571
 
12045
12572
  export { AbstractConnector, BUILTIN_CONNECTOR_CATALOG, BoxConnector, ClassifierSubpromptRunner, ConnectorManager, DeployConnector, DevServerConnector, FlowAdapter, GitConnector, GmailConnector, GoogleDriveConnector, LocalConnector, LogBuffer, MAX_REARM_MS, MIN_REFRESH_DELAY_MS, ManagedGitconfigCollisionError, MattermostConnector, MemoryConnector, MinIOConnector, NO_ACTIVE_FLOW_ERROR, PortPool, PostgresConnector, REFRESH_LEAD_MS, S3Connector, S3FetchStoppedError, S3PresignedRequestError, S3_PRESIGN_BATCH, SQLiteConnector, SessionInlineSubFlowRunner, SessionNodeExecutor, SessionSubpromptRunner, SharePointConnector, StaticServerConnector, TunnelConnector, WebDAVConnector, XStateConnector, XStateStoreConnector, __test, atomicReplaceCredential, buildConnectorPromptSection, buildConnectorToolDefs, buildFlowPromptEvent, buildFlowToolDefs, buildLazyFlowToolDefs, buildLazySdkFlowTools, buildSdkConnectorTools, buildSdkFlowTools, checkFleetMountLiveness, classifierQuestion, classifyConnectError, createConnector, createConnector10, createConnector11, createConnector12, createConnector13, createConnector14, createConnector15, createConnector16, createConnector17, createConnector18, createConnector19, createConnector2, createConnector20, createConnector3, createConnector4, createConnector5, createConnector6, createConnector7, createConnector8, createConnector9, createFsWatcher, createWorktree, defaultSpawn, describeFlowOperations, driveOrchestratorTurn, ensureDirMode, ensureFleetMounted, ensureRefreshAttemptStamp, findGitRoot, findMissingPackages, getConnector, installNpmPackages, isFleetManaged, isPackageResolvable, listConnectors, listWorktrees, parseListObjectsV2, presignScopePrefix, presignableKey, readGitSyncStatus, registerBuiltinConnectors, removeMountBlock, removeUnavailableMountBlock, renderCredentialHelperScript, tryGetConnector, worktreeRegistry, writeHelperScript, writeMountBlock, writeUnavailableMountBlock };
12046
- //# sourceMappingURL=chunk-3SZLZ56M.js.map
12047
- //# sourceMappingURL=chunk-3SZLZ56M.js.map
12573
+ //# sourceMappingURL=chunk-EZOFQW3G.js.map
12574
+ //# sourceMappingURL=chunk-EZOFQW3G.js.map