@solaqua/gji 0.6.0 → 0.6.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.
package/README.md CHANGED
@@ -168,7 +168,7 @@ gji trigger-hook afterCreate # re-run setup in the current worktree
168
168
 
169
169
  - **vs raw `git worktree`**: same underlying capability, but with branch-first commands, shell handoff, PR checkout, hooks, sync, and cleanup built into the workflow
170
170
  - **vs `lazygit`**: `lazygit` is a broad Git UI; `gji` is narrower and faster for opening, jumping between, and removing isolated branch directories
171
- - **vs `ghq`**: `ghq` organizes repositories; `gji` organizes active branches and PRs within one repository
171
+ - **vs `ghq`**: `ghq` organizes where repositories live; `gji` organizes which branch, PR, or worktree you should be in once you are inside one
172
172
 
173
173
  Use `gji` when your bottleneck is repeated context switching between features, reviews, and maintenance work without disturbing what is already open.
174
174
 
package/dist/config.d.ts CHANGED
@@ -2,6 +2,7 @@ export declare const CONFIG_FILE_NAME = ".gji.json";
2
2
  export declare const GLOBAL_CONFIG_DIRECTORY = ".config/gji";
3
3
  export declare const GLOBAL_CONFIG_NAME = "config.json";
4
4
  export declare const KNOWN_CONFIG_KEYS: ReadonlySet<string>;
5
+ export declare const KNOWN_GLOBAL_CONFIG_KEYS: ReadonlySet<string>;
5
6
  export type GjiConfig = Record<string, unknown>;
6
7
  export interface LoadedConfig {
7
8
  config: GjiConfig;
package/dist/config.js CHANGED
@@ -16,7 +16,7 @@ export const KNOWN_CONFIG_KEYS = new Set([
16
16
  'syncRemote',
17
17
  'worktreePath',
18
18
  ]);
19
- const KNOWN_GLOBAL_CONFIG_KEYS = new Set([
19
+ export const KNOWN_GLOBAL_CONFIG_KEYS = new Set([
20
20
  ...KNOWN_CONFIG_KEYS,
21
21
  'repos',
22
22
  ]);
@@ -9740,7 +9740,7 @@ var retryifyAsync = (fn, options) => {
9740
9740
  throw error;
9741
9741
  const delay2 = Math.round(interval * Math.random());
9742
9742
  if (delay2 > 0) {
9743
- const delayPromise = new Promise((resolve5) => setTimeout(resolve5, delay2));
9743
+ const delayPromise = new Promise((resolve6) => setTimeout(resolve6, delay2));
9744
9744
  return delayPromise.then(() => attempt.apply(void 0, args));
9745
9745
  } else {
9746
9746
  return attempt.apply(void 0, args);
@@ -9999,14 +9999,14 @@ var Temp = {
9999
9999
  }
10000
10000
  },
10001
10001
  truncate: (filePath) => {
10002
- const basename8 = path2.basename(filePath);
10003
- if (basename8.length <= LIMIT_BASENAME_LENGTH)
10002
+ const basename9 = path2.basename(filePath);
10003
+ if (basename9.length <= LIMIT_BASENAME_LENGTH)
10004
10004
  return filePath;
10005
- const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename8);
10005
+ const truncable = /^(\.?)(.*?)((?:\.[^.]+)?(?:\.tmp-\d{10}[a-f0-9]{6})?)$/.exec(basename9);
10006
10006
  if (!truncable)
10007
10007
  return filePath;
10008
- const truncationLength = basename8.length - LIMIT_BASENAME_LENGTH;
10009
- return `${filePath.slice(0, -basename8.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
10008
+ const truncationLength = basename9.length - LIMIT_BASENAME_LENGTH;
10009
+ return `${filePath.slice(0, -basename9.length)}${truncable[1]}${truncable[2].slice(0, -truncationLength)}${truncable[3]}`;
10010
10010
  }
10011
10011
  };
10012
10012
  node_default(Temp.purgeSyncAll);
@@ -11306,14 +11306,14 @@ var TimeoutError = class extends Error {
11306
11306
 
11307
11307
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/timeout.js
11308
11308
  async function timeout(request, init, abortController, options) {
11309
- return new Promise((resolve5, reject) => {
11309
+ return new Promise((resolve6, reject) => {
11310
11310
  const timeoutId = setTimeout(() => {
11311
11311
  if (abortController) {
11312
11312
  abortController.abort();
11313
11313
  }
11314
11314
  reject(new TimeoutError(request));
11315
11315
  }, options.timeout);
11316
- void options.fetch(request, init).then(resolve5).catch(reject).then(() => {
11316
+ void options.fetch(request, init).then(resolve6).catch(reject).then(() => {
11317
11317
  clearTimeout(timeoutId);
11318
11318
  });
11319
11319
  });
@@ -11321,7 +11321,7 @@ async function timeout(request, init, abortController, options) {
11321
11321
 
11322
11322
  // node_modules/.pnpm/ky@1.14.3/node_modules/ky/distribution/utils/delay.js
11323
11323
  async function delay(ms, { signal }) {
11324
- return new Promise((resolve5, reject) => {
11324
+ return new Promise((resolve6, reject) => {
11325
11325
  if (signal) {
11326
11326
  signal.throwIfAborted();
11327
11327
  signal.addEventListener("abort", abortHandler, { once: true });
@@ -11332,7 +11332,7 @@ async function delay(ms, { signal }) {
11332
11332
  }
11333
11333
  const timeoutId = setTimeout(() => {
11334
11334
  signal?.removeEventListener("abort", abortHandler);
11335
- resolve5();
11335
+ resolve6();
11336
11336
  }, ms);
11337
11337
  });
11338
11338
  }
@@ -13048,7 +13048,7 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
13048
13048
  stderr("gji: hook argv command must include a non-empty command\n");
13049
13049
  return;
13050
13050
  }
13051
- await new Promise((resolve5) => {
13051
+ await new Promise((resolve6) => {
13052
13052
  const child = spawn2(command, args, {
13053
13053
  cwd,
13054
13054
  shell: false,
@@ -13063,18 +13063,18 @@ async function runArgvHook(hookCmd, cwd, context, stderr) {
13063
13063
  stderr(`gji: hook exited with code ${code}: ${formatArgvHook(command, args)}
13064
13064
  `);
13065
13065
  }
13066
- resolve5();
13066
+ resolve6();
13067
13067
  });
13068
13068
  child.on("error", (err) => {
13069
13069
  stderr(`gji: hook failed to start: ${err.message}
13070
13070
  `);
13071
- resolve5();
13071
+ resolve6();
13072
13072
  });
13073
13073
  });
13074
13074
  }
13075
13075
  async function runShellHook(hookCmd, cwd, context, stderr) {
13076
13076
  const interpolated = interpolate(hookCmd, context);
13077
- await new Promise((resolve5) => {
13077
+ await new Promise((resolve6) => {
13078
13078
  const child = spawn2(interpolated, {
13079
13079
  cwd,
13080
13080
  shell: true,
@@ -13089,12 +13089,12 @@ async function runShellHook(hookCmd, cwd, context, stderr) {
13089
13089
  stderr(`gji: hook exited with code ${code}: ${interpolated}
13090
13090
  `);
13091
13091
  }
13092
- resolve5();
13092
+ resolve6();
13093
13093
  });
13094
13094
  child.on("error", (err) => {
13095
13095
  stderr(`gji: hook failed to start: ${err.message}
13096
13096
  `);
13097
- resolve5();
13097
+ resolve6();
13098
13098
  });
13099
13099
  });
13100
13100
  }
@@ -14482,7 +14482,11 @@ var TOP_LEVEL_COMMANDS = [
14482
14482
  { name: "init", description: "print or install shell integration" },
14483
14483
  { name: "completion", description: "print shell completion definitions" },
14484
14484
  { name: "pr", description: "fetch a pull request into a linked worktree" },
14485
+ { name: "back", description: "navigate to the previously visited worktree" },
14486
+ { name: "history", description: "show navigation history" },
14487
+ { name: "open", description: "open the worktree in an editor" },
14485
14488
  { name: "go", description: "print or select a worktree path" },
14489
+ { name: "jump", description: "alias of go" },
14486
14490
  { name: "root", description: "print the main repository root path" },
14487
14491
  { name: "status", description: "summarize repository and worktree health" },
14488
14492
  { name: "sync", description: "fetch and update one or all worktrees" },
@@ -14491,20 +14495,12 @@ var TOP_LEVEL_COMMANDS = [
14491
14495
  { name: "remove", description: "remove a linked worktree and delete its branch when present" },
14492
14496
  { name: "rm", description: "alias of remove" },
14493
14497
  { name: "trigger-hook", description: "run a named hook in the current worktree" },
14498
+ { name: "warp", description: "jump to any worktree across all known repos" },
14494
14499
  { name: "config", description: "manage global config defaults" }
14495
14500
  ];
14496
14501
  var SHELL_NAMES = ["bash", "fish", "zsh"];
14497
14502
  var HOOK_NAMES = ["afterCreate", "afterEnter", "beforeRemove"];
14498
- var CONFIG_KEYS = [
14499
- "branchPrefix",
14500
- "syncRemote",
14501
- "syncDefaultBranch",
14502
- "syncFiles",
14503
- "skipInstallPrompt",
14504
- "installSaveTarget",
14505
- "hooks",
14506
- "repos"
14507
- ];
14503
+ var CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
14508
14504
  function renderShellCompletion(shell) {
14509
14505
  switch (shell) {
14510
14506
  case "bash":
@@ -14538,7 +14534,7 @@ _gji_completion() {
14538
14534
 
14539
14535
  case "$command_name" in
14540
14536
  new)
14541
- COMPREPLY=( $(compgen -W "--detached --dry-run --json --help" -- "$cur") )
14537
+ COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
14542
14538
  ;;
14543
14539
  init)
14544
14540
  COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
@@ -14549,7 +14545,16 @@ _gji_completion() {
14549
14545
  pr)
14550
14546
  COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
14551
14547
  ;;
14552
- go)
14548
+ back)
14549
+ COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
14550
+ ;;
14551
+ history)
14552
+ COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
14553
+ ;;
14554
+ open)
14555
+ COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
14556
+ ;;
14557
+ go|jump)
14553
14558
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
14554
14559
  ;;
14555
14560
  root)
@@ -14573,6 +14578,9 @@ _gji_completion() {
14573
14578
  trigger-hook)
14574
14579
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
14575
14580
  ;;
14581
+ warp)
14582
+ COMPREPLY=( $(compgen -W "-n --new --print --json --help" -- "$cur") )
14583
+ ;;
14576
14584
  config)
14577
14585
  if [ "$COMP_CWORD" -eq 2 ]; then
14578
14586
  COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
@@ -14636,6 +14644,9 @@ complete -c gji -f
14636
14644
  ${commandLines}
14637
14645
 
14638
14646
  complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
14647
+ complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
14648
+ complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
14649
+ complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, \u2026)'
14639
14650
  complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
14640
14651
  complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
14641
14652
 
@@ -14649,8 +14660,17 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
14649
14660
  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'
14650
14661
  complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
14651
14662
 
14652
- complete -c gji -n '__fish_seen_subcommand_from go' -l print -d 'print the resolved worktree path explicitly'
14653
- complete -c gji -n '__fish_seen_subcommand_from go' -a '(__gji_worktree_branches)' -d 'worktree branch'
14663
+ complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
14664
+
14665
+ complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
14666
+
14667
+ complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)'
14668
+ complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
14669
+ complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
14670
+ complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
14671
+
14672
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
14673
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
14654
14674
 
14655
14675
  complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
14656
14676
 
@@ -14674,6 +14694,10 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
14674
14694
 
14675
14695
  ${hookLines}
14676
14696
 
14697
+ complete -c gji -n '__fish_seen_subcommand_from warp' -s n -l new -d 'create a new worktree in a registered repo'
14698
+ complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
14699
+ complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
14700
+
14677
14701
  complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
14678
14702
  ${configKeyLines}`;
14679
14703
  }
@@ -14684,89 +14708,99 @@ function renderZshCompletion() {
14684
14708
  const configKeys = CONFIG_KEYS.join(" ");
14685
14709
  const shells = SHELL_NAMES.join(" ");
14686
14710
  const hooks = HOOK_NAMES.join(" ");
14687
- return `__gji_worktree_branches() {
14711
+ return `#compdef gji
14712
+
14713
+ __gji_worktree_branches() {
14688
14714
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
14689
14715
  }
14690
14716
 
14691
- _gji_completion() {
14692
- local context state line
14693
- local -a commands worktree_branches
14694
-
14695
- commands=(
14696
- ${commandLines}
14697
- )
14717
+ local context state line
14718
+ local -a commands worktree_branches
14698
14719
 
14699
- if (( CURRENT == 2 )); then
14700
- _describe 'command' commands
14701
- return
14702
- fi
14720
+ commands=(
14721
+ ${commandLines}
14722
+ )
14703
14723
 
14704
- case "\${words[2]}" in
14705
- new)
14706
- _arguments '--detached[create a detached worktree without a branch]' '--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: '
14707
- ;;
14708
- init)
14709
- _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
14710
- ;;
14711
- completion)
14712
- _arguments '2:shell:(${shells})'
14713
- ;;
14714
- pr)
14715
- _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: '
14716
- ;;
14717
- go)
14718
- _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
14719
- ;;
14720
- root)
14721
- _arguments '--print[print the resolved repository root path explicitly]'
14722
- ;;
14723
- status)
14724
- _arguments '--json[print repository and worktree health as JSON]'
14725
- ;;
14726
- sync)
14727
- _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
14728
- ;;
14729
- ls)
14730
- _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
14731
- ;;
14732
- clean)
14733
- _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
14734
- ;;
14735
- remove|rm)
14736
- _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
14737
- ;;
14738
- trigger-hook)
14739
- _arguments "2:hook:(${hooks})"
14740
- ;;
14741
- config)
14742
- if (( CURRENT == 3 )); then
14743
- _values 'config action' get set unset
14744
- return
14745
- fi
14724
+ if (( CURRENT == 2 )); then
14725
+ _describe 'command' commands
14726
+ return
14727
+ fi
14746
14728
 
14747
- case "\${words[3]}" in
14748
- get|unset)
14749
- _arguments '3:key:->config_keys'
14750
- ;;
14751
- set)
14752
- _arguments '3:key:->config_keys' '4:value: '
14753
- ;;
14754
- esac
14755
- ;;
14756
- esac
14729
+ case "\${words[2]}" in
14730
+ new)
14731
+ _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, \u2026)]: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: '
14732
+ ;;
14733
+ init)
14734
+ _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
14735
+ ;;
14736
+ completion)
14737
+ _arguments '2:shell:(${shells})'
14738
+ ;;
14739
+ pr)
14740
+ _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: '
14741
+ ;;
14742
+ back)
14743
+ _arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
14744
+ ;;
14745
+ history)
14746
+ _arguments '--json[print history as JSON]'
14747
+ ;;
14748
+ open)
14749
+ _arguments '--editor[editor CLI to use (code, cursor, zed, windsurf, subl, \u2026)]:editor:' '--save[save the chosen editor to global config]' '--workspace[generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)]' '2:branch:->worktrees'
14750
+ ;;
14751
+ go|jump)
14752
+ _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
14753
+ ;;
14754
+ root)
14755
+ _arguments '--print[print the resolved repository root path explicitly]'
14756
+ ;;
14757
+ status)
14758
+ _arguments '--json[print repository and worktree health as JSON]'
14759
+ ;;
14760
+ sync)
14761
+ _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
14762
+ ;;
14763
+ ls)
14764
+ _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
14765
+ ;;
14766
+ clean)
14767
+ _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
14768
+ ;;
14769
+ remove|rm)
14770
+ _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
14771
+ ;;
14772
+ trigger-hook)
14773
+ _arguments "2:hook:(${hooks})"
14774
+ ;;
14775
+ warp)
14776
+ _arguments '(-n --new)'{-n,--new}'[create a new worktree in a registered repo]:branch:' '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
14777
+ ;;
14778
+ config)
14779
+ if (( CURRENT == 3 )); then
14780
+ _values 'config action' get set unset
14781
+ return
14782
+ fi
14757
14783
 
14758
- case "$state" in
14759
- worktrees)
14760
- worktree_branches=(\${(@f)$(__gji_worktree_branches)})
14761
- _describe 'worktree branch' worktree_branches
14762
- ;;
14763
- config_keys)
14764
- _values 'config key' ${configKeys}
14765
- ;;
14766
- esac
14767
- }
14784
+ case "\${words[3]}" in
14785
+ get|unset)
14786
+ _arguments '3:key:->config_keys'
14787
+ ;;
14788
+ set)
14789
+ _arguments '3:key:->config_keys' '4:value: '
14790
+ ;;
14791
+ esac
14792
+ ;;
14793
+ esac
14768
14794
 
14769
- compdef _gji_completion gji`;
14795
+ case "$state" in
14796
+ worktrees)
14797
+ worktree_branches=(\${(@f)$(__gji_worktree_branches)})
14798
+ _describe 'worktree branch' worktree_branches
14799
+ ;;
14800
+ config_keys)
14801
+ _values 'config key' ${configKeys}
14802
+ ;;
14803
+ esac`;
14770
14804
  }
14771
14805
  function escapeSingleQuotes(value) {
14772
14806
  return value.replace(/'/g, `'\\''`);
@@ -14847,7 +14881,11 @@ function writeJson(stdout, value) {
14847
14881
  }
14848
14882
 
14849
14883
  // src/go.ts
14850
- import { basename as basename5 } from "node:path";
14884
+ import { basename as basename6 } from "node:path";
14885
+
14886
+ // src/warp.ts
14887
+ import { realpath as realpath2 } from "node:fs/promises";
14888
+ import { basename as basename5, resolve as resolve5 } from "node:path";
14851
14889
 
14852
14890
  // src/new.ts
14853
14891
  import { mkdir as mkdir4 } from "node:fs/promises";
@@ -14866,9 +14904,9 @@ var EDITORS = [
14866
14904
  ];
14867
14905
  async function defaultSpawnEditor(cli, args) {
14868
14906
  const child = spawn3(cli, args, { detached: true, stdio: "ignore" });
14869
- await new Promise((resolve5, reject) => {
14907
+ await new Promise((resolve6, reject) => {
14870
14908
  child.once("error", reject);
14871
- child.once("spawn", resolve5);
14909
+ child.once("spawn", resolve6);
14872
14910
  });
14873
14911
  child.unref();
14874
14912
  }
@@ -15083,7 +15121,7 @@ async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dep
15083
15121
  }
15084
15122
  }
15085
15123
  async function defaultRunInstallCommand(command, cwd, stderr) {
15086
- await new Promise((resolve5, reject) => {
15124
+ await new Promise((resolve6, reject) => {
15087
15125
  const child = spawn4(command, { cwd, shell: true, stdio: ["ignore", "inherit", "pipe"] });
15088
15126
  child.stderr.on("data", (chunk) => {
15089
15127
  stderr(chunk.toString());
@@ -15092,7 +15130,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
15092
15130
  if (code !== 0) {
15093
15131
  reject(new Error(`exited with code ${code}`));
15094
15132
  } else {
15095
- resolve5();
15133
+ resolve6();
15096
15134
  }
15097
15135
  });
15098
15136
  child.on("error", (err) => {
@@ -15433,7 +15471,7 @@ async function openWorktree(worktreePath, editorCli, spawnFn, stderr) {
15433
15471
  }
15434
15472
 
15435
15473
  // src/repo-registry.ts
15436
- import { mkdir as mkdir5, readFile as readFile3, writeFile as writeFile4 } from "node:fs/promises";
15474
+ import { mkdir as mkdir5, readFile as readFile3, realpath, writeFile as writeFile4 } from "node:fs/promises";
15437
15475
  import { homedir as homedir4 } from "node:os";
15438
15476
  import { basename as basename4, dirname as dirname6, join as join6, resolve as resolve4 } from "node:path";
15439
15477
  var REGISTRY_FILE_NAME = "repos.json";
@@ -15456,21 +15494,46 @@ async function loadRegistry(home = homedir4()) {
15456
15494
  return [];
15457
15495
  }
15458
15496
  }
15497
+ async function canonicalizeRepoPath(repoPath) {
15498
+ try {
15499
+ return await realpath(repoPath);
15500
+ } catch {
15501
+ return resolve4(repoPath);
15502
+ }
15503
+ }
15459
15504
  async function registerRepo(repoPath, home = homedir4()) {
15460
15505
  const registryPath = REGISTRY_FILE_PATH(home);
15461
- const existing = await loadRegistry(home);
15462
- if (existing.length > 0 && existing[0].path === repoPath) return;
15506
+ const existing = await normalizeRegistryForWrite(await loadRegistry(home));
15507
+ const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
15508
+ if (existing.length > 0 && existing[0].path === canonicalRepoPath) return;
15463
15509
  const entry = {
15464
15510
  lastUsed: Date.now(),
15465
- name: basename4(repoPath),
15466
- path: repoPath
15511
+ name: basename4(canonicalRepoPath),
15512
+ path: canonicalRepoPath
15467
15513
  };
15468
- const filtered = existing.filter((e2) => e2.path !== repoPath);
15514
+ const filtered = existing.filter((e2) => e2.path !== canonicalRepoPath);
15469
15515
  const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
15470
15516
  await mkdir5(dirname6(registryPath), { recursive: true });
15471
15517
  await writeFile4(registryPath, `${JSON.stringify(next, null, 2)}
15472
15518
  `, "utf8");
15473
15519
  }
15520
+ async function normalizeRegistryForWrite(entries) {
15521
+ const normalized = [];
15522
+ const seenPaths = /* @__PURE__ */ new Set();
15523
+ for (const entry of entries) {
15524
+ const canonicalPath = await canonicalizeRepoPath(entry.path);
15525
+ if (seenPaths.has(canonicalPath)) {
15526
+ continue;
15527
+ }
15528
+ seenPaths.add(canonicalPath);
15529
+ normalized.push({
15530
+ ...entry,
15531
+ name: basename4(canonicalPath),
15532
+ path: canonicalPath
15533
+ });
15534
+ }
15535
+ return normalized;
15536
+ }
15474
15537
  function isRegistryEntry(value) {
15475
15538
  return typeof value === "object" && value !== null && "path" in value && typeof value.path === "string" && "name" in value && typeof value.name === "string" && "lastUsed" in value && typeof value.lastUsed === "number";
15476
15539
  }
@@ -15515,9 +15578,10 @@ async function runWarpNavigate(options) {
15515
15578
  return 0;
15516
15579
  }
15517
15580
  async function runWarpNew(options, registry) {
15581
+ const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
15518
15582
  let targetRepoRoot;
15519
- if (registry.length === 1) {
15520
- targetRepoRoot = registry[0].path;
15583
+ if (deduplicatedRegistry.length === 1) {
15584
+ targetRepoRoot = deduplicatedRegistry[0].path;
15521
15585
  } else {
15522
15586
  if (isHeadless()) {
15523
15587
  options.stderr(
@@ -15527,7 +15591,7 @@ async function runWarpNew(options, registry) {
15527
15591
  }
15528
15592
  const choice = await ve({
15529
15593
  message: "Create worktree in which repo?",
15530
- options: registry.map((entry) => ({
15594
+ options: deduplicatedRegistry.map((entry) => ({
15531
15595
  value: entry.path,
15532
15596
  label: entry.name,
15533
15597
  hint: entry.path
@@ -15568,6 +15632,30 @@ async function runWarpNew(options, registry) {
15568
15632
  await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
15569
15633
  return 0;
15570
15634
  }
15635
+ async function deduplicateRegistryForNew(registry) {
15636
+ const deduplicated = [];
15637
+ const seenPaths = /* @__PURE__ */ new Set();
15638
+ for (const entry of registry) {
15639
+ const canonicalPath = await canonicalizeRepoPath2(entry.path);
15640
+ if (seenPaths.has(canonicalPath)) {
15641
+ continue;
15642
+ }
15643
+ seenPaths.add(canonicalPath);
15644
+ deduplicated.push({
15645
+ ...entry,
15646
+ name: basename5(canonicalPath),
15647
+ path: canonicalPath
15648
+ });
15649
+ }
15650
+ return deduplicated;
15651
+ }
15652
+ async function canonicalizeRepoPath2(repoPath) {
15653
+ try {
15654
+ return await realpath2(repoPath);
15655
+ } catch {
15656
+ return resolve5(repoPath);
15657
+ }
15658
+ }
15571
15659
  function findByQuery(items, query) {
15572
15660
  const slashIdx = query.indexOf("/");
15573
15661
  if (slashIdx !== -1) {
@@ -15712,7 +15800,7 @@ function createGoCommand(dependencies = {}) {
15712
15800
  await runHook(
15713
15801
  hooks.afterEnter,
15714
15802
  resolvedPath,
15715
- { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename5(repository.repoRoot) },
15803
+ { branch: chosenWorktree?.branch ?? void 0, path: resolvedPath, repo: basename6(repository.repoRoot) },
15716
15804
  options.stderr
15717
15805
  );
15718
15806
  appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => void 0);
@@ -16286,7 +16374,7 @@ function sortWorktrees(worktrees) {
16286
16374
 
16287
16375
  // src/pr.ts
16288
16376
  import { mkdir as mkdir7 } from "node:fs/promises";
16289
- import { basename as basename6, dirname as dirname8 } from "node:path";
16377
+ import { basename as basename7, dirname as dirname8 } from "node:path";
16290
16378
  import { execFile as execFile5 } from "node:child_process";
16291
16379
  import { promisify as promisify6 } from "node:util";
16292
16380
  var execFileAsync5 = promisify6(execFile5);
@@ -16388,7 +16476,7 @@ function createPrCommand(dependencies = {}) {
16388
16476
  await runHook(
16389
16477
  hooks.afterCreate,
16390
16478
  worktreePath,
16391
- { branch: branchName, path: worktreePath, repo: basename6(repository.repoRoot) },
16479
+ { branch: branchName, path: worktreePath, repo: basename7(repository.repoRoot) },
16392
16480
  options.stderr
16393
16481
  );
16394
16482
  if (options.json) {
@@ -16461,7 +16549,7 @@ async function writeOutput2(worktreePath, stdout) {
16461
16549
  }
16462
16550
 
16463
16551
  // src/remove.ts
16464
- import { basename as basename7 } from "node:path";
16552
+ import { basename as basename8 } from "node:path";
16465
16553
  var REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
16466
16554
  function createRemoveCommand(dependencies = {}) {
16467
16555
  const promptForWorktree2 = dependencies.promptForWorktree ?? defaultPromptForWorktree2;
@@ -16526,7 +16614,7 @@ function createRemoveCommand(dependencies = {}) {
16526
16614
  await runHook(
16527
16615
  hooks.beforeRemove,
16528
16616
  worktree.path,
16529
- { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename7(repository.repoRoot) },
16617
+ { branch: worktree.branch ?? void 0, path: worktree.path, repo: basename8(repository.repoRoot) },
16530
16618
  options.stderr
16531
16619
  );
16532
16620
  try {
@@ -1,4 +1,4 @@
1
- import { mkdir, readFile, writeFile } from 'node:fs/promises';
1
+ import { mkdir, readFile, realpath, writeFile } from 'node:fs/promises';
2
2
  import { homedir } from 'node:os';
3
3
  import { basename, dirname, join, resolve } from 'node:path';
4
4
  import { GLOBAL_CONFIG_DIRECTORY } from './config.js';
@@ -24,22 +24,48 @@ export async function loadRegistry(home = homedir()) {
24
24
  return [];
25
25
  }
26
26
  }
27
+ async function canonicalizeRepoPath(repoPath) {
28
+ try {
29
+ return await realpath(repoPath);
30
+ }
31
+ catch {
32
+ return resolve(repoPath);
33
+ }
34
+ }
27
35
  export async function registerRepo(repoPath, home = homedir()) {
28
36
  const registryPath = REGISTRY_FILE_PATH(home);
29
- const existing = await loadRegistry(home);
37
+ const existing = await normalizeRegistryForWrite(await loadRegistry(home));
38
+ const canonicalRepoPath = await canonicalizeRepoPath(repoPath);
30
39
  // Skip write if this repo is already the most-recently-used entry (common case).
31
- if (existing.length > 0 && existing[0].path === repoPath)
40
+ if (existing.length > 0 && existing[0].path === canonicalRepoPath)
32
41
  return;
33
42
  const entry = {
34
43
  lastUsed: Date.now(),
35
- name: basename(repoPath),
36
- path: repoPath,
44
+ name: basename(canonicalRepoPath),
45
+ path: canonicalRepoPath,
37
46
  };
38
- const filtered = existing.filter((e) => e.path !== repoPath);
47
+ const filtered = existing.filter((e) => e.path !== canonicalRepoPath);
39
48
  const next = [entry, ...filtered].slice(0, MAX_REGISTRY_ENTRIES);
40
49
  await mkdir(dirname(registryPath), { recursive: true });
41
50
  await writeFile(registryPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
42
51
  }
52
+ async function normalizeRegistryForWrite(entries) {
53
+ const normalized = [];
54
+ const seenPaths = new Set();
55
+ for (const entry of entries) {
56
+ const canonicalPath = await canonicalizeRepoPath(entry.path);
57
+ if (seenPaths.has(canonicalPath)) {
58
+ continue;
59
+ }
60
+ seenPaths.add(canonicalPath);
61
+ normalized.push({
62
+ ...entry,
63
+ name: basename(canonicalPath),
64
+ path: canonicalPath,
65
+ });
66
+ }
67
+ return normalized;
68
+ }
43
69
  function isRegistryEntry(value) {
44
70
  return (typeof value === 'object' &&
45
71
  value !== null &&
@@ -1,9 +1,14 @@
1
+ import { KNOWN_GLOBAL_CONFIG_KEYS } from './config.js';
1
2
  const TOP_LEVEL_COMMANDS = [
2
3
  { name: 'new', description: 'create a new branch or detached linked worktree' },
3
4
  { name: 'init', description: 'print or install shell integration' },
4
5
  { name: 'completion', description: 'print shell completion definitions' },
5
6
  { name: 'pr', description: 'fetch a pull request into a linked worktree' },
7
+ { name: 'back', description: 'navigate to the previously visited worktree' },
8
+ { name: 'history', description: 'show navigation history' },
9
+ { name: 'open', description: 'open the worktree in an editor' },
6
10
  { name: 'go', description: 'print or select a worktree path' },
11
+ { name: 'jump', description: 'alias of go' },
7
12
  { name: 'root', description: 'print the main repository root path' },
8
13
  { name: 'status', description: 'summarize repository and worktree health' },
9
14
  { name: 'sync', description: 'fetch and update one or all worktrees' },
@@ -12,20 +17,12 @@ const TOP_LEVEL_COMMANDS = [
12
17
  { name: 'remove', description: 'remove a linked worktree and delete its branch when present' },
13
18
  { name: 'rm', description: 'alias of remove' },
14
19
  { name: 'trigger-hook', description: 'run a named hook in the current worktree' },
20
+ { name: 'warp', description: 'jump to any worktree across all known repos' },
15
21
  { name: 'config', description: 'manage global config defaults' },
16
22
  ];
17
23
  const SHELL_NAMES = ['bash', 'fish', 'zsh'];
18
24
  const HOOK_NAMES = ['afterCreate', 'afterEnter', 'beforeRemove'];
19
- const CONFIG_KEYS = [
20
- 'branchPrefix',
21
- 'syncRemote',
22
- 'syncDefaultBranch',
23
- 'syncFiles',
24
- 'skipInstallPrompt',
25
- 'installSaveTarget',
26
- 'hooks',
27
- 'repos',
28
- ];
25
+ const CONFIG_KEYS = Array.from(KNOWN_GLOBAL_CONFIG_KEYS);
29
26
  export function renderShellCompletion(shell) {
30
27
  switch (shell) {
31
28
  case 'bash':
@@ -59,7 +56,7 @@ _gji_completion() {
59
56
 
60
57
  case "$command_name" in
61
58
  new)
62
- COMPREPLY=( $(compgen -W "--detached --dry-run --json --help" -- "$cur") )
59
+ COMPREPLY=( $(compgen -W "--detached --force --open --editor --dry-run --json --help" -- "$cur") )
63
60
  ;;
64
61
  init)
65
62
  COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
@@ -70,7 +67,16 @@ _gji_completion() {
70
67
  pr)
71
68
  COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
72
69
  ;;
73
- go)
70
+ back)
71
+ COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
72
+ ;;
73
+ history)
74
+ COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
75
+ ;;
76
+ open)
77
+ COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --editor --save --workspace --help" -- "$cur") )
78
+ ;;
79
+ go|jump)
74
80
  COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
75
81
  ;;
76
82
  root)
@@ -94,6 +100,9 @@ _gji_completion() {
94
100
  trigger-hook)
95
101
  COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
96
102
  ;;
103
+ warp)
104
+ COMPREPLY=( $(compgen -W "-n --new --print --json --help" -- "$cur") )
105
+ ;;
97
106
  config)
98
107
  if [ "$COMP_CWORD" -eq 2 ]; then
99
108
  COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
@@ -149,6 +158,9 @@ complete -c gji -f
149
158
  ${commandLines}
150
159
 
151
160
  complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
161
+ complete -c gji -n '__fish_seen_subcommand_from new' -l force -d 'remove and recreate the worktree if the target path already exists'
162
+ complete -c gji -n '__fish_seen_subcommand_from new' -l open -d 'open the new worktree in an editor after creation'
163
+ complete -c gji -n '__fish_seen_subcommand_from new' -l editor -r -d 'editor CLI to use with --open (code, cursor, zed, …)'
152
164
  complete -c gji -n '__fish_seen_subcommand_from new' -l dry-run -d 'show what would be created without executing any git commands or writing files'
153
165
  complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
154
166
 
@@ -162,8 +174,17 @@ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
162
174
  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'
163
175
  complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
164
176
 
165
- complete -c gji -n '__fish_seen_subcommand_from go' -l print -d 'print the resolved worktree path explicitly'
166
- complete -c gji -n '__fish_seen_subcommand_from go' -a '(__gji_worktree_branches)' -d 'worktree branch'
177
+ complete -c gji -n '__fish_seen_subcommand_from back' -l print -d 'print the resolved worktree path explicitly'
178
+
179
+ complete -c gji -n '__fish_seen_subcommand_from history' -l json -d 'print history as JSON'
180
+
181
+ complete -c gji -n '__fish_seen_subcommand_from open' -l editor -r -d 'editor CLI to use (code, cursor, zed, windsurf, subl, …)'
182
+ complete -c gji -n '__fish_seen_subcommand_from open' -l save -d 'save the chosen editor to global config'
183
+ complete -c gji -n '__fish_seen_subcommand_from open' -l workspace -d 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)'
184
+ complete -c gji -n '__fish_seen_subcommand_from open' -a '(__gji_worktree_branches)' -d 'worktree branch'
185
+
186
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -l print -d 'print the resolved worktree path explicitly'
187
+ complete -c gji -n '__fish_seen_subcommand_from go jump' -a '(__gji_worktree_branches)' -d 'worktree branch'
167
188
 
168
189
  complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
169
190
 
@@ -187,6 +208,10 @@ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_b
187
208
 
188
209
  ${hookLines}
189
210
 
211
+ complete -c gji -n '__fish_seen_subcommand_from warp' -s n -l new -d 'create a new worktree in a registered repo'
212
+ complete -c gji -n '__fish_seen_subcommand_from warp' -l print -d 'print the resolved worktree path without changing directory'
213
+ complete -c gji -n '__fish_seen_subcommand_from warp' -l json -d 'emit JSON on success or error instead of human-readable output'
214
+
190
215
  complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
191
216
  ${configKeyLines}`;
192
217
  }
@@ -195,89 +220,99 @@ function renderZshCompletion() {
195
220
  const configKeys = CONFIG_KEYS.join(' ');
196
221
  const shells = SHELL_NAMES.join(' ');
197
222
  const hooks = HOOK_NAMES.join(' ');
198
- return `__gji_worktree_branches() {
223
+ return `#compdef gji
224
+
225
+ __gji_worktree_branches() {
199
226
  command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
200
227
  }
201
228
 
202
- _gji_completion() {
203
- local context state line
204
- local -a commands worktree_branches
205
-
206
- commands=(
207
- ${commandLines}
208
- )
229
+ local context state line
230
+ local -a commands worktree_branches
209
231
 
210
- if (( CURRENT == 2 )); then
211
- _describe 'command' commands
212
- return
213
- fi
232
+ commands=(
233
+ ${commandLines}
234
+ )
214
235
 
215
- case "\${words[2]}" in
216
- new)
217
- _arguments '--detached[create a detached worktree without a branch]' '--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: '
218
- ;;
219
- init)
220
- _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
221
- ;;
222
- completion)
223
- _arguments '2:shell:(${shells})'
224
- ;;
225
- pr)
226
- _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: '
227
- ;;
228
- go)
229
- _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
230
- ;;
231
- root)
232
- _arguments '--print[print the resolved repository root path explicitly]'
233
- ;;
234
- status)
235
- _arguments '--json[print repository and worktree health as JSON]'
236
- ;;
237
- sync)
238
- _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
239
- ;;
240
- ls)
241
- _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
242
- ;;
243
- clean)
244
- _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
245
- ;;
246
- remove|rm)
247
- _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
248
- ;;
249
- trigger-hook)
250
- _arguments "2:hook:(${hooks})"
251
- ;;
252
- config)
253
- if (( CURRENT == 3 )); then
254
- _values 'config action' get set unset
255
- return
256
- fi
236
+ if (( CURRENT == 2 )); then
237
+ _describe 'command' commands
238
+ return
239
+ fi
257
240
 
258
- case "\${words[3]}" in
259
- get|unset)
260
- _arguments '3:key:->config_keys'
261
- ;;
262
- set)
263
- _arguments '3:key:->config_keys' '4:value: '
264
- ;;
265
- esac
266
- ;;
267
- esac
241
+ case "\${words[2]}" in
242
+ new)
243
+ _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: '
244
+ ;;
245
+ init)
246
+ _arguments '--write[write the integration to the shell config file]' '2:shell:(${shells})'
247
+ ;;
248
+ completion)
249
+ _arguments '2:shell:(${shells})'
250
+ ;;
251
+ pr)
252
+ _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: '
253
+ ;;
254
+ back)
255
+ _arguments '--print[print the resolved worktree path explicitly]' '2:steps: '
256
+ ;;
257
+ history)
258
+ _arguments '--json[print history as JSON]'
259
+ ;;
260
+ open)
261
+ _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'
262
+ ;;
263
+ go|jump)
264
+ _arguments '--print[print the resolved worktree path explicitly]' '2:branch:->worktrees'
265
+ ;;
266
+ root)
267
+ _arguments '--print[print the resolved repository root path explicitly]'
268
+ ;;
269
+ status)
270
+ _arguments '--json[print repository and worktree health as JSON]'
271
+ ;;
272
+ sync)
273
+ _arguments '--all[sync every worktree in the repository]' '--json[emit JSON on success or error instead of human-readable output]'
274
+ ;;
275
+ ls)
276
+ _arguments '--compact[show only branch and path columns]' '--json[print active worktrees as JSON]'
277
+ ;;
278
+ clean)
279
+ _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches]' '--stale[only target clean worktrees whose upstream is gone and branch is merged into the default branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]'
280
+ ;;
281
+ remove|rm)
282
+ _arguments '(-f --force)'{-f,--force}'[bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch]' '--dry-run[show what would be deleted without removing anything]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch:->worktrees'
283
+ ;;
284
+ trigger-hook)
285
+ _arguments "2:hook:(${hooks})"
286
+ ;;
287
+ warp)
288
+ _arguments '(-n --new)'{-n,--new}'[create a new worktree in a registered repo]:branch:' '--print[print the resolved worktree path without changing directory]' '--json[emit JSON on success or error instead of human-readable output]' '2:branch: '
289
+ ;;
290
+ config)
291
+ if (( CURRENT == 3 )); then
292
+ _values 'config action' get set unset
293
+ return
294
+ fi
268
295
 
269
- case "$state" in
270
- worktrees)
271
- worktree_branches=(\${(@f)$(__gji_worktree_branches)})
272
- _describe 'worktree branch' worktree_branches
273
- ;;
274
- config_keys)
275
- _values 'config key' ${configKeys}
276
- ;;
277
- esac
278
- }
296
+ case "\${words[3]}" in
297
+ get|unset)
298
+ _arguments '3:key:->config_keys'
299
+ ;;
300
+ set)
301
+ _arguments '3:key:->config_keys' '4:value: '
302
+ ;;
303
+ esac
304
+ ;;
305
+ esac
279
306
 
280
- compdef _gji_completion gji`;
307
+ case "$state" in
308
+ worktrees)
309
+ worktree_branches=(\${(@f)$(__gji_worktree_branches)})
310
+ _describe 'worktree branch' worktree_branches
311
+ ;;
312
+ config_keys)
313
+ _values 'config key' ${configKeys}
314
+ ;;
315
+ esac`;
281
316
  }
282
317
  function escapeSingleQuotes(value) {
283
318
  return value.replace(/'/g, `'\\''`);
package/dist/warp.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { isCancel, select } from '@clack/prompts';
2
+ import { realpath } from 'node:fs/promises';
3
+ import { basename, resolve } from 'node:path';
2
4
  import { readWorktreeHealth } from './git.js';
3
5
  import { isHeadless } from './headless.js';
4
6
  import { appendHistory } from './history.js';
@@ -43,9 +45,10 @@ async function runWarpNavigate(options) {
43
45
  return 0;
44
46
  }
45
47
  async function runWarpNew(options, registry) {
48
+ const deduplicatedRegistry = await deduplicateRegistryForNew(registry);
46
49
  let targetRepoRoot;
47
- if (registry.length === 1) {
48
- targetRepoRoot = registry[0].path;
50
+ if (deduplicatedRegistry.length === 1) {
51
+ targetRepoRoot = deduplicatedRegistry[0].path;
49
52
  }
50
53
  else {
51
54
  if (isHeadless()) {
@@ -54,7 +57,7 @@ async function runWarpNew(options, registry) {
54
57
  }
55
58
  const choice = await select({
56
59
  message: 'Create worktree in which repo?',
57
- options: registry.map((entry) => ({
60
+ options: deduplicatedRegistry.map((entry) => ({
58
61
  value: entry.path,
59
62
  label: entry.name,
60
63
  hint: entry.path,
@@ -98,6 +101,31 @@ async function runWarpNew(options, registry) {
98
101
  await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
99
102
  return 0;
100
103
  }
104
+ async function deduplicateRegistryForNew(registry) {
105
+ const deduplicated = [];
106
+ const seenPaths = new Set();
107
+ for (const entry of registry) {
108
+ const canonicalPath = await canonicalizeRepoPath(entry.path);
109
+ if (seenPaths.has(canonicalPath)) {
110
+ continue;
111
+ }
112
+ seenPaths.add(canonicalPath);
113
+ deduplicated.push({
114
+ ...entry,
115
+ name: basename(canonicalPath),
116
+ path: canonicalPath,
117
+ });
118
+ }
119
+ return deduplicated;
120
+ }
121
+ async function canonicalizeRepoPath(repoPath) {
122
+ try {
123
+ return await realpath(repoPath);
124
+ }
125
+ catch {
126
+ return resolve(repoPath);
127
+ }
128
+ }
101
129
  function findByQuery(items, query) {
102
130
  const slashIdx = query.indexOf('/');
103
131
  if (slashIdx !== -1) {
@@ -1,4 +1,4 @@
1
- .TH GJI\-BACK 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-BACK 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-back \- navigate to the previously visited worktree, optionally N steps back
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-CLEAN 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-CLEAN 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-clean \- interactively prune linked worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-COMPLETION 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-COMPLETION 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-completion \- print shell completion definitions
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-CONFIG 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-CONFIG 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-config \- manage global config defaults
4
4
  .SH SYNOPSIS
package/man/man1/gji-go.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-GO 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-GO 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-go \- print or select a worktree path
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-HISTORY 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-HISTORY 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-history \- show navigation history
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-INIT 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-INIT 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-init \- print or install shell integration
4
4
  .SH SYNOPSIS
package/man/man1/gji-ls.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-LS 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-LS 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-ls \- list active worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-NEW 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-NEW 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-new \- create a new branch or detached linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-OPEN 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-OPEN 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-open \- open the worktree in an editor
4
4
  .SH SYNOPSIS
package/man/man1/gji-pr.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI\-PR 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-PR 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-pr \- fetch a pull request by number, #number, or URL into a linked worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-REMOVE 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-REMOVE 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-remove \- remove a linked worktree and delete its branch when present
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-ROOT 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-ROOT 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-root \- print the main repository root path
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-STATUS 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-STATUS 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-status \- summarize repository and worktree health
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-SYNC 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-SYNC 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-sync \- fetch and update one or all worktrees
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-TRIGGER\-HOOK 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-TRIGGER\-HOOK 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-trigger\-hook \- run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree
4
4
  .SH SYNOPSIS
@@ -1,4 +1,4 @@
1
- .TH GJI\-WARP 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI\-WARP 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji\-warp \- jump to any worktree across all known repos
4
4
  .SH SYNOPSIS
package/man/man1/gji.1 CHANGED
@@ -1,4 +1,4 @@
1
- .TH GJI 1 "May 2026" "gji 0.6.0" "User Commands"
1
+ .TH GJI 1 "May 2026" "gji 0.6.2" "User Commands"
2
2
  .SH NAME
3
3
  gji \- Context switching without the mess.
4
4
  .SH SYNOPSIS
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.6.0",
3
+ "version": "0.6.2",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",