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/index.js
CHANGED
|
@@ -83,6 +83,11 @@ var PATH_CONSTANTS = {
|
|
|
83
83
|
GIT_DIR: ".git",
|
|
84
84
|
README: "README"
|
|
85
85
|
};
|
|
86
|
+
var CONFIG_FILE_NAMES = [
|
|
87
|
+
"sync-worktrees.config.js",
|
|
88
|
+
"sync-worktrees.config.mjs",
|
|
89
|
+
"sync-worktrees.config.cjs"
|
|
90
|
+
];
|
|
86
91
|
var METADATA_CONSTANTS = {
|
|
87
92
|
MAX_HISTORY_ENTRIES: 10,
|
|
88
93
|
METADATA_FILENAME: "sync-metadata.json",
|
|
@@ -144,6 +149,24 @@ function filterBranchesByName(branches, include, exclude) {
|
|
|
144
149
|
|
|
145
150
|
// src/services/config-loader.service.ts
|
|
146
151
|
var ConfigLoaderService = class {
|
|
152
|
+
async findConfigUpward(startDir) {
|
|
153
|
+
let current = path.resolve(startDir);
|
|
154
|
+
const root = path.parse(current).root;
|
|
155
|
+
while (true) {
|
|
156
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
157
|
+
const candidate = path.join(current, name);
|
|
158
|
+
try {
|
|
159
|
+
await fs.access(candidate);
|
|
160
|
+
return candidate;
|
|
161
|
+
} catch {
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (current === root) return null;
|
|
165
|
+
const parent = path.dirname(current);
|
|
166
|
+
if (parent === current) return null;
|
|
167
|
+
current = parent;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
147
170
|
async loadConfigFile(configPath) {
|
|
148
171
|
const absolutePath = path.resolve(configPath);
|
|
149
172
|
try {
|
|
@@ -1955,6 +1978,43 @@ import * as fs4 from "fs/promises";
|
|
|
1955
1978
|
import * as path4 from "path";
|
|
1956
1979
|
import simpleGit3 from "simple-git";
|
|
1957
1980
|
|
|
1981
|
+
// src/errors/index.ts
|
|
1982
|
+
var SyncWorktreesError = class extends Error {
|
|
1983
|
+
constructor(message, code, cause) {
|
|
1984
|
+
super(message);
|
|
1985
|
+
this.code = code;
|
|
1986
|
+
this.cause = cause;
|
|
1987
|
+
this.name = this.constructor.name;
|
|
1988
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
1989
|
+
if (cause && cause.stack) {
|
|
1990
|
+
this.stack = `${this.stack}
|
|
1991
|
+
Caused by: ${cause.stack}`;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
};
|
|
1995
|
+
var GitError = class extends SyncWorktreesError {
|
|
1996
|
+
constructor(message, code, cause) {
|
|
1997
|
+
super(message, `GIT_${code}`, cause);
|
|
1998
|
+
}
|
|
1999
|
+
};
|
|
2000
|
+
var GitOperationError = class extends GitError {
|
|
2001
|
+
constructor(operation, details, cause) {
|
|
2002
|
+
super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
|
|
2003
|
+
}
|
|
2004
|
+
};
|
|
2005
|
+
var WorktreeError = class extends SyncWorktreesError {
|
|
2006
|
+
constructor(message, code, cause) {
|
|
2007
|
+
super(message, `WORKTREE_${code}`, cause);
|
|
2008
|
+
}
|
|
2009
|
+
};
|
|
2010
|
+
var WorktreeNotCleanError = class extends WorktreeError {
|
|
2011
|
+
constructor(path12, reasons) {
|
|
2012
|
+
super(`Worktree at '${path12}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
2013
|
+
this.path = path12;
|
|
2014
|
+
this.reasons = reasons;
|
|
2015
|
+
}
|
|
2016
|
+
};
|
|
2017
|
+
|
|
1958
2018
|
// src/utils/git-url.ts
|
|
1959
2019
|
function extractRepoNameFromUrl(gitUrl) {
|
|
1960
2020
|
const url = gitUrl.trim();
|
|
@@ -2352,45 +2412,6 @@ var WorktreeMetadataService = class {
|
|
|
2352
2412
|
import * as fs3 from "fs/promises";
|
|
2353
2413
|
import * as path3 from "path";
|
|
2354
2414
|
import simpleGit2 from "simple-git";
|
|
2355
|
-
|
|
2356
|
-
// src/errors/index.ts
|
|
2357
|
-
var SyncWorktreesError = class extends Error {
|
|
2358
|
-
constructor(message, code, cause) {
|
|
2359
|
-
super(message);
|
|
2360
|
-
this.code = code;
|
|
2361
|
-
this.cause = cause;
|
|
2362
|
-
this.name = this.constructor.name;
|
|
2363
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
2364
|
-
if (cause && cause.stack) {
|
|
2365
|
-
this.stack = `${this.stack}
|
|
2366
|
-
Caused by: ${cause.stack}`;
|
|
2367
|
-
}
|
|
2368
|
-
}
|
|
2369
|
-
};
|
|
2370
|
-
var GitError = class extends SyncWorktreesError {
|
|
2371
|
-
constructor(message, code, cause) {
|
|
2372
|
-
super(message, `GIT_${code}`, cause);
|
|
2373
|
-
}
|
|
2374
|
-
};
|
|
2375
|
-
var GitOperationError = class extends GitError {
|
|
2376
|
-
constructor(operation, details, cause) {
|
|
2377
|
-
super(`Git operation '${operation}' failed: ${details}`, "OPERATION_FAILED", cause);
|
|
2378
|
-
}
|
|
2379
|
-
};
|
|
2380
|
-
var WorktreeError = class extends SyncWorktreesError {
|
|
2381
|
-
constructor(message, code, cause) {
|
|
2382
|
-
super(message, `WORKTREE_${code}`, cause);
|
|
2383
|
-
}
|
|
2384
|
-
};
|
|
2385
|
-
var WorktreeNotCleanError = class extends WorktreeError {
|
|
2386
|
-
constructor(path12, reasons) {
|
|
2387
|
-
super(`Worktree at '${path12}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
2388
|
-
this.path = path12;
|
|
2389
|
-
this.reasons = reasons;
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
|
-
|
|
2393
|
-
// src/services/worktree-status.service.ts
|
|
2394
2415
|
var OPERATION_FILES = [
|
|
2395
2416
|
{ file: GIT_OPERATIONS.MERGE_HEAD, type: "merge" },
|
|
2396
2417
|
{ file: GIT_OPERATIONS.CHERRY_PICK_HEAD, type: "cherry-pick" },
|
|
@@ -2447,6 +2468,7 @@ var WorktreeStatusService = class {
|
|
|
2447
2468
|
if (hasUnpushedCommits) reasons.push("unpushed commits");
|
|
2448
2469
|
if (hasOperationInProgress) reasons.push("operation in progress");
|
|
2449
2470
|
if (hasModifiedSubmodules) reasons.push("modified submodules");
|
|
2471
|
+
if (upstreamGone) reasons.push("upstream gone");
|
|
2450
2472
|
const canRemove = isClean && !hasUnpushedCommits && !hasOperationInProgress && !hasModifiedSubmodules;
|
|
2451
2473
|
const details = includeDetails ? this.buildStatusDetails(snap) : void 0;
|
|
2452
2474
|
return {
|
|
@@ -3005,24 +3027,19 @@ var GitService = class {
|
|
|
3005
3027
|
} catch {
|
|
3006
3028
|
}
|
|
3007
3029
|
try {
|
|
3008
|
-
const
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3030
|
+
const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
|
|
3031
|
+
await this.runWorktreeAddByMatrix(
|
|
3032
|
+
bareGit,
|
|
3033
|
+
branchName,
|
|
3034
|
+
absoluteWorktreePath,
|
|
3035
|
+
localBranchExists,
|
|
3036
|
+
remoteBranchExists
|
|
3037
|
+
);
|
|
3038
|
+
if (localBranchExists && !remoteBranchExists) {
|
|
3039
|
+
this.logger.info(` - Created worktree for '${branchName}' (no remote yet \u2014 push to set upstream)`);
|
|
3014
3040
|
} else {
|
|
3015
|
-
|
|
3016
|
-
"worktree",
|
|
3017
|
-
"add",
|
|
3018
|
-
"--track",
|
|
3019
|
-
"-b",
|
|
3020
|
-
branchName,
|
|
3021
|
-
absoluteWorktreePath,
|
|
3022
|
-
`origin/${branchName}`
|
|
3023
|
-
]);
|
|
3041
|
+
this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
|
|
3024
3042
|
}
|
|
3025
|
-
this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
|
|
3026
3043
|
if (!this.isLfsSkipEnabled()) {
|
|
3027
3044
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
3028
3045
|
}
|
|
@@ -3038,6 +3055,9 @@ var GitService = class {
|
|
|
3038
3055
|
}
|
|
3039
3056
|
} catch (error) {
|
|
3040
3057
|
const errorMessage = getErrorMessage(error);
|
|
3058
|
+
if (error?.isUpstreamSetupFailure) {
|
|
3059
|
+
throw error;
|
|
3060
|
+
}
|
|
3041
3061
|
if (errorMessage.includes("Metadata creation failed")) {
|
|
3042
3062
|
throw error;
|
|
3043
3063
|
}
|
|
@@ -3055,15 +3075,14 @@ var GitService = class {
|
|
|
3055
3075
|
} catch {
|
|
3056
3076
|
}
|
|
3057
3077
|
try {
|
|
3058
|
-
await
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
"--track",
|
|
3062
|
-
"-b",
|
|
3078
|
+
const { local: localBranchExists, remote: remoteBranchExists } = await this.branchExists(branchName);
|
|
3079
|
+
await this.runWorktreeAddByMatrix(
|
|
3080
|
+
bareGit,
|
|
3063
3081
|
branchName,
|
|
3064
3082
|
absoluteWorktreePath,
|
|
3065
|
-
|
|
3066
|
-
|
|
3083
|
+
localBranchExists,
|
|
3084
|
+
remoteBranchExists
|
|
3085
|
+
);
|
|
3067
3086
|
this.logger.info(` - Created worktree for '${branchName}' after pruning`);
|
|
3068
3087
|
if (!this.isLfsSkipEnabled()) {
|
|
3069
3088
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
@@ -3132,6 +3151,43 @@ var GitService = class {
|
|
|
3132
3151
|
}
|
|
3133
3152
|
}
|
|
3134
3153
|
}
|
|
3154
|
+
async runWorktreeAddByMatrix(bareGit, branchName, absoluteWorktreePath, localExists, remoteExists) {
|
|
3155
|
+
if (localExists && remoteExists) {
|
|
3156
|
+
await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
|
|
3157
|
+
try {
|
|
3158
|
+
const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
|
|
3159
|
+
await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
|
|
3160
|
+
} catch (error) {
|
|
3161
|
+
let rollbackFailed = false;
|
|
3162
|
+
try {
|
|
3163
|
+
await bareGit.raw(["worktree", "remove", "--force", absoluteWorktreePath]);
|
|
3164
|
+
} catch (rollbackError) {
|
|
3165
|
+
rollbackFailed = true;
|
|
3166
|
+
this.logger.warn(
|
|
3167
|
+
` - Rollback failed for '${branchName}' at '${absoluteWorktreePath}' after upstream setup error: ${getErrorMessage(rollbackError)}`
|
|
3168
|
+
);
|
|
3169
|
+
}
|
|
3170
|
+
const detail = getErrorMessage(error);
|
|
3171
|
+
const suffix = rollbackFailed ? " (rollback failed; partial worktree may remain)" : "";
|
|
3172
|
+
const wrapped = new Error(`Failed to set upstream for '${branchName}': ${detail}${suffix}`);
|
|
3173
|
+
wrapped.isUpstreamSetupFailure = true;
|
|
3174
|
+
throw wrapped;
|
|
3175
|
+
}
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
if (localExists) {
|
|
3179
|
+
await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
|
|
3180
|
+
return;
|
|
3181
|
+
}
|
|
3182
|
+
if (remoteExists) {
|
|
3183
|
+
await bareGit.raw(["worktree", "add", "--track", "-b", branchName, absoluteWorktreePath, `origin/${branchName}`]);
|
|
3184
|
+
return;
|
|
3185
|
+
}
|
|
3186
|
+
throw new WorktreeError(
|
|
3187
|
+
`Branch '${branchName}' does not exist locally or on origin; create it first`,
|
|
3188
|
+
"BRANCH_NOT_FOUND"
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3135
3191
|
async removeWorktree(worktreePath) {
|
|
3136
3192
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
3137
3193
|
await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
|
|
@@ -3325,10 +3381,18 @@ var GitService = class {
|
|
|
3325
3381
|
}
|
|
3326
3382
|
async branchExists(branchName) {
|
|
3327
3383
|
const bareGit = this.getCachedGit(this.bareRepoPath);
|
|
3328
|
-
const
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3384
|
+
const checkRef = async (ref) => {
|
|
3385
|
+
try {
|
|
3386
|
+
await bareGit.raw(["show-ref", "--verify", "--quiet", ref]);
|
|
3387
|
+
return true;
|
|
3388
|
+
} catch {
|
|
3389
|
+
return false;
|
|
3390
|
+
}
|
|
3391
|
+
};
|
|
3392
|
+
const [local, remote] = await Promise.all([
|
|
3393
|
+
checkRef(`${GIT_CONSTANTS.REFS.HEADS}${branchName}`),
|
|
3394
|
+
checkRef(`${GIT_CONSTANTS.REFS.REMOTES}/${branchName}`)
|
|
3395
|
+
]);
|
|
3332
3396
|
return { local, remote };
|
|
3333
3397
|
}
|
|
3334
3398
|
async getLocalBranches() {
|
|
@@ -4274,13 +4338,17 @@ var HookExecutionService = class {
|
|
|
4274
4338
|
// src/utils/disk-space.ts
|
|
4275
4339
|
import fastFolderSize from "fast-folder-size";
|
|
4276
4340
|
async function calculateDirectorySize(dirPath) {
|
|
4277
|
-
return new Promise((resolve8) => {
|
|
4341
|
+
return new Promise((resolve8, reject) => {
|
|
4278
4342
|
fastFolderSize(dirPath, (err, bytes) => {
|
|
4279
|
-
if (err
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
resolve8(bytes);
|
|
4343
|
+
if (err) {
|
|
4344
|
+
reject(err);
|
|
4345
|
+
return;
|
|
4283
4346
|
}
|
|
4347
|
+
if (bytes === void 0) {
|
|
4348
|
+
reject(new Error(`fast-folder-size returned no bytes for ${dirPath}`));
|
|
4349
|
+
return;
|
|
4350
|
+
}
|
|
4351
|
+
resolve8(bytes);
|
|
4284
4352
|
});
|
|
4285
4353
|
});
|
|
4286
4354
|
}
|
|
@@ -4297,12 +4365,16 @@ async function calculateSyncDiskSpace(repoPaths, worktreeDirs) {
|
|
|
4297
4365
|
try {
|
|
4298
4366
|
let totalBytes = 0;
|
|
4299
4367
|
for (const repoPath of repoPaths) {
|
|
4300
|
-
|
|
4301
|
-
|
|
4368
|
+
try {
|
|
4369
|
+
totalBytes += await calculateDirectorySize(repoPath);
|
|
4370
|
+
} catch {
|
|
4371
|
+
}
|
|
4302
4372
|
}
|
|
4303
4373
|
for (const worktreeDir of worktreeDirs) {
|
|
4304
|
-
|
|
4305
|
-
|
|
4374
|
+
try {
|
|
4375
|
+
totalBytes += await calculateDirectorySize(worktreeDir);
|
|
4376
|
+
} catch {
|
|
4377
|
+
}
|
|
4306
4378
|
}
|
|
4307
4379
|
return formatBytes(totalBytes);
|
|
4308
4380
|
} catch (error) {
|
|
@@ -4742,7 +4814,7 @@ var InteractiveUIService = class {
|
|
|
4742
4814
|
originalBranch = match[2];
|
|
4743
4815
|
}
|
|
4744
4816
|
}
|
|
4745
|
-
const sizeBytes = await calculateDirectorySize(fullPath);
|
|
4817
|
+
const sizeBytes = await calculateDirectorySize(fullPath).catch(() => 0);
|
|
4746
4818
|
const sizeFormatted = formatBytes(sizeBytes);
|
|
4747
4819
|
return {
|
|
4748
4820
|
name: entry.name,
|
|
@@ -5219,9 +5291,8 @@ export default ${serializeToESM(configObject)};
|
|
|
5219
5291
|
function getDefaultConfigPath() {
|
|
5220
5292
|
return path9.join(process.cwd(), "sync-worktrees.config.js");
|
|
5221
5293
|
}
|
|
5222
|
-
var CONFIG_CANDIDATES = ["sync-worktrees.config.js", "sync-worktrees.config.mjs", "sync-worktrees.config.cjs"];
|
|
5223
5294
|
async function findConfigInCwd(cwd = process.cwd()) {
|
|
5224
|
-
for (const name of
|
|
5295
|
+
for (const name of CONFIG_FILE_NAMES) {
|
|
5225
5296
|
const full = path9.join(cwd, name);
|
|
5226
5297
|
try {
|
|
5227
5298
|
await fs9.access(full);
|