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 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 branches = await bareGit.branch();
3009
- const localBranchExists = branches.all.includes(branchName);
3010
- if (localBranchExists || branchName.includes("/")) {
3011
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
3012
- const worktreeGit = this.getCachedGit(absoluteWorktreePath, this.isLfsSkipEnabled());
3013
- await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
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
- await bareGit.raw([
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 bareGit.raw([
3059
- "worktree",
3060
- "add",
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
- `origin/${branchName}`
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 localBranches = await bareGit.branch();
3329
- const local = localBranches.all.includes(branchName);
3330
- const remoteBranches = await bareGit.branch(["-r"]);
3331
- const remote = remoteBranches.all.includes(`origin/${branchName}`);
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 || bytes === void 0) {
4280
- resolve8(0);
4281
- } else {
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
- const bareSize = await calculateDirectorySize(repoPath);
4301
- totalBytes += bareSize;
4368
+ try {
4369
+ totalBytes += await calculateDirectorySize(repoPath);
4370
+ } catch {
4371
+ }
4302
4372
  }
4303
4373
  for (const worktreeDir of worktreeDirs) {
4304
- const worktreeSize = await calculateDirectorySize(worktreeDir);
4305
- totalBytes += worktreeSize;
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 CONFIG_CANDIDATES) {
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);