@solaqua/gji 0.3.0 → 0.4.1
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 +124 -11
- package/dist/clean.d.ts +1 -0
- package/dist/clean.js +103 -13
- package/dist/cli.js +62 -4
- package/dist/completion.d.ts +6 -0
- package/dist/completion.js +11 -0
- package/dist/config.js +1 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +38 -0
- package/dist/gji +5 -0
- package/dist/gji-bundle.mjs +16536 -0
- package/dist/index.js +25 -1
- package/dist/init.d.ts +12 -2
- package/dist/init.js +100 -37
- package/dist/ls.d.ts +3 -0
- package/dist/ls.js +46 -2
- package/dist/pr.js +43 -1
- package/dist/shell-completion.d.ts +1 -0
- package/dist/shell-completion.js +284 -0
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +21 -0
- package/dist/sync.js +2 -13
- package/dist/worktree-info.d.ts +33 -0
- package/dist/worktree-info.js +105 -0
- package/man/man1/gji-clean.1 +22 -0
- package/man/man1/gji-completion.1 +9 -0
- package/man/man1/gji-config.1 +19 -0
- package/man/man1/gji-go.1 +13 -0
- package/man/man1/gji-init.1 +13 -0
- package/man/man1/gji-ls.1 +16 -0
- package/man/man1/gji-new.1 +22 -0
- package/man/man1/gji-pr.1 +16 -0
- package/man/man1/gji-remove.1 +19 -0
- package/man/man1/gji-root.1 +13 -0
- package/man/man1/gji-status.1 +13 -0
- package/man/man1/gji-sync.1 +16 -0
- package/man/man1/gji-trigger-hook.1 +9 -0
- package/man/man1/gji.1 +71 -0
- package/package.json +17 -3
package/dist/index.js
CHANGED
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { loadGlobalConfig } from './config.js';
|
|
2
4
|
import { runCli } from './cli.js';
|
|
3
5
|
async function main() {
|
|
4
6
|
try {
|
|
5
|
-
const
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
// Warn once (until fixed) when shell integration hasn't been set up.
|
|
9
|
+
// Only shown in interactive terminals — suppressed in pipes and after gji init --write.
|
|
10
|
+
const isMetaArg = argv[0] === 'init'
|
|
11
|
+
|| argv[0] === '--version' || argv[0] === '-V'
|
|
12
|
+
|| argv[0] === '--help' || argv[0] === '-h';
|
|
13
|
+
if (process.stderr.isTTY === true && !isMetaArg) {
|
|
14
|
+
await warnIfMissingShellIntegration();
|
|
15
|
+
}
|
|
16
|
+
const result = await runCli(argv, {
|
|
6
17
|
stderr: (chunk) => process.stderr.write(chunk),
|
|
7
18
|
stdout: (chunk) => process.stdout.write(chunk),
|
|
8
19
|
});
|
|
@@ -14,4 +25,17 @@ async function main() {
|
|
|
14
25
|
process.exitCode = 1;
|
|
15
26
|
}
|
|
16
27
|
}
|
|
28
|
+
async function warnIfMissingShellIntegration() {
|
|
29
|
+
try {
|
|
30
|
+
const { config } = await loadGlobalConfig(homedir());
|
|
31
|
+
if (!config.shellIntegration) {
|
|
32
|
+
const shellBin = (process.env.SHELL ?? '').split('/').at(-1);
|
|
33
|
+
const shellArg = shellBin && ['bash', 'zsh', 'fish'].includes(shellBin) ? ` ${shellBin}` : '';
|
|
34
|
+
process.stderr.write(`gji: shell integration not set up — run \`gji init${shellArg} --write\` to enable automatic cd.\n`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// best-effort; never block the command
|
|
39
|
+
}
|
|
40
|
+
}
|
|
17
41
|
void main();
|
package/dist/init.d.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import { type SupportedShell } from './shell.js';
|
|
2
2
|
export type InstallSaveTarget = 'local' | 'global';
|
|
3
|
+
export interface SetupWizardResult {
|
|
4
|
+
branchPrefix?: string;
|
|
5
|
+
hooks?: {
|
|
6
|
+
afterCreate?: string;
|
|
7
|
+
afterEnter?: string;
|
|
8
|
+
beforeRemove?: string;
|
|
9
|
+
};
|
|
10
|
+
installSaveTarget: InstallSaveTarget;
|
|
11
|
+
worktreePath?: string;
|
|
12
|
+
}
|
|
3
13
|
export interface InitCommandOptions {
|
|
4
14
|
cwd: string;
|
|
5
15
|
home?: string;
|
|
6
|
-
|
|
16
|
+
promptForSetup?: () => Promise<SetupWizardResult | null>;
|
|
7
17
|
shell?: string;
|
|
8
18
|
stderr?: (chunk: string) => void;
|
|
9
19
|
stdout: (chunk: string) => void;
|
package/dist/init.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
-
import { isCancel, select } from '@clack/prompts';
|
|
5
|
-
import { loadGlobalConfig, updateGlobalConfigKey } from './config.js';
|
|
4
|
+
import { intro, isCancel, outro, select, text } from '@clack/prompts';
|
|
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 = [
|
|
@@ -43,7 +44,7 @@ const SHELL_WRAPPED_COMMANDS = [
|
|
|
43
44
|
},
|
|
44
45
|
];
|
|
45
46
|
export async function runInitCommand(options) {
|
|
46
|
-
const shell =
|
|
47
|
+
const shell = resolveSupportedShell(options.shell, process.env.SHELL);
|
|
47
48
|
const home = options.home ?? homedir();
|
|
48
49
|
if (!shell) {
|
|
49
50
|
options.stderr?.('Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n');
|
|
@@ -60,19 +61,21 @@ export async function runInitCommand(options) {
|
|
|
60
61
|
const next = upsertShellIntegration(current, script);
|
|
61
62
|
await writeFile(rcPath, next, 'utf8');
|
|
62
63
|
options.stdout(`${rcPath}\n`);
|
|
63
|
-
//
|
|
64
|
-
// Skip if already configured. When using the default interactive prompt, also
|
|
65
|
-
// require a real TTY so we don't block in piped/headless environments.
|
|
64
|
+
// Run the setup wizard on the first-ever init (not on subsequent re-runs).
|
|
66
65
|
const { config: globalConfig } = await loadGlobalConfig(home);
|
|
67
|
-
const
|
|
66
|
+
const alreadyConfigured = 'shellIntegration' in globalConfig || 'installSaveTarget' in globalConfig;
|
|
67
|
+
const hasCustomPrompt = options.promptForSetup !== undefined;
|
|
68
68
|
const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
|
|
69
|
-
if (!
|
|
70
|
-
const prompt = options.
|
|
71
|
-
const
|
|
72
|
-
if (
|
|
73
|
-
await updateGlobalConfigKey('installSaveTarget',
|
|
69
|
+
if (!alreadyConfigured && canPrompt) {
|
|
70
|
+
const prompt = options.promptForSetup ?? defaultPromptForSetup;
|
|
71
|
+
const result = await prompt();
|
|
72
|
+
if (result) {
|
|
73
|
+
await updateGlobalConfigKey('installSaveTarget', result.installSaveTarget, home);
|
|
74
|
+
await saveWizardConfig(result, options.cwd, home);
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
// Mark shell integration as installed so the first-run nudge is suppressed.
|
|
78
|
+
await updateGlobalConfigKey('shellIntegration', true, home);
|
|
76
79
|
return 0;
|
|
77
80
|
}
|
|
78
81
|
export function renderShellIntegration(shell) {
|
|
@@ -111,25 +114,30 @@ export function upsertShellIntegration(existingConfig, script) {
|
|
|
111
114
|
}
|
|
112
115
|
return ensureTrailingNewline(`${prefix}\n\n${trimmedScript}`);
|
|
113
116
|
}
|
|
114
|
-
function
|
|
115
|
-
const
|
|
116
|
-
if (
|
|
117
|
-
|
|
117
|
+
async function saveWizardConfig(result, cwd, home) {
|
|
118
|
+
const values = {};
|
|
119
|
+
if (result.branchPrefix)
|
|
120
|
+
values.branchPrefix = result.branchPrefix;
|
|
121
|
+
if (result.worktreePath)
|
|
122
|
+
values.worktreePath = result.worktreePath;
|
|
123
|
+
const hooks = {};
|
|
124
|
+
if (result.hooks?.afterCreate)
|
|
125
|
+
hooks.afterCreate = result.hooks.afterCreate;
|
|
126
|
+
if (result.hooks?.afterEnter)
|
|
127
|
+
hooks.afterEnter = result.hooks.afterEnter;
|
|
128
|
+
if (result.hooks?.beforeRemove)
|
|
129
|
+
hooks.beforeRemove = result.hooks.beforeRemove;
|
|
130
|
+
if (Object.keys(hooks).length > 0)
|
|
131
|
+
values.hooks = hooks;
|
|
132
|
+
if (Object.keys(values).length === 0)
|
|
133
|
+
return;
|
|
134
|
+
if (result.installSaveTarget === 'local') {
|
|
135
|
+
const loaded = await loadConfig(cwd);
|
|
136
|
+
await saveLocalConfig(cwd, { ...loaded.config, ...values });
|
|
118
137
|
}
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!value) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
const candidate = value.split('/').at(-1)?.toLowerCase();
|
|
126
|
-
switch (candidate) {
|
|
127
|
-
case 'bash':
|
|
128
|
-
case 'fish':
|
|
129
|
-
case 'zsh':
|
|
130
|
-
return candidate;
|
|
131
|
-
default:
|
|
132
|
-
return null;
|
|
138
|
+
else {
|
|
139
|
+
const { config: existing } = await loadGlobalConfig(home);
|
|
140
|
+
await saveGlobalConfig({ ...existing, ...values }, home);
|
|
133
141
|
}
|
|
134
142
|
}
|
|
135
143
|
function resolveShellConfigPath(shell, home) {
|
|
@@ -211,16 +219,71 @@ function indentBlock(value, spaces) {
|
|
|
211
219
|
.map((line) => line.length === 0 ? '' : `${prefix}${line}`)
|
|
212
220
|
.join('\n');
|
|
213
221
|
}
|
|
214
|
-
async function
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
async function defaultPromptForSetup() {
|
|
223
|
+
intro('gji setup');
|
|
224
|
+
const installSaveTarget = await select({
|
|
225
|
+
message: 'Where should preferences be saved?',
|
|
217
226
|
options: [
|
|
218
|
-
{ value: '
|
|
219
|
-
{ value: '
|
|
227
|
+
{ value: 'global', label: '~/.config/gji/config.json', hint: 'personal — never committed' },
|
|
228
|
+
{ value: 'local', label: '.gji.json', hint: 'repo — committed with the project' },
|
|
220
229
|
],
|
|
221
230
|
});
|
|
222
|
-
if (isCancel(
|
|
231
|
+
if (isCancel(installSaveTarget)) {
|
|
232
|
+
outro('Setup skipped.');
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const branchPrefix = await text({
|
|
236
|
+
message: 'Default branch prefix?',
|
|
237
|
+
placeholder: 'e.g. feat/ or fix/ — leave blank to skip',
|
|
238
|
+
});
|
|
239
|
+
if (isCancel(branchPrefix)) {
|
|
240
|
+
outro('Setup skipped.');
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
const worktreePath = await text({
|
|
244
|
+
message: 'Worktree base path?',
|
|
245
|
+
placeholder: 'leave blank to use the default path',
|
|
246
|
+
});
|
|
247
|
+
if (isCancel(worktreePath)) {
|
|
248
|
+
outro('Setup skipped.');
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
const afterCreate = await text({
|
|
252
|
+
message: 'afterCreate hook — run after creating a worktree?',
|
|
253
|
+
placeholder: 'e.g. pnpm install — leave blank to skip',
|
|
254
|
+
});
|
|
255
|
+
if (isCancel(afterCreate)) {
|
|
256
|
+
outro('Setup skipped.');
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
const afterEnter = await text({
|
|
260
|
+
message: 'afterEnter hook — run after entering a worktree?',
|
|
261
|
+
placeholder: 'e.g. nvm use — leave blank to skip',
|
|
262
|
+
});
|
|
263
|
+
if (isCancel(afterEnter)) {
|
|
264
|
+
outro('Setup skipped.');
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const beforeRemove = await text({
|
|
268
|
+
message: 'beforeRemove hook — run before removing a worktree?',
|
|
269
|
+
placeholder: 'leave blank to skip',
|
|
270
|
+
});
|
|
271
|
+
if (isCancel(beforeRemove)) {
|
|
272
|
+
outro('Setup skipped.');
|
|
223
273
|
return null;
|
|
224
274
|
}
|
|
225
|
-
|
|
275
|
+
outro('Setup complete!');
|
|
276
|
+
const hooks = {};
|
|
277
|
+
if (afterCreate)
|
|
278
|
+
hooks.afterCreate = afterCreate;
|
|
279
|
+
if (afterEnter)
|
|
280
|
+
hooks.afterEnter = afterEnter;
|
|
281
|
+
if (beforeRemove)
|
|
282
|
+
hooks.beforeRemove = beforeRemove;
|
|
283
|
+
return {
|
|
284
|
+
branchPrefix: branchPrefix || undefined,
|
|
285
|
+
hooks: Object.keys(hooks).length > 0 ? hooks : undefined,
|
|
286
|
+
installSaveTarget,
|
|
287
|
+
worktreePath: worktreePath || undefined,
|
|
288
|
+
};
|
|
226
289
|
}
|
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(
|
|
16
|
+
options.stdout(`${JSON.stringify(infos, null, 2)}\n`);
|
|
7
17
|
return 0;
|
|
8
18
|
}
|
|
9
|
-
options.stdout(`${
|
|
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/pr.js
CHANGED
|
@@ -74,7 +74,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
74
74
|
return 0;
|
|
75
75
|
}
|
|
76
76
|
try {
|
|
77
|
-
await
|
|
77
|
+
await fetchPullRequestRef(repository.repoRoot, options.number, prNumber, remoteRef);
|
|
78
78
|
}
|
|
79
79
|
catch {
|
|
80
80
|
const message = `Failed to fetch PR #${prNumber} from origin`;
|
|
@@ -127,6 +127,48 @@ async function localBranchExists(repoRoot, branchName) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
export const runPrCommand = createPrCommand();
|
|
130
|
+
async function fetchPullRequestRef(repoRoot, input, prNumber, remoteRef) {
|
|
131
|
+
for (const sourceRef of listPullRequestSourceRefs(input, prNumber)) {
|
|
132
|
+
try {
|
|
133
|
+
await execFileAsync('git', ['fetch', 'origin', `${sourceRef}:${remoteRef}`], { cwd: repoRoot });
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// Try the next forge-specific ref namespace before failing the command.
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
throw new Error(`No pull request ref found for #${prNumber}`);
|
|
141
|
+
}
|
|
142
|
+
function listPullRequestSourceRefs(input, prNumber) {
|
|
143
|
+
const allForges = ['github', 'gitlab', 'bitbucket'];
|
|
144
|
+
const preferredForge = detectPullRequestForge(input);
|
|
145
|
+
const orderedForges = preferredForge === 'unknown'
|
|
146
|
+
? allForges
|
|
147
|
+
: [preferredForge, ...allForges.filter((forge) => forge !== preferredForge)];
|
|
148
|
+
return orderedForges.map((forge) => sourceRefForForge(forge, prNumber));
|
|
149
|
+
}
|
|
150
|
+
function detectPullRequestForge(input) {
|
|
151
|
+
if (/\/pull-requests\/\d+/.test(input)) {
|
|
152
|
+
return 'bitbucket';
|
|
153
|
+
}
|
|
154
|
+
if (/\/merge_requests\/\d+/.test(input)) {
|
|
155
|
+
return 'gitlab';
|
|
156
|
+
}
|
|
157
|
+
if (/\/pull\/\d+/.test(input)) {
|
|
158
|
+
return 'github';
|
|
159
|
+
}
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
function sourceRefForForge(forge, prNumber) {
|
|
163
|
+
switch (forge) {
|
|
164
|
+
case 'bitbucket':
|
|
165
|
+
return `refs/pull-requests/${prNumber}/from`;
|
|
166
|
+
case 'github':
|
|
167
|
+
return `refs/pull/${prNumber}/head`;
|
|
168
|
+
case 'gitlab':
|
|
169
|
+
return `refs/merge-requests/${prNumber}/head`;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
130
172
|
async function writeOutput(worktreePath, stdout) {
|
|
131
173
|
await writeShellOutput(PR_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
132
174
|
}
|
|
@@ -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
|
+
}
|
package/dist/shell.d.ts
ADDED