@solaqua/gji 0.6.2 → 0.7.1
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 +26 -1
- package/dist/back.d.ts +1 -1
- package/dist/back.js +23 -17
- package/dist/clean.d.ts +1 -1
- package/dist/clean.js +44 -35
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +266 -165
- package/dist/completion.js +3 -3
- package/dist/config-command.js +5 -5
- package/dist/config.js +41 -35
- package/dist/conflict.d.ts +1 -1
- package/dist/conflict.js +14 -6
- package/dist/editor.js +29 -9
- package/dist/file-sync.d.ts +1 -0
- package/dist/file-sync.js +15 -11
- package/dist/git.d.ts +1 -1
- package/dist/git.js +21 -19
- package/dist/gji-bundle.mjs +1688 -865
- package/dist/go.d.ts +2 -2
- package/dist/go.js +39 -26
- package/dist/headless.js +1 -1
- package/dist/history-command.js +3 -3
- package/dist/history.js +12 -12
- package/dist/hooks.d.ts +3 -3
- package/dist/hooks.js +19 -19
- package/dist/index.js +13 -9
- package/dist/init.d.ts +5 -5
- package/dist/init.js +115 -103
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +57 -38
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +97 -82
- package/dist/open.d.ts +2 -2
- package/dist/open.js +24 -21
- package/dist/package-manager.js +96 -45
- package/dist/pr.d.ts +2 -2
- package/dist/pr.js +47 -34
- package/dist/remove.d.ts +1 -1
- package/dist/remove.js +39 -27
- package/dist/repo-registry.js +14 -14
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/run-hook.d.ts +6 -0
- package/dist/{trigger-hook.js → run-hook.js} +18 -8
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +67 -39
- package/dist/shell-handoff.js +2 -2
- package/dist/shell.d.ts +1 -1
- package/dist/shell.js +4 -4
- package/dist/status.d.ts +5 -5
- package/dist/status.js +23 -23
- package/dist/sync-files-command.d.ts +10 -0
- package/dist/sync-files-command.js +137 -0
- package/dist/sync.js +23 -15
- package/dist/warp.js +37 -33
- package/dist/worktree-info.d.ts +9 -9
- package/dist/worktree-info.js +31 -29
- package/dist/worktree-management.d.ts +1 -1
- package/dist/worktree-management.js +26 -11
- package/dist/worktree-prompts.js +5 -5
- 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 +23 -0
- package/man/man1/gji-sync.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +10 -4
- package/package.json +8 -2
- package/dist/trigger-hook.d.ts +0 -6
- package/man/man1/gji-trigger-hook.1 +0 -9
package/dist/init.js
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from
|
|
2
|
-
import { homedir } from
|
|
3
|
-
import { dirname, join } from
|
|
4
|
-
import { intro, isCancel, outro, select, text } from
|
|
5
|
-
import { loadConfig, loadGlobalConfig, saveGlobalConfig, saveLocalConfig, updateGlobalConfigKey } from
|
|
6
|
-
import { resolveSupportedShell } from
|
|
7
|
-
const START_MARKER =
|
|
8
|
-
const END_MARKER =
|
|
1
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
5
|
+
import { loadConfig, loadGlobalConfig, saveGlobalConfig, saveLocalConfig, updateGlobalConfigKey, } from "./config.js";
|
|
6
|
+
import { resolveSupportedShell } from "./shell.js";
|
|
7
|
+
const START_MARKER = "# >>> gji init >>>";
|
|
8
|
+
const END_MARKER = "# <<< gji init <<<";
|
|
9
9
|
const SHELL_WRAPPED_COMMANDS = [
|
|
10
10
|
{
|
|
11
|
-
bypassOptions: [
|
|
12
|
-
commandName:
|
|
13
|
-
envVar:
|
|
14
|
-
names: [
|
|
15
|
-
tempPrefix:
|
|
11
|
+
bypassOptions: ["--help"],
|
|
12
|
+
commandName: "new",
|
|
13
|
+
envVar: "GJI_NEW_OUTPUT_FILE",
|
|
14
|
+
names: ["new"],
|
|
15
|
+
tempPrefix: "gji-new",
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
bypassOptions: [
|
|
19
|
-
commandName:
|
|
20
|
-
envVar:
|
|
21
|
-
names: [
|
|
22
|
-
tempPrefix:
|
|
18
|
+
bypassOptions: ["--help"],
|
|
19
|
+
commandName: "pr",
|
|
20
|
+
envVar: "GJI_PR_OUTPUT_FILE",
|
|
21
|
+
names: ["pr"],
|
|
22
|
+
tempPrefix: "gji-pr",
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
|
-
bypassOptions: [
|
|
26
|
-
commandName:
|
|
27
|
-
envVar:
|
|
28
|
-
names: [
|
|
29
|
-
tempPrefix:
|
|
25
|
+
bypassOptions: ["--print"],
|
|
26
|
+
commandName: "back",
|
|
27
|
+
envVar: "GJI_BACK_OUTPUT_FILE",
|
|
28
|
+
names: ["back"],
|
|
29
|
+
tempPrefix: "gji-back",
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
|
-
bypassOptions: [
|
|
33
|
-
commandName:
|
|
34
|
-
envVar:
|
|
35
|
-
names: [
|
|
36
|
-
tempPrefix:
|
|
32
|
+
bypassOptions: ["--print"],
|
|
33
|
+
commandName: "go",
|
|
34
|
+
envVar: "GJI_GO_OUTPUT_FILE",
|
|
35
|
+
names: ["go", "jump"],
|
|
36
|
+
tempPrefix: "gji-go",
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
bypassOptions: [
|
|
40
|
-
commandName:
|
|
41
|
-
envVar:
|
|
42
|
-
names: [
|
|
43
|
-
tempPrefix:
|
|
39
|
+
bypassOptions: ["--print"],
|
|
40
|
+
commandName: "root",
|
|
41
|
+
envVar: "GJI_ROOT_OUTPUT_FILE",
|
|
42
|
+
names: ["root"],
|
|
43
|
+
tempPrefix: "gji-root",
|
|
44
44
|
},
|
|
45
45
|
{
|
|
46
|
-
bypassOptions: [
|
|
47
|
-
commandName:
|
|
48
|
-
envVar:
|
|
49
|
-
names: [
|
|
50
|
-
tempPrefix:
|
|
46
|
+
bypassOptions: ["--help"],
|
|
47
|
+
commandName: "remove",
|
|
48
|
+
envVar: "GJI_REMOVE_OUTPUT_FILE",
|
|
49
|
+
names: ["remove", "rm"],
|
|
50
|
+
tempPrefix: "gji-remove",
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
|
-
bypassOptions: [
|
|
54
|
-
commandName:
|
|
55
|
-
envVar:
|
|
56
|
-
names: [
|
|
57
|
-
tempPrefix:
|
|
53
|
+
bypassOptions: ["--print", "--json"],
|
|
54
|
+
commandName: "warp",
|
|
55
|
+
envVar: "GJI_WARP_OUTPUT_FILE",
|
|
56
|
+
names: ["warp"],
|
|
57
|
+
tempPrefix: "gji-warp",
|
|
58
58
|
},
|
|
59
59
|
];
|
|
60
60
|
export async function runInitCommand(options) {
|
|
61
61
|
const shell = resolveSupportedShell(options.shell, process.env.SHELL);
|
|
62
62
|
const home = options.home ?? homedir();
|
|
63
63
|
if (!shell) {
|
|
64
|
-
options.stderr?.(
|
|
64
|
+
options.stderr?.("Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n");
|
|
65
65
|
return 1;
|
|
66
66
|
}
|
|
67
67
|
const script = renderShellIntegration(shell);
|
|
@@ -73,29 +73,29 @@ export async function runInitCommand(options) {
|
|
|
73
73
|
await mkdir(dirname(rcPath), { recursive: true });
|
|
74
74
|
const current = await readExistingConfig(rcPath);
|
|
75
75
|
const next = upsertShellIntegration(current, script);
|
|
76
|
-
await writeFile(rcPath, next,
|
|
76
|
+
await writeFile(rcPath, next, "utf8");
|
|
77
77
|
options.stdout(`${rcPath}\n`);
|
|
78
78
|
// Run the setup wizard on the first-ever init (not on subsequent re-runs).
|
|
79
79
|
const { config: globalConfig } = await loadGlobalConfig(home);
|
|
80
|
-
const alreadyConfigured =
|
|
80
|
+
const alreadyConfigured = "shellIntegration" in globalConfig || "installSaveTarget" in globalConfig;
|
|
81
81
|
const hasCustomPrompt = options.promptForSetup !== undefined;
|
|
82
82
|
const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
|
|
83
83
|
if (!alreadyConfigured && canPrompt) {
|
|
84
84
|
const prompt = options.promptForSetup ?? defaultPromptForSetup;
|
|
85
85
|
const result = await prompt();
|
|
86
86
|
if (result) {
|
|
87
|
-
await updateGlobalConfigKey(
|
|
87
|
+
await updateGlobalConfigKey("installSaveTarget", result.installSaveTarget, home);
|
|
88
88
|
await saveWizardConfig(result, options.cwd, home);
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
91
|
// Mark shell integration as installed so the first-run nudge is suppressed.
|
|
92
|
-
await updateGlobalConfigKey(
|
|
92
|
+
await updateGlobalConfigKey("shellIntegration", true, home);
|
|
93
93
|
return 0;
|
|
94
94
|
}
|
|
95
95
|
export function renderShellIntegration(shell) {
|
|
96
|
-
const commandBlocks = SHELL_WRAPPED_COMMANDS.map((command) => shell ===
|
|
96
|
+
const commandBlocks = SHELL_WRAPPED_COMMANDS.map((command) => shell === "fish" ? renderFishWrapper(command) : renderPosixWrapper(command)).join("\n\n");
|
|
97
97
|
switch (shell) {
|
|
98
|
-
case
|
|
98
|
+
case "fish":
|
|
99
99
|
return `${START_MARKER}
|
|
100
100
|
function gji --wraps gji --description 'gji shell integration'
|
|
101
101
|
${indentBlock(commandBlocks, 4)}
|
|
@@ -104,8 +104,8 @@ ${indentBlock(commandBlocks, 4)}
|
|
|
104
104
|
end
|
|
105
105
|
${END_MARKER}
|
|
106
106
|
`;
|
|
107
|
-
case
|
|
108
|
-
case
|
|
107
|
+
case "bash":
|
|
108
|
+
case "zsh":
|
|
109
109
|
return `${START_MARKER}
|
|
110
110
|
gji() {
|
|
111
111
|
${indentBlock(commandBlocks, 2)}
|
|
@@ -118,7 +118,7 @@ ${END_MARKER}
|
|
|
118
118
|
}
|
|
119
119
|
export function upsertShellIntegration(existingConfig, script) {
|
|
120
120
|
const trimmedScript = script.trimEnd();
|
|
121
|
-
const blockPattern = new RegExp(`${escapeForRegExp(START_MARKER)}[\\s\\S]*?${escapeForRegExp(END_MARKER)}\\n?`,
|
|
121
|
+
const blockPattern = new RegExp(`${escapeForRegExp(START_MARKER)}[\\s\\S]*?${escapeForRegExp(END_MARKER)}\\n?`, "m");
|
|
122
122
|
if (blockPattern.test(existingConfig)) {
|
|
123
123
|
return ensureTrailingNewline(existingConfig.replace(blockPattern, `${trimmedScript}\n`));
|
|
124
124
|
}
|
|
@@ -135,17 +135,17 @@ 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)
|
|
147
147
|
return;
|
|
148
|
-
if (result.installSaveTarget ===
|
|
148
|
+
if (result.installSaveTarget === "local") {
|
|
149
149
|
const loaded = await loadConfig(cwd);
|
|
150
150
|
await saveLocalConfig(cwd, { ...loaded.config, ...values });
|
|
151
151
|
}
|
|
@@ -156,43 +156,43 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
156
156
|
}
|
|
157
157
|
function resolveShellConfigPath(shell, home) {
|
|
158
158
|
switch (shell) {
|
|
159
|
-
case
|
|
160
|
-
return join(home,
|
|
161
|
-
case
|
|
162
|
-
return join(home,
|
|
163
|
-
case
|
|
164
|
-
return join(home,
|
|
159
|
+
case "bash":
|
|
160
|
+
return join(home, ".bashrc");
|
|
161
|
+
case "fish":
|
|
162
|
+
return join(home, ".config", "fish", "config.fish");
|
|
163
|
+
case "zsh":
|
|
164
|
+
return join(home, ".zshrc");
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
async function readExistingConfig(path) {
|
|
168
168
|
try {
|
|
169
|
-
return await readFile(path,
|
|
169
|
+
return await readFile(path, "utf8");
|
|
170
170
|
}
|
|
171
171
|
catch (error) {
|
|
172
172
|
if (isMissingFileError(error)) {
|
|
173
|
-
return
|
|
173
|
+
return "";
|
|
174
174
|
}
|
|
175
175
|
throw error;
|
|
176
176
|
}
|
|
177
177
|
}
|
|
178
178
|
function ensureTrailingNewline(value) {
|
|
179
|
-
return value.endsWith(
|
|
179
|
+
return value.endsWith("\n") ? value : `${value}\n`;
|
|
180
180
|
}
|
|
181
181
|
function escapeForRegExp(value) {
|
|
182
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g,
|
|
182
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
183
183
|
}
|
|
184
184
|
function isMissingFileError(error) {
|
|
185
|
-
return error instanceof Error &&
|
|
185
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
186
186
|
}
|
|
187
187
|
function renderFishWrapper(command) {
|
|
188
188
|
const nameTests = command.names.map((name) => `test $argv[1] = ${name}`);
|
|
189
189
|
const nameCondition = nameTests.length === 1
|
|
190
190
|
? nameTests[0]
|
|
191
|
-
: `begin; ${nameTests.join(
|
|
191
|
+
: `begin; ${nameTests.join("; or ")}; end`;
|
|
192
192
|
const bypassTests = command.bypassOptions.map((opt) => `test $argv[1] = ${opt}`);
|
|
193
193
|
const bypassCondition = bypassTests.length === 1
|
|
194
194
|
? bypassTests[0]
|
|
195
|
-
: `begin; ${bypassTests.join(
|
|
195
|
+
: `begin; ${bypassTests.join("; or ")}; end`;
|
|
196
196
|
return `if test (count $argv) -gt 0; and ${nameCondition}
|
|
197
197
|
set -e argv[1]
|
|
198
198
|
if test (count $argv) -gt 0; and ${bypassCondition}
|
|
@@ -215,8 +215,12 @@ function renderFishWrapper(command) {
|
|
|
215
215
|
end`;
|
|
216
216
|
}
|
|
217
217
|
function renderPosixWrapper(command) {
|
|
218
|
-
const tests = command.names
|
|
219
|
-
|
|
218
|
+
const tests = command.names
|
|
219
|
+
.map((name) => `[ "$1" = "${name}" ]`)
|
|
220
|
+
.join(" || ");
|
|
221
|
+
const bypassTests = command.bypassOptions
|
|
222
|
+
.map((opt) => `[ "\${1:-}" = "${opt}" ]`)
|
|
223
|
+
.join(" || ");
|
|
220
224
|
return `if ${tests}; then
|
|
221
225
|
shift
|
|
222
226
|
if ${bypassTests}; then
|
|
@@ -235,73 +239,81 @@ function renderPosixWrapper(command) {
|
|
|
235
239
|
fi`;
|
|
236
240
|
}
|
|
237
241
|
function indentBlock(value, spaces) {
|
|
238
|
-
const prefix =
|
|
242
|
+
const prefix = " ".repeat(spaces);
|
|
239
243
|
return value
|
|
240
|
-
.split(
|
|
241
|
-
.map((line) => line.length === 0 ?
|
|
242
|
-
.join(
|
|
244
|
+
.split("\n")
|
|
245
|
+
.map((line) => (line.length === 0 ? "" : `${prefix}${line}`))
|
|
246
|
+
.join("\n");
|
|
243
247
|
}
|
|
244
248
|
async function defaultPromptForSetup() {
|
|
245
|
-
intro(
|
|
249
|
+
intro("gji setup");
|
|
246
250
|
const installSaveTarget = await select({
|
|
247
|
-
message:
|
|
251
|
+
message: "Where should preferences be saved?",
|
|
248
252
|
options: [
|
|
249
|
-
{
|
|
250
|
-
|
|
253
|
+
{
|
|
254
|
+
value: "global",
|
|
255
|
+
label: "~/.config/gji/config.json",
|
|
256
|
+
hint: "personal — never committed",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
value: "local",
|
|
260
|
+
label: ".gji.json",
|
|
261
|
+
hint: "repo — committed with the project",
|
|
262
|
+
},
|
|
251
263
|
],
|
|
252
264
|
});
|
|
253
265
|
if (isCancel(installSaveTarget)) {
|
|
254
|
-
outro(
|
|
266
|
+
outro("Setup skipped.");
|
|
255
267
|
return null;
|
|
256
268
|
}
|
|
257
269
|
const branchPrefix = await text({
|
|
258
|
-
message:
|
|
259
|
-
placeholder:
|
|
270
|
+
message: "Default branch prefix?",
|
|
271
|
+
placeholder: "e.g. feat/ or fix/ — leave blank to skip",
|
|
260
272
|
});
|
|
261
273
|
if (isCancel(branchPrefix)) {
|
|
262
|
-
outro(
|
|
274
|
+
outro("Setup skipped.");
|
|
263
275
|
return null;
|
|
264
276
|
}
|
|
265
277
|
const worktreePath = await text({
|
|
266
|
-
message:
|
|
267
|
-
placeholder:
|
|
278
|
+
message: "Worktree base path?",
|
|
279
|
+
placeholder: "leave blank to use the default path",
|
|
268
280
|
});
|
|
269
281
|
if (isCancel(worktreePath)) {
|
|
270
|
-
outro(
|
|
282
|
+
outro("Setup skipped.");
|
|
271
283
|
return null;
|
|
272
284
|
}
|
|
273
285
|
const afterCreate = await text({
|
|
274
|
-
message:
|
|
275
|
-
placeholder:
|
|
286
|
+
message: "after-create hook — run after creating a worktree?",
|
|
287
|
+
placeholder: "e.g. pnpm install — leave blank to skip",
|
|
276
288
|
});
|
|
277
289
|
if (isCancel(afterCreate)) {
|
|
278
|
-
outro(
|
|
290
|
+
outro("Setup skipped.");
|
|
279
291
|
return null;
|
|
280
292
|
}
|
|
281
293
|
const afterEnter = await text({
|
|
282
|
-
message:
|
|
283
|
-
placeholder:
|
|
294
|
+
message: "after-enter hook — run after entering a worktree?",
|
|
295
|
+
placeholder: "e.g. nvm use — leave blank to skip",
|
|
284
296
|
});
|
|
285
297
|
if (isCancel(afterEnter)) {
|
|
286
|
-
outro(
|
|
298
|
+
outro("Setup skipped.");
|
|
287
299
|
return null;
|
|
288
300
|
}
|
|
289
301
|
const beforeRemove = await text({
|
|
290
|
-
message:
|
|
291
|
-
placeholder:
|
|
302
|
+
message: "before-remove hook — run before removing a worktree?",
|
|
303
|
+
placeholder: "leave blank to skip",
|
|
292
304
|
});
|
|
293
305
|
if (isCancel(beforeRemove)) {
|
|
294
|
-
outro(
|
|
306
|
+
outro("Setup skipped.");
|
|
295
307
|
return null;
|
|
296
308
|
}
|
|
297
|
-
outro(
|
|
309
|
+
outro("Setup complete!");
|
|
298
310
|
const hooks = {};
|
|
299
311
|
if (afterCreate)
|
|
300
|
-
hooks
|
|
312
|
+
hooks["after-create"] = afterCreate;
|
|
301
313
|
if (afterEnter)
|
|
302
|
-
hooks
|
|
314
|
+
hooks["after-enter"] = afterEnter;
|
|
303
315
|
if (beforeRemove)
|
|
304
|
-
hooks
|
|
316
|
+
hooks["before-remove"] = beforeRemove;
|
|
305
317
|
return {
|
|
306
318
|
branchPrefix: branchPrefix || undefined,
|
|
307
319
|
hooks: Object.keys(hooks).length > 0 ? hooks : undefined,
|
package/dist/install-prompt.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { type GjiConfig } from
|
|
2
|
-
import { type PackageManager } from
|
|
3
|
-
export type InstallChoice =
|
|
1
|
+
import { type GjiConfig } from "./config.js";
|
|
2
|
+
import { type PackageManager } from "./package-manager.js";
|
|
3
|
+
export type InstallChoice = "yes" | "no" | "always" | "never";
|
|
4
4
|
export interface InstallPromptDependencies {
|
|
5
5
|
detectInstallPackageManager?: (root: string) => Promise<PackageManager | null>;
|
|
6
6
|
promptForInstallChoice?: (pm: PackageManager) => Promise<InstallChoice | null>;
|
package/dist/install-prompt.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import { isCancel, select } from
|
|
3
|
-
import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey } from
|
|
4
|
-
import { isHeadless } from
|
|
5
|
-
import {
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { isCancel, select } from "@clack/prompts";
|
|
3
|
+
import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey, } from "./config.js";
|
|
4
|
+
import { isHeadless } from "./headless.js";
|
|
5
|
+
import { extractHooks } from "./hooks.js";
|
|
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.
|
|
@@ -24,10 +24,10 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
24
24
|
}
|
|
25
25
|
const prompt = dependencies.promptForInstallChoice ?? defaultPromptForInstallChoice;
|
|
26
26
|
const choice = await prompt(pm);
|
|
27
|
-
if (!choice || choice ===
|
|
27
|
+
if (!choice || choice === "no") {
|
|
28
28
|
return;
|
|
29
29
|
}
|
|
30
|
-
if (choice ===
|
|
30
|
+
if (choice === "yes" || choice === "always") {
|
|
31
31
|
const runner = dependencies.runInstallCommand ?? defaultRunInstallCommand;
|
|
32
32
|
try {
|
|
33
33
|
await runner(pm.installCommand, worktreePath, stderr);
|
|
@@ -36,34 +36,51 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
36
36
|
stderr(`gji: install command failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
const saveGlobal = config.installSaveTarget ===
|
|
39
|
+
const saveGlobal = config.installSaveTarget === "global";
|
|
40
40
|
const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
|
|
41
41
|
const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
|
|
42
|
-
if (choice ===
|
|
42
|
+
if (choice === "always") {
|
|
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
|
|
47
|
-
|
|
46
|
+
const existingRaw = await loadExistingGlobalRepoHooks(repoRoot);
|
|
47
|
+
const existing = extractHooks({ hooks: existingRaw });
|
|
48
|
+
await writeGlobalKey(repoRoot, "hooks", {
|
|
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,
|
|
56
|
+
});
|
|
48
57
|
}
|
|
49
58
|
else {
|
|
50
|
-
// 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.
|
|
51
60
|
const { config: localConfig } = await loadConfig(repoRoot);
|
|
52
|
-
const
|
|
53
|
-
await writeKey(repoRoot,
|
|
61
|
+
const existing = extractHooks(localConfig);
|
|
62
|
+
await writeKey(repoRoot, "hooks", {
|
|
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,
|
|
70
|
+
});
|
|
54
71
|
}
|
|
55
72
|
}
|
|
56
73
|
catch (error) {
|
|
57
74
|
stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
58
75
|
}
|
|
59
76
|
}
|
|
60
|
-
if (choice ===
|
|
77
|
+
if (choice === "never") {
|
|
61
78
|
try {
|
|
62
79
|
if (saveGlobal) {
|
|
63
|
-
await writeGlobalKey(repoRoot,
|
|
80
|
+
await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
|
|
64
81
|
}
|
|
65
82
|
else {
|
|
66
|
-
await writeKey(repoRoot,
|
|
83
|
+
await writeKey(repoRoot, "skipInstallPrompt", true);
|
|
67
84
|
}
|
|
68
85
|
}
|
|
69
86
|
catch (error) {
|
|
@@ -73,11 +90,15 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
73
90
|
}
|
|
74
91
|
async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
75
92
|
await new Promise((resolve, reject) => {
|
|
76
|
-
const child = spawn(command, {
|
|
77
|
-
|
|
93
|
+
const child = spawn(command, {
|
|
94
|
+
cwd,
|
|
95
|
+
shell: true,
|
|
96
|
+
stdio: ["ignore", "inherit", "pipe"],
|
|
97
|
+
});
|
|
98
|
+
child.stderr.on("data", (chunk) => {
|
|
78
99
|
stderr(chunk.toString());
|
|
79
100
|
});
|
|
80
|
-
child.on(
|
|
101
|
+
child.on("close", (code) => {
|
|
81
102
|
if (code !== 0) {
|
|
82
103
|
reject(new Error(`exited with code ${code}`));
|
|
83
104
|
}
|
|
@@ -85,7 +106,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
|
85
106
|
resolve();
|
|
86
107
|
}
|
|
87
108
|
});
|
|
88
|
-
child.on(
|
|
109
|
+
child.on("error", (err) => {
|
|
89
110
|
reject(err);
|
|
90
111
|
});
|
|
91
112
|
});
|
|
@@ -99,17 +120,23 @@ async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
|
|
|
99
120
|
async function loadExistingGlobalRepoHooks(repoRoot) {
|
|
100
121
|
const { config: globalConfig } = await loadGlobalConfig();
|
|
101
122
|
const repos = isPlainObject(globalConfig.repos) ? globalConfig.repos : {};
|
|
102
|
-
const perRepo = isPlainObject(repos[repoRoot])
|
|
123
|
+
const perRepo = isPlainObject(repos[repoRoot])
|
|
124
|
+
? repos[repoRoot]
|
|
125
|
+
: {};
|
|
103
126
|
return isPlainObject(perRepo.hooks) ? perRepo.hooks : {};
|
|
104
127
|
}
|
|
105
128
|
async function defaultPromptForInstallChoice(pm) {
|
|
106
129
|
const choice = await select({
|
|
107
130
|
message: `Run \`${pm.installCommand}\` in the new worktree?`,
|
|
108
131
|
options: [
|
|
109
|
-
{ value:
|
|
110
|
-
{ value:
|
|
111
|
-
{ value:
|
|
112
|
-
{
|
|
132
|
+
{ value: "yes", label: "Yes", hint: "run once" },
|
|
133
|
+
{ value: "no", label: "No", hint: "skip this time" },
|
|
134
|
+
{ value: "always", label: "Always", hint: "save as after-create hook" },
|
|
135
|
+
{
|
|
136
|
+
value: "never",
|
|
137
|
+
label: "Never",
|
|
138
|
+
hint: "disable this prompt for this repo",
|
|
139
|
+
},
|
|
113
140
|
],
|
|
114
141
|
});
|
|
115
142
|
if (isCancel(choice)) {
|
|
@@ -118,13 +145,5 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
118
145
|
return choice;
|
|
119
146
|
}
|
|
120
147
|
function isPlainObject(value) {
|
|
121
|
-
return typeof value ===
|
|
122
|
-
}
|
|
123
|
-
function isConfiguredHookCommand(value) {
|
|
124
|
-
if (typeof value === 'string')
|
|
125
|
-
return value.length > 0;
|
|
126
|
-
return (Array.isArray(value) &&
|
|
127
|
-
value.length > 0 &&
|
|
128
|
-
value[0] !== '' &&
|
|
129
|
-
value.every((item) => typeof item === 'string'));
|
|
148
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
130
149
|
}
|
package/dist/ls.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type WorktreeEntry } from
|
|
2
|
-
import { type WorktreeInfo } from
|
|
1
|
+
import { type WorktreeEntry } from "./repo.js";
|
|
2
|
+
import { type WorktreeInfo } from "./worktree-info.js";
|
|
3
3
|
export interface LsCommandOptions {
|
|
4
4
|
compact?: boolean;
|
|
5
5
|
cwd: string;
|