sync-worktrees 3.4.0 → 3.5.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.
@@ -5,7 +5,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
5
5
 
6
6
  // src/mcp/context.ts
7
7
  import * as fs7 from "fs/promises";
8
- import * as path8 from "path";
8
+ import * as path9 from "path";
9
9
  import simpleGit5 from "simple-git";
10
10
 
11
11
  // src/constants.ts
@@ -202,9 +202,9 @@ var WorktreeError = class extends SyncWorktreesError {
202
202
  }
203
203
  };
204
204
  var WorktreeNotCleanError = class extends WorktreeError {
205
- constructor(path10, reasons) {
206
- super(`Worktree at '${path10}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
207
- this.path = path10;
205
+ constructor(path11, reasons) {
206
+ super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
207
+ this.path = path11;
208
208
  this.reasons = reasons;
209
209
  }
210
210
  };
@@ -755,7 +755,7 @@ function defaultConsoleOutput(msg, level) {
755
755
 
756
756
  // src/services/worktree-sync.service.ts
757
757
  import * as fs6 from "fs/promises";
758
- import * as path7 from "path";
758
+ import * as path8 from "path";
759
759
  import pLimit from "p-limit";
760
760
  import * as lockfile from "proper-lockfile";
761
761
 
@@ -1007,7 +1007,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
1007
1007
 
1008
1008
  // src/services/git.service.ts
1009
1009
  import * as fs4 from "fs/promises";
1010
- import * as path5 from "path";
1010
+ import * as path6 from "path";
1011
1011
  import simpleGit4 from "simple-git";
1012
1012
 
1013
1013
  // src/utils/worktree-list-parser.ts
@@ -1052,11 +1052,13 @@ function parseWorktreeListPorcelain(output) {
1052
1052
  }
1053
1053
 
1054
1054
  // src/services/sparse-checkout.service.ts
1055
+ import * as path3 from "path";
1055
1056
  import simpleGit from "simple-git";
1056
1057
  var SparseCheckoutService = class {
1057
1058
  logger;
1058
1059
  gitFactory;
1059
1060
  warnedConfigs = /* @__PURE__ */ new WeakSet();
1061
+ matcherCache = /* @__PURE__ */ new WeakMap();
1060
1062
  constructor(logger, gitFactory) {
1061
1063
  this.logger = logger ?? Logger.createDefault();
1062
1064
  this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
@@ -1140,11 +1142,66 @@ var SparseCheckoutService = class {
1140
1142
  const bt = b.map((x) => x.trim());
1141
1143
  return at.every((v, i) => v === bt[i]);
1142
1144
  }
1145
+ /**
1146
+ * Decide whether a list of changed file paths intersects the sparse-checkout
1147
+ * set defined by `cfg`. Used to skip fast-forward updates when upstream
1148
+ * commits only touch files outside the materialized worktree.
1149
+ *
1150
+ * Cone mode materializes:
1151
+ * - all files at the repository root,
1152
+ * - all files directly inside every ancestor of an included directory
1153
+ * (e.g. include `tools/build` keeps `tools/foo.txt` checked out too),
1154
+ * - everything inside an included directory.
1155
+ * We mirror those rules here. Missing the ancestor-files case would let
1156
+ * stale files linger when only those parent files change upstream.
1157
+ *
1158
+ * No-cone mode: gitignore-style matching with negation is non-trivial and
1159
+ * not implemented here yet. We return `true` so the caller falls back to
1160
+ * the safe behavior of always running the update.
1161
+ *
1162
+ * The matcher derived from `cfg` is cached on the cfg object identity
1163
+ * (WeakMap), so callers should reuse the same `cfg` reference across
1164
+ * invocations to benefit from the cache.
1165
+ */
1166
+ pathsTouchSparse(changedPaths, cfg) {
1167
+ if (changedPaths.length === 0) return false;
1168
+ const matcher = this.getMatcher(cfg);
1169
+ if (matcher.mode === "no-cone") return true;
1170
+ if (matcher.patterns.length === 0) return true;
1171
+ return changedPaths.some((p) => {
1172
+ if (!p.includes("/")) return true;
1173
+ for (const pat of matcher.patterns) {
1174
+ if (p === pat || p.startsWith(pat + "/")) return true;
1175
+ }
1176
+ return matcher.ancestorDirs.has(path3.posix.dirname(p));
1177
+ });
1178
+ }
1179
+ getMatcher(cfg) {
1180
+ const cached = this.matcherCache.get(cfg);
1181
+ if (cached) return cached;
1182
+ const mode = this.resolveMode(cfg);
1183
+ if (mode === "no-cone") {
1184
+ const matcher2 = { mode, patterns: [], ancestorDirs: /* @__PURE__ */ new Set() };
1185
+ this.matcherCache.set(cfg, matcher2);
1186
+ return matcher2;
1187
+ }
1188
+ const patterns = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.endsWith("/") ? p.slice(0, -1) : p);
1189
+ const ancestorDirs = /* @__PURE__ */ new Set();
1190
+ for (const pat of patterns) {
1191
+ const parts = pat.split("/");
1192
+ for (let i = 1; i < parts.length; i++) {
1193
+ ancestorDirs.add(parts.slice(0, i).join("/"));
1194
+ }
1195
+ }
1196
+ const matcher = { mode, patterns, ancestorDirs };
1197
+ this.matcherCache.set(cfg, matcher);
1198
+ return matcher;
1199
+ }
1143
1200
  };
1144
1201
 
1145
1202
  // src/services/worktree-metadata.service.ts
1146
1203
  import * as fs2 from "fs/promises";
1147
- import * as path3 from "path";
1204
+ import * as path4 from "path";
1148
1205
  import simpleGit2 from "simple-git";
1149
1206
  var WorktreeMetadataService = class {
1150
1207
  logger;
@@ -1157,7 +1214,7 @@ var WorktreeMetadataService = class {
1157
1214
  * For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
1158
1215
  */
1159
1216
  getWorktreeDirectoryName(worktreePath) {
1160
- return path3.basename(worktreePath);
1217
+ return path4.basename(worktreePath);
1161
1218
  }
1162
1219
  async getMetadataPath(bareRepoPath, worktreeName) {
1163
1220
  if (worktreeName.includes("/") || worktreeName.includes("\\")) {
@@ -1165,7 +1222,7 @@ var WorktreeMetadataService = class {
1165
1222
  `getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
1166
1223
  );
1167
1224
  }
1168
- return path3.join(
1225
+ return path4.join(
1169
1226
  bareRepoPath,
1170
1227
  METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
1171
1228
  worktreeName,
@@ -1178,7 +1235,7 @@ var WorktreeMetadataService = class {
1178
1235
  }
1179
1236
  async saveMetadata(bareRepoPath, worktreeName, metadata) {
1180
1237
  const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
1181
- await fs2.mkdir(path3.dirname(metadataPath), { recursive: true });
1238
+ await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
1182
1239
  const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
1183
1240
  let renamed = false;
1184
1241
  try {
@@ -1371,7 +1428,7 @@ var WorktreeMetadataService = class {
1371
1428
 
1372
1429
  // src/services/worktree-status.service.ts
1373
1430
  import * as fs3 from "fs/promises";
1374
- import * as path4 from "path";
1431
+ import * as path5 from "path";
1375
1432
  import simpleGit3 from "simple-git";
1376
1433
  var OPERATION_FILES = [
1377
1434
  { file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
@@ -1574,7 +1631,7 @@ var WorktreeStatusService = class {
1574
1631
  async detectOperationFile(gitDir) {
1575
1632
  const results = await Promise.all(
1576
1633
  OPERATION_FILES.map(
1577
- ({ file }) => fs3.access(path4.join(gitDir, file)).then(
1634
+ ({ file }) => fs3.access(path5.join(gitDir, file)).then(
1578
1635
  () => true,
1579
1636
  () => false
1580
1637
  )
@@ -1695,14 +1752,14 @@ var WorktreeStatusService = class {
1695
1752
  }
1696
1753
  }
1697
1754
  async resolveGitDir(worktreePath) {
1698
- const gitPath = path4.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
1755
+ const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
1699
1756
  try {
1700
1757
  const stat4 = await fs3.stat(gitPath);
1701
1758
  if (stat4.isFile()) {
1702
1759
  const content = await fs3.readFile(gitPath, "utf-8");
1703
1760
  const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
1704
1761
  if (gitdirMatch) {
1705
- return path4.resolve(worktreePath, gitdirMatch[1].trim());
1762
+ return path5.resolve(worktreePath, gitdirMatch[1].trim());
1706
1763
  }
1707
1764
  throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
1708
1765
  }
@@ -1716,7 +1773,7 @@ var WorktreeStatusService = class {
1716
1773
  }
1717
1774
  }
1718
1775
  createGitInstance(worktreePath) {
1719
- const key = `${path4.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
1776
+ const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
1720
1777
  let git = this.gitInstances.get(key);
1721
1778
  if (!git) {
1722
1779
  git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
@@ -1739,7 +1796,7 @@ var GitService = class {
1739
1796
  this.config = config;
1740
1797
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
1741
1798
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
1742
- this.mainWorktreePath = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
1799
+ this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
1743
1800
  this.metadataService = new WorktreeMetadataService(this.logger);
1744
1801
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
1745
1802
  this.sparseCheckoutService = new SparseCheckoutService(this.logger);
@@ -1767,7 +1824,7 @@ var GitService = class {
1767
1824
  return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
1768
1825
  }
1769
1826
  getCachedGit(dirPath, useLfsSkip = false) {
1770
- const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
1827
+ const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
1771
1828
  let git = this.gitInstances.get(key);
1772
1829
  if (!git) {
1773
1830
  const block = this.getFetchTimeoutMs();
@@ -1784,10 +1841,10 @@ var GitService = class {
1784
1841
  async initialize() {
1785
1842
  const { repoUrl } = this.config;
1786
1843
  try {
1787
- await fs4.access(path5.join(this.bareRepoPath, "HEAD"));
1844
+ await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
1788
1845
  } catch {
1789
1846
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
1790
- await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
1847
+ await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
1791
1848
  const cloneBlock = this.getCloneTimeoutMs();
1792
1849
  const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
1793
1850
  const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
@@ -1807,17 +1864,17 @@ var GitService = class {
1807
1864
  this.logger.info("Fetching remote branches...");
1808
1865
  await bareGit.fetch(["--all"]);
1809
1866
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
1810
- this.mainWorktreePath = path5.join(this.config.worktreeDir, this.defaultBranch);
1867
+ this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
1811
1868
  let needsMainWorktree = true;
1812
1869
  try {
1813
1870
  const worktrees = await this.getWorktreesFromBare(bareGit);
1814
- needsMainWorktree = !worktrees.some((w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath));
1871
+ needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
1815
1872
  } catch {
1816
1873
  }
1817
1874
  if (needsMainWorktree) {
1818
1875
  this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
1819
1876
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
1820
- const absoluteWorktreePath = path5.resolve(this.mainWorktreePath);
1877
+ const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
1821
1878
  const branches = await bareGit.branch();
1822
1879
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
1823
1880
  const useNoCheckoutMain = !!this.config.sparseCheckout;
@@ -1853,7 +1910,7 @@ var GitService = class {
1853
1910
  }
1854
1911
  const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
1855
1912
  const mainWorktreeRegistered = updatedWorktrees.some(
1856
- (w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath)
1913
+ (w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
1857
1914
  );
1858
1915
  if (!mainWorktreeRegistered) {
1859
1916
  if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
@@ -1936,7 +1993,7 @@ var GitService = class {
1936
1993
  const existence = await Promise.all(
1937
1994
  lfsFileList.map(async (f) => {
1938
1995
  try {
1939
- await fs4.access(path5.join(worktreePath, f));
1996
+ await fs4.access(path6.join(worktreePath, f));
1940
1997
  return f;
1941
1998
  } catch {
1942
1999
  return null;
@@ -1964,7 +2021,7 @@ var GitService = class {
1964
2021
  let allDownloaded = true;
1965
2022
  const notDownloaded = [];
1966
2023
  for (const file of samplesToCheck) {
1967
- const filePath = path5.join(worktreePath, file);
2024
+ const filePath = path6.join(worktreePath, file);
1968
2025
  try {
1969
2026
  const handle = await fs4.open(filePath, "r");
1970
2027
  try {
@@ -2053,12 +2110,12 @@ var GitService = class {
2053
2110
  }
2054
2111
  async addWorktree(branchName, worktreePath) {
2055
2112
  const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
2056
- const absoluteWorktreePath = path5.resolve(worktreePath);
2057
- await fs4.mkdir(path5.dirname(absoluteWorktreePath), { recursive: true });
2113
+ const absoluteWorktreePath = path6.resolve(worktreePath);
2114
+ await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
2058
2115
  try {
2059
2116
  await fs4.access(absoluteWorktreePath);
2060
2117
  const worktrees = await this.getWorktreesFromBare(bareGit);
2061
- const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
2118
+ const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
2062
2119
  if (isValidWorktree) {
2063
2120
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
2064
2121
  return;
@@ -2103,7 +2160,7 @@ var GitService = class {
2103
2160
  }
2104
2161
  if (errorMessage.includes("already registered worktree")) {
2105
2162
  const worktrees = await this.getWorktreesFromBare(bareGit);
2106
- const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
2163
+ const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
2107
2164
  if (existingWorktree && !existingWorktree.isPrunable) {
2108
2165
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
2109
2166
  return;
@@ -2149,7 +2206,7 @@ var GitService = class {
2149
2206
  try {
2150
2207
  await fs4.access(absoluteWorktreePath);
2151
2208
  const worktrees = await this.getWorktreesFromBare(bareGit);
2152
- const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
2209
+ const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
2153
2210
  if (isValidWorktree) {
2154
2211
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
2155
2212
  return;
@@ -2179,7 +2236,7 @@ var GitService = class {
2179
2236
  const fallbackErrorMessage = getErrorMessage(fallbackError);
2180
2237
  if (fallbackErrorMessage.includes("already registered worktree")) {
2181
2238
  const worktrees = await this.getWorktreesFromBare(bareGit);
2182
- const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
2239
+ const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
2183
2240
  if (existingWorktree && !existingWorktree.isPrunable) {
2184
2241
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
2185
2242
  return;
@@ -2402,6 +2459,23 @@ var GitService = class {
2402
2459
  return false;
2403
2460
  }
2404
2461
  }
2462
+ async getChangedPathsInRange(worktreePath, fromRef, toRef) {
2463
+ const worktreeGit = this.getCachedGit(worktreePath);
2464
+ try {
2465
+ const out = await worktreeGit.raw([
2466
+ "-c",
2467
+ "core.quotePath=false",
2468
+ "diff",
2469
+ "--name-only",
2470
+ "--no-renames",
2471
+ `${fromRef}..${toRef}`
2472
+ ]);
2473
+ return out.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
2474
+ } catch (error) {
2475
+ this.logger.warn(`Failed to compute diff ${fromRef}..${toRef} in ${worktreePath}: ${getErrorMessage(error)}`);
2476
+ return null;
2477
+ }
2478
+ }
2405
2479
  async compareTreeContent(worktreePath, branch) {
2406
2480
  const worktreeGit = this.getCachedGit(worktreePath);
2407
2481
  try {
@@ -2486,7 +2560,7 @@ var GitService = class {
2486
2560
  // src/services/path-resolution.service.ts
2487
2561
  import { createHash } from "crypto";
2488
2562
  import * as fs5 from "fs";
2489
- import * as path6 from "path";
2563
+ import * as path7 from "path";
2490
2564
  var BRANCH_STEM_MAX = 80;
2491
2565
  var BRANCH_HASH_LEN = 8;
2492
2566
  var PathResolutionService = class {
@@ -2496,22 +2570,22 @@ var PathResolutionService = class {
2496
2570
  return `${stem}-${hash}`;
2497
2571
  }
2498
2572
  getBranchWorktreePath(worktreeDir, branchName) {
2499
- return path6.join(worktreeDir, this.sanitizeBranchName(branchName));
2573
+ return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
2500
2574
  }
2501
2575
  resolveRealPath(inputPath) {
2502
- const absolute = path6.resolve(inputPath);
2576
+ const absolute = path7.resolve(inputPath);
2503
2577
  const missing = [];
2504
2578
  let current = absolute;
2505
2579
  while (!fs5.existsSync(current)) {
2506
- const parent = path6.dirname(current);
2580
+ const parent = path7.dirname(current);
2507
2581
  if (parent === current) {
2508
2582
  return absolute;
2509
2583
  }
2510
- missing.unshift(path6.basename(current));
2584
+ missing.unshift(path7.basename(current));
2511
2585
  current = parent;
2512
2586
  }
2513
2587
  try {
2514
- return path6.join(fs5.realpathSync(current), ...missing);
2588
+ return path7.join(fs5.realpathSync(current), ...missing);
2515
2589
  } catch {
2516
2590
  return absolute;
2517
2591
  }
@@ -2521,7 +2595,7 @@ var PathResolutionService = class {
2521
2595
  const a = fold(resolved);
2522
2596
  const b = fold(resolvedBase);
2523
2597
  if (a === b) return true;
2524
- return a.length > b.length && a.charAt(b.length) === path6.sep && a.startsWith(b);
2598
+ return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
2525
2599
  }
2526
2600
  normalizeWorktreePath(worktreePath, worktreeBaseDir) {
2527
2601
  const resolved = this.resolveRealPath(worktreePath);
@@ -2529,7 +2603,7 @@ var PathResolutionService = class {
2529
2603
  if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
2530
2604
  throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
2531
2605
  }
2532
- return path6.relative(resolvedBase, resolved);
2606
+ return path7.relative(resolvedBase, resolved);
2533
2607
  }
2534
2608
  isPathInsideBaseDir(targetPath, baseDir) {
2535
2609
  const resolved = this.resolveRealPath(targetPath);
@@ -2635,7 +2709,7 @@ var WorktreeSyncService = class {
2635
2709
  };
2636
2710
  }
2637
2711
  const barePath = this.gitService.getBareRepoPath();
2638
- const lockTarget = path7.join(barePath, "HEAD");
2712
+ const lockTarget = path8.join(barePath, "HEAD");
2639
2713
  try {
2640
2714
  await fs6.access(lockTarget);
2641
2715
  } catch {
@@ -2833,12 +2907,12 @@ var WorktreeSyncService = class {
2833
2907
  }
2834
2908
  const reservedPaths = /* @__PURE__ */ new Map();
2835
2909
  for (const w of worktrees) {
2836
- reservedPaths.set(path7.resolve(w.path), w.branch);
2910
+ reservedPaths.set(path8.resolve(w.path), w.branch);
2837
2911
  }
2838
2912
  const plan = [];
2839
2913
  for (const branchName of newBranches) {
2840
2914
  const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
2841
- const resolved = path7.resolve(worktreePath);
2915
+ const resolved = path8.resolve(worktreePath);
2842
2916
  const conflict = reservedPaths.get(resolved);
2843
2917
  if (conflict && conflict !== branchName) {
2844
2918
  this.logger.error(
@@ -3043,12 +3117,12 @@ var WorktreeSyncService = class {
3043
3117
  }
3044
3118
  async updateExistingWorktrees(worktrees, remoteBranches) {
3045
3119
  this.logger.info("Step 4: Checking for worktrees that need updates...");
3046
- const divergedDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
3120
+ const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
3047
3121
  try {
3048
3122
  const diverged = await fs6.readdir(divergedDir);
3049
3123
  if (diverged.length > 0) {
3050
3124
  this.logger.info(
3051
- `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path7.relative(process.cwd(), divergedDir)}`
3125
+ `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
3052
3126
  );
3053
3127
  }
3054
3128
  } catch {
@@ -3078,7 +3152,23 @@ var WorktreeSyncService = class {
3078
3152
  return { action: "diverged", worktree };
3079
3153
  }
3080
3154
  const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
3081
- return isBehind ? { action: "update", worktree } : null;
3155
+ if (!isBehind) return null;
3156
+ const sparseCfg = this.config.sparseCheckout;
3157
+ if (sparseCfg && sparseCfg.skipUpdateWhenOutsideSparse !== false) {
3158
+ const sparseService = this.gitService.getSparseCheckoutService();
3159
+ if (sparseService.resolveMode(sparseCfg) === "cone") {
3160
+ const diff = await this.gitService.getChangedPathsInRange(
3161
+ worktree.path,
3162
+ "HEAD",
3163
+ `origin/${worktree.branch}`
3164
+ );
3165
+ if (diff !== null && !sparseService.pathsTouchSparse(diff, sparseCfg)) {
3166
+ this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - upstream changes outside sparse paths`);
3167
+ return null;
3168
+ }
3169
+ }
3170
+ }
3171
+ return { action: "update", worktree };
3082
3172
  })
3083
3173
  )
3084
3174
  );
@@ -3156,13 +3246,13 @@ var WorktreeSyncService = class {
3156
3246
  }
3157
3247
  async cleanupOrphanedDirectories(worktrees) {
3158
3248
  try {
3159
- const worktreeRelativePaths = worktrees.map((w) => path7.relative(this.config.worktreeDir, w.path));
3249
+ const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
3160
3250
  const allDirs = await fs6.readdir(this.config.worktreeDir);
3161
3251
  const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
3162
3252
  const orphanedDirs = [];
3163
3253
  for (const dir of regularDirs) {
3164
3254
  const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
3165
- return worktreePath === dir || worktreePath.startsWith(dir + path7.sep);
3255
+ return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
3166
3256
  });
3167
3257
  if (!isPartOfWorktree) {
3168
3258
  orphanedDirs.push(dir);
@@ -3171,7 +3261,7 @@ var WorktreeSyncService = class {
3171
3261
  if (orphanedDirs.length > 0) {
3172
3262
  this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
3173
3263
  for (const dir of orphanedDirs) {
3174
- const dirPath = path7.join(this.config.worktreeDir, dir);
3264
+ const dirPath = path8.join(this.config.worktreeDir, dir);
3175
3265
  try {
3176
3266
  const stat4 = await fs6.stat(dirPath);
3177
3267
  if (stat4.isDirectory()) {
@@ -3205,7 +3295,7 @@ var WorktreeSyncService = class {
3205
3295
  } else {
3206
3296
  this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
3207
3297
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
3208
- const relativePath = path7.relative(process.cwd(), divergedPath);
3298
+ const relativePath = path8.relative(process.cwd(), divergedPath);
3209
3299
  this.logger.info(` Moved to: ${relativePath}`);
3210
3300
  this.logger.info(` Your local changes are preserved. To review:`);
3211
3301
  this.logger.info(` cd ${relativePath}`);
@@ -3229,12 +3319,12 @@ var WorktreeSyncService = class {
3229
3319
  }
3230
3320
  }
3231
3321
  async divergeWorktree(worktreePath, branchName) {
3232
- const divergedBaseDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
3322
+ const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
3233
3323
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
3234
3324
  const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
3235
3325
  const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
3236
3326
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
3237
- const divergedPath = path7.join(divergedBaseDir, divergedName);
3327
+ const divergedPath = path8.join(divergedBaseDir, divergedName);
3238
3328
  await fs6.mkdir(divergedBaseDir, { recursive: true });
3239
3329
  try {
3240
3330
  await fs6.rename(worktreePath, divergedPath);
@@ -3261,7 +3351,7 @@ var WorktreeSyncService = class {
3261
3351
  Original worktree location: ${worktreePath}`
3262
3352
  };
3263
3353
  await fs6.writeFile(
3264
- path7.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
3354
+ path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
3265
3355
  JSON.stringify(metadata, null, 2)
3266
3356
  );
3267
3357
  return divergedPath;
@@ -3316,9 +3406,9 @@ var RepositoryContext = class {
3316
3406
  discoveryCache = /* @__PURE__ */ new Map();
3317
3407
  async loadConfig(configPath, options = {}) {
3318
3408
  const setDefaultCurrent = options.setDefaultCurrent ?? true;
3319
- const absolutePath = path8.resolve(configPath);
3409
+ const absolutePath = path9.resolve(configPath);
3320
3410
  const configFile = await this.configLoader.loadConfigFile(absolutePath);
3321
- const configDir = path8.dirname(absolutePath);
3411
+ const configDir = path9.dirname(absolutePath);
3322
3412
  const globalDefaults = configFile.defaults;
3323
3413
  const resolvedAll = [];
3324
3414
  for (const repo of configFile.repositories) {
@@ -3355,7 +3445,7 @@ var RepositoryContext = class {
3355
3445
  return configFile.repositories;
3356
3446
  }
3357
3447
  async detectFromPath(dirPath) {
3358
- const absolutePath = path8.resolve(dirPath);
3448
+ const absolutePath = path9.resolve(dirPath);
3359
3449
  const cached = this.discoveryCache.get(absolutePath);
3360
3450
  if (cached && await this.isCacheFresh(cached)) {
3361
3451
  return cached.result;
@@ -3374,8 +3464,8 @@ var RepositoryContext = class {
3374
3464
  const { result, adminDir } = await this.detectFromPathUncached(absolutePath);
3375
3465
  if (result.isWorktree && result.bareRepoPath && adminDir) {
3376
3466
  const [worktreeHeadMtimeMs, worktreesDirMtimeMs] = await Promise.all([
3377
- safeMtimeMs(path8.join(adminDir, "HEAD")),
3378
- safeMtimeMs(path8.join(result.bareRepoPath, "worktrees"))
3467
+ safeMtimeMs(path9.join(adminDir, "HEAD")),
3468
+ safeMtimeMs(path9.join(result.bareRepoPath, "worktrees"))
3379
3469
  ]);
3380
3470
  this.discoveryCache.set(absolutePath, {
3381
3471
  result,
@@ -3407,8 +3497,8 @@ var RepositoryContext = class {
3407
3497
  return this.discoveryCache.size;
3408
3498
  }
3409
3499
  async discoverSiblingRepositories(currentBareRepoPath) {
3410
- const repoDir = path8.dirname(currentBareRepoPath);
3411
- const workspaceRoot = path8.dirname(repoDir);
3500
+ const repoDir = path9.dirname(currentBareRepoPath);
3501
+ const workspaceRoot = path9.dirname(repoDir);
3412
3502
  if (workspaceRoot === repoDir) return [];
3413
3503
  let entries;
3414
3504
  try {
@@ -3425,15 +3515,15 @@ var RepositoryContext = class {
3425
3515
  const results = [];
3426
3516
  await Promise.all(
3427
3517
  entries.map(async (entry) => {
3428
- const candidate = path8.join(workspaceRoot, entry);
3429
- const bareCandidate = path8.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
3518
+ const candidate = path9.join(workspaceRoot, entry);
3519
+ const bareCandidate = path9.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
3430
3520
  try {
3431
3521
  const stat4 = await fs7.stat(bareCandidate);
3432
3522
  if (!stat4.isDirectory()) return;
3433
3523
  } catch {
3434
3524
  return;
3435
3525
  }
3436
- const resolvedBare = path8.resolve(bareCandidate);
3526
+ const resolvedBare = path9.resolve(bareCandidate);
3437
3527
  const matchedName = configBares.get(normalizePathForCompare(resolvedBare));
3438
3528
  results.push({
3439
3529
  name: matchedName ?? entry,
@@ -3455,8 +3545,8 @@ var RepositoryContext = class {
3455
3545
  if (Date.now() - cached.cachedAt >= DISCOVERY_CACHE_TTL_MS) return false;
3456
3546
  if (!cached.worktreeAdminDir || !cached.result.bareRepoPath) return true;
3457
3547
  const [currentHeadMtime, currentWorktreesDirMtime] = await Promise.all([
3458
- safeMtimeMs(path8.join(cached.worktreeAdminDir, "HEAD")),
3459
- safeMtimeMs(path8.join(cached.result.bareRepoPath, "worktrees"))
3548
+ safeMtimeMs(path9.join(cached.worktreeAdminDir, "HEAD")),
3549
+ safeMtimeMs(path9.join(cached.result.bareRepoPath, "worktrees"))
3460
3550
  ]);
3461
3551
  return currentHeadMtime === cached.worktreeHeadMtimeMs && currentWorktreesDirMtime === cached.worktreesDirMtimeMs;
3462
3552
  }
@@ -3497,13 +3587,13 @@ var RepositoryContext = class {
3497
3587
  return unsupported("Invalid .git file format (missing gitdir line)");
3498
3588
  }
3499
3589
  const gitdir = gitdirMatch[1].trim();
3500
- const resolvedGitdir = path8.isAbsolute(gitdir) ? gitdir : path8.resolve(worktreeRoot, gitdir);
3590
+ const resolvedGitdir = path9.isAbsolute(gitdir) ? gitdir : path9.resolve(worktreeRoot, gitdir);
3501
3591
  const worktreesMatch = resolvedGitdir.match(/^(.+?)[/\\]worktrees[/\\][^/\\]+$/);
3502
3592
  if (!worktreesMatch) {
3503
3593
  return unsupported("gitdir does not follow worktree structure (missing /worktrees/<name>)");
3504
3594
  }
3505
- const bareRepoPath = path8.resolve(worktreesMatch[1]);
3506
- const adminDir = path8.resolve(resolvedGitdir);
3595
+ const bareRepoPath = path9.resolve(worktreesMatch[1]);
3596
+ const adminDir = path9.resolve(resolvedGitdir);
3507
3597
  let repoUrl = null;
3508
3598
  let worktrees = [];
3509
3599
  let currentBranch = null;
@@ -3544,7 +3634,7 @@ var RepositoryContext = class {
3544
3634
  adminDir
3545
3635
  };
3546
3636
  }
3547
- const worktreeDir = path8.dirname(worktreeRoot);
3637
+ const worktreeDir = path9.dirname(worktreeRoot);
3548
3638
  const noUrlReason = "no remote origin URL detected";
3549
3639
  const capabilities = {
3550
3640
  listWorktrees: { available: true },
@@ -3580,7 +3670,7 @@ var RepositoryContext = class {
3580
3670
  cronSchedule: DEFAULT_CONFIG.CRON_SCHEDULE,
3581
3671
  runOnce: true
3582
3672
  };
3583
- const detectedKey = `${AUTO_DETECT_PREFIX}${path8.basename(bareRepoPath)}@${bareRepoPath}`;
3673
+ const detectedKey = `${AUTO_DETECT_PREFIX}${path9.basename(bareRepoPath)}@${bareRepoPath}`;
3584
3674
  if (!this.repos.has(detectedKey)) {
3585
3675
  this.repos.set(detectedKey, {
3586
3676
  name: detectedKey,
@@ -3672,7 +3762,7 @@ function parseWorktreeList(output, currentPath) {
3672
3762
  const foldedCurrent = normalizePathForCompare(currentPath);
3673
3763
  const results = [];
3674
3764
  for (const wt of parseWorktreeListPorcelain(output)) {
3675
- const resolved = path8.resolve(wt.path);
3765
+ const resolved = path9.resolve(wt.path);
3676
3766
  const branch = wt.branch ?? (wt.detached ? `(detached ${(wt.head ?? "").slice(0, 7)})` : null);
3677
3767
  if (!branch) continue;
3678
3768
  results.push({
@@ -3692,10 +3782,10 @@ async function safeMtimeMs(filePath) {
3692
3782
  }
3693
3783
  }
3694
3784
  async function findWorktreeRoot(startPath) {
3695
- let current = path8.resolve(startPath);
3696
- const root = path8.parse(current).root;
3785
+ let current = path9.resolve(startPath);
3786
+ const root = path9.parse(current).root;
3697
3787
  while (true) {
3698
- const gitPath = path8.join(current, ".git");
3788
+ const gitPath = path9.join(current, ".git");
3699
3789
  try {
3700
3790
  const content = await fs7.readFile(gitPath, "utf-8");
3701
3791
  return { kind: "worktree-file", worktreeRoot: current, gitFileContent: content };
@@ -3709,7 +3799,7 @@ async function findWorktreeRoot(startPath) {
3709
3799
  }
3710
3800
  }
3711
3801
  if (current === root) return null;
3712
- const parent = path8.dirname(current);
3802
+ const parent = path9.dirname(current);
3713
3803
  if (parent === current) return null;
3714
3804
  current = parent;
3715
3805
  }
@@ -3720,7 +3810,7 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3720
3810
  import { z } from "zod";
3721
3811
 
3722
3812
  // src/mcp/handlers.ts
3723
- import * as path9 from "path";
3813
+ import * as path10 from "path";
3724
3814
  import pLimit2 from "p-limit";
3725
3815
 
3726
3816
  // src/utils/disk-space.ts
@@ -3917,7 +4007,7 @@ async function getReadyService(ctx, repoName, options = {}) {
3917
4007
  }
3918
4008
  async function ensureRepoWorktreePath(ctx, params, git) {
3919
4009
  await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
3920
- return path9.resolve(params.path);
4010
+ return path10.resolve(params.path);
3921
4011
  }
3922
4012
  async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
3923
4013
  const discovered = ctx.getDiscoveredContext(repoName);
@@ -3978,7 +4068,7 @@ async function handleListWorktrees(ctx, params, _extra) {
3978
4068
  const results = await Promise.all(
3979
4069
  worktrees.map(
3980
4070
  (wt) => limit(async () => {
3981
- const resolvedPath = path9.resolve(wt.path);
4071
+ const resolvedPath = path10.resolve(wt.path);
3982
4072
  const isCurrent = currentPath !== null && pathsEqual(wt.path, currentPath);
3983
4073
  const [status, divergence, metadata, sizeBytes] = await Promise.all([
3984
4074
  git.getFullWorktreeStatus(wt.path, false).catch(() => null),
@@ -4058,7 +4148,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
4058
4148
  return formatToolResponse({
4059
4149
  success: true,
4060
4150
  branchName,
4061
- worktreePath: path9.resolve(worktreePath),
4151
+ worktreePath: path10.resolve(worktreePath),
4062
4152
  created,
4063
4153
  pushed
4064
4154
  });
@@ -4146,7 +4236,7 @@ async function handleLoadConfig(ctx, params, _extra) {
4146
4236
  }
4147
4237
  await ctx.loadConfig(configPath);
4148
4238
  return formatToolResponse({
4149
- configPath: path9.resolve(configPath),
4239
+ configPath: path10.resolve(configPath),
4150
4240
  currentRepository: ctx.getCurrentRepo(),
4151
4241
  repositories: ctx.getRepositoryList()
4152
4242
  });