sync-worktrees 3.2.0 → 3.3.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.
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import * as path11 from "path";
4
+ import * as path12 from "path";
5
5
  import { confirm as confirm2 } from "@inquirer/prompts";
6
6
  import * as cron3 from "node-cron";
7
7
  import pLimit3 from "p-limit";
@@ -77,6 +77,7 @@ var ERROR_MESSAGES = {
77
77
  };
78
78
  var ENV_CONSTANTS = {
79
79
  GIT_LFS_SKIP_SMUDGE: "GIT_LFS_SKIP_SMUDGE",
80
+ GIT_ATTR_SOURCE: "GIT_ATTR_SOURCE",
80
81
  NODE_ENV_TEST: "test"
81
82
  };
82
83
  var PATH_CONSTANTS = {
@@ -123,7 +124,7 @@ var HOOK_CONSTANTS = {
123
124
 
124
125
  // src/services/config-loader.service.ts
125
126
  import * as fs from "fs/promises";
126
- import * as path from "path";
127
+ import * as path2 from "path";
127
128
  import { pathToFileURL } from "url";
128
129
  import * as cron from "node-cron";
129
130
 
@@ -147,14 +148,121 @@ function filterBranchesByName(branches, include, exclude) {
147
148
  return result;
148
149
  }
149
150
 
151
+ // src/utils/git-url.ts
152
+ function extractRepoNameFromUrl(gitUrl) {
153
+ const url = gitUrl.trim();
154
+ const sshMatch = url.match(/^git@[^:]+:(?:.+\/)?([^/]+?)(?:\.git)?$/);
155
+ if (sshMatch) {
156
+ return sshMatch[1];
157
+ }
158
+ const sshUrlMatch = url.match(/^ssh:\/\/[^/]+\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
159
+ if (sshUrlMatch) {
160
+ return sshUrlMatch[1];
161
+ }
162
+ const httpsMatch = url.match(/^https?:\/\/[^/]+\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
163
+ if (httpsMatch) {
164
+ return httpsMatch[1];
165
+ }
166
+ const fileMatch = url.match(/^file:\/\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
167
+ if (fileMatch) {
168
+ return fileMatch[1];
169
+ }
170
+ throw new Error(`Invalid Git URL format: ${gitUrl}`);
171
+ }
172
+ function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
173
+ const repoName = extractRepoNameFromUrl(repoUrl);
174
+ return `${baseDir}/${repoName}`;
175
+ }
176
+
177
+ // src/utils/path-compare.ts
178
+ import * as path from "path";
179
+ var CASE_INSENSITIVE_PLATFORMS = /* @__PURE__ */ new Set(["darwin"]);
180
+ function isCaseInsensitiveFs(platform = process.platform) {
181
+ return CASE_INSENSITIVE_PLATFORMS.has(platform);
182
+ }
183
+ function normalizePathForCompare(p, platform = process.platform) {
184
+ const resolved = path.resolve(p);
185
+ return isCaseInsensitiveFs(platform) ? resolved.toLowerCase() : resolved;
186
+ }
187
+
188
+ // src/errors/index.ts
189
+ var SyncWorktreesError = class extends Error {
190
+ constructor(message, code, cause) {
191
+ super(message);
192
+ this.code = code;
193
+ this.cause = cause;
194
+ this.name = this.constructor.name;
195
+ Object.setPrototypeOf(this, new.target.prototype);
196
+ if (cause && cause.stack) {
197
+ this.stack = `${this.stack}
198
+ Caused by: ${cause.stack}`;
199
+ }
200
+ }
201
+ };
202
+ var GitError = class extends SyncWorktreesError {
203
+ constructor(message, code, cause) {
204
+ super(message, `GIT_${code}`, cause);
205
+ }
206
+ };
207
+ var GitOperationError = class extends GitError {
208
+ constructor(operation, details, cause) {
209
+ super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
210
+ }
211
+ };
212
+ var WorktreeError = class extends SyncWorktreesError {
213
+ constructor(message, code, cause) {
214
+ super(message, `WORKTREE_${code}`, cause);
215
+ }
216
+ };
217
+ var WorktreeNotCleanError = class extends WorktreeError {
218
+ constructor(path13, reasons) {
219
+ super(`Worktree at '${path13}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
220
+ this.path = path13;
221
+ this.reasons = reasons;
222
+ }
223
+ };
224
+ var ConfigError = class extends SyncWorktreesError {
225
+ constructor(message, code, cause) {
226
+ super(message, `CONFIG_${code}`, cause);
227
+ }
228
+ };
229
+ var ConfigValidationError = class extends ConfigError {
230
+ constructor(field, reason) {
231
+ super(`Invalid configuration for '${field}': ${reason}`, "VALIDATION_FAILED");
232
+ this.field = field;
233
+ this.reason = reason;
234
+ }
235
+ };
236
+
237
+ // src/utils/sanitize-name.ts
238
+ var WINDOWS_RESERVED = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
239
+ var ILLEGAL_CHARS = /[<>:"|?*\x00-\x1f]/g;
240
+ function sanitizeNameForPath(name, fieldContext = "name") {
241
+ if (!name || typeof name !== "string") {
242
+ throw new ConfigValidationError(fieldContext, "must be a non-empty string");
243
+ }
244
+ let cleaned = name.trim();
245
+ cleaned = cleaned.replace(/[/\\]/g, "-");
246
+ cleaned = cleaned.replace(/^\.+/, "");
247
+ cleaned = cleaned.replace(ILLEGAL_CHARS, "_");
248
+ cleaned = cleaned.replace(/[. ]+$/, "");
249
+ if (cleaned.length === 0) {
250
+ throw new ConfigValidationError(fieldContext, `'${name}' produces an empty path segment after sanitization`);
251
+ }
252
+ if (WINDOWS_RESERVED.test(cleaned)) {
253
+ throw new ConfigValidationError(fieldContext, `'${cleaned}' is a reserved name on Windows`);
254
+ }
255
+ return cleaned;
256
+ }
257
+
150
258
  // src/services/config-loader.service.ts
151
259
  var ConfigLoaderService = class {
152
260
  async findConfigUpward(startDir) {
153
- let current = path.resolve(startDir);
154
- const root = path.parse(current).root;
261
+ let current = path2.resolve(startDir);
262
+ const root = path2.parse(current).root;
155
263
  while (true) {
156
264
  for (const name of CONFIG_FILE_NAMES) {
157
- const candidate = path.join(current, name);
265
+ const candidate = path2.join(current, name);
158
266
  try {
159
267
  await fs.access(candidate);
160
268
  return candidate;
@@ -162,13 +270,13 @@ var ConfigLoaderService = class {
162
270
  }
163
271
  }
164
272
  if (current === root) return null;
165
- const parent = path.dirname(current);
273
+ const parent = path2.dirname(current);
166
274
  if (parent === current) return null;
167
275
  current = parent;
168
276
  }
169
277
  }
170
278
  async loadConfigFile(configPath) {
171
- const absolutePath = path.resolve(configPath);
279
+ const absolutePath = path2.resolve(configPath);
172
280
  try {
173
281
  await fs.access(absolutePath);
174
282
  } catch {
@@ -244,7 +352,11 @@ var ConfigLoaderService = class {
244
352
  if (repoObj.hooks !== void 0) {
245
353
  this.validateHooksConfig(repoObj.hooks, `Repository '${repoObj.name}'`);
246
354
  }
355
+ if (repoObj.sparseCheckout !== void 0) {
356
+ this.validateSparseCheckoutConfig(repoObj.sparseCheckout, `Repository '${repoObj.name}'`);
357
+ }
247
358
  });
359
+ this.warnOnDuplicateRepoUrls(configObj.repositories);
248
360
  if (configObj.defaults) {
249
361
  if (typeof configObj.defaults !== "object") {
250
362
  throw new Error("'defaults' must be an object");
@@ -268,6 +380,9 @@ var ConfigLoaderService = class {
268
380
  if (defaults.hooks !== void 0) {
269
381
  this.validateHooksConfig(defaults.hooks, "defaults");
270
382
  }
383
+ if (defaults.sparseCheckout !== void 0) {
384
+ this.validateSparseCheckoutConfig(defaults.sparseCheckout, "defaults");
385
+ }
271
386
  }
272
387
  if (configObj.retry !== void 0) {
273
388
  if (typeof configObj.retry !== "object") {
@@ -357,6 +472,60 @@ var ConfigLoaderService = class {
357
472
  }
358
473
  }
359
474
  }
475
+ validateSparseCheckoutConfig(value, context) {
476
+ if (typeof value !== "object" || value === null) {
477
+ throw new Error(`'sparseCheckout' in ${context} must be an object`);
478
+ }
479
+ const cfg = value;
480
+ if (!Array.isArray(cfg.include)) {
481
+ throw new Error(`'sparseCheckout.include' in ${context} must be an array`);
482
+ }
483
+ if (cfg.include.length === 0) {
484
+ throw new Error(`'sparseCheckout.include' in ${context} must contain at least one pattern`);
485
+ }
486
+ for (let i = 0; i < cfg.include.length; i++) {
487
+ const p = cfg.include[i];
488
+ if (typeof p !== "string" || p.trim() === "") {
489
+ throw new Error(
490
+ `'sparseCheckout.include' in ${context} must contain only non-empty strings (invalid at index ${i})`
491
+ );
492
+ }
493
+ }
494
+ if (cfg.exclude !== void 0) {
495
+ if (!Array.isArray(cfg.exclude)) {
496
+ throw new Error(`'sparseCheckout.exclude' in ${context} must be an array`);
497
+ }
498
+ for (let i = 0; i < cfg.exclude.length; i++) {
499
+ const p = cfg.exclude[i];
500
+ if (typeof p !== "string" || p.trim() === "") {
501
+ throw new Error(
502
+ `'sparseCheckout.exclude' in ${context} must contain only non-empty strings (invalid at index ${i})`
503
+ );
504
+ }
505
+ }
506
+ }
507
+ if (cfg.mode !== void 0 && cfg.mode !== "cone" && cfg.mode !== "no-cone") {
508
+ throw new Error(`'sparseCheckout.mode' in ${context} must be 'cone' or 'no-cone'`);
509
+ }
510
+ }
511
+ warnOnDuplicateRepoUrls(repositories) {
512
+ const seen = /* @__PURE__ */ new Map();
513
+ for (const repo of repositories) {
514
+ const url = typeof repo.repoUrl === "string" ? repo.repoUrl : null;
515
+ const name = typeof repo.name === "string" ? repo.name : null;
516
+ if (!url || !name) continue;
517
+ const list = seen.get(url) ?? [];
518
+ list.push(name);
519
+ seen.set(url, list);
520
+ }
521
+ for (const [url, names] of seen) {
522
+ if (names.length > 1) {
523
+ console.warn(
524
+ `[sync-worktrees] repoUrl '${url}' appears in multiple entries (${names.join(", ")}). Pin 'bareRepoDir' on duplicate entries to make config reorder-proof.`
525
+ );
526
+ }
527
+ }
528
+ }
360
529
  validateHooksConfig(hooks, context) {
361
530
  if (typeof hooks !== "object" || hooks === null) {
362
531
  throw new Error(`'hooks' in ${context} must be an object`);
@@ -376,7 +545,7 @@ var ConfigLoaderService = class {
376
545
  }
377
546
  }
378
547
  }
379
- resolveRepositoryConfig(repo, defaults, configDir, globalRetry) {
548
+ resolveRepositoryConfig(repo, defaults, configDir, globalRetry, allRepositories) {
380
549
  const resolved = {
381
550
  name: repo.name,
382
551
  repoUrl: repo.repoUrl,
@@ -386,6 +555,11 @@ var ConfigLoaderService = class {
386
555
  };
387
556
  if (repo.bareRepoDir) {
388
557
  resolved.bareRepoDir = this.resolvePath(repo.bareRepoDir, configDir);
558
+ } else if (allRepositories && this.isDuplicateRepoUrl(repo, allRepositories)) {
559
+ const sanitized = sanitizeNameForPath(repo.name, `Repository '${repo.name}' name`);
560
+ resolved.bareRepoDir = this.resolvePath(`.bare/${sanitized}`, configDir);
561
+ } else {
562
+ resolved.bareRepoDir = this.resolvePath(getDefaultBareRepoDir(repo.repoUrl), configDir);
389
563
  }
390
564
  if (repo.branchMaxAge || defaults?.branchMaxAge) {
391
565
  resolved.branchMaxAge = repo.branchMaxAge ?? defaults?.branchMaxAge;
@@ -425,8 +599,32 @@ var ConfigLoaderService = class {
425
599
  ...repo.hooks || {}
426
600
  };
427
601
  }
602
+ const sparse = repo.sparseCheckout ?? defaults?.sparseCheckout;
603
+ if (sparse) {
604
+ resolved.sparseCheckout = sparse;
605
+ }
428
606
  return resolved;
429
607
  }
608
+ isDuplicateRepoUrl(repo, all) {
609
+ const firstIndex = all.findIndex((r) => r.repoUrl === repo.repoUrl);
610
+ const myIndex = all.indexOf(repo);
611
+ return firstIndex !== -1 && myIndex !== -1 && myIndex !== firstIndex;
612
+ }
613
+ detectBareRepoDirCollisions(repositories) {
614
+ const seen = /* @__PURE__ */ new Map();
615
+ for (const repo of repositories) {
616
+ if (!repo.bareRepoDir) continue;
617
+ const key = normalizePathForCompare(repo.bareRepoDir);
618
+ const displayPath = path2.resolve(repo.bareRepoDir);
619
+ const existing = seen.get(key);
620
+ if (existing && existing.name !== repo.name) {
621
+ throw new Error(
622
+ `Repositories '${existing.name}' and '${repo.name}' resolve to the same bareRepoDir '${displayPath}'. Set distinct 'bareRepoDir' values for duplicate repoUrl entries.`
623
+ );
624
+ }
625
+ seen.set(key, { name: repo.name, displayPath });
626
+ }
627
+ }
430
628
  isValidGitUrl(url) {
431
629
  if (/^https?:\/\/.+/.test(url)) return true;
432
630
  if (/^(ssh:\/\/|git@).+/.test(url)) return true;
@@ -435,10 +633,10 @@ var ConfigLoaderService = class {
435
633
  return false;
436
634
  }
437
635
  resolvePath(inputPath, baseDir) {
438
- if (path.isAbsolute(inputPath)) {
636
+ if (path2.isAbsolute(inputPath)) {
439
637
  return inputPath;
440
638
  }
441
- return path.resolve(baseDir || process.cwd(), inputPath);
639
+ return path2.resolve(baseDir || process.cwd(), inputPath);
442
640
  }
443
641
  filterRepositories(repositories, filter) {
444
642
  if (!filter) {
@@ -451,10 +649,11 @@ var ConfigLoaderService = class {
451
649
  }
452
650
  async buildRepositories(configPath, overrides) {
453
651
  const configFile = await this.loadConfigFile(configPath);
454
- const configDir = path.dirname(path.resolve(configPath));
652
+ const configDir = path2.dirname(path2.resolve(configPath));
455
653
  let repositories = configFile.repositories.map(
456
- (repo) => this.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
654
+ (repo) => this.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry, configFile.repositories)
457
655
  );
656
+ this.detectBareRepoDirCollisions(repositories);
458
657
  if (overrides?.filter) {
459
658
  repositories = this.filterRepositories(repositories, overrides.filter);
460
659
  }
@@ -470,7 +669,7 @@ var ConfigLoaderService = class {
470
669
 
471
670
  // src/services/InteractiveUIService.tsx
472
671
  import React8 from "react";
473
- import * as path8 from "path";
672
+ import * as path9 from "path";
474
673
  import { render } from "ink";
475
674
  import * as cron2 from "node-cron";
476
675
  import pLimit2 from "p-limit";
@@ -1746,7 +1945,7 @@ var App_default = App;
1746
1945
 
1747
1946
  // src/services/worktree-sync.service.ts
1748
1947
  import * as fs6 from "fs/promises";
1749
- import * as path6 from "path";
1948
+ import * as path7 from "path";
1750
1949
  import pLimit from "p-limit";
1751
1950
 
1752
1951
  // src/utils/date-filter.ts
@@ -1867,7 +2066,7 @@ async function retry(fn, options = {}) {
1867
2066
  const jitter = opts.jitterMs > 0 ? Math.random() * opts.jitterMs : 0;
1868
2067
  const delay = baseDelay + jitter;
1869
2068
  opts.onRetry(error, attempt, lfsContext);
1870
- await new Promise((resolve8) => setTimeout(resolve8, delay));
2069
+ await new Promise((resolve9) => setTimeout(resolve9, delay));
1871
2070
  attempt++;
1872
2071
  }
1873
2072
  }
@@ -1975,71 +2174,8 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
1975
2174
 
1976
2175
  // src/services/git.service.ts
1977
2176
  import * as fs4 from "fs/promises";
1978
- import * as path4 from "path";
1979
- import simpleGit3 from "simple-git";
1980
-
1981
- // src/errors/index.ts
1982
- var SyncWorktreesError = class extends Error {
1983
- constructor(message, code, cause) {
1984
- super(message);
1985
- this.code = code;
1986
- this.cause = cause;
1987
- this.name = this.constructor.name;
1988
- Object.setPrototypeOf(this, new.target.prototype);
1989
- if (cause && cause.stack) {
1990
- this.stack = `${this.stack}
1991
- Caused by: ${cause.stack}`;
1992
- }
1993
- }
1994
- };
1995
- var GitError = class extends SyncWorktreesError {
1996
- constructor(message, code, cause) {
1997
- super(message, `GIT_${code}`, cause);
1998
- }
1999
- };
2000
- var GitOperationError = class extends GitError {
2001
- constructor(operation, details, cause) {
2002
- super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
2003
- }
2004
- };
2005
- var WorktreeError = class extends SyncWorktreesError {
2006
- constructor(message, code, cause) {
2007
- super(message, `WORKTREE_${code}`, cause);
2008
- }
2009
- };
2010
- var WorktreeNotCleanError = class extends WorktreeError {
2011
- constructor(path12, reasons) {
2012
- super(`Worktree at '${path12}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
2013
- this.path = path12;
2014
- this.reasons = reasons;
2015
- }
2016
- };
2017
-
2018
- // src/utils/git-url.ts
2019
- function extractRepoNameFromUrl(gitUrl) {
2020
- const url = gitUrl.trim();
2021
- const sshMatch = url.match(/^git@[^:]+:(?:.+\/)?([^/]+?)(?:\.git)?$/);
2022
- if (sshMatch) {
2023
- return sshMatch[1];
2024
- }
2025
- const sshUrlMatch = url.match(/^ssh:\/\/[^/]+\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
2026
- if (sshUrlMatch) {
2027
- return sshUrlMatch[1];
2028
- }
2029
- const httpsMatch = url.match(/^https?:\/\/[^/]+\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
2030
- if (httpsMatch) {
2031
- return httpsMatch[1];
2032
- }
2033
- const fileMatch = url.match(/^file:\/\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
2034
- if (fileMatch) {
2035
- return fileMatch[1];
2036
- }
2037
- throw new Error(`Invalid Git URL format: ${gitUrl}`);
2038
- }
2039
- function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
2040
- const repoName = extractRepoNameFromUrl(repoUrl);
2041
- return `${baseDir}/${repoName}`;
2042
- }
2177
+ import * as path5 from "path";
2178
+ import simpleGit4 from "simple-git";
2043
2179
 
2044
2180
  // src/utils/worktree-list-parser.ts
2045
2181
  function parseWorktreeListPorcelain(output) {
@@ -2181,10 +2317,101 @@ function defaultConsoleOutput(msg, level) {
2181
2317
  else console.log(msg);
2182
2318
  }
2183
2319
 
2320
+ // src/services/sparse-checkout.service.ts
2321
+ import simpleGit from "simple-git";
2322
+ var SparseCheckoutService = class {
2323
+ logger;
2324
+ gitFactory;
2325
+ warnedConfigs = /* @__PURE__ */ new WeakSet();
2326
+ constructor(logger, gitFactory) {
2327
+ this.logger = logger ?? Logger.createDefault();
2328
+ this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
2329
+ }
2330
+ updateLogger(logger) {
2331
+ this.logger = logger;
2332
+ }
2333
+ resolveMode(cfg) {
2334
+ const hasExclude = !!cfg.exclude && cfg.exclude.length > 0;
2335
+ const hasNegation = cfg.include.some((p) => p.trim().startsWith("!"));
2336
+ if (cfg.mode === "no-cone") return "no-cone";
2337
+ if (hasExclude || hasNegation) {
2338
+ if (cfg.mode === "cone" && !this.warnedConfigs.has(cfg)) {
2339
+ this.logger.warn(
2340
+ "sparseCheckout: mode 'cone' is incompatible with excludes or negation patterns; auto-promoting to 'no-cone'"
2341
+ );
2342
+ this.warnedConfigs.add(cfg);
2343
+ }
2344
+ return "no-cone";
2345
+ }
2346
+ return cfg.mode ?? "cone";
2347
+ }
2348
+ buildPatterns(cfg) {
2349
+ return this.buildPatternsForMode(cfg, this.resolveMode(cfg));
2350
+ }
2351
+ buildPatternsForMode(cfg, mode) {
2352
+ const includes = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0);
2353
+ if (mode === "cone") {
2354
+ return includes;
2355
+ }
2356
+ const excludes = (cfg.exclude ?? []).map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.startsWith("!") ? p : `!${p}`);
2357
+ return [...includes, ...excludes];
2358
+ }
2359
+ async applyToWorktree(worktreePath, cfg) {
2360
+ const mode = this.resolveMode(cfg);
2361
+ const patterns = this.buildPatternsForMode(cfg, mode);
2362
+ if (patterns.length === 0) {
2363
+ throw new Error("sparseCheckout produced no patterns; refusing to apply empty config");
2364
+ }
2365
+ const git = this.gitFactory(worktreePath);
2366
+ await git.raw(["sparse-checkout", "init", mode === "cone" ? "--cone" : "--no-cone"]);
2367
+ await git.raw(["sparse-checkout", "set", mode === "cone" ? "--cone" : "--no-cone", ...patterns]);
2368
+ }
2369
+ async readCurrent(worktreePath) {
2370
+ const git = this.gitFactory(worktreePath);
2371
+ try {
2372
+ const out = await git.raw(["sparse-checkout", "list"]);
2373
+ const lines = out.split("\n").map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
2374
+ return lines.length === 0 ? null : lines;
2375
+ } catch {
2376
+ return null;
2377
+ }
2378
+ }
2379
+ async needsUpdate(worktreePath, cfg) {
2380
+ const current = await this.readCurrent(worktreePath);
2381
+ const desired = this.buildPatterns(cfg);
2382
+ if (current === null) return true;
2383
+ return !this.patternsEqual(current, desired);
2384
+ }
2385
+ isNarrowing(currentPatterns, nextPatterns) {
2386
+ if (!currentPatterns || currentPatterns.length === 0) return false;
2387
+ const isNeg = (p) => p.startsWith("!");
2388
+ const trim = (xs) => xs.map((p) => p.trim()).filter((p) => p.length > 0);
2389
+ const cur = trim(currentPatterns);
2390
+ const next = trim(nextPatterns);
2391
+ const positiveCurrent = new Set(cur.filter((p) => !isNeg(p)));
2392
+ const negativeCurrent = new Set(cur.filter(isNeg));
2393
+ const positiveNext = new Set(next.filter((p) => !isNeg(p)));
2394
+ const negativeNext = new Set(next.filter(isNeg));
2395
+ for (const p of positiveCurrent) {
2396
+ if (!positiveNext.has(p)) return true;
2397
+ }
2398
+ for (const p of negativeNext) {
2399
+ if (!negativeCurrent.has(p)) return true;
2400
+ }
2401
+ return false;
2402
+ }
2403
+ patternsEqual(a, b) {
2404
+ if (a.length !== b.length) return false;
2405
+ const at = a.map((x) => x.trim());
2406
+ const bt = b.map((x) => x.trim());
2407
+ return at.every((v, i) => v === bt[i]);
2408
+ }
2409
+ };
2410
+
2184
2411
  // src/services/worktree-metadata.service.ts
2185
2412
  import * as fs2 from "fs/promises";
2186
- import * as path2 from "path";
2187
- import simpleGit from "simple-git";
2413
+ import * as path3 from "path";
2414
+ import simpleGit2 from "simple-git";
2188
2415
  var WorktreeMetadataService = class {
2189
2416
  logger;
2190
2417
  constructor(logger) {
@@ -2196,7 +2423,7 @@ var WorktreeMetadataService = class {
2196
2423
  * For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
2197
2424
  */
2198
2425
  getWorktreeDirectoryName(worktreePath) {
2199
- return path2.basename(worktreePath);
2426
+ return path3.basename(worktreePath);
2200
2427
  }
2201
2428
  async getMetadataPath(bareRepoPath, worktreeName) {
2202
2429
  if (worktreeName.includes("/") || worktreeName.includes("\\")) {
@@ -2204,7 +2431,7 @@ var WorktreeMetadataService = class {
2204
2431
  `getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
2205
2432
  );
2206
2433
  }
2207
- return path2.join(
2434
+ return path3.join(
2208
2435
  bareRepoPath,
2209
2436
  METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
2210
2437
  worktreeName,
@@ -2217,7 +2444,7 @@ var WorktreeMetadataService = class {
2217
2444
  }
2218
2445
  async saveMetadata(bareRepoPath, worktreeName, metadata) {
2219
2446
  const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
2220
- await fs2.mkdir(path2.dirname(metadataPath), { recursive: true });
2447
+ await fs2.mkdir(path3.dirname(metadataPath), { recursive: true });
2221
2448
  const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
2222
2449
  let renamed = false;
2223
2450
  try {
@@ -2308,7 +2535,7 @@ var WorktreeMetadataService = class {
2308
2535
  this.logger.warn(`No metadata found for worktree ${worktreeDirName}`);
2309
2536
  this.logger.info(` Attempting to create initial metadata...`);
2310
2537
  try {
2311
- const worktreeGit = simpleGit(worktreePath);
2538
+ const worktreeGit = simpleGit2(worktreePath);
2312
2539
  const currentCommit = await worktreeGit.revparse(["HEAD"]);
2313
2540
  const branchSummary = await worktreeGit.branch();
2314
2541
  const actualBranchName = branchSummary.current;
@@ -2410,8 +2637,8 @@ var WorktreeMetadataService = class {
2410
2637
 
2411
2638
  // src/services/worktree-status.service.ts
2412
2639
  import * as fs3 from "fs/promises";
2413
- import * as path3 from "path";
2414
- import simpleGit2 from "simple-git";
2640
+ import * as path4 from "path";
2641
+ import simpleGit3 from "simple-git";
2415
2642
  var OPERATION_FILES = [
2416
2643
  { file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
2417
2644
  { file: GIT_OPERATIONS.CHERRY_PICK_HEAD, type: "cherry-pick" },
@@ -2613,7 +2840,7 @@ var WorktreeStatusService = class {
2613
2840
  async detectOperationFile(gitDir) {
2614
2841
  const results = await Promise.all(
2615
2842
  OPERATION_FILES.map(
2616
- ({ file }) => fs3.access(path3.join(gitDir, file)).then(
2843
+ ({ file }) => fs3.access(path4.join(gitDir, file)).then(
2617
2844
  () => true,
2618
2845
  () => false
2619
2846
  )
@@ -2734,14 +2961,14 @@ var WorktreeStatusService = class {
2734
2961
  }
2735
2962
  }
2736
2963
  async resolveGitDir(worktreePath) {
2737
- const gitPath = path3.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
2964
+ const gitPath = path4.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
2738
2965
  try {
2739
2966
  const stat3 = await fs3.stat(gitPath);
2740
2967
  if (stat3.isFile()) {
2741
2968
  const content = await fs3.readFile(gitPath, "utf-8");
2742
2969
  const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
2743
2970
  if (gitdirMatch) {
2744
- return path3.resolve(worktreePath, gitdirMatch[1].trim());
2971
+ return path4.resolve(worktreePath, gitdirMatch[1].trim());
2745
2972
  }
2746
2973
  throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
2747
2974
  }
@@ -2755,10 +2982,10 @@ var WorktreeStatusService = class {
2755
2982
  }
2756
2983
  }
2757
2984
  createGitInstance(worktreePath) {
2758
- const key = `${path3.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
2985
+ const key = `${path4.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
2759
2986
  let git = this.gitInstances.get(key);
2760
2987
  if (!git) {
2761
- git = this.config.skipLfs ? simpleGit2(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit2(worktreePath);
2988
+ git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
2762
2989
  this.gitInstances.set(key, git);
2763
2990
  }
2764
2991
  return git;
@@ -2771,9 +2998,10 @@ var GitService = class {
2771
2998
  this.config = config;
2772
2999
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
2773
3000
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
2774
- this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
3001
+ this.mainWorktreePath = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
2775
3002
  this.metadataService = new WorktreeMetadataService(this.logger);
2776
3003
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
3004
+ this.sparseCheckoutService = new SparseCheckoutService(this.logger);
2777
3005
  }
2778
3006
  git = null;
2779
3007
  bareRepoPath;
@@ -2782,29 +3010,34 @@ var GitService = class {
2782
3010
  // Will be updated after detection
2783
3011
  metadataService;
2784
3012
  statusService;
3013
+ sparseCheckoutService;
2785
3014
  logger;
2786
3015
  lfsSkipOverride = false;
2787
3016
  gitInstances = /* @__PURE__ */ new Map();
3017
+ getSparseCheckoutService() {
3018
+ return this.sparseCheckoutService;
3019
+ }
2788
3020
  getCachedGit(dirPath, useLfsSkip = false) {
2789
- const key = `${path4.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3021
+ const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
2790
3022
  let git = this.gitInstances.get(key);
2791
3023
  if (!git) {
2792
- git = useLfsSkip ? simpleGit3(dirPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(dirPath);
3024
+ git = useLfsSkip ? simpleGit4(dirPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4(dirPath);
2793
3025
  this.gitInstances.set(key, git);
2794
3026
  }
2795
3027
  return git;
2796
3028
  }
2797
3029
  updateLogger(logger) {
2798
3030
  this.logger = logger;
3031
+ this.sparseCheckoutService.updateLogger(logger);
2799
3032
  }
2800
3033
  async initialize() {
2801
3034
  const { repoUrl } = this.config;
2802
3035
  try {
2803
- await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
3036
+ await fs4.access(path5.join(this.bareRepoPath, "HEAD"));
2804
3037
  } catch {
2805
3038
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
2806
- await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
2807
- const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3();
3039
+ await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
3040
+ const cloneGit = this.isLfsSkipEnabled() ? simpleGit4().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4();
2808
3041
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
2809
3042
  this.logger.info("\u2705 Clone successful.");
2810
3043
  }
@@ -2821,34 +3054,39 @@ var GitService = class {
2821
3054
  this.logger.info("Fetching remote branches...");
2822
3055
  await bareGit.fetch(["--all"]);
2823
3056
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
2824
- this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
3057
+ this.mainWorktreePath = path5.join(this.config.worktreeDir, this.defaultBranch);
2825
3058
  let needsMainWorktree = true;
2826
3059
  try {
2827
3060
  const worktrees = await this.getWorktreesFromBare(bareGit);
2828
- needsMainWorktree = !worktrees.some((w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath));
3061
+ needsMainWorktree = !worktrees.some((w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath));
2829
3062
  } catch {
2830
3063
  }
2831
3064
  if (needsMainWorktree) {
2832
3065
  this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
2833
3066
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
2834
- const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
3067
+ const absoluteWorktreePath = path5.resolve(this.mainWorktreePath);
2835
3068
  const branches = await bareGit.branch();
2836
3069
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
3070
+ const useNoCheckoutMain = !!this.config.sparseCheckout;
3071
+ const noCheckoutFlagMain = useNoCheckoutMain ? ["--no-checkout"] : [];
2837
3072
  try {
2838
3073
  if (defaultBranchExists) {
2839
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
3074
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlagMain, absoluteWorktreePath, this.defaultBranch]);
2840
3075
  const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
2841
3076
  await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
3077
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, this.defaultBranch, false);
2842
3078
  } else {
2843
3079
  await bareGit.raw([
2844
3080
  "worktree",
2845
3081
  "add",
3082
+ ...noCheckoutFlagMain,
2846
3083
  "--track",
2847
3084
  "-b",
2848
3085
  this.defaultBranch,
2849
3086
  absoluteWorktreePath,
2850
3087
  `origin/${this.defaultBranch}`
2851
3088
  ]);
3089
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, this.defaultBranch, true);
2852
3090
  }
2853
3091
  } catch (error) {
2854
3092
  const errorMessage = getErrorMessage(error);
@@ -2862,7 +3100,7 @@ var GitService = class {
2862
3100
  }
2863
3101
  const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
2864
3102
  const mainWorktreeRegistered = updatedWorktrees.some(
2865
- (w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
3103
+ (w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath)
2866
3104
  );
2867
3105
  if (!mainWorktreeRegistered) {
2868
3106
  if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
@@ -2931,13 +3169,29 @@ var GitService = class {
2931
3169
  return branches;
2932
3170
  }
2933
3171
  async verifyLfsFilesDownloaded(worktreePath, branchName) {
2934
- const worktreeGit = this.getCachedGit(worktreePath);
3172
+ const worktreeGit = this.config.sparseCheckout ? simpleGit4(worktreePath).env({ ...process.env, [ENV_CONSTANTS.GIT_ATTR_SOURCE]: "HEAD" }) : this.getCachedGit(worktreePath);
2935
3173
  try {
2936
3174
  const lfsFiles = await worktreeGit.raw(["lfs", "ls-files", "--name-only"]);
2937
- const lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
3175
+ let lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
2938
3176
  if (lfsFileList.length === 0) {
2939
3177
  return;
2940
3178
  }
3179
+ if (this.config.sparseCheckout) {
3180
+ const existence = await Promise.all(
3181
+ lfsFileList.map(async (f) => {
3182
+ try {
3183
+ await fs4.access(path5.join(worktreePath, f));
3184
+ return f;
3185
+ } catch {
3186
+ return null;
3187
+ }
3188
+ })
3189
+ );
3190
+ lfsFileList = existence.filter((f) => f !== null);
3191
+ if (lfsFileList.length === 0) {
3192
+ return;
3193
+ }
3194
+ }
2941
3195
  if (this.config.debug) {
2942
3196
  this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
2943
3197
  }
@@ -2954,7 +3208,7 @@ var GitService = class {
2954
3208
  let allDownloaded = true;
2955
3209
  const notDownloaded = [];
2956
3210
  for (const file of samplesToCheck) {
2957
- const filePath = path4.join(worktreePath, file);
3211
+ const filePath = path5.join(worktreePath, file);
2958
3212
  try {
2959
3213
  const handle = await fs4.open(filePath, "r");
2960
3214
  try {
@@ -2981,7 +3235,7 @@ var GitService = class {
2981
3235
  }
2982
3236
  retries++;
2983
3237
  if (retries < maxRetries) {
2984
- await new Promise((resolve8) => setTimeout(resolve8, retryDelay));
3238
+ await new Promise((resolve9) => setTimeout(resolve9, retryDelay));
2985
3239
  }
2986
3240
  }
2987
3241
  this.logger.warn(
@@ -2991,6 +3245,38 @@ var GitService = class {
2991
3245
  this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
2992
3246
  }
2993
3247
  }
3248
+ async checkoutHead(worktreePath) {
3249
+ const git = this.getCachedGit(worktreePath, this.isLfsSkipEnabled());
3250
+ await git.raw(["checkout", "HEAD"]);
3251
+ }
3252
+ async applySparseAndCheckout(absoluteWorktreePath) {
3253
+ if (!this.config.sparseCheckout) return;
3254
+ await this.sparseCheckoutService.applyToWorktree(absoluteWorktreePath, this.config.sparseCheckout);
3255
+ const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3256
+ await worktreeGit.raw(["checkout", "HEAD"]);
3257
+ }
3258
+ async rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch, failureContext) {
3259
+ let worktreeRemoved = true;
3260
+ try {
3261
+ await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3262
+ } catch (rollbackError) {
3263
+ worktreeRemoved = false;
3264
+ const ctx = failureContext ? ` after ${failureContext}` : "";
3265
+ this.logger.warn(
3266
+ ` - Rollback failed for '${branchName}' at '${absoluteWorktreePath}'${ctx}: ${getErrorMessage(rollbackError)}`
3267
+ );
3268
+ }
3269
+ if (createdNewBranch) {
3270
+ try {
3271
+ await bareGit.raw(["branch", "-D", branchName]);
3272
+ } catch (branchRollbackError) {
3273
+ this.logger.warn(
3274
+ ` - Rollback (branch delete) failed for '${branchName}': ${getErrorMessage(branchRollbackError)}`
3275
+ );
3276
+ }
3277
+ }
3278
+ return { worktreeRemoved };
3279
+ }
2994
3280
  async createWorktreeMetadata(bareGit, worktreePath, branchName) {
2995
3281
  try {
2996
3282
  const worktreeGit = this.getCachedGit(worktreePath, this.isLfsSkipEnabled());
@@ -3011,12 +3297,12 @@ var GitService = class {
3011
3297
  }
3012
3298
  async addWorktree(branchName, worktreePath) {
3013
3299
  const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
3014
- const absoluteWorktreePath = path4.resolve(worktreePath);
3015
- await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
3300
+ const absoluteWorktreePath = path5.resolve(worktreePath);
3301
+ await fs4.mkdir(path5.dirname(absoluteWorktreePath), { recursive: true });
3016
3302
  try {
3017
3303
  await fs4.access(absoluteWorktreePath);
3018
3304
  const worktrees = await this.getWorktreesFromBare(bareGit);
3019
- const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
3305
+ const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3020
3306
  if (isValidWorktree) {
3021
3307
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3022
3308
  return;
@@ -3026,9 +3312,10 @@ var GitService = class {
3026
3312
  }
3027
3313
  } catch {
3028
3314
  }
3315
+ let createdNewBranch = false;
3029
3316
  try {
3030
3317
  const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
3031
- await this.runWorktreeAddByMatrix(
3318
+ createdNewBranch = await this.runWorktreeAddByMatrix(
3032
3319
  bareGit,
3033
3320
  branchName,
3034
3321
  absoluteWorktreePath,
@@ -3047,10 +3334,7 @@ var GitService = class {
3047
3334
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3048
3335
  } catch (metadataError) {
3049
3336
  this.logger.warn(` - Metadata creation failed for '${branchName}', removing worktree to prevent orphan`);
3050
- try {
3051
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3052
- } catch {
3053
- }
3337
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch);
3054
3338
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3055
3339
  }
3056
3340
  } catch (error) {
@@ -3063,7 +3347,7 @@ var GitService = class {
3063
3347
  }
3064
3348
  if (errorMessage.includes("already registered worktree")) {
3065
3349
  const worktrees = await this.getWorktreesFromBare(bareGit);
3066
- const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
3350
+ const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3067
3351
  if (existingWorktree && !existingWorktree.isPrunable) {
3068
3352
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
3069
3353
  return;
@@ -3074,9 +3358,10 @@ var GitService = class {
3074
3358
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
3075
3359
  } catch {
3076
3360
  }
3361
+ let retryCreatedNewBranch = false;
3077
3362
  try {
3078
3363
  const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
3079
- await this.runWorktreeAddByMatrix(
3364
+ retryCreatedNewBranch = await this.runWorktreeAddByMatrix(
3080
3365
  bareGit,
3081
3366
  branchName,
3082
3367
  absoluteWorktreePath,
@@ -3091,10 +3376,7 @@ var GitService = class {
3091
3376
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3092
3377
  } catch (metadataError) {
3093
3378
  this.logger.warn(` - Metadata creation failed for '${branchName}', removing worktree to prevent orphan`);
3094
- try {
3095
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3096
- } catch {
3097
- }
3379
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, retryCreatedNewBranch);
3098
3380
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3099
3381
  }
3100
3382
  return;
@@ -3111,7 +3393,7 @@ var GitService = class {
3111
3393
  try {
3112
3394
  await fs4.access(absoluteWorktreePath);
3113
3395
  const worktrees = await this.getWorktreesFromBare(bareGit);
3114
- const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
3396
+ const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3115
3397
  if (isValidWorktree) {
3116
3398
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3117
3399
  return;
@@ -3122,7 +3404,10 @@ var GitService = class {
3122
3404
  } catch {
3123
3405
  }
3124
3406
  try {
3125
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3407
+ const useNoCheckout = !!this.config.sparseCheckout;
3408
+ const fallbackArgs = useNoCheckout ? ["worktree", "add", "--no-checkout", absoluteWorktreePath, branchName] : ["worktree", "add", absoluteWorktreePath, branchName];
3409
+ await bareGit.raw(fallbackArgs);
3410
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3126
3411
  this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
3127
3412
  if (!this.isLfsSkipEnabled()) {
3128
3413
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
@@ -3131,17 +3416,14 @@ var GitService = class {
3131
3416
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3132
3417
  } catch (metadataError) {
3133
3418
  this.logger.warn(` - Metadata creation failed for '${branchName}', removing worktree to prevent orphan`);
3134
- try {
3135
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3136
- } catch {
3137
- }
3419
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, false);
3138
3420
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3139
3421
  }
3140
3422
  } catch (fallbackError) {
3141
3423
  const fallbackErrorMessage = getErrorMessage(fallbackError);
3142
3424
  if (fallbackErrorMessage.includes("already registered worktree")) {
3143
3425
  const worktrees = await this.getWorktreesFromBare(bareGit);
3144
- const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
3426
+ const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3145
3427
  if (existingWorktree && !existingWorktree.isPrunable) {
3146
3428
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
3147
3429
  return;
@@ -3152,42 +3434,64 @@ var GitService = class {
3152
3434
  }
3153
3435
  }
3154
3436
  async runWorktreeAddByMatrix(bareGit, branchName, absoluteWorktreePath, localExists, remoteExists) {
3437
+ const useNoCheckout = !!this.config.sparseCheckout;
3438
+ const noCheckoutFlag = useNoCheckout ? ["--no-checkout"] : [];
3155
3439
  if (localExists && remoteExists) {
3156
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3440
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlag, absoluteWorktreePath, branchName]);
3157
3441
  try {
3158
3442
  const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3159
3443
  await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
3160
3444
  } catch (error) {
3161
- let rollbackFailed = false;
3162
- try {
3163
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3164
- } catch (rollbackError) {
3165
- rollbackFailed = true;
3166
- this.logger.warn(
3167
- ` - Rollback failed for '${branchName}' at '${absoluteWorktreePath}' after upstream setup error: ${getErrorMessage(rollbackError)}`
3168
- );
3169
- }
3170
- const detail = getErrorMessage(error);
3171
- const suffix = rollbackFailed ? " (rollback failed; partial worktree may remain)" : "";
3172
- const wrapped = new Error(`Failed to set upstream for '${branchName}': ${detail}${suffix}`);
3173
- wrapped.isUpstreamSetupFailure = true;
3174
- throw wrapped;
3445
+ throw await this.wrapUpstreamFailure(bareGit, absoluteWorktreePath, branchName, false, error);
3175
3446
  }
3176
- return;
3447
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3448
+ return false;
3177
3449
  }
3178
3450
  if (localExists) {
3179
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3180
- return;
3451
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlag, absoluteWorktreePath, branchName]);
3452
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3453
+ return false;
3181
3454
  }
3182
3455
  if (remoteExists) {
3183
- await bareGit.raw(["worktree", "add", "--track", "-b", branchName, absoluteWorktreePath, `origin/${branchName}`]);
3184
- return;
3456
+ await bareGit.raw([
3457
+ "worktree",
3458
+ "add",
3459
+ ...noCheckoutFlag,
3460
+ "--track",
3461
+ "-b",
3462
+ branchName,
3463
+ absoluteWorktreePath,
3464
+ `origin/${branchName}`
3465
+ ]);
3466
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, true);
3467
+ return true;
3185
3468
  }
3186
3469
  throw new WorktreeError(
3187
3470
  `Branch '${branchName}' does not exist locally or on origin; create it first`,
3188
3471
  "BRANCH_NOT_FOUND"
3189
3472
  );
3190
3473
  }
3474
+ async runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, createdNewBranch) {
3475
+ try {
3476
+ await this.applySparseAndCheckout(absoluteWorktreePath);
3477
+ } catch (sparseError) {
3478
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch);
3479
+ throw new Error(`Sparse-checkout setup failed for '${branchName}': ${getErrorMessage(sparseError)}`);
3480
+ }
3481
+ }
3482
+ async wrapUpstreamFailure(bareGit, absoluteWorktreePath, branchName, createdNewBranch, error) {
3483
+ const { worktreeRemoved } = await this.rollbackPartialWorktree(
3484
+ bareGit,
3485
+ absoluteWorktreePath,
3486
+ branchName,
3487
+ createdNewBranch,
3488
+ "upstream setup error"
3489
+ );
3490
+ const suffix = worktreeRemoved ? "" : " (rollback failed; partial worktree may remain)";
3491
+ const wrapped = new Error(`Failed to set upstream for '${branchName}': ${getErrorMessage(error)}${suffix}`);
3492
+ wrapped.isUpstreamSetupFailure = true;
3493
+ return wrapped;
3494
+ }
3191
3495
  async removeWorktree(worktreePath) {
3192
3496
  const bareGit = this.getCachedGit(this.bareRepoPath);
3193
3497
  await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
@@ -3426,15 +3730,7 @@ var GitService = class {
3426
3730
  // src/services/path-resolution.service.ts
3427
3731
  import { createHash } from "crypto";
3428
3732
  import * as fs5 from "fs";
3429
- import * as path5 from "path";
3430
-
3431
- // src/utils/path-compare.ts
3432
- var CASE_INSENSITIVE_PLATFORMS = /* @__PURE__ */ new Set(["darwin"]);
3433
- function isCaseInsensitiveFs(platform = process.platform) {
3434
- return CASE_INSENSITIVE_PLATFORMS.has(platform);
3435
- }
3436
-
3437
- // src/services/path-resolution.service.ts
3733
+ import * as path6 from "path";
3438
3734
  var BRANCH_STEM_MAX = 80;
3439
3735
  var BRANCH_HASH_LEN = 8;
3440
3736
  var PathResolutionService = class {
@@ -3444,22 +3740,22 @@ var PathResolutionService = class {
3444
3740
  return `${stem}-${hash}`;
3445
3741
  }
3446
3742
  getBranchWorktreePath(worktreeDir, branchName) {
3447
- return path5.join(worktreeDir, this.sanitizeBranchName(branchName));
3743
+ return path6.join(worktreeDir, this.sanitizeBranchName(branchName));
3448
3744
  }
3449
3745
  resolveRealPath(inputPath) {
3450
- const absolute = path5.resolve(inputPath);
3746
+ const absolute = path6.resolve(inputPath);
3451
3747
  const missing = [];
3452
3748
  let current = absolute;
3453
3749
  while (!fs5.existsSync(current)) {
3454
- const parent = path5.dirname(current);
3750
+ const parent = path6.dirname(current);
3455
3751
  if (parent === current) {
3456
3752
  return absolute;
3457
3753
  }
3458
- missing.unshift(path5.basename(current));
3754
+ missing.unshift(path6.basename(current));
3459
3755
  current = parent;
3460
3756
  }
3461
3757
  try {
3462
- return path5.join(fs5.realpathSync(current), ...missing);
3758
+ return path6.join(fs5.realpathSync(current), ...missing);
3463
3759
  } catch {
3464
3760
  return absolute;
3465
3761
  }
@@ -3469,7 +3765,7 @@ var PathResolutionService = class {
3469
3765
  const a = fold(resolved);
3470
3766
  const b = fold(resolvedBase);
3471
3767
  if (a === b) return true;
3472
- return a.length > b.length && a.charAt(b.length) === path5.sep && a.startsWith(b);
3768
+ return a.length > b.length && a.charAt(b.length) === path6.sep && a.startsWith(b);
3473
3769
  }
3474
3770
  normalizeWorktreePath(worktreePath, worktreeBaseDir) {
3475
3771
  const resolved = this.resolveRealPath(worktreePath);
@@ -3477,7 +3773,7 @@ var PathResolutionService = class {
3477
3773
  if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
3478
3774
  throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
3479
3775
  }
3480
- return path5.relative(resolvedBase, resolved);
3776
+ return path6.relative(resolvedBase, resolved);
3481
3777
  }
3482
3778
  isPathInsideBaseDir(targetPath, baseDir) {
3483
3779
  const resolved = this.resolveRealPath(targetPath);
@@ -3603,8 +3899,50 @@ var WorktreeSyncService = class {
3603
3899
  if (this.config.updateExistingWorktrees !== false) {
3604
3900
  await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
3605
3901
  }
3902
+ if (this.config.sparseCheckout) {
3903
+ await this.reapplySparseCheckout(worktrees);
3904
+ }
3606
3905
  await this.finalizeSyncAttempt(phaseTimer);
3607
3906
  }
3907
+ async reapplySparseCheckout(worktrees) {
3908
+ const sparseConfig = this.config.sparseCheckout;
3909
+ if (!sparseConfig) return;
3910
+ this.logger.info("Step 5: Reconciling sparse-checkout patterns on existing worktrees...");
3911
+ const sparseService = this.gitService.getSparseCheckoutService();
3912
+ const desired = sparseService.buildPatterns(sparseConfig);
3913
+ const limit = pLimit(this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
3914
+ await Promise.all(
3915
+ worktrees.map(
3916
+ (worktree) => limit(async () => {
3917
+ try {
3918
+ try {
3919
+ await fs6.access(worktree.path);
3920
+ } catch {
3921
+ return;
3922
+ }
3923
+ const current = await sparseService.readCurrent(worktree.path);
3924
+ if (current !== null && sparseService.patternsEqual(current, desired)) return;
3925
+ if (sparseService.isNarrowing(current, desired)) {
3926
+ const status = await this.gitService.getFullWorktreeStatus(worktree.path, false);
3927
+ if (!status.canRemove) {
3928
+ this.logger.warn(
3929
+ ` - Skipping sparse-checkout narrowing for '${worktree.branch}': ${status.reasons.join(", ")}.`
3930
+ );
3931
+ return;
3932
+ }
3933
+ }
3934
+ await sparseService.applyToWorktree(worktree.path, sparseConfig);
3935
+ await this.gitService.checkoutHead(worktree.path);
3936
+ this.logger.info(` - \u2705 Sparse-checkout updated for '${worktree.branch}'`);
3937
+ } catch (error) {
3938
+ this.logger.warn(
3939
+ ` - \u26A0\uFE0F Failed to update sparse-checkout for '${worktree.branch}': ${getErrorMessage(error)}`
3940
+ );
3941
+ }
3942
+ })
3943
+ )
3944
+ );
3945
+ }
3608
3946
  async fetchLatestRemoteData(phaseTimer, syncContext) {
3609
3947
  this.logger.info("Step 1: Fetching latest data from remote...");
3610
3948
  phaseTimer.startPhase("Phase 1: Fetch");
@@ -3696,12 +4034,12 @@ var WorktreeSyncService = class {
3696
4034
  }
3697
4035
  const reservedPaths = /* @__PURE__ */ new Map();
3698
4036
  for (const w of worktrees) {
3699
- reservedPaths.set(path6.resolve(w.path), w.branch);
4037
+ reservedPaths.set(path7.resolve(w.path), w.branch);
3700
4038
  }
3701
4039
  const plan = [];
3702
4040
  for (const branchName of newBranches) {
3703
4041
  const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
3704
- const resolved = path6.resolve(worktreePath);
4042
+ const resolved = path7.resolve(worktreePath);
3705
4043
  const conflict = reservedPaths.get(resolved);
3706
4044
  if (conflict && conflict !== branchName) {
3707
4045
  this.logger.error(
@@ -3906,12 +4244,12 @@ var WorktreeSyncService = class {
3906
4244
  }
3907
4245
  async updateExistingWorktrees(worktrees, remoteBranches) {
3908
4246
  this.logger.info("Step 4: Checking for worktrees that need updates...");
3909
- const divergedDir = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4247
+ const divergedDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
3910
4248
  try {
3911
4249
  const diverged = await fs6.readdir(divergedDir);
3912
4250
  if (diverged.length > 0) {
3913
4251
  this.logger.info(
3914
- `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path6.relative(process.cwd(), divergedDir)}`
4252
+ `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path7.relative(process.cwd(), divergedDir)}`
3915
4253
  );
3916
4254
  }
3917
4255
  } catch {
@@ -4019,13 +4357,13 @@ var WorktreeSyncService = class {
4019
4357
  }
4020
4358
  async cleanupOrphanedDirectories(worktrees) {
4021
4359
  try {
4022
- const worktreeRelativePaths = worktrees.map((w) => path6.relative(this.config.worktreeDir, w.path));
4360
+ const worktreeRelativePaths = worktrees.map((w) => path7.relative(this.config.worktreeDir, w.path));
4023
4361
  const allDirs = await fs6.readdir(this.config.worktreeDir);
4024
4362
  const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
4025
4363
  const orphanedDirs = [];
4026
4364
  for (const dir of regularDirs) {
4027
4365
  const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
4028
- return worktreePath === dir || worktreePath.startsWith(dir + path6.sep);
4366
+ return worktreePath === dir || worktreePath.startsWith(dir + path7.sep);
4029
4367
  });
4030
4368
  if (!isPartOfWorktree) {
4031
4369
  orphanedDirs.push(dir);
@@ -4034,7 +4372,7 @@ var WorktreeSyncService = class {
4034
4372
  if (orphanedDirs.length > 0) {
4035
4373
  this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
4036
4374
  for (const dir of orphanedDirs) {
4037
- const dirPath = path6.join(this.config.worktreeDir, dir);
4375
+ const dirPath = path7.join(this.config.worktreeDir, dir);
4038
4376
  try {
4039
4377
  const stat3 = await fs6.stat(dirPath);
4040
4378
  if (stat3.isDirectory()) {
@@ -4068,7 +4406,7 @@ var WorktreeSyncService = class {
4068
4406
  } else {
4069
4407
  this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
4070
4408
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
4071
- const relativePath = path6.relative(process.cwd(), divergedPath);
4409
+ const relativePath = path7.relative(process.cwd(), divergedPath);
4072
4410
  this.logger.info(` Moved to: ${relativePath}`);
4073
4411
  this.logger.info(` Your local changes are preserved. To review:`);
4074
4412
  this.logger.info(` cd ${relativePath}`);
@@ -4092,12 +4430,12 @@ var WorktreeSyncService = class {
4092
4430
  }
4093
4431
  }
4094
4432
  async divergeWorktree(worktreePath, branchName) {
4095
- const divergedBaseDir = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4433
+ const divergedBaseDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4096
4434
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4097
4435
  const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
4098
4436
  const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
4099
4437
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
4100
- const divergedPath = path6.join(divergedBaseDir, divergedName);
4438
+ const divergedPath = path7.join(divergedBaseDir, divergedName);
4101
4439
  await fs6.mkdir(divergedBaseDir, { recursive: true });
4102
4440
  try {
4103
4441
  await fs6.rename(worktreePath, divergedPath);
@@ -4124,7 +4462,7 @@ var WorktreeSyncService = class {
4124
4462
  Original worktree location: ${worktreePath}`
4125
4463
  };
4126
4464
  await fs6.writeFile(
4127
- path6.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4465
+ path7.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4128
4466
  JSON.stringify(metadata, null, 2)
4129
4467
  );
4130
4468
  return divergedPath;
@@ -4133,7 +4471,7 @@ var WorktreeSyncService = class {
4133
4471
 
4134
4472
  // src/services/file-copy.service.ts
4135
4473
  import * as fs7 from "fs/promises";
4136
- import * as path7 from "path";
4474
+ import * as path8 from "path";
4137
4475
  import { glob } from "glob";
4138
4476
  var DEFAULT_IGNORE_PATTERNS = [
4139
4477
  "**/node_modules/**",
@@ -4160,8 +4498,8 @@ var FileCopyService = class {
4160
4498
  }
4161
4499
  const filesToCopy = await this.expandPatterns(sourceDir, patterns);
4162
4500
  for (const relativePath of filesToCopy) {
4163
- const sourcePath = path7.join(sourceDir, relativePath);
4164
- const destPath = path7.join(destDir, relativePath);
4501
+ const sourcePath = path8.join(sourceDir, relativePath);
4502
+ const destPath = path8.join(destDir, relativePath);
4165
4503
  try {
4166
4504
  const copied = await this.copyFile(sourcePath, destPath);
4167
4505
  if (copied) {
@@ -4202,7 +4540,7 @@ var FileCopyService = class {
4202
4540
  return false;
4203
4541
  } catch {
4204
4542
  }
4205
- const destDir = path7.dirname(destPath);
4543
+ const destDir = path8.dirname(destPath);
4206
4544
  await fs7.mkdir(destDir, { recursive: true });
4207
4545
  await fs7.copyFile(sourcePath, destPath);
4208
4546
  return true;
@@ -4338,7 +4676,7 @@ var HookExecutionService = class {
4338
4676
  // src/utils/disk-space.ts
4339
4677
  import fastFolderSize from "fast-folder-size";
4340
4678
  async function calculateDirectorySize(dirPath) {
4341
- return new Promise((resolve8, reject) => {
4679
+ return new Promise((resolve9, reject) => {
4342
4680
  fastFolderSize(dirPath, (err, bytes) => {
4343
4681
  if (err) {
4344
4682
  reject(err);
@@ -4348,7 +4686,7 @@ async function calculateDirectorySize(dirPath) {
4348
4686
  reject(new Error(`fast-folder-size returned no bytes for ${dirPath}`));
4349
4687
  return;
4350
4688
  }
4351
- resolve8(bytes);
4689
+ resolve9(bytes);
4352
4690
  });
4353
4691
  });
4354
4692
  }
@@ -4550,8 +4888,8 @@ var InteractiveUIService = class {
4550
4888
  getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
4551
4889
  getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
4552
4890
  deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
4553
- openEditorInWorktree: (path12) => this.openEditorInWorktree(path12),
4554
- openTerminalInWorktree: (repoIndex, path12, branchName) => this.openTerminalInWorktree(repoIndex, path12, branchName),
4891
+ openEditorInWorktree: (path13) => this.openEditorInWorktree(path13),
4892
+ openTerminalInWorktree: (repoIndex, path13, branchName) => this.openTerminalInWorktree(repoIndex, path13, branchName),
4555
4893
  copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
4556
4894
  createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
4557
4895
  executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
@@ -4655,7 +4993,7 @@ var InteractiveUIService = class {
4655
4993
  if (Date.now() - startTime > timeout) {
4656
4994
  throw new Error("Timeout waiting for sync operations to complete");
4657
4995
  }
4658
- await new Promise((resolve8) => setTimeout(resolve8, checkInterval));
4996
+ await new Promise((resolve9) => setTimeout(resolve9, checkInterval));
4659
4997
  }
4660
4998
  });
4661
4999
  try {
@@ -4788,7 +5126,7 @@ var InteractiveUIService = class {
4788
5126
  }
4789
5127
  const service = this.syncServices[repoIndex];
4790
5128
  const worktreeDir = service.config.worktreeDir;
4791
- const divergedDir = path8.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5129
+ const divergedDir = path9.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4792
5130
  let dirEntries;
4793
5131
  try {
4794
5132
  dirEntries = await fs8.readdir(divergedDir, { withFileTypes: true, encoding: "utf-8" });
@@ -4798,8 +5136,8 @@ var InteractiveUIService = class {
4798
5136
  const subdirs = dirEntries.filter((e) => e.isDirectory());
4799
5137
  const results = await Promise.allSettled(
4800
5138
  subdirs.map(async (entry) => {
4801
- const fullPath = path8.join(divergedDir, entry.name);
4802
- const infoFilePath = path8.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
5139
+ const fullPath = path9.join(divergedDir, entry.name);
5140
+ const infoFilePath = path9.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
4803
5141
  let originalBranch = entry.name;
4804
5142
  let divergedAt = "";
4805
5143
  try {
@@ -4834,11 +5172,11 @@ var InteractiveUIService = class {
4834
5172
  }
4835
5173
  const service = this.syncServices[repoIndex];
4836
5174
  const worktreeDir = service.config.worktreeDir;
4837
- const divergedBase = path8.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5175
+ const divergedBase = path9.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4838
5176
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
4839
5177
  throw new Error(`Invalid diverged directory name: "${name}"`);
4840
5178
  }
4841
- const targetPath = path8.join(divergedBase, name);
5179
+ const targetPath = path9.join(divergedBase, name);
4842
5180
  if (!this.pathResolution.isPathInsideBaseDir(targetPath, divergedBase)) {
4843
5181
  throw new Error(`Path traversal rejected: "${name}" resolves outside the diverged directory`);
4844
5182
  }
@@ -5227,7 +5565,7 @@ function reconstructCliCommand(config) {
5227
5565
 
5228
5566
  // src/utils/config-generator.ts
5229
5567
  import * as fs9 from "fs/promises";
5230
- import * as path9 from "path";
5568
+ import * as path10 from "path";
5231
5569
  function serializeToESM(obj, indent = 0) {
5232
5570
  const spaces = " ".repeat(indent);
5233
5571
  const innerSpaces = " ".repeat(indent + 2);
@@ -5257,9 +5595,9 @@ ${spaces}}`;
5257
5595
  return String(obj);
5258
5596
  }
5259
5597
  async function generateConfigFile(config, configPath) {
5260
- const configDir = path9.dirname(configPath);
5598
+ const configDir = path10.dirname(configPath);
5261
5599
  await fs9.mkdir(configDir, { recursive: true });
5262
- const worktreeDirRelative = path9.relative(configDir, config.worktreeDir);
5600
+ const worktreeDirRelative = path10.relative(configDir, config.worktreeDir);
5263
5601
  const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
5264
5602
  const repoName = extractRepoNameFromUrl(config.repoUrl);
5265
5603
  const repository = {
@@ -5268,7 +5606,7 @@ async function generateConfigFile(config, configPath) {
5268
5606
  worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
5269
5607
  };
5270
5608
  if (config.bareRepoDir) {
5271
- const bareRepoDirRelative = path9.relative(configDir, config.bareRepoDir);
5609
+ const bareRepoDirRelative = path10.relative(configDir, config.bareRepoDir);
5272
5610
  const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
5273
5611
  repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
5274
5612
  }
@@ -5289,11 +5627,11 @@ export default ${serializeToESM(configObject)};
5289
5627
  await fs9.writeFile(configPath, configContent, "utf-8");
5290
5628
  }
5291
5629
  function getDefaultConfigPath() {
5292
- return path9.join(process.cwd(), "sync-worktrees.config.js");
5630
+ return path10.join(process.cwd(), "sync-worktrees.config.js");
5293
5631
  }
5294
5632
  async function findConfigInCwd(cwd = process.cwd()) {
5295
5633
  for (const name of CONFIG_FILE_NAMES) {
5296
- const full = path9.join(cwd, name);
5634
+ const full = path10.join(cwd, name);
5297
5635
  try {
5298
5636
  await fs9.access(full);
5299
5637
  return full;
@@ -5304,7 +5642,7 @@ async function findConfigInCwd(cwd = process.cwd()) {
5304
5642
  }
5305
5643
 
5306
5644
  // src/utils/interactive.ts
5307
- import * as path10 from "path";
5645
+ import * as path11 from "path";
5308
5646
  import { confirm, input, select } from "@inquirer/prompts";
5309
5647
  async function promptForConfig(partialConfig) {
5310
5648
  console.log("\u{1F527} Welcome to sync-worktrees interactive setup!\n");
@@ -5344,8 +5682,8 @@ async function promptForConfig(partialConfig) {
5344
5682
  if (!worktreeDir.trim() && defaultWorktreeDir) {
5345
5683
  worktreeDir = defaultWorktreeDir;
5346
5684
  }
5347
- if (!path10.isAbsolute(worktreeDir)) {
5348
- worktreeDir = path10.resolve(worktreeDir);
5685
+ if (!path11.isAbsolute(worktreeDir)) {
5686
+ worktreeDir = path11.resolve(worktreeDir);
5349
5687
  }
5350
5688
  }
5351
5689
  let bareRepoDir = partialConfig.bareRepoDir;
@@ -5364,8 +5702,8 @@ async function promptForConfig(partialConfig) {
5364
5702
  return true;
5365
5703
  }
5366
5704
  });
5367
- if (!path10.isAbsolute(bareRepoDir)) {
5368
- bareRepoDir = path10.resolve(bareRepoDir);
5705
+ if (!path11.isAbsolute(bareRepoDir)) {
5706
+ bareRepoDir = path11.resolve(bareRepoDir);
5369
5707
  }
5370
5708
  }
5371
5709
  let runOnce = partialConfig.runOnce;
@@ -5437,8 +5775,8 @@ async function promptForConfig(partialConfig) {
5437
5775
  return true;
5438
5776
  }
5439
5777
  });
5440
- if (!path10.isAbsolute(configPath)) {
5441
- configPath = path10.resolve(configPath);
5778
+ if (!path11.isAbsolute(configPath)) {
5779
+ configPath = path11.resolve(configPath);
5442
5780
  }
5443
5781
  try {
5444
5782
  await generateConfigFile(finalConfig, configPath);
@@ -5674,7 +6012,7 @@ async function main() {
5674
6012
  const discovered = await findConfigInCwd();
5675
6013
  if (discovered) {
5676
6014
  options.config = discovered;
5677
- console.log(`\u{1F4C4} Using config: ${path11.relative(process.cwd(), discovered)}`);
6015
+ console.log(`\u{1F4C4} Using config: ${path12.relative(process.cwd(), discovered)}`);
5678
6016
  }
5679
6017
  }
5680
6018
  if (options.config) {