yggtree 1.4.6 β 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 +36 -9
- 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 +5 -1
package/README.md
CHANGED
|
@@ -16,12 +16,14 @@ For guided workflows, command examples, and safety notes, read the full docs:
|
|
|
16
16
|
|
|
17
17
|
### Installation
|
|
18
18
|
|
|
19
|
-
Install
|
|
19
|
+
Install the latest release:
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
-
|
|
22
|
+
curl -fsSL https://yggtree.logbookfordevs.com/install.sh | bash
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
Prefer npm globals? `npm install -g yggtree` still works.
|
|
26
|
+
|
|
25
27
|
Or try it without installing:
|
|
26
28
|
|
|
27
29
|
```bash
|
|
@@ -155,8 +157,8 @@ Started in your main checkout? Carry staged, unstaged, and untracked work into a
|
|
|
155
157
|
π€ **AI-friendly isolation**
|
|
156
158
|
One worktree per agent, per experiment, per idea. No shared state, no collisions.
|
|
157
159
|
|
|
158
|
-
β‘ **
|
|
159
|
-
Run
|
|
160
|
+
β‘ **Configurable bootstrapping**
|
|
161
|
+
Run project setup commands for each worktree when configured.
|
|
160
162
|
|
|
161
163
|
πͺ **Enter, exec, and exit with ease**
|
|
162
164
|
Enter worktrees, execute commands, or run tasks without changing directories.
|
|
@@ -181,7 +183,7 @@ Each command creates:
|
|
|
181
183
|
|
|
182
184
|
* A clean folder
|
|
183
185
|
* A dedicated branch
|
|
184
|
-
*
|
|
186
|
+
* Optional bootstrap setup when configured
|
|
185
187
|
|
|
186
188
|
No stash juggling.
|
|
187
189
|
No branch confusion.
|
|
@@ -233,7 +235,8 @@ yggtree handoff --name auth-refactor
|
|
|
233
235
|
|
|
234
236
|
## β‘ Bootstrapping & Configuration
|
|
235
237
|
|
|
236
|
-
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.
|
|
237
240
|
|
|
238
241
|
Resolution order:
|
|
239
242
|
|
|
@@ -243,21 +246,39 @@ Resolution order:
|
|
|
243
246
|
4. `.yggtree/worktree-setup.json` inside the worktree (per-worktree fallback)
|
|
244
247
|
5. `yggtree-worktree.json` inside the worktree (legacy fallback)
|
|
245
248
|
6. `.cursor/worktrees.json` inside the worktree (legacy fallback)
|
|
246
|
-
7.
|
|
249
|
+
7. No setup commands
|
|
247
250
|
|
|
248
251
|
### Example configuration
|
|
249
252
|
|
|
250
253
|
```json
|
|
251
254
|
{
|
|
252
255
|
"setup-worktree": [
|
|
253
|
-
"
|
|
256
|
+
"pnpm install",
|
|
254
257
|
"git submodule sync --recursive",
|
|
255
258
|
"git submodule update --init --recursive",
|
|
256
|
-
"echo \"
|
|
259
|
+
"echo \"Realm ready\""
|
|
257
260
|
]
|
|
258
261
|
}
|
|
259
262
|
```
|
|
260
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
|
+
|
|
261
282
|
---
|
|
262
283
|
|
|
263
284
|
## ποΈ Global Worktree Paths
|
|
@@ -574,6 +595,12 @@ Reβrun bootstrap commands for a worktree.
|
|
|
574
595
|
|
|
575
596
|
---
|
|
576
597
|
|
|
598
|
+
### `yggtree config bootstrap`
|
|
599
|
+
|
|
600
|
+
Set local bootstrap commands in `.yggtree/worktree-setup.json` under the current directory.
|
|
601
|
+
|
|
602
|
+
---
|
|
603
|
+
|
|
577
604
|
### `yggtree delete [worktrees...]`
|
|
578
605
|
|
|
579
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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yggtree",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"packageManager": "pnpm@11.0.9",
|
|
5
5
|
"description": "Interactive CLI for managing git worktrees and configs",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -22,10 +22,14 @@
|
|
|
22
22
|
"build": "tsc",
|
|
23
23
|
"start": "node dist/index.js",
|
|
24
24
|
"dev": "tsc --watch",
|
|
25
|
+
"release:pack": "./scripts/package-yggtree-release.sh",
|
|
25
26
|
"site:dev": "pnpm --dir apps/site dev",
|
|
26
27
|
"test": "pnpm build && pnpm test:typecheck && vitest run",
|
|
27
28
|
"test:typecheck": "tsc --noEmit -p tsconfig.test.json"
|
|
28
29
|
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
29
33
|
"keywords": [
|
|
30
34
|
"cli",
|
|
31
35
|
"git",
|