@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 CHANGED
@@ -16,31 +16,28 @@ Standard branch switching gets annoying when you are:
16
16
 
17
17
  ## Install
18
18
 
19
- Install from npm:
19
+ Current source install:
20
20
 
21
21
  ```sh
22
- npm install -g @solaqua/gji
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
- gji go feature/refactor-auth
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; when omitted, it prompts with a placeholder branch name
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` prints the main repository root path from either the repo root or a linked worktree
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]` removes a linked worktree and deletes its branch when present
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
- if (process.env[GO_OUTPUT_FILE_ENV]) {
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
- if test (count $argv) -gt 0; and test $argv[1] = go
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
- if [ "$1" = "go" ]; then
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(`${worktreePath}\n`);
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(`${worktreePath}\n`);
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
- options.stdout(`${repository.repoRoot}\n`);
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 writeFile(process.env[ROOT_OUTPUT_FILE_ENV], output, 'utf8');
7
+ await writeShellOutput(ROOT_OUTPUT_FILE_ENV, repository.repoRoot, options.stdout);
9
8
  return 0;
10
9
  }
11
- options.stdout(output);
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>;
@@ -0,0 +1,9 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ export async function writeShellOutput(envVar, value, stdout) {
3
+ const output = `${value}\n`;
4
+ if (process.env[envVar]) {
5
+ await writeFile(process.env[envVar], output, 'utf8');
6
+ return;
7
+ }
8
+ stdout(output);
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@solaqua/gji",
3
- "version": "0.1.0-beta.6",
3
+ "version": "0.1.0-beta.7",
4
4
  "description": "Git worktree CLI for fast context switching.",
5
5
  "license": "MIT",
6
6
  "author": "sjquant",