@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.
- package/README.md +124 -11
- package/dist/back.d.ts +13 -0
- package/dist/back.js +74 -0
- package/dist/clean.d.ts +1 -0
- package/dist/clean.js +103 -13
- package/dist/cli.js +104 -4
- package/dist/completion.d.ts +6 -0
- package/dist/completion.js +11 -0
- package/dist/git.d.ts +3 -0
- package/dist/git.js +38 -0
- package/dist/gji +5 -0
- package/dist/gji-bundle.mjs +16705 -0
- package/dist/go.js +2 -0
- package/dist/history-command.d.ts +7 -0
- package/dist/history-command.js +15 -0
- package/dist/history.d.ts +9 -0
- package/dist/history.js +46 -0
- package/dist/init.d.ts +1 -1
- package/dist/init.js +9 -22
- package/dist/ls.d.ts +3 -0
- package/dist/ls.js +46 -2
- package/dist/new.js +3 -0
- package/dist/pr.js +46 -1
- package/dist/shell-completion.d.ts +1 -0
- package/dist/shell-completion.js +284 -0
- package/dist/shell.d.ts +2 -0
- package/dist/shell.js +21 -0
- package/dist/sync.js +2 -13
- package/dist/worktree-info.d.ts +33 -0
- package/dist/worktree-info.js +105 -0
- package/man/man1/gji-back.1 +13 -0
- package/man/man1/gji-clean.1 +4 -1
- package/man/man1/gji-completion.1 +9 -0
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +13 -0
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +4 -1
- package/man/man1/gji-new.1 +1 -1
- package/man/man1/gji-pr.1 +1 -1
- package/man/man1/gji-remove.1 +1 -1
- package/man/man1/gji-root.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-trigger-hook.1 +1 -1
- package/man/man1/gji.1 +13 -1
- 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
|
|
22
|
+
const packageMetadata = readPackageMetadata();
|
|
18
23
|
program
|
|
19
24
|
.name('gji')
|
|
20
25
|
.description('Context switching without the mess.')
|
|
21
|
-
.version(
|
|
26
|
+
.version(packageMetadata.version)
|
|
22
27
|
.showHelpAfterError()
|
|
23
28
|
.showSuggestionAfterError();
|
|
24
29
|
registerCommands(program);
|
|
25
30
|
return program;
|
|
26
31
|
}
|
|
27
|
-
function
|
|
32
|
+
function readPackageMetadata() {
|
|
28
33
|
const require = createRequire(import.meta.url);
|
|
29
34
|
const packageJson = require('../package.json');
|
|
30
|
-
return
|
|
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,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
|
+
}
|