sync-worktrees 3.2.0 → 3.3.1

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;
@@ -2766,14 +2993,22 @@ var WorktreeStatusService = class {
2766
2993
  };
2767
2994
 
2768
2995
  // src/services/git.service.ts
2996
+ function sanitizeGitEnv(env) {
2997
+ const sanitized = { ...env };
2998
+ delete sanitized.EDITOR;
2999
+ delete sanitized.GIT_EDITOR;
3000
+ delete sanitized.GIT_SEQUENCE_EDITOR;
3001
+ return sanitized;
3002
+ }
2769
3003
  var GitService = class {
2770
3004
  constructor(config, logger) {
2771
3005
  this.config = config;
2772
3006
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
2773
3007
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
2774
- this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
3008
+ this.mainWorktreePath = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
2775
3009
  this.metadataService = new WorktreeMetadataService(this.logger);
2776
3010
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
3011
+ this.sparseCheckoutService = new SparseCheckoutService(this.logger);
2777
3012
  }
2778
3013
  git = null;
2779
3014
  bareRepoPath;
@@ -2782,29 +3017,34 @@ var GitService = class {
2782
3017
  // Will be updated after detection
2783
3018
  metadataService;
2784
3019
  statusService;
3020
+ sparseCheckoutService;
2785
3021
  logger;
2786
3022
  lfsSkipOverride = false;
2787
3023
  gitInstances = /* @__PURE__ */ new Map();
3024
+ getSparseCheckoutService() {
3025
+ return this.sparseCheckoutService;
3026
+ }
2788
3027
  getCachedGit(dirPath, useLfsSkip = false) {
2789
- const key = `${path4.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3028
+ const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
2790
3029
  let git = this.gitInstances.get(key);
2791
3030
  if (!git) {
2792
- git = useLfsSkip ? simpleGit3(dirPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(dirPath);
3031
+ git = useLfsSkip ? simpleGit4(dirPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4(dirPath);
2793
3032
  this.gitInstances.set(key, git);
2794
3033
  }
2795
3034
  return git;
2796
3035
  }
2797
3036
  updateLogger(logger) {
2798
3037
  this.logger = logger;
3038
+ this.sparseCheckoutService.updateLogger(logger);
2799
3039
  }
2800
3040
  async initialize() {
2801
3041
  const { repoUrl } = this.config;
2802
3042
  try {
2803
- await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
3043
+ await fs4.access(path5.join(this.bareRepoPath, "HEAD"));
2804
3044
  } catch {
2805
3045
  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();
3046
+ await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
3047
+ const cloneGit = this.isLfsSkipEnabled() ? simpleGit4().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit4();
2808
3048
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
2809
3049
  this.logger.info("\u2705 Clone successful.");
2810
3050
  }
@@ -2821,34 +3061,39 @@ var GitService = class {
2821
3061
  this.logger.info("Fetching remote branches...");
2822
3062
  await bareGit.fetch(["--all"]);
2823
3063
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
2824
- this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
3064
+ this.mainWorktreePath = path5.join(this.config.worktreeDir, this.defaultBranch);
2825
3065
  let needsMainWorktree = true;
2826
3066
  try {
2827
3067
  const worktrees = await this.getWorktreesFromBare(bareGit);
2828
- needsMainWorktree = !worktrees.some((w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath));
3068
+ needsMainWorktree = !worktrees.some((w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath));
2829
3069
  } catch {
2830
3070
  }
2831
3071
  if (needsMainWorktree) {
2832
3072
  this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
2833
3073
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
2834
- const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
3074
+ const absoluteWorktreePath = path5.resolve(this.mainWorktreePath);
2835
3075
  const branches = await bareGit.branch();
2836
3076
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
3077
+ const useNoCheckoutMain = !!this.config.sparseCheckout;
3078
+ const noCheckoutFlagMain = useNoCheckoutMain ? ["--no-checkout"] : [];
2837
3079
  try {
2838
3080
  if (defaultBranchExists) {
2839
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
3081
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlagMain, absoluteWorktreePath, this.defaultBranch]);
2840
3082
  const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
2841
3083
  await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
3084
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, this.defaultBranch, false);
2842
3085
  } else {
2843
3086
  await bareGit.raw([
2844
3087
  "worktree",
2845
3088
  "add",
3089
+ ...noCheckoutFlagMain,
2846
3090
  "--track",
2847
3091
  "-b",
2848
3092
  this.defaultBranch,
2849
3093
  absoluteWorktreePath,
2850
3094
  `origin/${this.defaultBranch}`
2851
3095
  ]);
3096
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, this.defaultBranch, true);
2852
3097
  }
2853
3098
  } catch (error) {
2854
3099
  const errorMessage = getErrorMessage(error);
@@ -2862,7 +3107,7 @@ var GitService = class {
2862
3107
  }
2863
3108
  const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
2864
3109
  const mainWorktreeRegistered = updatedWorktrees.some(
2865
- (w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
3110
+ (w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath)
2866
3111
  );
2867
3112
  if (!mainWorktreeRegistered) {
2868
3113
  if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
@@ -2931,13 +3176,29 @@ var GitService = class {
2931
3176
  return branches;
2932
3177
  }
2933
3178
  async verifyLfsFilesDownloaded(worktreePath, branchName) {
2934
- const worktreeGit = this.getCachedGit(worktreePath);
3179
+ const worktreeGit = this.config.sparseCheckout ? simpleGit4(worktreePath).env({ ...sanitizeGitEnv(process.env), [ENV_CONSTANTS.GIT_ATTR_SOURCE]: "HEAD" }) : this.getCachedGit(worktreePath);
2935
3180
  try {
2936
3181
  const lfsFiles = await worktreeGit.raw(["lfs", "ls-files", "--name-only"]);
2937
- const lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
3182
+ let lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
2938
3183
  if (lfsFileList.length === 0) {
2939
3184
  return;
2940
3185
  }
3186
+ if (this.config.sparseCheckout) {
3187
+ const existence = await Promise.all(
3188
+ lfsFileList.map(async (f) => {
3189
+ try {
3190
+ await fs4.access(path5.join(worktreePath, f));
3191
+ return f;
3192
+ } catch {
3193
+ return null;
3194
+ }
3195
+ })
3196
+ );
3197
+ lfsFileList = existence.filter((f) => f !== null);
3198
+ if (lfsFileList.length === 0) {
3199
+ return;
3200
+ }
3201
+ }
2941
3202
  if (this.config.debug) {
2942
3203
  this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
2943
3204
  }
@@ -2954,7 +3215,7 @@ var GitService = class {
2954
3215
  let allDownloaded = true;
2955
3216
  const notDownloaded = [];
2956
3217
  for (const file of samplesToCheck) {
2957
- const filePath = path4.join(worktreePath, file);
3218
+ const filePath = path5.join(worktreePath, file);
2958
3219
  try {
2959
3220
  const handle = await fs4.open(filePath, "r");
2960
3221
  try {
@@ -2981,7 +3242,7 @@ var GitService = class {
2981
3242
  }
2982
3243
  retries++;
2983
3244
  if (retries < maxRetries) {
2984
- await new Promise((resolve8) => setTimeout(resolve8, retryDelay));
3245
+ await new Promise((resolve9) => setTimeout(resolve9, retryDelay));
2985
3246
  }
2986
3247
  }
2987
3248
  this.logger.warn(
@@ -2991,6 +3252,38 @@ var GitService = class {
2991
3252
  this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
2992
3253
  }
2993
3254
  }
3255
+ async checkoutHead(worktreePath) {
3256
+ const git = this.getCachedGit(worktreePath, this.isLfsSkipEnabled());
3257
+ await git.raw(["checkout", "HEAD"]);
3258
+ }
3259
+ async applySparseAndCheckout(absoluteWorktreePath) {
3260
+ if (!this.config.sparseCheckout) return;
3261
+ await this.sparseCheckoutService.applyToWorktree(absoluteWorktreePath, this.config.sparseCheckout);
3262
+ const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3263
+ await worktreeGit.raw(["checkout", "HEAD"]);
3264
+ }
3265
+ async rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch, failureContext) {
3266
+ let worktreeRemoved = true;
3267
+ try {
3268
+ await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3269
+ } catch (rollbackError) {
3270
+ worktreeRemoved = false;
3271
+ const ctx = failureContext ? ` after ${failureContext}` : "";
3272
+ this.logger.warn(
3273
+ ` - Rollback failed for '${branchName}' at '${absoluteWorktreePath}'${ctx}: ${getErrorMessage(rollbackError)}`
3274
+ );
3275
+ }
3276
+ if (createdNewBranch) {
3277
+ try {
3278
+ await bareGit.raw(["branch", "-D", branchName]);
3279
+ } catch (branchRollbackError) {
3280
+ this.logger.warn(
3281
+ ` - Rollback (branch delete) failed for '${branchName}': ${getErrorMessage(branchRollbackError)}`
3282
+ );
3283
+ }
3284
+ }
3285
+ return { worktreeRemoved };
3286
+ }
2994
3287
  async createWorktreeMetadata(bareGit, worktreePath, branchName) {
2995
3288
  try {
2996
3289
  const worktreeGit = this.getCachedGit(worktreePath, this.isLfsSkipEnabled());
@@ -3011,12 +3304,12 @@ var GitService = class {
3011
3304
  }
3012
3305
  async addWorktree(branchName, worktreePath) {
3013
3306
  const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
3014
- const absoluteWorktreePath = path4.resolve(worktreePath);
3015
- await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
3307
+ const absoluteWorktreePath = path5.resolve(worktreePath);
3308
+ await fs4.mkdir(path5.dirname(absoluteWorktreePath), { recursive: true });
3016
3309
  try {
3017
3310
  await fs4.access(absoluteWorktreePath);
3018
3311
  const worktrees = await this.getWorktreesFromBare(bareGit);
3019
- const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
3312
+ const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3020
3313
  if (isValidWorktree) {
3021
3314
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3022
3315
  return;
@@ -3026,9 +3319,10 @@ var GitService = class {
3026
3319
  }
3027
3320
  } catch {
3028
3321
  }
3322
+ let createdNewBranch = false;
3029
3323
  try {
3030
3324
  const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
3031
- await this.runWorktreeAddByMatrix(
3325
+ createdNewBranch = await this.runWorktreeAddByMatrix(
3032
3326
  bareGit,
3033
3327
  branchName,
3034
3328
  absoluteWorktreePath,
@@ -3047,10 +3341,7 @@ var GitService = class {
3047
3341
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3048
3342
  } catch (metadataError) {
3049
3343
  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
- }
3344
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch);
3054
3345
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3055
3346
  }
3056
3347
  } catch (error) {
@@ -3063,7 +3354,7 @@ var GitService = class {
3063
3354
  }
3064
3355
  if (errorMessage.includes("already registered worktree")) {
3065
3356
  const worktrees = await this.getWorktreesFromBare(bareGit);
3066
- const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
3357
+ const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3067
3358
  if (existingWorktree && !existingWorktree.isPrunable) {
3068
3359
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
3069
3360
  return;
@@ -3074,9 +3365,10 @@ var GitService = class {
3074
3365
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
3075
3366
  } catch {
3076
3367
  }
3368
+ let retryCreatedNewBranch = false;
3077
3369
  try {
3078
3370
  const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
3079
- await this.runWorktreeAddByMatrix(
3371
+ retryCreatedNewBranch = await this.runWorktreeAddByMatrix(
3080
3372
  bareGit,
3081
3373
  branchName,
3082
3374
  absoluteWorktreePath,
@@ -3091,10 +3383,7 @@ var GitService = class {
3091
3383
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3092
3384
  } catch (metadataError) {
3093
3385
  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
- }
3386
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, retryCreatedNewBranch);
3098
3387
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3099
3388
  }
3100
3389
  return;
@@ -3111,7 +3400,7 @@ var GitService = class {
3111
3400
  try {
3112
3401
  await fs4.access(absoluteWorktreePath);
3113
3402
  const worktrees = await this.getWorktreesFromBare(bareGit);
3114
- const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
3403
+ const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3115
3404
  if (isValidWorktree) {
3116
3405
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3117
3406
  return;
@@ -3122,7 +3411,10 @@ var GitService = class {
3122
3411
  } catch {
3123
3412
  }
3124
3413
  try {
3125
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3414
+ const useNoCheckout = !!this.config.sparseCheckout;
3415
+ const fallbackArgs = useNoCheckout ? ["worktree", "add", "--no-checkout", absoluteWorktreePath, branchName] : ["worktree", "add", absoluteWorktreePath, branchName];
3416
+ await bareGit.raw(fallbackArgs);
3417
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3126
3418
  this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
3127
3419
  if (!this.isLfsSkipEnabled()) {
3128
3420
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
@@ -3131,17 +3423,14 @@ var GitService = class {
3131
3423
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3132
3424
  } catch (metadataError) {
3133
3425
  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
- }
3426
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, false);
3138
3427
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3139
3428
  }
3140
3429
  } catch (fallbackError) {
3141
3430
  const fallbackErrorMessage = getErrorMessage(fallbackError);
3142
3431
  if (fallbackErrorMessage.includes("already registered worktree")) {
3143
3432
  const worktrees = await this.getWorktreesFromBare(bareGit);
3144
- const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
3433
+ const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3145
3434
  if (existingWorktree && !existingWorktree.isPrunable) {
3146
3435
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
3147
3436
  return;
@@ -3152,42 +3441,64 @@ var GitService = class {
3152
3441
  }
3153
3442
  }
3154
3443
  async runWorktreeAddByMatrix(bareGit, branchName, absoluteWorktreePath, localExists, remoteExists) {
3444
+ const useNoCheckout = !!this.config.sparseCheckout;
3445
+ const noCheckoutFlag = useNoCheckout ? ["--no-checkout"] : [];
3155
3446
  if (localExists && remoteExists) {
3156
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3447
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlag, absoluteWorktreePath, branchName]);
3157
3448
  try {
3158
3449
  const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3159
3450
  await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
3160
3451
  } 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;
3452
+ throw await this.wrapUpstreamFailure(bareGit, absoluteWorktreePath, branchName, false, error);
3175
3453
  }
3176
- return;
3454
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3455
+ return false;
3177
3456
  }
3178
3457
  if (localExists) {
3179
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3180
- return;
3458
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlag, absoluteWorktreePath, branchName]);
3459
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3460
+ return false;
3181
3461
  }
3182
3462
  if (remoteExists) {
3183
- await bareGit.raw(["worktree", "add", "--track", "-b", branchName, absoluteWorktreePath, `origin/${branchName}`]);
3184
- return;
3463
+ await bareGit.raw([
3464
+ "worktree",
3465
+ "add",
3466
+ ...noCheckoutFlag,
3467
+ "--track",
3468
+ "-b",
3469
+ branchName,
3470
+ absoluteWorktreePath,
3471
+ `origin/${branchName}`
3472
+ ]);
3473
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, true);
3474
+ return true;
3185
3475
  }
3186
3476
  throw new WorktreeError(
3187
3477
  `Branch '${branchName}' does not exist locally or on origin; create it first`,
3188
3478
  "BRANCH_NOT_FOUND"
3189
3479
  );
3190
3480
  }
3481
+ async runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, createdNewBranch) {
3482
+ try {
3483
+ await this.applySparseAndCheckout(absoluteWorktreePath);
3484
+ } catch (sparseError) {
3485
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch);
3486
+ throw new Error(`Sparse-checkout setup failed for '${branchName}': ${getErrorMessage(sparseError)}`);
3487
+ }
3488
+ }
3489
+ async wrapUpstreamFailure(bareGit, absoluteWorktreePath, branchName, createdNewBranch, error) {
3490
+ const { worktreeRemoved } = await this.rollbackPartialWorktree(
3491
+ bareGit,
3492
+ absoluteWorktreePath,
3493
+ branchName,
3494
+ createdNewBranch,
3495
+ "upstream setup error"
3496
+ );
3497
+ const suffix = worktreeRemoved ? "" : " (rollback failed; partial worktree may remain)";
3498
+ const wrapped = new Error(`Failed to set upstream for '${branchName}': ${getErrorMessage(error)}${suffix}`);
3499
+ wrapped.isUpstreamSetupFailure = true;
3500
+ return wrapped;
3501
+ }
3191
3502
  async removeWorktree(worktreePath) {
3192
3503
  const bareGit = this.getCachedGit(this.bareRepoPath);
3193
3504
  await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
@@ -3426,15 +3737,7 @@ var GitService = class {
3426
3737
  // src/services/path-resolution.service.ts
3427
3738
  import { createHash } from "crypto";
3428
3739
  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
3740
+ import * as path6 from "path";
3438
3741
  var BRANCH_STEM_MAX = 80;
3439
3742
  var BRANCH_HASH_LEN = 8;
3440
3743
  var PathResolutionService = class {
@@ -3444,22 +3747,22 @@ var PathResolutionService = class {
3444
3747
  return `${stem}-${hash}`;
3445
3748
  }
3446
3749
  getBranchWorktreePath(worktreeDir, branchName) {
3447
- return path5.join(worktreeDir, this.sanitizeBranchName(branchName));
3750
+ return path6.join(worktreeDir, this.sanitizeBranchName(branchName));
3448
3751
  }
3449
3752
  resolveRealPath(inputPath) {
3450
- const absolute = path5.resolve(inputPath);
3753
+ const absolute = path6.resolve(inputPath);
3451
3754
  const missing = [];
3452
3755
  let current = absolute;
3453
3756
  while (!fs5.existsSync(current)) {
3454
- const parent = path5.dirname(current);
3757
+ const parent = path6.dirname(current);
3455
3758
  if (parent === current) {
3456
3759
  return absolute;
3457
3760
  }
3458
- missing.unshift(path5.basename(current));
3761
+ missing.unshift(path6.basename(current));
3459
3762
  current = parent;
3460
3763
  }
3461
3764
  try {
3462
- return path5.join(fs5.realpathSync(current), ...missing);
3765
+ return path6.join(fs5.realpathSync(current), ...missing);
3463
3766
  } catch {
3464
3767
  return absolute;
3465
3768
  }
@@ -3469,7 +3772,7 @@ var PathResolutionService = class {
3469
3772
  const a = fold(resolved);
3470
3773
  const b = fold(resolvedBase);
3471
3774
  if (a === b) return true;
3472
- return a.length > b.length && a.charAt(b.length) === path5.sep && a.startsWith(b);
3775
+ return a.length > b.length && a.charAt(b.length) === path6.sep && a.startsWith(b);
3473
3776
  }
3474
3777
  normalizeWorktreePath(worktreePath, worktreeBaseDir) {
3475
3778
  const resolved = this.resolveRealPath(worktreePath);
@@ -3477,7 +3780,7 @@ var PathResolutionService = class {
3477
3780
  if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
3478
3781
  throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
3479
3782
  }
3480
- return path5.relative(resolvedBase, resolved);
3783
+ return path6.relative(resolvedBase, resolved);
3481
3784
  }
3482
3785
  isPathInsideBaseDir(targetPath, baseDir) {
3483
3786
  const resolved = this.resolveRealPath(targetPath);
@@ -3603,8 +3906,50 @@ var WorktreeSyncService = class {
3603
3906
  if (this.config.updateExistingWorktrees !== false) {
3604
3907
  await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
3605
3908
  }
3909
+ if (this.config.sparseCheckout) {
3910
+ await this.reapplySparseCheckout(worktrees);
3911
+ }
3606
3912
  await this.finalizeSyncAttempt(phaseTimer);
3607
3913
  }
3914
+ async reapplySparseCheckout(worktrees) {
3915
+ const sparseConfig = this.config.sparseCheckout;
3916
+ if (!sparseConfig) return;
3917
+ this.logger.info("Step 5: Reconciling sparse-checkout patterns on existing worktrees...");
3918
+ const sparseService = this.gitService.getSparseCheckoutService();
3919
+ const desired = sparseService.buildPatterns(sparseConfig);
3920
+ const limit = pLimit(this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
3921
+ await Promise.all(
3922
+ worktrees.map(
3923
+ (worktree) => limit(async () => {
3924
+ try {
3925
+ try {
3926
+ await fs6.access(worktree.path);
3927
+ } catch {
3928
+ return;
3929
+ }
3930
+ const current = await sparseService.readCurrent(worktree.path);
3931
+ if (current !== null && sparseService.patternsEqual(current, desired)) return;
3932
+ if (sparseService.isNarrowing(current, desired)) {
3933
+ const status = await this.gitService.getFullWorktreeStatus(worktree.path, false);
3934
+ if (!status.canRemove) {
3935
+ this.logger.warn(
3936
+ ` - Skipping sparse-checkout narrowing for '${worktree.branch}': ${status.reasons.join(", ")}.`
3937
+ );
3938
+ return;
3939
+ }
3940
+ }
3941
+ await sparseService.applyToWorktree(worktree.path, sparseConfig);
3942
+ await this.gitService.checkoutHead(worktree.path);
3943
+ this.logger.info(` - \u2705 Sparse-checkout updated for '${worktree.branch}'`);
3944
+ } catch (error) {
3945
+ this.logger.warn(
3946
+ ` - \u26A0\uFE0F Failed to update sparse-checkout for '${worktree.branch}': ${getErrorMessage(error)}`
3947
+ );
3948
+ }
3949
+ })
3950
+ )
3951
+ );
3952
+ }
3608
3953
  async fetchLatestRemoteData(phaseTimer, syncContext) {
3609
3954
  this.logger.info("Step 1: Fetching latest data from remote...");
3610
3955
  phaseTimer.startPhase("Phase 1: Fetch");
@@ -3696,12 +4041,12 @@ var WorktreeSyncService = class {
3696
4041
  }
3697
4042
  const reservedPaths = /* @__PURE__ */ new Map();
3698
4043
  for (const w of worktrees) {
3699
- reservedPaths.set(path6.resolve(w.path), w.branch);
4044
+ reservedPaths.set(path7.resolve(w.path), w.branch);
3700
4045
  }
3701
4046
  const plan = [];
3702
4047
  for (const branchName of newBranches) {
3703
4048
  const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
3704
- const resolved = path6.resolve(worktreePath);
4049
+ const resolved = path7.resolve(worktreePath);
3705
4050
  const conflict = reservedPaths.get(resolved);
3706
4051
  if (conflict && conflict !== branchName) {
3707
4052
  this.logger.error(
@@ -3906,12 +4251,12 @@ var WorktreeSyncService = class {
3906
4251
  }
3907
4252
  async updateExistingWorktrees(worktrees, remoteBranches) {
3908
4253
  this.logger.info("Step 4: Checking for worktrees that need updates...");
3909
- const divergedDir = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4254
+ const divergedDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
3910
4255
  try {
3911
4256
  const diverged = await fs6.readdir(divergedDir);
3912
4257
  if (diverged.length > 0) {
3913
4258
  this.logger.info(
3914
- `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path6.relative(process.cwd(), divergedDir)}`
4259
+ `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path7.relative(process.cwd(), divergedDir)}`
3915
4260
  );
3916
4261
  }
3917
4262
  } catch {
@@ -4019,13 +4364,13 @@ var WorktreeSyncService = class {
4019
4364
  }
4020
4365
  async cleanupOrphanedDirectories(worktrees) {
4021
4366
  try {
4022
- const worktreeRelativePaths = worktrees.map((w) => path6.relative(this.config.worktreeDir, w.path));
4367
+ const worktreeRelativePaths = worktrees.map((w) => path7.relative(this.config.worktreeDir, w.path));
4023
4368
  const allDirs = await fs6.readdir(this.config.worktreeDir);
4024
4369
  const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
4025
4370
  const orphanedDirs = [];
4026
4371
  for (const dir of regularDirs) {
4027
4372
  const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
4028
- return worktreePath === dir || worktreePath.startsWith(dir + path6.sep);
4373
+ return worktreePath === dir || worktreePath.startsWith(dir + path7.sep);
4029
4374
  });
4030
4375
  if (!isPartOfWorktree) {
4031
4376
  orphanedDirs.push(dir);
@@ -4034,7 +4379,7 @@ var WorktreeSyncService = class {
4034
4379
  if (orphanedDirs.length > 0) {
4035
4380
  this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
4036
4381
  for (const dir of orphanedDirs) {
4037
- const dirPath = path6.join(this.config.worktreeDir, dir);
4382
+ const dirPath = path7.join(this.config.worktreeDir, dir);
4038
4383
  try {
4039
4384
  const stat3 = await fs6.stat(dirPath);
4040
4385
  if (stat3.isDirectory()) {
@@ -4068,7 +4413,7 @@ var WorktreeSyncService = class {
4068
4413
  } else {
4069
4414
  this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
4070
4415
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
4071
- const relativePath = path6.relative(process.cwd(), divergedPath);
4416
+ const relativePath = path7.relative(process.cwd(), divergedPath);
4072
4417
  this.logger.info(` Moved to: ${relativePath}`);
4073
4418
  this.logger.info(` Your local changes are preserved. To review:`);
4074
4419
  this.logger.info(` cd ${relativePath}`);
@@ -4092,12 +4437,12 @@ var WorktreeSyncService = class {
4092
4437
  }
4093
4438
  }
4094
4439
  async divergeWorktree(worktreePath, branchName) {
4095
- const divergedBaseDir = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4440
+ const divergedBaseDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4096
4441
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4097
4442
  const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
4098
4443
  const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
4099
4444
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
4100
- const divergedPath = path6.join(divergedBaseDir, divergedName);
4445
+ const divergedPath = path7.join(divergedBaseDir, divergedName);
4101
4446
  await fs6.mkdir(divergedBaseDir, { recursive: true });
4102
4447
  try {
4103
4448
  await fs6.rename(worktreePath, divergedPath);
@@ -4124,7 +4469,7 @@ var WorktreeSyncService = class {
4124
4469
  Original worktree location: ${worktreePath}`
4125
4470
  };
4126
4471
  await fs6.writeFile(
4127
- path6.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4472
+ path7.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4128
4473
  JSON.stringify(metadata, null, 2)
4129
4474
  );
4130
4475
  return divergedPath;
@@ -4133,7 +4478,7 @@ var WorktreeSyncService = class {
4133
4478
 
4134
4479
  // src/services/file-copy.service.ts
4135
4480
  import * as fs7 from "fs/promises";
4136
- import * as path7 from "path";
4481
+ import * as path8 from "path";
4137
4482
  import { glob } from "glob";
4138
4483
  var DEFAULT_IGNORE_PATTERNS = [
4139
4484
  "**/node_modules/**",
@@ -4160,8 +4505,8 @@ var FileCopyService = class {
4160
4505
  }
4161
4506
  const filesToCopy = await this.expandPatterns(sourceDir, patterns);
4162
4507
  for (const relativePath of filesToCopy) {
4163
- const sourcePath = path7.join(sourceDir, relativePath);
4164
- const destPath = path7.join(destDir, relativePath);
4508
+ const sourcePath = path8.join(sourceDir, relativePath);
4509
+ const destPath = path8.join(destDir, relativePath);
4165
4510
  try {
4166
4511
  const copied = await this.copyFile(sourcePath, destPath);
4167
4512
  if (copied) {
@@ -4202,7 +4547,7 @@ var FileCopyService = class {
4202
4547
  return false;
4203
4548
  } catch {
4204
4549
  }
4205
- const destDir = path7.dirname(destPath);
4550
+ const destDir = path8.dirname(destPath);
4206
4551
  await fs7.mkdir(destDir, { recursive: true });
4207
4552
  await fs7.copyFile(sourcePath, destPath);
4208
4553
  return true;
@@ -4338,7 +4683,7 @@ var HookExecutionService = class {
4338
4683
  // src/utils/disk-space.ts
4339
4684
  import fastFolderSize from "fast-folder-size";
4340
4685
  async function calculateDirectorySize(dirPath) {
4341
- return new Promise((resolve8, reject) => {
4686
+ return new Promise((resolve9, reject) => {
4342
4687
  fastFolderSize(dirPath, (err, bytes) => {
4343
4688
  if (err) {
4344
4689
  reject(err);
@@ -4348,7 +4693,7 @@ async function calculateDirectorySize(dirPath) {
4348
4693
  reject(new Error(`fast-folder-size returned no bytes for ${dirPath}`));
4349
4694
  return;
4350
4695
  }
4351
- resolve8(bytes);
4696
+ resolve9(bytes);
4352
4697
  });
4353
4698
  });
4354
4699
  }
@@ -4550,8 +4895,8 @@ var InteractiveUIService = class {
4550
4895
  getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
4551
4896
  getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
4552
4897
  deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
4553
- openEditorInWorktree: (path12) => this.openEditorInWorktree(path12),
4554
- openTerminalInWorktree: (repoIndex, path12, branchName) => this.openTerminalInWorktree(repoIndex, path12, branchName),
4898
+ openEditorInWorktree: (path13) => this.openEditorInWorktree(path13),
4899
+ openTerminalInWorktree: (repoIndex, path13, branchName) => this.openTerminalInWorktree(repoIndex, path13, branchName),
4555
4900
  copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
4556
4901
  createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
4557
4902
  executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
@@ -4655,7 +5000,7 @@ var InteractiveUIService = class {
4655
5000
  if (Date.now() - startTime > timeout) {
4656
5001
  throw new Error("Timeout waiting for sync operations to complete");
4657
5002
  }
4658
- await new Promise((resolve8) => setTimeout(resolve8, checkInterval));
5003
+ await new Promise((resolve9) => setTimeout(resolve9, checkInterval));
4659
5004
  }
4660
5005
  });
4661
5006
  try {
@@ -4788,7 +5133,7 @@ var InteractiveUIService = class {
4788
5133
  }
4789
5134
  const service = this.syncServices[repoIndex];
4790
5135
  const worktreeDir = service.config.worktreeDir;
4791
- const divergedDir = path8.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5136
+ const divergedDir = path9.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4792
5137
  let dirEntries;
4793
5138
  try {
4794
5139
  dirEntries = await fs8.readdir(divergedDir, { withFileTypes: true, encoding: "utf-8" });
@@ -4798,8 +5143,8 @@ var InteractiveUIService = class {
4798
5143
  const subdirs = dirEntries.filter((e) => e.isDirectory());
4799
5144
  const results = await Promise.allSettled(
4800
5145
  subdirs.map(async (entry) => {
4801
- const fullPath = path8.join(divergedDir, entry.name);
4802
- const infoFilePath = path8.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
5146
+ const fullPath = path9.join(divergedDir, entry.name);
5147
+ const infoFilePath = path9.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
4803
5148
  let originalBranch = entry.name;
4804
5149
  let divergedAt = "";
4805
5150
  try {
@@ -4834,11 +5179,11 @@ var InteractiveUIService = class {
4834
5179
  }
4835
5180
  const service = this.syncServices[repoIndex];
4836
5181
  const worktreeDir = service.config.worktreeDir;
4837
- const divergedBase = path8.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5182
+ const divergedBase = path9.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4838
5183
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
4839
5184
  throw new Error(`Invalid diverged directory name: "${name}"`);
4840
5185
  }
4841
- const targetPath = path8.join(divergedBase, name);
5186
+ const targetPath = path9.join(divergedBase, name);
4842
5187
  if (!this.pathResolution.isPathInsideBaseDir(targetPath, divergedBase)) {
4843
5188
  throw new Error(`Path traversal rejected: "${name}" resolves outside the diverged directory`);
4844
5189
  }
@@ -5227,7 +5572,7 @@ function reconstructCliCommand(config) {
5227
5572
 
5228
5573
  // src/utils/config-generator.ts
5229
5574
  import * as fs9 from "fs/promises";
5230
- import * as path9 from "path";
5575
+ import * as path10 from "path";
5231
5576
  function serializeToESM(obj, indent = 0) {
5232
5577
  const spaces = " ".repeat(indent);
5233
5578
  const innerSpaces = " ".repeat(indent + 2);
@@ -5257,9 +5602,9 @@ ${spaces}}`;
5257
5602
  return String(obj);
5258
5603
  }
5259
5604
  async function generateConfigFile(config, configPath) {
5260
- const configDir = path9.dirname(configPath);
5605
+ const configDir = path10.dirname(configPath);
5261
5606
  await fs9.mkdir(configDir, { recursive: true });
5262
- const worktreeDirRelative = path9.relative(configDir, config.worktreeDir);
5607
+ const worktreeDirRelative = path10.relative(configDir, config.worktreeDir);
5263
5608
  const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
5264
5609
  const repoName = extractRepoNameFromUrl(config.repoUrl);
5265
5610
  const repository = {
@@ -5268,7 +5613,7 @@ async function generateConfigFile(config, configPath) {
5268
5613
  worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
5269
5614
  };
5270
5615
  if (config.bareRepoDir) {
5271
- const bareRepoDirRelative = path9.relative(configDir, config.bareRepoDir);
5616
+ const bareRepoDirRelative = path10.relative(configDir, config.bareRepoDir);
5272
5617
  const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
5273
5618
  repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
5274
5619
  }
@@ -5289,11 +5634,11 @@ export default ${serializeToESM(configObject)};
5289
5634
  await fs9.writeFile(configPath, configContent, "utf-8");
5290
5635
  }
5291
5636
  function getDefaultConfigPath() {
5292
- return path9.join(process.cwd(), "sync-worktrees.config.js");
5637
+ return path10.join(process.cwd(), "sync-worktrees.config.js");
5293
5638
  }
5294
5639
  async function findConfigInCwd(cwd = process.cwd()) {
5295
5640
  for (const name of CONFIG_FILE_NAMES) {
5296
- const full = path9.join(cwd, name);
5641
+ const full = path10.join(cwd, name);
5297
5642
  try {
5298
5643
  await fs9.access(full);
5299
5644
  return full;
@@ -5304,7 +5649,7 @@ async function findConfigInCwd(cwd = process.cwd()) {
5304
5649
  }
5305
5650
 
5306
5651
  // src/utils/interactive.ts
5307
- import * as path10 from "path";
5652
+ import * as path11 from "path";
5308
5653
  import { confirm, input, select } from "@inquirer/prompts";
5309
5654
  async function promptForConfig(partialConfig) {
5310
5655
  console.log("\u{1F527} Welcome to sync-worktrees interactive setup!\n");
@@ -5344,8 +5689,8 @@ async function promptForConfig(partialConfig) {
5344
5689
  if (!worktreeDir.trim() && defaultWorktreeDir) {
5345
5690
  worktreeDir = defaultWorktreeDir;
5346
5691
  }
5347
- if (!path10.isAbsolute(worktreeDir)) {
5348
- worktreeDir = path10.resolve(worktreeDir);
5692
+ if (!path11.isAbsolute(worktreeDir)) {
5693
+ worktreeDir = path11.resolve(worktreeDir);
5349
5694
  }
5350
5695
  }
5351
5696
  let bareRepoDir = partialConfig.bareRepoDir;
@@ -5364,8 +5709,8 @@ async function promptForConfig(partialConfig) {
5364
5709
  return true;
5365
5710
  }
5366
5711
  });
5367
- if (!path10.isAbsolute(bareRepoDir)) {
5368
- bareRepoDir = path10.resolve(bareRepoDir);
5712
+ if (!path11.isAbsolute(bareRepoDir)) {
5713
+ bareRepoDir = path11.resolve(bareRepoDir);
5369
5714
  }
5370
5715
  }
5371
5716
  let runOnce = partialConfig.runOnce;
@@ -5437,8 +5782,8 @@ async function promptForConfig(partialConfig) {
5437
5782
  return true;
5438
5783
  }
5439
5784
  });
5440
- if (!path10.isAbsolute(configPath)) {
5441
- configPath = path10.resolve(configPath);
5785
+ if (!path11.isAbsolute(configPath)) {
5786
+ configPath = path11.resolve(configPath);
5442
5787
  }
5443
5788
  try {
5444
5789
  await generateConfigFile(finalConfig, configPath);
@@ -5674,7 +6019,7 @@ async function main() {
5674
6019
  const discovered = await findConfigInCwd();
5675
6020
  if (discovered) {
5676
6021
  options.config = discovered;
5677
- console.log(`\u{1F4C4} Using config: ${path11.relative(process.cwd(), discovered)}`);
6022
+ console.log(`\u{1F4C4} Using config: ${path12.relative(process.cwd(), discovered)}`);
5678
6023
  }
5679
6024
  }
5680
6025
  if (options.config) {