yggtree 1.4.7 β 1.5.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 +32 -7
- package/dist/commands/config.js +22 -0
- package/dist/commands/update.js +10 -0
- package/dist/commands/wt/create-branch.js +13 -2
- package/dist/commands/wt/create-multi.js +1 -1
- package/dist/commands/wt/create-sandbox.js +1 -1
- package/dist/commands/wt/create.js +13 -2
- package/dist/index.js +18 -7
- package/dist/lib/bootstrap-config-prompt.js +26 -0
- package/dist/lib/bootstrap-setup-onboarding.js +35 -0
- package/dist/lib/config.js +39 -70
- package/dist/lib/registry.js +14 -0
- package/dist/lib/ui.js +1 -1
- package/dist/lib/update-check.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -157,8 +157,8 @@ Started in your main checkout? Carry staged, unstaged, and untracked work into a
|
|
|
157
157
|
π€ **AI-friendly isolation**
|
|
158
158
|
One worktree per agent, per experiment, per idea. No shared state, no collisions.
|
|
159
159
|
|
|
160
|
-
β‘ **
|
|
161
|
-
Run
|
|
160
|
+
β‘ **Configurable bootstrapping**
|
|
161
|
+
Run project setup commands for each worktree when configured.
|
|
162
162
|
|
|
163
163
|
πͺ **Enter, exec, and exit with ease**
|
|
164
164
|
Enter worktrees, execute commands, or run tasks without changing directories.
|
|
@@ -183,7 +183,7 @@ Each command creates:
|
|
|
183
183
|
|
|
184
184
|
* A clean folder
|
|
185
185
|
* A dedicated branch
|
|
186
|
-
*
|
|
186
|
+
* Optional bootstrap setup when configured
|
|
187
187
|
|
|
188
188
|
No stash juggling.
|
|
189
189
|
No branch confusion.
|
|
@@ -235,7 +235,8 @@ yggtree handoff --name auth-refactor
|
|
|
235
235
|
|
|
236
236
|
## β‘ Bootstrapping & Configuration
|
|
237
237
|
|
|
238
|
-
Yggdrasil
|
|
238
|
+
Yggdrasil can prepare each worktree with configured setup commands. If no bootstrap config is found, it skips setup and prints a small tip.
|
|
239
|
+
The first interactive create flow in a repo offers to create `.yggtree/worktree-setup.json`; declining skips the offer on later runs.
|
|
239
240
|
|
|
240
241
|
Resolution order:
|
|
241
242
|
|
|
@@ -245,21 +246,39 @@ Resolution order:
|
|
|
245
246
|
4. `.yggtree/worktree-setup.json` inside the worktree (per-worktree fallback)
|
|
246
247
|
5. `yggtree-worktree.json` inside the worktree (legacy fallback)
|
|
247
248
|
6. `.cursor/worktrees.json` inside the worktree (legacy fallback)
|
|
248
|
-
7.
|
|
249
|
+
7. No setup commands
|
|
249
250
|
|
|
250
251
|
### Example configuration
|
|
251
252
|
|
|
252
253
|
```json
|
|
253
254
|
{
|
|
254
255
|
"setup-worktree": [
|
|
255
|
-
"
|
|
256
|
+
"pnpm install",
|
|
256
257
|
"git submodule sync --recursive",
|
|
257
258
|
"git submodule update --init --recursive",
|
|
258
|
-
"echo \"
|
|
259
|
+
"echo \"Realm ready\""
|
|
259
260
|
]
|
|
260
261
|
}
|
|
261
262
|
```
|
|
262
263
|
|
|
264
|
+
Create local setup config in the current directory interactively:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
yggtree config bootstrap
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
Or create it non-interactively:
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
yggtree config bootstrap --command "pnpm install" --command "pnpm test"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Clear local setup config from the current directory:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
yggtree config bootstrap --clear
|
|
280
|
+
```
|
|
281
|
+
|
|
263
282
|
---
|
|
264
283
|
|
|
265
284
|
## ποΈ Global Worktree Paths
|
|
@@ -576,6 +595,12 @@ Reβrun bootstrap commands for a worktree.
|
|
|
576
595
|
|
|
577
596
|
---
|
|
578
597
|
|
|
598
|
+
### `yggtree config bootstrap`
|
|
599
|
+
|
|
600
|
+
Set local bootstrap commands in `.yggtree/worktree-setup.json` under the current directory.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
579
604
|
### `yggtree delete [worktrees...]`
|
|
580
605
|
|
|
581
606
|
Delete worktrees interactively or by explicit name.
|
package/dist/commands/config.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import { clearBootstrapCommands, writeBootstrapCommands, } from '../lib/config.js';
|
|
3
|
+
import { promptBootstrapCommands } from '../lib/bootstrap-config-prompt.js';
|
|
2
4
|
import { formatGlobalConfig, getPresetConfig, getWorktreePathConfig, normalizeWorktreesRootInput, readGlobalConfig, writeGlobalConfig, } from '../lib/global-config.js';
|
|
3
5
|
import { log } from '../lib/ui.js';
|
|
4
6
|
export async function configGetCommand() {
|
|
@@ -46,6 +48,26 @@ export async function configSetWorktreeLayoutCommand(layout) {
|
|
|
46
48
|
log.success('Updated worktree layout.');
|
|
47
49
|
await configGetCommand();
|
|
48
50
|
}
|
|
51
|
+
export async function configBootstrapCommand(options) {
|
|
52
|
+
const targetRoot = process.cwd();
|
|
53
|
+
if (options.clear) {
|
|
54
|
+
const configPath = await clearBootstrapCommands(targetRoot);
|
|
55
|
+
log.success('Cleared local bootstrap commands.');
|
|
56
|
+
log.dim(`Removed ${chalk.cyan(configPath)} if it existed.`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const commandOptions = options.command?.map(command => command.trim()).filter(Boolean) ?? [];
|
|
60
|
+
const commands = commandOptions.length > 0
|
|
61
|
+
? commandOptions
|
|
62
|
+
: await promptBootstrapCommands();
|
|
63
|
+
if (commands.length === 0) {
|
|
64
|
+
log.info('No bootstrap commands provided.');
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const configPath = await writeBootstrapCommands(targetRoot, commands);
|
|
68
|
+
log.success('Updated local bootstrap commands.');
|
|
69
|
+
log.dim(`Saved to ${chalk.cyan(configPath)}.`);
|
|
70
|
+
}
|
|
49
71
|
export async function configResetCommand() {
|
|
50
72
|
await writeGlobalConfig({});
|
|
51
73
|
log.success('Reset Yggtree config to defaults.');
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { log } from '../lib/ui.js';
|
|
3
|
+
export const INSTALL_COMMAND = 'curl -fsSL https://yggtree.logbookfordevs.com/install.sh | bash';
|
|
4
|
+
export async function updateCommand() {
|
|
5
|
+
log.info('Installing the latest Yggtree release...');
|
|
6
|
+
log.dim(`Run: ${INSTALL_COMMAND}`);
|
|
7
|
+
await execa('bash', ['-c', INSTALL_COMMAND], {
|
|
8
|
+
stdio: 'inherit',
|
|
9
|
+
});
|
|
10
|
+
}
|
|
@@ -3,9 +3,11 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensureCorrectUpstream, publishBranch } from '../../lib/git.js';
|
|
5
5
|
import { runBootstrap } from '../../lib/config.js';
|
|
6
|
+
import { maybeOfferBootstrapSetupConfig } from '../../lib/bootstrap-setup-onboarding.js';
|
|
6
7
|
import { buildManagedWorktreePath, getWorktreePathConfig } from '../../lib/global-config.js';
|
|
7
8
|
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
8
9
|
import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
|
|
10
|
+
import { readRegistry } from '../../lib/registry.js';
|
|
9
11
|
import { detectInstalledOpenTools, launchOpenTool, promptOpenToolSelection, } from './open.js';
|
|
10
12
|
import { enterCommand } from './enter.js';
|
|
11
13
|
import { execa } from 'execa';
|
|
@@ -15,11 +17,18 @@ export function shouldEnterCreatedWorktree(options) {
|
|
|
15
17
|
}
|
|
16
18
|
export async function createCommandNew(options) {
|
|
17
19
|
try {
|
|
20
|
+
const registeredReposBefore = await readRegistry();
|
|
18
21
|
const repoRoot = await getRepoRoot();
|
|
19
22
|
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
20
23
|
const shouldEnterShell = shouldEnterCreatedWorktree(options);
|
|
24
|
+
const isInteractiveCreate = !options.branch || !options.base || (!options.base && !options.source);
|
|
21
25
|
// 1. Gather inputs
|
|
22
26
|
const currentBranch = await getCurrentBranch();
|
|
27
|
+
const bootstrapSetupOnboarding = await maybeOfferBootstrapSetupConfig({
|
|
28
|
+
interactive: isInteractiveCreate,
|
|
29
|
+
registeredReposBefore,
|
|
30
|
+
repoRoot,
|
|
31
|
+
});
|
|
23
32
|
const answers = await inquirer.prompt([
|
|
24
33
|
{
|
|
25
34
|
type: 'input',
|
|
@@ -52,7 +61,7 @@ export async function createCommandNew(options) {
|
|
|
52
61
|
{
|
|
53
62
|
type: 'confirm',
|
|
54
63
|
name: 'bootstrap',
|
|
55
|
-
message: 'Run bootstrap?
|
|
64
|
+
message: 'Run configured bootstrap commands?',
|
|
56
65
|
default: true,
|
|
57
66
|
when: options.bootstrap !== false && options.bootstrap !== true,
|
|
58
67
|
},
|
|
@@ -67,7 +76,9 @@ export async function createCommandNew(options) {
|
|
|
67
76
|
const branchName = options.branch || answers.branch;
|
|
68
77
|
let baseRef = options.base || answers.base;
|
|
69
78
|
const source = options.source || answers.source;
|
|
70
|
-
const shouldBootstrap =
|
|
79
|
+
const shouldBootstrap = bootstrapSetupOnboarding.skipBootstrap
|
|
80
|
+
? false
|
|
81
|
+
: options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
71
82
|
const shouldOpenTool = options.open !== undefined
|
|
72
83
|
? options.open
|
|
73
84
|
: Boolean(answers.shouldOpenTool);
|
|
@@ -44,7 +44,7 @@ export async function createCommandMulti(options) {
|
|
|
44
44
|
{
|
|
45
45
|
type: 'confirm',
|
|
46
46
|
name: 'bootstrap',
|
|
47
|
-
message: 'Run bootstrap for all worktrees?
|
|
47
|
+
message: 'Run configured bootstrap commands for all worktrees?',
|
|
48
48
|
default: true,
|
|
49
49
|
when: options.bootstrap !== false && options.bootstrap !== true,
|
|
50
50
|
}
|
|
@@ -60,7 +60,7 @@ export async function createSandboxCommand(options = {}) {
|
|
|
60
60
|
{
|
|
61
61
|
type: 'confirm',
|
|
62
62
|
name: 'bootstrap',
|
|
63
|
-
message: 'Run bootstrap?
|
|
63
|
+
message: 'Run configured bootstrap commands?',
|
|
64
64
|
default: true,
|
|
65
65
|
when: options.bootstrap === undefined,
|
|
66
66
|
},
|
|
@@ -3,11 +3,13 @@ import inquirer from 'inquirer';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { getRepoName, createWorktree, fetchAll, listWorktrees } from '../../lib/git.js';
|
|
5
5
|
import { runBootstrap } from '../../lib/config.js';
|
|
6
|
+
import { maybeOfferBootstrapSetupConfig } from '../../lib/bootstrap-setup-onboarding.js';
|
|
6
7
|
import { buildManagedWorktreePath, getWorktreePathConfig } from '../../lib/global-config.js';
|
|
7
8
|
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
8
9
|
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
9
10
|
import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
|
|
10
11
|
import { ensureRepoContext } from '../../lib/repo-context.js';
|
|
12
|
+
import { readRegistry } from '../../lib/registry.js';
|
|
11
13
|
import { enterCommand } from './enter.js';
|
|
12
14
|
import { detectInstalledOpenTools, promptOpenActions, resolveOpenToolAction, runOpenActions, } from './open.js';
|
|
13
15
|
import { execa } from 'execa';
|
|
@@ -100,8 +102,15 @@ export function getWorktreePathCollisionMessage(worktreeName, wtPath, worktrees)
|
|
|
100
102
|
}
|
|
101
103
|
export async function createCommand(options) {
|
|
102
104
|
try {
|
|
105
|
+
const registeredReposBefore = await readRegistry();
|
|
103
106
|
const repoRoot = await ensureRepoContext();
|
|
104
107
|
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
108
|
+
const isInteractiveCreate = !options.ref || !options.name;
|
|
109
|
+
const bootstrapSetupOnboarding = await maybeOfferBootstrapSetupConfig({
|
|
110
|
+
interactive: isInteractiveCreate,
|
|
111
|
+
registeredReposBefore,
|
|
112
|
+
repoRoot,
|
|
113
|
+
});
|
|
105
114
|
// 1. Load branches
|
|
106
115
|
const loadingSpinner = createSpinner('Fetching branches...').start();
|
|
107
116
|
await fetchAll();
|
|
@@ -223,7 +232,7 @@ export async function createCommand(options) {
|
|
|
223
232
|
{
|
|
224
233
|
type: 'confirm',
|
|
225
234
|
name: 'bootstrap',
|
|
226
|
-
message: 'Run bootstrap?
|
|
235
|
+
message: 'Run configured bootstrap commands?',
|
|
227
236
|
default: true,
|
|
228
237
|
when: options.bootstrap !== false && options.bootstrap !== true,
|
|
229
238
|
},
|
|
@@ -252,7 +261,9 @@ export async function createCommand(options) {
|
|
|
252
261
|
]);
|
|
253
262
|
return;
|
|
254
263
|
}
|
|
255
|
-
const shouldBootstrap =
|
|
264
|
+
const shouldBootstrap = bootstrapSetupOnboarding.skipBootstrap
|
|
265
|
+
? false
|
|
266
|
+
: options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
256
267
|
const shouldOpenTool = options.tool
|
|
257
268
|
? true
|
|
258
269
|
: options.open !== undefined
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,8 @@ import { applyCommand } from './commands/wt/apply.js';
|
|
|
20
20
|
import { unapplyCommand } from './commands/wt/unapply.js';
|
|
21
21
|
import { handoffCommand } from './commands/wt/handoff.js';
|
|
22
22
|
import { copyEnvCommand } from './commands/wt/copy-env.js';
|
|
23
|
-
import {
|
|
23
|
+
import { updateCommand } from './commands/update.js';
|
|
24
|
+
import { configBootstrapCommand, configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
|
|
24
25
|
import { getVersion } from './lib/version.js';
|
|
25
26
|
import { checkForUpdate } from './lib/update-check.js';
|
|
26
27
|
import { findSandboxRoot } from './lib/sandbox.js';
|
|
@@ -48,7 +49,7 @@ const mainMenuChoices = {
|
|
|
48
49
|
list: mainMenuChoice('list', 'β', 'Survey realms', 'scan active worktrees and PR state', 'tending'),
|
|
49
50
|
'create-multi': mainMenuChoice('create-multi', 'β§', 'Grow many realms', 'create multiple branch worktrees', 'growth'),
|
|
50
51
|
'create-sandbox': mainMenuChoice('create-sandbox', 'β³', 'Forge sandbox', 'create a local experiment realm', 'sandbox'),
|
|
51
|
-
bootstrap: mainMenuChoice('bootstrap', 'β³', 'Bootstrap realm', '
|
|
52
|
+
bootstrap: mainMenuChoice('bootstrap', 'β³', 'Bootstrap realm', 'run configured setup commands', 'tending'),
|
|
52
53
|
exec: mainMenuChoice('exec', 'β', 'Cast command', 'run a command inside a worktree', 'tending'),
|
|
53
54
|
path: mainMenuChoice('path', 'β', 'Reveal path', 'print a worktree cd command', 'tending'),
|
|
54
55
|
'copy-env': mainMenuChoice('copy-env', 'β', 'Bring env files', 'copy local env files from main realm', 'tending'),
|
|
@@ -112,7 +113,7 @@ function registerWorktreeCommands(parent) {
|
|
|
112
113
|
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
113
114
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
114
115
|
.option('--source <type>', 'Base source (local or remote)')
|
|
115
|
-
.option('--no-bootstrap', 'Skip bootstrap
|
|
116
|
+
.option('--no-bootstrap', 'Skip configured bootstrap commands')
|
|
116
117
|
.option('--open', 'Open an editor after creation')
|
|
117
118
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
118
119
|
.option('--enter', 'Enter the worktree sub-shell after creation')
|
|
@@ -130,7 +131,7 @@ function registerWorktreeCommands(parent) {
|
|
|
130
131
|
.description('Bulk-create official branch-backed worktrees')
|
|
131
132
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
132
133
|
.option('--source <type>', 'Base source (local or remote)')
|
|
133
|
-
.option('--no-bootstrap', 'Skip bootstrap
|
|
134
|
+
.option('--no-bootstrap', 'Skip configured bootstrap commands')
|
|
134
135
|
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
135
136
|
.addHelpText('after', createMultiHelp)
|
|
136
137
|
.action(async (options) => {
|
|
@@ -141,7 +142,7 @@ function registerWorktreeCommands(parent) {
|
|
|
141
142
|
.description(description)
|
|
142
143
|
.option('-n, --name <slug>', 'Worktree name (slug)')
|
|
143
144
|
.option('-r, --ref <ref>', 'Existing branch or ref')
|
|
144
|
-
.option('--no-bootstrap', 'Skip bootstrap
|
|
145
|
+
.option('--no-bootstrap', 'Skip configured bootstrap commands')
|
|
145
146
|
.option('--open', 'Open editors or run a startup command before entering')
|
|
146
147
|
.option('--no-open', 'Skip opening editors or startup commands before entering')
|
|
147
148
|
.option('--tool <command>', 'Editor, app, or terminal command to open after checkout (skips open prompt)')
|
|
@@ -203,7 +204,7 @@ function registerWorktreeCommands(parent) {
|
|
|
203
204
|
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
204
205
|
.option('--carry', 'Copy uncommitted changes to sandbox')
|
|
205
206
|
.option('--no-carry', 'Do not copy uncommitted changes')
|
|
206
|
-
.option('--no-bootstrap', 'Skip bootstrap
|
|
207
|
+
.option('--no-bootstrap', 'Skip configured bootstrap commands')
|
|
207
208
|
.option('--open', 'Open an editor after creation')
|
|
208
209
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
209
210
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
@@ -217,7 +218,7 @@ function registerWorktreeCommands(parent) {
|
|
|
217
218
|
parent.command('handoff')
|
|
218
219
|
.description('Carry dirty current work into a named sandbox')
|
|
219
220
|
.option('-n, --name <name>', 'Optional handoff name (prompted when omitted)')
|
|
220
|
-
.option('--no-bootstrap', 'Skip bootstrap
|
|
221
|
+
.option('--no-bootstrap', 'Skip configured bootstrap commands')
|
|
221
222
|
.option('--open', 'Open an editor after creation')
|
|
222
223
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
223
224
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
@@ -341,6 +342,9 @@ wt.addHelpText('after', intentRouterHelp);
|
|
|
341
342
|
registerWorktreeCommands(program);
|
|
342
343
|
registerWorktreeCommands(wt);
|
|
343
344
|
program.addHelpCommand(true);
|
|
345
|
+
program.command('update')
|
|
346
|
+
.description('Install the latest Yggtree release')
|
|
347
|
+
.action(updateCommand);
|
|
344
348
|
program.command('bifrost')
|
|
345
349
|
.description('Summon the Bifrost (Easter Egg)')
|
|
346
350
|
.action(bifrostCommand);
|
|
@@ -360,6 +364,13 @@ config.command('set-worktrees-root <path>')
|
|
|
360
364
|
config.command('set-worktree-layout <layout>')
|
|
361
365
|
.description('Set the managed worktree path layout (yggtree, codex, or claude)')
|
|
362
366
|
.action(configSetWorktreeLayoutCommand);
|
|
367
|
+
config.command('bootstrap')
|
|
368
|
+
.description('Set local worktree bootstrap commands in the current directory')
|
|
369
|
+
.option('-c, --command <command>', 'Add a bootstrap command; repeat for multiple commands', (value, previous) => {
|
|
370
|
+
return [...(previous ?? []), value];
|
|
371
|
+
})
|
|
372
|
+
.option('--clear', 'Remove local bootstrap commands from the current directory')
|
|
373
|
+
.action(configBootstrapCommand);
|
|
363
374
|
config.command('reset')
|
|
364
375
|
.description('Reset global settings to defaults')
|
|
365
376
|
.action(configResetCommand);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
export async function promptBootstrapCommands() {
|
|
3
|
+
const commands = [];
|
|
4
|
+
let shouldContinue = true;
|
|
5
|
+
while (shouldContinue) {
|
|
6
|
+
const { command } = await inquirer.prompt([
|
|
7
|
+
{
|
|
8
|
+
type: 'input',
|
|
9
|
+
name: 'command',
|
|
10
|
+
message: commands.length === 0 ? 'Bootstrap command:' : 'Next bootstrap command:',
|
|
11
|
+
validate: (value) => value.trim().length > 0 || 'Enter a command.',
|
|
12
|
+
},
|
|
13
|
+
]);
|
|
14
|
+
commands.push(command.trim());
|
|
15
|
+
const answer = await inquirer.prompt([
|
|
16
|
+
{
|
|
17
|
+
type: 'confirm',
|
|
18
|
+
name: 'addAnother',
|
|
19
|
+
message: 'Add another command?',
|
|
20
|
+
default: false,
|
|
21
|
+
},
|
|
22
|
+
]);
|
|
23
|
+
shouldContinue = answer.addAnother;
|
|
24
|
+
}
|
|
25
|
+
return commands;
|
|
26
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getBootstrapCommands, writeBootstrapCommands } from './config.js';
|
|
5
|
+
import { promptBootstrapCommands } from './bootstrap-config-prompt.js';
|
|
6
|
+
import { hasRegisteredRepoPath, registerRepo } from './registry.js';
|
|
7
|
+
import { log } from './ui.js';
|
|
8
|
+
export async function maybeOfferBootstrapSetupConfig(options) {
|
|
9
|
+
if (!options.interactive || !process.stdin.isTTY) {
|
|
10
|
+
return { created: false, skipBootstrap: false };
|
|
11
|
+
}
|
|
12
|
+
if (await hasRegisteredRepoPath(options.repoRoot, options.registeredReposBefore)) {
|
|
13
|
+
return { created: false, skipBootstrap: false };
|
|
14
|
+
}
|
|
15
|
+
if (await getBootstrapCommands(options.repoRoot)) {
|
|
16
|
+
return { created: false, skipBootstrap: false };
|
|
17
|
+
}
|
|
18
|
+
const { shouldCreateSetupConfig } = await inquirer.prompt([
|
|
19
|
+
{
|
|
20
|
+
type: 'confirm',
|
|
21
|
+
name: 'shouldCreateSetupConfig',
|
|
22
|
+
message: 'Create .yggtree/worktree-setup.json for this repo?',
|
|
23
|
+
default: false,
|
|
24
|
+
},
|
|
25
|
+
]);
|
|
26
|
+
if (!shouldCreateSetupConfig) {
|
|
27
|
+
await registerRepo(path.basename(options.repoRoot), options.repoRoot);
|
|
28
|
+
return { created: false, skipBootstrap: true };
|
|
29
|
+
}
|
|
30
|
+
const commands = await promptBootstrapCommands();
|
|
31
|
+
const configPath = await writeBootstrapCommands(options.repoRoot, commands);
|
|
32
|
+
log.success('Created local bootstrap config.');
|
|
33
|
+
log.dim(`Saved to ${chalk.cyan(configPath)}.`);
|
|
34
|
+
return { created: true, skipBootstrap: false };
|
|
35
|
+
}
|
package/dist/lib/config.js
CHANGED
|
@@ -2,6 +2,21 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { log, createSpinner } from './ui.js';
|
|
5
|
+
export function getWorktreeSetupConfigPath(root) {
|
|
6
|
+
return path.join(root, '.yggtree', 'worktree-setup.json');
|
|
7
|
+
}
|
|
8
|
+
async function readBootstrapCommands(configPath) {
|
|
9
|
+
try {
|
|
10
|
+
const config = await fs.readJSON(configPath);
|
|
11
|
+
if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
|
|
12
|
+
return config['setup-worktree'];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
log.warning(`Failed to parse ${configPath}.`);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
5
20
|
export async function getBootstrapCommands(repoRoot, wtPath) {
|
|
6
21
|
// repoRoot first (source of truth β where yggtree is run from)
|
|
7
22
|
// wtPath second (per-worktree override if needed)
|
|
@@ -9,45 +24,38 @@ export async function getBootstrapCommands(repoRoot, wtPath) {
|
|
|
9
24
|
if (wtPath && wtPath !== repoRoot)
|
|
10
25
|
searchPaths.push(wtPath);
|
|
11
26
|
for (const searchPath of searchPaths) {
|
|
12
|
-
const yggtreeConfigPath =
|
|
27
|
+
const yggtreeConfigPath = getWorktreeSetupConfigPath(searchPath);
|
|
13
28
|
const configPath = path.join(searchPath, 'yggtree-worktree.json');
|
|
14
29
|
const cursorConfigPath = path.join(searchPath, '.cursor', 'worktrees.json');
|
|
15
30
|
if (await fs.pathExists(yggtreeConfigPath)) {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
return config['setup-worktree'];
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
catch (e) {
|
|
23
|
-
log.warning(`Failed to parse ${yggtreeConfigPath}.`);
|
|
24
|
-
}
|
|
31
|
+
const commands = await readBootstrapCommands(yggtreeConfigPath);
|
|
32
|
+
if (commands)
|
|
33
|
+
return commands;
|
|
25
34
|
}
|
|
26
35
|
if (await fs.pathExists(configPath)) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return config['setup-worktree'];
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
log.warning(`Failed to parse ${configPath}.`);
|
|
35
|
-
}
|
|
36
|
+
const commands = await readBootstrapCommands(configPath);
|
|
37
|
+
if (commands)
|
|
38
|
+
return commands;
|
|
36
39
|
}
|
|
37
40
|
if (await fs.pathExists(cursorConfigPath)) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return config['setup-worktree'];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch (e) {
|
|
45
|
-
log.warning(`Failed to parse ${cursorConfigPath}.`);
|
|
46
|
-
}
|
|
41
|
+
const commands = await readBootstrapCommands(cursorConfigPath);
|
|
42
|
+
if (commands)
|
|
43
|
+
return commands;
|
|
47
44
|
}
|
|
48
45
|
}
|
|
49
46
|
return null;
|
|
50
47
|
}
|
|
48
|
+
export async function writeBootstrapCommands(root, commands) {
|
|
49
|
+
const configPath = getWorktreeSetupConfigPath(root);
|
|
50
|
+
await fs.ensureDir(path.dirname(configPath));
|
|
51
|
+
await fs.writeJSON(configPath, { 'setup-worktree': commands }, { spaces: 2 });
|
|
52
|
+
return configPath;
|
|
53
|
+
}
|
|
54
|
+
export async function clearBootstrapCommands(root) {
|
|
55
|
+
const configPath = getWorktreeSetupConfigPath(root);
|
|
56
|
+
await fs.remove(configPath);
|
|
57
|
+
return configPath;
|
|
58
|
+
}
|
|
51
59
|
export async function runBootstrap(wtPath, repoRoot) {
|
|
52
60
|
const customCommands = await getBootstrapCommands(repoRoot, wtPath);
|
|
53
61
|
if (customCommands) {
|
|
@@ -67,47 +75,8 @@ export async function runBootstrap(wtPath, repoRoot) {
|
|
|
67
75
|
]);
|
|
68
76
|
}
|
|
69
77
|
}
|
|
78
|
+
return;
|
|
70
79
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
log.info('Running default bootstrap (npm install + submodules)...');
|
|
74
|
-
// npm install
|
|
75
|
-
try {
|
|
76
|
-
await execa('npm', ['--version']);
|
|
77
|
-
const installSpinner = createSpinner('Running npm install...').start();
|
|
78
|
-
try {
|
|
79
|
-
await execa('npm', ['install'], { cwd: wtPath });
|
|
80
|
-
installSpinner.succeed('Dependencies installed.');
|
|
81
|
-
}
|
|
82
|
-
catch (e) {
|
|
83
|
-
installSpinner.fail('npm install failed.');
|
|
84
|
-
log.actionableError(e.message, 'npm install', wtPath, [
|
|
85
|
-
`cd ${wtPath} && npm install`,
|
|
86
|
-
'Check your network connection and package-lock.json'
|
|
87
|
-
]);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
log.warning('npm not found, skipping install.');
|
|
92
|
-
}
|
|
93
|
-
// Submodules
|
|
94
|
-
const subSpinner = createSpinner('Syncing submodules...').start();
|
|
95
|
-
try {
|
|
96
|
-
await execa('git', ['submodule', 'sync', '--recursive'], { cwd: wtPath });
|
|
97
|
-
await execa('git', ['submodule', 'update', '--init', '--recursive'], { cwd: wtPath });
|
|
98
|
-
subSpinner.succeed('Submodules synced.');
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
subSpinner.fail('Submodule sync failed.');
|
|
102
|
-
const isAuthError = /permission denied|authentication|publickey/i.test(e.message);
|
|
103
|
-
const steps = [
|
|
104
|
-
'git submodule sync --recursive',
|
|
105
|
-
'git submodule update --init --recursive'
|
|
106
|
-
];
|
|
107
|
-
if (isAuthError) {
|
|
108
|
-
steps.unshift('ssh -T git@github.com', 'ssh-add --apple-use-keychain ~/.ssh/id_ed25519');
|
|
109
|
-
}
|
|
110
|
-
log.actionableError(e.message, 'git submodule update --init --recursive', wtPath, steps);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
80
|
+
log.info('No bootstrap commands configured. Skipping setup.');
|
|
81
|
+
log.dim('Tip: run yggtree config bootstrap --command "pnpm install" to create .yggtree/worktree-setup.json.');
|
|
113
82
|
}
|
package/dist/lib/registry.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
|
+
import { realpath } from 'fs/promises';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { YGG_ROOT } from './paths.js';
|
|
4
5
|
const REGISTRY_PATH = path.join(YGG_ROOT, 'registry.json');
|
|
@@ -79,3 +80,16 @@ export async function getValidRegisteredRepos() {
|
|
|
79
80
|
}
|
|
80
81
|
return valid;
|
|
81
82
|
}
|
|
83
|
+
async function normalizeRepoPath(repoPath) {
|
|
84
|
+
try {
|
|
85
|
+
return await realpath(repoPath);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return path.resolve(repoPath);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export async function hasRegisteredRepoPath(repoRoot, registry) {
|
|
92
|
+
const targetPath = await normalizeRepoPath(repoRoot);
|
|
93
|
+
const registeredPaths = await Promise.all(Object.values(registry.repos).map(normalizeRepoPath));
|
|
94
|
+
return registeredPaths.includes(targetPath);
|
|
95
|
+
}
|
package/dist/lib/ui.js
CHANGED
|
@@ -62,7 +62,7 @@ function renderUpdateNotice(update) {
|
|
|
62
62
|
return [
|
|
63
63
|
'',
|
|
64
64
|
`${yggWarn('Update available')} ${yggMuted(`yggtree ${update.currentVersion} -> ${update.latestVersion}`)}`,
|
|
65
|
-
yggMuted('Run:
|
|
65
|
+
yggMuted('Run: yggtree update'),
|
|
66
66
|
];
|
|
67
67
|
}
|
|
68
68
|
function badge(value) {
|
package/dist/lib/update-check.js
CHANGED
|
@@ -51,5 +51,5 @@ export async function notifyIfUpdateAvailable() {
|
|
|
51
51
|
if (!result?.updateAvailable)
|
|
52
52
|
return;
|
|
53
53
|
log.warning(`A newer yggtree is available: ${chalk.dim(`v${result.currentVersion}`)} -> ${chalk.green(`v${result.latestVersion}`)}`);
|
|
54
|
-
log.dim('Update with:
|
|
54
|
+
log.dim('Update with: yggtree update');
|
|
55
55
|
}
|