@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 +37 -3
- package/dist/cli.js +64 -1
- package/dist/config.js +1 -0
- package/dist/editor.d.ts +8 -0
- package/dist/editor.js +17 -0
- package/dist/gji-bundle.mjs +1306 -760
- package/dist/go.js +22 -4
- package/dist/hooks.d.ts +5 -4
- package/dist/hooks.js +61 -9
- package/dist/init.js +26 -11
- package/dist/install-prompt.js +9 -1
- package/dist/new.d.ts +3 -0
- package/dist/new.js +33 -1
- package/dist/open.d.ts +20 -0
- package/dist/open.js +155 -0
- package/dist/repo-registry.d.ts +8 -0
- package/dist/repo-registry.js +52 -0
- package/dist/warp.d.ts +20 -0
- package/dist/warp.js +196 -0
- package/man/man1/gji-back.1 +1 -1
- package/man/man1/gji-clean.1 +1 -1
- package/man/man1/gji-completion.1 +1 -1
- package/man/man1/gji-config.1 +1 -1
- package/man/man1/gji-go.1 +1 -1
- package/man/man1/gji-history.1 +1 -1
- package/man/man1/gji-init.1 +1 -1
- package/man/man1/gji-ls.1 +1 -1
- package/man/man1/gji-new.1 +7 -1
- package/man/man1/gji-open.1 +19 -0
- 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-warp.1 +19 -0
- package/man/man1/gji.1 +11 -1
- package/package.json +4 -2
package/dist/warp.js
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { isCancel, select } from '@clack/prompts';
|
|
2
|
+
import { readWorktreeHealth } from './git.js';
|
|
3
|
+
import { isHeadless } from './headless.js';
|
|
4
|
+
import { appendHistory } from './history.js';
|
|
5
|
+
import { runNewCommand } from './new.js';
|
|
6
|
+
import { loadRegistry } from './repo-registry.js';
|
|
7
|
+
import { listWorktrees } from './repo.js';
|
|
8
|
+
import { writeShellOutput } from './shell-handoff.js';
|
|
9
|
+
const WARP_OUTPUT_FILE_ENV = 'GJI_WARP_OUTPUT_FILE';
|
|
10
|
+
export async function runWarpCommand(options) {
|
|
11
|
+
if (options.newWorktree) {
|
|
12
|
+
const registry = await loadRegistry();
|
|
13
|
+
if (registry.length === 0) {
|
|
14
|
+
options.stderr('gji warp: no repos registered yet.\n' +
|
|
15
|
+
'Use any gji command in a repository to register it automatically.\n');
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
return runWarpNew(options, registry);
|
|
19
|
+
}
|
|
20
|
+
return runWarpNavigate(options);
|
|
21
|
+
}
|
|
22
|
+
async function runWarpNavigate(options) {
|
|
23
|
+
if ((isHeadless() || options.json) && !options.branch) {
|
|
24
|
+
const message = 'branch argument is required';
|
|
25
|
+
if (options.json) {
|
|
26
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
options.stderr('gji warp: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
|
|
30
|
+
}
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const target = await resolveWarpTarget({ ...options, commandName: 'gji warp', json: options.json });
|
|
34
|
+
if (!target)
|
|
35
|
+
return 1;
|
|
36
|
+
if (options.json) {
|
|
37
|
+
// json callers use the output programmatically; skip history and shell handoff.
|
|
38
|
+
options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}\n`);
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
appendHistory(target.path, target.branch).catch(() => undefined);
|
|
42
|
+
await writeShellOutput(WARP_OUTPUT_FILE_ENV, target.path, options.stdout);
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
async function runWarpNew(options, registry) {
|
|
46
|
+
let targetRepoRoot;
|
|
47
|
+
if (registry.length === 1) {
|
|
48
|
+
targetRepoRoot = registry[0].path;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (isHeadless()) {
|
|
52
|
+
options.stderr('gji warp: repo argument is required in non-interactive mode (GJI_NO_TUI=1)\n');
|
|
53
|
+
return 1;
|
|
54
|
+
}
|
|
55
|
+
const choice = await select({
|
|
56
|
+
message: 'Create worktree in which repo?',
|
|
57
|
+
options: registry.map((entry) => ({
|
|
58
|
+
value: entry.path,
|
|
59
|
+
label: entry.name,
|
|
60
|
+
hint: entry.path,
|
|
61
|
+
})),
|
|
62
|
+
});
|
|
63
|
+
if (isCancel(choice)) {
|
|
64
|
+
options.stderr('Aborted\n');
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
targetRepoRoot = choice;
|
|
68
|
+
}
|
|
69
|
+
if (options.json) {
|
|
70
|
+
return runNewCommand({
|
|
71
|
+
branch: options.branch,
|
|
72
|
+
cwd: targetRepoRoot,
|
|
73
|
+
json: true,
|
|
74
|
+
stderr: options.stderr,
|
|
75
|
+
stdout: options.stdout,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// runNewCommand writes the created path to options.stdout via writeShellOutput.
|
|
79
|
+
// Since GJI_NEW_OUTPUT_FILE is not set in the warp shell context, it falls
|
|
80
|
+
// through to our captured stdout, giving us the path to hand off.
|
|
81
|
+
let capturedPath = '';
|
|
82
|
+
const captureStdout = (chunk) => {
|
|
83
|
+
capturedPath = chunk.trim();
|
|
84
|
+
};
|
|
85
|
+
const exitCode = await runNewCommand({
|
|
86
|
+
branch: options.branch,
|
|
87
|
+
cwd: targetRepoRoot,
|
|
88
|
+
stderr: options.stderr,
|
|
89
|
+
stdout: captureStdout,
|
|
90
|
+
});
|
|
91
|
+
if (exitCode !== 0) {
|
|
92
|
+
return exitCode;
|
|
93
|
+
}
|
|
94
|
+
if (!capturedPath) {
|
|
95
|
+
options.stderr('gji warp: could not determine new worktree path\n');
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
await writeShellOutput(WARP_OUTPUT_FILE_ENV, capturedPath, options.stdout);
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
function findByQuery(items, query) {
|
|
102
|
+
const slashIdx = query.indexOf('/');
|
|
103
|
+
if (slashIdx !== -1) {
|
|
104
|
+
const repoQuery = query.slice(0, slashIdx);
|
|
105
|
+
const branchQuery = query.slice(slashIdx + 1);
|
|
106
|
+
const match = items.find((item) => item.repoName === repoQuery && item.worktree.branch === branchQuery);
|
|
107
|
+
if (match)
|
|
108
|
+
return match;
|
|
109
|
+
}
|
|
110
|
+
return items.find((item) => item.worktree.branch === query) ?? null;
|
|
111
|
+
}
|
|
112
|
+
export async function resolveWarpTarget(options) {
|
|
113
|
+
const cmd = options.commandName ?? 'gji';
|
|
114
|
+
const emitError = (message, hint) => {
|
|
115
|
+
if (options.json) {
|
|
116
|
+
options.stderr(`${JSON.stringify({ error: message }, null, 2)}\n`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
options.stderr(`${cmd}: ${message}\n`);
|
|
120
|
+
if (hint)
|
|
121
|
+
options.stderr(hint);
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const registry = await loadRegistry();
|
|
125
|
+
if (registry.length === 0) {
|
|
126
|
+
emitError('not in a git repository and no repos registered yet.', 'Use any gji command inside a repository to register it.\n');
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const results = await Promise.allSettled(registry.map(async (entry) => {
|
|
130
|
+
const worktrees = await listWorktrees(entry.path);
|
|
131
|
+
return { repoName: entry.name, worktrees };
|
|
132
|
+
}));
|
|
133
|
+
const allItems = [];
|
|
134
|
+
for (const result of results) {
|
|
135
|
+
if (result.status === 'rejected')
|
|
136
|
+
continue;
|
|
137
|
+
const { repoName, worktrees } = result.value;
|
|
138
|
+
for (const worktree of worktrees) {
|
|
139
|
+
allItems.push({ repoName, worktree });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (allItems.length === 0) {
|
|
143
|
+
emitError('no accessible worktrees found in any registered repo.');
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
if (options.branch) {
|
|
147
|
+
const match = findByQuery(allItems, options.branch);
|
|
148
|
+
if (!match) {
|
|
149
|
+
emitError(`no worktree found matching: ${options.branch}`);
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return { branch: match.worktree.branch, path: match.worktree.path };
|
|
153
|
+
}
|
|
154
|
+
const path = await promptForWarpTarget(allItems);
|
|
155
|
+
if (!path) {
|
|
156
|
+
options.stderr('Aborted\n');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const chosen = allItems.find((item) => item.worktree.path === path);
|
|
160
|
+
return { branch: chosen?.worktree.branch ?? null, path };
|
|
161
|
+
}
|
|
162
|
+
async function promptForWarpTarget(items) {
|
|
163
|
+
const healthResults = await Promise.allSettled(items.map((item) => readWorktreeHealth(item.worktree.path)));
|
|
164
|
+
const choice = await select({
|
|
165
|
+
message: 'Warp to a worktree',
|
|
166
|
+
options: items.map((item, i) => {
|
|
167
|
+
const health = healthResults[i].status === 'fulfilled' ? healthResults[i].value : null;
|
|
168
|
+
const upstream = health ? formatHint(item.worktree.branch, health) : null;
|
|
169
|
+
const label = `${item.repoName} › ${item.worktree.branch ?? '(detached)'}`;
|
|
170
|
+
const pathHint = item.worktree.isCurrent
|
|
171
|
+
? `${item.worktree.path} (current)`
|
|
172
|
+
: item.worktree.path;
|
|
173
|
+
const hint = upstream ? `${upstream} · ${pathHint}` : pathHint;
|
|
174
|
+
return { hint, label, value: item.worktree.path };
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
if (isCancel(choice)) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
return choice;
|
|
181
|
+
}
|
|
182
|
+
function formatHint(branch, health) {
|
|
183
|
+
if (branch === null)
|
|
184
|
+
return null;
|
|
185
|
+
if (!health.hasUpstream)
|
|
186
|
+
return 'no upstream';
|
|
187
|
+
if (health.upstreamGone)
|
|
188
|
+
return 'upstream gone';
|
|
189
|
+
if (health.ahead === 0 && health.behind === 0)
|
|
190
|
+
return 'up to date';
|
|
191
|
+
if (health.ahead === 0)
|
|
192
|
+
return `behind ${health.behind}`;
|
|
193
|
+
if (health.behind === 0)
|
|
194
|
+
return `ahead ${health.ahead}`;
|
|
195
|
+
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
196
|
+
}
|
package/man/man1/gji-back.1
CHANGED
package/man/man1/gji-clean.1
CHANGED
package/man/man1/gji-config.1
CHANGED
package/man/man1/gji-go.1
CHANGED
package/man/man1/gji-history.1
CHANGED
package/man/man1/gji-init.1
CHANGED
package/man/man1/gji-ls.1
CHANGED
package/man/man1/gji-new.1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH GJI\-NEW 1 "May 2026" "gji 0.
|
|
1
|
+
.TH GJI\-NEW 1 "May 2026" "gji 0.6.0" "User Commands"
|
|
2
2
|
.SH NAME
|
|
3
3
|
gji\-new \- create a new branch or detached linked worktree
|
|
4
4
|
.SH SYNOPSIS
|
|
@@ -13,6 +13,12 @@ remove and recreate the worktree if the target path already exists
|
|
|
13
13
|
.B \-\-detached
|
|
14
14
|
create a detached worktree without a branch
|
|
15
15
|
.TP
|
|
16
|
+
.B \-\-open
|
|
17
|
+
open the new worktree in an editor after creation
|
|
18
|
+
.TP
|
|
19
|
+
.B \-\-editor <cli>
|
|
20
|
+
editor CLI to use with \-\-open (code, cursor, zed, …)
|
|
21
|
+
.TP
|
|
16
22
|
.B \-\-dry\-run
|
|
17
23
|
show what would be created without executing any git commands or writing files
|
|
18
24
|
.TP
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.TH GJI\-OPEN 1 "May 2026" "gji 0.6.0" "User Commands"
|
|
2
|
+
.SH NAME
|
|
3
|
+
gji\-open \- open the worktree in an editor
|
|
4
|
+
.SH SYNOPSIS
|
|
5
|
+
.B gji open [\fIoptions\fR] [options] [branch]
|
|
6
|
+
.SH DESCRIPTION
|
|
7
|
+
open the worktree in an editor
|
|
8
|
+
.SH OPTIONS
|
|
9
|
+
.TP
|
|
10
|
+
.B \-\-editor <cli>
|
|
11
|
+
editor CLI to use (code, cursor, zed, windsurf, subl, …)
|
|
12
|
+
.TP
|
|
13
|
+
.B \-\-save
|
|
14
|
+
save the chosen editor to global config
|
|
15
|
+
.TP
|
|
16
|
+
.B \-\-workspace
|
|
17
|
+
generate a .code\-workspace file before opening (VS Code / Cursor / Windsurf)
|
|
18
|
+
.SH "SEE ALSO"
|
|
19
|
+
.BR gji (1)
|
package/man/man1/gji-pr.1
CHANGED
package/man/man1/gji-remove.1
CHANGED
package/man/man1/gji-root.1
CHANGED
package/man/man1/gji-status.1
CHANGED
package/man/man1/gji-sync.1
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.TH GJI\-WARP 1 "May 2026" "gji 0.6.0" "User Commands"
|
|
2
|
+
.SH NAME
|
|
3
|
+
gji\-warp \- jump to any worktree across all known repos
|
|
4
|
+
.SH SYNOPSIS
|
|
5
|
+
.B gji warp [\fIoptions\fR] [options] [branch]
|
|
6
|
+
.SH DESCRIPTION
|
|
7
|
+
jump to any worktree across all known repos
|
|
8
|
+
.SH OPTIONS
|
|
9
|
+
.TP
|
|
10
|
+
.B \-n, \-\-new [branch]
|
|
11
|
+
create a new worktree in a registered repo
|
|
12
|
+
.TP
|
|
13
|
+
.B \-\-print
|
|
14
|
+
print the resolved worktree path without changing directory
|
|
15
|
+
.TP
|
|
16
|
+
.B \-\-json
|
|
17
|
+
emit JSON on success or error instead of human\-readable output
|
|
18
|
+
.SH "SEE ALSO"
|
|
19
|
+
.BR gji (1)
|
package/man/man1/gji.1
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
.TH GJI 1 "May 2026" "gji 0.
|
|
1
|
+
.TH GJI 1 "May 2026" "gji 0.6.0" "User Commands"
|
|
2
2
|
.SH NAME
|
|
3
3
|
gji \- Context switching without the mess.
|
|
4
4
|
.SH SYNOPSIS
|
|
@@ -29,8 +29,13 @@ navigate to the previously visited worktree, optionally N steps back
|
|
|
29
29
|
.B history [options]
|
|
30
30
|
show navigation history
|
|
31
31
|
.TP
|
|
32
|
+
.B open [options] [branch]
|
|
33
|
+
open the worktree in an editor
|
|
34
|
+
.TP
|
|
32
35
|
.B go [options] [branch]
|
|
33
36
|
print or select a worktree path
|
|
37
|
+
.br
|
|
38
|
+
Alias: \fBjump\fR
|
|
34
39
|
.TP
|
|
35
40
|
.B root [options]
|
|
36
41
|
print the main repository root path
|
|
@@ -55,6 +60,9 @@ Alias: \fBrm\fR
|
|
|
55
60
|
.B trigger\-hook [options] <hook>
|
|
56
61
|
run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree
|
|
57
62
|
.TP
|
|
63
|
+
.B warp [options] [branch]
|
|
64
|
+
jump to any worktree across all known repos
|
|
65
|
+
.TP
|
|
58
66
|
.B config [options] [command]
|
|
59
67
|
manage global config defaults
|
|
60
68
|
.SH OPTIONS
|
|
@@ -68,6 +76,7 @@ output the version number
|
|
|
68
76
|
.BR gji\-pr (1),
|
|
69
77
|
.BR gji\-back (1),
|
|
70
78
|
.BR gji\-history (1),
|
|
79
|
+
.BR gji\-open (1),
|
|
71
80
|
.BR gji\-go (1),
|
|
72
81
|
.BR gji\-root (1),
|
|
73
82
|
.BR gji\-status (1),
|
|
@@ -76,4 +85,5 @@ output the version number
|
|
|
76
85
|
.BR gji\-clean (1),
|
|
77
86
|
.BR gji\-remove (1),
|
|
78
87
|
.BR gji\-trigger\-hook (1),
|
|
88
|
+
.BR gji\-warp (1),
|
|
79
89
|
.BR gji\-config (1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solaqua/gji",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Git worktree CLI for fast context switching.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "sjquant",
|
|
@@ -64,6 +64,8 @@
|
|
|
64
64
|
"vitest": "^3.2.4"
|
|
65
65
|
},
|
|
66
66
|
"pnpm": {
|
|
67
|
-
"onlyBuiltDependencies": [
|
|
67
|
+
"onlyBuiltDependencies": [
|
|
68
|
+
"esbuild"
|
|
69
|
+
]
|
|
68
70
|
}
|
|
69
71
|
}
|