sync-worktrees 3.6.1 → 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 +58 -41
- package/dist/index.js.map +2 -2
- package/dist/mcp-server.js +146 -122
- package/dist/mcp-server.js.map +2 -2
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -2538,7 +2538,7 @@ var GitService = class {
|
|
|
2538
2538
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
2539
2539
|
const checkRef = async (ref) => {
|
|
2540
2540
|
try {
|
|
2541
|
-
await bareGit.raw(["show-ref", "--verify",
|
|
2541
|
+
await bareGit.raw(["show-ref", "--verify", ref]);
|
|
2542
2542
|
return true;
|
|
2543
2543
|
} catch {
|
|
2544
2544
|
return false;
|
|
@@ -2555,10 +2555,22 @@ var GitService = class {
|
|
|
2555
2555
|
const branches = await bareGit.branch();
|
|
2556
2556
|
return branches.all;
|
|
2557
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
|
+
}
|
|
2558
2569
|
async createBranch(branchName, baseBranch) {
|
|
2559
2570
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
2560
|
-
await
|
|
2561
|
-
|
|
2571
|
+
const baseRef = await this.resolveCreateBranchBaseRef(bareGit, baseBranch);
|
|
2572
|
+
await bareGit.raw(["branch", branchName, baseRef]);
|
|
2573
|
+
this.logger.info(`Created branch '${branchName}' from '${baseRef}'`);
|
|
2562
2574
|
}
|
|
2563
2575
|
async pushBranch(branchName) {
|
|
2564
2576
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
@@ -2670,17 +2682,9 @@ var WorktreeSyncService = class {
|
|
|
2670
2682
|
this.progressListeners.add(listener);
|
|
2671
2683
|
return () => this.progressListeners.delete(listener);
|
|
2672
2684
|
}
|
|
2673
|
-
|
|
2674
|
-
for (const listener of this.progressListeners) {
|
|
2675
|
-
try {
|
|
2676
|
-
listener(event);
|
|
2677
|
-
} catch {
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
async sync() {
|
|
2685
|
+
async runExclusiveRepoOperation(operation) {
|
|
2682
2686
|
if (this.syncInProgress) {
|
|
2683
|
-
this.logger.warn("\u26A0\uFE0F
|
|
2687
|
+
this.logger.warn("\u26A0\uFE0F Another repository operation is already in progress, skipping...");
|
|
2684
2688
|
return { started: false, reason: "in_progress" };
|
|
2685
2689
|
}
|
|
2686
2690
|
const release = await this.acquireBareLock();
|
|
@@ -2689,36 +2693,55 @@ var WorktreeSyncService = class {
|
|
|
2689
2693
|
return { started: false, reason: "locked" };
|
|
2690
2694
|
}
|
|
2691
2695
|
this.syncInProgress = true;
|
|
2692
|
-
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
|
|
2693
|
-
const totalTimer = new Timer();
|
|
2694
|
-
const phaseTimer = new PhaseTimer();
|
|
2695
|
-
const syncContext = { lfsSkipEnabled: false };
|
|
2696
|
-
const retryOptions = this.createRetryOptions(syncContext);
|
|
2697
2696
|
try {
|
|
2698
|
-
|
|
2699
|
-
} catch (error) {
|
|
2700
|
-
this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
|
|
2701
|
-
throw error;
|
|
2697
|
+
return { started: true, value: await operation() };
|
|
2702
2698
|
} finally {
|
|
2703
|
-
if (syncContext.lfsSkipEnabled && !this.config.skipLfs) {
|
|
2704
|
-
this.gitService.setLfsSkipEnabled(false);
|
|
2705
|
-
}
|
|
2706
2699
|
this.syncInProgress = false;
|
|
2707
2700
|
try {
|
|
2708
2701
|
await release();
|
|
2709
2702
|
} catch (releaseError) {
|
|
2710
2703
|
this.logger.warn(`Failed to release sync lock: ${getErrorMessage(releaseError)}`);
|
|
2711
2704
|
}
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
emitProgress(event) {
|
|
2708
|
+
for (const listener of this.progressListeners) {
|
|
2709
|
+
try {
|
|
2710
|
+
listener(event);
|
|
2711
|
+
} catch {
|
|
2719
2712
|
}
|
|
2720
2713
|
}
|
|
2721
|
-
|
|
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;
|
|
2722
2745
|
}
|
|
2723
2746
|
async acquireBareLock() {
|
|
2724
2747
|
if (process.env.NODE_ENV === ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
@@ -2730,15 +2753,9 @@ var WorktreeSyncService = class {
|
|
|
2730
2753
|
};
|
|
2731
2754
|
}
|
|
2732
2755
|
const barePath = this.gitService.getBareRepoPath();
|
|
2733
|
-
|
|
2734
|
-
try {
|
|
2735
|
-
await fs6.access(lockTarget);
|
|
2736
|
-
} catch {
|
|
2737
|
-
return async () => {
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2756
|
+
await fs6.mkdir(barePath, { recursive: true });
|
|
2740
2757
|
try {
|
|
2741
|
-
const release = await lockfile.lock(
|
|
2758
|
+
const release = await lockfile.lock(barePath, {
|
|
2742
2759
|
stale: DEFAULT_CONFIG.LOCK_STALE_MS,
|
|
2743
2760
|
update: DEFAULT_CONFIG.LOCK_UPDATE_MS,
|
|
2744
2761
|
retries: 0,
|
|
@@ -4001,21 +4018,11 @@ function ensureCapability(discovered, key, toolName) {
|
|
|
4001
4018
|
throw new CapabilityUnavailableError(toolName, reasons);
|
|
4002
4019
|
}
|
|
4003
4020
|
}
|
|
4004
|
-
async function ensureNotSyncing(ctx, repoName) {
|
|
4005
|
-
const entry = ctx.getEntry(repoName);
|
|
4006
|
-
if (!entry?.service) return;
|
|
4007
|
-
if (entry.service.isSyncInProgress()) {
|
|
4008
|
-
throw new SyncInProgressError(entry.name);
|
|
4009
|
-
}
|
|
4010
|
-
}
|
|
4011
4021
|
async function getReadyService(ctx, repoName, options = {}) {
|
|
4012
4022
|
const discovered = ctx.getDiscoveredContext(repoName);
|
|
4013
4023
|
if (options.capability && options.toolName) {
|
|
4014
4024
|
ensureCapability(discovered, options.capability, options.toolName);
|
|
4015
4025
|
}
|
|
4016
|
-
if (options.ensureNotSyncing) {
|
|
4017
|
-
await ensureNotSyncing(ctx, repoName);
|
|
4018
|
-
}
|
|
4019
4026
|
const service = await ctx.getService(repoName);
|
|
4020
4027
|
if (options.ensureInitialized && !service.isInitialized()) {
|
|
4021
4028
|
await service.initialize();
|
|
@@ -4026,6 +4033,14 @@ async function getReadyService(ctx, repoName, options = {}) {
|
|
|
4026
4033
|
git: service.getGitService()
|
|
4027
4034
|
};
|
|
4028
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
|
+
}
|
|
4029
4044
|
async function ensureRepoWorktreePath(ctx, params, git) {
|
|
4030
4045
|
await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
|
|
4031
4046
|
return path10.resolve(params.path);
|
|
@@ -4137,69 +4152,74 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
4137
4152
|
}
|
|
4138
4153
|
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4139
4154
|
capability: "createWorktree",
|
|
4140
|
-
toolName: "create_worktree"
|
|
4141
|
-
ensureInitialized: true,
|
|
4142
|
-
ensureNotSyncing: true
|
|
4155
|
+
toolName: "create_worktree"
|
|
4143
4156
|
});
|
|
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
|
-
|
|
4174
|
-
|
|
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
|
+
});
|
|
4175
4193
|
});
|
|
4176
4194
|
}
|
|
4177
4195
|
async function handleRemoveWorktree(ctx, params, _extra) {
|
|
4178
|
-
const { git } = await getReadyService(ctx, params.repoName, {
|
|
4196
|
+
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4179
4197
|
capability: "removeWorktree",
|
|
4180
|
-
toolName: "remove_worktree"
|
|
4181
|
-
ensureInitialized: true,
|
|
4182
|
-
ensureNotSyncing: true
|
|
4198
|
+
toolName: "remove_worktree"
|
|
4183
4199
|
});
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
if (!status.canRemove) {
|
|
4188
|
-
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();
|
|
4189
4203
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
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
|
+
});
|
|
4196
4217
|
});
|
|
4197
4218
|
}
|
|
4198
4219
|
async function handleSync(ctx, params, extra) {
|
|
4199
4220
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
4200
4221
|
capability: "sync",
|
|
4201
|
-
toolName: "sync"
|
|
4202
|
-
ensureInitialized: true
|
|
4222
|
+
toolName: "sync"
|
|
4203
4223
|
});
|
|
4204
4224
|
const dispose = attachProgressReporter(service, extra);
|
|
4205
4225
|
try {
|
|
@@ -4216,35 +4236,39 @@ async function handleSync(ctx, params, extra) {
|
|
|
4216
4236
|
}
|
|
4217
4237
|
}
|
|
4218
4238
|
async function handleUpdateWorktree(ctx, params, _extra) {
|
|
4219
|
-
const { git } = await getReadyService(ctx, params.repoName, {
|
|
4239
|
+
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4220
4240
|
capability: "updateWorktree",
|
|
4221
|
-
toolName: "update_worktree"
|
|
4222
|
-
ensureInitialized: true,
|
|
4223
|
-
ensureNotSyncing: true
|
|
4241
|
+
toolName: "update_worktree"
|
|
4224
4242
|
});
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
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
|
+
});
|
|
4231
4254
|
});
|
|
4232
4255
|
}
|
|
4233
4256
|
async function handleInitialize(ctx, params, extra) {
|
|
4234
4257
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
4235
4258
|
capability: "initialize",
|
|
4236
|
-
toolName: "initialize"
|
|
4237
|
-
ensureNotSyncing: true
|
|
4259
|
+
toolName: "initialize"
|
|
4238
4260
|
});
|
|
4239
4261
|
const dispose = attachProgressReporter(service, extra);
|
|
4240
4262
|
try {
|
|
4241
|
-
await service
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
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
|
+
});
|
|
4248
4272
|
});
|
|
4249
4273
|
} finally {
|
|
4250
4274
|
dispose();
|
|
@@ -4497,7 +4521,7 @@ function createServer(context, snapshot) {
|
|
|
4497
4521
|
server.registerTool(
|
|
4498
4522
|
"load_config",
|
|
4499
4523
|
{
|
|
4500
|
-
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 }] }.",
|
|
4501
4525
|
inputSchema: {
|
|
4502
4526
|
configPath: z.string().optional().describe(
|
|
4503
4527
|
"Path to the config file. If omitted, falls back to the SYNC_WORKTREES_CONFIG env var. Errors if neither is set."
|