sync-worktrees 3.6.0 → 3.6.2
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 +60 -42
- package/dist/index.js.map +2 -2
- package/dist/mcp-server.js +148 -123
- package/dist/mcp-server.js.map +2 -2
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -1485,10 +1485,11 @@ var WorktreeStatusService = class {
|
|
|
1485
1485
|
const reasons = [];
|
|
1486
1486
|
if (!isClean) reasons.push("uncommitted changes");
|
|
1487
1487
|
if (hasUnpushedCommits) reasons.push("unpushed commits");
|
|
1488
|
+
if (hasStashedChanges) reasons.push("stashed changes");
|
|
1488
1489
|
if (hasOperationInProgress) reasons.push("operation in progress");
|
|
1489
1490
|
if (hasModifiedSubmodules) reasons.push("modified submodules");
|
|
1490
1491
|
if (upstreamGone) reasons.push("upstream gone");
|
|
1491
|
-
const canRemove = isClean && !hasUnpushedCommits && !hasOperationInProgress && !hasModifiedSubmodules;
|
|
1492
|
+
const canRemove = isClean && !hasUnpushedCommits && !hasStashedChanges && !hasOperationInProgress && !hasModifiedSubmodules;
|
|
1492
1493
|
const details = includeDetails ? this.buildStatusDetails(snap) : void 0;
|
|
1493
1494
|
return {
|
|
1494
1495
|
isClean,
|
|
@@ -2537,7 +2538,7 @@ var GitService = class {
|
|
|
2537
2538
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
2538
2539
|
const checkRef = async (ref) => {
|
|
2539
2540
|
try {
|
|
2540
|
-
await bareGit.raw(["show-ref", "--verify",
|
|
2541
|
+
await bareGit.raw(["show-ref", "--verify", ref]);
|
|
2541
2542
|
return true;
|
|
2542
2543
|
} catch {
|
|
2543
2544
|
return false;
|
|
@@ -2554,10 +2555,22 @@ var GitService = class {
|
|
|
2554
2555
|
const branches = await bareGit.branch();
|
|
2555
2556
|
return branches.all;
|
|
2556
2557
|
}
|
|
2558
|
+
async resolveCreateBranchBaseRef(bareGit, baseBranch) {
|
|
2559
|
+
const candidates = baseBranch.startsWith(GIT_CONSTANTS.REMOTE_PREFIX) || baseBranch.startsWith("refs/") ? [baseBranch] : [`${GIT_CONSTANTS.REMOTE_PREFIX}${baseBranch}`, baseBranch];
|
|
2560
|
+
for (const candidate of candidates) {
|
|
2561
|
+
try {
|
|
2562
|
+
await bareGit.revparse(["--verify", candidate]);
|
|
2563
|
+
return candidate;
|
|
2564
|
+
} catch {
|
|
2565
|
+
}
|
|
2566
|
+
}
|
|
2567
|
+
return candidates[0];
|
|
2568
|
+
}
|
|
2557
2569
|
async createBranch(branchName, baseBranch) {
|
|
2558
2570
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
2559
|
-
await
|
|
2560
|
-
|
|
2571
|
+
const baseRef = await this.resolveCreateBranchBaseRef(bareGit, baseBranch);
|
|
2572
|
+
await bareGit.raw(["branch", branchName, baseRef]);
|
|
2573
|
+
this.logger.info(`Created branch '${branchName}' from '${baseRef}'`);
|
|
2561
2574
|
}
|
|
2562
2575
|
async pushBranch(branchName) {
|
|
2563
2576
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
@@ -2669,17 +2682,9 @@ var WorktreeSyncService = class {
|
|
|
2669
2682
|
this.progressListeners.add(listener);
|
|
2670
2683
|
return () => this.progressListeners.delete(listener);
|
|
2671
2684
|
}
|
|
2672
|
-
|
|
2673
|
-
for (const listener of this.progressListeners) {
|
|
2674
|
-
try {
|
|
2675
|
-
listener(event);
|
|
2676
|
-
} catch {
|
|
2677
|
-
}
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
async sync() {
|
|
2685
|
+
async runExclusiveRepoOperation(operation) {
|
|
2681
2686
|
if (this.syncInProgress) {
|
|
2682
|
-
this.logger.warn("\u26A0\uFE0F
|
|
2687
|
+
this.logger.warn("\u26A0\uFE0F Another repository operation is already in progress, skipping...");
|
|
2683
2688
|
return { started: false, reason: "in_progress" };
|
|
2684
2689
|
}
|
|
2685
2690
|
const release = await this.acquireBareLock();
|
|
@@ -2688,36 +2693,55 @@ var WorktreeSyncService = class {
|
|
|
2688
2693
|
return { started: false, reason: "locked" };
|
|
2689
2694
|
}
|
|
2690
2695
|
this.syncInProgress = true;
|
|
2691
|
-
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
|
|
2692
|
-
const totalTimer = new Timer();
|
|
2693
|
-
const phaseTimer = new PhaseTimer();
|
|
2694
|
-
const syncContext = { lfsSkipEnabled: false };
|
|
2695
|
-
const retryOptions = this.createRetryOptions(syncContext);
|
|
2696
2696
|
try {
|
|
2697
|
-
|
|
2698
|
-
} catch (error) {
|
|
2699
|
-
this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
|
|
2700
|
-
throw error;
|
|
2697
|
+
return { started: true, value: await operation() };
|
|
2701
2698
|
} finally {
|
|
2702
|
-
if (syncContext.lfsSkipEnabled && !this.config.skipLfs) {
|
|
2703
|
-
this.gitService.setLfsSkipEnabled(false);
|
|
2704
|
-
}
|
|
2705
2699
|
this.syncInProgress = false;
|
|
2706
2700
|
try {
|
|
2707
2701
|
await release();
|
|
2708
2702
|
} catch (releaseError) {
|
|
2709
2703
|
this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
|
|
2710
2704
|
}
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
emitProgress(event) {
|
|
2708
|
+
for (const listener of this.progressListeners) {
|
|
2709
|
+
try {
|
|
2710
|
+
listener(event);
|
|
2711
|
+
} catch {
|
|
2718
2712
|
}
|
|
2719
2713
|
}
|
|
2720
|
-
|
|
2714
|
+
}
|
|
2715
|
+
async sync() {
|
|
2716
|
+
const result = await this.runExclusiveRepoOperation(async () => {
|
|
2717
|
+
if (!this.isInitialized()) {
|
|
2718
|
+
await this.initialize();
|
|
2719
|
+
}
|
|
2720
|
+
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
|
|
2721
|
+
const totalTimer = new Timer();
|
|
2722
|
+
const phaseTimer = new PhaseTimer();
|
|
2723
|
+
const syncContext = { lfsSkipEnabled: false };
|
|
2724
|
+
const retryOptions = this.createRetryOptions(syncContext);
|
|
2725
|
+
try {
|
|
2726
|
+
await retry(() => this.runSyncAttempt(phaseTimer, syncContext), retryOptions);
|
|
2727
|
+
} catch (error) {
|
|
2728
|
+
this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
|
|
2729
|
+
throw error;
|
|
2730
|
+
} finally {
|
|
2731
|
+
if (syncContext.lfsSkipEnabled && !this.config.skipLfs) {
|
|
2732
|
+
this.gitService.setLfsSkipEnabled(false);
|
|
2733
|
+
}
|
|
2734
|
+
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
|
|
2735
|
+
`);
|
|
2736
|
+
if (this.config.debug) {
|
|
2737
|
+
const totalDuration = totalTimer.stop();
|
|
2738
|
+
const phaseResults = phaseTimer.getResults();
|
|
2739
|
+
const repoName = this.config.name;
|
|
2740
|
+
this.logger.table(formatTimingTable(totalDuration, phaseResults, repoName));
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
});
|
|
2744
|
+
return result.started ? { started: true } : result;
|
|
2721
2745
|
}
|
|
2722
2746
|
async acquireBareLock() {
|
|
2723
2747
|
if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -2729,15 +2753,9 @@ var WorktreeSyncService = class {
|
|
|
2729
2753
|
};
|
|
2730
2754
|
}
|
|
2731
2755
|
const barePath = this.gitService.getBareRepoPath();
|
|
2732
|
-
|
|
2733
|
-
try {
|
|
2734
|
-
await fs6.access(lockTarget);
|
|
2735
|
-
} catch {
|
|
2736
|
-
return async () => {
|
|
2737
|
-
};
|
|
2738
|
-
}
|
|
2756
|
+
await fs6.mkdir(barePath, { recursive: true });
|
|
2739
2757
|
try {
|
|
2740
|
-
const release = await lockfile.lock(
|
|
2758
|
+
const release = await lockfile.lock(barePath, {
|
|
2741
2759
|
stale: DEFAULT_CONFIG.LOCK_STALE_MS,
|
|
2742
2760
|
update: DEFAULT_CONFIG.LOCK_UPDATE_MS,
|
|
2743
2761
|
retries: 0,
|
|
@@ -4000,21 +4018,11 @@ function ensureCapability(discovered, key, toolName) {
|
|
|
4000
4018
|
throw new CapabilityUnavailableError(toolName, reasons);
|
|
4001
4019
|
}
|
|
4002
4020
|
}
|
|
4003
|
-
async function ensureNotSyncing(ctx, repoName) {
|
|
4004
|
-
const entry = ctx.getEntry(repoName);
|
|
4005
|
-
if (!entry?.service) return;
|
|
4006
|
-
if (entry.service.isSyncInProgress()) {
|
|
4007
|
-
throw new SyncInProgressError(entry.name);
|
|
4008
|
-
}
|
|
4009
|
-
}
|
|
4010
4021
|
async function getReadyService(ctx, repoName, options = {}) {
|
|
4011
4022
|
const discovered = ctx.getDiscoveredContext(repoName);
|
|
4012
4023
|
if (options.capability && options.toolName) {
|
|
4013
4024
|
ensureCapability(discovered, options.capability, options.toolName);
|
|
4014
4025
|
}
|
|
4015
|
-
if (options.ensureNotSyncing) {
|
|
4016
|
-
await ensureNotSyncing(ctx, repoName);
|
|
4017
|
-
}
|
|
4018
4026
|
const service = await ctx.getService(repoName);
|
|
4019
4027
|
if (options.ensureInitialized && !service.isInitialized()) {
|
|
4020
4028
|
await service.initialize();
|
|
@@ -4025,6 +4033,14 @@ async function getReadyService(ctx, repoName, options = {}) {
|
|
|
4025
4033
|
git: service.getGitService()
|
|
4026
4034
|
};
|
|
4027
4035
|
}
|
|
4036
|
+
async function runExclusiveRepoOperation(ctx, repoName, service, operation) {
|
|
4037
|
+
const result = await service.runExclusiveRepoOperation(operation);
|
|
4038
|
+
if (!result.started) {
|
|
4039
|
+
const name = ctx.getEntry(repoName)?.name ?? repoName ?? "unknown";
|
|
4040
|
+
throw new SyncInProgressError(name);
|
|
4041
|
+
}
|
|
4042
|
+
return result.value;
|
|
4043
|
+
}
|
|
4028
4044
|
async function ensureRepoWorktreePath(ctx, params, git) {
|
|
4029
4045
|
await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
|
|
4030
4046
|
return path10.resolve(params.path);
|
|
@@ -4136,69 +4152,74 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
4136
4152
|
}
|
|
4137
4153
|
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4138
4154
|
capability: "createWorktree",
|
|
4139
|
-
toolName: "create_worktree"
|
|
4140
|
-
ensureInitialized: true,
|
|
4141
|
-
ensureNotSyncing: true
|
|
4155
|
+
toolName: "create_worktree"
|
|
4142
4156
|
});
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
);
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4170
|
-
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4157
|
+
return runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4158
|
+
if (!service.isInitialized()) {
|
|
4159
|
+
await service.initialize();
|
|
4160
|
+
}
|
|
4161
|
+
const existence = await git.branchExists(branchName);
|
|
4162
|
+
let created = false;
|
|
4163
|
+
let pushed = false;
|
|
4164
|
+
if (!existence.local && !existence.remote) {
|
|
4165
|
+
if (!baseBranch) {
|
|
4166
|
+
throw new Error(`Branch '${branchName}' does not exist. Provide 'baseBranch' to create it.`);
|
|
4167
|
+
}
|
|
4168
|
+
await git.createBranch(branchName, baseBranch);
|
|
4169
|
+
created = true;
|
|
4170
|
+
}
|
|
4171
|
+
const worktreeDir = service.config.worktreeDir;
|
|
4172
|
+
const worktreePath = pathResolution.getBranchWorktreePath(worktreeDir, branchName);
|
|
4173
|
+
const existing = await git.getWorktrees();
|
|
4174
|
+
const collision = existing.find((w) => pathsEqual(w.path, worktreePath) && w.branch !== branchName);
|
|
4175
|
+
if (collision) {
|
|
4176
|
+
throw new Error(
|
|
4177
|
+
`Sanitized worktree path '${worktreePath}' collides with existing branch '${collision.branch}'. Rename or remove the conflicting branch first.`
|
|
4178
|
+
);
|
|
4179
|
+
}
|
|
4180
|
+
await git.addWorktree(branchName, worktreePath);
|
|
4181
|
+
ctx.invalidateDiscovered();
|
|
4182
|
+
if (created && push) {
|
|
4183
|
+
await git.pushBranch(branchName);
|
|
4184
|
+
pushed = true;
|
|
4185
|
+
}
|
|
4186
|
+
return formatToolResponse({
|
|
4187
|
+
success: true,
|
|
4188
|
+
branchName,
|
|
4189
|
+
worktreePath: path10.resolve(worktreePath),
|
|
4190
|
+
created,
|
|
4191
|
+
pushed
|
|
4192
|
+
});
|
|
4174
4193
|
});
|
|
4175
4194
|
}
|
|
4176
4195
|
async function handleRemoveWorktree(ctx, params, _extra) {
|
|
4177
|
-
const { git } = await getReadyService(ctx, params.repoName, {
|
|
4196
|
+
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4178
4197
|
capability: "removeWorktree",
|
|
4179
|
-
toolName: "remove_worktree"
|
|
4180
|
-
ensureInitialized: true,
|
|
4181
|
-
ensureNotSyncing: true
|
|
4198
|
+
toolName: "remove_worktree"
|
|
4182
4199
|
});
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
if (!status.canRemove) {
|
|
4187
|
-
throw new Error(`Cannot remove worktree: ${status.reasons.join(", ")}. Use force=true to override.`);
|
|
4200
|
+
return runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4201
|
+
if (!service.isInitialized()) {
|
|
4202
|
+
await service.initialize();
|
|
4188
4203
|
}
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4204
|
+
const removedPath = await ensureRepoWorktreePath(ctx, params, git);
|
|
4205
|
+
if (!params.force) {
|
|
4206
|
+
const status = await git.getFullWorktreeStatus(params.path, false);
|
|
4207
|
+
if (!status.canRemove) {
|
|
4208
|
+
throw new Error(`Cannot remove worktree: ${status.reasons.join(", ")}. Use force=true to override.`);
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
await git.removeWorktree(params.path);
|
|
4212
|
+
ctx.invalidateDiscovered();
|
|
4213
|
+
return formatToolResponse({
|
|
4214
|
+
success: true,
|
|
4215
|
+
removedPath
|
|
4216
|
+
});
|
|
4195
4217
|
});
|
|
4196
4218
|
}
|
|
4197
4219
|
async function handleSync(ctx, params, extra) {
|
|
4198
4220
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
4199
4221
|
capability: "sync",
|
|
4200
|
-
toolName: "sync"
|
|
4201
|
-
ensureInitialized: true
|
|
4222
|
+
toolName: "sync"
|
|
4202
4223
|
});
|
|
4203
4224
|
const dispose = attachProgressReporter(service, extra);
|
|
4204
4225
|
try {
|
|
@@ -4215,35 +4236,39 @@ async function handleSync(ctx, params, extra) {
|
|
|
4215
4236
|
}
|
|
4216
4237
|
}
|
|
4217
4238
|
async function handleUpdateWorktree(ctx, params, _extra) {
|
|
4218
|
-
const { git } = await getReadyService(ctx, params.repoName, {
|
|
4239
|
+
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4219
4240
|
capability: "updateWorktree",
|
|
4220
|
-
toolName: "update_worktree"
|
|
4221
|
-
ensureInitialized: true,
|
|
4222
|
-
ensureNotSyncing: true
|
|
4241
|
+
toolName: "update_worktree"
|
|
4223
4242
|
});
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4243
|
+
return runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4244
|
+
if (!service.isInitialized()) {
|
|
4245
|
+
await service.initialize();
|
|
4246
|
+
}
|
|
4247
|
+
const worktreePath = await ensureRepoWorktreePath(ctx, params, git);
|
|
4248
|
+
await git.updateWorktree(params.path);
|
|
4249
|
+
ctx.invalidateDiscovered();
|
|
4250
|
+
return formatToolResponse({
|
|
4251
|
+
success: true,
|
|
4252
|
+
worktreePath
|
|
4253
|
+
});
|
|
4230
4254
|
});
|
|
4231
4255
|
}
|
|
4232
4256
|
async function handleInitialize(ctx, params, extra) {
|
|
4233
4257
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
4234
4258
|
capability: "initialize",
|
|
4235
|
-
toolName: "initialize"
|
|
4236
|
-
ensureNotSyncing: true
|
|
4259
|
+
toolName: "initialize"
|
|
4237
4260
|
});
|
|
4238
4261
|
const dispose = attachProgressReporter(service, extra);
|
|
4239
4262
|
try {
|
|
4240
|
-
await service
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4263
|
+
return await runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4264
|
+
await service.initialize();
|
|
4265
|
+
const git = service.getGitService();
|
|
4266
|
+
ctx.invalidateDiscovered();
|
|
4267
|
+
return formatToolResponse({
|
|
4268
|
+
success: true,
|
|
4269
|
+
defaultBranch: git.getDefaultBranch(),
|
|
4270
|
+
worktreeDir: service.config.worktreeDir
|
|
4271
|
+
});
|
|
4247
4272
|
});
|
|
4248
4273
|
} finally {
|
|
4249
4274
|
dispose();
|
|
@@ -4496,7 +4521,7 @@ function createServer(context, snapshot) {
|
|
|
4496
4521
|
server.registerTool(
|
|
4497
4522
|
"load_config",
|
|
4498
4523
|
{
|
|
4499
|
-
description: "Load or reload a sync-worktrees JavaScript config file into the server's session. Replaces any previously loaded repositories. Call this before sync/initialize/create_worktree when using a config-driven workflow. Returns: { configPath, currentRepository, repositories: [{ name,
|
|
4524
|
+
description: "Load or reload a sync-worktrees JavaScript config file into the server's session. Replaces any previously loaded repositories. Call this before sync/initialize/create_worktree when using a config-driven workflow. Returns: { configPath, currentRepository, repositories: [{ name, repoUrl, worktreeDir, source }] }.",
|
|
4500
4525
|
inputSchema: {
|
|
4501
4526
|
configPath: z.string().optional().describe(
|
|
4502
4527
|
"Path to the config file. If omitted, falls back to the SYNC_WORKTREES_CONFIG env var. Errors if neither is set."
|