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.
@@ -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) return [];
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 results = [];
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
- try {
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 matchedName = configBares.get(normalizePathForCompare(resolvedBare));
3566
- results.push({
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.sort((a, b) => a.name.localeCompare(b.name));
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
- if (!params.includeStatus || discovered.allWorktrees.length === 0) {
4065
- return formatToolResponse(discovered);
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 limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
4069
- const enriched = await Promise.all(
4070
- discovered.allWorktrees.map(
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 { discovered, git } = await getReadyService(ctx, params.repoName, {
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
- params.includeSize ? calculateDirectorySize(wt.path).catch(() => null) : Promise.resolve(null)
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 formatToolResponse({ worktrees: results });
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, push } = params;
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 under the workspace root, and which capabilities are available. It walks up to auto-discover sync-worktrees.config.{js,mjs,cjs,ts}, lists sibling worktrees, and reports per-capability {available, reason} so you can tell which tool is gated and why.";
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 bare repos under the workspace root. 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[].",
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 each entry in allWorktrees with label, divergence, and staleHint. Adds one git status + rev-list per worktree. Default: false (cheap path)."
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 all worktrees of a repository with enriched status. Returns: array of { path, branch, isCurrent, label (clean|dirty|stale|current|unknown), status, divergence (ahead/behind), safeToRemove: { safe, reason }, lastSyncAt, sizeBytes }.",
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(REPO_NAME_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. Optionally pushes the new branch to origin. Key params: baseBranch is required only when the branch does not yet exist \u2014 pass it defensively if unsure. push=true only affects newly created branches. Preconditions: repository must be initialized (auto-runs on first call). Returns: { success, branchName, worktreePath, created, pushed }.",
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: {