yggtree 1.4.4 → 1.4.6

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,7 @@ 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, 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.
150
151
 
151
152
  🤝 **Handoff current work**
152
153
  Started in your main checkout? Carry staged, unstaged, and untracked work into a named sandbox worktree and continue there.
@@ -161,7 +162,7 @@ Run installs, submodules, and setup scripts automatically for each worktree.
161
162
  Enter worktrees, execute commands, or run tasks without changing directories.
162
163
 
163
164
  📍 **Predictable structure**
164
- 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.
165
166
 
166
167
  🧭 **Interactive or scriptable**
167
168
  Use the interactive UI or drive everything through commands and flags.
@@ -217,12 +218,12 @@ Sometimes you don't want to "commit to a branch" yet. You just want to try somet
217
218
 
218
219
  1. **Create**: `yggtree create-sandbox` (creates something like `sandbox-a3f2_feature-branch`).
219
220
  2. **Experiment**: Change files, run tests, try that risky refactor.
220
- 3. **Apply**: `yggtree apply` to "push" those file changes back to your origin directory.
221
- 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.
222
223
 
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.
224
225
 
225
- Use `handoff` when you started in the origin checkout and want to continue that dirty work in a sandbox:
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:
226
227
 
227
228
  ```bash
228
229
  yggtree handoff --name auth-refactor
@@ -259,6 +260,67 @@ Resolution order:
259
260
 
260
261
  ---
261
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
+ Use the Claude Code layout when you want Yggtree-created worktrees to match Claude's native repo-local worktree directory:
290
+
291
+ ```bash
292
+ yggtree config use claude
293
+ ```
294
+
295
+ That preset creates new managed worktrees like:
296
+
297
+ ```bash
298
+ <repo-root>/.claude/worktrees/<worktree-slug>
299
+ ```
300
+
301
+ Set a custom managed root while keeping the Yggtree layout:
302
+
303
+ ```bash
304
+ yggtree config set-worktrees-root ~/Worktrees
305
+ ```
306
+
307
+ Return to defaults:
308
+
309
+ ```bash
310
+ yggtree config reset
311
+ ```
312
+
313
+ Use a path preset for one worktree without changing the saved global config:
314
+
315
+ ```bash
316
+ yggtree create feat/agent-native --config claude
317
+ yggtree wc --ref main --name fresh-main --config yggtree
318
+ ```
319
+
320
+ Cursor does 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.
321
+
322
+ ---
323
+
262
324
  ## 🛠️ Command Reference
263
325
 
264
326
  ### `yggtree`
@@ -280,6 +342,7 @@ Options:
280
342
  * `--open / --no-open`
281
343
  * `--enter / --no-enter`
282
344
  * `--exec "<command>"`
345
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
283
346
 
284
347
  Interactive flow:
285
348
 
@@ -321,6 +384,7 @@ Options:
321
384
  * `--tool <command>`: open a specific editor, app, or terminal target and skip the open prompt (`cursor`, `code`, `codex-app`, `cmux`, `tmux`)
322
385
  * `--no-enter`: finish after create/open and return to the caller
323
386
  * `--exec "<command>"`
387
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
324
388
 
325
389
  Interactive flow:
326
390
 
@@ -349,15 +413,16 @@ yggtree wc hotfix-auth main --open --no-enter
349
413
 
350
414
  ### `yggtree create-sandbox`
351
415
 
352
- Create a temporary sandbox from your current local branch.
416
+ Create a temporary local-only sandbox from your current branch.
353
417
 
354
418
  Options:
355
419
 
356
420
  * `-n, --name <name>`: Optional sandbox name (auto-generated if omitted).
357
- * `--carry / --no-carry`: Bring uncommitted changes (staged/unstaged/untracked) with you.
421
+ * `--carry / --no-carry`: Explicitly choose whether to copy uncommitted changes into the sandbox. Prefer `handoff` when the intent is continuing dirty work.
358
422
  * `--no-bootstrap`
359
423
  * `--open / --no-open`
360
424
  * `--exec "<command>"`
425
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
361
426
 
362
427
  Interactive flow:
363
428
 
@@ -377,36 +442,41 @@ Options:
377
442
  * `--no-bootstrap`
378
443
  * `--open / --no-open`
379
444
  * `--exec "<command>"`
445
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
380
446
 
381
- This is the continuation-focused version of `create-sandbox --carry`: it keeps sandbox metadata and apply/unapply behavior, but defaults to carrying staged, unstaged, and untracked files.
447
+ 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.
382
448
 
383
449
  ---
384
450
 
385
451
  ### `yggtree apply`
386
452
 
387
- Apply changes from the current sandbox back to the origin repository.
388
- * **Backs up** origin files before overwriting.
453
+ Copy changed files from the current sandbox back to the origin checkout.
454
+ * **Backs up** origin files in sandbox metadata before overwriting.
389
455
  * **Offers to delete** the sandbox after applying.
456
+ * **Is not a Git merge**: review the origin diff after applying, especially when deletions are involved.
390
457
 
391
458
  ---
392
459
 
393
460
  ### `yggtree unapply`
394
461
 
395
462
  Undo a previous `apply` operation.
396
- * Restores origin files from the sandbox's backup.
463
+ * Restores origin files from the sandbox's backup metadata.
397
464
  * *Note: Only works if the sandbox worktree still exists.*
398
465
 
399
466
  ---
400
467
 
401
468
  ### `yggtree create-multi`
402
469
 
403
- Create multiple worktrees at once.
470
+ Create multiple official branch-backed worktrees at once. This is a bulk setup
471
+ command; it does not have the full `create` lifecycle flags such as `--open`,
472
+ `--enter`, or `--exec`.
404
473
 
405
474
  Options:
406
475
 
407
476
  * `--base <ref>`
408
477
  * `--source local|remote`
409
478
  * `--no-bootstrap`
479
+ * `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
410
480
 
411
481
  <details>
412
482
  <summary>Example</summary>
@@ -434,7 +504,7 @@ Columns:
434
504
  Notes:
435
505
 
436
506
  * Entries are grouped by `TYPE`.
437
- * `SANDBOX` and `MANAGED` are worktrees inside `~/.yggtree`.
507
+ * `SANDBOX` and `MANAGED` are worktrees inside the configured managed worktree root.
438
508
  * External worktrees are labeled `LINKED`.
439
509
  * Use `--open` to switch this flow into "pick and open in tool" mode.
440
510
  * 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.
@@ -449,7 +519,7 @@ Behavior:
449
519
 
450
520
  * If `[worktree]` is omitted, you can pick from the worktree list with type-to-filter search.
451
521
  * Detects available editor commands in your `PATH` (for example: `cursor`, `code`, `zed`, `webstorm`).
452
- * Detects Codex App on macOS and launches it with `open -b com.openai.codex`.
522
+ * Detects Codex App on macOS and launches the selected worktree with `codex app <path>`.
453
523
  * Detects Cmux and Tmux when their CLI commands are available.
454
524
  * Lets you choose one editor, app, or terminal target interactively, or pass `--tool`.
455
525
  * Keeps Cmux, Tmux, and `Other command...` mutually exclusive by using a single action picker.
@@ -504,20 +574,31 @@ Re‑run bootstrap commands for a worktree.
504
574
 
505
575
  ---
506
576
 
507
- ### `yggtree delete`
577
+ ### `yggtree delete [worktrees...]`
508
578
 
509
- Interactively delete worktrees.
579
+ Delete worktrees interactively or by explicit name.
510
580
 
511
581
  Behavior:
512
582
 
513
583
  * Default flow targets managed worktrees.
514
584
  * In interactive mode, yggtree asks whether to include external linked worktrees.
515
- * In direct CLI usage, `--all` includes external linked worktrees (main/current are still excluded for safety).
585
+ * In direct CLI usage, `--all` includes external worktrees labeled `LINKED` in `yggtree list` (main/current are still excluded for safety).
586
+ * Non-interactive deletion requires explicit targets or `--all`, plus `--yes`.
516
587
  * The delete selector shows 6 items per page.
517
588
 
518
589
  Optional:
519
590
 
520
- * `--all` includes linked worktrees outside `~/.yggtree` (main/current worktree is excluded for safety)
591
+ * `--all` includes external worktrees outside the configured managed root (main/current worktree is excluded for safety)
592
+ * `-y, --yes` confirms deletion without prompts
593
+
594
+ Examples:
595
+
596
+ ```bash
597
+ yggtree delete my-feature --yes
598
+ yggtree delete my-feature other-feature --yes
599
+ yggtree delete external-feature --all --yes
600
+ yggtree delete --all --yes
601
+ ```
521
602
 
522
603
  ---
523
604
 
@@ -661,7 +742,7 @@ Useful when you want to manually navigate or copy the path into scripts.
661
742
  **Command:**
662
743
 
663
744
  ```bash
664
- yggtree create-sandbox --carry
745
+ yggtree handoff --name risky-refactor
665
746
  ```
666
747
 
667
748
  **Scenario:**
@@ -670,7 +751,7 @@ yggtree create-sandbox --carry
670
751
  2. Run `handoff --name risky-refactor` to carry those changes into an isolated `sandbox-risky-refactor` folder.
671
752
  3. Experiment freely.
672
753
  4. If it works: `yggtree apply`.
673
- 5. If it fails: Just delete the sandbox or `unapply`.
754
+ 5. If it fails after applying: run `yggtree unapply` before deleting the sandbox.
674
755
 
675
756
  </details>
676
757
 
@@ -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, claude');
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' && layout !== 'claude') {
37
+ log.error(`Unknown worktree layout "${layout}".`);
38
+ log.dim('Available layouts: yggtree, codex, claude');
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
- const _ = await getRepoRoot();
11
+ const repoRoot = await getRepoRoot();
11
12
  const worktrees = await listWorktrees();
13
+ const managedRoot = await getManagedWorktreesRoot(repoRoot);
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,
@@ -33,7 +35,6 @@ export async function bootstrapCommand() {
33
35
  },
34
36
  ]);
35
37
  const wtPath = selectedPath;
36
- const repoRoot = await getRepoRoot();
37
38
  await runBootstrap(wtPath, repoRoot);
38
39
  log.success('Bootstrap completed!');
39
40
  }
@@ -0,0 +1,31 @@
1
+ import path from 'path';
2
+ import { findLocalEnvFiles, promptAndCopyEnvFiles } from '../../lib/env-files.js';
3
+ import { getRepoRoot, listWorktrees } from '../../lib/git.js';
4
+ import { log } from '../../lib/ui.js';
5
+ export async function copyEnvCommand() {
6
+ try {
7
+ const currentWorktreePath = await getRepoRoot();
8
+ const worktrees = await listWorktrees();
9
+ const mainWorktreePath = worktrees[0]?.path;
10
+ if (!mainWorktreePath) {
11
+ log.info('No main worktree found.');
12
+ return;
13
+ }
14
+ if (path.resolve(currentWorktreePath) === path.resolve(mainWorktreePath)) {
15
+ log.info('Already in the main worktree. Nothing to copy.');
16
+ return;
17
+ }
18
+ const envFiles = await findLocalEnvFiles(mainWorktreePath);
19
+ if (envFiles.length === 0) {
20
+ log.info('No local env files found in the main worktree.');
21
+ return;
22
+ }
23
+ await promptAndCopyEnvFiles(mainWorktreePath, currentWorktreePath, envFiles, {
24
+ promptMessage: `Copy local env file${envFiles.length === 1 ? '' : 's'} from the main worktree to this worktree? (${envFiles.join(', ')})`,
25
+ });
26
+ }
27
+ catch (error) {
28
+ log.actionableError(error.message, 'yggtree wt copy-env');
29
+ process.exit(1);
30
+ }
31
+ }
@@ -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 { promptAndCopyEnvFiles } from '../../lib/env-files.js';
9
9
  import { detectInstalledOpenTools, launchOpenTool, promptOpenToolSelection, } from './open.js';
@@ -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(repoRoot, options.config);
93
+ const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
93
94
  // 2. Validation
94
95
  if (!slug)
95
96
  throw new Error('Invalid name');
@@ -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(repoRoot, options.config);
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(repoRoot, options.config);
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';
@@ -204,7 +204,8 @@ export async function createCommand(options) {
204
204
  // 3. Gather remaining inputs
205
205
  const defaultSlug = toSlug(selectedBranch.branchName);
206
206
  const repoName = await getRepoName();
207
- const resolveWorktreePath = (name) => path.join(WORKTREES_ROOT, repoName, toSlug(name));
207
+ const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
208
+ const resolveWorktreePath = (name) => buildManagedWorktreePath(repoName, toSlug(name), worktreePathConfig);
208
209
  const answers = await inquirer.prompt([
209
210
  {
210
211
  type: 'input',
@@ -1,19 +1,60 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import { listWorktrees, removeWorktree, getRepoRoot, getLastActivity } from '../../lib/git.js';
4
+ import { getManagedWorktreesRoot } from '../../lib/global-config.js';
4
5
  import { log, createSpinner, timeAgo } from '../../lib/ui.js';
5
6
  import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
6
- export async function deleteCommand(options = {}) {
7
+ function normalizeTarget(value) {
8
+ return value.trim().toLowerCase();
9
+ }
10
+ function matchesDeleteTarget(wtPath, branchName, displayPath, target) {
11
+ const normalizedTarget = normalizeTarget(target);
12
+ return [
13
+ wtPath,
14
+ branchName,
15
+ displayPath,
16
+ wtPath.split('/').pop() || '',
17
+ ].some(value => normalizeTarget(value) === normalizedTarget);
18
+ }
19
+ function resolveDeleteWorktrees(worktrees, targets, options) {
20
+ const eligibleWts = worktrees.filter(wt => {
21
+ const isMainWorktree = wt.path === options.mainWorktreePath;
22
+ const isCurrentWorktree = wt.path === options.currentWorktreePath;
23
+ if (isMainWorktree || isCurrentWorktree)
24
+ return false;
25
+ if (options.includeAll) {
26
+ return true;
27
+ }
28
+ return isManagedWorktreePath(wt.path, options.managedRoot);
29
+ });
30
+ if (targets.length === 0) {
31
+ return eligibleWts;
32
+ }
33
+ return eligibleWts.filter(wt => {
34
+ const branchName = getWorktreeBranchName(wt);
35
+ const displayPath = formatWorktreeDisplayPath(wt.path, options.managedRoot);
36
+ return targets.some(target => matchesDeleteTarget(wt.path, branchName, displayPath, target));
37
+ });
38
+ }
39
+ export async function deleteCommand(targets = [], options = {}) {
7
40
  try {
8
41
  const currentWorktreePath = await getRepoRoot();
9
42
  const worktrees = await listWorktrees();
43
+ const managedRoot = await getManagedWorktreesRoot(currentWorktreePath);
10
44
  let showAll = options.all;
11
- if (showAll === undefined) {
45
+ const hasExplicitTargets = targets.length > 0;
46
+ const isNonInteractive = process.env.CI === 'true' || !process.stdin.isTTY;
47
+ if (showAll === undefined && !hasExplicitTargets) {
48
+ if (isNonInteractive) {
49
+ log.error('Non-interactive delete requires worktree names or --all.');
50
+ log.dim('Try: yggtree delete <worktree> --yes');
51
+ return;
52
+ }
12
53
  const { includeExternal } = await inquirer.prompt([
13
54
  {
14
55
  type: 'confirm',
15
56
  name: 'includeExternal',
16
- message: 'Include external linked worktrees (outside ~/.yggtree)?',
57
+ message: 'Include external linked worktrees (outside the managed root)?',
17
58
  default: false,
18
59
  },
19
60
  ]);
@@ -21,62 +62,79 @@ export async function deleteCommand(options = {}) {
21
62
  }
22
63
  const includeAll = Boolean(showAll);
23
64
  const mainWorktreePath = worktrees[0]?.path;
24
- const deletableWts = worktrees.filter(wt => {
25
- if (includeAll) {
26
- const isMainWorktree = wt.path === mainWorktreePath;
27
- const isCurrentWorktree = wt.path === currentWorktreePath;
28
- return !isMainWorktree && !isCurrentWorktree;
29
- }
30
- return isManagedWorktreePath(wt.path);
65
+ const deletableWts = resolveDeleteWorktrees(worktrees, targets, {
66
+ includeAll,
67
+ currentWorktreePath,
68
+ mainWorktreePath,
69
+ managedRoot,
31
70
  });
32
71
  if (deletableWts.length === 0) {
72
+ if (hasExplicitTargets) {
73
+ log.error(`No deletable worktrees matched: ${targets.join(', ')}`);
74
+ log.dim(includeAll
75
+ ? 'Main and current worktrees are protected from deletion.'
76
+ : 'Use "yggtree delete --all <worktree> --yes" to include linked worktrees outside the managed root.');
77
+ return;
78
+ }
33
79
  log.info(includeAll
34
80
  ? 'No deletable linked worktrees found.'
35
81
  : 'No managed worktrees found to delete. Use "yggtree wt delete --all" to include external linked worktrees.');
36
82
  return;
37
83
  }
38
- const choices = await Promise.all(deletableWts.map(async (wt) => {
39
- const [activity, type] = await Promise.all([
40
- getLastActivity(wt.path),
41
- detectWorktreeType(wt, mainWorktreePath || ''),
84
+ let selectedPaths = deletableWts.map(wt => wt.path);
85
+ const shouldPromptForSelection = !hasExplicitTargets && !(options.all === true && options.yes);
86
+ if (shouldPromptForSelection) {
87
+ const choices = await Promise.all(deletableWts.map(async (wt) => {
88
+ const [activity, type] = await Promise.all([
89
+ getLastActivity(wt.path),
90
+ detectWorktreeType(wt, mainWorktreePath || '', managedRoot),
91
+ ]);
92
+ const branchName = getWorktreeBranchName(wt);
93
+ const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
94
+ const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
95
+ return {
96
+ name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
97
+ value: wt.path,
98
+ };
99
+ }));
100
+ const answer = await inquirer.prompt([
101
+ {
102
+ type: 'checkbox',
103
+ name: 'selectedPaths',
104
+ message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
105
+ choices: choices,
106
+ pageSize: 10,
107
+ },
42
108
  ]);
43
- const branchName = getWorktreeBranchName(wt);
44
- const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('—');
45
- const displayPath = formatWorktreeDisplayPath(wt.path);
46
- return {
47
- name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
48
- value: wt.path,
49
- };
50
- }));
51
- const { selectedPaths } = await inquirer.prompt([
52
- {
53
- type: 'checkbox',
54
- name: 'selectedPaths',
55
- message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
56
- choices: choices,
57
- pageSize: 10,
58
- },
59
- ]);
109
+ selectedPaths = answer.selectedPaths;
110
+ }
60
111
  if (!selectedPaths || selectedPaths.length === 0) {
61
112
  log.info('No worktrees selected.');
62
113
  return;
63
114
  }
64
115
  const count = selectedPaths.length;
65
- const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p));
66
- const { confirm } = await inquirer.prompt([
67
- {
68
- type: 'confirm',
69
- name: 'confirm',
70
- message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
71
- default: false,
72
- },
73
- ]);
74
- if (!confirm) {
75
- log.info('Deletion aborted.');
76
- return;
116
+ const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p, managedRoot));
117
+ if (!options.yes) {
118
+ if (isNonInteractive) {
119
+ log.error('Non-interactive delete requires --yes.');
120
+ log.dim(`Try: yggtree delete ${targets.join(' ')} --yes`);
121
+ return;
122
+ }
123
+ const { confirm } = await inquirer.prompt([
124
+ {
125
+ type: 'confirm',
126
+ name: 'confirm',
127
+ message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
128
+ default: false,
129
+ },
130
+ ]);
131
+ if (!confirm) {
132
+ log.info('Deletion aborted.');
133
+ return;
134
+ }
77
135
  }
78
136
  for (const wtPath of selectedPaths) {
79
- const worktreeName = formatWorktreeDisplayPath(wtPath);
137
+ const worktreeName = formatWorktreeDisplayPath(wtPath, managedRoot);
80
138
  const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
81
139
  try {
82
140
  await removeWorktree(wtPath);