@solaqua/gji 0.4.0 → 0.5.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 (47) hide show
  1. package/README.md +124 -11
  2. package/dist/back.d.ts +13 -0
  3. package/dist/back.js +74 -0
  4. package/dist/clean.d.ts +1 -0
  5. package/dist/clean.js +103 -13
  6. package/dist/cli.js +104 -4
  7. package/dist/completion.d.ts +6 -0
  8. package/dist/completion.js +11 -0
  9. package/dist/git.d.ts +3 -0
  10. package/dist/git.js +38 -0
  11. package/dist/gji +5 -0
  12. package/dist/gji-bundle.mjs +16705 -0
  13. package/dist/go.js +2 -0
  14. package/dist/history-command.d.ts +7 -0
  15. package/dist/history-command.js +15 -0
  16. package/dist/history.d.ts +9 -0
  17. package/dist/history.js +46 -0
  18. package/dist/init.d.ts +1 -1
  19. package/dist/init.js +9 -22
  20. package/dist/ls.d.ts +3 -0
  21. package/dist/ls.js +46 -2
  22. package/dist/new.js +3 -0
  23. package/dist/pr.js +46 -1
  24. package/dist/shell-completion.d.ts +1 -0
  25. package/dist/shell-completion.js +284 -0
  26. package/dist/shell.d.ts +2 -0
  27. package/dist/shell.js +21 -0
  28. package/dist/sync.js +2 -13
  29. package/dist/worktree-info.d.ts +33 -0
  30. package/dist/worktree-info.js +105 -0
  31. package/man/man1/gji-back.1 +13 -0
  32. package/man/man1/gji-clean.1 +4 -1
  33. package/man/man1/gji-completion.1 +9 -0
  34. package/man/man1/gji-config.1 +1 -1
  35. package/man/man1/gji-go.1 +1 -1
  36. package/man/man1/gji-history.1 +13 -0
  37. package/man/man1/gji-init.1 +1 -1
  38. package/man/man1/gji-ls.1 +4 -1
  39. package/man/man1/gji-new.1 +1 -1
  40. package/man/man1/gji-pr.1 +1 -1
  41. package/man/man1/gji-remove.1 +1 -1
  42. package/man/man1/gji-root.1 +1 -1
  43. package/man/man1/gji-status.1 +1 -1
  44. package/man/man1/gji-sync.1 +1 -1
  45. package/man/man1/gji-trigger-hook.1 +1 -1
  46. package/man/man1/gji.1 +13 -1
  47. package/package.json +11 -2
package/dist/cli.js CHANGED
@@ -1,8 +1,13 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import { Command } from 'commander';
3
+ import updateNotifier from 'update-notifier';
4
+ import { runBackCommand } from './back.js';
3
5
  import { runCleanCommand } from './clean.js';
6
+ import { runHistoryCommand } from './history-command.js';
7
+ import { runCompletionCommand } from './completion.js';
4
8
  import { runConfigCommand } from './config-command.js';
5
9
  import { runGoCommand } from './go.js';
10
+ import { isHeadless } from './headless.js';
6
11
  import { runInitCommand } from './init.js';
7
12
  import { runLsCommand } from './ls.js';
8
13
  import { runNewCommand } from './new.js';
@@ -14,22 +19,26 @@ import { runSyncCommand } from './sync.js';
14
19
  import { runTriggerHookCommand } from './trigger-hook.js';
15
20
  export function createProgram() {
16
21
  const program = new Command();
17
- const packageVersion = readPackageVersion();
22
+ const packageMetadata = readPackageMetadata();
18
23
  program
19
24
  .name('gji')
20
25
  .description('Context switching without the mess.')
21
- .version(packageVersion)
26
+ .version(packageMetadata.version)
22
27
  .showHelpAfterError()
23
28
  .showSuggestionAfterError();
24
29
  registerCommands(program);
25
30
  return program;
26
31
  }
27
- function readPackageVersion() {
32
+ function readPackageMetadata() {
28
33
  const require = createRequire(import.meta.url);
29
34
  const packageJson = require('../package.json');
30
- return typeof packageJson.version === 'string' ? packageJson.version : '0.0.0';
35
+ return {
36
+ name: typeof packageJson.name === 'string' ? packageJson.name : 'gji',
37
+ version: typeof packageJson.version === 'string' ? packageJson.version : '0.0.0',
38
+ };
31
39
  }
32
40
  export async function runCli(argv, options = {}) {
41
+ await maybeNotifyForUpdates(argv);
33
42
  const program = createProgram();
34
43
  const cwd = options.cwd ?? process.cwd();
35
44
  const stdout = options.stdout ?? (() => undefined);
@@ -55,6 +64,36 @@ export async function runCli(argv, options = {}) {
55
64
  throw error;
56
65
  }
57
66
  }
67
+ async function maybeNotifyForUpdates(argv) {
68
+ if (shouldSkipUpdateNotification(argv)) {
69
+ return;
70
+ }
71
+ try {
72
+ defaultNotifyForUpdates(readPackageMetadata());
73
+ }
74
+ catch {
75
+ // Ignore notifier failures so startup behaviour stays stable.
76
+ }
77
+ }
78
+ function shouldSkipUpdateNotification(argv) {
79
+ return (argv.length === 0
80
+ || argv.includes('--json')
81
+ || argv.some(isHelpOrVersionArgument)
82
+ || isHeadless()
83
+ || process.stdout.isTTY !== true
84
+ || process.stderr.isTTY !== true);
85
+ }
86
+ function isHelpOrVersionArgument(argument) {
87
+ return (argument === '--help'
88
+ || argument === '-h'
89
+ || argument === 'help'
90
+ || argument === '--version'
91
+ || argument === '-V');
92
+ }
93
+ function defaultNotifyForUpdates(pkg) {
94
+ const notifier = updateNotifier({ pkg });
95
+ notifier.notify();
96
+ }
58
97
  function registerCommands(program) {
59
98
  program
60
99
  .command('new [branch]')
@@ -69,12 +108,26 @@ function registerCommands(program) {
69
108
  .description('print or install shell integration')
70
109
  .option('--write', 'write the integration to the shell config file')
71
110
  .action(notImplemented('init'));
111
+ program
112
+ .command('completion [shell]')
113
+ .description('print shell completion definitions')
114
+ .action(notImplemented('completion'));
72
115
  program
73
116
  .command('pr <ref>')
74
117
  .description('fetch a pull request by number, #number, or URL into a linked worktree')
75
118
  .option('--dry-run', 'show what would be created without executing any git commands or writing files')
76
119
  .option('--json', 'emit JSON on success or error instead of human-readable output')
77
120
  .action(notImplemented('pr'));
121
+ program
122
+ .command('back [n]')
123
+ .description('navigate to the previously visited worktree, optionally N steps back')
124
+ .option('--print', 'print the resolved worktree path explicitly')
125
+ .action(notImplemented('back'));
126
+ program
127
+ .command('history')
128
+ .description('show navigation history')
129
+ .option('--json', 'print history as JSON')
130
+ .action(notImplemented('history'));
78
131
  program
79
132
  .command('go [branch]')
80
133
  .description('print or select a worktree path')
@@ -99,12 +152,14 @@ function registerCommands(program) {
99
152
  program
100
153
  .command('ls')
101
154
  .description('list active worktrees')
155
+ .option('--compact', 'show only branch and path columns')
102
156
  .option('--json', 'print active worktrees as JSON')
103
157
  .action(notImplemented('ls'));
104
158
  program
105
159
  .command('clean')
106
160
  .description('interactively prune linked worktrees')
107
161
  .option('-f, --force', 'bypass prompts, force-remove dirty worktrees, and force-delete unmerged branches')
162
+ .option('--stale', 'only target clean worktrees whose upstream is gone and branch is merged into the default branch')
108
163
  .option('--dry-run', 'show what would be deleted without removing anything')
109
164
  .option('--json', 'emit JSON on success or error instead of human-readable output')
110
165
  .action(notImplemented('clean'));
@@ -160,6 +215,18 @@ function attachCommandActions(program, options) {
160
215
  throw commanderExit(exitCode);
161
216
  }
162
217
  });
218
+ program.commands
219
+ .find((command) => command.name() === 'completion')
220
+ ?.action(async (shell) => {
221
+ const exitCode = await runCompletionCommand({
222
+ shell,
223
+ stderr: options.stderr,
224
+ stdout: options.stdout,
225
+ });
226
+ if (exitCode !== 0) {
227
+ throw commanderExit(exitCode);
228
+ }
229
+ });
163
230
  program.commands
164
231
  .find((command) => command.name() === 'pr')
165
232
  ?.action(async (number, commandOptions) => {
@@ -168,6 +235,37 @@ function attachCommandActions(program, options) {
168
235
  throw commanderExit(exitCode);
169
236
  }
170
237
  });
238
+ program.commands
239
+ .find((command) => command.name() === 'back')
240
+ ?.action(async (n, commandOptions) => {
241
+ if (n !== undefined && !/^\d+$/.test(n)) {
242
+ options.stderr(`gji back: invalid step count: ${n}\n`);
243
+ throw commanderExit(1);
244
+ }
245
+ const steps = n !== undefined ? parseInt(n, 10) : undefined;
246
+ const exitCode = await runBackCommand({
247
+ cwd: options.cwd,
248
+ n: steps,
249
+ print: commandOptions.print,
250
+ stderr: options.stderr,
251
+ stdout: options.stdout,
252
+ });
253
+ if (exitCode !== 0) {
254
+ throw commanderExit(exitCode);
255
+ }
256
+ });
257
+ program.commands
258
+ .find((command) => command.name() === 'history')
259
+ ?.action(async (commandOptions) => {
260
+ const exitCode = await runHistoryCommand({
261
+ cwd: options.cwd,
262
+ json: commandOptions.json,
263
+ stdout: options.stdout,
264
+ });
265
+ if (exitCode !== 0) {
266
+ throw commanderExit(exitCode);
267
+ }
268
+ });
171
269
  program.commands
172
270
  .find((command) => command.name() === 'go')
173
271
  ?.action(async (branch, commandOptions) => {
@@ -224,6 +322,7 @@ function attachCommandActions(program, options) {
224
322
  .find((command) => command.name() === 'ls')
225
323
  ?.action(async (commandOptions) => {
226
324
  const exitCode = await runLsCommand({
325
+ compact: commandOptions.compact,
227
326
  cwd: options.cwd,
228
327
  json: commandOptions.json,
229
328
  stdout: options.stdout,
@@ -240,6 +339,7 @@ function attachCommandActions(program, options) {
240
339
  dryRun: commandOptions.dryRun,
241
340
  force: commandOptions.force,
242
341
  json: commandOptions.json,
342
+ stale: commandOptions.stale,
243
343
  stderr: options.stderr,
244
344
  stdout: options.stdout,
245
345
  });
@@ -0,0 +1,6 @@
1
+ export interface CompletionCommandOptions {
2
+ shell?: string;
3
+ stderr?: (chunk: string) => void;
4
+ stdout: (chunk: string) => void;
5
+ }
6
+ export declare function runCompletionCommand(options: CompletionCommandOptions): Promise<number>;
@@ -0,0 +1,11 @@
1
+ import { renderShellCompletion } from './shell-completion.js';
2
+ import { resolveSupportedShell } from './shell.js';
3
+ export async function runCompletionCommand(options) {
4
+ const shell = resolveSupportedShell(options.shell, process.env.SHELL);
5
+ if (!shell) {
6
+ options.stderr?.('Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n');
7
+ return 1;
8
+ }
9
+ options.stdout(renderShellCompletion(shell));
10
+ return 0;
11
+ }
package/dist/git.d.ts CHANGED
@@ -8,3 +8,6 @@ export interface WorktreeHealth {
8
8
  export declare function runGit(cwd: string, args: string[]): Promise<string>;
9
9
  export declare function readWorktreeHealth(cwd: string): Promise<WorktreeHealth>;
10
10
  export declare function isDirtyWorktree(cwd: string): Promise<boolean>;
11
+ export declare function isBranchMergedInto(cwd: string, branch: string, base?: string): Promise<boolean>;
12
+ export declare function resolveRemoteDefaultBranch(cwd: string, remote: string): Promise<string | null>;
13
+ export declare function readBranchLastCommitTimestamp(cwd: string, branch: string): Promise<number | null>;
package/dist/git.js CHANGED
@@ -19,6 +19,39 @@ export async function isDirtyWorktree(cwd) {
19
19
  const health = await readWorktreeHealth(cwd);
20
20
  return health.status === 'dirty';
21
21
  }
22
+ export async function isBranchMergedInto(cwd, branch, base = 'HEAD') {
23
+ try {
24
+ await execFileAsync('git', ['merge-base', '--is-ancestor', branch, base], { cwd });
25
+ return true;
26
+ }
27
+ catch (error) {
28
+ if (hasExitCode(error, 1)) {
29
+ return false;
30
+ }
31
+ throw error;
32
+ }
33
+ }
34
+ export async function resolveRemoteDefaultBranch(cwd, remote) {
35
+ const { stdout } = await execFileAsync('git', ['ls-remote', '--symref', remote, 'HEAD'], { cwd });
36
+ const refLine = stdout
37
+ .split('\n')
38
+ .find((line) => line.startsWith('ref: refs/heads/'));
39
+ if (!refLine) {
40
+ return null;
41
+ }
42
+ const match = /^ref: refs\/heads\/(.+)\tHEAD$/.exec(refLine);
43
+ return match?.[1] ?? null;
44
+ }
45
+ export async function readBranchLastCommitTimestamp(cwd, branch) {
46
+ try {
47
+ const { stdout } = await execFileAsync('git', ['log', '-1', '--format=%ct', branch], { cwd });
48
+ const timestamp = Number(stdout.trim());
49
+ return Number.isFinite(timestamp) ? timestamp : null;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
22
55
  function parseWorktreeHealth(output) {
23
56
  let ahead = 0;
24
57
  let behind = 0;
@@ -52,3 +85,8 @@ function parseWorktreeHealth(output) {
52
85
  status: dirty ? 'dirty' : 'clean',
53
86
  };
54
87
  }
88
+ function hasExitCode(error, code) {
89
+ return error instanceof Error
90
+ && 'code' in error
91
+ && error.code === code;
92
+ }
package/dist/gji ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('./gji-bundle.mjs').catch(err => {
3
+ process.stderr.write(err.message + '\n');
4
+ process.exit(1);
5
+ });