@ogpoyraz/wtx 0.9.2 → 0.9.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.
Files changed (2) hide show
  1. package/dist/cli.mjs +177 -150
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -25068,7 +25068,7 @@ var init_owner = __esm(() => {
25068
25068
  });
25069
25069
 
25070
25070
  // src/lib/workspace.ts
25071
- import fs23 from "fs";
25071
+ import fs24 from "fs";
25072
25072
  import path29 from "path";
25073
25073
  function getWorkspaceRoot(config2) {
25074
25074
  return path29.resolve(expandTilde(config2.workspace_root ?? path29.join(config2.root, "wtx-workspaces")));
@@ -25119,12 +25119,12 @@ function assertNoPathNesting(workspacePath, members) {
25119
25119
  async function writeManifest(workspacePath, manifest) {
25120
25120
  const manifestPath = path29.join(workspacePath, MANIFEST_FILE);
25121
25121
  const tmpPath = path29.join(workspacePath, `${MANIFEST_FILE}.${process.pid}.${Date.now()}.tmp`);
25122
- await fs23.promises.writeFile(tmpPath, `${JSON.stringify(manifest, null, 2)}
25122
+ await fs24.promises.writeFile(tmpPath, `${JSON.stringify(manifest, null, 2)}
25123
25123
  `, "utf8");
25124
- await fs23.promises.rename(tmpPath, manifestPath);
25124
+ await fs24.promises.rename(tmpPath, manifestPath);
25125
25125
  }
25126
25126
  async function readManifest(workspacePath) {
25127
- const raw = await fs23.promises.readFile(path29.join(workspacePath, MANIFEST_FILE), "utf8");
25127
+ const raw = await fs24.promises.readFile(path29.join(workspacePath, MANIFEST_FILE), "utf8");
25128
25128
  const parsed = JSON.parse(raw);
25129
25129
  if (parsed.version !== 1) {
25130
25130
  throw new Error(`Unsupported workspace manifest version: ${parsed.version}`);
@@ -25139,7 +25139,7 @@ async function writeAgentsFile(workspacePath, linkNames) {
25139
25139
  ...linkNames.map((name) => `- ${name}`),
25140
25140
  ""
25141
25141
  ];
25142
- await fs23.promises.writeFile(path29.join(workspacePath, AGENTS_FILE), lines.join(`
25142
+ await fs24.promises.writeFile(path29.join(workspacePath, AGENTS_FILE), lines.join(`
25143
25143
  `), "utf8");
25144
25144
  }
25145
25145
  async function createMemberSymlinks(workspacePath, members) {
@@ -25149,13 +25149,13 @@ async function createMemberSymlinks(workspacePath, members) {
25149
25149
  const linkName = memberLinkName(member, taken);
25150
25150
  const linkPath = path29.join(workspacePath, linkName);
25151
25151
  const target = path29.resolve(member.path);
25152
- await fs23.promises.symlink(target, linkPath, process.platform === "win32" ? "junction" : "dir");
25152
+ await fs24.promises.symlink(target, linkPath, process.platform === "win32" ? "junction" : "dir");
25153
25153
  linkNames.push(linkName);
25154
25154
  }
25155
25155
  return linkNames;
25156
25156
  }
25157
25157
  async function workspaceLinkNames(workspacePath) {
25158
- const entries = await fs23.promises.readdir(workspacePath, { withFileTypes: true });
25158
+ const entries = await fs24.promises.readdir(workspacePath, { withFileTypes: true });
25159
25159
  return entries.filter((entry) => entry.name !== MANIFEST_FILE && entry.name !== AGENTS_FILE && entry.isSymbolicLink()).map((entry) => entry.name).sort();
25160
25160
  }
25161
25161
  async function rebuildAgentsFile(workspacePath) {
@@ -25165,7 +25165,7 @@ async function createWorkspace(opts) {
25165
25165
  validateWorkspaceName(opts.name);
25166
25166
  const workspacePath = workspacePathFor(opts.name, opts.config);
25167
25167
  assertNoPathNesting(workspacePath, opts.members);
25168
- await fs23.promises.mkdir(workspacePath, { recursive: true });
25168
+ await fs24.promises.mkdir(workspacePath, { recursive: true });
25169
25169
  const linkNames = await createMemberSymlinks(workspacePath, opts.members);
25170
25170
  await writeManifest(workspacePath, {
25171
25171
  version: 1,
@@ -25183,7 +25183,7 @@ async function addMember(opts) {
25183
25183
  const taken = new Set(await workspaceLinkNames(opts.workspacePath));
25184
25184
  const linkName = memberLinkName(opts.member, taken);
25185
25185
  const target = path29.resolve(opts.member.path);
25186
- await fs23.promises.symlink(target, path29.join(opts.workspacePath, linkName), process.platform === "win32" ? "junction" : "dir");
25186
+ await fs24.promises.symlink(target, path29.join(opts.workspacePath, linkName), process.platform === "win32" ? "junction" : "dir");
25187
25187
  manifest.members.push({ repo: opts.member.repo, branch: opts.member.branch });
25188
25188
  await writeManifest(opts.workspacePath, manifest);
25189
25189
  await rebuildAgentsFile(opts.workspacePath);
@@ -25197,7 +25197,7 @@ async function removeMember(opts) {
25197
25197
  const linkNames = await workspaceLinkNames(opts.workspacePath);
25198
25198
  for (const linkName of linkNames) {
25199
25199
  if (linkName === opts.repo || linkName === `${opts.repo}-${opts.branch.replace(/[\\/]+/g, "-")}`) {
25200
- await fs23.promises.unlink(path29.join(opts.workspacePath, linkName));
25200
+ await fs24.promises.unlink(path29.join(opts.workspacePath, linkName));
25201
25201
  break;
25202
25202
  }
25203
25203
  }
@@ -25206,7 +25206,7 @@ async function removeMember(opts) {
25206
25206
  }
25207
25207
  async function listWorkspaces(workspaceRoot) {
25208
25208
  try {
25209
- const entries = await fs23.promises.readdir(path29.resolve(expandTilde(workspaceRoot)), { withFileTypes: true });
25209
+ const entries = await fs24.promises.readdir(path29.resolve(expandTilde(workspaceRoot)), { withFileTypes: true });
25210
25210
  const workspaces = [];
25211
25211
  for (const entry of entries) {
25212
25212
  if (!entry.isDirectory())
@@ -25235,7 +25235,7 @@ async function verifyLink(linkPath, visited) {
25235
25235
  visited.add(resolvedLink);
25236
25236
  let stats;
25237
25237
  try {
25238
- stats = await fs23.promises.lstat(linkPath);
25238
+ stats = await fs24.promises.lstat(linkPath);
25239
25239
  } catch {
25240
25240
  return "broken";
25241
25241
  }
@@ -25243,13 +25243,13 @@ async function verifyLink(linkPath, visited) {
25243
25243
  return "ok";
25244
25244
  let rawTarget;
25245
25245
  try {
25246
- rawTarget = await fs23.promises.readlink(linkPath);
25246
+ rawTarget = await fs24.promises.readlink(linkPath);
25247
25247
  } catch {
25248
25248
  return "broken";
25249
25249
  }
25250
25250
  const target = path29.isAbsolute(rawTarget) ? rawTarget : path29.resolve(path29.dirname(linkPath), rawTarget);
25251
25251
  try {
25252
- const targetStats = await fs23.promises.lstat(target);
25252
+ const targetStats = await fs24.promises.lstat(target);
25253
25253
  if (!targetStats.isSymbolicLink())
25254
25254
  return "ok";
25255
25255
  } catch {
@@ -25260,7 +25260,7 @@ async function verifyLink(linkPath, visited) {
25260
25260
  async function verify(workspacePath) {
25261
25261
  const result = { ok: true, broken: [], cycles: [] };
25262
25262
  try {
25263
- const entries = await fs23.promises.readdir(workspacePath, { withFileTypes: true });
25263
+ const entries = await fs24.promises.readdir(workspacePath, { withFileTypes: true });
25264
25264
  for (const entry of entries) {
25265
25265
  if (entry.name === MANIFEST_FILE || entry.name === AGENTS_FILE || !entry.isSymbolicLink())
25266
25266
  continue;
@@ -27195,7 +27195,7 @@ var init_DetailsTab = __esm(() => {
27195
27195
  });
27196
27196
 
27197
27197
  // src/lib/changes.ts
27198
- import fs33 from "node:fs";
27198
+ import fs34 from "node:fs";
27199
27199
  import path37 from "node:path";
27200
27200
  function cacheKey(opts, filePath) {
27201
27201
  return [opts.repoPath, opts.branch, opts.scope, filePath ?? ""].join("\x00");
@@ -27328,7 +27328,7 @@ async function getChangedFiles(opts) {
27328
27328
  continue;
27329
27329
  try {
27330
27330
  const full = path37.join(opts.repoPath, p);
27331
- const buf = fs33.readFileSync(full);
27331
+ const buf = fs34.readFileSync(full);
27332
27332
  const binary = buf.includes(0);
27333
27333
  const str = binary ? "" : buf.toString("utf8");
27334
27334
  const added = binary ? 0 : str.split(`
@@ -27379,7 +27379,7 @@ async function getFileDiff(opts) {
27379
27379
  if (!rawDiff && opts.scope === "worktree" && changedFile.status === "A") {
27380
27380
  try {
27381
27381
  const full = path37.join(opts.repoPath, opts.filePath);
27382
- const buf = fs33.readFileSync(full);
27382
+ const buf = fs34.readFileSync(full);
27383
27383
  if (!buf.includes(0)) {
27384
27384
  const content = buf.toString("utf8");
27385
27385
  const lines = content.split(`
@@ -28408,7 +28408,7 @@ var init_ActionLogModal = __esm(() => {
28408
28408
  });
28409
28409
 
28410
28410
  // src/lib/history.ts
28411
- import fs34 from "fs";
28411
+ import fs35 from "fs";
28412
28412
  import path38 from "path";
28413
28413
  import os4 from "os";
28414
28414
  function getHistoryDir() {
@@ -28422,24 +28422,24 @@ function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_
28422
28422
  const historyPath = getHistoryPath();
28423
28423
  let size = 0;
28424
28424
  try {
28425
- size = fs34.statSync(historyPath).size;
28425
+ size = fs35.statSync(historyPath).size;
28426
28426
  } catch {
28427
28427
  return;
28428
28428
  }
28429
28429
  if (size <= maxBytes)
28430
28430
  return;
28431
- const lines = fs34.readFileSync(historyPath, "utf-8").split(`
28431
+ const lines = fs35.readFileSync(historyPath, "utf-8").split(`
28432
28432
  `).filter(Boolean);
28433
28433
  const kept = lines.slice(-keepLines);
28434
28434
  const tmpPath = `${historyPath}.tmp.${process.pid}`;
28435
28435
  try {
28436
- fs34.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
28436
+ fs35.writeFileSync(tmpPath, kept.length > 0 ? `${kept.join(`
28437
28437
  `)}
28438
28438
  ` : "", "utf-8");
28439
- fs34.renameSync(tmpPath, historyPath);
28439
+ fs35.renameSync(tmpPath, historyPath);
28440
28440
  } catch (err) {
28441
- if (fs34.existsSync(tmpPath)) {
28442
- fs34.unlinkSync(tmpPath);
28441
+ if (fs35.existsSync(tmpPath)) {
28442
+ fs35.unlinkSync(tmpPath);
28443
28443
  }
28444
28444
  throw err;
28445
28445
  }
@@ -28447,18 +28447,18 @@ function rotateHistory(maxBytes = HISTORY_MAX_BYTES, keepLines = HISTORY_ROTATE_
28447
28447
  function appendHistory(entry) {
28448
28448
  try {
28449
28449
  const dir = getHistoryDir();
28450
- if (!fs34.existsSync(dir)) {
28451
- fs34.mkdirSync(dir, { recursive: true });
28450
+ if (!fs35.existsSync(dir)) {
28451
+ fs35.mkdirSync(dir, { recursive: true });
28452
28452
  }
28453
28453
  rotateHistory();
28454
- fs34.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
28454
+ fs35.appendFileSync(getHistoryPath(), `${JSON.stringify(entry)}
28455
28455
  `, "utf-8");
28456
28456
  } catch {}
28457
28457
  }
28458
28458
  function readRecentHistory(limit = 50) {
28459
28459
  let content;
28460
28460
  try {
28461
- content = fs34.readFileSync(getHistoryPath(), "utf-8");
28461
+ content = fs35.readFileSync(getHistoryPath(), "utf-8");
28462
28462
  } catch {
28463
28463
  return [];
28464
28464
  }
@@ -34137,8 +34137,66 @@ init_config();
34137
34137
  init_log();
34138
34138
  init_git();
34139
34139
  init_resolver();
34140
- import fs22 from "fs";
34140
+ import fs23 from "fs";
34141
34141
  import path28 from "path";
34142
+
34143
+ // src/lib/worktree-remove.ts
34144
+ init_git();
34145
+ import fs22 from "fs";
34146
+ function isSubmoduleWorktreeError(message) {
34147
+ return message.includes("submodule") || message.includes("containing submodules");
34148
+ }
34149
+ function isMissingWorktreePathError(message) {
34150
+ return message.includes("ENOENT") || message.includes("No such file") || message.includes("does not exist") || message.includes("not a git repository") || message.includes("cannot change to");
34151
+ }
34152
+ function isRecoverableWorktreeRemoveError(message) {
34153
+ return message.includes("not a working tree") || message.includes("already removed") || isSubmoduleWorktreeError(message) || isMissingWorktreePathError(message);
34154
+ }
34155
+ function removePathIfExists(wtPath, opts) {
34156
+ if (!opts.dryRun && fs22.existsSync(wtPath)) {
34157
+ fs22.rmSync(wtPath, { recursive: true, force: true });
34158
+ }
34159
+ }
34160
+ async function pruneWorktreeAdmin(mainPath, opts) {
34161
+ try {
34162
+ await gitExec(["-C", mainPath, "worktree", "prune"], opts);
34163
+ } catch {}
34164
+ }
34165
+ async function removeWorktreeSafely(params) {
34166
+ const { mainPath, wtPath, force, opts } = params;
34167
+ const args = ["-C", mainPath, "worktree", "remove", wtPath];
34168
+ if (force) {
34169
+ args.push("--force");
34170
+ }
34171
+ try {
34172
+ await gitExec(args, opts);
34173
+ return { usedForce: force, manualCleanup: false };
34174
+ } catch (err) {
34175
+ const msg = err instanceof Error ? err.message : String(err);
34176
+ if (isSubmoduleWorktreeError(msg) && !force) {
34177
+ try {
34178
+ await gitExec(["-C", mainPath, "worktree", "remove", "--force", wtPath], opts);
34179
+ return { usedForce: true, manualCleanup: false };
34180
+ } catch (forceErr) {
34181
+ const forceMsg = forceErr instanceof Error ? forceErr.message : String(forceErr);
34182
+ if (isRecoverableWorktreeRemoveError(forceMsg)) {
34183
+ await pruneWorktreeAdmin(mainPath, opts);
34184
+ removePathIfExists(wtPath, opts);
34185
+ return { usedForce: true, manualCleanup: true };
34186
+ }
34187
+ throw forceErr;
34188
+ }
34189
+ }
34190
+ if (isRecoverableWorktreeRemoveError(msg)) {
34191
+ await pruneWorktreeAdmin(mainPath, opts);
34192
+ removePathIfExists(wtPath, opts);
34193
+ return { usedForce: force, manualCleanup: true };
34194
+ }
34195
+ throw err;
34196
+ }
34197
+ }
34198
+
34199
+ // src/commands/pull.ts
34142
34200
  init_forge();
34143
34201
  init_stack();
34144
34202
  function registerPullCommand(program2) {
@@ -34255,7 +34313,7 @@ function registerPullCommand(program2) {
34255
34313
  const worktrees = await getWorktreeList(target.mainPath);
34256
34314
  const existingWorktree = worktrees.find((w) => w.path === wtPath || w.branch === branch);
34257
34315
  const localBranchFound = await localBranchExists(target.mainPath, branch, globalOpts);
34258
- const dirExists = Boolean(existingWorktree) || fs22.existsSync(wtPath);
34316
+ const dirExists = Boolean(existingWorktree) || fs23.existsSync(wtPath);
34259
34317
  if (localBranchFound || dirExists) {
34260
34318
  if (options.force) {
34261
34319
  stepWarning(`Branch '${branch}' already exists — --force: will override`, dirExists ? wtPath : "local branch exists");
@@ -34268,17 +34326,22 @@ function registerPullCommand(program2) {
34268
34326
  if (options.force && (localBranchFound || dirExists)) {
34269
34327
  if (existingWorktree) {
34270
34328
  try {
34271
- await gitExec(["-C", target.mainPath, "worktree", "remove", "--force", existingWorktree.path], { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun });
34329
+ await removeWorktreeSafely({
34330
+ mainPath: target.mainPath,
34331
+ wtPath: existingWorktree.path,
34332
+ force: true,
34333
+ opts: { verbose: globalOpts.verbose, dryRun: globalOpts.dryRun }
34334
+ });
34272
34335
  } catch (err) {
34273
34336
  const message = err instanceof Error ? err.message : String(err);
34274
- if (!message.includes("not a working tree")) {
34337
+ if (!isRecoverableWorktreeRemoveError(message)) {
34275
34338
  stepWarning("Worktree removal failed", message.split(`
34276
34339
  `)[0] ?? message);
34277
34340
  }
34278
34341
  }
34279
34342
  }
34280
- if (!globalOpts.dryRun && fs22.existsSync(wtPath)) {
34281
- fs22.rmSync(wtPath, { recursive: true, force: true });
34343
+ if (!globalOpts.dryRun && fs23.existsSync(wtPath)) {
34344
+ fs23.rmSync(wtPath, { recursive: true, force: true });
34282
34345
  }
34283
34346
  if (localBranchFound) {
34284
34347
  try {
@@ -34322,7 +34385,7 @@ function registerPullCommand(program2) {
34322
34385
  }
34323
34386
  stepSuccess("Fetched");
34324
34387
  if (!globalOpts.dryRun) {
34325
- fs22.mkdirSync(path28.dirname(wtPath), { recursive: true });
34388
+ fs23.mkdirSync(path28.dirname(wtPath), { recursive: true });
34326
34389
  }
34327
34390
  try {
34328
34391
  const createBranchFlag = options.force ? "-B" : "-b";
@@ -34456,10 +34519,10 @@ function registerPullBranchCommand(program2) {
34456
34519
  init_config();
34457
34520
  init_log();
34458
34521
  init_git();
34522
+ import fs25 from "fs";
34523
+ import path30 from "path";
34459
34524
  init_resolver();
34460
34525
  init_path_safety();
34461
- import fs24 from "fs";
34462
- import path30 from "path";
34463
34526
 
34464
34527
  // src/lib/prompts.ts
34465
34528
  import * as readline2 from "readline";
@@ -34507,15 +34570,9 @@ init_workspace();
34507
34570
  function errorMessage2(err) {
34508
34571
  return err instanceof Error ? err.message : String(err);
34509
34572
  }
34510
- function isMissingWorktreePathError(message) {
34511
- return message.includes("ENOENT") || message.includes("No such file") || message.includes("does not exist") || message.includes("not a git repository") || message.includes("cannot change to");
34512
- }
34513
- function isRecoverableWorktreeRemoveError(message) {
34514
- return message.includes("not a working tree") || message.includes("already removed") || message.includes("submodule") || message.includes("containing submodules") || isMissingWorktreePathError(message);
34515
- }
34516
34573
  function removeExistingPath(wtPath, opts) {
34517
- if (!opts.dryRun && fs24.existsSync(wtPath)) {
34518
- fs24.rmSync(wtPath, { recursive: true, force: true });
34574
+ if (!opts.dryRun && fs25.existsSync(wtPath)) {
34575
+ fs25.rmSync(wtPath, { recursive: true, force: true });
34519
34576
  }
34520
34577
  }
34521
34578
  async function removeLocalBranchIfExists(repoPath, branch, opts) {
@@ -34604,7 +34661,7 @@ function registerRemoveCommand(program2) {
34604
34661
  const worktrees = await getWorktreeList(repo.mainPath);
34605
34662
  const target = findWorktreeForBranch(worktrees, branch, repo.mainPath, candidatePath);
34606
34663
  if (!target) {
34607
- if (options.force && (fs24.existsSync(candidatePath) || await localBranchExists(repo.mainPath, branch, globalOpts))) {
34664
+ if (options.force && (fs25.existsSync(candidatePath) || await localBranchExists(repo.mainPath, branch, globalOpts))) {
34608
34665
  if (!await confirmDeletionIfNeeded(repo, candidatePath, options, globalOpts)) {
34609
34666
  skipCount++;
34610
34667
  continue;
@@ -34646,7 +34703,7 @@ function registerRemoveCommand(program2) {
34646
34703
  }
34647
34704
  } catch (err) {
34648
34705
  const msg = errorMessage2(err);
34649
- if (isMissingWorktreePathError(msg) || !fs24.existsSync(wtPath)) {
34706
+ if (isMissingWorktreePathError(msg) || !fs25.existsSync(wtPath)) {
34650
34707
  stepWarning("Worktree path missing", "continuing with removal cleanup");
34651
34708
  } else {
34652
34709
  stepError("Failed to check for uncommitted changes", msg);
@@ -34655,7 +34712,7 @@ function registerRemoveCommand(program2) {
34655
34712
  }
34656
34713
  }
34657
34714
  }
34658
- if (options.force && !globalOpts.dryRun && !fs24.existsSync(wtPath)) {
34715
+ if (options.force && !globalOpts.dryRun && !fs25.existsSync(wtPath)) {
34659
34716
  if (!await confirmDeletionIfNeeded(repo, wtPath, options, globalOpts)) {
34660
34717
  skipCount++;
34661
34718
  continue;
@@ -34679,52 +34736,16 @@ function registerRemoveCommand(program2) {
34679
34736
  skipCount++;
34680
34737
  continue;
34681
34738
  }
34682
- const args = ["-C", repo.mainPath, "worktree", "remove", wtPath];
34683
- if (options.force) {
34684
- args.push("--force");
34685
- }
34686
- try {
34687
- await gitExec(args, globalOpts);
34688
- stepSuccess("Worktree removed", wtPath);
34689
- } catch (err) {
34690
- const msg = errorMessage2(err);
34691
- const isSubmodule = msg.includes("submodule") || msg.includes("containing submodules");
34692
- if (isSubmodule && !options.force && !args.includes("--force")) {
34693
- stepWarning("Worktree contains submodules — retrying with --force", msg.split(`
34694
- `)[0] ?? msg);
34695
- try {
34696
- await gitExec(["-C", repo.mainPath, "worktree", "remove", "--force", wtPath], globalOpts);
34697
- stepSuccess("Worktree removed", wtPath);
34698
- } catch (forceErr) {
34699
- const forceMsg = errorMessage2(forceErr);
34700
- if (isRecoverableWorktreeRemoveError(forceMsg)) {
34701
- stepWarning("Worktree already removed or invalid", forceMsg);
34702
- try {
34703
- await gitExec(["-C", repo.mainPath, "worktree", "prune"], globalOpts);
34704
- } catch (pruneErr) {
34705
- stepWarning("Worktree prune failed", errorMessage2(pruneErr));
34706
- }
34707
- removeExistingPath(wtPath, globalOpts);
34708
- await removeLocalBranchBestEffort(repo.mainPath, branch, globalOpts);
34709
- stepSuccess("Worktree removed", wtPath);
34710
- } else {
34711
- throw forceErr;
34712
- }
34713
- }
34714
- } else if (isRecoverableWorktreeRemoveError(msg)) {
34715
- stepWarning("Worktree already removed or invalid", msg);
34716
- try {
34717
- await gitExec(["-C", repo.mainPath, "worktree", "prune"], globalOpts);
34718
- } catch (pruneErr) {
34719
- stepWarning("Worktree prune failed", errorMessage2(pruneErr));
34720
- }
34721
- removeExistingPath(wtPath, globalOpts);
34722
- await removeLocalBranchBestEffort(repo.mainPath, branch, globalOpts);
34723
- stepSuccess("Worktree removed", wtPath);
34724
- } else {
34725
- throw err;
34726
- }
34739
+ const result = await removeWorktreeSafely({
34740
+ mainPath: repo.mainPath,
34741
+ wtPath,
34742
+ force: !!options.force,
34743
+ opts: globalOpts
34744
+ });
34745
+ if (result.manualCleanup) {
34746
+ await removeLocalBranchBestEffort(repo.mainPath, branch, globalOpts);
34727
34747
  }
34748
+ stepSuccess("Worktree removed", wtPath);
34728
34749
  await finishCleanup(repo, wtPath, branch, globalOpts);
34729
34750
  await unlinkRemovedWorktreeFromWorkspaces({ workspaceRoot, repo: repo.name, branch });
34730
34751
  if (children.length > 0) {
@@ -34751,9 +34772,9 @@ init_source();
34751
34772
  init_config();
34752
34773
  init_log();
34753
34774
  init_git();
34775
+ import path31 from "path";
34754
34776
  init_resolver();
34755
34777
  init_forge();
34756
- import path31 from "path";
34757
34778
 
34758
34779
  // src/lib/prune.ts
34759
34780
  function selectMergedCandidates(worktrees, mainPath, prMap) {
@@ -34890,16 +34911,17 @@ function registerPruneCommand(program2) {
34890
34911
  repoHeader(repo.name);
34891
34912
  currentRepo = repo.name;
34892
34913
  }
34893
- const args = ["-C", repo.mainPath, "worktree", "remove", candidate.path];
34894
- if (options.force) {
34895
- args.push("--force");
34896
- }
34897
34914
  try {
34898
- await gitExec(args, globalOpts);
34915
+ await removeWorktreeSafely({
34916
+ mainPath: repo.mainPath,
34917
+ wtPath: candidate.path,
34918
+ force: !!options.force,
34919
+ opts: globalOpts
34920
+ });
34899
34921
  stepSuccess("Worktree removed", label);
34900
34922
  } catch (err) {
34901
34923
  const msg = err instanceof Error ? err.message : String(err);
34902
- if (msg.includes("not a working tree") || msg.includes("already removed")) {
34924
+ if (isRecoverableWorktreeRemoveError(msg)) {
34903
34925
  stepWarning("Worktree already removed or invalid", msg);
34904
34926
  skippedCount++;
34905
34927
  continue;
@@ -35002,7 +35024,7 @@ init_source();
35002
35024
  init_stack();
35003
35025
  init_stack();
35004
35026
  import path32 from "path";
35005
- import fs25 from "fs";
35027
+ import fs26 from "fs";
35006
35028
  function buildLsJson(reposData) {
35007
35029
  const result = [];
35008
35030
  for (const repo of reposData) {
@@ -35112,7 +35134,7 @@ function registerLsCommand(program2) {
35112
35134
  statusStr = source_default.blue("[main checkout]");
35113
35135
  } else if (wt.isLocked) {
35114
35136
  statusStr = source_default.red("locked \uD83D\uDD12");
35115
- } else if (fs25.existsSync(wt.path)) {
35137
+ } else if (fs26.existsSync(wt.path)) {
35116
35138
  try {
35117
35139
  dirtyFiles = await getDirtyFiles(wt.path);
35118
35140
  if (dirtyFiles.length > 0) {
@@ -35238,7 +35260,7 @@ end
35238
35260
  init_config();
35239
35261
  init_log();
35240
35262
  init_git();
35241
- import fs26 from "fs";
35263
+ import fs27 from "fs";
35242
35264
  init_resolver();
35243
35265
  init_stack();
35244
35266
  function registerRebaseCommand(program2) {
@@ -35253,7 +35275,7 @@ function registerRebaseCommand(program2) {
35253
35275
  repoHeader(repo.name);
35254
35276
  const mainBranch = await resolveMainBranch(repo, config2);
35255
35277
  const wtPath = getWorktreePath(repo, branch);
35256
- if (!fs26.existsSync(wtPath)) {
35278
+ if (!fs27.existsSync(wtPath)) {
35257
35279
  stepError("No worktree found", `${wtPath} (skipped)`);
35258
35280
  failCount++;
35259
35281
  continue;
@@ -35379,7 +35401,7 @@ init_execa();
35379
35401
  init_config();
35380
35402
  init_log();
35381
35403
  init_resolver();
35382
- import fs27 from "fs";
35404
+ import fs28 from "fs";
35383
35405
  import path33 from "path";
35384
35406
  function registerSyncCommand(program2) {
35385
35407
  program2.command("sync <branch>").description("re-copy sync files + run post_sync").option("--repo <repos...>", "comma-separated list of repos to target").action(async (branch, _options, cmd) => {
@@ -35391,14 +35413,14 @@ function registerSyncCommand(program2) {
35391
35413
  for (const repo of repos) {
35392
35414
  repoHeader(repo.name);
35393
35415
  const wtPath = getWorktreePath(repo, branch);
35394
- if (!fs27.existsSync(wtPath)) {
35416
+ if (!fs28.existsSync(wtPath)) {
35395
35417
  stepWarning("No worktree found", `${wtPath} (skipped)`);
35396
35418
  continue;
35397
35419
  }
35398
35420
  try {
35399
35421
  if (repo.config.sync_files) {
35400
35422
  for (const file2 of repo.config.sync_files) {
35401
- if (fs27.existsSync(path33.join(repo.mainPath, file2))) {
35423
+ if (fs28.existsSync(path33.join(repo.mainPath, file2))) {
35402
35424
  if (!opts.dryRun) {
35403
35425
  syncEntry(repo.mainPath, wtPath, file2);
35404
35426
  }
@@ -35451,14 +35473,14 @@ function registerSyncCommand(program2) {
35451
35473
  }
35452
35474
  }
35453
35475
  const wtNodeModules = path33.join(wtPath, "node_modules");
35454
- if (fs27.existsSync(wtNodeModules) && fs27.lstatSync(wtNodeModules).isSymbolicLink()) {
35476
+ if (fs28.existsSync(wtNodeModules) && fs28.lstatSync(wtNodeModules).isSymbolicLink()) {
35455
35477
  const lockfiles = ["yarn.lock", "package-lock.json", "pnpm-lock.yaml", "bun.lockb", "bun.lock"];
35456
35478
  for (const lock of lockfiles) {
35457
35479
  const mainLock = path33.join(repo.mainPath, lock);
35458
35480
  const wtLock = path33.join(wtPath, lock);
35459
- if (fs27.existsSync(mainLock) && fs27.existsSync(wtLock)) {
35460
- const mainContent = fs27.readFileSync(mainLock);
35461
- const wtContent = fs27.readFileSync(wtLock);
35481
+ if (fs28.existsSync(mainLock) && fs28.existsSync(wtLock)) {
35482
+ const mainContent = fs28.readFileSync(mainLock);
35483
+ const wtContent = fs28.readFileSync(wtLock);
35462
35484
  if (!mainContent.equals(wtContent)) {
35463
35485
  stepWarning(`${lock} differs from main — node_modules is symlinked`);
35464
35486
  indented(`Run: wtx deps ${branch} --repo ${repo.name} --install`);
@@ -35487,7 +35509,7 @@ init_config();
35487
35509
  init_log();
35488
35510
  init_resolver();
35489
35511
  init_deps();
35490
- import fs28 from "fs";
35512
+ import fs29 from "fs";
35491
35513
  function registerDepsCommand(program2) {
35492
35514
  program2.command("deps [branch]").description("Manage node_modules strategy per worktree").option("-r, --repo <repos...>", "Target specific repo(s)").option("--install", "Switch to independent node_modules (run install)").option("--symlink", "Switch to legacy symlinked node_modules").option("--json", "Output machine-readable JSON state").action(async (branch, options) => {
35493
35515
  const globalOpts = program2.opts();
@@ -35524,7 +35546,7 @@ function registerDepsCommand(program2) {
35524
35546
  continue;
35525
35547
  }
35526
35548
  const wtPath = getWorktreePath(repo, branch);
35527
- if (!fs28.existsSync(wtPath)) {
35549
+ if (!fs29.existsSync(wtPath)) {
35528
35550
  if (options.json) {
35529
35551
  jsonResults[repo.name] = { error: "No worktree found", branch };
35530
35552
  } else {
@@ -35582,7 +35604,7 @@ function registerDepsCommand(program2) {
35582
35604
  init_config();
35583
35605
  init_log();
35584
35606
  init_resolver();
35585
- import fs29 from "fs";
35607
+ import fs30 from "fs";
35586
35608
  init_git();
35587
35609
  async function resolveMainCheckoutPath(repoCtx, branch) {
35588
35610
  try {
@@ -35607,7 +35629,7 @@ function registerOpenCommand(program2) {
35607
35629
  for (const repo of repos) {
35608
35630
  const wtPath = getWorktreePath(repo, branch);
35609
35631
  let targetPath = wtPath;
35610
- if (!fs29.existsSync(wtPath)) {
35632
+ if (!fs30.existsSync(wtPath)) {
35611
35633
  const mainCheckoutPath = await resolveMainCheckoutPath(repo, branch);
35612
35634
  if (!mainCheckoutPath) {
35613
35635
  continue;
@@ -35643,7 +35665,7 @@ import path35 from "path";
35643
35665
  init_git();
35644
35666
  init_resolver();
35645
35667
  init_path_safety();
35646
- import fs30 from "fs";
35668
+ import fs31 from "fs";
35647
35669
  import path34 from "path";
35648
35670
  init_workspace();
35649
35671
  async function getUpstream(wtPath) {
@@ -35686,7 +35708,7 @@ async function planRename(repo, oldBranch, newBranch, opts) {
35686
35708
  throw new Error(`Worktree '${oldBranch}' is locked — unlock it before renaming`);
35687
35709
  }
35688
35710
  const newPath = `${repo.wtRoot}/${newBranch}`;
35689
- if (fs30.existsSync(newPath)) {
35711
+ if (fs31.existsSync(newPath)) {
35690
35712
  throw new Error(`Target path already exists: ${newPath}`);
35691
35713
  }
35692
35714
  if (!opts.dryRun && await localBranchExists(repo.mainPath, newBranch, opts)) {
@@ -35719,7 +35741,7 @@ async function renameWorktree(params) {
35719
35741
  }
35720
35742
  await gitExec(["-C", planned.worktreePath, "branch", "-m", oldBranch, newBranch], opts);
35721
35743
  try {
35722
- fs30.mkdirSync(path34.dirname(planned.newPath), { recursive: true });
35744
+ fs31.mkdirSync(path34.dirname(planned.newPath), { recursive: true });
35723
35745
  await gitExec(["-C", repo.mainPath, "worktree", "move", planned.worktreePath, planned.newPath], opts);
35724
35746
  } catch (err) {
35725
35747
  const message = err instanceof Error ? err.message : String(err);
@@ -35738,10 +35760,10 @@ async function renameWorktree(params) {
35738
35760
  const resyncedFiles = [];
35739
35761
  const keptLocalSyncFiles = [];
35740
35762
  for (const entry of repo.config.sync_files ?? []) {
35741
- if (!fs30.existsSync(path34.join(repo.mainPath, entry)))
35763
+ if (!fs31.existsSync(path34.join(repo.mainPath, entry)))
35742
35764
  continue;
35743
35765
  const destPath = path34.join(planned.newPath, entry);
35744
- if (!fs30.existsSync(destPath)) {
35766
+ if (!fs31.existsSync(destPath)) {
35745
35767
  if (syncEntry(repo.mainPath, planned.newPath, entry)) {
35746
35768
  resyncedFiles.push(entry);
35747
35769
  }
@@ -35880,7 +35902,7 @@ init_resolver();
35880
35902
  init_deps();
35881
35903
  init_forge();
35882
35904
  init_types2();
35883
- import fs31 from "fs";
35905
+ import fs32 from "fs";
35884
35906
  init_owner();
35885
35907
  init_stack();
35886
35908
  init_source();
@@ -35930,7 +35952,7 @@ function registerStatusCommand(program2) {
35930
35952
  const jsonOutputs = [];
35931
35953
  for (const repo of repos) {
35932
35954
  const wtPath = getWorktreePath(repo, branch);
35933
- if (!fs31.existsSync(wtPath)) {
35955
+ if (!fs32.existsSync(wtPath)) {
35934
35956
  continue;
35935
35957
  }
35936
35958
  found++;
@@ -36230,7 +36252,7 @@ function registerPrsCommand(program2) {
36230
36252
 
36231
36253
  // src/commands/skill.ts
36232
36254
  init_log();
36233
- import fs32 from "fs";
36255
+ import fs33 from "fs";
36234
36256
  import path36 from "path";
36235
36257
  import { URL as URL2 } from "url";
36236
36258
  var __dirname2 = new URL2(".", import.meta.url).pathname;
@@ -36387,7 +36409,7 @@ function registerSkillCommand(program2) {
36387
36409
  }
36388
36410
  let projectRoot = path36.resolve(__dirname2, "..", "..");
36389
36411
  let skillPath = path36.join(projectRoot, "skills", `${platform2}.md`);
36390
- if (fs32.existsSync(skillPath)) {
36412
+ if (fs33.existsSync(skillPath)) {
36391
36413
  process.stdout.write(skillPath + `
36392
36414
  `);
36393
36415
  } else {
@@ -36400,7 +36422,7 @@ function registerSkillCommand(program2) {
36400
36422
 
36401
36423
  // src/commands/terminal.ts
36402
36424
  import { spawnSync as spawnSync3 } from "node:child_process";
36403
- import fs35 from "node:fs";
36425
+ import fs36 from "node:fs";
36404
36426
  import path39 from "node:path";
36405
36427
  import { fileURLToPath as fileURLToPath4 } from "node:url";
36406
36428
  function findEntry() {
@@ -36416,20 +36438,20 @@ function findEntry() {
36416
36438
  for (const p of candidates) {
36417
36439
  try {
36418
36440
  if (p.endsWith(".mjs") || p.endsWith(".js")) {
36419
- if (fs35.existsSync(p) && fs35.statSync(p).isFile())
36441
+ if (fs36.existsSync(p) && fs36.statSync(p).isFile())
36420
36442
  return p;
36421
- } else if (fs35.existsSync(p)) {
36422
- const stat = fs35.statSync(p);
36443
+ } else if (fs36.existsSync(p)) {
36444
+ const stat = fs36.statSync(p);
36423
36445
  if (stat.isFile()) {
36424
- const head = fs35.readFileSync(p, "utf8").slice(0, 1024);
36446
+ const head = fs36.readFileSync(p, "utf8").slice(0, 1024);
36425
36447
  if (head.includes("cli.mjs") || head.includes("#!/usr/bin/env node")) {
36426
36448
  if (p.endsWith("wtx") || p.endsWith("cli.mjs")) {
36427
- const dir = path39.dirname(fs35.realpathSync(p));
36449
+ const dir = path39.dirname(fs36.realpathSync(p));
36428
36450
  const cli = path39.join(dir, "cli.mjs");
36429
- if (fs35.existsSync(cli))
36451
+ if (fs36.existsSync(cli))
36430
36452
  return cli;
36431
36453
  const cli2 = path39.resolve(dir, "../dist/cli.mjs");
36432
- if (fs35.existsSync(cli2))
36454
+ if (fs36.existsSync(cli2))
36433
36455
  return cli2;
36434
36456
  if (head.includes("import") || head.includes("commander"))
36435
36457
  return p;
@@ -36443,7 +36465,7 @@ function findEntry() {
36443
36465
  const thisPath = fileURLToPath4(import.meta.url);
36444
36466
  const pkgRoot = path39.resolve(path39.dirname(thisPath), "../..");
36445
36467
  const cli = path39.join(pkgRoot, "dist", "cli.mjs");
36446
- if (fs35.existsSync(cli))
36468
+ if (fs36.existsSync(cli))
36447
36469
  return cli;
36448
36470
  } catch {}
36449
36471
  return null;
@@ -36547,7 +36569,7 @@ init_deps();
36547
36569
  import readline3 from "readline";
36548
36570
  init_path_safety();
36549
36571
  init_stack();
36550
- import fs36 from "fs";
36572
+ import fs37 from "fs";
36551
36573
  async function runMcpServer(opts = {}) {
36552
36574
  const input = opts.input ?? process.stdin;
36553
36575
  const output = opts.output ?? process.stdout;
@@ -36730,7 +36752,7 @@ async function handleToolCall(name, args, config2, _opts) {
36730
36752
  for (const wt of wts) {
36731
36753
  if (!wt.path.startsWith(repo.wtRoot))
36732
36754
  continue;
36733
- const dirtyCount = fs36.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
36755
+ const dirtyCount = fs37.existsSync(wt.path) ? (await getDirtyFiles(wt.path)).length : 0;
36734
36756
  items.push({
36735
36757
  repo: repo.name,
36736
36758
  branch: wt.branch || null,
@@ -36758,7 +36780,7 @@ async function handleToolCall(name, args, config2, _opts) {
36758
36780
  }
36759
36781
  const repo = resolveRepos(config2, [args.repo])[0];
36760
36782
  const wtPath = getWorktreePath(repo, args.branch);
36761
- if (!fs36.existsSync(wtPath)) {
36783
+ if (!fs37.existsSync(wtPath)) {
36762
36784
  throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
36763
36785
  }
36764
36786
  const mainBranch = await resolveMainBranch(repo, config2);
@@ -36877,13 +36899,18 @@ async function handleToolCall(name, args, config2, _opts) {
36877
36899
  if (children.length > 0 && args.force !== true) {
36878
36900
  throw { isToolError: true, message: `Branch has dependent worktrees: ${children.join(", ")}. Use force:true to override.` };
36879
36901
  }
36880
- if (!args.force && fs36.existsSync(wtPath)) {
36902
+ if (!args.force && fs37.existsSync(wtPath)) {
36881
36903
  const dirty = await getDirtyFiles(wtPath);
36882
36904
  if (dirty.length > 0) {
36883
36905
  throw { isToolError: true, message: `Worktree is dirty. Use force:true to remove it.` };
36884
36906
  }
36885
36907
  }
36886
- await gitExec(["-C", repo.mainPath, "worktree", "remove", args.force ? "--force" : "", wtPath].filter(Boolean));
36908
+ await removeWorktreeSafely({
36909
+ mainPath: repo.mainPath,
36910
+ wtPath,
36911
+ force: args.force === true,
36912
+ opts: { verbose: _opts.verbose === true, dryRun: false }
36913
+ });
36887
36914
  cleanupEmptyParents(repo.wtRoot, repo.mainPath, wtPath);
36888
36915
  await removeStackEntry(repo.mainPath, args.branch, { verbose: _opts.verbose === true, dryRun: false });
36889
36916
  return {
@@ -36896,7 +36923,7 @@ async function handleToolCall(name, args, config2, _opts) {
36896
36923
  }
36897
36924
  const repo = resolveRepos(config2, [args.repo])[0];
36898
36925
  const wtPath = getWorktreePath(repo, args.branch);
36899
- if (!fs36.existsSync(wtPath)) {
36926
+ if (!fs37.existsSync(wtPath)) {
36900
36927
  throw { isToolError: true, message: `Worktree not found for branch ${args.branch}` };
36901
36928
  }
36902
36929
  const mainBranch = await resolveMainBranch(repo, config2);
@@ -37118,7 +37145,7 @@ init_config();
37118
37145
  init_log();
37119
37146
  init_git();
37120
37147
  init_resolver();
37121
- import fs37 from "fs";
37148
+ import fs38 from "fs";
37122
37149
  import path40 from "path";
37123
37150
  init_workspace();
37124
37151
  var DEPS_STRATEGIES2 = ["auto", "link", "symlink", "install", "off"];
@@ -37141,7 +37168,7 @@ async function resolveWorktreePathForMember(config2, repoName, branch) {
37141
37168
  if (match)
37142
37169
  return match.path;
37143
37170
  const candidate = getWorktreePath(repo, branch);
37144
- if (fs37.existsSync(candidate))
37171
+ if (fs38.existsSync(candidate))
37145
37172
  return candidate;
37146
37173
  return null;
37147
37174
  }
@@ -37199,7 +37226,7 @@ function registerCreateSub(workspace) {
37199
37226
  process.exit(1);
37200
37227
  }
37201
37228
  const wsPath = workspacePathFor2(config2, name);
37202
- if (fs37.existsSync(wsPath)) {
37229
+ if (fs38.existsSync(wsPath)) {
37203
37230
  stepError("Workspace already exists", wsPath);
37204
37231
  process.exit(1);
37205
37232
  }
@@ -37300,7 +37327,7 @@ function registerAddSub(workspace) {
37300
37327
  const globalOpts = workspace.parent.opts();
37301
37328
  const config2 = loadConfig();
37302
37329
  const wsPath = workspacePathFor2(config2, name);
37303
- if (!fs37.existsSync(wsPath)) {
37330
+ if (!fs38.existsSync(wsPath)) {
37304
37331
  stepError("Workspace not found", wsPath);
37305
37332
  process.exit(1);
37306
37333
  }
@@ -37336,7 +37363,7 @@ function registerRmSub(workspace) {
37336
37363
  const globalOpts = workspace.parent.opts();
37337
37364
  const config2 = loadConfig();
37338
37365
  const wsPath = workspacePathFor2(config2, name);
37339
- if (!fs37.existsSync(wsPath)) {
37366
+ if (!fs38.existsSync(wsPath)) {
37340
37367
  stepError("Workspace not found", wsPath);
37341
37368
  process.exit(1);
37342
37369
  }
@@ -37358,7 +37385,7 @@ function registerRemoveSub(workspace) {
37358
37385
  const globalOpts = workspace.parent.opts();
37359
37386
  const config2 = loadConfig();
37360
37387
  const wsPath = workspacePathFor2(config2, name);
37361
- if (!fs37.existsSync(wsPath)) {
37388
+ if (!fs38.existsSync(wsPath)) {
37362
37389
  stepError("Workspace not found", wsPath);
37363
37390
  process.exit(1);
37364
37391
  }
@@ -37372,7 +37399,7 @@ function registerRemoveSub(workspace) {
37372
37399
  return;
37373
37400
  }
37374
37401
  try {
37375
- fs37.rmSync(wsPath, { recursive: true, force: true });
37402
+ fs38.rmSync(wsPath, { recursive: true, force: true });
37376
37403
  stepSuccess("Workspace removed", wsPath);
37377
37404
  summary(`Done — workspace ${name} deleted (member worktrees preserved)`);
37378
37405
  } catch (err) {
@@ -37385,7 +37412,7 @@ function registerVerifySub(workspace) {
37385
37412
  workspace.command("verify <name>").description("Verify that every workspace member link resolves").action(async (name) => {
37386
37413
  const config2 = loadConfig();
37387
37414
  const wsPath = workspacePathFor2(config2, name);
37388
- if (!fs37.existsSync(wsPath)) {
37415
+ if (!fs38.existsSync(wsPath)) {
37389
37416
  stepError("Workspace not found", wsPath);
37390
37417
  process.exit(1);
37391
37418
  }
@@ -37420,7 +37447,7 @@ init_config();
37420
37447
  init_resolver();
37421
37448
  init_log();
37422
37449
  init_history();
37423
- import fs38 from "fs";
37450
+ import fs39 from "fs";
37424
37451
  var program2 = new Command;
37425
37452
  program2.name("wtx").description("Multi-repo git worktree manager").version(VERSION).option("-q, --quiet", "Suppress progress indicators", false).option("--verbose", "Show git commands as they run", false).option("--dry-run", "Show what would happen", false);
37426
37453
  var MUTATING_COMMANDS = new Set([
@@ -37530,7 +37557,7 @@ program2.command("_resolve-path <repo> <branch>", { hidden: true }).description(
37530
37557
  }
37531
37558
  const repo = repos[0];
37532
37559
  const wtPath = getWorktreePath(repo, branch);
37533
- if (!fs38.existsSync(wtPath)) {
37560
+ if (!fs39.existsSync(wtPath)) {
37534
37561
  process.stderr.write(`✗ No worktree at ${wtPath}
37535
37562
  `);
37536
37563
  process.exit(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ogpoyraz/wtx",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "Multi-repo git worktree manager",
5
5
  "type": "module",
6
6
  "bin": {