sync-worktrees 2.0.0 → 2.1.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
@@ -4,6 +4,96 @@
4
4
  import * as path8 from "path";
5
5
  import { confirm as confirm2 } from "@inquirer/prompts";
6
6
  import * as cron2 from "node-cron";
7
+ import pLimit2 from "p-limit";
8
+
9
+ // src/constants.ts
10
+ var GIT_CONSTANTS = {
11
+ REMOTE_PREFIX: "origin/",
12
+ REMOTE_NAME: "origin",
13
+ HEAD_REF: "/HEAD",
14
+ DEFAULT_BRANCH: "main",
15
+ COMMON_DEFAULT_BRANCHES: ["main", "master", "develop", "trunk"],
16
+ BARE_DIR_NAME: ".bare",
17
+ DIVERGED_DIR_NAME: ".diverged",
18
+ LFS_HEADER: "version https://git-lfs.github.com/spec/",
19
+ SUBMODULE_STATUS_ADDED: "+",
20
+ SUBMODULE_STATUS_REMOVED: "-",
21
+ GITDIR_PREFIX: "gitdir:",
22
+ GIT_CHECK_IGNORE_NO_MATCH: "exit code: 1",
23
+ REFS: {
24
+ HEADS: "refs/heads/",
25
+ REMOTES: "refs/remotes/origin",
26
+ REMOTES_ORIGIN: "refs/remotes/origin/*"
27
+ },
28
+ FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*"
29
+ };
30
+ var GIT_OPERATIONS = {
31
+ MERGE_HEAD: "MERGE_HEAD",
32
+ CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD",
33
+ REVERT_HEAD: "REVERT_HEAD",
34
+ BISECT_LOG: "BISECT_LOG",
35
+ REBASE_MERGE: "rebase-merge",
36
+ REBASE_APPLY: "rebase-apply"
37
+ };
38
+ var DEFAULT_CONFIG = {
39
+ CRON_SCHEDULE: "0 * * * *",
40
+ RETRY: {
41
+ MAX_ATTEMPTS: 3,
42
+ MAX_LFS_RETRIES: 2,
43
+ INITIAL_DELAY_MS: 1e3,
44
+ MAX_DELAY_MS: 3e4,
45
+ BACKOFF_MULTIPLIER: 2,
46
+ JITTER_MS: 500
47
+ },
48
+ PARALLELISM: {
49
+ MAX_REPOSITORIES: 2,
50
+ MAX_WORKTREE_CREATION: 1,
51
+ MAX_WORKTREE_UPDATES: 3,
52
+ MAX_WORKTREE_REMOVAL: 3,
53
+ MAX_STATUS_CHECKS: 20,
54
+ MAX_SAFE_TOTAL_CONCURRENT_OPS: 100
55
+ },
56
+ UPDATE_EXISTING_WORKTREES: true
57
+ };
58
+ var ERROR_MESSAGES = {
59
+ GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.",
60
+ ALREADY_EXISTS: "already exists",
61
+ ALREADY_REGISTERED: "already registered worktree",
62
+ FAST_FORWARD_FAILED: [
63
+ "Not possible to fast-forward",
64
+ "fatal: Not possible to fast-forward, aborting",
65
+ "cannot fast-forward"
66
+ ],
67
+ NO_UPSTREAM: [
68
+ "fatal: no upstream configured",
69
+ "no upstream configured for branch",
70
+ "fatal: ambiguous argument",
71
+ "unknown revision or path"
72
+ ],
73
+ LFS_ERROR: ["smudge filter lfs failed", "git-lfs", "LFS"],
74
+ EXDEV: "EXDEV"
75
+ };
76
+ var ENV_CONSTANTS = {
77
+ GIT_LFS_SKIP_SMUDGE: "GIT_LFS_SKIP_SMUDGE",
78
+ NODE_ENV_TEST: "test"
79
+ };
80
+ var PATH_CONSTANTS = {
81
+ GIT_DIR: ".git",
82
+ README: "README"
83
+ };
84
+ var CONFIG_CONSTANTS = {
85
+ WILDCARD_PATTERN: ".*"
86
+ };
87
+ var METADATA_CONSTANTS = {
88
+ MAX_HISTORY_ENTRIES: 10,
89
+ METADATA_FILENAME: "sync-metadata.json",
90
+ WORKTREE_METADATA_PATH: ".git/worktrees",
91
+ DIVERGED_INFO_FILE: ".diverged-info.json",
92
+ DIVERGED_REASON: "diverged-history-with-changes",
93
+ ACTION_CREATED: "created",
94
+ ACTION_UPDATED: "updated",
95
+ ACTION_FETCHED: "fetched"
96
+ };
7
97
 
8
98
  // src/services/config-loader.service.ts
9
99
  import * as fs from "fs/promises";
@@ -114,13 +204,66 @@ var ConfigLoaderService = class {
114
204
  throw new Error("Invalid 'backoffMultiplier' in retry config");
115
205
  }
116
206
  }
207
+ if (configObj.parallelism !== void 0) {
208
+ this.validateParallelismConfig(configObj.parallelism, "global");
209
+ }
210
+ if (configObj.defaults && typeof configObj.defaults === "object") {
211
+ const defaults = configObj.defaults;
212
+ if (defaults.parallelism !== void 0) {
213
+ this.validateParallelismConfig(defaults.parallelism, "defaults");
214
+ }
215
+ }
216
+ }
217
+ validateParallelismConfig(parallelism, context) {
218
+ if (typeof parallelism !== "object" || parallelism === null) {
219
+ throw new Error(`'parallelism' in ${context} must be an object`);
220
+ }
221
+ const config = parallelism;
222
+ if (config.maxRepositories !== void 0) {
223
+ if (typeof config.maxRepositories !== "number" || config.maxRepositories < 1) {
224
+ throw new Error(`Invalid 'maxRepositories' in ${context} parallelism config. Must be a positive number`);
225
+ }
226
+ }
227
+ if (config.maxWorktreeCreation !== void 0) {
228
+ if (typeof config.maxWorktreeCreation !== "number" || config.maxWorktreeCreation < 1) {
229
+ throw new Error(`Invalid 'maxWorktreeCreation' in ${context} parallelism config. Must be a positive number`);
230
+ }
231
+ }
232
+ if (config.maxWorktreeUpdates !== void 0) {
233
+ if (typeof config.maxWorktreeUpdates !== "number" || config.maxWorktreeUpdates < 1) {
234
+ throw new Error(`Invalid 'maxWorktreeUpdates' in ${context} parallelism config. Must be a positive number`);
235
+ }
236
+ }
237
+ if (config.maxWorktreeRemoval !== void 0) {
238
+ if (typeof config.maxWorktreeRemoval !== "number" || config.maxWorktreeRemoval < 1) {
239
+ throw new Error(`Invalid 'maxWorktreeRemoval' in ${context} parallelism config. Must be a positive number`);
240
+ }
241
+ }
242
+ if (config.maxStatusChecks !== void 0) {
243
+ if (typeof config.maxStatusChecks !== "number" || config.maxStatusChecks < 1) {
244
+ throw new Error(`Invalid 'maxStatusChecks' in ${context} parallelism config. Must be a positive number`);
245
+ }
246
+ }
247
+ const maxRepos = config.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
248
+ const maxCreation = config.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
249
+ const maxUpdates = config.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
250
+ const maxRemoval = config.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL;
251
+ const maxStatus = config.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
252
+ const maxPerRepoOps = maxCreation + maxUpdates + maxRemoval + maxStatus;
253
+ const totalMaxConcurrent = maxRepos * maxPerRepoOps;
254
+ if (totalMaxConcurrent > DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS) {
255
+ const safeMaxRepos = Math.floor(DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS / maxPerRepoOps);
256
+ throw new Error(
257
+ `Total concurrent operations (${totalMaxConcurrent}) exceeds safe limit (${DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS}). With current per-repository limits (creation: ${maxCreation}, updates: ${maxUpdates}, removal: ${maxRemoval}, status: ${maxStatus}), maximum safe maxRepositories is ${safeMaxRepos}. Consider reducing maxRepositories or lowering per-operation limits.`
258
+ );
259
+ }
117
260
  }
118
261
  resolveRepositoryConfig(repo, defaults, configDir, globalRetry) {
119
262
  const resolved = {
120
263
  name: repo.name,
121
264
  repoUrl: repo.repoUrl,
122
265
  worktreeDir: this.resolvePath(repo.worktreeDir, configDir),
123
- cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? "0 * * * *",
266
+ cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? DEFAULT_CONFIG.CRON_SCHEDULE,
124
267
  runOnce: repo.runOnce ?? defaults?.runOnce ?? false
125
268
  };
126
269
  if (repo.bareRepoDir) {
@@ -139,6 +282,12 @@ var ConfigLoaderService = class {
139
282
  ...repo.retry || {}
140
283
  };
141
284
  }
285
+ if (repo.parallelism || defaults?.parallelism) {
286
+ resolved.parallelism = {
287
+ ...defaults?.parallelism || {},
288
+ ...repo.parallelism || {}
289
+ };
290
+ }
142
291
  if (repo.updateExistingWorktrees !== void 0 || defaults?.updateExistingWorktrees !== void 0) {
143
292
  resolved.updateExistingWorktrees = repo.updateExistingWorktrees ?? defaults?.updateExistingWorktrees ?? true;
144
293
  }
@@ -158,7 +307,7 @@ var ConfigLoaderService = class {
158
307
  return repositories.filter((repo) => {
159
308
  return patterns.some((pattern) => {
160
309
  if (pattern.includes("*")) {
161
- const regex = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
310
+ const regex = new RegExp("^" + pattern.replace(/\*/g, CONFIG_CONSTANTS.WILDCARD_PATTERN) + "$");
162
311
  return regex.test(repo.name);
163
312
  }
164
313
  return repo.name === pattern;
@@ -294,6 +443,7 @@ var App_default = App;
294
443
  // src/services/worktree-sync.service.ts
295
444
  import * as fs5 from "fs/promises";
296
445
  import * as path5 from "path";
446
+ import pLimit from "p-limit";
297
447
 
298
448
  // src/utils/date-filter.ts
299
449
  function parseDuration(durationStr) {
@@ -373,6 +523,7 @@ var DEFAULT_OPTIONS = {
373
523
  maxDelayMs: 6e5,
374
524
  // 10 minutes
375
525
  backoffMultiplier: 2,
526
+ jitterMs: 0,
376
527
  shouldRetry: (error, context) => {
377
528
  const err = error;
378
529
  if (isLfsErrorFromError(error)) {
@@ -430,7 +581,9 @@ async function retry(fn, options = {}) {
430
581
  if (lfsContext.isLfsError && opts.lfsRetryHandler) {
431
582
  opts.lfsRetryHandler(lfsContext);
432
583
  }
433
- const delay = Math.min(opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs);
584
+ const baseDelay = Math.min(opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs);
585
+ const jitter = opts.jitterMs > 0 ? Math.random() * opts.jitterMs : 0;
586
+ const delay = baseDelay + jitter;
434
587
  opts.onRetry(error, attempt, lfsContext);
435
588
  await new Promise((resolve6) => setTimeout(resolve6, delay));
436
589
  attempt++;
@@ -438,6 +591,106 @@ async function retry(fn, options = {}) {
438
591
  }
439
592
  }
440
593
 
594
+ // src/utils/timing.ts
595
+ import Table from "cli-table3";
596
+ var Timer = class {
597
+ startTime;
598
+ endTime;
599
+ constructor() {
600
+ this.startTime = Date.now();
601
+ }
602
+ stop() {
603
+ this.endTime = Date.now();
604
+ return this.getDuration();
605
+ }
606
+ getDuration() {
607
+ const end = this.endTime ?? Date.now();
608
+ return end - this.startTime;
609
+ }
610
+ };
611
+ var PhaseTimer = class {
612
+ phases = /* @__PURE__ */ new Map();
613
+ currentPhase;
614
+ startPhase(name, parallelism) {
615
+ if (this.currentPhase) {
616
+ this.endPhase();
617
+ }
618
+ this.currentPhase = name;
619
+ this.phases.set(name, { timer: new Timer(), parallelism });
620
+ }
621
+ endPhase() {
622
+ if (this.currentPhase) {
623
+ const phase = this.phases.get(this.currentPhase);
624
+ if (phase) {
625
+ phase.timer.stop();
626
+ }
627
+ this.currentPhase = void 0;
628
+ }
629
+ }
630
+ setPhaseCount(name, count) {
631
+ const phase = this.phases.get(name);
632
+ if (phase) {
633
+ phase.count = count;
634
+ }
635
+ }
636
+ getResults() {
637
+ if (this.currentPhase) {
638
+ this.endPhase();
639
+ }
640
+ const results = [];
641
+ for (const [name, { timer, count, parallelism }] of this.phases.entries()) {
642
+ const duration = timer.getDuration();
643
+ const result = {
644
+ name,
645
+ duration,
646
+ count
647
+ };
648
+ if (count && count > 0 && parallelism && parallelism > 1) {
649
+ const batches = Math.ceil(count / parallelism);
650
+ const avgTimePerBatch = duration / batches;
651
+ const theoreticalSequentialTime = count * avgTimePerBatch;
652
+ result.efficiency = theoreticalSequentialTime > 0 ? Math.round(theoreticalSequentialTime / duration * 100) : 100;
653
+ }
654
+ results.push(result);
655
+ }
656
+ return results;
657
+ }
658
+ };
659
+ function formatDuration2(ms) {
660
+ if (ms < 1e3) {
661
+ return `${ms}ms`;
662
+ }
663
+ if (ms < 6e4) {
664
+ return `${(ms / 1e3).toFixed(1)}s`;
665
+ }
666
+ const minutes = Math.floor(ms / 6e4);
667
+ const seconds = Math.floor(ms % 6e4 / 1e3);
668
+ return `${minutes}m ${seconds}s`;
669
+ }
670
+ function formatTimingTable(totalDuration, phaseResults, repoName) {
671
+ const header = repoName ? `Performance Summary - [${repoName}]` : "Performance Summary";
672
+ const table = new Table({
673
+ head: ["Operation", "Duration", "Efficiency"],
674
+ colWidths: [35, 12, 12],
675
+ style: {
676
+ head: ["cyan", "bold"],
677
+ border: ["gray"]
678
+ }
679
+ });
680
+ table.push([{ colSpan: 3, content: header, hAlign: "center" }]);
681
+ table.push(["Total Sync", formatDuration2(totalDuration), ""]);
682
+ for (let i = 0; i < phaseResults.length; i++) {
683
+ const result = phaseResults[i];
684
+ const isLast = i === phaseResults.length - 1;
685
+ const countStr = result.count ? ` (${result.count})` : "";
686
+ const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
687
+ const name = ` ${prefix} ${result.name}${countStr}`;
688
+ const efficiency = result.efficiency ? `${result.efficiency}%` : "";
689
+ table.push([name, formatDuration2(result.duration), efficiency]);
690
+ }
691
+ return table.toString();
692
+ }
693
+
441
694
  // src/services/git.service.ts
442
695
  import * as fs4 from "fs/promises";
443
696
  import * as path4 from "path";
@@ -469,29 +722,54 @@ function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
469
722
  return `${baseDir}/${repoName}`;
470
723
  }
471
724
 
725
+ // src/services/logger.service.ts
726
+ var Logger = class _Logger {
727
+ repoName;
728
+ debugEnabled;
729
+ constructor(options = {}) {
730
+ this.repoName = options.repoName;
731
+ this.debugEnabled = options.debug ?? false;
732
+ }
733
+ prefix() {
734
+ return this.repoName ? `[${this.repoName}] ` : "";
735
+ }
736
+ debug(message, ...args) {
737
+ if (!this.debugEnabled) return;
738
+ console.log(this.prefix() + this.formatMessage(message, args));
739
+ }
740
+ info(message, ...args) {
741
+ console.log(this.prefix() + this.formatMessage(message, args));
742
+ }
743
+ warn(message, ...args) {
744
+ console.warn(this.prefix() + this.formatMessage(message, args));
745
+ }
746
+ error(message, error) {
747
+ if (error instanceof Error) {
748
+ console.error(this.prefix() + message, error);
749
+ } else if (error) {
750
+ console.error(this.prefix() + message, error);
751
+ } else {
752
+ console.error(this.prefix() + message);
753
+ }
754
+ }
755
+ table(content) {
756
+ console.log("\n" + content + "\n");
757
+ }
758
+ formatMessage(message, args) {
759
+ if (args.length === 0) {
760
+ return message;
761
+ }
762
+ return args.reduce((msg, arg) => msg.replace("%s", String(arg)), message);
763
+ }
764
+ static createDefault(repoName, debug) {
765
+ return new _Logger({ repoName, debug });
766
+ }
767
+ };
768
+
472
769
  // src/services/worktree-metadata.service.ts
473
770
  import * as fs2 from "fs/promises";
474
771
  import * as path2 from "path";
475
772
  import simpleGit from "simple-git";
476
-
477
- // src/constants.ts
478
- var GIT_OPERATIONS = {
479
- MERGE_HEAD: "MERGE_HEAD",
480
- CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD",
481
- REVERT_HEAD: "REVERT_HEAD",
482
- BISECT_LOG: "BISECT_LOG",
483
- REBASE_MERGE: "rebase-merge",
484
- REBASE_APPLY: "rebase-apply"
485
- };
486
- var PATH_CONSTANTS = {
487
- GIT_DIR: ".git",
488
- README: "README"
489
- };
490
- var METADATA_CONSTANTS = {
491
- MAX_HISTORY_ENTRIES: 10
492
- };
493
-
494
- // src/services/worktree-metadata.service.ts
495
773
  var WorktreeMetadataService = class {
496
774
  /**
497
775
  * Gets the internal worktree directory name from a worktree path.
@@ -502,7 +780,12 @@ var WorktreeMetadataService = class {
502
780
  return path2.basename(worktreePath);
503
781
  }
504
782
  async getMetadataPath(bareRepoPath, worktreeName) {
505
- return path2.join(bareRepoPath, ".git", "worktrees", worktreeName, "sync-metadata.json");
783
+ return path2.join(
784
+ bareRepoPath,
785
+ METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
786
+ worktreeName,
787
+ METADATA_CONSTANTS.METADATA_FILENAME
788
+ );
506
789
  }
507
790
  async getMetadataPathFromWorktreePath(bareRepoPath, worktreePath) {
508
791
  const worktreeDirName = this.getWorktreeDirectoryName(worktreePath);
@@ -537,7 +820,12 @@ var WorktreeMetadataService = class {
537
820
  const branchName = path2.basename(worktreePath);
538
821
  const parentDir = path2.dirname(worktreePath);
539
822
  const possibleBranchWithSlash = path2.join(path2.basename(parentDir), branchName);
540
- const oldPath = path2.join(bareRepoPath, ".git", "worktrees", possibleBranchWithSlash, "sync-metadata.json");
823
+ const oldPath = path2.join(
824
+ bareRepoPath,
825
+ METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
826
+ possibleBranchWithSlash,
827
+ METADATA_CONSTANTS.METADATA_FILENAME
828
+ );
541
829
  const content = await fs2.readFile(oldPath, "utf-8");
542
830
  const metadata = JSON.parse(content);
543
831
  if (!await this.validateMetadata(metadata)) {
@@ -620,7 +908,7 @@ var WorktreeMetadataService = class {
620
908
  }
621
909
  } catch {
622
910
  }
623
- const parentBranch = defaultBranch || "main";
911
+ const parentBranch = defaultBranch || GIT_CONSTANTS.DEFAULT_BRANCH;
624
912
  await this.createInitialMetadataFromPath(
625
913
  bareRepoPath,
626
914
  worktreePath,
@@ -853,7 +1141,7 @@ var WorktreeStatusService = class {
853
1141
  const lines = result.split("\n").filter((line) => line.trim());
854
1142
  for (const line of lines) {
855
1143
  const firstChar = line.charAt(0);
856
- if (firstChar === "+" || firstChar === "-") {
1144
+ if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
857
1145
  return true;
858
1146
  }
859
1147
  }
@@ -951,7 +1239,7 @@ var WorktreeStatusService = class {
951
1239
  const modifiedSubmodules = [];
952
1240
  for (const line of lines) {
953
1241
  const firstChar = line.charAt(0);
954
- if (firstChar === "+" || firstChar === "-") {
1242
+ if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
955
1243
  const match = line.match(/^[+-]\s*(\S+)/);
956
1244
  if (match) {
957
1245
  modifiedSubmodules.push(match[1]);
@@ -1000,7 +1288,7 @@ var WorktreeStatusService = class {
1000
1288
  return files.filter((f) => !ignoredFiles.has(f));
1001
1289
  } catch (error) {
1002
1290
  const errorMessage = getErrorMessage(error);
1003
- if (errorMessage.includes("exit code: 1")) {
1291
+ if (errorMessage.includes(GIT_CONSTANTS.GIT_CHECK_IGNORE_NO_MATCH)) {
1004
1292
  return files;
1005
1293
  }
1006
1294
  console.warn(`Warning: Could not check gitignore status for files in ${worktreePath}: ${errorMessage}`);
@@ -1021,7 +1309,7 @@ var WorktreeStatusService = class {
1021
1309
  const stat4 = await fs3.stat(gitPath);
1022
1310
  if (stat4.isFile()) {
1023
1311
  const content = await fs3.readFile(gitPath, "utf-8");
1024
- const gitdirMatch = content.match(/^gitdir:\s*(.+)$/m);
1312
+ const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
1025
1313
  if (gitdirMatch) {
1026
1314
  return path3.resolve(worktreePath, gitdirMatch[1].trim());
1027
1315
  }
@@ -1043,31 +1331,33 @@ var WorktreeStatusService = class {
1043
1331
 
1044
1332
  // src/services/git.service.ts
1045
1333
  var GitService = class {
1046
- constructor(config) {
1334
+ constructor(config, logger) {
1047
1335
  this.config = config;
1336
+ this.logger = logger ?? Logger.createDefault(void 0, config.debug);
1048
1337
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
1049
- this.mainWorktreePath = path4.join(this.config.worktreeDir, "main");
1338
+ this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
1050
1339
  this.metadataService = new WorktreeMetadataService();
1051
1340
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs });
1052
1341
  }
1053
1342
  git = null;
1054
1343
  bareRepoPath;
1055
1344
  mainWorktreePath;
1056
- defaultBranch = "main";
1345
+ defaultBranch = GIT_CONSTANTS.DEFAULT_BRANCH;
1057
1346
  // Will be updated after detection
1058
1347
  metadataService;
1059
1348
  statusService;
1349
+ logger;
1060
1350
  async initialize() {
1061
1351
  const { repoUrl } = this.config;
1062
1352
  try {
1063
1353
  await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
1064
- console.log(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
1354
+ this.logger.info(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
1065
1355
  } catch {
1066
- console.log(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
1356
+ this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
1067
1357
  await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
1068
- const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3();
1358
+ const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3();
1069
1359
  await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
1070
- console.log("\u2705 Clone successful.");
1360
+ this.logger.info("\u2705 Clone successful.");
1071
1361
  }
1072
1362
  const bareGit = simpleGit3(this.bareRepoPath);
1073
1363
  try {
@@ -1079,11 +1369,11 @@ var GitService = class {
1079
1369
  } catch {
1080
1370
  await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
1081
1371
  }
1082
- console.log("Fetching remote branches...");
1372
+ this.logger.info("Fetching remote branches...");
1083
1373
  await bareGit.fetch(["--all"]);
1084
1374
  this.defaultBranch = await this.detectDefaultBranch(bareGit);
1085
1375
  this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
1086
- console.log(`Detected default branch: ${this.defaultBranch}`);
1376
+ this.logger.info(`Detected default branch: ${this.defaultBranch}`);
1087
1377
  let needsMainWorktree = true;
1088
1378
  try {
1089
1379
  const worktrees = await this.getWorktreesFromBare(bareGit);
@@ -1091,7 +1381,7 @@ var GitService = class {
1091
1381
  } catch {
1092
1382
  }
1093
1383
  if (needsMainWorktree) {
1094
- console.log(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
1384
+ this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
1095
1385
  await fs4.mkdir(this.config.worktreeDir, { recursive: true });
1096
1386
  const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
1097
1387
  try {
@@ -1099,7 +1389,7 @@ var GitService = class {
1099
1389
  const defaultBranchExists = branches.all.includes(this.defaultBranch);
1100
1390
  if (defaultBranchExists) {
1101
1391
  await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
1102
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
1392
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
1103
1393
  await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
1104
1394
  } else {
1105
1395
  await bareGit.raw([
@@ -1115,17 +1405,17 @@ var GitService = class {
1115
1405
  } catch (error) {
1116
1406
  const errorMessage = getErrorMessage(error);
1117
1407
  if (errorMessage.includes("already exists")) {
1118
- console.log(
1408
+ this.logger.info(
1119
1409
  `${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
1120
1410
  );
1121
1411
  } else {
1122
- console.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
1412
+ this.logger.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
1123
1413
  try {
1124
1414
  await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
1125
1415
  } catch (fallbackError) {
1126
1416
  const fallbackErrorMessage = getErrorMessage(fallbackError);
1127
1417
  if (fallbackErrorMessage.includes("already exists")) {
1128
- console.log(
1418
+ this.logger.info(
1129
1419
  `${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
1130
1420
  );
1131
1421
  } else {
@@ -1139,8 +1429,8 @@ var GitService = class {
1139
1429
  (w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
1140
1430
  );
1141
1431
  if (!mainWorktreeRegistered) {
1142
- if (process.env.NODE_ENV !== "test") {
1143
- console.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
1432
+ if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
1433
+ this.logger.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
1144
1434
  }
1145
1435
  }
1146
1436
  }
@@ -1158,9 +1448,9 @@ var GitService = class {
1158
1448
  }
1159
1449
  async fetchAll() {
1160
1450
  const git = this.getGit();
1161
- console.log("Fetching latest data from remote...");
1451
+ this.logger.info("Fetching latest data from remote...");
1162
1452
  if (this.isLfsSkipEnabled()) {
1163
- await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all", "--prune"]);
1453
+ await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["--all", "--prune"]);
1164
1454
  } else {
1165
1455
  await git.fetch(["--all", "--prune"]);
1166
1456
  }
@@ -1168,7 +1458,7 @@ var GitService = class {
1168
1458
  async fetchBranch(branchName) {
1169
1459
  const git = this.getGit();
1170
1460
  if (this.isLfsSkipEnabled()) {
1171
- await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["origin", branchName, "--prune"]);
1461
+ await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["origin", branchName, "--prune"]);
1172
1462
  } else {
1173
1463
  await git.fetch(["origin", branchName, "--prune"]);
1174
1464
  }
@@ -1211,7 +1501,7 @@ var GitService = class {
1211
1501
  return;
1212
1502
  }
1213
1503
  if (this.config.debug) {
1214
- console.log(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
1504
+ this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
1215
1505
  }
1216
1506
  const sampleSize = Math.min(5, lfsFileList.length);
1217
1507
  const samplesToCheck = [];
@@ -1233,7 +1523,7 @@ var GitService = class {
1233
1523
  const buffer = Buffer.alloc(200);
1234
1524
  const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
1235
1525
  const header = buffer.subarray(0, bytesRead).toString("utf8");
1236
- if (header.startsWith("version https://git-lfs.github.com/spec/")) {
1526
+ if (header.startsWith(GIT_CONSTANTS.LFS_HEADER)) {
1237
1527
  allDownloaded = false;
1238
1528
  notDownloaded.push(file);
1239
1529
  }
@@ -1247,7 +1537,7 @@ var GitService = class {
1247
1537
  }
1248
1538
  if (allDownloaded) {
1249
1539
  if (this.config.debug) {
1250
- console.log(` - \u2705 LFS files verified (${samplesToCheck.length} samples checked)`);
1540
+ this.logger.info(` - \u2705 LFS files verified (${samplesToCheck.length} samples checked)`);
1251
1541
  }
1252
1542
  return;
1253
1543
  }
@@ -1256,16 +1546,16 @@ var GitService = class {
1256
1546
  await new Promise((resolve6) => setTimeout(resolve6, retryDelay));
1257
1547
  }
1258
1548
  }
1259
- console.warn(
1549
+ this.logger.warn(
1260
1550
  ` - \u26A0\uFE0F Warning: Some LFS files may not be fully downloaded after ${maxRetries} seconds. This might cause issues if tools access the worktree immediately.`
1261
1551
  );
1262
1552
  } catch (error) {
1263
- console.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
1553
+ this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
1264
1554
  }
1265
1555
  }
1266
1556
  async createWorktreeMetadata(bareGit, worktreePath, branchName) {
1267
1557
  try {
1268
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
1558
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
1269
1559
  const currentCommit = await worktreeGit.revparse(["HEAD"]);
1270
1560
  const parentCommit = await bareGit.revparse([this.defaultBranch]);
1271
1561
  await this.metadataService.createInitialMetadataFromPath(
@@ -1277,12 +1567,12 @@ var GitService = class {
1277
1567
  parentCommit.trim()
1278
1568
  );
1279
1569
  } catch (metadataError) {
1280
- console.error(` - \u274C Failed to create metadata for '${branchName}': ${metadataError}`);
1570
+ this.logger.error(` - \u274C Failed to create metadata for '${branchName}': ${metadataError}`);
1281
1571
  throw new Error(`Metadata creation failed for ${branchName}. This worktree cannot be auto-managed.`);
1282
1572
  }
1283
1573
  }
1284
1574
  async addWorktree(branchName, worktreePath) {
1285
- const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(this.bareRepoPath);
1575
+ const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(this.bareRepoPath);
1286
1576
  const absoluteWorktreePath = path4.resolve(worktreePath);
1287
1577
  await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
1288
1578
  try {
@@ -1290,10 +1580,10 @@ var GitService = class {
1290
1580
  const worktrees = await this.getWorktreesFromBare(bareGit);
1291
1581
  const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
1292
1582
  if (isValidWorktree) {
1293
- console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
1583
+ this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
1294
1584
  return;
1295
1585
  } else {
1296
- console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
1586
+ this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
1297
1587
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
1298
1588
  }
1299
1589
  } catch {
@@ -1303,7 +1593,7 @@ var GitService = class {
1303
1593
  const localBranchExists = branches.all.includes(branchName);
1304
1594
  if (localBranchExists || branchName.includes("/")) {
1305
1595
  await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1306
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
1596
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
1307
1597
  await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
1308
1598
  } else {
1309
1599
  await bareGit.raw([
@@ -1316,7 +1606,7 @@ var GitService = class {
1316
1606
  `origin/${branchName}`
1317
1607
  ]);
1318
1608
  }
1319
- console.log(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
1609
+ this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
1320
1610
  if (!this.isLfsSkipEnabled()) {
1321
1611
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1322
1612
  }
@@ -1327,7 +1617,13 @@ var GitService = class {
1327
1617
  throw error;
1328
1618
  }
1329
1619
  if (errorMessage.includes("already registered worktree")) {
1330
- console.warn(` - Worktree already registered but missing. Pruning and retrying...`);
1620
+ const worktrees = await this.getWorktreesFromBare(bareGit);
1621
+ const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
1622
+ if (existingWorktree && !existingWorktree.isPrunable) {
1623
+ this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
1624
+ return;
1625
+ }
1626
+ this.logger.warn(` - Worktree already registered but missing. Pruning and retrying...`);
1331
1627
  await bareGit.raw(["worktree", "prune"]);
1332
1628
  try {
1333
1629
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
@@ -1343,53 +1639,66 @@ var GitService = class {
1343
1639
  absoluteWorktreePath,
1344
1640
  `origin/${branchName}`
1345
1641
  ]);
1346
- console.log(` - Created worktree for '${branchName}' after pruning`);
1642
+ this.logger.info(` - Created worktree for '${branchName}' after pruning`);
1347
1643
  if (!this.isLfsSkipEnabled()) {
1348
1644
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1349
1645
  }
1350
1646
  await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
1351
1647
  return;
1352
1648
  } catch (retryError) {
1353
- console.error(` - Failed to create worktree after pruning: ${retryError}`);
1649
+ this.logger.error(` - Failed to create worktree after pruning: ${retryError}`);
1354
1650
  throw retryError;
1355
1651
  }
1356
1652
  }
1357
- console.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
1653
+ this.logger.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
1358
1654
  try {
1359
1655
  await fs4.access(absoluteWorktreePath);
1360
1656
  const worktrees = await this.getWorktreesFromBare(bareGit);
1361
1657
  const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
1362
1658
  if (isValidWorktree) {
1363
- console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
1659
+ this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
1364
1660
  return;
1365
1661
  } else {
1366
- console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
1662
+ this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
1367
1663
  await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
1368
1664
  }
1369
1665
  } catch {
1370
1666
  }
1371
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1372
- console.log(` - Created worktree for '${branchName}' (without tracking)`);
1373
- if (!this.isLfsSkipEnabled()) {
1374
- await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1667
+ try {
1668
+ await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1669
+ this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
1670
+ if (!this.isLfsSkipEnabled()) {
1671
+ await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1672
+ }
1673
+ await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
1674
+ } catch (fallbackError) {
1675
+ const fallbackErrorMessage = getErrorMessage(fallbackError);
1676
+ if (fallbackErrorMessage.includes("already registered worktree")) {
1677
+ const worktrees = await this.getWorktreesFromBare(bareGit);
1678
+ const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
1679
+ if (existingWorktree && !existingWorktree.isPrunable) {
1680
+ this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
1681
+ return;
1682
+ }
1683
+ }
1684
+ throw fallbackError;
1375
1685
  }
1376
- await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
1377
1686
  }
1378
1687
  }
1379
1688
  async removeWorktree(worktreePath) {
1380
1689
  const bareGit = simpleGit3(this.bareRepoPath);
1381
1690
  await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
1382
- console.log(` - \u2705 Safely removed stale worktree at '${worktreePath}'.`);
1691
+ this.logger.info(` - \u2705 Safely removed stale worktree at '${worktreePath}'.`);
1383
1692
  try {
1384
1693
  await this.metadataService.deleteMetadataFromPath(this.bareRepoPath, worktreePath);
1385
1694
  } catch (metadataError) {
1386
- console.warn(`Failed to delete metadata for worktree: ${metadataError}`);
1695
+ this.logger.warn(`Failed to delete metadata for worktree: ${metadataError}`);
1387
1696
  }
1388
1697
  }
1389
1698
  async pruneWorktrees() {
1390
1699
  const bareGit = simpleGit3(this.bareRepoPath);
1391
1700
  await bareGit.raw(["worktree", "prune"]);
1392
- console.log("Pruned worktree metadata.");
1701
+ this.logger.info("Pruned worktree metadata.");
1393
1702
  }
1394
1703
  async checkWorktreeStatus(worktreePath) {
1395
1704
  const worktreeGit = simpleGit3(worktreePath);
@@ -1428,7 +1737,7 @@ var GitService = class {
1428
1737
  const unpushedCount = parseInt(result.trim(), 10);
1429
1738
  return unpushedCount > 0;
1430
1739
  } catch (error) {
1431
- console.error(`Error checking unpushed commits: ${error}`);
1740
+ this.logger.error(`Error checking unpushed commits: ${error}`);
1432
1741
  return false;
1433
1742
  }
1434
1743
  }
@@ -1466,7 +1775,7 @@ var GitService = class {
1466
1775
  }
1467
1776
  return false;
1468
1777
  }
1469
- console.error(
1778
+ this.logger.error(
1470
1779
  `Unexpected error checking upstream status for ${worktreePath}. This might indicate a real issue rather than a missing upstream. Error: ${errorMessage}`
1471
1780
  );
1472
1781
  return false;
@@ -1478,7 +1787,7 @@ var GitService = class {
1478
1787
  const stashList = await worktreeGit.stashList();
1479
1788
  return stashList.total > 0;
1480
1789
  } catch (error) {
1481
- console.error(`Error checking stash: ${error}`);
1790
+ this.logger.error(`Error checking stash: ${error}`);
1482
1791
  return true;
1483
1792
  }
1484
1793
  }
@@ -1544,7 +1853,7 @@ var GitService = class {
1544
1853
  } catch {
1545
1854
  try {
1546
1855
  const remoteBranches = await bareGit.branch(["-r"]);
1547
- const commonDefaults = ["main", "master", "develop", "trunk"];
1856
+ const commonDefaults = GIT_CONSTANTS.COMMON_DEFAULT_BRANCHES;
1548
1857
  for (const defaultName of commonDefaults) {
1549
1858
  if (remoteBranches.all.some((branch) => branch === `origin/${defaultName}`)) {
1550
1859
  return defaultName;
@@ -1554,10 +1863,10 @@ var GitService = class {
1554
1863
  }
1555
1864
  }
1556
1865
  }
1557
- return "main";
1866
+ return GIT_CONSTANTS.DEFAULT_BRANCH;
1558
1867
  }
1559
1868
  isLfsSkipEnabled() {
1560
- return this.config.skipLfs || process.env.GIT_LFS_SKIP_SMUDGE === "1";
1869
+ return this.config.skipLfs || process.env[ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE] === "1";
1561
1870
  }
1562
1871
  async getWorktrees() {
1563
1872
  const bareGit = simpleGit3(this.bareRepoPath);
@@ -1579,7 +1888,7 @@ var GitService = class {
1579
1888
  }
1580
1889
  }
1581
1890
  async updateWorktree(worktreePath) {
1582
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
1891
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
1583
1892
  const branchSummary = await worktreeGit.branch();
1584
1893
  const currentBranch = branchSummary.current;
1585
1894
  await worktreeGit.merge([`origin/${currentBranch}`, "--ff-only"]);
@@ -1597,14 +1906,14 @@ var GitService = class {
1597
1906
  this.defaultBranch
1598
1907
  );
1599
1908
  } catch (metadataError) {
1600
- console.warn(`Failed to update metadata for worktree: ${metadataError}`);
1909
+ this.logger.warn(`Failed to update metadata for worktree: ${metadataError}`);
1601
1910
  }
1602
1911
  }
1603
1912
  async hasDivergedHistory(worktreePath, expectedBranch) {
1604
1913
  const worktreeGit = simpleGit3(worktreePath);
1605
1914
  const branchInfo = await worktreeGit.branch();
1606
1915
  if (branchInfo.current !== expectedBranch) {
1607
- console.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
1916
+ this.logger.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
1608
1917
  return false;
1609
1918
  }
1610
1919
  try {
@@ -1633,12 +1942,12 @@ var GitService = class {
1633
1942
  const remoteTree = await worktreeGit.raw(["rev-parse", `origin/${branch}^{tree}`]);
1634
1943
  return localTree.trim() === remoteTree.trim();
1635
1944
  } catch (error) {
1636
- console.error(`Error comparing tree content: ${error}`);
1945
+ this.logger.error(`Error comparing tree content: ${error}`);
1637
1946
  return false;
1638
1947
  }
1639
1948
  }
1640
1949
  async resetToUpstream(worktreePath, branch) {
1641
- const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
1950
+ const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
1642
1951
  await worktreeGit.reset(["--hard", `origin/${branch}`]);
1643
1952
  try {
1644
1953
  const currentCommit = await worktreeGit.revparse(["HEAD"]);
@@ -1650,7 +1959,7 @@ var GitService = class {
1650
1959
  this.defaultBranch
1651
1960
  );
1652
1961
  } catch (metadataError) {
1653
- console.warn(`Failed to update metadata after reset: ${metadataError}`);
1962
+ this.logger.warn(`Failed to update metadata after reset: ${metadataError}`);
1654
1963
  }
1655
1964
  }
1656
1965
  async getCurrentCommit(worktreePath) {
@@ -1678,17 +1987,27 @@ var GitService = class {
1678
1987
  currentWorktree.branch = line.substring(7).replace("refs/heads/", "");
1679
1988
  } else if (line === "detached") {
1680
1989
  currentWorktree.detached = true;
1990
+ } else if (line === "prunable") {
1991
+ currentWorktree.prunable = true;
1681
1992
  } else if (line.trim() === "") {
1682
1993
  if (currentWorktree.path) {
1683
1994
  if (currentWorktree.branch && !currentWorktree.detached) {
1684
- worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch });
1995
+ worktrees.push({
1996
+ path: currentWorktree.path,
1997
+ branch: currentWorktree.branch,
1998
+ isPrunable: currentWorktree.prunable || false
1999
+ });
1685
2000
  }
1686
2001
  }
1687
2002
  currentWorktree = {};
1688
2003
  }
1689
2004
  }
1690
2005
  if (currentWorktree.path && currentWorktree.branch && !currentWorktree.detached) {
1691
- worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch });
2006
+ worktrees.push({
2007
+ path: currentWorktree.path,
2008
+ branch: currentWorktree.branch,
2009
+ isPrunable: currentWorktree.prunable || false
2010
+ });
1692
2011
  }
1693
2012
  return worktrees;
1694
2013
  }
@@ -1698,9 +2017,11 @@ var GitService = class {
1698
2017
  var WorktreeSyncService = class {
1699
2018
  constructor(config) {
1700
2019
  this.config = config;
1701
- this.gitService = new GitService(config);
2020
+ this.logger = config.logger ?? Logger.createDefault(void 0, config.debug);
2021
+ this.gitService = new GitService(config, this.logger);
1702
2022
  }
1703
2023
  gitService;
2024
+ logger;
1704
2025
  syncInProgress = false;
1705
2026
  async initialize() {
1706
2027
  await this.gitService.initialize();
@@ -1710,11 +2031,13 @@ var WorktreeSyncService = class {
1710
2031
  }
1711
2032
  async sync() {
1712
2033
  if (this.syncInProgress) {
1713
- console.warn("\u26A0\uFE0F Sync already in progress, skipping...");
2034
+ this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
1714
2035
  return;
1715
2036
  }
1716
2037
  this.syncInProgress = true;
1717
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
2038
+ this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
2039
+ const totalTimer = new Timer();
2040
+ const phaseTimer = new PhaseTimer();
1718
2041
  let lfsSkipEnabled = false;
1719
2042
  const retryOptions = {
1720
2043
  maxAttempts: this.config.retry?.maxAttempts ?? 3,
@@ -1724,18 +2047,18 @@ var WorktreeSyncService = class {
1724
2047
  backoffMultiplier: this.config.retry?.backoffMultiplier ?? 2,
1725
2048
  onRetry: (error, attempt, context) => {
1726
2049
  const errorMessage = getErrorMessage(error);
1727
- console.log(`
2050
+ this.logger.info(`
1728
2051
  \u26A0\uFE0F Sync attempt ${attempt} failed: ${errorMessage}`);
1729
2052
  if (context?.isLfsError && !this.config.skipLfs) {
1730
- console.log(`\u{1F504} LFS error detected. Will retry with LFS skipped...`);
2053
+ this.logger.info(`\u{1F504} LFS error detected. Will retry with LFS skipped...`);
1731
2054
  } else {
1732
- console.log(`\u{1F504} Retrying synchronization...
2055
+ this.logger.info(`\u{1F504} Retrying synchronization...
1733
2056
  `);
1734
2057
  }
1735
2058
  },
1736
2059
  lfsRetryHandler: () => {
1737
2060
  if (!this.config.skipLfs && !lfsSkipEnabled) {
1738
- console.log("\u26A0\uFE0F Temporarily disabling LFS downloads for this sync...");
2061
+ this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for this sync...");
1739
2062
  process.env.GIT_LFS_SKIP_SMUDGE = "1";
1740
2063
  lfsSkipEnabled = true;
1741
2064
  }
@@ -1744,14 +2067,15 @@ var WorktreeSyncService = class {
1744
2067
  try {
1745
2068
  await retry(async () => {
1746
2069
  await this.gitService.pruneWorktrees();
1747
- console.log("Step 1: Fetching latest data from remote...");
2070
+ this.logger.info("Step 1: Fetching latest data from remote...");
2071
+ phaseTimer.startPhase("Phase 1: Fetch");
1748
2072
  try {
1749
2073
  await this.gitService.fetchAll();
1750
2074
  } catch (fetchError) {
1751
2075
  const errorMessage = getErrorMessage(fetchError);
1752
2076
  if (isLfsError(errorMessage) && !lfsSkipEnabled && !this.config.skipLfs) {
1753
- console.log("\u26A0\uFE0F Fetch all failed due to LFS error. Attempting branch-by-branch fetch...");
1754
- console.log("\u26A0\uFE0F Temporarily disabling LFS downloads for branch-by-branch fetch...");
2077
+ this.logger.info("\u26A0\uFE0F Fetch all failed due to LFS error. Attempting branch-by-branch fetch...");
2078
+ this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for branch-by-branch fetch...");
1755
2079
  process.env.GIT_LFS_SKIP_SMUDGE = "1";
1756
2080
  lfsSkipEnabled = true;
1757
2081
  await this.fetchBranchByBranch();
@@ -1759,143 +2083,221 @@ var WorktreeSyncService = class {
1759
2083
  throw fetchError;
1760
2084
  }
1761
2085
  }
2086
+ phaseTimer.endPhase();
1762
2087
  let remoteBranches;
1763
2088
  if (this.config.branchMaxAge) {
1764
2089
  const branchesWithActivity = await this.gitService.getRemoteBranchesWithActivity();
1765
2090
  const filteredBranches = filterBranchesByAge(branchesWithActivity, this.config.branchMaxAge);
1766
2091
  remoteBranches = filteredBranches.map((b) => b.branch);
1767
- console.log(`Found ${branchesWithActivity.length} remote branches.`);
1768
- console.log(
2092
+ this.logger.info(`Found ${branchesWithActivity.length} remote branches.`);
2093
+ this.logger.info(
1769
2094
  `After filtering by age (${formatDuration(this.config.branchMaxAge)}): ${remoteBranches.length} branches.`
1770
2095
  );
1771
2096
  if (branchesWithActivity.length > remoteBranches.length) {
1772
2097
  const excludedCount = branchesWithActivity.length - remoteBranches.length;
1773
- console.log(` - Excluded ${excludedCount} stale branches.`);
2098
+ this.logger.info(` - Excluded ${excludedCount} stale branches.`);
1774
2099
  }
1775
2100
  } else {
1776
2101
  remoteBranches = await this.gitService.getRemoteBranches();
1777
- console.log(`Found ${remoteBranches.length} remote branches.`);
2102
+ this.logger.info(`Found ${remoteBranches.length} remote branches.`);
1778
2103
  }
1779
2104
  const defaultBranch = this.gitService.getDefaultBranch();
1780
2105
  if (!remoteBranches.includes(defaultBranch)) {
1781
2106
  remoteBranches.push(defaultBranch);
1782
- console.log(`Ensuring default branch '${defaultBranch}' is retained.`);
2107
+ this.logger.info(`Ensuring default branch '${defaultBranch}' is retained.`);
1783
2108
  }
1784
2109
  await fs5.mkdir(this.config.worktreeDir, { recursive: true });
1785
2110
  const worktrees = await this.gitService.getWorktrees();
1786
2111
  const worktreeBranches = worktrees.map((w) => w.branch);
1787
- console.log(`Found ${worktrees.length} existing Git worktrees.`);
2112
+ this.logger.info(`Found ${worktrees.length} existing Git worktrees.`);
1788
2113
  await this.cleanupOrphanedDirectories(worktrees);
1789
- await this.createNewWorktrees(remoteBranches, worktreeBranches, defaultBranch);
1790
- await this.pruneOldWorktrees(remoteBranches, worktreeBranches);
2114
+ await this.createNewWorktreesWithTiming(remoteBranches, worktreeBranches, defaultBranch, phaseTimer);
2115
+ await this.pruneOldWorktreesWithTiming(remoteBranches, worktreeBranches, phaseTimer);
1791
2116
  if (this.config.updateExistingWorktrees !== false) {
1792
- await this.updateExistingWorktrees(worktrees, remoteBranches);
2117
+ await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
1793
2118
  }
2119
+ phaseTimer.startPhase("Phase 5: Cleanup");
1794
2120
  await this.gitService.pruneWorktrees();
1795
- console.log("Step 5: Pruned worktree metadata.");
2121
+ this.logger.info("Step 5: Pruned worktree metadata.");
2122
+ phaseTimer.endPhase();
1796
2123
  }, retryOptions);
1797
2124
  } catch (error) {
1798
- console.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
2125
+ this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
1799
2126
  throw error;
1800
2127
  } finally {
1801
2128
  if (lfsSkipEnabled && !this.config.skipLfs) {
1802
2129
  delete process.env.GIT_LFS_SKIP_SMUDGE;
1803
2130
  }
1804
2131
  this.syncInProgress = false;
1805
- console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
2132
+ this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
1806
2133
  `);
2134
+ if (this.config.debug) {
2135
+ const totalDuration = totalTimer.stop();
2136
+ const phaseResults = phaseTimer.getResults();
2137
+ const repoName = this.config.name;
2138
+ this.logger.table(formatTimingTable(totalDuration, phaseResults, repoName));
2139
+ }
1807
2140
  }
1808
2141
  }
2142
+ async createNewWorktreesWithTiming(remoteBranches, existingWorktreeBranches, defaultBranch, phaseTimer) {
2143
+ const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
2144
+ phaseTimer.startPhase("Phase 2: Create", maxConcurrent);
2145
+ await this.createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch);
2146
+ const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
2147
+ phaseTimer.setPhaseCount("Phase 2: Create", newBranches.length);
2148
+ phaseTimer.endPhase();
2149
+ }
1809
2150
  async createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch) {
1810
2151
  const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
1811
2152
  if (newBranches.length > 0) {
1812
- console.log(`Step 2: Creating new worktrees for: ${newBranches.join(", ")}`);
1813
- for (const branchName of newBranches) {
1814
- const worktreePath = path5.join(this.config.worktreeDir, branchName);
1815
- await this.gitService.addWorktree(branchName, worktreePath);
1816
- }
2153
+ this.logger.info(`Step 2: Creating ${newBranches.length} new worktrees...`);
2154
+ const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
2155
+ const limit = pLimit(maxConcurrent);
2156
+ const results = await Promise.allSettled(
2157
+ newBranches.map(
2158
+ (branchName) => limit(async () => {
2159
+ const worktreePath = path5.join(this.config.worktreeDir, branchName);
2160
+ try {
2161
+ await this.gitService.addWorktree(branchName, worktreePath);
2162
+ this.logger.info(` \u2705 Created worktree for '${branchName}'`);
2163
+ } catch (error) {
2164
+ this.logger.error(` \u274C Failed to create worktree for '${branchName}':`, getErrorMessage(error));
2165
+ throw error;
2166
+ }
2167
+ })
2168
+ )
2169
+ );
2170
+ const successCount = results.filter((r) => r.status === "fulfilled").length;
2171
+ this.logger.info(` Created ${successCount}/${newBranches.length} worktrees successfully`);
1817
2172
  } else {
1818
- console.log("Step 2: No new branches to create worktrees for.");
2173
+ this.logger.info("Step 2: No new branches to create worktrees for.");
1819
2174
  }
1820
2175
  }
2176
+ async pruneOldWorktreesWithTiming(remoteBranches, existingWorktreeBranches, phaseTimer) {
2177
+ const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
2178
+ phaseTimer.startPhase("Phase 3: Prune", maxConcurrent);
2179
+ await this.pruneOldWorktrees(remoteBranches, existingWorktreeBranches);
2180
+ const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
2181
+ phaseTimer.setPhaseCount("Phase 3: Prune", deletedBranches.length);
2182
+ phaseTimer.endPhase();
2183
+ }
1821
2184
  async pruneOldWorktrees(remoteBranches, existingWorktreeBranches) {
1822
2185
  const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
1823
2186
  if (deletedBranches.length > 0) {
1824
- console.log(`Step 3: Checking for stale worktrees to prune: ${deletedBranches.join(", ")}`);
1825
- for (const branchName of deletedBranches) {
1826
- const worktreePath = path5.join(this.config.worktreeDir, branchName);
1827
- try {
1828
- const status = await this.gitService.getFullWorktreeStatus(worktreePath, this.config.debug);
2187
+ this.logger.info(`Step 3: Checking ${deletedBranches.length} stale worktrees to prune...`);
2188
+ const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
2189
+ const limit = pLimit(maxConcurrent);
2190
+ const statusResults = await Promise.allSettled(
2191
+ deletedBranches.map(
2192
+ (branchName) => limit(async () => {
2193
+ const worktreePath = path5.join(this.config.worktreeDir, branchName);
2194
+ const status = await this.gitService.getFullWorktreeStatus(worktreePath, this.config.debug);
2195
+ return { branchName, worktreePath, status };
2196
+ })
2197
+ )
2198
+ );
2199
+ const toRemove = [];
2200
+ const toSkip = [];
2201
+ for (const result of statusResults) {
2202
+ if (result.status === "fulfilled") {
2203
+ const { branchName, worktreePath, status } = result.value;
1829
2204
  if (status.canRemove) {
1830
- await this.gitService.removeWorktree(worktreePath);
2205
+ toRemove.push({ branchName, worktreePath });
1831
2206
  } else {
1832
- if (status.upstreamGone && status.hasUnpushedCommits) {
1833
- console.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
1834
- console.log(` Please review manually: cd ${worktreePath} && git log`);
1835
- console.log(
1836
- ` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
1837
- );
1838
- } else {
1839
- console.log(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
1840
- }
1841
- if (this.config.debug && status.details) {
1842
- this.logDebugDetails(branchName, status.details);
1843
- }
2207
+ toSkip.push({ branchName, status });
1844
2208
  }
1845
- } catch (error) {
1846
- console.error(` - Error checking worktree '${branchName}':`, error);
2209
+ } else {
2210
+ this.logger.error(` - Error checking worktree:`, result.reason);
2211
+ }
2212
+ }
2213
+ if (toRemove.length > 0) {
2214
+ const removeLimit = pLimit(
2215
+ this.config.parallelism?.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL
2216
+ );
2217
+ const removeResults = await Promise.allSettled(
2218
+ toRemove.map(
2219
+ ({ branchName, worktreePath }) => removeLimit(async () => {
2220
+ try {
2221
+ await this.gitService.removeWorktree(worktreePath);
2222
+ this.logger.info(` \u2705 Removed worktree for '${branchName}'`);
2223
+ } catch (error) {
2224
+ this.logger.error(` \u274C Failed to remove worktree for '${branchName}':`, getErrorMessage(error));
2225
+ throw error;
2226
+ }
2227
+ })
2228
+ )
2229
+ );
2230
+ const removedCount = removeResults.filter((r) => r.status === "fulfilled").length;
2231
+ this.logger.info(` Removed ${removedCount}/${toRemove.length} worktrees successfully`);
2232
+ }
2233
+ if (toSkip.length > 0) {
2234
+ this.logger.info(` Skipped ${toSkip.length} worktree(s) with local changes or unpushed commits`);
2235
+ }
2236
+ for (const { branchName, status } of toSkip) {
2237
+ if (status.upstreamGone && status.hasUnpushedCommits) {
2238
+ const worktreePath = path5.join(this.config.worktreeDir, branchName);
2239
+ this.logger.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
2240
+ this.logger.info(` Please review manually: cd ${worktreePath} && git log`);
2241
+ this.logger.info(
2242
+ ` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
2243
+ );
2244
+ } else {
2245
+ this.logger.info(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
2246
+ }
2247
+ if (this.config.debug && status.details) {
2248
+ this.logDebugDetails(branchName, status.details);
1847
2249
  }
1848
2250
  }
1849
2251
  } else {
1850
- console.log("Step 3: No stale worktrees to prune.");
2252
+ this.logger.info("Step 3: No stale worktrees to prune.");
1851
2253
  }
1852
2254
  }
1853
2255
  logDebugDetails(branchName, details) {
1854
- console.log(`
2256
+ this.logger.info(`
1855
2257
  \u{1F50D} Debug details for '${branchName}':`);
1856
2258
  if (details.modifiedFiles > 0 && details.modifiedFilesList) {
1857
- console.log(` - Modified files (${details.modifiedFiles}):`);
1858
- details.modifiedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
2259
+ this.logger.info(` - Modified files (${details.modifiedFiles}):`);
2260
+ details.modifiedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1859
2261
  }
1860
2262
  if (details.deletedFiles > 0 && details.deletedFilesList) {
1861
- console.log(` - Deleted files (${details.deletedFiles}):`);
1862
- details.deletedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
2263
+ this.logger.info(` - Deleted files (${details.deletedFiles}):`);
2264
+ details.deletedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1863
2265
  }
1864
2266
  if (details.renamedFiles > 0 && details.renamedFilesList) {
1865
- console.log(` - Renamed files (${details.renamedFiles}):`);
1866
- details.renamedFilesList.forEach((file) => console.log(` \u2022 ${file.from} \u2192 ${file.to}`));
2267
+ this.logger.info(` - Renamed files (${details.renamedFiles}):`);
2268
+ details.renamedFilesList.forEach((file) => this.logger.info(` \u2022 ${file.from} \u2192 ${file.to}`));
1867
2269
  }
1868
2270
  if (details.createdFiles > 0 && details.createdFilesList) {
1869
- console.log(` - Created files (${details.createdFiles}):`);
1870
- details.createdFilesList.forEach((file) => console.log(` \u2022 ${file}`));
2271
+ this.logger.info(` - Created files (${details.createdFiles}):`);
2272
+ details.createdFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1871
2273
  }
1872
2274
  if (details.conflictedFiles > 0 && details.conflictedFilesList) {
1873
- console.log(` - Conflicted files (${details.conflictedFiles}):`);
1874
- details.conflictedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
2275
+ this.logger.info(` - Conflicted files (${details.conflictedFiles}):`);
2276
+ details.conflictedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1875
2277
  }
1876
2278
  if (details.untrackedFiles > 0 && details.untrackedFilesList) {
1877
- console.log(` - Untracked files (not ignored) (${details.untrackedFiles}):`);
1878
- details.untrackedFilesList.forEach((file) => console.log(` \u2022 ${file}`));
2279
+ this.logger.info(` - Untracked files (not ignored) (${details.untrackedFiles}):`);
2280
+ details.untrackedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
1879
2281
  }
1880
2282
  if (details.unpushedCommitCount !== void 0 && details.unpushedCommitCount > 0) {
1881
- console.log(` - Unpushed commits: ${details.unpushedCommitCount}`);
2283
+ this.logger.info(` - Unpushed commits: ${details.unpushedCommitCount}`);
1882
2284
  }
1883
2285
  if (details.stashCount !== void 0 && details.stashCount > 0) {
1884
- console.log(` - Stashed changes: ${details.stashCount}`);
2286
+ this.logger.info(` - Stashed changes: ${details.stashCount}`);
1885
2287
  }
1886
2288
  if (details.operationType) {
1887
- console.log(` - Operation in progress: ${details.operationType}`);
2289
+ this.logger.info(` - Operation in progress: ${details.operationType}`);
1888
2290
  }
1889
2291
  if (details.modifiedSubmodules && details.modifiedSubmodules.length > 0) {
1890
- console.log(` - Modified submodules (${details.modifiedSubmodules.length}):`);
1891
- details.modifiedSubmodules.forEach((submodule) => console.log(` \u2022 ${submodule}`));
2292
+ this.logger.info(` - Modified submodules (${details.modifiedSubmodules.length}):`);
2293
+ details.modifiedSubmodules.forEach((submodule) => this.logger.info(` \u2022 ${submodule}`));
1892
2294
  }
1893
- console.log("");
2295
+ this.logger.info("");
1894
2296
  }
1895
2297
  async fetchBranchByBranch() {
1896
- console.log("Fetching branches individually to isolate LFS errors...");
2298
+ this.logger.info("Fetching branches individually to isolate LFS errors...");
1897
2299
  const remoteBranches = await this.gitService.getRemoteBranches();
1898
- console.log(`Found ${remoteBranches.length} remote branches to fetch.`);
2300
+ this.logger.info(`Found ${remoteBranches.length} remote branches to fetch.`);
1899
2301
  const failedBranches = [];
1900
2302
  let successCount = 0;
1901
2303
  for (const branch of remoteBranches) {
@@ -1904,79 +2306,104 @@ var WorktreeSyncService = class {
1904
2306
  successCount++;
1905
2307
  } catch (error) {
1906
2308
  const errorMessage = getErrorMessage(error);
1907
- console.log(` \u26A0\uFE0F Failed to fetch branch '${branch}': ${errorMessage}`);
2309
+ this.logger.info(` \u26A0\uFE0F Failed to fetch branch '${branch}': ${errorMessage}`);
1908
2310
  failedBranches.push(branch);
1909
2311
  }
1910
2312
  }
1911
- console.log(`Branch-by-branch fetch completed: ${successCount}/${remoteBranches.length} successful`);
2313
+ this.logger.info(`Branch-by-branch fetch completed: ${successCount}/${remoteBranches.length} successful`);
1912
2314
  if (failedBranches.length > 0) {
1913
- console.log(`\u26A0\uFE0F Failed to fetch ${failedBranches.length} branches due to errors.`);
1914
- console.log(` These branches will be skipped: ${failedBranches.join(", ")}`);
2315
+ this.logger.info(`\u26A0\uFE0F Failed to fetch ${failedBranches.length} branches due to errors.`);
2316
+ this.logger.info(` These branches will be skipped: ${failedBranches.join(", ")}`);
1915
2317
  }
1916
2318
  }
2319
+ async updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer) {
2320
+ const maxConcurrent = this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
2321
+ phaseTimer.startPhase("Phase 4: Update", maxConcurrent);
2322
+ await this.updateExistingWorktrees(worktrees, remoteBranches);
2323
+ const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
2324
+ phaseTimer.setPhaseCount("Phase 4: Update", activeWorktrees.length);
2325
+ phaseTimer.endPhase();
2326
+ }
1917
2327
  async updateExistingWorktrees(worktrees, remoteBranches) {
1918
- const worktreesToUpdate = [];
1919
- console.log("Step 4: Checking for worktrees that need updates...");
1920
- const divergedDir = path5.join(this.config.worktreeDir, ".diverged");
2328
+ this.logger.info("Step 4: Checking for worktrees that need updates...");
2329
+ const divergedDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
1921
2330
  try {
1922
2331
  const diverged = await fs5.readdir(divergedDir);
1923
2332
  if (diverged.length > 0) {
1924
- console.log(`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path5.relative(process.cwd(), divergedDir)}`);
2333
+ this.logger.info(
2334
+ `\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path5.relative(process.cwd(), divergedDir)}`
2335
+ );
1925
2336
  }
1926
2337
  } catch {
1927
2338
  }
1928
2339
  const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
1929
- for (const worktree of activeWorktrees) {
1930
- try {
1931
- try {
1932
- await fs5.access(worktree.path);
1933
- } catch {
1934
- continue;
1935
- }
1936
- const hasOp = await this.gitService.hasOperationInProgress(worktree.path);
1937
- if (hasOp) {
1938
- continue;
1939
- }
1940
- const isClean = await this.gitService.checkWorktreeStatus(worktree.path);
1941
- if (!isClean) {
1942
- continue;
1943
- }
1944
- const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
1945
- if (!canFastForward) {
1946
- await this.handleDivergedBranch(worktree);
1947
- continue;
1948
- }
1949
- const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
1950
- if (isBehind) {
1951
- worktreesToUpdate.push(worktree);
1952
- }
1953
- } catch (error) {
1954
- console.error(` - Error checking worktree '${worktree.branch}':`, error);
2340
+ const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
2341
+ const limit = pLimit(maxConcurrent);
2342
+ const checkResults = await Promise.allSettled(
2343
+ activeWorktrees.map(
2344
+ (worktree) => limit(async () => {
2345
+ try {
2346
+ await fs5.access(worktree.path);
2347
+ } catch {
2348
+ return null;
2349
+ }
2350
+ const hasOp = await this.gitService.hasOperationInProgress(worktree.path);
2351
+ if (hasOp) return null;
2352
+ const isClean = await this.gitService.checkWorktreeStatus(worktree.path);
2353
+ if (!isClean) return null;
2354
+ const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
2355
+ if (!canFastForward) {
2356
+ await this.handleDivergedBranch(worktree);
2357
+ return null;
2358
+ }
2359
+ const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
2360
+ return isBehind ? worktree : null;
2361
+ })
2362
+ )
2363
+ );
2364
+ const worktreesToUpdate = [];
2365
+ for (const result of checkResults) {
2366
+ if (result.status === "fulfilled" && result.value) {
2367
+ worktreesToUpdate.push(result.value);
2368
+ } else if (result.status === "rejected") {
2369
+ this.logger.error(` - Error checking worktree:`, result.reason);
1955
2370
  }
1956
2371
  }
1957
2372
  if (worktreesToUpdate.length > 0) {
1958
- console.log(` - Found ${worktreesToUpdate.length} worktrees behind their upstream branches.`);
1959
- for (const worktree of worktreesToUpdate) {
1960
- try {
1961
- console.log(` - Updating worktree '${worktree.branch}'...`);
1962
- await this.gitService.updateWorktree(worktree.path);
1963
- console.log(` \u2705 Successfully updated '${worktree.branch}'.`);
1964
- } catch (error) {
1965
- const errorMessage = getErrorMessage(error);
1966
- if (errorMessage.includes("Not possible to fast-forward") || errorMessage.includes("fatal: Not possible to fast-forward, aborting") || errorMessage.includes("cannot fast-forward")) {
1967
- console.log(` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`);
2373
+ this.logger.info(` - Found ${worktreesToUpdate.length} worktrees behind their upstream branches.`);
2374
+ const updateLimit = pLimit(
2375
+ this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES
2376
+ );
2377
+ const updateResults = await Promise.allSettled(
2378
+ worktreesToUpdate.map(
2379
+ (worktree) => updateLimit(async () => {
1968
2380
  try {
1969
- await this.handleDivergedBranch(worktree);
1970
- } catch (divergedError) {
1971
- console.error(` \u274C Failed to handle diverged branch '${worktree.branch}':`, divergedError);
2381
+ this.logger.info(` - Updating worktree '${worktree.branch}'...`);
2382
+ await this.gitService.updateWorktree(worktree.path);
2383
+ this.logger.info(` \u2705 Successfully updated '${worktree.branch}'.`);
2384
+ } catch (error) {
2385
+ const errorMessage = getErrorMessage(error);
2386
+ if (ERROR_MESSAGES.FAST_FORWARD_FAILED.some((msg) => errorMessage.includes(msg))) {
2387
+ this.logger.info(
2388
+ ` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`
2389
+ );
2390
+ try {
2391
+ await this.handleDivergedBranch(worktree);
2392
+ } catch (divergedError) {
2393
+ this.logger.error(` \u274C Failed to handle diverged branch '${worktree.branch}':`, divergedError);
2394
+ }
2395
+ } else {
2396
+ this.logger.error(` \u274C Failed to update '${worktree.branch}':`, error);
2397
+ }
2398
+ throw error;
1972
2399
  }
1973
- } else {
1974
- console.error(` \u274C Failed to update '${worktree.branch}':`, error);
1975
- }
1976
- }
1977
- }
2400
+ })
2401
+ )
2402
+ );
2403
+ const successCount = updateResults.filter((r) => r.status === "fulfilled").length;
2404
+ this.logger.info(` Updated ${successCount}/${worktreesToUpdate.length} worktrees successfully`);
1978
2405
  } else {
1979
- console.log(" - All worktrees are up to date.");
2406
+ this.logger.info(" - All worktrees are up to date.");
1980
2407
  }
1981
2408
  }
1982
2409
  async cleanupOrphanedDirectories(worktrees) {
@@ -1994,50 +2421,50 @@ var WorktreeSyncService = class {
1994
2421
  }
1995
2422
  }
1996
2423
  if (orphanedDirs.length > 0) {
1997
- console.log(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
2424
+ this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
1998
2425
  for (const dir of orphanedDirs) {
1999
2426
  const dirPath = path5.join(this.config.worktreeDir, dir);
2000
2427
  try {
2001
2428
  const stat4 = await fs5.stat(dirPath);
2002
2429
  if (stat4.isDirectory()) {
2003
2430
  await fs5.rm(dirPath, { recursive: true, force: true });
2004
- console.log(` - Removed orphaned directory: ${dir}`);
2431
+ this.logger.info(` - Removed orphaned directory: ${dir}`);
2005
2432
  }
2006
2433
  } catch (error) {
2007
- console.error(` - Failed to remove orphaned directory ${dir}:`, error);
2434
+ this.logger.error(` - Failed to remove orphaned directory ${dir}:`, error);
2008
2435
  }
2009
2436
  }
2010
2437
  }
2011
2438
  } catch (error) {
2012
- console.error("Error during orphaned directory cleanup:", error);
2439
+ this.logger.error("Error during orphaned directory cleanup:", error);
2013
2440
  }
2014
2441
  }
2015
2442
  async handleDivergedBranch(worktree) {
2016
- console.log(`\u26A0\uFE0F Branch '${worktree.branch}' has diverged from upstream. Analyzing...`);
2443
+ this.logger.info(`\u26A0\uFE0F Branch '${worktree.branch}' has diverged from upstream. Analyzing...`);
2017
2444
  const treesIdentical = await this.gitService.compareTreeContent(worktree.path, worktree.branch);
2018
2445
  if (treesIdentical) {
2019
- console.log(`\u2705 Branch '${worktree.branch}' was rebased but files are identical. Resetting to upstream...`);
2446
+ this.logger.info(`\u2705 Branch '${worktree.branch}' was rebased but files are identical. Resetting to upstream...`);
2020
2447
  await this.gitService.resetToUpstream(worktree.path, worktree.branch);
2021
- console.log(` Successfully updated '${worktree.branch}' to match upstream.`);
2448
+ this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
2022
2449
  } else {
2023
2450
  const hasLocalChanges = await this.hasLocalChangesSinceLastSync(worktree.path);
2024
2451
  if (!hasLocalChanges) {
2025
- console.log(
2452
+ this.logger.info(
2026
2453
  `\u2705 Branch '${worktree.branch}' has diverged but you made no local changes. Resetting to upstream...`
2027
2454
  );
2028
2455
  await this.gitService.resetToUpstream(worktree.path, worktree.branch);
2029
- console.log(` Successfully updated '${worktree.branch}' to match upstream.`);
2456
+ this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
2030
2457
  } else {
2031
- console.log(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
2458
+ this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
2032
2459
  const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
2033
2460
  const relativePath = path5.relative(process.cwd(), divergedPath);
2034
- console.log(` Moved to: ${relativePath}`);
2035
- console.log(` Your local changes are preserved. To review:`);
2036
- console.log(` cd ${relativePath}`);
2037
- console.log(` git diff origin/${worktree.branch}`);
2461
+ this.logger.info(` Moved to: ${relativePath}`);
2462
+ this.logger.info(` Your local changes are preserved. To review:`);
2463
+ this.logger.info(` cd ${relativePath}`);
2464
+ this.logger.info(` git diff origin/${worktree.branch}`);
2038
2465
  await this.gitService.removeWorktree(worktree.path);
2039
2466
  await this.gitService.addWorktree(worktree.branch, worktree.path);
2040
- console.log(` Created fresh worktree from upstream at: ${worktree.path}`);
2467
+ this.logger.info(` Created fresh worktree from upstream at: ${worktree.path}`);
2041
2468
  }
2042
2469
  }
2043
2470
  }
@@ -2054,9 +2481,9 @@ var WorktreeSyncService = class {
2054
2481
  }
2055
2482
  }
2056
2483
  async divergeWorktree(worktreePath, branchName) {
2057
- const divergedBaseDir = path5.join(this.config.worktreeDir, ".diverged");
2484
+ const divergedBaseDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
2058
2485
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2059
- const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
2486
+ const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
2060
2487
  const safeBranchName = branchName.replace(/\//g, "-");
2061
2488
  const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
2062
2489
  const divergedPath = path5.join(divergedBaseDir, divergedName);
@@ -2075,7 +2502,7 @@ var WorktreeSyncService = class {
2075
2502
  const metadata = {
2076
2503
  originalBranch: branchName,
2077
2504
  divergedAt: (/* @__PURE__ */ new Date()).toISOString(),
2078
- reason: "diverged-history-with-changes",
2505
+ reason: METADATA_CONSTANTS.DIVERGED_REASON,
2079
2506
  originalPath: worktreePath,
2080
2507
  localCommit: await this.gitService.getCurrentCommit(divergedPath),
2081
2508
  remoteCommit: await this.gitService.getRemoteCommit(`origin/${branchName}`),
@@ -2086,7 +2513,10 @@ var WorktreeSyncService = class {
2086
2513
 
2087
2514
  Original worktree location: ${worktreePath}`
2088
2515
  };
2089
- await fs5.writeFile(path5.join(divergedPath, ".diverged-info.json"), JSON.stringify(metadata, null, 2));
2516
+ await fs5.writeFile(
2517
+ path5.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
2518
+ JSON.stringify(metadata, null, 2)
2519
+ );
2090
2520
  return divergedPath;
2091
2521
  }
2092
2522
  };
@@ -2685,14 +3115,18 @@ async function promptForConfig(partialConfig) {
2685
3115
 
2686
3116
  // src/index.ts
2687
3117
  async function runSingleRepository(config) {
2688
- console.log("\n\u{1F4CB} CLI Command (for future reference):");
2689
- console.log(` ${reconstructCliCommand(config)}`);
2690
- console.log("");
3118
+ const logger = Logger.createDefault(void 0, config.debug);
3119
+ logger.info("\n\u{1F4CB} CLI Command (for future reference):");
3120
+ logger.info(` ${reconstructCliCommand(config)}`);
3121
+ logger.info("");
3122
+ if (!config.logger) {
3123
+ config.logger = logger;
3124
+ }
2691
3125
  const syncService = new WorktreeSyncService(config);
2692
3126
  try {
2693
3127
  await syncService.initialize();
2694
3128
  if (config.runOnce) {
2695
- console.log("Running the sync process once as requested by --runOnce flag.");
3129
+ logger.info("Running the sync process once as requested by --runOnce flag.");
2696
3130
  await syncService.sync();
2697
3131
  } else {
2698
3132
  const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
@@ -2706,37 +3140,66 @@ async function runSingleRepository(config) {
2706
3140
  uiService.updateLastSyncTime();
2707
3141
  void uiService.calculateAndUpdateDiskSpace();
2708
3142
  } catch (error) {
2709
- console.error(`Error during scheduled sync: ${error.message}`);
3143
+ logger.error(`Error during scheduled sync: ${error.message}`, error);
2710
3144
  uiService.setStatus("idle");
2711
3145
  }
2712
3146
  });
2713
3147
  }
2714
3148
  } catch (error) {
2715
- console.error("\u274C Fatal Error during initialization:", error.message);
3149
+ logger.error("\u274C Fatal Error during initialization:", error);
2716
3150
  process.exit(1);
2717
3151
  }
2718
3152
  }
2719
- async function runMultipleRepositories(repositories, runOnce, configPath) {
3153
+ async function runMultipleRepositories(repositories, runOnce, configPath, maxParallel) {
2720
3154
  const services = /* @__PURE__ */ new Map();
2721
- console.log(`
3155
+ const globalLogger = Logger.createDefault();
3156
+ globalLogger.info(`
2722
3157
  \u{1F504} Syncing ${repositories.length} repositories...`);
2723
- for (const repoConfig of repositories) {
2724
- console.log(`
3158
+ const limit = pLimit2(maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
3159
+ const initResults = await Promise.allSettled(
3160
+ repositories.map(
3161
+ (repoConfig) => limit(async () => {
3162
+ const repoLogger = Logger.createDefault(repoConfig.name, repoConfig.debug);
3163
+ repoLogger.info(`
2725
3164
  \u{1F4E6} Repository: ${repoConfig.name}`);
2726
- console.log(` URL: ${repoConfig.repoUrl}`);
2727
- console.log(` Worktrees: ${repoConfig.worktreeDir}`);
2728
- if (repoConfig.bareRepoDir) {
2729
- console.log(` Bare repo: ${repoConfig.bareRepoDir}`);
2730
- }
2731
- const syncService = new WorktreeSyncService(repoConfig);
2732
- services.set(repoConfig.name, syncService);
2733
- try {
2734
- await syncService.initialize();
2735
- await syncService.sync();
2736
- } catch (error) {
2737
- console.error(`\u274C Error syncing repository '${repoConfig.name}':`, error.message);
3165
+ repoLogger.info(` URL: ${repoConfig.repoUrl}`);
3166
+ repoLogger.info(` Worktrees: ${repoConfig.worktreeDir}`);
3167
+ if (repoConfig.bareRepoDir) {
3168
+ repoLogger.info(` Bare repo: ${repoConfig.bareRepoDir}`);
3169
+ }
3170
+ if (!repoConfig.logger) {
3171
+ repoConfig.logger = repoLogger;
3172
+ }
3173
+ const syncService = new WorktreeSyncService(repoConfig);
3174
+ await syncService.initialize();
3175
+ return { name: repoConfig.name, service: syncService };
3176
+ })
3177
+ )
3178
+ );
3179
+ const servicesToSync = [];
3180
+ for (const result of initResults) {
3181
+ if (result.status === "fulfilled") {
3182
+ services.set(result.value.name, result.value.service);
3183
+ servicesToSync.push(result.value);
3184
+ } else {
3185
+ globalLogger.error(`\u274C Failed to initialize repository:`, result.reason);
2738
3186
  }
2739
3187
  }
3188
+ const syncResults = await Promise.allSettled(
3189
+ servicesToSync.map(
3190
+ ({ name, service }) => limit(async () => {
3191
+ try {
3192
+ await service.sync();
3193
+ } catch (error) {
3194
+ globalLogger.error(`\u274C Error syncing repository '${name}':`, error);
3195
+ throw error;
3196
+ }
3197
+ })
3198
+ )
3199
+ );
3200
+ const successCount = syncResults.filter((r) => r.status === "fulfilled").length;
3201
+ globalLogger.info(`
3202
+ \u2705 Successfully synced ${successCount}/${servicesToSync.length} repositories`);
2740
3203
  if (!runOnce) {
2741
3204
  const uniqueSchedules = [...new Set(repositories.map((r) => r.cronSchedule))];
2742
3205
  const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
@@ -2753,25 +3216,29 @@ async function runMultipleRepositories(repositories, runOnce, configPath) {
2753
3216
  cron2.schedule(repoConfig.cronSchedule, async () => {
2754
3217
  const reposToSync = repositories.filter((r) => r.cronSchedule === repoConfig.cronSchedule);
2755
3218
  uiService.setStatus("syncing");
2756
- for (const repo of reposToSync) {
2757
- const service = services.get(repo.name);
2758
- if (!service) continue;
2759
- console.log(`Running scheduled sync for: ${repo.name}`);
2760
- try {
2761
- await service.sync();
2762
- } catch (error) {
2763
- console.error(`Error syncing '${repo.name}': ${error.message}`);
2764
- }
2765
- }
3219
+ await Promise.allSettled(
3220
+ reposToSync.map(
3221
+ (repo) => limit(async () => {
3222
+ const service = services.get(repo.name);
3223
+ if (!service) return;
3224
+ globalLogger.info(`Running scheduled sync for: ${repo.name}`);
3225
+ try {
3226
+ await service.sync();
3227
+ } catch (error) {
3228
+ globalLogger.error(`Error syncing '${repo.name}':`, error);
3229
+ }
3230
+ })
3231
+ )
3232
+ );
2766
3233
  uiService.updateLastSyncTime();
2767
3234
  void uiService.calculateAndUpdateDiskSpace();
2768
3235
  });
2769
3236
  }
2770
3237
  }
2771
- console.log(`All ${repositories.length} repositories scheduled`);
3238
+ globalLogger.info(`All ${repositories.length} repositories scheduled`);
2772
3239
  for (const [schedule3] of cronJobs) {
2773
3240
  const repoCount = repositories.filter((r) => r.cronSchedule === schedule3).length;
2774
- console.log(`${schedule3}: ${repoCount} repository(ies)`);
3241
+ globalLogger.info(`${schedule3}: ${repoCount} repository(ies)`);
2775
3242
  }
2776
3243
  }
2777
3244
  }
@@ -2844,7 +3311,8 @@ async function main() {
2844
3311
  debug: true
2845
3312
  }));
2846
3313
  }
2847
- await runMultipleRepositories(repositories, globalRunOnce, options.config);
3314
+ const maxParallel = configFile.parallelism?.maxRepositories ?? configFile.defaults?.parallelism?.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
3315
+ await runMultipleRepositories(repositories, globalRunOnce, options.config, maxParallel);
2848
3316
  } catch (error) {
2849
3317
  if (error instanceof Error && error.message.includes("Config file not found")) {
2850
3318
  console.error(`