sync-worktrees 3.1.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,34 +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/utils/git-url.ts
1982
- function extractRepoNameFromUrl(gitUrl) {
1983
- const url = gitUrl.trim();
1984
- const sshMatch = url.match(/^git@[^:]+:(?:.+\/)?([^/]+?)(?:\.git)?$/);
1985
- if (sshMatch) {
1986
- return sshMatch[1];
1987
- }
1988
- const sshUrlMatch = url.match(/^ssh:\/\/[^/]+\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
1989
- if (sshUrlMatch) {
1990
- return sshUrlMatch[1];
1991
- }
1992
- const httpsMatch = url.match(/^https?:\/\/[^/]+\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
1993
- if (httpsMatch) {
1994
- return httpsMatch[1];
1995
- }
1996
- const fileMatch = url.match(/^file:\/\/(?:.+\/)?([^/]+?)(?:\.git)?$/);
1997
- if (fileMatch) {
1998
- return fileMatch[1];
1999
- }
2000
- throw new Error(`Invalid Git URL format: ${gitUrl}`);
2001
- }
2002
- function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
2003
- const repoName = extractRepoNameFromUrl(repoUrl);
2004
- return `${baseDir}/${repoName}`;
2005
- }
2177
+ import * as path5 from "path";
2178
+ import simpleGit4 from "simple-git";
2006
2179
 
2007
2180
  // src/utils/worktree-list-parser.ts
2008
2181
  function parseWorktreeListPorcelain(output) {
@@ -2144,10 +2317,101 @@ function defaultConsoleOutput(msg, level) {
2144
2317
  else console.log(msg);
2145
2318
  }
2146
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
+
2147
2411
  // src/services/worktree-metadata.service.ts
2148
2412
  import * as fs2 from "fs/promises";
2149
- import * as path2 from "path";
2150
- import simpleGit from "simple-git";
2413
+ import * as path3 from "path";
2414
+ import simpleGit2 from "simple-git";
2151
2415
  var WorktreeMetadataService = class {
2152
2416
  logger;
2153
2417
  constructor(logger) {
@@ -2159,7 +2423,7 @@ var WorktreeMetadataService = class {
2159
2423
  * For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
2160
2424
  */
2161
2425
  getWorktreeDirectoryName(worktreePath) {
2162
- return path2.basename(worktreePath);
2426
+ return path3.basename(worktreePath);
2163
2427
  }
2164
2428
  async getMetadataPath(bareRepoPath, worktreeName) {
2165
2429
  if (worktreeName.includes("/") || worktreeName.includes("\\")) {
@@ -2167,7 +2431,7 @@ var WorktreeMetadataService = class {
2167
2431
  `getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
2168
2432
  );
2169
2433
  }
2170
- return path2.join(
2434
+ return path3.join(
2171
2435
  bareRepoPath,
2172
2436
  METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
2173
2437
  worktreeName,
@@ -2180,7 +2444,7 @@ var WorktreeMetadataService = class {
2180
2444
  }
2181
2445
  async saveMetadata(bareRepoPath, worktreeName, metadata) {
2182
2446
  const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
2183
- await fs2.mkdir(path2.dirname(metadataPath), { recursive: true });
2447
+ await fs2.mkdir(path3.dirname(metadataPath), { recursive: true });
2184
2448
  const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
2185
2449
  let renamed = false;
2186
2450
  try {
@@ -2271,7 +2535,7 @@ var WorktreeMetadataService = class {
2271
2535
  this.logger.warn(`No metadata found for worktree ${worktreeDirName}`);
2272
2536
  this.logger.info(` Attempting to create initial metadata...`);
2273
2537
  try {
2274
- const worktreeGit = simpleGit(worktreePath);
2538
+ const worktreeGit = simpleGit2(worktreePath);
2275
2539
  const currentCommit = await worktreeGit.revparse(["HEAD"]);
2276
2540
  const branchSummary = await worktreeGit.branch();
2277
2541
  const actualBranchName = branchSummary.current;
@@ -2373,47 +2637,8 @@ var WorktreeMetadataService = class {
2373
2637
 
2374
2638
  // src/services/worktree-status.service.ts
2375
2639
  import * as fs3 from "fs/promises";
2376
- import * as path3 from "path";
2377
- import simpleGit2 from "simple-git";
2378
-
2379
- // src/errors/index.ts
2380
- var SyncWorktreesError = class extends Error {
2381
- constructor(message, code, cause) {
2382
- super(message);
2383
- this.code = code;
2384
- this.cause = cause;
2385
- this.name = this.constructor.name;
2386
- Object.setPrototypeOf(this, new.target.prototype);
2387
- if (cause && cause.stack) {
2388
- this.stack = `${this.stack}
2389
- Caused by: ${cause.stack}`;
2390
- }
2391
- }
2392
- };
2393
- var GitError = class extends SyncWorktreesError {
2394
- constructor(message, code, cause) {
2395
- super(message, `GIT_${code}`, cause);
2396
- }
2397
- };
2398
- var GitOperationError = class extends GitError {
2399
- constructor(operation, details, cause) {
2400
- super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
2401
- }
2402
- };
2403
- var WorktreeError = class extends SyncWorktreesError {
2404
- constructor(message, code, cause) {
2405
- super(message, `WORKTREE_${code}`, cause);
2406
- }
2407
- };
2408
- var WorktreeNotCleanError = class extends WorktreeError {
2409
- constructor(path12, reasons) {
2410
- super(`Worktree at '${path12}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
2411
- this.path = path12;
2412
- this.reasons = reasons;
2413
- }
2414
- };
2415
-
2416
- // src/services/worktree-status.service.ts
2640
+ import * as path4 from "path";
2641
+ import simpleGit3 from "simple-git";
2417
2642
  var OPERATION_FILES = [
2418
2643
  { file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
2419
2644
  { file: GIT_OPERATIONS.CHERRY_PICK_HEAD, type: "cherry-pick" },
@@ -2615,7 +2840,7 @@ var WorktreeStatusService = class {
2615
2840
  async detectOperationFile(gitDir) {
2616
2841
  const results = await Promise.all(
2617
2842
  OPERATION_FILES.map(
2618
- ({ file }) => fs3.access(path3.join(gitDir, file)).then(
2843
+ ({ file }) => fs3.access(path4.join(gitDir, file)).then(
2619
2844
  () => true,
2620
2845
  () => false
2621
2846
  )
@@ -2736,14 +2961,14 @@ var WorktreeStatusService = class {
2736
2961
  }
2737
2962
  }
2738
2963
  async resolveGitDir(worktreePath) {
2739
- const gitPath = path3.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
2964
+ const gitPath = path4.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
2740
2965
  try {
2741
2966
  const stat3 = await fs3.stat(gitPath);
2742
2967
  if (stat3.isFile()) {
2743
2968
  const content = await fs3.readFile(gitPath, "utf-8");
2744
2969
  const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
2745
2970
  if (gitdirMatch) {
2746
- return path3.resolve(worktreePath, gitdirMatch[1].trim());
2971
+ return path4.resolve(worktreePath, gitdirMatch[1].trim());
2747
2972
  }
2748
2973
  throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
2749
2974
  }
@@ -2757,10 +2982,10 @@ var WorktreeStatusService = class {
2757
2982
  }
2758
2983
  }
2759
2984
  createGitInstance(worktreePath) {
2760
- const key = `${path3.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
2985
+ const key = `${path4.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
2761
2986
  let git = this.gitInstances.get(key);
2762
2987
  if (!git) {
2763
- 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);
2764
2989
  this.gitInstances.set(key, git);
2765
2990
  }
2766
2991
  return git;
@@ -2773,9 +2998,10 @@ var GitService = class {
2773
2998
  this.config = config;
2774
2999
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
2775
3000
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
2776
- this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
3001
+ this.mainWorktreePath = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
2777
3002
  this.metadataService = new WorktreeMetadataService(this.logger);
2778
3003
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
3004
+ this.sparseCheckoutService = new SparseCheckoutService(this.logger);
2779
3005
  }
2780
3006
  git = null;
2781
3007
  bareRepoPath;
@@ -2784,29 +3010,34 @@ var GitService = class {
2784
3010
  // Will be updated after detection
2785
3011
  metadataService;
2786
3012
  statusService;
3013
+ sparseCheckoutService;
2787
3014
  logger;
2788
3015
  lfsSkipOverride = false;
2789
3016
  gitInstances = /* @__PURE__ */ new Map();
3017
+ getSparseCheckoutService() {
3018
+ return this.sparseCheckoutService;
3019
+ }
2790
3020
  getCachedGit(dirPath, useLfsSkip = false) {
2791
- const key = `${path4.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3021
+ const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
2792
3022
  let git = this.gitInstances.get(key);
2793
3023
  if (!git) {
2794
- 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);
2795
3025
  this.gitInstances.set(key, git);
2796
3026
  }
2797
3027
  return git;
2798
3028
  }
2799
3029
  updateLogger(logger) {
2800
3030
  this.logger = logger;
3031
+ this.sparseCheckoutService.updateLogger(logger);
2801
3032
  }
2802
3033
  async initialize() {
2803
3034
  const { repoUrl } = this.config;
2804
3035
  try {
2805
- await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
3036
+ await fs4.access(path5.join(this.bareRepoPath, "HEAD"));
2806
3037
  } catch {
2807
3038
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
2808
- await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
2809
- 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();
2810
3041
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
2811
3042
  this.logger.info("\u2705 Clone successful.");
2812
3043
  }
@@ -2823,34 +3054,39 @@ var GitService = class {
2823
3054
  this.logger.info("Fetching remote branches...");
2824
3055
  await bareGit.fetch(["--all"]);
2825
3056
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
2826
- this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
3057
+ this.mainWorktreePath = path5.join(this.config.worktreeDir, this.defaultBranch);
2827
3058
  let needsMainWorktree = true;
2828
3059
  try {
2829
3060
  const worktrees = await this.getWorktreesFromBare(bareGit);
2830
- 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));
2831
3062
  } catch {
2832
3063
  }
2833
3064
  if (needsMainWorktree) {
2834
3065
  this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
2835
3066
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
2836
- const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
3067
+ const absoluteWorktreePath = path5.resolve(this.mainWorktreePath);
2837
3068
  const branches = await bareGit.branch();
2838
3069
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
3070
+ const useNoCheckoutMain = !!this.config.sparseCheckout;
3071
+ const noCheckoutFlagMain = useNoCheckoutMain ? ["--no-checkout"] : [];
2839
3072
  try {
2840
3073
  if (defaultBranchExists) {
2841
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
3074
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlagMain, absoluteWorktreePath, this.defaultBranch]);
2842
3075
  const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
2843
3076
  await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
3077
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, this.defaultBranch, false);
2844
3078
  } else {
2845
3079
  await bareGit.raw([
2846
3080
  "worktree",
2847
3081
  "add",
3082
+ ...noCheckoutFlagMain,
2848
3083
  "--track",
2849
3084
  "-b",
2850
3085
  this.defaultBranch,
2851
3086
  absoluteWorktreePath,
2852
3087
  `origin/${this.defaultBranch}`
2853
3088
  ]);
3089
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, this.defaultBranch, true);
2854
3090
  }
2855
3091
  } catch (error) {
2856
3092
  const errorMessage = getErrorMessage(error);
@@ -2864,7 +3100,7 @@ var GitService = class {
2864
3100
  }
2865
3101
  const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
2866
3102
  const mainWorktreeRegistered = updatedWorktrees.some(
2867
- (w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
3103
+ (w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath)
2868
3104
  );
2869
3105
  if (!mainWorktreeRegistered) {
2870
3106
  if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
@@ -2933,13 +3169,29 @@ var GitService = class {
2933
3169
  return branches;
2934
3170
  }
2935
3171
  async verifyLfsFilesDownloaded(worktreePath, branchName) {
2936
- 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);
2937
3173
  try {
2938
3174
  const lfsFiles = await worktreeGit.raw(["lfs", "ls-files", "--name-only"]);
2939
- const lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
3175
+ let lfsFileList = lfsFiles.trim().split("\n").filter((f) => f.length > 0);
2940
3176
  if (lfsFileList.length === 0) {
2941
3177
  return;
2942
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
+ }
2943
3195
  if (this.config.debug) {
2944
3196
  this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
2945
3197
  }
@@ -2956,7 +3208,7 @@ var GitService = class {
2956
3208
  let allDownloaded = true;
2957
3209
  const notDownloaded = [];
2958
3210
  for (const file of samplesToCheck) {
2959
- const filePath = path4.join(worktreePath, file);
3211
+ const filePath = path5.join(worktreePath, file);
2960
3212
  try {
2961
3213
  const handle = await fs4.open(filePath, "r");
2962
3214
  try {
@@ -2983,7 +3235,7 @@ var GitService = class {
2983
3235
  }
2984
3236
  retries++;
2985
3237
  if (retries < maxRetries) {
2986
- await new Promise((resolve8) => setTimeout(resolve8, retryDelay));
3238
+ await new Promise((resolve9) => setTimeout(resolve9, retryDelay));
2987
3239
  }
2988
3240
  }
2989
3241
  this.logger.warn(
@@ -2993,6 +3245,38 @@ var GitService = class {
2993
3245
  this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
2994
3246
  }
2995
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
+ }
2996
3280
  async createWorktreeMetadata(bareGit, worktreePath, branchName) {
2997
3281
  try {
2998
3282
  const worktreeGit = this.getCachedGit(worktreePath, this.isLfsSkipEnabled());
@@ -3013,12 +3297,12 @@ var GitService = class {
3013
3297
  }
3014
3298
  async addWorktree(branchName, worktreePath) {
3015
3299
  const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
3016
- const absoluteWorktreePath = path4.resolve(worktreePath);
3017
- await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
3300
+ const absoluteWorktreePath = path5.resolve(worktreePath);
3301
+ await fs4.mkdir(path5.dirname(absoluteWorktreePath), { recursive: true });
3018
3302
  try {
3019
3303
  await fs4.access(absoluteWorktreePath);
3020
3304
  const worktrees = await this.getWorktreesFromBare(bareGit);
3021
- const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
3305
+ const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3022
3306
  if (isValidWorktree) {
3023
3307
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3024
3308
  return;
@@ -3028,25 +3312,21 @@ var GitService = class {
3028
3312
  }
3029
3313
  } catch {
3030
3314
  }
3315
+ let createdNewBranch = false;
3031
3316
  try {
3032
- const branches = await bareGit.branch();
3033
- const localBranchExists = branches.all.includes(branchName);
3034
- if (localBranchExists || branchName.includes("/")) {
3035
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3036
- const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3037
- await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
3317
+ const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
3318
+ createdNewBranch = await this.runWorktreeAddByMatrix(
3319
+ bareGit,
3320
+ branchName,
3321
+ absoluteWorktreePath,
3322
+ localBranchExists,
3323
+ remoteBranchExists
3324
+ );
3325
+ if (localBranchExists && !remoteBranchExists) {
3326
+ this.logger.info(` - Created worktree for '${branchName}' (no remote yet \u2014 push to set upstream)`);
3038
3327
  } else {
3039
- await bareGit.raw([
3040
- "worktree",
3041
- "add",
3042
- "--track",
3043
- "-b",
3044
- branchName,
3045
- absoluteWorktreePath,
3046
- `origin/${branchName}`
3047
- ]);
3328
+ this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
3048
3329
  }
3049
- this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
3050
3330
  if (!this.isLfsSkipEnabled()) {
3051
3331
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
3052
3332
  }
@@ -3054,20 +3334,20 @@ var GitService = class {
3054
3334
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3055
3335
  } catch (metadataError) {
3056
3336
  this.logger.warn(` - Metadata creation failed for '${branchName}', removing worktree to prevent orphan`);
3057
- try {
3058
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3059
- } catch {
3060
- }
3337
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, createdNewBranch);
3061
3338
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3062
3339
  }
3063
3340
  } catch (error) {
3064
3341
  const errorMessage = getErrorMessage(error);
3342
+ if (error?.isUpstreamSetupFailure) {
3343
+ throw error;
3344
+ }
3065
3345
  if (errorMessage.includes("Metadata creation failed")) {
3066
3346
  throw error;
3067
3347
  }
3068
3348
  if (errorMessage.includes("already registered worktree")) {
3069
3349
  const worktrees = await this.getWorktreesFromBare(bareGit);
3070
- const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
3350
+ const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3071
3351
  if (existingWorktree && !existingWorktree.isPrunable) {
3072
3352
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
3073
3353
  return;
@@ -3078,16 +3358,16 @@ var GitService = class {
3078
3358
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
3079
3359
  } catch {
3080
3360
  }
3361
+ let retryCreatedNewBranch = false;
3081
3362
  try {
3082
- await bareGit.raw([
3083
- "worktree",
3084
- "add",
3085
- "--track",
3086
- "-b",
3363
+ const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
3364
+ retryCreatedNewBranch = await this.runWorktreeAddByMatrix(
3365
+ bareGit,
3087
3366
  branchName,
3088
3367
  absoluteWorktreePath,
3089
- `origin/${branchName}`
3090
- ]);
3368
+ localBranchExists,
3369
+ remoteBranchExists
3370
+ );
3091
3371
  this.logger.info(` - Created worktree for '${branchName}' after pruning`);
3092
3372
  if (!this.isLfsSkipEnabled()) {
3093
3373
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
@@ -3096,10 +3376,7 @@ var GitService = class {
3096
3376
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3097
3377
  } catch (metadataError) {
3098
3378
  this.logger.warn(` - Metadata creation failed for '${branchName}', removing worktree to prevent orphan`);
3099
- try {
3100
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3101
- } catch {
3102
- }
3379
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, retryCreatedNewBranch);
3103
3380
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3104
3381
  }
3105
3382
  return;
@@ -3116,7 +3393,7 @@ var GitService = class {
3116
3393
  try {
3117
3394
  await fs4.access(absoluteWorktreePath);
3118
3395
  const worktrees = await this.getWorktreesFromBare(bareGit);
3119
- const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
3396
+ const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3120
3397
  if (isValidWorktree) {
3121
3398
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3122
3399
  return;
@@ -3127,7 +3404,10 @@ var GitService = class {
3127
3404
  } catch {
3128
3405
  }
3129
3406
  try {
3130
- 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);
3131
3411
  this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
3132
3412
  if (!this.isLfsSkipEnabled()) {
3133
3413
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
@@ -3136,17 +3416,14 @@ var GitService = class {
3136
3416
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
3137
3417
  } catch (metadataError) {
3138
3418
  this.logger.warn(` - Metadata creation failed for '${branchName}', removing worktree to prevent orphan`);
3139
- try {
3140
- await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
3141
- } catch {
3142
- }
3419
+ await this.rollbackPartialWorktree(bareGit, absoluteWorktreePath, branchName, false);
3143
3420
  throw new Error(`Metadata creation failed for '${branchName}': ${getErrorMessage(metadataError)}`);
3144
3421
  }
3145
3422
  } catch (fallbackError) {
3146
3423
  const fallbackErrorMessage = getErrorMessage(fallbackError);
3147
3424
  if (fallbackErrorMessage.includes("already registered worktree")) {
3148
3425
  const worktrees = await this.getWorktreesFromBare(bareGit);
3149
- const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
3426
+ const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3150
3427
  if (existingWorktree && !existingWorktree.isPrunable) {
3151
3428
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
3152
3429
  return;
@@ -3156,6 +3433,65 @@ var GitService = class {
3156
3433
  }
3157
3434
  }
3158
3435
  }
3436
+ async runWorktreeAddByMatrix(bareGit, branchName, absoluteWorktreePath, localExists, remoteExists) {
3437
+ const useNoCheckout = !!this.config.sparseCheckout;
3438
+ const noCheckoutFlag = useNoCheckout ? ["--no-checkout"] : [];
3439
+ if (localExists && remoteExists) {
3440
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlag, absoluteWorktreePath, branchName]);
3441
+ try {
3442
+ const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3443
+ await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
3444
+ } catch (error) {
3445
+ throw await this.wrapUpstreamFailure(bareGit, absoluteWorktreePath, branchName, false, error);
3446
+ }
3447
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3448
+ return false;
3449
+ }
3450
+ if (localExists) {
3451
+ await bareGit.raw(["worktree", "add", ...noCheckoutFlag, absoluteWorktreePath, branchName]);
3452
+ await this.runSparseStepWithRollback(bareGit, absoluteWorktreePath, branchName, false);
3453
+ return false;
3454
+ }
3455
+ if (remoteExists) {
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;
3468
+ }
3469
+ throw new WorktreeError(
3470
+ `Branch '${branchName}' does not exist locally or on origin; create it first`,
3471
+ "BRANCH_NOT_FOUND"
3472
+ );
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
+ }
3159
3495
  async removeWorktree(worktreePath) {
3160
3496
  const bareGit = this.getCachedGit(this.bareRepoPath);
3161
3497
  await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
@@ -3349,10 +3685,18 @@ var GitService = class {
3349
3685
  }
3350
3686
  async branchExists(branchName) {
3351
3687
  const bareGit = this.getCachedGit(this.bareRepoPath);
3352
- const localBranches = await bareGit.branch();
3353
- const local = localBranches.all.includes(branchName);
3354
- const remoteBranches = await bareGit.branch(["-r"]);
3355
- const remote = remoteBranches.all.includes(`origin/${branchName}`);
3688
+ const checkRef = async (ref) => {
3689
+ try {
3690
+ await bareGit.raw(["show-ref", "--verify", "--quiet", ref]);
3691
+ return true;
3692
+ } catch {
3693
+ return false;
3694
+ }
3695
+ };
3696
+ const [local, remote] = await Promise.all([
3697
+ checkRef(`${GIT_CONSTANTS.REFS.HEADS}${branchName}`),
3698
+ checkRef(`${GIT_CONSTANTS.REFS.REMOTES}/${branchName}`)
3699
+ ]);
3356
3700
  return { local, remote };
3357
3701
  }
3358
3702
  async getLocalBranches() {
@@ -3386,15 +3730,7 @@ var GitService = class {
3386
3730
  // src/services/path-resolution.service.ts
3387
3731
  import { createHash } from "crypto";
3388
3732
  import * as fs5 from "fs";
3389
- import * as path5 from "path";
3390
-
3391
- // src/utils/path-compare.ts
3392
- var CASE_INSENSITIVE_PLATFORMS = /* @__PURE__ */ new Set(["darwin"]);
3393
- function isCaseInsensitiveFs(platform = process.platform) {
3394
- return CASE_INSENSITIVE_PLATFORMS.has(platform);
3395
- }
3396
-
3397
- // src/services/path-resolution.service.ts
3733
+ import * as path6 from "path";
3398
3734
  var BRANCH_STEM_MAX = 80;
3399
3735
  var BRANCH_HASH_LEN = 8;
3400
3736
  var PathResolutionService = class {
@@ -3404,22 +3740,22 @@ var PathResolutionService = class {
3404
3740
  return `${stem}-${hash}`;
3405
3741
  }
3406
3742
  getBranchWorktreePath(worktreeDir, branchName) {
3407
- return path5.join(worktreeDir, this.sanitizeBranchName(branchName));
3743
+ return path6.join(worktreeDir, this.sanitizeBranchName(branchName));
3408
3744
  }
3409
3745
  resolveRealPath(inputPath) {
3410
- const absolute = path5.resolve(inputPath);
3746
+ const absolute = path6.resolve(inputPath);
3411
3747
  const missing = [];
3412
3748
  let current = absolute;
3413
3749
  while (!fs5.existsSync(current)) {
3414
- const parent = path5.dirname(current);
3750
+ const parent = path6.dirname(current);
3415
3751
  if (parent === current) {
3416
3752
  return absolute;
3417
3753
  }
3418
- missing.unshift(path5.basename(current));
3754
+ missing.unshift(path6.basename(current));
3419
3755
  current = parent;
3420
3756
  }
3421
3757
  try {
3422
- return path5.join(fs5.realpathSync(current), ...missing);
3758
+ return path6.join(fs5.realpathSync(current), ...missing);
3423
3759
  } catch {
3424
3760
  return absolute;
3425
3761
  }
@@ -3429,7 +3765,7 @@ var PathResolutionService = class {
3429
3765
  const a = fold(resolved);
3430
3766
  const b = fold(resolvedBase);
3431
3767
  if (a === b) return true;
3432
- 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);
3433
3769
  }
3434
3770
  normalizeWorktreePath(worktreePath, worktreeBaseDir) {
3435
3771
  const resolved = this.resolveRealPath(worktreePath);
@@ -3437,7 +3773,7 @@ var PathResolutionService = class {
3437
3773
  if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
3438
3774
  throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
3439
3775
  }
3440
- return path5.relative(resolvedBase, resolved);
3776
+ return path6.relative(resolvedBase, resolved);
3441
3777
  }
3442
3778
  isPathInsideBaseDir(targetPath, baseDir) {
3443
3779
  const resolved = this.resolveRealPath(targetPath);
@@ -3563,8 +3899,50 @@ var WorktreeSyncService = class {
3563
3899
  if (this.config.updateExistingWorktrees !== false) {
3564
3900
  await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
3565
3901
  }
3902
+ if (this.config.sparseCheckout) {
3903
+ await this.reapplySparseCheckout(worktrees);
3904
+ }
3566
3905
  await this.finalizeSyncAttempt(phaseTimer);
3567
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
+ }
3568
3946
  async fetchLatestRemoteData(phaseTimer, syncContext) {
3569
3947
  this.logger.info("Step 1: Fetching latest data from remote...");
3570
3948
  phaseTimer.startPhase("Phase 1: Fetch");
@@ -3656,12 +4034,12 @@ var WorktreeSyncService = class {
3656
4034
  }
3657
4035
  const reservedPaths = /* @__PURE__ */ new Map();
3658
4036
  for (const w of worktrees) {
3659
- reservedPaths.set(path6.resolve(w.path), w.branch);
4037
+ reservedPaths.set(path7.resolve(w.path), w.branch);
3660
4038
  }
3661
4039
  const plan = [];
3662
4040
  for (const branchName of newBranches) {
3663
4041
  const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
3664
- const resolved = path6.resolve(worktreePath);
4042
+ const resolved = path7.resolve(worktreePath);
3665
4043
  const conflict = reservedPaths.get(resolved);
3666
4044
  if (conflict && conflict !== branchName) {
3667
4045
  this.logger.error(
@@ -3866,12 +4244,12 @@ var WorktreeSyncService = class {
3866
4244
  }
3867
4245
  async updateExistingWorktrees(worktrees, remoteBranches) {
3868
4246
  this.logger.info("Step 4: Checking for worktrees that need updates...");
3869
- 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);
3870
4248
  try {
3871
4249
  const diverged = await fs6.readdir(divergedDir);
3872
4250
  if (diverged.length > 0) {
3873
4251
  this.logger.info(
3874
- `\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)}`
3875
4253
  );
3876
4254
  }
3877
4255
  } catch {
@@ -3979,13 +4357,13 @@ var WorktreeSyncService = class {
3979
4357
  }
3980
4358
  async cleanupOrphanedDirectories(worktrees) {
3981
4359
  try {
3982
- 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));
3983
4361
  const allDirs = await fs6.readdir(this.config.worktreeDir);
3984
4362
  const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
3985
4363
  const orphanedDirs = [];
3986
4364
  for (const dir of regularDirs) {
3987
4365
  const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
3988
- return worktreePath === dir || worktreePath.startsWith(dir + path6.sep);
4366
+ return worktreePath === dir || worktreePath.startsWith(dir + path7.sep);
3989
4367
  });
3990
4368
  if (!isPartOfWorktree) {
3991
4369
  orphanedDirs.push(dir);
@@ -3994,7 +4372,7 @@ var WorktreeSyncService = class {
3994
4372
  if (orphanedDirs.length > 0) {
3995
4373
  this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
3996
4374
  for (const dir of orphanedDirs) {
3997
- const dirPath = path6.join(this.config.worktreeDir, dir);
4375
+ const dirPath = path7.join(this.config.worktreeDir, dir);
3998
4376
  try {
3999
4377
  const stat3 = await fs6.stat(dirPath);
4000
4378
  if (stat3.isDirectory()) {
@@ -4028,7 +4406,7 @@ var WorktreeSyncService = class {
4028
4406
  } else {
4029
4407
  this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
4030
4408
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
4031
- const relativePath = path6.relative(process.cwd(), divergedPath);
4409
+ const relativePath = path7.relative(process.cwd(), divergedPath);
4032
4410
  this.logger.info(` Moved to: ${relativePath}`);
4033
4411
  this.logger.info(` Your local changes are preserved. To review:`);
4034
4412
  this.logger.info(` cd ${relativePath}`);
@@ -4052,12 +4430,12 @@ var WorktreeSyncService = class {
4052
4430
  }
4053
4431
  }
4054
4432
  async divergeWorktree(worktreePath, branchName) {
4055
- 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);
4056
4434
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4057
4435
  const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
4058
4436
  const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
4059
4437
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
4060
- const divergedPath = path6.join(divergedBaseDir, divergedName);
4438
+ const divergedPath = path7.join(divergedBaseDir, divergedName);
4061
4439
  await fs6.mkdir(divergedBaseDir, { recursive: true });
4062
4440
  try {
4063
4441
  await fs6.rename(worktreePath, divergedPath);
@@ -4084,7 +4462,7 @@ var WorktreeSyncService = class {
4084
4462
  Original worktree location: ${worktreePath}`
4085
4463
  };
4086
4464
  await fs6.writeFile(
4087
- path6.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4465
+ path7.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4088
4466
  JSON.stringify(metadata, null, 2)
4089
4467
  );
4090
4468
  return divergedPath;
@@ -4093,7 +4471,7 @@ var WorktreeSyncService = class {
4093
4471
 
4094
4472
  // src/services/file-copy.service.ts
4095
4473
  import * as fs7 from "fs/promises";
4096
- import * as path7 from "path";
4474
+ import * as path8 from "path";
4097
4475
  import { glob } from "glob";
4098
4476
  var DEFAULT_IGNORE_PATTERNS = [
4099
4477
  "**/node_modules/**",
@@ -4120,8 +4498,8 @@ var FileCopyService = class {
4120
4498
  }
4121
4499
  const filesToCopy = await this.expandPatterns(sourceDir, patterns);
4122
4500
  for (const relativePath of filesToCopy) {
4123
- const sourcePath = path7.join(sourceDir, relativePath);
4124
- const destPath = path7.join(destDir, relativePath);
4501
+ const sourcePath = path8.join(sourceDir, relativePath);
4502
+ const destPath = path8.join(destDir, relativePath);
4125
4503
  try {
4126
4504
  const copied = await this.copyFile(sourcePath, destPath);
4127
4505
  if (copied) {
@@ -4162,7 +4540,7 @@ var FileCopyService = class {
4162
4540
  return false;
4163
4541
  } catch {
4164
4542
  }
4165
- const destDir = path7.dirname(destPath);
4543
+ const destDir = path8.dirname(destPath);
4166
4544
  await fs7.mkdir(destDir, { recursive: true });
4167
4545
  await fs7.copyFile(sourcePath, destPath);
4168
4546
  return true;
@@ -4298,7 +4676,7 @@ var HookExecutionService = class {
4298
4676
  // src/utils/disk-space.ts
4299
4677
  import fastFolderSize from "fast-folder-size";
4300
4678
  async function calculateDirectorySize(dirPath) {
4301
- return new Promise((resolve8, reject) => {
4679
+ return new Promise((resolve9, reject) => {
4302
4680
  fastFolderSize(dirPath, (err, bytes) => {
4303
4681
  if (err) {
4304
4682
  reject(err);
@@ -4308,7 +4686,7 @@ async function calculateDirectorySize(dirPath) {
4308
4686
  reject(new Error(`fast-folder-size returned no bytes for ${dirPath}`));
4309
4687
  return;
4310
4688
  }
4311
- resolve8(bytes);
4689
+ resolve9(bytes);
4312
4690
  });
4313
4691
  });
4314
4692
  }
@@ -4510,8 +4888,8 @@ var InteractiveUIService = class {
4510
4888
  getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
4511
4889
  getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
4512
4890
  deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
4513
- openEditorInWorktree: (path12) => this.openEditorInWorktree(path12),
4514
- 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),
4515
4893
  copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
4516
4894
  createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
4517
4895
  executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
@@ -4615,7 +4993,7 @@ var InteractiveUIService = class {
4615
4993
  if (Date.now() - startTime > timeout) {
4616
4994
  throw new Error("Timeout waiting for sync operations to complete");
4617
4995
  }
4618
- await new Promise((resolve8) => setTimeout(resolve8, checkInterval));
4996
+ await new Promise((resolve9) => setTimeout(resolve9, checkInterval));
4619
4997
  }
4620
4998
  });
4621
4999
  try {
@@ -4748,7 +5126,7 @@ var InteractiveUIService = class {
4748
5126
  }
4749
5127
  const service = this.syncServices[repoIndex];
4750
5128
  const worktreeDir = service.config.worktreeDir;
4751
- const divergedDir = path8.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5129
+ const divergedDir = path9.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4752
5130
  let dirEntries;
4753
5131
  try {
4754
5132
  dirEntries = await fs8.readdir(divergedDir, { withFileTypes: true, encoding: "utf-8" });
@@ -4758,8 +5136,8 @@ var InteractiveUIService = class {
4758
5136
  const subdirs = dirEntries.filter((e) => e.isDirectory());
4759
5137
  const results = await Promise.allSettled(
4760
5138
  subdirs.map(async (entry) => {
4761
- const fullPath = path8.join(divergedDir, entry.name);
4762
- 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);
4763
5141
  let originalBranch = entry.name;
4764
5142
  let divergedAt = "";
4765
5143
  try {
@@ -4794,11 +5172,11 @@ var InteractiveUIService = class {
4794
5172
  }
4795
5173
  const service = this.syncServices[repoIndex];
4796
5174
  const worktreeDir = service.config.worktreeDir;
4797
- const divergedBase = path8.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5175
+ const divergedBase = path9.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4798
5176
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
4799
5177
  throw new Error(`Invalid diverged directory name: "${name}"`);
4800
5178
  }
4801
- const targetPath = path8.join(divergedBase, name);
5179
+ const targetPath = path9.join(divergedBase, name);
4802
5180
  if (!this.pathResolution.isPathInsideBaseDir(targetPath, divergedBase)) {
4803
5181
  throw new Error(`Path traversal rejected: "${name}" resolves outside the diverged directory`);
4804
5182
  }
@@ -5187,7 +5565,7 @@ function reconstructCliCommand(config) {
5187
5565
 
5188
5566
  // src/utils/config-generator.ts
5189
5567
  import * as fs9 from "fs/promises";
5190
- import * as path9 from "path";
5568
+ import * as path10 from "path";
5191
5569
  function serializeToESM(obj, indent = 0) {
5192
5570
  const spaces = " ".repeat(indent);
5193
5571
  const innerSpaces = " ".repeat(indent + 2);
@@ -5217,9 +5595,9 @@ ${spaces}}`;
5217
5595
  return String(obj);
5218
5596
  }
5219
5597
  async function generateConfigFile(config, configPath) {
5220
- const configDir = path9.dirname(configPath);
5598
+ const configDir = path10.dirname(configPath);
5221
5599
  await fs9.mkdir(configDir, { recursive: true });
5222
- const worktreeDirRelative = path9.relative(configDir, config.worktreeDir);
5600
+ const worktreeDirRelative = path10.relative(configDir, config.worktreeDir);
5223
5601
  const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
5224
5602
  const repoName = extractRepoNameFromUrl(config.repoUrl);
5225
5603
  const repository = {
@@ -5228,7 +5606,7 @@ async function generateConfigFile(config, configPath) {
5228
5606
  worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
5229
5607
  };
5230
5608
  if (config.bareRepoDir) {
5231
- const bareRepoDirRelative = path9.relative(configDir, config.bareRepoDir);
5609
+ const bareRepoDirRelative = path10.relative(configDir, config.bareRepoDir);
5232
5610
  const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
5233
5611
  repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
5234
5612
  }
@@ -5249,11 +5627,11 @@ export default ${serializeToESM(configObject)};
5249
5627
  await fs9.writeFile(configPath, configContent, "utf-8");
5250
5628
  }
5251
5629
  function getDefaultConfigPath() {
5252
- return path9.join(process.cwd(), "sync-worktrees.config.js");
5630
+ return path10.join(process.cwd(), "sync-worktrees.config.js");
5253
5631
  }
5254
5632
  async function findConfigInCwd(cwd = process.cwd()) {
5255
5633
  for (const name of CONFIG_FILE_NAMES) {
5256
- const full = path9.join(cwd, name);
5634
+ const full = path10.join(cwd, name);
5257
5635
  try {
5258
5636
  await fs9.access(full);
5259
5637
  return full;
@@ -5264,7 +5642,7 @@ async function findConfigInCwd(cwd = process.cwd()) {
5264
5642
  }
5265
5643
 
5266
5644
  // src/utils/interactive.ts
5267
- import * as path10 from "path";
5645
+ import * as path11 from "path";
5268
5646
  import { confirm, input, select } from "@inquirer/prompts";
5269
5647
  async function promptForConfig(partialConfig) {
5270
5648
  console.log("\u{1F527} Welcome to sync-worktrees interactive setup!\n");
@@ -5304,8 +5682,8 @@ async function promptForConfig(partialConfig) {
5304
5682
  if (!worktreeDir.trim() && defaultWorktreeDir) {
5305
5683
  worktreeDir = defaultWorktreeDir;
5306
5684
  }
5307
- if (!path10.isAbsolute(worktreeDir)) {
5308
- worktreeDir = path10.resolve(worktreeDir);
5685
+ if (!path11.isAbsolute(worktreeDir)) {
5686
+ worktreeDir = path11.resolve(worktreeDir);
5309
5687
  }
5310
5688
  }
5311
5689
  let bareRepoDir = partialConfig.bareRepoDir;
@@ -5324,8 +5702,8 @@ async function promptForConfig(partialConfig) {
5324
5702
  return true;
5325
5703
  }
5326
5704
  });
5327
- if (!path10.isAbsolute(bareRepoDir)) {
5328
- bareRepoDir = path10.resolve(bareRepoDir);
5705
+ if (!path11.isAbsolute(bareRepoDir)) {
5706
+ bareRepoDir = path11.resolve(bareRepoDir);
5329
5707
  }
5330
5708
  }
5331
5709
  let runOnce = partialConfig.runOnce;
@@ -5397,8 +5775,8 @@ async function promptForConfig(partialConfig) {
5397
5775
  return true;
5398
5776
  }
5399
5777
  });
5400
- if (!path10.isAbsolute(configPath)) {
5401
- configPath = path10.resolve(configPath);
5778
+ if (!path11.isAbsolute(configPath)) {
5779
+ configPath = path11.resolve(configPath);
5402
5780
  }
5403
5781
  try {
5404
5782
  await generateConfigFile(finalConfig, configPath);
@@ -5634,7 +6012,7 @@ async function main() {
5634
6012
  const discovered = await findConfigInCwd();
5635
6013
  if (discovered) {
5636
6014
  options.config = discovered;
5637
- console.log(`\u{1F4C4} Using config: ${path11.relative(process.cwd(), discovered)}`);
6015
+ console.log(`\u{1F4C4} Using config: ${path12.relative(process.cwd(), discovered)}`);
5638
6016
  }
5639
6017
  }
5640
6018
  if (options.config) {