@solaqua/gji 0.13.0 → 0.15.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.
Files changed (45) hide show
  1. package/dist/application/worktree/go-resolution.js +5 -3
  2. package/dist/application/worktree/sources.d.ts +13 -4
  3. package/dist/application/worktree/sources.js +34 -13
  4. package/dist/cli/commands/clean.d.ts +4 -2
  5. package/dist/cli/commands/clean.js +338 -65
  6. package/dist/cli/commands/go.d.ts +2 -1
  7. package/dist/cli/commands/go.js +17 -13
  8. package/dist/cli/commands/hub.js +4 -1
  9. package/dist/cli/commands/warp.js +6 -4
  10. package/dist/cli/program.js +5 -0
  11. package/dist/domain/shared/concurrency.d.ts +1 -1
  12. package/dist/domain/shared/concurrency.js +6 -1
  13. package/dist/domain/worktree/path-filter.d.ts +13 -0
  14. package/dist/domain/worktree/path-filter.js +72 -0
  15. package/dist/domain/worktree/policy.d.ts +1 -0
  16. package/dist/domain/worktree/policy.js +1 -1
  17. package/dist/gji-bundle.mjs +746 -368
  18. package/dist/infrastructure/git/refs.js +3 -11
  19. package/dist/presentation/shell/completion.js +7 -2
  20. package/dist/presentation/worktree/picker.d.ts +9 -6
  21. package/dist/presentation/worktree/picker.js +19 -2
  22. package/man/man1/gji-back.1 +1 -1
  23. package/man/man1/gji-clean.1 +4 -1
  24. package/man/man1/gji-completion.1 +1 -1
  25. package/man/man1/gji-config.1 +1 -1
  26. package/man/man1/gji-doctor.1 +1 -1
  27. package/man/man1/gji-done.1 +1 -1
  28. package/man/man1/gji-go.1 +1 -1
  29. package/man/man1/gji-history.1 +1 -1
  30. package/man/man1/gji-init.1 +1 -1
  31. package/man/man1/gji-ls.1 +1 -1
  32. package/man/man1/gji-new.1 +1 -1
  33. package/man/man1/gji-open.1 +1 -1
  34. package/man/man1/gji-pr.1 +1 -1
  35. package/man/man1/gji-remove.1 +1 -1
  36. package/man/man1/gji-root.1 +1 -1
  37. package/man/man1/gji-run-hook.1 +1 -1
  38. package/man/man1/gji-status.1 +1 -1
  39. package/man/man1/gji-sync-files.1 +1 -1
  40. package/man/man1/gji-sync.1 +1 -1
  41. package/man/man1/gji-task.1 +1 -1
  42. package/man/man1/gji-undo.1 +1 -1
  43. package/man/man1/gji-warp.1 +1 -1
  44. package/man/man1/gji.1 +1 -1
  45. package/package.json +1 -1
@@ -5386,7 +5386,7 @@ var require_minimist = __commonJS({
5386
5386
  var require_rc = __commonJS({
5387
5387
  "node_modules/.pnpm/rc@1.2.8/node_modules/rc/index.js"(exports, module) {
5388
5388
  var cc = require_utils();
5389
- var join16 = __require("path").join;
5389
+ var join17 = __require("path").join;
5390
5390
  var deepExtend = require_deep_extend();
5391
5391
  var etc = "/etc";
5392
5392
  var win = process.platform === "win32";
@@ -5411,15 +5411,15 @@ var require_rc = __commonJS({
5411
5411
  }
5412
5412
  if (!win)
5413
5413
  [
5414
- join16(etc, name, "config"),
5415
- join16(etc, name + "rc")
5414
+ join17(etc, name, "config"),
5415
+ join17(etc, name + "rc")
5416
5416
  ].forEach(addConfigFile);
5417
5417
  if (home)
5418
5418
  [
5419
- join16(home, ".config", name, "config"),
5420
- join16(home, ".config", name),
5421
- join16(home, "." + name, "config"),
5422
- join16(home, "." + name + "rc")
5419
+ join17(home, ".config", name, "config"),
5420
+ join17(home, ".config", name),
5421
+ join17(home, "." + name, "config"),
5422
+ join17(home, "." + name + "rc")
5423
5423
  ].forEach(addConfigFile);
5424
5424
  addConfigFile(cc.find("." + name + "rc"));
5425
5425
  if (env5.config) addConfigFile(env5.config);
@@ -6757,11 +6757,11 @@ var require_util = __commonJS({
6757
6757
  if (files.includes("node_modules") || files.includes("package.json") || files.includes("package.json5") || files.includes("package.yaml") || files.includes("pnpm-workspace.yaml")) {
6758
6758
  return name2;
6759
6759
  }
6760
- const dirname14 = path9.dirname(name2);
6761
- if (dirname14 === name2) {
6760
+ const dirname15 = path9.dirname(name2);
6761
+ if (dirname15 === name2) {
6762
6762
  return original;
6763
6763
  }
6764
- return find(dirname14, original);
6764
+ return find(dirname15, original);
6765
6765
  } catch (error) {
6766
6766
  if (name2 === original) {
6767
6767
  if (error.code === "ENOENT") {
@@ -9519,7 +9519,7 @@ var retryifyAsync = (fn, options) => {
9519
9519
  throw error;
9520
9520
  const delay2 = Math.round(interval * Math.random());
9521
9521
  if (delay2 > 0) {
9522
- const delayPromise = new Promise((resolve12) => setTimeout(resolve12, delay2));
9522
+ const delayPromise = new Promise((resolve13) => setTimeout(resolve13, delay2));
9523
9523
  return delayPromise.then(() => attempt.apply(void 0, args));
9524
9524
  } else {
9525
9525
  return attempt.apply(void 0, args);
@@ -9778,14 +9778,14 @@ var Temp = {
9778
9778
  }
9779
9779
  },
9780
9780
  truncate: (filePath) => {
9781
- const basename13 = path2.basename(filePath);
9782
- if (basename13.length <= LIMIT_BASENAME_LENGTH)
9781
+ const basename14 = path2.basename(filePath);
9782
+ if (basename14.length <= LIMIT_BASENAME_LENGTH)
9783
9783
  return filePath;
9784
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename13);
9784
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename14);
9785
9785
  if (!truncable)
9786
9786
  return filePath;
9787
- const truncationLength = basename13.length - LIMIT_BASENAME_LENGTH;
9788
- return `${filePath.slice(0, -basename13.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
9787
+ const truncationLength = basename14.length - LIMIT_BASENAME_LENGTH;
9788
+ return `${filePath.slice(0, -basename14.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
9789
9789
  }
9790
9790
  };
9791
9791
  node_default(Temp.purgeSyncAll);
@@ -11085,14 +11085,14 @@ var TimeoutError = class extends Error {
11085
11085
 
11086
11086
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
11087
11087
  async function timeout(request, init, abortController, options) {
11088
- return new Promise((resolve12, reject) => {
11088
+ return new Promise((resolve13, reject) => {
11089
11089
  const timeoutId = setTimeout(() => {
11090
11090
  if (abortController) {
11091
11091
  abortController.abort();
11092
11092
  }
11093
11093
  reject(new TimeoutError(request));
11094
11094
  }, options.timeout);
11095
- void options.fetch(request, init).then(resolve12).catch(reject).then(() => {
11095
+ void options.fetch(request, init).then(resolve13).catch(reject).then(() => {
11096
11096
  clearTimeout(timeoutId);
11097
11097
  });
11098
11098
  });
@@ -11100,7 +11100,7 @@ async function timeout(request, init, abortController, options) {
11100
11100
 
11101
11101
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
11102
11102
  async function delay(ms, { signal }) {
11103
- return new Promise((resolve12, reject) => {
11103
+ return new Promise((resolve13, reject) => {
11104
11104
  if (signal) {
11105
11105
  signal.throwIfAborted();
11106
11106
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -11111,7 +11111,7 @@ async function delay(ms, { signal }) {
11111
11111
  }
11112
11112
  const timeoutId = setTimeout(() => {
11113
11113
  signal?.removeEventListener("abort", abortHandler);
11114
- resolve12();
11114
+ resolve13();
11115
11115
  }, ms);
11116
11116
  });
11117
11117
  }
@@ -12810,7 +12810,7 @@ function updateNotifier(options) {
12810
12810
  // package.json
12811
12811
  var package_default = {
12812
12812
  name: "@solaqua/gji",
12813
- version: "0.13.0",
12813
+ version: "0.15.0",
12814
12814
  description: "Git worktree CLI for fast context switching.",
12815
12815
  license: "MIT",
12816
12816
  author: "sjquant",
@@ -13093,7 +13093,7 @@ async function ensureDestinationDirectory(root, path9) {
13093
13093
  // src/infrastructure/process/command-runner.ts
13094
13094
  import { spawn as spawn2 } from "node:child_process";
13095
13095
  var runCommand = async (command, cwd, stderr, stdout3 = (chunk) => process.stdout.write(chunk), options) => {
13096
- await new Promise((resolve12, reject) => {
13096
+ await new Promise((resolve13, reject) => {
13097
13097
  const shell = options?.shell ?? true;
13098
13098
  const [executable, args] = shell ? [command, []] : splitCommand(command);
13099
13099
  const child = spawn2(executable, args, {
@@ -13112,7 +13112,7 @@ var runCommand = async (command, cwd, stderr, stdout3 = (chunk) => process.stdou
13112
13112
  if (code !== 0) {
13113
13113
  reject(new Error(`exited with code ${code}`));
13114
13114
  } else {
13115
- resolve12();
13115
+ resolve13();
13116
13116
  }
13117
13117
  });
13118
13118
  child.on("error", reject);
@@ -13931,16 +13931,7 @@ async function resolveCachedRemoteDefaultBranch(cwd, remote) {
13931
13931
  const prefix = `${remote}/`;
13932
13932
  return ref.startsWith(prefix) ? ref.slice(prefix.length) : void 0;
13933
13933
  } catch {
13934
- try {
13935
- const refs = await runGit(cwd, [
13936
- "for-each-ref",
13937
- "--format=%(refname:strip=3)",
13938
- `refs/remotes/${remote}`
13939
- ]);
13940
- return refs.split("\n").find((ref) => ref !== "HEAD");
13941
- } catch {
13942
- return void 0;
13943
- }
13934
+ return void 0;
13944
13935
  }
13945
13936
  }
13946
13937
  async function readBranchLastCommitTimestamp(cwd, branch) {
@@ -14028,9 +14019,9 @@ var EDITORS = [
14028
14019
  ];
14029
14020
  async function defaultSpawnEditor(cli, args) {
14030
14021
  const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
14031
- await new Promise((resolve12, reject) => {
14022
+ await new Promise((resolve13, reject) => {
14032
14023
  child.once("error", reject);
14033
- child.once("spawn", resolve12);
14024
+ child.once("spawn", resolve13);
14034
14025
  });
14035
14026
  child.unref();
14036
14027
  }
@@ -14872,7 +14863,7 @@ async function runArgvHook(hookCmd, cwd, context, stderr, stdout3) {
14872
14863
  stderr("gji: hook argv command must include a non-empty command\n");
14873
14864
  return;
14874
14865
  }
14875
- await new Promise((resolve12) => {
14866
+ await new Promise((resolve13) => {
14876
14867
  const child = spawn4(command, args, {
14877
14868
  cwd,
14878
14869
  shell: false,
@@ -14892,18 +14883,18 @@ async function runArgvHook(hookCmd, cwd, context, stderr, stdout3) {
14892
14883
  `
14893
14884
  );
14894
14885
  }
14895
- resolve12();
14886
+ resolve13();
14896
14887
  });
14897
14888
  child.on("error", (err) => {
14898
14889
  stderr(`gji: hook failed to start: ${err.message}
14899
14890
  `);
14900
- resolve12();
14891
+ resolve13();
14901
14892
  });
14902
14893
  });
14903
14894
  }
14904
14895
  async function runShellHook(hookCmd, cwd, context, stderr, stdout3) {
14905
14896
  const interpolated = interpolate(hookCmd, context);
14906
- await new Promise((resolve12) => {
14897
+ await new Promise((resolve13) => {
14907
14898
  const child = spawn4(interpolated, {
14908
14899
  cwd,
14909
14900
  shell: true,
@@ -14921,12 +14912,12 @@ async function runShellHook(hookCmd, cwd, context, stderr, stdout3) {
14921
14912
  stderr(`gji: hook exited with code ${code}: ${interpolated}
14922
14913
  `);
14923
14914
  }
14924
- resolve12();
14915
+ resolve13();
14925
14916
  });
14926
14917
  child.on("error", (err) => {
14927
14918
  stderr(`gji: hook failed to start: ${err.message}
14928
14919
  `);
14929
- resolve12();
14920
+ resolve13();
14930
14921
  });
14931
14922
  });
14932
14923
  }
@@ -15091,8 +15082,8 @@ async function removeMissingRegistryEntries(paths, home = homedir4()) {
15091
15082
  async function withRegistryLock(registryPath, operation) {
15092
15083
  const previous = registryLocks.get(registryPath);
15093
15084
  let release;
15094
- const current = new Promise((resolve12) => {
15095
- release = resolve12;
15085
+ const current = new Promise((resolve13) => {
15086
+ release = resolve13;
15096
15087
  });
15097
15088
  registryLocks.set(registryPath, current);
15098
15089
  if (previous) await previous;
@@ -15294,11 +15285,12 @@ function toErrorMessage2(error) {
15294
15285
  }
15295
15286
 
15296
15287
  // src/domain/shared/concurrency.ts
15297
- async function mapWithConcurrency(items, limit, mapper) {
15288
+ async function mapWithConcurrency(items, limit, mapper, signal) {
15298
15289
  const results = new Array(items.length);
15299
15290
  let nextIndex = 0;
15300
15291
  async function readNext() {
15301
15292
  for (; ; ) {
15293
+ throwIfAborted(signal);
15302
15294
  const index = nextIndex;
15303
15295
  nextIndex += 1;
15304
15296
  if (index >= items.length) return;
@@ -15310,6 +15302,9 @@ async function mapWithConcurrency(items, limit, mapper) {
15310
15302
  );
15311
15303
  return results;
15312
15304
  }
15305
+ function throwIfAborted(signal) {
15306
+ if (signal?.aborted) throw new Error("Operation cancelled");
15307
+ }
15313
15308
 
15314
15309
  // src/infrastructure/worktree/info.ts
15315
15310
  var MAX_WORKTREE_INFO_READ_CONCURRENCY = 8;
@@ -15695,6 +15690,10 @@ function formatAge(timestamp) {
15695
15690
  return `${days}d ago`;
15696
15691
  }
15697
15692
 
15693
+ // src/cli/commands/clean.ts
15694
+ import { realpath as realpath5 } from "node:fs/promises";
15695
+ import { basename as basename8, dirname as dirname10, join as join12, resolve as resolve11 } from "node:path";
15696
+
15698
15697
  // node_modules/.pnpm/@clack+core@0.5.0/node_modules/@clack/core/dist/index.mjs
15699
15698
  var import_sisteransi = __toESM(require_src(), 1);
15700
15699
  var import_picocolors = __toESM(require_picocolors(), 1);
@@ -16401,6 +16400,205 @@ function sortPullRequests2(pullRequests) {
16401
16400
  );
16402
16401
  }
16403
16402
 
16403
+ // src/application/worktree/sources.ts
16404
+ var MAX_REPOSITORY_DISCOVERY_CONCURRENCY = 4;
16405
+ async function listRegisteredWorktreeSources(options) {
16406
+ const { cwd, repositoryPort, signal } = options;
16407
+ throwIfAborted2(signal);
16408
+ const registry = await repositoryPort.loadRegistry();
16409
+ const currentRoot = await repositoryPort.detectRepository(cwd).then((repository) => repository.currentRoot).catch(() => null);
16410
+ throwIfAborted2(signal);
16411
+ const results = await mapWithConcurrency(
16412
+ registry,
16413
+ MAX_REPOSITORY_DISCOVERY_CONCURRENCY,
16414
+ async (entry) => {
16415
+ throwIfAborted2(signal);
16416
+ try {
16417
+ const worktrees = await repositoryPort.listWorktrees(entry.path);
16418
+ return { entry, skipped: false, worktrees };
16419
+ } catch {
16420
+ return { entry, skipped: true, worktrees: [] };
16421
+ }
16422
+ },
16423
+ signal
16424
+ );
16425
+ const allItems = [];
16426
+ const skipped = [];
16427
+ for (const result of results) {
16428
+ if (result.skipped) {
16429
+ skipped.push(result.entry);
16430
+ continue;
16431
+ }
16432
+ const { entry, worktrees } = result;
16433
+ for (const worktree of worktrees) {
16434
+ allItems.push({
16435
+ repoRoot: entry.path,
16436
+ repoName: entry.name,
16437
+ worktree: {
16438
+ ...worktree,
16439
+ isCurrent: currentRoot !== null && worktree.path === currentRoot
16440
+ }
16441
+ });
16442
+ }
16443
+ }
16444
+ return { skipped, sources: allItems };
16445
+ }
16446
+ async function listDiscoverableWorktreeSources(options) {
16447
+ const { cwd, repositoryPort } = options;
16448
+ const currentRepository = await repositoryPort.detectRepository(cwd).catch(() => null);
16449
+ const registered = await listRegisteredWorktreeSources(options);
16450
+ if (currentRepository === null) {
16451
+ return {
16452
+ skipped: registered.skipped,
16453
+ sources: deduplicateWorktreeSources(registered.sources)
16454
+ };
16455
+ }
16456
+ let currentSources = [];
16457
+ try {
16458
+ currentSources = (await repositoryPort.listWorktrees(cwd)).map(
16459
+ (worktree) => ({
16460
+ repoName: currentRepository.repoName,
16461
+ repoRoot: currentRepository.repoRoot,
16462
+ worktree
16463
+ })
16464
+ );
16465
+ } catch {
16466
+ }
16467
+ return {
16468
+ skipped: registered.skipped,
16469
+ sources: deduplicateWorktreeSources([
16470
+ ...currentSources,
16471
+ ...registered.sources
16472
+ ])
16473
+ };
16474
+ }
16475
+ function deduplicateWorktreeSources(sources) {
16476
+ const seen = /* @__PURE__ */ new Set();
16477
+ return sources.filter((source) => {
16478
+ if (seen.has(source.worktree.path)) return false;
16479
+ seen.add(source.worktree.path);
16480
+ return true;
16481
+ });
16482
+ }
16483
+ function throwIfAborted2(signal) {
16484
+ if (signal?.aborted) throw new Error("Operation cancelled");
16485
+ }
16486
+
16487
+ // src/domain/worktree/path-filter.ts
16488
+ function createWorktreePathFilter(patterns) {
16489
+ const matchers = patterns.map(createPathMatcher);
16490
+ if (matchers.length === 0) return () => true;
16491
+ return (worktreePath) => matchers.some((matches) => matches(worktreePath));
16492
+ }
16493
+ function findGlobIndex(pattern) {
16494
+ for (let index = 0; index < pattern.length; index += 1) {
16495
+ const character = pattern[index];
16496
+ if (character === "\\") index += 1;
16497
+ else if (character === "*" || character === "?") return index;
16498
+ }
16499
+ return -1;
16500
+ }
16501
+ function escapePathPattern(path9) {
16502
+ return path9.replace(/[\\*?]/g, "\\$&");
16503
+ }
16504
+ function unescapePathPattern(pattern) {
16505
+ return pattern.replace(/\\(.)/g, "$1");
16506
+ }
16507
+ function createPathMatcher(pattern) {
16508
+ if (findGlobIndex(pattern) !== -1) {
16509
+ const regExp = globToRegExp(pattern);
16510
+ return (path9) => regExp.test(path9);
16511
+ }
16512
+ const literal = unescapePathPattern(pattern);
16513
+ const prefix = literal === "/" ? "/" : `${literal}/`;
16514
+ return (path9) => path9 === literal || path9.startsWith(prefix);
16515
+ }
16516
+ function globToRegExp(pattern) {
16517
+ let source = "";
16518
+ for (let index = 0; index < pattern.length; index += 1) {
16519
+ let character = pattern[index];
16520
+ if (character === "*") {
16521
+ if (pattern[index + 1] === "*") {
16522
+ index += 1;
16523
+ if (pattern[index + 1] === "/") {
16524
+ index += 1;
16525
+ source += "(?:.*/)?";
16526
+ } else {
16527
+ source += ".*";
16528
+ }
16529
+ } else {
16530
+ source += "[^/]*";
16531
+ }
16532
+ } else if (character === "?") {
16533
+ source += "[^/]";
16534
+ } else {
16535
+ if (character === "\\" && index + 1 < pattern.length) {
16536
+ index += 1;
16537
+ character = pattern[index];
16538
+ }
16539
+ source += character.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
16540
+ }
16541
+ }
16542
+ return new RegExp(`^${source}$`);
16543
+ }
16544
+
16545
+ // src/domain/worktree/policy.ts
16546
+ import { homedir as homedir5 } from "node:os";
16547
+ import { basename as basename7, dirname as dirname8, join as join10 } from "node:path";
16548
+ function resolveWorktreePath(repoRoot, branch, basePath) {
16549
+ const segments = branch.split("/").filter(Boolean);
16550
+ if (segments.length === 0) {
16551
+ throw new Error("Branch name must not be empty.");
16552
+ }
16553
+ if (segments.some((segment) => segment === "." || segment === "..")) {
16554
+ throw new Error(
16555
+ `Branch name '${branch}' contains an invalid path segment.`
16556
+ );
16557
+ }
16558
+ const base = basePath ? expandTildeInPath(basePath) : join10(dirname8(repoRoot), "worktrees", basename7(repoRoot));
16559
+ return join10(base, ...segments);
16560
+ }
16561
+ function validateBranchName(name) {
16562
+ if (name.length === 0) {
16563
+ return "Branch name must not be empty.";
16564
+ }
16565
+ if (/[\x00-\x1f\x7f ~^:?*[\\\s]/.test(name)) {
16566
+ return `Branch name '${name}' contains an invalid character.`;
16567
+ }
16568
+ if (name.startsWith("-")) {
16569
+ return `Branch name '${name}' must not start with a dash.`;
16570
+ }
16571
+ if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
16572
+ return `Branch name '${name}' has invalid slash placement.`;
16573
+ }
16574
+ if (name.includes("..")) {
16575
+ return `Branch name '${name}' must not contain '..'.`;
16576
+ }
16577
+ if (name.endsWith(".")) {
16578
+ return `Branch name '${name}' must not end with '.'.`;
16579
+ }
16580
+ if (name.includes("@{")) {
16581
+ return `Branch name '${name}' must not contain '@{'.`;
16582
+ }
16583
+ if (name === "@") {
16584
+ return "Branch name cannot be '@'.";
16585
+ }
16586
+ for (const segment of name.split("/")) {
16587
+ if (segment.startsWith(".")) {
16588
+ return `Branch name '${name}' contains a path component starting with '.'.`;
16589
+ }
16590
+ if (segment.endsWith(".lock")) {
16591
+ return `Branch name '${name}' contains a path component ending with '.lock'.`;
16592
+ }
16593
+ }
16594
+ return null;
16595
+ }
16596
+ function expandTildeInPath(path9) {
16597
+ if (path9 === "~") return homedir5();
16598
+ if (path9.startsWith("~/")) return join10(homedir5(), path9.slice(2));
16599
+ return path9;
16600
+ }
16601
+
16404
16602
  // src/presentation/worktree/picker.ts
16405
16603
  import { env as env4, platform as platform2, stdin, stdout } from "node:process";
16406
16604
 
@@ -16501,6 +16699,7 @@ async function promptForSingleWorktree(message, worktrees, io = {}) {
16501
16699
  message,
16502
16700
  multiple: false,
16503
16701
  output: io.output,
16702
+ catalog: io.catalog,
16504
16703
  scope: io.scope
16505
16704
  });
16506
16705
  return typeof choice === "string" ? choice : null;
@@ -16511,7 +16710,9 @@ async function promptForMultipleWorktrees(message, worktrees, io = {}) {
16511
16710
  input: io.input,
16512
16711
  message,
16513
16712
  multiple: true,
16514
- output: io.output
16713
+ output: io.output,
16714
+ catalog: io.catalog,
16715
+ scope: io.scope
16515
16716
  });
16516
16717
  return Array.isArray(choice) ? choice : null;
16517
16718
  }
@@ -16600,6 +16801,7 @@ var SearchablePrompt = class {
16600
16801
  scope;
16601
16802
  scopeTogglePending = false;
16602
16803
  promptClosed = false;
16804
+ cancellation = new AbortController();
16603
16805
  prompt;
16604
16806
  run() {
16605
16807
  return this.prompt.prompt();
@@ -16650,9 +16852,14 @@ var SearchablePrompt = class {
16650
16852
  this.scopeTogglePending = true;
16651
16853
  renderPrompt(prompt);
16652
16854
  try {
16653
- const nextScope = await currentScope.toggle();
16855
+ const nextScope = await currentScope.toggle(this.cancellation.signal);
16654
16856
  if (this.promptClosed) return;
16655
- this.entries = nextScope.entries.map(buildSearchableWorktreeEntry);
16857
+ const nextEntries = await buildWorktreePromptEntries(nextScope.sources, {
16858
+ catalog: this.options.catalog,
16859
+ metadata: nextScope.metadata
16860
+ });
16861
+ this.entries = nextEntries.map(buildSearchableWorktreeEntry);
16862
+ this.pruneHiddenSelections();
16656
16863
  this.scope = {
16657
16864
  label: nextScope.label,
16658
16865
  toggle: currentScope.toggle,
@@ -16683,6 +16890,7 @@ var SearchablePrompt = class {
16683
16890
  }
16684
16891
  cancel(prompt) {
16685
16892
  this.promptClosed = true;
16893
+ this.cancellation.abort();
16686
16894
  prompt.state = "cancel";
16687
16895
  renderPrompt(prompt);
16688
16896
  closePrompt(prompt);
@@ -16773,6 +16981,14 @@ var SearchablePrompt = class {
16773
16981
  }
16774
16982
  this.selected.add(entry.value);
16775
16983
  }
16984
+ pruneHiddenSelections() {
16985
+ const visibleValues = new Set(
16986
+ this.entries.filter(isSelectableEntry).map((entry) => entry.value)
16987
+ );
16988
+ for (const value of this.selected) {
16989
+ if (!visibleValues.has(value)) this.selected.delete(value);
16990
+ }
16991
+ }
16776
16992
  render(state, error) {
16777
16993
  const entries = this.visibleEntries();
16778
16994
  const visibleEntries = windowPromptEntries(
@@ -17392,21 +17608,21 @@ import {
17392
17608
  unlink as unlink2,
17393
17609
  writeFile as writeFile7
17394
17610
  } from "node:fs/promises";
17395
- import { homedir as homedir5 } from "node:os";
17396
- import { dirname as dirname8, join as join10, resolve as resolve10 } from "node:path";
17611
+ import { homedir as homedir6 } from "node:os";
17612
+ import { dirname as dirname9, join as join11, resolve as resolve10 } from "node:path";
17397
17613
  var MAX_UNDO_RECORDS = 20;
17398
17614
  var UNDO_LOCK_STALE_AFTER_MS = 6e4;
17399
17615
  var UNDO_LOCK_RETRY_DELAY_MS = 5;
17400
17616
  var UNDO_LOCK_WAIT_TIMEOUT_MS = UNDO_LOCK_STALE_AFTER_MS + UNDO_LOCK_RETRY_DELAY_MS;
17401
17617
  var undoLogQueues = /* @__PURE__ */ new Map();
17402
- function undoLogPath(home = homedir5(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17618
+ function undoLogPath(home = homedir6(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17403
17619
  const configured = process.env.GJI_CONFIG_DIR;
17404
- return join10(
17405
- configured ? resolve10(configured) : join10(home, globalConfigDirectory),
17620
+ return join11(
17621
+ configured ? resolve10(configured) : join11(home, globalConfigDirectory),
17406
17622
  "undo-log.json"
17407
17623
  );
17408
17624
  }
17409
- async function loadUndoRecords(home = homedir5(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17625
+ async function loadUndoRecords(home = homedir6(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17410
17626
  try {
17411
17627
  const parsed = JSON.parse(
17412
17628
  await readFile7(undoLogPath(home, globalConfigDirectory), "utf8")
@@ -17416,7 +17632,7 @@ async function loadUndoRecords(home = homedir5(), globalConfigDirectory = defaul
17416
17632
  return [];
17417
17633
  }
17418
17634
  }
17419
- async function recordUndoOperation(op, repoRoot, worktrees, home = homedir5(), runtime = defaultCliDependencies) {
17635
+ async function recordUndoOperation(op, repoRoot, worktrees, home = homedir6(), runtime = defaultCliDependencies) {
17420
17636
  const globalConfigDirectory = runtime.configStore.GLOBAL_CONFIG_DIRECTORY;
17421
17637
  const { isDirtyWorktree: isDirtyWorktree2, runGit: runGit2 } = runtime.git;
17422
17638
  const entries = [];
@@ -17466,7 +17682,7 @@ async function recordUndoOperation(op, repoRoot, worktrees, home = homedir5(), r
17466
17682
  globalConfigDirectory
17467
17683
  );
17468
17684
  }
17469
- async function finalizeUndoOperation(record, removedWorktrees, home = homedir5(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17685
+ async function finalizeUndoOperation(record, removedWorktrees, home = homedir6(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17470
17686
  const removedPaths = new Set(
17471
17687
  removedWorktrees.map((worktree) => worktree.path)
17472
17688
  );
@@ -17486,7 +17702,7 @@ async function finalizeUndoOperation(record, removedWorktrees, home = homedir5()
17486
17702
  globalConfigDirectory
17487
17703
  );
17488
17704
  }
17489
- async function restoreUndoRecord(record, home = homedir5(), runtime = defaultCliDependencies) {
17705
+ async function restoreUndoRecord(record, home = homedir6(), runtime = defaultCliDependencies) {
17490
17706
  const globalConfigDirectory = runtime.configStore.GLOBAL_CONFIG_DIRECTORY;
17491
17707
  const restored = [];
17492
17708
  const failed = [];
@@ -17531,7 +17747,7 @@ async function runUndoCommand(options) {
17531
17747
  options.stderr(
17532
17748
  "Warning: undo journal is invalid; starting with an empty journal\n"
17533
17749
  );
17534
- const records = await loadUndoRecords(homedir5(), globalConfigDirectory);
17750
+ const records = await loadUndoRecords(homedir6(), globalConfigDirectory);
17535
17751
  if (options.list) {
17536
17752
  if (options.json) options.stdout(`${JSON.stringify(records, null, 2)}
17537
17753
  `);
@@ -17578,7 +17794,7 @@ async function runUndoCommand(options) {
17578
17794
  }
17579
17795
  if (!record)
17580
17796
  return emitUndoError(options, `undo record not found: ${options.id}`);
17581
- const result = await restoreUndoRecord(record, homedir5(), runtime);
17797
+ const result = await restoreUndoRecord(record, homedir6(), runtime);
17582
17798
  if (options.json)
17583
17799
  options.stdout(
17584
17800
  `${JSON.stringify({ restored: result.restored.map((entry) => ({ branch: entry.branch, path: entry.path })), failed: result.failed.map((entry) => ({ branch: entry.branch, path: entry.path, error: entry.message })) }, null, 2)}
@@ -17606,7 +17822,7 @@ async function runUndoCommand(options) {
17606
17822
  async function hasMalformedUndoLog(globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17607
17823
  try {
17608
17824
  const parsed = JSON.parse(
17609
- await readFile7(undoLogPath(homedir5(), globalConfigDirectory), "utf8")
17825
+ await readFile7(undoLogPath(homedir6(), globalConfigDirectory), "utf8")
17610
17826
  );
17611
17827
  return !Array.isArray(parsed) || parsed.some((entry) => !isUndoRecord(entry));
17612
17828
  } catch (error) {
@@ -17698,7 +17914,7 @@ async function assertCommitExists(repoRoot, headSha, runGit2) {
17698
17914
  }
17699
17915
  async function enqueueUndoLogWrite(home, operation, globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17700
17916
  const path9 = undoLogPath(home, globalConfigDirectory);
17701
- await mkdir6(dirname8(path9), { recursive: true });
17917
+ await mkdir6(dirname9(path9), { recursive: true });
17702
17918
  const previous = undoLogQueues.get(path9) ?? Promise.resolve();
17703
17919
  const runLocked = async () => {
17704
17920
  const release = await acquireUndoLogLock(path9);
@@ -17739,7 +17955,7 @@ async function acquireUndoLogLock(path9) {
17739
17955
  const remainingMs = deadline - Date.now();
17740
17956
  if (remainingMs > 0)
17741
17957
  await new Promise(
17742
- (resolve12) => setTimeout(resolve12, Math.min(UNDO_LOCK_RETRY_DELAY_MS, remainingMs))
17958
+ (resolve13) => setTimeout(resolve13, Math.min(UNDO_LOCK_RETRY_DELAY_MS, remainingMs))
17743
17959
  );
17744
17960
  }
17745
17961
  }
@@ -17747,7 +17963,7 @@ async function acquireUndoLogLock(path9) {
17747
17963
  }
17748
17964
  async function writeUndoRecords(home, records, globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17749
17965
  const path9 = undoLogPath(home, globalConfigDirectory);
17750
- await mkdir6(dirname8(path9), { recursive: true });
17966
+ await mkdir6(dirname9(path9), { recursive: true });
17751
17967
  const temporaryPath = `${path9}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
17752
17968
  try {
17753
17969
  await writeFile7(temporaryPath, `${JSON.stringify(records, null, 2)}
@@ -17790,6 +18006,15 @@ function emitUndoError(options, message) {
17790
18006
  }
17791
18007
 
17792
18008
  // src/cli/commands/clean.ts
18009
+ function toPromptSource(candidate) {
18010
+ return {
18011
+ repoName: candidate.repoName,
18012
+ repoRoot: candidate.repoRoot,
18013
+ worktree: candidate.worktree
18014
+ };
18015
+ }
18016
+ var MAX_CLEAN_REPOSITORY_CONCURRENCY = 4;
18017
+ var MAX_CLEAN_WORKTREE_CONCURRENCY = 8;
17793
18018
  function createCleanCommand(dependencies = {}) {
17794
18019
  const promptForWorktrees = dependencies.promptForWorktrees ?? defaultPromptForWorktrees;
17795
18020
  const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval;
@@ -17800,7 +18025,7 @@ function createCleanCommand(dependencies = {}) {
17800
18025
  const {
17801
18026
  readWorktreeHealth: readWorktreeHealth2,
17802
18027
  isBranchMergedInto: isBranchMergedInto2,
17803
- resolveRemoteDefaultBranch: resolveRemoteDefaultBranch2,
18028
+ resolveRemoteBase: resolveRemoteBase2,
17804
18029
  runGit: runGit2
17805
18030
  } = runtime.git;
17806
18031
  const { loadEffectiveConfig: loadEffectiveConfig2 } = runtime.config;
@@ -17830,30 +18055,115 @@ function createCleanCommand(dependencies = {}) {
17830
18055
  options.cwd,
17831
18056
  sourceDependencies
17832
18057
  );
17833
- const linkedCleanupCandidates = linkedWorktrees.filter(
17834
- (worktree) => worktree.path !== repository.currentRoot
18058
+ const pathPatterns = await canonicalizePathPatterns(
18059
+ options.cwd,
18060
+ options.paths ?? []
17835
18061
  );
17836
- const staleBaseRef = options.stale ? await resolveStaleBaseRef(
17837
- repository.repoRoot,
17838
- options.stderr,
17839
- loadEffectiveConfig2,
17840
- resolveRemoteDefaultBranch2,
17841
- runGit2
17842
- ) : null;
17843
- const cleanupCandidates = options.stale ? await filterStaleCleanupCandidates(
17844
- repository.repoRoot,
17845
- linkedCleanupCandidates,
17846
- staleBaseRef,
17847
- readWorktreeHealth2,
17848
- isBranchMergedInto2
17849
- ) : linkedCleanupCandidates;
17850
- if (cleanupCandidates.length === 0) {
18062
+ const matchesPathFilter = createWorktreePathFilter(pathPatterns);
18063
+ const searchAllRepositories = pathPatterns.length > 0;
18064
+ let allSourceCount = 0;
18065
+ const emitNoCandidates = (sourceCount) => {
18066
+ if (pathPatterns.length > 0 && sourceCount === 0) {
18067
+ emitError(options, "No linked worktrees match --path");
18068
+ return 1;
18069
+ }
17851
18070
  if (options.stale) {
17852
18071
  emitNoStaleCandidates(options);
17853
18072
  return 0;
17854
18073
  }
17855
18074
  emitError(options, "No linked worktrees to clean");
17856
18075
  return 1;
18076
+ };
18077
+ const currentSources = linkedWorktrees.filter(
18078
+ (worktree) => worktree.path !== repository.currentRoot && matchesPathFilter(worktree.path)
18079
+ ).map((worktree) => ({
18080
+ repoName: repository.repoName,
18081
+ repoRoot: repository.repoRoot,
18082
+ worktree
18083
+ }));
18084
+ const cleanupDependencies = {
18085
+ isBranchMergedInto: isBranchMergedInto2,
18086
+ loadEffectiveConfig: loadEffectiveConfig2,
18087
+ readWorktreeHealth: readWorktreeHealth2,
18088
+ resolveRemoteBase: resolveRemoteBase2,
18089
+ runGit: runGit2
18090
+ };
18091
+ const currentCandidates = await resolveCleanupCandidates(
18092
+ {
18093
+ sources: currentSources,
18094
+ stale: options.stale,
18095
+ stderr: options.stderr
18096
+ },
18097
+ cleanupDependencies
18098
+ );
18099
+ let activeCandidates = currentCandidates;
18100
+ let allCandidates = null;
18101
+ let currentScope = true;
18102
+ const interactiveSelection = !options.force && !options.json && !isHeadless();
18103
+ const skippedRepositories = [];
18104
+ const loadAllCandidates = async (signal) => {
18105
+ throwIfAborted3(signal);
18106
+ if (allCandidates !== null) return allCandidates;
18107
+ const registered = await listRegisteredWorktreeSources({
18108
+ cwd: options.cwd,
18109
+ repositoryPort: sourceDependencies,
18110
+ signal
18111
+ });
18112
+ skippedRepositories.push(...registered.skipped);
18113
+ if (registered.skipped.length > 0) {
18114
+ reportSkippedRepositories(skippedRepositories, options.stderr);
18115
+ }
18116
+ const sources = deduplicateWorktreeSources([
18117
+ ...currentSources,
18118
+ ...registered.sources
18119
+ ]).filter(
18120
+ (source) => source.repoRoot !== void 0 && source.worktree.path !== source.repoRoot && source.worktree.path !== repository.currentRoot && matchesPathFilter(source.worktree.path)
18121
+ );
18122
+ allSourceCount = sources.length;
18123
+ const otherRepositorySources = sources.filter(
18124
+ (source) => source.repoRoot !== repository.repoRoot
18125
+ );
18126
+ const otherRepositoryCandidates = await resolveCleanupCandidates(
18127
+ {
18128
+ sources: otherRepositorySources,
18129
+ stale: options.stale,
18130
+ stderr: options.stderr,
18131
+ signal
18132
+ },
18133
+ cleanupDependencies
18134
+ );
18135
+ allCandidates = [...currentCandidates, ...otherRepositoryCandidates];
18136
+ throwIfAborted3(signal);
18137
+ return allCandidates;
18138
+ };
18139
+ const scope = {
18140
+ label: "current repository",
18141
+ toggleLabel: "all repositories",
18142
+ toggle: async (signal) => {
18143
+ const nextCurrentScope = !currentScope;
18144
+ const nextCandidates = nextCurrentScope ? currentCandidates : await loadAllCandidates(signal);
18145
+ throwIfAborted3(signal);
18146
+ currentScope = nextCurrentScope;
18147
+ activeCandidates = nextCandidates;
18148
+ return {
18149
+ sources: nextCandidates.map(toPromptSource),
18150
+ metadata: nextCurrentScope ? "full" : "fast",
18151
+ label: nextCurrentScope ? "current repository" : "all repositories",
18152
+ toggleLabel: nextCurrentScope ? "all repositories" : "current repository"
18153
+ };
18154
+ }
18155
+ };
18156
+ if (searchAllRepositories || interactiveSelection && currentCandidates.length === 0) {
18157
+ const loadedAllCandidates = await loadAllCandidates();
18158
+ if (loadedAllCandidates.length === 0) {
18159
+ return emitNoCandidates(allSourceCount);
18160
+ }
18161
+ currentScope = false;
18162
+ activeCandidates = loadedAllCandidates;
18163
+ scope.label = "all repositories";
18164
+ scope.toggleLabel = "current repository";
18165
+ } else if (currentCandidates.length === 0) {
18166
+ return emitNoCandidates(currentSources.length);
17857
18167
  }
17858
18168
  if (!options.dryRun && !options.force && (options.json || isHeadless())) {
17859
18169
  const message = "--force is required";
@@ -17868,27 +18178,29 @@ function createCleanCommand(dependencies = {}) {
17868
18178
  return 1;
17869
18179
  }
17870
18180
  const shouldSelectAll = options.force || options.dryRun && (options.stale || options.json || isHeadless());
17871
- const selections = shouldSelectAll ? cleanupCandidates.map((w2) => w2.path) : await promptForWorktrees(
17872
- await buildWorktreePromptEntries(
17873
- cleanupCandidates.map((worktree) => ({
17874
- repoName: repository.repoName,
17875
- worktree
17876
- })),
17877
- { catalog: runtime.worktreeCatalog }
17878
- )
18181
+ const selections = shouldSelectAll ? activeCandidates.map(({ worktree }) => worktree.path) : await promptForWorktrees(
18182
+ await buildWorktreePromptEntries(activeCandidates, {
18183
+ metadata: currentScope ? "full" : "fast",
18184
+ catalog: runtime.worktreeCatalog
18185
+ }),
18186
+ scope,
18187
+ runtime.worktreeCatalog
17879
18188
  );
17880
18189
  if (!selections || selections.length === 0) {
17881
18190
  options.stderr("Aborted\n");
17882
18191
  return 1;
17883
18192
  }
17884
- const selectedWorktrees = resolveSelectedWorktrees(
17885
- cleanupCandidates,
18193
+ const selectedCandidates = resolveSelectedCandidates(
18194
+ activeCandidates,
17886
18195
  selections
17887
18196
  );
17888
- if (selectedWorktrees.length !== selections.length) {
18197
+ if (selectedCandidates.length !== selections.length) {
17889
18198
  options.stderr("Selected worktree no longer exists\n");
17890
18199
  return 1;
17891
18200
  }
18201
+ const selectedWorktrees = selectedCandidates.map(
18202
+ ({ worktree }) => worktree
18203
+ );
17892
18204
  const selectedWorktreeInfos = await readWorktreeInfos2(selectedWorktrees);
17893
18205
  const selectedInfoByPath = new Map(
17894
18206
  selectedWorktreeInfos.map((info) => [info.path, info])
@@ -17916,38 +18228,48 @@ function createCleanCommand(dependencies = {}) {
17916
18228
  }
17917
18229
  return 0;
17918
18230
  }
17919
- const removedWorktrees = [];
18231
+ const candidatesByRepository = groupCandidatesByRepository(selectedCandidates);
18232
+ const journals = /* @__PURE__ */ new Map();
17920
18233
  const failures = [];
17921
- let journal;
17922
18234
  try {
17923
- journal = await recordUndoOperation(
17924
- "clean",
17925
- repository.repoRoot,
17926
- selectedWorktrees,
17927
- void 0,
17928
- runtime
17929
- );
18235
+ for (const [repoRoot, candidates] of candidatesByRepository) {
18236
+ const journal = await recordUndoOperation(
18237
+ "clean",
18238
+ repoRoot,
18239
+ candidates.map(({ worktree }) => worktree),
18240
+ void 0,
18241
+ runtime
18242
+ );
18243
+ if (!journal) {
18244
+ await discardUndoRecords(
18245
+ journals.values(),
18246
+ runtime.configStore.GLOBAL_CONFIG_DIRECTORY
18247
+ );
18248
+ emitError(
18249
+ options,
18250
+ "could not capture undo state; no worktrees were removed"
18251
+ );
18252
+ return 1;
18253
+ }
18254
+ journals.set(repoRoot, journal);
18255
+ }
17930
18256
  } catch (error) {
17931
- emitError(
17932
- options,
17933
- `could not write undo journal; no worktrees were removed: ${toMessage2(error)}`
18257
+ await discardUndoRecords(
18258
+ journals.values(),
18259
+ runtime.configStore.GLOBAL_CONFIG_DIRECTORY
17934
18260
  );
17935
- return 1;
17936
- }
17937
- if (!journal) {
17938
18261
  emitError(
17939
18262
  options,
17940
- "could not capture undo state; no worktrees were removed"
18263
+ `could not write undo journal; no worktrees were removed: ${toMessage2(error)}`
17941
18264
  );
17942
18265
  return 1;
17943
18266
  }
17944
- for (const worktree of selectedWorktrees) {
18267
+ const removedCandidates = [];
18268
+ for (const candidate of selectedCandidates) {
18269
+ const { repoRoot, staleBaseRef, worktree } = candidate;
17945
18270
  if (options.stale && !await isStaleCleanupCandidate(
17946
- repository.repoRoot,
17947
- worktree,
17948
- staleBaseRef,
17949
- readWorktreeHealth2,
17950
- isBranchMergedInto2
18271
+ { repoRoot, worktree, baseBranch: staleBaseRef },
18272
+ cleanupDependencies
17951
18273
  )) {
17952
18274
  options.stderr(
17953
18275
  `Skipped ${worktree.path}: no longer a safe stale cleanup candidate
@@ -17956,7 +18278,7 @@ function createCleanCommand(dependencies = {}) {
17956
18278
  continue;
17957
18279
  }
17958
18280
  try {
17959
- await removeWorktree2(repository.repoRoot, worktree.path);
18281
+ await removeWorktree2(repoRoot, worktree.path);
17960
18282
  } catch (error) {
17961
18283
  if (!isWorktreeForceRemovalError2(error)) {
17962
18284
  failures.push({
@@ -17982,7 +18304,7 @@ function createCleanCommand(dependencies = {}) {
17982
18304
  continue;
17983
18305
  }
17984
18306
  try {
17985
- await forceRemoveWorktree2(repository.repoRoot, worktree.path);
18307
+ await forceRemoveWorktree2(repoRoot, worktree.path);
17986
18308
  } catch (forceError) {
17987
18309
  failures.push({
17988
18310
  branch: worktree.branch,
@@ -17992,10 +18314,10 @@ function createCleanCommand(dependencies = {}) {
17992
18314
  continue;
17993
18315
  }
17994
18316
  }
17995
- removedWorktrees.push(worktree);
18317
+ removedCandidates.push(candidate);
17996
18318
  if (worktree.branch) {
17997
18319
  try {
17998
- await deleteBranch2(repository.repoRoot, worktree.branch);
18320
+ await deleteBranch2(repoRoot, worktree.branch);
17999
18321
  } catch (error) {
18000
18322
  if (!isBranchUnmergedError2(error)) {
18001
18323
  failures.push({
@@ -18007,7 +18329,7 @@ function createCleanCommand(dependencies = {}) {
18007
18329
  }
18008
18330
  if (options.force || await confirmForceDeleteBranch(worktree.branch)) {
18009
18331
  try {
18010
- await forceDeleteBranch2(repository.repoRoot, worktree.branch);
18332
+ await forceDeleteBranch2(repoRoot, worktree.branch);
18011
18333
  } catch (forceError) {
18012
18334
  options.stderr(
18013
18335
  `Failed to delete branch ${worktree.branch}: ${toMessage2(forceError)}
@@ -18023,83 +18345,205 @@ function createCleanCommand(dependencies = {}) {
18023
18345
  }
18024
18346
  }
18025
18347
  }
18026
- await finalizeUndoOperation(
18027
- journal,
18028
- removedWorktrees,
18029
- void 0,
18030
- runtime.configStore.GLOBAL_CONFIG_DIRECTORY
18348
+ for (const [repoRoot, journal] of journals) {
18349
+ await finalizeUndoOperation(
18350
+ journal,
18351
+ removedCandidates.filter((candidate) => candidate.repoRoot === repoRoot).map(({ worktree }) => worktree),
18352
+ void 0,
18353
+ runtime.configStore.GLOBAL_CONFIG_DIRECTORY
18354
+ );
18355
+ }
18356
+ const undoRecords = remainingUndoRecords(
18357
+ journals.values(),
18358
+ removedCandidates
18031
18359
  );
18032
18360
  await Promise.all(
18033
- removedWorktrees.map((worktree) => releaseWorktreeSlot2(worktree.path))
18361
+ removedCandidates.map(
18362
+ ({ worktree }) => releaseWorktreeSlot2(worktree.path)
18363
+ )
18034
18364
  );
18035
18365
  if (options.json) {
18036
- const removed = removedWorktrees.map((worktree) => {
18366
+ const removed = removedCandidates.map(({ worktree }) => {
18037
18367
  const info = selectedInfoByPath.get(worktree.path);
18038
18368
  return info === void 0 ? { branch: worktree.branch, path: worktree.path } : serializeWorktreeInfo2(info);
18039
18369
  });
18040
- const payload = failures.length === 0 ? { removed } : { removed, failed: failures };
18370
+ const payload = {
18371
+ removed,
18372
+ ...failures.length === 0 ? {} : { failed: failures },
18373
+ ...needsExplicitUndoIds(undoRecords, repository.repoRoot) ? {
18374
+ undo: undoRecords.map(({ id, repoRoot }) => ({ id, repoRoot }))
18375
+ } : {}
18376
+ };
18041
18377
  options.stdout(`${JSON.stringify(payload, null, 2)}
18042
18378
  `);
18043
18379
  } else if (failures.length > 0) {
18044
18380
  reportCleanFailures(failures, options.stderr);
18381
+ emitUndoHints(undoRecords, repository.repoRoot, options.stderr);
18045
18382
  } else {
18046
- options.stderr("undo: gji undo\n");
18047
- options.stdout(`${repository.repoRoot}
18048
- `);
18383
+ emitUndoHints(undoRecords, repository.repoRoot, options.stderr);
18384
+ options.stdout(
18385
+ `${[...new Set(selectedCandidates.map(({ repoRoot }) => repoRoot))].join("\n")}
18386
+ `
18387
+ );
18049
18388
  }
18050
18389
  return failures.length === 0 ? 0 : 1;
18051
18390
  };
18052
18391
  }
18053
18392
  var runCleanCommand = createCleanCommand();
18054
- async function filterStaleCleanupCandidates(repoRoot, worktrees, baseBranch, readWorktreeHealth2, isBranchMergedInto2) {
18055
- if (baseBranch === null) {
18056
- return [];
18393
+ async function canonicalizePathPatterns(cwd, patterns) {
18394
+ return Promise.all(
18395
+ patterns.map(
18396
+ (pattern) => canonicalizeLiteralPrefix(resolve11(cwd, expandTildeInPath(pattern)))
18397
+ )
18398
+ );
18399
+ }
18400
+ async function canonicalizeLiteralPrefix(pattern) {
18401
+ const globIndex = findGlobIndex(pattern);
18402
+ let prefix = globIndex === -1 ? pattern : dirname10(`${pattern.slice(0, globIndex)}x`);
18403
+ let suffix = pattern.slice(prefix.length);
18404
+ for (; ; ) {
18405
+ try {
18406
+ const canonicalPrefix = escapePathPattern(
18407
+ await realpath5(unescapePathPattern(prefix))
18408
+ );
18409
+ return `${canonicalPrefix}${suffix}`.replace(/^\/\//, "/");
18410
+ } catch {
18411
+ const parent = dirname10(prefix);
18412
+ if (parent === prefix) return pattern;
18413
+ suffix = join12("/", basename8(prefix), suffix);
18414
+ prefix = parent;
18415
+ }
18057
18416
  }
18058
- const results = await Promise.all(
18059
- worktrees.map(
18060
- (worktree) => isStaleCleanupCandidate(
18417
+ }
18418
+ async function resolveCleanupCandidates(options, dependencies) {
18419
+ const { sources, stale, stderr, signal } = options;
18420
+ const grouped = /* @__PURE__ */ new Map();
18421
+ for (const source of sources) {
18422
+ if (source.repoRoot === void 0) continue;
18423
+ const existing = grouped.get(source.repoRoot);
18424
+ if (existing === void 0) {
18425
+ grouped.set(source.repoRoot, {
18426
+ repoName: source.repoName,
18427
+ worktrees: [source.worktree]
18428
+ });
18429
+ } else {
18430
+ existing.worktrees.push(source.worktree);
18431
+ }
18432
+ }
18433
+ const results = await mapWithConcurrency(
18434
+ [...grouped.entries()],
18435
+ MAX_CLEAN_REPOSITORY_CONCURRENCY,
18436
+ async ([repoRoot, group]) => {
18437
+ throwIfAborted3(signal);
18438
+ const staleBaseRef = stale ? await resolveStaleBaseRef({ repoRoot, stderr }, dependencies) : null;
18439
+ const worktrees = stale ? await filterStaleCleanupCandidates(
18440
+ {
18441
+ repoRoot,
18442
+ worktrees: group.worktrees,
18443
+ baseBranch: staleBaseRef,
18444
+ signal
18445
+ },
18446
+ dependencies
18447
+ ) : group.worktrees;
18448
+ return worktrees.map((worktree) => ({
18449
+ repoName: group.repoName,
18061
18450
  repoRoot,
18062
- worktree,
18063
- baseBranch,
18064
- readWorktreeHealth2,
18065
- isBranchMergedInto2
18066
- )
18451
+ staleBaseRef,
18452
+ worktree
18453
+ }));
18454
+ },
18455
+ signal
18456
+ );
18457
+ return results.flat();
18458
+ }
18459
+ function resolveSelectedCandidates(candidates, selections) {
18460
+ const selected = [];
18461
+ const seenPaths = /* @__PURE__ */ new Set();
18462
+ for (const selection of selections) {
18463
+ const candidate = candidates.find(
18464
+ (entry) => entry.worktree.path === selection || entry.worktree.branch === selection
18465
+ );
18466
+ if (candidate === void 0 || seenPaths.has(candidate.worktree.path)) {
18467
+ continue;
18468
+ }
18469
+ selected.push(candidate);
18470
+ seenPaths.add(candidate.worktree.path);
18471
+ }
18472
+ return selected;
18473
+ }
18474
+ function groupCandidatesByRepository(candidates) {
18475
+ const grouped = /* @__PURE__ */ new Map();
18476
+ for (const candidate of candidates) {
18477
+ const existing = grouped.get(candidate.repoRoot);
18478
+ if (existing === void 0) grouped.set(candidate.repoRoot, [candidate]);
18479
+ else existing.push(candidate);
18480
+ }
18481
+ return grouped;
18482
+ }
18483
+ function remainingUndoRecords(records, removedCandidates) {
18484
+ return [...records].filter(
18485
+ (record) => removedCandidates.some(
18486
+ (candidate) => candidate.repoRoot === record.repoRoot && record.entries.some((entry) => entry.path === candidate.worktree.path)
18487
+ )
18488
+ );
18489
+ }
18490
+ async function discardUndoRecords(records, globalConfigDirectory) {
18491
+ await Promise.all(
18492
+ [...records].map(
18493
+ (record) => finalizeUndoOperation(record, [], void 0, globalConfigDirectory)
18067
18494
  )
18068
18495
  );
18496
+ }
18497
+ async function filterStaleCleanupCandidates(options, dependencies) {
18498
+ const { repoRoot, worktrees, baseBranch, signal } = options;
18499
+ throwIfAborted3(signal);
18500
+ if (baseBranch === null) {
18501
+ return [];
18502
+ }
18503
+ const results = await mapWithConcurrency(
18504
+ worktrees,
18505
+ MAX_CLEAN_WORKTREE_CONCURRENCY,
18506
+ (worktree) => isStaleCleanupCandidate({ repoRoot, worktree, baseBranch }, dependencies),
18507
+ signal
18508
+ );
18509
+ throwIfAborted3(signal);
18069
18510
  return worktrees.filter((_3, index) => results[index]);
18070
18511
  }
18071
- async function resolveStaleBaseRef(repoRoot, stderr, loadEffectiveConfig2, resolveRemoteDefaultBranch2, runGit2) {
18072
- const config = await loadEffectiveConfig2(repoRoot, void 0, stderr);
18512
+ async function resolveStaleBaseRef(options, dependencies) {
18513
+ const { repoRoot, stderr } = options;
18514
+ const config = await dependencies.loadEffectiveConfig(
18515
+ repoRoot,
18516
+ void 0,
18517
+ stderr
18518
+ );
18073
18519
  const remote = resolveConfiguredString(config.syncRemote) ?? "origin";
18074
18520
  const configuredDefaultBranch = resolveConfiguredString(
18075
18521
  config.syncDefaultBranch
18076
18522
  );
18077
- if (configuredDefaultBranch) {
18078
- return await resolveFetchedRemoteRef(
18079
- repoRoot,
18080
- remote,
18081
- configuredDefaultBranch,
18082
- runGit2
18083
- );
18084
- }
18085
18523
  try {
18086
- const remoteDefaultBranch = await resolveRemoteDefaultBranch2(
18087
- repoRoot,
18088
- remote
18089
- );
18090
- return remoteDefaultBranch === null ? null : await resolveFetchedRemoteRef(
18524
+ const remoteBase = await dependencies.resolveRemoteBase(
18091
18525
  repoRoot,
18092
18526
  remote,
18093
- remoteDefaultBranch,
18094
- runGit2
18527
+ configuredDefaultBranch ?? void 0
18528
+ );
18529
+ return remoteBase === null ? null : resolveFetchedRemoteRef(
18530
+ { branch: remoteBase.branch, remote, repoRoot },
18531
+ dependencies.runGit
18095
18532
  );
18096
18533
  } catch {
18097
18534
  return null;
18098
18535
  }
18099
18536
  }
18100
- async function resolveFetchedRemoteRef(repoRoot, remote, branch, runGit2) {
18537
+ async function resolveFetchedRemoteRef(options, runGit2) {
18538
+ const { repoRoot, remote, branch } = options;
18101
18539
  try {
18102
18540
  await runGit2(repoRoot, ["fetch", "--prune", remote]);
18541
+ await runGit2(repoRoot, [
18542
+ "rev-parse",
18543
+ "--verify",
18544
+ "--quiet",
18545
+ `${remote}/${branch}`
18546
+ ]);
18103
18547
  return `${remote}/${branch}`;
18104
18548
  } catch {
18105
18549
  return null;
@@ -18108,33 +18552,39 @@ async function resolveFetchedRemoteRef(repoRoot, remote, branch, runGit2) {
18108
18552
  function resolveConfiguredString(value) {
18109
18553
  return typeof value === "string" && value.length > 0 ? value : null;
18110
18554
  }
18111
- async function isStaleCleanupCandidate(repoRoot, worktree, baseBranch, readWorktreeHealth2, isBranchMergedInto2) {
18555
+ async function isStaleCleanupCandidate(options, dependencies) {
18556
+ const { repoRoot, worktree, baseBranch } = options;
18112
18557
  if (baseBranch === null) {
18113
18558
  return false;
18114
18559
  }
18115
18560
  if (worktree.branch === null) {
18116
18561
  return false;
18117
18562
  }
18118
- const health = await readWorktreeHealth2(worktree.path);
18563
+ const health = await dependencies.readWorktreeHealth(worktree.path);
18119
18564
  if (health.status !== "clean" || !health.upstreamGone) {
18120
18565
  return false;
18121
18566
  }
18122
- return isBranchMergedInto2(repoRoot, worktree.branch, baseBranch);
18567
+ if (await dependencies.isBranchMergedInto(repoRoot, worktree.branch, baseBranch)) {
18568
+ return true;
18569
+ }
18570
+ return isBranchPatchEquivalentInto(
18571
+ { baseBranch, repoRoot, worktreeBranch: worktree.branch },
18572
+ dependencies.runGit
18573
+ );
18123
18574
  }
18124
- function resolveSelectedWorktrees(worktrees, selections) {
18125
- const selectedWorktrees = [];
18126
- const seenPaths = /* @__PURE__ */ new Set();
18127
- for (const selection of selections) {
18128
- const worktree = worktrees.find(
18129
- (entry) => entry.path === selection || entry.branch === selection
18130
- );
18131
- if (!worktree || seenPaths.has(worktree.path)) {
18132
- continue;
18133
- }
18134
- selectedWorktrees.push(worktree);
18135
- seenPaths.add(worktree.path);
18575
+ async function isBranchPatchEquivalentInto(options, runGit2) {
18576
+ const { repoRoot, worktreeBranch, baseBranch } = options;
18577
+ try {
18578
+ const output = await runGit2(repoRoot, [
18579
+ "cherry",
18580
+ baseBranch,
18581
+ worktreeBranch
18582
+ ]);
18583
+ const lines = output.split("\n").filter(Boolean);
18584
+ return lines.length > 0 && lines.every((line) => line.startsWith("-"));
18585
+ } catch {
18586
+ return false;
18136
18587
  }
18137
- return selectedWorktrees;
18138
18588
  }
18139
18589
  function reportCleanFailures(failures, stderr) {
18140
18590
  const noun = failures.length === 1 ? "worktree" : "worktrees";
@@ -18146,6 +18596,37 @@ function reportCleanFailures(failures, stderr) {
18146
18596
  `);
18147
18597
  }
18148
18598
  }
18599
+ function emitUndoHints(records, currentRepositoryRoot, stderr) {
18600
+ if (records.length === 0) return;
18601
+ if (!needsExplicitUndoIds(records, currentRepositoryRoot)) {
18602
+ stderr("undo: gji undo\n");
18603
+ return;
18604
+ }
18605
+ const message = records.length === 1 ? "undo: restore with:" : "undo: restore each repository with:";
18606
+ stderr(`${message}
18607
+ `);
18608
+ for (const record of records) {
18609
+ stderr(` gji undo --id ${record.id} (${record.repoRoot})
18610
+ `);
18611
+ }
18612
+ }
18613
+ function needsExplicitUndoIds(records, currentRepositoryRoot) {
18614
+ return records.length > 1 || records.some((record) => record.repoRoot !== currentRepositoryRoot);
18615
+ }
18616
+ function reportSkippedRepositories(repositories, stderr) {
18617
+ const noun = repositories.length === 1 ? "repository" : "repositories";
18618
+ stderr(
18619
+ `Skipped ${repositories.length} registered ${noun} because worktrees could not be listed:
18620
+ `
18621
+ );
18622
+ for (const repository of repositories) {
18623
+ stderr(`- ${repository.name} (${repository.path})
18624
+ `);
18625
+ }
18626
+ }
18627
+ function throwIfAborted3(signal) {
18628
+ if (signal?.aborted) throw new Error("Operation cancelled");
18629
+ }
18149
18630
  function formatCleanInfo(info, formatLastCommit3, formatUpstreamState3) {
18150
18631
  const branch = info.branch === null ? "detached" : `branch: ${info.branch}`;
18151
18632
  const status = `status: ${info.status}`;
@@ -18174,8 +18655,11 @@ function emitNoStaleCandidates(options) {
18174
18655
  function toMessage2(error) {
18175
18656
  return error instanceof Error ? error.message : String(error);
18176
18657
  }
18177
- async function defaultPromptForWorktrees(worktrees) {
18178
- return promptForMultipleWorktrees("Choose worktrees to clean", worktrees);
18658
+ async function defaultPromptForWorktrees(worktrees, scope, catalog) {
18659
+ return promptForMultipleWorktrees("Choose worktrees to clean", worktrees, {
18660
+ catalog,
18661
+ scope
18662
+ });
18179
18663
  }
18180
18664
  async function defaultConfirmRemoval(worktrees) {
18181
18665
  const branchCount = worktrees.filter(
@@ -18432,7 +18916,11 @@ _gji_completion() {
18432
18916
  COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
18433
18917
  ;;
18434
18918
  clean)
18435
- COMPREPLY=( $(compgen -W "-f --force --stale --dry-run --json --help" -- "$cur") )
18919
+ if [ "\${COMP_WORDS[COMP_CWORD-1]}" = "--path" ]; then
18920
+ COMPREPLY=( $(compgen -d -- "$cur") )
18921
+ else
18922
+ COMPREPLY=( $(compgen -W "-f --force --stale --path --dry-run --json --help" -- "$cur") )
18923
+ fi
18436
18924
  ;;
18437
18925
  remove|rm)
18438
18926
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
@@ -18675,6 +19163,7 @@ complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active wor
18675
19163
 
18676
19164
  complete -c gji -n '__fish_seen_subcommand_from clean' -s f -l force -d 'bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches'
18677
19165
  complete -c gji -n '__fish_seen_subcommand_from clean' -l stale -d 'only target clean worktrees whose upstream is gone and branch is merged into the default branch'
19166
+ complete -c gji -n '__fish_seen_subcommand_from clean' -l path -r -F -d 'only target worktrees under a directory or matching a path glob (repeatable)'
18678
19167
  complete -c gji -n '__fish_seen_subcommand_from clean' -l dry-run -d 'show what would be deleted without removing anything'
18679
19168
  complete -c gji -n '__fish_seen_subcommand_from clean' -l json -d 'emit JSON on success or error instead of human-readable output'
18680
19169
 
@@ -18835,7 +19324,7 @@ case "\${words[2]}" in
18835
19324
  _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
18836
19325
  ;;
18837
19326
  clean)
18838
- _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
19327
+ _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '*--path[only target worktrees under a directory or matching a path glob]:pattern:_files' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
18839
19328
  ;;
18840
19329
  remove|rm)
18841
19330
  _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
@@ -18972,20 +19461,20 @@ function writeJson(stdout3, value) {
18972
19461
  import { execFile as execFile8 } from "node:child_process";
18973
19462
  import { constants as constants6 } from "node:fs";
18974
19463
  import { access as access6, readFile as readFile8 } from "node:fs/promises";
18975
- import { homedir as homedir6 } from "node:os";
18976
- import { basename as basename7, dirname as dirname9, join as join12 } from "node:path";
19464
+ import { homedir as homedir7 } from "node:os";
19465
+ import { basename as basename9, dirname as dirname11, join as join14 } from "node:path";
18977
19466
  import { promisify as promisify9 } from "node:util";
18978
19467
 
18979
19468
  // src/presentation/shell/setup.ts
18980
19469
  import { constants as constants5 } from "node:fs";
18981
19470
  import { access as access5 } from "node:fs/promises";
18982
- import { delimiter, join as join11 } from "node:path";
19471
+ import { delimiter, join as join13 } from "node:path";
18983
19472
  var SHELL_INTEGRATION_START_MARKER = "# >>> gji init >>>";
18984
19473
  var SHELL_INTEGRATION_END_MARKER = "# <<< gji init <<<";
18985
19474
  var ONBOARDING_SHELL_INTEGRATION_START_MARKER = "# >>> gji shell integration >>>";
18986
19475
  var ONBOARDING_SHELL_INTEGRATION_END_MARKER = "# <<< gji shell integration <<<";
18987
19476
  async function executableExists(command) {
18988
- const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => join11(directory, command));
19477
+ const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(delimiter).filter(Boolean).map((directory) => join13(directory, command));
18989
19478
  for (const path9 of candidates) {
18990
19479
  try {
18991
19480
  await access5(path9, constants5.X_OK);
@@ -18998,17 +19487,17 @@ async function executableExists(command) {
18998
19487
  function resolveShellConfigPath(shell, home) {
18999
19488
  switch (shell) {
19000
19489
  case "bash":
19001
- return join11(home, ".bashrc");
19490
+ return join13(home, ".bashrc");
19002
19491
  case "fish":
19003
- return join11(home, ".config", "fish", "config.fish");
19492
+ return join13(home, ".config", "fish", "config.fish");
19004
19493
  case "zsh":
19005
- return join11(home, ".zshrc");
19494
+ return join13(home, ".zshrc");
19006
19495
  }
19007
19496
  }
19008
19497
  function resolveCompletionPath(shell, home) {
19009
19498
  switch (shell) {
19010
19499
  case "bash":
19011
- return join11(
19500
+ return join13(
19012
19501
  home,
19013
19502
  ".local",
19014
19503
  "share",
@@ -19017,9 +19506,9 @@ function resolveCompletionPath(shell, home) {
19017
19506
  "gji"
19018
19507
  );
19019
19508
  case "fish":
19020
- return join11(home, ".config", "fish", "completions", "gji.fish");
19509
+ return join13(home, ".config", "fish", "completions", "gji.fish");
19021
19510
  case "zsh":
19022
- return join11(home, ".zsh", "completions", "_gji");
19511
+ return join13(home, ".zsh", "completions", "_gji");
19023
19512
  }
19024
19513
  }
19025
19514
  function hasShellIntegration(config, shell) {
@@ -19051,7 +19540,7 @@ async function runDoctorCommand(options) {
19051
19540
  }
19052
19541
  return 1;
19053
19542
  }
19054
- const home = options.home ?? homedir6();
19543
+ const home = options.home ?? homedir7();
19055
19544
  const shell = resolveSupportedShell(
19056
19545
  void 0,
19057
19546
  options.shell ?? process.env.SHELL
@@ -19116,7 +19605,7 @@ async function collectDoctorInspection(cwd, home, shell, runtime) {
19116
19605
  KNOWN_GLOBAL_CONFIG_KEYS2
19117
19606
  );
19118
19607
  const localConfig = repository ? await inspectConfig(
19119
- join12(repository.repoRoot, CONFIG_FILE_NAME2),
19608
+ join14(repository.repoRoot, CONFIG_FILE_NAME2),
19120
19609
  "local",
19121
19610
  KNOWN_CONFIG_KEYS2
19122
19611
  ) : null;
@@ -19295,7 +19784,7 @@ function resolvePerRepoConfig(repos, repoRoot, home) {
19295
19784
  }
19296
19785
  function expandTilde2(path9, home) {
19297
19786
  if (path9 === "~") return home;
19298
- if (path9.startsWith("~/")) return join12(home, path9.slice(2));
19787
+ if (path9.startsWith("~/")) return join14(home, path9.slice(2));
19299
19788
  return path9;
19300
19789
  }
19301
19790
  async function checkGitVersion() {
@@ -19422,7 +19911,7 @@ function resolveWorktreeBase(repoRoot, config, home) {
19422
19911
  if (typeof configuredPath === "string" && (configuredPath.startsWith("/") || configuredPath.startsWith("~"))) {
19423
19912
  return expandTilde2(configuredPath, home);
19424
19913
  }
19425
- return join12(dirname9(repoRoot), "worktrees", basename7(repoRoot));
19914
+ return join14(dirname11(repoRoot), "worktrees", basename9(repoRoot));
19426
19915
  }
19427
19916
  async function findNearestExistingPath(path9) {
19428
19917
  let candidate = path9;
@@ -19431,7 +19920,7 @@ async function findNearestExistingPath(path9) {
19431
19920
  await access6(candidate, constants6.F_OK);
19432
19921
  return candidate;
19433
19922
  } catch {
19434
- const parent = dirname9(candidate);
19923
+ const parent = dirname11(candidate);
19435
19924
  if (parent === candidate) return null;
19436
19925
  candidate = parent;
19437
19926
  }
@@ -19582,7 +20071,7 @@ function statusSymbol(status) {
19582
20071
  }
19583
20072
 
19584
20073
  // src/cli/commands/done.ts
19585
- import { basename as basename8 } from "node:path";
20074
+ import { basename as basename10 } from "node:path";
19586
20075
  var DONE_OUTPUT_FILE_ENV = "GJI_DONE_OUTPUT_FILE";
19587
20076
  async function runDoneCommand(options) {
19588
20077
  const runtime = options.runtime ?? defaultCliDependencies;
@@ -19690,7 +20179,7 @@ async function runDoneCommand(options) {
19690
20179
  {
19691
20180
  branch: target.branch ?? void 0,
19692
20181
  path: target.path,
19693
- repo: basename8(repository.repoRoot)
20182
+ repo: basename10(repository.repoRoot)
19694
20183
  },
19695
20184
  options.stderr
19696
20185
  );
@@ -19881,7 +20370,7 @@ function toMessage3(error) {
19881
20370
  }
19882
20371
 
19883
20372
  // src/cli/commands/go.ts
19884
- import { basename as basename10 } from "node:path";
20373
+ import { basename as basename11 } from "node:path";
19885
20374
 
19886
20375
  // src/application/worktree/context-card.ts
19887
20376
  async function loadContextCardModel(worktreePath, dependencies) {
@@ -19984,71 +20473,6 @@ function parsePrInput(input) {
19984
20473
  return urlMatch?.[1] ?? null;
19985
20474
  }
19986
20475
 
19987
- // src/application/worktree/sources.ts
19988
- var MAX_REPOSITORY_DISCOVERY_CONCURRENCY = 4;
19989
- async function listRegisteredWorktreeSources(cwd, repositoryPort, onSkipped) {
19990
- const registry = await repositoryPort.loadRegistry();
19991
- const currentRoot = await repositoryPort.detectRepository(cwd).then((repository) => repository.currentRoot).catch(() => null);
19992
- const results = await mapWithConcurrency(
19993
- registry,
19994
- MAX_REPOSITORY_DISCOVERY_CONCURRENCY,
19995
- async (entry) => {
19996
- try {
19997
- const worktrees = await repositoryPort.listWorktrees(entry.path);
19998
- return { entry, worktrees };
19999
- } catch {
20000
- onSkipped?.(entry);
20001
- return null;
20002
- }
20003
- }
20004
- );
20005
- const allItems = [];
20006
- for (const result of results) {
20007
- if (result === null) continue;
20008
- const { entry, worktrees } = result;
20009
- for (const worktree of worktrees) {
20010
- allItems.push({
20011
- repoRoot: entry.path,
20012
- repoName: entry.name,
20013
- worktree: {
20014
- ...worktree,
20015
- isCurrent: currentRoot !== null && worktree.path === currentRoot
20016
- }
20017
- });
20018
- }
20019
- }
20020
- return allItems;
20021
- }
20022
- async function listDiscoverableWorktreeSources(cwd, repositoryPort, onSkipped) {
20023
- const currentRepository = await repositoryPort.detectRepository(cwd).catch(() => null);
20024
- const registeredSources = await listRegisteredWorktreeSources(
20025
- cwd,
20026
- repositoryPort,
20027
- onSkipped
20028
- );
20029
- if (currentRepository === null) return dedupeSources(registeredSources);
20030
- let currentSources = [];
20031
- try {
20032
- currentSources = (await repositoryPort.listWorktrees(cwd)).map(
20033
- (worktree) => ({
20034
- repoName: currentRepository.repoName,
20035
- repoRoot: currentRepository.repoRoot,
20036
- worktree
20037
- })
20038
- );
20039
- } catch {
20040
- }
20041
- return dedupeSources([...currentSources, ...registeredSources]);
20042
- }
20043
- function dedupeSources(sources) {
20044
- const seen = /* @__PURE__ */ new Set();
20045
- return sources.filter((source) => {
20046
- if (seen.has(source.worktree.path)) return false;
20047
- seen.add(source.worktree.path);
20048
- return true;
20049
- });
20050
- }
20051
-
20052
20476
  // src/application/worktree/go-resolution.ts
20053
20477
  async function resolveGoBranch(options) {
20054
20478
  const {
@@ -20115,14 +20539,12 @@ async function resolveGoBranch(options) {
20115
20539
  return finish({ kind: "existing", source: pullRequestMatches[0] }, false);
20116
20540
  }
20117
20541
  }
20118
- let skippedRegisteredRepos = 0;
20119
- const registeredSources = await listRegisteredWorktreeSources(
20542
+ const registered = await listRegisteredWorktreeSources({
20120
20543
  cwd,
20121
- sourceDependencies,
20122
- () => {
20123
- skippedRegisteredRepos++;
20124
- }
20125
- );
20544
+ repositoryPort: sourceDependencies
20545
+ });
20546
+ const registeredSources = registered.sources;
20547
+ const skippedRegisteredRepos = registered.skipped.length;
20126
20548
  const crossRepoSources = registeredSources.filter(
20127
20549
  (source) => source.repoRoot !== repository?.repoRoot
20128
20550
  );
@@ -20279,64 +20701,7 @@ function formatLastCommit2(info) {
20279
20701
 
20280
20702
  // src/cli/commands/new.ts
20281
20703
  import { access as access8, mkdir as mkdir7 } from "node:fs/promises";
20282
- import { dirname as dirname11, resolve as resolve11 } from "node:path";
20283
-
20284
- // src/domain/worktree/policy.ts
20285
- import { homedir as homedir7 } from "node:os";
20286
- import { basename as basename9, dirname as dirname10, join as join13 } from "node:path";
20287
- function resolveWorktreePath(repoRoot, branch, basePath) {
20288
- const segments = branch.split("/").filter(Boolean);
20289
- if (segments.length === 0) {
20290
- throw new Error("Branch name must not be empty.");
20291
- }
20292
- if (segments.some((segment) => segment === "." || segment === "..")) {
20293
- throw new Error(
20294
- `Branch name '${branch}' contains an invalid path segment.`
20295
- );
20296
- }
20297
- const base = basePath ? expandTildeInPath(basePath) : join13(dirname10(repoRoot), "worktrees", basename9(repoRoot));
20298
- return join13(base, ...segments);
20299
- }
20300
- function validateBranchName(name) {
20301
- if (name.length === 0) {
20302
- return "Branch name must not be empty.";
20303
- }
20304
- if (/[\x00-\x1f\x7f ~^:?*[\\\s]/.test(name)) {
20305
- return `Branch name '${name}' contains an invalid character.`;
20306
- }
20307
- if (name.startsWith("-")) {
20308
- return `Branch name '${name}' must not start with a dash.`;
20309
- }
20310
- if (name.startsWith("/") || name.endsWith("/") || name.includes("//")) {
20311
- return `Branch name '${name}' has invalid slash placement.`;
20312
- }
20313
- if (name.includes("..")) {
20314
- return `Branch name '${name}' must not contain '..'.`;
20315
- }
20316
- if (name.endsWith(".")) {
20317
- return `Branch name '${name}' must not end with '.'.`;
20318
- }
20319
- if (name.includes("@{")) {
20320
- return `Branch name '${name}' must not contain '@{'.`;
20321
- }
20322
- if (name === "@") {
20323
- return "Branch name cannot be '@'.";
20324
- }
20325
- for (const segment of name.split("/")) {
20326
- if (segment.startsWith(".")) {
20327
- return `Branch name '${name}' contains a path component starting with '.'.`;
20328
- }
20329
- if (segment.endsWith(".lock")) {
20330
- return `Branch name '${name}' contains a path component ending with '.lock'.`;
20331
- }
20332
- }
20333
- return null;
20334
- }
20335
- function expandTildeInPath(path9) {
20336
- if (path9 === "~") return homedir7();
20337
- if (path9.startsWith("~/")) return join13(homedir7(), path9.slice(2));
20338
- return path9;
20339
- }
20704
+ import { dirname as dirname12, resolve as resolve12 } from "node:path";
20340
20705
 
20341
20706
  // src/presentation/bootstrap/output.ts
20342
20707
  function createBootstrapReporter(write, json) {
@@ -20646,7 +21011,7 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
20646
21011
  }
20647
21012
  return 0;
20648
21013
  }
20649
- await mkdir7(dirname11(worktreePath), { recursive: true });
21014
+ await mkdir7(dirname12(worktreePath), { recursive: true });
20650
21015
  const startPoint = options.fromCurrent && !options.detached ? await resolveCurrentWorktreeHead(options.cwd, runtime.git.runGit) : void 0;
20651
21016
  const takeStartPoint = options.take ? await resolveCurrentWorktreeHead(options.cwd, runtime.git.runGit) : void 0;
20652
21017
  const changedTakeFiles = options.take ? await listTakeFiles(options.cwd, runtime.git.runGitRaw) : [];
@@ -21238,7 +21603,7 @@ async function detectInProgressGitState(cwd, runGit2) {
21238
21603
  for (const [marker, name] of markers) {
21239
21604
  try {
21240
21605
  const gitPath = await runGit2(cwd, ["rev-parse", "--git-path", marker]);
21241
- await access8(resolve11(cwd, gitPath));
21606
+ await access8(resolve12(cwd, gitPath));
21242
21607
  return name;
21243
21608
  } catch {
21244
21609
  }
@@ -21390,7 +21755,7 @@ function emitNewError(options, message, details) {
21390
21755
 
21391
21756
  // src/cli/commands/pr.ts
21392
21757
  import { mkdir as mkdir8 } from "node:fs/promises";
21393
- import { dirname as dirname12 } from "node:path";
21758
+ import { dirname as dirname13 } from "node:path";
21394
21759
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
21395
21760
  function createPrCommand(dependencies = {}) {
21396
21761
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
@@ -21529,7 +21894,7 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21529
21894
  }
21530
21895
  return 1;
21531
21896
  }
21532
- await mkdir8(dirname12(worktreePath), { recursive: true });
21897
+ await mkdir8(dirname13(worktreePath), { recursive: true });
21533
21898
  const branchAlreadyExists = await localBranchExists2(
21534
21899
  repository.repoRoot,
21535
21900
  branchName,
@@ -21705,17 +22070,16 @@ function createGoCommand(dependencies = {}) {
21705
22070
  let promptSources = currentSources;
21706
22071
  const loadAllSources = async () => {
21707
22072
  if (registeredSources === null) {
21708
- registeredSources = await listRegisteredWorktreeSources(
21709
- options.cwd,
21710
- {
22073
+ const registered = await listRegisteredWorktreeSources({
22074
+ cwd: options.cwd,
22075
+ repositoryPort: {
21711
22076
  ...runtime.repositoryContext,
21712
22077
  ...runtime.repositoryRegistry,
21713
22078
  ...runtime.worktrees
21714
- },
21715
- () => {
21716
- skippedRegisteredRepos++;
21717
22079
  }
21718
- );
22080
+ });
22081
+ registeredSources = registered.sources;
22082
+ skippedRegisteredRepos = registered.skipped.length;
21719
22083
  }
21720
22084
  return deduplicateSources([...currentSources, ...registeredSources]);
21721
22085
  };
@@ -21737,13 +22101,8 @@ function createGoCommand(dependencies = {}) {
21737
22101
  currentRepositoryScope = nextCurrentRepositoryScope;
21738
22102
  promptSources = nextSources;
21739
22103
  return {
21740
- entries: await buildWorktreePromptEntries(promptSources, {
21741
- metadata: currentRepositoryScope ? "full" : "fast",
21742
- catalog: withPullRequestQueries(
21743
- runtime,
21744
- dependencies.queryPullRequests
21745
- )
21746
- }),
22104
+ sources: promptSources,
22105
+ metadata: currentRepositoryScope ? "full" : "fast",
21747
22106
  label: currentRepositoryScope ? "current repository" : "all repositories",
21748
22107
  toggleLabel: currentRepositoryScope ? "all repositories" : "current repository"
21749
22108
  };
@@ -21757,7 +22116,11 @@ function createGoCommand(dependencies = {}) {
21757
22116
  dependencies.queryPullRequests
21758
22117
  )
21759
22118
  });
21760
- const selectedPath = await prompt(promptEntries, scope);
22119
+ const selectedPath = await prompt(
22120
+ promptEntries,
22121
+ scope,
22122
+ withPullRequestQueries(runtime, dependencies.queryPullRequests)
22123
+ );
21761
22124
  if (!selectedPath) {
21762
22125
  options.stderr("Aborted\n");
21763
22126
  return 1;
@@ -21940,7 +22303,7 @@ async function navigateToExistingWorktree(options, path9, worktree, runtime) {
21940
22303
  {
21941
22304
  branch: worktree?.branch ?? void 0,
21942
22305
  path: path9,
21943
- repo: basename10(repository.repoRoot),
22306
+ repo: basename11(repository.repoRoot),
21944
22307
  slot: await runtime.slots.getWorktreeSlot(path9)
21945
22308
  },
21946
22309
  options.stderr
@@ -22055,8 +22418,11 @@ async function defaultConfirmBranchCreation(branch) {
22055
22418
  });
22056
22419
  return !pD(choice) && choice;
22057
22420
  }
22058
- async function promptForWorktree(worktrees, scope) {
22059
- return promptForSingleWorktree("Choose a worktree", worktrees, { scope });
22421
+ async function promptForWorktree(worktrees, scope, catalog) {
22422
+ return promptForSingleWorktree("Choose a worktree", worktrees, {
22423
+ catalog,
22424
+ scope
22425
+ });
22060
22426
  }
22061
22427
 
22062
22428
  // src/cli/commands/history-command.ts
@@ -22077,7 +22443,7 @@ async function runHistoryCommand(options) {
22077
22443
  }
22078
22444
 
22079
22445
  // src/cli/commands/hub.ts
22080
- import { basename as basename11 } from "node:path";
22446
+ import { basename as basename12 } from "node:path";
22081
22447
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
22082
22448
  var MAX_HUB_REPOSITORY_CONCURRENCY = 4;
22083
22449
  async function buildHubData(cwd, dependencies = {}, runtime = defaultCliDependencies) {
@@ -22089,10 +22455,10 @@ async function buildHubData(cwd, dependencies = {}, runtime = defaultCliDependen
22089
22455
  };
22090
22456
  const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ?? runtime.pullRequests.listOpenPullRequestsForRepository;
22091
22457
  const currentRepository = await detectRepository2(cwd).catch(() => null);
22092
- const sources = await listDiscoverableWorktreeSources(
22458
+ const { sources } = await listDiscoverableWorktreeSources({
22093
22459
  cwd,
22094
- sourceDependencies
22095
- );
22460
+ repositoryPort: sourceDependencies
22461
+ });
22096
22462
  const groups = /* @__PURE__ */ new Map();
22097
22463
  for (const source of sources) {
22098
22464
  if (!source.repoRoot) continue;
@@ -22102,7 +22468,7 @@ async function buildHubData(cwd, dependencies = {}, runtime = defaultCliDependen
22102
22468
  existing.sources.push(source);
22103
22469
  } else {
22104
22470
  groups.set(repoRoot, {
22105
- name: source.repoName || basename11(repoRoot),
22471
+ name: source.repoName || basename12(repoRoot),
22106
22472
  sources: [source]
22107
22473
  });
22108
22474
  }
@@ -22353,7 +22719,7 @@ function formatHumanText(value, maxLength) {
22353
22719
  import { constants as constants8 } from "node:fs";
22354
22720
  import { access as access9, mkdir as mkdir9, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
22355
22721
  import { homedir as homedir8 } from "node:os";
22356
- import { dirname as dirname13 } from "node:path";
22722
+ import { dirname as dirname14 } from "node:path";
22357
22723
  var ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
22358
22724
  var SHELL_WRAPPED_COMMANDS = [
22359
22725
  {
@@ -22478,7 +22844,7 @@ async function runDefaultOnboarding(context, runtime) {
22478
22844
  const installCompletion = await promptForCompletion(shell, completionPath);
22479
22845
  if (installCompletion === null) return abortDefaultOnboarding();
22480
22846
  if (installCompletion) {
22481
- await mkdir9(dirname13(completionPath), { recursive: true });
22847
+ await mkdir9(dirname14(completionPath), { recursive: true });
22482
22848
  await writeFile8(completionPath, renderShellCompletion(shell), "utf8");
22483
22849
  }
22484
22850
  if (shell === "zsh" && (completionAlreadyInstalled || installCompletion)) {
@@ -22584,7 +22950,7 @@ async function applyOnboardingResult(result, home, runtime) {
22584
22950
  runtime
22585
22951
  );
22586
22952
  if (result.installCompletion) {
22587
- await mkdir9(dirname13(completionPath), { recursive: true });
22953
+ await mkdir9(dirname14(completionPath), { recursive: true });
22588
22954
  await writeFile8(
22589
22955
  completionPath,
22590
22956
  renderShellCompletion(result.shell),
@@ -22604,7 +22970,7 @@ async function applyOnboardingResult(result, home, runtime) {
22604
22970
  }
22605
22971
  async function installOnboardingShellIntegration(shell, home) {
22606
22972
  const rcPath = resolveShellConfigPath(shell, home);
22607
- await mkdir9(dirname13(rcPath), { recursive: true });
22973
+ await mkdir9(dirname14(rcPath), { recursive: true });
22608
22974
  const current = await readExistingConfig(rcPath);
22609
22975
  if (shell === "fish") {
22610
22976
  await writeFile8(
@@ -22692,7 +23058,7 @@ async function runLegacyInitCommand(options, runtime) {
22692
23058
  return 0;
22693
23059
  }
22694
23060
  const rcPath = resolveShellConfigPath(shell, home);
22695
- await mkdir9(dirname13(rcPath), { recursive: true });
23061
+ await mkdir9(dirname14(rcPath), { recursive: true });
22696
23062
  const current = await readExistingConfig(rcPath);
22697
23063
  const next = upsertShellIntegration(current, script);
22698
23064
  await writeFile8(rcPath, next, "utf8");
@@ -23050,7 +23416,7 @@ function sortWorktrees(worktrees) {
23050
23416
  // src/cli/commands/open.ts
23051
23417
  import { execFile as execFile9 } from "node:child_process";
23052
23418
  import { access as access10, writeFile as writeFile9 } from "node:fs/promises";
23053
- import { join as join14 } from "node:path";
23419
+ import { join as join15 } from "node:path";
23054
23420
  import { promisify as promisify10 } from "node:util";
23055
23421
  var execFileAsync9 = promisify10(execFile9);
23056
23422
  function createOpenCommand(dependencies = {}) {
@@ -23229,7 +23595,7 @@ async function defaultPromptForEditor(editors) {
23229
23595
  return choice;
23230
23596
  }
23231
23597
  async function ensureWorkspaceFile(worktreePath, repoName) {
23232
- const workspacePath = join14(worktreePath, `${repoName}.code-workspace`);
23598
+ const workspacePath = join15(worktreePath, `${repoName}.code-workspace`);
23233
23599
  try {
23234
23600
  await access10(workspacePath);
23235
23601
  return workspacePath;
@@ -23616,7 +23982,7 @@ function formatError(error) {
23616
23982
  }
23617
23983
 
23618
23984
  // src/cli/commands/remove.ts
23619
- import { basename as basename12 } from "node:path";
23985
+ import { basename as basename13 } from "node:path";
23620
23986
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
23621
23987
  function createRemoveCommand(dependencies = {}) {
23622
23988
  const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree3;
@@ -23745,7 +24111,7 @@ function createRemoveCommand(dependencies = {}) {
23745
24111
  {
23746
24112
  branch: worktree.branch ?? void 0,
23747
24113
  path: worktree.path,
23748
- repo: basename12(repository.repoRoot)
24114
+ repo: basename13(repository.repoRoot)
23749
24115
  },
23750
24116
  options.stderr
23751
24117
  );
@@ -24184,7 +24550,7 @@ function resolveConfiguredString2(value) {
24184
24550
 
24185
24551
  // src/cli/commands/sync-files-command.ts
24186
24552
  import { homedir as homedir9 } from "node:os";
24187
- import { join as join15 } from "node:path";
24553
+ import { join as join16 } from "node:path";
24188
24554
  async function runSyncFilesCommand(options) {
24189
24555
  const runtime = options.runtime ?? defaultCliDependencies;
24190
24556
  const { validateSyncFilePattern: validateSyncFilePattern2 } = runtime.filesystem;
@@ -24338,7 +24704,7 @@ function writeError(options, message) {
24338
24704
  }
24339
24705
  function expandTilde3(value, home) {
24340
24706
  if (value === "~") return home;
24341
- if (value.startsWith("~/")) return join15(home, value.slice(2));
24707
+ if (value.startsWith("~/")) return join16(home, value.slice(2));
24342
24708
  return value;
24343
24709
  }
24344
24710
 
@@ -24446,10 +24812,14 @@ async function resolveWarpTarget(options) {
24446
24812
  );
24447
24813
  return null;
24448
24814
  }
24449
- let skippedRegisteredRepos = 0;
24450
- const allItems = (await listRegisteredWorktreeSources(options.cwd, sourceDependencies, () => {
24451
- skippedRegisteredRepos++;
24452
- })).filter((item) => item.repoRoot !== options.excludeRepoRoot);
24815
+ const registered = await listRegisteredWorktreeSources({
24816
+ cwd: options.cwd,
24817
+ repositoryPort: sourceDependencies
24818
+ });
24819
+ const skippedRegisteredRepos = registered.skipped.length;
24820
+ const allItems = registered.sources.filter(
24821
+ (item) => item.repoRoot !== options.excludeRepoRoot
24822
+ );
24453
24823
  if (allItems.length === 0) {
24454
24824
  emitError5(
24455
24825
  "no accessible worktrees found in any registered repo.",
@@ -24702,6 +25072,10 @@ function registerCommands(program2) {
24702
25072
  ).option(
24703
25073
  "--stale",
24704
25074
  "only target clean worktrees whose upstream is gone and branch is merged into the default branch"
25075
+ ).option(
25076
+ "--path <pattern>",
25077
+ "only target worktrees under a directory or matching a path glob (repeatable)",
25078
+ collectOption
24705
25079
  ).option("--dry-run", "show what would be deleted without removing anything").option(
24706
25080
  "--json",
24707
25081
  "emit JSON on success or error instead of human-readable output"
@@ -25069,6 +25443,7 @@ function attachCommandActions(program2, options) {
25069
25443
  dryRun: commandOptions2.dryRun,
25070
25444
  force: commandOptions2.force,
25071
25445
  json: commandOptions2.json,
25446
+ paths: commandOptions2.path,
25072
25447
  stale: commandOptions2.stale,
25073
25448
  runtime: options.dependencies,
25074
25449
  stderr: options.stderr,
@@ -25196,6 +25571,9 @@ function requireCommand(parent, name) {
25196
25571
  }
25197
25572
  return command;
25198
25573
  }
25574
+ function collectOption(value, previous = []) {
25575
+ return [...previous, value];
25576
+ }
25199
25577
  function notImplemented(commandName) {
25200
25578
  return () => {
25201
25579
  throw new Error(`'${commandName}' is not implemented yet.`);