@solaqua/gji 0.8.1 → 0.10.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 (51) hide show
  1. package/README.md +29 -33
  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 +63 -4
  6. package/dist/doctor.d.ts +28 -0
  7. package/dist/doctor.js +454 -0
  8. package/dist/gji-bundle.mjs +2507 -359
  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/new.d.ts +1 -0
  15. package/dist/new.js +24 -1
  16. package/dist/open.d.ts +3 -1
  17. package/dist/open.js +16 -3
  18. package/dist/pr-open.d.ts +19 -0
  19. package/dist/pr-open.js +290 -0
  20. package/dist/pull-requests.d.ts +33 -0
  21. package/dist/pull-requests.js +384 -0
  22. package/dist/repo-registry.d.ts +5 -0
  23. package/dist/repo-registry.js +87 -14
  24. package/dist/shell-completion.js +284 -14
  25. package/dist/shell-setup.d.ts +9 -0
  26. package/dist/shell-setup.js +56 -0
  27. package/dist/warp.d.ts +3 -0
  28. package/dist/warp.js +8 -3
  29. package/dist/worktree-picker.d.ts +12 -1
  30. package/dist/worktree-picker.js +84 -6
  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-doctor.1 +19 -0
  36. package/man/man1/gji-go.1 +1 -1
  37. package/man/man1/gji-history.1 +1 -1
  38. package/man/man1/gji-init.1 +6 -3
  39. package/man/man1/gji-ls.1 +1 -1
  40. package/man/man1/gji-new.1 +4 -1
  41. package/man/man1/gji-open.1 +4 -1
  42. package/man/man1/gji-pr.1 +5 -1
  43. package/man/man1/gji-remove.1 +1 -1
  44. package/man/man1/gji-root.1 +1 -1
  45. package/man/man1/gji-run-hook.1 +1 -1
  46. package/man/man1/gji-status.1 +1 -1
  47. package/man/man1/gji-sync-files.1 +1 -1
  48. package/man/man1/gji-sync.1 +1 -1
  49. package/man/man1/gji-warp.1 +1 -1
  50. package/man/man1/gji.1 +6 -2
  51. 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=()
@@ -69,16 +137,40 @@ _gji_completion() {
69
137
 
70
138
  case "$command_name" in
71
139
  new)
72
- COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
140
+ COMPREPLY=( $(compgen -W "--detached --from-current --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") )
@@ -87,7 +179,7 @@ _gji_completion() {
87
179
  COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
88
180
  ;;
89
181
  open)
90
- COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
182
+ COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --select --save --workspace --help" -- "$cur") )
91
183
  ;;
92
184
  go|jump)
93
185
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --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
@@ -177,6 +364,7 @@ complete -c gji -f
177
364
  ${commandLines}
178
365
 
179
366
  complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
367
+ complete -c gji -n '__fish_seen_subcommand_from new' -l from-current -d 'base the new branch on the current worktree instead of the main worktree'
180
368
  complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
181
369
  complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
182
370
  complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
@@ -184,20 +372,29 @@ complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what wo
184
372
  complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
185
373
 
186
374
  complete -c gji -n '__fish_seen_subcommand_from init' -l write -d 'write the integration to the shell config file'
375
+ complete -c gji -n '__fish_seen_subcommand_from init' -l json -d 'emit a JSON error in non-interactive mode'
187
376
  ${shellLines}
188
377
 
378
+ complete -c gji -n '__fish_seen_subcommand_from doctor' -l fix -d 'apply safe automatic fixes after showing the plan'
379
+ complete -c gji -n '__fish_seen_subcommand_from doctor' -l yes -d 'apply --fix without prompting'
380
+ complete -c gji -n '__fish_seen_subcommand_from doctor' -l json -d 'emit diagnostic checks as JSON'
381
+
189
382
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'bash' -d 'shell'
190
383
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'fish' -d 'shell'
191
384
  complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
192
385
 
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'
386
+ 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'
387
+ 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'
388
+ complete -c gji -n '__fish_seen_subcommand_from pr' -a 'open' -d 'open a pull request in the default browser'
389
+ complete -c gji -n '__gji_should_complete_pr_select' -l select -d 'choose a pull request from any linked worktree'
390
+ complete -c gji -n '__gji_should_complete_pr_target' -a '(__gji_pr_targets)' -d 'branch or PR number'
195
391
 
196
392
  complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
197
393
 
198
394
  complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
199
395
 
200
396
  complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, …)'
397
+ complete -c gji -n '__fish_seen_subcommand_from open' -l select -d 'choose a worktree with the interactive selector'
201
398
  complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
202
399
  complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
203
400
  complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
@@ -251,30 +448,99 @@ __gji_worktree_branches() {
251
448
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
252
449
  }
253
450
 
451
+ typeset -g __gji_pr_targets_cache=()
452
+ typeset -g __gji_pr_targets_cache_at=0
453
+ typeset -g __gji_pr_targets_cache_loaded=0
454
+ typeset -g __gji_pr_targets_cache_repo=""
455
+
456
+ __gji_run_bounded() {
457
+ local output_file pid status attempt
458
+ output_file=$(mktemp "\${TMPDIR:-/tmp}/gji-completion.XXXXXX") || return 124
459
+ "$@" >"$output_file" 2>/dev/null &
460
+ pid=$!
461
+ for ((attempt = 0; attempt < 20; attempt++)); do
462
+ if ! kill -0 "$pid" 2>/dev/null; then
463
+ wait "$pid"
464
+ status=$?
465
+ cat "$output_file"
466
+ rm -f "$output_file"
467
+ return "$status"
468
+ fi
469
+ sleep 0.1
470
+ done
471
+ kill "$pid" 2>/dev/null
472
+ wait "$pid" 2>/dev/null
473
+ rm -f "$output_file"
474
+ return 124
475
+ }
476
+
477
+ __gji_pr_targets() {
478
+ local now repo_key
479
+ local -a branches targets
480
+ now=$(date +%s)
481
+ repo_key=$(command git rev-parse --show-toplevel 2>/dev/null)
482
+ if (( __gji_pr_targets_cache_loaded == 1 && "\${repo_key}" == "\${__gji_pr_targets_cache_repo}" && now - __gji_pr_targets_cache_at < 30 )); then
483
+ print -rl -- "\${__gji_pr_targets_cache[@]}"
484
+ return
485
+ fi
486
+
487
+ branches=("\${(@f)$(__gji_worktree_branches)}")
488
+ targets=()
489
+ if [[ -n "\${repo_key}" && $+commands[gh] -eq 1 ]]; then
490
+ targets=("\${(@f)$(__gji_run_bounded env GH_PROMPT_DISABLED=1 gh pr list --state open --json number --limit 100 |
491
+ tr '{},' '\\n' | sed -n 's/.*"number":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')}")
492
+ elif [[ -n "\${repo_key}" && $+commands[glab] -eq 1 ]]; then
493
+ targets=("\${(@f)$(__gji_run_bounded env GLAB_NON_INTERACTIVE=1 glab mr list --state opened --output json --per-page 100 |
494
+ tr '{},' '\\n' | sed -n 's/.*"iid":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')}")
495
+ elif [[ -n "\${repo_key}" && $+commands[bb] -eq 1 ]]; then
496
+ targets=("\${(@f)$(__gji_run_bounded bb pr list --state OPEN --format json |
497
+ tr '{},' '\\n' | sed -n 's/.*"id":[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p')}")
498
+ fi
499
+
500
+ __gji_pr_targets_cache=("\${branches[@]}" "\${targets[@]}")
501
+ __gji_pr_targets_cache_at="$now"
502
+ __gji_pr_targets_cache_loaded=1
503
+ __gji_pr_targets_cache_repo="$repo_key"
504
+ print -rl -- "\${__gji_pr_targets_cache[@]}"
505
+ }
506
+
254
507
  local context state line
255
- local -a commands worktree_branches
508
+ local -a command_entries worktree_branches
256
509
 
257
- commands=(
510
+ command_entries=(
258
511
  ${commandLines}
259
512
  )
260
513
 
261
514
  if (( CURRENT == 2 )); then
262
- _describe 'command' commands
515
+ _describe 'command' command_entries
263
516
  return
264
517
  fi
265
518
 
266
519
  case "\${words[2]}" in
267
520
  new)
268
- _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: '
521
+ _arguments '--detached[create a detached worktree without a branch]' '--from-current[base the new branch on the current worktree instead of the main worktree]' '--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
522
  ;;
270
523
  init)
271
- _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
524
+ _arguments '--write[write the integration to the shell config file]' '--json[emit a JSON error in non-interactive mode]' '2:shell:(${shells})'
525
+ ;;
526
+ doctor)
527
+ _arguments '--fix[apply safe automatic fixes after showing the plan]' '--yes[apply --fix without prompting]' '--json[emit diagnostic checks as JSON]'
272
528
  ;;
273
529
  completion)
274
530
  _arguments '2:shell:(${shells})'
275
531
  ;;
276
532
  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: '
533
+ if [[ "\${words[3]}" == "open" ]]; then
534
+ if (( \${words[(I)--select]} )); then
535
+ _arguments
536
+ elif [[ -n "\${words[4]}" && "\${words[4]}" != -* ]]; then
537
+ _arguments
538
+ else
539
+ _arguments '--select[choose a pull request from any linked worktree]' '4:branch or PR number:->pr_targets'
540
+ fi
541
+ else
542
+ _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)'
543
+ fi
278
544
  ;;
279
545
  back)
280
546
  _arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
@@ -283,7 +549,7 @@ case "\${words[2]}" in
283
549
  _arguments '--json[print history as JSON]'
284
550
  ;;
285
551
  open)
286
- _arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, …)]:editor:' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
552
+ _arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, …)]:editor:' '--select[choose a worktree with the interactive selector]' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
287
553
  ;;
288
554
  go|jump)
289
555
  _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
@@ -337,6 +603,10 @@ case "$state" in
337
603
  worktree_branches=(\${(@f)$(__gji_worktree_branches)})
338
604
  _describe 'worktree branch' worktree_branches
339
605
  ;;
606
+ pr_targets)
607
+ worktree_branches=(\${(@f)$(__gji_pr_targets)})
608
+ _describe 'branch or PR number' worktree_branches
609
+ ;;
340
610
  config_keys)
341
611
  _values 'config key' ${configKeys}
342
612
  ;;
@@ -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;