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 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
- ⚑ **Automatic bootstrapping**
161
- Run installs, submodules, and setup scripts automatically for each worktree.
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
- * A bootstrapped environment
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 automatically prepares each worktree.
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. Fallback: `npm install` + submodules
249
+ 7. No setup commands
249
250
 
250
251
  ### Example configuration
251
252
 
252
253
  ```json
253
254
  {
254
255
  "setup-worktree": [
255
- "npm install",
256
+ "pnpm install",
256
257
  "git submodule sync --recursive",
257
258
  "git submodule update --init --recursive",
258
- "echo \"🌳 Realm ready\""
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.
@@ -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? (npm install + submodules)',
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 = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
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? (npm install + submodules)',
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? (npm install + submodules)',
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? (npm install + submodules)',
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 = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
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 { configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
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', 'install dependencies and submodules', 'tending'),
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 (npm install + submodules)')
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 (npm install + submodules)')
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 (npm install + submodules)')
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 (npm install + submodules)')
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 (npm install + submodules)')
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
+ }
@@ -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 = path.join(searchPath, '.yggtree', 'worktree-setup.json');
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
- try {
17
- const config = await fs.readJSON(yggtreeConfigPath);
18
- if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
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
- try {
28
- const config = await fs.readJSON(configPath);
29
- if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
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
- try {
39
- const config = await fs.readJSON(cursorConfigPath);
40
- if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
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
- else {
72
- // Fallback to default behavior
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
  }
@@ -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: npm install -g yggtree'),
65
+ yggMuted('Run: yggtree update'),
66
66
  ];
67
67
  }
68
68
  function badge(value) {
@@ -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: npm install -g yggtree');
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.4.7",
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",