@solaqua/gji 0.8.1 → 0.9.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 (49) hide show
  1. package/README.md +24 -30
  2. package/dist/browser.d.ts +7 -0
  3. package/dist/browser.js +37 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +59 -4
  6. package/dist/doctor.d.ts +28 -0
  7. package/dist/doctor.js +454 -0
  8. package/dist/gji-bundle.mjs +2453 -350
  9. package/dist/go.d.ts +2 -1
  10. package/dist/go.js +4 -1
  11. package/dist/index.js +12 -6
  12. package/dist/init.d.ts +15 -0
  13. package/dist/init.js +232 -16
  14. package/dist/open.d.ts +2 -1
  15. package/dist/open.js +2 -1
  16. package/dist/pr-open.d.ts +19 -0
  17. package/dist/pr-open.js +290 -0
  18. package/dist/pull-requests.d.ts +33 -0
  19. package/dist/pull-requests.js +384 -0
  20. package/dist/repo-registry.d.ts +5 -0
  21. package/dist/repo-registry.js +87 -14
  22. package/dist/shell-completion.js +278 -10
  23. package/dist/shell-setup.d.ts +9 -0
  24. package/dist/shell-setup.js +56 -0
  25. package/dist/warp.d.ts +3 -0
  26. package/dist/warp.js +8 -3
  27. package/dist/worktree-picker.d.ts +12 -1
  28. package/dist/worktree-picker.js +84 -6
  29. package/man/man1/gji-back.1 +1 -1
  30. package/man/man1/gji-clean.1 +1 -1
  31. package/man/man1/gji-completion.1 +1 -1
  32. package/man/man1/gji-config.1 +1 -1
  33. package/man/man1/gji-doctor.1 +19 -0
  34. package/man/man1/gji-go.1 +1 -1
  35. package/man/man1/gji-history.1 +1 -1
  36. package/man/man1/gji-init.1 +6 -3
  37. package/man/man1/gji-ls.1 +1 -1
  38. package/man/man1/gji-new.1 +1 -1
  39. package/man/man1/gji-open.1 +1 -1
  40. package/man/man1/gji-pr.1 +5 -1
  41. package/man/man1/gji-remove.1 +1 -1
  42. package/man/man1/gji-root.1 +1 -1
  43. package/man/man1/gji-run-hook.1 +1 -1
  44. package/man/man1/gji-status.1 +1 -1
  45. package/man/man1/gji-sync-files.1 +1 -1
  46. package/man/man1/gji-sync.1 +1 -1
  47. package/man/man1/gji-warp.1 +1 -1
  48. package/man/man1/gji.1 +6 -2
  49. package/package.json +1 -1
@@ -4,7 +4,14 @@ const TOP_LEVEL_COMMANDS = [
4
4
  name: "new",
5
5
  description: "create a new branch or detached linked worktree",
6
6
  },
7
- { name: "init", description: "print or install shell integration" },
7
+ {
8
+ name: "init",
9
+ description: "set up shell integration interactively or print a shell wrapper",
10
+ },
11
+ {
12
+ name: "doctor",
13
+ description: "check gji installation and configuration health",
14
+ },
8
15
  { name: "completion", description: "print shell completion definitions" },
9
16
  { name: "pr", description: "fetch a pull request into a linked worktree" },
10
17
  { name: "back", description: "navigate to the previously visited worktree" },
@@ -55,6 +62,67 @@ function renderBashCompletion() {
55
62
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
56
63
  }
57
64
 
65
+ __gji_pr_targets_cache=""
66
+ __gji_pr_targets_cache_at=0
67
+ __gji_pr_targets_cache_loaded=0
68
+ __gji_pr_targets_cache_repo=""
69
+
70
+ __gji_run_bounded() {
71
+ local output_file pid status attempt
72
+ output_file=$(mktemp "\${TMPDIR:-/tmp}/gji-completion.XXXXXX") || return 124
73
+ "$@" >"$output_file" 2>/dev/null &
74
+ pid=$!
75
+ for ((attempt = 0; attempt < 20; attempt++)); do
76
+ if ! kill -0 "$pid" 2>/dev/null; then
77
+ wait "$pid"
78
+ status=$?
79
+ cat "$output_file"
80
+ rm -f "$output_file"
81
+ return "$status"
82
+ fi
83
+ sleep 0.1
84
+ done
85
+ kill "$pid" 2>/dev/null
86
+ wait "$pid" 2>/dev/null
87
+ rm -f "$output_file"
88
+ return 124
89
+ }
90
+
91
+ __gji_pr_targets() {
92
+ local now repo_key branches targets
93
+ now=$(date +%s)
94
+ repo_key=$(command git rev-parse --show-toplevel 2>/dev/null)
95
+ if [ "$__gji_pr_targets_cache_loaded" -eq 1 ] && [ "$repo_key" = "$__gji_pr_targets_cache_repo" ] && [ "$((now - __gji_pr_targets_cache_at))" -lt 30 ]; then
96
+ printf '%s\n' "$__gji_pr_targets_cache"
97
+ return
98
+ fi
99
+
100
+ branches="$(__gji_worktree_branches)"
101
+ targets=""
102
+ if [ -n "$repo_key" ] && command -v gh >/dev/null 2>&1; then
103
+ targets=$(__gji_run_bounded env GH_PROMPT_DISABLED=1 gh pr list --state open --json number --limit 100 |
104
+ tr '{},' '\\n' | sed -n 's/.*"number":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')
105
+ elif [ -n "$repo_key" ] && command -v glab >/dev/null 2>&1; then
106
+ targets=$(__gji_run_bounded env GLAB_NON_INTERACTIVE=1 glab mr list --state opened --output json --per-page 100 |
107
+ tr '{},' '\\n' | sed -n 's/.*"iid":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')
108
+ elif [ -n "$repo_key" ] && command -v bb >/dev/null 2>&1; then
109
+ targets=$(__gji_run_bounded bb pr list --state OPEN --format json |
110
+ tr '{},' '\\n' | sed -n 's/.*"id":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')
111
+ fi
112
+
113
+ __gji_pr_targets_cache="$branches"
114
+ if [ -n "$targets" ]; then
115
+ if [ -n "$__gji_pr_targets_cache" ]; then
116
+ __gji_pr_targets_cache+=$'\n'
117
+ fi
118
+ __gji_pr_targets_cache+="$targets"
119
+ fi
120
+ __gji_pr_targets_cache_at="$now"
121
+ __gji_pr_targets_cache_loaded=1
122
+ __gji_pr_targets_cache_repo="$repo_key"
123
+ printf '%s\n' "$__gji_pr_targets_cache"
124
+ }
125
+
58
126
  _gji_completion() {
59
127
  local cur command_name
60
128
  COMPREPLY=()
@@ -72,13 +140,37 @@ _gji_completion() {
72
140
  COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
73
141
  ;;
74
142
  init)
75
- COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
143
+ COMPREPLY=( $(compgen -W "${shells} --write --json --help" -- "$cur") )
144
+ ;;
145
+ doctor)
146
+ COMPREPLY=( $(compgen -W "--fix --yes --json --help" -- "$cur") )
76
147
  ;;
77
148
  completion)
78
149
  COMPREPLY=( $(compgen -W "${shells} --help" -- "$cur") )
79
150
  ;;
80
151
  pr)
81
- COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
152
+ if [ "$COMP_CWORD" -eq 2 ]; then
153
+ COMPREPLY=( $(compgen -W "open --dry-run --json --help" -- "$cur") )
154
+ elif [ "\${COMP_WORDS[2]}" = "open" ]; then
155
+ local has_select=0 has_target=0 word
156
+ for word in "\${COMP_WORDS[@]:3:$((COMP_CWORD - 3))}"; do
157
+ if [ "$word" = "--select" ]; then has_select=1; fi
158
+ if [[ "$word" != -* && "$word" != "" ]]; then has_target=1; fi
159
+ done
160
+ if [[ "$cur" == -* ]]; then
161
+ if [[ "$has_select" -eq 1 || "$has_target" -eq 1 ]]; then
162
+ COMPREPLY=( $(compgen -W "--help" -- "$cur") )
163
+ else
164
+ COMPREPLY=( $(compgen -W "--select --help" -- "$cur") )
165
+ fi
166
+ elif [[ "$has_select" -eq 1 || "$has_target" -eq 1 ]]; then
167
+ COMPREPLY=()
168
+ else
169
+ COMPREPLY=( $(compgen -W "$(__gji_pr_targets) --select --help" -- "$cur") )
170
+ fi
171
+ else
172
+ COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
173
+ fi
82
174
  ;;
83
175
  back)
84
176
  COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
@@ -155,6 +247,101 @@ function renderFishCompletion() {
155
247
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
156
248
  end
157
249
 
250
+ set -g __gji_pr_targets_cache
251
+ set -g __gji_pr_targets_cache_at 0
252
+ set -g __gji_pr_targets_cache_loaded 0
253
+ set -g __gji_pr_targets_cache_repo
254
+
255
+ function __gji_run_bounded
256
+ set -l output_file (mktemp "$TMPDIR/gji-completion.XXXXXX" 2>/dev/null)
257
+ if test -z "$output_file"
258
+ set output_file (mktemp "/tmp/gji-completion.XXXXXX" 2>/dev/null)
259
+ end
260
+ if test -z "$output_file"
261
+ return 124
262
+ end
263
+
264
+ command $argv >$output_file 2>/dev/null &
265
+ set -l pid $last_pid
266
+ for attempt in (seq 1 20)
267
+ if not kill -0 $pid 2>/dev/null
268
+ wait $pid
269
+ set -l exit_status $status
270
+ command cat $output_file
271
+ command rm -f $output_file
272
+ return $exit_status
273
+ end
274
+ sleep 0.1
275
+ end
276
+ kill $pid 2>/dev/null
277
+ wait $pid 2>/dev/null
278
+ command rm -f $output_file
279
+ return 124
280
+ end
281
+
282
+ function __gji_pr_targets
283
+ set -l now (date +%s)
284
+ set -l repo_key (command git rev-parse --show-toplevel 2>/dev/null)
285
+ if test $__gji_pr_targets_cache_loaded -eq 1; and test "$repo_key" = "$__gji_pr_targets_cache_repo"; and test (math $now - $__gji_pr_targets_cache_at) -lt 30
286
+ printf '%s\n' $__gji_pr_targets_cache
287
+ return
288
+ end
289
+
290
+ set -l branches (__gji_worktree_branches)
291
+ set -l targets
292
+ if test -n "$repo_key"; and command -q gh
293
+ set targets (__gji_run_bounded env GH_PROMPT_DISABLED=1 gh pr list --state open --json number --limit 100 |
294
+ string replace -a -r '[{},]' '\\n' |
295
+ string match -r '"number"[[:space:]]*:[[:space:]]*[0-9]+' | string replace -r '.*:[[:space:]]*' '')
296
+ else if test -n "$repo_key"; and command -q glab
297
+ set targets (__gji_run_bounded env GLAB_NON_INTERACTIVE=1 glab mr list --state opened --output json --per-page 100 |
298
+ string replace -a -r '[{},]' '\\n' |
299
+ string match -r '"iid"[[:space:]]*:[[:space:]]*[0-9]+' | string replace -r '.*:[[:space:]]*' '')
300
+ else if test -n "$repo_key"; and command -q bb
301
+ set targets (__gji_run_bounded bb pr list --state OPEN --format json |
302
+ string replace -a -r '[{},]' '\\n' |
303
+ string match -r '"id"[[:space:]]*:[[:space:]]*[0-9]+' | string replace -r '.*:[[:space:]]*' '')
304
+ end
305
+
306
+ set -g __gji_pr_targets_cache $branches $targets
307
+ set -g __gji_pr_targets_cache_at $now
308
+ set -g __gji_pr_targets_cache_loaded 1
309
+ set -g __gji_pr_targets_cache_repo $repo_key
310
+ printf '%s\n' $__gji_pr_targets_cache
311
+ end
312
+
313
+ function __gji_should_complete_pr_select
314
+ set -l tokens (commandline -opc)
315
+ if test (count $tokens) -lt 3; or test $tokens[2] != pr; or test $tokens[3] != open
316
+ return 1
317
+ end
318
+ for token in $tokens[4..-1]
319
+ if test $token = --select; or not string match -q -- '-*' $token
320
+ return 1
321
+ end
322
+ end
323
+ return 0
324
+ end
325
+
326
+ function __gji_should_complete_pr_target
327
+ set -l tokens (commandline -opc)
328
+ if test (count $tokens) -lt 3; or test $tokens[2] != pr; or test $tokens[3] != open
329
+ return 1
330
+ end
331
+ if string match -q -- '-*' (commandline -ct)
332
+ return 1
333
+ end
334
+ for token in $tokens[4..-1]
335
+ if test $token = --select
336
+ return 1
337
+ end
338
+ if not string match -q -- '-*' $token
339
+ return 1
340
+ end
341
+ end
342
+ return 0
343
+ end
344
+
158
345
  function __gji_should_complete_config_action
159
346
  set -l tokens (commandline -opc)
160
347
  test (count $tokens) -eq 2
@@ -184,14 +371,22 @@ complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what wo
184
371
  complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
185
372
 
186
373
  complete -c gji -n '__fish_seen_subcommand_from init' -l write -d 'write the integration to the shell config file'
374
+ complete -c gji -n '__fish_seen_subcommand_from init' -l json -d 'emit a JSON error in non-interactive mode'
187
375
  ${shellLines}
188
376
 
377
+ complete -c gji -n '__fish_seen_subcommand_from doctor' -l fix -d 'apply safe automatic fixes after showing the plan'
378
+ complete -c gji -n '__fish_seen_subcommand_from doctor' -l yes -d 'apply --fix without prompting'
379
+ complete -c gji -n '__fish_seen_subcommand_from doctor' -l json -d 'emit diagnostic checks as JSON'
380
+
189
381
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'bash' -d 'shell'
190
382
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'fish' -d 'shell'
191
383
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
192
384
 
193
- complete -c gji -n '__fish_seen_subcommand_from pr' -l dry-run -d 'show what would be created without executing any git commands or writing files'
194
- complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
385
+ complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l dry-run -d 'show what would be created without executing any git commands or writing files'
386
+ complete -c gji -n '__fish_seen_subcommand_from pr; and test (commandline -opc)[3] != open' -l json -d 'emit JSON on success or error instead of human-readable output'
387
+ complete -c gji -n '__fish_seen_subcommand_from pr' -a 'open' -d 'open a pull request in the default browser'
388
+ complete -c gji -n '__gji_should_complete_pr_select' -l select -d 'choose a pull request from any linked worktree'
389
+ complete -c gji -n '__gji_should_complete_pr_target' -a '(__gji_pr_targets)' -d 'branch or PR number'
195
390
 
196
391
  complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
197
392
 
@@ -251,15 +446,71 @@ __gji_worktree_branches() {
251
446
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
252
447
  }
253
448
 
449
+ typeset -g __gji_pr_targets_cache=()
450
+ typeset -g __gji_pr_targets_cache_at=0
451
+ typeset -g __gji_pr_targets_cache_loaded=0
452
+ typeset -g __gji_pr_targets_cache_repo=""
453
+
454
+ __gji_run_bounded() {
455
+ local output_file pid status attempt
456
+ output_file=$(mktemp "\${TMPDIR:-/tmp}/gji-completion.XXXXXX") || return 124
457
+ "$@" >"$output_file" 2>/dev/null &
458
+ pid=$!
459
+ for ((attempt = 0; attempt < 20; attempt++)); do
460
+ if ! kill -0 "$pid" 2>/dev/null; then
461
+ wait "$pid"
462
+ status=$?
463
+ cat "$output_file"
464
+ rm -f "$output_file"
465
+ return "$status"
466
+ fi
467
+ sleep 0.1
468
+ done
469
+ kill "$pid" 2>/dev/null
470
+ wait "$pid" 2>/dev/null
471
+ rm -f "$output_file"
472
+ return 124
473
+ }
474
+
475
+ __gji_pr_targets() {
476
+ local now repo_key
477
+ local -a branches targets
478
+ now=$(date +%s)
479
+ repo_key=$(command git rev-parse --show-toplevel 2>/dev/null)
480
+ if (( __gji_pr_targets_cache_loaded == 1 && "\${repo_key}" == "\${__gji_pr_targets_cache_repo}" && now - __gji_pr_targets_cache_at < 30 )); then
481
+ print -rl -- "\${__gji_pr_targets_cache[@]}"
482
+ return
483
+ fi
484
+
485
+ branches=("\${(@f)$(__gji_worktree_branches)}")
486
+ targets=()
487
+ if [[ -n "\${repo_key}" && $+commands[gh] -eq 1 ]]; then
488
+ targets=("\${(@f)$(__gji_run_bounded env GH_PROMPT_DISABLED=1 gh pr list --state open --json number --limit 100 |
489
+ tr '{},' '\\n' | sed -n 's/.*"number":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')}")
490
+ elif [[ -n "\${repo_key}" && $+commands[glab] -eq 1 ]]; then
491
+ targets=("\${(@f)$(__gji_run_bounded env GLAB_NON_INTERACTIVE=1 glab mr list --state opened --output json --per-page 100 |
492
+ tr '{},' '\\n' | sed -n 's/.*"iid":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')}")
493
+ elif [[ -n "\${repo_key}" && $+commands[bb] -eq 1 ]]; then
494
+ targets=("\${(@f)$(__gji_run_bounded bb pr list --state OPEN --format json |
495
+ tr '{},' '\\n' | sed -n 's/.*"id":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')}")
496
+ fi
497
+
498
+ __gji_pr_targets_cache=("\${branches[@]}" "\${targets[@]}")
499
+ __gji_pr_targets_cache_at="$now"
500
+ __gji_pr_targets_cache_loaded=1
501
+ __gji_pr_targets_cache_repo="$repo_key"
502
+ print -rl -- "\${__gji_pr_targets_cache[@]}"
503
+ }
504
+
254
505
  local context state line
255
- local -a commands worktree_branches
506
+ local -a command_entries worktree_branches
256
507
 
257
- commands=(
508
+ command_entries=(
258
509
  ${commandLines}
259
510
  )
260
511
 
261
512
  if (( CURRENT == 2 )); then
262
- _describe 'command' commands
513
+ _describe 'command' command_entries
263
514
  return
264
515
  fi
265
516
 
@@ -268,13 +519,26 @@ case "\${words[2]}" in
268
519
  _arguments '--detached[create a detached worktree without a branch]' '--force[remove and recreate the worktree if the target path already exists]' '--open[open the new worktree in an editor after creation]' '--editor[editor CLI to use with --open (code, cursor, zed, …)]:editor:' '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
269
520
  ;;
270
521
  init)
271
- _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
522
+ _arguments '--write[write the integration to the shell config file]' '--json[emit a JSON error in non-interactive mode]' '2:shell:(${shells})'
523
+ ;;
524
+ doctor)
525
+ _arguments '--fix[apply safe automatic fixes after showing the plan]' '--yes[apply --fix without prompting]' '--json[emit diagnostic checks as JSON]'
272
526
  ;;
273
527
  completion)
274
528
  _arguments '2:shell:(${shells})'
275
529
  ;;
276
530
  pr)
277
- _arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref: '
531
+ if [[ "\${words[3]}" == "open" ]]; then
532
+ if (( \${words[(I)--select]} )); then
533
+ _arguments
534
+ elif [[ -n "\${words[4]}" && "\${words[4]}" != -* ]]; then
535
+ _arguments
536
+ else
537
+ _arguments '--select[choose a pull request from any linked worktree]' '4:branch or PR number:->pr_targets'
538
+ fi
539
+ else
540
+ _arguments '--dry-run[show what would be created without executing any git commands or writing files]' '--json[emit JSON on success or error instead of human-readable output]' '2:ref:(open)'
541
+ fi
278
542
  ;;
279
543
  back)
280
544
  _arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
@@ -337,6 +601,10 @@ case "$state" in
337
601
  worktree_branches=(\${(@f)$(__gji_worktree_branches)})
338
602
  _describe 'worktree branch' worktree_branches
339
603
  ;;
604
+ pr_targets)
605
+ worktree_branches=(\${(@f)$(__gji_pr_targets)})
606
+ _describe 'branch or PR number' worktree_branches
607
+ ;;
340
608
  config_keys)
341
609
  _values 'config key' ${configKeys}
342
610
  ;;
@@ -0,0 +1,9 @@
1
+ import type { SupportedShell } from "./shell.js";
2
+ export declare const SHELL_INTEGRATION_START_MARKER = "# >>> gji init >>>";
3
+ export declare const SHELL_INTEGRATION_END_MARKER = "# <<< gji init <<<";
4
+ export declare const ONBOARDING_SHELL_INTEGRATION_START_MARKER = "# >>> gji shell integration >>>";
5
+ export declare const ONBOARDING_SHELL_INTEGRATION_END_MARKER = "# <<< gji shell integration <<<";
6
+ export declare function executableExists(command: string): Promise<boolean>;
7
+ export declare function resolveShellConfigPath(shell: SupportedShell, home: string): string;
8
+ export declare function resolveCompletionPath(shell: SupportedShell, home: string): string;
9
+ export declare function hasShellIntegration(config: string, shell: SupportedShell): boolean;
@@ -0,0 +1,56 @@
1
+ import { constants } from "node:fs";
2
+ import { access } from "node:fs/promises";
3
+ import { delimiter, join } from "node:path";
4
+ export const SHELL_INTEGRATION_START_MARKER = "# >>> gji init >>>";
5
+ export const SHELL_INTEGRATION_END_MARKER = "# <<< gji init <<<";
6
+ export const ONBOARDING_SHELL_INTEGRATION_START_MARKER = "# >>> gji shell integration >>>";
7
+ export const ONBOARDING_SHELL_INTEGRATION_END_MARKER = "# <<< gji shell integration <<<";
8
+ export async function executableExists(command) {
9
+ const candidates = command.includes("/")
10
+ ? [command]
11
+ : (process.env.PATH ?? "")
12
+ .split(delimiter)
13
+ .filter(Boolean)
14
+ .map((directory) => join(directory, command));
15
+ for (const path of candidates) {
16
+ try {
17
+ await access(path, constants.X_OK);
18
+ return true;
19
+ }
20
+ catch {
21
+ // Continue until a matching executable is found.
22
+ }
23
+ }
24
+ return false;
25
+ }
26
+ export function resolveShellConfigPath(shell, home) {
27
+ switch (shell) {
28
+ case "bash":
29
+ return join(home, ".bashrc");
30
+ case "fish":
31
+ return join(home, ".config", "fish", "config.fish");
32
+ case "zsh":
33
+ return join(home, ".zshrc");
34
+ }
35
+ }
36
+ export function resolveCompletionPath(shell, home) {
37
+ switch (shell) {
38
+ case "bash":
39
+ return join(home, ".local", "share", "bash-completion", "completions", "gji");
40
+ case "fish":
41
+ return join(home, ".config", "fish", "completions", "gji.fish");
42
+ case "zsh":
43
+ return join(home, ".zsh", "completions", "_gji");
44
+ }
45
+ }
46
+ export function hasShellIntegration(config, shell) {
47
+ if ((config.includes(SHELL_INTEGRATION_START_MARKER) &&
48
+ config.includes(SHELL_INTEGRATION_END_MARKER)) ||
49
+ (config.includes(ONBOARDING_SHELL_INTEGRATION_START_MARKER) &&
50
+ config.includes(ONBOARDING_SHELL_INTEGRATION_END_MARKER))) {
51
+ return true;
52
+ }
53
+ if (shell === "fish")
54
+ return false;
55
+ return new RegExp(`^[\\t ]*eval\\s+["']?\\$\\(\\s*gji\\s+init\\s+${shell}\\s*\\)["']?\\s*(?:#.*)?$`, "m").test(config);
56
+ }
package/dist/warp.d.ts CHANGED
@@ -1,8 +1,10 @@
1
+ import { type QueryWorktreePullRequests } from "./worktree-picker.js";
1
2
  export interface WarpCommandOptions {
2
3
  branch?: string;
3
4
  cwd: string;
4
5
  json?: boolean;
5
6
  newWorktree?: boolean;
7
+ queryPullRequests?: QueryWorktreePullRequests;
6
8
  stderr: (chunk: string) => void;
7
9
  stdout: (chunk: string) => void;
8
10
  }
@@ -16,5 +18,6 @@ export declare function resolveWarpTarget(options: {
16
18
  commandName?: string;
17
19
  cwd: string;
18
20
  json?: boolean;
21
+ queryPullRequests?: QueryWorktreePullRequests;
19
22
  stderr: (chunk: string) => void;
20
23
  }): Promise<WarpTarget | null>;
package/dist/warp.js CHANGED
@@ -36,6 +36,7 @@ async function runWarpNavigate(options) {
36
36
  ...options,
37
37
  commandName: "gji warp",
38
38
  json: options.json,
39
+ queryPullRequests: options.queryPullRequests,
39
40
  });
40
41
  if (!target)
41
42
  return 1;
@@ -152,15 +153,16 @@ export async function resolveWarpTarget(options) {
152
153
  }
153
154
  const results = await Promise.allSettled(registry.map(async (entry) => {
154
155
  const worktrees = await listWorktrees(entry.path);
155
- return { repoName: entry.name, worktrees };
156
+ return { repoName: entry.name, repoRoot: entry.path, worktrees };
156
157
  }));
157
158
  const allItems = [];
158
159
  for (const result of results) {
159
160
  if (result.status === "rejected")
160
161
  continue;
161
- const { repoName, worktrees } = result.value;
162
+ const { repoName, repoRoot, worktrees } = result.value;
162
163
  for (const worktree of worktrees) {
163
164
  allItems.push({
165
+ repoRoot,
164
166
  repoName,
165
167
  worktree: {
166
168
  ...worktree,
@@ -174,6 +176,7 @@ export async function resolveWarpTarget(options) {
174
176
  return null;
175
177
  }
176
178
  const promptSources = allItems.map((item) => ({
179
+ repoRoot: item.repoRoot,
177
180
  repoName: item.repoName,
178
181
  worktree: item.worktree,
179
182
  }));
@@ -185,7 +188,9 @@ export async function resolveWarpTarget(options) {
185
188
  }
186
189
  return { branch: match.worktree.branch, path: match.worktree.path };
187
190
  }
188
- const promptEntries = await buildWorktreePromptEntries(promptSources);
191
+ const promptEntries = await buildWorktreePromptEntries(promptSources, {
192
+ queryPullRequests: options.queryPullRequests,
193
+ });
189
194
  const path = await promptForWarpTarget(promptEntries);
190
195
  if (!path) {
191
196
  options.stderr("Aborted\n");
@@ -1,6 +1,8 @@
1
1
  import type { Readable, Writable } from "node:stream";
2
+ import { type PullRequestInfo } from "./pull-requests.js";
2
3
  import type { WorktreeEntry } from "./repo.js";
3
4
  export interface WorktreePromptSource {
5
+ repoRoot?: string;
4
6
  repoName: string;
5
7
  worktree: WorktreeEntry;
6
8
  }
@@ -8,8 +10,16 @@ export interface WorktreePromptEntry extends WorktreeEntry {
8
10
  group: "recent" | "other";
9
11
  label: string;
10
12
  metadata?: string | null;
13
+ pullRequestNumbers?: number[];
14
+ pullRequestUrls?: string[];
11
15
  repoName: string;
12
16
  }
17
+ export type QueryWorktreePullRequests = (repoRoot: string, sourceBranch: string) => Promise<PullRequestInfo[]>;
18
+ export type QueryRepositoryPullRequests = (repoRoot: string) => Promise<PullRequestInfo[]>;
19
+ export interface BuildWorktreePromptEntriesDependencies {
20
+ queryPullRequests?: QueryWorktreePullRequests;
21
+ queryRepositoryPullRequests?: QueryRepositoryPullRequests;
22
+ }
13
23
  export interface WorktreePickerIO {
14
24
  input?: Readable & {
15
25
  isTTY?: boolean;
@@ -20,7 +30,8 @@ export interface WorktreePickerIO {
20
30
  rows?: number;
21
31
  };
22
32
  }
23
- export declare function buildWorktreePromptEntries(sources: WorktreePromptSource[]): Promise<WorktreePromptEntry[]>;
33
+ export declare function buildWorktreePromptEntries(sources: WorktreePromptSource[], dependencies?: BuildWorktreePromptEntriesDependencies): Promise<WorktreePromptEntry[]>;
24
34
  export declare function resolveWorktreeQuery(sources: WorktreePromptSource[], query: string): WorktreePromptSource | null;
25
35
  export declare function promptForSingleWorktree(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO): Promise<string | null>;
26
36
  export declare function promptForMultipleWorktrees(message: string, worktrees: WorktreePromptEntry[], io?: WorktreePickerIO): Promise<string[] | null>;
37
+ export declare function formatWorktreeBranchLabel(branch: string | null, pullRequestNumbers?: number[]): string;
@@ -1,14 +1,27 @@
1
1
  import { env, platform, stdin, stdout } from "node:process";
2
2
  import { isCancel, Prompt } from "@clack/core";
3
3
  import { loadHistory } from "./history.js";
4
+ import { createPullRequestQuery, } from "./pull-requests.js";
4
5
  import { readWorktreeInfos, } from "./worktree-info.js";
5
- export async function buildWorktreePromptEntries(sources) {
6
- const [history, infos] = await Promise.all([
6
+ const MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY = 4;
7
+ export async function buildWorktreePromptEntries(sources, dependencies = {}) {
8
+ const pullRequestQuery = createPullRequestQuery();
9
+ const queryPullRequests = dependencies.queryPullRequests ?? pullRequestQuery.listOpenPullRequests;
10
+ const queryRepositoryPullRequests = dependencies.queryRepositoryPullRequests ??
11
+ (dependencies.queryPullRequests === undefined
12
+ ? pullRequestQuery.listOpenPullRequestsForRepository
13
+ : undefined);
14
+ const repositoryPullRequests = queryRepositoryPullRequests === undefined
15
+ ? Promise.resolve(null)
16
+ : readRepositoryPullRequests(sources, queryRepositoryPullRequests);
17
+ const [history, infos, pullRequestsByRepository] = await Promise.all([
7
18
  loadHistory(),
8
19
  readWorktreeInfos(sources.map((source) => source.worktree)),
20
+ repositoryPullRequests,
9
21
  ]);
22
+ const pullRequests = await Promise.all(sources.map((source) => readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository)));
10
23
  const historyByPath = new Map(history.map((entry) => [entry.path, entry]));
11
- const entries = sources.map((source, index) => buildWorktreePromptEntry(source, infos[index], historyByPath.get(source.worktree.path)?.timestamp ?? null, Date.now()));
24
+ const entries = sources.map((source, index) => buildWorktreePromptEntry(source, infos[index], pullRequests[index], historyByPath.get(source.worktree.path)?.timestamp ?? null, Date.now()));
12
25
  return entries
13
26
  .sort(comparePromptEntries)
14
27
  .map(({ lastActivityTimestamp: _lastActivityTimestamp, ...entry }) => entry);
@@ -110,7 +123,7 @@ function buildSearchableWorktreeEntry(worktree) {
110
123
  searchText: buildPromptSearchText(worktree),
111
124
  value: worktree.path,
112
125
  worktree: {
113
- branch,
126
+ branch: formatWorktreeBranchLabel(worktree.branch, worktree.pullRequestNumbers),
114
127
  metadata,
115
128
  path: worktree.path,
116
129
  repoName: worktree.repoName,
@@ -715,11 +728,13 @@ function buildPromptSearchText(worktree) {
715
728
  worktree.path,
716
729
  worktree.label,
717
730
  `${worktree.repoName}/${worktree.branch ?? "detached"}`,
731
+ ...(worktree.pullRequestNumbers ?? []).map((number) => `#${number}`),
732
+ ...(worktree.pullRequestUrls ?? []),
718
733
  ]
719
734
  .join(" ")
720
735
  .toLowerCase();
721
736
  }
722
- function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
737
+ function buildWorktreePromptEntry(source, info, pullRequests, lastUsedTimestamp, now) {
723
738
  const lastWorkedTimestamp = info.lastCommitTimestamp === null ? null : info.lastCommitTimestamp * 1000;
724
739
  const lastActivityTimestamp = lastUsedTimestamp ?? lastWorkedTimestamp;
725
740
  const lastActivityType = lastUsedTimestamp !== null
@@ -727,7 +742,7 @@ function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
727
742
  : lastWorkedTimestamp !== null
728
743
  ? "worked"
729
744
  : null;
730
- const branch = source.worktree.branch ?? "(detached)";
745
+ const branch = formatWorktreeBranchLabel(source.worktree.branch, pullRequests.map((pullRequest) => pullRequest.number));
731
746
  const badges = buildStatusBadges(info);
732
747
  const recency = formatPromptRecency(lastActivityTimestamp, lastActivityType, now);
733
748
  const status = badges.length > 0 ? badges.map((badge) => `[${badge}]`).join(" ") : null;
@@ -748,9 +763,72 @@ function buildWorktreePromptEntry(source, info, lastUsedTimestamp, now) {
748
763
  label,
749
764
  lastActivityTimestamp,
750
765
  metadata,
766
+ pullRequestNumbers: pullRequests.map((pullRequest) => pullRequest.number),
767
+ pullRequestUrls: pullRequests.map((pullRequest) => pullRequest.url),
751
768
  repoName: source.repoName,
752
769
  };
753
770
  }
771
+ async function readSourcePullRequests(source, queryPullRequests, pullRequestsByRepository) {
772
+ if (source.repoRoot === undefined || source.worktree.branch === null) {
773
+ return [];
774
+ }
775
+ if (pullRequestsByRepository !== null) {
776
+ return sortSourcePullRequests(pullRequestsByRepository
777
+ .get(source.repoRoot)
778
+ ?.filter((pullRequest) => pullRequest.sourceBranch === source.worktree.branch));
779
+ }
780
+ try {
781
+ const pullRequests = await queryPullRequests(source.repoRoot, source.worktree.branch);
782
+ return sortSourcePullRequests(pullRequests);
783
+ }
784
+ catch {
785
+ // PR metadata is optional selector decoration; preserve the worktree entry on lookup failures.
786
+ return [];
787
+ }
788
+ }
789
+ async function readRepositoryPullRequests(sources, queryPullRequests) {
790
+ const repoRoots = [
791
+ ...new Set(sources.flatMap((source) => source.repoRoot === undefined || source.worktree.branch === null
792
+ ? []
793
+ : [source.repoRoot])),
794
+ ];
795
+ const results = await mapWithConcurrency(repoRoots, MAX_PULL_REQUEST_REPOSITORY_QUERY_CONCURRENCY, async (repoRoot) => {
796
+ try {
797
+ return [
798
+ repoRoot,
799
+ sortSourcePullRequests(await queryPullRequests(repoRoot)),
800
+ ];
801
+ }
802
+ catch {
803
+ return [repoRoot, []];
804
+ }
805
+ });
806
+ return new Map(results);
807
+ }
808
+ async function mapWithConcurrency(items, limit, mapper) {
809
+ const results = new Array(items.length);
810
+ let nextIndex = 0;
811
+ async function readNext() {
812
+ for (;;) {
813
+ const index = nextIndex;
814
+ nextIndex += 1;
815
+ if (index >= items.length)
816
+ return;
817
+ results[index] = await mapper(items[index]);
818
+ }
819
+ }
820
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
821
+ return results;
822
+ }
823
+ function sortSourcePullRequests(pullRequests) {
824
+ return [...(pullRequests ?? [])].sort((a, b) => a.number - b.number);
825
+ }
826
+ export function formatWorktreeBranchLabel(branch, pullRequestNumbers = []) {
827
+ const branchLabel = branch ?? "(detached)";
828
+ if (pullRequestNumbers.length === 0)
829
+ return branchLabel;
830
+ return `${branchLabel} (${pullRequestNumbers.map((number) => `#${number}`).join(", ")})`;
831
+ }
754
832
  function buildStatusBadges(info) {
755
833
  const badges = [];
756
834
  if (info.isCurrent) {
@@ -1,4 +1,4 @@
1
- .TH GJI\-BACK 1 "July 2026" "gji 0.8.1" "User Commands"
1
+ .TH GJI\-BACK 1 "July 2026" "gji 0.9.0" "User Commands"
2
2
  .SH NAME
3
3
  gji\-back \- navigate to the previously visited worktree, optionally N steps back
4
4
  .SH SYNOPSIS