@solaqua/gji 0.5.0 → 0.6.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
@@ -107,6 +107,9 @@ gji completion fish > ~/.config/fish/completions/gji.fish
107
107
  # start a new task
108
108
  gji new feature/dark-mode
109
109
 
110
+ # start a task and open it straight in your editor
111
+ gji new feature/dark-mode --open --editor cursor
112
+
110
113
  # review a pull request
111
114
  gji pr 1234
112
115
 
@@ -117,6 +120,10 @@ gji status
117
120
  gji go feature/dark-mode
118
121
  gji go main
119
122
 
123
+ # open any worktree in an editor (interactive picker)
124
+ gji open
125
+ gji open feature/dark-mode --editor code
126
+
120
127
  # clean up when done
121
128
  gji remove feature/dark-mode
122
129
  ```
@@ -234,8 +241,9 @@ path=$(gji root --print)
234
241
 
235
242
  | Command | Description |
236
243
  |---|---|
237
- | `gji new [branch] [--detached] [--json]` | create branch + worktree, cd in (validates branch name against Git rules) |
244
+ | `gji new [branch] [--detached] [--open] [--editor <cli>] [--json]` | create branch + worktree, cd in (validates branch name against Git rules) |
238
245
  | `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 |
239
247
  | `gji go [branch] [--print]` | jump to a worktree |
240
248
  | `gji root [--print]` | jump to the main repo root |
241
249
  | `gji status [--json]` | repo overview, worktree health, ahead/behind |
@@ -260,6 +268,7 @@ No setup required. Optional config lives in:
260
268
  | Key | Description |
261
269
  |---|---|
262
270
  | `branchPrefix` | prefix added to new branch names (e.g. `"feature/"`) |
271
+ | `editor` | default editor CLI for `gji open` and `gji new --open` (e.g. `"cursor"`, `"code"`, `"zed"`); set automatically with `gji open --save` |
263
272
  | `worktreePath` | base directory for new worktrees (absolute or `~/…`); overrides the default `../worktrees/<repo>/` layout |
264
273
  | `syncRemote` | remote for `gji sync` (default: `origin`) |
265
274
  | `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
@@ -314,8 +323,8 @@ Run scripts automatically at key lifecycle moments:
314
323
  ```json
315
324
  {
316
325
  "hooks": {
317
- "afterCreate": "pnpm install",
318
- "afterEnter": "echo 'switched to {{branch}}'",
326
+ "afterCreate": ["pnpm", "install"],
327
+ "afterEnter": ["printf", "switched to %s\n", "{{branch}}"],
319
328
  "beforeRemove": "pnpm run cleanup"
320
329
  }
321
330
  }
@@ -329,6 +338,31 @@ Run scripts automatically at key lifecycle moments:
329
338
 
330
339
  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.
331
340
 
341
+ Prefer argv-array hooks for simple commands:
342
+
343
+ ```json
344
+ {
345
+ "hooks": {
346
+ "afterCreate": ["pnpm", "install"],
347
+ "afterEnter": ["printf", "switched to %s at %s\n", "{{branch}}", "{{path}}"]
348
+ }
349
+ }
350
+ ```
351
+
352
+ Array hooks run without a shell and pass each array item as exactly one argument. Use string hooks only when you need shell features like `&&`, pipes, redirects, shell functions, or `nvm use`.
353
+
354
+ Template values are interpolated before the shell parses string hooks, so avoid putting `{{branch}}`, `{{path}}`, or `{{repo}}` directly into shell strings. For shell-string hooks, the safer pattern is to use the environment variables and double-quote each expansion:
355
+
356
+ ```json
357
+ {
358
+ "hooks": {
359
+ "afterCreate": "pnpm install && printf 'ready: %s\n' \"$GJI_PATH\""
360
+ }
361
+ }
362
+ ```
363
+
364
+ Avoid unquoted template values in shell strings, such as `echo {{branch}}` or `cd {{path}}`.
365
+
332
366
  Hooks from all three config layers merge per key — different keys from different layers both apply, same key the higher-precedence layer wins:
333
367
 
334
368
  ```jsonc
package/dist/cli.js CHANGED
@@ -7,16 +7,20 @@ import { runHistoryCommand } from './history-command.js';
7
7
  import { runCompletionCommand } from './completion.js';
8
8
  import { runConfigCommand } from './config-command.js';
9
9
  import { runGoCommand } from './go.js';
10
+ import { runOpenCommand } from './open.js';
10
11
  import { isHeadless } from './headless.js';
11
12
  import { runInitCommand } from './init.js';
12
13
  import { runLsCommand } from './ls.js';
13
14
  import { runNewCommand } from './new.js';
14
15
  import { runPrCommand } from './pr.js';
15
16
  import { runRemoveCommand } from './remove.js';
17
+ import { registerRepo } from './repo-registry.js';
18
+ import { detectRepository } from './repo.js';
16
19
  import { runRootCommand } from './root.js';
17
20
  import { runStatusCommand } from './status.js';
18
21
  import { runSyncCommand } from './sync.js';
19
22
  import { runTriggerHookCommand } from './trigger-hook.js';
23
+ import { runWarpCommand } from './warp.js';
20
24
  export function createProgram() {
21
25
  const program = new Command();
22
26
  const packageMetadata = readPackageMetadata();
@@ -39,6 +43,7 @@ function readPackageMetadata() {
39
43
  }
40
44
  export async function runCli(argv, options = {}) {
41
45
  await maybeNotifyForUpdates(argv);
46
+ maybeRegisterCurrentRepo(options.cwd ?? process.cwd());
42
47
  const program = createProgram();
43
48
  const cwd = options.cwd ?? process.cwd();
44
49
  const stdout = options.stdout ?? (() => undefined);
@@ -94,12 +99,19 @@ function defaultNotifyForUpdates(pkg) {
94
99
  const notifier = updateNotifier({ pkg });
95
100
  notifier.notify();
96
101
  }
102
+ function maybeRegisterCurrentRepo(cwd) {
103
+ detectRepository(cwd)
104
+ .then(({ repoRoot }) => registerRepo(repoRoot))
105
+ .catch(() => undefined);
106
+ }
97
107
  function registerCommands(program) {
98
108
  program
99
109
  .command('new [branch]')
100
110
  .description('create a new branch or detached linked worktree')
101
111
  .option('-f, --force', 'remove and recreate the worktree if the target path already exists')
102
112
  .option('--detached', 'create a detached worktree without a branch')
113
+ .option('--open', 'open the new worktree in an editor after creation')
114
+ .option('--editor <cli>', 'editor CLI to use with --open (code, cursor, zed, …)')
103
115
  .option('--dry-run', 'show what would be created without executing any git commands or writing files')
104
116
  .option('--json', 'emit JSON on success or error instead of human-readable output')
105
117
  .action(notImplemented('new'));
@@ -128,8 +140,16 @@ function registerCommands(program) {
128
140
  .description('show navigation history')
129
141
  .option('--json', 'print history as JSON')
130
142
  .action(notImplemented('history'));
143
+ program
144
+ .command('open [branch]')
145
+ .description('open the worktree in an editor')
146
+ .option('--editor <cli>', 'editor CLI to use (code, cursor, zed, windsurf, subl, …)')
147
+ .option('--save', 'save the chosen editor to global config')
148
+ .option('--workspace', 'generate a .code-workspace file before opening (VS Code / Cursor / Windsurf)')
149
+ .action(notImplemented('open'));
131
150
  program
132
151
  .command('go [branch]')
152
+ .alias('jump')
133
153
  .description('print or select a worktree path')
134
154
  .option('--print', 'print the resolved worktree path explicitly')
135
155
  .action(notImplemented('go'));
@@ -175,6 +195,15 @@ function registerCommands(program) {
175
195
  .command('trigger-hook <hook>')
176
196
  .description('run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree')
177
197
  .action(notImplemented('trigger-hook'));
198
+ program
199
+ .command('warp [branch]')
200
+ .description('jump to any worktree across all known repos')
201
+ .option('-n, --new [branch]', 'create a new worktree in a registered repo')
202
+ // --print is the shell-wrapper bypass signal (see SHELL_WRAPPED_COMMANDS in init.ts).
203
+ // The shell omits GJI_WARP_OUTPUT_FILE, so writeShellOutput falls through to stdout.
204
+ .option('--print', 'print the resolved worktree path without changing directory')
205
+ .option('--json', 'emit JSON on success or error instead of human-readable output')
206
+ .action(notImplemented('warp'));
178
207
  const configCommand = program
179
208
  .command('config')
180
209
  .description('manage global config defaults')
@@ -196,7 +225,7 @@ function attachCommandActions(program, options) {
196
225
  program.commands
197
226
  .find((command) => command.name() === 'new')
198
227
  ?.action(async (branch, commandOptions) => {
199
- const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, force: commandOptions.force, json: commandOptions.json });
228
+ const exitCode = await runNewCommand({ ...options, branch, detached: commandOptions.detached, dryRun: commandOptions.dryRun, editor: commandOptions.editor, force: commandOptions.force, json: commandOptions.json, open: commandOptions.open });
200
229
  if (exitCode !== 0) {
201
230
  throw commanderExit(exitCode);
202
231
  }
@@ -266,6 +295,22 @@ function attachCommandActions(program, options) {
266
295
  throw commanderExit(exitCode);
267
296
  }
268
297
  });
298
+ program.commands
299
+ .find((command) => command.name() === 'open')
300
+ ?.action(async (branch, commandOptions) => {
301
+ const exitCode = await runOpenCommand({
302
+ branch,
303
+ cwd: options.cwd,
304
+ editor: commandOptions.editor,
305
+ save: commandOptions.save,
306
+ stderr: options.stderr,
307
+ stdout: options.stdout,
308
+ workspace: commandOptions.workspace,
309
+ });
310
+ if (exitCode !== 0) {
311
+ throw commanderExit(exitCode);
312
+ }
313
+ });
269
314
  program.commands
270
315
  .find((command) => command.name() === 'go')
271
316
  ?.action(async (branch, commandOptions) => {
@@ -376,6 +421,24 @@ function attachCommandActions(program, options) {
376
421
  throw commanderExit(exitCode);
377
422
  }
378
423
  });
424
+ program.commands
425
+ .find((command) => command.name() === 'warp')
426
+ ?.action(async (branch, commandOptions) => {
427
+ const newFlag = commandOptions.new;
428
+ const newWorktree = newFlag !== undefined && newFlag !== false;
429
+ const newBranch = typeof newFlag === 'string' ? newFlag : undefined;
430
+ const exitCode = await runWarpCommand({
431
+ branch: newWorktree ? (newBranch ?? branch) : branch,
432
+ cwd: options.cwd,
433
+ json: commandOptions.json,
434
+ newWorktree,
435
+ stderr: options.stderr,
436
+ stdout: options.stdout,
437
+ });
438
+ if (exitCode !== 0) {
439
+ throw commanderExit(exitCode);
440
+ }
441
+ });
379
442
  const configCommand = program.commands.find((command) => command.name() === 'config');
380
443
  configCommand?.action(async () => {
381
444
  const exitCode = await runConfigCommand({
package/dist/config.js CHANGED
@@ -6,6 +6,7 @@ export const GLOBAL_CONFIG_DIRECTORY = '.config/gji';
6
6
  export const GLOBAL_CONFIG_NAME = 'config.json';
7
7
  export const KNOWN_CONFIG_KEYS = new Set([
8
8
  'branchPrefix',
9
+ 'editor',
9
10
  'hooks',
10
11
  'installSaveTarget',
11
12
  'shellIntegration',
@@ -0,0 +1,8 @@
1
+ export interface EditorDefinition {
2
+ cli: string;
3
+ name: string;
4
+ newWindowFlag?: string;
5
+ supportsWorkspace: boolean;
6
+ }
7
+ export declare const EDITORS: EditorDefinition[];
8
+ export declare function defaultSpawnEditor(cli: string, args: string[]): Promise<void>;
package/dist/editor.js ADDED
@@ -0,0 +1,17 @@
1
+ import { spawn } from 'node:child_process';
2
+ // Ordered by likely popularity among the target audience.
3
+ export const EDITORS = [
4
+ { cli: 'cursor', name: 'Cursor', newWindowFlag: '--new-window', supportsWorkspace: true },
5
+ { cli: 'code', name: 'VS Code', newWindowFlag: '--new-window', supportsWorkspace: true },
6
+ { cli: 'windsurf', name: 'Windsurf', newWindowFlag: '--new-window', supportsWorkspace: true },
7
+ { cli: 'zed', name: 'Zed', supportsWorkspace: false },
8
+ { cli: 'subl', name: 'Sublime Text', newWindowFlag: '--new-window', supportsWorkspace: false },
9
+ ];
10
+ export async function defaultSpawnEditor(cli, args) {
11
+ const child = spawn(cli, args, { detached: true, stdio: 'ignore' });
12
+ await new Promise((resolve, reject) => {
13
+ child.once('error', reject);
14
+ child.once('spawn', resolve);
15
+ });
16
+ child.unref();
17
+ }