yggtree 1.4.5 → 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 +40 -5
- package/dist/commands/config.js +3 -3
- package/dist/commands/wt/bootstrap.js +2 -3
- package/dist/commands/wt/copy-env.js +31 -0
- package/dist/commands/wt/create-branch.js +1 -1
- package/dist/commands/wt/create-multi.js +1 -1
- package/dist/commands/wt/create-sandbox.js +1 -1
- package/dist/commands/wt/create.js +1 -1
- package/dist/commands/wt/delete.js +98 -42
- package/dist/commands/wt/enter.js +2 -2
- package/dist/commands/wt/exec.js +2 -2
- package/dist/commands/wt/list.js +2 -2
- package/dist/commands/wt/open.js +2 -2
- package/dist/commands/wt/path.js +2 -2
- package/dist/index.js +93 -59
- package/dist/lib/global-config.js +41 -12
- package/dist/lib/help.js +34 -0
- package/dist/lib/main-menu.js +71 -0
- package/dist/lib/ui.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -286,6 +286,18 @@ That preset creates new managed worktrees like:
|
|
|
286
286
|
~/.codex/worktrees/<worktree-slug>/<repo-name>
|
|
287
287
|
```
|
|
288
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
|
+
|
|
289
301
|
Set a custom managed root while keeping the Yggtree layout:
|
|
290
302
|
|
|
291
303
|
```bash
|
|
@@ -298,7 +310,14 @@ Return to defaults:
|
|
|
298
310
|
yggtree config reset
|
|
299
311
|
```
|
|
300
312
|
|
|
301
|
-
|
|
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.
|
|
302
321
|
|
|
303
322
|
---
|
|
304
323
|
|
|
@@ -323,6 +342,7 @@ Options:
|
|
|
323
342
|
* `--open / --no-open`
|
|
324
343
|
* `--enter / --no-enter`
|
|
325
344
|
* `--exec "<command>"`
|
|
345
|
+
* `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
|
|
326
346
|
|
|
327
347
|
Interactive flow:
|
|
328
348
|
|
|
@@ -364,6 +384,7 @@ Options:
|
|
|
364
384
|
* `--tool <command>`: open a specific editor, app, or terminal target and skip the open prompt (`cursor`, `code`, `codex-app`, `cmux`, `tmux`)
|
|
365
385
|
* `--no-enter`: finish after create/open and return to the caller
|
|
366
386
|
* `--exec "<command>"`
|
|
387
|
+
* `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
|
|
367
388
|
|
|
368
389
|
Interactive flow:
|
|
369
390
|
|
|
@@ -401,6 +422,7 @@ Options:
|
|
|
401
422
|
* `--no-bootstrap`
|
|
402
423
|
* `--open / --no-open`
|
|
403
424
|
* `--exec "<command>"`
|
|
425
|
+
* `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
|
|
404
426
|
|
|
405
427
|
Interactive flow:
|
|
406
428
|
|
|
@@ -420,6 +442,7 @@ Options:
|
|
|
420
442
|
* `--no-bootstrap`
|
|
421
443
|
* `--open / --no-open`
|
|
422
444
|
* `--exec "<command>"`
|
|
445
|
+
* `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
|
|
423
446
|
|
|
424
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.
|
|
425
448
|
|
|
@@ -453,6 +476,7 @@ Options:
|
|
|
453
476
|
* `--base <ref>`
|
|
454
477
|
* `--source local|remote`
|
|
455
478
|
* `--no-bootstrap`
|
|
479
|
+
* `--config <preset>`: use `yggtree`, `codex`, or `claude` path settings for this run only
|
|
456
480
|
|
|
457
481
|
<details>
|
|
458
482
|
<summary>Example</summary>
|
|
@@ -550,20 +574,31 @@ Re‑run bootstrap commands for a worktree.
|
|
|
550
574
|
|
|
551
575
|
---
|
|
552
576
|
|
|
553
|
-
### `yggtree delete`
|
|
577
|
+
### `yggtree delete [worktrees...]`
|
|
554
578
|
|
|
555
|
-
|
|
579
|
+
Delete worktrees interactively or by explicit name.
|
|
556
580
|
|
|
557
581
|
Behavior:
|
|
558
582
|
|
|
559
583
|
* Default flow targets managed worktrees.
|
|
560
584
|
* In interactive mode, yggtree asks whether to include external linked worktrees.
|
|
561
|
-
* 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`.
|
|
562
587
|
* The delete selector shows 6 items per page.
|
|
563
588
|
|
|
564
589
|
Optional:
|
|
565
590
|
|
|
566
|
-
* `--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
|
+
```
|
|
567
602
|
|
|
568
603
|
---
|
|
569
604
|
|
package/dist/commands/config.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function configUseCommand(preset) {
|
|
|
15
15
|
const presetConfig = getPresetConfig(preset);
|
|
16
16
|
if (!presetConfig) {
|
|
17
17
|
log.error(`Unknown config preset "${preset}".`);
|
|
18
|
-
log.dim('Available presets: default, yggtree, codex');
|
|
18
|
+
log.dim('Available presets: default, yggtree, codex, claude');
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
await writeGlobalConfig(presetConfig);
|
|
@@ -33,9 +33,9 @@ export async function configSetWorktreesRootCommand(root) {
|
|
|
33
33
|
await configGetCommand();
|
|
34
34
|
}
|
|
35
35
|
export async function configSetWorktreeLayoutCommand(layout) {
|
|
36
|
-
if (layout !== 'yggtree' && layout !== 'codex') {
|
|
36
|
+
if (layout !== 'yggtree' && layout !== 'codex' && layout !== 'claude') {
|
|
37
37
|
log.error(`Unknown worktree layout "${layout}".`);
|
|
38
|
-
log.dim('Available layouts: yggtree, codex');
|
|
38
|
+
log.dim('Available layouts: yggtree, codex, claude');
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
const currentConfig = await readGlobalConfig();
|
|
@@ -8,9 +8,9 @@ import { runBootstrap } from '../../lib/config.js';
|
|
|
8
8
|
import { isManagedWorktreePath } from '../../lib/worktree.js';
|
|
9
9
|
export async function bootstrapCommand() {
|
|
10
10
|
try {
|
|
11
|
-
const
|
|
11
|
+
const repoRoot = await getRepoRoot();
|
|
12
12
|
const worktrees = await listWorktrees();
|
|
13
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
13
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
14
14
|
// 1. Select Worktree
|
|
15
15
|
// Filter managed or just show all? The prompt said "managed", let's prioritize managed but maybe allow all if needed?
|
|
16
16
|
// User requirements say "managed", sticking to that for consistency.
|
|
@@ -35,7 +35,6 @@ export async function bootstrapCommand() {
|
|
|
35
35
|
},
|
|
36
36
|
]);
|
|
37
37
|
const wtPath = selectedPath;
|
|
38
|
-
const repoRoot = await getRepoRoot();
|
|
39
38
|
await runBootstrap(wtPath, repoRoot);
|
|
40
39
|
log.success('Bootstrap completed!');
|
|
41
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
|
+
}
|
|
@@ -89,7 +89,7 @@ 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 worktreePathConfig = await getWorktreePathConfig();
|
|
92
|
+
const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
|
|
93
93
|
const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
|
|
94
94
|
// 2. Validation
|
|
95
95
|
if (!slug)
|
|
@@ -70,7 +70,7 @@ 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
|
+
const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
|
|
74
74
|
for (const branchName of branchNames) {
|
|
75
75
|
const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
|
|
76
76
|
const wtPath = buildManagedWorktreePath(repoName, slug, worktreePathConfig);
|
|
@@ -136,7 +136,7 @@ export async function createSandboxCommand(options = {}) {
|
|
|
136
136
|
}
|
|
137
137
|
// 5. Create worktree
|
|
138
138
|
const repoName = await getRepoName();
|
|
139
|
-
const worktreePathConfig = await getWorktreePathConfig();
|
|
139
|
+
const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
|
|
140
140
|
const wtPath = buildManagedWorktreePath(repoName, sandboxName, worktreePathConfig);
|
|
141
141
|
const spinner = createSpinner('Creating sandbox worktree...').start();
|
|
142
142
|
try {
|
|
@@ -204,7 +204,7 @@ 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 worktreePathConfig = await getWorktreePathConfig();
|
|
207
|
+
const worktreePathConfig = await getWorktreePathConfig(repoRoot, options.config);
|
|
208
208
|
const resolveWorktreePath = (name) => buildManagedWorktreePath(repoName, toSlug(name), worktreePathConfig);
|
|
209
209
|
const answers = await inquirer.prompt([
|
|
210
210
|
{
|
|
@@ -4,13 +4,52 @@ import { listWorktrees, removeWorktree, getRepoRoot, getLastActivity } from '../
|
|
|
4
4
|
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
5
5
|
import { log, createSpinner, timeAgo } from '../../lib/ui.js';
|
|
6
6
|
import { detectWorktreeType, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, isManagedWorktreePath, } from '../../lib/worktree.js';
|
|
7
|
-
|
|
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 = {}) {
|
|
8
40
|
try {
|
|
9
41
|
const currentWorktreePath = await getRepoRoot();
|
|
10
42
|
const worktrees = await listWorktrees();
|
|
11
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
43
|
+
const managedRoot = await getManagedWorktreesRoot(currentWorktreePath);
|
|
12
44
|
let showAll = options.all;
|
|
13
|
-
|
|
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
|
+
}
|
|
14
53
|
const { includeExternal } = await inquirer.prompt([
|
|
15
54
|
{
|
|
16
55
|
type: 'confirm',
|
|
@@ -23,59 +62,76 @@ export async function deleteCommand(options = {}) {
|
|
|
23
62
|
}
|
|
24
63
|
const includeAll = Boolean(showAll);
|
|
25
64
|
const mainWorktreePath = worktrees[0]?.path;
|
|
26
|
-
const deletableWts = worktrees
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
return isManagedWorktreePath(wt.path, managedRoot);
|
|
65
|
+
const deletableWts = resolveDeleteWorktrees(worktrees, targets, {
|
|
66
|
+
includeAll,
|
|
67
|
+
currentWorktreePath,
|
|
68
|
+
mainWorktreePath,
|
|
69
|
+
managedRoot,
|
|
33
70
|
});
|
|
34
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
|
+
}
|
|
35
79
|
log.info(includeAll
|
|
36
80
|
? 'No deletable linked worktrees found.'
|
|
37
81
|
: 'No managed worktrees found to delete. Use "yggtree wt delete --all" to include external linked worktrees.');
|
|
38
82
|
return;
|
|
39
83
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
},
|
|
44
108
|
]);
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
48
|
-
return {
|
|
49
|
-
name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('·')} ${active} ${chalk.dim('·')} ${chalk.dim(displayPath)}`,
|
|
50
|
-
value: wt.path,
|
|
51
|
-
};
|
|
52
|
-
}));
|
|
53
|
-
const { selectedPaths } = await inquirer.prompt([
|
|
54
|
-
{
|
|
55
|
-
type: 'checkbox',
|
|
56
|
-
name: 'selectedPaths',
|
|
57
|
-
message: includeAll ? 'Select worktrees to delete:' : 'Select managed worktrees to delete:',
|
|
58
|
-
choices: choices,
|
|
59
|
-
pageSize: 10,
|
|
60
|
-
},
|
|
61
|
-
]);
|
|
109
|
+
selectedPaths = answer.selectedPaths;
|
|
110
|
+
}
|
|
62
111
|
if (!selectedPaths || selectedPaths.length === 0) {
|
|
63
112
|
log.info('No worktrees selected.');
|
|
64
113
|
return;
|
|
65
114
|
}
|
|
66
115
|
const count = selectedPaths.length;
|
|
67
116
|
const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p, managedRoot));
|
|
68
|
-
|
|
69
|
-
{
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
135
|
}
|
|
80
136
|
for (const wtPath of selectedPaths) {
|
|
81
137
|
const worktreeName = formatWorktreeDisplayPath(wtPath, managedRoot);
|
|
@@ -38,10 +38,10 @@ function formatChoiceLabel(type, branchName, displayPath, terminalColumns) {
|
|
|
38
38
|
}
|
|
39
39
|
export async function enterCommand(wtName, options = {}) {
|
|
40
40
|
try {
|
|
41
|
-
await ensureRepoContext();
|
|
41
|
+
const repoRoot = await ensureRepoContext();
|
|
42
42
|
const worktrees = await listWorktrees();
|
|
43
43
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
44
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
44
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
45
45
|
if (worktrees.length === 0) {
|
|
46
46
|
log.info('No worktrees found.');
|
|
47
47
|
return;
|
package/dist/commands/wt/exec.js
CHANGED
|
@@ -9,9 +9,9 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
9
9
|
let command = '';
|
|
10
10
|
let args = [];
|
|
11
11
|
try {
|
|
12
|
-
await getRepoRoot();
|
|
12
|
+
const repoRoot = await getRepoRoot();
|
|
13
13
|
const worktrees = await listWorktrees();
|
|
14
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
14
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
15
15
|
if (worktrees.length === 0) {
|
|
16
16
|
log.info('No worktrees found.');
|
|
17
17
|
return;
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -5,10 +5,10 @@ import { log, timeAgo } from '../../lib/ui.js';
|
|
|
5
5
|
import { detectWorktreeType, formatWorktreeType, formatPrStatus, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
6
6
|
export async function listCommand() {
|
|
7
7
|
try {
|
|
8
|
-
await getRepoRoot(); // Verify we are in a git repo
|
|
8
|
+
const repoRoot = await getRepoRoot(); // Verify we are in a git repo
|
|
9
9
|
const worktrees = await listWorktrees();
|
|
10
10
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
11
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
11
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
12
12
|
if (worktrees.length === 0) {
|
|
13
13
|
log.info('No worktrees found.');
|
|
14
14
|
return;
|
package/dist/commands/wt/open.js
CHANGED
|
@@ -381,10 +381,10 @@ export async function runOpenActions(actions, wtPath, options = {}) {
|
|
|
381
381
|
}
|
|
382
382
|
export async function openCommand(wtName, options = {}) {
|
|
383
383
|
try {
|
|
384
|
-
await getRepoRoot();
|
|
384
|
+
const repoRoot = await getRepoRoot();
|
|
385
385
|
const worktrees = await listWorktrees();
|
|
386
386
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
387
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
387
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
388
388
|
if (worktrees.length === 0) {
|
|
389
389
|
log.info('No worktrees found.');
|
|
390
390
|
return;
|
package/dist/commands/wt/path.js
CHANGED
|
@@ -5,9 +5,9 @@ import { log } from '../../lib/ui.js';
|
|
|
5
5
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
6
6
|
export async function pathCommand(wtName) {
|
|
7
7
|
try {
|
|
8
|
-
await getRepoRoot();
|
|
8
|
+
const repoRoot = await getRepoRoot();
|
|
9
9
|
const worktrees = await listWorktrees();
|
|
10
|
-
const managedRoot = await getManagedWorktreesRoot();
|
|
10
|
+
const managedRoot = await getManagedWorktreesRoot(repoRoot);
|
|
11
11
|
if (worktrees.length === 0) {
|
|
12
12
|
log.info('No worktrees found.');
|
|
13
13
|
return;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { execa } from 'execa';
|
|
4
6
|
import { formatMainMenuChoice, renderMainMenuIntro, welcome, log, } from './lib/ui.js';
|
|
5
|
-
import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
|
|
7
|
+
import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, deleteHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
|
|
6
8
|
import { listCommand } from './commands/wt/list.js';
|
|
7
9
|
import { createCommand } from './commands/wt/create.js';
|
|
8
10
|
import { createCommandNew } from './commands/wt/create-branch.js';
|
|
@@ -17,14 +19,18 @@ import { openCommand } from './commands/wt/open.js';
|
|
|
17
19
|
import { applyCommand } from './commands/wt/apply.js';
|
|
18
20
|
import { unapplyCommand } from './commands/wt/unapply.js';
|
|
19
21
|
import { handoffCommand } from './commands/wt/handoff.js';
|
|
22
|
+
import { copyEnvCommand } from './commands/wt/copy-env.js';
|
|
20
23
|
import { configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
|
|
21
24
|
import { getVersion } from './lib/version.js';
|
|
22
25
|
import { checkForUpdate } from './lib/update-check.js';
|
|
23
26
|
import { findSandboxRoot } from './lib/sandbox.js';
|
|
24
27
|
import { bifrostCommand } from './commands/bifrost.js';
|
|
25
28
|
import { thorCommand } from './commands/thor.js';
|
|
29
|
+
import { buildMainMenuEntries, } from './lib/main-menu.js';
|
|
30
|
+
import { getRepoRoot, listWorktrees } from './lib/git.js';
|
|
26
31
|
const program = new Command();
|
|
27
32
|
const argv = process.argv.map((arg) => arg === '-v' || arg === '—version' ? '--version' : arg);
|
|
33
|
+
const DOCS_URL = 'https://yggtree.logbookfordevs.com/docs';
|
|
28
34
|
function mainMenuChoice(value, glyph, label, detail, tone) {
|
|
29
35
|
return {
|
|
30
36
|
name: formatMainMenuChoice({ glyph, label, detail, tone }),
|
|
@@ -34,6 +40,62 @@ function mainMenuChoice(value, glyph, label, detail, tone) {
|
|
|
34
40
|
function mainMenuSeparator(label) {
|
|
35
41
|
return new inquirer.Separator(chalk.dim(` · ${label}`));
|
|
36
42
|
}
|
|
43
|
+
const mainMenuChoices = {
|
|
44
|
+
'create-smart': mainMenuChoice('create-smart', '◆', 'Grow new realm', 'create a branch-backed worktree', 'growth'),
|
|
45
|
+
'worktree-checkout': mainMenuChoice('worktree-checkout', '⇄', 'Check out branch', 'open an existing ref in a new realm', 'travel'),
|
|
46
|
+
handoff: mainMenuChoice('handoff', '✦', 'Hand off current work', 'carry dirty work into a named sandbox', 'sandbox'),
|
|
47
|
+
open: mainMenuChoice('open', '◇', 'Open realm', 'jump into an existing worktree', 'travel'),
|
|
48
|
+
list: mainMenuChoice('list', '○', 'Survey realms', 'scan active worktrees and PR state', 'tending'),
|
|
49
|
+
'create-multi': mainMenuChoice('create-multi', '✧', 'Grow many realms', 'create multiple branch worktrees', 'growth'),
|
|
50
|
+
'create-sandbox': mainMenuChoice('create-sandbox', '△', 'Forge sandbox', 'create a local experiment realm', 'sandbox'),
|
|
51
|
+
bootstrap: mainMenuChoice('bootstrap', '↳', 'Bootstrap realm', 'install dependencies and submodules', 'tending'),
|
|
52
|
+
exec: mainMenuChoice('exec', '⌁', 'Cast command', 'run a command inside a worktree', 'tending'),
|
|
53
|
+
path: mainMenuChoice('path', '⌖', 'Reveal path', 'print a worktree cd command', 'tending'),
|
|
54
|
+
'copy-env': mainMenuChoice('copy-env', '≋', 'Bring env files', 'copy local env files from main realm', 'tending'),
|
|
55
|
+
delete: mainMenuChoice('delete', '×', 'Fell realm', 'delete managed worktrees', 'danger'),
|
|
56
|
+
prune: mainMenuChoice('prune', '⌫', 'Prune stale realms', 'remove stale worktree metadata', 'danger'),
|
|
57
|
+
apply: mainMenuChoice('apply', '◆', 'Graft sandbox changes', 'apply sandbox changes to origin', 'sandbox'),
|
|
58
|
+
unapply: mainMenuChoice('unapply', '↶', 'Undo sandbox graft', 'restore origin from sandbox backups', 'sandbox'),
|
|
59
|
+
bifrost: mainMenuChoice('bifrost', '✺', 'Summon the Bifrost', 'open the long way around', 'lore'),
|
|
60
|
+
thor: mainMenuChoice('thor', 'ϟ', 'Consult Thor', 'ask the thunder route', 'lore'),
|
|
61
|
+
docs: mainMenuChoice('docs', '?', 'Open docs', 'read the Yggtree documentation', 'tending'),
|
|
62
|
+
exit: mainMenuChoice('exit', '·', 'Leave Yggdrasil', 'exit without changing anything', 'exit'),
|
|
63
|
+
};
|
|
64
|
+
async function detectMainMenuContext() {
|
|
65
|
+
if (await findSandboxRoot(process.cwd())) {
|
|
66
|
+
return 'sandbox';
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
const repoRoot = await getRepoRoot();
|
|
70
|
+
const worktrees = await listWorktrees();
|
|
71
|
+
const mainWorktreePath = worktrees[0]?.path;
|
|
72
|
+
if (mainWorktreePath && path.resolve(repoRoot) !== path.resolve(mainWorktreePath)) {
|
|
73
|
+
return 'worktree';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return 'main';
|
|
78
|
+
}
|
|
79
|
+
return 'main';
|
|
80
|
+
}
|
|
81
|
+
async function openDocs() {
|
|
82
|
+
const command = process.platform === 'darwin'
|
|
83
|
+
? 'open'
|
|
84
|
+
: process.platform === 'win32'
|
|
85
|
+
? 'cmd'
|
|
86
|
+
: 'xdg-open';
|
|
87
|
+
const args = process.platform === 'win32'
|
|
88
|
+
? ['/c', 'start', '', DOCS_URL]
|
|
89
|
+
: [DOCS_URL];
|
|
90
|
+
try {
|
|
91
|
+
await execa(command, args, { stdio: 'ignore' });
|
|
92
|
+
log.success(`Opened docs: ${DOCS_URL}`);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
log.warning('Could not open the docs in your browser.');
|
|
96
|
+
log.info(DOCS_URL);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
37
99
|
function registerWorktreeCommands(parent) {
|
|
38
100
|
parent.command('list')
|
|
39
101
|
.description('List all repo-linked worktrees')
|
|
@@ -56,6 +118,7 @@ function registerWorktreeCommands(parent) {
|
|
|
56
118
|
.option('--enter', 'Enter the worktree sub-shell after creation')
|
|
57
119
|
.option('--no-enter', 'Do not enter the worktree sub-shell after creation')
|
|
58
120
|
.option('--exec <command>', 'Command to execute after creation')
|
|
121
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
59
122
|
.addHelpText('after', createHelp)
|
|
60
123
|
.action(async (branch, options) => {
|
|
61
124
|
await createCommandNew({
|
|
@@ -68,6 +131,7 @@ function registerWorktreeCommands(parent) {
|
|
|
68
131
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
69
132
|
.option('--source <type>', 'Base source (local or remote)')
|
|
70
133
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
134
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
71
135
|
.addHelpText('after', createMultiHelp)
|
|
72
136
|
.action(async (options) => {
|
|
73
137
|
await createCommandMulti(options);
|
|
@@ -84,6 +148,7 @@ function registerWorktreeCommands(parent) {
|
|
|
84
148
|
.addOption(new Option('--enter', 'Enter the worktree sub-shell after checkout/opening').hideHelp())
|
|
85
149
|
.option('--no-enter', 'Do not enter the worktree sub-shell after checkout/opening')
|
|
86
150
|
.option('--exec <command>', 'Command to execute after creation')
|
|
151
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
87
152
|
.addHelpText('after', checkoutHelp)
|
|
88
153
|
.action(async (name, ref, options) => {
|
|
89
154
|
await createCommand({
|
|
@@ -95,11 +160,13 @@ function registerWorktreeCommands(parent) {
|
|
|
95
160
|
};
|
|
96
161
|
registerWorktreeCheckout('worktree-checkout', 'Create a checkout-style worktree from an existing branch');
|
|
97
162
|
registerWorktreeCheckout('wc', 'Alias for worktree-checkout');
|
|
98
|
-
parent.command('delete')
|
|
163
|
+
parent.command('delete [worktrees...]')
|
|
99
164
|
.description('Delete managed worktrees')
|
|
100
|
-
.option('-a, --all', 'Include
|
|
101
|
-
.
|
|
102
|
-
|
|
165
|
+
.option('-a, --all', 'Include external LINKED worktrees outside the managed root (except main/current)')
|
|
166
|
+
.option('-y, --yes', 'Confirm deletion without prompts')
|
|
167
|
+
.addHelpText('after', deleteHelp)
|
|
168
|
+
.action(async (worktrees, options) => {
|
|
169
|
+
await deleteCommand(worktrees, options);
|
|
103
170
|
});
|
|
104
171
|
parent.command('open [worktree]')
|
|
105
172
|
.description('Open a worktree in an editor, supported app, or terminal target')
|
|
@@ -128,6 +195,9 @@ function registerWorktreeCommands(parent) {
|
|
|
128
195
|
.action(async (worktree) => {
|
|
129
196
|
await pathCommand(worktree);
|
|
130
197
|
});
|
|
198
|
+
parent.command('copy-env')
|
|
199
|
+
.description('Copy local env files from the main worktree to the current worktree')
|
|
200
|
+
.action(copyEnvCommand);
|
|
131
201
|
parent.command('create-sandbox')
|
|
132
202
|
.description('Create a local-only disposable experiment sandbox')
|
|
133
203
|
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
@@ -139,6 +209,7 @@ function registerWorktreeCommands(parent) {
|
|
|
139
209
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
140
210
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
141
211
|
.option('--exec <command>', 'Command to execute after creation')
|
|
212
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
142
213
|
.addHelpText('after', createSandboxHelp)
|
|
143
214
|
.action(async (options) => {
|
|
144
215
|
await createSandboxCommand(options);
|
|
@@ -152,6 +223,7 @@ function registerWorktreeCommands(parent) {
|
|
|
152
223
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
153
224
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
154
225
|
.option('--exec <command>', 'Command to execute after creation')
|
|
226
|
+
.option('--config <preset>', 'Use a path preset for this run only (yggtree, codex, claude)')
|
|
155
227
|
.addHelpText('after', handoffHelp)
|
|
156
228
|
.action(async (options) => {
|
|
157
229
|
await handoffCommand(options);
|
|
@@ -185,58 +257,14 @@ program
|
|
|
185
257
|
.action(async () => {
|
|
186
258
|
const update = await checkForUpdate();
|
|
187
259
|
await welcome({ update });
|
|
188
|
-
const
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
const growthChoices = [
|
|
197
|
-
mainMenuChoice('create-multi', '✧', 'Grow many realms', 'create multiple branch worktrees', 'growth'),
|
|
198
|
-
mainMenuChoice('create-sandbox', '△', 'Forge sandbox', 'create a local experiment realm', 'sandbox'),
|
|
199
|
-
];
|
|
200
|
-
const tendingChoices = [
|
|
201
|
-
mainMenuChoice('bootstrap', '↳', 'Bootstrap realm', 'install dependencies and submodules', 'tending'),
|
|
202
|
-
mainMenuChoice('exec', '⌁', 'Cast command', 'run a command inside a worktree', 'tending'),
|
|
203
|
-
mainMenuChoice('path', '⌖', 'Reveal path', 'print a worktree cd command', 'tending'),
|
|
204
|
-
mainMenuChoice('delete', '×', 'Fell realm', 'delete managed worktrees', 'danger'),
|
|
205
|
-
mainMenuChoice('prune', '⌫', 'Prune stale realms', 'remove stale worktree metadata', 'danger'),
|
|
206
|
-
];
|
|
207
|
-
const sandboxChoices = [
|
|
208
|
-
mainMenuChoice('apply', '◆', 'Graft sandbox changes', 'apply sandbox changes to origin', 'sandbox'),
|
|
209
|
-
mainMenuChoice('unapply', '↶', 'Undo sandbox graft', 'restore origin from sandbox backups', 'sandbox'),
|
|
210
|
-
];
|
|
211
|
-
const loreChoices = [
|
|
212
|
-
mainMenuChoice('bifrost', '✺', 'Summon the Bifrost', 'open the long way around', 'lore'),
|
|
213
|
-
mainMenuChoice('thor', 'ϟ', 'Consult Thor', 'ask the thunder route', 'lore'),
|
|
214
|
-
mainMenuChoice('exit', '·', 'Leave Yggdrasil', 'exit without changing anything', 'exit'),
|
|
215
|
-
];
|
|
216
|
-
const choices = isInSandbox
|
|
217
|
-
? [
|
|
218
|
-
...sandboxChoices,
|
|
219
|
-
mainMenuSeparator('Daily routes'),
|
|
220
|
-
...dailyChoices,
|
|
221
|
-
mainMenuSeparator('Growth and experiments'),
|
|
222
|
-
...growthChoices,
|
|
223
|
-
mainMenuSeparator('Tend realms'),
|
|
224
|
-
...tendingChoices,
|
|
225
|
-
mainMenuSeparator('Lore'),
|
|
226
|
-
...loreChoices,
|
|
227
|
-
]
|
|
228
|
-
: [
|
|
229
|
-
...dailyChoices,
|
|
230
|
-
mainMenuSeparator('Growth and experiments'),
|
|
231
|
-
...growthChoices,
|
|
232
|
-
mainMenuSeparator('Tend realms'),
|
|
233
|
-
...tendingChoices,
|
|
234
|
-
mainMenuSeparator('Sandbox grafts'),
|
|
235
|
-
...sandboxChoices,
|
|
236
|
-
mainMenuSeparator('Lore'),
|
|
237
|
-
...loreChoices,
|
|
238
|
-
];
|
|
239
|
-
console.log(renderMainMenuIntro({ isInSandbox }));
|
|
260
|
+
const menuContext = await detectMainMenuContext();
|
|
261
|
+
const choices = buildMainMenuEntries(menuContext).map((entry) => {
|
|
262
|
+
if (entry.type === 'separator') {
|
|
263
|
+
return mainMenuSeparator(entry.label);
|
|
264
|
+
}
|
|
265
|
+
return mainMenuChoices[entry.value];
|
|
266
|
+
});
|
|
267
|
+
console.log(renderMainMenuIntro({ context: menuContext }));
|
|
240
268
|
const { action } = await inquirer.prompt([
|
|
241
269
|
{
|
|
242
270
|
type: 'list',
|
|
@@ -278,6 +306,9 @@ program
|
|
|
278
306
|
case 'path':
|
|
279
307
|
await pathCommand();
|
|
280
308
|
break;
|
|
309
|
+
case 'copy-env':
|
|
310
|
+
await copyEnvCommand();
|
|
311
|
+
break;
|
|
281
312
|
case 'apply':
|
|
282
313
|
await applyCommand();
|
|
283
314
|
break;
|
|
@@ -296,6 +327,9 @@ program
|
|
|
296
327
|
case 'thor':
|
|
297
328
|
await thorCommand();
|
|
298
329
|
break;
|
|
330
|
+
case 'docs':
|
|
331
|
+
await openDocs();
|
|
332
|
+
break;
|
|
299
333
|
case 'exit':
|
|
300
334
|
log.info('Bye! 👋');
|
|
301
335
|
process.exit(0);
|
|
@@ -318,13 +352,13 @@ config.command('get')
|
|
|
318
352
|
.description('Show resolved global settings')
|
|
319
353
|
.action(configGetCommand);
|
|
320
354
|
config.command('use <preset>')
|
|
321
|
-
.description('Use a global worktree path preset (default, yggtree, codex)')
|
|
355
|
+
.description('Use a global worktree path preset (default, yggtree, codex, claude)')
|
|
322
356
|
.action(configUseCommand);
|
|
323
357
|
config.command('set-worktrees-root <path>')
|
|
324
358
|
.description('Set the global managed worktrees root')
|
|
325
359
|
.action(configSetWorktreesRootCommand);
|
|
326
360
|
config.command('set-worktree-layout <layout>')
|
|
327
|
-
.description('Set the managed worktree path layout (yggtree or
|
|
361
|
+
.description('Set the managed worktree path layout (yggtree, codex, or claude)')
|
|
328
362
|
.action(configSetWorktreeLayoutCommand);
|
|
329
363
|
config.command('reset')
|
|
330
364
|
.description('Reset global settings to defaults')
|
|
@@ -19,8 +19,13 @@ export function formatHome(value) {
|
|
|
19
19
|
return `~/${path.relative(home, value)}`;
|
|
20
20
|
return value;
|
|
21
21
|
}
|
|
22
|
-
function normalizeRoot(value) {
|
|
23
|
-
|
|
22
|
+
function normalizeRoot(value, layout, repoRoot) {
|
|
23
|
+
if (value)
|
|
24
|
+
return path.resolve(expandHome(value));
|
|
25
|
+
if (layout === 'claude') {
|
|
26
|
+
return path.join(path.resolve(repoRoot || process.cwd()), '.claude', 'worktrees');
|
|
27
|
+
}
|
|
28
|
+
return path.resolve(YGG_ROOT);
|
|
24
29
|
}
|
|
25
30
|
export function normalizeWorktreesRootInput(value) {
|
|
26
31
|
return path.resolve(expandHome(value));
|
|
@@ -28,13 +33,16 @@ export function normalizeWorktreesRootInput(value) {
|
|
|
28
33
|
function normalizeLayout(value) {
|
|
29
34
|
return value || DEFAULT_WORKTREE_LAYOUT;
|
|
30
35
|
}
|
|
36
|
+
function isWorktreeLayout(value) {
|
|
37
|
+
return value === 'yggtree' || value === 'codex' || value === 'claude';
|
|
38
|
+
}
|
|
31
39
|
export async function readGlobalConfig() {
|
|
32
40
|
try {
|
|
33
41
|
if (await fs.pathExists(CONFIG_PATH)) {
|
|
34
42
|
const config = await fs.readJSON(CONFIG_PATH);
|
|
35
43
|
return {
|
|
36
44
|
worktreesRoot: typeof config.worktreesRoot === 'string' ? config.worktreesRoot : undefined,
|
|
37
|
-
worktreeLayout: config.worktreeLayout
|
|
45
|
+
worktreeLayout: isWorktreeLayout(config.worktreeLayout) ? config.worktreeLayout : undefined,
|
|
38
46
|
};
|
|
39
47
|
}
|
|
40
48
|
}
|
|
@@ -47,21 +55,30 @@ export async function writeGlobalConfig(config) {
|
|
|
47
55
|
await fs.ensureDir(YGG_ROOT);
|
|
48
56
|
await fs.writeJSON(CONFIG_PATH, config, { spaces: 2 });
|
|
49
57
|
}
|
|
50
|
-
export async function getWorktreePathConfig() {
|
|
51
|
-
const config =
|
|
58
|
+
export async function getWorktreePathConfig(repoRoot, presetOverride) {
|
|
59
|
+
const config = presetOverride === undefined
|
|
60
|
+
? await readGlobalConfig()
|
|
61
|
+
: getPresetConfig(presetOverride);
|
|
62
|
+
if (!config) {
|
|
63
|
+
throw new Error(`Unknown config preset "${presetOverride}". Available presets: default, yggtree, codex, claude.`);
|
|
64
|
+
}
|
|
65
|
+
const layout = normalizeLayout(config.worktreeLayout);
|
|
52
66
|
return {
|
|
53
|
-
root: normalizeRoot(config.worktreesRoot),
|
|
54
|
-
layout
|
|
67
|
+
root: normalizeRoot(config.worktreesRoot, layout, repoRoot),
|
|
68
|
+
layout,
|
|
55
69
|
};
|
|
56
70
|
}
|
|
57
|
-
export async function getManagedWorktreesRoot() {
|
|
58
|
-
const config = await getWorktreePathConfig();
|
|
71
|
+
export async function getManagedWorktreesRoot(repoRoot) {
|
|
72
|
+
const config = await getWorktreePathConfig(repoRoot);
|
|
59
73
|
return config.root;
|
|
60
74
|
}
|
|
61
75
|
export function buildManagedWorktreePath(repoName, worktreeSlug, config) {
|
|
62
76
|
if (config.layout === 'codex') {
|
|
63
77
|
return path.join(config.root, worktreeSlug, repoName);
|
|
64
78
|
}
|
|
79
|
+
if (config.layout === 'claude') {
|
|
80
|
+
return path.join(config.root, worktreeSlug);
|
|
81
|
+
}
|
|
65
82
|
return path.join(config.root, repoName, worktreeSlug);
|
|
66
83
|
}
|
|
67
84
|
export function getPresetConfig(preset) {
|
|
@@ -75,15 +92,27 @@ export function getPresetConfig(preset) {
|
|
|
75
92
|
worktreeLayout: 'codex',
|
|
76
93
|
};
|
|
77
94
|
}
|
|
95
|
+
if (normalized === 'claude') {
|
|
96
|
+
return {
|
|
97
|
+
worktreeLayout: 'claude',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
78
100
|
return undefined;
|
|
79
101
|
}
|
|
102
|
+
function inferPreset(config) {
|
|
103
|
+
if (!config.worktreesRoot && !config.worktreeLayout)
|
|
104
|
+
return 'default';
|
|
105
|
+
if (!config.worktreesRoot && config.worktreeLayout === 'claude')
|
|
106
|
+
return 'claude';
|
|
107
|
+
if (config.worktreesRoot === '~/.codex/worktrees' && config.worktreeLayout === 'codex')
|
|
108
|
+
return 'codex';
|
|
109
|
+
return 'custom';
|
|
110
|
+
}
|
|
80
111
|
export function formatGlobalConfig(config, resolved) {
|
|
81
112
|
return [
|
|
82
113
|
`worktreesRoot: ${formatHome(resolved.root)}`,
|
|
83
114
|
`worktreeLayout: ${resolved.layout}`,
|
|
84
115
|
`configFile: ${formatHome(CONFIG_PATH)}`,
|
|
85
|
-
|
|
86
|
-
? 'preset: custom'
|
|
87
|
-
: 'preset: default',
|
|
116
|
+
`preset: ${inferPreset(config)}`,
|
|
88
117
|
];
|
|
89
118
|
}
|
package/dist/lib/help.js
CHANGED
|
@@ -6,25 +6,38 @@ Choose by intent:
|
|
|
6
6
|
Disposable local experiment yggtree create-sandbox
|
|
7
7
|
Copy sandbox files to origin yggtree apply # from inside sandbox, not a Git merge
|
|
8
8
|
Bulk official worktrees yggtree create-multi
|
|
9
|
+
Agent-native path layout yggtree config use claude # or codex
|
|
10
|
+
Delete without prompts yggtree delete branch-name --yes
|
|
9
11
|
|
|
10
12
|
More detail: run yggtree help <command>, for example yggtree help handoff.
|
|
11
13
|
`;
|
|
14
|
+
const agentPathTip = `
|
|
15
|
+
Agent-native paths:
|
|
16
|
+
Add --config claude for Claude Code's repo-local .claude/worktrees layout.
|
|
17
|
+
Add --config codex for Codex's ~/.codex/worktrees layout.
|
|
18
|
+
`;
|
|
12
19
|
export const createHelp = `
|
|
13
20
|
Behavior:
|
|
14
21
|
Creates an official branch-backed worktree, publishes the branch to origin, and enters the shell by default.
|
|
15
22
|
Use for real task branches. For disposable experiments, use create-sandbox.
|
|
16
23
|
|
|
24
|
+
${agentPathTip}
|
|
25
|
+
|
|
17
26
|
Examples:
|
|
18
27
|
yggtree create feat/new-flow --base main --source remote
|
|
19
28
|
yggtree create feat/background-task --base main --source remote --no-open --no-enter
|
|
29
|
+
yggtree create feat/claude-layout --base main --source remote --config claude
|
|
20
30
|
`;
|
|
21
31
|
export const createMultiHelp = `
|
|
22
32
|
Behavior:
|
|
23
33
|
Bulk-creates official branch-backed worktrees.
|
|
24
34
|
This does not share create's open/enter/exec lifecycle. Use create for one task branch.
|
|
25
35
|
|
|
36
|
+
${agentPathTip}
|
|
37
|
+
|
|
26
38
|
Example:
|
|
27
39
|
yggtree create-multi --base main --source remote
|
|
40
|
+
yggtree create-multi --base main --source remote --config codex
|
|
28
41
|
`;
|
|
29
42
|
export const checkoutHelp = `
|
|
30
43
|
Behavior:
|
|
@@ -32,9 +45,12 @@ Behavior:
|
|
|
32
45
|
Use for interruptions and branch review. Use create for a new official task branch.
|
|
33
46
|
Enters the worktree shell by default; add --no-enter when automation should return.
|
|
34
47
|
|
|
48
|
+
${agentPathTip}
|
|
49
|
+
|
|
35
50
|
Examples:
|
|
36
51
|
yggtree wc --ref hotfix/payment-timeout
|
|
37
52
|
yggtree wc --ref main --name fresh-main --no-open --no-enter
|
|
53
|
+
yggtree wc --ref main --name fresh-main --config yggtree
|
|
38
54
|
`;
|
|
39
55
|
export const openHelp = `
|
|
40
56
|
Behavior:
|
|
@@ -46,14 +62,29 @@ Examples:
|
|
|
46
62
|
yggtree open my-feature --tool codex-app
|
|
47
63
|
yggtree list --open
|
|
48
64
|
`;
|
|
65
|
+
export const deleteHelp = `
|
|
66
|
+
Behavior:
|
|
67
|
+
Deletes selected worktrees. Main and current worktrees are always protected.
|
|
68
|
+
Without arguments, delete stays interactive. For automation, pass explicit targets and --yes.
|
|
69
|
+
Add --all only when the target is outside Yggtree's managed root and appears as LINKED in list.
|
|
70
|
+
|
|
71
|
+
Examples:
|
|
72
|
+
yggtree delete my-feature --yes
|
|
73
|
+
yggtree delete my-feature other-feature --yes
|
|
74
|
+
yggtree delete --all --yes
|
|
75
|
+
yggtree delete external-feature --all --yes
|
|
76
|
+
`;
|
|
49
77
|
export const createSandboxHelp = `
|
|
50
78
|
Behavior:
|
|
51
79
|
Creates a local-only disposable experiment from the current branch.
|
|
52
80
|
For continuing current dirty work, prefer handoff instead of teaching create-sandbox --carry.
|
|
53
81
|
|
|
82
|
+
${agentPathTip}
|
|
83
|
+
|
|
54
84
|
Examples:
|
|
55
85
|
yggtree create-sandbox
|
|
56
86
|
yggtree create-sandbox --name ui-option-a --no-open
|
|
87
|
+
yggtree create-sandbox --name claude-scratch --config claude
|
|
57
88
|
|
|
58
89
|
Related:
|
|
59
90
|
handoff carries current dirty work into a named sandbox.
|
|
@@ -64,8 +95,11 @@ Behavior:
|
|
|
64
95
|
Carry staged, unstaged, and untracked work into a named sandbox so you can continue there.
|
|
65
96
|
The origin checkout is not cleaned; review or reset it separately if needed.
|
|
66
97
|
|
|
98
|
+
${agentPathTip}
|
|
99
|
+
|
|
67
100
|
Example:
|
|
68
101
|
yggtree handoff --name continue-auth-refactor
|
|
102
|
+
yggtree handoff --name continue-auth-refactor --config codex
|
|
69
103
|
|
|
70
104
|
Related:
|
|
71
105
|
create-sandbox starts a disposable local experiment.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export function buildMainMenuEntries(context) {
|
|
2
|
+
const dailyChoices = actions([
|
|
3
|
+
'create-smart',
|
|
4
|
+
'worktree-checkout',
|
|
5
|
+
'handoff',
|
|
6
|
+
'open',
|
|
7
|
+
'list',
|
|
8
|
+
]);
|
|
9
|
+
const growthChoices = actions([
|
|
10
|
+
'create-multi',
|
|
11
|
+
'create-sandbox',
|
|
12
|
+
]);
|
|
13
|
+
const tendingChoices = actions([
|
|
14
|
+
'bootstrap',
|
|
15
|
+
'exec',
|
|
16
|
+
'path',
|
|
17
|
+
'delete',
|
|
18
|
+
'prune',
|
|
19
|
+
]);
|
|
20
|
+
const sandboxChoices = actions([
|
|
21
|
+
'apply',
|
|
22
|
+
'unapply',
|
|
23
|
+
]);
|
|
24
|
+
const loreChoices = actions([
|
|
25
|
+
'bifrost',
|
|
26
|
+
'thor',
|
|
27
|
+
'docs',
|
|
28
|
+
'exit',
|
|
29
|
+
]);
|
|
30
|
+
if (context === 'sandbox') {
|
|
31
|
+
return [
|
|
32
|
+
...sandboxChoices,
|
|
33
|
+
separator('Daily routes'),
|
|
34
|
+
...dailyChoices,
|
|
35
|
+
separator('Growth and experiments'),
|
|
36
|
+
...growthChoices,
|
|
37
|
+
separator('Tend realms'),
|
|
38
|
+
...tendingChoices,
|
|
39
|
+
separator('Lore'),
|
|
40
|
+
...loreChoices,
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
if (context === 'worktree') {
|
|
44
|
+
return [
|
|
45
|
+
...actions(['copy-env', 'path', 'exec', 'bootstrap']),
|
|
46
|
+
separator('Daily routes'),
|
|
47
|
+
...dailyChoices,
|
|
48
|
+
separator('Growth and experiments'),
|
|
49
|
+
...growthChoices,
|
|
50
|
+
separator('Tend realms'),
|
|
51
|
+
...actions(['delete', 'prune']),
|
|
52
|
+
separator('Lore'),
|
|
53
|
+
...loreChoices,
|
|
54
|
+
];
|
|
55
|
+
}
|
|
56
|
+
return [
|
|
57
|
+
...dailyChoices,
|
|
58
|
+
separator('Growth and experiments'),
|
|
59
|
+
...growthChoices,
|
|
60
|
+
separator('Tend realms'),
|
|
61
|
+
...tendingChoices,
|
|
62
|
+
separator('Lore'),
|
|
63
|
+
...loreChoices,
|
|
64
|
+
];
|
|
65
|
+
}
|
|
66
|
+
function actions(values) {
|
|
67
|
+
return values.map(value => ({ type: 'action', value }));
|
|
68
|
+
}
|
|
69
|
+
function separator(label) {
|
|
70
|
+
return { type: 'separator', label };
|
|
71
|
+
}
|
package/dist/lib/ui.js
CHANGED
|
@@ -34,9 +34,15 @@ export const welcome = async (options = {}) => {
|
|
|
34
34
|
console.log(renderWelcome(options));
|
|
35
35
|
};
|
|
36
36
|
export function renderMainMenuIntro(options) {
|
|
37
|
-
const detail =
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
const detail = (() => {
|
|
38
|
+
if (options.context === 'sandbox') {
|
|
39
|
+
return 'Sandbox tools appear first because this realm can apply or undo grafted changes.';
|
|
40
|
+
}
|
|
41
|
+
if (options.context === 'worktree') {
|
|
42
|
+
return 'Current-realm tools appear first for this worktree.';
|
|
43
|
+
}
|
|
44
|
+
return 'Create, enter, inspect, or tend your worktrees.';
|
|
45
|
+
})();
|
|
40
46
|
return [
|
|
41
47
|
'Choose the next realm action.',
|
|
42
48
|
yggMuted('Daily routes first. Maintenance and lore wait below.'),
|