sync-worktrees 3.1.0 → 3.2.0

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.
@@ -803,6 +803,43 @@ import * as fs4 from "fs/promises";
803
803
  import * as path4 from "path";
804
804
  import simpleGit3 from "simple-git";
805
805
 
806
+ // src/errors/index.ts
807
+ var SyncWorktreesError = class extends Error {
808
+ constructor(message, code, cause) {
809
+ super(message);
810
+ this.code = code;
811
+ this.cause = cause;
812
+ this.name = this.constructor.name;
813
+ Object.setPrototypeOf(this, new.target.prototype);
814
+ if (cause && cause.stack) {
815
+ this.stack = `${this.stack}
816
+ Caused by: ${cause.stack}`;
817
+ }
818
+ }
819
+ };
820
+ var GitError = class extends SyncWorktreesError {
821
+ constructor(message, code, cause) {
822
+ super(message, `GIT_${code}`, cause);
823
+ }
824
+ };
825
+ var GitOperationError = class extends GitError {
826
+ constructor(operation, details, cause) {
827
+ super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
828
+ }
829
+ };
830
+ var WorktreeError = class extends SyncWorktreesError {
831
+ constructor(message, code, cause) {
832
+ super(message, `WORKTREE_${code}`, cause);
833
+ }
834
+ };
835
+ var WorktreeNotCleanError = class extends WorktreeError {
836
+ constructor(path10, reasons) {
837
+ super(`Worktree at '${path10}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
838
+ this.path = path10;
839
+ this.reasons = reasons;
840
+ }
841
+ };
842
+
806
843
  // src/utils/git-url.ts
807
844
  function extractRepoNameFromUrl(gitUrl) {
808
845
  const url = gitUrl.trim();
@@ -1101,45 +1138,6 @@ var WorktreeMetadataService = class {
1101
1138
  import * as fs3 from "fs/promises";
1102
1139
  import * as path3 from "path";
1103
1140
  import simpleGit2 from "simple-git";
1104
-
1105
- // src/errors/index.ts
1106
- var SyncWorktreesError = class extends Error {
1107
- constructor(message, code, cause) {
1108
- super(message);
1109
- this.code = code;
1110
- this.cause = cause;
1111
- this.name = this.constructor.name;
1112
- Object.setPrototypeOf(this, new.target.prototype);
1113
- if (cause && cause.stack) {
1114
- this.stack = `${this.stack}
1115
- Caused by: ${cause.stack}`;
1116
- }
1117
- }
1118
- };
1119
- var GitError = class extends SyncWorktreesError {
1120
- constructor(message, code, cause) {
1121
- super(message, `GIT_${code}`, cause);
1122
- }
1123
- };
1124
- var GitOperationError = class extends GitError {
1125
- constructor(operation, details, cause) {
1126
- super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
1127
- }
1128
- };
1129
- var WorktreeError = class extends SyncWorktreesError {
1130
- constructor(message, code, cause) {
1131
- super(message, `WORKTREE_${code}`, cause);
1132
- }
1133
- };
1134
- var WorktreeNotCleanError = class extends WorktreeError {
1135
- constructor(path10, reasons) {
1136
- super(`Worktree at '${path10}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
1137
- this.path = path10;
1138
- this.reasons = reasons;
1139
- }
1140
- };
1141
-
1142
- // src/services/worktree-status.service.ts
1143
1141
  var OPERATION_FILES = [
1144
1142
  { file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
1145
1143
  { file: GIT_OPERATIONS.CHERRY_PICK_HEAD, type: "cherry-pick" },
@@ -1755,24 +1753,19 @@ var GitService = class {
1755
1753
  } catch {
1756
1754
  }
1757
1755
  try {
1758
- const branches = await bareGit.branch();
1759
- const localBranchExists = branches.all.includes(branchName);
1760
- if (localBranchExists || branchName.includes("/")) {
1761
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1762
- const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
1763
- await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
1756
+ const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
1757
+ await this.runWorktreeAddByMatrix(
1758
+ bareGit,
1759
+ branchName,
1760
+ absoluteWorktreePath,
1761
+ localBranchExists,
1762
+ remoteBranchExists
1763
+ );
1764
+ if (localBranchExists && !remoteBranchExists) {
1765
+ this.logger.info(` - Created worktree for '${branchName}' (no remote yet \u2014 push to set upstream)`);
1764
1766
  } else {
1765
- await bareGit.raw([
1766
- "worktree",
1767
- "add",
1768
- "--track",
1769
- "-b",
1770
- branchName,
1771
- absoluteWorktreePath,
1772
- `origin/${branchName}`
1773
- ]);
1767
+ this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
1774
1768
  }
1775
- this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
1776
1769
  if (!this.isLfsSkipEnabled()) {
1777
1770
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
1778
1771
  }
@@ -1788,6 +1781,9 @@ var GitService = class {
1788
1781
  }
1789
1782
  } catch (error) {
1790
1783
  const errorMessage = getErrorMessage(error);
1784
+ if (error?.isUpstreamSetupFailure) {
1785
+ throw error;
1786
+ }
1791
1787
  if (errorMessage.includes("Metadata creation failed")) {
1792
1788
  throw error;
1793
1789
  }
@@ -1805,15 +1801,14 @@ var GitService = class {
1805
1801
  } catch {
1806
1802
  }
1807
1803
  try {
1808
- await bareGit.raw([
1809
- "worktree",
1810
- "add",
1811
- "--track",
1812
- "-b",
1804
+ const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
1805
+ await this.runWorktreeAddByMatrix(
1806
+ bareGit,
1813
1807
  branchName,
1814
1808
  absoluteWorktreePath,
1815
- `origin/${branchName}`
1816
- ]);
1809
+ localBranchExists,
1810
+ remoteBranchExists
1811
+ );
1817
1812
  this.logger.info(` - Created worktree for '${branchName}' after pruning`);
1818
1813
  if (!this.isLfsSkipEnabled()) {
1819
1814
  await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
@@ -1882,6 +1877,43 @@ var GitService = class {
1882
1877
  }
1883
1878
  }
1884
1879
  }
1880
+ async runWorktreeAddByMatrix(bareGit, branchName, absoluteWorktreePath, localExists, remoteExists) {
1881
+ if (localExists && remoteExists) {
1882
+ await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1883
+ try {
1884
+ const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
1885
+ await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
1886
+ } catch (error) {
1887
+ let rollbackFailed = false;
1888
+ try {
1889
+ await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
1890
+ } catch (rollbackError) {
1891
+ rollbackFailed = true;
1892
+ this.logger.warn(
1893
+ ` - Rollback failed for '${branchName}' at '${absoluteWorktreePath}' after upstream setup error: ${getErrorMessage(rollbackError)}`
1894
+ );
1895
+ }
1896
+ const detail = getErrorMessage(error);
1897
+ const suffix = rollbackFailed ? " (rollback failed; partial worktree may remain)" : "";
1898
+ const wrapped = new Error(`Failed to set upstream for '${branchName}': ${detail}${suffix}`);
1899
+ wrapped.isUpstreamSetupFailure = true;
1900
+ throw wrapped;
1901
+ }
1902
+ return;
1903
+ }
1904
+ if (localExists) {
1905
+ await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
1906
+ return;
1907
+ }
1908
+ if (remoteExists) {
1909
+ await bareGit.raw(["worktree", "add", "--track", "-b", branchName, absoluteWorktreePath, `origin/${branchName}`]);
1910
+ return;
1911
+ }
1912
+ throw new WorktreeError(
1913
+ `Branch '${branchName}' does not exist locally or on origin; create it first`,
1914
+ "BRANCH_NOT_FOUND"
1915
+ );
1916
+ }
1885
1917
  async removeWorktree(worktreePath) {
1886
1918
  const bareGit = this.getCachedGit(this.bareRepoPath);
1887
1919
  await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
@@ -2075,10 +2107,18 @@ var GitService = class {
2075
2107
  }
2076
2108
  async branchExists(branchName) {
2077
2109
  const bareGit = this.getCachedGit(this.bareRepoPath);
2078
- const localBranches = await bareGit.branch();
2079
- const local = localBranches.all.includes(branchName);
2080
- const remoteBranches = await bareGit.branch(["-r"]);
2081
- const remote = remoteBranches.all.includes(`origin/${branchName}`);
2110
+ const checkRef = async (ref) => {
2111
+ try {
2112
+ await bareGit.raw(["show-ref", "--verify", "--quiet", ref]);
2113
+ return true;
2114
+ } catch {
2115
+ return false;
2116
+ }
2117
+ };
2118
+ const [local, remote] = await Promise.all([
2119
+ checkRef(`${GIT_CONSTANTS.REFS.HEADS}${branchName}`),
2120
+ checkRef(`${GIT_CONSTANTS.REFS.REMOTES}/${branchName}`)
2121
+ ]);
2082
2122
  return { local, remote };
2083
2123
  }
2084
2124
  async getLocalBranches() {
@@ -3730,14 +3770,26 @@ function attachProgressReporter(service, extra) {
3730
3770
  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.";
3731
3771
  var PATH_DESCRIBE_SUFFIX = "Absolute path preferred; relative paths resolve from the server's CWD.";
3732
3772
  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.";
3733
- function createServer(context) {
3773
+ function buildInstructions(snapshot) {
3774
+ const d = snapshot?.discovered;
3775
+ if (!d || !d.isWorktree || d.kind !== "managed") return SERVER_INSTRUCTIONS;
3776
+ const lines = ["Connect-time context (call `detect_context` for live state):"];
3777
+ if (d.kind) lines.push(`- kind: ${d.kind}`);
3778
+ if (d.currentWorktreePath) lines.push(`- currentWorktreePath: ${d.currentWorktreePath}`);
3779
+ if (d.currentBranch) lines.push(`- currentBranch: ${d.currentBranch}`);
3780
+ if (d.configPath) lines.push(`- configPath: ${d.configPath}`);
3781
+ return `${SERVER_INSTRUCTIONS}
3782
+
3783
+ ${lines.join("\n")}`;
3784
+ }
3785
+ function createServer(context, snapshot) {
3734
3786
  const server = new McpServer(
3735
3787
  {
3736
3788
  name: "sync-worktrees",
3737
3789
  version: "1.0.0"
3738
3790
  },
3739
3791
  {
3740
- instructions: SERVER_INSTRUCTIONS
3792
+ instructions: buildInstructions(snapshot)
3741
3793
  }
3742
3794
  );
3743
3795
  server.registerResource(
@@ -3970,8 +4022,9 @@ async function main() {
3970
4022
  `);
3971
4023
  }
3972
4024
  }
4025
+ let discovered = null;
3973
4026
  try {
3974
- const discovered = await context.detectFromPath(process.cwd());
4027
+ discovered = await context.detectFromPath(process.cwd());
3975
4028
  if (discovered.isWorktree) {
3976
4029
  process.stderr.write(
3977
4030
  `[sync-worktrees-mcp] Auto-detected ${discovered.kind} worktree at ${discovered.currentWorktreePath} (branch: ${discovered.currentBranch})
@@ -3982,7 +4035,7 @@ async function main() {
3982
4035
  process.stderr.write(`[sync-worktrees-mcp] Auto-detect failed: ${err.message}
3983
4036
  `);
3984
4037
  }
3985
- const server = createServer(context);
4038
+ const server = createServer(context, { discovered });
3986
4039
  const transport = new StdioServerTransport();
3987
4040
  await server.connect(transport);
3988
4041
  }