yggtree 1.4.3 โ†’ 1.4.5

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
@@ -40,8 +40,8 @@ worktree workflow first, then load the smallest reference they need:
40
40
  the work already in progress.
41
41
  * **Bootstrap and use a realm**: prepare a worktree, open it in your IDE, or
42
42
  run commands inside it.
43
- * **Run sandbox experiments**: try alternative approaches locally and
44
- apply/unapply the winner safely.
43
+ * **Run sandbox experiments**: try alternative approaches locally, then copy the
44
+ winner back with sandbox-backed apply/unapply.
45
45
 
46
46
  This skill is especially useful with agents like **Claude Code**,
47
47
  **Codex**, **Cursor**, **Gemini CLI**, and other tools that support the open
@@ -110,6 +110,7 @@ All managed worktrees live under:
110
110
  ```
111
111
 
112
112
  This keeps your main repository clean while enabling true parallelism.
113
+ You can change the global managed worktree location with `yggtree config`.
113
114
 
114
115
  ---
115
116
 
@@ -146,7 +147,10 @@ Create, manage, and navigate Git worktrees as a primary workflow, not an afterth
146
147
  Work on multiple branches at the same time, each in its own isolated environment.
147
148
 
148
149
  ๐Ÿงช **Sandbox worktrees for experimentation**
149
- Prototyping something risky? Create a sandbox with a random name, try different strategies, and apply the winner back to your origin branch.
150
+ Prototyping something risky? Create a sandbox, try different strategies, and copy the winner back to your origin checkout.
151
+
152
+ ๐Ÿค **Handoff current work**
153
+ Started in your main checkout? Carry staged, unstaged, and untracked work into a named sandbox worktree and continue there.
150
154
 
151
155
  ๐Ÿค– **AI-friendly isolation**
152
156
  One worktree per agent, per experiment, per idea. No shared state, no collisions.
@@ -158,7 +162,7 @@ Run installs, submodules, and setup scripts automatically for each worktree.
158
162
  Enter worktrees, execute commands, or run tasks without changing directories.
159
163
 
160
164
  ๐Ÿ“ **Predictable structure**
161
- All managed worktrees live under `~/.yggtree`, keeping your repository clean.
165
+ Managed worktrees default to `~/.yggtree`, with an optional global path setting for agent-native layouts.
162
166
 
163
167
  ๐Ÿงญ **Interactive or scriptable**
164
168
  Use the interactive UI or drive everything through commands and flags.
@@ -214,10 +218,16 @@ Sometimes you don't want to "commit to a branch" yet. You just want to try somet
214
218
 
215
219
  1. **Create**: `yggtree create-sandbox` (creates something like `sandbox-a3f2_feature-branch`).
216
220
  2. **Experiment**: Change files, run tests, try that risky refactor.
217
- 3. **Apply**: `yggtree apply` to "push" those file changes back to your origin directory.
218
- 4. **Unapply**: Don't like it? `yggtree unapply` restores your origin to exactly how it was before.
221
+ 3. **Apply**: `yggtree apply` to copy changed sandbox files back to your origin directory with backups.
222
+ 4. **Unapply**: Don't like it? `yggtree unapply` restores those backed-up origin files while the sandbox still exists.
223
+
224
+ Sandboxes are **not pushed to remote**. Omit the name for a generated temporary sandbox, or provide one when the work needs to be easy to find later.
225
+
226
+ Use `handoff` when you started in the origin checkout and want to continue that dirty work in a sandbox. This is the taught path for staged, unstaged, or untracked work:
219
227
 
220
- Sandboxes are **not pushed to remote** and their names are randomly generated because they are meant to be temporary playgrounds.
228
+ ```bash
229
+ yggtree handoff --name auth-refactor
230
+ ```
221
231
 
222
232
  ---
223
233
 
@@ -250,6 +260,48 @@ Resolution order:
250
260
 
251
261
  ---
252
262
 
263
+ ## ๐Ÿ—‚๏ธ Global Worktree Paths
264
+
265
+ By default, managed worktrees use the Yggtree layout:
266
+
267
+ ```bash
268
+ ~/.yggtree/<repo-name>/<worktree-slug>
269
+ ```
270
+
271
+ Show the active global setting:
272
+
273
+ ```bash
274
+ yggtree config get
275
+ ```
276
+
277
+ Use the Codex-style layout when you want Yggtree-created worktrees to live under Codex's worktree root:
278
+
279
+ ```bash
280
+ yggtree config use codex
281
+ ```
282
+
283
+ That preset creates new managed worktrees like:
284
+
285
+ ```bash
286
+ ~/.codex/worktrees/<worktree-slug>/<repo-name>
287
+ ```
288
+
289
+ Set a custom managed root while keeping the Yggtree layout:
290
+
291
+ ```bash
292
+ yggtree config set-worktrees-root ~/Worktrees
293
+ ```
294
+
295
+ Return to defaults:
296
+
297
+ ```bash
298
+ yggtree config reset
299
+ ```
300
+
301
+ Claude and Cursor do not currently have a confirmed native worktree directory pattern in Yggtree. Until that is explicit, use `set-worktrees-root` with the directory you want rather than relying on an unverified preset.
302
+
303
+ ---
304
+
253
305
  ## ๐Ÿ› ๏ธ Command Reference
254
306
 
255
307
  ### `yggtree`
@@ -269,11 +321,13 @@ Options:
269
321
  * `--source local|remote`
270
322
  * `--no-bootstrap`
271
323
  * `--open / --no-open`
324
+ * `--enter / --no-enter`
272
325
  * `--exec "<command>"`
273
326
 
274
327
  Interactive flow:
275
328
 
276
329
  * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open an editor after creation.
330
+ * After creation, yggtree enters the new worktree shell unless you pass `--no-enter`.
277
331
  * `--exec` remains available as an advanced explicit override.
278
332
 
279
333
  <details>
@@ -307,15 +361,17 @@ Options:
307
361
  * `-r, --ref <ref>`: skip picker and use a specific branch (`feature/x` or `origin/feature/x`)
308
362
  * `--no-bootstrap`
309
363
  * `--open / --no-open`: choose whether to open editors or run a startup command before the worktree shell starts
310
- * `--tool <command>`: open a specific editor/app and skip the open prompt (`cursor`, `code`, `codex-app`)
364
+ * `--tool <command>`: open a specific editor, app, or terminal target and skip the open prompt (`cursor`, `code`, `codex-app`, `cmux`, `tmux`)
311
365
  * `--no-enter`: finish after create/open and return to the caller
312
366
  * `--exec "<command>"`
313
367
 
314
368
  Interactive flow:
315
369
 
316
370
  * Yggtree asks what to open before starting the worktree shell.
317
- * Editor choices can be selected together.
318
- * Use `--tool` to skip the open prompt and launch one editor/app directly.
371
+ * Shell-entry flows use a single action picker, so pressing Enter on Cmux or Tmux chooses that terminal target directly.
372
+ * Plain `yggtree open` flows use the same single action picker and return after opening the selected target.
373
+ * Use `--tool` to skip the open prompt and launch one editor/app or terminal target directly.
374
+ * Cmux, Tmux, and `Other command...` are mutually exclusive because the open picker accepts one action.
319
375
  * `Other command...` runs a command in the Yggtree shell first, then leaves you there.
320
376
  * Use `--no-enter` when you only want the worktree created/opened and the command to return.
321
377
  * `--exec` remains available as an advanced explicit override.
@@ -336,12 +392,12 @@ yggtree wc hotfix-auth main --open --no-enter
336
392
 
337
393
  ### `yggtree create-sandbox`
338
394
 
339
- Create a temporary sandbox from your current local branch.
395
+ Create a temporary local-only sandbox from your current branch.
340
396
 
341
397
  Options:
342
398
 
343
399
  * `-n, --name <name>`: Optional sandbox name (auto-generated if omitted).
344
- * `--carry / --no-carry`: Bring uncommitted changes (staged/unstaged/untracked) with you.
400
+ * `--carry / --no-carry`: Explicitly choose whether to copy uncommitted changes into the sandbox. Prefer `handoff` when the intent is continuing dirty work.
345
401
  * `--no-bootstrap`
346
402
  * `--open / --no-open`
347
403
  * `--exec "<command>"`
@@ -354,25 +410,43 @@ Interactive flow:
354
410
 
355
411
  ---
356
412
 
413
+ ### `yggtree handoff`
414
+
415
+ Carry uncommitted work from the current checkout into a sandbox worktree.
416
+
417
+ Options:
418
+
419
+ * `-n, --name <name>`: Optional handoff name (prompted when omitted).
420
+ * `--no-bootstrap`
421
+ * `--open / --no-open`
422
+ * `--exec "<command>"`
423
+
424
+ This is the continuation-focused semantic path for dirty work. It uses sandbox metadata and apply/unapply behavior, but defaults to carrying staged, unstaged, and untracked files.
425
+
426
+ ---
427
+
357
428
  ### `yggtree apply`
358
429
 
359
- Apply changes from the current sandbox back to the origin repository.
360
- * **Backs up** origin files before overwriting.
430
+ Copy changed files from the current sandbox back to the origin checkout.
431
+ * **Backs up** origin files in sandbox metadata before overwriting.
361
432
  * **Offers to delete** the sandbox after applying.
433
+ * **Is not a Git merge**: review the origin diff after applying, especially when deletions are involved.
362
434
 
363
435
  ---
364
436
 
365
437
  ### `yggtree unapply`
366
438
 
367
439
  Undo a previous `apply` operation.
368
- * Restores origin files from the sandbox's backup.
440
+ * Restores origin files from the sandbox's backup metadata.
369
441
  * *Note: Only works if the sandbox worktree still exists.*
370
442
 
371
443
  ---
372
444
 
373
445
  ### `yggtree create-multi`
374
446
 
375
- Create multiple worktrees at once.
447
+ Create multiple official branch-backed worktrees at once. This is a bulk setup
448
+ command; it does not have the full `create` lifecycle flags such as `--open`,
449
+ `--enter`, or `--exec`.
376
450
 
377
451
  Options:
378
452
 
@@ -406,7 +480,7 @@ Columns:
406
480
  Notes:
407
481
 
408
482
  * Entries are grouped by `TYPE`.
409
- * `SANDBOX` and `MANAGED` are worktrees inside `~/.yggtree`.
483
+ * `SANDBOX` and `MANAGED` are worktrees inside the configured managed worktree root.
410
484
  * External worktrees are labeled `LINKED`.
411
485
  * Use `--open` to switch this flow into "pick and open in tool" mode.
412
486
  * The **PR** column shows the pull request status for each branch (e.g. `OPEN`, `IN REVIEW`, `APPROVED`, `MERGED`, `DRAFT`, `CHANGES`). It only appears when `gh` CLI is installed and authenticated โ€” otherwise it's silently omitted.
@@ -415,20 +489,22 @@ Notes:
415
489
 
416
490
  ### `yggtree open [worktree]`
417
491
 
418
- Open a worktree in an editor or supported desktop app.
492
+ Open a worktree in an editor, supported desktop app, or terminal target.
419
493
 
420
494
  Behavior:
421
495
 
422
496
  * If `[worktree]` is omitted, you can pick from the worktree list with type-to-filter search.
423
497
  * Detects available editor commands in your `PATH` (for example: `cursor`, `code`, `zed`, `webstorm`).
424
- * Detects Codex App on macOS and launches it with `open -b com.openai.codex`.
425
- * Lets you choose one or more editors/apps interactively, or pass `--tool`.
426
- * By default, `open` launches editors/apps and returns.
498
+ * Detects Codex App on macOS and launches the selected worktree with `codex app <path>`.
499
+ * Detects Cmux and Tmux when their CLI commands are available.
500
+ * Lets you choose one editor, app, or terminal target interactively, or pass `--tool`.
501
+ * Keeps Cmux, Tmux, and `Other command...` mutually exclusive by using a single action picker.
502
+ * By default, `open` launches the selected target and returns, except foreground terminal targets such as Tmux.
427
503
  * Use `wc --open` when you want to open a worktree and continue in its shell.
428
504
 
429
505
  Options:
430
506
 
431
- * `--tool <command>` (for example: `cursor`, `code`, `codex`, or `codex-app`)
507
+ * `--tool <command>` (for example: `cursor`, `code`, `codex`, `codex-app`, `cmux`, or `tmux`)
432
508
 
433
509
  <details>
434
510
  <summary>Examples</summary>
@@ -437,6 +513,7 @@ Options:
437
513
  yggtree open
438
514
  yggtree open feat/new-ui --tool cursor
439
515
  yggtree open feat/new-ui --tool codex-app
516
+ yggtree open feat/new-ui --tool tmux
440
517
  yggtree list --open
441
518
  ```
442
519
 
@@ -486,7 +563,7 @@ Behavior:
486
563
 
487
564
  Optional:
488
565
 
489
- * `--all` includes linked worktrees outside `~/.yggtree` (main/current worktree is excluded for safety)
566
+ * `--all` includes linked worktrees outside the configured managed root (main/current worktree is excluded for safety)
490
567
 
491
568
  ---
492
569
 
@@ -531,12 +608,12 @@ yggtree create feat/login-flow
531
608
  ---
532
609
 
533
610
  <details>
534
- <summary>Create a worktree without bootstrap and without opening a tool</summary>
611
+ <summary>Create a worktree without bootstrap, opening a tool, or entering</summary>
535
612
 
536
613
  **Command:**
537
614
 
538
615
  ```
539
- yggtree create feat/cleanup-api --no-bootstrap --no-open
616
+ yggtree create feat/cleanup-api --no-bootstrap --no-open --no-enter
540
617
  ```
541
618
 
542
619
  **When to use:**
@@ -630,16 +707,16 @@ Useful when you want to manually navigate or copy the path into scripts.
630
707
  **Command:**
631
708
 
632
709
  ```bash
633
- yggtree create-sandbox --carry
710
+ yggtree handoff --name risky-refactor
634
711
  ```
635
712
 
636
713
  **Scenario:**
637
714
 
638
715
  1. You have 5 files changed in your main repo but aren't sure about the direction.
639
- 2. Run `create-sandbox --carry` to move those changes into an isolated `sandbox-a3f2_feature-branch` folder.
716
+ 2. Run `handoff --name risky-refactor` to carry those changes into an isolated `sandbox-risky-refactor` folder.
640
717
  3. Experiment freely.
641
718
  4. If it works: `yggtree apply`.
642
- 5. If it fails: Just delete the sandbox or `unapply`.
719
+ 5. If it fails after applying: run `yggtree unapply` before deleting the sandbox.
643
720
 
644
721
  </details>
645
722
 
@@ -0,0 +1,53 @@
1
+ import chalk from 'chalk';
2
+ import { formatGlobalConfig, getPresetConfig, getWorktreePathConfig, normalizeWorktreesRootInput, readGlobalConfig, writeGlobalConfig, } from '../lib/global-config.js';
3
+ import { log } from '../lib/ui.js';
4
+ export async function configGetCommand() {
5
+ const [rawConfig, resolvedConfig] = await Promise.all([
6
+ readGlobalConfig(),
7
+ getWorktreePathConfig(),
8
+ ]);
9
+ console.log(chalk.bold('\n Yggtree config\n'));
10
+ formatGlobalConfig(rawConfig, resolvedConfig)
11
+ .forEach(line => console.log(` ${line}`));
12
+ console.log('');
13
+ }
14
+ export async function configUseCommand(preset) {
15
+ const presetConfig = getPresetConfig(preset);
16
+ if (!presetConfig) {
17
+ log.error(`Unknown config preset "${preset}".`);
18
+ log.dim('Available presets: default, yggtree, codex');
19
+ return;
20
+ }
21
+ await writeGlobalConfig(presetConfig);
22
+ log.success(`Using ${preset} worktree layout.`);
23
+ await configGetCommand();
24
+ }
25
+ export async function configSetWorktreesRootCommand(root) {
26
+ const currentConfig = await readGlobalConfig();
27
+ await writeGlobalConfig({
28
+ ...currentConfig,
29
+ worktreesRoot: normalizeWorktreesRootInput(root),
30
+ worktreeLayout: currentConfig.worktreeLayout || 'yggtree',
31
+ });
32
+ log.success('Updated worktrees root.');
33
+ await configGetCommand();
34
+ }
35
+ export async function configSetWorktreeLayoutCommand(layout) {
36
+ if (layout !== 'yggtree' && layout !== 'codex') {
37
+ log.error(`Unknown worktree layout "${layout}".`);
38
+ log.dim('Available layouts: yggtree, codex');
39
+ return;
40
+ }
41
+ const currentConfig = await readGlobalConfig();
42
+ await writeGlobalConfig({
43
+ ...currentConfig,
44
+ worktreeLayout: layout,
45
+ });
46
+ log.success('Updated worktree layout.');
47
+ await configGetCommand();
48
+ }
49
+ export async function configResetCommand() {
50
+ await writeGlobalConfig({});
51
+ log.success('Reset Yggtree config to defaults.');
52
+ await configGetCommand();
53
+ }
@@ -2,23 +2,25 @@ import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import path from 'path';
4
4
  import { listWorktrees, getRepoRoot } from '../../lib/git.js';
5
- import { WORKTREES_ROOT } from '../../lib/paths.js';
5
+ import { getManagedWorktreesRoot } from '../../lib/global-config.js';
6
6
  import { log } from '../../lib/ui.js';
7
7
  import { runBootstrap } from '../../lib/config.js';
8
+ import { isManagedWorktreePath } from '../../lib/worktree.js';
8
9
  export async function bootstrapCommand() {
9
10
  try {
10
11
  const _ = await getRepoRoot();
11
12
  const worktrees = await listWorktrees();
13
+ const managedRoot = await getManagedWorktreesRoot();
12
14
  // 1. Select Worktree
13
15
  // Filter managed or just show all? The prompt said "managed", let's prioritize managed but maybe allow all if needed?
14
16
  // User requirements say "managed", sticking to that for consistency.
15
- const managedWts = worktrees.filter(wt => wt.path.startsWith(WORKTREES_ROOT));
17
+ const managedWts = worktrees.filter(wt => isManagedWorktreePath(wt.path, managedRoot));
16
18
  if (managedWts.length === 0) {
17
19
  log.info('No managed worktrees found to bootstrap.');
18
20
  return;
19
21
  }
20
22
  const choices = managedWts.map(wt => {
21
- const relative = path.relative(WORKTREES_ROOT, wt.path);
23
+ const relative = path.relative(managedRoot, wt.path);
22
24
  return {
23
25
  name: `${chalk.bold(relative)} (${chalk.dim(wt.branch || wt.HEAD)})`,
24
26
  value: wt.path,
@@ -3,19 +3,21 @@ 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 { WORKTREES_ROOT } from '../../lib/paths.js';
6
+ import { buildManagedWorktreePath, getWorktreePathConfig } from '../../lib/global-config.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
8
  import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
9
9
  import { detectInstalledOpenTools, launchOpenTool, promptOpenToolSelection, } from './open.js';
10
+ import { enterCommand } from './enter.js';
10
11
  import { execa } from 'execa';
11
12
  import fs from 'fs-extra';
13
+ export function shouldEnterCreatedWorktree(options) {
14
+ return options.enter !== false;
15
+ }
12
16
  export async function createCommandNew(options) {
13
17
  try {
14
18
  const repoRoot = await getRepoRoot();
15
19
  log.info(`Repo: ${chalk.dim(repoRoot)}`);
16
- if (options.enter !== undefined) {
17
- log.warning('`--enter` / `--no-enter` is deprecated. Use `--open` / `--no-open` instead.');
18
- }
20
+ const shouldEnterShell = shouldEnterCreatedWorktree(options);
19
21
  // 1. Gather inputs
20
22
  const currentBranch = await getCurrentBranch();
21
23
  const answers = await inquirer.prompt([
@@ -68,9 +70,7 @@ export async function createCommandNew(options) {
68
70
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
69
71
  const shouldOpenTool = options.open !== undefined
70
72
  ? options.open
71
- : options.enter !== undefined
72
- ? options.enter
73
- : Boolean(answers.shouldOpenTool);
73
+ : Boolean(answers.shouldOpenTool);
74
74
  let selectedTool;
75
75
  if (options.exec === undefined && shouldOpenTool) {
76
76
  const installedTools = await detectInstalledOpenTools();
@@ -89,7 +89,8 @@ export async function createCommandNew(options) {
89
89
  // e.g. feat/new-button -> feat-new-button
90
90
  const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
91
91
  const repoName = await getRepoName();
92
- const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
92
+ const worktreePathConfig = await getWorktreePathConfig();
93
+ const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
93
94
  // 2. Validation
94
95
  if (!slug)
95
96
  throw new Error('Invalid name');
@@ -153,35 +154,47 @@ export async function createCommandNew(options) {
153
154
  if (shouldBootstrap) {
154
155
  await runBootstrap(wtPath, repoRoot);
155
156
  }
156
- // 5. Exec Command
157
+ // 5. Post-create actions
157
158
  if (options.exec && options.exec.trim()) {
158
- log.info(`Executing: ${options.exec} in ${ui.path(wtPath)}`);
159
- try {
160
- await execa(options.exec, {
161
- cwd: wtPath,
162
- stdio: 'inherit',
163
- shell: true
164
- });
159
+ if (shouldEnterShell) {
160
+ await enterCommand(wtPath, { exec: options.exec });
165
161
  }
166
- catch (error) {
167
- log.actionableError(error.message, options.exec, wtPath, [
168
- `cd ${wtPath} && ${options.exec}`,
169
- 'Check your command syntax and environment variables'
170
- ]);
162
+ else {
163
+ log.info(`Executing: ${options.exec} in ${ui.path(wtPath)}`);
164
+ try {
165
+ await execa(options.exec, {
166
+ cwd: wtPath,
167
+ stdio: 'inherit',
168
+ shell: true
169
+ });
170
+ }
171
+ catch (error) {
172
+ log.actionableError(error.message, options.exec, wtPath, [
173
+ `cd ${wtPath} && ${options.exec}`,
174
+ 'Check your command syntax and environment variables'
175
+ ]);
176
+ }
171
177
  }
172
178
  }
173
- else if (selectedTool) {
174
- try {
175
- log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
176
- await launchOpenTool(selectedTool, wtPath);
179
+ else {
180
+ if (selectedTool) {
181
+ try {
182
+ log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
183
+ await launchOpenTool(selectedTool, wtPath);
184
+ }
185
+ catch (error) {
186
+ log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
187
+ }
177
188
  }
178
- catch (error) {
179
- log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
189
+ if (shouldEnterShell) {
190
+ await enterCommand(wtPath);
180
191
  }
181
192
  }
182
193
  // 6. Final Output
183
194
  log.success('Worktree ready!');
184
- log.header(`cd "${wtPath}"`);
195
+ if (!shouldEnterShell) {
196
+ log.header(`cd "${wtPath}"`);
197
+ }
185
198
  }
186
199
  catch (error) {
187
200
  log.actionableError(error.message, 'yggtree wt create');
@@ -3,7 +3,7 @@ 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 { WORKTREES_ROOT } from '../../lib/paths.js';
6
+ import { buildManagedWorktreePath, getWorktreePathConfig } from '../../lib/global-config.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
8
  import { findLocalEnvFiles, promptAndCopyEnvFilesToWorktrees } from '../../lib/env-files.js';
9
9
  import { execa } from 'execa';
@@ -70,9 +70,10 @@ export async function createCommandMulti(options) {
70
70
  const createdWorktrees = [];
71
71
  // 3. Execution for each branch
72
72
  const repoName = await getRepoName();
73
+ const worktreePathConfig = await getWorktreePathConfig();
73
74
  for (const branchName of branchNames) {
74
75
  const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
75
- const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
76
+ const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
76
77
  log.header(`Processing: ${branchName}`);
77
78
  const wtSpinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
78
79
  // Check if target branch already exists
@@ -3,7 +3,7 @@ import inquirer from 'inquirer';
3
3
  import path from 'path';
4
4
  import { getRepoRoot, getRepoName, getCurrentBranch } from '../../lib/git.js';
5
5
  import { runBootstrap } from '../../lib/config.js';
6
- import { WORKTREES_ROOT } from '../../lib/paths.js';
6
+ import { buildManagedWorktreePath, getWorktreePathConfig } from '../../lib/global-config.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
8
  import { generateSandboxName, normalizeSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
9
9
  import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
@@ -136,7 +136,8 @@ export async function createSandboxCommand(options = {}) {
136
136
  }
137
137
  // 5. Create worktree
138
138
  const repoName = await getRepoName();
139
- const wtPath = path.join(WORKTREES_ROOT, repoName, sandboxName);
139
+ const worktreePathConfig = await getWorktreePathConfig();
140
+ const wtPath = buildManagedWorktreePath(repoName, sandboxName, worktreePathConfig);
140
141
  const spinner = createSpinner('Creating sandbox worktree...').start();
141
142
  try {
142
143
  await fs.ensureDir(path.dirname(wtPath));
@@ -3,7 +3,7 @@ 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 { WORKTREES_ROOT } from '../../lib/paths.js';
6
+ import { buildManagedWorktreePath, getWorktreePathConfig } from '../../lib/global-config.js';
7
7
  import { log, ui, createSpinner } from '../../lib/ui.js';
8
8
  import { ensureAutocompletePrompt } from '../../lib/prompt.js';
9
9
  import { promptAndCopyEnvFiles } from '../../lib/env-files.js';
@@ -74,6 +74,30 @@ export function findExistingBranchWorktree(worktrees, branchName) {
74
74
  !wt.prunable &&
75
75
  fs.pathExistsSync(wt.path));
76
76
  }
77
+ function normalizeExistingPath(value) {
78
+ try {
79
+ return fs.realpathSync(value);
80
+ }
81
+ catch {
82
+ return path.resolve(value);
83
+ }
84
+ }
85
+ function findWorktreeByPath(worktrees, wtPath) {
86
+ const normalizedPath = normalizeExistingPath(wtPath);
87
+ return worktrees.find(wt => normalizeExistingPath(wt.path) === normalizedPath);
88
+ }
89
+ export function getWorktreePathCollisionMessage(worktreeName, wtPath, worktrees) {
90
+ if (!fs.pathExistsSync(wtPath))
91
+ return undefined;
92
+ const occupyingWorktree = findWorktreeByPath(worktrees, wtPath);
93
+ if (occupyingWorktree?.branch) {
94
+ return `Worktree name "${worktreeName}" is already used by branch "${occupyingWorktree.branch}".`;
95
+ }
96
+ if (occupyingWorktree?.prunable) {
97
+ return `Worktree path exists but Git marks its metadata as prunable: ${occupyingWorktree.prunable}.`;
98
+ }
99
+ return 'Worktree path already exists.';
100
+ }
77
101
  export async function createCommand(options) {
78
102
  try {
79
103
  const repoRoot = await ensureRepoContext();
@@ -179,6 +203,9 @@ export async function createCommand(options) {
179
203
  }
180
204
  // 3. Gather remaining inputs
181
205
  const defaultSlug = toSlug(selectedBranch.branchName);
206
+ const repoName = await getRepoName();
207
+ const worktreePathConfig = await getWorktreePathConfig();
208
+ const resolveWorktreePath = (name) => buildManagedWorktreePath(repoName, toSlug(name), worktreePathConfig);
182
209
  const answers = await inquirer.prompt([
183
210
  {
184
211
  type: 'input',
@@ -186,7 +213,12 @@ export async function createCommand(options) {
186
213
  message: 'Worktree name (slug):',
187
214
  default: options.name || defaultSlug,
188
215
  when: !options.name,
189
- validate: (input) => input.trim().length > 0 || 'Name is required',
216
+ validate: (input) => {
217
+ const slug = toSlug(input);
218
+ if (!slug)
219
+ return 'Name is required';
220
+ return getWorktreePathCollisionMessage(slug, resolveWorktreePath(input), existingWorktrees) || true;
221
+ },
190
222
  },
191
223
  {
192
224
  type: 'confirm',
@@ -206,6 +238,20 @@ export async function createCommand(options) {
206
238
  },
207
239
  ]);
208
240
  const name = options.name || answers.name;
241
+ const slug = toSlug(name);
242
+ const wtPath = resolveWorktreePath(name);
243
+ // 4. Validation
244
+ if (!slug)
245
+ throw new Error('Invalid name');
246
+ const pathCollisionMessage = getWorktreePathCollisionMessage(slug, wtPath, existingWorktrees);
247
+ if (pathCollisionMessage) {
248
+ log.actionableError(pathCollisionMessage, `yggtree wc --name ${slug}`, wtPath, [
249
+ 'Choose a different worktree name with --name',
250
+ `Inspect the occupying path: ls ${wtPath}`,
251
+ 'Use yggtree wt prune only if Git reports stale worktree metadata',
252
+ ]);
253
+ return;
254
+ }
209
255
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
210
256
  const shouldOpenTool = options.tool
211
257
  ? true
@@ -234,12 +280,6 @@ export async function createCommand(options) {
234
280
  });
235
281
  }
236
282
  }
237
- const slug = toSlug(name);
238
- const repoName = await getRepoName();
239
- const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
240
- // 4. Validation
241
- if (!slug)
242
- throw new Error('Invalid name');
243
283
  // 5. Execution (checkout-style: attach to selected branch)
244
284
  const spinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
245
285
  try {