@solaqua/gji 0.15.0 → 0.16.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 (63) hide show
  1. package/README.md +63 -120
  2. package/dist/cli/commands/back.d.ts +1 -1
  3. package/dist/cli/commands/back.js +1 -0
  4. package/dist/cli/commands/clean.d.ts +1 -1
  5. package/dist/cli/commands/clean.js +11 -2
  6. package/dist/cli/commands/done.js +3 -2
  7. package/dist/cli/commands/go.js +18 -5
  8. package/dist/cli/commands/init.js +0 -14
  9. package/dist/cli/commands/new.d.ts +2 -1
  10. package/dist/cli/commands/new.js +71 -63
  11. package/dist/cli/commands/open.js +1 -1
  12. package/dist/cli/commands/pr-open.js +2 -2
  13. package/dist/cli/commands/pr.js +17 -6
  14. package/dist/cli/commands/run-hook.js +4 -0
  15. package/dist/cli/commands/undo.js +10 -74
  16. package/dist/cli/dependencies.d.ts +5 -1
  17. package/dist/cli/dependencies.js +7 -2
  18. package/dist/cli/program.js +1 -56
  19. package/dist/cli/runtime/headless.d.ts +3 -3
  20. package/dist/cli/runtime/headless.js +6 -4
  21. package/dist/gji-bundle.mjs +500 -842
  22. package/dist/infrastructure/filesystem/locked-file.d.ts +4 -0
  23. package/dist/infrastructure/filesystem/locked-file.js +53 -0
  24. package/dist/infrastructure/git/runner.d.ts +7 -0
  25. package/dist/infrastructure/git/runner.js +35 -1
  26. package/dist/infrastructure/persistence/history.js +12 -10
  27. package/dist/infrastructure/persistence/slots.js +24 -21
  28. package/dist/infrastructure/process/hooks.js +2 -0
  29. package/dist/infrastructure/repository/registry.js +11 -41
  30. package/dist/infrastructure/worktree/lifecycle.d.ts +25 -0
  31. package/dist/infrastructure/worktree/lifecycle.js +120 -19
  32. package/dist/ports/git.d.ts +1 -0
  33. package/dist/ports/hooks.d.ts +7 -0
  34. package/dist/presentation/shell/completion.js +7 -30
  35. package/dist/presentation/terminal/context-card.js +1 -1
  36. package/man/man1/gji-back.1 +1 -1
  37. package/man/man1/gji-clean.1 +1 -1
  38. package/man/man1/gji-completion.1 +1 -1
  39. package/man/man1/gji-config.1 +1 -1
  40. package/man/man1/gji-doctor.1 +1 -1
  41. package/man/man1/gji-done.1 +1 -1
  42. package/man/man1/gji-go.1 +1 -1
  43. package/man/man1/gji-history.1 +1 -1
  44. package/man/man1/gji-init.1 +1 -1
  45. package/man/man1/gji-ls.1 +1 -1
  46. package/man/man1/gji-new.1 +1 -1
  47. package/man/man1/gji-open.1 +1 -1
  48. package/man/man1/gji-pr.1 +1 -1
  49. package/man/man1/gji-root.1 +1 -1
  50. package/man/man1/gji-run-hook.1 +3 -3
  51. package/man/man1/gji-status.1 +1 -1
  52. package/man/man1/gji-sync-files.1 +1 -1
  53. package/man/man1/gji-sync.1 +1 -1
  54. package/man/man1/gji-task.1 +1 -1
  55. package/man/man1/gji-undo.1 +1 -1
  56. package/man/man1/gji.1 +2 -12
  57. package/package.json +3 -2
  58. package/dist/cli/commands/remove.d.ts +0 -21
  59. package/dist/cli/commands/remove.js +0 -189
  60. package/dist/cli/commands/warp.d.ts +0 -28
  61. package/dist/cli/commands/warp.js +0 -123
  62. package/man/man1/gji-remove.1 +0 -19
  63. package/man/man1/gji-warp.1 +0 -16
@@ -3526,7 +3526,7 @@ var require_polyfills = __commonJS({
3526
3526
  }
3527
3527
  if (platform3 === "win32") {
3528
3528
  fs5.rename = typeof fs5.rename !== "function" ? fs5.rename : (function(fs$rename) {
3529
- function rename3(from, to, cb) {
3529
+ function rename2(from, to, cb) {
3530
3530
  var start = Date.now();
3531
3531
  var backoff = 0;
3532
3532
  fs$rename(from, to, function CB(er) {
@@ -3546,8 +3546,8 @@ var require_polyfills = __commonJS({
3546
3546
  if (cb) cb(er);
3547
3547
  });
3548
3548
  }
3549
- if (Object.setPrototypeOf) Object.setPrototypeOf(rename3, fs$rename);
3550
- return rename3;
3549
+ if (Object.setPrototypeOf) Object.setPrototypeOf(rename2, fs$rename);
3550
+ return rename2;
3551
3551
  })(fs5.rename);
3552
3552
  }
3553
3553
  fs5.read = typeof fs5.read !== "function" ? fs5.read : (function(fs$read) {
@@ -3968,8 +3968,8 @@ var require_graceful_fs = __commonJS({
3968
3968
  }
3969
3969
  }
3970
3970
  var fs$writeFile = fs6.writeFile;
3971
- fs6.writeFile = writeFile10;
3972
- function writeFile10(path9, data, options, cb) {
3971
+ fs6.writeFile = writeFile7;
3972
+ function writeFile7(path9, data, options, cb) {
3973
3973
  if (typeof options === "function")
3974
3974
  cb = options, options = null;
3975
3975
  return go$writeFile(path9, data, options, cb);
@@ -5500,7 +5500,7 @@ var require_polyfills2 = __commonJS({
5500
5500
  }
5501
5501
  if (platform3 === "win32") {
5502
5502
  fs5.rename = typeof fs5.rename !== "function" ? fs5.rename : (function(fs$rename) {
5503
- function rename3(from, to, cb) {
5503
+ function rename2(from, to, cb) {
5504
5504
  var start = Date.now();
5505
5505
  var backoff = 0;
5506
5506
  fs$rename(from, to, function CB(er) {
@@ -5520,8 +5520,8 @@ var require_polyfills2 = __commonJS({
5520
5520
  if (cb) cb(er);
5521
5521
  });
5522
5522
  }
5523
- if (Object.setPrototypeOf) Object.setPrototypeOf(rename3, fs$rename);
5524
- return rename3;
5523
+ if (Object.setPrototypeOf) Object.setPrototypeOf(rename2, fs$rename);
5524
+ return rename2;
5525
5525
  })(fs5.rename);
5526
5526
  }
5527
5527
  fs5.read = typeof fs5.read !== "function" ? fs5.read : (function(fs$read) {
@@ -5942,8 +5942,8 @@ var require_graceful_fs2 = __commonJS({
5942
5942
  }
5943
5943
  }
5944
5944
  var fs$writeFile = fs6.writeFile;
5945
- fs6.writeFile = writeFile10;
5946
- function writeFile10(path9, data, options, cb) {
5945
+ fs6.writeFile = writeFile7;
5946
+ function writeFile7(path9, data, options, cb) {
5947
5947
  if (typeof options === "function")
5948
5948
  cb = options, options = null;
5949
5949
  return go$writeFile(path9, data, options, cb);
@@ -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 dirname15 = path9.dirname(name2);
6761
- if (dirname15 === name2) {
6760
+ const dirname12 = path9.dirname(name2);
6761
+ if (dirname12 === name2) {
6762
6762
  return original;
6763
6763
  }
6764
- return find(dirname15, original);
6764
+ return find(dirname12, original);
6765
6765
  } catch (error) {
6766
6766
  if (name2 === original) {
6767
6767
  if (error.code === "ENOENT") {
@@ -9517,9 +9517,9 @@ var retryifyAsync = (fn, options) => {
9517
9517
  throw error;
9518
9518
  if (Date.now() >= timestamp)
9519
9519
  throw error;
9520
- const delay2 = Math.round(interval * Math.random());
9521
- if (delay2 > 0) {
9522
- const delayPromise = new Promise((resolve13) => setTimeout(resolve13, delay2));
9520
+ const delay3 = Math.round(interval * Math.random());
9521
+ if (delay3 > 0) {
9522
+ const delayPromise = new Promise((resolve13) => setTimeout(resolve13, delay3));
9523
9523
  return delayPromise.then(() => attempt.apply(void 0, args));
9524
9524
  } else {
9525
9525
  return attempt.apply(void 0, args);
@@ -12810,8 +12810,8 @@ function updateNotifier(options) {
12810
12810
  // package.json
12811
12811
  var package_default = {
12812
12812
  name: "@solaqua/gji",
12813
- version: "0.15.0",
12814
- description: "Git worktree CLI for fast context switching.",
12813
+ version: "0.16.0",
12814
+ description: "Git worktree CLI for switching tasks without disturbing your work.",
12815
12815
  license: "MIT",
12816
12816
  author: "sjquant",
12817
12817
  type: "module",
@@ -12875,6 +12875,7 @@ var package_default = {
12875
12875
  devDependencies: {
12876
12876
  "@biomejs/biome": "^2.4.15",
12877
12877
  "@j178/prek": "^0.3.13",
12878
+ "@resvg/resvg-js": "^2.6.2",
12878
12879
  "@types/node": "^24.6.0",
12879
12880
  "@vitest/coverage-v8": "3.2.4",
12880
12881
  esbuild: "^0.27.0",
@@ -13859,18 +13860,44 @@ import { promisify as promisify5 } from "node:util";
13859
13860
  import { execFile as execFile3 } from "node:child_process";
13860
13861
  import { promisify as promisify4 } from "node:util";
13861
13862
  var execFileAsync3 = promisify4(execFile3);
13863
+ var CONTENTION_RETRY_TIMEOUT_MS = 1e4;
13864
+ var CONTENTION_RETRY_MAX_DELAY_MS = 1e3;
13865
+ var GIT_ENV = { ...process.env, LC_ALL: "C" };
13862
13866
  async function runGit(cwd, args) {
13863
13867
  return (await runGitRaw(cwd, args)).trim();
13864
13868
  }
13865
13869
  async function runGitRaw(cwd, args) {
13870
+ return execGit(cwd, args);
13871
+ }
13872
+ async function runGitRetryingContention(cwd, args) {
13873
+ return (await retryGitContention(() => execGit(cwd, args, GIT_ENV))).trim();
13874
+ }
13875
+ async function retryGitContention(operation, timeoutMs = CONTENTION_RETRY_TIMEOUT_MS) {
13876
+ const deadline = Date.now() + timeoutMs;
13877
+ for (let delayMs = 25; ; delayMs *= 2) {
13878
+ try {
13879
+ return await operation();
13880
+ } catch (error) {
13881
+ if (!isTransientGitError(error) || Date.now() >= deadline) throw error;
13882
+ }
13883
+ const jitteredDelayMs = Math.min(delayMs, CONTENTION_RETRY_MAX_DELAY_MS) * (0.5 + Math.random() / 2);
13884
+ await new Promise((resolve13) => setTimeout(resolve13, jitteredDelayMs));
13885
+ }
13886
+ }
13887
+ async function execGit(cwd, args, env5) {
13866
13888
  try {
13867
- const { stdout: stdout3 } = await execFileAsync3("git", args, { cwd });
13889
+ const { stdout: stdout3 } = await execFileAsync3("git", args, { cwd, env: env5 });
13868
13890
  return stdout3;
13869
13891
  } catch (error) {
13870
13892
  const message = error instanceof Error ? error.message : String(error);
13871
13893
  throw new Error(`Git command failed in '${cwd}': ${message}`);
13872
13894
  }
13873
13895
  }
13896
+ function isTransientGitError(error) {
13897
+ return error instanceof Error && /(could not lock config file .*|Unable to create '.*\.lock'): File exists|cannot lock ref .*: is at .* but expected |failed to read .*\/commondir|Invalid path '.*\/worktrees\//.test(
13898
+ error.message
13899
+ );
13900
+ }
13874
13901
 
13875
13902
  // src/infrastructure/git/refs.ts
13876
13903
  var execFileAsync4 = promisify5(execFile4);
@@ -14690,9 +14717,72 @@ function levenshtein(a, b3) {
14690
14717
  }
14691
14718
 
14692
14719
  // src/infrastructure/persistence/history.ts
14693
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
14720
+ import { readFile as readFile3 } from "node:fs/promises";
14694
14721
  import { homedir as homedir2 } from "node:os";
14695
- import { dirname as dirname4, join as join6, resolve as resolve6 } from "node:path";
14722
+ import { join as join6, resolve as resolve6 } from "node:path";
14723
+
14724
+ // src/infrastructure/filesystem/locked-file.ts
14725
+ import { randomUUID } from "node:crypto";
14726
+ import {
14727
+ mkdir as mkdir3,
14728
+ open as open3,
14729
+ rename,
14730
+ rm,
14731
+ stat,
14732
+ unlink,
14733
+ writeFile as writeFile3
14734
+ } from "node:fs/promises";
14735
+ import { dirname as dirname4 } from "node:path";
14736
+ import { setTimeout as delay2 } from "node:timers/promises";
14737
+ var LOCK_STALE_AFTER_MS = 1e4;
14738
+ var LOCK_RETRY_DELAY_MS = 5;
14739
+ var LOCK_WAIT_TIMEOUT_MS = LOCK_STALE_AFTER_MS + LOCK_RETRY_DELAY_MS;
14740
+ async function withFileLock(path9, operation) {
14741
+ await mkdir3(dirname4(path9), { recursive: true });
14742
+ const release = await acquireFileLock(path9);
14743
+ try {
14744
+ return await operation();
14745
+ } finally {
14746
+ await release();
14747
+ }
14748
+ }
14749
+ async function writeJsonAtomic(path9, value) {
14750
+ const temporaryPath = `${path9}.tmp-${process.pid}-${randomUUID()}`;
14751
+ try {
14752
+ await writeFile3(
14753
+ temporaryPath,
14754
+ `${JSON.stringify(value, null, 2)}
14755
+ `,
14756
+ "utf8"
14757
+ );
14758
+ await rename(temporaryPath, path9);
14759
+ } catch (error) {
14760
+ await rm(temporaryPath, { force: true }).catch(() => void 0);
14761
+ throw error;
14762
+ }
14763
+ }
14764
+ async function acquireFileLock(path9) {
14765
+ const lockPath = `${path9}.lock`;
14766
+ const deadline = Date.now() + LOCK_WAIT_TIMEOUT_MS;
14767
+ while (Date.now() < deadline) {
14768
+ try {
14769
+ const handle = await open3(lockPath, "wx");
14770
+ return async () => {
14771
+ await handle.close();
14772
+ await unlink(lockPath).catch(() => void 0);
14773
+ };
14774
+ } catch (error) {
14775
+ if (!isAlreadyExistsError(error)) throw error;
14776
+ const lockStat = await stat(lockPath).catch(() => null);
14777
+ if (lockStat && Date.now() - lockStat.mtimeMs > LOCK_STALE_AFTER_MS)
14778
+ await unlink(lockPath).catch(() => void 0);
14779
+ await delay2(LOCK_RETRY_DELAY_MS);
14780
+ }
14781
+ }
14782
+ throw new Error(`timed out waiting for the lock on ${path9}`);
14783
+ }
14784
+
14785
+ // src/infrastructure/persistence/history.ts
14696
14786
  var HISTORY_FILE_NAME = "history.json";
14697
14787
  var MAX_HISTORY_ENTRIES = 50;
14698
14788
  function HISTORY_FILE_PATH(home = homedir2()) {
@@ -14715,15 +14805,15 @@ async function loadHistory(home = homedir2()) {
14715
14805
  }
14716
14806
  async function appendHistory(path9, branch, home = homedir2()) {
14717
14807
  const historyPath = HISTORY_FILE_PATH(home);
14718
- const existing = await loadHistory(home);
14719
- const entry = { branch, path: path9, timestamp: Date.now() };
14720
- const next = [
14721
- entry,
14722
- ...existing.filter((existingEntry) => existingEntry.path !== path9)
14723
- ].slice(0, MAX_HISTORY_ENTRIES);
14724
- await mkdir3(dirname4(historyPath), { recursive: true });
14725
- await writeFile3(historyPath, `${JSON.stringify(next, null, 2)}
14726
- `, "utf8");
14808
+ await withFileLock(historyPath, async () => {
14809
+ const existing = await loadHistory(home);
14810
+ const entry = { branch, path: path9, timestamp: Date.now() };
14811
+ const next = [
14812
+ entry,
14813
+ ...existing.filter((existingEntry) => existingEntry.path !== path9)
14814
+ ].slice(0, MAX_HISTORY_ENTRIES);
14815
+ await writeJsonAtomic(historyPath, next);
14816
+ });
14727
14817
  }
14728
14818
  async function recordWorktreeUsage(path9, branch, home = homedir2()) {
14729
14819
  try {
@@ -14736,9 +14826,9 @@ function isHistoryEntry(value) {
14736
14826
  }
14737
14827
 
14738
14828
  // src/infrastructure/persistence/slots.ts
14739
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
14829
+ import { readFile as readFile4 } from "node:fs/promises";
14740
14830
  import { homedir as homedir3 } from "node:os";
14741
- import { dirname as dirname5, join as join7, resolve as resolve7 } from "node:path";
14831
+ import { join as join7, resolve as resolve7 } from "node:path";
14742
14832
  function SLOTS_FILE_PATH(home = homedir3()) {
14743
14833
  const configDir = process.env.GJI_CONFIG_DIR;
14744
14834
  return configDir ? join7(resolve7(configDir), "slots.json") : join7(home, GLOBAL_CONFIG_DIRECTORY, "slots.json");
@@ -14755,32 +14845,33 @@ async function loadSlots(home = homedir3()) {
14755
14845
  }
14756
14846
  }
14757
14847
  async function assignWorktreeSlot(repoRoot, worktreePath, home = homedir3()) {
14758
- const slots = await loadSlots(home);
14759
- const existing = slots[worktreePath];
14760
- if (existing !== void 0) return existing;
14761
- if (worktreePath !== repoRoot && slots[repoRoot] === void 0)
14762
- slots[repoRoot] = 0;
14763
- const used = new Set(Object.values(slots));
14764
- const slot = worktreePath === repoRoot ? 0 : nextAvailableSlot(used);
14765
- slots[worktreePath] = slot;
14766
- await saveSlots(slots, home);
14767
- return slot;
14848
+ return withFileLock(SLOTS_FILE_PATH(home), async () => {
14849
+ const slots = await loadSlots(home);
14850
+ const existing = slots[worktreePath];
14851
+ if (existing !== void 0) return existing;
14852
+ if (worktreePath !== repoRoot && slots[repoRoot] === void 0)
14853
+ slots[repoRoot] = 0;
14854
+ const used = new Set(Object.values(slots));
14855
+ const slot = worktreePath === repoRoot ? 0 : nextAvailableSlot(used);
14856
+ slots[worktreePath] = slot;
14857
+ await saveSlots(slots, home);
14858
+ return slot;
14859
+ });
14768
14860
  }
14769
14861
  async function getWorktreeSlot(worktreePath, home = homedir3()) {
14770
14862
  const slot = (await loadSlots(home))[worktreePath];
14771
14863
  return slot === void 0 ? null : slot;
14772
14864
  }
14773
14865
  async function releaseWorktreeSlot(worktreePath, home = homedir3()) {
14774
- const slots = await loadSlots(home);
14775
- if (!(worktreePath in slots)) return;
14776
- delete slots[worktreePath];
14777
- await saveSlots(slots, home);
14866
+ await withFileLock(SLOTS_FILE_PATH(home), async () => {
14867
+ const slots = await loadSlots(home);
14868
+ if (!(worktreePath in slots)) return;
14869
+ delete slots[worktreePath];
14870
+ await saveSlots(slots, home);
14871
+ });
14778
14872
  }
14779
14873
  async function saveSlots(slots, home) {
14780
- const path9 = SLOTS_FILE_PATH(home);
14781
- await mkdir4(dirname5(path9), { recursive: true });
14782
- await writeFile4(path9, `${JSON.stringify(slots, null, 2)}
14783
- `, "utf8");
14874
+ await writeJsonAtomic(SLOTS_FILE_PATH(home), slots);
14784
14875
  }
14785
14876
  function nextAvailableSlot(used) {
14786
14877
  let slot = 1;
@@ -14792,16 +14883,16 @@ function isSlotStore(value) {
14792
14883
  }
14793
14884
 
14794
14885
  // src/infrastructure/persistence/task.ts
14795
- import { readFile as readFile5, unlink, writeFile as writeFile5 } from "node:fs/promises";
14886
+ import { readFile as readFile5, unlink as unlink2, writeFile as writeFile4 } from "node:fs/promises";
14796
14887
  import { join as join8 } from "node:path";
14797
14888
 
14798
14889
  // src/infrastructure/repository/context.ts
14799
- import { basename as basename3, dirname as dirname6, isAbsolute as isAbsolute4, resolve as resolve8 } from "node:path";
14890
+ import { basename as basename3, dirname as dirname5, isAbsolute as isAbsolute4, resolve as resolve8 } from "node:path";
14800
14891
  async function detectRepository(cwd) {
14801
14892
  const currentRoot = await runGit(cwd, ["rev-parse", "--show-toplevel"]);
14802
14893
  const rawCommonDir = await runGit(cwd, ["rev-parse", "--git-common-dir"]);
14803
14894
  const gitCommonDir = isAbsolute4(rawCommonDir) ? rawCommonDir : resolve8(currentRoot, rawCommonDir);
14804
- const repoRoot = dirname6(gitCommonDir);
14895
+ const repoRoot = dirname5(gitCommonDir);
14805
14896
  return {
14806
14897
  currentRoot,
14807
14898
  isWorktree: currentRoot !== repoRoot,
@@ -14828,13 +14919,13 @@ async function readTask(worktreePath) {
14828
14919
  async function writeTask(worktreePath, task, source = "manual") {
14829
14920
  const metadata = { source, task, updatedAt: Date.now() };
14830
14921
  const path9 = await taskPath(worktreePath);
14831
- await writeFile5(path9, `${JSON.stringify(metadata, null, 2)}
14922
+ await writeFile4(path9, `${JSON.stringify(metadata, null, 2)}
14832
14923
  `, "utf8");
14833
14924
  return metadata;
14834
14925
  }
14835
14926
  async function clearTask(worktreePath) {
14836
14927
  try {
14837
- await unlink(await taskPath(worktreePath));
14928
+ await unlink2(await taskPath(worktreePath));
14838
14929
  } catch {
14839
14930
  }
14840
14931
  }
@@ -14946,10 +15037,16 @@ function extractHooks(config) {
14946
15037
  }
14947
15038
  const hooks = raw;
14948
15039
  return {
15040
+ "after-branch-create": parseHookCommand(
15041
+ hooks["after-branch-create"] ?? hooks.afterBranchCreate
15042
+ ),
14949
15043
  "after-create": parseHookCommand(
14950
15044
  hooks["after-create"] ?? hooks.afterCreate
14951
15045
  ),
14952
15046
  "after-enter": parseHookCommand(hooks["after-enter"] ?? hooks.afterEnter),
15047
+ "after-switch": parseHookCommand(
15048
+ hooks["after-switch"] ?? hooks.afterSwitch
15049
+ ),
14953
15050
  "before-remove": parseHookCommand(
14954
15051
  hooks["before-remove"] ?? hooks.beforeRemove
14955
15052
  )
@@ -14999,22 +15096,12 @@ async function getRepositoryRemoteUrl(repoRoot, remote) {
14999
15096
  }
15000
15097
 
15001
15098
  // src/infrastructure/repository/registry.ts
15002
- import { randomUUID } from "node:crypto";
15003
15099
  import { constants as constants4 } from "node:fs";
15004
- import {
15005
- access,
15006
- mkdir as mkdir5,
15007
- readFile as readFile6,
15008
- realpath as realpath4,
15009
- rename,
15010
- rm,
15011
- writeFile as writeFile6
15012
- } from "node:fs/promises";
15100
+ import { access, readFile as readFile6, realpath as realpath4 } from "node:fs/promises";
15013
15101
  import { homedir as homedir4 } from "node:os";
15014
- import { basename as basename4, dirname as dirname7, join as join9, resolve as resolve9 } from "node:path";
15102
+ import { basename as basename4, join as join9, resolve as resolve9 } from "node:path";
15015
15103
  var REGISTRY_FILE_NAME = "repos.json";
15016
15104
  var MAX_REGISTRY_ENTRIES = 100;
15017
- var registryLocks = /* @__PURE__ */ new Map();
15018
15105
  function REGISTRY_FILE_PATH(home = homedir4()) {
15019
15106
  const configDir = process.env.GJI_CONFIG_DIR;
15020
15107
  if (configDir) {
@@ -15042,9 +15129,10 @@ async function canonicalizeRepoPath(repoPath) {
15042
15129
  }
15043
15130
  async function registerRepo(repoPath, home = homedir4()) {
15044
15131
  const registryPath = REGISTRY_FILE_PATH(home);
15045
- await withRegistryLock(registryPath, async () => {
15132
+ const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
15133
+ if ((await loadRegistry(home))[0]?.path === canonicalRepoPath) return;
15134
+ await withFileLock(registryPath, async () => {
15046
15135
  const existing = await normalizeRegistryForWrite(await loadRegistry(home));
15047
- const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
15048
15136
  if (existing.length > 0 && existing[0].path === canonicalRepoPath) return;
15049
15137
  const entry = {
15050
15138
  lastUsed: Date.now(),
@@ -15053,13 +15141,13 @@ async function registerRepo(repoPath, home = homedir4()) {
15053
15141
  };
15054
15142
  const filtered = existing.filter((e2) => e2.path !== canonicalRepoPath);
15055
15143
  const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
15056
- await writeRegistry(next, registryPath);
15144
+ await writeJsonAtomic(registryPath, next);
15057
15145
  });
15058
15146
  }
15059
15147
  async function removeMissingRegistryEntries(paths, home = homedir4()) {
15060
15148
  if (paths.size === 0) return { removedPaths: [], skippedPaths: [] };
15061
15149
  const registryPath = REGISTRY_FILE_PATH(home);
15062
- return withRegistryLock(registryPath, async () => {
15150
+ return withFileLock(registryPath, async () => {
15063
15151
  const entries = await loadRegistry(home);
15064
15152
  const candidates = entries.filter((entry) => paths.has(entry.path));
15065
15153
  const status = await Promise.all(
@@ -15071,31 +15159,14 @@ async function removeMissingRegistryEntries(paths, home = homedir4()) {
15071
15159
  const removedPaths = status.filter(({ missing }) => missing).map(({ entry }) => entry.path);
15072
15160
  const skippedPaths = status.filter(({ missing }) => !missing).map(({ entry }) => entry.path);
15073
15161
  if (removedPaths.length > 0) {
15074
- await writeRegistry(
15075
- entries.filter((entry) => !removedPaths.includes(entry.path)),
15076
- registryPath
15162
+ await writeJsonAtomic(
15163
+ registryPath,
15164
+ entries.filter((entry) => !removedPaths.includes(entry.path))
15077
15165
  );
15078
15166
  }
15079
15167
  return { removedPaths, skippedPaths };
15080
15168
  });
15081
15169
  }
15082
- async function withRegistryLock(registryPath, operation) {
15083
- const previous = registryLocks.get(registryPath);
15084
- let release;
15085
- const current = new Promise((resolve13) => {
15086
- release = resolve13;
15087
- });
15088
- registryLocks.set(registryPath, current);
15089
- if (previous) await previous;
15090
- try {
15091
- return await operation();
15092
- } finally {
15093
- release();
15094
- if (registryLocks.get(registryPath) === current) {
15095
- registryLocks.delete(registryPath);
15096
- }
15097
- }
15098
- }
15099
15170
  async function isMissingPath(path9) {
15100
15171
  try {
15101
15172
  await access(path9, constants4.F_OK);
@@ -15109,22 +15180,6 @@ function isMissingPathError(error) {
15109
15180
  const code = error.code;
15110
15181
  return code === "ENOENT" || code === "ENOTDIR";
15111
15182
  }
15112
- async function writeRegistry(entries, registryPath) {
15113
- await mkdir5(dirname7(registryPath), { recursive: true });
15114
- const temporaryPath = `${registryPath}.tmp-${process.pid}-${randomUUID()}`;
15115
- try {
15116
- await writeFile6(
15117
- temporaryPath,
15118
- `${JSON.stringify(entries, null, 2)}
15119
- `,
15120
- "utf8"
15121
- );
15122
- await rename(temporaryPath, registryPath);
15123
- } catch (error) {
15124
- await rm(temporaryPath, { force: true }).catch(() => void 0);
15125
- throw error;
15126
- }
15127
- }
15128
15183
  async function normalizeRegistryForWrite(entries) {
15129
15184
  const normalized = [];
15130
15185
  const seenPaths = /* @__PURE__ */ new Set();
@@ -15368,15 +15423,93 @@ function serializeWorktreeInfo(info) {
15368
15423
 
15369
15424
  // src/infrastructure/worktree/lifecycle.ts
15370
15425
  import { execFile as execFile7 } from "node:child_process";
15371
- import { rm as rm2 } from "node:fs/promises";
15426
+ import { mkdir as mkdir4, rm as rm2, rmdir } from "node:fs/promises";
15372
15427
  import { promisify as promisify8 } from "node:util";
15373
15428
  var execFileAsync7 = promisify8(execFile7);
15374
- var GIT_ENV = { ...process.env, LC_ALL: "C" };
15429
+ var BRANCH_CONFIG_CLEANUP_TIMEOUT_MS = 1e3;
15430
+ async function addWorktree(repoRoot, worktreePath, target) {
15431
+ await claimWorktreePath(worktreePath);
15432
+ if ("create" in target) {
15433
+ try {
15434
+ await createBranch(repoRoot, target.branch, target.create);
15435
+ } catch (error) {
15436
+ await rmdir(worktreePath).catch(() => void 0);
15437
+ throw error;
15438
+ }
15439
+ }
15440
+ const args = "branch" in target ? [target.branch] : ["--detach", ...target.detachedAt ? [target.detachedAt] : []];
15441
+ try {
15442
+ await retryGitContention(
15443
+ () => execFileAsync7("git", ["worktree", "add", worktreePath, ...args], {
15444
+ cwd: repoRoot
15445
+ })
15446
+ );
15447
+ } catch (error) {
15448
+ await removeCreatedWorktree(
15449
+ repoRoot,
15450
+ worktreePath,
15451
+ "create" in target ? target.branch : void 0
15452
+ );
15453
+ throw error;
15454
+ }
15455
+ }
15456
+ async function removeCreatedWorktree(repoRoot, worktreePath, createdBranch) {
15457
+ await runLifecycleGit(repoRoot, [
15458
+ "worktree",
15459
+ "remove",
15460
+ "--force",
15461
+ worktreePath
15462
+ ]).catch(() => void 0);
15463
+ await rmdir(worktreePath).catch(() => void 0);
15464
+ if (createdBranch) await deleteCreatedBranch(repoRoot, createdBranch);
15465
+ }
15466
+ async function claimWorktreePath(worktreePath) {
15467
+ try {
15468
+ await mkdir4(worktreePath);
15469
+ } catch (error) {
15470
+ if (!isAlreadyExistsError(error)) throw error;
15471
+ throw new Error(`target worktree path already exists: ${worktreePath}`);
15472
+ }
15473
+ }
15474
+ async function createBranch(repoRoot, branch, options = {}) {
15475
+ const args = [
15476
+ "branch",
15477
+ ...options.track ? ["--track"] : [],
15478
+ branch,
15479
+ ...options.startPoint ? [options.startPoint] : []
15480
+ ];
15481
+ try {
15482
+ await retryGitContention(async () => {
15483
+ try {
15484
+ await execFileAsync7("git", args, { cwd: repoRoot, env: GIT_ENV });
15485
+ } catch (error) {
15486
+ if (isGitConfigLockError(error))
15487
+ await forceDeleteBranch(repoRoot, branch);
15488
+ throw error;
15489
+ }
15490
+ });
15491
+ } catch (error) {
15492
+ if (isGitConfigLockError(error))
15493
+ await removeBranchConfig(repoRoot, branch).catch(() => void 0);
15494
+ throw error;
15495
+ }
15496
+ }
15497
+ async function deleteCreatedBranch(repoRoot, branch) {
15498
+ try {
15499
+ await forceDeleteBranch(repoRoot, branch);
15500
+ await removeBranchConfig(repoRoot, branch);
15501
+ } catch {
15502
+ }
15503
+ }
15504
+ async function removeBranchConfig(repoRoot, branch) {
15505
+ await runLifecycleGit(
15506
+ repoRoot,
15507
+ ["config", "--remove-section", `branch.${branch}`],
15508
+ BRANCH_CONFIG_CLEANUP_TIMEOUT_MS
15509
+ );
15510
+ }
15375
15511
  async function removeWorktree(repoRoot, worktreePath) {
15376
- await execFileAsync7("git", ["worktree", "remove", worktreePath], {
15377
- cwd: repoRoot,
15378
- env: GIT_ENV
15379
- });
15512
+ await runLifecycleGit(repoRoot, ["worktree", "remove", worktreePath]);
15380
15513
  }
15381
15514
  async function forceRemoveWorktree(repoRoot, worktreePath) {
15382
15515
  try {
@@ -15397,10 +15530,12 @@ async function forceRemoveWorktree(repoRoot, worktreePath) {
15397
15530
  }
15398
15531
  }
15399
15532
  async function removeWorktreeWithForce(repoRoot, worktreePath) {
15400
- await execFileAsync7("git", ["worktree", "remove", "--force", worktreePath], {
15401
- cwd: repoRoot,
15402
- env: GIT_ENV
15403
- });
15533
+ await runLifecycleGit(repoRoot, [
15534
+ "worktree",
15535
+ "remove",
15536
+ "--force",
15537
+ worktreePath
15538
+ ]);
15404
15539
  }
15405
15540
  async function deinitializeSubmodules(worktreePath) {
15406
15541
  await execFileAsync7("git", ["submodule", "deinit", "--force", "--all"], {
@@ -15409,16 +15544,10 @@ async function deinitializeSubmodules(worktreePath) {
15409
15544
  });
15410
15545
  }
15411
15546
  async function deleteBranch(repoRoot, branch) {
15412
- await execFileAsync7("git", ["branch", "-d", branch], {
15413
- cwd: repoRoot,
15414
- env: GIT_ENV
15415
- });
15547
+ await runLifecycleGit(repoRoot, ["branch", "-d", branch]);
15416
15548
  }
15417
15549
  async function forceDeleteBranch(repoRoot, branch) {
15418
- await execFileAsync7("git", ["branch", "-D", branch], {
15419
- cwd: repoRoot,
15420
- env: GIT_ENV
15421
- });
15550
+ await runLifecycleGit(repoRoot, ["branch", "-D", branch]);
15422
15551
  }
15423
15552
  function isWorktreeForceRemovalError(error) {
15424
15553
  if (!hasStderr(error)) {
@@ -15440,6 +15569,15 @@ function isBranchUnmergedError(error) {
15440
15569
  function hasStderr(error) {
15441
15570
  return error instanceof Error && "stderr" in error && typeof error.stderr === "string";
15442
15571
  }
15572
+ async function runLifecycleGit(repoRoot, args, timeoutMs) {
15573
+ await retryGitContention(
15574
+ () => execFileAsync7("git", args, { cwd: repoRoot, env: GIT_ENV }),
15575
+ timeoutMs
15576
+ );
15577
+ }
15578
+ function isGitConfigLockError(error) {
15579
+ return error instanceof Error && error.message.includes("could not lock config file");
15580
+ }
15443
15581
 
15444
15582
  // src/presentation/worktree/format.ts
15445
15583
  function formatUpstreamState(upstream) {
@@ -15507,6 +15645,11 @@ function createCliDependencies() {
15507
15645
  git: {
15508
15646
  runGit,
15509
15647
  runGitRaw,
15648
+ runGitRetryingContention,
15649
+ addWorktree,
15650
+ createBranch,
15651
+ deleteCreatedBranch,
15652
+ removeCreatedWorktree,
15510
15653
  isDirtyWorktree,
15511
15654
  readWorktreeHealth,
15512
15655
  isBranchMergedInto,
@@ -15650,7 +15793,8 @@ async function runBackCommand(options) {
15650
15793
  {
15651
15794
  branch: target.branch ?? void 0,
15652
15795
  path: target.path,
15653
- repo: basename6(repository.repoRoot)
15796
+ repo: basename6(repository.repoRoot),
15797
+ slot: await runtime.slots.getWorktreeSlot(target.path)
15654
15798
  },
15655
15799
  options.stderr
15656
15800
  );
@@ -15692,7 +15836,7 @@ function formatAge(timestamp) {
15692
15836
 
15693
15837
  // src/cli/commands/clean.ts
15694
15838
  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";
15839
+ import { basename as basename8, dirname as dirname7, join as join12, resolve as resolve11 } from "node:path";
15696
15840
 
15697
15841
  // node_modules/.pnpm/@clack+core@0.5.0/node_modules/@clack/core/dist/index.mjs
15698
15842
  var import_sisteransi = __toESM(require_src(), 1);
@@ -16544,7 +16688,7 @@ function globToRegExp(pattern) {
16544
16688
 
16545
16689
  // src/domain/worktree/policy.ts
16546
16690
  import { homedir as homedir5 } from "node:os";
16547
- import { basename as basename7, dirname as dirname8, join as join10 } from "node:path";
16691
+ import { basename as basename7, dirname as dirname6, join as join10 } from "node:path";
16548
16692
  function resolveWorktreePath(repoRoot, branch, basePath) {
16549
16693
  const segments = branch.split("/").filter(Boolean);
16550
16694
  if (segments.length === 0) {
@@ -16555,7 +16699,7 @@ function resolveWorktreePath(repoRoot, branch, basePath) {
16555
16699
  `Branch name '${branch}' contains an invalid path segment.`
16556
16700
  );
16557
16701
  }
16558
- const base = basePath ? expandTildeInPath(basePath) : join10(dirname8(repoRoot), "worktrees", basename7(repoRoot));
16702
+ const base = basePath ? expandTildeInPath(basePath) : join10(dirname6(repoRoot), "worktrees", basename7(repoRoot));
16559
16703
  return join10(base, ...segments);
16560
16704
  }
16561
16705
  function validateBranchName(name) {
@@ -17594,27 +17738,14 @@ async function defaultConfirmForceDeleteBranch(branch) {
17594
17738
 
17595
17739
  // src/cli/runtime/headless.ts
17596
17740
  function isHeadless() {
17597
- return process.env.GJI_NO_TUI === "1";
17741
+ return process.env.GJI_NO_TUI === "1" || process.stdin.isTTY !== true || process.stdout.isTTY !== true;
17598
17742
  }
17599
17743
 
17600
17744
  // src/cli/commands/undo.ts
17601
- import {
17602
- access as access4,
17603
- mkdir as mkdir6,
17604
- open as open3,
17605
- readFile as readFile7,
17606
- rename as rename2,
17607
- stat,
17608
- unlink as unlink2,
17609
- writeFile as writeFile7
17610
- } from "node:fs/promises";
17745
+ import { access as access4, readFile as readFile7 } from "node:fs/promises";
17611
17746
  import { homedir as homedir6 } from "node:os";
17612
- import { dirname as dirname9, join as join11, resolve as resolve10 } from "node:path";
17747
+ import { join as join11, resolve as resolve10 } from "node:path";
17613
17748
  var MAX_UNDO_RECORDS = 20;
17614
- var UNDO_LOCK_STALE_AFTER_MS = 6e4;
17615
- var UNDO_LOCK_RETRY_DELAY_MS = 5;
17616
- var UNDO_LOCK_WAIT_TIMEOUT_MS = UNDO_LOCK_STALE_AFTER_MS + UNDO_LOCK_RETRY_DELAY_MS;
17617
- var undoLogQueues = /* @__PURE__ */ new Map();
17618
17749
  function undoLogPath(home = homedir6(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17619
17750
  const configured = process.env.GJI_CONFIG_DIR;
17620
17751
  return join11(
@@ -17668,39 +17799,31 @@ async function recordUndoOperation(op, repoRoot, worktrees, home = homedir6(), r
17668
17799
  timestamp,
17669
17800
  entries
17670
17801
  };
17671
- return enqueueUndoLogWrite(
17672
- home,
17673
- async () => {
17674
- const existing = await loadUndoRecords(home, globalConfigDirectory);
17675
- await writeUndoRecords(
17676
- home,
17677
- [record, ...existing].slice(0, MAX_UNDO_RECORDS),
17678
- globalConfigDirectory
17679
- );
17680
- return record;
17681
- },
17682
- globalConfigDirectory
17683
- );
17802
+ return withFileLock(undoLogPath(home, globalConfigDirectory), async () => {
17803
+ const existing = await loadUndoRecords(home, globalConfigDirectory);
17804
+ await writeUndoRecords(
17805
+ home,
17806
+ [record, ...existing].slice(0, MAX_UNDO_RECORDS),
17807
+ globalConfigDirectory
17808
+ );
17809
+ return record;
17810
+ });
17684
17811
  }
17685
17812
  async function finalizeUndoOperation(record, removedWorktrees, home = homedir6(), globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17686
17813
  const removedPaths = new Set(
17687
17814
  removedWorktrees.map((worktree) => worktree.path)
17688
17815
  );
17689
- await enqueueUndoLogWrite(
17690
- home,
17691
- async () => {
17692
- const records = await loadUndoRecords(home, globalConfigDirectory);
17693
- const remaining = records.flatMap((candidate) => {
17694
- if (candidate.id !== record.id) return [candidate];
17695
- const entries = candidate.entries.filter(
17696
- (entry) => removedPaths.has(entry.path)
17697
- );
17698
- return entries.length === 0 ? [] : [{ ...candidate, entries }];
17699
- });
17700
- await writeUndoRecords(home, remaining, globalConfigDirectory);
17701
- },
17702
- globalConfigDirectory
17703
- );
17816
+ await withFileLock(undoLogPath(home, globalConfigDirectory), async () => {
17817
+ const records = await loadUndoRecords(home, globalConfigDirectory);
17818
+ const remaining = records.flatMap((candidate) => {
17819
+ if (candidate.id !== record.id) return [candidate];
17820
+ const entries = candidate.entries.filter(
17821
+ (entry) => removedPaths.has(entry.path)
17822
+ );
17823
+ return entries.length === 0 ? [] : [{ ...candidate, entries }];
17824
+ });
17825
+ await writeUndoRecords(home, remaining, globalConfigDirectory);
17826
+ });
17704
17827
  }
17705
17828
  async function restoreUndoRecord(record, home = homedir6(), runtime = defaultCliDependencies) {
17706
17829
  const globalConfigDirectory = runtime.configStore.GLOBAL_CONFIG_DIRECTORY;
@@ -17724,19 +17847,15 @@ async function restoreUndoRecord(record, home = homedir6(), runtime = defaultCli
17724
17847
  unresolved.push(entry);
17725
17848
  }
17726
17849
  }
17727
- await enqueueUndoLogWrite(
17728
- home,
17729
- async () => {
17730
- const records = await loadUndoRecords(home, globalConfigDirectory);
17731
- const remaining = records.flatMap((candidate) => {
17732
- if (candidate.id !== record.id) return [candidate];
17733
- if (unresolved.length === 0) return [];
17734
- return [{ ...candidate, entries: unresolved }];
17735
- });
17736
- await writeUndoRecords(home, remaining, globalConfigDirectory);
17737
- },
17738
- globalConfigDirectory
17739
- );
17850
+ await withFileLock(undoLogPath(home, globalConfigDirectory), async () => {
17851
+ const records = await loadUndoRecords(home, globalConfigDirectory);
17852
+ const remaining = records.flatMap((candidate) => {
17853
+ if (candidate.id !== record.id) return [candidate];
17854
+ if (unresolved.length === 0) return [];
17855
+ return [{ ...candidate, entries: unresolved }];
17856
+ });
17857
+ await writeUndoRecords(home, remaining, globalConfigDirectory);
17858
+ });
17740
17859
  return { restored, failed };
17741
17860
  }
17742
17861
  async function runUndoCommand(options) {
@@ -17912,69 +18031,8 @@ async function assertCommitExists(repoRoot, headSha, runGit2) {
17912
18031
  throw new Error("commit no longer exists (gc'd) \u2014 cannot restore");
17913
18032
  }
17914
18033
  }
17915
- async function enqueueUndoLogWrite(home, operation, globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17916
- const path9 = undoLogPath(home, globalConfigDirectory);
17917
- await mkdir6(dirname9(path9), { recursive: true });
17918
- const previous = undoLogQueues.get(path9) ?? Promise.resolve();
17919
- const runLocked = async () => {
17920
- const release = await acquireUndoLogLock(path9);
17921
- try {
17922
- return await operation();
17923
- } finally {
17924
- await release();
17925
- }
17926
- };
17927
- const current = previous.then(runLocked, runLocked);
17928
- const settled = current.then(
17929
- () => void 0,
17930
- () => void 0
17931
- );
17932
- undoLogQueues.set(path9, settled);
17933
- try {
17934
- return await current;
17935
- } finally {
17936
- if (undoLogQueues.get(path9) === settled) undoLogQueues.delete(path9);
17937
- }
17938
- }
17939
- async function acquireUndoLogLock(path9) {
17940
- const lockPath = `${path9}.lock`;
17941
- const deadline = Date.now() + UNDO_LOCK_WAIT_TIMEOUT_MS;
17942
- while (Date.now() < deadline) {
17943
- try {
17944
- const handle = await open3(lockPath, "wx");
17945
- return async () => {
17946
- await handle.close();
17947
- await unlink2(lockPath).catch(() => void 0);
17948
- };
17949
- } catch (error) {
17950
- if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
17951
- throw error;
17952
- const lockStat = await stat(lockPath).catch(() => null);
17953
- if (lockStat && Date.now() - lockStat.mtimeMs > UNDO_LOCK_STALE_AFTER_MS)
17954
- await unlink2(lockPath).catch(() => void 0);
17955
- const remainingMs = deadline - Date.now();
17956
- if (remainingMs > 0)
17957
- await new Promise(
17958
- (resolve13) => setTimeout(resolve13, Math.min(UNDO_LOCK_RETRY_DELAY_MS, remainingMs))
17959
- );
17960
- }
17961
- }
17962
- throw new Error("timed out waiting for the undo journal lock");
17963
- }
17964
18034
  async function writeUndoRecords(home, records, globalConfigDirectory = defaultCliDependencies.configStore.GLOBAL_CONFIG_DIRECTORY) {
17965
- const path9 = undoLogPath(home, globalConfigDirectory);
17966
- await mkdir6(dirname9(path9), { recursive: true });
17967
- const temporaryPath = `${path9}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
17968
- try {
17969
- await writeFile7(temporaryPath, `${JSON.stringify(records, null, 2)}
17970
- `, {
17971
- encoding: "utf8",
17972
- flag: "wx"
17973
- });
17974
- await rename2(temporaryPath, path9);
17975
- } finally {
17976
- await unlink2(temporaryPath).catch(() => void 0);
17977
- }
18035
+ await writeJsonAtomic(undoLogPath(home, globalConfigDirectory), records);
17978
18036
  }
17979
18037
  function isUndoRecord(value) {
17980
18038
  if (!value || typeof value !== "object") return false;
@@ -18029,7 +18087,8 @@ function createCleanCommand(dependencies = {}) {
18029
18087
  runGit: runGit2
18030
18088
  } = runtime.git;
18031
18089
  const { loadEffectiveConfig: loadEffectiveConfig2 } = runtime.config;
18032
- const { releaseWorktreeSlot: releaseWorktreeSlot2 } = runtime.slots;
18090
+ const { getWorktreeSlot: getWorktreeSlot2, releaseWorktreeSlot: releaseWorktreeSlot2 } = runtime.slots;
18091
+ const { extractHooks: extractHooks2, runHook: runHook2 } = runtime.hooks;
18033
18092
  const sourceDependencies = {
18034
18093
  ...runtime.repositoryContext,
18035
18094
  ...runtime.repositoryRegistry,
@@ -18170,9 +18229,10 @@ function createCleanCommand(dependencies = {}) {
18170
18229
  if (options.json) {
18171
18230
  emitError(options, message);
18172
18231
  } else {
18232
+ options.stderr(`gji clean: ${message} in non-interactive mode
18233
+ `);
18173
18234
  options.stderr(
18174
- `gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)
18175
- `
18235
+ "Hint: --force removes every candidate; use --dry-run to preview them first\n"
18176
18236
  );
18177
18237
  }
18178
18238
  return 1;
@@ -18277,6 +18337,19 @@ function createCleanCommand(dependencies = {}) {
18277
18337
  );
18278
18338
  continue;
18279
18339
  }
18340
+ const hooks = extractHooks2(await loadEffectiveConfig2(repoRoot));
18341
+ await runHook2(
18342
+ hooks["before-remove"],
18343
+ worktree.path,
18344
+ {
18345
+ branch: worktree.branch ?? void 0,
18346
+ path: worktree.path,
18347
+ repo: basename8(repoRoot),
18348
+ slot: await getWorktreeSlot2(worktree.path)
18349
+ },
18350
+ options.stderr,
18351
+ options.json ? () => void 0 : options.stdout
18352
+ );
18280
18353
  try {
18281
18354
  await removeWorktree2(repoRoot, worktree.path);
18282
18355
  } catch (error) {
@@ -18399,7 +18472,7 @@ async function canonicalizePathPatterns(cwd, patterns) {
18399
18472
  }
18400
18473
  async function canonicalizeLiteralPrefix(pattern) {
18401
18474
  const globIndex = findGlobIndex(pattern);
18402
- let prefix = globIndex === -1 ? pattern : dirname10(`${pattern.slice(0, globIndex)}x`);
18475
+ let prefix = globIndex === -1 ? pattern : dirname7(`${pattern.slice(0, globIndex)}x`);
18403
18476
  let suffix = pattern.slice(prefix.length);
18404
18477
  for (; ; ) {
18405
18478
  try {
@@ -18408,7 +18481,7 @@ async function canonicalizeLiteralPrefix(pattern) {
18408
18481
  );
18409
18482
  return `${canonicalPrefix}${suffix}`.replace(/^\/\//, "/");
18410
18483
  } catch {
18411
- const parent = dirname10(prefix);
18484
+ const parent = dirname7(prefix);
18412
18485
  if (parent === prefix) return pattern;
18413
18486
  suffix = join12("/", basename8(prefix), suffix);
18414
18487
  prefix = parent;
@@ -18727,23 +18800,20 @@ var TOP_LEVEL_COMMANDS = [
18727
18800
  },
18728
18801
  { name: "ls", description: "list active worktrees" },
18729
18802
  { name: "clean", description: "interactively prune linked worktrees" },
18730
- {
18731
- name: "remove",
18732
- description: "deprecated: use done for one worktree or clean for bulk cleanup"
18733
- },
18734
- { name: "rm", description: "deprecated alias of remove" },
18735
18803
  {
18736
18804
  name: "run-hook",
18737
18805
  description: "run a named hook in the current worktree"
18738
18806
  },
18739
- {
18740
- name: "warp",
18741
- description: "deprecated: use go (press Tab for all known repos)"
18742
- },
18743
18807
  { name: "config", description: "manage global config defaults" }
18744
18808
  ];
18745
18809
  var SHELL_NAMES = ["bash", "fish", "zsh"];
18746
- var HOOK_NAMES = ["after-create", "after-enter", "before-remove"];
18810
+ var HOOK_NAMES = [
18811
+ "after-branch-create",
18812
+ "after-create",
18813
+ "after-enter",
18814
+ "after-switch",
18815
+ "before-remove"
18816
+ ];
18747
18817
  var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
18748
18818
  function renderShellCompletion(shell) {
18749
18819
  switch (shell) {
@@ -18922,15 +18992,9 @@ _gji_completion() {
18922
18992
  COMPREPLY=( $(compgen -W "-f --force --stale --path --dry-run --json --help" -- "$cur") )
18923
18993
  fi
18924
18994
  ;;
18925
- remove|rm)
18926
- COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
18927
- ;;
18928
18995
  run-hook)
18929
18996
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
18930
18997
  ;;
18931
- warp)
18932
- COMPREPLY=( $(compgen -W "--print --json --help" -- "$cur") )
18933
- ;;
18934
18998
  config)
18935
18999
  if [ "$COMP_CWORD" -eq 2 ]; then
18936
19000
  COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
@@ -19167,18 +19231,10 @@ complete -c gji -n '__fish_seen_subcommand_from clean' -l path -r -F -d 'only ta
19167
19231
  complete -c gji -n '__fish_seen_subcommand_from clean' -l dry-run -d 'show what would be deleted without removing anything'
19168
19232
  complete -c gji -n '__fish_seen_subcommand_from clean' -l json -d 'emit JSON on success or error instead of human-readable output'
19169
19233
 
19170
- complete -c gji -n '__fish_seen_subcommand_from remove rm' -s f -l force -d 'bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch'
19171
- complete -c gji -n '__fish_seen_subcommand_from remove rm' -l dry-run -d 'show what would be deleted without removing anything'
19172
- complete -c gji -n '__fish_seen_subcommand_from remove rm' -l json -d 'emit JSON on success or error instead of human-readable output'
19173
- complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_branches)' -d 'worktree branch'
19174
-
19175
19234
  ${hookLines}
19176
19235
 
19177
19236
  complete -c gji -n '__fish_use_subcommand' -l json -d 'emit the repository hub as JSON'
19178
19237
 
19179
- complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
19180
- complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
19181
-
19182
19238
  complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
19183
19239
  ${configKeyLines}`;
19184
19240
  }
@@ -19326,15 +19382,9 @@ case "\${words[2]}" in
19326
19382
  clean)
19327
19383
  _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]'
19328
19384
  ;;
19329
- remove|rm)
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'
19331
- ;;
19332
19385
  run-hook)
19333
19386
  _arguments "2:hook:(${hooks})"
19334
19387
  ;;
19335
- warp)
19336
- _arguments '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
19337
- ;;
19338
19388
  config)
19339
19389
  if (( CURRENT == 3 )); then
19340
19390
  _values 'config action' get set unset
@@ -19462,7 +19512,7 @@ import { execFile as execFile8 } from "node:child_process";
19462
19512
  import { constants as constants6 } from "node:fs";
19463
19513
  import { access as access6, readFile as readFile8 } from "node:fs/promises";
19464
19514
  import { homedir as homedir7 } from "node:os";
19465
- import { basename as basename9, dirname as dirname11, join as join14 } from "node:path";
19515
+ import { basename as basename9, dirname as dirname8, join as join14 } from "node:path";
19466
19516
  import { promisify as promisify9 } from "node:util";
19467
19517
 
19468
19518
  // src/presentation/shell/setup.ts
@@ -19911,7 +19961,7 @@ function resolveWorktreeBase(repoRoot, config, home) {
19911
19961
  if (typeof configuredPath === "string" && (configuredPath.startsWith("/") || configuredPath.startsWith("~"))) {
19912
19962
  return expandTilde2(configuredPath, home);
19913
19963
  }
19914
- return join14(dirname11(repoRoot), "worktrees", basename9(repoRoot));
19964
+ return join14(dirname8(repoRoot), "worktrees", basename9(repoRoot));
19915
19965
  }
19916
19966
  async function findNearestExistingPath(path9) {
19917
19967
  let candidate = path9;
@@ -19920,7 +19970,7 @@ async function findNearestExistingPath(path9) {
19920
19970
  await access6(candidate, constants6.F_OK);
19921
19971
  return candidate;
19922
19972
  } catch {
19923
- const parent = dirname11(candidate);
19973
+ const parent = dirname8(candidate);
19924
19974
  if (parent === candidate) return null;
19925
19975
  candidate = parent;
19926
19976
  }
@@ -20083,7 +20133,7 @@ async function runDoneCommand(options) {
20083
20133
  } = runtime.git;
20084
20134
  const { loadEffectiveConfig: loadEffectiveConfig2, resolveConfigString: resolveConfigString2 } = runtime.config;
20085
20135
  const { loadHistory: loadHistory2 } = runtime.historyStore;
20086
- const { releaseWorktreeSlot: releaseWorktreeSlot2 } = runtime.slots;
20136
+ const { getWorktreeSlot: getWorktreeSlot2, releaseWorktreeSlot: releaseWorktreeSlot2 } = runtime.slots;
20087
20137
  const { extractHooks: extractHooks2, runHook: runHook2 } = runtime.hooks;
20088
20138
  const { detectRepository: detectRepository2 } = runtime.repositoryContext;
20089
20139
  const { listWorktrees: listWorktrees2 } = runtime.worktrees;
@@ -20179,9 +20229,11 @@ async function runDoneCommand(options) {
20179
20229
  {
20180
20230
  branch: target.branch ?? void 0,
20181
20231
  path: target.path,
20182
- repo: basename10(repository.repoRoot)
20232
+ repo: basename10(repository.repoRoot),
20233
+ slot: await getWorktreeSlot2(target.path)
20183
20234
  },
20184
- options.stderr
20235
+ options.stderr,
20236
+ options.json ? () => void 0 : options.stdout
20185
20237
  );
20186
20238
  try {
20187
20239
  await removeWorktree2(repository.repoRoot, target.path);
@@ -20370,7 +20422,7 @@ function toMessage3(error) {
20370
20422
  }
20371
20423
 
20372
20424
  // src/cli/commands/go.ts
20373
- import { basename as basename11 } from "node:path";
20425
+ import { basename as basename12 } from "node:path";
20374
20426
 
20375
20427
  // src/application/worktree/context-card.ts
20376
20428
  async function loadContextCardModel(worktreePath, dependencies) {
@@ -20696,12 +20748,12 @@ function formatState(info) {
20696
20748
  return `${info.status}${info.upstream.kind === "tracked" ? ` \xB7 \u2191${info.upstream.ahead} \u2193${info.upstream.behind}` : ""}`;
20697
20749
  }
20698
20750
  function formatLastCommit2(info) {
20699
- return new Date(info.lastCommitTimestamp).toISOString();
20751
+ return new Date(info.lastCommitTimestamp * 1e3).toISOString();
20700
20752
  }
20701
20753
 
20702
20754
  // src/cli/commands/new.ts
20703
- import { access as access8, mkdir as mkdir7 } from "node:fs/promises";
20704
- import { dirname as dirname12, resolve as resolve12 } from "node:path";
20755
+ import { access as access8, mkdir as mkdir5, rmdir as rmdir2 } from "node:fs/promises";
20756
+ import { basename as basename11, dirname as dirname9, resolve as resolve12 } from "node:path";
20705
20757
 
20706
20758
  // src/presentation/bootstrap/output.ts
20707
20759
  function createBootstrapReporter(write, json) {
@@ -20816,10 +20868,8 @@ function createNewCommand(dependencies = {}) {
20816
20868
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
20817
20869
  `);
20818
20870
  } else {
20819
- options.stderr(
20820
- `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
20821
- `
20822
- );
20871
+ options.stderr(`gji new: ${message} in non-interactive mode
20872
+ `);
20823
20873
  }
20824
20874
  return 1;
20825
20875
  }
@@ -20870,13 +20920,14 @@ function createNewCommand(dependencies = {}) {
20870
20920
  if (options.force) {
20871
20921
  if (!options.dryRun) {
20872
20922
  try {
20873
- await runtime.git.runGit(repository.repoRoot, [
20923
+ await runtime.git.runGitRetryingContention(repository.repoRoot, [
20874
20924
  "worktree",
20875
20925
  "remove",
20876
20926
  "--force",
20877
20927
  worktreePath
20878
20928
  ]);
20879
20929
  } catch (err) {
20930
+ await rmdir2(worktreePath).catch(() => void 0);
20880
20931
  if (!isNotRegisteredWorktreeError(err)) {
20881
20932
  const msg = `could not remove existing worktree at ${worktreePath}: ${toExecMessage(err)}`;
20882
20933
  if (options.json) {
@@ -20907,10 +20958,8 @@ function createNewCommand(dependencies = {}) {
20907
20958
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
20908
20959
  `);
20909
20960
  } else {
20910
- options.stderr(
20911
- `gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)
20912
- `
20913
- );
20961
+ options.stderr(`gji new: ${message} in non-interactive mode
20962
+ `);
20914
20963
  options.stderr(
20915
20964
  `Hint: Use 'gji done ${worktreeName}' or 'gji clean' to remove the existing worktree
20916
20965
  `
@@ -21011,7 +21060,7 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21011
21060
  }
21012
21061
  return 0;
21013
21062
  }
21014
- await mkdir7(dirname12(worktreePath), { recursive: true });
21063
+ await mkdir5(dirname9(worktreePath), { recursive: true });
21015
21064
  const startPoint = options.fromCurrent && !options.detached ? await resolveCurrentWorktreeHead(options.cwd, runtime.git.runGit) : void 0;
21016
21065
  const takeStartPoint = options.take ? await resolveCurrentWorktreeHead(options.cwd, runtime.git.runGit) : void 0;
21017
21066
  const changedTakeFiles = options.take ? await listTakeFiles(options.cwd, runtime.git.runGitRaw) : [];
@@ -21039,7 +21088,8 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21039
21088
  worktreeName,
21040
21089
  runtime
21041
21090
  );
21042
- const shouldRefreshBase = !options.noFetch && !options.detached && !options.fromCurrent && !options.take && options.mode !== "track" && options.mode !== "checkout" && !existingLocalBranch;
21091
+ const createsBranch = !options.detached && options.mode !== "track" && options.mode !== "checkout" && !existingLocalBranch;
21092
+ const shouldRefreshBase = createsBranch && !options.noFetch && !options.fromCurrent && !options.take;
21043
21093
  const freshBaseRef = shouldRefreshBase ? await resolveFreshBaseRef(
21044
21094
  repository.repoRoot,
21045
21095
  config,
@@ -21072,30 +21122,19 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21072
21122
  );
21073
21123
  }
21074
21124
  }
21075
- const gitArgs = options.detached ? [
21076
- "worktree",
21077
- "add",
21078
- "--detach",
21079
- worktreePath,
21080
- ...takeStartPoint ? [takeStartPoint] : []
21081
- ] : options.mode === "track" ? [
21082
- "worktree",
21083
- "add",
21084
- "--track",
21085
- "-b",
21086
- worktreeName,
21087
- worktreePath,
21088
- `${options.remote ?? "origin"}/${worktreeName}`
21089
- ] : options.mode === "checkout" || existingLocalBranch ? ["worktree", "add", worktreePath, worktreeName] : [
21090
- "worktree",
21091
- "add",
21092
- "-b",
21093
- worktreeName,
21094
- worktreePath,
21095
- ...takeStartPoint ? [takeStartPoint] : startPoint ? [startPoint] : freshBaseRef ? [freshBaseRef] : []
21096
- ];
21125
+ const addsNewBranch = createsBranch || !options.detached && options.mode === "track";
21097
21126
  try {
21098
- await runtime.git.runGit(repository.repoRoot, gitArgs);
21127
+ await runtime.git.addWorktree(
21128
+ repository.repoRoot,
21129
+ worktreePath,
21130
+ options.detached ? { detachedAt: takeStartPoint } : addsNewBranch ? {
21131
+ branch: worktreeName,
21132
+ create: {
21133
+ startPoint: options.mode === "track" ? `${options.remote ?? "origin"}/${worktreeName}` : takeStartPoint ?? startPoint ?? freshBaseRef,
21134
+ track: options.mode === "track"
21135
+ }
21136
+ } : { branch: worktreeName }
21137
+ );
21099
21138
  } catch (error) {
21100
21139
  if (stashSha)
21101
21140
  await restoreTakeStash(
@@ -21117,15 +21156,11 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21117
21156
  options.stderr,
21118
21157
  runtime.git.runGit
21119
21158
  )) {
21120
- try {
21121
- await runtime.git.runGit(repository.repoRoot, [
21122
- "worktree",
21123
- "remove",
21124
- "--force",
21125
- worktreePath
21126
- ]);
21127
- } catch {
21128
- }
21159
+ await runtime.git.removeCreatedWorktree(
21160
+ repository.repoRoot,
21161
+ worktreePath,
21162
+ addsNewBranch ? worktreeName : void 0
21163
+ );
21129
21164
  return emitNewError(
21130
21165
  options,
21131
21166
  `could not apply taken changes; your changes are safe in stash: ${stashSha}`
@@ -21161,6 +21196,17 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21161
21196
  syncFiles: bootstrap.syncFileFailures
21162
21197
  });
21163
21198
  }
21199
+ if (createsBranch) {
21200
+ await runNewHooks({
21201
+ branch: worktreeName,
21202
+ config,
21203
+ hookNames: ["after-branch-create"],
21204
+ options,
21205
+ path: worktreePath,
21206
+ repository,
21207
+ runtime
21208
+ });
21209
+ }
21164
21210
  if (options.json) {
21165
21211
  const navigation = createNavigationTarget(
21166
21212
  createNavigationRepository(repository.repoName, repository.repoRoot),
@@ -21293,7 +21339,7 @@ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
21293
21339
  try {
21294
21340
  let startPoint = baseBranch;
21295
21341
  if (!options.noFetch) {
21296
- await runtime.git.runGit(repository.repoRoot, [
21342
+ await runtime.git.runGitRetryingContention(repository.repoRoot, [
21297
21343
  "fetch",
21298
21344
  "--prune",
21299
21345
  remote,
@@ -21301,19 +21347,31 @@ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
21301
21347
  ]);
21302
21348
  startPoint = `${remote}/${baseBranch}`;
21303
21349
  }
21304
- await runtime.git.runGit(options.cwd, [
21305
- "switch",
21306
- "-c",
21307
- branchName,
21350
+ await runtime.git.createBranch(repository.repoRoot, branchName, {
21308
21351
  startPoint
21309
- ]);
21352
+ });
21353
+ try {
21354
+ await runtime.git.runGit(options.cwd, ["switch", branchName]);
21355
+ } catch (error) {
21356
+ await runtime.git.deleteCreatedBranch(repository.repoRoot, branchName);
21357
+ throw error;
21358
+ }
21310
21359
  } catch (error) {
21311
21360
  return emitNewError(
21312
21361
  options,
21313
21362
  `failed to create branch in the current worktree: ${toExecMessage(error)}`
21314
21363
  );
21315
21364
  }
21316
- if (options.json) {
21365
+ await runNewHooks({
21366
+ branch: branchName,
21367
+ config,
21368
+ hookNames: ["after-branch-create", "after-switch"],
21369
+ options,
21370
+ path: repository.currentRoot,
21371
+ repository,
21372
+ runtime
21373
+ });
21374
+ if (options.json) {
21317
21375
  const navigation = createNavigationTarget(
21318
21376
  createNavigationRepository(repository.repoName, repository.repoRoot),
21319
21377
  repository.currentRoot,
@@ -21340,6 +21398,32 @@ async function runBranchOnly(options, runtime, repository, config, rawBranch) {
21340
21398
  }
21341
21399
  return 0;
21342
21400
  }
21401
+ async function runNewHooks({
21402
+ branch,
21403
+ config,
21404
+ hookNames,
21405
+ options,
21406
+ path: path9,
21407
+ repository,
21408
+ runtime
21409
+ }) {
21410
+ const hooks = runtime.hooks.extractHooks(config);
21411
+ const context = {
21412
+ branch,
21413
+ path: path9,
21414
+ repo: basename11(repository.repoRoot),
21415
+ slot: await runtime.slots.getWorktreeSlot(path9)
21416
+ };
21417
+ for (const hookName of hookNames) {
21418
+ await runtime.hooks.runHook(
21419
+ hooks[hookName],
21420
+ path9,
21421
+ context,
21422
+ options.json ? () => void 0 : options.stderr,
21423
+ options.json ? () => void 0 : options.stdout
21424
+ );
21425
+ }
21426
+ }
21343
21427
  var runNewCommand = createNewCommand();
21344
21428
  async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFailure, runtime) {
21345
21429
  const remote = runtime.config.resolveConfigString(config, "syncRemote") ?? "origin";
@@ -21362,7 +21446,7 @@ async function resolveFreshBaseRef(repoRoot, config, options, promptForFetchFail
21362
21446
  throw new Error("the remote default branch is unknown");
21363
21447
  }
21364
21448
  baseBranch = remoteBase.branch;
21365
- await runtime.git.runGit(repoRoot, [
21449
+ await runtime.git.runGitRetryingContention(repoRoot, [
21366
21450
  "fetch",
21367
21451
  "--prune",
21368
21452
  remote,
@@ -21754,8 +21838,8 @@ function emitNewError(options, message, details) {
21754
21838
  }
21755
21839
 
21756
21840
  // src/cli/commands/pr.ts
21757
- import { mkdir as mkdir8 } from "node:fs/promises";
21758
- import { dirname as dirname13 } from "node:path";
21841
+ import { mkdir as mkdir6 } from "node:fs/promises";
21842
+ import { dirname as dirname10 } from "node:path";
21759
21843
  var PR_OUTPUT_FILE_ENV = "GJI_PR_OUTPUT_FILE";
21760
21844
  function createPrCommand(dependencies = {}) {
21761
21845
  const prompt = dependencies.promptForPathConflict ?? promptForPathConflict;
@@ -21813,10 +21897,8 @@ function createPrCommand(dependencies = {}) {
21813
21897
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
21814
21898
  `);
21815
21899
  } else {
21816
- options.stderr(
21817
- `gji pr: ${message} in non-interactive mode (GJI_NO_TUI=1)
21818
- `
21819
- );
21900
+ options.stderr(`gji pr: ${message} in non-interactive mode
21901
+ `);
21820
21902
  options.stderr(
21821
21903
  `Hint: Use 'gji done pr/${prNumber}' or 'gji clean' to remove the existing worktree
21822
21904
  `
@@ -21894,14 +21976,29 @@ ${formatDependencyBootstrapPreview(dryRunDependencyBootstrap)}`
21894
21976
  }
21895
21977
  return 1;
21896
21978
  }
21897
- await mkdir8(dirname13(worktreePath), { recursive: true });
21979
+ await mkdir6(dirname10(worktreePath), { recursive: true });
21898
21980
  const branchAlreadyExists = await localBranchExists2(
21899
21981
  repository.repoRoot,
21900
21982
  branchName,
21901
21983
  runtime
21902
21984
  );
21903
- const worktreeArgs = branchAlreadyExists ? ["worktree", "add", worktreePath, branchName] : ["worktree", "add", "-b", branchName, worktreePath, remoteRef];
21904
- await runtime.git.runGit(repository.repoRoot, worktreeArgs);
21985
+ try {
21986
+ await runtime.git.addWorktree(
21987
+ repository.repoRoot,
21988
+ worktreePath,
21989
+ branchAlreadyExists ? { branch: branchName } : { branch: branchName, create: { startPoint: remoteRef } }
21990
+ );
21991
+ } catch (error) {
21992
+ const message = `failed to create worktree: ${error instanceof Error ? error.message : String(error)}`;
21993
+ if (options.json) {
21994
+ options.stderr(`${JSON.stringify({ error: message }, null, 2)}
21995
+ `);
21996
+ } else {
21997
+ options.stderr(`gji pr: ${message}
21998
+ `);
21999
+ }
22000
+ return 1;
22001
+ }
21905
22002
  const bootstrap = await runtime.bootstrap.bootstrapWorktree({
21906
22003
  branch: branchName,
21907
22004
  config,
@@ -21974,7 +22071,7 @@ var runPrCommand = createPrCommand();
21974
22071
  async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef, runtime) {
21975
22072
  for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
21976
22073
  try {
21977
- await runtime.git.runGit(repoRoot, [
22074
+ await runtime.git.runGitRetryingContention(repoRoot, [
21978
22075
  "fetch",
21979
22076
  "origin",
21980
22077
  `${sourceRef}:${remoteRef}`
@@ -22062,7 +22159,7 @@ function createGoCommand(dependencies = {}) {
22062
22159
  if (options.json || isHeadless()) {
22063
22160
  return emitError2(
22064
22161
  options,
22065
- "branch argument is required in non-interactive mode (GJI_NO_TUI=1)"
22162
+ "branch argument is required in non-interactive mode"
22066
22163
  );
22067
22164
  }
22068
22165
  let skippedRegisteredRepos = 0;
@@ -22206,10 +22303,10 @@ async function handleGoBranchResolution(options, resolution, confirmBranchCreati
22206
22303
  runtime
22207
22304
  );
22208
22305
  case "pull-request":
22209
- if (options.json || isHeadless() || options.print) {
22306
+ if (options.json || options.print) {
22210
22307
  return emitError2(
22211
22308
  options,
22212
- "PR navigation creates a worktree; use `gji pr <ref>` in an interactive shell"
22309
+ "PR navigation creates a worktree; use `gji pr <ref>`"
22213
22310
  );
22214
22311
  }
22215
22312
  return runPrCommand({
@@ -22254,9 +22351,16 @@ function deduplicateSources(sources) {
22254
22351
  }
22255
22352
  async function createExistingBranchWorktree(options, confirmBranchCreation, repository, branch, mode, remote, runtime) {
22256
22353
  if (options.json || isHeadless() || options.print) {
22354
+ const command = await resolveNonInteractiveCreateCommand(
22355
+ repository,
22356
+ branch,
22357
+ mode,
22358
+ remote,
22359
+ runtime ?? defaultCliDependencies
22360
+ );
22257
22361
  return emitError2(
22258
22362
  options,
22259
- `branch "${branch}" exists but has no worktree; use interactive gji go ${branch} to create one`
22363
+ `branch "${branch}" exists but has no worktree; run ${command} to create one`
22260
22364
  );
22261
22365
  }
22262
22366
  if (!await confirmBranchCreation(branch)) {
@@ -22274,6 +22378,13 @@ async function createExistingBranchWorktree(options, confirmBranchCreation, repo
22274
22378
  runtime
22275
22379
  });
22276
22380
  }
22381
+ async function resolveNonInteractiveCreateCommand(repository, branch, mode, remote, runtime) {
22382
+ if (mode === "track") {
22383
+ return `git worktree add --track -b ${branch} <path> ${remote ?? "origin"}/${branch}`;
22384
+ }
22385
+ const config = await runtime.config.loadEffectiveConfig(repository.repoRoot).catch(() => null);
22386
+ return applyConfiguredBranchPrefix(branch, config?.branchPrefix) === branch ? `gji new ${branch}` : `git worktree add <path> ${branch}`;
22387
+ }
22277
22388
  async function navigateToExistingWorktree(options, path9, worktree, runtime) {
22278
22389
  const repository = await runtime.repositoryContext.detectRepository(path9);
22279
22390
  if (options.json) {
@@ -22303,7 +22414,7 @@ async function navigateToExistingWorktree(options, path9, worktree, runtime) {
22303
22414
  {
22304
22415
  branch: worktree?.branch ?? void 0,
22305
22416
  path: path9,
22306
- repo: basename11(repository.repoRoot),
22417
+ repo: basename12(repository.repoRoot),
22307
22418
  slot: await runtime.slots.getWorktreeSlot(path9)
22308
22419
  },
22309
22420
  options.stderr
@@ -22443,7 +22554,7 @@ async function runHistoryCommand(options) {
22443
22554
  }
22444
22555
 
22445
22556
  // src/cli/commands/hub.ts
22446
- import { basename as basename12 } from "node:path";
22557
+ import { basename as basename13 } from "node:path";
22447
22558
  import { stdin as stdin2, stdout as stdout2 } from "node:process";
22448
22559
  var MAX_HUB_REPOSITORY_CONCURRENCY = 4;
22449
22560
  async function buildHubData(cwd, dependencies = {}, runtime = defaultCliDependencies) {
@@ -22468,7 +22579,7 @@ async function buildHubData(cwd, dependencies = {}, runtime = defaultCliDependen
22468
22579
  existing.sources.push(source);
22469
22580
  } else {
22470
22581
  groups.set(repoRoot, {
22471
- name: source.repoName || basename12(repoRoot),
22582
+ name: source.repoName || basename13(repoRoot),
22472
22583
  sources: [source]
22473
22584
  });
22474
22585
  }
@@ -22717,9 +22828,9 @@ function formatHumanText(value, maxLength) {
22717
22828
 
22718
22829
  // src/cli/commands/init.ts
22719
22830
  import { constants as constants8 } from "node:fs";
22720
- import { access as access9, mkdir as mkdir9, readFile as readFile9, writeFile as writeFile8 } from "node:fs/promises";
22831
+ import { access as access9, mkdir as mkdir7, readFile as readFile9, writeFile as writeFile5 } from "node:fs/promises";
22721
22832
  import { homedir as homedir8 } from "node:os";
22722
- import { dirname as dirname14 } from "node:path";
22833
+ import { dirname as dirname11 } from "node:path";
22723
22834
  var ZSH_COMPLETION_PATH_LINE = "fpath=(~/.zsh/completions $fpath)";
22724
22835
  var SHELL_WRAPPED_COMMANDS = [
22725
22836
  {
@@ -22757,26 +22868,12 @@ var SHELL_WRAPPED_COMMANDS = [
22757
22868
  names: ["root"],
22758
22869
  tempPrefix: "gji-root"
22759
22870
  },
22760
- {
22761
- bypassOptions: ["--help", "-h", "--json"],
22762
- commandName: "remove",
22763
- envVar: "GJI_REMOVE_OUTPUT_FILE",
22764
- names: ["remove", "rm"],
22765
- tempPrefix: "gji-remove"
22766
- },
22767
22871
  {
22768
22872
  bypassOptions: ["--help", "-h", "--json"],
22769
22873
  commandName: "done",
22770
22874
  envVar: "GJI_DONE_OUTPUT_FILE",
22771
22875
  names: ["done"],
22772
22876
  tempPrefix: "gji-done"
22773
- },
22774
- {
22775
- bypassOptions: ["--print", "--json", "--help", "-h"],
22776
- commandName: "warp",
22777
- envVar: "GJI_WARP_OUTPUT_FILE",
22778
- names: ["warp"],
22779
- tempPrefix: "gji-warp"
22780
22877
  }
22781
22878
  ];
22782
22879
  async function runInitCommand(options) {
@@ -22844,8 +22941,8 @@ async function runDefaultOnboarding(context, runtime) {
22844
22941
  const installCompletion = await promptForCompletion(shell, completionPath);
22845
22942
  if (installCompletion === null) return abortDefaultOnboarding();
22846
22943
  if (installCompletion) {
22847
- await mkdir9(dirname14(completionPath), { recursive: true });
22848
- await writeFile8(completionPath, renderShellCompletion(shell), "utf8");
22944
+ await mkdir7(dirname11(completionPath), { recursive: true });
22945
+ await writeFile5(completionPath, renderShellCompletion(shell), "utf8");
22849
22946
  }
22850
22947
  if (shell === "zsh" && (completionAlreadyInstalled || installCompletion)) {
22851
22948
  await ensureZshCompletionPath(rcPath);
@@ -22950,8 +23047,8 @@ async function applyOnboardingResult(result, home, runtime) {
22950
23047
  runtime
22951
23048
  );
22952
23049
  if (result.installCompletion) {
22953
- await mkdir9(dirname14(completionPath), { recursive: true });
22954
- await writeFile8(
23050
+ await mkdir7(dirname11(completionPath), { recursive: true });
23051
+ await writeFile5(
22955
23052
  completionPath,
22956
23053
  renderShellCompletion(result.shell),
22957
23054
  "utf8"
@@ -22970,17 +23067,17 @@ async function applyOnboardingResult(result, home, runtime) {
22970
23067
  }
22971
23068
  async function installOnboardingShellIntegration(shell, home) {
22972
23069
  const rcPath = resolveShellConfigPath(shell, home);
22973
- await mkdir9(dirname14(rcPath), { recursive: true });
23070
+ await mkdir7(dirname11(rcPath), { recursive: true });
22974
23071
  const current = await readExistingConfig(rcPath);
22975
23072
  if (shell === "fish") {
22976
- await writeFile8(
23073
+ await writeFile5(
22977
23074
  rcPath,
22978
23075
  upsertShellIntegration(current, renderShellIntegration(shell)),
22979
23076
  "utf8"
22980
23077
  );
22981
23078
  return;
22982
23079
  }
22983
- await writeFile8(
23080
+ await writeFile5(
22984
23081
  rcPath,
22985
23082
  upsertOnboardingShellIntegration(current, shell),
22986
23083
  "utf8"
@@ -23014,7 +23111,7 @@ async function ensureZshCompletionPath(rcPath) {
23014
23111
  const current = await readExistingConfig(rcPath);
23015
23112
  const next = upsertZshCompletionPath(current);
23016
23113
  if (next !== current) {
23017
- await writeFile8(rcPath, next, "utf8");
23114
+ await writeFile5(rcPath, next, "utf8");
23018
23115
  }
23019
23116
  }
23020
23117
  function upsertZshCompletionPath(existingConfig) {
@@ -23058,10 +23155,10 @@ async function runLegacyInitCommand(options, runtime) {
23058
23155
  return 0;
23059
23156
  }
23060
23157
  const rcPath = resolveShellConfigPath(shell, home);
23061
- await mkdir9(dirname14(rcPath), { recursive: true });
23158
+ await mkdir7(dirname11(rcPath), { recursive: true });
23062
23159
  const current = await readExistingConfig(rcPath);
23063
23160
  const next = upsertShellIntegration(current, script);
23064
- await writeFile8(rcPath, next, "utf8");
23161
+ await writeFile5(rcPath, next, "utf8");
23065
23162
  options.stdout(`${rcPath}
23066
23163
  `);
23067
23164
  const { config: globalConfig } = await runtime.configStore.loadGlobalConfig(home);
@@ -23415,7 +23512,7 @@ function sortWorktrees(worktrees) {
23415
23512
 
23416
23513
  // src/cli/commands/open.ts
23417
23514
  import { execFile as execFile9 } from "node:child_process";
23418
- import { access as access10, writeFile as writeFile9 } from "node:fs/promises";
23515
+ import { access as access10, writeFile as writeFile6 } from "node:fs/promises";
23419
23516
  import { join as join15 } from "node:path";
23420
23517
  import { promisify as promisify10 } from "node:util";
23421
23518
  var execFileAsync9 = promisify10(execFile9);
@@ -23438,7 +23535,7 @@ function createOpenCommand(dependencies = {}) {
23438
23535
  }
23439
23536
  if (options.select && isHeadless()) {
23440
23537
  options.stderr(
23441
- "gji open --select: selector is unavailable in non-interactive mode (GJI_NO_TUI=1)\n"
23538
+ "gji open --select: selector is unavailable in non-interactive mode; pass a branch instead\n"
23442
23539
  );
23443
23540
  return 1;
23444
23541
  }
@@ -23602,7 +23699,7 @@ async function ensureWorkspaceFile(worktreePath, repoName) {
23602
23699
  } catch {
23603
23700
  }
23604
23701
  const workspace = { folders: [{ path: "." }], settings: {} };
23605
- await writeFile9(
23702
+ await writeFile6(
23606
23703
  workspacePath,
23607
23704
  `${JSON.stringify(workspace, null, 2)}
23608
23705
  `,
@@ -23630,7 +23727,7 @@ function createPrOpenCommand(dependencies = {}) {
23630
23727
  }
23631
23728
  if (options.select && isHeadless()) {
23632
23729
  options.stderr(
23633
- "gji pr open --select: selector is unavailable in non-interactive mode (GJI_NO_TUI=1)\n"
23730
+ "gji pr open --select: selector is unavailable in non-interactive mode; pass a branch or PR number instead\n"
23634
23731
  );
23635
23732
  return 1;
23636
23733
  }
@@ -23930,7 +24027,7 @@ async function openPullRequests(pullRequests, options, promptForPullRequest, ope
23930
24027
  }
23931
24028
  if (pullRequests.length > 1 && isHeadless()) {
23932
24029
  options.stderr(
23933
- "gji pr open: multiple open PRs found; pass an explicit PR number in non-interactive mode (GJI_NO_TUI=1)\n"
24030
+ "gji pr open: multiple open PRs found; pass an explicit PR number in non-interactive mode\n"
23934
24031
  );
23935
24032
  return 1;
23936
24033
  }
@@ -23981,250 +24078,6 @@ function formatError(error) {
23981
24078
  return error instanceof Error ? error.message : String(error);
23982
24079
  }
23983
24080
 
23984
- // src/cli/commands/remove.ts
23985
- import { basename as basename13 } from "node:path";
23986
- var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
23987
- function createRemoveCommand(dependencies = {}) {
23988
- const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree3;
23989
- const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval2;
23990
- const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
23991
- const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
23992
- return async function runRemoveCommand2(options) {
23993
- const runtime = options.runtime ?? defaultCliDependencies;
23994
- const { loadEffectiveConfig: loadEffectiveConfig2 } = runtime.config;
23995
- const { releaseWorktreeSlot: releaseWorktreeSlot2 } = runtime.slots;
23996
- const { extractHooks: extractHooks2, runHook: runHook2 } = runtime.hooks;
23997
- const sourceDependencies = {
23998
- ...runtime.repositoryContext,
23999
- ...runtime.repositoryRegistry,
24000
- ...runtime.worktrees
24001
- };
24002
- const {
24003
- deleteBranch: deleteBranch2,
24004
- forceDeleteBranch: forceDeleteBranch2,
24005
- forceRemoveWorktree: forceRemoveWorktree2,
24006
- isBranchUnmergedError: isBranchUnmergedError2,
24007
- isWorktreeForceRemovalError: isWorktreeForceRemovalError2,
24008
- removeWorktree: removeWorktree2
24009
- } = runtime.worktreeLifecycle;
24010
- const { linkedWorktrees, repository } = await loadLinkedWorktrees(
24011
- options.cwd,
24012
- sourceDependencies
24013
- );
24014
- if (linkedWorktrees.length === 0) {
24015
- emitError3(options, "No linked worktrees to finish");
24016
- return 1;
24017
- }
24018
- if (!options.branch && (options.json || isHeadless())) {
24019
- const message = "branch argument is required";
24020
- if (options.json) {
24021
- emitError3(options, message);
24022
- } else {
24023
- options.stderr(
24024
- `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
24025
- `
24026
- );
24027
- }
24028
- return 1;
24029
- }
24030
- const selection = options.branch ?? await promptForWorktree2(
24031
- await buildWorktreePromptEntries(
24032
- linkedWorktrees.map((worktree2) => ({
24033
- repoName: repository.repoName,
24034
- worktree: worktree2
24035
- })),
24036
- { catalog: runtime.worktreeCatalog }
24037
- )
24038
- );
24039
- if (!selection) {
24040
- options.stderr("Aborted\n");
24041
- return 1;
24042
- }
24043
- const worktree = linkedWorktrees.find(
24044
- (entry) => entry.branch === selection || entry.path === selection
24045
- );
24046
- if (!worktree) {
24047
- emitError3(options, `No linked worktree found for branch: ${selection}`);
24048
- return 1;
24049
- }
24050
- if (!options.dryRun && !options.force && (options.json || isHeadless())) {
24051
- const message = "--force is required";
24052
- if (options.json) {
24053
- emitError3(options, message);
24054
- } else {
24055
- options.stderr(
24056
- `gji remove: ${message} in non-interactive mode (GJI_NO_TUI=1)
24057
- `
24058
- );
24059
- }
24060
- return 1;
24061
- }
24062
- if (!options.dryRun && !options.force && !await confirmRemoval(worktree)) {
24063
- options.stderr("Aborted\n");
24064
- return 1;
24065
- }
24066
- if (options.dryRun) {
24067
- if (options.json) {
24068
- options.stdout(
24069
- `${JSON.stringify({ branch: worktree.branch, path: worktree.path, dryRun: true }, null, 2)}
24070
- `
24071
- );
24072
- } else {
24073
- const desc = worktree.branch ? `branch: ${worktree.branch}` : "detached";
24074
- options.stdout(`Would remove worktree at ${worktree.path} (${desc})
24075
- `);
24076
- }
24077
- return 0;
24078
- }
24079
- const config = await loadEffectiveConfig2(
24080
- repository.repoRoot,
24081
- void 0,
24082
- options.stderr
24083
- );
24084
- const hooks = extractHooks2(config);
24085
- let journal;
24086
- try {
24087
- journal = await recordUndoOperation(
24088
- "remove",
24089
- repository.repoRoot,
24090
- [worktree],
24091
- void 0,
24092
- runtime
24093
- );
24094
- } catch (error) {
24095
- emitError3(
24096
- options,
24097
- `could not write undo journal; no worktree was removed: ${toMessage4(error)}`
24098
- );
24099
- return 1;
24100
- }
24101
- if (!journal) {
24102
- emitError3(
24103
- options,
24104
- "could not capture undo state; no worktree was removed"
24105
- );
24106
- return 1;
24107
- }
24108
- await runHook2(
24109
- hooks["before-remove"],
24110
- worktree.path,
24111
- {
24112
- branch: worktree.branch ?? void 0,
24113
- path: worktree.path,
24114
- repo: basename13(repository.repoRoot)
24115
- },
24116
- options.stderr
24117
- );
24118
- try {
24119
- await removeWorktree2(repository.repoRoot, worktree.path);
24120
- } catch (error) {
24121
- if (!isWorktreeForceRemovalError2(error)) {
24122
- await finalizeUndoOperation(
24123
- journal,
24124
- [],
24125
- void 0,
24126
- runtime.configStore.GLOBAL_CONFIG_DIRECTORY
24127
- );
24128
- throw error;
24129
- }
24130
- if (!options.force && !await confirmForceRemoveWorktree(worktree.path)) {
24131
- await finalizeUndoOperation(
24132
- journal,
24133
- [],
24134
- void 0,
24135
- runtime.configStore.GLOBAL_CONFIG_DIRECTORY
24136
- );
24137
- options.stderr("Aborted\n");
24138
- return 1;
24139
- }
24140
- try {
24141
- await forceRemoveWorktree2(repository.repoRoot, worktree.path);
24142
- } catch (forceError) {
24143
- await finalizeUndoOperation(
24144
- journal,
24145
- [],
24146
- void 0,
24147
- runtime.configStore.GLOBAL_CONFIG_DIRECTORY
24148
- );
24149
- emitError3(
24150
- options,
24151
- `Failed to remove worktree at ${worktree.path}: ${toMessage4(forceError)}`
24152
- );
24153
- return 1;
24154
- }
24155
- }
24156
- await finalizeUndoOperation(
24157
- journal,
24158
- [worktree],
24159
- void 0,
24160
- runtime.configStore.GLOBAL_CONFIG_DIRECTORY
24161
- );
24162
- await releaseWorktreeSlot2(worktree.path);
24163
- if (worktree.branch) {
24164
- try {
24165
- await deleteBranch2(repository.repoRoot, worktree.branch);
24166
- } catch (error) {
24167
- if (!isBranchUnmergedError2(error)) {
24168
- throw error;
24169
- }
24170
- if (options.force || await confirmForceDeleteBranch(worktree.branch)) {
24171
- try {
24172
- await forceDeleteBranch2(repository.repoRoot, worktree.branch);
24173
- } catch (forceError) {
24174
- options.stderr(
24175
- `Failed to delete branch ${worktree.branch}: ${toMessage4(forceError)}
24176
- `
24177
- );
24178
- }
24179
- } else {
24180
- options.stderr(
24181
- `Branch ${worktree.branch} was not deleted (has unmerged commits)
24182
- `
24183
- );
24184
- }
24185
- }
24186
- }
24187
- if (options.json) {
24188
- options.stdout(
24189
- `${JSON.stringify({ branch: worktree.branch, path: worktree.path, deleted: true }, null, 2)}
24190
- `
24191
- );
24192
- } else {
24193
- options.stderr("undo: gji undo\n");
24194
- await writeOutput3(repository.repoRoot, options.stdout);
24195
- }
24196
- return 0;
24197
- };
24198
- }
24199
- var runRemoveCommand = createRemoveCommand();
24200
- async function defaultPromptForWorktree3(worktrees) {
24201
- return promptForSingleWorktree("Choose a worktree to finish", worktrees);
24202
- }
24203
- async function defaultConfirmRemoval2(worktree) {
24204
- const choice = await ye({
24205
- message: worktree.branch ? `Remove worktree and delete branch ${worktree.branch}?` : `Remove detached worktree ${worktree.path}?`,
24206
- active: "Yes",
24207
- inactive: "No",
24208
- initialValue: true
24209
- });
24210
- return !pD(choice) && choice;
24211
- }
24212
- async function writeOutput3(repoRoot, stdout3) {
24213
- await writeShellOutput(REMOVE_OUTPUT_FILE_ENV, repoRoot, stdout3);
24214
- }
24215
- function emitError3(options, message) {
24216
- if (options.json) {
24217
- options.stderr(`${JSON.stringify({ error: message }, null, 2)}
24218
- `);
24219
- } else {
24220
- options.stderr(`${message}
24221
- `);
24222
- }
24223
- }
24224
- function toMessage4(error) {
24225
- return error instanceof Error ? error.message : String(error);
24226
- }
24227
-
24228
24081
  // src/cli/commands/root.ts
24229
24082
  var ROOT_OUTPUT_FILE_ENV = "GJI_ROOT_OUTPUT_FILE";
24230
24083
  async function runRootCommand(options) {
@@ -24245,13 +24098,17 @@ async function runRootCommand(options) {
24245
24098
 
24246
24099
  // src/cli/commands/run-hook.ts
24247
24100
  var VALID_HOOKS = [
24101
+ "after-branch-create",
24248
24102
  "after-create",
24249
24103
  "after-enter",
24104
+ "after-switch",
24250
24105
  "before-remove"
24251
24106
  ];
24252
24107
  var CAMEL_ALIASES = {
24108
+ afterBranchCreate: "after-branch-create",
24253
24109
  afterCreate: "after-create",
24254
24110
  afterEnter: "after-enter",
24111
+ afterSwitch: "after-switch",
24255
24112
  beforeRemove: "before-remove"
24256
24113
  };
24257
24114
  function isValidHook(hook) {
@@ -24448,7 +24305,7 @@ async function runSyncCommand(options) {
24448
24305
  resolveConfiguredString2(config.syncDefaultBranch) ?? void 0
24449
24306
  );
24450
24307
  } catch {
24451
- emitError4(options, `Unable to reach remote '${remote}'`);
24308
+ emitError3(options, `Unable to reach remote '${remote}'`);
24452
24309
  if (!options.json) {
24453
24310
  options.stderr(
24454
24311
  `Hint: Add the remote with: git remote add ${remote} <url>
@@ -24458,7 +24315,7 @@ async function runSyncCommand(options) {
24458
24315
  return 1;
24459
24316
  }
24460
24317
  if (!remoteBase) {
24461
- emitError4(options, "Unable to determine the default branch for sync.");
24318
+ emitError3(options, "Unable to determine the default branch for sync.");
24462
24319
  if (!options.json) {
24463
24320
  options.stderr(
24464
24321
  `Hint: Add the remote with: git remote add ${remote} <url>
@@ -24473,7 +24330,7 @@ async function runSyncCommand(options) {
24473
24330
  options.all
24474
24331
  );
24475
24332
  if (targetWorktrees === "detached") {
24476
- emitError4(
24333
+ emitError3(
24477
24334
  options,
24478
24335
  `Cannot sync detached worktree: ${repository.currentRoot}`
24479
24336
  );
@@ -24481,14 +24338,14 @@ async function runSyncCommand(options) {
24481
24338
  }
24482
24339
  for (const worktree of targetWorktrees) {
24483
24340
  if (await isDirtyWorktree2(worktree.path)) {
24484
- emitError4(options, `Cannot sync dirty worktree: ${worktree.path}`);
24341
+ emitError3(options, `Cannot sync dirty worktree: ${worktree.path}`);
24485
24342
  return 1;
24486
24343
  }
24487
24344
  }
24488
24345
  try {
24489
24346
  await runGit2(repository.repoRoot, ["fetch", "--prune", remote]);
24490
24347
  } catch {
24491
- emitError4(options, `Failed to fetch from remote '${remote}'`);
24348
+ emitError3(options, `Failed to fetch from remote '${remote}'`);
24492
24349
  if (!options.json) {
24493
24350
  options.stderr(
24494
24351
  `Hint: Add the remote with: git remote add ${remote} <url>
@@ -24520,7 +24377,7 @@ async function runSyncCommand(options) {
24520
24377
  }
24521
24378
  return 0;
24522
24379
  }
24523
- function emitError4(options, message) {
24380
+ function emitError3(options, message) {
24524
24381
  if (options.json) {
24525
24382
  options.stderr(`${JSON.stringify({ error: message }, null, 2)}
24526
24383
  `);
@@ -24741,144 +24598,6 @@ function emitTaskError(options, message) {
24741
24598
  return 1;
24742
24599
  }
24743
24600
 
24744
- // src/cli/commands/warp.ts
24745
- var WARP_OUTPUT_FILE_ENV = "GJI_WARP_OUTPUT_FILE";
24746
- async function runWarpCommand(options) {
24747
- return runWarpNavigate(options);
24748
- }
24749
- async function runWarpNavigate(options) {
24750
- const runtime = options.runtime ?? defaultCliDependencies;
24751
- const { recordWorktreeUsage: recordWorktreeUsage2 } = runtime.history;
24752
- if ((isHeadless() || options.json) && !options.branch) {
24753
- const message = "branch argument is required";
24754
- if (options.json) {
24755
- options.stderr(`${JSON.stringify({ error: message }, null, 2)}
24756
- `);
24757
- } else {
24758
- options.stderr(
24759
- "gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n"
24760
- );
24761
- }
24762
- return 1;
24763
- }
24764
- const target = await resolveWarpTarget({
24765
- ...options,
24766
- commandName: "gji warp",
24767
- json: options.json,
24768
- queryPullRequests: options.queryPullRequests,
24769
- runtime
24770
- });
24771
- if (!target) return 1;
24772
- if (options.json) {
24773
- options.stdout(
24774
- `${JSON.stringify(
24775
- createNavigationTarget(target.repository, target.path, target.branch),
24776
- null,
24777
- 2
24778
- )}
24779
- `
24780
- );
24781
- return 0;
24782
- }
24783
- await recordWorktreeUsage2(target.path, target.branch);
24784
- await writeShellOutput(WARP_OUTPUT_FILE_ENV, target.path, options.stdout);
24785
- return 0;
24786
- }
24787
- async function resolveWarpTarget(options) {
24788
- const cmd = options.commandName ?? "gji";
24789
- const runtime = options.runtime ?? defaultCliDependencies;
24790
- const { detectRepository: detectRepository2 } = runtime.repositoryContext;
24791
- const { loadRegistry: loadRegistry2 } = runtime.repositoryRegistry;
24792
- const sourceDependencies = {
24793
- ...runtime.repositoryContext,
24794
- ...runtime.repositoryRegistry,
24795
- ...runtime.worktrees
24796
- };
24797
- const emitError5 = (message, hint) => {
24798
- if (options.json) {
24799
- options.stderr(`${JSON.stringify({ error: message }, null, 2)}
24800
- `);
24801
- } else {
24802
- options.stderr(`${cmd}: ${message}
24803
- `);
24804
- if (hint) options.stderr(hint);
24805
- }
24806
- };
24807
- const registry = await loadRegistry2();
24808
- if (registry.length === 0) {
24809
- emitError5(
24810
- "not in a git repository and no repos registered yet.",
24811
- "Use any gji command inside a repository to register it.\n"
24812
- );
24813
- return null;
24814
- }
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
- );
24823
- if (allItems.length === 0) {
24824
- emitError5(
24825
- "no accessible worktrees found in any registered repo.",
24826
- skippedRegisteredRepos > 0 ? "Hint: Run 'gji doctor' to inspect stale repository entries.\n" : void 0
24827
- );
24828
- return null;
24829
- }
24830
- const promptSources = allItems.map((item) => ({
24831
- repoRoot: item.repoRoot,
24832
- repoName: item.repoName,
24833
- worktree: item.worktree
24834
- }));
24835
- if (options.branch) {
24836
- const match = resolveWorktreeQuery(promptSources, options.branch);
24837
- if (!match) {
24838
- emitError5(
24839
- `no worktree found matching: ${options.branch}`,
24840
- skippedRegisteredRepos > 0 ? "Hint: Run 'gji doctor' to inspect stale repository entries.\n" : void 0
24841
- );
24842
- return null;
24843
- }
24844
- return {
24845
- branch: match.worktree.branch,
24846
- path: match.worktree.path,
24847
- repository: createNavigationRepository(
24848
- match.repoName,
24849
- match.repoRoot ?? (await detectRepository2(match.worktree.path)).repoRoot
24850
- )
24851
- };
24852
- }
24853
- const promptEntries = await buildWorktreePromptEntries(promptSources, {
24854
- metadata: "fast",
24855
- catalog: withPullRequestQueries(runtime, options.queryPullRequests)
24856
- });
24857
- const path9 = await promptForWarpTarget(promptEntries);
24858
- if (!path9) {
24859
- options.stderr("Aborted\n");
24860
- return null;
24861
- }
24862
- const chosenSource = promptSources.find(
24863
- (item) => item.worktree.path === path9
24864
- );
24865
- if (chosenSource === void 0) {
24866
- emitError5("selected worktree is no longer registered");
24867
- return null;
24868
- }
24869
- return {
24870
- branch: chosenSource.worktree.branch,
24871
- path: path9,
24872
- repository: createNavigationRepository(
24873
- chosenSource.repoName,
24874
- chosenSource.repoRoot ?? (await detectRepository2(chosenSource.worktree.path)).repoRoot
24875
- )
24876
- };
24877
- }
24878
- async function promptForWarpTarget(items) {
24879
- return promptForSingleWorktree("Warp to a worktree", items);
24880
- }
24881
-
24882
24601
  // src/cli/program.ts
24883
24602
  function createProgram() {
24884
24603
  const program2 = new Command();
@@ -25080,25 +24799,9 @@ function registerCommands(program2) {
25080
24799
  "--json",
25081
24800
  "emit JSON on success or error instead of human-readable output"
25082
24801
  ).action(notImplemented("clean"));
25083
- program2.command("remove [branch]").alias("rm").description(
25084
- "deprecated: use gji done for one worktree or gji clean for bulk cleanup"
25085
- ).option(
25086
- "-f, --force",
25087
- "bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch"
25088
- ).option("--dry-run", "show what would be deleted without removing anything").option(
25089
- "--json",
25090
- "emit JSON on success or error instead of human-readable output"
25091
- ).action(notImplemented("remove"));
25092
24802
  program2.command("run-hook <hook>").alias("trigger-hook").description(
25093
- "run a named hook (after-create, after-enter, before-remove) in the current worktree"
24803
+ "run a named hook (after-branch-create, after-create, after-enter, after-switch, before-remove) in the current worktree"
25094
24804
  ).action(notImplemented("run-hook"));
25095
- program2.command("warp [branch]").description("deprecated: use gji go (press Tab for all known repos)").option(
25096
- "--print",
25097
- "print the resolved worktree path without changing directory"
25098
- ).option(
25099
- "--json",
25100
- "emit JSON on success or error instead of human-readable output"
25101
- ).action(notImplemented("warp"));
25102
24805
  const configCommand = program2.command("config").description("manage global config defaults").action(notImplemented("config"));
25103
24806
  configCommand.command("get [key]").description("print the global config or a single key").action(notImplemented("config get"));
25104
24807
  configCommand.command("set <key> <value>").description("set a global config value").action(notImplemented("config set"));
@@ -25454,31 +25157,6 @@ function attachCommandActions(program2, options) {
25454
25157
  }
25455
25158
  }
25456
25159
  );
25457
- const runRemovalCommand = async (branch, commandOptions2 = {}) => {
25458
- const exitCode = await runRemoveCommand({
25459
- branch,
25460
- cwd: options.cwd,
25461
- dryRun: commandOptions2.dryRun,
25462
- force: commandOptions2.force,
25463
- json: commandOptions2.json,
25464
- runtime: options.dependencies,
25465
- stderr: options.stderr,
25466
- stdout: options.stdout
25467
- });
25468
- if (exitCode !== 0) {
25469
- throw commanderExit(exitCode);
25470
- }
25471
- };
25472
- requireCommand(program2, "remove").action(
25473
- async (branch, commandOptions2) => {
25474
- if (!commandOptions2.json) {
25475
- options.stderr(
25476
- "gji remove is deprecated; use 'gji done' for one worktree or 'gji clean' for bulk cleanup.\n"
25477
- );
25478
- }
25479
- return runRemovalCommand(branch, commandOptions2);
25480
- }
25481
- );
25482
25160
  requireCommand(program2, "run-hook").action(async (hook) => {
25483
25161
  const exitCode = await runHookCommand({
25484
25162
  cwd: options.cwd,
@@ -25490,26 +25168,6 @@ function attachCommandActions(program2, options) {
25490
25168
  throw commanderExit(exitCode);
25491
25169
  }
25492
25170
  });
25493
- requireCommand(program2, "warp").action(
25494
- async (branch, commandOptions2) => {
25495
- if (!commandOptions2.json) {
25496
- options.stderr(
25497
- "gji warp is deprecated; use 'gji go' (press Tab for all repositories).\n"
25498
- );
25499
- }
25500
- const exitCode = await runWarpCommand({
25501
- branch,
25502
- cwd: options.cwd,
25503
- json: commandOptions2.json,
25504
- runtime: options.dependencies,
25505
- stderr: options.stderr,
25506
- stdout: options.stdout
25507
- });
25508
- if (exitCode !== 0) {
25509
- throw commanderExit(exitCode);
25510
- }
25511
- }
25512
- );
25513
25171
  const configCommand = requireCommand(program2, "config");
25514
25172
  configCommand.action(async () => {
25515
25173
  const exitCode = await runConfigCommand({