@solaqua/gji 0.8.0 → 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 (53) 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/clean.js +34 -19
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +59 -4
  7. package/dist/doctor.d.ts +28 -0
  8. package/dist/doctor.js +454 -0
  9. package/dist/gji-bundle.mjs +2514 -378
  10. package/dist/go.d.ts +2 -1
  11. package/dist/go.js +4 -1
  12. package/dist/index.js +12 -6
  13. package/dist/init.d.ts +15 -0
  14. package/dist/init.js +232 -16
  15. package/dist/open.d.ts +2 -1
  16. package/dist/open.js +2 -1
  17. package/dist/pr-open.d.ts +19 -0
  18. package/dist/pr-open.js +290 -0
  19. package/dist/pull-requests.d.ts +33 -0
  20. package/dist/pull-requests.js +384 -0
  21. package/dist/remove.js +2 -2
  22. package/dist/repo-registry.d.ts +5 -0
  23. package/dist/repo-registry.js +87 -14
  24. package/dist/shell-completion.js +278 -10
  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-management.d.ts +2 -1
  30. package/dist/worktree-management.js +23 -7
  31. package/dist/worktree-picker.d.ts +12 -1
  32. package/dist/worktree-picker.js +84 -6
  33. package/man/man1/gji-back.1 +1 -1
  34. package/man/man1/gji-clean.1 +1 -1
  35. package/man/man1/gji-completion.1 +1 -1
  36. package/man/man1/gji-config.1 +1 -1
  37. package/man/man1/gji-doctor.1 +19 -0
  38. package/man/man1/gji-go.1 +1 -1
  39. package/man/man1/gji-history.1 +1 -1
  40. package/man/man1/gji-init.1 +6 -3
  41. package/man/man1/gji-ls.1 +1 -1
  42. package/man/man1/gji-new.1 +1 -1
  43. package/man/man1/gji-open.1 +1 -1
  44. package/man/man1/gji-pr.1 +5 -1
  45. package/man/man1/gji-remove.1 +1 -1
  46. package/man/man1/gji-root.1 +1 -1
  47. package/man/man1/gji-run-hook.1 +1 -1
  48. package/man/man1/gji-status.1 +1 -1
  49. package/man/man1/gji-sync-files.1 +1 -1
  50. package/man/man1/gji-sync.1 +1 -1
  51. package/man/man1/gji-warp.1 +1 -1
  52. package/man/man1/gji.1 +6 -2
  53. 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");
@@ -8,5 +8,6 @@ export declare function removeWorktree(repoRoot: string, worktreePath: string):
8
8
  export declare function forceRemoveWorktree(repoRoot: string, worktreePath: string): Promise<void>;
9
9
  export declare function deleteBranch(repoRoot: string, branch: string): Promise<void>;
10
10
  export declare function forceDeleteBranch(repoRoot: string, branch: string): Promise<void>;
11
- export declare function isWorktreeDirtyError(error: unknown): boolean;
11
+ export declare function isWorktreeForceRemovalError(error: unknown): boolean;
12
+ export declare function isWorktreeDeletionError(error: unknown): boolean;
12
13
  export declare function isBranchUnmergedError(error: unknown): boolean;
@@ -1,4 +1,5 @@
1
1
  import { execFile } from "node:child_process";
2
+ import { rm } from "node:fs/promises";
2
3
  import { promisify } from "node:util";
3
4
  import { detectRepository, listWorktrees, } from "./repo.js";
4
5
  const execFileAsync = promisify(execFile);
@@ -19,10 +20,19 @@ export async function removeWorktree(repoRoot, worktreePath) {
19
20
  });
20
21
  }
21
22
  export async function forceRemoveWorktree(repoRoot, worktreePath) {
22
- await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
23
- cwd: repoRoot,
24
- env: GIT_ENV,
25
- });
23
+ try {
24
+ await execFileAsync("git", ["worktree", "remove", "--force", worktreePath], {
25
+ cwd: repoRoot,
26
+ env: GIT_ENV,
27
+ });
28
+ }
29
+ catch (error) {
30
+ const worktreeIsStillRegistered = (await listWorktrees(repoRoot)).some((worktree) => worktree.path === worktreePath);
31
+ if (worktreeIsStillRegistered) {
32
+ throw error;
33
+ }
34
+ await rm(worktreePath, { force: true, recursive: true });
35
+ }
26
36
  }
27
37
  export async function deleteBranch(repoRoot, branch) {
28
38
  await execFileAsync("git", ["branch", "-d", branch], {
@@ -36,9 +46,15 @@ export async function forceDeleteBranch(repoRoot, branch) {
36
46
  env: GIT_ENV,
37
47
  });
38
48
  }
39
- export function isWorktreeDirtyError(error) {
40
- return (hasStderr(error) &&
41
- error.stderr.includes("contains modified or untracked files"));
49
+ export function isWorktreeForceRemovalError(error) {
50
+ if (!hasStderr(error)) {
51
+ return false;
52
+ }
53
+ return (error.stderr.includes("contains modified or untracked files") ||
54
+ isWorktreeDeletionError(error));
55
+ }
56
+ export function isWorktreeDeletionError(error) {
57
+ return hasStderr(error) && error.stderr.includes("failed to delete");
42
58
  }
43
59
  export function isBranchUnmergedError(error) {
44
60
  return hasStderr(error) && error.stderr.includes("is not fully merged");
@@ -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;