sync-worktrees 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 path12 from "path";
4
+ import * as path13 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";
@@ -219,9 +219,9 @@ var WorktreeError = class extends SyncWorktreesError {
219
219
  }
220
220
  };
221
221
  var WorktreeNotCleanError = class extends WorktreeError {
222
- constructor(path13, reasons) {
223
- super(`Worktree at '${path13}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
224
- this.path = path13;
222
+ constructor(path14, reasons) {
223
+ super(`Worktree at '${path14}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
224
+ this.path = path14;
225
225
  this.reasons = reasons;
226
226
  }
227
227
  };
@@ -673,7 +673,7 @@ var ConfigLoaderService = class {
673
673
 
674
674
  // src/services/InteractiveUIService.tsx
675
675
  import React8 from "react";
676
- import * as path9 from "path";
676
+ import * as path10 from "path";
677
677
  import { render } from "ink";
678
678
  import * as cron2 from "node-cron";
679
679
  import pLimit2 from "p-limit";
@@ -1949,7 +1949,7 @@ var App_default = App;
1949
1949
 
1950
1950
  // src/services/worktree-sync.service.ts
1951
1951
  import * as fs6 from "fs/promises";
1952
- import * as path7 from "path";
1952
+ import * as path8 from "path";
1953
1953
  import pLimit from "p-limit";
1954
1954
  import * as lockfile from "proper-lockfile";
1955
1955
 
@@ -2179,7 +2179,7 @@ function formatTimingTable(totalDuration, phaseResults, repoName) {
2179
2179
 
2180
2180
  // src/services/git.service.ts
2181
2181
  import * as fs4 from "fs/promises";
2182
- import * as path5 from "path";
2182
+ import * as path6 from "path";
2183
2183
  import simpleGit4 from "simple-git";
2184
2184
 
2185
2185
  // src/utils/worktree-list-parser.ts
@@ -2323,11 +2323,13 @@ function defaultConsoleOutput(msg, level) {
2323
2323
  }
2324
2324
 
2325
2325
  // src/services/sparse-checkout.service.ts
2326
+ import * as path3 from "path";
2326
2327
  import simpleGit from "simple-git";
2327
2328
  var SparseCheckoutService = class {
2328
2329
  logger;
2329
2330
  gitFactory;
2330
2331
  warnedConfigs = /* @__PURE__ */ new WeakSet();
2332
+ matcherCache = /* @__PURE__ */ new WeakMap();
2331
2333
  constructor(logger, gitFactory) {
2332
2334
  this.logger = logger ?? Logger.createDefault();
2333
2335
  this.gitFactory = gitFactory ?? ((p) => simpleGit(p));
@@ -2411,11 +2413,66 @@ var SparseCheckoutService = class {
2411
2413
  const bt = b.map((x) => x.trim());
2412
2414
  return at.every((v, i) => v === bt[i]);
2413
2415
  }
2416
+ /**
2417
+ * Decide whether a list of changed file paths intersects the sparse-checkout
2418
+ * set defined by `cfg`. Used to skip fast-forward updates when upstream
2419
+ * commits only touch files outside the materialized worktree.
2420
+ *
2421
+ * Cone mode materializes:
2422
+ * - all files at the repository root,
2423
+ * - all files directly inside every ancestor of an included directory
2424
+ * (e.g. include `tools/build` keeps `tools/foo.txt` checked out too),
2425
+ * - everything inside an included directory.
2426
+ * We mirror those rules here. Missing the ancestor-files case would let
2427
+ * stale files linger when only those parent files change upstream.
2428
+ *
2429
+ * No-cone mode: gitignore-style matching with negation is non-trivial and
2430
+ * not implemented here yet. We return `true` so the caller falls back to
2431
+ * the safe behavior of always running the update.
2432
+ *
2433
+ * The matcher derived from `cfg` is cached on the cfg object identity
2434
+ * (WeakMap), so callers should reuse the same `cfg` reference across
2435
+ * invocations to benefit from the cache.
2436
+ */
2437
+ pathsTouchSparse(changedPaths, cfg) {
2438
+ if (changedPaths.length === 0) return false;
2439
+ const matcher = this.getMatcher(cfg);
2440
+ if (matcher.mode === "no-cone") return true;
2441
+ if (matcher.patterns.length === 0) return true;
2442
+ return changedPaths.some((p) => {
2443
+ if (!p.includes("/")) return true;
2444
+ for (const pat of matcher.patterns) {
2445
+ if (p === pat || p.startsWith(pat + "/")) return true;
2446
+ }
2447
+ return matcher.ancestorDirs.has(path3.posix.dirname(p));
2448
+ });
2449
+ }
2450
+ getMatcher(cfg) {
2451
+ const cached = this.matcherCache.get(cfg);
2452
+ if (cached) return cached;
2453
+ const mode = this.resolveMode(cfg);
2454
+ if (mode === "no-cone") {
2455
+ const matcher2 = { mode, patterns: [], ancestorDirs: /* @__PURE__ */ new Set() };
2456
+ this.matcherCache.set(cfg, matcher2);
2457
+ return matcher2;
2458
+ }
2459
+ const patterns = cfg.include.map((p) => p.trim()).filter((p) => p.length > 0).map((p) => p.endsWith("/") ? p.slice(0, -1) : p);
2460
+ const ancestorDirs = /* @__PURE__ */ new Set();
2461
+ for (const pat of patterns) {
2462
+ const parts = pat.split("/");
2463
+ for (let i = 1; i < parts.length; i++) {
2464
+ ancestorDirs.add(parts.slice(0, i).join("/"));
2465
+ }
2466
+ }
2467
+ const matcher = { mode, patterns, ancestorDirs };
2468
+ this.matcherCache.set(cfg, matcher);
2469
+ return matcher;
2470
+ }
2414
2471
  };
2415
2472
 
2416
2473
  // src/services/worktree-metadata.service.ts
2417
2474
  import * as fs2 from "fs/promises";
2418
- import * as path3 from "path";
2475
+ import * as path4 from "path";
2419
2476
  import simpleGit2 from "simple-git";
2420
2477
  var WorktreeMetadataService = class {
2421
2478
  logger;
@@ -2428,7 +2485,7 @@ var WorktreeMetadataService = class {
2428
2485
  * For example: /worktrees/fix/test-branch -> test-branch (not fix/test-branch)
2429
2486
  */
2430
2487
  getWorktreeDirectoryName(worktreePath) {
2431
- return path3.basename(worktreePath);
2488
+ return path4.basename(worktreePath);
2432
2489
  }
2433
2490
  async getMetadataPath(bareRepoPath, worktreeName) {
2434
2491
  if (worktreeName.includes("/") || worktreeName.includes("\\")) {
@@ -2436,7 +2493,7 @@ var WorktreeMetadataService = class {
2436
2493
  `getMetadataPath requires a filesystem-safe worktree directory name, got '${worktreeName}'. Use getMetadataPathFromWorktreePath when starting from a raw branch name.`
2437
2494
  );
2438
2495
  }
2439
- return path3.join(
2496
+ return path4.join(
2440
2497
  bareRepoPath,
2441
2498
  METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
2442
2499
  worktreeName,
@@ -2449,7 +2506,7 @@ var WorktreeMetadataService = class {
2449
2506
  }
2450
2507
  async saveMetadata(bareRepoPath, worktreeName, metadata) {
2451
2508
  const metadataPath = await this.getMetadataPath(bareRepoPath, worktreeName);
2452
- await fs2.mkdir(path3.dirname(metadataPath), { recursive: true });
2509
+ await fs2.mkdir(path4.dirname(metadataPath), { recursive: true });
2453
2510
  const tmpPath = `${metadataPath}.${process.pid}.${Date.now()}.tmp`;
2454
2511
  let renamed = false;
2455
2512
  try {
@@ -2642,7 +2699,7 @@ var WorktreeMetadataService = class {
2642
2699
 
2643
2700
  // src/services/worktree-status.service.ts
2644
2701
  import * as fs3 from "fs/promises";
2645
- import * as path4 from "path";
2702
+ import * as path5 from "path";
2646
2703
  import simpleGit3 from "simple-git";
2647
2704
  var OPERATION_FILES = [
2648
2705
  { file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
@@ -2845,7 +2902,7 @@ var WorktreeStatusService = class {
2845
2902
  async detectOperationFile(gitDir) {
2846
2903
  const results = await Promise.all(
2847
2904
  OPERATION_FILES.map(
2848
- ({ file }) => fs3.access(path4.join(gitDir, file)).then(
2905
+ ({ file }) => fs3.access(path5.join(gitDir, file)).then(
2849
2906
  () => true,
2850
2907
  () => false
2851
2908
  )
@@ -2966,14 +3023,14 @@ var WorktreeStatusService = class {
2966
3023
  }
2967
3024
  }
2968
3025
  async resolveGitDir(worktreePath) {
2969
- const gitPath = path4.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
3026
+ const gitPath = path5.join(worktreePath, PATH_CONSTANTS.GIT_DIR);
2970
3027
  try {
2971
3028
  const stat3 = await fs3.stat(gitPath);
2972
3029
  if (stat3.isFile()) {
2973
3030
  const content = await fs3.readFile(gitPath, "utf-8");
2974
3031
  const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
2975
3032
  if (gitdirMatch) {
2976
- return path4.resolve(worktreePath, gitdirMatch[1].trim());
3033
+ return path5.resolve(worktreePath, gitdirMatch[1].trim());
2977
3034
  }
2978
3035
  throw new GitOperationError("resolve-git-dir", `Failed to parse gitdir from .git file at ${gitPath}`);
2979
3036
  }
@@ -2987,7 +3044,7 @@ var WorktreeStatusService = class {
2987
3044
  }
2988
3045
  }
2989
3046
  createGitInstance(worktreePath) {
2990
- const key = `${path4.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
3047
+ const key = `${path5.resolve(worktreePath)}::${this.config.skipLfs ? "1" : "0"}`;
2991
3048
  let git = this.gitInstances.get(key);
2992
3049
  if (!git) {
2993
3050
  git = this.config.skipLfs ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
@@ -3010,7 +3067,7 @@ var GitService = class {
3010
3067
  this.config = config;
3011
3068
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
3012
3069
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
3013
- this.mainWorktreePath = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
3070
+ this.mainWorktreePath = path6.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
3014
3071
  this.metadataService = new WorktreeMetadataService(this.logger);
3015
3072
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
3016
3073
  this.sparseCheckoutService = new SparseCheckoutService(this.logger);
@@ -3038,7 +3095,7 @@ var GitService = class {
3038
3095
  return this.config.cloneTimeoutMs ?? DEFAULT_CONFIG.CLONE_TIMEOUT_MS;
3039
3096
  }
3040
3097
  getCachedGit(dirPath, useLfsSkip = false) {
3041
- const key = `${path5.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3098
+ const key = `${path6.resolve(dirPath)}::${useLfsSkip ? "1" : "0"}`;
3042
3099
  let git = this.gitInstances.get(key);
3043
3100
  if (!git) {
3044
3101
  const block = this.getFetchTimeoutMs();
@@ -3055,10 +3112,10 @@ var GitService = class {
3055
3112
  async initialize() {
3056
3113
  const { repoUrl } = this.config;
3057
3114
  try {
3058
- await fs4.access(path5.join(this.bareRepoPath, "HEAD"));
3115
+ await fs4.access(path6.join(this.bareRepoPath, "HEAD"));
3059
3116
  } catch {
3060
3117
  this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
3061
- await fs4.mkdir(path5.dirname(this.bareRepoPath), { recursive: true });
3118
+ await fs4.mkdir(path6.dirname(this.bareRepoPath), { recursive: true });
3062
3119
  const cloneBlock = this.getCloneTimeoutMs();
3063
3120
  const cloneBase = cloneBlock > 0 ? simpleGit4({ timeout: { block: cloneBlock } }) : simpleGit4();
3064
3121
  const cloneGit = this.isLfsSkipEnabled() ? cloneBase.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : cloneBase;
@@ -3078,17 +3135,17 @@ var GitService = class {
3078
3135
  this.logger.info("Fetching remote branches...");
3079
3136
  await bareGit.fetch(["--all"]);
3080
3137
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
3081
- this.mainWorktreePath = path5.join(this.config.worktreeDir, this.defaultBranch);
3138
+ this.mainWorktreePath = path6.join(this.config.worktreeDir, this.defaultBranch);
3082
3139
  let needsMainWorktree = true;
3083
3140
  try {
3084
3141
  const worktrees = await this.getWorktreesFromBare(bareGit);
3085
- needsMainWorktree = !worktrees.some((w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath));
3142
+ needsMainWorktree = !worktrees.some((w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath));
3086
3143
  } catch {
3087
3144
  }
3088
3145
  if (needsMainWorktree) {
3089
3146
  this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
3090
3147
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
3091
- const absoluteWorktreePath = path5.resolve(this.mainWorktreePath);
3148
+ const absoluteWorktreePath = path6.resolve(this.mainWorktreePath);
3092
3149
  const branches = await bareGit.branch();
3093
3150
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
3094
3151
  const useNoCheckoutMain = !!this.config.sparseCheckout;
@@ -3124,7 +3181,7 @@ var GitService = class {
3124
3181
  }
3125
3182
  const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
3126
3183
  const mainWorktreeRegistered = updatedWorktrees.some(
3127
- (w) => path5.resolve(w.path) === path5.resolve(this.mainWorktreePath)
3184
+ (w) => path6.resolve(w.path) === path6.resolve(this.mainWorktreePath)
3128
3185
  );
3129
3186
  if (!mainWorktreeRegistered) {
3130
3187
  if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
@@ -3207,7 +3264,7 @@ var GitService = class {
3207
3264
  const existence = await Promise.all(
3208
3265
  lfsFileList.map(async (f) => {
3209
3266
  try {
3210
- await fs4.access(path5.join(worktreePath, f));
3267
+ await fs4.access(path6.join(worktreePath, f));
3211
3268
  return f;
3212
3269
  } catch {
3213
3270
  return null;
@@ -3235,7 +3292,7 @@ var GitService = class {
3235
3292
  let allDownloaded = true;
3236
3293
  const notDownloaded = [];
3237
3294
  for (const file of samplesToCheck) {
3238
- const filePath = path5.join(worktreePath, file);
3295
+ const filePath = path6.join(worktreePath, file);
3239
3296
  try {
3240
3297
  const handle = await fs4.open(filePath, "r");
3241
3298
  try {
@@ -3324,12 +3381,12 @@ var GitService = class {
3324
3381
  }
3325
3382
  async addWorktree(branchName, worktreePath) {
3326
3383
  const bareGit = this.getCachedGit(this.bareRepoPath, this.isLfsSkipEnabled());
3327
- const absoluteWorktreePath = path5.resolve(worktreePath);
3328
- await fs4.mkdir(path5.dirname(absoluteWorktreePath), { recursive: true });
3384
+ const absoluteWorktreePath = path6.resolve(worktreePath);
3385
+ await fs4.mkdir(path6.dirname(absoluteWorktreePath), { recursive: true });
3329
3386
  try {
3330
3387
  await fs4.access(absoluteWorktreePath);
3331
3388
  const worktrees = await this.getWorktreesFromBare(bareGit);
3332
- const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3389
+ const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
3333
3390
  if (isValidWorktree) {
3334
3391
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3335
3392
  return;
@@ -3374,7 +3431,7 @@ var GitService = class {
3374
3431
  }
3375
3432
  if (errorMessage.includes("already registered worktree")) {
3376
3433
  const worktrees = await this.getWorktreesFromBare(bareGit);
3377
- const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3434
+ const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
3378
3435
  if (existingWorktree && !existingWorktree.isPrunable) {
3379
3436
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
3380
3437
  return;
@@ -3420,7 +3477,7 @@ var GitService = class {
3420
3477
  try {
3421
3478
  await fs4.access(absoluteWorktreePath);
3422
3479
  const worktrees = await this.getWorktreesFromBare(bareGit);
3423
- const isValidWorktree = worktrees.some((w) => path5.resolve(w.path) === absoluteWorktreePath);
3480
+ const isValidWorktree = worktrees.some((w) => path6.resolve(w.path) === absoluteWorktreePath);
3424
3481
  if (isValidWorktree) {
3425
3482
  this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
3426
3483
  return;
@@ -3450,7 +3507,7 @@ var GitService = class {
3450
3507
  const fallbackErrorMessage = getErrorMessage(fallbackError);
3451
3508
  if (fallbackErrorMessage.includes("already registered worktree")) {
3452
3509
  const worktrees = await this.getWorktreesFromBare(bareGit);
3453
- const existingWorktree = worktrees.find((w) => path5.resolve(w.path) === absoluteWorktreePath);
3510
+ const existingWorktree = worktrees.find((w) => path6.resolve(w.path) === absoluteWorktreePath);
3454
3511
  if (existingWorktree && !existingWorktree.isPrunable) {
3455
3512
  this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
3456
3513
  return;
@@ -3673,6 +3730,23 @@ var GitService = class {
3673
3730
  return false;
3674
3731
  }
3675
3732
  }
3733
+ async getChangedPathsInRange(worktreePath, fromRef, toRef) {
3734
+ const worktreeGit = this.getCachedGit(worktreePath);
3735
+ try {
3736
+ const out = await worktreeGit.raw([
3737
+ "-c",
3738
+ "core.quotePath=false",
3739
+ "diff",
3740
+ "--name-only",
3741
+ "--no-renames",
3742
+ `${fromRef}..${toRef}`
3743
+ ]);
3744
+ return out.split("\n").map((l) => l.replace(/\r$/, "")).filter((l) => l.length > 0);
3745
+ } catch (error) {
3746
+ this.logger.warn(`Failed to compute diff ${fromRef}..${toRef} in ${worktreePath}: ${getErrorMessage(error)}`);
3747
+ return null;
3748
+ }
3749
+ }
3676
3750
  async compareTreeContent(worktreePath, branch) {
3677
3751
  const worktreeGit = this.getCachedGit(worktreePath);
3678
3752
  try {
@@ -3757,7 +3831,7 @@ var GitService = class {
3757
3831
  // src/services/path-resolution.service.ts
3758
3832
  import { createHash } from "crypto";
3759
3833
  import * as fs5 from "fs";
3760
- import * as path6 from "path";
3834
+ import * as path7 from "path";
3761
3835
  var BRANCH_STEM_MAX = 80;
3762
3836
  var BRANCH_HASH_LEN = 8;
3763
3837
  var PathResolutionService = class {
@@ -3767,22 +3841,22 @@ var PathResolutionService = class {
3767
3841
  return `${stem}-${hash}`;
3768
3842
  }
3769
3843
  getBranchWorktreePath(worktreeDir, branchName) {
3770
- return path6.join(worktreeDir, this.sanitizeBranchName(branchName));
3844
+ return path7.join(worktreeDir, this.sanitizeBranchName(branchName));
3771
3845
  }
3772
3846
  resolveRealPath(inputPath) {
3773
- const absolute = path6.resolve(inputPath);
3847
+ const absolute = path7.resolve(inputPath);
3774
3848
  const missing = [];
3775
3849
  let current = absolute;
3776
3850
  while (!fs5.existsSync(current)) {
3777
- const parent = path6.dirname(current);
3851
+ const parent = path7.dirname(current);
3778
3852
  if (parent === current) {
3779
3853
  return absolute;
3780
3854
  }
3781
- missing.unshift(path6.basename(current));
3855
+ missing.unshift(path7.basename(current));
3782
3856
  current = parent;
3783
3857
  }
3784
3858
  try {
3785
- return path6.join(fs5.realpathSync(current), ...missing);
3859
+ return path7.join(fs5.realpathSync(current), ...missing);
3786
3860
  } catch {
3787
3861
  return absolute;
3788
3862
  }
@@ -3792,7 +3866,7 @@ var PathResolutionService = class {
3792
3866
  const a = fold(resolved);
3793
3867
  const b = fold(resolvedBase);
3794
3868
  if (a === b) return true;
3795
- return a.length > b.length && a.charAt(b.length) === path6.sep && a.startsWith(b);
3869
+ return a.length > b.length && a.charAt(b.length) === path7.sep && a.startsWith(b);
3796
3870
  }
3797
3871
  normalizeWorktreePath(worktreePath, worktreeBaseDir) {
3798
3872
  const resolved = this.resolveRealPath(worktreePath);
@@ -3800,7 +3874,7 @@ var PathResolutionService = class {
3800
3874
  if (!this.isResolvedPathInsideBase(resolved, resolvedBase)) {
3801
3875
  throw new Error(`Worktree path '${worktreePath}' is outside base directory '${worktreeBaseDir}'`);
3802
3876
  }
3803
- return path6.relative(resolvedBase, resolved);
3877
+ return path7.relative(resolvedBase, resolved);
3804
3878
  }
3805
3879
  isPathInsideBaseDir(targetPath, baseDir) {
3806
3880
  const resolved = this.resolveRealPath(targetPath);
@@ -3906,7 +3980,7 @@ var WorktreeSyncService = class {
3906
3980
  };
3907
3981
  }
3908
3982
  const barePath = this.gitService.getBareRepoPath();
3909
- const lockTarget = path7.join(barePath, "HEAD");
3983
+ const lockTarget = path8.join(barePath, "HEAD");
3910
3984
  try {
3911
3985
  await fs6.access(lockTarget);
3912
3986
  } catch {
@@ -4104,12 +4178,12 @@ var WorktreeSyncService = class {
4104
4178
  }
4105
4179
  const reservedPaths = /* @__PURE__ */ new Map();
4106
4180
  for (const w of worktrees) {
4107
- reservedPaths.set(path7.resolve(w.path), w.branch);
4181
+ reservedPaths.set(path8.resolve(w.path), w.branch);
4108
4182
  }
4109
4183
  const plan = [];
4110
4184
  for (const branchName of newBranches) {
4111
4185
  const worktreePath = this.pathResolution.getBranchWorktreePath(this.config.worktreeDir, branchName);
4112
- const resolved = path7.resolve(worktreePath);
4186
+ const resolved = path8.resolve(worktreePath);
4113
4187
  const conflict = reservedPaths.get(resolved);
4114
4188
  if (conflict && conflict !== branchName) {
4115
4189
  this.logger.error(
@@ -4314,12 +4388,12 @@ var WorktreeSyncService = class {
4314
4388
  }
4315
4389
  async updateExistingWorktrees(worktrees, remoteBranches) {
4316
4390
  this.logger.info("Step 4: Checking for worktrees that need updates...");
4317
- const divergedDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4391
+ const divergedDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4318
4392
  try {
4319
4393
  const diverged = await fs6.readdir(divergedDir);
4320
4394
  if (diverged.length > 0) {
4321
4395
  this.logger.info(
4322
- `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path7.relative(process.cwd(), divergedDir)}`
4396
+ `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path8.relative(process.cwd(), divergedDir)}`
4323
4397
  );
4324
4398
  }
4325
4399
  } catch {
@@ -4349,7 +4423,23 @@ var WorktreeSyncService = class {
4349
4423
  return { action: "diverged", worktree };
4350
4424
  }
4351
4425
  const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
4352
- return isBehind ? { action: "update", worktree } : null;
4426
+ if (!isBehind) return null;
4427
+ const sparseCfg = this.config.sparseCheckout;
4428
+ if (sparseCfg && sparseCfg.skipUpdateWhenOutsideSparse !== false) {
4429
+ const sparseService = this.gitService.getSparseCheckoutService();
4430
+ if (sparseService.resolveMode(sparseCfg) === "cone") {
4431
+ const diff = await this.gitService.getChangedPathsInRange(
4432
+ worktree.path,
4433
+ "HEAD",
4434
+ `origin/${worktree.branch}`
4435
+ );
4436
+ if (diff !== null && !sparseService.pathsTouchSparse(diff, sparseCfg)) {
4437
+ this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - upstream changes outside sparse paths`);
4438
+ return null;
4439
+ }
4440
+ }
4441
+ }
4442
+ return { action: "update", worktree };
4353
4443
  })
4354
4444
  )
4355
4445
  );
@@ -4427,13 +4517,13 @@ var WorktreeSyncService = class {
4427
4517
  }
4428
4518
  async cleanupOrphanedDirectories(worktrees) {
4429
4519
  try {
4430
- const worktreeRelativePaths = worktrees.map((w) => path7.relative(this.config.worktreeDir, w.path));
4520
+ const worktreeRelativePaths = worktrees.map((w) => path8.relative(this.config.worktreeDir, w.path));
4431
4521
  const allDirs = await fs6.readdir(this.config.worktreeDir);
4432
4522
  const regularDirs = allDirs.filter((dir) => !dir.startsWith("."));
4433
4523
  const orphanedDirs = [];
4434
4524
  for (const dir of regularDirs) {
4435
4525
  const isPartOfWorktree = worktreeRelativePaths.some((worktreePath) => {
4436
- return worktreePath === dir || worktreePath.startsWith(dir + path7.sep);
4526
+ return worktreePath === dir || worktreePath.startsWith(dir + path8.sep);
4437
4527
  });
4438
4528
  if (!isPartOfWorktree) {
4439
4529
  orphanedDirs.push(dir);
@@ -4442,7 +4532,7 @@ var WorktreeSyncService = class {
4442
4532
  if (orphanedDirs.length > 0) {
4443
4533
  this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
4444
4534
  for (const dir of orphanedDirs) {
4445
- const dirPath = path7.join(this.config.worktreeDir, dir);
4535
+ const dirPath = path8.join(this.config.worktreeDir, dir);
4446
4536
  try {
4447
4537
  const stat3 = await fs6.stat(dirPath);
4448
4538
  if (stat3.isDirectory()) {
@@ -4476,7 +4566,7 @@ var WorktreeSyncService = class {
4476
4566
  } else {
4477
4567
  this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
4478
4568
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
4479
- const relativePath = path7.relative(process.cwd(), divergedPath);
4569
+ const relativePath = path8.relative(process.cwd(), divergedPath);
4480
4570
  this.logger.info(` Moved to: ${relativePath}`);
4481
4571
  this.logger.info(` Your local changes are preserved. To review:`);
4482
4572
  this.logger.info(` cd ${relativePath}`);
@@ -4500,12 +4590,12 @@ var WorktreeSyncService = class {
4500
4590
  }
4501
4591
  }
4502
4592
  async divergeWorktree(worktreePath, branchName) {
4503
- const divergedBaseDir = path7.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4593
+ const divergedBaseDir = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
4504
4594
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
4505
4595
  const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
4506
4596
  const safeBranchName = this.pathResolution.sanitizeBranchName(branchName);
4507
4597
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
4508
- const divergedPath = path7.join(divergedBaseDir, divergedName);
4598
+ const divergedPath = path8.join(divergedBaseDir, divergedName);
4509
4599
  await fs6.mkdir(divergedBaseDir, { recursive: true });
4510
4600
  try {
4511
4601
  await fs6.rename(worktreePath, divergedPath);
@@ -4532,7 +4622,7 @@ var WorktreeSyncService = class {
4532
4622
  Original worktree location: ${worktreePath}`
4533
4623
  };
4534
4624
  await fs6.writeFile(
4535
- path7.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4625
+ path8.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
4536
4626
  JSON.stringify(metadata, null, 2)
4537
4627
  );
4538
4628
  return divergedPath;
@@ -4541,7 +4631,7 @@ var WorktreeSyncService = class {
4541
4631
 
4542
4632
  // src/services/file-copy.service.ts
4543
4633
  import * as fs7 from "fs/promises";
4544
- import * as path8 from "path";
4634
+ import * as path9 from "path";
4545
4635
  import { glob } from "glob";
4546
4636
  var DEFAULT_IGNORE_PATTERNS = [
4547
4637
  "**/node_modules/**",
@@ -4568,8 +4658,8 @@ var FileCopyService = class {
4568
4658
  }
4569
4659
  const filesToCopy = await this.expandPatterns(sourceDir, patterns);
4570
4660
  for (const relativePath of filesToCopy) {
4571
- const sourcePath = path8.join(sourceDir, relativePath);
4572
- const destPath = path8.join(destDir, relativePath);
4661
+ const sourcePath = path9.join(sourceDir, relativePath);
4662
+ const destPath = path9.join(destDir, relativePath);
4573
4663
  try {
4574
4664
  const copied = await this.copyFile(sourcePath, destPath);
4575
4665
  if (copied) {
@@ -4610,7 +4700,7 @@ var FileCopyService = class {
4610
4700
  return false;
4611
4701
  } catch {
4612
4702
  }
4613
- const destDir = path8.dirname(destPath);
4703
+ const destDir = path9.dirname(destPath);
4614
4704
  await fs7.mkdir(destDir, { recursive: true });
4615
4705
  await fs7.copyFile(sourcePath, destPath);
4616
4706
  return true;
@@ -4828,6 +4918,8 @@ var AppEventEmitter = class {
4828
4918
 
4829
4919
  // src/services/InteractiveUIService.tsx
4830
4920
  import * as fs8 from "fs/promises";
4921
+ var WAIT_SYNC_FAST_TIMEOUT_MS = 2e3;
4922
+ var WAIT_SYNC_DEFAULT_TIMEOUT_MS = 3e4;
4831
4923
  var InteractiveUIService = class {
4832
4924
  app = null;
4833
4925
  syncServices;
@@ -4935,6 +5027,9 @@ var InteractiveUIService = class {
4935
5027
  }
4936
5028
  this.cronJobs = [];
4937
5029
  }
5030
+ registerCronJob(job) {
5031
+ this.cronJobs.push(job);
5032
+ }
4938
5033
  renderUI() {
4939
5034
  if (this.app) {
4940
5035
  this.app.unmount();
@@ -4958,8 +5053,8 @@ var InteractiveUIService = class {
4958
5053
  getWorktreeStatusForRepo: (index) => this.getWorktreeStatusForRepo(index),
4959
5054
  getDivergedDirectoriesForRepo: (index) => this.getDivergedDirectoriesForRepo(index),
4960
5055
  deleteDivergedDirectory: (repoIndex, name) => this.deleteDivergedDirectory(repoIndex, name),
4961
- openEditorInWorktree: (path13) => this.openEditorInWorktree(path13),
4962
- openTerminalInWorktree: (repoIndex, path13, branchName) => this.openTerminalInWorktree(repoIndex, path13, branchName),
5056
+ openEditorInWorktree: (path14) => this.openEditorInWorktree(path14),
5057
+ openTerminalInWorktree: (repoIndex, path14, branchName) => this.openTerminalInWorktree(repoIndex, path14, branchName),
4963
5058
  copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
4964
5059
  createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName),
4965
5060
  executeOnBranchCreatedHooks: (repoIndex, context) => this.executeOnBranchCreatedHooks(repoIndex, context)
@@ -5049,18 +5144,17 @@ var InteractiveUIService = class {
5049
5144
  await this.destroy();
5050
5145
  process.exit(0);
5051
5146
  }
5052
- async waitForInProgressSyncs() {
5147
+ async waitForInProgressSyncs(timeoutMs = WAIT_SYNC_DEFAULT_TIMEOUT_MS) {
5053
5148
  const inProgressServices = this.syncServices.filter((s) => s.isSyncInProgress());
5054
5149
  if (inProgressServices.length === 0) {
5055
5150
  return;
5056
5151
  }
5057
5152
  this.addLog(`Waiting for ${inProgressServices.length} in-progress sync(s) to finish...`, "info");
5058
5153
  const syncChecks = inProgressServices.map(async (service) => {
5059
- const timeout = 3e4;
5060
5154
  const checkInterval = 500;
5061
5155
  const startTime = Date.now();
5062
5156
  while (service.isSyncInProgress()) {
5063
- if (Date.now() - startTime > timeout) {
5157
+ if (Date.now() - startTime > timeoutMs) {
5064
5158
  throw new Error("Timeout waiting for sync operations to complete");
5065
5159
  }
5066
5160
  await new Promise((resolve9) => setTimeout(resolve9, checkInterval));
@@ -5070,7 +5164,7 @@ var InteractiveUIService = class {
5070
5164
  await Promise.all(syncChecks);
5071
5165
  } catch {
5072
5166
  this.addLog(
5073
- "Warning: Timeout waiting for sync operations to complete after 30s. Proceeding with potential data loss risk.",
5167
+ `Warning: Timeout waiting for sync operations to complete after ${formatDuration2(timeoutMs)}. Proceeding with potential data loss risk.`,
5074
5168
  "warn"
5075
5169
  );
5076
5170
  }
@@ -5196,7 +5290,7 @@ var InteractiveUIService = class {
5196
5290
  }
5197
5291
  const service = this.syncServices[repoIndex];
5198
5292
  const worktreeDir = service.config.worktreeDir;
5199
- const divergedDir = path9.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5293
+ const divergedDir = path10.join(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5200
5294
  let dirEntries;
5201
5295
  try {
5202
5296
  dirEntries = await fs8.readdir(divergedDir, { withFileTypes: true, encoding: "utf-8" });
@@ -5206,8 +5300,8 @@ var InteractiveUIService = class {
5206
5300
  const subdirs = dirEntries.filter((e) => e.isDirectory());
5207
5301
  const results = await Promise.allSettled(
5208
5302
  subdirs.map(async (entry) => {
5209
- const fullPath = path9.join(divergedDir, entry.name);
5210
- const infoFilePath = path9.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
5303
+ const fullPath = path10.join(divergedDir, entry.name);
5304
+ const infoFilePath = path10.join(fullPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE);
5211
5305
  let originalBranch = entry.name;
5212
5306
  let divergedAt = "";
5213
5307
  try {
@@ -5242,11 +5336,11 @@ var InteractiveUIService = class {
5242
5336
  }
5243
5337
  const service = this.syncServices[repoIndex];
5244
5338
  const worktreeDir = service.config.worktreeDir;
5245
- const divergedBase = path9.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5339
+ const divergedBase = path10.resolve(worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
5246
5340
  if (!name || name === "." || name === ".." || name.includes("/") || name.includes("\\")) {
5247
5341
  throw new Error(`Invalid diverged directory name: "${name}"`);
5248
5342
  }
5249
- const targetPath = path9.join(divergedBase, name);
5343
+ const targetPath = path10.join(divergedBase, name);
5250
5344
  if (!this.pathResolution.isPathInsideBaseDir(targetPath, divergedBase)) {
5251
5345
  throw new Error(`Path traversal rejected: "${name}" resolves outside the diverged directory`);
5252
5346
  }
@@ -5485,11 +5579,11 @@ var InteractiveUIService = class {
5485
5579
  this.addLog(`Failed to copy files to new branch: ${error}`, "error");
5486
5580
  }
5487
5581
  }
5488
- async destroy() {
5582
+ async destroy(fast = false) {
5489
5583
  this.isDestroyed = true;
5490
5584
  this.cancelCronJobs();
5491
5585
  try {
5492
- await this.waitForInProgressSyncs();
5586
+ await this.waitForInProgressSyncs(fast ? WAIT_SYNC_FAST_TIMEOUT_MS : WAIT_SYNC_DEFAULT_TIMEOUT_MS);
5493
5587
  } catch {
5494
5588
  }
5495
5589
  this.hookExecutionService.cleanup();
@@ -5635,7 +5729,7 @@ function reconstructCliCommand(config) {
5635
5729
 
5636
5730
  // src/utils/config-generator.ts
5637
5731
  import * as fs9 from "fs/promises";
5638
- import * as path10 from "path";
5732
+ import * as path11 from "path";
5639
5733
  function serializeToESM(obj, indent = 0) {
5640
5734
  const spaces = " ".repeat(indent);
5641
5735
  const innerSpaces = " ".repeat(indent + 2);
@@ -5665,9 +5759,9 @@ ${spaces}}`;
5665
5759
  return String(obj);
5666
5760
  }
5667
5761
  async function generateConfigFile(config, configPath) {
5668
- const configDir = path10.dirname(configPath);
5762
+ const configDir = path11.dirname(configPath);
5669
5763
  await fs9.mkdir(configDir, { recursive: true });
5670
- const worktreeDirRelative = path10.relative(configDir, config.worktreeDir);
5764
+ const worktreeDirRelative = path11.relative(configDir, config.worktreeDir);
5671
5765
  const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
5672
5766
  const repoName = extractRepoNameFromUrl(config.repoUrl);
5673
5767
  const repository = {
@@ -5676,7 +5770,7 @@ async function generateConfigFile(config, configPath) {
5676
5770
  worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
5677
5771
  };
5678
5772
  if (config.bareRepoDir) {
5679
- const bareRepoDirRelative = path10.relative(configDir, config.bareRepoDir);
5773
+ const bareRepoDirRelative = path11.relative(configDir, config.bareRepoDir);
5680
5774
  const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
5681
5775
  repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
5682
5776
  }
@@ -5697,11 +5791,11 @@ export default ${serializeToESM(configObject)};
5697
5791
  await fs9.writeFile(configPath, configContent, "utf-8");
5698
5792
  }
5699
5793
  function getDefaultConfigPath() {
5700
- return path10.join(process.cwd(), "sync-worktrees.config.js");
5794
+ return path11.join(process.cwd(), "sync-worktrees.config.js");
5701
5795
  }
5702
5796
  async function findConfigInCwd(cwd = process.cwd()) {
5703
5797
  for (const name of CONFIG_FILE_NAMES) {
5704
- const full = path10.join(cwd, name);
5798
+ const full = path11.join(cwd, name);
5705
5799
  try {
5706
5800
  await fs9.access(full);
5707
5801
  return full;
@@ -5712,7 +5806,7 @@ async function findConfigInCwd(cwd = process.cwd()) {
5712
5806
  }
5713
5807
 
5714
5808
  // src/utils/interactive.ts
5715
- import * as path11 from "path";
5809
+ import * as path12 from "path";
5716
5810
  import { confirm, input, select } from "@inquirer/prompts";
5717
5811
  async function promptForConfig(partialConfig) {
5718
5812
  console.log("\u{1F527} Welcome to sync-worktrees interactive setup!\n");
@@ -5752,8 +5846,8 @@ async function promptForConfig(partialConfig) {
5752
5846
  if (!worktreeDir.trim() && defaultWorktreeDir) {
5753
5847
  worktreeDir = defaultWorktreeDir;
5754
5848
  }
5755
- if (!path11.isAbsolute(worktreeDir)) {
5756
- worktreeDir = path11.resolve(worktreeDir);
5849
+ if (!path12.isAbsolute(worktreeDir)) {
5850
+ worktreeDir = path12.resolve(worktreeDir);
5757
5851
  }
5758
5852
  }
5759
5853
  let bareRepoDir = partialConfig.bareRepoDir;
@@ -5772,8 +5866,8 @@ async function promptForConfig(partialConfig) {
5772
5866
  return true;
5773
5867
  }
5774
5868
  });
5775
- if (!path11.isAbsolute(bareRepoDir)) {
5776
- bareRepoDir = path11.resolve(bareRepoDir);
5869
+ if (!path12.isAbsolute(bareRepoDir)) {
5870
+ bareRepoDir = path12.resolve(bareRepoDir);
5777
5871
  }
5778
5872
  }
5779
5873
  let runOnce = partialConfig.runOnce;
@@ -5845,8 +5939,8 @@ async function promptForConfig(partialConfig) {
5845
5939
  return true;
5846
5940
  }
5847
5941
  });
5848
- if (!path11.isAbsolute(configPath)) {
5849
- configPath = path11.resolve(configPath);
5942
+ if (!path12.isAbsolute(configPath)) {
5943
+ configPath = path12.resolve(configPath);
5850
5944
  }
5851
5945
  try {
5852
5946
  await generateConfigFile(finalConfig, configPath);
@@ -5864,26 +5958,55 @@ async function promptForConfig(partialConfig) {
5864
5958
  return { config: finalConfig, savedConfigPath };
5865
5959
  }
5866
5960
 
5867
- // src/index.ts
5868
- var cleanupFns = [];
5869
- function setupSignalHandlers() {
5870
- let shuttingDown = false;
5871
- const handler = async (signal) => {
5872
- if (shuttingDown) return;
5873
- shuttingDown = true;
5874
- console.log(`
5875
- Received ${signal}, shutting down gracefully...`);
5876
- for (const fn of cleanupFns) {
5877
- try {
5878
- await fn();
5879
- } catch {
5880
- }
5961
+ // src/utils/signal-handlers.ts
5962
+ var DEFAULT_FORCE_EXIT_MS = 3e3;
5963
+ function setupSignalHandlers(options = {}) {
5964
+ const forceExitMs = options.forceExitMs ?? DEFAULT_FORCE_EXIT_MS;
5965
+ const log = options.log ?? ((msg) => console.log(msg));
5966
+ const exit = options.exit ?? ((code) => process.exit(code));
5967
+ const target = options.process ?? process;
5968
+ const cleanupFns = [];
5969
+ let signalCount = 0;
5970
+ const handler = (signal) => {
5971
+ signalCount += 1;
5972
+ if (signalCount >= 2) {
5973
+ log(`
5974
+ Received second ${signal}, forcing exit.`);
5975
+ exit(130);
5976
+ return;
5977
+ }
5978
+ log(`
5979
+ Received ${signal}, shutting down (Ctrl+C again to force exit)...`);
5980
+ const watchdog = setTimeout(() => {
5981
+ log(`
5982
+ Shutdown took longer than ${forceExitMs}ms, forcing exit.`);
5983
+ exit(130);
5984
+ }, forceExitMs);
5985
+ if (typeof watchdog.unref === "function") {
5986
+ watchdog.unref();
5987
+ }
5988
+ void Promise.allSettled(cleanupFns.map((fn) => Promise.resolve().then(() => fn(true)))).then(() => {
5989
+ clearTimeout(watchdog);
5990
+ exit(0);
5991
+ });
5992
+ };
5993
+ const sigintListener = () => handler("SIGINT");
5994
+ const sigtermListener = () => handler("SIGTERM");
5995
+ target.on("SIGINT", sigintListener);
5996
+ target.on("SIGTERM", sigtermListener);
5997
+ return {
5998
+ register: (fn) => {
5999
+ cleanupFns.push(fn);
6000
+ },
6001
+ dispose: () => {
6002
+ target.removeListener("SIGINT", sigintListener);
6003
+ target.removeListener("SIGTERM", sigtermListener);
5881
6004
  }
5882
- process.exit(0);
5883
6005
  };
5884
- process.on("SIGINT", () => void handler("SIGINT"));
5885
- process.on("SIGTERM", () => void handler("SIGTERM"));
5886
6006
  }
6007
+
6008
+ // src/index.ts
6009
+ var signalHandle = setupSignalHandlers();
5887
6010
  async function runSingleRepository(config) {
5888
6011
  const logger = Logger.createDefault(void 0, config.debug);
5889
6012
  logger.info("\n\u{1F4CB} CLI Command (for future reference):");
@@ -5900,11 +6023,11 @@ async function runSingleRepository(config) {
5900
6023
  await syncService.sync();
5901
6024
  } else {
5902
6025
  const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
5903
- cleanupFns.push(() => uiService.destroy());
6026
+ signalHandle.register((fast) => uiService.destroy(fast));
5904
6027
  await syncService.sync();
5905
6028
  uiService.updateLastSyncTime();
5906
6029
  void uiService.calculateAndUpdateDiskSpace();
5907
- cron3.schedule(config.cronSchedule, async () => {
6030
+ const job = cron3.schedule(config.cronSchedule, async () => {
5908
6031
  try {
5909
6032
  uiService.setStatus("syncing");
5910
6033
  await syncService.sync();
@@ -5915,6 +6038,7 @@ async function runSingleRepository(config) {
5915
6038
  uiService.setStatus("idle");
5916
6039
  }
5917
6040
  });
6041
+ uiService.registerCronJob(job);
5918
6042
  }
5919
6043
  } catch (error) {
5920
6044
  logger.error("\u274C Fatal Error during initialization:", error);
@@ -5981,7 +6105,7 @@ async function runMultipleRepositories(repositories, runOnce, configPath, maxPar
5981
6105
  const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
5982
6106
  const allServices = Array.from(services.values());
5983
6107
  const uiService = new InteractiveUIService(allServices, configPath, displaySchedule, maxParallel, reloadOptions);
5984
- cleanupFns.push(() => uiService.destroy());
6108
+ signalHandle.register((fast) => uiService.destroy(fast));
5985
6109
  void uiService.calculateAndUpdateDiskSpace();
5986
6110
  uiService.setupCronJobs();
5987
6111
  uiService.addLog(`\u{1F4CB} ${repositories.length} repositories configured`);
@@ -6076,13 +6200,12 @@ async function runInteractive(partial, options) {
6076
6200
  await runSingleRepository(config);
6077
6201
  }
6078
6202
  async function main() {
6079
- setupSignalHandlers();
6080
6203
  const options = parseArguments();
6081
6204
  if (!options.config && !options.repoUrl && !options.worktreeDir) {
6082
6205
  const discovered = await findConfigInCwd();
6083
6206
  if (discovered) {
6084
6207
  options.config = discovered;
6085
- console.log(`\u{1F4C4} Using config: ${path12.relative(process.cwd(), discovered)}`);
6208
+ console.log(`\u{1F4C4} Using config: ${path13.relative(process.cwd(), discovered)}`);
6086
6209
  }
6087
6210
  }
6088
6211
  if (options.config) {