@solaqua/gji 0.2.2 → 0.2.3
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 +44 -4
- package/dist/cli.js +17 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +45 -4
- package/dist/init.d.ts +3 -0
- package/dist/init.js +30 -1
- package/dist/install-prompt.d.ts +1 -0
- package/dist/install-prompt.js +29 -6
- package/dist/new.js +1 -0
- package/dist/trigger-hook.d.ts +6 -0
- package/dist/trigger-hook.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,6 +93,8 @@ gji sync --all # rebase every worktree
|
|
|
93
93
|
|
|
94
94
|
gji clean # interactive bulk cleanup
|
|
95
95
|
gji remove feature/auth-refactor # remove one worktree and its branch
|
|
96
|
+
|
|
97
|
+
gji trigger-hook afterCreate # re-run setup in the current worktree
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
## Shell setup
|
|
@@ -139,6 +141,7 @@ path=$(gji root --print)
|
|
|
139
141
|
| `gji sync [--all]` | fetch and rebase worktrees onto default branch |
|
|
140
142
|
| `gji clean [--force] [--json]` | interactively prune stale worktrees |
|
|
141
143
|
| `gji remove [branch] [--force] [--json]` | remove a worktree and its branch |
|
|
144
|
+
| `gji trigger-hook <hook>` | run a hook in the current worktree |
|
|
142
145
|
| `gji config [get\|set\|unset] [key] [value]` | manage global defaults |
|
|
143
146
|
| `gji init [shell]` | print or install shell integration |
|
|
144
147
|
|
|
@@ -158,7 +161,9 @@ No setup required. Optional config lives in:
|
|
|
158
161
|
| `syncDefaultBranch` | branch to rebase onto (default: remote `HEAD`) |
|
|
159
162
|
| `syncFiles` | files to copy from main worktree into each new worktree |
|
|
160
163
|
| `skipInstallPrompt` | `true` to disable the auto-install prompt permanently |
|
|
164
|
+
| `installSaveTarget` | `"local"` or `"global"` — where **Always**/**Never** choices are persisted (default: `"local"`); set once during `gji init --write` |
|
|
161
165
|
| `hooks` | lifecycle scripts (see [Hooks](#hooks)) |
|
|
166
|
+
| `repos` | per-repo overrides inside the global config (see below) |
|
|
162
167
|
|
|
163
168
|
```json
|
|
164
169
|
{
|
|
@@ -169,6 +174,26 @@ No setup required. Optional config lives in:
|
|
|
169
174
|
}
|
|
170
175
|
```
|
|
171
176
|
|
|
177
|
+
### Per-repo overrides in global config
|
|
178
|
+
|
|
179
|
+
If you work across many repositories, you can scope config to a specific repo inside `~/.config/gji/config.json` without adding a `.gji.json` to that repo:
|
|
180
|
+
|
|
181
|
+
```json
|
|
182
|
+
{
|
|
183
|
+
"branchPrefix": "feature/",
|
|
184
|
+
"repos": {
|
|
185
|
+
"/home/me/code/my-repo": {
|
|
186
|
+
"branchPrefix": "fix/",
|
|
187
|
+
"hooks": {
|
|
188
|
+
"afterCreate": "npm install"
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Precedence (lowest → highest): **global defaults → per-repo global → local `.gji.json`**. Hooks from all three layers are merged per key — different keys all apply, same key the higher-precedence layer wins.
|
|
196
|
+
|
|
172
197
|
### Config commands
|
|
173
198
|
|
|
174
199
|
```sh
|
|
@@ -200,19 +225,34 @@ Run scripts automatically at key lifecycle moments:
|
|
|
200
225
|
|
|
201
226
|
Hooks receive `{{branch}}`, `{{path}}`, `{{repo}}` as template variables and `GJI_BRANCH`, `GJI_PATH`, `GJI_REPO` as environment variables. A failing hook emits a warning but never aborts the command.
|
|
202
227
|
|
|
203
|
-
|
|
228
|
+
Hooks from all three config layers merge per key — different keys from different layers both apply, same key the higher-precedence layer wins:
|
|
204
229
|
|
|
205
230
|
```jsonc
|
|
206
231
|
// ~/.config/gji/config.json
|
|
207
232
|
{ "hooks": { "afterCreate": "nvm use", "afterEnter": "echo hi" } }
|
|
208
233
|
|
|
234
|
+
// per-repo entry in ~/.config/gji/config.json
|
|
235
|
+
{ "repos": { "/my/repo": { "hooks": { "afterCreate": "npm install" } } } }
|
|
236
|
+
|
|
209
237
|
// .gji.json
|
|
210
|
-
{ "hooks": { "
|
|
238
|
+
{ "hooks": { "beforeRemove": "echo bye" } }
|
|
211
239
|
|
|
212
240
|
// effective
|
|
213
|
-
{ "hooks": { "afterCreate": "
|
|
241
|
+
{ "hooks": { "afterCreate": "npm install", "afterEnter": "echo hi", "beforeRemove": "echo bye" } }
|
|
214
242
|
```
|
|
215
243
|
|
|
244
|
+
### Triggering hooks manually
|
|
245
|
+
|
|
246
|
+
Run any hook in the current worktree on demand:
|
|
247
|
+
|
|
248
|
+
```sh
|
|
249
|
+
gji trigger-hook afterCreate # re-run the setup script
|
|
250
|
+
gji trigger-hook afterEnter # re-run the enter script
|
|
251
|
+
gji trigger-hook beforeRemove # dry-run the cleanup script
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
This is useful after cloning on a new machine, recovering a broken worktree, or letting an AI agent bootstrap an already-existing worktree without needing interactive prompts.
|
|
255
|
+
|
|
216
256
|
## Install prompt
|
|
217
257
|
|
|
218
258
|
When `gji new` or `gji pr` creates a worktree, `gji` detects the project's package manager from its lockfile and offers to run the install command:
|
|
@@ -225,7 +265,7 @@ Run `pnpm install` in the new worktree?
|
|
|
225
265
|
Never disable this prompt for this repo
|
|
226
266
|
```
|
|
227
267
|
|
|
228
|
-
**Always** saves `hooks.afterCreate
|
|
268
|
+
**Always** saves `hooks.afterCreate`; **Never** writes `skipInstallPrompt: true`. Where they are saved depends on `installSaveTarget` (see [Available keys](#available-keys)) — defaults to `.gji.json`.
|
|
229
269
|
|
|
230
270
|
## JSON output
|
|
231
271
|
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { runRemoveCommand } from './remove.js';
|
|
|
11
11
|
import { runRootCommand } from './root.js';
|
|
12
12
|
import { runStatusCommand } from './status.js';
|
|
13
13
|
import { runSyncCommand } from './sync.js';
|
|
14
|
+
import { runTriggerHookCommand } from './trigger-hook.js';
|
|
14
15
|
export function createProgram() {
|
|
15
16
|
const program = new Command();
|
|
16
17
|
const packageVersion = readPackageVersion();
|
|
@@ -114,6 +115,10 @@ function registerCommands(program) {
|
|
|
114
115
|
.option('--dry-run', 'show what would be deleted without removing anything')
|
|
115
116
|
.option('--json', 'emit JSON on success or error instead of human-readable output')
|
|
116
117
|
.action(notImplemented('remove'));
|
|
118
|
+
program
|
|
119
|
+
.command('trigger-hook <hook>')
|
|
120
|
+
.description('run a named hook (afterCreate, afterEnter, beforeRemove) in the current worktree')
|
|
121
|
+
.action(notImplemented('trigger-hook'));
|
|
117
122
|
const configCommand = program
|
|
118
123
|
.command('config')
|
|
119
124
|
.description('manage global config defaults')
|
|
@@ -257,6 +262,18 @@ function attachCommandActions(program, options) {
|
|
|
257
262
|
program.commands
|
|
258
263
|
.find((command) => command.name() === 'remove')
|
|
259
264
|
?.action(runRemovalCommand);
|
|
265
|
+
program.commands
|
|
266
|
+
.find((command) => command.name() === 'trigger-hook')
|
|
267
|
+
?.action(async (hook) => {
|
|
268
|
+
const exitCode = await runTriggerHookCommand({
|
|
269
|
+
cwd: options.cwd,
|
|
270
|
+
hook,
|
|
271
|
+
stderr: options.stderr,
|
|
272
|
+
});
|
|
273
|
+
if (exitCode !== 0) {
|
|
274
|
+
throw commanderExit(exitCode);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
260
277
|
const configCommand = program.commands.find((command) => command.name() === 'config');
|
|
261
278
|
configCommand?.action(async () => {
|
|
262
279
|
const exitCode = await runConfigCommand({
|
package/dist/config.d.ts
CHANGED
|
@@ -16,5 +16,6 @@ export declare function updateLocalConfigKey(root: string, key: string, value: u
|
|
|
16
16
|
export declare function saveGlobalConfig(config: GjiConfig, home?: string): Promise<string>;
|
|
17
17
|
export declare function unsetGlobalConfigKey(key: string, home?: string): Promise<GjiConfig>;
|
|
18
18
|
export declare function updateGlobalConfigKey(key: string, value: unknown, home?: string): Promise<GjiConfig>;
|
|
19
|
+
export declare function updateGlobalRepoConfigKey(repoRoot: string, key: string, value: unknown, home?: string): Promise<GjiConfig>;
|
|
19
20
|
export declare function GLOBAL_CONFIG_FILE_PATH(home?: string): string;
|
|
20
21
|
export declare function parseConfigValue(value: string): unknown;
|
package/dist/config.js
CHANGED
|
@@ -14,11 +14,27 @@ export async function loadEffectiveConfig(root, home = homedir()) {
|
|
|
14
14
|
loadGlobalConfig(home),
|
|
15
15
|
loadConfig(root),
|
|
16
16
|
]);
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// Extract per-repo override keyed by the absolute repo path.
|
|
18
|
+
// Keys may use ~ as shorthand for the home directory (e.g. ~/code/my-repo).
|
|
19
|
+
const repos = globalConfig.config.repos;
|
|
20
|
+
const perRepoConfig = isPlainObject(repos)
|
|
21
|
+
? findPerRepoConfig(repos, root, home)
|
|
22
|
+
: {};
|
|
23
|
+
// Strip the internal `repos` registry from the global base before merging.
|
|
24
|
+
const globalBase = { ...globalConfig.config };
|
|
25
|
+
delete globalBase.repos;
|
|
26
|
+
// Precedence (lowest → highest): global base → per-repo global → local.
|
|
27
|
+
const merged = mergeConfig(globalBase, perRepoConfig, localConfig.config);
|
|
28
|
+
// Hooks are spread across all three layers so that different hook keys from
|
|
29
|
+
// different layers both apply (e.g. global afterEnter + local afterCreate).
|
|
30
|
+
// Within each key the higher-precedence layer wins (same spread order).
|
|
31
|
+
const globalHooks = isPlainObject(globalBase.hooks) ? globalBase.hooks : {};
|
|
32
|
+
const perRepoHooks = isPlainObject(perRepoConfig.hooks) ? perRepoConfig.hooks : {};
|
|
19
33
|
const localHooks = isPlainObject(localConfig.config.hooks) ? localConfig.config.hooks : {};
|
|
20
|
-
if (Object.keys(globalHooks).length > 0 ||
|
|
21
|
-
|
|
34
|
+
if (Object.keys(globalHooks).length > 0 ||
|
|
35
|
+
Object.keys(perRepoHooks).length > 0 ||
|
|
36
|
+
Object.keys(localHooks).length > 0) {
|
|
37
|
+
merged.hooks = { ...globalHooks, ...perRepoHooks, ...localHooks };
|
|
22
38
|
}
|
|
23
39
|
return merged;
|
|
24
40
|
}
|
|
@@ -61,6 +77,15 @@ export async function updateGlobalConfigKey(key, value, home = homedir()) {
|
|
|
61
77
|
await saveGlobalConfig(nextConfig, home);
|
|
62
78
|
return nextConfig;
|
|
63
79
|
}
|
|
80
|
+
export async function updateGlobalRepoConfigKey(repoRoot, key, value, home = homedir()) {
|
|
81
|
+
const loaded = await loadGlobalConfig(home);
|
|
82
|
+
const repos = isPlainObject(loaded.config.repos) ? { ...loaded.config.repos } : {};
|
|
83
|
+
const existing = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
|
|
84
|
+
repos[repoRoot] = { ...existing, [key]: value };
|
|
85
|
+
const nextConfig = { ...loaded.config, repos };
|
|
86
|
+
await saveGlobalConfig(nextConfig, home);
|
|
87
|
+
return nextConfig;
|
|
88
|
+
}
|
|
64
89
|
export function GLOBAL_CONFIG_FILE_PATH(home = homedir()) {
|
|
65
90
|
return join(home, GLOBAL_CONFIG_DIRECTORY, GLOBAL_CONFIG_NAME);
|
|
66
91
|
}
|
|
@@ -99,6 +124,22 @@ function mergeConfig(...values) {
|
|
|
99
124
|
...value,
|
|
100
125
|
}), { ...DEFAULT_CONFIG });
|
|
101
126
|
}
|
|
127
|
+
function findPerRepoConfig(repos, repoRoot, home) {
|
|
128
|
+
for (const [key, value] of Object.entries(repos)) {
|
|
129
|
+
const expandedKey = expandTilde(key, home);
|
|
130
|
+
if (expandedKey === repoRoot && isPlainObject(value)) {
|
|
131
|
+
return value;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {};
|
|
135
|
+
}
|
|
136
|
+
function expandTilde(value, home) {
|
|
137
|
+
if (value === '~')
|
|
138
|
+
return home;
|
|
139
|
+
if (value.startsWith('~/'))
|
|
140
|
+
return join(home, value.slice(2));
|
|
141
|
+
return value;
|
|
142
|
+
}
|
|
102
143
|
function isPlainObject(value) {
|
|
103
144
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
104
145
|
}
|
package/dist/init.d.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
export type SupportedShell = 'bash' | 'fish' | 'zsh';
|
|
2
|
+
export type InstallSaveTarget = 'local' | 'global';
|
|
2
3
|
export interface InitCommandOptions {
|
|
3
4
|
cwd: string;
|
|
5
|
+
home?: string;
|
|
6
|
+
promptForInstallSaveTarget?: () => Promise<InstallSaveTarget | null>;
|
|
4
7
|
shell?: string;
|
|
5
8
|
stderr?: (chunk: string) => void;
|
|
6
9
|
stdout: (chunk: string) => void;
|
package/dist/init.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { isCancel, select } from '@clack/prompts';
|
|
5
|
+
import { loadGlobalConfig, updateGlobalConfigKey } from './config.js';
|
|
4
6
|
const START_MARKER = '# >>> gji init >>>';
|
|
5
7
|
const END_MARKER = '# <<< gji init <<<';
|
|
6
8
|
const SHELL_WRAPPED_COMMANDS = [
|
|
@@ -42,6 +44,7 @@ const SHELL_WRAPPED_COMMANDS = [
|
|
|
42
44
|
];
|
|
43
45
|
export async function runInitCommand(options) {
|
|
44
46
|
const shell = resolveShell(options.shell, process.env.SHELL);
|
|
47
|
+
const home = options.home ?? homedir();
|
|
45
48
|
if (!shell) {
|
|
46
49
|
options.stderr?.('Unable to detect a supported shell. Specify one explicitly: bash, fish, or zsh.\n');
|
|
47
50
|
return 1;
|
|
@@ -51,12 +54,25 @@ export async function runInitCommand(options) {
|
|
|
51
54
|
options.stdout(script);
|
|
52
55
|
return 0;
|
|
53
56
|
}
|
|
54
|
-
const rcPath = resolveShellConfigPath(shell,
|
|
57
|
+
const rcPath = resolveShellConfigPath(shell, home);
|
|
55
58
|
await mkdir(dirname(rcPath), { recursive: true });
|
|
56
59
|
const current = await readExistingConfig(rcPath);
|
|
57
60
|
const next = upsertShellIntegration(current, script);
|
|
58
61
|
await writeFile(rcPath, next, 'utf8');
|
|
59
62
|
options.stdout(`${rcPath}\n`);
|
|
63
|
+
// After the shell integration is in place, ask once where to save hooks/prefs.
|
|
64
|
+
// Skip if already configured. When using the default interactive prompt, also
|
|
65
|
+
// require a real TTY so we don't block in piped/headless environments.
|
|
66
|
+
const { config: globalConfig } = await loadGlobalConfig(home);
|
|
67
|
+
const hasCustomPrompt = options.promptForInstallSaveTarget !== undefined;
|
|
68
|
+
const canPrompt = hasCustomPrompt || process.stdout.isTTY === true;
|
|
69
|
+
if (!('installSaveTarget' in globalConfig) && canPrompt) {
|
|
70
|
+
const prompt = options.promptForInstallSaveTarget ?? defaultPromptForInstallSaveTarget;
|
|
71
|
+
const target = await prompt();
|
|
72
|
+
if (target) {
|
|
73
|
+
await updateGlobalConfigKey('installSaveTarget', target, home);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
60
76
|
return 0;
|
|
61
77
|
}
|
|
62
78
|
export function renderShellIntegration(shell) {
|
|
@@ -195,3 +211,16 @@ function indentBlock(value, spaces) {
|
|
|
195
211
|
.map((line) => line.length === 0 ? '' : `${prefix}${line}`)
|
|
196
212
|
.join('\n');
|
|
197
213
|
}
|
|
214
|
+
async function defaultPromptForInstallSaveTarget() {
|
|
215
|
+
const choice = await select({
|
|
216
|
+
message: 'Where should saved hooks and preferences be stored by default?',
|
|
217
|
+
options: [
|
|
218
|
+
{ value: 'local', label: '.gji.json', hint: 'local — committed, shared with the team' },
|
|
219
|
+
{ value: 'global', label: '~/.config/gji/config.json', hint: 'global — personal, never committed' },
|
|
220
|
+
],
|
|
221
|
+
});
|
|
222
|
+
if (isCancel(choice)) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return choice;
|
|
226
|
+
}
|
package/dist/install-prompt.d.ts
CHANGED
|
@@ -6,5 +6,6 @@ export interface InstallPromptDependencies {
|
|
|
6
6
|
promptForInstallChoice?: (pm: PackageManager) => Promise<InstallChoice | null>;
|
|
7
7
|
runInstallCommand?: (command: string, cwd: string, stderr: (chunk: string) => void) => Promise<void>;
|
|
8
8
|
writeConfigKey?: (root: string, key: string, value: unknown) => Promise<void>;
|
|
9
|
+
writeGlobalRepoConfigKey?: (repoRoot: string, key: string, value: unknown) => Promise<void>;
|
|
9
10
|
}
|
|
10
11
|
export declare function maybeRunInstallPrompt(worktreePath: string, repoRoot: string, config: GjiConfig, stderr: (chunk: string) => void, dependencies?: InstallPromptDependencies, nonInteractive?: boolean): Promise<void>;
|
package/dist/install-prompt.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { isCancel, select } from '@clack/prompts';
|
|
3
|
-
import { loadConfig, updateLocalConfigKey } from './config.js';
|
|
3
|
+
import { loadConfig, loadGlobalConfig, updateGlobalRepoConfigKey, updateLocalConfigKey } from './config.js';
|
|
4
4
|
import { isHeadless } from './headless.js';
|
|
5
5
|
import { detectPackageManager } from './package-manager.js';
|
|
6
6
|
export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stderr, dependencies = {}, nonInteractive = false) {
|
|
@@ -36,13 +36,22 @@ 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 === 'global';
|
|
39
40
|
const writeKey = dependencies.writeConfigKey ?? defaultWriteConfigKey;
|
|
41
|
+
const writeGlobalKey = dependencies.writeGlobalRepoConfigKey ?? defaultWriteGlobalRepoConfigKey;
|
|
40
42
|
if (choice === 'always') {
|
|
41
43
|
try {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
if (saveGlobal) {
|
|
45
|
+
// Deep-merge with any existing per-repo global hooks so other keys are preserved.
|
|
46
|
+
const existingHooks = await loadExistingGlobalRepoHooks(repoRoot);
|
|
47
|
+
await writeGlobalKey(repoRoot, 'hooks', { ...existingHooks, afterCreate: pm.installCommand });
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Read local config hooks to deep-merge so other hook keys (e.g. afterEnter) are preserved.
|
|
51
|
+
const { config: localConfig } = await loadConfig(repoRoot);
|
|
52
|
+
const existingLocalHooks = isPlainObject(localConfig.hooks) ? localConfig.hooks : {};
|
|
53
|
+
await writeKey(repoRoot, 'hooks', { ...existingLocalHooks, afterCreate: pm.installCommand });
|
|
54
|
+
}
|
|
46
55
|
}
|
|
47
56
|
catch (error) {
|
|
48
57
|
stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -50,7 +59,12 @@ export async function maybeRunInstallPrompt(worktreePath, repoRoot, config, stde
|
|
|
50
59
|
}
|
|
51
60
|
if (choice === 'never') {
|
|
52
61
|
try {
|
|
53
|
-
|
|
62
|
+
if (saveGlobal) {
|
|
63
|
+
await writeGlobalKey(repoRoot, 'skipInstallPrompt', true);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await writeKey(repoRoot, 'skipInstallPrompt', true);
|
|
67
|
+
}
|
|
54
68
|
}
|
|
55
69
|
catch (error) {
|
|
56
70
|
stderr(`gji: failed to save config: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
@@ -79,6 +93,15 @@ async function defaultRunInstallCommand(command, cwd, stderr) {
|
|
|
79
93
|
async function defaultWriteConfigKey(root, key, value) {
|
|
80
94
|
await updateLocalConfigKey(root, key, value);
|
|
81
95
|
}
|
|
96
|
+
async function defaultWriteGlobalRepoConfigKey(repoRoot, key, value) {
|
|
97
|
+
await updateGlobalRepoConfigKey(repoRoot, key, value);
|
|
98
|
+
}
|
|
99
|
+
async function loadExistingGlobalRepoHooks(repoRoot) {
|
|
100
|
+
const { config: globalConfig } = await loadGlobalConfig();
|
|
101
|
+
const repos = isPlainObject(globalConfig.repos) ? globalConfig.repos : {};
|
|
102
|
+
const perRepo = isPlainObject(repos[repoRoot]) ? repos[repoRoot] : {};
|
|
103
|
+
return isPlainObject(perRepo.hooks) ? perRepo.hooks : {};
|
|
104
|
+
}
|
|
82
105
|
async function defaultPromptForInstallChoice(pm) {
|
|
83
106
|
const choice = await select({
|
|
84
107
|
message: `Run \`${pm.installCommand}\` in the new worktree?`,
|
package/dist/new.js
CHANGED
|
@@ -58,6 +58,7 @@ export function createNewCommand(dependencies = {}) {
|
|
|
58
58
|
else {
|
|
59
59
|
options.stderr(`gji new: ${message} in non-interactive mode (GJI_NO_TUI=1)\n`);
|
|
60
60
|
options.stderr(`Hint: Use 'gji remove ${worktreeName}' or 'gji clean' to remove the existing worktree\n`);
|
|
61
|
+
options.stderr(`Hint: Use 'gji trigger-hook afterCreate' inside the worktree to re-run setup hooks\n`);
|
|
61
62
|
}
|
|
62
63
|
return 1;
|
|
63
64
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { loadEffectiveConfig } from './config.js';
|
|
2
|
+
import { extractHooks, runHook } from './hooks.js';
|
|
3
|
+
import { detectRepository, listWorktrees } from './repo.js';
|
|
4
|
+
const VALID_HOOKS = ['afterCreate', 'afterEnter', 'beforeRemove'];
|
|
5
|
+
function isValidHook(hook) {
|
|
6
|
+
return VALID_HOOKS.includes(hook);
|
|
7
|
+
}
|
|
8
|
+
export async function runTriggerHookCommand(options) {
|
|
9
|
+
if (!isValidHook(options.hook)) {
|
|
10
|
+
options.stderr(`gji trigger-hook: unknown hook '${options.hook}'. Valid hooks: ${VALID_HOOKS.join(', ')}\n`);
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
13
|
+
const hookName = options.hook;
|
|
14
|
+
const repository = await detectRepository(options.cwd);
|
|
15
|
+
const config = await loadEffectiveConfig(repository.repoRoot);
|
|
16
|
+
const hooks = extractHooks(config);
|
|
17
|
+
// Find the branch for the current worktree (undefined for detached HEAD).
|
|
18
|
+
const worktrees = await listWorktrees(options.cwd);
|
|
19
|
+
const currentWorktree = worktrees.find((w) => w.path === repository.currentRoot);
|
|
20
|
+
await runHook(hooks[hookName], repository.currentRoot, {
|
|
21
|
+
branch: currentWorktree?.branch ?? undefined,
|
|
22
|
+
path: repository.currentRoot,
|
|
23
|
+
repo: repository.repoName,
|
|
24
|
+
}, options.stderr);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|