@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 CHANGED
@@ -1,277 +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
18
21
 
19
- Current source install:
22
+ You are deep in a feature branch. A colleague asks for a quick review. You:
20
23
 
21
- ```sh
22
- git clone https://github.com/sjquant/gji.git
23
- cd gji
24
- pnpm install
25
- pnpm build
26
- npm install -g .
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
- Confirm the CLI is available:
32
+ **Or you use `gji` and it is just `gji pr 1234`.**
33
+
34
+ ## Install
30
35
 
31
36
  ```sh
32
- gji --version
33
- gji --help
37
+ npm install -g @solaqua/gji
34
38
  ```
35
39
 
36
- ## Quick start
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
- gji new feature/login-form
42
- gji status
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
- ```text
48
- ../worktrees/<repo>/<branch>
46
+ # bash
47
+ echo 'eval "$(gji init bash)"' >> ~/.bashrc && source ~/.bashrc
49
48
  ```
50
49
 
51
- ## Shell setup
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
- echo 'eval "$(gji init zsh)"' >> ~/.zshrc
59
- source ~/.zshrc
60
- ```
53
+ # start a new task
54
+ gji new feature/dark-mode
61
55
 
62
- After that:
56
+ # review a pull request
57
+ gji pr 1234
63
58
 
64
- ```sh
65
- gji new feature/login-form
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
- If you reinstall or upgrade `gji`, refresh the shell function:
62
+ # jump between worktrees
63
+ gji go feature/dark-mode
64
+ gji go main
74
65
 
75
- ```sh
76
- eval "$(gji init zsh)"
66
+ # clean up when done
67
+ gji remove feature/dark-mode
77
68
  ```
78
69
 
79
- For scripts or explicit piping:
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
- `gji new` and `gji remove` print their destination paths in raw CLI mode, but in a shell-integrated session they change directory directly.
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-auth
95
- ```
79
+ gji new feature/auth-refactor # new branch + worktree
80
+ gji new --detached # scratch space, auto-named
96
81
 
97
- Start a detached scratch worktree:
82
+ gji pr 1234 # checkout PR locally
83
+ gji pr https://github.com/org/repo/pull/1234 # or paste the URL
98
84
 
99
- ```sh
100
- gji new --detached
101
- ```
85
+ gji go feature/auth-refactor # jump to a worktree
86
+ gji root # jump to repo root
102
87
 
103
- Check what is active:
88
+ gji status # health overview + ahead/behind counts
89
+ gji ls # compact list
104
90
 
105
- ```sh
106
- gji status
107
- gji ls
108
- ```
91
+ gji sync # rebase current worktree onto default branch
92
+ gji sync --all # rebase every worktree
109
93
 
110
- Pull a PR into its own worktree:
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
- Sync the current worktree with the latest default branch:
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 sync
103
+ gji init zsh # prints the shell function, review it if you like
122
104
  ```
123
105
 
124
- Sync every worktree in the repository:
106
+ To install automatically:
125
107
 
126
108
  ```sh
127
- gji sync --all
109
+ # zsh
110
+ echo 'eval "$(gji init zsh)"' >> ~/.zshrc
111
+
112
+ # bash
113
+ echo 'eval "$(gji init bash)"' >> ~/.bashrc
128
114
  ```
129
115
 
130
- Clean up stale linked worktrees interactively:
116
+ After a reinstall or upgrade, re-source to pick up changes:
131
117
 
132
118
  ```sh
133
- gji clean
119
+ eval "$(gji init zsh)"
134
120
  ```
135
121
 
136
- Finish a single worktree explicitly:
122
+ For scripts that need the raw path, use `--print`:
137
123
 
138
124
  ```sh
139
- gji remove feature/refactor-auth
140
- # or
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
- - `gji --version` prints the installed CLI version
149
- - `gji init [shell]` prints shell integration for `zsh`, `bash`, or `fish`
150
- - `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
151
- - `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
152
- - `gji go [branch] [--print]` jumps to an existing worktree when shell integration is installed, or prints the matching worktree path otherwise
153
- - `gji root [--print]` jumps to the main repository root when shell integration is installed, or prints it otherwise
154
- - `gji status [--json]` prints repository metadata, worktree health, and upstream divergence
155
- - `gji sync [--all]` fetches from the configured remote and rebases or fast-forwards worktrees onto the configured default branch
156
- - `gji ls [--json]` lists active worktrees in a table or JSON
157
- - `gji clean` interactively prunes one or more linked worktrees, including detached entries, while excluding the current worktree
158
- - `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
159
- - `gji config` reads or updates global defaults
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
- `gji` is usable without setup, but it supports defaults through:
147
+ No setup required. Optional config lives in:
164
148
 
165
- - global config at `~/.config/gji/config.json`
166
- - repo-local config at `.gji.json`
149
+ - `~/.config/gji/config.json` — global defaults
150
+ - `.gji.json` — repo-local overrides (takes precedence)
167
151
 
168
- Repo-local values override global defaults.
152
+ ### Available keys
169
153
 
170
- Supported keys:
171
-
172
- - `branchPrefix`
173
- - `syncRemote`
174
- - `syncDefaultBranch`
175
- - `hooks`
176
-
177
- Example:
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
- Behavior:
172
+ ### Config commands
188
173
 
189
- - if `syncRemote` is unset, `gji sync` defaults to `origin`
190
- - if `syncDefaultBranch` is unset, `gji sync` resolves the remote default branch from `HEAD`
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
- `hooks` runs shell commands at key points in the worktree lifecycle. Configure it in `.gji.json` or `~/.config/gji/config.json`:
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 keys:
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
- | `{{branch}}` | branch name, or empty string for detached worktrees |
219
- | `{{path}}` | absolute path to the worktree |
220
- | `{{repo}}` | repository directory name |
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
- **Environment variables** (available to the hook process):
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
- | Variable | Value |
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
- ```json
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 hooks
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
- `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.
248
233
 
249
234
  ```sh
250
- 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" }
251
253
  ```
252
254
 
253
- `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.
254
258
 
255
- - `repoRoot`
256
- - `currentRoot`
257
- - `worktrees`
259
+ ## Non-interactive / CI mode
258
260
 
259
- 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
+ ```
260
266
 
261
- - `branch`: branch name or `null` for detached worktrees
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
- - `gji` works from either the main repository root or any linked worktree
273
- - the current worktree is never offered as a `gji clean` removal candidate
274
- - `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`
275
274
 
276
275
  ## License
277
276
 
package/dist/clean.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import type { WorktreeEntry } from './repo.js';
2
2
  export interface CleanCommandOptions {
3
3
  cwd: string;
4
+ dryRun?: boolean;
4
5
  force?: boolean;
6
+ json?: boolean;
5
7
  stderr: (chunk: string) => void;
6
8
  stdout: (chunk: string) => void;
7
9
  }
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.stderr('No linked worktrees to clean\n');
14
+ emitError(options, 'No linked worktrees to clean');
14
15
  return 1;
15
16
  }
16
- const selections = await promptForWorktrees(cleanupCandidates);
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
- reportRemovedPaths(removedPaths, options.stderr);
49
- options.stderr(`Failed to remove worktree at ${worktree.path}: ${toMessage(forceError)}\n`);
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.stdout(`${repository.repoRoot}\n`);
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 <number>')
70
- .description('fetch a pull request ref and create a linked worktree')
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>;