@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
package/dist/go.d.ts CHANGED
@@ -1,5 +1,4 @@
1
- import { type WorktreeHealth } from "./git.js";
2
- import { type WorktreeEntry } from "./repo.js";
1
+ import { type WorktreePromptEntry } from "./worktree-picker.js";
3
2
  export interface GoCommandOptions {
4
3
  branch?: string;
5
4
  cwd: string;
@@ -8,8 +7,7 @@ export interface GoCommandOptions {
8
7
  stdout: (chunk: string) => void;
9
8
  }
10
9
  export interface GoCommandDependencies {
11
- promptForWorktree: (worktrees: WorktreeEntry[]) => Promise<string | null>;
10
+ promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
12
11
  }
13
12
  export declare function createGoCommand(dependencies?: Partial<GoCommandDependencies>): (options: GoCommandOptions) => Promise<number>;
14
13
  export declare const runGoCommand: (options: GoCommandOptions) => Promise<number>;
15
- export declare function formatUpstreamHint(branch: string | null, health: WorktreeHealth): string | null;
package/dist/go.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { basename } from "node:path";
2
- import { isCancel, select } from "@clack/prompts";
3
2
  import { loadEffectiveConfig } from "./config.js";
4
- import { readWorktreeHealth } from "./git.js";
5
3
  import { isHeadless } from "./headless.js";
6
- import { appendHistory } from "./history.js";
4
+ import { recordWorktreeUsage } from "./history.js";
7
5
  import { extractHooks, runHook } from "./hooks.js";
8
- import { detectRepository, listWorktrees, sortByCurrentFirst, } from "./repo.js";
6
+ import { detectRepository, listWorktrees } from "./repo.js";
9
7
  import { writeShellOutput } from "./shell-handoff.js";
10
8
  import { resolveWarpTarget } from "./warp.js";
9
+ import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
11
10
  const GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
12
11
  export function createGoCommand(dependencies = {}) {
13
12
  const prompt = dependencies.promptForWorktree ?? promptForWorktree;
@@ -32,7 +31,7 @@ export function createGoCommand(dependencies = {}) {
32
31
  });
33
32
  if (!target)
34
33
  return 1;
35
- appendHistory(target.path, target.branch).catch(() => undefined);
34
+ await recordWorktreeUsage(target.path, target.branch);
36
35
  await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
37
36
  return 0;
38
37
  }
@@ -40,11 +39,19 @@ export function createGoCommand(dependencies = {}) {
40
39
  options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
41
40
  return 1;
42
41
  }
43
- const prompted = options.branch
44
- ? null
45
- : await prompt(sortByCurrentFirst(worktrees));
42
+ const promptSources = worktrees.map((worktree) => ({
43
+ repoName: repository.repoName,
44
+ worktree,
45
+ }));
46
+ const promptEntries = options.branch
47
+ ? []
48
+ : await buildWorktreePromptEntries(promptSources);
49
+ const queried = options.branch
50
+ ? resolveWorktreeQuery(promptSources, options.branch)
51
+ : null;
52
+ const prompted = options.branch ? null : await prompt(promptEntries);
46
53
  const resolvedPath = options.branch
47
- ? worktrees.find((entry) => entry.branch === options.branch)?.path
54
+ ? queried?.worktree.path
48
55
  : (prompted ?? undefined);
49
56
  if (!resolvedPath) {
50
57
  if (options.branch) {
@@ -59,53 +66,17 @@ export function createGoCommand(dependencies = {}) {
59
66
  const chosenWorktree = worktrees.find((w) => w.path === resolvedPath);
60
67
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
61
68
  const hooks = extractHooks(config);
62
- await runHook(hooks.afterEnter, resolvedPath, {
69
+ await runHook(hooks["after-enter"], resolvedPath, {
63
70
  branch: chosenWorktree?.branch ?? undefined,
64
71
  path: resolvedPath,
65
72
  repo: basename(repository.repoRoot),
66
73
  }, options.stderr);
67
- appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => undefined);
74
+ await recordWorktreeUsage(resolvedPath, chosenWorktree?.branch ?? null);
68
75
  await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
69
76
  return 0;
70
77
  };
71
78
  }
72
79
  export const runGoCommand = createGoCommand();
73
80
  async function promptForWorktree(worktrees) {
74
- const healthResults = await Promise.allSettled(worktrees.map((w) => readWorktreeHealth(w.path)));
75
- const choice = await select({
76
- message: "Choose a worktree",
77
- options: worktrees.map((worktree, i) => {
78
- const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
79
- const pathHint = worktree.isCurrent
80
- ? `${worktree.path} (current)`
81
- : worktree.path;
82
- const upstream = health
83
- ? formatUpstreamHint(worktree.branch, health)
84
- : null;
85
- return {
86
- value: worktree.path,
87
- label: worktree.branch ?? "(detached)",
88
- hint: upstream ? `${upstream} · ${pathHint}` : pathHint,
89
- };
90
- }),
91
- });
92
- if (isCancel(choice)) {
93
- return null;
94
- }
95
- return choice;
96
- }
97
- export function formatUpstreamHint(branch, health) {
98
- if (branch === null)
99
- return null;
100
- if (!health.hasUpstream)
101
- return "no upstream";
102
- if (health.upstreamGone)
103
- return "upstream gone";
104
- if (health.ahead === 0 && health.behind === 0)
105
- return "up to date";
106
- if (health.ahead === 0)
107
- return `behind ${health.behind}`;
108
- if (health.behind === 0)
109
- return `ahead ${health.ahead}`;
110
- return `ahead ${health.ahead}, behind ${health.behind}`;
81
+ return promptForSingleWorktree("Choose a worktree", worktrees);
111
82
  }
package/dist/history.d.ts CHANGED
@@ -7,3 +7,4 @@ export interface HistoryEntry {
7
7
  export declare function HISTORY_FILE_PATH(home?: string): string;
8
8
  export declare function loadHistory(home?: string): Promise<HistoryEntry[]>;
9
9
  export declare function appendHistory(path: string, branch: string | null, home?: string): Promise<void>;
10
+ export declare function recordWorktreeUsage(path: string, branch: string | null, home?: string): Promise<void>;
package/dist/history.js CHANGED
@@ -27,15 +27,22 @@ export async function loadHistory(home = homedir()) {
27
27
  export async function appendHistory(path, branch, home = homedir()) {
28
28
  const historyPath = HISTORY_FILE_PATH(home);
29
29
  const existing = await loadHistory(home);
30
- // Skip if the most recent entry is the same path (no-op navigation)
31
- if (existing.length > 0 && existing[0].path === path) {
32
- return;
33
- }
34
30
  const entry = { branch, path, timestamp: Date.now() };
35
- const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
31
+ const next = [
32
+ entry,
33
+ ...existing.filter((existingEntry) => existingEntry.path !== path),
34
+ ].slice(0, MAX_HISTORY_ENTRIES);
36
35
  await mkdir(dirname(historyPath), { recursive: true });
37
36
  await writeFile(historyPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
38
37
  }
38
+ export async function recordWorktreeUsage(path, branch, home = homedir()) {
39
+ try {
40
+ await appendHistory(path, branch, home);
41
+ }
42
+ catch {
43
+ // Usage history is advisory metadata; primary command success should stand.
44
+ }
45
+ }
39
46
  function isHistoryEntry(value) {
40
47
  return (typeof value === "object" &&
41
48
  value !== null &&
package/dist/hooks.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  export type GjiHookCommand = string | string[];
2
2
  export interface GjiHooks {
3
- afterCreate?: GjiHookCommand;
4
- afterEnter?: GjiHookCommand;
5
- beforeRemove?: GjiHookCommand;
3
+ "after-create"?: GjiHookCommand;
4
+ "after-enter"?: GjiHookCommand;
5
+ "before-remove"?: GjiHookCommand;
6
6
  }
7
7
  export interface HookContext {
8
8
  branch?: string;
package/dist/hooks.js CHANGED
@@ -84,9 +84,9 @@ export function extractHooks(config) {
84
84
  }
85
85
  const hooks = raw;
86
86
  return {
87
- afterCreate: parseHookCommand(hooks.afterCreate),
88
- afterEnter: parseHookCommand(hooks.afterEnter),
89
- beforeRemove: parseHookCommand(hooks.beforeRemove),
87
+ "after-create": parseHookCommand(hooks["after-create"] ?? hooks.afterCreate),
88
+ "after-enter": parseHookCommand(hooks["after-enter"] ?? hooks.afterEnter),
89
+ "before-remove": parseHookCommand(hooks["before-remove"] ?? hooks.beforeRemove),
90
90
  };
91
91
  }
92
92
  function parseHookCommand(value) {
package/dist/init.d.ts CHANGED
@@ -3,9 +3,9 @@ export type InstallSaveTarget = "local" | "global";
3
3
  export interface SetupWizardResult {
4
4
  branchPrefix?: string;
5
5
  hooks?: {
6
- afterCreate?: string;
7
- afterEnter?: string;
8
- beforeRemove?: string;
6
+ "after-create"?: string;
7
+ "after-enter"?: string;
8
+ "before-remove"?: string;
9
9
  };
10
10
  installSaveTarget: InstallSaveTarget;
11
11
  worktreePath?: string;
package/dist/init.js CHANGED
@@ -135,12 +135,12 @@ async function saveWizardConfig(result, cwd, home) {
135
135
  if (result.worktreePath)
136
136
  values.worktreePath = result.worktreePath;
137
137
  const hooks = {};
138
- if (result.hooks?.afterCreate)
139
- hooks.afterCreate = result.hooks.afterCreate;
140
- if (result.hooks?.afterEnter)
141
- hooks.afterEnter = result.hooks.afterEnter;
142
- if (result.hooks?.beforeRemove)
143
- hooks.beforeRemove = result.hooks.beforeRemove;
138
+ if (result.hooks?.["after-create"])
139
+ hooks["after-create"] = result.hooks["after-create"];
140
+ if (result.hooks?.["after-enter"])
141
+ hooks["after-enter"] = result.hooks["after-enter"];
142
+ if (result.hooks?.["before-remove"])
143
+ hooks["before-remove"] = result.hooks["before-remove"];
144
144
  if (Object.keys(hooks).length > 0)
145
145
  values.hooks = hooks;
146
146
  if (Object.keys(values).length === 0)
@@ -283,7 +283,7 @@ async function defaultPromptForSetup() {
283
283
  return null;
284
284
  }
285
285
  const afterCreate = await text({
286
- message: "afterCreate hook — run after creating a worktree?",
286
+ message: "after-create hook — run after creating a worktree?",
287
287
  placeholder: "e.g. pnpm install — leave blank to skip",
288
288
  });
289
289
  if (isCancel(afterCreate)) {
@@ -291,7 +291,7 @@ async function defaultPromptForSetup() {
291
291
  return null;
292
292
  }
293
293
  const afterEnter = await text({
294
- message: "afterEnter hook — run after entering a worktree?",
294
+ message: "after-enter hook — run after entering a worktree?",
295
295
  placeholder: "e.g. nvm use — leave blank to skip",
296
296
  });
297
297
  if (isCancel(afterEnter)) {
@@ -299,7 +299,7 @@ async function defaultPromptForSetup() {
299
299
  return null;
300
300
  }
301
301
  const beforeRemove = await text({
302
- message: "beforeRemove hook — run before removing a worktree?",
302
+ message: "before-remove hook — run before removing a worktree?",
303
303
  placeholder: "leave blank to skip",
304
304
  });
305
305
  if (isCancel(beforeRemove)) {
@@ -309,11 +309,11 @@ async function defaultPromptForSetup() {
309
309
  outro("Setup complete!");
310
310
  const hooks = {};
311
311
  if (afterCreate)
312
- hooks.afterCreate = afterCreate;
312
+ hooks["after-create"] = afterCreate;
313
313
  if (afterEnter)
314
- hooks.afterEnter = afterEnter;
314
+ hooks["after-enter"] = afterEnter;
315
315
  if (beforeRemove)
316
- hooks.beforeRemove = beforeRemove;
316
+ hooks["before-remove"] = beforeRemove;
317
317
  return {
318
318
  branchPrefix: branchPrefix || undefined,
319
319
  hooks: Object.keys(hooks).length > 0 ? hooks : undefined,
@@ -2,15 +2,15 @@ import { spawn } from "node:child_process";
2
2
  import { isCancel, select } from "@clack/prompts";
3
3
  import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey, } from "./config.js";
4
4
  import { isHeadless } from "./headless.js";
5
+ import { extractHooks } from "./hooks.js";
5
6
  import { detectPackageManager, } from "./package-manager.js";
6
7
  export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
7
8
  // Skip in non-interactive mode — no prompt can be shown.
8
9
  if (isHeadless() || nonInteractive) {
9
10
  return;
10
11
  }
11
- // Skip if afterCreate hook is already configured in effective config.
12
- const hooks = isPlainObject(config.hooks) ? config.hooks : null;
13
- if (isConfiguredHookCommand(hooks?.afterCreate)) {
12
+ // Skip if an after-create hook is already configured (accepts both kebab and legacy camelCase).
13
+ if (extractHooks(config)["after-create"]) {
14
14
  return;
15
15
  }
16
16
  // Skip if user has permanently opted out of install prompts.
@@ -43,21 +43,30 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
43
43
  try {
44
44
  if (saveGlobal) {
45
45
  // Deep-merge with any existing per-repo global hooks so other keys are preserved.
46
- const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
46
+ const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
47
+ const existing = extractHooks({ hooks: existingRaw });
47
48
  await writeGlobalKey(repoRoot, "hooks", {
48
- ...existingHooks,
49
- afterCreate: pm.installCommand,
49
+ ...(existing["after-enter"] !== undefined && {
50
+ "after-enter": existing["after-enter"],
51
+ }),
52
+ ...(existing["before-remove"] !== undefined && {
53
+ "before-remove": existing["before-remove"],
54
+ }),
55
+ "after-create": pm.installCommand,
50
56
  });
51
57
  }
52
58
  else {
53
- // Read local config hooks to deep-merge so other hook keys (e.g. afterEnter) are preserved.
59
+ // Read local config hooks to deep-merge so other hook keys (e.g. after-enter) are preserved.
54
60
  const { config: localConfig } = await loadConfig(repoRoot);
55
- const existingLocalHooks = isPlainObject(localConfig.hooks)
56
- ? localConfig.hooks
57
- : {};
61
+ const existing = extractHooks(localConfig);
58
62
  await writeKey(repoRoot, "hooks", {
59
- ...existingLocalHooks,
60
- afterCreate: pm.installCommand,
63
+ ...(existing["after-enter"] !== undefined && {
64
+ "after-enter": existing["after-enter"],
65
+ }),
66
+ ...(existing["before-remove"] !== undefined && {
67
+ "before-remove": existing["before-remove"],
68
+ }),
69
+ "after-create": pm.installCommand,
61
70
  });
62
71
  }
63
72
  }
@@ -122,7 +131,7 @@ async function defaultPromptForInstallChoice(pm) {
122
131
  options: [
123
132
  { value: "yes", label: "Yes", hint: "run once" },
124
133
  { value: "no", label: "No", hint: "skip this time" },
125
- { value: "always", label: "Always", hint: "save as afterCreate hook" },
134
+ { value: "always", label: "Always", hint: "save as after-create hook" },
126
135
  {
127
136
  value: "never",
128
137
  label: "Never",
@@ -138,11 +147,3 @@ async function defaultPromptForInstallChoice(pm) {
138
147
  function isPlainObject(value) {
139
148
  return typeof value === "object" && value !== null && !Array.isArray(value);
140
149
  }
141
- function isConfiguredHookCommand(value) {
142
- if (typeof value === "string")
143
- return value.length > 0;
144
- return (Array.isArray(value) &&
145
- value.length > 0 &&
146
- value[0] !== "" &&
147
- value.every((item) => typeof item === "string"));
148
- }
package/dist/new.js CHANGED
@@ -8,7 +8,7 @@ import { pathExists, promptForPathConflict, } from "./conflict.js";
8
8
  import { defaultSpawnEditor, EDITORS } from "./editor.js";
9
9
  import { syncFiles } from "./file-sync.js";
10
10
  import { isHeadless } from "./headless.js";
11
- import { appendHistory } from "./history.js";
11
+ import { recordWorktreeUsage } from "./history.js";
12
12
  import { extractHooks, runHook } from "./hooks.js";
13
13
  import { maybeRunInstallPrompt, } from "./install-prompt.js";
14
14
  import { detectRepository, resolveWorktreePath, validateBranchName, } from "./repo.js";
@@ -110,14 +110,14 @@ export function createNewCommand(dependencies = {}) {
110
110
  else {
111
111
  options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
112
112
  options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
113
- options.stderr(`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks\n`);
113
+ options.stderr(`Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks\n`);
114
114
  }
115
115
  return 1;
116
116
  }
117
117
  else {
118
118
  const choice = await prompt(worktreePath);
119
119
  if (choice === "reuse") {
120
- appendHistory(worktreePath, worktreeName).catch(() => undefined);
120
+ await recordWorktreeUsage(worktreePath, worktreeName);
121
121
  await writeOutput(worktreePath, options.stdout);
122
122
  return 0;
123
123
  }
@@ -161,7 +161,7 @@ export function createNewCommand(dependencies = {}) {
161
161
  }
162
162
  await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
163
163
  const hooks = extractHooks(config);
164
- await runHook(hooks.afterCreate, worktreePath, {
164
+ await runHook(hooks["after-create"], worktreePath, {
165
165
  branch: worktreeName,
166
166
  path: worktreePath,
167
167
  repo: basename(repository.repoRoot),
@@ -170,7 +170,7 @@ export function createNewCommand(dependencies = {}) {
170
170
  options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}\n`);
171
171
  }
172
172
  else {
173
- await appendHistory(worktreePath, worktreeName);
173
+ await recordWorktreeUsage(worktreePath, worktreeName);
174
174
  await writeOutput(worktreePath, options.stdout);
175
175
  }
176
176
  if (options.open) {
@@ -203,6 +203,31 @@ export function generateBranchPlaceholder(random = Math.random) {
203
203
  "lovelace",
204
204
  "nietzsche",
205
205
  "kafka",
206
+ "sappho",
207
+ "aristotle",
208
+ "pythagoras",
209
+ "artemis",
210
+ "apollo",
211
+ "minerva",
212
+ "persephone",
213
+ "icarus",
214
+ "odysseus",
215
+ "murasaki",
216
+ "shakespeare",
217
+ "frida",
218
+ "davinci",
219
+ "kepler",
220
+ "copernicus",
221
+ "faraday",
222
+ "noether",
223
+ "hopper",
224
+ "boole",
225
+ "shannon",
226
+ "gauss",
227
+ "ramanujan",
228
+ "austen",
229
+ "borges",
230
+ "zeno",
206
231
  ];
207
232
  const antics = [
208
233
  "borrowed-a-bike",
@@ -220,8 +245,49 @@ export function generateBranchPlaceholder(random = Math.random) {
220
245
  "washed-the-dishes",
221
246
  "folded-the-laundry",
222
247
  "took-a-nap",
248
+ "lost-a-sock",
249
+ "patched-the-boat",
250
+ "alphabetized-the-spoons",
251
+ "argued-with-the-calendar",
252
+ "misplaced-the-moon",
253
+ "painted-the-fence",
254
+ "overcooked-the-rice",
255
+ "packed-the-snacks",
256
+ "dropped-the-spoon",
257
+ "hid-the-remote",
258
+ "untangled-the-cables",
259
+ "rebooted-the-kettle",
260
+ "indexed-the-attic",
261
+ "forgot-the-password",
262
+ "sorted-the-buttons",
263
+ "mopped-the-ceiling",
264
+ "polished-the-doorknob",
265
+ "misread-the-map",
266
+ "reheated-the-tea",
267
+ "fixed-the-squeak",
268
+ "labeled-the-drawer",
269
+ "stacked-the-chairs",
270
+ "overslept-the-standup",
271
+ "claimed-the-last-bagel",
272
+ "debugged-the-toaster",
223
273
  ];
224
- return `${pickRandom(roots, random)}-${pickRandom(antics, random)}`;
274
+ const root = pickRandom(roots, random);
275
+ const antic = pickRandom(antics, random);
276
+ const suffix = generateBranchPlaceholderSuffix(random);
277
+ return `${root}-${antic}-${suffix}`;
278
+ }
279
+ function pickRandom(values, random) {
280
+ const index = Math.floor(random() * values.length);
281
+ return values[Math.min(index, values.length - 1)];
282
+ }
283
+ function generateBranchPlaceholderSuffix(random) {
284
+ const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
285
+ let suffix = "";
286
+ for (let index = 0; index < 3; index += 1) {
287
+ const characterIndex = Math.floor(random() * characters.length);
288
+ suffix += characters[Math.min(characterIndex, characters.length - 1)];
289
+ }
290
+ return suffix;
225
291
  }
226
292
  function applyConfiguredBranchPrefix(branch, branchPrefix) {
227
293
  if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
@@ -258,10 +324,6 @@ async function defaultPromptForBranch(placeholder) {
258
324
  }
259
325
  return choice.trim();
260
326
  }
261
- function pickRandom(values, random) {
262
- const index = Math.floor(random() * values.length);
263
- return values[Math.min(index, values.length - 1)];
264
- }
265
327
  async function localBranchExists(repoRoot, branchName) {
266
328
  try {
267
329
  await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
package/dist/open.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type EditorDefinition } from "./editor.js";
2
- import { type WorktreeEntry } from "./repo.js";
2
+ import { type WorktreePromptEntry } from "./worktree-picker.js";
3
3
  export type { EditorDefinition };
4
4
  export interface OpenCommandOptions {
5
5
  branch?: string;
@@ -13,7 +13,7 @@ export interface OpenCommandOptions {
13
13
  export interface OpenCommandDependencies {
14
14
  detectEditors: () => Promise<EditorDefinition[]>;
15
15
  promptForEditor: (editors: EditorDefinition[]) => Promise<string | null>;
16
- promptForWorktree: (worktrees: WorktreeEntry[]) => Promise<string | null>;
16
+ promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
17
17
  spawnEditor: (cli: string, args: string[]) => Promise<void>;
18
18
  }
19
19
  export declare function createOpenCommand(dependencies?: Partial<OpenCommandDependencies>): (options: OpenCommandOptions) => Promise<number>;
package/dist/open.js CHANGED
@@ -6,7 +6,9 @@ import { isCancel, select } from "@clack/prompts";
6
6
  import { loadEffectiveConfig, resolveConfigString, updateGlobalConfigKey, } from "./config.js";
7
7
  import { defaultSpawnEditor, EDITORS, } from "./editor.js";
8
8
  import { isHeadless } from "./headless.js";
9
- import { detectRepository, listWorktrees, sortByCurrentFirst, } from "./repo.js";
9
+ import { recordWorktreeUsage } from "./history.js";
10
+ import { detectRepository, listWorktrees } from "./repo.js";
11
+ import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
10
12
  const execFileAsync = promisify(execFile);
11
13
  export function createOpenCommand(dependencies = {}) {
12
14
  const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
@@ -20,25 +22,36 @@ export function createOpenCommand(dependencies = {}) {
20
22
  ]);
21
23
  // Resolve target worktree path.
22
24
  let targetPath;
25
+ let targetWorktree;
23
26
  if (options.branch) {
24
- const entry = worktrees.find((w) => w.branch === options.branch);
25
- if (!entry) {
26
- options.stderr(`gji open: no worktree found for branch: ${options.branch}\n`);
27
+ const match = resolveWorktreeQuery(worktrees.map((worktree) => ({
28
+ repoName: repository.repoName,
29
+ worktree,
30
+ })), options.branch);
31
+ if (!match) {
32
+ options.stderr(`gji open: no worktree found matching: ${options.branch}\n`);
27
33
  options.stderr(`Hint: Use 'gji ls' to see available worktrees\n`);
28
34
  return 1;
29
35
  }
30
- targetPath = entry.path;
36
+ targetPath = match.worktree.path;
37
+ targetWorktree = match.worktree;
31
38
  }
32
39
  else if (isHeadless()) {
33
- targetPath = worktrees.find((w) => w.isCurrent)?.path ?? options.cwd;
40
+ targetWorktree = worktrees.find((w) => w.isCurrent);
41
+ targetPath = targetWorktree?.path ?? options.cwd;
34
42
  }
35
43
  else {
36
- const chosen = await promptForWorktree(sortByCurrentFirst(worktrees));
44
+ const entries = await buildWorktreePromptEntries(worktrees.map((worktree) => ({
45
+ repoName: repository.repoName,
46
+ worktree,
47
+ })));
48
+ const chosen = await promptForWorktree(entries);
37
49
  if (!chosen) {
38
50
  options.stderr("Aborted\n");
39
51
  return 1;
40
52
  }
41
53
  targetPath = chosen;
54
+ targetWorktree = worktrees.find((w) => w.path === chosen);
42
55
  }
43
56
  // Resolve which editor to use.
44
57
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
@@ -100,6 +113,7 @@ export function createOpenCommand(dependencies = {}) {
100
113
  return 1;
101
114
  }
102
115
  const displayName = editorDef?.name ?? editorCli;
116
+ await recordWorktreeUsage(targetPath, targetWorktree?.branch ?? null);
103
117
  options.stdout(`Opened ${targetPath} in ${displayName}\n`);
104
118
  return 0;
105
119
  };
@@ -122,17 +136,7 @@ async function isCommandAvailable(command) {
122
136
  }
123
137
  }
124
138
  async function defaultPromptForWorktree(worktrees) {
125
- const choice = await select({
126
- message: "Choose a worktree to open",
127
- options: worktrees.map((w) => ({
128
- value: w.path,
129
- label: w.branch ?? "(detached)",
130
- hint: w.isCurrent ? `${w.path} (current)` : w.path,
131
- })),
132
- });
133
- if (isCancel(choice))
134
- return null;
135
- return choice;
139
+ return promptForSingleWorktree("Choose a worktree to open", worktrees);
136
140
  }
137
141
  async function defaultPromptForEditor(editors) {
138
142
  const choice = await select({
package/dist/pr.js CHANGED
@@ -6,7 +6,7 @@ import { loadEffectiveConfig, resolveConfigString } from "./config.js";
6
6
  import { pathExists, promptForPathConflict, } from "./conflict.js";
7
7
  import { syncFiles } from "./file-sync.js";
8
8
  import { isHeadless } from "./headless.js";
9
- import { appendHistory } from "./history.js";
9
+ import { recordWorktreeUsage } from "./history.js";
10
10
  import { extractHooks, runHook } from "./hooks.js";
11
11
  import { maybeRunInstallPrompt, } from "./install-prompt.js";
12
12
  import { detectRepository, resolveWorktreePath } from "./repo.js";
@@ -61,7 +61,7 @@ export function createPrCommand(dependencies = {}) {
61
61
  }
62
62
  const choice = await prompt(worktreePath);
63
63
  if (choice === "reuse") {
64
- appendHistory(worktreePath, branchName).catch(() => undefined);
64
+ await recordWorktreeUsage(worktreePath, branchName);
65
65
  await writeOutput(worktreePath, options.stdout);
66
66
  return 0;
67
67
  }
@@ -111,7 +111,7 @@ export function createPrCommand(dependencies = {}) {
111
111
  }
112
112
  await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
113
113
  const hooks = extractHooks(config);
114
- await runHook(hooks.afterCreate, worktreePath, {
114
+ await runHook(hooks["after-create"], worktreePath, {
115
115
  branch: branchName,
116
116
  path: worktreePath,
117
117
  repo: basename(repository.repoRoot),
@@ -120,7 +120,7 @@ export function createPrCommand(dependencies = {}) {
120
120
  options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}\n`);
121
121
  }
122
122
  else {
123
- await appendHistory(worktreePath, branchName);
123
+ await recordWorktreeUsage(worktreePath, branchName);
124
124
  await writeOutput(worktreePath, options.stdout);
125
125
  }
126
126
  return 0;
package/dist/remove.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { type WorktreeEntry } from "./repo.js";
1
+ import type { WorktreeEntry } from "./repo.js";
2
+ import { type WorktreePromptEntry } from "./worktree-picker.js";
2
3
  export interface RemoveCommandOptions {
3
4
  branch?: string;
4
5
  cwd: string;
@@ -12,7 +13,7 @@ export interface RemoveCommandDependencies {
12
13
  confirmForceDeleteBranch: (branch: string) => Promise<boolean>;
13
14
  confirmForceRemoveWorktree: (worktreePath: string) => Promise<boolean>;
14
15
  confirmRemoval: (worktree: WorktreeEntry) => Promise<boolean>;
15
- promptForWorktree: (worktrees: WorktreeEntry[]) => Promise<string | null>;
16
+ promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
16
17
  }
17
18
  export declare function createRemoveCommand(dependencies?: Partial<RemoveCommandDependencies>): (options: RemoveCommandOptions) => Promise<number>;
18
19
  export declare const runRemoveCommand: (options: RemoveCommandOptions) => Promise<number>;