sync-worktrees 3.6.2 → 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 +1 -1
- package/dist/index.js.map +2 -2
- package/dist/mcp-server.js +158 -40
- package/dist/mcp-server.js.map +3 -3
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -2569,7 +2569,7 @@ var GitService = class {
|
|
|
2569
2569
|
async createBranch(branchName, baseBranch) {
|
|
2570
2570
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
2571
2571
|
const baseRef = await this.resolveCreateBranchBaseRef(bareGit, baseBranch);
|
|
2572
|
-
await bareGit.raw(["branch", branchName, baseRef]);
|
|
2572
|
+
await bareGit.raw(["branch", "--no-track", branchName, baseRef]);
|
|
2573
2573
|
this.logger.info(`Created branch '${branchName}' from '${baseRef}'`);
|
|
2574
2574
|
}
|
|
2575
2575
|
async pushBranch(branchName) {
|
|
@@ -3535,43 +3535,60 @@ var RepositoryContext = class {
|
|
|
3535
3535
|
return this.discoveryCache.size;
|
|
3536
3536
|
}
|
|
3537
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
|
+
});
|
|
3538
3560
|
const repoDir = path9.dirname(currentBareRepoPath);
|
|
3539
3561
|
const workspaceRoot = path9.dirname(repoDir);
|
|
3540
|
-
if (workspaceRoot === repoDir)
|
|
3562
|
+
if (workspaceRoot === repoDir) {
|
|
3563
|
+
return Array.from(results.values()).sort(byName);
|
|
3564
|
+
}
|
|
3541
3565
|
let entries;
|
|
3542
3566
|
try {
|
|
3543
3567
|
entries = await fs7.readdir(workspaceRoot);
|
|
3544
3568
|
} catch {
|
|
3545
|
-
return
|
|
3546
|
-
}
|
|
3547
|
-
const configBares = /* @__PURE__ */ new Map();
|
|
3548
|
-
for (const entry of this.repos.values()) {
|
|
3549
|
-
if (entry.source === "config" && entry.config.bareRepoDir) {
|
|
3550
|
-
configBares.set(normalizePathForCompare(entry.config.bareRepoDir), entry.name);
|
|
3551
|
-
}
|
|
3569
|
+
return Array.from(results.values()).sort(byName);
|
|
3552
3570
|
}
|
|
3553
|
-
const
|
|
3571
|
+
const configBares = new Map(configCandidates.map((c) => [c.foldedBare, c.entry.name]));
|
|
3554
3572
|
await Promise.all(
|
|
3555
3573
|
entries.map(async (entry) => {
|
|
3556
3574
|
const candidate = path9.join(workspaceRoot, entry);
|
|
3557
3575
|
const bareCandidate = path9.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
|
|
3558
|
-
|
|
3559
|
-
const stat4 = await fs7.stat(bareCandidate);
|
|
3560
|
-
if (!stat4.isDirectory()) return;
|
|
3561
|
-
} catch {
|
|
3562
|
-
return;
|
|
3563
|
-
}
|
|
3576
|
+
if (!await isDirectory(bareCandidate)) return;
|
|
3564
3577
|
const resolvedBare = path9.resolve(bareCandidate);
|
|
3565
|
-
const
|
|
3566
|
-
results.
|
|
3578
|
+
const foldedBare = normalizePathForCompare(resolvedBare);
|
|
3579
|
+
if (foldedBare === currentBare || results.has(foldedBare)) return;
|
|
3580
|
+
const matchedName = configBares.get(foldedBare);
|
|
3581
|
+
results.set(foldedBare, {
|
|
3567
3582
|
name: matchedName ?? entry,
|
|
3568
3583
|
bareRepoPath: resolvedBare,
|
|
3584
|
+
worktreeDir: null,
|
|
3585
|
+
repoUrl: null,
|
|
3586
|
+
present: true,
|
|
3569
3587
|
configMatched: matchedName !== void 0
|
|
3570
3588
|
});
|
|
3571
3589
|
})
|
|
3572
3590
|
);
|
|
3573
|
-
results.
|
|
3574
|
-
return results;
|
|
3591
|
+
return Array.from(results.values()).sort(byName);
|
|
3575
3592
|
}
|
|
3576
3593
|
bootstrapCurrentRepo(candidate, force = false) {
|
|
3577
3594
|
if (this.currentRepo !== null) return;
|
|
@@ -3792,12 +3809,44 @@ var RepositoryContext = class {
|
|
|
3792
3809
|
source: e.source
|
|
3793
3810
|
}));
|
|
3794
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
|
+
}
|
|
3795
3833
|
getConfigPath() {
|
|
3796
3834
|
return this.configPath;
|
|
3797
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
|
+
}
|
|
3798
3847
|
};
|
|
3799
3848
|
function parseWorktreeList(output, currentPath) {
|
|
3800
|
-
const foldedCurrent = normalizePathForCompare(currentPath);
|
|
3849
|
+
const foldedCurrent = currentPath ? normalizePathForCompare(currentPath) : null;
|
|
3801
3850
|
const results = [];
|
|
3802
3851
|
for (const wt of parseWorktreeListPorcelain(output)) {
|
|
3803
3852
|
const resolved = path9.resolve(wt.path);
|
|
@@ -3806,7 +3855,7 @@ function parseWorktreeList(output, currentPath) {
|
|
|
3806
3855
|
results.push({
|
|
3807
3856
|
path: resolved,
|
|
3808
3857
|
branch,
|
|
3809
|
-
isCurrent: normalizePathForCompare(resolved) === foldedCurrent
|
|
3858
|
+
isCurrent: foldedCurrent !== null && normalizePathForCompare(resolved) === foldedCurrent
|
|
3810
3859
|
});
|
|
3811
3860
|
}
|
|
3812
3861
|
return results;
|
|
@@ -3819,6 +3868,14 @@ async function safeMtimeMs(filePath) {
|
|
|
3819
3868
|
return null;
|
|
3820
3869
|
}
|
|
3821
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
|
+
}
|
|
3822
3879
|
async function findWorktreeRoot(startPath) {
|
|
3823
3880
|
let current = path9.resolve(startPath);
|
|
3824
3881
|
const root = path9.parse(current).root;
|
|
@@ -4061,13 +4118,38 @@ async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
|
|
|
4061
4118
|
async function handleDetectContext(ctx, params, _extra) {
|
|
4062
4119
|
const target = params.path ?? process.cwd();
|
|
4063
4120
|
const discovered = await ctx.detectFromPath(target);
|
|
4064
|
-
|
|
4065
|
-
|
|
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);
|
|
4066
4133
|
}
|
|
4067
4134
|
const statusService = new WorktreeStatusService();
|
|
4068
|
-
const
|
|
4069
|
-
const enriched = await
|
|
4070
|
-
|
|
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(
|
|
4071
4153
|
(wt) => limit(async () => {
|
|
4072
4154
|
const [status, divergence] = await Promise.all([
|
|
4073
4155
|
statusService.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
@@ -4082,10 +4164,41 @@ async function handleDetectContext(ctx, params, _extra) {
|
|
|
4082
4164
|
})
|
|
4083
4165
|
)
|
|
4084
4166
|
);
|
|
4085
|
-
return formatToolResponse({ ...discovered, allWorktrees: enriched });
|
|
4086
4167
|
}
|
|
4087
4168
|
async function handleListWorktrees(ctx, params, _extra) {
|
|
4088
|
-
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, {
|
|
4089
4202
|
capability: "listWorktrees",
|
|
4090
4203
|
toolName: "list_worktrees"
|
|
4091
4204
|
});
|
|
@@ -4100,7 +4213,6 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
4100
4213
|
}
|
|
4101
4214
|
}
|
|
4102
4215
|
const currentPath = discovered?.currentWorktreePath ?? null;
|
|
4103
|
-
const limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
|
|
4104
4216
|
const results = await Promise.all(
|
|
4105
4217
|
worktrees.map(
|
|
4106
4218
|
(wt) => limit(async () => {
|
|
@@ -4110,7 +4222,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
4110
4222
|
git.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
4111
4223
|
getDivergence(wt.path),
|
|
4112
4224
|
git.getWorktreeMetadata(wt.path).catch(() => null),
|
|
4113
|
-
|
|
4225
|
+
includeSize ? calculateDirectorySize(wt.path).catch(() => null) : Promise.resolve(null)
|
|
4114
4226
|
]);
|
|
4115
4227
|
return {
|
|
4116
4228
|
path: resolvedPath,
|
|
@@ -4126,7 +4238,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
4126
4238
|
})
|
|
4127
4239
|
)
|
|
4128
4240
|
);
|
|
4129
|
-
return
|
|
4241
|
+
return results;
|
|
4130
4242
|
}
|
|
4131
4243
|
async function handleGetWorktreeStatus(ctx, params, _extra) {
|
|
4132
4244
|
const { git } = await getReadyService(ctx, params.repoName, {
|
|
@@ -4145,7 +4257,8 @@ async function handleGetWorktreeStatus(ctx, params, _extra) {
|
|
|
4145
4257
|
});
|
|
4146
4258
|
}
|
|
4147
4259
|
async function handleCreateWorktree(ctx, params, _extra) {
|
|
4148
|
-
const { branchName, baseBranch
|
|
4260
|
+
const { branchName, baseBranch } = params;
|
|
4261
|
+
const push = params.push ?? true;
|
|
4149
4262
|
const validation = isValidGitBranchName(branchName);
|
|
4150
4263
|
if (!validation.valid) {
|
|
4151
4264
|
throw new Error(`Invalid branch name '${branchName}': ${validation.error}`);
|
|
@@ -4318,7 +4431,7 @@ function attachProgressReporter(service, extra) {
|
|
|
4318
4431
|
// src/mcp/server.ts
|
|
4319
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.";
|
|
4320
4433
|
var PATH_DESCRIBE_SUFFIX = "Absolute path preferred; relative paths resolve from the server's CWD.";
|
|
4321
|
-
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.";
|
|
4322
4435
|
function buildInstructions(snapshot) {
|
|
4323
4436
|
const d = snapshot?.discovered;
|
|
4324
4437
|
if (!d || !d.isWorktree || d.kind !== "managed") return SERVER_INSTRUCTIONS;
|
|
@@ -4370,11 +4483,14 @@ function createServer(context, snapshot) {
|
|
|
4370
4483
|
server.registerTool(
|
|
4371
4484
|
"detect_context",
|
|
4372
4485
|
{
|
|
4373
|
-
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[].",
|
|
4374
4487
|
inputSchema: {
|
|
4375
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
|
+
),
|
|
4376
4492
|
includeStatus: z.boolean().optional().describe(
|
|
4377
|
-
"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)."
|
|
4378
4494
|
)
|
|
4379
4495
|
},
|
|
4380
4496
|
annotations: {
|
|
@@ -4389,9 +4505,11 @@ function createServer(context, snapshot) {
|
|
|
4389
4505
|
server.registerTool(
|
|
4390
4506
|
"list_worktrees",
|
|
4391
4507
|
{
|
|
4392
|
-
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 }.",
|
|
4393
4509
|
inputSchema: {
|
|
4394
|
-
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
|
+
),
|
|
4395
4513
|
includeSize: z.boolean().optional().describe(
|
|
4396
4514
|
"If true, computes the on-disk size of each worktree (in bytes). Slow on large worktrees. Default: false (sizeBytes returned as null)."
|
|
4397
4515
|
)
|
|
@@ -4426,13 +4544,13 @@ function createServer(context, snapshot) {
|
|
|
4426
4544
|
server.registerTool(
|
|
4427
4545
|
"create_worktree",
|
|
4428
4546
|
{
|
|
4429
|
-
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 }.",
|
|
4430
4548
|
inputSchema: {
|
|
4431
4549
|
branchName: z.string().describe("Branch name. Slashes and special chars are sanitized for the worktree directory name."),
|
|
4432
4550
|
baseBranch: z.string().optional().describe(
|
|
4433
4551
|
"Base branch for creating a new branch. Required if branchName does not exist locally or remotely; ignored otherwise."
|
|
4434
4552
|
),
|
|
4435
|
-
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."),
|
|
4436
4554
|
repoName: z.string().optional().describe(REPO_NAME_DESCRIBE)
|
|
4437
4555
|
},
|
|
4438
4556
|
annotations: {
|