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 +105 -28
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +5 -3
- package/dist/commands/wt/create-branch.js +41 -28
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create-sandbox.js +3 -2
- package/dist/commands/wt/create.js +48 -8
- package/dist/commands/wt/delete.js +8 -6
- package/dist/commands/wt/enter.js +5 -3
- package/dist/commands/wt/exec.js +4 -2
- package/dist/commands/wt/handoff.js +7 -0
- package/dist/commands/wt/list.js +3 -1
- package/dist/commands/wt/open.js +152 -79
- package/dist/commands/wt/path.js +4 -2
- package/dist/index.js +113 -46
- package/dist/lib/global-config.js +89 -0
- package/dist/lib/help.js +93 -0
- package/dist/lib/paths.js +0 -1
- package/dist/lib/repo-context.js +3 -1
- package/dist/lib/ui.js +71 -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,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
|
|
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
|
-
|
|
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
|
|
218
|
-
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
318
|
-
*
|
|
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
|
|
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`:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
425
|
-
*
|
|
426
|
-
*
|
|
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`,
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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:
|
|
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 {
|
|
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
|
|
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,
|
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
|
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.
|
|
157
|
+
// 5. Post-create actions
|
|
157
158
|
if (options.exec && options.exec.trim()) {
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
167
|
-
log.
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
179
|
-
|
|
189
|
+
if (shouldEnterShell) {
|
|
190
|
+
await enterCommand(wtPath);
|
|
180
191
|
}
|
|
181
192
|
}
|
|
182
193
|
// 6. Final Output
|
|
183
194
|
log.success('Worktree ready!');
|
|
184
|
-
|
|
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 {
|
|
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 =
|
|
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();
|
|
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';
|
|
@@ -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) =>
|
|
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 {
|