@solaqua/gji 0.8.1 → 0.10.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 (51) hide show
  1. package/README.md +29 -33
  2. package/dist/browser.d.ts +7 -0
  3. package/dist/browser.js +37 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +63 -4
  6. package/dist/doctor.d.ts +28 -0
  7. package/dist/doctor.js +454 -0
  8. package/dist/gji-bundle.mjs +2507 -359
  9. package/dist/go.d.ts +2 -1
  10. package/dist/go.js +4 -1
  11. package/dist/index.js +12 -6
  12. package/dist/init.d.ts +15 -0
  13. package/dist/init.js +232 -16
  14. package/dist/new.d.ts +1 -0
  15. package/dist/new.js +24 -1
  16. package/dist/open.d.ts +3 -1
  17. package/dist/open.js +16 -3
  18. package/dist/pr-open.d.ts +19 -0
  19. package/dist/pr-open.js +290 -0
  20. package/dist/pull-requests.d.ts +33 -0
  21. package/dist/pull-requests.js +384 -0
  22. package/dist/repo-registry.d.ts +5 -0
  23. package/dist/repo-registry.js +87 -14
  24. package/dist/shell-completion.js +284 -14
  25. package/dist/shell-setup.d.ts +9 -0
  26. package/dist/shell-setup.js +56 -0
  27. package/dist/warp.d.ts +3 -0
  28. package/dist/warp.js +8 -3
  29. package/dist/worktree-picker.d.ts +12 -1
  30. package/dist/worktree-picker.js +84 -6
  31. package/man/man1/gji-back.1 +1 -1
  32. package/man/man1/gji-clean.1 +1 -1
  33. package/man/man1/gji-completion.1 +1 -1
  34. package/man/man1/gji-config.1 +1 -1
  35. package/man/man1/gji-doctor.1 +19 -0
  36. package/man/man1/gji-go.1 +1 -1
  37. package/man/man1/gji-history.1 +1 -1
  38. package/man/man1/gji-init.1 +6 -3
  39. package/man/man1/gji-ls.1 +1 -1
  40. package/man/man1/gji-new.1 +4 -1
  41. package/man/man1/gji-open.1 +4 -1
  42. package/man/man1/gji-pr.1 +5 -1
  43. package/man/man1/gji-remove.1 +1 -1
  44. package/man/man1/gji-root.1 +1 -1
  45. package/man/man1/gji-run-hook.1 +1 -1
  46. package/man/man1/gji-status.1 +1 -1
  47. package/man/man1/gji-sync-files.1 +1 -1
  48. package/man/man1/gji-sync.1 +1 -1
  49. package/man/man1/gji-warp.1 +1 -1
  50. package/man/man1/gji.1 +6 -2
  51. package/package.json +1 -1
package/README.md CHANGED
@@ -9,6 +9,7 @@ That matters even more in AI-assisted workflows, where one repository often has
9
9
  ```sh
10
10
  gji new feature/payment-refactor # new branch + worktree, cd in
11
11
  gji pr 1234 # review PR in isolation, cd in
12
+ gji pr open # open the PR for the current worktree
12
13
  gji go main # jump back, shell changes directory
13
14
  gji remove feature/payment-refactor
14
15
  ```
@@ -69,36 +70,18 @@ That makes Git worktrees more important, because a single shared checkout become
69
70
  npm install -g @solaqua/gji
70
71
  ```
71
72
 
72
- Then add shell integration so `gji go`, `gji new`, and `gji remove` can change your directory:
73
+ Then run the guided setup in an interactive terminal:
73
74
 
74
75
  ```sh
75
- # zsh
76
- echo 'eval "$(gji init zsh)"' >> ~/.zshrc
77
-
78
- # bash
79
- echo 'eval "$(gji init bash)"' >> ~/.bashrc
80
-
81
- # fish
82
- gji init fish --write
83
- source ~/.config/fish/config.fish
76
+ gji init
77
+ # restart your shell, or source the rc file shown by the wizard
78
+ gji doctor
84
79
  ```
85
80
 
86
- Install completions as separate files:
81
+ The wizard installs shell integration, completions, and an available editor. In a non-interactive environment, use the existing explicit shell command instead:
87
82
 
88
83
  ```sh
89
- # zsh
90
- mkdir -p ~/.zsh/completions
91
- gji completion zsh > ~/.zsh/completions/_gji
92
- # add this before running compinit in ~/.zshrc
93
- fpath=(~/.zsh/completions $fpath)
94
-
95
- # bash
96
- mkdir -p ~/.local/share/bash-completion/completions
97
- gji completion bash > ~/.local/share/bash-completion/completions/gji
98
-
99
- # fish
100
- mkdir -p ~/.config/fish/completions
101
- gji completion fish > ~/.config/fish/completions/gji.fish
84
+ gji init zsh --write
102
85
  ```
103
86
 
104
87
  ## Quick start
@@ -112,6 +95,10 @@ gji new feature/dark-mode --open --editor cursor
112
95
 
113
96
  # review a pull request
114
97
  gji pr 1234
98
+ gji pr open # open the PR for the current worktree
99
+ gji pr open --select # choose a PR from any linked worktree
100
+ gji pr open feature/auth-refactor # open the PR for a branch
101
+ gji pr open '#1234' # open an open PR directly
115
102
 
116
103
  # see what's open
117
104
  gji status
@@ -120,8 +107,9 @@ gji status
120
107
  gji go feature/dark-mode
121
108
  gji go main
122
109
 
123
- # open any worktree in an editor (interactive picker)
110
+ # open the current worktree in an editor
124
111
  gji open
112
+ gji open --select # choose another worktree interactively
125
113
  gji open feature/dark-mode --editor code
126
114
 
127
115
  # clean up when done
@@ -140,6 +128,7 @@ Set `worktreePath` in your config to use a different base (e.g. `"~/worktrees"`
140
128
 
141
129
  ```sh
142
130
  gji new feature/auth-refactor # new branch + worktree
131
+ gji new feature/auth-tests --from-current # branch from the current worktree
143
132
  gji new --detached # scratch space, auto-named
144
133
 
145
134
  gji pr 1234 # checkout PR locally
@@ -178,13 +167,14 @@ See the full comparison in [website/docs/comparison.mdx](./website/docs/comparis
178
167
 
179
168
  ## Shell setup
180
169
 
181
- 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:
170
+ 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, completions, and an editor once with:
182
171
 
183
172
  ```sh
184
- gji init zsh # prints the shell function, review it if you like
173
+ gji init
174
+ gji doctor
185
175
  ```
186
176
 
187
- Install the wrapper once:
177
+ `gji init` is interactive. For dotfiles or CI, the explicit shell commands remain available and preserve their existing output:
188
178
 
189
179
  ```sh
190
180
  # zsh
@@ -241,9 +231,10 @@ path=$(gji root --print)
241
231
 
242
232
  | Command | Description |
243
233
  |---|---|
244
- | `gji new [branch] [--detached] [--open] [--editor <cli>] [--json]` | create branch + worktree, cd in (validates branch name against Git rules) |
234
+ | `gji new [branch] [--from-current] [--detached] [--open] [--editor <cli>] [--json]` | create branch + worktree, optionally based on the current worktree |
245
235
  | `gji pr <ref> [--json]` | fetch PR ref, create worktree, cd in |
246
- | `gji open [branch] [--editor <cli>] [--save] [--workspace]` | open a worktree in an editor |
236
+ | `gji pr open [branch|#N] [--select]` | open the current worktree PR, or choose a linked worktree with `--select` |
237
+ | `gji open [branch] [--select] [--editor <cli>] [--save] [--workspace]` | open the current or selected worktree in an editor |
247
238
  | `gji go [branch] [--print]` | jump to a worktree |
248
239
  | `gji root [--print]` | jump to the main repo root |
249
240
  | `gji status [--json]` | repo overview, worktree health, ahead/behind |
@@ -254,7 +245,8 @@ path=$(gji root --print)
254
245
  | `gji remove [branch] [--force] [--json]` | remove a worktree and its branch |
255
246
  | `gji trigger-hook <hook>` | run a hook in the current worktree |
256
247
  | `gji config [get\|set\|unset] [key] [value]` | manage global defaults |
257
- | `gji init [shell]` | print or install shell integration |
248
+ | `gji init [shell]` | interactively set up onboarding, or print/install a shell wrapper |
249
+ | `gji doctor [--json] [--fix] [--yes]` | check installation and configuration health; optionally remove stale repository entries |
258
250
  | `gji completion [shell]` | print shell completion definitions |
259
251
 
260
252
  ## Configuration
@@ -275,7 +267,7 @@ No setup required. Optional config lives in:
275
267
  | `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
276
268
  | `syncFiles` | files to copy from main worktree into each new worktree; use global per-repo config for private files |
277
269
  | `skipInstallPrompt` | `true` to disable the auto-install prompt permanently |
278
- | `installSaveTarget` | `"local"` or `"global"` — where **Always**/**Never** choices are persisted (default: `"local"`); set once during `gji init --write` |
270
+ | `installSaveTarget` | `"local"` or `"global"` — where **Always**/**Never** choices are persisted (default: `"local"`); set during `gji init <shell> --write` |
279
271
  | `hooks` | lifecycle scripts (see [Hooks](#hooks)) |
280
272
  | `repos` | per-repo overrides inside the global config (see below) |
281
273
 
@@ -477,7 +469,10 @@ GJI_NO_TUI=1 gji remove --force feature/ci-branch
477
469
  GJI_NO_TUI=1 gji clean --force
478
470
  ```
479
471
 
480
- `GJI_NO_TUI=1` disables all prompts. Commands that need confirmation require `--force`. `--json` implies the same behaviour.
472
+ `GJI_NO_TUI=1` disables all prompts. Commands that need confirmation require their non-interactive approval flag (`--force` for cleanup commands, `--yes` for `doctor --fix`). `--json` implies the same behaviour.
473
+
474
+ `gji pr open --select` requires an interactive terminal; plain `gji pr open` opens the PR for the current worktree without prompting.
475
+ If the current worktree has no open PR or has multiple open PRs in headless mode, pass `gji pr open <branch|#N>` explicitly.
481
476
 
482
477
  Update notifications are also suppressed automatically in non-interactive and `--json` runs. Users can opt out explicitly with `NO_UPDATE_NOTIFIER=1` or `--no-update-notifier`.
483
478
 
@@ -486,6 +481,7 @@ Update notifications are also suppressed automatically in non-interactive and `-
486
481
  - Works from either the main repo root or inside any linked worktree
487
482
  - The current worktree is never offered as a `gji clean` candidate
488
483
  - `gji pr` fetches from `origin` using the first matching forge ref namespace: GitHub `refs/pull/<number>/head`, GitLab `refs/merge-requests/<number>/head`, then Bitbucket `refs/pull-requests/<number>/from`
484
+ - `gji pr open` reads open PRs from the `origin` forge (GitHub, GitLab, or Bitbucket), preferring an installed authenticated provider CLI and falling back to its public API
489
485
 
490
486
  ## License
491
487
 
@@ -0,0 +1,7 @@
1
+ export type BrowserCommandRunner = (command: string, args: string[]) => Promise<void>;
2
+ export interface BrowserOpenerDependencies {
3
+ platform?: NodeJS.Platform;
4
+ runCommand?: BrowserCommandRunner;
5
+ }
6
+ export declare function createBrowserOpener(dependencies?: BrowserOpenerDependencies): (url: string) => Promise<void>;
7
+ export declare const openBrowser: (url: string) => Promise<void>;
@@ -0,0 +1,37 @@
1
+ import { execFile } from "node:child_process";
2
+ import { platform } from "node:process";
3
+ import { promisify } from "node:util";
4
+ const execFileAsync = promisify(execFile);
5
+ export function createBrowserOpener(dependencies = {}) {
6
+ const currentPlatform = dependencies.platform ?? platform;
7
+ const runCommand = dependencies.runCommand ?? defaultRunCommand;
8
+ return async function openBrowser(url) {
9
+ validateBrowserUrl(url);
10
+ const command = browserCommand(currentPlatform, url);
11
+ await runCommand(command.command, command.args);
12
+ };
13
+ }
14
+ export const openBrowser = createBrowserOpener();
15
+ function browserCommand(currentPlatform, url) {
16
+ if (currentPlatform === "darwin")
17
+ return { args: [url], command: "open" };
18
+ if (currentPlatform === "win32") {
19
+ return { args: [url], command: "explorer.exe" };
20
+ }
21
+ return { args: [url], command: "xdg-open" };
22
+ }
23
+ function validateBrowserUrl(url) {
24
+ let parsed;
25
+ try {
26
+ parsed = new URL(url);
27
+ }
28
+ catch {
29
+ throw new Error("browser URL is invalid");
30
+ }
31
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
32
+ throw new Error("browser URL must use http or https");
33
+ }
34
+ }
35
+ async function defaultRunCommand(command, args) {
36
+ await execFileAsync(command, args, { timeout: 10_000 });
37
+ }
package/dist/cli.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Command } from "commander";
2
+ import { type PrOpenCommandDependencies } from "./pr-open.js";
2
3
  export interface RunCliOptions {
3
4
  cwd?: string;
5
+ prOpenDependencies?: Partial<PrOpenCommandDependencies>;
4
6
  stderr?: (chunk: string) => void;
5
7
  stdout?: (chunk: string) => void;
6
8
  }
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { runBackCommand } from "./back.js";
5
5
  import { runCleanCommand } from "./clean.js";
6
6
  import { runCompletionCommand } from "./completion.js";
7
7
  import { runConfigCommand } from "./config-command.js";
8
+ import { runDoctorCommand } from "./doctor.js";
8
9
  import { runGoCommand } from "./go.js";
9
10
  import { isHeadless } from "./headless.js";
10
11
  import { runHistoryCommand } from "./history-command.js";
@@ -13,6 +14,7 @@ import { runLsCommand } from "./ls.js";
13
14
  import { runNewCommand } from "./new.js";
14
15
  import { runOpenCommand } from "./open.js";
15
16
  import { runPrCommand } from "./pr.js";
17
+ import { createPrOpenCommand, } from "./pr-open.js";
16
18
  import { runRemoveCommand } from "./remove.js";
17
19
  import { detectRepository } from "./repo.js";
18
20
  import { registerRepo } from "./repo-registry.js";
@@ -44,7 +46,9 @@ function readPackageMetadata() {
44
46
  }
45
47
  export async function runCli(argv, options = {}) {
46
48
  await maybeNotifyForUpdates(argv);
47
- maybeRegisterCurrentRepo(options.cwd ?? process.cwd());
49
+ if (argv[0] !== "doctor") {
50
+ maybeRegisterCurrentRepo(options.cwd ?? process.cwd());
51
+ }
48
52
  const program = createProgram();
49
53
  const cwd = options.cwd ?? process.cwd();
50
54
  const stdout = options.stdout ?? (() => undefined);
@@ -59,7 +63,12 @@ export async function runCli(argv, options = {}) {
59
63
  return { exitCode: 0 };
60
64
  }
61
65
  try {
62
- attachCommandActions(program, { cwd, stderr, stdout });
66
+ attachCommandActions(program, {
67
+ cwd,
68
+ prOpenDependencies: options.prOpenDependencies,
69
+ stderr,
70
+ stdout,
71
+ });
63
72
  await program.parseAsync(["node", "gji", ...argv], { from: "node" });
64
73
  return { exitCode: 0 };
65
74
  }
@@ -111,6 +120,7 @@ function registerCommands(program) {
111
120
  .description("create a new branch or detached linked worktree")
112
121
  .option("-f, --force", "remove and recreate the worktree if the target path already exists")
113
122
  .option("--detached", "create a detached worktree without a branch")
123
+ .option("--from-current", "base the new branch on the current worktree instead of the main worktree (cannot be combined with --detached)")
114
124
  .option("--open", "open the new worktree in an editor after creation")
115
125
  .option("--editor <cli>", "editor CLI to use with --open (code, cursor, zed, …)")
116
126
  .option("--dry-run", "show what would be created without executing any git commands or writing files")
@@ -118,19 +128,33 @@ function registerCommands(program) {
118
128
  .action(notImplemented("new"));
119
129
  program
120
130
  .command("init [shell]")
121
- .description("print or install shell integration")
131
+ .description("set up shell integration interactively or print a shell wrapper")
122
132
  .option("--write", "write the integration to the shell config file")
133
+ .option("--json", "emit a JSON error in non-interactive mode")
123
134
  .action(notImplemented("init"));
135
+ program
136
+ .command("doctor")
137
+ .description("check gji installation and configuration health")
138
+ .option("--fix", "apply safe automatic fixes after showing the plan")
139
+ .option("--yes", "apply --fix without prompting")
140
+ .option("--json", "emit diagnostic checks as JSON")
141
+ .action(notImplemented("doctor"));
124
142
  program
125
143
  .command("completion [shell]")
126
144
  .description("print shell completion definitions")
127
145
  .action(notImplemented("completion"));
128
- program
146
+ const prCommand = program
129
147
  .command("pr <ref>")
148
+ .usage("[options] <ref>")
130
149
  .description("fetch a pull request by number, #number, or URL into a linked worktree")
131
150
  .option("--dry-run", "show what would be created without executing any git commands or writing files")
132
151
  .option("--json", "emit JSON on success or error instead of human-readable output")
133
152
  .action(notImplemented("pr"));
153
+ prCommand
154
+ .command("open [target]")
155
+ .description("open an existing pull request in the default browser")
156
+ .option("--select", "choose a pull request from any linked worktree")
157
+ .action(notImplemented("pr open"));
134
158
  program
135
159
  .command("back [n]")
136
160
  .description("navigate to the previously visited worktree, optionally N steps back")
@@ -144,6 +168,7 @@ function registerCommands(program) {
144
168
  program
145
169
  .command("open [branch]")
146
170
  .description("open the worktree in an editor")
171
+ .option("--select", "choose a worktree with the interactive selector")
147
172
  .option("--editor <cli>", "editor CLI to use (code, cursor, zed, windsurf, subl, …)")
148
173
  .option("--save", "save the chosen editor to global config")
149
174
  .option("--workspace", "generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)")
@@ -254,6 +279,7 @@ function attachCommandActions(program, options) {
254
279
  detached: commandOptions.detached,
255
280
  dryRun: commandOptions.dryRun,
256
281
  editor: commandOptions.editor,
282
+ fromCurrent: commandOptions.fromCurrent,
257
283
  force: commandOptions.force,
258
284
  json: commandOptions.json,
259
285
  open: commandOptions.open,
@@ -267,6 +293,7 @@ function attachCommandActions(program, options) {
267
293
  ?.action(async (shell, commandOptions) => {
268
294
  const exitCode = await runInitCommand({
269
295
  cwd: options.cwd,
296
+ json: commandOptions.json,
270
297
  shell,
271
298
  stderr: options.stderr,
272
299
  stdout: options.stdout,
@@ -276,6 +303,21 @@ function attachCommandActions(program, options) {
276
303
  throw commanderExit(exitCode);
277
304
  }
278
305
  });
306
+ program.commands
307
+ .find((command) => command.name() === "doctor")
308
+ ?.action(async (commandOptions) => {
309
+ const exitCode = await runDoctorCommand({
310
+ cwd: options.cwd,
311
+ fix: commandOptions.fix,
312
+ json: commandOptions.json,
313
+ stderr: options.stderr,
314
+ stdout: options.stdout,
315
+ yes: commandOptions.yes,
316
+ });
317
+ if (exitCode !== 0) {
318
+ throw commanderExit(exitCode);
319
+ }
320
+ });
279
321
  program.commands
280
322
  .find((command) => command.name() === "completion")
281
323
  ?.action(async (shell) => {
@@ -303,6 +345,22 @@ function attachCommandActions(program, options) {
303
345
  throw commanderExit(exitCode);
304
346
  }
305
347
  });
348
+ const prOpenCommand = program.commands
349
+ .find((command) => command.name() === "pr")
350
+ ?.commands.find((command) => command.name() === "open");
351
+ const runPrOpenCommand = createPrOpenCommand(options.prOpenDependencies);
352
+ prOpenCommand?.action(async (target, commandOptions) => {
353
+ const exitCode = await runPrOpenCommand({
354
+ cwd: options.cwd,
355
+ stderr: options.stderr,
356
+ stdout: options.stdout,
357
+ select: commandOptions.select,
358
+ target,
359
+ });
360
+ if (exitCode !== 0) {
361
+ throw commanderExit(exitCode);
362
+ }
363
+ });
306
364
  program.commands
307
365
  .find((command) => command.name() === "back")
308
366
  ?.action(async (n, commandOptions) => {
@@ -342,6 +400,7 @@ function attachCommandActions(program, options) {
342
400
  cwd: options.cwd,
343
401
  editor: commandOptions.editor,
344
402
  save: commandOptions.save,
403
+ select: commandOptions.select,
345
404
  stderr: options.stderr,
346
405
  stdout: options.stdout,
347
406
  workspace: commandOptions.workspace,
@@ -0,0 +1,28 @@
1
+ export type DoctorCheckStatus = "fail" | "ok" | "skip";
2
+ export interface DoctorCheck {
3
+ hint?: string;
4
+ id: string;
5
+ message: string;
6
+ status: DoctorCheckStatus;
7
+ }
8
+ export type DoctorFixStatus = "applied" | "declined" | "failed" | "pending" | "skipped";
9
+ export interface DoctorFix {
10
+ hint?: string;
11
+ id: string;
12
+ message: string;
13
+ paths?: string[];
14
+ status: DoctorFixStatus;
15
+ }
16
+ export interface DoctorCommandOptions {
17
+ cwd: string;
18
+ home?: string;
19
+ fix?: boolean;
20
+ json?: boolean;
21
+ interactive?: boolean;
22
+ confirmFixes?: (fixes: DoctorFix[]) => Promise<boolean>;
23
+ shell?: string;
24
+ yes?: boolean;
25
+ stderr?: (chunk: string) => void;
26
+ stdout: (chunk: string) => void;
27
+ }
28
+ export declare function runDoctorCommand(options: DoctorCommandOptions): Promise<number>;