@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 CHANGED
@@ -1,222 +1,276 @@
1
- # gji
1
+ # gji — Git worktrees without the hassle
2
2
 
3
- Context switching without the mess.
3
+ > Jump between tasks instantly. No stash. No reinstall. No mess.
4
4
 
5
- `gji` is a Git worktree CLI for people who jump between tasks all day. It gives each branch or PR its own directory, so you stop doing `stash`, `pop`, reinstall cycles, and fragile branch juggling.
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
- ## Why
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
- Standard branch switching gets annoying when you are:
14
+ ---
10
15
 
11
- - fixing one bug while reviewing another branch
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
- `gji` keeps those contexts isolated in separate worktrees with deterministic paths.
18
+ ---
16
19
 
17
- ## Install
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
- Current source install:
32
+ **Or you use `gji` and it is just `gji pr 1234`.**
33
+
34
+ ## Install
20
35
 
21
36
  ```sh
22
- git clone https://github.com/sjquant/gji.git
23
- cd gji
24
- pnpm build
25
- npm install -g .
37
+ npm install -g @solaqua/gji
26
38
  ```
27
39
 
28
- Confirm the CLI is available:
40
+ Then add shell integration so `gji go`, `gji new`, and `gji remove` can change your directory:
29
41
 
30
42
  ```sh
31
- gji --version
32
- gji --help
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
- gji new feature/login-form
41
- gji status
42
- ```
53
+ # start a new task
54
+ gji new feature/dark-mode
43
55
 
44
- That creates a linked worktree at a deterministic path:
56
+ # review a pull request
57
+ gji pr 1234
45
58
 
46
- ```text
47
- ../worktrees/<repo>/<branch>
48
- ```
59
+ # see what's open
60
+ gji status
49
61
 
50
- ## Shell setup
62
+ # jump between worktrees
63
+ gji go feature/dark-mode
64
+ gji go main
51
65
 
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.
66
+ # clean up when done
67
+ gji remove feature/dark-mode
68
+ ```
53
69
 
54
- For zsh:
70
+ Worktrees land at a deterministic path so your editor bookmarks and scripts always know where to look:
55
71
 
56
- ```sh
57
- echo 'eval "$(gji init zsh)"' >> ~/.zshrc
58
- source ~/.zshrc
72
+ ```
73
+ ../worktrees/<repo>/<branch>
59
74
  ```
60
75
 
61
- After that:
76
+ ## Daily workflow
62
77
 
63
78
  ```sh
64
- gji new feature/login-form
65
- gji go feature/login-form
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
- changes your shell directory directly.
82
+ gji pr 1234 # checkout PR locally
83
+ gji pr https://github.com/org/repo/pull/1234 # or paste the URL
71
84
 
72
- If you reinstall or upgrade `gji`, refresh the shell function:
85
+ gji go feature/auth-refactor # jump to a worktree
86
+ gji root # jump to repo root
73
87
 
74
- ```sh
75
- eval "$(gji init zsh)"
76
- ```
88
+ gji status # health overview + ahead/behind counts
89
+ gji ls # compact list
77
90
 
78
- For scripts or explicit piping:
91
+ gji sync # rebase current worktree onto default branch
92
+ gji sync --all # rebase every worktree
79
93
 
80
- ```sh
81
- gji new feature/login-form
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
- `gji new` and `gji remove` print their destination paths in raw CLI mode, but in a shell-integrated session they change directory directly.
87
-
88
- ## Daily workflow
98
+ ## Shell setup
89
99
 
90
- Start a task:
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 new feature/refactor-auth
103
+ gji init zsh # prints the shell function, review it if you like
94
104
  ```
95
105
 
96
- Start a detached scratch worktree:
106
+ To install automatically:
97
107
 
98
108
  ```sh
99
- gji new --detached
109
+ # zsh
110
+ echo 'eval "$(gji init zsh)"' >> ~/.zshrc
111
+
112
+ # bash
113
+ echo 'eval "$(gji init bash)"' >> ~/.bashrc
100
114
  ```
101
115
 
102
- Check what is active:
116
+ After a reinstall or upgrade, re-source to pick up changes:
103
117
 
104
118
  ```sh
105
- gji status
106
- gji ls
119
+ eval "$(gji init zsh)"
107
120
  ```
108
121
 
109
- Pull a PR into its own worktree:
122
+ For scripts that need the raw path, use `--print`:
110
123
 
111
124
  ```sh
112
- gji pr 123
113
- gji pr #123
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
- Sync the current worktree with the latest default branch:
129
+ ## Commands
118
130
 
119
- ```sh
120
- gji sync
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
- Sync every worktree in the repository:
145
+ ## Configuration
124
146
 
125
- ```sh
126
- gji sync --all
127
- ```
147
+ No setup required. Optional config lives in:
128
148
 
129
- Clean up stale linked worktrees interactively:
149
+ - `~/.config/gji/config.json` global defaults
150
+ - `.gji.json` — repo-local overrides (takes precedence)
130
151
 
131
- ```sh
132
- gji clean
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
- Finish a single worktree explicitly:
172
+ ### Config commands
136
173
 
137
174
  ```sh
138
- gji remove feature/refactor-auth
139
- # or
140
- gji rm feature/refactor-auth
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
- After removal, the shell-integrated command returns you to the repository root.
181
+ ## Hooks
144
182
 
145
- ## Commands
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
- - `gji --version` prints the installed CLI version
148
- - `gji init [shell]` prints shell integration for `zsh`, `bash`, or `fish`
149
- - `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
150
- - `gji pr <ref>` accepts `123`, `#123`, or a full PR/MR URL, extracts the numeric ID, then fetches `origin/pull/<number>/head` and creates a linked `pr/<number>` worktree
151
- - `gji go [branch] [--print]` jumps to an existing worktree when shell integration is installed, or prints the matching worktree path otherwise
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
- ## Configuration
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
- `gji` is usable without setup, but it supports defaults through:
203
+ Global and repo-local hooks deep-merge per key:
163
204
 
164
- - global config at `~/.config/gji/config.json`
165
- - repo-local config at `.gji.json`
205
+ ```jsonc
206
+ // ~/.config/gji/config.json
207
+ { "hooks": { "afterCreate": "nvm use", "afterEnter": "echo hi" } }
166
208
 
167
- Repo-local values override global defaults.
209
+ // .gji.json
210
+ { "hooks": { "afterCreate": "pnpm install" } }
168
211
 
169
- Supported keys:
212
+ // effective
213
+ { "hooks": { "afterCreate": "pnpm install", "afterEnter": "echo hi" } }
214
+ ```
170
215
 
171
- - `branchPrefix`
172
- - `syncRemote`
173
- - `syncDefaultBranch`
216
+ ## Install prompt
174
217
 
175
- Example:
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
- ```json
178
- {
179
- "branchPrefix": "feature/",
180
- "syncRemote": "upstream",
181
- "syncDefaultBranch": "main"
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
- Behavior:
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
- `gji ls --json` returns branch/path entries:
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
- gji ls --json
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
- `gji status --json` returns a top-level object with:
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
- - `repoRoot`
201
- - `currentRoot`
202
- - `worktrees`
259
+ ## Non-interactive / CI mode
203
260
 
204
- Each worktree entry contains:
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
- - `branch`: branch name or `null` for detached worktrees
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
- - `gji` works from either the main repository root or any linked worktree
218
- - the current worktree is never offered as a `gji clean` removal candidate
219
- - `gji pr` accepts GitHub, GitLab, and Bitbucket-style PR/MR links, but still fetches from `origin` using GitHub-style `refs/pull/<number>/head`
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 { deleteBranch, loadLinkedWorktrees, removeWorktree, } from './worktree-management.js';
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.stderr('No linked worktrees to clean\n');
14
+ emitError(options, 'No linked worktrees to clean');
11
15
  return 1;
12
16
  }
13
- const selections = await promptForWorktrees(cleanupCandidates);
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
- await removeWorktree(repository.repoRoot, worktree.path);
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
- await deleteBranch(repository.repoRoot, worktree.branch);
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.stdout(`${repository.repoRoot}\n`);
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>;