sync-worktrees 3.6.1 → 3.6.3
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/README.md +6 -3
- package/dist/index.js +58 -41
- package/dist/index.js.map +2 -2
- package/dist/mcp-server.js +303 -161
- package/dist/mcp-server.js.map +3 -3
- 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", "--no-track", 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
|
-
|
|
2756
|
+
await fs6.mkdir(barePath, { recursive: true });
|
|
2734
2757
|
try {
|
|
2735
|
-
await
|
|
2736
|
-
} catch {
|
|
2737
|
-
return async () => {
|
|
2738
|
-
};
|
|
2739
|
-
}
|
|
2740
|
-
try {
|
|
2741
|
-
const release = await lockfile.lock(lockTarget, {
|
|
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,
|
|
@@ -3518,43 +3535,60 @@ var RepositoryContext = class {
|
|
|
3518
3535
|
return this.discoveryCache.size;
|
|
3519
3536
|
}
|
|
3520
3537
|
async discoverSiblingRepositories(currentBareRepoPath) {
|
|
3538
|
+
const currentBare = normalizePathForCompare(currentBareRepoPath);
|
|
3539
|
+
const results = /* @__PURE__ */ new Map();
|
|
3540
|
+
const byName = (a, b) => a.name.localeCompare(b.name);
|
|
3541
|
+
const configCandidates = Array.from(this.repos.values()).filter((entry) => entry.source === "config" && !!entry.config.bareRepoDir).map((entry) => {
|
|
3542
|
+
const bareRepoPath = path9.resolve(entry.config.bareRepoDir);
|
|
3543
|
+
return { entry, bareRepoPath, foldedBare: normalizePathForCompare(bareRepoPath) };
|
|
3544
|
+
}).filter((c) => c.foldedBare !== currentBare);
|
|
3545
|
+
const configPresence = await Promise.all(configCandidates.map((c) => isDirectory(c.bareRepoPath)));
|
|
3546
|
+
configCandidates.forEach(({ entry, bareRepoPath, foldedBare }, i) => {
|
|
3547
|
+
const sibling = {
|
|
3548
|
+
name: entry.name,
|
|
3549
|
+
bareRepoPath,
|
|
3550
|
+
worktreeDir: path9.resolve(entry.config.worktreeDir),
|
|
3551
|
+
repoUrl: entry.config.repoUrl,
|
|
3552
|
+
present: configPresence[i],
|
|
3553
|
+
configMatched: true
|
|
3554
|
+
};
|
|
3555
|
+
if (entry.config.sparseCheckout) {
|
|
3556
|
+
sibling.sparseCheckout = entry.config.sparseCheckout;
|
|
3557
|
+
}
|
|
3558
|
+
results.set(foldedBare, sibling);
|
|
3559
|
+
});
|
|
3521
3560
|
const repoDir = path9.dirname(currentBareRepoPath);
|
|
3522
3561
|
const workspaceRoot = path9.dirname(repoDir);
|
|
3523
|
-
if (workspaceRoot === repoDir)
|
|
3562
|
+
if (workspaceRoot === repoDir) {
|
|
3563
|
+
return Array.from(results.values()).sort(byName);
|
|
3564
|
+
}
|
|
3524
3565
|
let entries;
|
|
3525
3566
|
try {
|
|
3526
3567
|
entries = await fs7.readdir(workspaceRoot);
|
|
3527
3568
|
} catch {
|
|
3528
|
-
return
|
|
3529
|
-
}
|
|
3530
|
-
const configBares = /* @__PURE__ */ new Map();
|
|
3531
|
-
for (const entry of this.repos.values()) {
|
|
3532
|
-
if (entry.source === "config" && entry.config.bareRepoDir) {
|
|
3533
|
-
configBares.set(normalizePathForCompare(entry.config.bareRepoDir), entry.name);
|
|
3534
|
-
}
|
|
3569
|
+
return Array.from(results.values()).sort(byName);
|
|
3535
3570
|
}
|
|
3536
|
-
const
|
|
3571
|
+
const configBares = new Map(configCandidates.map((c) => [c.foldedBare, c.entry.name]));
|
|
3537
3572
|
await Promise.all(
|
|
3538
3573
|
entries.map(async (entry) => {
|
|
3539
3574
|
const candidate = path9.join(workspaceRoot, entry);
|
|
3540
3575
|
const bareCandidate = path9.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
|
|
3541
|
-
|
|
3542
|
-
const stat4 = await fs7.stat(bareCandidate);
|
|
3543
|
-
if (!stat4.isDirectory()) return;
|
|
3544
|
-
} catch {
|
|
3545
|
-
return;
|
|
3546
|
-
}
|
|
3576
|
+
if (!await isDirectory(bareCandidate)) return;
|
|
3547
3577
|
const resolvedBare = path9.resolve(bareCandidate);
|
|
3548
|
-
const
|
|
3549
|
-
results.
|
|
3578
|
+
const foldedBare = normalizePathForCompare(resolvedBare);
|
|
3579
|
+
if (foldedBare === currentBare || results.has(foldedBare)) return;
|
|
3580
|
+
const matchedName = configBares.get(foldedBare);
|
|
3581
|
+
results.set(foldedBare, {
|
|
3550
3582
|
name: matchedName ?? entry,
|
|
3551
3583
|
bareRepoPath: resolvedBare,
|
|
3584
|
+
worktreeDir: null,
|
|
3585
|
+
repoUrl: null,
|
|
3586
|
+
present: true,
|
|
3552
3587
|
configMatched: matchedName !== void 0
|
|
3553
3588
|
});
|
|
3554
3589
|
})
|
|
3555
3590
|
);
|
|
3556
|
-
results.
|
|
3557
|
-
return results;
|
|
3591
|
+
return Array.from(results.values()).sort(byName);
|
|
3558
3592
|
}
|
|
3559
3593
|
bootstrapCurrentRepo(candidate, force = false) {
|
|
3560
3594
|
if (this.currentRepo !== null) return;
|
|
@@ -3775,12 +3809,44 @@ var RepositoryContext = class {
|
|
|
3775
3809
|
source: e.source
|
|
3776
3810
|
}));
|
|
3777
3811
|
}
|
|
3812
|
+
getConfiguredRepositoryNames() {
|
|
3813
|
+
return Array.from(this.repos.values()).filter((entry) => entry.source === "config").map((entry) => entry.name);
|
|
3814
|
+
}
|
|
3815
|
+
async getAllConfiguredWorktreeDetails(currentWorktreePath = null) {
|
|
3816
|
+
const entries = Array.from(this.repos.values()).filter((entry) => entry.source === "config");
|
|
3817
|
+
const results = await Promise.all(
|
|
3818
|
+
entries.map(async (entry) => ({
|
|
3819
|
+
name: entry.name,
|
|
3820
|
+
result: await this.readConfiguredWorktrees(entry, currentWorktreePath)
|
|
3821
|
+
}))
|
|
3822
|
+
);
|
|
3823
|
+
const worktreesByRepo = {};
|
|
3824
|
+
const errorsByRepo = {};
|
|
3825
|
+
for (const entry of results) {
|
|
3826
|
+
worktreesByRepo[entry.name] = entry.result.worktrees;
|
|
3827
|
+
if (entry.result.error) {
|
|
3828
|
+
errorsByRepo[entry.name] = entry.result.error;
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
return { worktreesByRepo, errorsByRepo };
|
|
3832
|
+
}
|
|
3778
3833
|
getConfigPath() {
|
|
3779
3834
|
return this.configPath;
|
|
3780
3835
|
}
|
|
3836
|
+
async readConfiguredWorktrees(entry, currentWorktreePath) {
|
|
3837
|
+
if (entry.source !== "config" || !entry.config.bareRepoDir) return { worktrees: [] };
|
|
3838
|
+
const bareRepoPath = path9.resolve(entry.config.bareRepoDir);
|
|
3839
|
+
if (!await isDirectory(bareRepoPath)) return { worktrees: [] };
|
|
3840
|
+
try {
|
|
3841
|
+
const output = await simpleGit5(bareRepoPath).raw(["worktree", "list", "--porcelain"]);
|
|
3842
|
+
return { worktrees: parseWorktreeList(output, currentWorktreePath) };
|
|
3843
|
+
} catch (err) {
|
|
3844
|
+
return { worktrees: [], error: err instanceof Error ? err.message : String(err) };
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3781
3847
|
};
|
|
3782
3848
|
function parseWorktreeList(output, currentPath) {
|
|
3783
|
-
const foldedCurrent = normalizePathForCompare(currentPath);
|
|
3849
|
+
const foldedCurrent = currentPath ? normalizePathForCompare(currentPath) : null;
|
|
3784
3850
|
const results = [];
|
|
3785
3851
|
for (const wt of parseWorktreeListPorcelain(output)) {
|
|
3786
3852
|
const resolved = path9.resolve(wt.path);
|
|
@@ -3789,7 +3855,7 @@ function parseWorktreeList(output, currentPath) {
|
|
|
3789
3855
|
results.push({
|
|
3790
3856
|
path: resolved,
|
|
3791
3857
|
branch,
|
|
3792
|
-
isCurrent: normalizePathForCompare(resolved) === foldedCurrent
|
|
3858
|
+
isCurrent: foldedCurrent !== null && normalizePathForCompare(resolved) === foldedCurrent
|
|
3793
3859
|
});
|
|
3794
3860
|
}
|
|
3795
3861
|
return results;
|
|
@@ -3802,6 +3868,14 @@ async function safeMtimeMs(filePath) {
|
|
|
3802
3868
|
return null;
|
|
3803
3869
|
}
|
|
3804
3870
|
}
|
|
3871
|
+
async function isDirectory(filePath) {
|
|
3872
|
+
try {
|
|
3873
|
+
const stat4 = await fs7.stat(filePath);
|
|
3874
|
+
return stat4.isDirectory();
|
|
3875
|
+
} catch {
|
|
3876
|
+
return false;
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3805
3879
|
async function findWorktreeRoot(startPath) {
|
|
3806
3880
|
let current = path9.resolve(startPath);
|
|
3807
3881
|
const root = path9.parse(current).root;
|
|
@@ -4001,21 +4075,11 @@ function ensureCapability(discovered, key, toolName) {
|
|
|
4001
4075
|
throw new CapabilityUnavailableError(toolName, reasons);
|
|
4002
4076
|
}
|
|
4003
4077
|
}
|
|
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
4078
|
async function getReadyService(ctx, repoName, options = {}) {
|
|
4012
4079
|
const discovered = ctx.getDiscoveredContext(repoName);
|
|
4013
4080
|
if (options.capability && options.toolName) {
|
|
4014
4081
|
ensureCapability(discovered, options.capability, options.toolName);
|
|
4015
4082
|
}
|
|
4016
|
-
if (options.ensureNotSyncing) {
|
|
4017
|
-
await ensureNotSyncing(ctx, repoName);
|
|
4018
|
-
}
|
|
4019
4083
|
const service = await ctx.getService(repoName);
|
|
4020
4084
|
if (options.ensureInitialized && !service.isInitialized()) {
|
|
4021
4085
|
await service.initialize();
|
|
@@ -4026,6 +4090,14 @@ async function getReadyService(ctx, repoName, options = {}) {
|
|
|
4026
4090
|
git: service.getGitService()
|
|
4027
4091
|
};
|
|
4028
4092
|
}
|
|
4093
|
+
async function runExclusiveRepoOperation(ctx, repoName, service, operation) {
|
|
4094
|
+
const result = await service.runExclusiveRepoOperation(operation);
|
|
4095
|
+
if (!result.started) {
|
|
4096
|
+
const name = ctx.getEntry(repoName)?.name ?? repoName ?? "unknown";
|
|
4097
|
+
throw new SyncInProgressError(name);
|
|
4098
|
+
}
|
|
4099
|
+
return result.value;
|
|
4100
|
+
}
|
|
4029
4101
|
async function ensureRepoWorktreePath(ctx, params, git) {
|
|
4030
4102
|
await ensurePathBelongsToRepo(ctx, params.path, params.repoName, git);
|
|
4031
4103
|
return path10.resolve(params.path);
|
|
@@ -4046,13 +4118,38 @@ async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
|
|
|
4046
4118
|
async function handleDetectContext(ctx, params, _extra) {
|
|
4047
4119
|
const target = params.path ?? process.cwd();
|
|
4048
4120
|
const discovered = await ctx.detectFromPath(target);
|
|
4049
|
-
|
|
4050
|
-
|
|
4121
|
+
let response = discovered;
|
|
4122
|
+
if (params.includeAllWorktrees) {
|
|
4123
|
+
const details = await ctx.getAllConfiguredWorktreeDetails(discovered.currentWorktreePath);
|
|
4124
|
+
const errorsByRepo = Object.keys(details.errorsByRepo).length > 0 ? details.errorsByRepo : void 0;
|
|
4125
|
+
response = {
|
|
4126
|
+
...response,
|
|
4127
|
+
allWorktreesByRepo: details.worktreesByRepo,
|
|
4128
|
+
allWorktreeErrorsByRepo: errorsByRepo
|
|
4129
|
+
};
|
|
4130
|
+
}
|
|
4131
|
+
if (!params.includeStatus) {
|
|
4132
|
+
return formatToolResponse(response);
|
|
4051
4133
|
}
|
|
4052
4134
|
const statusService = new WorktreeStatusService();
|
|
4053
|
-
const
|
|
4054
|
-
const enriched = await
|
|
4055
|
-
|
|
4135
|
+
const statusLimit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
|
|
4136
|
+
const enriched = await enrichDetectedWorktrees(response.allWorktrees, statusService, statusLimit);
|
|
4137
|
+
let allWorktreesByRepo = response.allWorktreesByRepo;
|
|
4138
|
+
if (allWorktreesByRepo) {
|
|
4139
|
+
const entries = await Promise.all(
|
|
4140
|
+
Object.entries(allWorktreesByRepo).map(async ([repoName, worktrees]) => [
|
|
4141
|
+
repoName,
|
|
4142
|
+
await enrichDetectedWorktrees(worktrees, statusService, statusLimit)
|
|
4143
|
+
])
|
|
4144
|
+
);
|
|
4145
|
+
allWorktreesByRepo = Object.fromEntries(entries);
|
|
4146
|
+
}
|
|
4147
|
+
return formatToolResponse({ ...response, allWorktrees: enriched, allWorktreesByRepo });
|
|
4148
|
+
}
|
|
4149
|
+
async function enrichDetectedWorktrees(worktrees, statusService, limit) {
|
|
4150
|
+
if (worktrees.length === 0) return worktrees;
|
|
4151
|
+
return Promise.all(
|
|
4152
|
+
worktrees.map(
|
|
4056
4153
|
(wt) => limit(async () => {
|
|
4057
4154
|
const [status, divergence] = await Promise.all([
|
|
4058
4155
|
statusService.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
@@ -4067,10 +4164,41 @@ async function handleDetectContext(ctx, params, _extra) {
|
|
|
4067
4164
|
})
|
|
4068
4165
|
)
|
|
4069
4166
|
);
|
|
4070
|
-
return formatToolResponse({ ...discovered, allWorktrees: enriched });
|
|
4071
4167
|
}
|
|
4072
4168
|
async function handleListWorktrees(ctx, params, _extra) {
|
|
4073
|
-
const
|
|
4169
|
+
const configuredRepoNames = params.repoName ? [] : ctx.getConfiguredRepositoryNames();
|
|
4170
|
+
if (configuredRepoNames.length > 0) {
|
|
4171
|
+
const limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
|
|
4172
|
+
const statusLimit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
|
|
4173
|
+
const repositories = await Promise.all(
|
|
4174
|
+
configuredRepoNames.map(
|
|
4175
|
+
(repoName) => limit(async () => {
|
|
4176
|
+
try {
|
|
4177
|
+
return [
|
|
4178
|
+
repoName,
|
|
4179
|
+
{
|
|
4180
|
+
worktrees: await listWorktreesForRepo(ctx, repoName, params.includeSize, statusLimit)
|
|
4181
|
+
}
|
|
4182
|
+
];
|
|
4183
|
+
} catch (err) {
|
|
4184
|
+
return [
|
|
4185
|
+
repoName,
|
|
4186
|
+
{
|
|
4187
|
+
worktrees: [],
|
|
4188
|
+
error: err instanceof Error ? err.message : String(err)
|
|
4189
|
+
}
|
|
4190
|
+
];
|
|
4191
|
+
}
|
|
4192
|
+
})
|
|
4193
|
+
)
|
|
4194
|
+
);
|
|
4195
|
+
return formatToolResponse({ repositories: Object.fromEntries(repositories) });
|
|
4196
|
+
}
|
|
4197
|
+
const results = await listWorktreesForRepo(ctx, params.repoName, params.includeSize);
|
|
4198
|
+
return formatToolResponse({ worktrees: results });
|
|
4199
|
+
}
|
|
4200
|
+
async function listWorktreesForRepo(ctx, repoName, includeSize, limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS)) {
|
|
4201
|
+
const { discovered, git } = await getReadyService(ctx, repoName, {
|
|
4074
4202
|
capability: "listWorktrees",
|
|
4075
4203
|
toolName: "list_worktrees"
|
|
4076
4204
|
});
|
|
@@ -4085,7 +4213,6 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
4085
4213
|
}
|
|
4086
4214
|
}
|
|
4087
4215
|
const currentPath = discovered?.currentWorktreePath ?? null;
|
|
4088
|
-
const limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
|
|
4089
4216
|
const results = await Promise.all(
|
|
4090
4217
|
worktrees.map(
|
|
4091
4218
|
(wt) => limit(async () => {
|
|
@@ -4095,7 +4222,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
4095
4222
|
git.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
4096
4223
|
getDivergence(wt.path),
|
|
4097
4224
|
git.getWorktreeMetadata(wt.path).catch(() => null),
|
|
4098
|
-
|
|
4225
|
+
includeSize ? calculateDirectorySize(wt.path).catch(() => null) : Promise.resolve(null)
|
|
4099
4226
|
]);
|
|
4100
4227
|
return {
|
|
4101
4228
|
path: resolvedPath,
|
|
@@ -4111,7 +4238,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
4111
4238
|
})
|
|
4112
4239
|
)
|
|
4113
4240
|
);
|
|
4114
|
-
return
|
|
4241
|
+
return results;
|
|
4115
4242
|
}
|
|
4116
4243
|
async function handleGetWorktreeStatus(ctx, params, _extra) {
|
|
4117
4244
|
const { git } = await getReadyService(ctx, params.repoName, {
|
|
@@ -4130,76 +4257,82 @@ async function handleGetWorktreeStatus(ctx, params, _extra) {
|
|
|
4130
4257
|
});
|
|
4131
4258
|
}
|
|
4132
4259
|
async function handleCreateWorktree(ctx, params, _extra) {
|
|
4133
|
-
const { branchName, baseBranch
|
|
4260
|
+
const { branchName, baseBranch } = params;
|
|
4261
|
+
const push = params.push ?? true;
|
|
4134
4262
|
const validation = isValidGitBranchName(branchName);
|
|
4135
4263
|
if (!validation.valid) {
|
|
4136
4264
|
throw new Error(`Invalid branch name '${branchName}': ${validation.error}`);
|
|
4137
4265
|
}
|
|
4138
4266
|
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4139
4267
|
capability: "createWorktree",
|
|
4140
|
-
toolName: "create_worktree"
|
|
4141
|
-
ensureInitialized: true,
|
|
4142
|
-
ensureNotSyncing: true
|
|
4268
|
+
toolName: "create_worktree"
|
|
4143
4269
|
});
|
|
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
|
-
|
|
4270
|
+
return runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4271
|
+
if (!service.isInitialized()) {
|
|
4272
|
+
await service.initialize();
|
|
4273
|
+
}
|
|
4274
|
+
const existence = await git.branchExists(branchName);
|
|
4275
|
+
let created = false;
|
|
4276
|
+
let pushed = false;
|
|
4277
|
+
if (!existence.local && !existence.remote) {
|
|
4278
|
+
if (!baseBranch) {
|
|
4279
|
+
throw new Error(`Branch '${branchName}' does not exist. Provide 'baseBranch' to create it.`);
|
|
4280
|
+
}
|
|
4281
|
+
await git.createBranch(branchName, baseBranch);
|
|
4282
|
+
created = true;
|
|
4283
|
+
}
|
|
4284
|
+
const worktreeDir = service.config.worktreeDir;
|
|
4285
|
+
const worktreePath = pathResolution.getBranchWorktreePath(worktreeDir, branchName);
|
|
4286
|
+
const existing = await git.getWorktrees();
|
|
4287
|
+
const collision = existing.find((w) => pathsEqual(w.path, worktreePath) && w.branch !== branchName);
|
|
4288
|
+
if (collision) {
|
|
4289
|
+
throw new Error(
|
|
4290
|
+
`Sanitized worktree path '${worktreePath}' collides with existing branch '${collision.branch}'. Rename or remove the conflicting branch first.`
|
|
4291
|
+
);
|
|
4292
|
+
}
|
|
4293
|
+
await git.addWorktree(branchName, worktreePath);
|
|
4294
|
+
ctx.invalidateDiscovered();
|
|
4295
|
+
if (created && push) {
|
|
4296
|
+
await git.pushBranch(branchName);
|
|
4297
|
+
pushed = true;
|
|
4298
|
+
}
|
|
4299
|
+
return formatToolResponse({
|
|
4300
|
+
success: true,
|
|
4301
|
+
branchName,
|
|
4302
|
+
worktreePath: path10.resolve(worktreePath),
|
|
4303
|
+
created,
|
|
4304
|
+
pushed
|
|
4305
|
+
});
|
|
4175
4306
|
});
|
|
4176
4307
|
}
|
|
4177
4308
|
async function handleRemoveWorktree(ctx, params, _extra) {
|
|
4178
|
-
const { git } = await getReadyService(ctx, params.repoName, {
|
|
4309
|
+
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4179
4310
|
capability: "removeWorktree",
|
|
4180
|
-
toolName: "remove_worktree"
|
|
4181
|
-
ensureInitialized: true,
|
|
4182
|
-
ensureNotSyncing: true
|
|
4311
|
+
toolName: "remove_worktree"
|
|
4183
4312
|
});
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
if (!status.canRemove) {
|
|
4188
|
-
throw new Error(`Cannot remove worktree: ${status.reasons.join(", ")}. Use force=true to override.`);
|
|
4313
|
+
return runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4314
|
+
if (!service.isInitialized()) {
|
|
4315
|
+
await service.initialize();
|
|
4189
4316
|
}
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4317
|
+
const removedPath = await ensureRepoWorktreePath(ctx, params, git);
|
|
4318
|
+
if (!params.force) {
|
|
4319
|
+
const status = await git.getFullWorktreeStatus(params.path, false);
|
|
4320
|
+
if (!status.canRemove) {
|
|
4321
|
+
throw new Error(`Cannot remove worktree: ${status.reasons.join(", ")}. Use force=true to override.`);
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
await git.removeWorktree(params.path);
|
|
4325
|
+
ctx.invalidateDiscovered();
|
|
4326
|
+
return formatToolResponse({
|
|
4327
|
+
success: true,
|
|
4328
|
+
removedPath
|
|
4329
|
+
});
|
|
4196
4330
|
});
|
|
4197
4331
|
}
|
|
4198
4332
|
async function handleSync(ctx, params, extra) {
|
|
4199
4333
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
4200
4334
|
capability: "sync",
|
|
4201
|
-
toolName: "sync"
|
|
4202
|
-
ensureInitialized: true
|
|
4335
|
+
toolName: "sync"
|
|
4203
4336
|
});
|
|
4204
4337
|
const dispose = attachProgressReporter(service, extra);
|
|
4205
4338
|
try {
|
|
@@ -4216,35 +4349,39 @@ async function handleSync(ctx, params, extra) {
|
|
|
4216
4349
|
}
|
|
4217
4350
|
}
|
|
4218
4351
|
async function handleUpdateWorktree(ctx, params, _extra) {
|
|
4219
|
-
const { git } = await getReadyService(ctx, params.repoName, {
|
|
4352
|
+
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
4220
4353
|
capability: "updateWorktree",
|
|
4221
|
-
toolName: "update_worktree"
|
|
4222
|
-
ensureInitialized: true,
|
|
4223
|
-
ensureNotSyncing: true
|
|
4354
|
+
toolName: "update_worktree"
|
|
4224
4355
|
});
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4356
|
+
return runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4357
|
+
if (!service.isInitialized()) {
|
|
4358
|
+
await service.initialize();
|
|
4359
|
+
}
|
|
4360
|
+
const worktreePath = await ensureRepoWorktreePath(ctx, params, git);
|
|
4361
|
+
await git.updateWorktree(params.path);
|
|
4362
|
+
ctx.invalidateDiscovered();
|
|
4363
|
+
return formatToolResponse({
|
|
4364
|
+
success: true,
|
|
4365
|
+
worktreePath
|
|
4366
|
+
});
|
|
4231
4367
|
});
|
|
4232
4368
|
}
|
|
4233
4369
|
async function handleInitialize(ctx, params, extra) {
|
|
4234
4370
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
4235
4371
|
capability: "initialize",
|
|
4236
|
-
toolName: "initialize"
|
|
4237
|
-
ensureNotSyncing: true
|
|
4372
|
+
toolName: "initialize"
|
|
4238
4373
|
});
|
|
4239
4374
|
const dispose = attachProgressReporter(service, extra);
|
|
4240
4375
|
try {
|
|
4241
|
-
await service
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4376
|
+
return await runExclusiveRepoOperation(ctx, params.repoName, service, async () => {
|
|
4377
|
+
await service.initialize();
|
|
4378
|
+
const git = service.getGitService();
|
|
4379
|
+
ctx.invalidateDiscovered();
|
|
4380
|
+
return formatToolResponse({
|
|
4381
|
+
success: true,
|
|
4382
|
+
defaultBranch: git.getDefaultBranch(),
|
|
4383
|
+
worktreeDir: service.config.worktreeDir
|
|
4384
|
+
});
|
|
4248
4385
|
});
|
|
4249
4386
|
} finally {
|
|
4250
4387
|
dispose();
|
|
@@ -4294,7 +4431,7 @@ function attachProgressReporter(service, extra) {
|
|
|
4294
4431
|
// src/mcp/server.ts
|
|
4295
4432
|
var REPO_NAME_DESCRIBE = "Repository name from loaded config. If omitted, uses the current repository set via set_current_repository or the only loaded repo.";
|
|
4296
4433
|
var PATH_DESCRIBE_SUFFIX = "Absolute path preferred; relative paths resolve from the server's CWD.";
|
|
4297
|
-
var SERVER_INSTRUCTIONS = "Before running git worktree operations, call `detect_context` to learn the current repo, current branch, sibling repositories
|
|
4434
|
+
var SERVER_INSTRUCTIONS = "Before running git worktree operations, call `detect_context` with `includeAllWorktrees: true` at session start to learn every configured repository and worktree, plus the current repo, current branch, sibling repositories, and available capabilities. It walks up to auto-discover sync-worktrees.config.{js,mjs,cjs,ts}, reports config-driven sibling repositories, and reports per-capability {available, reason} so you can tell which tool is gated and why.";
|
|
4298
4435
|
function buildInstructions(snapshot) {
|
|
4299
4436
|
const d = snapshot?.discovered;
|
|
4300
4437
|
if (!d || !d.isWorktree || d.kind !== "managed") return SERVER_INSTRUCTIONS;
|
|
@@ -4346,11 +4483,14 @@ function createServer(context, snapshot) {
|
|
|
4346
4483
|
server.registerTool(
|
|
4347
4484
|
"detect_context",
|
|
4348
4485
|
{
|
|
4349
|
-
description: "Detect sync-worktrees structure from a filesystem path. Reads .git file, resolves bare repo, discovers sibling worktrees, walks up for a sync-worktrees.config.{js,mjs,cjs,ts}, and lists sibling
|
|
4486
|
+
description: "Detect sync-worktrees structure from a filesystem path. Reads .git file, resolves bare repo, discovers sibling worktrees, walks up for a sync-worktrees.config.{js,mjs,cjs,ts}, and lists configured sibling repositories. Defaults to CWD. Use when: bootstrapping from an unknown checkout. Returns: discovered repo root, bare repo path, all sibling worktrees, sibling repositories, current worktree path, configPath (auto-found), per-capability {available, reason}, notes[].",
|
|
4350
4487
|
inputSchema: {
|
|
4351
4488
|
path: z.string().optional().describe("Directory path to inspect. Defaults to the server's CWD."),
|
|
4489
|
+
includeAllWorktrees: z.boolean().optional().describe(
|
|
4490
|
+
"If true, includes allWorktreesByRepo with worktrees for every configured repository, keyed by repoName, and allWorktreeErrorsByRepo for repos that could not be enumerated. Default: false."
|
|
4491
|
+
),
|
|
4352
4492
|
includeStatus: z.boolean().optional().describe(
|
|
4353
|
-
"If true, enriches
|
|
4493
|
+
"If true, enriches worktree entries with label, divergence, and staleHint. Adds one git status + rev-list per worktree. Default: false (cheap path)."
|
|
4354
4494
|
)
|
|
4355
4495
|
},
|
|
4356
4496
|
annotations: {
|
|
@@ -4365,9 +4505,11 @@ function createServer(context, snapshot) {
|
|
|
4365
4505
|
server.registerTool(
|
|
4366
4506
|
"list_worktrees",
|
|
4367
4507
|
{
|
|
4368
|
-
description: "List
|
|
4508
|
+
description: "List worktrees with enriched status. Without repoName and with a loaded config, returns all configured repositories grouped by repoName. With repoName, returns that single repository. Returns worktree entries as { path, branch, isCurrent, label (clean|dirty|stale|current|unknown), status, divergence (ahead/behind), safeToRemove: { safe, reason }, lastSyncAt, sizeBytes }.",
|
|
4369
4509
|
inputSchema: {
|
|
4370
|
-
repoName: z.string().optional().describe(
|
|
4510
|
+
repoName: z.string().optional().describe(
|
|
4511
|
+
"Repository name from loaded config. If omitted and a config is loaded, lists all configured repos."
|
|
4512
|
+
),
|
|
4371
4513
|
includeSize: z.boolean().optional().describe(
|
|
4372
4514
|
"If true, computes the on-disk size of each worktree (in bytes). Slow on large worktrees. Default: false (sizeBytes returned as null)."
|
|
4373
4515
|
)
|
|
@@ -4402,13 +4544,13 @@ function createServer(context, snapshot) {
|
|
|
4402
4544
|
server.registerTool(
|
|
4403
4545
|
"create_worktree",
|
|
4404
4546
|
{
|
|
4405
|
-
description: "Create a worktree for a branch. If the branch exists (local or remote), checks it out; otherwise creates it from baseBranch
|
|
4547
|
+
description: "Create a worktree for a branch. If the branch exists (local or remote), checks it out; otherwise creates it from baseBranch and pushes the new branch to origin by default. Key params: baseBranch is required only when the branch does not yet exist \u2014 pass it defensively if unsure. push=false opts out for newly created branches. Preconditions: repository must be initialized (auto-runs on first call). Returns: { success, branchName, worktreePath, created, pushed }.",
|
|
4406
4548
|
inputSchema: {
|
|
4407
4549
|
branchName: z.string().describe("Branch name. Slashes and special chars are sanitized for the worktree directory name."),
|
|
4408
4550
|
baseBranch: z.string().optional().describe(
|
|
4409
4551
|
"Base branch for creating a new branch. Required if branchName does not exist locally or remotely; ignored otherwise."
|
|
4410
4552
|
),
|
|
4411
|
-
push: z.boolean().optional().describe("Push the newly created branch to origin. Ignored if the branch already existed."),
|
|
4553
|
+
push: z.boolean().optional().describe("Push the newly created branch to origin. Default: true. Ignored if the branch already existed."),
|
|
4412
4554
|
repoName: z.string().optional().describe(REPO_NAME_DESCRIBE)
|
|
4413
4555
|
},
|
|
4414
4556
|
annotations: {
|
|
@@ -4497,7 +4639,7 @@ function createServer(context, snapshot) {
|
|
|
4497
4639
|
server.registerTool(
|
|
4498
4640
|
"load_config",
|
|
4499
4641
|
{
|
|
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,
|
|
4642
|
+
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
4643
|
inputSchema: {
|
|
4502
4644
|
configPath: z.string().optional().describe(
|
|
4503
4645
|
"Path to the config file. If omitted, falls back to the SYNC_WORKTREES_CONFIG env var. Errors if neither is set."
|