@solaqua/gji 0.7.1 → 0.7.2
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/dist/clean.d.ts +2 -1
- package/dist/clean.js +8 -16
- package/dist/gji-bundle.mjs +466 -189
- package/dist/go.d.ts +2 -4
- package/dist/go.js +18 -47
- package/dist/history.d.ts +1 -0
- package/dist/history.js +12 -5
- package/dist/new.js +70 -8
- package/dist/open.d.ts +2 -2
- package/dist/open.js +22 -18
- package/dist/pr.js +3 -3
- package/dist/remove.d.ts +3 -2
- package/dist/remove.js +7 -12
- package/dist/repo.d.ts +0 -1
- package/dist/repo.js +0 -9
- package/dist/warp.js +24 -53
- package/dist/worktree-info.d.ts +0 -1
- package/dist/worktree-info.js +17 -11
- package/dist/worktree-picker.d.ts +14 -0
- package/dist/worktree-picker.js +228 -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 +1 -1
- package/man/man1/gji-open.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-run-hook.1 +1 -1
- package/man/man1/gji-status.1 +1 -1
- package/man/man1/gji-sync-files.1 +1 -1
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +1 -1
- package/package.json +3 -7
package/dist/go.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type WorktreeEntry } from "./repo.js";
|
|
1
|
+
import { type WorktreePromptEntry } from "./worktree-picker.js";
|
|
3
2
|
export interface GoCommandOptions {
|
|
4
3
|
branch?: string;
|
|
5
4
|
cwd: string;
|
|
@@ -8,8 +7,7 @@ export interface GoCommandOptions {
|
|
|
8
7
|
stdout: (chunk: string) => void;
|
|
9
8
|
}
|
|
10
9
|
export interface GoCommandDependencies {
|
|
11
|
-
promptForWorktree: (worktrees:
|
|
10
|
+
promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
|
|
12
11
|
}
|
|
13
12
|
export declare function createGoCommand(dependencies?: Partial<GoCommandDependencies>): (options: GoCommandOptions) => Promise<number>;
|
|
14
13
|
export declare const runGoCommand: (options: GoCommandOptions) => Promise<number>;
|
|
15
|
-
export declare function formatUpstreamHint(branch: string | null, health: WorktreeHealth): string | null;
|
package/dist/go.js
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
-
import { isCancel, select } from "@clack/prompts";
|
|
3
2
|
import { loadEffectiveConfig } from "./config.js";
|
|
4
|
-
import { readWorktreeHealth } from "./git.js";
|
|
5
3
|
import { isHeadless } from "./headless.js";
|
|
6
|
-
import {
|
|
4
|
+
import { recordWorktreeUsage } from "./history.js";
|
|
7
5
|
import { extractHooks, runHook } from "./hooks.js";
|
|
8
|
-
import { detectRepository, listWorktrees
|
|
6
|
+
import { detectRepository, listWorktrees } from "./repo.js";
|
|
9
7
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
10
8
|
import { resolveWarpTarget } from "./warp.js";
|
|
9
|
+
import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
|
|
11
10
|
const GO_OUTPUT_FILE_ENV = "GJI_GO_OUTPUT_FILE";
|
|
12
11
|
export function createGoCommand(dependencies = {}) {
|
|
13
12
|
const prompt = dependencies.promptForWorktree ?? promptForWorktree;
|
|
@@ -32,7 +31,7 @@ export function createGoCommand(dependencies = {}) {
|
|
|
32
31
|
});
|
|
33
32
|
if (!target)
|
|
34
33
|
return 1;
|
|
35
|
-
|
|
34
|
+
await recordWorktreeUsage(target.path, target.branch);
|
|
36
35
|
await writeShellOutput(GO_OUTPUT_FILE_ENV, target.path, options.stdout);
|
|
37
36
|
return 0;
|
|
38
37
|
}
|
|
@@ -40,11 +39,19 @@ export function createGoCommand(dependencies = {}) {
|
|
|
40
39
|
options.stderr("gji go: branch argument is required in non-interactive mode (GJI_NO_TUI=1)\n");
|
|
41
40
|
return 1;
|
|
42
41
|
}
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
const promptSources = worktrees.map((worktree) => ({
|
|
43
|
+
repoName: repository.repoName,
|
|
44
|
+
worktree,
|
|
45
|
+
}));
|
|
46
|
+
const promptEntries = options.branch
|
|
47
|
+
? []
|
|
48
|
+
: await buildWorktreePromptEntries(promptSources);
|
|
49
|
+
const queried = options.branch
|
|
50
|
+
? resolveWorktreeQuery(promptSources, options.branch)
|
|
51
|
+
: null;
|
|
52
|
+
const prompted = options.branch ? null : await prompt(promptEntries);
|
|
46
53
|
const resolvedPath = options.branch
|
|
47
|
-
?
|
|
54
|
+
? queried?.worktree.path
|
|
48
55
|
: (prompted ?? undefined);
|
|
49
56
|
if (!resolvedPath) {
|
|
50
57
|
if (options.branch) {
|
|
@@ -64,48 +71,12 @@ export function createGoCommand(dependencies = {}) {
|
|
|
64
71
|
path: resolvedPath,
|
|
65
72
|
repo: basename(repository.repoRoot),
|
|
66
73
|
}, options.stderr);
|
|
67
|
-
|
|
74
|
+
await recordWorktreeUsage(resolvedPath, chosenWorktree?.branch ?? null);
|
|
68
75
|
await writeShellOutput(GO_OUTPUT_FILE_ENV, resolvedPath, options.stdout);
|
|
69
76
|
return 0;
|
|
70
77
|
};
|
|
71
78
|
}
|
|
72
79
|
export const runGoCommand = createGoCommand();
|
|
73
80
|
async function promptForWorktree(worktrees) {
|
|
74
|
-
|
|
75
|
-
const choice = await select({
|
|
76
|
-
message: "Choose a worktree",
|
|
77
|
-
options: worktrees.map((worktree, i) => {
|
|
78
|
-
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
79
|
-
const pathHint = worktree.isCurrent
|
|
80
|
-
? `${worktree.path} (current)`
|
|
81
|
-
: worktree.path;
|
|
82
|
-
const upstream = health
|
|
83
|
-
? formatUpstreamHint(worktree.branch, health)
|
|
84
|
-
: null;
|
|
85
|
-
return {
|
|
86
|
-
value: worktree.path,
|
|
87
|
-
label: worktree.branch ?? "(detached)",
|
|
88
|
-
hint: upstream ? `${upstream} · ${pathHint}` : pathHint,
|
|
89
|
-
};
|
|
90
|
-
}),
|
|
91
|
-
});
|
|
92
|
-
if (isCancel(choice)) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return choice;
|
|
96
|
-
}
|
|
97
|
-
export function formatUpstreamHint(branch, health) {
|
|
98
|
-
if (branch === null)
|
|
99
|
-
return null;
|
|
100
|
-
if (!health.hasUpstream)
|
|
101
|
-
return "no upstream";
|
|
102
|
-
if (health.upstreamGone)
|
|
103
|
-
return "upstream gone";
|
|
104
|
-
if (health.ahead === 0 && health.behind === 0)
|
|
105
|
-
return "up to date";
|
|
106
|
-
if (health.ahead === 0)
|
|
107
|
-
return `behind ${health.behind}`;
|
|
108
|
-
if (health.behind === 0)
|
|
109
|
-
return `ahead ${health.ahead}`;
|
|
110
|
-
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
81
|
+
return promptForSingleWorktree("Choose a worktree", worktrees);
|
|
111
82
|
}
|
package/dist/history.d.ts
CHANGED
|
@@ -7,3 +7,4 @@ export interface HistoryEntry {
|
|
|
7
7
|
export declare function HISTORY_FILE_PATH(home?: string): string;
|
|
8
8
|
export declare function loadHistory(home?: string): Promise<HistoryEntry[]>;
|
|
9
9
|
export declare function appendHistory(path: string, branch: string | null, home?: string): Promise<void>;
|
|
10
|
+
export declare function recordWorktreeUsage(path: string, branch: string | null, home?: string): Promise<void>;
|
package/dist/history.js
CHANGED
|
@@ -27,15 +27,22 @@ export async function loadHistory(home = homedir()) {
|
|
|
27
27
|
export async function appendHistory(path, branch, home = homedir()) {
|
|
28
28
|
const historyPath = HISTORY_FILE_PATH(home);
|
|
29
29
|
const existing = await loadHistory(home);
|
|
30
|
-
// Skip if the most recent entry is the same path (no-op navigation)
|
|
31
|
-
if (existing.length > 0 && existing[0].path === path) {
|
|
32
|
-
return;
|
|
33
|
-
}
|
|
34
30
|
const entry = { branch, path, timestamp: Date.now() };
|
|
35
|
-
const next = [
|
|
31
|
+
const next = [
|
|
32
|
+
entry,
|
|
33
|
+
...existing.filter((existingEntry) => existingEntry.path !== path),
|
|
34
|
+
].slice(0, MAX_HISTORY_ENTRIES);
|
|
36
35
|
await mkdir(dirname(historyPath), { recursive: true });
|
|
37
36
|
await writeFile(historyPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
38
37
|
}
|
|
38
|
+
export async function recordWorktreeUsage(path, branch, home = homedir()) {
|
|
39
|
+
try {
|
|
40
|
+
await appendHistory(path, branch, home);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Usage history is advisory metadata; primary command success should stand.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
39
46
|
function isHistoryEntry(value) {
|
|
40
47
|
return (typeof value === "object" &&
|
|
41
48
|
value !== null &&
|
package/dist/new.js
CHANGED
|
@@ -8,7 +8,7 @@ import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
|
8
8
|
import { defaultSpawnEditor, EDITORS } from "./editor.js";
|
|
9
9
|
import { syncFiles } from "./file-sync.js";
|
|
10
10
|
import { isHeadless } from "./headless.js";
|
|
11
|
-
import {
|
|
11
|
+
import { recordWorktreeUsage } from "./history.js";
|
|
12
12
|
import { extractHooks, runHook } from "./hooks.js";
|
|
13
13
|
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
14
14
|
import { detectRepository, resolveWorktreePath, validateBranchName, } from "./repo.js";
|
|
@@ -117,7 +117,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
117
117
|
else {
|
|
118
118
|
const choice = await prompt(worktreePath);
|
|
119
119
|
if (choice === "reuse") {
|
|
120
|
-
|
|
120
|
+
await recordWorktreeUsage(worktreePath, worktreeName);
|
|
121
121
|
await writeOutput(worktreePath, options.stdout);
|
|
122
122
|
return 0;
|
|
123
123
|
}
|
|
@@ -170,7 +170,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
170
170
|
options.stdout(`${JSON.stringify({ branch: worktreeName, path: worktreePath }, null, 2)}\n`);
|
|
171
171
|
}
|
|
172
172
|
else {
|
|
173
|
-
await
|
|
173
|
+
await recordWorktreeUsage(worktreePath, worktreeName);
|
|
174
174
|
await writeOutput(worktreePath, options.stdout);
|
|
175
175
|
}
|
|
176
176
|
if (options.open) {
|
|
@@ -203,6 +203,31 @@ export function generateBranchPlaceholder(random = Math.random) {
|
|
|
203
203
|
"lovelace",
|
|
204
204
|
"nietzsche",
|
|
205
205
|
"kafka",
|
|
206
|
+
"sappho",
|
|
207
|
+
"aristotle",
|
|
208
|
+
"pythagoras",
|
|
209
|
+
"artemis",
|
|
210
|
+
"apollo",
|
|
211
|
+
"minerva",
|
|
212
|
+
"persephone",
|
|
213
|
+
"icarus",
|
|
214
|
+
"odysseus",
|
|
215
|
+
"murasaki",
|
|
216
|
+
"shakespeare",
|
|
217
|
+
"frida",
|
|
218
|
+
"davinci",
|
|
219
|
+
"kepler",
|
|
220
|
+
"copernicus",
|
|
221
|
+
"faraday",
|
|
222
|
+
"noether",
|
|
223
|
+
"hopper",
|
|
224
|
+
"boole",
|
|
225
|
+
"shannon",
|
|
226
|
+
"gauss",
|
|
227
|
+
"ramanujan",
|
|
228
|
+
"austen",
|
|
229
|
+
"borges",
|
|
230
|
+
"zeno",
|
|
206
231
|
];
|
|
207
232
|
const antics = [
|
|
208
233
|
"borrowed-a-bike",
|
|
@@ -220,8 +245,49 @@ export function generateBranchPlaceholder(random = Math.random) {
|
|
|
220
245
|
"washed-the-dishes",
|
|
221
246
|
"folded-the-laundry",
|
|
222
247
|
"took-a-nap",
|
|
248
|
+
"lost-a-sock",
|
|
249
|
+
"patched-the-boat",
|
|
250
|
+
"alphabetized-the-spoons",
|
|
251
|
+
"argued-with-the-calendar",
|
|
252
|
+
"misplaced-the-moon",
|
|
253
|
+
"painted-the-fence",
|
|
254
|
+
"overcooked-the-rice",
|
|
255
|
+
"packed-the-snacks",
|
|
256
|
+
"dropped-the-spoon",
|
|
257
|
+
"hid-the-remote",
|
|
258
|
+
"untangled-the-cables",
|
|
259
|
+
"rebooted-the-kettle",
|
|
260
|
+
"indexed-the-attic",
|
|
261
|
+
"forgot-the-password",
|
|
262
|
+
"sorted-the-buttons",
|
|
263
|
+
"mopped-the-ceiling",
|
|
264
|
+
"polished-the-doorknob",
|
|
265
|
+
"misread-the-map",
|
|
266
|
+
"reheated-the-tea",
|
|
267
|
+
"fixed-the-squeak",
|
|
268
|
+
"labeled-the-drawer",
|
|
269
|
+
"stacked-the-chairs",
|
|
270
|
+
"overslept-the-standup",
|
|
271
|
+
"claimed-the-last-bagel",
|
|
272
|
+
"debugged-the-toaster",
|
|
223
273
|
];
|
|
224
|
-
|
|
274
|
+
const root = pickRandom(roots, random);
|
|
275
|
+
const antic = pickRandom(antics, random);
|
|
276
|
+
const suffix = generateBranchPlaceholderSuffix(random);
|
|
277
|
+
return `${root}-${antic}-${suffix}`;
|
|
278
|
+
}
|
|
279
|
+
function pickRandom(values, random) {
|
|
280
|
+
const index = Math.floor(random() * values.length);
|
|
281
|
+
return values[Math.min(index, values.length - 1)];
|
|
282
|
+
}
|
|
283
|
+
function generateBranchPlaceholderSuffix(random) {
|
|
284
|
+
const characters = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
285
|
+
let suffix = "";
|
|
286
|
+
for (let index = 0; index < 3; index += 1) {
|
|
287
|
+
const characterIndex = Math.floor(random() * characters.length);
|
|
288
|
+
suffix += characters[Math.min(characterIndex, characters.length - 1)];
|
|
289
|
+
}
|
|
290
|
+
return suffix;
|
|
225
291
|
}
|
|
226
292
|
function applyConfiguredBranchPrefix(branch, branchPrefix) {
|
|
227
293
|
if (typeof branchPrefix !== "string" || branchPrefix.length === 0) {
|
|
@@ -258,10 +324,6 @@ async function defaultPromptForBranch(placeholder) {
|
|
|
258
324
|
}
|
|
259
325
|
return choice.trim();
|
|
260
326
|
}
|
|
261
|
-
function pickRandom(values, random) {
|
|
262
|
-
const index = Math.floor(random() * values.length);
|
|
263
|
-
return values[Math.min(index, values.length - 1)];
|
|
264
|
-
}
|
|
265
327
|
async function localBranchExists(repoRoot, branchName) {
|
|
266
328
|
try {
|
|
267
329
|
await execFileAsync("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branchName}`], { cwd: repoRoot });
|
package/dist/open.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type EditorDefinition } from "./editor.js";
|
|
2
|
-
import { type
|
|
2
|
+
import { type WorktreePromptEntry } from "./worktree-picker.js";
|
|
3
3
|
export type { EditorDefinition };
|
|
4
4
|
export interface OpenCommandOptions {
|
|
5
5
|
branch?: string;
|
|
@@ -13,7 +13,7 @@ export interface OpenCommandOptions {
|
|
|
13
13
|
export interface OpenCommandDependencies {
|
|
14
14
|
detectEditors: () => Promise<EditorDefinition[]>;
|
|
15
15
|
promptForEditor: (editors: EditorDefinition[]) => Promise<string | null>;
|
|
16
|
-
promptForWorktree: (worktrees:
|
|
16
|
+
promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
|
|
17
17
|
spawnEditor: (cli: string, args: string[]) => Promise<void>;
|
|
18
18
|
}
|
|
19
19
|
export declare function createOpenCommand(dependencies?: Partial<OpenCommandDependencies>): (options: OpenCommandOptions) => Promise<number>;
|
package/dist/open.js
CHANGED
|
@@ -6,7 +6,9 @@ import { isCancel, select } from "@clack/prompts";
|
|
|
6
6
|
import { loadEffectiveConfig, resolveConfigString, updateGlobalConfigKey, } from "./config.js";
|
|
7
7
|
import { defaultSpawnEditor, EDITORS, } from "./editor.js";
|
|
8
8
|
import { isHeadless } from "./headless.js";
|
|
9
|
-
import {
|
|
9
|
+
import { recordWorktreeUsage } from "./history.js";
|
|
10
|
+
import { detectRepository, listWorktrees } from "./repo.js";
|
|
11
|
+
import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
|
|
10
12
|
const execFileAsync = promisify(execFile);
|
|
11
13
|
export function createOpenCommand(dependencies = {}) {
|
|
12
14
|
const detectEditors = dependencies.detectEditors ?? detectInstalledEditors;
|
|
@@ -20,25 +22,36 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
20
22
|
]);
|
|
21
23
|
// Resolve target worktree path.
|
|
22
24
|
let targetPath;
|
|
25
|
+
let targetWorktree;
|
|
23
26
|
if (options.branch) {
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const match = resolveWorktreeQuery(worktrees.map((worktree) => ({
|
|
28
|
+
repoName: repository.repoName,
|
|
29
|
+
worktree,
|
|
30
|
+
})), options.branch);
|
|
31
|
+
if (!match) {
|
|
32
|
+
options.stderr(`gji open: no worktree found matching: ${options.branch}\n`);
|
|
27
33
|
options.stderr(`Hint: Use 'gji ls' to see available worktrees\n`);
|
|
28
34
|
return 1;
|
|
29
35
|
}
|
|
30
|
-
targetPath =
|
|
36
|
+
targetPath = match.worktree.path;
|
|
37
|
+
targetWorktree = match.worktree;
|
|
31
38
|
}
|
|
32
39
|
else if (isHeadless()) {
|
|
33
|
-
|
|
40
|
+
targetWorktree = worktrees.find((w) => w.isCurrent);
|
|
41
|
+
targetPath = targetWorktree?.path ?? options.cwd;
|
|
34
42
|
}
|
|
35
43
|
else {
|
|
36
|
-
const
|
|
44
|
+
const entries = await buildWorktreePromptEntries(worktrees.map((worktree) => ({
|
|
45
|
+
repoName: repository.repoName,
|
|
46
|
+
worktree,
|
|
47
|
+
})));
|
|
48
|
+
const chosen = await promptForWorktree(entries);
|
|
37
49
|
if (!chosen) {
|
|
38
50
|
options.stderr("Aborted\n");
|
|
39
51
|
return 1;
|
|
40
52
|
}
|
|
41
53
|
targetPath = chosen;
|
|
54
|
+
targetWorktree = worktrees.find((w) => w.path === chosen);
|
|
42
55
|
}
|
|
43
56
|
// Resolve which editor to use.
|
|
44
57
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
@@ -100,6 +113,7 @@ export function createOpenCommand(dependencies = {}) {
|
|
|
100
113
|
return 1;
|
|
101
114
|
}
|
|
102
115
|
const displayName = editorDef?.name ?? editorCli;
|
|
116
|
+
await recordWorktreeUsage(targetPath, targetWorktree?.branch ?? null);
|
|
103
117
|
options.stdout(`Opened ${targetPath} in ${displayName}\n`);
|
|
104
118
|
return 0;
|
|
105
119
|
};
|
|
@@ -122,17 +136,7 @@ async function isCommandAvailable(command) {
|
|
|
122
136
|
}
|
|
123
137
|
}
|
|
124
138
|
async function defaultPromptForWorktree(worktrees) {
|
|
125
|
-
|
|
126
|
-
message: "Choose a worktree to open",
|
|
127
|
-
options: worktrees.map((w) => ({
|
|
128
|
-
value: w.path,
|
|
129
|
-
label: w.branch ?? "(detached)",
|
|
130
|
-
hint: w.isCurrent ? `${w.path} (current)` : w.path,
|
|
131
|
-
})),
|
|
132
|
-
});
|
|
133
|
-
if (isCancel(choice))
|
|
134
|
-
return null;
|
|
135
|
-
return choice;
|
|
139
|
+
return promptForSingleWorktree("Choose a worktree to open", worktrees);
|
|
136
140
|
}
|
|
137
141
|
async function defaultPromptForEditor(editors) {
|
|
138
142
|
const choice = await select({
|
package/dist/pr.js
CHANGED
|
@@ -6,7 +6,7 @@ import { loadEffectiveConfig, resolveConfigString } from "./config.js";
|
|
|
6
6
|
import { pathExists, promptForPathConflict, } from "./conflict.js";
|
|
7
7
|
import { syncFiles } from "./file-sync.js";
|
|
8
8
|
import { isHeadless } from "./headless.js";
|
|
9
|
-
import {
|
|
9
|
+
import { recordWorktreeUsage } from "./history.js";
|
|
10
10
|
import { extractHooks, runHook } from "./hooks.js";
|
|
11
11
|
import { maybeRunInstallPrompt, } from "./install-prompt.js";
|
|
12
12
|
import { detectRepository, resolveWorktreePath } from "./repo.js";
|
|
@@ -61,7 +61,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
const choice = await prompt(worktreePath);
|
|
63
63
|
if (choice === "reuse") {
|
|
64
|
-
|
|
64
|
+
await recordWorktreeUsage(worktreePath, branchName);
|
|
65
65
|
await writeOutput(worktreePath, options.stdout);
|
|
66
66
|
return 0;
|
|
67
67
|
}
|
|
@@ -120,7 +120,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
120
120
|
options.stdout(`${JSON.stringify({ branch: branchName, path: worktreePath }, null, 2)}\n`);
|
|
121
121
|
}
|
|
122
122
|
else {
|
|
123
|
-
await
|
|
123
|
+
await recordWorktreeUsage(worktreePath, branchName);
|
|
124
124
|
await writeOutput(worktreePath, options.stdout);
|
|
125
125
|
}
|
|
126
126
|
return 0;
|
package/dist/remove.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { WorktreeEntry } from "./repo.js";
|
|
2
|
+
import { type WorktreePromptEntry } from "./worktree-picker.js";
|
|
2
3
|
export interface RemoveCommandOptions {
|
|
3
4
|
branch?: string;
|
|
4
5
|
cwd: string;
|
|
@@ -12,7 +13,7 @@ export interface RemoveCommandDependencies {
|
|
|
12
13
|
confirmForceDeleteBranch: (branch: string) => Promise<boolean>;
|
|
13
14
|
confirmForceRemoveWorktree: (worktreePath: string) => Promise<boolean>;
|
|
14
15
|
confirmRemoval: (worktree: WorktreeEntry) => Promise<boolean>;
|
|
15
|
-
promptForWorktree: (worktrees:
|
|
16
|
+
promptForWorktree: (worktrees: WorktreePromptEntry[]) => Promise<string | null>;
|
|
16
17
|
}
|
|
17
18
|
export declare function createRemoveCommand(dependencies?: Partial<RemoveCommandDependencies>): (options: RemoveCommandOptions) => Promise<number>;
|
|
18
19
|
export declare const runRemoveCommand: (options: RemoveCommandOptions) => Promise<number>;
|
package/dist/remove.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { basename } from "node:path";
|
|
2
|
-
import { confirm, isCancel
|
|
2
|
+
import { confirm, isCancel } from "@clack/prompts";
|
|
3
3
|
import { loadEffectiveConfig } from "./config.js";
|
|
4
4
|
import { isHeadless } from "./headless.js";
|
|
5
5
|
import { extractHooks, runHook } from "./hooks.js";
|
|
6
|
-
import { sortByCurrentFirst } from "./repo.js";
|
|
7
6
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
8
7
|
import { deleteBranch, forceDeleteBranch, forceRemoveWorktree, isBranchUnmergedError, isWorktreeDirtyError, loadLinkedWorktrees, removeWorktree, } from "./worktree-management.js";
|
|
8
|
+
import { buildWorktreePromptEntries, promptForSingleWorktree, } from "./worktree-picker.js";
|
|
9
9
|
import { defaultConfirmForceDeleteBranch, defaultConfirmForceRemoveWorktree, } from "./worktree-prompts.js";
|
|
10
10
|
const REMOVE_OUTPUT_FILE_ENV = "GJI_REMOVE_OUTPUT_FILE";
|
|
11
11
|
export function createRemoveCommand(dependencies = {}) {
|
|
@@ -31,7 +31,10 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
31
31
|
return 1;
|
|
32
32
|
}
|
|
33
33
|
const selection = options.branch ??
|
|
34
|
-
(await promptForWorktree(
|
|
34
|
+
(await promptForWorktree(await buildWorktreePromptEntries(linkedWorktrees.map((worktree) => ({
|
|
35
|
+
repoName: repository.repoName,
|
|
36
|
+
worktree,
|
|
37
|
+
})))));
|
|
35
38
|
if (!selection) {
|
|
36
39
|
options.stderr("Aborted\n");
|
|
37
40
|
return 1;
|
|
@@ -129,15 +132,7 @@ export function createRemoveCommand(dependencies = {}) {
|
|
|
129
132
|
}
|
|
130
133
|
export const runRemoveCommand = createRemoveCommand();
|
|
131
134
|
async function defaultPromptForWorktree(worktrees) {
|
|
132
|
-
|
|
133
|
-
message: "Choose a worktree to finish",
|
|
134
|
-
options: worktrees.map((worktree) => ({
|
|
135
|
-
hint: worktree.isCurrent ? `${worktree.path} (current)` : worktree.path,
|
|
136
|
-
label: worktree.branch ?? "(detached)",
|
|
137
|
-
value: worktree.path,
|
|
138
|
-
})),
|
|
139
|
-
});
|
|
140
|
-
return isCancel(choice) ? null : choice;
|
|
135
|
+
return promptForSingleWorktree("Choose a worktree to finish", worktrees);
|
|
141
136
|
}
|
|
142
137
|
async function defaultConfirmRemoval(worktree) {
|
|
143
138
|
const choice = await confirm({
|
package/dist/repo.d.ts
CHANGED
|
@@ -14,4 +14,3 @@ export declare function detectRepository(cwd: string): Promise<RepositoryContext
|
|
|
14
14
|
export declare function resolveWorktreePath(repoRoot: string, branch: string, basePath?: string): string;
|
|
15
15
|
export declare function validateBranchName(name: string): string | null;
|
|
16
16
|
export declare function listWorktrees(cwd: string): Promise<WorktreeEntry[]>;
|
|
17
|
-
export declare function sortByCurrentFirst(worktrees: WorktreeEntry[]): WorktreeEntry[];
|
package/dist/repo.js
CHANGED
|
@@ -88,15 +88,6 @@ export async function listWorktrees(cwd) {
|
|
|
88
88
|
};
|
|
89
89
|
});
|
|
90
90
|
}
|
|
91
|
-
export function sortByCurrentFirst(worktrees) {
|
|
92
|
-
return [...worktrees].sort((a, b) => {
|
|
93
|
-
if (a.isCurrent && !b.isCurrent)
|
|
94
|
-
return -1;
|
|
95
|
-
if (!a.isCurrent && b.isCurrent)
|
|
96
|
-
return 1;
|
|
97
|
-
return 0;
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
91
|
function findPorcelainValue(block, key) {
|
|
101
92
|
const value = findOptionalPorcelainValue(block, key);
|
|
102
93
|
if (!value) {
|
package/dist/warp.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { realpath } from "node:fs/promises";
|
|
2
2
|
import { basename, resolve } from "node:path";
|
|
3
3
|
import { isCancel, select } from "@clack/prompts";
|
|
4
|
-
import { readWorktreeHealth } from "./git.js";
|
|
5
4
|
import { isHeadless } from "./headless.js";
|
|
6
|
-
import {
|
|
5
|
+
import { recordWorktreeUsage } from "./history.js";
|
|
7
6
|
import { runNewCommand } from "./new.js";
|
|
8
|
-
import { listWorktrees } from "./repo.js";
|
|
7
|
+
import { detectRepository, listWorktrees } from "./repo.js";
|
|
9
8
|
import { loadRegistry } from "./repo-registry.js";
|
|
10
9
|
import { writeShellOutput } from "./shell-handoff.js";
|
|
10
|
+
import { buildWorktreePromptEntries, promptForSingleWorktree, resolveWorktreeQuery, } from "./worktree-picker.js";
|
|
11
11
|
const WARP_OUTPUT_FILE_ENV = "GJI_WARP_OUTPUT_FILE";
|
|
12
12
|
export async function runWarpCommand(options) {
|
|
13
13
|
if (options.newWorktree) {
|
|
@@ -44,7 +44,7 @@ async function runWarpNavigate(options) {
|
|
|
44
44
|
options.stdout(`${JSON.stringify({ branch: target.branch, path: target.path }, null, 2)}\n`);
|
|
45
45
|
return 0;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
await recordWorktreeUsage(target.path, target.branch);
|
|
48
48
|
await writeShellOutput(WARP_OUTPUT_FILE_ENV, target.path, options.stdout);
|
|
49
49
|
return 0;
|
|
50
50
|
}
|
|
@@ -130,17 +130,6 @@ async function canonicalizeRepoPath(repoPath) {
|
|
|
130
130
|
return resolve(repoPath);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
function findByQuery(items, query) {
|
|
134
|
-
const slashIdx = query.indexOf("/");
|
|
135
|
-
if (slashIdx !== -1) {
|
|
136
|
-
const repoQuery = query.slice(0, slashIdx);
|
|
137
|
-
const branchQuery = query.slice(slashIdx + 1);
|
|
138
|
-
const match = items.find((item) => item.repoName === repoQuery && item.worktree.branch === branchQuery);
|
|
139
|
-
if (match)
|
|
140
|
-
return match;
|
|
141
|
-
}
|
|
142
|
-
return items.find((item) => item.worktree.branch === query) ?? null;
|
|
143
|
-
}
|
|
144
133
|
export async function resolveWarpTarget(options) {
|
|
145
134
|
const cmd = options.commandName ?? "gji";
|
|
146
135
|
const emitError = (message, hint) => {
|
|
@@ -154,6 +143,9 @@ export async function resolveWarpTarget(options) {
|
|
|
154
143
|
}
|
|
155
144
|
};
|
|
156
145
|
const registry = await loadRegistry();
|
|
146
|
+
const currentRoot = await detectRepository(options.cwd)
|
|
147
|
+
.then((repository) => repository.currentRoot)
|
|
148
|
+
.catch(() => null);
|
|
157
149
|
if (registry.length === 0) {
|
|
158
150
|
emitError("not in a git repository and no repos registered yet.", "Use any gji command inside a repository to register it.\n");
|
|
159
151
|
return null;
|
|
@@ -168,61 +160,40 @@ export async function resolveWarpTarget(options) {
|
|
|
168
160
|
continue;
|
|
169
161
|
const { repoName, worktrees } = result.value;
|
|
170
162
|
for (const worktree of worktrees) {
|
|
171
|
-
allItems.push({
|
|
163
|
+
allItems.push({
|
|
164
|
+
repoName,
|
|
165
|
+
worktree: {
|
|
166
|
+
...worktree,
|
|
167
|
+
isCurrent: currentRoot !== null && worktree.path === currentRoot,
|
|
168
|
+
},
|
|
169
|
+
});
|
|
172
170
|
}
|
|
173
171
|
}
|
|
174
172
|
if (allItems.length === 0) {
|
|
175
173
|
emitError("no accessible worktrees found in any registered repo.");
|
|
176
174
|
return null;
|
|
177
175
|
}
|
|
176
|
+
const promptSources = allItems.map((item) => ({
|
|
177
|
+
repoName: item.repoName,
|
|
178
|
+
worktree: item.worktree,
|
|
179
|
+
}));
|
|
178
180
|
if (options.branch) {
|
|
179
|
-
const match =
|
|
181
|
+
const match = resolveWorktreeQuery(promptSources, options.branch);
|
|
180
182
|
if (!match) {
|
|
181
183
|
emitError(`no worktree found matching: ${options.branch}`);
|
|
182
184
|
return null;
|
|
183
185
|
}
|
|
184
186
|
return { branch: match.worktree.branch, path: match.worktree.path };
|
|
185
187
|
}
|
|
186
|
-
const
|
|
188
|
+
const promptEntries = await buildWorktreePromptEntries(promptSources);
|
|
189
|
+
const path = await promptForWarpTarget(promptEntries);
|
|
187
190
|
if (!path) {
|
|
188
191
|
options.stderr("Aborted\n");
|
|
189
192
|
return null;
|
|
190
193
|
}
|
|
191
|
-
const chosen =
|
|
192
|
-
return { branch: chosen?.
|
|
194
|
+
const chosen = promptEntries.find((item) => item.path === path);
|
|
195
|
+
return { branch: chosen?.branch ?? null, path };
|
|
193
196
|
}
|
|
194
197
|
async function promptForWarpTarget(items) {
|
|
195
|
-
|
|
196
|
-
const choice = await select({
|
|
197
|
-
message: "Warp to a worktree",
|
|
198
|
-
options: items.map((item, i) => {
|
|
199
|
-
const health = healthResults[i].status === "fulfilled" ? healthResults[i].value : null;
|
|
200
|
-
const upstream = health ? formatHint(item.worktree.branch, health) : null;
|
|
201
|
-
const label = `${item.repoName} › ${item.worktree.branch ?? "(detached)"}`;
|
|
202
|
-
const pathHint = item.worktree.isCurrent
|
|
203
|
-
? `${item.worktree.path} (current)`
|
|
204
|
-
: item.worktree.path;
|
|
205
|
-
const hint = upstream ? `${upstream} · ${pathHint}` : pathHint;
|
|
206
|
-
return { hint, label, value: item.worktree.path };
|
|
207
|
-
}),
|
|
208
|
-
});
|
|
209
|
-
if (isCancel(choice)) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
return choice;
|
|
213
|
-
}
|
|
214
|
-
function formatHint(branch, health) {
|
|
215
|
-
if (branch === null)
|
|
216
|
-
return null;
|
|
217
|
-
if (!health.hasUpstream)
|
|
218
|
-
return "no upstream";
|
|
219
|
-
if (health.upstreamGone)
|
|
220
|
-
return "upstream gone";
|
|
221
|
-
if (health.ahead === 0 && health.behind === 0)
|
|
222
|
-
return "up to date";
|
|
223
|
-
if (health.ahead === 0)
|
|
224
|
-
return `behind ${health.behind}`;
|
|
225
|
-
if (health.behind === 0)
|
|
226
|
-
return `ahead ${health.ahead}`;
|
|
227
|
-
return `ahead ${health.ahead}, behind ${health.behind}`;
|
|
198
|
+
return promptForSingleWorktree("Warp to a worktree", items);
|
|
228
199
|
}
|
package/dist/worktree-info.d.ts
CHANGED
|
@@ -27,7 +27,6 @@ export type UpstreamState = {
|
|
|
27
27
|
};
|
|
28
28
|
export declare function readWorktreeInfos(worktrees: WorktreeEntry[]): Promise<WorktreeInfo[]>;
|
|
29
29
|
export declare function serializeWorktreeInfo(info: WorktreeInfo): SerializedWorktreeInfo;
|
|
30
|
-
export declare function formatWorktreeHint(info: WorktreeInfo): string;
|
|
31
30
|
export declare function formatUpstreamState(upstream: UpstreamState): string;
|
|
32
31
|
export declare function formatLastCommit(timestampSeconds: number | null): string;
|
|
33
32
|
export declare function formatRelativeAge(timestampSeconds: number): string;
|
package/dist/worktree-info.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { readBranchLastCommitTimestamp, readWorktreeHealth, } from "./git.js";
|
|
2
|
+
const MAX_WORKTREE_INFO_READ_CONCURRENCY = 8;
|
|
2
3
|
export async function readWorktreeInfos(worktrees) {
|
|
3
|
-
return
|
|
4
|
+
return mapWithConcurrency(worktrees, MAX_WORKTREE_INFO_READ_CONCURRENCY, readWorktreeInfo);
|
|
5
|
+
}
|
|
6
|
+
async function mapWithConcurrency(items, limit, mapper) {
|
|
7
|
+
const results = new Array(items.length);
|
|
8
|
+
let nextIndex = 0;
|
|
9
|
+
async function readNext() {
|
|
10
|
+
for (;;) {
|
|
11
|
+
const index = nextIndex;
|
|
12
|
+
nextIndex += 1;
|
|
13
|
+
if (index >= items.length)
|
|
14
|
+
return;
|
|
15
|
+
results[index] = await mapper(items[index]);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => readNext()));
|
|
19
|
+
return results;
|
|
4
20
|
}
|
|
5
21
|
async function readWorktreeInfo(worktree) {
|
|
6
22
|
const [healthResult, lastCommitResult] = await Promise.allSettled([
|
|
@@ -46,16 +62,6 @@ export function serializeWorktreeInfo(info) {
|
|
|
46
62
|
upstream: info.upstream,
|
|
47
63
|
};
|
|
48
64
|
}
|
|
49
|
-
export function formatWorktreeHint(info) {
|
|
50
|
-
const details = [
|
|
51
|
-
`status: ${info.status}`,
|
|
52
|
-
`upstream: ${formatUpstreamState(info.upstream)}`,
|
|
53
|
-
];
|
|
54
|
-
if (info.lastCommitTimestamp !== null) {
|
|
55
|
-
details.push(`last: ${formatRelativeAge(info.lastCommitTimestamp)}`);
|
|
56
|
-
}
|
|
57
|
-
return `${info.path} (${details.join(", ")})`;
|
|
58
|
-
}
|
|
59
65
|
export function formatUpstreamState(upstream) {
|
|
60
66
|
if (upstream.kind === "detached") {
|
|
61
67
|
return "n/a";
|