@solaqua/gji 0.7.0 → 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/back.js +1 -1
- package/dist/clean.d.ts +2 -1
- package/dist/clean.js +8 -16
- package/dist/cli.js +7 -6
- package/dist/gji-bundle.mjs +568 -273
- package/dist/go.d.ts +2 -4
- package/dist/go.js +19 -48
- package/dist/history.d.ts +1 -0
- package/dist/history.js +12 -5
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +3 -3
- package/dist/init.d.ts +3 -3
- package/dist/init.js +12 -12
- package/dist/install-prompt.js +22 -21
- package/dist/new.js +72 -10
- package/dist/open.d.ts +2 -2
- package/dist/open.js +22 -18
- package/dist/pr.js +4 -4
- package/dist/remove.d.ts +3 -2
- package/dist/remove.js +8 -13
- package/dist/repo.d.ts +0 -1
- package/dist/repo.js +0 -9
- package/dist/run-hook.d.ts +6 -0
- package/dist/{trigger-hook.js → run-hook.js} +13 -7
- package/dist/shell-completion.js +5 -5
- 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 +9 -0
- 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 +6 -4
- package/package.json +3 -7
- package/dist/trigger-hook.d.ts +0 -6
- package/man/man1/gji-trigger-hook.1 +0 -9
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) {
|
|
@@ -59,53 +66,17 @@ export function createGoCommand(dependencies = {}) {
|
|
|
59
66
|
const chosenWorktree = worktrees.find((w) => w.path === resolvedPath);
|
|
60
67
|
const config = await loadEffectiveConfig(repository.repoRoot, undefined, options.stderr);
|
|
61
68
|
const hooks = extractHooks(config);
|
|
62
|
-
await runHook(hooks
|
|
69
|
+
await runHook(hooks["after-enter"], resolvedPath, {
|
|
63
70
|
branch: chosenWorktree?.branch ?? undefined,
|
|
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/hooks.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export type GjiHookCommand = string | string[];
|
|
2
2
|
export interface GjiHooks {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
"after-create"?: GjiHookCommand;
|
|
4
|
+
"after-enter"?: GjiHookCommand;
|
|
5
|
+
"before-remove"?: GjiHookCommand;
|
|
6
6
|
}
|
|
7
7
|
export interface HookContext {
|
|
8
8
|
branch?: string;
|
package/dist/hooks.js
CHANGED
|
@@ -84,9 +84,9 @@ export function extractHooks(config) {
|
|
|
84
84
|
}
|
|
85
85
|
const hooks = raw;
|
|
86
86
|
return {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
"after-create": parseHookCommand(hooks["after-create"] ?? hooks.afterCreate),
|
|
88
|
+
"after-enter": parseHookCommand(hooks["after-enter"] ?? hooks.afterEnter),
|
|
89
|
+
"before-remove": parseHookCommand(hooks["before-remove"] ?? hooks.beforeRemove),
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
function parseHookCommand(value) {
|
package/dist/init.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ export type InstallSaveTarget = "local" | "global";
|
|
|
3
3
|
export interface SetupWizardResult {
|
|
4
4
|
branchPrefix?: string;
|
|
5
5
|
hooks?: {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
"after-create"?: string;
|
|
7
|
+
"after-enter"?: string;
|
|
8
|
+
"before-remove"?: string;
|
|
9
9
|
};
|
|
10
10
|
installSaveTarget: InstallSaveTarget;
|
|
11
11
|
worktreePath?: string;
|
package/dist/init.js
CHANGED
|
@@ -135,12 +135,12 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
135
135
|
if (result.worktreePath)
|
|
136
136
|
values.worktreePath = result.worktreePath;
|
|
137
137
|
const hooks = {};
|
|
138
|
-
if (result.hooks?.
|
|
139
|
-
hooks
|
|
140
|
-
if (result.hooks?.
|
|
141
|
-
hooks
|
|
142
|
-
if (result.hooks?.
|
|
143
|
-
hooks
|
|
138
|
+
if (result.hooks?.["after-create"])
|
|
139
|
+
hooks["after-create"] = result.hooks["after-create"];
|
|
140
|
+
if (result.hooks?.["after-enter"])
|
|
141
|
+
hooks["after-enter"] = result.hooks["after-enter"];
|
|
142
|
+
if (result.hooks?.["before-remove"])
|
|
143
|
+
hooks["before-remove"] = result.hooks["before-remove"];
|
|
144
144
|
if (Object.keys(hooks).length > 0)
|
|
145
145
|
values.hooks = hooks;
|
|
146
146
|
if (Object.keys(values).length === 0)
|
|
@@ -283,7 +283,7 @@ async function defaultPromptForSetup() {
|
|
|
283
283
|
return null;
|
|
284
284
|
}
|
|
285
285
|
const afterCreate = await text({
|
|
286
|
-
message: "
|
|
286
|
+
message: "after-create hook — run after creating a worktree?",
|
|
287
287
|
placeholder: "e.g. pnpm install — leave blank to skip",
|
|
288
288
|
});
|
|
289
289
|
if (isCancel(afterCreate)) {
|
|
@@ -291,7 +291,7 @@ async function defaultPromptForSetup() {
|
|
|
291
291
|
return null;
|
|
292
292
|
}
|
|
293
293
|
const afterEnter = await text({
|
|
294
|
-
message: "
|
|
294
|
+
message: "after-enter hook — run after entering a worktree?",
|
|
295
295
|
placeholder: "e.g. nvm use — leave blank to skip",
|
|
296
296
|
});
|
|
297
297
|
if (isCancel(afterEnter)) {
|
|
@@ -299,7 +299,7 @@ async function defaultPromptForSetup() {
|
|
|
299
299
|
return null;
|
|
300
300
|
}
|
|
301
301
|
const beforeRemove = await text({
|
|
302
|
-
message: "
|
|
302
|
+
message: "before-remove hook — run before removing a worktree?",
|
|
303
303
|
placeholder: "leave blank to skip",
|
|
304
304
|
});
|
|
305
305
|
if (isCancel(beforeRemove)) {
|
|
@@ -309,11 +309,11 @@ async function defaultPromptForSetup() {
|
|
|
309
309
|
outro("Setup complete!");
|
|
310
310
|
const hooks = {};
|
|
311
311
|
if (afterCreate)
|
|
312
|
-
hooks
|
|
312
|
+
hooks["after-create"] = afterCreate;
|
|
313
313
|
if (afterEnter)
|
|
314
|
-
hooks
|
|
314
|
+
hooks["after-enter"] = afterEnter;
|
|
315
315
|
if (beforeRemove)
|
|
316
|
-
hooks
|
|
316
|
+
hooks["before-remove"] = beforeRemove;
|
|
317
317
|
return {
|
|
318
318
|
branchPrefix: branchPrefix || undefined,
|
|
319
319
|
hooks: Object.keys(hooks).length > 0 ? hooks : undefined,
|
package/dist/install-prompt.js
CHANGED
|
@@ -2,15 +2,15 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { isCancel, select } from "@clack/prompts";
|
|
3
3
|
import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey, } from "./config.js";
|
|
4
4
|
import { isHeadless } from "./headless.js";
|
|
5
|
+
import { extractHooks } from "./hooks.js";
|
|
5
6
|
import { detectPackageManager, } from "./package-manager.js";
|
|
6
7
|
export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
|
|
7
8
|
// Skip in non-interactive mode — no prompt can be shown.
|
|
8
9
|
if (isHeadless() || nonInteractive) {
|
|
9
10
|
return;
|
|
10
11
|
}
|
|
11
|
-
// Skip if
|
|
12
|
-
|
|
13
|
-
if (isConfiguredHookCommand(hooks?.afterCreate)) {
|
|
12
|
+
// Skip if an after-create hook is already configured (accepts both kebab and legacy camelCase).
|
|
13
|
+
if (extractHooks(config)["after-create"]) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
16
|
// Skip if user has permanently opted out of install prompts.
|
|
@@ -43,21 +43,30 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
43
43
|
try {
|
|
44
44
|
if (saveGlobal) {
|
|
45
45
|
// Deep-merge with any existing per-repo global hooks so other keys are preserved.
|
|
46
|
-
const
|
|
46
|
+
const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
|
|
47
|
+
const existing = extractHooks({ hooks: existingRaw });
|
|
47
48
|
await writeGlobalKey(repoRoot, "hooks", {
|
|
48
|
-
...
|
|
49
|
-
|
|
49
|
+
...(existing["after-enter"] !== undefined && {
|
|
50
|
+
"after-enter": existing["after-enter"],
|
|
51
|
+
}),
|
|
52
|
+
...(existing["before-remove"] !== undefined && {
|
|
53
|
+
"before-remove": existing["before-remove"],
|
|
54
|
+
}),
|
|
55
|
+
"after-create": pm.installCommand,
|
|
50
56
|
});
|
|
51
57
|
}
|
|
52
58
|
else {
|
|
53
|
-
// Read local config hooks to deep-merge so other hook keys (e.g.
|
|
59
|
+
// Read local config hooks to deep-merge so other hook keys (e.g. after-enter) are preserved.
|
|
54
60
|
const { config: localConfig } = await loadConfig(repoRoot);
|
|
55
|
-
const
|
|
56
|
-
? localConfig.hooks
|
|
57
|
-
: {};
|
|
61
|
+
const existing = extractHooks(localConfig);
|
|
58
62
|
await writeKey(repoRoot, "hooks", {
|
|
59
|
-
...
|
|
60
|
-
|
|
63
|
+
...(existing["after-enter"] !== undefined && {
|
|
64
|
+
"after-enter": existing["after-enter"],
|
|
65
|
+
}),
|
|
66
|
+
...(existing["before-remove"] !== undefined && {
|
|
67
|
+
"before-remove": existing["before-remove"],
|
|
68
|
+
}),
|
|
69
|
+
"after-create": pm.installCommand,
|
|
61
70
|
});
|
|
62
71
|
}
|
|
63
72
|
}
|
|
@@ -122,7 +131,7 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
122
131
|
options: [
|
|
123
132
|
{ value: "yes", label: "Yes", hint: "run once" },
|
|
124
133
|
{ value: "no", label: "No", hint: "skip this time" },
|
|
125
|
-
{ value: "always", label: "Always", hint: "save as
|
|
134
|
+
{ value: "always", label: "Always", hint: "save as after-create hook" },
|
|
126
135
|
{
|
|
127
136
|
value: "never",
|
|
128
137
|
label: "Never",
|
|
@@ -138,11 +147,3 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
138
147
|
function isPlainObject(value) {
|
|
139
148
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
140
149
|
}
|
|
141
|
-
function isConfiguredHookCommand(value) {
|
|
142
|
-
if (typeof value === "string")
|
|
143
|
-
return value.length > 0;
|
|
144
|
-
return (Array.isArray(value) &&
|
|
145
|
-
value.length > 0 &&
|
|
146
|
-
value[0] !== "" &&
|
|
147
|
-
value.every((item) => typeof item === "string"));
|
|
148
|
-
}
|
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";
|
|
@@ -110,14 +110,14 @@ export function createNewCommand(dependencies = {}) {
|
|
|
110
110
|
else {
|
|
111
111
|
options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
|
|
112
112
|
options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
|
|
113
|
-
options.stderr(`Hint: Use 'gji
|
|
113
|
+
options.stderr(`Hint: Use 'gji run-hook after-create' inside the worktree to re-run setup hooks\n`);
|
|
114
114
|
}
|
|
115
115
|
return 1;
|
|
116
116
|
}
|
|
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
|
}
|
|
@@ -161,7 +161,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
161
161
|
}
|
|
162
162
|
await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
|
|
163
163
|
const hooks = extractHooks(config);
|
|
164
|
-
await runHook(hooks
|
|
164
|
+
await runHook(hooks["after-create"], worktreePath, {
|
|
165
165
|
branch: worktreeName,
|
|
166
166
|
path: worktreePath,
|
|
167
167
|
repo: basename(repository.repoRoot),
|
|
@@ -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
|
}
|
|
@@ -111,7 +111,7 @@ export function createPrCommand(dependencies = {}) {
|
|
|
111
111
|
}
|
|
112
112
|
await maybeRunInstallPrompt(worktreePath, repository.repoRoot, config, options.stderr, dependencies, !!options.json);
|
|
113
113
|
const hooks = extractHooks(config);
|
|
114
|
-
await runHook(hooks
|
|
114
|
+
await runHook(hooks["after-create"], worktreePath, {
|
|
115
115
|
branch: branchName,
|
|
116
116
|
path: worktreePath,
|
|
117
117
|
repo: basename(repository.repoRoot),
|
|
@@ -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>;
|