@solaqua/gji 0.4.0 → 0.5.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 (47) hide show
  1. package/README.md +124 -11
  2. package/dist/back.d.ts +13 -0
  3. package/dist/back.js +74 -0
  4. package/dist/clean.d.ts +1 -0
  5. package/dist/clean.js +103 -13
  6. package/dist/cli.js +104 -4
  7. package/dist/completion.d.ts +6 -0
  8. package/dist/completion.js +11 -0
  9. package/dist/git.d.ts +3 -0
  10. package/dist/git.js +38 -0
  11. package/dist/gji +5 -0
  12. package/dist/gji-bundle.mjs +16705 -0
  13. package/dist/go.js +2 -0
  14. package/dist/history-command.d.ts +7 -0
  15. package/dist/history-command.js +15 -0
  16. package/dist/history.d.ts +9 -0
  17. package/dist/history.js +46 -0
  18. package/dist/init.d.ts +1 -1
  19. package/dist/init.js +9 -22
  20. package/dist/ls.d.ts +3 -0
  21. package/dist/ls.js +46 -2
  22. package/dist/new.js +3 -0
  23. package/dist/pr.js +46 -1
  24. package/dist/shell-completion.d.ts +1 -0
  25. package/dist/shell-completion.js +284 -0
  26. package/dist/shell.d.ts +2 -0
  27. package/dist/shell.js +21 -0
  28. package/dist/sync.js +2 -13
  29. package/dist/worktree-info.d.ts +33 -0
  30. package/dist/worktree-info.js +105 -0
  31. package/man/man1/gji-back.1 +13 -0
  32. package/man/man1/gji-clean.1 +4 -1
  33. package/man/man1/gji-completion.1 +9 -0
  34. package/man/man1/gji-config.1 +1 -1
  35. package/man/man1/gji-go.1 +1 -1
  36. package/man/man1/gji-history.1 +13 -0
  37. package/man/man1/gji-init.1 +1 -1
  38. package/man/man1/gji-ls.1 +4 -1
  39. package/man/man1/gji-new.1 +1 -1
  40. package/man/man1/gji-pr.1 +1 -1
  41. package/man/man1/gji-remove.1 +1 -1
  42. package/man/man1/gji-root.1 +1 -1
  43. package/man/man1/gji-status.1 +1 -1
  44. package/man/man1/gji-sync.1 +1 -1
  45. package/man/man1/gji-trigger-hook.1 +1 -1
  46. package/man/man1/gji.1 +13 -1
  47. package/package.json +11 -2
package/dist/go.js CHANGED
@@ -4,6 +4,7 @@ import { loadEffectiveConfig } from './config.js';
4
4
  import { readWorktreeHealth } from './git.js';
5
5
  import { isHeadless } from './headless.js';
6
6
  import { extractHooks, runHook } from './hooks.js';
7
+ import { appendHistory } from './history.js';
7
8
  import { detectRepository, listWorktrees, sortByCurrentFirst } from './repo.js';
8
9
  import { writeShellOutput } from './shell-handoff.js';
9
10
  const GO_OUTPUT_FILE_ENV = 'GJI_GO_OUTPUT_FILE';
@@ -36,6 +37,7 @@ export function createGoCommand(dependencies = {}) {
36
37
  const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
37
38
  const hooks = extractHooks(config);
38
39
  await runHook(hooks.afterEnter, resolvedPath, { branch: chosenWorktree?.branch ?? undefined, path: resolvedPath, repo: basename(repository.repoRoot) }, options.stderr);
40
+ appendHistory(resolvedPath, chosenWorktree?.branch ?? null).catch(() => undefined);
39
41
  await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
40
42
  return 0;
41
43
  };
@@ -0,0 +1,7 @@
1
+ export interface HistoryCommandOptions {
2
+ cwd: string;
3
+ home?: string;
4
+ json?: boolean;
5
+ stdout: (chunk: string) => void;
6
+ }
7
+ export declare function runHistoryCommand(options: HistoryCommandOptions): Promise<number>;
@@ -0,0 +1,15 @@
1
+ import { loadHistory } from './history.js';
2
+ import { formatHistoryList } from './back.js';
3
+ export async function runHistoryCommand(options) {
4
+ const history = await loadHistory(options.home);
5
+ if (options.json) {
6
+ options.stdout(`${JSON.stringify(history, null, 2)}\n`);
7
+ return 0;
8
+ }
9
+ if (history.length === 0) {
10
+ options.stdout('No navigation history.\n');
11
+ return 0;
12
+ }
13
+ options.stdout(formatHistoryList(history, options.cwd));
14
+ return 0;
15
+ }
@@ -0,0 +1,9 @@
1
+ export declare const HISTORY_FILE_NAME = "history.json";
2
+ export interface HistoryEntry {
3
+ branch: string | null;
4
+ path: string;
5
+ timestamp: number;
6
+ }
7
+ export declare function HISTORY_FILE_PATH(home?: string): string;
8
+ export declare function loadHistory(home?: string): Promise<HistoryEntry[]>;
9
+ export declare function appendHistory(path: string, branch: string | null, home?: string): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { GLOBAL_CONFIG_DIRECTORY } from './config.js';
5
+ export const HISTORY_FILE_NAME = 'history.json';
6
+ const MAX_HISTORY_ENTRIES = 50;
7
+ export function HISTORY_FILE_PATH(home = homedir()) {
8
+ const configDir = process.env.GJI_CONFIG_DIR;
9
+ if (configDir) {
10
+ return join(resolve(configDir), HISTORY_FILE_NAME);
11
+ }
12
+ return join(home, GLOBAL_CONFIG_DIRECTORY, HISTORY_FILE_NAME);
13
+ }
14
+ export async function loadHistory(home = homedir()) {
15
+ const path = HISTORY_FILE_PATH(home);
16
+ try {
17
+ const raw = await readFile(path, 'utf8');
18
+ const parsed = JSON.parse(raw);
19
+ if (!Array.isArray(parsed))
20
+ return [];
21
+ return parsed.filter(isHistoryEntry);
22
+ }
23
+ catch {
24
+ return [];
25
+ }
26
+ }
27
+ export async function appendHistory(path, branch, home = homedir()) {
28
+ const historyPath = HISTORY_FILE_PATH(home);
29
+ const existing = await loadHistory(home);
30
+ // Skip if the most recent entry is the same path (no-op navigation)
31
+ if (existing.length > 0 && existing[0].path === path) {
32
+ return;
33
+ }
34
+ const entry = { branch, path, timestamp: Date.now() };
35
+ const next = [entry, ...existing].slice(0, MAX_HISTORY_ENTRIES);
36
+ await mkdir(dirname(historyPath), { recursive: true });
37
+ await writeFile(historyPath, `${JSON.stringify(next, null, 2)}\n`, 'utf8');
38
+ }
39
+ function isHistoryEntry(value) {
40
+ return (typeof value === 'object' &&
41
+ value !== null &&
42
+ 'path' in value &&
43
+ typeof value.path === 'string' &&
44
+ 'timestamp' in value &&
45
+ typeof value.timestamp === 'number');
46
+ }
package/dist/init.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type SupportedShell = 'bash' | 'fish' | 'zsh';
1
+ import { type SupportedShell } from './shell.js';
2
2
  export type InstallSaveTarget = 'local' | 'global';
3
3
  export interface SetupWizardResult {
4
4
  branchPrefix?: string;
package/dist/init.js CHANGED
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  import { intro, isCancel, outro, select, text } from '@clack/prompts';
5
5
  import { loadConfig, loadGlobalConfig, saveGlobalConfig, saveLocalConfig, updateGlobalConfigKey } from './config.js';
6
+ import { resolveSupportedShell } from './shell.js';
6
7
  const START_MARKER = '# >>> gji init >>>';
7
8
  const END_MARKER = '# <<< gji init <<<';
8
9
  const SHELL_WRAPPED_COMMANDS = [
@@ -20,6 +21,13 @@ const SHELL_WRAPPED_COMMANDS = [
20
21
  names: ['pr'],
21
22
  tempPrefix: 'gji-pr',
22
23
  },
24
+ {
25
+ bypassOption: '--print',
26
+ commandName: 'back',
27
+ envVar: 'GJI_BACK_OUTPUT_FILE',
28
+ names: ['back'],
29
+ tempPrefix: 'gji-back',
30
+ },
23
31
  {
24
32
  bypassOption: '--print',
25
33
  commandName: 'go',
@@ -43,7 +51,7 @@ const SHELL_WRAPPED_COMMANDS = [
43
51
  },
44
52
  ];
45
53
  export async function runInitCommand(options) {
46
- const shell = resolveShell(options.shell, process.env.SHELL);
54
+ const shell = resolveSupportedShell(options.shell, process.env.SHELL);
47
55
  const home = options.home ?? homedir();
48
56
  if (!shell) {
49
57
  options.stderr?.('Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n');
@@ -139,27 +147,6 @@ async function saveWizardConfig(result, cwd, home) {
139
147
  await saveGlobalConfig({ ...existing, ...values }, home);
140
148
  }
141
149
  }
142
- function resolveShell(requestedShell, detectedShell) {
143
- const requested = normalizeShell(requestedShell);
144
- if (requested) {
145
- return requested;
146
- }
147
- return normalizeShell(detectedShell);
148
- }
149
- function normalizeShell(value) {
150
- if (!value) {
151
- return null;
152
- }
153
- const candidate = value.split('/').at(-1)?.toLowerCase();
154
- switch (candidate) {
155
- case 'bash':
156
- case 'fish':
157
- case 'zsh':
158
- return candidate;
159
- default:
160
- return null;
161
- }
162
- }
163
150
  function resolveShellConfigPath(shell, home) {
164
151
  switch (shell) {
165
152
  case 'bash':
package/dist/ls.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { type WorktreeEntry } from './repo.js';
2
+ import { type WorktreeInfo } from './worktree-info.js';
2
3
  export interface LsCommandOptions {
4
+ compact?: boolean;
3
5
  cwd: string;
4
6
  json?: boolean;
5
7
  stdout: (chunk: string) => void;
6
8
  }
7
9
  export declare function runLsCommand(options: LsCommandOptions): Promise<number>;
10
+ export declare function formatDetailedWorktreeTable(worktrees: WorktreeInfo[]): string;
8
11
  export declare function formatWorktreeTable(worktrees: WorktreeEntry[]): string;
package/dist/ls.js CHANGED
@@ -1,14 +1,58 @@
1
1
  import { listWorktrees } from './repo.js';
2
2
  import { comparePaths } from './paths.js';
3
+ import { formatLastCommit, formatUpstreamState, readWorktreeInfos, } from './worktree-info.js';
3
4
  export async function runLsCommand(options) {
4
5
  const worktrees = sortWorktrees(await listWorktrees(options.cwd));
6
+ if (options.compact) {
7
+ if (options.json) {
8
+ options.stdout(`${JSON.stringify(worktrees, null, 2)}\n`);
9
+ return 0;
10
+ }
11
+ options.stdout(`${formatWorktreeTable(worktrees)}\n`);
12
+ return 0;
13
+ }
14
+ const infos = await readWorktreeInfos(worktrees);
5
15
  if (options.json) {
6
- options.stdout(`${JSON.stringify(worktrees, null, 2)}\n`);
16
+ options.stdout(`${JSON.stringify(infos, null, 2)}\n`);
7
17
  return 0;
8
18
  }
9
- options.stdout(`${formatWorktreeTable(worktrees)}\n`);
19
+ options.stdout(`${formatDetailedWorktreeTable(infos)}\n`);
10
20
  return 0;
11
21
  }
22
+ export function formatDetailedWorktreeTable(worktrees) {
23
+ const rows = worktrees.map((worktree) => ({
24
+ branch: worktree.branch ?? '(detached)',
25
+ isCurrent: worktree.isCurrent,
26
+ lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
27
+ path: worktree.path,
28
+ status: worktree.status,
29
+ upstream: formatUpstreamState(worktree.upstream),
30
+ }));
31
+ const branchWidth = Math.max('BRANCH'.length, ...rows.map((row) => row.branch.length));
32
+ const statusWidth = Math.max('STATUS'.length, ...rows.map((row) => row.status.length));
33
+ const upstreamWidth = Math.max('UPSTREAM'.length, ...rows.map((row) => row.upstream.length));
34
+ const lastCommitWidth = Math.max('LAST'.length, ...rows.map((row) => row.lastCommit.length));
35
+ const lines = [
36
+ ' '
37
+ + 'BRANCH'.padEnd(branchWidth, ' ')
38
+ + ' '
39
+ + 'STATUS'.padEnd(statusWidth, ' ')
40
+ + ' '
41
+ + 'UPSTREAM'.padEnd(upstreamWidth, ' ')
42
+ + ' '
43
+ + 'LAST'.padEnd(lastCommitWidth, ' ')
44
+ + ' PATH',
45
+ ];
46
+ for (const row of rows) {
47
+ lines.push(`${row.isCurrent ? '*' : ' '} `
48
+ + `${row.branch.padEnd(branchWidth, ' ')} `
49
+ + `${row.status.padEnd(statusWidth, ' ')} `
50
+ + `${row.upstream.padEnd(upstreamWidth, ' ')} `
51
+ + `${row.lastCommit.padEnd(lastCommitWidth, ' ')} `
52
+ + row.path);
53
+ }
54
+ return lines.join('\n');
55
+ }
12
56
  export function formatWorktreeTable(worktrees) {
13
57
  const rows = worktrees.map((worktree) => ({
14
58
  branch: worktree.branch ?? '(detached)',
package/dist/new.js CHANGED
@@ -6,6 +6,7 @@ import { isCancel, text } from '@clack/prompts';
6
6
  import { loadEffectiveConfig, resolveConfigString } from './config.js';
7
7
  import { syncFiles } from './file-sync.js';
8
8
  import { extractHooks, runHook } from './hooks.js';
9
+ import { appendHistory } from './history.js';
9
10
  import { isHeadless } from './headless.js';
10
11
  import { maybeRunInstallPrompt } from './install-prompt.js';
11
12
  import { pathExists, promptForPathConflict } from './conflict.js';
@@ -105,6 +106,7 @@ export function createNewCommand(dependencies = {}) {
105
106
  else {
106
107
  const choice = await prompt(worktreePath);
107
108
  if (choice === 'reuse') {
109
+ appendHistory(worktreePath, worktreeName).catch(() => undefined);
108
110
  await writeOutput(worktreePath, options.stdout);
109
111
  return 0;
110
112
  }
@@ -147,6 +149,7 @@ export function createNewCommand(dependencies = {}) {
147
149
  options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}\n`);
148
150
  }
149
151
  else {
152
+ await appendHistory(worktreePath, worktreeName);
150
153
  await writeOutput(worktreePath, options.stdout);
151
154
  }
152
155
  return 0;
package/dist/pr.js CHANGED
@@ -6,6 +6,7 @@ import { loadEffectiveConfig, resolveConfigString } from './config.js';
6
6
  import { syncFiles } from './file-sync.js';
7
7
  import { pathExists, promptForPathConflict } from './conflict.js';
8
8
  import { extractHooks, runHook } from './hooks.js';
9
+ import { appendHistory } from './history.js';
9
10
  import { isHeadless } from './headless.js';
10
11
  import { maybeRunInstallPrompt } from './install-prompt.js';
11
12
  import { detectRepository, resolveWorktreePath } from './repo.js';
@@ -58,6 +59,7 @@ export function createPrCommand(dependencies = {}) {
58
59
  }
59
60
  const choice = await prompt(worktreePath);
60
61
  if (choice === 'reuse') {
62
+ appendHistory(worktreePath, branchName).catch(() => undefined);
61
63
  await writeOutput(worktreePath, options.stdout);
62
64
  return 0;
63
65
  }
@@ -74,7 +76,7 @@ export function createPrCommand(dependencies = {}) {
74
76
  return 0;
75
77
  }
76
78
  try {
77
- await execFileAsync('git', ['fetch', 'origin', `refs/pull/${prNumber}/head:${remoteRef}`], { cwd: repository.repoRoot });
79
+ await fetchPullRequestRef(repository.repoRoot, options.number, prNumber, remoteRef);
78
80
  }
79
81
  catch {
80
82
  const message = `Failed to fetch PR #${prNumber} from origin`;
@@ -112,6 +114,7 @@ export function createPrCommand(dependencies = {}) {
112
114
  options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}\n`);
113
115
  }
114
116
  else {
117
+ await appendHistory(worktreePath, branchName);
115
118
  await writeOutput(worktreePath, options.stdout);
116
119
  }
117
120
  return 0;
@@ -127,6 +130,48 @@ async function localBranchExists(repoRoot, branchName) {
127
130
  }
128
131
  }
129
132
  export const runPrCommand = createPrCommand();
133
+ async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
134
+ for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
135
+ try {
136
+ await execFileAsync('git', ['fetch', 'origin', `${sourceRef}:${remoteRef}`], { cwd: repoRoot });
137
+ return;
138
+ }
139
+ catch {
140
+ // Try the next forge-specific ref namespace before failing the command.
141
+ }
142
+ }
143
+ throw new Error(`No pull request ref found for #${prNumber}`);
144
+ }
145
+ function listPullRequestSourceRefs(input, prNumber) {
146
+ const allForges = ['github', 'gitlab', 'bitbucket'];
147
+ const preferredForge = detectPullRequestForge(input);
148
+ const orderedForges = preferredForge === 'unknown'
149
+ ? allForges
150
+ : [preferredForge, ...allForges.filter((forge) => forge !== preferredForge)];
151
+ return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
152
+ }
153
+ function detectPullRequestForge(input) {
154
+ if (/\/pull-requests\/\d+/.test(input)) {
155
+ return 'bitbucket';
156
+ }
157
+ if (/\/merge_requests\/\d+/.test(input)) {
158
+ return 'gitlab';
159
+ }
160
+ if (/\/pull\/\d+/.test(input)) {
161
+ return 'github';
162
+ }
163
+ return 'unknown';
164
+ }
165
+ function sourceRefForForge(forge, prNumber) {
166
+ switch (forge) {
167
+ case 'bitbucket':
168
+ return `refs/pull-requests/${prNumber}/from`;
169
+ case 'github':
170
+ return `refs/pull/${prNumber}/head`;
171
+ case 'gitlab':
172
+ return `refs/merge-requests/${prNumber}/head`;
173
+ }
174
+ }
130
175
  async function writeOutput(worktreePath, stdout) {
131
176
  await writeShellOutput(PR_OUTPUT_FILE_ENV, worktreePath, stdout);
132
177
  }
@@ -0,0 +1 @@
1
+ export declare function renderShellCompletion(shell: 'bash' | 'fish' | 'zsh'): string;
@@ -0,0 +1,284 @@
1
+ const TOP_LEVEL_COMMANDS = [
2
+ { name: 'new', description: 'create a new branch or detached linked worktree' },
3
+ { name: 'init', description: 'print or install shell integration' },
4
+ { name: 'completion', description: 'print shell completion definitions' },
5
+ { name: 'pr', description: 'fetch a pull request into a linked worktree' },
6
+ { name: 'go', description: 'print or select a worktree path' },
7
+ { name: 'root', description: 'print the main repository root path' },
8
+ { name: 'status', description: 'summarize repository and worktree health' },
9
+ { name: 'sync', description: 'fetch and update one or all worktrees' },
10
+ { name: 'ls', description: 'list active worktrees' },
11
+ { name: 'clean', description: 'interactively prune linked worktrees' },
12
+ { name: 'remove', description: 'remove a linked worktree and delete its branch when present' },
13
+ { name: 'rm', description: 'alias of remove' },
14
+ { name: 'trigger-hook', description: 'run a named hook in the current worktree' },
15
+ { name: 'config', description: 'manage global config defaults' },
16
+ ];
17
+ const SHELL_NAMES = ['bash', 'fish', 'zsh'];
18
+ 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
+ ];
29
+ export function renderShellCompletion(shell) {
30
+ switch (shell) {
31
+ case 'bash':
32
+ return renderBashCompletion();
33
+ case 'fish':
34
+ return renderFishCompletion();
35
+ case 'zsh':
36
+ return renderZshCompletion();
37
+ }
38
+ }
39
+ function renderBashCompletion() {
40
+ const topLevelCommands = TOP_LEVEL_COMMANDS.map((command) => command.name).join(' ');
41
+ const shells = SHELL_NAMES.join(' ');
42
+ const hooks = HOOK_NAMES.join(' ');
43
+ const configKeys = CONFIG_KEYS.join(' ');
44
+ return `__gji_worktree_branches() {
45
+ command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
46
+ }
47
+
48
+ _gji_completion() {
49
+ local cur command_name
50
+ COMPREPLY=()
51
+ cur="\${COMP_WORDS[COMP_CWORD]:-}"
52
+
53
+ if [ "$COMP_CWORD" -eq 1 ]; then
54
+ COMPREPLY=( $(compgen -W "${topLevelCommands}" -- "$cur") )
55
+ return 0
56
+ fi
57
+
58
+ command_name="\${COMP_WORDS[1]}"
59
+
60
+ case "$command_name" in
61
+ new)
62
+ COMPREPLY=( $(compgen -W "--detached --dry-run --json --help" -- "$cur") )
63
+ ;;
64
+ init)
65
+ COMPREPLY=( $(compgen -W "${shells} --write --help" -- "$cur") )
66
+ ;;
67
+ completion)
68
+ COMPREPLY=( $(compgen -W "${shells} --help" -- "$cur") )
69
+ ;;
70
+ pr)
71
+ COMPREPLY=( $(compgen -W "--dry-run --json --help" -- "$cur") )
72
+ ;;
73
+ go)
74
+ COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) --print --help" -- "$cur") )
75
+ ;;
76
+ root)
77
+ COMPREPLY=( $(compgen -W "--print --help" -- "$cur") )
78
+ ;;
79
+ status)
80
+ COMPREPLY=( $(compgen -W "--json --help" -- "$cur") )
81
+ ;;
82
+ sync)
83
+ COMPREPLY=( $(compgen -W "--all --json --help" -- "$cur") )
84
+ ;;
85
+ ls)
86
+ COMPREPLY=( $(compgen -W "--compact --json --help" -- "$cur") )
87
+ ;;
88
+ clean)
89
+ COMPREPLY=( $(compgen -W "-f --force --stale --dry-run --json --help" -- "$cur") )
90
+ ;;
91
+ remove|rm)
92
+ COMPREPLY=( $(compgen -W "$(__gji_worktree_branches) -f --force --dry-run --json --help" -- "$cur") )
93
+ ;;
94
+ trigger-hook)
95
+ COMPREPLY=( $(compgen -W "${hooks} --help" -- "$cur") )
96
+ ;;
97
+ config)
98
+ if [ "$COMP_CWORD" -eq 2 ]; then
99
+ COMPREPLY=( $(compgen -W "get set unset" -- "$cur") )
100
+ return 0
101
+ fi
102
+
103
+ case "\${COMP_WORDS[2]}" in
104
+ get|unset)
105
+ if [ "$COMP_CWORD" -eq 3 ]; then
106
+ COMPREPLY=( $(compgen -W "${configKeys}" -- "$cur") )
107
+ fi
108
+ ;;
109
+ set)
110
+ if [ "$COMP_CWORD" -eq 3 ]; then
111
+ COMPREPLY=( $(compgen -W "${configKeys}" -- "$cur") )
112
+ fi
113
+ ;;
114
+ esac
115
+ ;;
116
+ esac
117
+ }
118
+
119
+ complete -F _gji_completion gji`;
120
+ }
121
+ function renderFishCompletion() {
122
+ const commandLines = TOP_LEVEL_COMMANDS.map((command) => `complete -c gji -n '__fish_use_subcommand' -a '${command.name}' -d '${escapeSingleQuotes(command.description)}'`).join('\n');
123
+ const shellLines = SHELL_NAMES.map((shell) => `complete -c gji -n '__fish_seen_subcommand_from init' -a '${shell}' -d 'shell'`).join('\n');
124
+ const hookLines = HOOK_NAMES.map((hook) => `complete -c gji -n '__fish_seen_subcommand_from trigger-hook' -a '${hook}' -d 'hook'`).join('\n');
125
+ const configKeyLines = CONFIG_KEYS.map((key) => `complete -c gji -n '__gji_should_complete_config_key' -a '${key}' -d 'config key'`).join('\n');
126
+ return `function __gji_worktree_branches
127
+ command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
128
+ end
129
+
130
+ function __gji_should_complete_config_action
131
+ set -l tokens (commandline -opc)
132
+ test (count $tokens) -eq 2
133
+ end
134
+
135
+ function __gji_should_complete_config_key
136
+ set -l tokens (commandline -opc)
137
+ if test (count $tokens) -ne 3
138
+ return 1
139
+ end
140
+
141
+ if test $tokens[2] != config
142
+ return 1
143
+ end
144
+
145
+ contains -- $tokens[3] get set unset
146
+ end
147
+
148
+ complete -c gji -f
149
+ ${commandLines}
150
+
151
+ complete -c gji -n '__fish_seen_subcommand_from new' -l detached -d 'create a detached worktree without a branch'
152
+ 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
+ complete -c gji -n '__fish_seen_subcommand_from new' -l json -d 'emit JSON on success or error instead of human-readable output'
154
+
155
+ complete -c gji -n '__fish_seen_subcommand_from init' -l write -d 'write the integration to the shell config file'
156
+ ${shellLines}
157
+
158
+ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'bash' -d 'shell'
159
+ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'fish' -d 'shell'
160
+ complete -c gji -n '__fish_seen_subcommand_from completion' -a 'zsh' -d 'shell'
161
+
162
+ 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
+ complete -c gji -n '__fish_seen_subcommand_from pr' -l json -d 'emit JSON on success or error instead of human-readable output'
164
+
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'
167
+
168
+ complete -c gji -n '__fish_seen_subcommand_from root' -l print -d 'print the resolved repository root path explicitly'
169
+
170
+ complete -c gji -n '__fish_seen_subcommand_from status' -l json -d 'print repository and worktree health as JSON'
171
+
172
+ complete -c gji -n '__fish_seen_subcommand_from sync' -l all -d 'sync every worktree in the repository'
173
+ complete -c gji -n '__fish_seen_subcommand_from sync' -l json -d 'emit JSON on success or error instead of human-readable output'
174
+
175
+ complete -c gji -n '__fish_seen_subcommand_from ls' -l compact -d 'show only branch and path columns'
176
+ complete -c gji -n '__fish_seen_subcommand_from ls' -l json -d 'print active worktrees as JSON'
177
+
178
+ complete -c gji -n '__fish_seen_subcommand_from clean' -s f -l force -d 'bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches'
179
+ complete -c gji -n '__fish_seen_subcommand_from clean' -l stale -d 'only target clean worktrees whose upstream is gone and branch is merged into the default branch'
180
+ complete -c gji -n '__fish_seen_subcommand_from clean' -l dry-run -d 'show what would be deleted without removing anything'
181
+ complete -c gji -n '__fish_seen_subcommand_from clean' -l json -d 'emit JSON on success or error instead of human-readable output'
182
+
183
+ complete -c gji -n '__fish_seen_subcommand_from remove rm' -s f -l force -d 'bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch'
184
+ complete -c gji -n '__fish_seen_subcommand_from remove rm' -l dry-run -d 'show what would be deleted without removing anything'
185
+ complete -c gji -n '__fish_seen_subcommand_from remove rm' -l json -d 'emit JSON on success or error instead of human-readable output'
186
+ complete -c gji -n '__fish_seen_subcommand_from remove rm' -a '(__gji_worktree_branches)' -d 'worktree branch'
187
+
188
+ ${hookLines}
189
+
190
+ complete -c gji -n '__fish_seen_subcommand_from config; and __gji_should_complete_config_action' -a 'get set unset' -d 'config action'
191
+ ${configKeyLines}`;
192
+ }
193
+ function renderZshCompletion() {
194
+ const commandLines = TOP_LEVEL_COMMANDS.map((command) => `'${command.name}:${escapeSingleQuotes(command.description)}'`).join('\n ');
195
+ const configKeys = CONFIG_KEYS.join(' ');
196
+ const shells = SHELL_NAMES.join(' ');
197
+ const hooks = HOOK_NAMES.join(' ');
198
+ return `__gji_worktree_branches() {
199
+ command gji ls --compact 2>/dev/null | awk 'NR > 1 { branch = ($1 == "*" ? $2 : $1); if (branch != "(detached)") print branch }'
200
+ }
201
+
202
+ _gji_completion() {
203
+ local context state line
204
+ local -a commands worktree_branches
205
+
206
+ commands=(
207
+ ${commandLines}
208
+ )
209
+
210
+ if (( CURRENT == 2 )); then
211
+ _describe 'command' commands
212
+ return
213
+ fi
214
+
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
257
+
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
268
+
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
+ }
279
+
280
+ compdef _gji_completion gji`;
281
+ }
282
+ function escapeSingleQuotes(value) {
283
+ return value.replace(/'/g, `'\\''`);
284
+ }
@@ -0,0 +1,2 @@
1
+ export type SupportedShell = 'bash' | 'fish' | 'zsh';
2
+ export declare function resolveSupportedShell(requestedShell: string | undefined, detectedShell: string | undefined): SupportedShell | null;
package/dist/shell.js ADDED
@@ -0,0 +1,21 @@
1
+ export function resolveSupportedShell(requestedShell, detectedShell) {
2
+ const requested = normalizeShell(requestedShell);
3
+ if (requested) {
4
+ return requested;
5
+ }
6
+ return normalizeShell(detectedShell);
7
+ }
8
+ function normalizeShell(value) {
9
+ if (!value) {
10
+ return null;
11
+ }
12
+ const candidate = value.split('/').at(-1)?.toLowerCase();
13
+ switch (candidate) {
14
+ case 'bash':
15
+ case 'fish':
16
+ case 'zsh':
17
+ return candidate;
18
+ default:
19
+ return null;
20
+ }
21
+ }