@solaqua/gji 0.1.0-beta.6 → 0.1.0-beta.7
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 +35 -15
- package/dist/go.js +2 -6
- package/dist/init.js +82 -74
- package/dist/new.js +7 -2
- package/dist/remove.js +6 -1
- package/dist/root.js +3 -4
- package/dist/shell-handoff.d.ts +1 -0
- package/dist/shell-handoff.js +9 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,31 +16,28 @@ Standard branch switching gets annoying when you are:
|
|
|
16
16
|
|
|
17
17
|
## Install
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Current source install:
|
|
20
20
|
|
|
21
21
|
```sh
|
|
22
|
-
|
|
22
|
+
git clone https://github.com/sjquant/gji.git
|
|
23
|
+
cd gji
|
|
24
|
+
pnpm build
|
|
25
|
+
npm install -g .
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
Confirm the CLI is available:
|
|
26
29
|
|
|
27
30
|
```sh
|
|
31
|
+
gji --version
|
|
28
32
|
gji --help
|
|
29
33
|
```
|
|
30
34
|
|
|
31
|
-
The installed command is still:
|
|
32
|
-
|
|
33
|
-
```sh
|
|
34
|
-
gji
|
|
35
|
-
```
|
|
36
|
-
|
|
37
35
|
## Quick start
|
|
38
36
|
|
|
39
37
|
Inside a Git repository:
|
|
40
38
|
|
|
41
39
|
```sh
|
|
42
40
|
gji new feature/login-form
|
|
43
|
-
gji go feature/login-form
|
|
44
41
|
gji status
|
|
45
42
|
```
|
|
46
43
|
|
|
@@ -52,7 +49,7 @@ That creates a linked worktree at a deterministic path:
|
|
|
52
49
|
|
|
53
50
|
## Shell setup
|
|
54
51
|
|
|
55
|
-
`gji go` can only change your current directory when shell integration is installed. Without shell integration, the raw CLI prints the target path so it stays script-friendly.
|
|
52
|
+
`gji new`, `gji go`, `gji root`, and `gji remove`/`gji rm` can only change your current directory when shell integration is installed. Without shell integration, the raw CLI prints the target path so it stays script-friendly.
|
|
56
53
|
|
|
57
54
|
For zsh:
|
|
58
55
|
|
|
@@ -64,24 +61,42 @@ source ~/.zshrc
|
|
|
64
61
|
After that:
|
|
65
62
|
|
|
66
63
|
```sh
|
|
64
|
+
gji new feature/login-form
|
|
67
65
|
gji go feature/login-form
|
|
66
|
+
gji root
|
|
67
|
+
gji rm feature/login-form
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
changes your shell directory directly.
|
|
71
71
|
|
|
72
|
+
If you reinstall or upgrade `gji`, refresh the shell function:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
eval "$(gji init zsh)"
|
|
76
|
+
```
|
|
77
|
+
|
|
72
78
|
For scripts or explicit piping:
|
|
73
79
|
|
|
74
80
|
```sh
|
|
81
|
+
gji new feature/login-form
|
|
75
82
|
gji go --print feature/login-form
|
|
83
|
+
gji root --print
|
|
76
84
|
```
|
|
77
85
|
|
|
86
|
+
`gji new` and `gji remove` print their destination paths in raw CLI mode, but in a shell-integrated session they change directory directly.
|
|
87
|
+
|
|
78
88
|
## Daily workflow
|
|
79
89
|
|
|
80
90
|
Start a task:
|
|
81
91
|
|
|
82
92
|
```sh
|
|
83
93
|
gji new feature/refactor-auth
|
|
84
|
-
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Start a detached scratch worktree:
|
|
97
|
+
|
|
98
|
+
```sh
|
|
99
|
+
gji new --detached
|
|
85
100
|
```
|
|
86
101
|
|
|
87
102
|
Check what is active:
|
|
@@ -119,20 +134,25 @@ Finish a single worktree explicitly:
|
|
|
119
134
|
|
|
120
135
|
```sh
|
|
121
136
|
gji remove feature/refactor-auth
|
|
137
|
+
# or
|
|
138
|
+
gji rm feature/refactor-auth
|
|
122
139
|
```
|
|
123
140
|
|
|
141
|
+
After removal, the shell-integrated command returns you to the repository root.
|
|
142
|
+
|
|
124
143
|
## Commands
|
|
125
144
|
|
|
145
|
+
- `gji --version` prints the installed CLI version
|
|
126
146
|
- `gji init [shell]` prints shell integration for `zsh`, `bash`, or `fish`
|
|
127
|
-
- `gji new [branch]` creates a branch and linked worktree;
|
|
147
|
+
- `gji new [branch] [--detached]` creates a branch and linked worktree; with shell integration it moves into the new worktree, and `--detached` creates a detached worktree instead
|
|
128
148
|
- `gji pr <number>` fetches `origin/pull/<number>/head` and creates a linked `pr/<number>` worktree
|
|
129
|
-
- `gji go [branch]` jumps to an existing worktree when shell integration is installed, or prints the matching worktree path otherwise
|
|
130
|
-
- `gji root`
|
|
149
|
+
- `gji go [branch] [--print]` jumps to an existing worktree when shell integration is installed, or prints the matching worktree path otherwise
|
|
150
|
+
- `gji root [--print]` jumps to the main repository root when shell integration is installed, or prints it otherwise
|
|
131
151
|
- `gji status [--json]` prints repository metadata, worktree health, and upstream divergence
|
|
132
152
|
- `gji sync [--all]` fetches from the configured remote and rebases or fast-forwards worktrees onto the configured default branch
|
|
133
153
|
- `gji ls [--json]` lists active worktrees in a table or JSON
|
|
134
154
|
- `gji clean` interactively prunes one or more linked worktrees, including detached entries, while excluding the current worktree
|
|
135
|
-
- `gji remove [branch]`
|
|
155
|
+
- `gji remove [branch]` and `gji rm [branch]` remove a linked worktree and delete its branch when present; with shell integration they return to the repository root
|
|
136
156
|
- `gji config` reads or updates global defaults
|
|
137
157
|
|
|
138
158
|
## Configuration
|
package/dist/go.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
2
1
|
import { isCancel, select } from '@clack/prompts';
|
|
3
2
|
import { listWorktrees } from './repo.js';
|
|
3
|
+
import { writeShellOutput } from './shell-handoff.js';
|
|
4
4
|
const GO_OUTPUT_FILE_ENV = 'GJI_GO_OUTPUT_FILE';
|
|
5
5
|
export function createGoCommand(dependencies = {}) {
|
|
6
6
|
const prompt = dependencies.promptForWorktree ?? promptForWorktree;
|
|
@@ -20,11 +20,7 @@ export function createGoCommand(dependencies = {}) {
|
|
|
20
20
|
options.stderr('Aborted\n');
|
|
21
21
|
return 1;
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
await writeFile(process.env[GO_OUTPUT_FILE_ENV], `${chosenPath}\n`, 'utf8');
|
|
25
|
-
return 0;
|
|
26
|
-
}
|
|
27
|
-
options.stdout(`${chosenPath}\n`);
|
|
23
|
+
await writeShellOutput(GO_OUTPUT_FILE_ENV, chosenPath, options.stdout);
|
|
28
24
|
return 0;
|
|
29
25
|
};
|
|
30
26
|
}
|
package/dist/init.js
CHANGED
|
@@ -3,6 +3,36 @@ import { homedir } from 'node:os';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
const START_MARKER = '# >>> gji init >>>';
|
|
5
5
|
const END_MARKER = '# <<< gji init <<<';
|
|
6
|
+
const SHELL_WRAPPED_COMMANDS = [
|
|
7
|
+
{
|
|
8
|
+
bypassOption: '--help',
|
|
9
|
+
commandName: 'new',
|
|
10
|
+
envVar: 'GJI_NEW_OUTPUT_FILE',
|
|
11
|
+
names: ['new'],
|
|
12
|
+
tempPrefix: 'gji-new',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
bypassOption: '--print',
|
|
16
|
+
commandName: 'go',
|
|
17
|
+
envVar: 'GJI_GO_OUTPUT_FILE',
|
|
18
|
+
names: ['go'],
|
|
19
|
+
tempPrefix: 'gji-go',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
bypassOption: '--print',
|
|
23
|
+
commandName: 'root',
|
|
24
|
+
envVar: 'GJI_ROOT_OUTPUT_FILE',
|
|
25
|
+
names: ['root'],
|
|
26
|
+
tempPrefix: 'gji-root',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
bypassOption: '--help',
|
|
30
|
+
commandName: 'remove',
|
|
31
|
+
envVar: 'GJI_REMOVE_OUTPUT_FILE',
|
|
32
|
+
names: ['remove', 'rm'],
|
|
33
|
+
tempPrefix: 'gji-remove',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
6
36
|
export async function runInitCommand(options) {
|
|
7
37
|
const shell = resolveShell(options.shell, process.env.SHELL);
|
|
8
38
|
if (!shell) {
|
|
@@ -23,51 +53,12 @@ export async function runInitCommand(options) {
|
|
|
23
53
|
return 0;
|
|
24
54
|
}
|
|
25
55
|
export function renderShellIntegration(shell) {
|
|
56
|
+
const commandBlocks = SHELL_WRAPPED_COMMANDS.map((command) => shell === 'fish' ? renderFishWrapper(command) : renderPosixWrapper(command)).join('\n\n');
|
|
26
57
|
switch (shell) {
|
|
27
58
|
case 'fish':
|
|
28
59
|
return `${START_MARKER}
|
|
29
60
|
function gji --wraps gji --description 'gji shell integration'
|
|
30
|
-
|
|
31
|
-
set -e argv[1]
|
|
32
|
-
if test (count $argv) -gt 0; and test $argv[1] = --print
|
|
33
|
-
command gji go $argv
|
|
34
|
-
return $status
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
set -l output_file (mktemp -t gji-go.XXXXXX)
|
|
38
|
-
or return 1
|
|
39
|
-
env GJI_GO_OUTPUT_FILE=$output_file command gji go $argv
|
|
40
|
-
or begin
|
|
41
|
-
set -l status_code $status
|
|
42
|
-
rm -f $output_file
|
|
43
|
-
return $status_code
|
|
44
|
-
end
|
|
45
|
-
set -l target (cat $output_file)
|
|
46
|
-
rm -f $output_file
|
|
47
|
-
cd $target
|
|
48
|
-
return $status
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
if test (count $argv) -gt 0; and test $argv[1] = root
|
|
52
|
-
set -e argv[1]
|
|
53
|
-
if test (count $argv) -gt 0; and test $argv[1] = --print
|
|
54
|
-
command gji root $argv
|
|
55
|
-
return $status
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
set -l output_file (mktemp -t gji-root.XXXXXX)
|
|
59
|
-
or return 1
|
|
60
|
-
env GJI_ROOT_OUTPUT_FILE=$output_file command gji root $argv
|
|
61
|
-
or begin
|
|
62
|
-
set -l status_code $status
|
|
63
|
-
rm -f $output_file
|
|
64
|
-
return $status_code
|
|
65
|
-
end
|
|
66
|
-
set -l target (cat $output_file)
|
|
67
|
-
rm -f $output_file
|
|
68
|
-
cd $target
|
|
69
|
-
return $status
|
|
70
|
-
end
|
|
61
|
+
${indentBlock(commandBlocks, 4)}
|
|
71
62
|
|
|
72
63
|
command gji $argv
|
|
73
64
|
end
|
|
@@ -77,39 +68,7 @@ ${END_MARKER}
|
|
|
77
68
|
case 'zsh':
|
|
78
69
|
return `${START_MARKER}
|
|
79
70
|
gji() {
|
|
80
|
-
|
|
81
|
-
shift
|
|
82
|
-
if [ "\${1:-}" = "--print" ]; then
|
|
83
|
-
command gji go "$@"
|
|
84
|
-
return $?
|
|
85
|
-
fi
|
|
86
|
-
|
|
87
|
-
local target
|
|
88
|
-
local output_file
|
|
89
|
-
output_file="$(mktemp -t gji-go.XXXXXX)" || return 1
|
|
90
|
-
GJI_GO_OUTPUT_FILE="$output_file" command gji go "$@" || { local status=$?; rm -f "$output_file"; return $status; }
|
|
91
|
-
target="$(cat "$output_file")"
|
|
92
|
-
rm -f "$output_file"
|
|
93
|
-
cd "$target" || return $?
|
|
94
|
-
return 0
|
|
95
|
-
fi
|
|
96
|
-
|
|
97
|
-
if [ "$1" = "root" ]; then
|
|
98
|
-
shift
|
|
99
|
-
if [ "\${1:-}" = "--print" ]; then
|
|
100
|
-
command gji root "$@"
|
|
101
|
-
return $?
|
|
102
|
-
fi
|
|
103
|
-
|
|
104
|
-
local target
|
|
105
|
-
local output_file
|
|
106
|
-
output_file="$(mktemp -t gji-root.XXXXXX)" || return 1
|
|
107
|
-
GJI_ROOT_OUTPUT_FILE="$output_file" command gji root "$@" || { local status=$?; rm -f "$output_file"; return $status; }
|
|
108
|
-
target="$(cat "$output_file")"
|
|
109
|
-
rm -f "$output_file"
|
|
110
|
-
cd "$target" || return $?
|
|
111
|
-
return 0
|
|
112
|
-
fi
|
|
71
|
+
${indentBlock(commandBlocks, 2)}
|
|
113
72
|
|
|
114
73
|
command gji "$@"
|
|
115
74
|
}
|
|
@@ -180,3 +139,52 @@ function escapeForRegExp(value) {
|
|
|
180
139
|
function isMissingFileError(error) {
|
|
181
140
|
return error instanceof Error && 'code' in error && error.code === 'ENOENT';
|
|
182
141
|
}
|
|
142
|
+
function renderFishWrapper(command) {
|
|
143
|
+
const tests = command.names.map((name) => `test $argv[1] = ${name}`).join('; or ');
|
|
144
|
+
return `if test (count $argv) -gt 0; and ${tests}
|
|
145
|
+
set -e argv[1]
|
|
146
|
+
if test (count $argv) -gt 0; and test $argv[1] = ${command.bypassOption}
|
|
147
|
+
command gji ${command.commandName} $argv
|
|
148
|
+
return $status
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
set -l output_file (mktemp -t ${command.tempPrefix}.XXXXXX)
|
|
152
|
+
or return 1
|
|
153
|
+
env ${command.envVar}=$output_file command gji ${command.commandName} $argv
|
|
154
|
+
or begin
|
|
155
|
+
set -l status_code $status
|
|
156
|
+
rm -f $output_file
|
|
157
|
+
return $status_code
|
|
158
|
+
end
|
|
159
|
+
set -l target (cat $output_file)
|
|
160
|
+
rm -f $output_file
|
|
161
|
+
cd $target
|
|
162
|
+
return $status
|
|
163
|
+
end`;
|
|
164
|
+
}
|
|
165
|
+
function renderPosixWrapper(command) {
|
|
166
|
+
const tests = command.names.map((name) => `[ "$1" = "${name}" ]`).join(' || ');
|
|
167
|
+
return `if ${tests}; then
|
|
168
|
+
shift
|
|
169
|
+
if [ "\${1:-}" = "${command.bypassOption}" ]; then
|
|
170
|
+
command gji ${command.commandName} "$@"
|
|
171
|
+
return $?
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
local target
|
|
175
|
+
local output_file
|
|
176
|
+
output_file="$(mktemp -t ${command.tempPrefix}.XXXXXX)" || return 1
|
|
177
|
+
${command.envVar}="$output_file" command gji ${command.commandName} "$@" || { local status=$?; rm -f "$output_file"; return $status; }
|
|
178
|
+
target="$(cat "$output_file")"
|
|
179
|
+
rm -f "$output_file"
|
|
180
|
+
cd "$target" || return $?
|
|
181
|
+
return 0
|
|
182
|
+
fi`;
|
|
183
|
+
}
|
|
184
|
+
function indentBlock(value, spaces) {
|
|
185
|
+
const prefix = ' '.repeat(spaces);
|
|
186
|
+
return value
|
|
187
|
+
.split('\n')
|
|
188
|
+
.map((line) => line.length === 0 ? '' : `${prefix}${line}`)
|
|
189
|
+
.join('\n');
|
|
190
|
+
}
|
package/dist/new.js
CHANGED
|
@@ -6,7 +6,9 @@ import { promisify } from 'node:util';
|
|
|
6
6
|
import { isCancel, select, text } from '@clack/prompts';
|
|
7
7
|
import { loadEffectiveConfig } from './config.js';
|
|
8
8
|
import { detectRepository, resolveWorktreePath } from './repo.js';
|
|
9
|
+
import { writeShellOutput } from './shell-handoff.js';
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
11
|
+
const NEW_OUTPUT_FILE_ENV = 'GJI_NEW_OUTPUT_FILE';
|
|
10
12
|
export function createNewCommand(dependencies = {}) {
|
|
11
13
|
const createBranchPlaceholder = dependencies.createBranchPlaceholder ?? generateBranchPlaceholder;
|
|
12
14
|
const promptForBranch = dependencies.promptForBranch ?? defaultPromptForBranch;
|
|
@@ -31,7 +33,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
31
33
|
if (!usesGeneratedDetachedName && await pathExists(worktreePath)) {
|
|
32
34
|
const choice = await prompt(worktreePath);
|
|
33
35
|
if (choice === 'reuse') {
|
|
34
|
-
options.stdout
|
|
36
|
+
await writeOutput(worktreePath, options.stdout);
|
|
35
37
|
return 0;
|
|
36
38
|
}
|
|
37
39
|
options.stderr(`Aborted because target worktree path already exists: ${worktreePath}\n`);
|
|
@@ -42,7 +44,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
42
44
|
? ['worktree', 'add', '--detach', worktreePath]
|
|
43
45
|
: ['worktree', 'add', '-b', worktreeName, worktreePath];
|
|
44
46
|
await execFileAsync('git', gitArgs, { cwd: repository.repoRoot });
|
|
45
|
-
options.stdout
|
|
47
|
+
await writeOutput(worktreePath, options.stdout);
|
|
46
48
|
return 0;
|
|
47
49
|
};
|
|
48
50
|
}
|
|
@@ -147,3 +149,6 @@ function pickRandom(values, random) {
|
|
|
147
149
|
const index = Math.floor(random() * values.length);
|
|
148
150
|
return values[Math.min(index, values.length - 1)];
|
|
149
151
|
}
|
|
152
|
+
async function writeOutput(worktreePath, stdout) {
|
|
153
|
+
await writeShellOutput(NEW_OUTPUT_FILE_ENV, worktreePath, stdout);
|
|
154
|
+
}
|
package/dist/remove.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { confirm, isCancel, select } from '@clack/prompts';
|
|
2
2
|
import { deleteBranch, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
|
|
3
|
+
import { writeShellOutput } from './shell-handoff.js';
|
|
4
|
+
const REMOVE_OUTPUT_FILE_ENV = 'GJI_REMOVE_OUTPUT_FILE';
|
|
3
5
|
export function createRemoveCommand(dependencies = {}) {
|
|
4
6
|
const promptForWorktree = dependencies.promptForWorktree ?? defaultPromptForWorktree;
|
|
5
7
|
const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval;
|
|
@@ -27,7 +29,7 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
27
29
|
if (worktree.branch) {
|
|
28
30
|
await deleteBranch(repository.repoRoot, worktree.branch);
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
await writeOutput(repository.repoRoot, options.stdout);
|
|
31
33
|
return 0;
|
|
32
34
|
};
|
|
33
35
|
}
|
|
@@ -54,3 +56,6 @@ async function defaultConfirmRemoval(worktree) {
|
|
|
54
56
|
});
|
|
55
57
|
return !isCancel(choice) && choice;
|
|
56
58
|
}
|
|
59
|
+
async function writeOutput(repoRoot, stdout) {
|
|
60
|
+
await writeShellOutput(REMOVE_OUTPUT_FILE_ENV, repoRoot, stdout);
|
|
61
|
+
}
|
package/dist/root.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import { writeFile } from 'node:fs/promises';
|
|
2
1
|
import { detectRepository } from './repo.js';
|
|
2
|
+
import { writeShellOutput } from './shell-handoff.js';
|
|
3
3
|
const ROOT_OUTPUT_FILE_ENV = 'GJI_ROOT_OUTPUT_FILE';
|
|
4
4
|
export async function runRootCommand(options) {
|
|
5
5
|
const repository = await detectRepository(options.cwd);
|
|
6
|
-
const output = `${repository.repoRoot}\n`;
|
|
7
6
|
if (!options.print && process.env[ROOT_OUTPUT_FILE_ENV]) {
|
|
8
|
-
await
|
|
7
|
+
await writeShellOutput(ROOT_OUTPUT_FILE_ENV, repository.repoRoot, options.stdout);
|
|
9
8
|
return 0;
|
|
10
9
|
}
|
|
11
|
-
options.stdout(
|
|
10
|
+
options.stdout(`${repository.repoRoot}\n`);
|
|
12
11
|
return 0;
|
|
13
12
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeShellOutput(envVar: string, value: string, stdout: (chunk: string) => void): Promise<void>;
|