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 +103 -22
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +6 -5
- package/dist/commands/wt/copy-env.js +31 -0
- package/dist/commands/wt/create-branch.js +3 -2
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create-sandbox.js +3 -2
- package/dist/commands/wt/create.js +3 -2
- package/dist/commands/wt/delete.js +102 -44
- package/dist/commands/wt/enter.js +6 -4
- package/dist/commands/wt/exec.js +5 -3
- package/dist/commands/wt/list.js +4 -2
- package/dist/commands/wt/open.js +30 -12
- package/dist/commands/wt/path.js +5 -3
- package/dist/index.js +141 -57
- package/dist/lib/global-config.js +118 -0
- package/dist/lib/help.js +127 -0
- package/dist/lib/main-menu.js +71 -0
- package/dist/lib/paths.js +0 -1
- package/dist/lib/repo-context.js +3 -1
- package/dist/lib/ui.js +77 -6
- package/dist/lib/worktree.js +14 -10
- package/package.json +1 -1
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
|
|
44
|
-
apply/unapply
|
|
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
|
|
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
|
-
|
|
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
|
|
221
|
-
4. **Unapply**: Don't like it? `yggtree unapply` restores
|
|
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
|
|
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`:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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 {
|
|
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
|
|
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
|
|
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(
|
|
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 {
|
|
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
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
44
|
-
|
|
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
|
-
|
|
67
|
-
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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);
|