sync-worktrees 4.0.0 → 4.2.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.
@@ -6,7 +6,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  // src/mcp/context.ts
7
7
  import * as fs10 from "fs/promises";
8
8
  import * as path14 from "path";
9
- import pLimit2 from "p-limit";
9
+ import pLimit3 from "p-limit";
10
10
  import simpleGit6 from "simple-git";
11
11
 
12
12
  // src/constants.ts
@@ -127,6 +127,8 @@ var SyncWorktreesError = class extends Error {
127
127
  Caused by: ${cause.stack}`;
128
128
  }
129
129
  }
130
+ code;
131
+ cause;
130
132
  };
131
133
  var GitError = class extends SyncWorktreesError {
132
134
  constructor(message, code, cause) {
@@ -149,6 +151,8 @@ var WorktreeNotCleanError = class extends WorktreeError {
149
151
  this.path = path16;
150
152
  this.reasons = reasons;
151
153
  }
154
+ path;
155
+ reasons;
152
156
  };
153
157
  var ConfigError = class extends SyncWorktreesError {
154
158
  constructor(message, code, cause) {
@@ -161,12 +165,15 @@ var ConfigValidationError = class extends ConfigError {
161
165
  this.field = field;
162
166
  this.reason = reason;
163
167
  }
168
+ field;
169
+ reason;
164
170
  };
165
171
  var ConfigFileNotFoundError = class extends ConfigError {
166
172
  constructor(configPath) {
167
173
  super(`Config file not found: ${configPath}`, "FILE_NOT_FOUND");
168
174
  this.configPath = configPath;
169
175
  }
176
+ configPath;
170
177
  };
171
178
 
172
179
  // src/utils/branch-filter.ts
@@ -886,6 +893,9 @@ function defaultConsoleOutput(msg, level) {
886
893
  else console.log(msg);
887
894
  }
888
895
 
896
+ // src/services/worktree-sync.service.ts
897
+ import pLimit2 from "p-limit";
898
+
889
899
  // src/utils/lfs-error.ts
890
900
  function getErrorMessage(error) {
891
901
  if (error instanceof Error) {
@@ -1110,7 +1120,7 @@ function makeGitProgressHandler(logger, emitProgress) {
1110
1120
  lastBucket.set(key, bucket);
1111
1121
  const total = event.total > 0 ? `${event.processed}/${event.total}` : `${event.processed}`;
1112
1122
  const message = `${event.method} ${event.stage}: ${event.progress}% (${total})`;
1113
- logger.info(` \u21B3 ${message}`);
1123
+ logger.debug(` \u21B3 ${message}`);
1114
1124
  emitProgress?.({
1115
1125
  phase: event.method,
1116
1126
  message,
@@ -1316,6 +1326,7 @@ var SyncOutcomeAccumulator = class {
1316
1326
  constructor(options) {
1317
1327
  this.options = options;
1318
1328
  }
1329
+ options;
1319
1330
  counts = cloneCounts(EMPTY_COUNTS);
1320
1331
  actions = [];
1321
1332
  add(action) {
@@ -1402,6 +1413,9 @@ var CloneSyncService = class {
1402
1413
  this.progressEmitter = options.progressEmitter;
1403
1414
  this.onSkip = options.onSkip;
1404
1415
  }
1416
+ config;
1417
+ gitService;
1418
+ logger;
1405
1419
  initialized = false;
1406
1420
  resolvedBranch = null;
1407
1421
  branchCreatedActions;
@@ -2408,6 +2422,7 @@ var WorktreeStatusService = class {
2408
2422
  this.config = config;
2409
2423
  this.logger = logger ?? Logger.createDefault();
2410
2424
  }
2425
+ config;
2411
2426
  gitInstances = /* @__PURE__ */ new Map();
2412
2427
  logger;
2413
2428
  async checkWorktreeStatus(worktreePath) {
@@ -2756,8 +2771,9 @@ function sanitizeGitEnv(env) {
2756
2771
  return sanitized;
2757
2772
  }
2758
2773
  var GitService = class {
2759
- constructor(config, logger) {
2774
+ constructor(config, logger, progressEmitter) {
2760
2775
  this.config = config;
2776
+ this.progressEmitter = progressEmitter;
2761
2777
  this.logger = logger ?? Logger.createDefault(void 0, config.debug);
2762
2778
  this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
2763
2779
  this.mainWorktreePath = path8.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
@@ -2765,6 +2781,8 @@ var GitService = class {
2765
2781
  this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs }, this.logger);
2766
2782
  this.sparseCheckoutService = new SparseCheckoutService(this.logger);
2767
2783
  }
2784
+ config;
2785
+ progressEmitter;
2768
2786
  git = null;
2769
2787
  bareRepoPath;
2770
2788
  mainWorktreePath;
@@ -2798,7 +2816,9 @@ var GitService = class {
2798
2816
  return git;
2799
2817
  }
2800
2818
  buildSimpleGitOptions(blockMs) {
2801
- const options = { progress: makeGitProgressHandler(this.logger) };
2819
+ const options = {
2820
+ progress: makeGitProgressHandler(this.logger, (event) => this.progressEmitter?.(event))
2821
+ };
2802
2822
  if (blockMs > 0) options.timeout = { block: blockMs };
2803
2823
  return options;
2804
2824
  }
@@ -3657,6 +3677,9 @@ var RepoOperationLock = class {
3657
3677
  this.gitService = gitService;
3658
3678
  this.logger = logger;
3659
3679
  }
3680
+ config;
3681
+ gitService;
3682
+ logger;
3660
3683
  updateLogger(logger) {
3661
3684
  this.logger = logger;
3662
3685
  }
@@ -3718,6 +3741,9 @@ var SyncRetryPolicy = class {
3718
3741
  this.gitService = gitService;
3719
3742
  this.logger = logger;
3720
3743
  }
3744
+ config;
3745
+ gitService;
3746
+ logger;
3721
3747
  updateLogger(logger) {
3722
3748
  this.logger = logger;
3723
3749
  }
@@ -3938,6 +3964,10 @@ var WorktreeModeSyncRunner = class {
3938
3964
  this.logger = logger;
3939
3965
  this.progressEmitter = progressEmitter;
3940
3966
  }
3967
+ config;
3968
+ gitService;
3969
+ logger;
3970
+ progressEmitter;
3941
3971
  pathResolution = new PathResolutionService();
3942
3972
  updateLogger(logger) {
3943
3973
  this.logger = logger;
@@ -4618,7 +4648,7 @@ var WorktreeSyncService = class {
4618
4648
  constructor(config) {
4619
4649
  this.config = config;
4620
4650
  this.logger = config.logger ?? Logger.createDefault(void 0, config.debug);
4621
- this.gitService = new GitService(config, this.logger);
4651
+ this.gitService = new GitService(config, this.logger, (event) => this.emitProgress(event));
4622
4652
  this.repoOperationLock = new RepoOperationLock(config, this.gitService, this.logger);
4623
4653
  this.retryPolicy = new SyncRetryPolicy(config, this.gitService, this.logger);
4624
4654
  this.worktreeModeSyncRunner = new WorktreeModeSyncRunner(
@@ -4636,10 +4666,15 @@ var WorktreeSyncService = class {
4636
4666
  });
4637
4667
  }
4638
4668
  }
4669
+ config;
4639
4670
  gitService;
4640
4671
  cloneSyncService = null;
4641
4672
  logger;
4642
- syncInProgress = false;
4673
+ // In-process FIFO serializer for all bare-repo-mutating operations (sync, init,
4674
+ // interactive create). One per repo. wait:true callers queue behind an in-flight op;
4675
+ // wait:false callers fail fast. The cross-process file lock (RepoOperationLock) is
4676
+ // acquired inside the mutex body for multi-process safety.
4677
+ repoMutex = pLimit2(1);
4643
4678
  progressEmitter = new ProgressEmitter();
4644
4679
  repoOperationLock;
4645
4680
  retryPolicy;
@@ -4691,7 +4726,7 @@ var WorktreeSyncService = class {
4691
4726
  return this.gitService.isInitialized();
4692
4727
  }
4693
4728
  isSyncInProgress() {
4694
- return this.syncInProgress;
4729
+ return this.repoMutex.activeCount + this.repoMutex.pendingCount > 0;
4695
4730
  }
4696
4731
  getGitService() {
4697
4732
  return this.gitService;
@@ -4707,34 +4742,31 @@ var WorktreeSyncService = class {
4707
4742
  onProgress(listener) {
4708
4743
  return this.progressEmitter.onProgress(listener);
4709
4744
  }
4710
- async runExclusiveRepoOperation(operation) {
4711
- if (this.syncInProgress) {
4745
+ async runExclusiveRepoOperation(operation, options = {}) {
4746
+ if (!options.wait && this.repoMutex.activeCount + this.repoMutex.pendingCount > 0) {
4712
4747
  this.logger.warn("\u26A0\uFE0F Another repository operation is already in progress, skipping...");
4713
4748
  return { started: false, reason: "in_progress" };
4714
4749
  }
4715
- this.syncInProgress = true;
4716
- let release;
4717
- try {
4718
- release = await this.repoOperationLock.acquire();
4719
- } catch (error) {
4720
- this.syncInProgress = false;
4721
- throw error;
4722
- }
4723
- if (release === null) {
4724
- this.syncInProgress = false;
4725
- this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
4726
- return { started: false, reason: "locked" };
4727
- }
4728
- try {
4729
- return { started: true, value: await operation() };
4730
- } finally {
4750
+ return this.repoMutex(async () => {
4751
+ const release = await this.repoOperationLock.acquire();
4752
+ if (release === null) {
4753
+ this.logger.warn("\u26A0\uFE0F Another process holds the sync lock for this repo, skipping...");
4754
+ return { started: false, reason: "locked" };
4755
+ }
4731
4756
  try {
4732
- await release();
4733
- } catch (releaseError) {
4734
- this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
4757
+ return { started: true, value: await operation() };
4758
+ } finally {
4759
+ try {
4760
+ await release();
4761
+ } catch (releaseError) {
4762
+ this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
4763
+ }
4735
4764
  }
4736
- this.syncInProgress = false;
4737
- }
4765
+ });
4766
+ }
4767
+ // Interactive variant: queues behind any in-flight sync/op instead of failing fast.
4768
+ async runQueuedRepoOperation(operation) {
4769
+ return this.runExclusiveRepoOperation(operation, { wait: true });
4738
4770
  }
4739
4771
  emitProgress(event) {
4740
4772
  this.progressEmitter.emit(event);
@@ -5337,7 +5369,7 @@ var RepositoryContext = class {
5337
5369
  if (!options.detailed) {
5338
5370
  return entries.map(buildLean);
5339
5371
  }
5340
- const limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
5372
+ const limit = pLimit3(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
5341
5373
  return Promise.all(
5342
5374
  entries.map(
5343
5375
  (entry) => limit(async () => {
@@ -5566,7 +5598,7 @@ import { z } from "zod";
5566
5598
 
5567
5599
  // src/mcp/handlers.ts
5568
5600
  import * as path15 from "path";
5569
- import pLimit3 from "p-limit";
5601
+ import pLimit4 from "p-limit";
5570
5602
 
5571
5603
  // src/utils/disk-space.ts
5572
5604
  import fastFolderSize from "fast-folder-size";
@@ -5813,7 +5845,7 @@ async function handleDetectContext(ctx, params, _extra) {
5813
5845
  return formatToolResponse(response);
5814
5846
  }
5815
5847
  const statusService = new WorktreeStatusService();
5816
- const statusLimit = pLimit3(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
5848
+ const statusLimit = pLimit4(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
5817
5849
  const enriched = await enrichDetectedWorktrees(response.allWorktrees, statusService, statusLimit);
5818
5850
  let allWorktreesByRepo = response.allWorktreesByRepo;
5819
5851
  if (allWorktreesByRepo) {
@@ -5849,8 +5881,8 @@ async function enrichDetectedWorktrees(worktrees, statusService, limit) {
5849
5881
  async function handleListWorktrees(ctx, params, _extra) {
5850
5882
  const configuredRepoNames = params.repoName ? [] : ctx.getConfiguredRepositoryNames();
5851
5883
  if (configuredRepoNames.length > 0) {
5852
- const limit = pLimit3(DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
5853
- const statusLimit = pLimit3(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
5884
+ const limit = pLimit4(DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
5885
+ const statusLimit = pLimit4(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
5854
5886
  const repositories = await Promise.all(
5855
5887
  configuredRepoNames.map(
5856
5888
  (repoName) => limit(async () => {
@@ -5878,7 +5910,7 @@ async function handleListWorktrees(ctx, params, _extra) {
5878
5910
  const results = await listWorktreesForRepo(ctx, params.repoName, params.includeSize);
5879
5911
  return formatToolResponse({ worktrees: results });
5880
5912
  }
5881
- async function listWorktreesForRepo(ctx, repoName, includeSize, limit = pLimit3(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS)) {
5913
+ async function listWorktreesForRepo(ctx, repoName, includeSize, limit = pLimit4(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS)) {
5882
5914
  const { discovered, service, git } = await getReadyService(ctx, repoName, {
5883
5915
  capability: "listWorktrees",
5884
5916
  toolName: "list_worktrees"