@solaqua/gji 0.14.0 → 0.16.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.
Files changed (67) hide show
  1. package/README.md +63 -120
  2. package/dist/cli/commands/back.d.ts +1 -1
  3. package/dist/cli/commands/back.js +1 -0
  4. package/dist/cli/commands/clean.d.ts +2 -1
  5. package/dist/cli/commands/clean.js +66 -18
  6. package/dist/cli/commands/done.js +3 -2
  7. package/dist/cli/commands/go.js +18 -5
  8. package/dist/cli/commands/init.js +0 -14
  9. package/dist/cli/commands/new.d.ts +2 -1
  10. package/dist/cli/commands/new.js +71 -63
  11. package/dist/cli/commands/open.js +1 -1
  12. package/dist/cli/commands/pr-open.js +2 -2
  13. package/dist/cli/commands/pr.js +17 -6
  14. package/dist/cli/commands/run-hook.js +4 -0
  15. package/dist/cli/commands/undo.js +10 -74
  16. package/dist/cli/dependencies.d.ts +5 -1
  17. package/dist/cli/dependencies.js +7 -2
  18. package/dist/cli/program.js +6 -56
  19. package/dist/cli/runtime/headless.d.ts +3 -3
  20. package/dist/cli/runtime/headless.js +6 -4
  21. package/dist/domain/worktree/path-filter.d.ts +13 -0
  22. package/dist/domain/worktree/path-filter.js +72 -0
  23. package/dist/domain/worktree/policy.d.ts +1 -0
  24. package/dist/domain/worktree/policy.js +1 -1
  25. package/dist/gji-bundle.mjs +738 -969
  26. package/dist/infrastructure/filesystem/locked-file.d.ts +4 -0
  27. package/dist/infrastructure/filesystem/locked-file.js +53 -0
  28. package/dist/infrastructure/git/runner.d.ts +7 -0
  29. package/dist/infrastructure/git/runner.js +35 -1
  30. package/dist/infrastructure/persistence/history.js +12 -10
  31. package/dist/infrastructure/persistence/slots.js +24 -21
  32. package/dist/infrastructure/process/hooks.js +2 -0
  33. package/dist/infrastructure/repository/registry.js +11 -41
  34. package/dist/infrastructure/worktree/lifecycle.d.ts +25 -0
  35. package/dist/infrastructure/worktree/lifecycle.js +120 -19
  36. package/dist/ports/git.d.ts +1 -0
  37. package/dist/ports/hooks.d.ts +7 -0
  38. package/dist/presentation/shell/completion.js +14 -32
  39. package/dist/presentation/terminal/context-card.js +1 -1
  40. package/man/man1/gji-back.1 +1 -1
  41. package/man/man1/gji-clean.1 +4 -1
  42. package/man/man1/gji-completion.1 +1 -1
  43. package/man/man1/gji-config.1 +1 -1
  44. package/man/man1/gji-doctor.1 +1 -1
  45. package/man/man1/gji-done.1 +1 -1
  46. package/man/man1/gji-go.1 +1 -1
  47. package/man/man1/gji-history.1 +1 -1
  48. package/man/man1/gji-init.1 +1 -1
  49. package/man/man1/gji-ls.1 +1 -1
  50. package/man/man1/gji-new.1 +1 -1
  51. package/man/man1/gji-open.1 +1 -1
  52. package/man/man1/gji-pr.1 +1 -1
  53. package/man/man1/gji-root.1 +1 -1
  54. package/man/man1/gji-run-hook.1 +3 -3
  55. package/man/man1/gji-status.1 +1 -1
  56. package/man/man1/gji-sync-files.1 +1 -1
  57. package/man/man1/gji-sync.1 +1 -1
  58. package/man/man1/gji-task.1 +1 -1
  59. package/man/man1/gji-undo.1 +1 -1
  60. package/man/man1/gji.1 +2 -12
  61. package/package.json +3 -2
  62. package/dist/cli/commands/remove.d.ts +0 -21
  63. package/dist/cli/commands/remove.js +0 -189
  64. package/dist/cli/commands/warp.d.ts +0 -28
  65. package/dist/cli/commands/warp.js +0 -123
  66. package/man/man1/gji-remove.1 +0 -19
  67. package/man/man1/gji-warp.1 +0 -16
package/README.md CHANGED
@@ -1,157 +1,100 @@
1
- # gji — Switch Git contexts without stashing
1
+ <picture>
2
+ <source media="(prefers-color-scheme: dark)" srcset="website/static/img/logo-dark.svg" />
3
+ <img src="website/static/img/logo.svg" alt="gji branch mascot" width="72" height="68" />
4
+ </picture>
2
5
 
3
- > Start, review, and switch between Git tasks without disturbing the work already open.
6
+ # gji — Switch tasks. Keep your place.
4
7
 
5
- `gji` makes Git worktrees feel like normal shell navigation. Each task gets its
6
- own directory, dependencies, editor context, and terminal — so moving to a
7
- pull request or experiment does not require stashing, checking out, or
8
- reinstalling.
8
+ Open a new task or review a PR. Your current work stays right where you left it.
9
9
 
10
- ## Start your next task
10
+ `gji` is a Git worktree CLI for developers who keep more than one task open.
11
+ Each task gets a separate working directory and dependency installation, so
12
+ switching tasks doesn't mean packing up your changes first. Keep using your
13
+ usual Git commands, editor, and coding tools inside each worktree.
11
14
 
12
- Run `gji new` and let the interactive flow guide you into a fresh worktree:
15
+ [Get started](#get-started) · [Documentation](https://gji.solaqua.dev/docs/intro) · [Is gji a fit?](https://gji.solaqua.dev/docs/comparison)
13
16
 
14
- ```sh
15
- gji new
16
- ```
17
-
18
- Choose a task name and setup options, then continue working in the new
19
- directory. For repeatable workflows, you can still provide a branch directly:
20
-
21
- ```sh
22
- gji new feature/payment-refactor
23
- ```
24
-
25
- If you prefer to reuse the current worktree, create a branch in place and
26
- refresh the configured default branch first:
27
-
28
- ```sh
29
- gji new --branch-only feature/payment-refactor
30
- ```
31
-
32
- This creates the new branch from the latest configured remote default branch
33
- without creating another worktree or installing dependencies. The current
34
- worktree must be clean; use `--no-fetch` to create from the local default
35
- branch as-is.
36
-
37
- ## The problem
38
-
39
- You are halfway through a feature when someone asks you to review a pull
40
- request. Without worktrees, a quick review often becomes:
41
-
42
- ```text
43
- stash → checkout → reinstall → review → checkout back → stash pop
44
- ```
45
-
46
- With `gji`, the review gets its own workspace and your current task stays
47
- untouched:
17
+ <a href="https://gji.solaqua.dev/#workflow">
18
+ <picture>
19
+ <source media="(prefers-reduced-motion: reduce)" srcset=".github/assets/readme-workflow.png" />
20
+ <img src=".github/assets/readme-workflow.gif" alt="Start a new task, open a PR in a separate worktree, then return to feature/payments with the same two modified files." width="960" height="336" />
21
+ </picture>
22
+ </a>
48
23
 
49
- ```sh
50
- gji pr 1234
51
- ```
52
-
53
- <table>
54
- <tr>
55
- <td width="50%" valign="top">
56
- <strong>Before</strong><br />
57
- <img src=".github/assets/readme-before.gif" alt="Traditional branch switching with stash and reinstall steps" />
58
- </td>
59
- <td width="50%" valign="top">
60
- <strong>After</strong><br />
61
- <img src=".github/assets/readme-after.gif" alt="Creating an isolated pull request worktree with gji" />
62
- </td>
63
- </tr>
64
- </table>
65
-
66
- ## Remember four commands
67
-
68
- ```sh
69
- gji new # interactively start a task
70
- gji pr 1234 # open a pull request in isolation
71
- gji go main # jump to another worktree
72
- gji done feature/payment-refactor # finish and clean up a task
73
- ```
24
+ *Illustrated workflow. Shell integration is enabled; setup time is omitted.*
74
25
 
75
- With shell integration enabled, each command moves your shell into the
76
- selected worktree automatically.
26
+ ## Get started
77
27
 
78
- ## Install
28
+ You need **Node.js 18+**, Git with worktree support, and **zsh, bash, or fish**
29
+ for automatic directory changes.
79
30
 
80
31
  ```sh
81
32
  npm install -g @solaqua/gji
82
33
  gji init
83
34
  ```
84
35
 
85
- `gji init` interactively installs shell integration, completions, and an
86
- available editor. Restart your shell, or source the rc file shown by the
87
- wizard, then run:
36
+ Restart your shell, or load the shell configuration file shown by the wizard.
37
+ Then, inside a repository you already work in:
88
38
 
89
39
  ```sh
90
40
  gji doctor
41
+ gji new feature/try-gji
42
+ gji back
91
43
  ```
92
44
 
93
- Without shell integration, commands still print paths for scripts and other
94
- non-interactive use. See the [installation guide](https://gji.solaqua.dev/docs/installation)
95
- for explicit shell setup.
96
-
97
- ## A typical day
45
+ You just opened a separate worktree and returned to your previous one. Any
46
+ uncommitted files you left there stay there. New worktrees install dependencies
47
+ automatically when a supported lockfile is detected, so the first setup can take
48
+ time. Use `--no-install` to skip it for one task.
98
49
 
99
- ```sh
100
- # Start feature work
101
- gji new
102
-
103
- # Check what is active
104
- gji
50
+ [Installation and shell setup](https://gji.solaqua.dev/docs/installation) · [Complete the first-worktree walkthrough](https://gji.solaqua.dev/docs/quick-start)
105
51
 
106
- # Review a PR without changing the current task
107
- gji pr 1234
52
+ ## Keep several tasks open
108
53
 
109
- # Move between tasks
110
- gji go feature/payment-refactor
111
- gji back
54
+ For example, while working on `feature/payments`:
112
55
 
113
- # Keep worktrees current and remove finished work
114
- gji sync
115
- gji done feature/payment-refactor
56
+ ```sh
57
+ gji new feature/search # give another task its own directory
58
+ gji pr 1234 # open a real PR from your repository
59
+ gji go feature/payments # return to the changes you left open
116
60
  ```
117
61
 
118
- Worktrees use a predictable location by default:
62
+ Replace `1234` with a GitHub pull request in your repository. Each task stays in
63
+ its own worktree. Navigation moves your shell; it doesn't move changes between
64
+ branches or keep files synchronized.
119
65
 
120
- ```text
121
- ../worktrees/<repo>/<branch>
122
- ```
123
-
124
- That keeps editor bookmarks, scripts, and terminal navigation stable.
66
+ | When you want to… | Run |
67
+ | --- | --- |
68
+ | Start a task | `gji new` |
69
+ | Review a PR locally | `gji pr 1234` |
70
+ | Find an open task | `gji` or `gji go` |
71
+ | Return to the last worktree | `gji back` |
72
+ | Open the current worktree in an editor | `gji open` |
73
+ | Finish a linked worktree | `gji done` |
125
74
 
126
- ## More capabilities
75
+ With `gji go`, press **Tab** to browse all registered repositories. Worktrees
76
+ have predictable paths: `../worktrees/<repo>/<branch>`.
127
77
 
128
- Use the documentation when you need to go beyond the core workflow:
78
+ ## Make the next worktree ready for your project
129
79
 
130
- - [Daily workflow](https://gji.solaqua.dev/docs/daily-workflow) — navigation, PRs, sync, and cleanup
131
- - [Commands](https://gji.solaqua.dev/docs/commands) — complete command reference
132
- - [Shell integration](https://gji.solaqua.dev/docs/shell-integration) — zsh, bash, and fish setup
133
- - [Configuration](https://gji.solaqua.dev/docs/configuration) — paths, defaults, and local files
134
- - [Hooks](https://gji.solaqua.dev/docs/hooks) — automate setup and cleanup
135
- - [Sync and cleanup](https://gji.solaqua.dev/docs/sync-and-cleanup) — maintain active worktrees
136
- - [Comparison](https://gji.solaqua.dev/docs/comparison) — `gji` vs `git worktree`, `lazygit`, and `ghq`
137
- - [FAQ and troubleshooting](https://gji.solaqua.dev/docs/faq) — common questions and fixes
80
+ - [Local files and defaults](https://gji.solaqua.dev/docs/configuration) — copy `.env.local`, choose an editor, or adjust dependency setup.
81
+ - [Hooks](https://gji.solaqua.dev/docs/hooks) — run project-specific setup after the built-in dependency install.
82
+ - [Daily workflow](https://gji.solaqua.dev/docs/daily-workflow) — review, experiment, navigate, and open editors.
83
+ - [Sync and cleanup](https://gji.solaqua.dev/docs/sync-and-cleanup) — rebase active worktrees and remove finished ones. Cleanup can delete branches; undo does not recover uncommitted files.
84
+ - [Command reference](https://gji.solaqua.dev/docs/commands) — options, JSON output, and scripting.
85
+ - [Troubleshooting](https://gji.solaqua.dev/docs/troubleshooting) — recover from setup, navigation, or install problems.
138
86
 
139
- `gji` also supports JSON output for scripts and AI-assisted workflows:
87
+ ## Contributing
140
88
 
141
89
  ```sh
142
- gji --json
143
- gji new --json feature/dark-mode
144
- gji go --json feature/dark-mode
90
+ pnpm install
91
+ pnpm test
92
+ pnpm build
145
93
  ```
146
94
 
147
- Read the [full documentation](https://gji.solaqua.dev) for configuration,
148
- dependency bootstrap, hooks, automation, and the complete CLI reference.
149
-
150
- ---
151
-
152
- If `gji` has saved you from a `git stash` spiral, a ⭐ on
153
- [GitHub](https://github.com/sjquant/gji) helps other developers find it.
95
+ The CLI source is in `src/`. See [website/README.md](website/README.md) for site
96
+ and documentation development.
154
97
 
155
98
  ## License
156
99
 
157
- MIT
100
+ [MIT](LICENSE)
@@ -9,7 +9,7 @@ export interface BackCommandOptions {
9
9
  commandName?: string;
10
10
  outputEnv?: string;
11
11
  print?: boolean;
12
- runtime?: CliRuntime<"config" | "historyStore" | "hooks" | "repositoryContext">;
12
+ runtime?: CliRuntime<"config" | "historyStore" | "hooks" | "repositoryContext" | "slots">;
13
13
  stderr: (chunk: string) => void;
14
14
  stdout: (chunk: string) => void;
15
15
  }
@@ -59,6 +59,7 @@ export async function runBackCommand(options) {
59
59
  branch: target.branch ?? undefined,
60
60
  path: target.path,
61
61
  repo: basename(repository.repoRoot),
62
+ slot: await runtime.slots.getWorktreeSlot(target.path),
62
63
  }, options.stderr);
63
64
  }
64
65
  catch {
@@ -7,8 +7,9 @@ export interface CleanCommandOptions {
7
7
  dryRun?: boolean;
8
8
  force?: boolean;
9
9
  json?: boolean;
10
+ paths?: string[];
10
11
  stale?: boolean;
11
- runtime?: CliRuntime<"git" | "config" | "configStore" | "slots" | "repositoryContext" | "repositoryRegistry" | "worktrees" | "worktreeInfo" | "worktreeLifecycle" | "worktreeCatalog">;
12
+ runtime?: CliRuntime<"git" | "config" | "configStore" | "hooks" | "slots" | "repositoryContext" | "repositoryRegistry" | "worktrees" | "worktreeInfo" | "worktreeLifecycle" | "worktreeCatalog">;
12
13
  stderr: (chunk: string) => void;
13
14
  stdout: (chunk: string) => void;
14
15
  }
@@ -1,7 +1,11 @@
1
+ import { realpath } from "node:fs/promises";
2
+ import { basename, dirname, join, resolve } from "node:path";
1
3
  import { confirm, isCancel } from "@clack/prompts";
2
4
  import { loadLinkedWorktrees, } from "../../application/worktree/catalog.js";
3
5
  import { deduplicateWorktreeSources, listRegisteredWorktreeSources, } from "../../application/worktree/sources.js";
4
6
  import { mapWithConcurrency } from "../../domain/shared/concurrency.js";
7
+ import { createWorktreePathFilter, escapePathPattern, findGlobIndex, unescapePathPattern, } from "../../domain/worktree/path-filter.js";
8
+ import { expandTildeInPath } from "../../domain/worktree/policy.js";
5
9
  import { buildWorktreePromptEntries, promptForMultipleWorktrees, } from "../../presentation/worktree/picker.js";
6
10
  import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "../../presentation/worktree/prompts.js";
7
11
  import { defaultCliDependencies, } from "../dependencies.js";
@@ -26,7 +30,8 @@ export function createCleanCommand(dependencies = {}) {
26
30
  const runtime = options.runtime ?? defaultCliDependencies;
27
31
  const { readWorktreeHealth, isBranchMergedInto, resolveRemoteBase, runGit, } = runtime.git;
28
32
  const { loadEffectiveConfig } = runtime.config;
29
- const { releaseWorktreeSlot } = runtime.slots;
33
+ const { getWorktreeSlot, releaseWorktreeSlot } = runtime.slots;
34
+ const { extractHooks, runHook } = runtime.hooks;
30
35
  const sourceDependencies = {
31
36
  ...runtime.repositoryContext,
32
37
  ...runtime.repositoryRegistry,
@@ -35,8 +40,27 @@ export function createCleanCommand(dependencies = {}) {
35
40
  const { formatLastCommit, formatUpstreamState, readWorktreeInfos, serializeWorktreeInfo, } = runtime.worktreeInfo;
36
41
  const { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isSubmoduleWorktreeRemovalError, isWorktreeDeletionError, isWorktreeForceRemovalError, removeWorktree, } = runtime.worktreeLifecycle;
37
42
  const { linkedWorktrees, repository } = await loadLinkedWorktrees(options.cwd, sourceDependencies);
43
+ const pathPatterns = await canonicalizePathPatterns(options.cwd, options.paths ?? []);
44
+ const matchesPathFilter = createWorktreePathFilter(pathPatterns);
45
+ // An explicit path may point at another registered repository, so
46
+ // --path starts in the all-repositories scope.
47
+ const searchAllRepositories = pathPatterns.length > 0;
48
+ let allSourceCount = 0;
49
+ const emitNoCandidates = (sourceCount) => {
50
+ if (pathPatterns.length > 0 && sourceCount === 0) {
51
+ emitError(options, "No linked worktrees match --path");
52
+ return 1;
53
+ }
54
+ if (options.stale) {
55
+ emitNoStaleCandidates(options);
56
+ return 0;
57
+ }
58
+ emitError(options, "No linked worktrees to clean");
59
+ return 1;
60
+ };
38
61
  const currentSources = linkedWorktrees
39
- .filter((worktree) => worktree.path !== repository.currentRoot)
62
+ .filter((worktree) => worktree.path !== repository.currentRoot &&
63
+ matchesPathFilter(worktree.path))
40
64
  .map((worktree) => ({
41
65
  repoName: repository.repoName,
42
66
  repoRoot: repository.repoRoot,
@@ -77,7 +101,9 @@ export function createCleanCommand(dependencies = {}) {
77
101
  ...registered.sources,
78
102
  ]).filter((source) => source.repoRoot !== undefined &&
79
103
  source.worktree.path !== source.repoRoot &&
80
- source.worktree.path !== repository.currentRoot);
104
+ source.worktree.path !== repository.currentRoot &&
105
+ matchesPathFilter(source.worktree.path));
106
+ allSourceCount = sources.length;
81
107
  const otherRepositorySources = sources.filter((source) => source.repoRoot !== repository.repoRoot);
82
108
  const otherRepositoryCandidates = await resolveCleanupCandidates({
83
109
  sources: otherRepositorySources,
@@ -110,28 +136,19 @@ export function createCleanCommand(dependencies = {}) {
110
136
  };
111
137
  },
112
138
  };
113
- if (currentCandidates.length === 0 && interactiveSelection) {
139
+ if (searchAllRepositories ||
140
+ (interactiveSelection && currentCandidates.length === 0)) {
114
141
  const loadedAllCandidates = await loadAllCandidates();
115
142
  if (loadedAllCandidates.length === 0) {
116
- if (options.stale) {
117
- emitNoStaleCandidates(options);
118
- return 0;
119
- }
120
- emitError(options, "No linked worktrees to clean");
121
- return 1;
143
+ return emitNoCandidates(allSourceCount);
122
144
  }
123
145
  currentScope = false;
124
146
  activeCandidates = loadedAllCandidates;
125
147
  scope.label = "all repositories";
126
148
  scope.toggleLabel = "current repository";
127
149
  }
128
- if (currentCandidates.length === 0 && !interactiveSelection) {
129
- if (options.stale) {
130
- emitNoStaleCandidates(options);
131
- return 0;
132
- }
133
- emitError(options, "No linked worktrees to clean");
134
- return 1;
150
+ else if (currentCandidates.length === 0) {
151
+ return emitNoCandidates(currentSources.length);
135
152
  }
136
153
  if (!options.dryRun && !options.force && (options.json || isHeadless())) {
137
154
  const message = "--force is required";
@@ -139,7 +156,8 @@ export function createCleanCommand(dependencies = {}) {
139
156
  emitError(options, message);
140
157
  }
141
158
  else {
142
- options.stderr(`gji clean: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
159
+ options.stderr(`gji clean: ${message} in non-interactive mode\n`);
160
+ options.stderr("Hint: --force removes every candidate; use --dry-run to preview them first\n");
143
161
  }
144
162
  return 1;
145
163
  }
@@ -209,6 +227,13 @@ export function createCleanCommand(dependencies = {}) {
209
227
  options.stderr(`Skipped ${worktree.path}: no longer a safe stale cleanup candidate\n`);
210
228
  continue;
211
229
  }
230
+ const hooks = extractHooks(await loadEffectiveConfig(repoRoot));
231
+ await runHook(hooks["before-remove"], worktree.path, {
232
+ branch: worktree.branch ?? undefined,
233
+ path: worktree.path,
234
+ repo: basename(repoRoot),
235
+ slot: await getWorktreeSlot(worktree.path),
236
+ }, options.stderr, options.json ? () => undefined : options.stdout);
212
237
  try {
213
238
  await removeWorktree(repoRoot, worktree.path);
214
239
  }
@@ -314,6 +339,29 @@ export function createCleanCommand(dependencies = {}) {
314
339
  };
315
340
  }
316
341
  export const runCleanCommand = createCleanCommand();
342
+ async function canonicalizePathPatterns(cwd, patterns) {
343
+ return Promise.all(patterns.map((pattern) => canonicalizeLiteralPrefix(resolve(cwd, expandTildeInPath(pattern)))));
344
+ }
345
+ // Git records canonical worktree paths, so resolve symlinks in the pattern's
346
+ // longest existing literal prefix before matching.
347
+ async function canonicalizeLiteralPrefix(pattern) {
348
+ const globIndex = findGlobIndex(pattern);
349
+ let prefix = globIndex === -1 ? pattern : dirname(`${pattern.slice(0, globIndex)}x`);
350
+ let suffix = pattern.slice(prefix.length);
351
+ for (;;) {
352
+ try {
353
+ const canonicalPrefix = escapePathPattern(await realpath(unescapePathPattern(prefix)));
354
+ return `${canonicalPrefix}${suffix}`.replace(/^\/\//, "/");
355
+ }
356
+ catch {
357
+ const parent = dirname(prefix);
358
+ if (parent === prefix)
359
+ return pattern;
360
+ suffix = join("/", basename(prefix), suffix);
361
+ prefix = parent;
362
+ }
363
+ }
364
+ }
317
365
  async function resolveCleanupCandidates(options, dependencies) {
318
366
  const { sources, stale, stderr, signal } = options;
319
367
  const grouped = new Map();
@@ -10,7 +10,7 @@ export async function runDoneCommand(options) {
10
10
  const { readWorktreeHealth, isBranchMergedInto, resolveRemoteDefaultBranch, runGit, } = runtime.git;
11
11
  const { loadEffectiveConfig, resolveConfigString } = runtime.config;
12
12
  const { loadHistory } = runtime.historyStore;
13
- const { releaseWorktreeSlot } = runtime.slots;
13
+ const { getWorktreeSlot, releaseWorktreeSlot } = runtime.slots;
14
14
  const { extractHooks, runHook } = runtime.hooks;
15
15
  const { detectRepository } = runtime.repositoryContext;
16
16
  const { listWorktrees } = runtime.worktrees;
@@ -69,7 +69,8 @@ export async function runDoneCommand(options) {
69
69
  branch: target.branch ?? undefined,
70
70
  path: target.path,
71
71
  repo: basename(repository.repoRoot),
72
- }, options.stderr);
72
+ slot: await getWorktreeSlot(target.path),
73
+ }, options.stderr, options.json ? () => undefined : options.stdout);
73
74
  try {
74
75
  await removeWorktree(repository.repoRoot, target.path);
75
76
  }
@@ -10,7 +10,7 @@ import { buildWorktreePromptEntries, promptForSingleWorktree, } from "../../pres
10
10
  import { defaultCliDependencies, withPullRequestQueries, } from "../dependencies.js";
11
11
  import { isHeadless } from "../runtime/headless.js";
12
12
  import { runBackCommand } from "./back.js";
13
- import { runNewCommand } from "./new.js";
13
+ import { applyConfiguredBranchPrefix, runNewCommand } from "./new.js";
14
14
  import { runPrCommand } from "./pr.js";
15
15
  const GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
16
16
  export function createGoCommand(dependencies = {}) {
@@ -42,7 +42,7 @@ export function createGoCommand(dependencies = {}) {
42
42
  : [];
43
43
  if (!options.branch) {
44
44
  if (options.json || isHeadless()) {
45
- return emitError(options, "branch argument is required in non-interactive mode (GJI_NO_TUI=1)");
45
+ return emitError(options, "branch argument is required in non-interactive mode");
46
46
  }
47
47
  let skippedRegisteredRepos = 0;
48
48
  let registeredSources = null;
@@ -150,8 +150,8 @@ async function handleGoBranchResolution(options, resolution, confirmBranchCreati
150
150
  case "create":
151
151
  return createExistingBranchWorktree(options, confirmBranchCreation, resolution.repository, resolution.branch, resolution.mode, resolution.remote, runtime);
152
152
  case "pull-request":
153
- if (options.json || isHeadless() || options.print) {
154
- return emitError(options, "PR navigation creates a worktree; use `gji pr <ref>` in an interactive shell");
153
+ if (options.json || options.print) {
154
+ return emitError(options, "PR navigation creates a worktree; use `gji pr <ref>`");
155
155
  }
156
156
  return runPrCommand({
157
157
  cwd: resolution.repository.repoRoot,
@@ -197,7 +197,8 @@ function deduplicateSources(sources) {
197
197
  }
198
198
  async function createExistingBranchWorktree(options, confirmBranchCreation, repository, branch, mode, remote, runtime) {
199
199
  if (options.json || isHeadless() || options.print) {
200
- return emitError(options, `branch "${branch}" exists but has no worktree; use interactive gji go ${branch} to create one`);
200
+ const command = await resolveNonInteractiveCreateCommand(repository, branch, mode, remote, runtime ?? defaultCliDependencies);
201
+ return emitError(options, `branch "${branch}" exists but has no worktree; run ${command} to create one`);
201
202
  }
202
203
  if (!(await confirmBranchCreation(branch))) {
203
204
  options.stderr("Aborted\n");
@@ -214,6 +215,18 @@ async function createExistingBranchWorktree(options, confirmBranchCreation, repo
214
215
  runtime,
215
216
  });
216
217
  }
218
+ async function resolveNonInteractiveCreateCommand(repository, branch, mode, remote, runtime) {
219
+ if (mode === "track") {
220
+ return `git worktree add --track -b ${branch} <path> ${remote ?? "origin"}/${branch}`;
221
+ }
222
+ // gji new applies branchPrefix, so it would create a different branch.
223
+ const config = await runtime.config
224
+ .loadEffectiveConfig(repository.repoRoot)
225
+ .catch(() => null);
226
+ return applyConfiguredBranchPrefix(branch, config?.branchPrefix) === branch
227
+ ? `gji new ${branch}`
228
+ : `git worktree add <path> ${branch}`;
229
+ }
217
230
  async function navigateToExistingWorktree(options, path, worktree, runtime) {
218
231
  const repository = await runtime.repositoryContext.detectRepository(path);
219
232
  if (options.json) {
@@ -45,13 +45,6 @@ const SHELL_WRAPPED_COMMANDS = [
45
45
  names: ["root"],
46
46
  tempPrefix: "gji-root",
47
47
  },
48
- {
49
- bypassOptions: ["--help", "-h", "--json"],
50
- commandName: "remove",
51
- envVar: "GJI_REMOVE_OUTPUT_FILE",
52
- names: ["remove", "rm"],
53
- tempPrefix: "gji-remove",
54
- },
55
48
  {
56
49
  bypassOptions: ["--help", "-h", "--json"],
57
50
  commandName: "done",
@@ -59,13 +52,6 @@ const SHELL_WRAPPED_COMMANDS = [
59
52
  names: ["done"],
60
53
  tempPrefix: "gji-done",
61
54
  },
62
- {
63
- bypassOptions: ["--print", "--json", "--help", "-h"],
64
- commandName: "warp",
65
- envVar: "GJI_WARP_OUTPUT_FILE",
66
- names: ["warp"],
67
- tempPrefix: "gji-warp",
68
- },
69
55
  ];
70
56
  export async function runInitCommand(options) {
71
57
  const runtime = options.runtime ?? defaultCliDependencies;
@@ -26,7 +26,7 @@ export interface NewCommandOptions {
26
26
  task?: string;
27
27
  runtime?: NewRuntime;
28
28
  }
29
- type NewRuntime = CliRuntime<"bootstrap" | "config" | "git" | "history" | "integrations" | "repositoryContext" | "tasks">;
29
+ type NewRuntime = CliRuntime<"bootstrap" | "config" | "git" | "history" | "hooks" | "integrations" | "repositoryContext" | "slots" | "tasks">;
30
30
  export interface NewCommandDependencies {
31
31
  createBranchPlaceholder: () => string;
32
32
  promptForBranch: (placeholder: string) => Promise<string | null>;
@@ -38,3 +38,4 @@ export interface NewCommandDependencies {
38
38
  export declare function createNewCommand(dependencies?: Partial<NewCommandDependencies>): (options: NewCommandOptions) => Promise<number>;
39
39
  export declare const runNewCommand: (options: NewCommandOptions) => Promise<number>;
40
40
  export declare function generateBranchPlaceholder(random?: () => number): string;
41
+ export declare function applyConfiguredBranchPrefix(branch: string, branchPrefix: unknown): string;