sync-worktrees 3.0.1 → 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.
- package/dist/index.js +149 -78
- package/dist/index.js.map +4 -4
- package/dist/mcp-server.js +358 -163
- package/dist/mcp-server.js.map +4 -4
- package/package.json +1 -1
package/dist/mcp-server.js
CHANGED
|
@@ -85,6 +85,11 @@ var PATH_CONSTANTS = {
|
|
|
85
85
|
GIT_DIR: ".git",
|
|
86
86
|
README: "README"
|
|
87
87
|
};
|
|
88
|
+
var CONFIG_FILE_NAMES = [
|
|
89
|
+
"sync-worktrees.config.js",
|
|
90
|
+
"sync-worktrees.config.mjs",
|
|
91
|
+
"sync-worktrees.config.cjs"
|
|
92
|
+
];
|
|
88
93
|
var METADATA_CONSTANTS = {
|
|
89
94
|
MAX_HISTORY_ENTRIES: 10,
|
|
90
95
|
METADATA_FILENAME: "sync-metadata.json",
|
|
@@ -124,6 +129,24 @@ function filterBranchesByName(branches, include, exclude) {
|
|
|
124
129
|
|
|
125
130
|
// src/services/config-loader.service.ts
|
|
126
131
|
var ConfigLoaderService = class {
|
|
132
|
+
async findConfigUpward(startDir) {
|
|
133
|
+
let current = path.resolve(startDir);
|
|
134
|
+
const root = path.parse(current).root;
|
|
135
|
+
while (true) {
|
|
136
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
137
|
+
const candidate = path.join(current, name);
|
|
138
|
+
try {
|
|
139
|
+
await fs.access(candidate);
|
|
140
|
+
return candidate;
|
|
141
|
+
} catch {
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (current === root) return null;
|
|
145
|
+
const parent = path.dirname(current);
|
|
146
|
+
if (parent === current) return null;
|
|
147
|
+
current = parent;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
127
150
|
async loadConfigFile(configPath) {
|
|
128
151
|
const absolutePath = path.resolve(configPath);
|
|
129
152
|
try {
|
|
@@ -780,6 +803,43 @@ import * as fs4 from "fs/promises";
|
|
|
780
803
|
import * as path4 from "path";
|
|
781
804
|
import simpleGit3 from "simple-git";
|
|
782
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
|
+
|
|
783
843
|
// src/utils/git-url.ts
|
|
784
844
|
function extractRepoNameFromUrl(gitUrl) {
|
|
785
845
|
const url = gitUrl.trim();
|
|
@@ -1078,45 +1138,6 @@ var WorktreeMetadataService = class {
|
|
|
1078
1138
|
import * as fs3 from "fs/promises";
|
|
1079
1139
|
import * as path3 from "path";
|
|
1080
1140
|
import simpleGit2 from "simple-git";
|
|
1081
|
-
|
|
1082
|
-
// src/errors/index.ts
|
|
1083
|
-
var SyncWorktreesError = class extends Error {
|
|
1084
|
-
constructor(message, code, cause) {
|
|
1085
|
-
super(message);
|
|
1086
|
-
this.code = code;
|
|
1087
|
-
this.cause = cause;
|
|
1088
|
-
this.name = this.constructor.name;
|
|
1089
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
1090
|
-
if (cause && cause.stack) {
|
|
1091
|
-
this.stack = `${this.stack}
|
|
1092
|
-
Caused by: ${cause.stack}`;
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
};
|
|
1096
|
-
var GitError = class extends SyncWorktreesError {
|
|
1097
|
-
constructor(message, code, cause) {
|
|
1098
|
-
super(message, `GIT_${code}`, cause);
|
|
1099
|
-
}
|
|
1100
|
-
};
|
|
1101
|
-
var GitOperationError = class extends GitError {
|
|
1102
|
-
constructor(operation, details, cause) {
|
|
1103
|
-
super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
|
|
1104
|
-
}
|
|
1105
|
-
};
|
|
1106
|
-
var WorktreeError = class extends SyncWorktreesError {
|
|
1107
|
-
constructor(message, code, cause) {
|
|
1108
|
-
super(message, `WORKTREE_${code}`, cause);
|
|
1109
|
-
}
|
|
1110
|
-
};
|
|
1111
|
-
var WorktreeNotCleanError = class extends WorktreeError {
|
|
1112
|
-
constructor(path10, reasons) {
|
|
1113
|
-
super(`Worktree at '${path10}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
1114
|
-
this.path = path10;
|
|
1115
|
-
this.reasons = reasons;
|
|
1116
|
-
}
|
|
1117
|
-
};
|
|
1118
|
-
|
|
1119
|
-
// src/services/worktree-status.service.ts
|
|
1120
1141
|
var OPERATION_FILES = [
|
|
1121
1142
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
1122
1143
|
{ file: GIT_OPERATIONS.CHERRY_PICK_HEAD, type: "cherry-pick" },
|
|
@@ -1173,6 +1194,7 @@ var WorktreeStatusService = class {
|
|
|
1173
1194
|
if (hasUnpushedCommits) reasons.push("unpushed commits");
|
|
1174
1195
|
if (hasOperationInProgress) reasons.push("operation in progress");
|
|
1175
1196
|
if (hasModifiedSubmodules) reasons.push("modified submodules");
|
|
1197
|
+
if (upstreamGone) reasons.push("upstream gone");
|
|
1176
1198
|
const canRemove = isClean && !hasUnpushedCommits && !hasOperationInProgress && !hasModifiedSubmodules;
|
|
1177
1199
|
const details = includeDetails ? this.buildStatusDetails(snap) : void 0;
|
|
1178
1200
|
return {
|
|
@@ -1731,24 +1753,19 @@ var GitService = class {
|
|
|
1731
1753
|
} catch {
|
|
1732
1754
|
}
|
|
1733
1755
|
try {
|
|
1734
|
-
const
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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)`);
|
|
1740
1766
|
} else {
|
|
1741
|
-
|
|
1742
|
-
"worktree",
|
|
1743
|
-
"add",
|
|
1744
|
-
"--track",
|
|
1745
|
-
"-b",
|
|
1746
|
-
branchName,
|
|
1747
|
-
absoluteWorktreePath,
|
|
1748
|
-
`origin/${branchName}`
|
|
1749
|
-
]);
|
|
1767
|
+
this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
|
|
1750
1768
|
}
|
|
1751
|
-
this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
|
|
1752
1769
|
if (!this.isLfsSkipEnabled()) {
|
|
1753
1770
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
1754
1771
|
}
|
|
@@ -1764,6 +1781,9 @@ var GitService = class {
|
|
|
1764
1781
|
}
|
|
1765
1782
|
} catch (error) {
|
|
1766
1783
|
const errorMessage = getErrorMessage(error);
|
|
1784
|
+
if (error?.isUpstreamSetupFailure) {
|
|
1785
|
+
throw error;
|
|
1786
|
+
}
|
|
1767
1787
|
if (errorMessage.includes("Metadata creation failed")) {
|
|
1768
1788
|
throw error;
|
|
1769
1789
|
}
|
|
@@ -1781,15 +1801,14 @@ var GitService = class {
|
|
|
1781
1801
|
} catch {
|
|
1782
1802
|
}
|
|
1783
1803
|
try {
|
|
1784
|
-
await
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
"--track",
|
|
1788
|
-
"-b",
|
|
1804
|
+
const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
|
|
1805
|
+
await this.runWorktreeAddByMatrix(
|
|
1806
|
+
bareGit,
|
|
1789
1807
|
branchName,
|
|
1790
1808
|
absoluteWorktreePath,
|
|
1791
|
-
|
|
1792
|
-
|
|
1809
|
+
localBranchExists,
|
|
1810
|
+
remoteBranchExists
|
|
1811
|
+
);
|
|
1793
1812
|
this.logger.info(` - Created worktree for '${branchName}' after pruning`);
|
|
1794
1813
|
if (!this.isLfsSkipEnabled()) {
|
|
1795
1814
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
@@ -1858,6 +1877,43 @@ var GitService = class {
|
|
|
1858
1877
|
}
|
|
1859
1878
|
}
|
|
1860
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
|
+
}
|
|
1861
1917
|
async removeWorktree(worktreePath) {
|
|
1862
1918
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
1863
1919
|
await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
|
|
@@ -2051,10 +2107,18 @@ var GitService = class {
|
|
|
2051
2107
|
}
|
|
2052
2108
|
async branchExists(branchName) {
|
|
2053
2109
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
2054
|
-
const
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
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
|
+
]);
|
|
2058
2122
|
return { local, remote };
|
|
2059
2123
|
}
|
|
2060
2124
|
async getLocalBranches() {
|
|
@@ -2804,15 +2868,18 @@ var WorktreeSyncService = class {
|
|
|
2804
2868
|
// src/mcp/context.ts
|
|
2805
2869
|
var AUTO_DETECT_PREFIX = "__auto_detected__:";
|
|
2806
2870
|
var DISCOVERY_CACHE_TTL_MS = 5e3;
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
}
|
|
2871
|
+
function emptyCapabilities(reason) {
|
|
2872
|
+
const state = reason ? { available: false, reason } : { available: false };
|
|
2873
|
+
return {
|
|
2874
|
+
listWorktrees: { ...state },
|
|
2875
|
+
getStatus: { ...state },
|
|
2876
|
+
createWorktree: { ...state },
|
|
2877
|
+
removeWorktree: { ...state },
|
|
2878
|
+
updateWorktree: { ...state },
|
|
2879
|
+
sync: { ...state },
|
|
2880
|
+
initialize: { ...state }
|
|
2881
|
+
};
|
|
2882
|
+
}
|
|
2816
2883
|
function buildUnsupportedContext(currentPath, reason) {
|
|
2817
2884
|
return {
|
|
2818
2885
|
isWorktree: false,
|
|
@@ -2823,10 +2890,11 @@ function buildUnsupportedContext(currentPath, reason) {
|
|
|
2823
2890
|
repoUrl: null,
|
|
2824
2891
|
worktreeDir: null,
|
|
2825
2892
|
allWorktrees: [],
|
|
2826
|
-
|
|
2893
|
+
siblingRepositories: [],
|
|
2894
|
+
configPath: null,
|
|
2827
2895
|
repoName: null,
|
|
2828
|
-
capabilities:
|
|
2829
|
-
|
|
2896
|
+
capabilities: emptyCapabilities(reason),
|
|
2897
|
+
notes: [reason]
|
|
2830
2898
|
};
|
|
2831
2899
|
}
|
|
2832
2900
|
function createStderrLogger(repoName) {
|
|
@@ -2843,7 +2911,8 @@ var RepositoryContext = class {
|
|
|
2843
2911
|
configPath = null;
|
|
2844
2912
|
configLoader = new ConfigLoaderService();
|
|
2845
2913
|
discoveryCache = /* @__PURE__ */ new Map();
|
|
2846
|
-
async loadConfig(configPath) {
|
|
2914
|
+
async loadConfig(configPath, options = {}) {
|
|
2915
|
+
const setDefaultCurrent = options.setDefaultCurrent ?? true;
|
|
2847
2916
|
const absolutePath = path8.resolve(configPath);
|
|
2848
2917
|
const configFile = await this.configLoader.loadConfigFile(absolutePath);
|
|
2849
2918
|
for (const [name, entry] of this.repos) {
|
|
@@ -2865,9 +2934,10 @@ var RepositoryContext = class {
|
|
|
2865
2934
|
if (this.currentRepo && !this.repos.has(this.currentRepo)) {
|
|
2866
2935
|
this.currentRepo = null;
|
|
2867
2936
|
}
|
|
2868
|
-
if (!this.currentRepo && configFile.repositories.length > 0) {
|
|
2937
|
+
if (setDefaultCurrent && !this.currentRepo && configFile.repositories.length > 0) {
|
|
2869
2938
|
this.currentRepo = configFile.repositories[0].name;
|
|
2870
2939
|
}
|
|
2940
|
+
this.discoveryCache.clear();
|
|
2871
2941
|
return configFile.repositories;
|
|
2872
2942
|
}
|
|
2873
2943
|
async detectFromPath(dirPath) {
|
|
@@ -2876,6 +2946,17 @@ var RepositoryContext = class {
|
|
|
2876
2946
|
if (cached && await this.isCacheFresh(cached)) {
|
|
2877
2947
|
return cached.result;
|
|
2878
2948
|
}
|
|
2949
|
+
if (this.configPath === null) {
|
|
2950
|
+
const found = await this.configLoader.findConfigUpward(absolutePath);
|
|
2951
|
+
if (found) {
|
|
2952
|
+
try {
|
|
2953
|
+
await this.loadConfig(found, { setDefaultCurrent: false });
|
|
2954
|
+
} catch (err) {
|
|
2955
|
+
process.stderr.write(`[sync-worktrees] auto-loaded config failed: ${err.message}
|
|
2956
|
+
`);
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2879
2960
|
const { result, adminDir } = await this.detectFromPathUncached(absolutePath);
|
|
2880
2961
|
if (result.isWorktree && result.bareRepoPath && adminDir) {
|
|
2881
2962
|
const [worktreeHeadMtimeMs, worktreesDirMtimeMs] = await Promise.all([
|
|
@@ -2911,10 +2992,49 @@ var RepositoryContext = class {
|
|
|
2911
2992
|
__discoveryCacheSizeForTest() {
|
|
2912
2993
|
return this.discoveryCache.size;
|
|
2913
2994
|
}
|
|
2914
|
-
|
|
2995
|
+
async discoverSiblingRepositories(currentBareRepoPath) {
|
|
2996
|
+
const repoDir = path8.dirname(currentBareRepoPath);
|
|
2997
|
+
const workspaceRoot = path8.dirname(repoDir);
|
|
2998
|
+
if (workspaceRoot === repoDir) return [];
|
|
2999
|
+
let entries;
|
|
3000
|
+
try {
|
|
3001
|
+
entries = await fs7.readdir(workspaceRoot);
|
|
3002
|
+
} catch {
|
|
3003
|
+
return [];
|
|
3004
|
+
}
|
|
3005
|
+
const configBares = /* @__PURE__ */ new Map();
|
|
3006
|
+
for (const entry of this.repos.values()) {
|
|
3007
|
+
if (entry.source === "config" && entry.config.bareRepoDir) {
|
|
3008
|
+
configBares.set(normalizePathForCompare(entry.config.bareRepoDir), entry.name);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
const results = [];
|
|
3012
|
+
await Promise.all(
|
|
3013
|
+
entries.map(async (entry) => {
|
|
3014
|
+
const candidate = path8.join(workspaceRoot, entry);
|
|
3015
|
+
const bareCandidate = path8.join(candidate, GIT_CONSTANTS.BARE_DIR_NAME);
|
|
3016
|
+
try {
|
|
3017
|
+
const stat4 = await fs7.stat(bareCandidate);
|
|
3018
|
+
if (!stat4.isDirectory()) return;
|
|
3019
|
+
} catch {
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
const resolvedBare = path8.resolve(bareCandidate);
|
|
3023
|
+
const matchedName = configBares.get(normalizePathForCompare(resolvedBare));
|
|
3024
|
+
results.push({
|
|
3025
|
+
name: matchedName ?? entry,
|
|
3026
|
+
bareRepoPath: resolvedBare,
|
|
3027
|
+
configMatched: matchedName !== void 0
|
|
3028
|
+
});
|
|
3029
|
+
})
|
|
3030
|
+
);
|
|
3031
|
+
results.sort((a, b) => a.name.localeCompare(b.name));
|
|
3032
|
+
return results;
|
|
3033
|
+
}
|
|
3034
|
+
bootstrapCurrentRepo(candidate, force = false) {
|
|
2915
3035
|
if (this.currentRepo !== null) return;
|
|
2916
3036
|
if (!this.repos.has(candidate)) return;
|
|
2917
|
-
if (this.repos.size !== 1) return;
|
|
3037
|
+
if (!force && this.repos.size !== 1) return;
|
|
2918
3038
|
this.currentRepo = candidate;
|
|
2919
3039
|
}
|
|
2920
3040
|
async isCacheFresh(cached) {
|
|
@@ -2927,14 +3047,14 @@ var RepositoryContext = class {
|
|
|
2927
3047
|
return currentHeadMtime === cached.worktreeHeadMtimeMs && currentWorktreesDirMtime === cached.worktreesDirMtimeMs;
|
|
2928
3048
|
}
|
|
2929
3049
|
async detectFromPathUncached(absolutePath) {
|
|
2930
|
-
const
|
|
3050
|
+
const notes = [];
|
|
2931
3051
|
const located = await findWorktreeRoot(absolutePath);
|
|
2932
3052
|
const worktreeRoot = located?.worktreeRoot ?? absolutePath;
|
|
2933
|
-
const unsupported = (reason
|
|
2934
|
-
|
|
3053
|
+
const unsupported = (reason) => {
|
|
3054
|
+
notes.push(reason);
|
|
2935
3055
|
return {
|
|
2936
3056
|
result: {
|
|
2937
|
-
isWorktree,
|
|
3057
|
+
isWorktree: false,
|
|
2938
3058
|
kind: "unsupported",
|
|
2939
3059
|
currentBranch: null,
|
|
2940
3060
|
currentWorktreePath: worktreeRoot,
|
|
@@ -2942,10 +3062,11 @@ var RepositoryContext = class {
|
|
|
2942
3062
|
repoUrl: null,
|
|
2943
3063
|
worktreeDir: null,
|
|
2944
3064
|
allWorktrees: [],
|
|
2945
|
-
|
|
3065
|
+
siblingRepositories: [],
|
|
3066
|
+
configPath: this.configPath,
|
|
2946
3067
|
repoName: null,
|
|
2947
|
-
capabilities:
|
|
2948
|
-
|
|
3068
|
+
capabilities: emptyCapabilities(reason),
|
|
3069
|
+
notes
|
|
2949
3070
|
},
|
|
2950
3071
|
adminDir: null
|
|
2951
3072
|
};
|
|
@@ -2979,7 +3100,7 @@ var RepositoryContext = class {
|
|
|
2979
3100
|
const urlStr = typeof remoteResult === "string" ? remoteResult.trim() : "";
|
|
2980
3101
|
repoUrl = urlStr || null;
|
|
2981
3102
|
} catch {
|
|
2982
|
-
|
|
3103
|
+
notes.push("Could not read remote origin URL");
|
|
2983
3104
|
}
|
|
2984
3105
|
const listOutput = await bareGit.raw(["worktree", "list", "--porcelain"]);
|
|
2985
3106
|
worktrees = parseWorktreeList(listOutput, worktreeRoot);
|
|
@@ -2988,7 +3109,8 @@ var RepositoryContext = class {
|
|
|
2988
3109
|
currentBranch = current.branch;
|
|
2989
3110
|
}
|
|
2990
3111
|
} catch (err) {
|
|
2991
|
-
|
|
3112
|
+
const reason = `Failed to read bare repo at ${bareRepoPath}: ${err.message}`;
|
|
3113
|
+
notes.push(reason);
|
|
2992
3114
|
return {
|
|
2993
3115
|
result: {
|
|
2994
3116
|
isWorktree: true,
|
|
@@ -2999,34 +3121,31 @@ var RepositoryContext = class {
|
|
|
2999
3121
|
repoUrl: null,
|
|
3000
3122
|
worktreeDir: null,
|
|
3001
3123
|
allWorktrees: [],
|
|
3002
|
-
|
|
3124
|
+
siblingRepositories: [],
|
|
3125
|
+
configPath: this.configPath,
|
|
3003
3126
|
repoName: null,
|
|
3004
|
-
capabilities:
|
|
3005
|
-
|
|
3127
|
+
capabilities: emptyCapabilities(reason),
|
|
3128
|
+
notes
|
|
3006
3129
|
},
|
|
3007
3130
|
adminDir
|
|
3008
3131
|
};
|
|
3009
3132
|
}
|
|
3010
3133
|
const worktreeDir = path8.dirname(worktreeRoot);
|
|
3134
|
+
const noUrlReason = "no remote origin URL detected";
|
|
3011
3135
|
const capabilities = {
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3136
|
+
listWorktrees: { available: true },
|
|
3137
|
+
getStatus: { available: true },
|
|
3138
|
+
createWorktree: repoUrl !== null ? { available: true } : { available: false, reason: noUrlReason },
|
|
3139
|
+
removeWorktree: { available: true },
|
|
3140
|
+
updateWorktree: { available: true },
|
|
3141
|
+
sync: { available: false, reason: "no config and no remote URL" },
|
|
3142
|
+
initialize: { available: false, reason: "no config and no remote URL" }
|
|
3019
3143
|
};
|
|
3020
|
-
|
|
3021
|
-
reasons.push("create_worktree unavailable: no remote origin URL detected");
|
|
3022
|
-
}
|
|
3023
|
-
const foldPath = (p) => isCaseInsensitiveFs() ? p.toLowerCase() : p;
|
|
3024
|
-
const foldedBare = foldPath(bareRepoPath);
|
|
3144
|
+
const foldedBare = normalizePathForCompare(bareRepoPath);
|
|
3025
3145
|
let matchedConfig = null;
|
|
3026
3146
|
for (const entry of this.repos.values()) {
|
|
3027
|
-
if (entry.source === "config") {
|
|
3028
|
-
|
|
3029
|
-
if (entryBare && foldPath(entryBare) === foldedBare) {
|
|
3147
|
+
if (entry.source === "config" && entry.config.bareRepoDir) {
|
|
3148
|
+
if (normalizePathForCompare(entry.config.bareRepoDir) === foldedBare) {
|
|
3030
3149
|
matchedConfig = entry;
|
|
3031
3150
|
break;
|
|
3032
3151
|
}
|
|
@@ -3037,8 +3156,8 @@ var RepositoryContext = class {
|
|
|
3037
3156
|
if (matchedConfig) {
|
|
3038
3157
|
repoName = matchedConfig.name;
|
|
3039
3158
|
kind = "managed";
|
|
3040
|
-
capabilities.
|
|
3041
|
-
capabilities.
|
|
3159
|
+
capabilities.sync = { available: true };
|
|
3160
|
+
capabilities.initialize = { available: true };
|
|
3042
3161
|
} else if (repoUrl) {
|
|
3043
3162
|
const syntheticConfig = {
|
|
3044
3163
|
repoUrl,
|
|
@@ -3056,13 +3175,14 @@ var RepositoryContext = class {
|
|
|
3056
3175
|
});
|
|
3057
3176
|
}
|
|
3058
3177
|
repoName = detectedKey;
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3178
|
+
const autoReason = "no config file loaded (running in auto-detect mode)";
|
|
3179
|
+
capabilities.sync = { available: false, reason: autoReason };
|
|
3180
|
+
capabilities.initialize = { available: false, reason: autoReason };
|
|
3062
3181
|
}
|
|
3063
3182
|
if (repoName) {
|
|
3064
|
-
this.bootstrapCurrentRepo(repoName);
|
|
3183
|
+
this.bootstrapCurrentRepo(repoName, matchedConfig !== null);
|
|
3065
3184
|
}
|
|
3185
|
+
const siblingRepositories = await this.discoverSiblingRepositories(bareRepoPath);
|
|
3066
3186
|
const discovered = {
|
|
3067
3187
|
isWorktree: true,
|
|
3068
3188
|
kind,
|
|
@@ -3072,10 +3192,11 @@ var RepositoryContext = class {
|
|
|
3072
3192
|
repoUrl,
|
|
3073
3193
|
worktreeDir,
|
|
3074
3194
|
allWorktrees: worktrees,
|
|
3075
|
-
|
|
3195
|
+
siblingRepositories,
|
|
3196
|
+
configPath: this.configPath,
|
|
3076
3197
|
repoName,
|
|
3077
3198
|
capabilities,
|
|
3078
|
-
|
|
3199
|
+
notes
|
|
3079
3200
|
};
|
|
3080
3201
|
if (repoName) {
|
|
3081
3202
|
const entry = this.repos.get(repoName);
|
|
@@ -3134,9 +3255,7 @@ var RepositoryContext = class {
|
|
|
3134
3255
|
}
|
|
3135
3256
|
};
|
|
3136
3257
|
function parseWorktreeList(output, currentPath) {
|
|
3137
|
-
const
|
|
3138
|
-
const fold = (p) => isCaseInsensitiveFs() ? p.toLowerCase() : p;
|
|
3139
|
-
const foldedCurrent = fold(resolvedCurrent);
|
|
3258
|
+
const foldedCurrent = normalizePathForCompare(currentPath);
|
|
3140
3259
|
const results = [];
|
|
3141
3260
|
for (const wt of parseWorktreeListPorcelain(output)) {
|
|
3142
3261
|
const resolved = path8.resolve(wt.path);
|
|
@@ -3145,7 +3264,7 @@ function parseWorktreeList(output, currentPath) {
|
|
|
3145
3264
|
results.push({
|
|
3146
3265
|
path: resolved,
|
|
3147
3266
|
branch,
|
|
3148
|
-
isCurrent:
|
|
3267
|
+
isCurrent: normalizePathForCompare(resolved) === foldedCurrent
|
|
3149
3268
|
});
|
|
3150
3269
|
}
|
|
3151
3270
|
return results;
|
|
@@ -3189,7 +3308,24 @@ import { z } from "zod";
|
|
|
3189
3308
|
// src/mcp/handlers.ts
|
|
3190
3309
|
import * as path9 from "path";
|
|
3191
3310
|
import pLimit2 from "p-limit";
|
|
3192
|
-
|
|
3311
|
+
|
|
3312
|
+
// src/utils/disk-space.ts
|
|
3313
|
+
import fastFolderSize from "fast-folder-size";
|
|
3314
|
+
async function calculateDirectorySize(dirPath) {
|
|
3315
|
+
return new Promise((resolve9, reject) => {
|
|
3316
|
+
fastFolderSize(dirPath, (err, bytes) => {
|
|
3317
|
+
if (err) {
|
|
3318
|
+
reject(err);
|
|
3319
|
+
return;
|
|
3320
|
+
}
|
|
3321
|
+
if (bytes === void 0) {
|
|
3322
|
+
reject(new Error(`fast-folder-size returned no bytes for ${dirPath}`));
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
resolve9(bytes);
|
|
3326
|
+
});
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3193
3329
|
|
|
3194
3330
|
// src/utils/git-validation.ts
|
|
3195
3331
|
function isValidGitBranchName(name) {
|
|
@@ -3299,12 +3435,45 @@ function wrapHandler(fn) {
|
|
|
3299
3435
|
};
|
|
3300
3436
|
}
|
|
3301
3437
|
|
|
3438
|
+
// src/mcp/worktree-summary.ts
|
|
3439
|
+
import simpleGit5 from "simple-git";
|
|
3440
|
+
function deriveLabel(status, isCurrent) {
|
|
3441
|
+
if (isCurrent) return "current";
|
|
3442
|
+
if (!status.isClean || status.hasUnpushedCommits || status.hasStashedChanges) return "dirty";
|
|
3443
|
+
if (status.upstreamGone) return "stale";
|
|
3444
|
+
return "clean";
|
|
3445
|
+
}
|
|
3446
|
+
function deriveSafeToRemove(status) {
|
|
3447
|
+
if (status.canRemove && !status.upstreamGone) {
|
|
3448
|
+
return { safe: true, reason: "clean tree, no unpushed commits" };
|
|
3449
|
+
}
|
|
3450
|
+
if (status.canRemove && status.upstreamGone) {
|
|
3451
|
+
return { safe: false, reason: "branch deleted upstream \u2014 verify no work is lost before removal" };
|
|
3452
|
+
}
|
|
3453
|
+
if (status.reasons.length > 0) {
|
|
3454
|
+
return { safe: false, reason: status.reasons.join(", ") };
|
|
3455
|
+
}
|
|
3456
|
+
return { safe: false, reason: "not safe to remove" };
|
|
3457
|
+
}
|
|
3458
|
+
async function getDivergence(worktreePath) {
|
|
3459
|
+
try {
|
|
3460
|
+
const git = simpleGit5(worktreePath);
|
|
3461
|
+
const output = await git.raw(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
|
|
3462
|
+
const [aheadStr, behindStr] = output.trim().split(/\s+/);
|
|
3463
|
+
return { ahead: parseInt(aheadStr, 10), behind: parseInt(behindStr, 10) };
|
|
3464
|
+
} catch {
|
|
3465
|
+
return null;
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3302
3469
|
// src/mcp/handlers.ts
|
|
3303
3470
|
var pathResolution = new PathResolutionService();
|
|
3304
3471
|
function ensureCapability(discovered, key, toolName) {
|
|
3305
3472
|
if (!discovered) return;
|
|
3306
|
-
|
|
3307
|
-
|
|
3473
|
+
const cap = discovered.capabilities[key];
|
|
3474
|
+
if (!cap.available) {
|
|
3475
|
+
const reasons = cap.reason ? [cap.reason] : discovered.notes;
|
|
3476
|
+
throw new CapabilityUnavailableError(toolName, reasons);
|
|
3308
3477
|
}
|
|
3309
3478
|
}
|
|
3310
3479
|
async function ensureNotSyncing(ctx, repoName) {
|
|
@@ -3349,30 +3518,35 @@ async function ensurePathBelongsToRepo(ctx, targetPath, repoName, git) {
|
|
|
3349
3518
|
}
|
|
3350
3519
|
throw new Error(`Path '${targetPath}' is not a registered worktree of the current repository`);
|
|
3351
3520
|
}
|
|
3352
|
-
function deriveLabel(status, isCurrent) {
|
|
3353
|
-
if (isCurrent) return "current";
|
|
3354
|
-
if (!status.isClean || status.hasUnpushedCommits || status.hasStashedChanges) return "dirty";
|
|
3355
|
-
if (status.upstreamGone) return "stale";
|
|
3356
|
-
return "clean";
|
|
3357
|
-
}
|
|
3358
|
-
async function getDivergence(worktreePath) {
|
|
3359
|
-
try {
|
|
3360
|
-
const git = simpleGit5(worktreePath);
|
|
3361
|
-
const output = await git.raw(["rev-list", "--left-right", "--count", "HEAD...@{upstream}"]);
|
|
3362
|
-
const [aheadStr, behindStr] = output.trim().split(/\s+/);
|
|
3363
|
-
return { ahead: parseInt(aheadStr, 10), behind: parseInt(behindStr, 10) };
|
|
3364
|
-
} catch {
|
|
3365
|
-
return null;
|
|
3366
|
-
}
|
|
3367
|
-
}
|
|
3368
3521
|
async function handleDetectContext(ctx, params, _extra) {
|
|
3369
3522
|
const target = params.path ?? process.cwd();
|
|
3370
3523
|
const discovered = await ctx.detectFromPath(target);
|
|
3371
|
-
|
|
3524
|
+
if (!params.includeStatus || discovered.allWorktrees.length === 0) {
|
|
3525
|
+
return formatToolResponse(discovered);
|
|
3526
|
+
}
|
|
3527
|
+
const statusService = new WorktreeStatusService();
|
|
3528
|
+
const limit = pLimit2(DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS);
|
|
3529
|
+
const enriched = await Promise.all(
|
|
3530
|
+
discovered.allWorktrees.map(
|
|
3531
|
+
(wt) => limit(async () => {
|
|
3532
|
+
const [status, divergence] = await Promise.all([
|
|
3533
|
+
statusService.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
3534
|
+
getDivergence(wt.path)
|
|
3535
|
+
]);
|
|
3536
|
+
return {
|
|
3537
|
+
...wt,
|
|
3538
|
+
label: status ? deriveLabel(status, wt.isCurrent) : wt.isCurrent ? "current" : "unknown",
|
|
3539
|
+
divergence,
|
|
3540
|
+
staleHint: status?.upstreamGone ?? false
|
|
3541
|
+
};
|
|
3542
|
+
})
|
|
3543
|
+
)
|
|
3544
|
+
);
|
|
3545
|
+
return formatToolResponse({ ...discovered, allWorktrees: enriched });
|
|
3372
3546
|
}
|
|
3373
3547
|
async function handleListWorktrees(ctx, params, _extra) {
|
|
3374
3548
|
const { discovered, git } = await getReadyService(ctx, params.repoName, {
|
|
3375
|
-
capability: "
|
|
3549
|
+
capability: "listWorktrees",
|
|
3376
3550
|
toolName: "list_worktrees"
|
|
3377
3551
|
});
|
|
3378
3552
|
let worktrees;
|
|
@@ -3392,20 +3566,22 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
3392
3566
|
(wt) => limit(async () => {
|
|
3393
3567
|
const resolvedPath = path9.resolve(wt.path);
|
|
3394
3568
|
const isCurrent = currentPath !== null && pathsEqual(wt.path, currentPath);
|
|
3395
|
-
const [status, divergence, metadata] = await Promise.all([
|
|
3569
|
+
const [status, divergence, metadata, sizeBytes] = await Promise.all([
|
|
3396
3570
|
git.getFullWorktreeStatus(wt.path, false).catch(() => null),
|
|
3397
3571
|
getDivergence(wt.path),
|
|
3398
|
-
git.getWorktreeMetadata(wt.path).catch(() => null)
|
|
3572
|
+
git.getWorktreeMetadata(wt.path).catch(() => null),
|
|
3573
|
+
params.includeSize ? calculateDirectorySize(wt.path).catch(() => null) : Promise.resolve(null)
|
|
3399
3574
|
]);
|
|
3400
3575
|
return {
|
|
3401
3576
|
path: resolvedPath,
|
|
3402
3577
|
branch: wt.branch,
|
|
3403
3578
|
isCurrent,
|
|
3404
|
-
label: status ? deriveLabel(status, isCurrent) : "unknown",
|
|
3579
|
+
label: status ? deriveLabel(status, isCurrent) : isCurrent ? "current" : "unknown",
|
|
3405
3580
|
status,
|
|
3406
3581
|
divergence,
|
|
3407
|
-
safeToRemove: status ? status
|
|
3408
|
-
lastSyncAt: metadata?.lastSyncDate ?? null
|
|
3582
|
+
safeToRemove: status ? deriveSafeToRemove(status) : { safe: false, reason: "status unavailable" },
|
|
3583
|
+
lastSyncAt: metadata?.lastSyncDate ?? null,
|
|
3584
|
+
sizeBytes
|
|
3409
3585
|
};
|
|
3410
3586
|
})
|
|
3411
3587
|
)
|
|
@@ -3414,7 +3590,7 @@ async function handleListWorktrees(ctx, params, _extra) {
|
|
|
3414
3590
|
}
|
|
3415
3591
|
async function handleGetWorktreeStatus(ctx, params, _extra) {
|
|
3416
3592
|
const { git } = await getReadyService(ctx, params.repoName, {
|
|
3417
|
-
capability: "
|
|
3593
|
+
capability: "getStatus",
|
|
3418
3594
|
toolName: "get_worktree_status"
|
|
3419
3595
|
});
|
|
3420
3596
|
const resolvedPath = await ensureRepoWorktreePath(ctx, params, git);
|
|
@@ -3435,7 +3611,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
3435
3611
|
throw new Error(`Invalid branch name '${branchName}': ${validation.error}`);
|
|
3436
3612
|
}
|
|
3437
3613
|
const { service, git } = await getReadyService(ctx, params.repoName, {
|
|
3438
|
-
capability: "
|
|
3614
|
+
capability: "createWorktree",
|
|
3439
3615
|
toolName: "create_worktree",
|
|
3440
3616
|
ensureInitialized: true,
|
|
3441
3617
|
ensureNotSyncing: true
|
|
@@ -3475,7 +3651,7 @@ async function handleCreateWorktree(ctx, params, _extra) {
|
|
|
3475
3651
|
}
|
|
3476
3652
|
async function handleRemoveWorktree(ctx, params, _extra) {
|
|
3477
3653
|
const { git } = await getReadyService(ctx, params.repoName, {
|
|
3478
|
-
capability: "
|
|
3654
|
+
capability: "removeWorktree",
|
|
3479
3655
|
toolName: "remove_worktree",
|
|
3480
3656
|
ensureInitialized: true,
|
|
3481
3657
|
ensureNotSyncing: true
|
|
@@ -3496,7 +3672,7 @@ async function handleRemoveWorktree(ctx, params, _extra) {
|
|
|
3496
3672
|
}
|
|
3497
3673
|
async function handleSync(ctx, params, extra) {
|
|
3498
3674
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
3499
|
-
capability: "
|
|
3675
|
+
capability: "sync",
|
|
3500
3676
|
toolName: "sync",
|
|
3501
3677
|
ensureInitialized: true
|
|
3502
3678
|
});
|
|
@@ -3516,7 +3692,7 @@ async function handleSync(ctx, params, extra) {
|
|
|
3516
3692
|
}
|
|
3517
3693
|
async function handleUpdateWorktree(ctx, params, _extra) {
|
|
3518
3694
|
const { git } = await getReadyService(ctx, params.repoName, {
|
|
3519
|
-
capability: "
|
|
3695
|
+
capability: "updateWorktree",
|
|
3520
3696
|
toolName: "update_worktree",
|
|
3521
3697
|
ensureInitialized: true,
|
|
3522
3698
|
ensureNotSyncing: true
|
|
@@ -3531,7 +3707,7 @@ async function handleUpdateWorktree(ctx, params, _extra) {
|
|
|
3531
3707
|
}
|
|
3532
3708
|
async function handleInitialize(ctx, params, extra) {
|
|
3533
3709
|
const { service } = await getReadyService(ctx, params.repoName, {
|
|
3534
|
-
capability: "
|
|
3710
|
+
capability: "initialize",
|
|
3535
3711
|
toolName: "initialize",
|
|
3536
3712
|
ensureNotSyncing: true
|
|
3537
3713
|
});
|
|
@@ -3593,15 +3769,27 @@ function attachProgressReporter(service, extra) {
|
|
|
3593
3769
|
// src/mcp/server.ts
|
|
3594
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.";
|
|
3595
3771
|
var PATH_DESCRIBE_SUFFIX = "Absolute path preferred; relative paths resolve from the server's CWD.";
|
|
3596
|
-
var SERVER_INSTRUCTIONS = "Before running git worktree operations, call `detect_context` to learn the current repo, current branch, and which capabilities are available. It
|
|
3597
|
-
function
|
|
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.";
|
|
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) {
|
|
3598
3786
|
const server = new McpServer(
|
|
3599
3787
|
{
|
|
3600
3788
|
name: "sync-worktrees",
|
|
3601
3789
|
version: "1.0.0"
|
|
3602
3790
|
},
|
|
3603
3791
|
{
|
|
3604
|
-
instructions:
|
|
3792
|
+
instructions: buildInstructions(snapshot)
|
|
3605
3793
|
}
|
|
3606
3794
|
);
|
|
3607
3795
|
server.registerResource(
|
|
@@ -3609,7 +3797,7 @@ function createServer(context) {
|
|
|
3609
3797
|
"sync-worktrees://workspace",
|
|
3610
3798
|
{
|
|
3611
3799
|
title: "Workspace context",
|
|
3612
|
-
description: "Current sync-worktrees workspace context: whether CWD is inside a managed worktree, the current branch, sibling worktrees, and available
|
|
3800
|
+
description: "Current sync-worktrees workspace context: whether CWD is inside a managed worktree, the current branch, sibling worktrees, sibling repositories, auto-discovered configPath, and per-capability {available, reason}. Returns { isWorktree: false } when CWD is outside any workspace.",
|
|
3613
3801
|
mimeType: "application/json"
|
|
3614
3802
|
},
|
|
3615
3803
|
async (uri) => {
|
|
@@ -3633,9 +3821,12 @@ function createServer(context) {
|
|
|
3633
3821
|
server.registerTool(
|
|
3634
3822
|
"detect_context",
|
|
3635
3823
|
{
|
|
3636
|
-
description: "Detect sync-worktrees structure from a filesystem path. Reads .git file, resolves bare repo, discovers sibling worktrees. Defaults to CWD. Use when: bootstrapping from an unknown checkout
|
|
3824
|
+
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[].",
|
|
3637
3825
|
inputSchema: {
|
|
3638
|
-
path: z.string().optional().describe("Directory path to inspect. Defaults to the server's CWD.")
|
|
3826
|
+
path: z.string().optional().describe("Directory path to inspect. Defaults to the server's CWD."),
|
|
3827
|
+
includeStatus: z.boolean().optional().describe(
|
|
3828
|
+
"If true, enriches each entry in allWorktrees with label, divergence, and staleHint. Adds one git status + rev-list per worktree. Default: false (cheap path)."
|
|
3829
|
+
)
|
|
3639
3830
|
},
|
|
3640
3831
|
annotations: {
|
|
3641
3832
|
title: "Detect sync-worktrees context",
|
|
@@ -3649,9 +3840,12 @@ function createServer(context) {
|
|
|
3649
3840
|
server.registerTool(
|
|
3650
3841
|
"list_worktrees",
|
|
3651
3842
|
{
|
|
3652
|
-
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, lastSyncAt }.",
|
|
3843
|
+
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 }.",
|
|
3653
3844
|
inputSchema: {
|
|
3654
|
-
repoName: z.string().optional().describe(REPO_NAME_DESCRIBE)
|
|
3845
|
+
repoName: z.string().optional().describe(REPO_NAME_DESCRIBE),
|
|
3846
|
+
includeSize: z.boolean().optional().describe(
|
|
3847
|
+
"If true, computes the on-disk size of each worktree (in bytes). Slow on large worktrees. Default: false (sizeBytes returned as null)."
|
|
3848
|
+
)
|
|
3655
3849
|
},
|
|
3656
3850
|
annotations: {
|
|
3657
3851
|
title: "List worktrees with status",
|
|
@@ -3828,8 +4022,9 @@ async function main() {
|
|
|
3828
4022
|
`);
|
|
3829
4023
|
}
|
|
3830
4024
|
}
|
|
4025
|
+
let discovered = null;
|
|
3831
4026
|
try {
|
|
3832
|
-
|
|
4027
|
+
discovered = await context.detectFromPath(process.cwd());
|
|
3833
4028
|
if (discovered.isWorktree) {
|
|
3834
4029
|
process.stderr.write(
|
|
3835
4030
|
`[sync-worktrees-mcp] Auto-detected ${discovered.kind} worktree at ${discovered.currentWorktreePath} (branch: ${discovered.currentBranch})
|
|
@@ -3840,7 +4035,7 @@ async function main() {
|
|
|
3840
4035
|
process.stderr.write(`[sync-worktrees-mcp] Auto-detect failed: ${err.message}
|
|
3841
4036
|
`);
|
|
3842
4037
|
}
|
|
3843
|
-
const server = createServer(context);
|
|
4038
|
+
const server = createServer(context, { discovered });
|
|
3844
4039
|
const transport = new StdioServerTransport();
|
|
3845
4040
|
await server.connect(transport);
|
|
3846
4041
|
}
|