@solaqua/gji 0.6.1 → 0.7.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 +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 +264 -164
- 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 +1709 -850
- 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.js +16 -16
- package/dist/index.js +13 -9
- package/dist/init.d.ts +2 -2
- package/dist/init.js +106 -94
- package/dist/install-prompt.d.ts +3 -3
- package/dist/install-prompt.js +46 -28
- package/dist/ls.d.ts +2 -2
- package/dist/ls.js +29 -29
- package/dist/new.d.ts +2 -2
- package/dist/new.js +96 -81
- 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 +45 -19
- package/dist/repo.js +29 -28
- package/dist/root.js +3 -3
- package/dist/shell-completion.d.ts +1 -1
- package/dist/shell-completion.js +65 -37
- 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/trigger-hook.js +9 -5
- package/dist/warp.js +66 -34
- 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-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-trigger-hook.1 +1 -1
- package/man/man1/gji-warp.1 +1 -1
- package/man/man1/gji.1 +5 -1
- package/package.json +8 -2
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
|
}
|
|
@@ -145,7 +145,7 @@ async function saveWizardConfig(result, cwd, home) {
|
|
|
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,66 +239,74 @@ 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: "afterCreate 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: "afterEnter 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: "beforeRemove 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
312
|
hooks.afterCreate = afterCreate;
|
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,8 +1,8 @@
|
|
|
1
|
-
import { spawn } from
|
|
2
|
-
import { isCancel, select } from
|
|
3
|
-
import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey } from
|
|
4
|
-
import { isHeadless } from
|
|
5
|
-
import { detectPackageManager } from
|
|
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 { detectPackageManager, } from "./package-manager.js";
|
|
6
6
|
export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
|
|
7
7
|
// Skip in non-interactive mode — no prompt can be shown.
|
|
8
8
|
if (isHeadless() || nonInteractive) {
|
|
@@ -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,42 @@ 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
46
|
const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
|
|
47
|
-
await writeGlobalKey(repoRoot,
|
|
47
|
+
await writeGlobalKey(repoRoot, "hooks", {
|
|
48
|
+
...existingHooks,
|
|
49
|
+
afterCreate: pm.installCommand,
|
|
50
|
+
});
|
|
48
51
|
}
|
|
49
52
|
else {
|
|
50
53
|
// Read local config hooks to deep-merge so other hook keys (e.g. afterEnter) are preserved.
|
|
51
54
|
const { config: localConfig } = await loadConfig(repoRoot);
|
|
52
|
-
const existingLocalHooks = isPlainObject(localConfig.hooks)
|
|
53
|
-
|
|
55
|
+
const existingLocalHooks = isPlainObject(localConfig.hooks)
|
|
56
|
+
? localConfig.hooks
|
|
57
|
+
: {};
|
|
58
|
+
await writeKey(repoRoot, "hooks", {
|
|
59
|
+
...existingLocalHooks,
|
|
60
|
+
afterCreate: pm.installCommand,
|
|
61
|
+
});
|
|
54
62
|
}
|
|
55
63
|
}
|
|
56
64
|
catch (error) {
|
|
57
65
|
stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
58
66
|
}
|
|
59
67
|
}
|
|
60
|
-
if (choice ===
|
|
68
|
+
if (choice === "never") {
|
|
61
69
|
try {
|
|
62
70
|
if (saveGlobal) {
|
|
63
|
-
await writeGlobalKey(repoRoot,
|
|
71
|
+
await writeGlobalKey(repoRoot, "skipInstallPrompt", true);
|
|
64
72
|
}
|
|
65
73
|
else {
|
|
66
|
-
await writeKey(repoRoot,
|
|
74
|
+
await writeKey(repoRoot, "skipInstallPrompt", true);
|
|
67
75
|
}
|
|
68
76
|
}
|
|
69
77
|
catch (error) {
|
|
@@ -73,11 +81,15 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
73
81
|
}
|
|
74
82
|
async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
75
83
|
await new Promise((resolve, reject) => {
|
|
76
|
-
const child = spawn(command, {
|
|
77
|
-
|
|
84
|
+
const child = spawn(command, {
|
|
85
|
+
cwd,
|
|
86
|
+
shell: true,
|
|
87
|
+
stdio: ["ignore", "inherit", "pipe"],
|
|
88
|
+
});
|
|
89
|
+
child.stderr.on("data", (chunk) => {
|
|
78
90
|
stderr(chunk.toString());
|
|
79
91
|
});
|
|
80
|
-
child.on(
|
|
92
|
+
child.on("close", (code) => {
|
|
81
93
|
if (code !== 0) {
|
|
82
94
|
reject(new Error(`exited with code ${code}`));
|
|
83
95
|
}
|
|
@@ -85,7 +97,7 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
|
85
97
|
resolve();
|
|
86
98
|
}
|
|
87
99
|
});
|
|
88
|
-
child.on(
|
|
100
|
+
child.on("error", (err) => {
|
|
89
101
|
reject(err);
|
|
90
102
|
});
|
|
91
103
|
});
|
|
@@ -99,17 +111,23 @@ async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
|
|
|
99
111
|
async function loadExistingGlobalRepoHooks(repoRoot) {
|
|
100
112
|
const { config: globalConfig } = await loadGlobalConfig();
|
|
101
113
|
const repos = isPlainObject(globalConfig.repos) ? globalConfig.repos : {};
|
|
102
|
-
const perRepo = isPlainObject(repos[repoRoot])
|
|
114
|
+
const perRepo = isPlainObject(repos[repoRoot])
|
|
115
|
+
? repos[repoRoot]
|
|
116
|
+
: {};
|
|
103
117
|
return isPlainObject(perRepo.hooks) ? perRepo.hooks : {};
|
|
104
118
|
}
|
|
105
119
|
async function defaultPromptForInstallChoice(pm) {
|
|
106
120
|
const choice = await select({
|
|
107
121
|
message: `Run \`${pm.installCommand}\` in the new worktree?`,
|
|
108
122
|
options: [
|
|
109
|
-
{ value:
|
|
110
|
-
{ value:
|
|
111
|
-
{ value:
|
|
112
|
-
{
|
|
123
|
+
{ value: "yes", label: "Yes", hint: "run once" },
|
|
124
|
+
{ value: "no", label: "No", hint: "skip this time" },
|
|
125
|
+
{ value: "always", label: "Always", hint: "save as afterCreate hook" },
|
|
126
|
+
{
|
|
127
|
+
value: "never",
|
|
128
|
+
label: "Never",
|
|
129
|
+
hint: "disable this prompt for this repo",
|
|
130
|
+
},
|
|
113
131
|
],
|
|
114
132
|
});
|
|
115
133
|
if (isCancel(choice)) {
|
|
@@ -118,13 +136,13 @@ async function defaultPromptForInstallChoice(pm) {
|
|
|
118
136
|
return choice;
|
|
119
137
|
}
|
|
120
138
|
function isPlainObject(value) {
|
|
121
|
-
return typeof value ===
|
|
139
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
122
140
|
}
|
|
123
141
|
function isConfiguredHookCommand(value) {
|
|
124
|
-
if (typeof value ===
|
|
142
|
+
if (typeof value === "string")
|
|
125
143
|
return value.length > 0;
|
|
126
144
|
return (Array.isArray(value) &&
|
|
127
145
|
value.length > 0 &&
|
|
128
|
-
value[0] !==
|
|
129
|
-
value.every((item) => typeof item ===
|
|
146
|
+
value[0] !== "" &&
|
|
147
|
+
value.every((item) => typeof item === "string"));
|
|
130
148
|
}
|
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;
|
package/dist/ls.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { formatLastCommit, formatUpstreamState, readWorktreeInfos, } from
|
|
1
|
+
import { comparePaths } from "./paths.js";
|
|
2
|
+
import { listWorktrees } from "./repo.js";
|
|
3
|
+
import { formatLastCommit, formatUpstreamState, readWorktreeInfos, } from "./worktree-info.js";
|
|
4
4
|
export async function runLsCommand(options) {
|
|
5
5
|
const worktrees = sortWorktrees(await listWorktrees(options.cwd));
|
|
6
6
|
if (options.compact) {
|
|
@@ -21,50 +21,50 @@ export async function runLsCommand(options) {
|
|
|
21
21
|
}
|
|
22
22
|
export function formatDetailedWorktreeTable(worktrees) {
|
|
23
23
|
const rows = worktrees.map((worktree) => ({
|
|
24
|
-
branch: worktree.branch ??
|
|
24
|
+
branch: worktree.branch ?? "(detached)",
|
|
25
25
|
isCurrent: worktree.isCurrent,
|
|
26
26
|
lastCommit: formatLastCommit(worktree.lastCommitTimestamp),
|
|
27
27
|
path: worktree.path,
|
|
28
28
|
status: worktree.status,
|
|
29
29
|
upstream: formatUpstreamState(worktree.upstream),
|
|
30
30
|
}));
|
|
31
|
-
const branchWidth = Math.max(
|
|
32
|
-
const statusWidth = Math.max(
|
|
33
|
-
const upstreamWidth = Math.max(
|
|
34
|
-
const lastCommitWidth = Math.max(
|
|
31
|
+
const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
|
|
32
|
+
const statusWidth = Math.max("STATUS".length, ...rows.map((row) => row.status.length));
|
|
33
|
+
const upstreamWidth = Math.max("UPSTREAM".length, ...rows.map((row) => row.upstream.length));
|
|
34
|
+
const lastCommitWidth = Math.max("LAST".length, ...rows.map((row) => row.lastCommit.length));
|
|
35
35
|
const lines = [
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
" " +
|
|
37
|
+
"BRANCH".padEnd(branchWidth, " ") +
|
|
38
|
+
" " +
|
|
39
|
+
"STATUS".padEnd(statusWidth, " ") +
|
|
40
|
+
" " +
|
|
41
|
+
"UPSTREAM".padEnd(upstreamWidth, " ") +
|
|
42
|
+
" " +
|
|
43
|
+
"LAST".padEnd(lastCommitWidth, " ") +
|
|
44
|
+
" PATH",
|
|
45
45
|
];
|
|
46
46
|
for (const row of rows) {
|
|
47
|
-
lines.push(`${row.isCurrent ?
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
47
|
+
lines.push(`${row.isCurrent ? "*" : " "} ` +
|
|
48
|
+
`${row.branch.padEnd(branchWidth, " ")} ` +
|
|
49
|
+
`${row.status.padEnd(statusWidth, " ")} ` +
|
|
50
|
+
`${row.upstream.padEnd(upstreamWidth, " ")} ` +
|
|
51
|
+
`${row.lastCommit.padEnd(lastCommitWidth, " ")} ` +
|
|
52
|
+
row.path);
|
|
53
53
|
}
|
|
54
|
-
return lines.join(
|
|
54
|
+
return lines.join("\n");
|
|
55
55
|
}
|
|
56
56
|
export function formatWorktreeTable(worktrees) {
|
|
57
57
|
const rows = worktrees.map((worktree) => ({
|
|
58
|
-
branch: worktree.branch ??
|
|
58
|
+
branch: worktree.branch ?? "(detached)",
|
|
59
59
|
isCurrent: worktree.isCurrent,
|
|
60
60
|
path: worktree.path,
|
|
61
61
|
}));
|
|
62
|
-
const branchWidth = Math.max(
|
|
63
|
-
const lines = [
|
|
62
|
+
const branchWidth = Math.max("BRANCH".length, ...rows.map((row) => row.branch.length));
|
|
63
|
+
const lines = [" " + "BRANCH".padEnd(branchWidth, " ") + " PATH"];
|
|
64
64
|
for (const row of rows) {
|
|
65
|
-
lines.push(`${row.isCurrent ?
|
|
65
|
+
lines.push(`${row.isCurrent ? "*" : " "} ${row.branch.padEnd(branchWidth, " ")} ${row.path}`);
|
|
66
66
|
}
|
|
67
|
-
return lines.join(
|
|
67
|
+
return lines.join("\n");
|
|
68
68
|
}
|
|
69
69
|
function sortWorktrees(worktrees) {
|
|
70
70
|
return [...worktrees].sort((left, right) => {
|
package/dist/new.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type
|
|
1
|
+
import { type PathConflictChoice } from "./conflict.js";
|
|
2
|
+
import { type InstallPromptDependencies } from "./install-prompt.js";
|
|
3
3
|
export type { PathConflictChoice };
|
|
4
4
|
export interface NewCommandOptions {
|
|
5
5
|
branch?: string;
|