@solaqua/gji 0.1.0 → 0.2.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 +163 -164
- package/dist/clean.d.ts +2 -0
- package/dist/clean.js +52 -6
- package/dist/cli.js +19 -5
- package/dist/config.d.ts +2 -0
- package/dist/config.js +14 -0
- package/dist/file-sync.d.ts +9 -0
- package/dist/file-sync.js +52 -0
- package/dist/go.js +12 -3
- package/dist/headless.d.ts +6 -0
- package/dist/headless.js +8 -0
- package/dist/install-prompt.d.ts +10 -0
- package/dist/install-prompt.js +99 -0
- package/dist/new.d.ts +4 -1
- package/dist/new.js +58 -2
- package/dist/package-manager.d.ts +5 -0
- package/dist/package-manager.js +108 -0
- package/dist/pr.d.ts +4 -1
- package/dist/pr.js +58 -4
- package/dist/remove.d.ts +2 -0
- package/dist/remove.js +49 -5
- package/dist/sync.d.ts +1 -0
- package/dist/sync.js +45 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,277 +1,276 @@
|
|
|
1
|
-
# gji
|
|
1
|
+
# gji — Git worktrees without the hassle
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> Jump between tasks instantly. No stash. No reinstall. No mess.
|
|
4
4
|
|
|
5
|
-
`gji`
|
|
5
|
+
`gji` wraps Git worktrees into a fast, ergonomic CLI. Each branch gets its own directory, its own `node_modules`, and its own terminal — so switching context is a single command instead of a ritual.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```sh
|
|
8
|
+
gji new feature/payment-refactor # new branch + worktree, cd in
|
|
9
|
+
gji pr 1234 # review PR in isolation, cd in
|
|
10
|
+
gji go main # jump back, shell changes directory
|
|
11
|
+
gji remove feature/payment-refactor
|
|
12
|
+
```
|
|
8
13
|
|
|
9
|
-
|
|
14
|
+
---
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
- hopping between feature work and PR checks
|
|
13
|
-
- using multiple terminals, editors, or AI agents at the same time
|
|
16
|
+
**If `gji` has saved you from a `git stash` spiral, a ⭐ on [GitHub](https://github.com/sjquant/gji) means a lot — it helps other developers find this tool.**
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
---
|
|
16
19
|
|
|
17
|
-
##
|
|
20
|
+
## The problem
|
|
18
21
|
|
|
19
|
-
|
|
22
|
+
You are deep in a feature branch. A colleague asks for a quick review. You:
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
1. stash your changes
|
|
25
|
+
2. checkout their branch
|
|
26
|
+
3. wait for `pnpm install` to finish
|
|
27
|
+
4. review
|
|
28
|
+
5. checkout back
|
|
29
|
+
6. pop your stash
|
|
30
|
+
7. realize something is broken
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
**Or you use `gji` and it is just `gji pr 1234`.**
|
|
33
|
+
|
|
34
|
+
## Install
|
|
30
35
|
|
|
31
36
|
```sh
|
|
32
|
-
gji
|
|
33
|
-
gji --help
|
|
37
|
+
npm install -g @solaqua/gji
|
|
34
38
|
```
|
|
35
39
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
Inside a Git repository:
|
|
40
|
+
Then add shell integration so `gji go`, `gji new`, and `gji remove` can change your directory:
|
|
39
41
|
|
|
40
42
|
```sh
|
|
41
|
-
|
|
42
|
-
gji
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
That creates a linked worktree at a deterministic path:
|
|
43
|
+
# zsh
|
|
44
|
+
echo 'eval "$(gji init zsh)"' >> ~/.zshrc && source ~/.zshrc
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
46
|
+
# bash
|
|
47
|
+
echo 'eval "$(gji init bash)"' >> ~/.bashrc && source ~/.bashrc
|
|
49
48
|
```
|
|
50
49
|
|
|
51
|
-
##
|
|
52
|
-
|
|
53
|
-
`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.
|
|
54
|
-
|
|
55
|
-
For zsh:
|
|
50
|
+
## Quick start
|
|
56
51
|
|
|
57
52
|
```sh
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```
|
|
53
|
+
# start a new task
|
|
54
|
+
gji new feature/dark-mode
|
|
61
55
|
|
|
62
|
-
|
|
56
|
+
# review a pull request
|
|
57
|
+
gji pr 1234
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
gji
|
|
66
|
-
gji go feature/login-form
|
|
67
|
-
gji root
|
|
68
|
-
gji rm feature/login-form
|
|
69
|
-
```
|
|
70
|
-
|
|
71
|
-
changes your shell directory directly.
|
|
59
|
+
# see what's open
|
|
60
|
+
gji status
|
|
72
61
|
|
|
73
|
-
|
|
62
|
+
# jump between worktrees
|
|
63
|
+
gji go feature/dark-mode
|
|
64
|
+
gji go main
|
|
74
65
|
|
|
75
|
-
|
|
76
|
-
|
|
66
|
+
# clean up when done
|
|
67
|
+
gji remove feature/dark-mode
|
|
77
68
|
```
|
|
78
69
|
|
|
79
|
-
|
|
70
|
+
Worktrees land at a deterministic path so your editor bookmarks and scripts always know where to look:
|
|
80
71
|
|
|
81
|
-
```sh
|
|
82
|
-
gji new feature/login-form
|
|
83
|
-
gji go --print feature/login-form
|
|
84
|
-
gji root --print
|
|
85
72
|
```
|
|
86
|
-
|
|
87
|
-
|
|
73
|
+
../worktrees/<repo>/<branch>
|
|
74
|
+
```
|
|
88
75
|
|
|
89
76
|
## Daily workflow
|
|
90
77
|
|
|
91
|
-
Start a task:
|
|
92
|
-
|
|
93
78
|
```sh
|
|
94
|
-
gji new feature/refactor
|
|
95
|
-
|
|
79
|
+
gji new feature/auth-refactor # new branch + worktree
|
|
80
|
+
gji new --detached # scratch space, auto-named
|
|
96
81
|
|
|
97
|
-
|
|
82
|
+
gji pr 1234 # checkout PR locally
|
|
83
|
+
gji pr https://github.com/org/repo/pull/1234 # or paste the URL
|
|
98
84
|
|
|
99
|
-
|
|
100
|
-
gji
|
|
101
|
-
```
|
|
85
|
+
gji go feature/auth-refactor # jump to a worktree
|
|
86
|
+
gji root # jump to repo root
|
|
102
87
|
|
|
103
|
-
|
|
88
|
+
gji status # health overview + ahead/behind counts
|
|
89
|
+
gji ls # compact list
|
|
104
90
|
|
|
105
|
-
|
|
106
|
-
gji
|
|
107
|
-
gji ls
|
|
108
|
-
```
|
|
91
|
+
gji sync # rebase current worktree onto default branch
|
|
92
|
+
gji sync --all # rebase every worktree
|
|
109
93
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
```sh
|
|
113
|
-
gji pr 123
|
|
114
|
-
gji pr #123
|
|
115
|
-
gji pr https://github.com/owner/repo/pull/123
|
|
94
|
+
gji clean # interactive bulk cleanup
|
|
95
|
+
gji remove feature/auth-refactor # remove one worktree and its branch
|
|
116
96
|
```
|
|
117
97
|
|
|
118
|
-
|
|
98
|
+
## Shell setup
|
|
99
|
+
|
|
100
|
+
Without shell integration `gji` prints paths and exits — which is fine for scripts but means it cannot `cd` you into a new worktree. Install the integration once:
|
|
119
101
|
|
|
120
102
|
```sh
|
|
121
|
-
gji
|
|
103
|
+
gji init zsh # prints the shell function, review it if you like
|
|
122
104
|
```
|
|
123
105
|
|
|
124
|
-
|
|
106
|
+
To install automatically:
|
|
125
107
|
|
|
126
108
|
```sh
|
|
127
|
-
|
|
109
|
+
# zsh
|
|
110
|
+
echo 'eval "$(gji init zsh)"' >> ~/.zshrc
|
|
111
|
+
|
|
112
|
+
# bash
|
|
113
|
+
echo 'eval "$(gji init bash)"' >> ~/.bashrc
|
|
128
114
|
```
|
|
129
115
|
|
|
130
|
-
|
|
116
|
+
After a reinstall or upgrade, re-source to pick up changes:
|
|
131
117
|
|
|
132
118
|
```sh
|
|
133
|
-
gji
|
|
119
|
+
eval "$(gji init zsh)"
|
|
134
120
|
```
|
|
135
121
|
|
|
136
|
-
|
|
122
|
+
For scripts that need the raw path, use `--print`:
|
|
137
123
|
|
|
138
124
|
```sh
|
|
139
|
-
gji
|
|
140
|
-
|
|
141
|
-
gji rm feature/refactor-auth
|
|
125
|
+
path=$(gji go --print feature/dark-mode)
|
|
126
|
+
path=$(gji root --print)
|
|
142
127
|
```
|
|
143
128
|
|
|
144
|
-
After removal, the shell-integrated command returns you to the repository root.
|
|
145
|
-
|
|
146
129
|
## Commands
|
|
147
130
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
131
|
+
| Command | Description |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `gji new [branch] [--detached] [--json]` | create branch + worktree, cd in |
|
|
134
|
+
| `gji pr <ref> [--json]` | fetch PR ref, create worktree, cd in |
|
|
135
|
+
| `gji go [branch] [--print]` | jump to a worktree |
|
|
136
|
+
| `gji root [--print]` | jump to the main repo root |
|
|
137
|
+
| `gji status [--json]` | repo overview, worktree health, ahead/behind |
|
|
138
|
+
| `gji ls [--json]` | list active worktrees |
|
|
139
|
+
| `gji sync [--all]` | fetch and rebase worktrees onto default branch |
|
|
140
|
+
| `gji clean [--force] [--json]` | interactively prune stale worktrees |
|
|
141
|
+
| `gji remove [branch] [--force] [--json]` | remove a worktree and its branch |
|
|
142
|
+
| `gji config [get\|set\|unset] [key] [value]` | manage global defaults |
|
|
143
|
+
| `gji init [shell]` | print or install shell integration |
|
|
160
144
|
|
|
161
145
|
## Configuration
|
|
162
146
|
|
|
163
|
-
|
|
147
|
+
No setup required. Optional config lives in:
|
|
164
148
|
|
|
165
|
-
-
|
|
166
|
-
- repo-local
|
|
149
|
+
- `~/.config/gji/config.json` — global defaults
|
|
150
|
+
- `.gji.json` — repo-local overrides (takes precedence)
|
|
167
151
|
|
|
168
|
-
|
|
152
|
+
### Available keys
|
|
169
153
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
154
|
+
| Key | Description |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `branchPrefix` | prefix added to new branch names (e.g. `"feature/"`) |
|
|
157
|
+
| `syncRemote` | remote for `gji sync` (default: `origin`) |
|
|
158
|
+
| `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
|
|
159
|
+
| `syncFiles` | files to copy from main worktree into each new worktree |
|
|
160
|
+
| `skipInstallPrompt` | `true` to disable the auto-install prompt permanently |
|
|
161
|
+
| `hooks` | lifecycle scripts (see [Hooks](#hooks)) |
|
|
178
162
|
|
|
179
163
|
```json
|
|
180
164
|
{
|
|
181
165
|
"branchPrefix": "feature/",
|
|
182
166
|
"syncRemote": "upstream",
|
|
183
|
-
"syncDefaultBranch": "main"
|
|
167
|
+
"syncDefaultBranch": "main",
|
|
168
|
+
"syncFiles": [".env.example", ".nvmrc"]
|
|
184
169
|
}
|
|
185
170
|
```
|
|
186
171
|
|
|
187
|
-
|
|
172
|
+
### Config commands
|
|
188
173
|
|
|
189
|
-
|
|
190
|
-
|
|
174
|
+
```sh
|
|
175
|
+
gji config get
|
|
176
|
+
gji config get branchPrefix
|
|
177
|
+
gji config set branchPrefix feature/
|
|
178
|
+
gji config unset branchPrefix
|
|
179
|
+
```
|
|
191
180
|
|
|
192
181
|
## Hooks
|
|
193
182
|
|
|
194
|
-
|
|
183
|
+
Run scripts automatically at key lifecycle moments:
|
|
195
184
|
|
|
196
185
|
```json
|
|
197
186
|
{
|
|
198
187
|
"hooks": {
|
|
199
188
|
"afterCreate": "pnpm install",
|
|
200
|
-
"afterEnter": "echo switched to {{branch}}",
|
|
189
|
+
"afterEnter": "echo 'switched to {{branch}}'",
|
|
201
190
|
"beforeRemove": "pnpm run cleanup"
|
|
202
191
|
}
|
|
203
192
|
}
|
|
204
193
|
```
|
|
205
194
|
|
|
206
|
-
Hook
|
|
207
|
-
|
|
208
|
-
- `afterCreate` — runs after a new worktree is created, whether via `gji new` or `gji pr`
|
|
209
|
-
- `afterEnter` — runs after switching to a worktree via `gji go`
|
|
210
|
-
- `beforeRemove` — runs before a worktree is removed via `gji remove`
|
|
211
|
-
|
|
212
|
-
Each hook receives context in two ways:
|
|
213
|
-
|
|
214
|
-
**Template variables** (substituted into the command string):
|
|
215
|
-
|
|
216
|
-
| Variable | Value |
|
|
195
|
+
| Hook | When it runs |
|
|
217
196
|
|---|---|
|
|
218
|
-
| `
|
|
219
|
-
| `
|
|
220
|
-
| `
|
|
197
|
+
| `afterCreate` | after `gji new` or `gji pr` creates a worktree |
|
|
198
|
+
| `afterEnter` | after `gji go` switches to a worktree |
|
|
199
|
+
| `beforeRemove` | before `gji remove` deletes a worktree |
|
|
221
200
|
|
|
222
|
-
|
|
201
|
+
Hooks receive `{{branch}}`, `{{path}}`, `{{repo}}` as template variables and `GJI_BRANCH`, `GJI_PATH`, `GJI_REPO` as environment variables. A failing hook emits a warning but never aborts the command.
|
|
223
202
|
|
|
224
|
-
|
|
225
|
-
|---|---|
|
|
226
|
-
| `GJI_BRANCH` | branch name, or empty string for detached worktrees |
|
|
227
|
-
| `GJI_PATH` | absolute path to the worktree |
|
|
228
|
-
| `GJI_REPO` | repository directory name |
|
|
229
|
-
|
|
230
|
-
Hooks run inside the worktree directory. A non-zero exit emits a warning but does not abort the command.
|
|
231
|
-
|
|
232
|
-
Global and project-level hooks are merged per key — project values override global values for the same key, while keys only present in the global config still apply:
|
|
203
|
+
Global and repo-local hooks deep-merge per key:
|
|
233
204
|
|
|
234
|
-
```
|
|
205
|
+
```jsonc
|
|
235
206
|
// ~/.config/gji/config.json
|
|
236
207
|
{ "hooks": { "afterCreate": "nvm use", "afterEnter": "echo hi" } }
|
|
237
208
|
|
|
238
209
|
// .gji.json
|
|
239
210
|
{ "hooks": { "afterCreate": "pnpm install" } }
|
|
240
211
|
|
|
241
|
-
// effective
|
|
242
|
-
{ "afterCreate": "pnpm install", "afterEnter": "echo hi" }
|
|
212
|
+
// effective
|
|
213
|
+
{ "hooks": { "afterCreate": "pnpm install", "afterEnter": "echo hi" } }
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Install prompt
|
|
217
|
+
|
|
218
|
+
When `gji new` or `gji pr` creates a worktree, `gji` detects the project's package manager from its lockfile and offers to run the install command:
|
|
219
|
+
|
|
220
|
+
```
|
|
221
|
+
Run `pnpm install` in the new worktree?
|
|
222
|
+
› Yes run once
|
|
223
|
+
No skip this time
|
|
224
|
+
Always save as afterCreate hook
|
|
225
|
+
Never disable this prompt for this repo
|
|
243
226
|
```
|
|
244
227
|
|
|
228
|
+
**Always** saves `hooks.afterCreate` to `.gji.json`; **Never** writes `skipInstallPrompt: true`. Both are local-only — global config is never modified.
|
|
229
|
+
|
|
245
230
|
## JSON output
|
|
246
231
|
|
|
247
|
-
|
|
232
|
+
Every mutating command supports `--json` for scripting and AI agent use. Success goes to stdout, errors go to stderr with exit code 1.
|
|
248
233
|
|
|
249
234
|
```sh
|
|
250
|
-
|
|
235
|
+
# create
|
|
236
|
+
gji new --json feature/dark-mode
|
|
237
|
+
# → { "branch": "feature/dark-mode", "path": "/…/worktrees/repo/feature/dark-mode" }
|
|
238
|
+
|
|
239
|
+
# fetch PR
|
|
240
|
+
gji pr --json 1234
|
|
241
|
+
# → { "branch": "pr/1234", "path": "/…/worktrees/repo/pr/1234" }
|
|
242
|
+
|
|
243
|
+
# remove
|
|
244
|
+
gji remove --json --force feature/dark-mode
|
|
245
|
+
# → { "branch": "feature/dark-mode", "path": "/…", "deleted": true }
|
|
246
|
+
|
|
247
|
+
# bulk clean
|
|
248
|
+
gji clean --json --force
|
|
249
|
+
# → { "removed": [{ "branch": "...", "path": "..." }, …] }
|
|
250
|
+
|
|
251
|
+
# error shape (any command)
|
|
252
|
+
# stderr → { "error": "branch argument is required" }
|
|
251
253
|
```
|
|
252
254
|
|
|
253
|
-
`
|
|
255
|
+
`--json` suppresses all interactive prompts. `--force` is required for `remove` and `clean` in JSON mode. `branch` is `null` for detached worktrees.
|
|
256
|
+
|
|
257
|
+
`gji ls --json` and `gji status --json` also produce structured output — see `gji status --json | jq` for the full schema.
|
|
254
258
|
|
|
255
|
-
-
|
|
256
|
-
- `currentRoot`
|
|
257
|
-
- `worktrees`
|
|
259
|
+
## Non-interactive / CI mode
|
|
258
260
|
|
|
259
|
-
|
|
261
|
+
```sh
|
|
262
|
+
GJI_NO_TUI=1 gji new feature/ci-branch
|
|
263
|
+
GJI_NO_TUI=1 gji remove --force feature/ci-branch
|
|
264
|
+
GJI_NO_TUI=1 gji clean --force
|
|
265
|
+
```
|
|
260
266
|
|
|
261
|
-
|
|
262
|
-
- `current`
|
|
263
|
-
- `path`
|
|
264
|
-
- `status`: `clean` or `dirty`
|
|
265
|
-
- `upstream`: one of
|
|
266
|
-
- `{ "kind": "detached" }`
|
|
267
|
-
- `{ "kind": "no-upstream" }`
|
|
268
|
-
- `{ "kind": "tracked", "ahead": number, "behind": number }`
|
|
267
|
+
`GJI_NO_TUI=1` disables all prompts. Commands that need confirmation require `--force`. `--json` implies the same behaviour.
|
|
269
268
|
|
|
270
269
|
## Notes
|
|
271
270
|
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
- `gji pr`
|
|
271
|
+
- Works from either the main repo root or inside any linked worktree
|
|
272
|
+
- The current worktree is never offered as a `gji clean` candidate
|
|
273
|
+
- `gji pr` parses GitHub, GitLab, and Bitbucket URLs but always fetches via `refs/pull/<number>/head` from `origin`
|
|
275
274
|
|
|
276
275
|
## License
|
|
277
276
|
|
package/dist/clean.d.ts
CHANGED
package/dist/clean.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { confirm, isCancel, multiselect } from '@clack/prompts';
|
|
2
|
+
import { isHeadless } from './headless.js';
|
|
2
3
|
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
|
|
3
4
|
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree } from './worktree-prompts.js';
|
|
4
5
|
export function createCleanCommand(dependencies = {}) {
|
|
@@ -10,10 +11,24 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
10
11
|
const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
|
|
11
12
|
const cleanupCandidates = linkedWorktrees.filter((worktree) => worktree.path !== repository.currentRoot);
|
|
12
13
|
if (cleanupCandidates.length === 0) {
|
|
13
|
-
options
|
|
14
|
+
emitError(options, 'No linked worktrees to clean');
|
|
14
15
|
return 1;
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
+
if (!options.dryRun && !options.force && (options.json || isHeadless())) {
|
|
18
|
+
const message = '--force is required';
|
|
19
|
+
if (options.json) {
|
|
20
|
+
emitError(options, message);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
options.stderr(`gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
|
|
24
|
+
}
|
|
25
|
+
return 1;
|
|
26
|
+
}
|
|
27
|
+
// With --force, or dry-run in headless/json mode, skip selection prompt and target all candidates.
|
|
28
|
+
const shouldSelectAll = options.force || (options.dryRun && (options.json || isHeadless()));
|
|
29
|
+
const selections = shouldSelectAll
|
|
30
|
+
? cleanupCandidates.map((w) => w.path)
|
|
31
|
+
: await promptForWorktrees(cleanupCandidates);
|
|
17
32
|
if (!selections || selections.length === 0) {
|
|
18
33
|
options.stderr('Aborted\n');
|
|
19
34
|
return 1;
|
|
@@ -23,11 +38,25 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
23
38
|
options.stderr('Selected worktree no longer exists\n');
|
|
24
39
|
return 1;
|
|
25
40
|
}
|
|
26
|
-
if (!options.force && !(await confirmRemoval(selectedWorktrees))) {
|
|
41
|
+
if (!options.dryRun && !options.force && !(await confirmRemoval(selectedWorktrees))) {
|
|
27
42
|
options.stderr('Aborted\n');
|
|
28
43
|
return 1;
|
|
29
44
|
}
|
|
45
|
+
if (options.dryRun) {
|
|
46
|
+
if (options.json) {
|
|
47
|
+
const removed = selectedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
|
|
48
|
+
options.stdout(`${JSON.stringify({ removed, dryRun: true }, null, 2)}\n`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
for (const w of selectedWorktrees) {
|
|
52
|
+
const desc = w.branch ? `branch: ${w.branch}` : 'detached';
|
|
53
|
+
options.stdout(`Would remove worktree at ${w.path} (${desc})\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
30
58
|
const removedPaths = [];
|
|
59
|
+
const removedWorktrees = [];
|
|
31
60
|
for (const worktree of selectedWorktrees) {
|
|
32
61
|
try {
|
|
33
62
|
await removeWorktree(repository.repoRoot, worktree.path);
|
|
@@ -45,12 +74,15 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
45
74
|
await forceRemoveWorktree(repository.repoRoot, worktree.path);
|
|
46
75
|
}
|
|
47
76
|
catch (forceError) {
|
|
48
|
-
|
|
49
|
-
|
|
77
|
+
if (!options.json) {
|
|
78
|
+
reportRemovedPaths(removedPaths, options.stderr);
|
|
79
|
+
}
|
|
80
|
+
emitError(options, `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`);
|
|
50
81
|
return 1;
|
|
51
82
|
}
|
|
52
83
|
}
|
|
53
84
|
removedPaths.push(worktree.path);
|
|
85
|
+
removedWorktrees.push(worktree);
|
|
54
86
|
if (worktree.branch) {
|
|
55
87
|
try {
|
|
56
88
|
await deleteBranch(repository.repoRoot, worktree.branch);
|
|
@@ -73,7 +105,13 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
73
105
|
}
|
|
74
106
|
}
|
|
75
107
|
}
|
|
76
|
-
options.
|
|
108
|
+
if (options.json) {
|
|
109
|
+
const removed = removedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
|
|
110
|
+
options.stdout(`${JSON.stringify({ removed }, null, 2)}\n`);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
options.stdout(`${repository.repoRoot}\n`);
|
|
114
|
+
}
|
|
77
115
|
return 0;
|
|
78
116
|
};
|
|
79
117
|
}
|
|
@@ -96,6 +134,14 @@ function reportRemovedPaths(paths, stderr) {
|
|
|
96
134
|
stderr(`Already removed: ${paths.join(', ')}\n`);
|
|
97
135
|
}
|
|
98
136
|
}
|
|
137
|
+
function emitError(options, message) {
|
|
138
|
+
if (options.json) {
|
|
139
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
options.stderr(`${message}\n`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
99
145
|
function toMessage(error) {
|
|
100
146
|
return error instanceof Error ? error.message : String(error);
|
|
101
147
|
}
|
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,8 @@ function registerCommands(program) {
|
|
|
59
59
|
.command('new [branch]')
|
|
60
60
|
.description('create a new branch or detached linked worktree')
|
|
61
61
|
.option('--detached', 'create a detached worktree without a branch')
|
|
62
|
+
.option('--dry-run', 'show what would be created without executing any git commands or writing files')
|
|
63
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
62
64
|
.action(notImplemented('new'));
|
|
63
65
|
program
|
|
64
66
|
.command('init [shell]')
|
|
@@ -66,8 +68,10 @@ function registerCommands(program) {
|
|
|
66
68
|
.option('--write', 'write the integration to the shell config file')
|
|
67
69
|
.action(notImplemented('init'));
|
|
68
70
|
program
|
|
69
|
-
.command('pr <
|
|
70
|
-
.description('fetch a pull request
|
|
71
|
+
.command('pr <ref>')
|
|
72
|
+
.description('fetch a pull request by number, #number, or URL into a linked worktree')
|
|
73
|
+
.option('--dry-run', 'show what would be created without executing any git commands or writing files')
|
|
74
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
71
75
|
.action(notImplemented('pr'));
|
|
72
76
|
program
|
|
73
77
|
.command('go [branch]')
|
|
@@ -88,6 +92,7 @@ function registerCommands(program) {
|
|
|
88
92
|
.command('sync')
|
|
89
93
|
.description('fetch and update one or all worktrees')
|
|
90
94
|
.option('--all', 'sync every worktree in the repository')
|
|
95
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
91
96
|
.action(notImplemented('sync'));
|
|
92
97
|
program
|
|
93
98
|
.command('ls')
|
|
@@ -98,12 +103,16 @@ function registerCommands(program) {
|
|
|
98
103
|
.command('clean')
|
|
99
104
|
.description('interactively prune linked worktrees')
|
|
100
105
|
.option('-f, --force', 'bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches')
|
|
106
|
+
.option('--dry-run', 'show what would be deleted without removing anything')
|
|
107
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
101
108
|
.action(notImplemented('clean'));
|
|
102
109
|
program
|
|
103
110
|
.command('remove [branch]')
|
|
104
111
|
.alias('rm')
|
|
105
112
|
.description('remove a linked worktree and delete its branch when present')
|
|
106
113
|
.option('-f, --force', 'bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch')
|
|
114
|
+
.option('--dry-run', 'show what would be deleted without removing anything')
|
|
115
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
107
116
|
.action(notImplemented('remove'));
|
|
108
117
|
const configCommand = program
|
|
109
118
|
.command('config')
|
|
@@ -126,7 +135,7 @@ function attachCommandActions(program, options) {
|
|
|
126
135
|
program.commands
|
|
127
136
|
.find((command) => command.name() === 'new')
|
|
128
137
|
?.action(async (branch, commandOptions) => {
|
|
129
|
-
const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached });
|
|
138
|
+
const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, json: commandOptions.json });
|
|
130
139
|
if (exitCode !== 0) {
|
|
131
140
|
throw commanderExit(exitCode);
|
|
132
141
|
}
|
|
@@ -146,8 +155,8 @@ function attachCommandActions(program, options) {
|
|
|
146
155
|
});
|
|
147
156
|
program.commands
|
|
148
157
|
.find((command) => command.name() === 'pr')
|
|
149
|
-
?.action(async (number) => {
|
|
150
|
-
const exitCode = await runPrCommand({ cwd: options.cwd, number, stderr: options.stderr, stdout: options.stdout });
|
|
158
|
+
?.action(async (number, commandOptions) => {
|
|
159
|
+
const exitCode = await runPrCommand({ cwd: options.cwd, dryRun: commandOptions.dryRun, json: commandOptions.json, number, stderr: options.stderr, stdout: options.stdout });
|
|
151
160
|
if (exitCode !== 0) {
|
|
152
161
|
throw commanderExit(exitCode);
|
|
153
162
|
}
|
|
@@ -196,6 +205,7 @@ function attachCommandActions(program, options) {
|
|
|
196
205
|
const exitCode = await runSyncCommand({
|
|
197
206
|
all: commandOptions.all,
|
|
198
207
|
cwd: options.cwd,
|
|
208
|
+
json: commandOptions.json,
|
|
199
209
|
stderr: options.stderr,
|
|
200
210
|
stdout: options.stdout,
|
|
201
211
|
});
|
|
@@ -220,7 +230,9 @@ function attachCommandActions(program, options) {
|
|
|
220
230
|
?.action(async (commandOptions) => {
|
|
221
231
|
const exitCode = await runCleanCommand({
|
|
222
232
|
cwd: options.cwd,
|
|
233
|
+
dryRun: commandOptions.dryRun,
|
|
223
234
|
force: commandOptions.force,
|
|
235
|
+
json: commandOptions.json,
|
|
224
236
|
stderr: options.stderr,
|
|
225
237
|
stdout: options.stdout,
|
|
226
238
|
});
|
|
@@ -232,7 +244,9 @@ function attachCommandActions(program, options) {
|
|
|
232
244
|
const exitCode = await runRemoveCommand({
|
|
233
245
|
branch,
|
|
234
246
|
cwd: options.cwd,
|
|
247
|
+
dryRun: commandOptions.dryRun,
|
|
235
248
|
force: commandOptions.force,
|
|
249
|
+
json: commandOptions.json,
|
|
236
250
|
stderr: options.stderr,
|
|
237
251
|
stdout: options.stdout,
|
|
238
252
|
});
|
package/dist/config.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const DEFAULT_CONFIG: GjiConfig;
|
|
|
11
11
|
export declare function loadConfig(root: string): Promise<LoadedConfig>;
|
|
12
12
|
export declare function loadEffectiveConfig(root: string, home?: string): Promise<GjiConfig>;
|
|
13
13
|
export declare function loadGlobalConfig(home?: string): Promise<LoadedConfig>;
|
|
14
|
+
export declare function saveLocalConfig(root: string, config: GjiConfig): Promise<string>;
|
|
15
|
+
export declare function updateLocalConfigKey(root: string, key: string, value: unknown): Promise<GjiConfig>;
|
|
14
16
|
export declare function saveGlobalConfig(config: GjiConfig, home?: string): Promise<string>;
|
|
15
17
|
export declare function unsetGlobalConfigKey(key: string, home?: string): Promise<GjiConfig>;
|
|
16
18
|
export declare function updateGlobalConfigKey(key: string, value: unknown, home?: string): Promise<GjiConfig>;
|