@solaqua/gji 0.7.0 → 0.7.2

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 (52) hide show
  1. package/dist/back.js +1 -1
  2. package/dist/clean.d.ts +2 -1
  3. package/dist/clean.js +8 -16
  4. package/dist/cli.js +7 -6
  5. package/dist/gji-bundle.mjs +568 -273
  6. package/dist/go.d.ts +2 -4
  7. package/dist/go.js +19 -48
  8. package/dist/history.d.ts +1 -0
  9. package/dist/history.js +12 -5
  10. package/dist/hooks.d.ts +3 -3
  11. package/dist/hooks.js +3 -3
  12. package/dist/init.d.ts +3 -3
  13. package/dist/init.js +12 -12
  14. package/dist/install-prompt.js +22 -21
  15. package/dist/new.js +72 -10
  16. package/dist/open.d.ts +2 -2
  17. package/dist/open.js +22 -18
  18. package/dist/pr.js +4 -4
  19. package/dist/remove.d.ts +3 -2
  20. package/dist/remove.js +8 -13
  21. package/dist/repo.d.ts +0 -1
  22. package/dist/repo.js +0 -9
  23. package/dist/run-hook.d.ts +6 -0
  24. package/dist/{trigger-hook.js → run-hook.js} +13 -7
  25. package/dist/shell-completion.js +5 -5
  26. package/dist/warp.js +24 -53
  27. package/dist/worktree-info.d.ts +0 -1
  28. package/dist/worktree-info.js +17 -11
  29. package/dist/worktree-picker.d.ts +14 -0
  30. package/dist/worktree-picker.js +228 -0
  31. package/man/man1/gji-back.1 +1 -1
  32. package/man/man1/gji-clean.1 +1 -1
  33. package/man/man1/gji-completion.1 +1 -1
  34. package/man/man1/gji-config.1 +1 -1
  35. package/man/man1/gji-go.1 +1 -1
  36. package/man/man1/gji-history.1 +1 -1
  37. package/man/man1/gji-init.1 +1 -1
  38. package/man/man1/gji-ls.1 +1 -1
  39. package/man/man1/gji-new.1 +1 -1
  40. package/man/man1/gji-open.1 +1 -1
  41. package/man/man1/gji-pr.1 +1 -1
  42. package/man/man1/gji-remove.1 +1 -1
  43. package/man/man1/gji-root.1 +1 -1
  44. package/man/man1/gji-run-hook.1 +9 -0
  45. package/man/man1/gji-status.1 +1 -1
  46. package/man/man1/gji-sync-files.1 +1 -1
  47. package/man/man1/gji-sync.1 +1 -1
  48. package/man/man1/gji-warp.1 +1 -1
  49. package/man/man1/gji.1 +6 -4
  50. package/package.json +3 -7
  51. package/dist/trigger-hook.d.ts +0 -6
  52. package/man/man1/gji-trigger-hook.1 +0 -9
@@ -13074,15 +13074,21 @@ async function loadHistory(home = homedir2()) {
13074
13074
  async function appendHistory(path9, branch, home = homedir2()) {
13075
13075
  const historyPath = HISTORY_FILE_PATH(home);
13076
13076
  const existing = await loadHistory(home);
13077
- if (existing.length > 0 && existing[0].path === path9) {
13078
- return;
13079
- }
13080
13077
  const entry = { branch, path: path9, timestamp: Date.now() };
13081
- const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
13078
+ const next = [
13079
+ entry,
13080
+ ...existing.filter((existingEntry) => existingEntry.path !== path9)
13081
+ ].slice(0, MAX_HISTORY_ENTRIES);
13082
13082
  await mkdir2(dirname2(historyPath), { recursive: true });
13083
13083
  await writeFile2(historyPath, `${JSON.stringify(next, null, 2)}
13084
13084
  `, "utf8");
13085
13085
  }
13086
+ async function recordWorktreeUsage(path9, branch, home = homedir2()) {
13087
+ try {
13088
+ await appendHistory(path9, branch, home);
13089
+ } catch {
13090
+ }
13091
+ }
13086
13092
  function isHistoryEntry(value) {
13087
13093
  return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "timestamp" in value && typeof value.timestamp === "number";
13088
13094
  }
@@ -13176,9 +13182,13 @@ function extractHooks(config) {
13176
13182
  }
13177
13183
  const hooks = raw;
13178
13184
  return {
13179
- afterCreate: parseHookCommand(hooks.afterCreate),
13180
- afterEnter: parseHookCommand(hooks.afterEnter),
13181
- beforeRemove: parseHookCommand(hooks.beforeRemove)
13185
+ "after-create": parseHookCommand(
13186
+ hooks["after-create"] ?? hooks.afterCreate
13187
+ ),
13188
+ "after-enter": parseHookCommand(hooks["after-enter"] ?? hooks.afterEnter),
13189
+ "before-remove": parseHookCommand(
13190
+ hooks["before-remove"] ?? hooks.beforeRemove
13191
+ )
13182
13192
  };
13183
13193
  }
13184
13194
  function parseHookCommand(value) {
@@ -13377,13 +13387,6 @@ async function listWorktrees(cwd) {
13377
13387
  };
13378
13388
  });
13379
13389
  }
13380
- function sortByCurrentFirst(worktrees) {
13381
- return [...worktrees].sort((a, b3) => {
13382
- if (a.isCurrent && !b3.isCurrent) return -1;
13383
- if (!a.isCurrent && b3.isCurrent) return 1;
13384
- return 0;
13385
- });
13386
- }
13387
13390
  function findPorcelainValue(block, key) {
13388
13391
  const value = findOptionalPorcelainValue(block, key);
13389
13392
  if (!value) {
@@ -13450,7 +13453,7 @@ async function runBackCommand(options) {
13450
13453
  );
13451
13454
  const hooks = extractHooks(config);
13452
13455
  await runHook(
13453
- hooks.afterEnter,
13456
+ hooks["after-enter"],
13454
13457
  target.path,
13455
13458
  {
13456
13459
  branch: target.branch ?? void 0,
@@ -13817,43 +13820,63 @@ var dD = class extends x {
13817
13820
  });
13818
13821
  }
13819
13822
  };
13823
+ var mD = Object.defineProperty;
13824
+ var bD = (e2, u2, t) => u2 in e2 ? mD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
13825
+ var Z = (e2, u2, t) => (bD(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
13826
+ var q = (e2, u2, t) => {
13827
+ if (!u2.has(e2)) throw TypeError("Cannot " + t);
13828
+ };
13829
+ var T = (e2, u2, t) => (q(e2, u2, "read from private field"), t ? t.call(e2) : u2.get(e2));
13830
+ var wD = (e2, u2, t) => {
13831
+ if (u2.has(e2)) throw TypeError("Cannot add the same private member more than once");
13832
+ u2 instanceof WeakSet ? u2.add(e2) : u2.set(e2, t);
13833
+ };
13834
+ var yD = (e2, u2, t, F2) => (q(e2, u2, "write to private field"), F2 ? F2.call(e2, t) : u2.set(e2, t), t);
13820
13835
  var A;
13821
- A = /* @__PURE__ */ new WeakMap();
13822
- var kD = Object.defineProperty;
13823
- var $D = (e2, u2, t) => u2 in e2 ? kD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
13824
- var H = (e2, u2, t) => ($D(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
13825
- var SD = class extends x {
13836
+ var _D = class extends x {
13826
13837
  constructor(u2) {
13827
- super(u2, false), H(this, "options"), H(this, "cursor", 0), this.options = u2.options, this.value = [...u2.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: t }) => t === u2.cursorAt), 0), this.on("key", (t) => {
13828
- t === "a" && this.toggleAll();
13829
- }), this.on("cursor", (t) => {
13830
- switch (t) {
13838
+ super(u2, false), Z(this, "options"), Z(this, "cursor", 0), wD(this, A, void 0);
13839
+ const { options: t } = u2;
13840
+ yD(this, A, u2.selectableGroups !== false), this.options = Object.entries(t).flatMap(([F2, s]) => [{ value: F2, group: true, label: F2 }, ...s.map((i) => ({ ...i, group: F2 }))]), this.value = [...u2.initialValues ?? []], this.cursor = Math.max(this.options.findIndex(({ value: F2 }) => F2 === u2.cursorAt), T(this, A) ? 0 : 1), this.on("cursor", (F2) => {
13841
+ switch (F2) {
13831
13842
  case "left":
13832
- case "up":
13843
+ case "up": {
13833
13844
  this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1;
13845
+ const s = this.options[this.cursor]?.group === true;
13846
+ !T(this, A) && s && (this.cursor = this.cursor === 0 ? this.options.length - 1 : this.cursor - 1);
13834
13847
  break;
13848
+ }
13835
13849
  case "down":
13836
- case "right":
13850
+ case "right": {
13837
13851
  this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1;
13852
+ const s = this.options[this.cursor]?.group === true;
13853
+ !T(this, A) && s && (this.cursor = this.cursor === this.options.length - 1 ? 0 : this.cursor + 1);
13838
13854
  break;
13855
+ }
13839
13856
  case "space":
13840
13857
  this.toggleValue();
13841
13858
  break;
13842
13859
  }
13843
13860
  });
13844
13861
  }
13845
- get _value() {
13846
- return this.options[this.cursor].value;
13862
+ getGroupItems(u2) {
13863
+ return this.options.filter((t) => t.group === u2);
13847
13864
  }
13848
- toggleAll() {
13849
- const u2 = this.value.length === this.options.length;
13850
- this.value = u2 ? [] : this.options.map((t) => t.value);
13865
+ isGroupSelected(u2) {
13866
+ return this.getGroupItems(u2).every((t) => this.value.includes(t.value));
13851
13867
  }
13852
13868
  toggleValue() {
13853
- const u2 = this.value.includes(this._value);
13854
- this.value = u2 ? this.value.filter((t) => t !== this._value) : [...this.value, this._value];
13869
+ const u2 = this.options[this.cursor];
13870
+ if (u2.group === true) {
13871
+ const t = u2.value, F2 = this.getGroupItems(t);
13872
+ this.isGroupSelected(t) ? this.value = this.value.filter((s) => F2.findIndex((i) => i.value === s) === -1) : this.value = [...this.value, ...F2.map((s) => s.value)], this.value = Array.from(new Set(this.value));
13873
+ } else {
13874
+ const t = this.value.includes(u2.value);
13875
+ this.value = t ? this.value.filter((F2) => F2 !== u2.value) : [...this.value, u2.value];
13876
+ }
13855
13877
  }
13856
13878
  };
13879
+ A = /* @__PURE__ */ new WeakMap();
13857
13880
  var OD = Object.defineProperty;
13858
13881
  var PD = (e2, u2, t) => u2 in e2 ? OD(e2, u2, { enumerable: true, configurable: true, writable: true, value: t }) : e2[u2] = t;
13859
13882
  var J = (e2, u2, t) => (PD(e2, typeof u2 != "symbol" ? u2 + "" : u2, t), t);
@@ -13916,14 +13939,14 @@ var d2 = u("\u2514", "\u2014");
13916
13939
  var k2 = u("\u25CF", ">");
13917
13940
  var P2 = u("\u25CB", " ");
13918
13941
  var A2 = u("\u25FB", "[\u2022]");
13919
- var T = u("\u25FC", "[+]");
13942
+ var T2 = u("\u25FC", "[+]");
13920
13943
  var F = u("\u25FB", "[ ]");
13921
13944
  var $e = u("\u25AA", "\u2022");
13922
13945
  var _2 = u("\u2500", "-");
13923
13946
  var me = u("\u256E", "+");
13924
13947
  var de = u("\u251C", "+");
13925
13948
  var pe = u("\u256F", "+");
13926
- var q = u("\u25CF", "\u2022");
13949
+ var q2 = u("\u25CF", "\u2022");
13927
13950
  var D = u("\u25C6", "*");
13928
13951
  var U = u("\u25B2", "!");
13929
13952
  var K2 = u("\u25A0", "x");
@@ -14022,40 +14045,54 @@ ${import_picocolors2.default.cyan(d2)}
14022
14045
  }
14023
14046
  } }).prompt();
14024
14047
  };
14025
- var fe = (t) => {
14026
- const n = (r2, i) => {
14027
- const s = r2.label ?? String(r2.value);
14028
- return i === "active" ? `${import_picocolors2.default.cyan(A2)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "selected" ? `${import_picocolors2.default.green(T)} ${import_picocolors2.default.dim(s)} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "cancelled" ? `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(s))}` : i === "active-selected" ? `${import_picocolors2.default.green(T)} ${s} ${r2.hint ? import_picocolors2.default.dim(`(${r2.hint})`) : ""}` : i === "submitted" ? `${import_picocolors2.default.dim(s)}` : `${import_picocolors2.default.dim(F)} ${import_picocolors2.default.dim(s)}`;
14048
+ var be = (t) => {
14049
+ const { selectableGroups: n = true } = t, r2 = (i, s, c = []) => {
14050
+ const a = i.label ?? String(i.value), l2 = typeof i.group == "string", $2 = l2 && (c[c.indexOf(i) + 1] ?? { group: true }), g = l2 && $2.group === true, p2 = l2 ? n ? `${g ? d2 : o} ` : " " : "";
14051
+ if (s === "active") return `${import_picocolors2.default.dim(p2)}${import_picocolors2.default.cyan(A2)} ${a} ${i.hint ? import_picocolors2.default.dim(`(${i.hint})`) : ""}`;
14052
+ if (s === "group-active") return `${p2}${import_picocolors2.default.cyan(A2)} ${import_picocolors2.default.dim(a)}`;
14053
+ if (s === "group-active-selected") return `${p2}${import_picocolors2.default.green(T2)} ${import_picocolors2.default.dim(a)}`;
14054
+ if (s === "selected") {
14055
+ const f = l2 || n ? import_picocolors2.default.green(T2) : "";
14056
+ return `${import_picocolors2.default.dim(p2)}${f} ${import_picocolors2.default.dim(a)} ${i.hint ? import_picocolors2.default.dim(`(${i.hint})`) : ""}`;
14057
+ }
14058
+ if (s === "cancelled") return `${import_picocolors2.default.strikethrough(import_picocolors2.default.dim(a))}`;
14059
+ if (s === "active-selected") return `${import_picocolors2.default.dim(p2)}${import_picocolors2.default.green(T2)} ${a} ${i.hint ? import_picocolors2.default.dim(`(${i.hint})`) : ""}`;
14060
+ if (s === "submitted") return `${import_picocolors2.default.dim(a)}`;
14061
+ const v2 = l2 || n ? import_picocolors2.default.dim(F) : "";
14062
+ return `${import_picocolors2.default.dim(p2)}${v2} ${import_picocolors2.default.dim(a)}`;
14029
14063
  };
14030
- return new SD({ options: t.options, initialValues: t.initialValues, required: t.required ?? true, cursorAt: t.cursorAt, validate(r2) {
14031
- if (this.required && r2.length === 0) return `Please select at least one option.
14064
+ return new _D({ options: t.options, initialValues: t.initialValues, required: t.required ?? true, cursorAt: t.cursorAt, selectableGroups: n, validate(i) {
14065
+ if (this.required && i.length === 0) return `Please select at least one option.
14032
14066
  ${import_picocolors2.default.reset(import_picocolors2.default.dim(`Press ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" space ")))} to select, ${import_picocolors2.default.gray(import_picocolors2.default.bgWhite(import_picocolors2.default.inverse(" enter ")))} to submit`))}`;
14033
14067
  }, render() {
14034
- const r2 = `${import_picocolors2.default.gray(o)}
14068
+ const i = `${import_picocolors2.default.gray(o)}
14035
14069
  ${b2(this.state)} ${t.message}
14036
- `, i = (s, c) => {
14037
- const a = this.value.includes(s.value);
14038
- return c && a ? n(s, "active-selected") : a ? n(s, "selected") : n(s, c ? "active" : "inactive");
14039
- };
14070
+ `;
14040
14071
  switch (this.state) {
14041
14072
  case "submit":
14042
- return `${r2}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => n(s, "submitted")).join(import_picocolors2.default.dim(", ")) || import_picocolors2.default.dim("none")}`;
14073
+ return `${i}${import_picocolors2.default.gray(o)} ${this.options.filter(({ value: s }) => this.value.includes(s)).map((s) => r2(s, "submitted")).join(import_picocolors2.default.dim(", "))}`;
14043
14074
  case "cancel": {
14044
- const s = this.options.filter(({ value: c }) => this.value.includes(c)).map((c) => n(c, "cancelled")).join(import_picocolors2.default.dim(", "));
14045
- return `${r2}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
14075
+ const s = this.options.filter(({ value: c }) => this.value.includes(c)).map((c) => r2(c, "cancelled")).join(import_picocolors2.default.dim(", "));
14076
+ return `${i}${import_picocolors2.default.gray(o)} ${s.trim() ? `${s}
14046
14077
  ${import_picocolors2.default.gray(o)}` : ""}`;
14047
14078
  }
14048
14079
  case "error": {
14049
14080
  const s = this.error.split(`
14050
14081
  `).map((c, a) => a === 0 ? `${import_picocolors2.default.yellow(d2)} ${import_picocolors2.default.yellow(c)}` : ` ${c}`).join(`
14051
14082
  `);
14052
- return `${r2 + import_picocolors2.default.yellow(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
14083
+ return `${i}${import_picocolors2.default.yellow(o)} ${this.options.map((c, a, l2) => {
14084
+ const $2 = this.value.includes(c.value) || c.group === true && this.isGroupSelected(`${c.value}`), g = a === this.cursor;
14085
+ return !g && typeof c.group == "string" && this.options[this.cursor].value === c.group ? r2(c, $2 ? "group-active-selected" : "group-active", l2) : g && $2 ? r2(c, "active-selected", l2) : $2 ? r2(c, "selected", l2) : r2(c, g ? "active" : "inactive", l2);
14086
+ }).join(`
14053
14087
  ${import_picocolors2.default.yellow(o)} `)}
14054
14088
  ${s}
14055
14089
  `;
14056
14090
  }
14057
14091
  default:
14058
- return `${r2}${import_picocolors2.default.cyan(o)} ${G2({ options: this.options, cursor: this.cursor, maxItems: t.maxItems, style: i }).join(`
14092
+ return `${i}${import_picocolors2.default.cyan(o)} ${this.options.map((s, c, a) => {
14093
+ const l2 = this.value.includes(s.value) || s.group === true && this.isGroupSelected(`${s.value}`), $2 = c === this.cursor;
14094
+ return !$2 && typeof s.group == "string" && this.options[this.cursor].value === s.group ? r2(s, l2 ? "group-active-selected" : "group-active", a) : $2 && l2 ? r2(s, "active-selected", a) : l2 ? r2(s, "selected", a) : r2(s, $2 ? "active" : "inactive", a);
14095
+ }).join(`
14059
14096
  ${import_picocolors2.default.cyan(o)} `)}
14060
14097
  ${import_picocolors2.default.cyan(d2)}
14061
14098
  `;
@@ -14080,8 +14117,29 @@ function isHeadless() {
14080
14117
  }
14081
14118
 
14082
14119
  // src/worktree-info.ts
14120
+ var MAX_WORKTREE_INFO_READ_CONCURRENCY = 8;
14083
14121
  async function readWorktreeInfos(worktrees) {
14084
- return Promise.all(worktrees.map((worktree) => readWorktreeInfo(worktree)));
14122
+ return mapWithConcurrency(
14123
+ worktrees,
14124
+ MAX_WORKTREE_INFO_READ_CONCURRENCY,
14125
+ readWorktreeInfo
14126
+ );
14127
+ }
14128
+ async function mapWithConcurrency(items, limit, mapper) {
14129
+ const results = new Array(items.length);
14130
+ let nextIndex = 0;
14131
+ async function readNext() {
14132
+ for (; ; ) {
14133
+ const index = nextIndex;
14134
+ nextIndex += 1;
14135
+ if (index >= items.length) return;
14136
+ results[index] = await mapper(items[index]);
14137
+ }
14138
+ }
14139
+ await Promise.all(
14140
+ Array.from({ length: Math.min(limit, items.length) }, () => readNext())
14141
+ );
14142
+ return results;
14085
14143
  }
14086
14144
  async function readWorktreeInfo(worktree) {
14087
14145
  const [healthResult, lastCommitResult] = await Promise.allSettled([
@@ -14125,16 +14183,6 @@ function serializeWorktreeInfo(info) {
14125
14183
  upstream: info.upstream
14126
14184
  };
14127
14185
  }
14128
- function formatWorktreeHint(info) {
14129
- const details = [
14130
- `status: ${info.status}`,
14131
- `upstream: ${formatUpstreamState(info.upstream)}`
14132
- ];
14133
- if (info.lastCommitTimestamp !== null) {
14134
- details.push(`last: ${formatRelativeAge(info.lastCommitTimestamp)}`);
14135
- }
14136
- return `${info.path} (${details.join(", ")})`;
14137
- }
14138
14186
  function formatUpstreamState(upstream) {
14139
14187
  if (upstream.kind === "detached") {
14140
14188
  return "n/a";
@@ -14235,6 +14283,225 @@ function hasStderr(error) {
14235
14283
  return error instanceof Error && "stderr" in error && typeof error.stderr === "string";
14236
14284
  }
14237
14285
 
14286
+ // src/worktree-picker.ts
14287
+ async function buildWorktreePromptEntries(sources) {
14288
+ const [history, infos] = await Promise.all([
14289
+ loadHistory(),
14290
+ readWorktreeInfos(sources.map((source) => source.worktree))
14291
+ ]);
14292
+ const historyByPath = new Map(history.map((entry) => [entry.path, entry]));
14293
+ const entries = sources.map(
14294
+ (source, index) => buildWorktreePromptEntry(
14295
+ source,
14296
+ infos[index],
14297
+ historyByPath.get(source.worktree.path)?.timestamp ?? null,
14298
+ Date.now()
14299
+ )
14300
+ );
14301
+ return entries.sort(comparePromptEntries).map(
14302
+ ({ lastActivityTimestamp: _lastActivityTimestamp, ...entry }) => entry
14303
+ );
14304
+ }
14305
+ function resolveWorktreeQuery(sources, query) {
14306
+ const normalizedQuery = normalizeQuery(query);
14307
+ if (normalizedQuery === null) return null;
14308
+ const matches = findWorktreePromptSourceMatches(sources, normalizedQuery);
14309
+ if (isAmbiguousRepoOnlyQuery(matches, normalizedQuery)) return null;
14310
+ return matches[0]?.source ?? null;
14311
+ }
14312
+ function findWorktreePromptSourceMatches(sources, normalizedQuery) {
14313
+ return sources.flatMap((source) => {
14314
+ const matchScore = scoreWorktreeMatch(
14315
+ {
14316
+ ...source.worktree,
14317
+ repoName: source.repoName
14318
+ },
14319
+ normalizedQuery
14320
+ );
14321
+ return matchScore === null ? [] : [{ matchScore, source }];
14322
+ }).sort(compareQueryMatches);
14323
+ }
14324
+ function isAmbiguousRepoOnlyQuery(matches, query) {
14325
+ if (matches[0]?.matchScore === 1e3) return false;
14326
+ return matches.filter((match) => match.source.repoName.toLowerCase() === query).length > 1;
14327
+ }
14328
+ async function promptForSingleWorktree(message, worktrees) {
14329
+ const choice = await ve({
14330
+ message,
14331
+ options: worktrees.map((worktree) => ({
14332
+ label: worktree.label,
14333
+ value: worktree.path
14334
+ })),
14335
+ maxItems: 12
14336
+ });
14337
+ return pD(choice) ? null : choice;
14338
+ }
14339
+ async function promptForMultipleWorktrees(message, worktrees) {
14340
+ const choice = await be({
14341
+ message,
14342
+ options: groupPromptEntries(worktrees),
14343
+ required: true,
14344
+ selectableGroups: false
14345
+ });
14346
+ return pD(choice) ? null : choice;
14347
+ }
14348
+ function compareQueryMatches(a, b3) {
14349
+ if (a.matchScore !== b3.matchScore) {
14350
+ return b3.matchScore - a.matchScore;
14351
+ }
14352
+ if (a.source.worktree.isCurrent && !b3.source.worktree.isCurrent) return -1;
14353
+ if (!a.source.worktree.isCurrent && b3.source.worktree.isCurrent) return 1;
14354
+ return a.source.repoName.localeCompare(b3.source.repoName) || (a.source.worktree.branch ?? "").localeCompare(
14355
+ b3.source.worktree.branch ?? ""
14356
+ ) || a.source.worktree.path.localeCompare(b3.source.worktree.path);
14357
+ }
14358
+ function groupPromptEntries(worktrees) {
14359
+ const groups = {};
14360
+ for (const worktree of worktrees) {
14361
+ const group = worktree.group === "recent" ? "Recent worktrees" : "Other worktrees";
14362
+ groups[group] ??= [];
14363
+ groups[group].push({
14364
+ label: worktree.label,
14365
+ value: worktree.path
14366
+ });
14367
+ }
14368
+ return groups;
14369
+ }
14370
+ function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
14371
+ const lastWorkedTimestamp = info.lastCommitTimestamp === null ? null : info.lastCommitTimestamp * 1e3;
14372
+ const lastActivityTimestamp = lastUsedTimestamp ?? lastWorkedTimestamp;
14373
+ const lastActivityType = lastUsedTimestamp !== null ? "used" : lastWorkedTimestamp !== null ? "worked" : null;
14374
+ const branch = source.worktree.branch ?? "(detached)";
14375
+ const badges = buildStatusBadges(info);
14376
+ const recency = formatPromptRecency(
14377
+ lastActivityTimestamp,
14378
+ lastActivityType,
14379
+ now
14380
+ );
14381
+ const status = badges.length > 0 ? badges.map((badge) => `[${badge}]`).join(" ") : null;
14382
+ const path9 = middleEllipsize(source.worktree.path, 76);
14383
+ const label = [
14384
+ middleEllipsize(source.repoName, 22),
14385
+ middleEllipsize(branch, 34),
14386
+ status,
14387
+ recency,
14388
+ path9
14389
+ ].filter((part) => part !== null && part.length > 0).join(" \xB7 ");
14390
+ return {
14391
+ ...source.worktree,
14392
+ group: lastUsedTimestamp !== null ? "recent" : "other",
14393
+ label,
14394
+ lastActivityTimestamp,
14395
+ repoName: source.repoName
14396
+ };
14397
+ }
14398
+ function buildStatusBadges(info) {
14399
+ const badges = [];
14400
+ if (info.isCurrent) {
14401
+ badges.push("current");
14402
+ }
14403
+ if (info.branch === null) {
14404
+ badges.push("detached");
14405
+ }
14406
+ if (info.status === "dirty") {
14407
+ badges.push("dirty");
14408
+ }
14409
+ if (info.upstream.kind === "stale") {
14410
+ badges.push("stale", "gone");
14411
+ }
14412
+ if (isUpToDate(info.upstream)) {
14413
+ badges.push("up to date");
14414
+ }
14415
+ return badges;
14416
+ }
14417
+ function isUpToDate(upstream) {
14418
+ return upstream.kind === "tracked" && upstream.ahead === 0 && upstream.behind === 0;
14419
+ }
14420
+ function formatPromptRecency(timestamp, type, now) {
14421
+ if (timestamp === null || type === null) {
14422
+ return "last used: never";
14423
+ }
14424
+ const label = type === "used" ? "last used" : "last worked";
14425
+ return `${label}: ${formatPickerAge(timestamp, now)}`;
14426
+ }
14427
+ function formatPickerAge(timestamp, now) {
14428
+ const ageSeconds = Math.max(0, Math.floor((now - timestamp) / 1e3));
14429
+ if (ageSeconds < 60) {
14430
+ return "now";
14431
+ }
14432
+ if (ageSeconds < 60 * 60) {
14433
+ return `${Math.floor(ageSeconds / 60)}m ago`;
14434
+ }
14435
+ if (ageSeconds < 24 * 60 * 60) {
14436
+ return `${Math.floor(ageSeconds / (60 * 60))}h ago`;
14437
+ }
14438
+ if (isYesterday(timestamp, now)) {
14439
+ return "yesterday";
14440
+ }
14441
+ return new Intl.DateTimeFormat("en-US", {
14442
+ day: "numeric",
14443
+ month: "short"
14444
+ }).format(new Date(timestamp));
14445
+ }
14446
+ function isYesterday(timestamp, now) {
14447
+ const date = new Date(timestamp);
14448
+ const yesterday = new Date(now);
14449
+ yesterday.setDate(yesterday.getDate() - 1);
14450
+ return date.getFullYear() === yesterday.getFullYear() && date.getMonth() === yesterday.getMonth() && date.getDate() === yesterday.getDate();
14451
+ }
14452
+ function buildSearchText(repoName, worktree) {
14453
+ return [
14454
+ repoName,
14455
+ worktree.branch ?? "detached",
14456
+ worktree.path,
14457
+ `${repoName}/${worktree.branch ?? "detached"}`
14458
+ ].join(" ").toLowerCase();
14459
+ }
14460
+ function normalizeQuery(query) {
14461
+ const normalized = query?.trim().toLowerCase();
14462
+ return normalized && normalized.length > 0 ? normalized : null;
14463
+ }
14464
+ function comparePromptEntries(a, b3) {
14465
+ if (a.isCurrent && !b3.isCurrent) return -1;
14466
+ if (!a.isCurrent && b3.isCurrent) return 1;
14467
+ if (a.group !== b3.group) {
14468
+ return groupRank(a.group) - groupRank(b3.group);
14469
+ }
14470
+ const aRecent = a.lastActivityTimestamp ?? 0;
14471
+ const bRecent = b3.lastActivityTimestamp ?? 0;
14472
+ if (aRecent !== bRecent) {
14473
+ return bRecent - aRecent;
14474
+ }
14475
+ return a.repoName.localeCompare(b3.repoName) || (a.branch ?? "").localeCompare(b3.branch ?? "") || a.path.localeCompare(b3.path);
14476
+ }
14477
+ function groupRank(group) {
14478
+ return group === "recent" ? 0 : 1;
14479
+ }
14480
+ function scoreWorktreeMatch(entry, query) {
14481
+ const branch = entry.branch ?? "detached";
14482
+ const exactCandidates = [
14483
+ branch,
14484
+ entry.path,
14485
+ `${entry.repoName}/${branch}`
14486
+ ].map((candidate) => candidate.toLowerCase());
14487
+ if (exactCandidates.includes(query)) {
14488
+ return 1e3;
14489
+ }
14490
+ return buildSearchText(entry.repoName, entry).includes(query) ? 1 : null;
14491
+ }
14492
+ function middleEllipsize(value, maxLength) {
14493
+ if (value.length <= maxLength) {
14494
+ return value;
14495
+ }
14496
+ if (maxLength <= 1) {
14497
+ return "\u2026";
14498
+ }
14499
+ const keep = maxLength - 1;
14500
+ const start = Math.ceil(keep / 2);
14501
+ const end = Math.floor(keep / 2);
14502
+ return `${value.slice(0, start)}\u2026${value.slice(value.length - end)}`;
14503
+ }
14504
+
14238
14505
  // src/worktree-prompts.ts
14239
14506
  async function defaultConfirmForceRemoveWorktree(worktreePath) {
14240
14507
  const choice = await ye({
@@ -14295,7 +14562,14 @@ function createCleanCommand(dependencies = {}) {
14295
14562
  return 1;
14296
14563
  }
14297
14564
  const shouldSelectAll = options.force || options.dryRun && (options.stale || options.json || isHeadless());
14298
- const selections = shouldSelectAll ? cleanupCandidates.map((w2) => w2.path) : await promptForWorktrees(cleanupCandidates);
14565
+ const selections = shouldSelectAll ? cleanupCandidates.map((w2) => w2.path) : await promptForWorktrees(
14566
+ await buildWorktreePromptEntries(
14567
+ cleanupCandidates.map((worktree) => ({
14568
+ repoName: repository.repoName,
14569
+ worktree
14570
+ }))
14571
+ )
14572
+ );
14299
14573
  if (!selections || selections.length === 0) {
14300
14574
  options.stderr("Aborted\n");
14301
14575
  return 1;
@@ -14530,19 +14804,7 @@ function toMessage(error) {
14530
14804
  return error instanceof Error ? error.message : String(error);
14531
14805
  }
14532
14806
  async function defaultPromptForWorktrees(worktrees) {
14533
- const infos = await readWorktreeInfos(worktrees);
14534
- const choice = await fe({
14535
- message: "Choose worktrees to clean",
14536
- options: worktrees.map((worktree, i) => {
14537
- return {
14538
- hint: formatWorktreeHint(infos[i]),
14539
- label: worktree.branch ?? "(detached)",
14540
- value: worktree.path
14541
- };
14542
- }),
14543
- required: true
14544
- });
14545
- return pD(choice) ? null : choice;
14807
+ return promptForMultipleWorktrees("Choose worktrees to clean", worktrees);
14546
14808
  }
14547
14809
  async function defaultConfirmRemoval(worktrees) {
14548
14810
  const branchCount = worktrees.filter(
@@ -14623,14 +14885,14 @@ var TOP_LEVEL_COMMANDS = [
14623
14885
  },
14624
14886
  { name: "rm", description: "alias of remove" },
14625
14887
  {
14626
- name: "trigger-hook",
14888
+ name: "run-hook",
14627
14889
  description: "run a named hook in the current worktree"
14628
14890
  },
14629
14891
  { name: "warp", description: "jump to any worktree across all known repos" },
14630
14892
  { name: "config", description: "manage global config defaults" }
14631
14893
  ];
14632
14894
  var SHELL_NAMES = ["bash", "fish", "zsh"];
14633
- var HOOK_NAMES = ["afterCreate", "afterEnter", "beforeRemove"];
14895
+ var HOOK_NAMES = ["after-create", "after-enter", "before-remove"];
14634
14896
  var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
14635
14897
  function renderShellCompletion(shell) {
14636
14898
  switch (shell) {
@@ -14714,7 +14976,7 @@ _gji_completion() {
14714
14976
  remove|rm)
14715
14977
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
14716
14978
  ;;
14717
- trigger-hook)
14979
+ run-hook)
14718
14980
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
14719
14981
  ;;
14720
14982
  warp)
@@ -14752,7 +15014,7 @@ function renderFishCompletion() {
14752
15014
  (shell) => `complete -c gji -n '__fish_seen_subcommand_from init' -a '${shell}' -d 'shell'`
14753
15015
  ).join("\n");
14754
15016
  const hookLines = HOOK_NAMES.map(
14755
- (hook) => `complete -c gji -n '__fish_seen_subcommand_from trigger-hook' -a '${hook}' -d 'hook'`
15017
+ (hook) => `complete -c gji -n '__fish_seen_subcommand_from run-hook' -a '${hook}' -d 'hook'`
14756
15018
  ).join("\n");
14757
15019
  const configKeyLines = CONFIG_KEYS.map(
14758
15020
  (key) => `complete -c gji -n '__gji_should_complete_config_key' -a '${key}' -d 'config key'`
@@ -14917,7 +15179,7 @@ case "\${words[2]}" in
14917
15179
  remove|rm)
14918
15180
  _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'
14919
15181
  ;;
14920
- trigger-hook)
15182
+ run-hook)
14921
15183
  _arguments "2:hook:(${hooks})"
14922
15184
  ;;
14923
15185
  warp)
@@ -15303,8 +15565,7 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15303
15565
  if (isHeadless() || nonInteractive) {
15304
15566
  return;
15305
15567
  }
15306
- const hooks = isPlainObject2(config.hooks) ? config.hooks : null;
15307
- if (isConfiguredHookCommand(hooks?.afterCreate)) {
15568
+ if (extractHooks(config)["after-create"]) {
15308
15569
  return;
15309
15570
  }
15310
15571
  if (config.skipInstallPrompt === true) {
@@ -15337,17 +15598,28 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15337
15598
  if (choice === "always") {
15338
15599
  try {
15339
15600
  if (saveGlobal) {
15340
- const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
15601
+ const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
15602
+ const existing = extractHooks({ hooks: existingRaw });
15341
15603
  await writeGlobalKey(repoRoot, "hooks", {
15342
- ...existingHooks,
15343
- afterCreate: pm.installCommand
15604
+ ...existing["after-enter"] !== void 0 && {
15605
+ "after-enter": existing["after-enter"]
15606
+ },
15607
+ ...existing["before-remove"] !== void 0 && {
15608
+ "before-remove": existing["before-remove"]
15609
+ },
15610
+ "after-create": pm.installCommand
15344
15611
  });
15345
15612
  } else {
15346
15613
  const { config: localConfig } = await loadConfig(repoRoot);
15347
- const existingLocalHooks = isPlainObject2(localConfig.hooks) ? localConfig.hooks : {};
15614
+ const existing = extractHooks(localConfig);
15348
15615
  await writeKey(repoRoot, "hooks", {
15349
- ...existingLocalHooks,
15350
- afterCreate: pm.installCommand
15616
+ ...existing["after-enter"] !== void 0 && {
15617
+ "after-enter": existing["after-enter"]
15618
+ },
15619
+ ...existing["before-remove"] !== void 0 && {
15620
+ "before-remove": existing["before-remove"]
15621
+ },
15622
+ "after-create": pm.installCommand
15351
15623
  });
15352
15624
  }
15353
15625
  } catch (error) {
@@ -15412,7 +15684,7 @@ async function defaultPromptForInstallChoice(pm) {
15412
15684
  options: [
15413
15685
  { value: "yes", label: "Yes", hint: "run once" },
15414
15686
  { value: "no", label: "No", hint: "skip this time" },
15415
- { value: "always", label: "Always", hint: "save as afterCreate hook" },
15687
+ { value: "always", label: "Always", hint: "save as after-create hook" },
15416
15688
  {
15417
15689
  value: "never",
15418
15690
  label: "Never",
@@ -15428,10 +15700,6 @@ async function defaultPromptForInstallChoice(pm) {
15428
15700
  function isPlainObject2(value) {
15429
15701
  return typeof value === "object" && value !== null && !Array.isArray(value);
15430
15702
  }
15431
- function isConfiguredHookCommand(value) {
15432
- if (typeof value === "string") return value.length > 0;
15433
- return Array.isArray(value) && value.length > 0 && value[0] !== "" && value.every((item) => typeof item === "string");
15434
- }
15435
15703
 
15436
15704
  // src/new.ts
15437
15705
  var execFileAsync3 = promisify4(execFile3);
@@ -15549,7 +15817,7 @@ function createNewCommand(dependencies = {}) {
15549
15817
  `
15550
15818
  );
15551
15819
  options.stderr(
15552
- `Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks
15820
+ `Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks
15553
15821
  `
15554
15822
  );
15555
15823
  }
@@ -15557,7 +15825,7 @@ function createNewCommand(dependencies = {}) {
15557
15825
  } else {
15558
15826
  const choice = await prompt(worktreePath);
15559
15827
  if (choice === "reuse") {
15560
- appendHistory(worktreePath, worktreeName).catch(() => void 0);
15828
+ await recordWorktreeUsage(worktreePath, worktreeName);
15561
15829
  await writeOutput(worktreePath, options.stdout);
15562
15830
  return 0;
15563
15831
  }
@@ -15610,7 +15878,7 @@ function createNewCommand(dependencies = {}) {
15610
15878
  );
15611
15879
  const hooks = extractHooks(config);
15612
15880
  await runHook(
15613
- hooks.afterCreate,
15881
+ hooks["after-create"],
15614
15882
  worktreePath,
15615
15883
  {
15616
15884
  branch: worktreeName,
@@ -15625,7 +15893,7 @@ function createNewCommand(dependencies = {}) {
15625
15893
  `
15626
15894
  );
15627
15895
  } else {
15628
- await appendHistory(worktreePath, worktreeName);
15896
+ await recordWorktreeUsage(worktreePath, worktreeName);
15629
15897
  await writeOutput(worktreePath, options.stdout);
15630
15898
  }
15631
15899
  if (options.open) {
@@ -15662,7 +15930,32 @@ function generateBranchPlaceholder(random = Math.random) {
15662
15930
  "newton",
15663
15931
  "lovelace",
15664
15932
  "nietzsche",
15665
- "kafka"
15933
+ "kafka",
15934
+ "sappho",
15935
+ "aristotle",
15936
+ "pythagoras",
15937
+ "artemis",
15938
+ "apollo",
15939
+ "minerva",
15940
+ "persephone",
15941
+ "icarus",
15942
+ "odysseus",
15943
+ "murasaki",
15944
+ "shakespeare",
15945
+ "frida",
15946
+ "davinci",
15947
+ "kepler",
15948
+ "copernicus",
15949
+ "faraday",
15950
+ "noether",
15951
+ "hopper",
15952
+ "boole",
15953
+ "shannon",
15954
+ "gauss",
15955
+ "ramanujan",
15956
+ "austen",
15957
+ "borges",
15958
+ "zeno"
15666
15959
  ];
15667
15960
  const antics = [
15668
15961
  "borrowed-a-bike",
@@ -15679,9 +15972,50 @@ function generateBranchPlaceholder(random = Math.random) {
15679
15972
  "watered-the-plants",
15680
15973
  "washed-the-dishes",
15681
15974
  "folded-the-laundry",
15682
- "took-a-nap"
15975
+ "took-a-nap",
15976
+ "lost-a-sock",
15977
+ "patched-the-boat",
15978
+ "alphabetized-the-spoons",
15979
+ "argued-with-the-calendar",
15980
+ "misplaced-the-moon",
15981
+ "painted-the-fence",
15982
+ "overcooked-the-rice",
15983
+ "packed-the-snacks",
15984
+ "dropped-the-spoon",
15985
+ "hid-the-remote",
15986
+ "untangled-the-cables",
15987
+ "rebooted-the-kettle",
15988
+ "indexed-the-attic",
15989
+ "forgot-the-password",
15990
+ "sorted-the-buttons",
15991
+ "mopped-the-ceiling",
15992
+ "polished-the-doorknob",
15993
+ "misread-the-map",
15994
+ "reheated-the-tea",
15995
+ "fixed-the-squeak",
15996
+ "labeled-the-drawer",
15997
+ "stacked-the-chairs",
15998
+ "overslept-the-standup",
15999
+ "claimed-the-last-bagel",
16000
+ "debugged-the-toaster"
15683
16001
  ];
15684
- return `${pickRandom(roots, random)}-${pickRandom(antics, random)}`;
16002
+ const root = pickRandom(roots, random);
16003
+ const antic = pickRandom(antics, random);
16004
+ const suffix = generateBranchPlaceholderSuffix(random);
16005
+ return `${root}-${antic}-${suffix}`;
16006
+ }
16007
+ function pickRandom(values, random) {
16008
+ const index = Math.floor(random() * values.length);
16009
+ return values[Math.min(index, values.length - 1)];
16010
+ }
16011
+ function generateBranchPlaceholderSuffix(random) {
16012
+ const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
16013
+ let suffix = "";
16014
+ for (let index = 0; index < 3; index += 1) {
16015
+ const characterIndex = Math.floor(random() * characters.length);
16016
+ suffix += characters[Math.min(characterIndex, characters.length - 1)];
16017
+ }
16018
+ return suffix;
15685
16019
  }
15686
16020
  function applyConfiguredBranchPrefix(branch, branchPrefix) {
15687
16021
  if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
@@ -15722,10 +16056,6 @@ async function defaultPromptForBranch(placeholder) {
15722
16056
  }
15723
16057
  return choice.trim();
15724
16058
  }
15725
- function pickRandom(values, random) {
15726
- const index = Math.floor(random() * values.length);
15727
- return values[Math.min(index, values.length - 1)];
15728
- }
15729
16059
  async function localBranchExists(repoRoot, branchName) {
15730
16060
  try {
15731
16061
  await execFileAsync3(
@@ -15882,7 +16212,7 @@ async function runWarpNavigate(options) {
15882
16212
  );
15883
16213
  return 0;
15884
16214
  }
15885
- appendHistory(target.path, target.branch).catch(() => void 0);
16215
+ await recordWorktreeUsage(target.path, target.branch);
15886
16216
  await writeShellOutput(WARP_OUTPUT_FILE_ENV, target.path, options.stdout);
15887
16217
  return 0;
15888
16218
  }
@@ -15965,18 +16295,6 @@ async function canonicalizeRepoPath2(repoPath) {
15965
16295
  return resolve5(repoPath);
15966
16296
  }
15967
16297
  }
15968
- function findByQuery(items, query) {
15969
- const slashIdx = query.indexOf("/");
15970
- if (slashIdx !== -1) {
15971
- const repoQuery = query.slice(0, slashIdx);
15972
- const branchQuery = query.slice(slashIdx + 1);
15973
- const match = items.find(
15974
- (item) => item.repoName === repoQuery && item.worktree.branch === branchQuery
15975
- );
15976
- if (match) return match;
15977
- }
15978
- return items.find((item) => item.worktree.branch === query) ?? null;
15979
- }
15980
16298
  async function resolveWarpTarget(options) {
15981
16299
  const cmd = options.commandName ?? "gji";
15982
16300
  const emitError4 = (message, hint) => {
@@ -15990,6 +16308,7 @@ async function resolveWarpTarget(options) {
15990
16308
  }
15991
16309
  };
15992
16310
  const registry = await loadRegistry();
16311
+ const currentRoot = await detectRepository(options.cwd).then((repository) => repository.currentRoot).catch(() => null);
15993
16312
  if (registry.length === 0) {
15994
16313
  emitError4(
15995
16314
  "not in a git repository and no repos registered yet.",
@@ -16008,57 +16327,42 @@ async function resolveWarpTarget(options) {
16008
16327
  if (result.status === "rejected") continue;
16009
16328
  const { repoName, worktrees } = result.value;
16010
16329
  for (const worktree of worktrees) {
16011
- allItems.push({ repoName, worktree });
16330
+ allItems.push({
16331
+ repoName,
16332
+ worktree: {
16333
+ ...worktree,
16334
+ isCurrent: currentRoot !== null && worktree.path === currentRoot
16335
+ }
16336
+ });
16012
16337
  }
16013
16338
  }
16014
16339
  if (allItems.length === 0) {
16015
16340
  emitError4("no accessible worktrees found in any registered repo.");
16016
16341
  return null;
16017
16342
  }
16343
+ const promptSources = allItems.map((item) => ({
16344
+ repoName: item.repoName,
16345
+ worktree: item.worktree
16346
+ }));
16018
16347
  if (options.branch) {
16019
- const match = findByQuery(allItems, options.branch);
16348
+ const match = resolveWorktreeQuery(promptSources, options.branch);
16020
16349
  if (!match) {
16021
16350
  emitError4(`no worktree found matching: ${options.branch}`);
16022
16351
  return null;
16023
16352
  }
16024
16353
  return { branch: match.worktree.branch, path: match.worktree.path };
16025
16354
  }
16026
- const path9 = await promptForWarpTarget(allItems);
16355
+ const promptEntries = await buildWorktreePromptEntries(promptSources);
16356
+ const path9 = await promptForWarpTarget(promptEntries);
16027
16357
  if (!path9) {
16028
16358
  options.stderr("Aborted\n");
16029
16359
  return null;
16030
16360
  }
16031
- const chosen = allItems.find((item) => item.worktree.path === path9);
16032
- return { branch: chosen?.worktree.branch ?? null, path: path9 };
16361
+ const chosen = promptEntries.find((item) => item.path === path9);
16362
+ return { branch: chosen?.branch ?? null, path: path9 };
16033
16363
  }
16034
16364
  async function promptForWarpTarget(items) {
16035
- const healthResults = await Promise.allSettled(
16036
- items.map((item) => readWorktreeHealth(item.worktree.path))
16037
- );
16038
- const choice = await ve({
16039
- message: "Warp to a worktree",
16040
- options: items.map((item, i) => {
16041
- const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
16042
- const upstream = health ? formatHint(item.worktree.branch, health) : null;
16043
- const label = `${item.repoName} \u203A ${item.worktree.branch ?? "(detached)"}`;
16044
- const pathHint = item.worktree.isCurrent ? `${item.worktree.path} (current)` : item.worktree.path;
16045
- const hint = upstream ? `${upstream} \xB7 ${pathHint}` : pathHint;
16046
- return { hint, label, value: item.worktree.path };
16047
- })
16048
- });
16049
- if (pD(choice)) {
16050
- return null;
16051
- }
16052
- return choice;
16053
- }
16054
- function formatHint(branch, health) {
16055
- if (branch === null) return null;
16056
- if (!health.hasUpstream) return "no upstream";
16057
- if (health.upstreamGone) return "upstream gone";
16058
- if (health.ahead === 0 && health.behind === 0) return "up to date";
16059
- if (health.ahead === 0) return `behind ${health.behind}`;
16060
- if (health.behind === 0) return `ahead ${health.ahead}`;
16061
- return `ahead ${health.ahead}, behind ${health.behind}`;
16365
+ return promptForSingleWorktree("Warp to a worktree", items);
16062
16366
  }
16063
16367
 
16064
16368
  // src/go.ts
@@ -16085,7 +16389,7 @@ function createGoCommand(dependencies = {}) {
16085
16389
  commandName: "gji go"
16086
16390
  });
16087
16391
  if (!target) return 1;
16088
- appendHistory(target.path, target.branch).catch(() => void 0);
16392
+ await recordWorktreeUsage(target.path, target.branch);
16089
16393
  await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
16090
16394
  return 0;
16091
16395
  }
@@ -16095,8 +16399,14 @@ function createGoCommand(dependencies = {}) {
16095
16399
  );
16096
16400
  return 1;
16097
16401
  }
16098
- const prompted = options.branch ? null : await prompt(sortByCurrentFirst(worktrees));
16099
- const resolvedPath = options.branch ? worktrees.find((entry) => entry.branch === options.branch)?.path : prompted ?? void 0;
16402
+ const promptSources = worktrees.map((worktree) => ({
16403
+ repoName: repository.repoName,
16404
+ worktree
16405
+ }));
16406
+ const promptEntries = options.branch ? [] : await buildWorktreePromptEntries(promptSources);
16407
+ const queried = options.branch ? resolveWorktreeQuery(promptSources, options.branch) : null;
16408
+ const prompted = options.branch ? null : await prompt(promptEntries);
16409
+ const resolvedPath = options.branch ? queried?.worktree.path : prompted ?? void 0;
16100
16410
  if (!resolvedPath) {
16101
16411
  if (options.branch) {
16102
16412
  options.stderr(`No worktree found for branch: ${options.branch}
@@ -16116,7 +16426,7 @@ function createGoCommand(dependencies = {}) {
16116
16426
  );
16117
16427
  const hooks = extractHooks(config);
16118
16428
  await runHook(
16119
- hooks.afterEnter,
16429
+ hooks["after-enter"],
16120
16430
  resolvedPath,
16121
16431
  {
16122
16432
  branch: chosenWorktree?.branch ?? void 0,
@@ -16125,44 +16435,14 @@ function createGoCommand(dependencies = {}) {
16125
16435
  },
16126
16436
  options.stderr
16127
16437
  );
16128
- appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(
16129
- () => void 0
16130
- );
16438
+ await recordWorktreeUsage(resolvedPath, chosenWorktree?.branch ?? null);
16131
16439
  await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
16132
16440
  return 0;
16133
16441
  };
16134
16442
  }
16135
16443
  var runGoCommand = createGoCommand();
16136
16444
  async function promptForWorktree(worktrees) {
16137
- const healthResults = await Promise.allSettled(
16138
- worktrees.map((w2) => readWorktreeHealth(w2.path))
16139
- );
16140
- const choice = await ve({
16141
- message: "Choose a worktree",
16142
- options: worktrees.map((worktree, i) => {
16143
- const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
16144
- const pathHint = worktree.isCurrent ? `${worktree.path} (current)` : worktree.path;
16145
- const upstream = health ? formatUpstreamHint(worktree.branch, health) : null;
16146
- return {
16147
- value: worktree.path,
16148
- label: worktree.branch ?? "(detached)",
16149
- hint: upstream ? `${upstream} \xB7 ${pathHint}` : pathHint
16150
- };
16151
- })
16152
- });
16153
- if (pD(choice)) {
16154
- return null;
16155
- }
16156
- return choice;
16157
- }
16158
- function formatUpstreamHint(branch, health) {
16159
- if (branch === null) return null;
16160
- if (!health.hasUpstream) return "no upstream";
16161
- if (health.upstreamGone) return "upstream gone";
16162
- if (health.ahead === 0 && health.behind === 0) return "up to date";
16163
- if (health.ahead === 0) return `behind ${health.behind}`;
16164
- if (health.behind === 0) return `ahead ${health.ahead}`;
16165
- return `ahead ${health.ahead}, behind ${health.behind}`;
16445
+ return promptForSingleWorktree("Choose a worktree", worktrees);
16166
16446
  }
16167
16447
 
16168
16448
  // src/history-command.ts
@@ -16329,10 +16609,12 @@ async function saveWizardConfig(result, cwd, home) {
16329
16609
  if (result.branchPrefix) values.branchPrefix = result.branchPrefix;
16330
16610
  if (result.worktreePath) values.worktreePath = result.worktreePath;
16331
16611
  const hooks = {};
16332
- if (result.hooks?.afterCreate) hooks.afterCreate = result.hooks.afterCreate;
16333
- if (result.hooks?.afterEnter) hooks.afterEnter = result.hooks.afterEnter;
16334
- if (result.hooks?.beforeRemove)
16335
- hooks.beforeRemove = result.hooks.beforeRemove;
16612
+ if (result.hooks?.["after-create"])
16613
+ hooks["after-create"] = result.hooks["after-create"];
16614
+ if (result.hooks?.["after-enter"])
16615
+ hooks["after-enter"] = result.hooks["after-enter"];
16616
+ if (result.hooks?.["before-remove"])
16617
+ hooks["before-remove"] = result.hooks["before-remove"];
16336
16618
  if (Object.keys(hooks).length > 0) values.hooks = hooks;
16337
16619
  if (Object.keys(values).length === 0) return;
16338
16620
  if (result.installSaveTarget === "local") {
@@ -16463,7 +16745,7 @@ async function defaultPromptForSetup() {
16463
16745
  return null;
16464
16746
  }
16465
16747
  const afterCreate = await he({
16466
- message: "afterCreate hook \u2014 run after creating a worktree?",
16748
+ message: "after-create hook \u2014 run after creating a worktree?",
16467
16749
  placeholder: "e.g. pnpm install \u2014 leave blank to skip"
16468
16750
  });
16469
16751
  if (pD(afterCreate)) {
@@ -16471,7 +16753,7 @@ async function defaultPromptForSetup() {
16471
16753
  return null;
16472
16754
  }
16473
16755
  const afterEnter = await he({
16474
- message: "afterEnter hook \u2014 run after entering a worktree?",
16756
+ message: "after-enter hook \u2014 run after entering a worktree?",
16475
16757
  placeholder: "e.g. nvm use \u2014 leave blank to skip"
16476
16758
  });
16477
16759
  if (pD(afterEnter)) {
@@ -16479,7 +16761,7 @@ async function defaultPromptForSetup() {
16479
16761
  return null;
16480
16762
  }
16481
16763
  const beforeRemove = await he({
16482
- message: "beforeRemove hook \u2014 run before removing a worktree?",
16764
+ message: "before-remove hook \u2014 run before removing a worktree?",
16483
16765
  placeholder: "leave blank to skip"
16484
16766
  });
16485
16767
  if (pD(beforeRemove)) {
@@ -16488,9 +16770,9 @@ async function defaultPromptForSetup() {
16488
16770
  }
16489
16771
  Se("Setup complete!");
16490
16772
  const hooks = {};
16491
- if (afterCreate) hooks.afterCreate = afterCreate;
16492
- if (afterEnter) hooks.afterEnter = afterEnter;
16493
- if (beforeRemove) hooks.beforeRemove = beforeRemove;
16773
+ if (afterCreate) hooks["after-create"] = afterCreate;
16774
+ if (afterEnter) hooks["after-enter"] = afterEnter;
16775
+ if (beforeRemove) hooks["before-remove"] = beforeRemove;
16494
16776
  return {
16495
16777
  branchPrefix: branchPrefix || void 0,
16496
16778
  hooks: Object.keys(hooks).length > 0 ? hooks : void 0,
@@ -16611,27 +16893,43 @@ function createOpenCommand(dependencies = {}) {
16611
16893
  detectRepository(options.cwd)
16612
16894
  ]);
16613
16895
  let targetPath;
16896
+ let targetWorktree;
16614
16897
  if (options.branch) {
16615
- const entry = worktrees.find((w2) => w2.branch === options.branch);
16616
- if (!entry) {
16898
+ const match = resolveWorktreeQuery(
16899
+ worktrees.map((worktree) => ({
16900
+ repoName: repository.repoName,
16901
+ worktree
16902
+ })),
16903
+ options.branch
16904
+ );
16905
+ if (!match) {
16617
16906
  options.stderr(
16618
- `gji open: no worktree found for branch: ${options.branch}
16907
+ `gji open: no worktree found matching: ${options.branch}
16619
16908
  `
16620
16909
  );
16621
16910
  options.stderr(`Hint: Use 'gji ls' to see available worktrees
16622
16911
  `);
16623
16912
  return 1;
16624
16913
  }
16625
- targetPath = entry.path;
16914
+ targetPath = match.worktree.path;
16915
+ targetWorktree = match.worktree;
16626
16916
  } else if (isHeadless()) {
16627
- targetPath = worktrees.find((w2) => w2.isCurrent)?.path ?? options.cwd;
16917
+ targetWorktree = worktrees.find((w2) => w2.isCurrent);
16918
+ targetPath = targetWorktree?.path ?? options.cwd;
16628
16919
  } else {
16629
- const chosen = await promptForWorktree2(sortByCurrentFirst(worktrees));
16920
+ const entries = await buildWorktreePromptEntries(
16921
+ worktrees.map((worktree) => ({
16922
+ repoName: repository.repoName,
16923
+ worktree
16924
+ }))
16925
+ );
16926
+ const chosen = await promptForWorktree2(entries);
16630
16927
  if (!chosen) {
16631
16928
  options.stderr("Aborted\n");
16632
16929
  return 1;
16633
16930
  }
16634
16931
  targetPath = chosen;
16932
+ targetWorktree = worktrees.find((w2) => w2.path === chosen);
16635
16933
  }
16636
16934
  const config = await loadEffectiveConfig(
16637
16935
  repository.repoRoot,
@@ -16696,6 +16994,7 @@ function createOpenCommand(dependencies = {}) {
16696
16994
  return 1;
16697
16995
  }
16698
16996
  const displayName = editorDef?.name ?? editorCli;
16997
+ await recordWorktreeUsage(targetPath, targetWorktree?.branch ?? null);
16699
16998
  options.stdout(`Opened ${targetPath} in ${displayName}
16700
16999
  `);
16701
17000
  return 0;
@@ -16720,16 +17019,7 @@ async function isCommandAvailable(command) {
16720
17019
  }
16721
17020
  }
16722
17021
  async function defaultPromptForWorktree(worktrees) {
16723
- const choice = await ve({
16724
- message: "Choose a worktree to open",
16725
- options: worktrees.map((w2) => ({
16726
- value: w2.path,
16727
- label: w2.branch ?? "(detached)",
16728
- hint: w2.isCurrent ? `${w2.path} (current)` : w2.path
16729
- }))
16730
- });
16731
- if (pD(choice)) return null;
16732
- return choice;
17022
+ return promptForSingleWorktree("Choose a worktree to open", worktrees);
16733
17023
  }
16734
17024
  async function defaultPromptForEditor(editors) {
16735
17025
  const choice = await ve({
@@ -16823,7 +17113,7 @@ function createPrCommand(dependencies = {}) {
16823
17113
  }
16824
17114
  const choice = await prompt(worktreePath);
16825
17115
  if (choice === "reuse") {
16826
- appendHistory(worktreePath, branchName).catch(() => void 0);
17116
+ await recordWorktreeUsage(worktreePath, branchName);
16827
17117
  await writeOutput2(worktreePath, options.stdout);
16828
17118
  return 0;
16829
17119
  }
@@ -16899,7 +17189,7 @@ function createPrCommand(dependencies = {}) {
16899
17189
  );
16900
17190
  const hooks = extractHooks(config);
16901
17191
  await runHook(
16902
- hooks.afterCreate,
17192
+ hooks["after-create"],
16903
17193
  worktreePath,
16904
17194
  {
16905
17195
  branch: branchName,
@@ -16914,7 +17204,7 @@ function createPrCommand(dependencies = {}) {
16914
17204
  `
16915
17205
  );
16916
17206
  } else {
16917
- await appendHistory(worktreePath, branchName);
17207
+ await recordWorktreeUsage(worktreePath, branchName);
16918
17208
  await writeOutput2(worktreePath, options.stdout);
16919
17209
  }
16920
17210
  return 0;
@@ -17014,7 +17304,14 @@ function createRemoveCommand(dependencies = {}) {
17014
17304
  }
17015
17305
  return 1;
17016
17306
  }
17017
- const selection = options.branch ?? await promptForWorktree2(sortByCurrentFirst(linkedWorktrees));
17307
+ const selection = options.branch ?? await promptForWorktree2(
17308
+ await buildWorktreePromptEntries(
17309
+ linkedWorktrees.map((worktree2) => ({
17310
+ repoName: repository.repoName,
17311
+ worktree: worktree2
17312
+ }))
17313
+ )
17314
+ );
17018
17315
  if (!selection) {
17019
17316
  options.stderr("Aborted\n");
17020
17317
  return 1;
@@ -17062,7 +17359,7 @@ function createRemoveCommand(dependencies = {}) {
17062
17359
  );
17063
17360
  const hooks = extractHooks(config);
17064
17361
  await runHook(
17065
- hooks.beforeRemove,
17362
+ hooks["before-remove"],
17066
17363
  worktree.path,
17067
17364
  {
17068
17365
  branch: worktree.branch ?? void 0,
@@ -17128,15 +17425,7 @@ function createRemoveCommand(dependencies = {}) {
17128
17425
  }
17129
17426
  var runRemoveCommand = createRemoveCommand();
17130
17427
  async function defaultPromptForWorktree2(worktrees) {
17131
- const choice = await ve({
17132
- message: "Choose a worktree to finish",
17133
- options: worktrees.map((worktree) => ({
17134
- hint: worktree.isCurrent ? `${worktree.path} (current)` : worktree.path,
17135
- label: worktree.branch ?? "(detached)",
17136
- value: worktree.path
17137
- }))
17138
- });
17139
- return pD(choice) ? null : choice;
17428
+ return promptForSingleWorktree("Choose a worktree to finish", worktrees);
17140
17429
  }
17141
17430
  async function defaultConfirmRemoval2(worktree) {
17142
17431
  const choice = await ye({
@@ -17180,6 +17469,54 @@ async function runRootCommand(options) {
17180
17469
  return 0;
17181
17470
  }
17182
17471
 
17472
+ // src/run-hook.ts
17473
+ var VALID_HOOKS = [
17474
+ "after-create",
17475
+ "after-enter",
17476
+ "before-remove"
17477
+ ];
17478
+ var CAMEL_ALIASES = {
17479
+ afterCreate: "after-create",
17480
+ afterEnter: "after-enter",
17481
+ beforeRemove: "before-remove"
17482
+ };
17483
+ function isValidHook(hook) {
17484
+ return VALID_HOOKS.includes(hook);
17485
+ }
17486
+ async function runHookCommand(options) {
17487
+ const normalized = CAMEL_ALIASES[options.hook] ?? options.hook;
17488
+ if (!isValidHook(normalized)) {
17489
+ options.stderr(
17490
+ `gji run-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}
17491
+ `
17492
+ );
17493
+ return 1;
17494
+ }
17495
+ const hookName = normalized;
17496
+ const repository = await detectRepository(options.cwd);
17497
+ const config = await loadEffectiveConfig(
17498
+ repository.repoRoot,
17499
+ void 0,
17500
+ options.stderr
17501
+ );
17502
+ const hooks = extractHooks(config);
17503
+ const worktrees = await listWorktrees(options.cwd);
17504
+ const currentWorktree = worktrees.find(
17505
+ (w2) => w2.path === repository.currentRoot
17506
+ );
17507
+ await runHook(
17508
+ hooks[hookName],
17509
+ repository.currentRoot,
17510
+ {
17511
+ branch: currentWorktree?.branch ?? void 0,
17512
+ path: repository.currentRoot,
17513
+ repo: repository.repoName
17514
+ },
17515
+ options.stderr
17516
+ );
17517
+ return 0;
17518
+ }
17519
+
17183
17520
  // src/status.ts
17184
17521
  async function runStatusCommand(options) {
17185
17522
  const repository = await detectRepository(options.cwd);
@@ -17552,48 +17889,6 @@ function expandTilde2(value, home) {
17552
17889
  return value;
17553
17890
  }
17554
17891
 
17555
- // src/trigger-hook.ts
17556
- var VALID_HOOKS = [
17557
- "afterCreate",
17558
- "afterEnter",
17559
- "beforeRemove"
17560
- ];
17561
- function isValidHook(hook) {
17562
- return VALID_HOOKS.includes(hook);
17563
- }
17564
- async function runTriggerHookCommand(options) {
17565
- if (!isValidHook(options.hook)) {
17566
- options.stderr(
17567
- `gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(", ")}
17568
- `
17569
- );
17570
- return 1;
17571
- }
17572
- const hookName = options.hook;
17573
- const repository = await detectRepository(options.cwd);
17574
- const config = await loadEffectiveConfig(
17575
- repository.repoRoot,
17576
- void 0,
17577
- options.stderr
17578
- );
17579
- const hooks = extractHooks(config);
17580
- const worktrees = await listWorktrees(options.cwd);
17581
- const currentWorktree = worktrees.find(
17582
- (w2) => w2.path === repository.currentRoot
17583
- );
17584
- await runHook(
17585
- hooks[hookName],
17586
- repository.currentRoot,
17587
- {
17588
- branch: currentWorktree?.branch ?? void 0,
17589
- path: repository.currentRoot,
17590
- repo: repository.repoName
17591
- },
17592
- options.stderr
17593
- );
17594
- return 0;
17595
- }
17596
-
17597
17892
  // src/cli.ts
17598
17893
  function createProgram() {
17599
17894
  const program2 = new Command();
@@ -17724,9 +18019,9 @@ function registerCommands(program2) {
17724
18019
  "--json",
17725
18020
  "emit JSON on success or error instead of human-readable output"
17726
18021
  ).action(notImplemented("remove"));
17727
- program2.command("trigger-hook <hook>").description(
17728
- "run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree"
17729
- ).action(notImplemented("trigger-hook"));
18022
+ program2.command("run-hook <hook>").alias("trigger-hook").description(
18023
+ "run a named hook (after-create, after-enter, before-remove) in the current worktree"
18024
+ ).action(notImplemented("run-hook"));
17730
18025
  program2.command("warp [branch]").description("jump to any worktree across all known repos").option("-n, --new [branch]", "create a new worktree in a registered repo").option(
17731
18026
  "--print",
17732
18027
  "print the resolved worktree path without changing directory"
@@ -17984,8 +18279,8 @@ function attachCommandActions(program2, options) {
17984
18279
  }
17985
18280
  };
17986
18281
  program2.commands.find((command) => command.name() === "remove")?.action(runRemovalCommand);
17987
- program2.commands.find((command) => command.name() === "trigger-hook")?.action(async (hook) => {
17988
- const exitCode = await runTriggerHookCommand({
18282
+ program2.commands.find((command) => command.name() === "run-hook")?.action(async (hook) => {
18283
+ const exitCode = await runHookCommand({
17989
18284
  cwd: options.cwd,
17990
18285
  hook,
17991
18286
  stderr: options.stderr