@solaqua/gji 0.1.0-beta.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -130
- package/dist/clean.d.ts +4 -0
- package/dist/clean.js +90 -7
- package/dist/cli.js +15 -5
- package/dist/config.d.ts +2 -0
- package/dist/config.js +24 -1
- package/dist/file-sync.d.ts +9 -0
- package/dist/file-sync.js +52 -0
- package/dist/go.js +25 -14
- package/dist/headless.d.ts +6 -0
- package/dist/headless.js +8 -0
- package/dist/hooks.d.ts +13 -0
- package/dist/hooks.js +50 -0
- package/dist/init.js +1 -1
- package/dist/install-prompt.d.ts +10 -0
- package/dist/install-prompt.js +99 -0
- package/dist/new.d.ts +3 -1
- package/dist/new.js +52 -3
- package/dist/package-manager.d.ts +5 -0
- package/dist/package-manager.js +108 -0
- package/dist/pr.d.ts +3 -1
- package/dist/pr.js +52 -4
- package/dist/remove.d.ts +4 -0
- package/dist/remove.js +89 -7
- package/dist/worktree-management.d.ts +4 -0
- package/dist/worktree-management.js +19 -2
- package/dist/worktree-prompts.d.ts +2 -0
- package/dist/worktree-prompts.js +19 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,222 +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
|
|
21
|
+
|
|
22
|
+
You are deep in a feature branch. A colleague asks for a quick review. You:
|
|
23
|
+
|
|
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
|
|
18
31
|
|
|
19
|
-
|
|
32
|
+
**Or you use `gji` and it is just `gji pr 1234`.**
|
|
33
|
+
|
|
34
|
+
## Install
|
|
20
35
|
|
|
21
36
|
```sh
|
|
22
|
-
|
|
23
|
-
cd gji
|
|
24
|
-
pnpm build
|
|
25
|
-
npm install -g .
|
|
37
|
+
npm install -g @solaqua/gji
|
|
26
38
|
```
|
|
27
39
|
|
|
28
|
-
|
|
40
|
+
Then add shell integration so `gji go`, `gji new`, and `gji remove` can change your directory:
|
|
29
41
|
|
|
30
42
|
```sh
|
|
31
|
-
|
|
32
|
-
gji
|
|
43
|
+
# zsh
|
|
44
|
+
echo 'eval "$(gji init zsh)"' >> ~/.zshrc && source ~/.zshrc
|
|
45
|
+
|
|
46
|
+
# bash
|
|
47
|
+
echo 'eval "$(gji init bash)"' >> ~/.bashrc && source ~/.bashrc
|
|
33
48
|
```
|
|
34
49
|
|
|
35
50
|
## Quick start
|
|
36
51
|
|
|
37
|
-
Inside a Git repository:
|
|
38
|
-
|
|
39
52
|
```sh
|
|
40
|
-
|
|
41
|
-
gji
|
|
42
|
-
```
|
|
53
|
+
# start a new task
|
|
54
|
+
gji new feature/dark-mode
|
|
43
55
|
|
|
44
|
-
|
|
56
|
+
# review a pull request
|
|
57
|
+
gji pr 1234
|
|
45
58
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
```
|
|
59
|
+
# see what's open
|
|
60
|
+
gji status
|
|
49
61
|
|
|
50
|
-
|
|
62
|
+
# jump between worktrees
|
|
63
|
+
gji go feature/dark-mode
|
|
64
|
+
gji go main
|
|
51
65
|
|
|
52
|
-
|
|
66
|
+
# clean up when done
|
|
67
|
+
gji remove feature/dark-mode
|
|
68
|
+
```
|
|
53
69
|
|
|
54
|
-
|
|
70
|
+
Worktrees land at a deterministic path so your editor bookmarks and scripts always know where to look:
|
|
55
71
|
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
source ~/.zshrc
|
|
72
|
+
```
|
|
73
|
+
../worktrees/<repo>/<branch>
|
|
59
74
|
```
|
|
60
75
|
|
|
61
|
-
|
|
76
|
+
## Daily workflow
|
|
62
77
|
|
|
63
78
|
```sh
|
|
64
|
-
gji new feature/
|
|
65
|
-
gji
|
|
66
|
-
gji root
|
|
67
|
-
gji rm feature/login-form
|
|
68
|
-
```
|
|
79
|
+
gji new feature/auth-refactor # new branch + worktree
|
|
80
|
+
gji new --detached # scratch space, auto-named
|
|
69
81
|
|
|
70
|
-
|
|
82
|
+
gji pr 1234 # checkout PR locally
|
|
83
|
+
gji pr https://github.com/org/repo/pull/1234 # or paste the URL
|
|
71
84
|
|
|
72
|
-
|
|
85
|
+
gji go feature/auth-refactor # jump to a worktree
|
|
86
|
+
gji root # jump to repo root
|
|
73
87
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
```
|
|
88
|
+
gji status # health overview + ahead/behind counts
|
|
89
|
+
gji ls # compact list
|
|
77
90
|
|
|
78
|
-
|
|
91
|
+
gji sync # rebase current worktree onto default branch
|
|
92
|
+
gji sync --all # rebase every worktree
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
gji
|
|
82
|
-
gji go --print feature/login-form
|
|
83
|
-
gji root --print
|
|
94
|
+
gji clean # interactive bulk cleanup
|
|
95
|
+
gji remove feature/auth-refactor # remove one worktree and its branch
|
|
84
96
|
```
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
## Daily workflow
|
|
98
|
+
## Shell setup
|
|
89
99
|
|
|
90
|
-
|
|
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:
|
|
91
101
|
|
|
92
102
|
```sh
|
|
93
|
-
gji
|
|
103
|
+
gji init zsh # prints the shell function, review it if you like
|
|
94
104
|
```
|
|
95
105
|
|
|
96
|
-
|
|
106
|
+
To install automatically:
|
|
97
107
|
|
|
98
108
|
```sh
|
|
99
|
-
|
|
109
|
+
# zsh
|
|
110
|
+
echo 'eval "$(gji init zsh)"' >> ~/.zshrc
|
|
111
|
+
|
|
112
|
+
# bash
|
|
113
|
+
echo 'eval "$(gji init bash)"' >> ~/.bashrc
|
|
100
114
|
```
|
|
101
115
|
|
|
102
|
-
|
|
116
|
+
After a reinstall or upgrade, re-source to pick up changes:
|
|
103
117
|
|
|
104
118
|
```sh
|
|
105
|
-
gji
|
|
106
|
-
gji ls
|
|
119
|
+
eval "$(gji init zsh)"
|
|
107
120
|
```
|
|
108
121
|
|
|
109
|
-
|
|
122
|
+
For scripts that need the raw path, use `--print`:
|
|
110
123
|
|
|
111
124
|
```sh
|
|
112
|
-
gji
|
|
113
|
-
gji
|
|
114
|
-
gji pr https://github.com/owner/repo/pull/123
|
|
125
|
+
path=$(gji go --print feature/dark-mode)
|
|
126
|
+
path=$(gji root --print)
|
|
115
127
|
```
|
|
116
128
|
|
|
117
|
-
|
|
129
|
+
## Commands
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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 |
|
|
122
144
|
|
|
123
|
-
|
|
145
|
+
## Configuration
|
|
124
146
|
|
|
125
|
-
|
|
126
|
-
gji sync --all
|
|
127
|
-
```
|
|
147
|
+
No setup required. Optional config lives in:
|
|
128
148
|
|
|
129
|
-
|
|
149
|
+
- `~/.config/gji/config.json` — global defaults
|
|
150
|
+
- `.gji.json` — repo-local overrides (takes precedence)
|
|
130
151
|
|
|
131
|
-
|
|
132
|
-
|
|
152
|
+
### Available keys
|
|
153
|
+
|
|
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)) |
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"branchPrefix": "feature/",
|
|
166
|
+
"syncRemote": "upstream",
|
|
167
|
+
"syncDefaultBranch": "main",
|
|
168
|
+
"syncFiles": [".env.example", ".nvmrc"]
|
|
169
|
+
}
|
|
133
170
|
```
|
|
134
171
|
|
|
135
|
-
|
|
172
|
+
### Config commands
|
|
136
173
|
|
|
137
174
|
```sh
|
|
138
|
-
gji
|
|
139
|
-
|
|
140
|
-
gji
|
|
175
|
+
gji config get
|
|
176
|
+
gji config get branchPrefix
|
|
177
|
+
gji config set branchPrefix feature/
|
|
178
|
+
gji config unset branchPrefix
|
|
141
179
|
```
|
|
142
180
|
|
|
143
|
-
|
|
181
|
+
## Hooks
|
|
144
182
|
|
|
145
|
-
|
|
183
|
+
Run scripts automatically at key lifecycle moments:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{
|
|
187
|
+
"hooks": {
|
|
188
|
+
"afterCreate": "pnpm install",
|
|
189
|
+
"afterEnter": "echo 'switched to {{branch}}'",
|
|
190
|
+
"beforeRemove": "pnpm run cleanup"
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
146
194
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- `gji root [--print]` jumps to the main repository root when shell integration is installed, or prints it otherwise
|
|
153
|
-
- `gji status [--json]` prints repository metadata, worktree health, and upstream divergence
|
|
154
|
-
- `gji sync [--all]` fetches from the configured remote and rebases or fast-forwards worktrees onto the configured default branch
|
|
155
|
-
- `gji ls [--json]` lists active worktrees in a table or JSON
|
|
156
|
-
- `gji clean` interactively prunes one or more linked worktrees, including detached entries, while excluding the current worktree
|
|
157
|
-
- `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
|
|
158
|
-
- `gji config` reads or updates global defaults
|
|
195
|
+
| Hook | When it runs |
|
|
196
|
+
|---|---|
|
|
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 |
|
|
159
200
|
|
|
160
|
-
|
|
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.
|
|
161
202
|
|
|
162
|
-
|
|
203
|
+
Global and repo-local hooks deep-merge per key:
|
|
163
204
|
|
|
164
|
-
|
|
165
|
-
|
|
205
|
+
```jsonc
|
|
206
|
+
// ~/.config/gji/config.json
|
|
207
|
+
{ "hooks": { "afterCreate": "nvm use", "afterEnter": "echo hi" } }
|
|
166
208
|
|
|
167
|
-
|
|
209
|
+
// .gji.json
|
|
210
|
+
{ "hooks": { "afterCreate": "pnpm install" } }
|
|
168
211
|
|
|
169
|
-
|
|
212
|
+
// effective
|
|
213
|
+
{ "hooks": { "afterCreate": "pnpm install", "afterEnter": "echo hi" } }
|
|
214
|
+
```
|
|
170
215
|
|
|
171
|
-
|
|
172
|
-
- `syncRemote`
|
|
173
|
-
- `syncDefaultBranch`
|
|
216
|
+
## Install prompt
|
|
174
217
|
|
|
175
|
-
|
|
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:
|
|
176
219
|
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
183
226
|
```
|
|
184
227
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
- if `syncRemote` is unset, `gji sync` defaults to `origin`
|
|
188
|
-
- if `syncDefaultBranch` is unset, `gji sync` resolves the remote default branch from `HEAD`
|
|
228
|
+
**Always** saves `hooks.afterCreate` to `.gji.json`; **Never** writes `skipInstallPrompt: true`. Both are local-only — global config is never modified.
|
|
189
229
|
|
|
190
230
|
## JSON output
|
|
191
231
|
|
|
192
|
-
|
|
232
|
+
Every mutating command supports `--json` for scripting and AI agent use. Success goes to stdout, errors go to stderr with exit code 1.
|
|
193
233
|
|
|
194
234
|
```sh
|
|
195
|
-
|
|
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" }
|
|
196
253
|
```
|
|
197
254
|
|
|
198
|
-
`
|
|
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.
|
|
199
258
|
|
|
200
|
-
-
|
|
201
|
-
- `currentRoot`
|
|
202
|
-
- `worktrees`
|
|
259
|
+
## Non-interactive / CI mode
|
|
203
260
|
|
|
204
|
-
|
|
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
|
+
```
|
|
205
266
|
|
|
206
|
-
|
|
207
|
-
- `current`
|
|
208
|
-
- `path`
|
|
209
|
-
- `status`: `clean` or `dirty`
|
|
210
|
-
- `upstream`: one of
|
|
211
|
-
- `{ "kind": "detached" }`
|
|
212
|
-
- `{ "kind": "no-upstream" }`
|
|
213
|
-
- `{ "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.
|
|
214
268
|
|
|
215
269
|
## Notes
|
|
216
270
|
|
|
217
|
-
-
|
|
218
|
-
-
|
|
219
|
-
- `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`
|
|
220
274
|
|
|
221
275
|
## License
|
|
222
276
|
|
package/dist/clean.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { WorktreeEntry } from './repo.js';
|
|
2
2
|
export interface CleanCommandOptions {
|
|
3
3
|
cwd: string;
|
|
4
|
+
force?: boolean;
|
|
5
|
+
json?: boolean;
|
|
4
6
|
stderr: (chunk: string) => void;
|
|
5
7
|
stdout: (chunk: string) => void;
|
|
6
8
|
}
|
|
7
9
|
export interface CleanCommandDependencies {
|
|
10
|
+
confirmForceDeleteBranch: (branch: string) => Promise<boolean>;
|
|
11
|
+
confirmForceRemoveWorktree: (worktreePath: string) => Promise<boolean>;
|
|
8
12
|
confirmRemoval: (worktrees: WorktreeEntry[]) => Promise<boolean>;
|
|
9
13
|
promptForWorktrees: (worktrees: WorktreeEntry[]) => Promise<string[] | null>;
|
|
10
14
|
}
|
package/dist/clean.js
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
import { confirm, isCancel, multiselect } from '@clack/prompts';
|
|
2
|
-
import {
|
|
2
|
+
import { isHeadless } from './headless.js';
|
|
3
|
+
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
|
|
4
|
+
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree } from './worktree-prompts.js';
|
|
3
5
|
export function createCleanCommand(dependencies = {}) {
|
|
4
6
|
const promptForWorktrees = dependencies.promptForWorktrees ?? defaultPromptForWorktrees;
|
|
5
7
|
const confirmRemoval = dependencies.confirmRemoval ?? defaultConfirmRemoval;
|
|
8
|
+
const confirmForceRemoveWorktree = dependencies.confirmForceRemoveWorktree ?? defaultConfirmForceRemoveWorktree;
|
|
9
|
+
const confirmForceDeleteBranch = dependencies.confirmForceDeleteBranch ?? defaultConfirmForceDeleteBranch;
|
|
6
10
|
return async function runCleanCommand(options) {
|
|
7
11
|
const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd);
|
|
8
12
|
const cleanupCandidates = linkedWorktrees.filter((worktree) => worktree.path !== repository.currentRoot);
|
|
9
13
|
if (cleanupCandidates.length === 0) {
|
|
10
|
-
options
|
|
14
|
+
emitError(options, 'No linked worktrees to clean');
|
|
11
15
|
return 1;
|
|
12
16
|
}
|
|
13
|
-
|
|
17
|
+
if (!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, skip selection prompt and target all candidates.
|
|
28
|
+
const selections = options.force
|
|
29
|
+
? cleanupCandidates.map((w) => w.path)
|
|
30
|
+
: await promptForWorktrees(cleanupCandidates);
|
|
14
31
|
if (!selections || selections.length === 0) {
|
|
15
32
|
options.stderr('Aborted\n');
|
|
16
33
|
return 1;
|
|
@@ -20,17 +37,67 @@ export function createCleanCommand(dependencies = {}) {
|
|
|
20
37
|
options.stderr('Selected worktree no longer exists\n');
|
|
21
38
|
return 1;
|
|
22
39
|
}
|
|
23
|
-
if (!(await confirmRemoval(selectedWorktrees))) {
|
|
40
|
+
if (!options.force && !(await confirmRemoval(selectedWorktrees))) {
|
|
24
41
|
options.stderr('Aborted\n');
|
|
25
42
|
return 1;
|
|
26
43
|
}
|
|
44
|
+
const removedPaths = [];
|
|
45
|
+
const removedWorktrees = [];
|
|
27
46
|
for (const worktree of selectedWorktrees) {
|
|
28
|
-
|
|
47
|
+
try {
|
|
48
|
+
await removeWorktree(repository.repoRoot, worktree.path);
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (!isWorktreeDirtyError(error)) {
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (!options.force && !(await confirmForceRemoveWorktree(worktree.path))) {
|
|
55
|
+
reportRemovedPaths(removedPaths, options.stderr);
|
|
56
|
+
options.stderr('Aborted\n');
|
|
57
|
+
return 1;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await forceRemoveWorktree(repository.repoRoot, worktree.path);
|
|
61
|
+
}
|
|
62
|
+
catch (forceError) {
|
|
63
|
+
if (!options.json) {
|
|
64
|
+
reportRemovedPaths(removedPaths, options.stderr);
|
|
65
|
+
}
|
|
66
|
+
emitError(options, `Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}`);
|
|
67
|
+
return 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
removedPaths.push(worktree.path);
|
|
71
|
+
removedWorktrees.push(worktree);
|
|
29
72
|
if (worktree.branch) {
|
|
30
|
-
|
|
73
|
+
try {
|
|
74
|
+
await deleteBranch(repository.repoRoot, worktree.branch);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (!isBranchUnmergedError(error)) {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (options.force || (await confirmForceDeleteBranch(worktree.branch))) {
|
|
81
|
+
try {
|
|
82
|
+
await forceDeleteBranch(repository.repoRoot, worktree.branch);
|
|
83
|
+
}
|
|
84
|
+
catch (forceError) {
|
|
85
|
+
options.stderr(`Failed to delete branch ${worktree.branch}: ${toMessage(forceError)}\n`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
options.stderr(`Branch ${worktree.branch} was not deleted (has unmerged commits)\n`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
31
92
|
}
|
|
32
93
|
}
|
|
33
|
-
options.
|
|
94
|
+
if (options.json) {
|
|
95
|
+
const removed = removedWorktrees.map((w) => ({ branch: w.branch, path: w.path }));
|
|
96
|
+
options.stdout(`${JSON.stringify({ removed }, null, 2)}\n`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
options.stdout(`${repository.repoRoot}\n`);
|
|
100
|
+
}
|
|
34
101
|
return 0;
|
|
35
102
|
};
|
|
36
103
|
}
|
|
@@ -48,6 +115,22 @@ function resolveSelectedWorktrees(worktrees, selections) {
|
|
|
48
115
|
}
|
|
49
116
|
return selectedWorktrees;
|
|
50
117
|
}
|
|
118
|
+
function reportRemovedPaths(paths, stderr) {
|
|
119
|
+
if (paths.length > 0) {
|
|
120
|
+
stderr(`Already removed: ${paths.join(', ')}\n`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function emitError(options, message) {
|
|
124
|
+
if (options.json) {
|
|
125
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
options.stderr(`${message}\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
function toMessage(error) {
|
|
132
|
+
return error instanceof Error ? error.message : String(error);
|
|
133
|
+
}
|
|
51
134
|
async function defaultPromptForWorktrees(worktrees) {
|
|
52
135
|
const choice = await multiselect({
|
|
53
136
|
message: 'Choose worktrees to clean',
|
package/dist/cli.js
CHANGED
|
@@ -59,6 +59,7 @@ 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('--json', 'emit JSON on success or error instead of human-readable output')
|
|
62
63
|
.action(notImplemented('new'));
|
|
63
64
|
program
|
|
64
65
|
.command('init [shell]')
|
|
@@ -68,6 +69,7 @@ function registerCommands(program) {
|
|
|
68
69
|
program
|
|
69
70
|
.command('pr <number>')
|
|
70
71
|
.description('fetch a pull request ref and create a linked worktree')
|
|
72
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
71
73
|
.action(notImplemented('pr'));
|
|
72
74
|
program
|
|
73
75
|
.command('go [branch]')
|
|
@@ -97,11 +99,15 @@ function registerCommands(program) {
|
|
|
97
99
|
program
|
|
98
100
|
.command('clean')
|
|
99
101
|
.description('interactively prune linked worktrees')
|
|
102
|
+
.option('-f, --force', 'bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches')
|
|
103
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
100
104
|
.action(notImplemented('clean'));
|
|
101
105
|
program
|
|
102
106
|
.command('remove [branch]')
|
|
103
107
|
.alias('rm')
|
|
104
108
|
.description('remove a linked worktree and delete its branch when present')
|
|
109
|
+
.option('-f, --force', 'bypass prompts, force-remove a dirty worktree, and force-delete an unmerged branch')
|
|
110
|
+
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
105
111
|
.action(notImplemented('remove'));
|
|
106
112
|
const configCommand = program
|
|
107
113
|
.command('config')
|
|
@@ -124,7 +130,7 @@ function attachCommandActions(program, options) {
|
|
|
124
130
|
program.commands
|
|
125
131
|
.find((command) => command.name() === 'new')
|
|
126
132
|
?.action(async (branch, commandOptions) => {
|
|
127
|
-
const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached });
|
|
133
|
+
const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, json: commandOptions.json });
|
|
128
134
|
if (exitCode !== 0) {
|
|
129
135
|
throw commanderExit(exitCode);
|
|
130
136
|
}
|
|
@@ -144,8 +150,8 @@ function attachCommandActions(program, options) {
|
|
|
144
150
|
});
|
|
145
151
|
program.commands
|
|
146
152
|
.find((command) => command.name() === 'pr')
|
|
147
|
-
?.action(async (number) => {
|
|
148
|
-
const exitCode = await runPrCommand({ cwd: options.cwd, number, stderr: options.stderr, stdout: options.stdout });
|
|
153
|
+
?.action(async (number, commandOptions) => {
|
|
154
|
+
const exitCode = await runPrCommand({ cwd: options.cwd, json: commandOptions.json, number, stderr: options.stderr, stdout: options.stdout });
|
|
149
155
|
if (exitCode !== 0) {
|
|
150
156
|
throw commanderExit(exitCode);
|
|
151
157
|
}
|
|
@@ -215,9 +221,11 @@ function attachCommandActions(program, options) {
|
|
|
215
221
|
});
|
|
216
222
|
program.commands
|
|
217
223
|
.find((command) => command.name() === 'clean')
|
|
218
|
-
?.action(async () => {
|
|
224
|
+
?.action(async (commandOptions) => {
|
|
219
225
|
const exitCode = await runCleanCommand({
|
|
220
226
|
cwd: options.cwd,
|
|
227
|
+
force: commandOptions.force,
|
|
228
|
+
json: commandOptions.json,
|
|
221
229
|
stderr: options.stderr,
|
|
222
230
|
stdout: options.stdout,
|
|
223
231
|
});
|
|
@@ -225,10 +233,12 @@ function attachCommandActions(program, options) {
|
|
|
225
233
|
throw commanderExit(exitCode);
|
|
226
234
|
}
|
|
227
235
|
});
|
|
228
|
-
const runRemovalCommand = async (branch) => {
|
|
236
|
+
const runRemovalCommand = async (branch, commandOptions = {}) => {
|
|
229
237
|
const exitCode = await runRemoveCommand({
|
|
230
238
|
branch,
|
|
231
239
|
cwd: options.cwd,
|
|
240
|
+
force: commandOptions.force,
|
|
241
|
+
json: commandOptions.json,
|
|
232
242
|
stderr: options.stderr,
|
|
233
243
|
stdout: options.stdout,
|
|
234
244
|
});
|
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>;
|