yggtree 1.4.4 โ 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 +65 -19
- package/dist/commands/config.js +53 -0
- package/dist/commands/wt/bootstrap.js +5 -3
- 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 +8 -6
- package/dist/commands/wt/enter.js +5 -3
- package/dist/commands/wt/exec.js +4 -2
- package/dist/commands/wt/list.js +3 -1
- package/dist/commands/wt/open.js +29 -11
- package/dist/commands/wt/path.js +4 -2
- package/dist/index.js +93 -43
- 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,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,48 @@ 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
|
+
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
|
+
|
|
262
305
|
## ๐ ๏ธ Command Reference
|
|
263
306
|
|
|
264
307
|
### `yggtree`
|
|
@@ -349,12 +392,12 @@ yggtree wc hotfix-auth main --open --no-enter
|
|
|
349
392
|
|
|
350
393
|
### `yggtree create-sandbox`
|
|
351
394
|
|
|
352
|
-
Create a temporary sandbox from your current
|
|
395
|
+
Create a temporary local-only sandbox from your current branch.
|
|
353
396
|
|
|
354
397
|
Options:
|
|
355
398
|
|
|
356
399
|
* `-n, --name <name>`: Optional sandbox name (auto-generated if omitted).
|
|
357
|
-
* `--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.
|
|
358
401
|
* `--no-bootstrap`
|
|
359
402
|
* `--open / --no-open`
|
|
360
403
|
* `--exec "<command>"`
|
|
@@ -378,29 +421,32 @@ Options:
|
|
|
378
421
|
* `--open / --no-open`
|
|
379
422
|
* `--exec "<command>"`
|
|
380
423
|
|
|
381
|
-
This is the continuation-focused
|
|
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.
|
|
382
425
|
|
|
383
426
|
---
|
|
384
427
|
|
|
385
428
|
### `yggtree apply`
|
|
386
429
|
|
|
387
|
-
|
|
388
|
-
* **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.
|
|
389
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.
|
|
390
434
|
|
|
391
435
|
---
|
|
392
436
|
|
|
393
437
|
### `yggtree unapply`
|
|
394
438
|
|
|
395
439
|
Undo a previous `apply` operation.
|
|
396
|
-
* Restores origin files from the sandbox's backup.
|
|
440
|
+
* Restores origin files from the sandbox's backup metadata.
|
|
397
441
|
* *Note: Only works if the sandbox worktree still exists.*
|
|
398
442
|
|
|
399
443
|
---
|
|
400
444
|
|
|
401
445
|
### `yggtree create-multi`
|
|
402
446
|
|
|
403
|
-
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`.
|
|
404
450
|
|
|
405
451
|
Options:
|
|
406
452
|
|
|
@@ -434,7 +480,7 @@ Columns:
|
|
|
434
480
|
Notes:
|
|
435
481
|
|
|
436
482
|
* Entries are grouped by `TYPE`.
|
|
437
|
-
* `SANDBOX` and `MANAGED` are worktrees inside
|
|
483
|
+
* `SANDBOX` and `MANAGED` are worktrees inside the configured managed worktree root.
|
|
438
484
|
* External worktrees are labeled `LINKED`.
|
|
439
485
|
* Use `--open` to switch this flow into "pick and open in tool" mode.
|
|
440
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.
|
|
@@ -449,7 +495,7 @@ Behavior:
|
|
|
449
495
|
|
|
450
496
|
* If `[worktree]` is omitted, you can pick from the worktree list with type-to-filter search.
|
|
451
497
|
* Detects available editor commands in your `PATH` (for example: `cursor`, `code`, `zed`, `webstorm`).
|
|
452
|
-
* Detects Codex App on macOS and launches
|
|
498
|
+
* Detects Codex App on macOS and launches the selected worktree with `codex app <path>`.
|
|
453
499
|
* Detects Cmux and Tmux when their CLI commands are available.
|
|
454
500
|
* Lets you choose one editor, app, or terminal target interactively, or pass `--tool`.
|
|
455
501
|
* Keeps Cmux, Tmux, and `Other command...` mutually exclusive by using a single action picker.
|
|
@@ -517,7 +563,7 @@ Behavior:
|
|
|
517
563
|
|
|
518
564
|
Optional:
|
|
519
565
|
|
|
520
|
-
* `--all` includes linked worktrees outside
|
|
566
|
+
* `--all` includes linked worktrees outside the configured managed root (main/current worktree is excluded for safety)
|
|
521
567
|
|
|
522
568
|
---
|
|
523
569
|
|
|
@@ -661,7 +707,7 @@ Useful when you want to manually navigate or copy the path into scripts.
|
|
|
661
707
|
**Command:**
|
|
662
708
|
|
|
663
709
|
```bash
|
|
664
|
-
yggtree
|
|
710
|
+
yggtree handoff --name risky-refactor
|
|
665
711
|
```
|
|
666
712
|
|
|
667
713
|
**Scenario:**
|
|
@@ -670,7 +716,7 @@ yggtree create-sandbox --carry
|
|
|
670
716
|
2. Run `handoff --name risky-refactor` to carry those changes into an isolated `sandbox-risky-refactor` folder.
|
|
671
717
|
3. Experiment freely.
|
|
672
718
|
4. If it works: `yggtree apply`.
|
|
673
|
-
5. If it fails:
|
|
719
|
+
5. If it fails after applying: run `yggtree unapply` before deleting the sandbox.
|
|
674
720
|
|
|
675
721
|
</details>
|
|
676
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,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();
|
|
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();
|
|
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';
|
|
@@ -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();
|
|
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,21 @@
|
|
|
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
|
export async function deleteCommand(options = {}) {
|
|
7
8
|
try {
|
|
8
9
|
const currentWorktreePath = await getRepoRoot();
|
|
9
10
|
const worktrees = await listWorktrees();
|
|
11
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
10
12
|
let showAll = options.all;
|
|
11
13
|
if (showAll === undefined) {
|
|
12
14
|
const { includeExternal } = await inquirer.prompt([
|
|
13
15
|
{
|
|
14
16
|
type: 'confirm',
|
|
15
17
|
name: 'includeExternal',
|
|
16
|
-
message: 'Include external linked worktrees (outside
|
|
18
|
+
message: 'Include external linked worktrees (outside the managed root)?',
|
|
17
19
|
default: false,
|
|
18
20
|
},
|
|
19
21
|
]);
|
|
@@ -27,7 +29,7 @@ export async function deleteCommand(options = {}) {
|
|
|
27
29
|
const isCurrentWorktree = wt.path === currentWorktreePath;
|
|
28
30
|
return !isMainWorktree && !isCurrentWorktree;
|
|
29
31
|
}
|
|
30
|
-
return isManagedWorktreePath(wt.path);
|
|
32
|
+
return isManagedWorktreePath(wt.path, managedRoot);
|
|
31
33
|
});
|
|
32
34
|
if (deletableWts.length === 0) {
|
|
33
35
|
log.info(includeAll
|
|
@@ -38,11 +40,11 @@ export async function deleteCommand(options = {}) {
|
|
|
38
40
|
const choices = await Promise.all(deletableWts.map(async (wt) => {
|
|
39
41
|
const [activity, type] = await Promise.all([
|
|
40
42
|
getLastActivity(wt.path),
|
|
41
|
-
detectWorktreeType(wt, mainWorktreePath || ''),
|
|
43
|
+
detectWorktreeType(wt, mainWorktreePath || '', managedRoot),
|
|
42
44
|
]);
|
|
43
45
|
const branchName = getWorktreeBranchName(wt);
|
|
44
46
|
const active = activity ? chalk.magenta(timeAgo(activity)) : chalk.dim('โ');
|
|
45
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
47
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
46
48
|
return {
|
|
47
49
|
name: `${formatWorktreeType(type)} ${chalk.bold.yellow(branchName)} ${chalk.dim('ยท')} ${active} ${chalk.dim('ยท')} ${chalk.dim(displayPath)}`,
|
|
48
50
|
value: wt.path,
|
|
@@ -62,7 +64,7 @@ export async function deleteCommand(options = {}) {
|
|
|
62
64
|
return;
|
|
63
65
|
}
|
|
64
66
|
const count = selectedPaths.length;
|
|
65
|
-
const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p));
|
|
67
|
+
const names = selectedPaths.map((p) => formatWorktreeDisplayPath(p, managedRoot));
|
|
66
68
|
const { confirm } = await inquirer.prompt([
|
|
67
69
|
{
|
|
68
70
|
type: 'confirm',
|
|
@@ -76,7 +78,7 @@ export async function deleteCommand(options = {}) {
|
|
|
76
78
|
return;
|
|
77
79
|
}
|
|
78
80
|
for (const wtPath of selectedPaths) {
|
|
79
|
-
const worktreeName = formatWorktreeDisplayPath(wtPath);
|
|
81
|
+
const worktreeName = formatWorktreeDisplayPath(wtPath, managedRoot);
|
|
80
82
|
const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
|
|
81
83
|
try {
|
|
82
84
|
await removeWorktree(wtPath);
|
|
@@ -3,6 +3,7 @@ import { spawn } from 'child_process';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import { listWorktrees } from '../../lib/git.js';
|
|
6
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
6
7
|
import { log, ui } from '../../lib/ui.js';
|
|
7
8
|
import { ensureRepoContext } from '../../lib/repo-context.js';
|
|
8
9
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
@@ -40,13 +41,14 @@ export async function enterCommand(wtName, options = {}) {
|
|
|
40
41
|
await ensureRepoContext();
|
|
41
42
|
const worktrees = await listWorktrees();
|
|
42
43
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
44
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
43
45
|
if (worktrees.length === 0) {
|
|
44
46
|
log.info('No worktrees found.');
|
|
45
47
|
return;
|
|
46
48
|
}
|
|
47
49
|
let targetWt;
|
|
48
50
|
if (wtName) {
|
|
49
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
51
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
50
52
|
if (!targetWt) {
|
|
51
53
|
log.error(`Worktree "${wtName}" not found.`);
|
|
52
54
|
return;
|
|
@@ -56,9 +58,9 @@ export async function enterCommand(wtName, options = {}) {
|
|
|
56
58
|
// Interactive Selection
|
|
57
59
|
const terminalColumns = process.stdout.columns || 100;
|
|
58
60
|
const choices = await Promise.all(worktrees.map(async (wt) => {
|
|
59
|
-
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
61
|
+
const type = await detectWorktreeType(wt, mainWorktreePath, managedRoot);
|
|
60
62
|
const branchName = getWorktreeBranchName(wt);
|
|
61
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
63
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
62
64
|
return {
|
|
63
65
|
name: formatChoiceLabel(type, branchName, displayPath, terminalColumns),
|
|
64
66
|
value: wt
|
package/dist/commands/wt/exec.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { execa } from 'execa';
|
|
3
3
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
4
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
4
5
|
import { log } from '../../lib/ui.js';
|
|
5
6
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
6
7
|
export async function execCommand(wtName, commandArgs) {
|
|
@@ -10,12 +11,13 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
10
11
|
try {
|
|
11
12
|
await getRepoRoot();
|
|
12
13
|
const worktrees = await listWorktrees();
|
|
14
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
13
15
|
if (worktrees.length === 0) {
|
|
14
16
|
log.info('No worktrees found.');
|
|
15
17
|
return;
|
|
16
18
|
}
|
|
17
19
|
if (wtName) {
|
|
18
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
20
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
19
21
|
if (!targetWt) {
|
|
20
22
|
log.error(`Worktree "${wtName}" not found.`);
|
|
21
23
|
return;
|
|
@@ -25,7 +27,7 @@ export async function execCommand(wtName, commandArgs) {
|
|
|
25
27
|
// Interactive Selection
|
|
26
28
|
const choices = worktrees.map(wt => {
|
|
27
29
|
const branchName = getWorktreeBranchName(wt);
|
|
28
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
30
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
29
31
|
return {
|
|
30
32
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
31
33
|
value: wt
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { listWorktrees, getRepoRoot, isGitClean, getLastActivity, isGhAvailable, getPrStatusBatch } from '../../lib/git.js';
|
|
3
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
3
4
|
import { log, timeAgo } from '../../lib/ui.js';
|
|
4
5
|
import { detectWorktreeType, formatWorktreeType, formatPrStatus, getWorktreeBranchName, WORKTREE_TYPE_ORDER, } from '../../lib/worktree.js';
|
|
5
6
|
export async function listCommand() {
|
|
@@ -7,6 +8,7 @@ export async function listCommand() {
|
|
|
7
8
|
await getRepoRoot(); // Verify we are in a git repo
|
|
8
9
|
const worktrees = await listWorktrees();
|
|
9
10
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
11
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
10
12
|
if (worktrees.length === 0) {
|
|
11
13
|
log.info('No worktrees found.');
|
|
12
14
|
return;
|
|
@@ -24,7 +26,7 @@ export async function listCommand() {
|
|
|
24
26
|
console.log(chalk.dim(' ' + '-'.repeat(showPr ? 90 : 70)));
|
|
25
27
|
const rows = await Promise.all(worktrees.map(async (wt, index) => {
|
|
26
28
|
const [typeKey, isClean, lastActive] = await Promise.all([
|
|
27
|
-
detectWorktreeType(wt, mainWorktreePath),
|
|
29
|
+
detectWorktreeType(wt, mainWorktreePath, managedRoot),
|
|
28
30
|
isGitClean(wt.path),
|
|
29
31
|
getLastActivity(wt.path),
|
|
30
32
|
]);
|
package/dist/commands/wt/open.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
|
|
|
5
5
|
import { constants as fsConstants } from 'fs';
|
|
6
6
|
import { execa } from 'execa';
|
|
7
7
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
8
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
8
9
|
import { log, ui } from '../../lib/ui.js';
|
|
9
10
|
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
10
11
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
@@ -24,6 +25,7 @@ export const OPEN_TOOL_CANDIDATES = [
|
|
|
24
25
|
kind: 'app',
|
|
25
26
|
aliases: ['codex', 'codex-app'],
|
|
26
27
|
bundleId: 'com.openai.codex',
|
|
28
|
+
requiredCommand: 'codex',
|
|
27
29
|
},
|
|
28
30
|
{ id: 'cmux', name: 'Cmux Panel', command: 'cmux', kind: 'terminal', aliases: ['cmux', 'cmux-panel'] },
|
|
29
31
|
{ id: 'tmux', name: 'Tmux Session', command: 'tmux', kind: 'terminal', aliases: ['tmux', 'tmux-session'] },
|
|
@@ -175,16 +177,27 @@ export async function macOSAppBundleExists(bundleId) {
|
|
|
175
177
|
export async function detectInstalledOpenTools() {
|
|
176
178
|
const checks = await Promise.all(OPEN_TOOL_CANDIDATES.map(async (tool) => ({
|
|
177
179
|
tool,
|
|
178
|
-
exists: tool
|
|
179
|
-
? await macOSAppBundleExists(tool.bundleId)
|
|
180
|
-
: await commandExists(tool.command),
|
|
180
|
+
exists: await isOpenToolAvailable(tool),
|
|
181
181
|
})));
|
|
182
182
|
return checks
|
|
183
183
|
.filter(check => check.exists)
|
|
184
184
|
.map(check => check.tool);
|
|
185
185
|
}
|
|
186
|
-
function
|
|
187
|
-
|
|
186
|
+
export async function isOpenToolAvailable(tool, checks = {}) {
|
|
187
|
+
const hasCommand = checks.commandExists || commandExists;
|
|
188
|
+
const hasAppBundle = checks.macOSAppBundleExists || macOSAppBundleExists;
|
|
189
|
+
if (tool.requiredCommand) {
|
|
190
|
+
const commandAvailable = await hasCommand(tool.requiredCommand);
|
|
191
|
+
if (!commandAvailable)
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
if (tool.bundleId) {
|
|
195
|
+
return hasAppBundle(tool.bundleId);
|
|
196
|
+
}
|
|
197
|
+
return hasCommand(tool.command);
|
|
198
|
+
}
|
|
199
|
+
function resolveWorktreeByName(worktrees, wtName, managedRoot) {
|
|
200
|
+
return findWorktreeByName(worktrees, wtName, managedRoot);
|
|
188
201
|
}
|
|
189
202
|
export function resolveOpenToolOption(input, installed) {
|
|
190
203
|
const normalized = input.trim().toLowerCase();
|
|
@@ -202,9 +215,7 @@ async function resolveKnownOpenToolOption(input) {
|
|
|
202
215
|
const candidate = resolveOpenToolCandidate(input);
|
|
203
216
|
if (!candidate)
|
|
204
217
|
return undefined;
|
|
205
|
-
const exists = candidate
|
|
206
|
-
? await macOSAppBundleExists(candidate.bundleId)
|
|
207
|
-
: await commandExists(candidate.command);
|
|
218
|
+
const exists = await isOpenToolAvailable(candidate);
|
|
208
219
|
return exists ? candidate : undefined;
|
|
209
220
|
}
|
|
210
221
|
export async function resolveOpenToolAction(input, installed) {
|
|
@@ -331,6 +342,12 @@ export async function launchOpenTool(tool, wtPath) {
|
|
|
331
342
|
});
|
|
332
343
|
}
|
|
333
344
|
export function buildOpenToolLaunchCommand(tool, wtPath) {
|
|
345
|
+
if (tool.id === 'codex-app') {
|
|
346
|
+
return {
|
|
347
|
+
executable: 'codex',
|
|
348
|
+
args: ['app', wtPath],
|
|
349
|
+
};
|
|
350
|
+
}
|
|
334
351
|
if (tool.bundleId) {
|
|
335
352
|
return {
|
|
336
353
|
executable: '/usr/bin/open',
|
|
@@ -367,13 +384,14 @@ export async function openCommand(wtName, options = {}) {
|
|
|
367
384
|
await getRepoRoot();
|
|
368
385
|
const worktrees = await listWorktrees();
|
|
369
386
|
const mainWorktreePath = worktrees[0]?.path || '';
|
|
387
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
370
388
|
if (worktrees.length === 0) {
|
|
371
389
|
log.info('No worktrees found.');
|
|
372
390
|
return;
|
|
373
391
|
}
|
|
374
392
|
let targetWt;
|
|
375
393
|
if (wtName) {
|
|
376
|
-
targetWt = resolveWorktreeByName(worktrees, wtName);
|
|
394
|
+
targetWt = resolveWorktreeByName(worktrees, wtName, managedRoot);
|
|
377
395
|
if (!targetWt) {
|
|
378
396
|
log.error(`Worktree "${wtName}" not found.`);
|
|
379
397
|
return;
|
|
@@ -383,9 +401,9 @@ export async function openCommand(wtName, options = {}) {
|
|
|
383
401
|
ensureAutocompletePrompt();
|
|
384
402
|
const terminalColumns = process.stdout.columns || 100;
|
|
385
403
|
const candidates = await Promise.all(worktrees.map(async (wt) => {
|
|
386
|
-
const type = await detectWorktreeType(wt, mainWorktreePath);
|
|
404
|
+
const type = await detectWorktreeType(wt, mainWorktreePath, managedRoot);
|
|
387
405
|
const branchName = getWorktreeBranchName(wt);
|
|
388
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
406
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
389
407
|
const rawType = type.toLowerCase();
|
|
390
408
|
const rawDisplayPath = displayPath.toLowerCase();
|
|
391
409
|
const rawBranchName = branchName.toLowerCase();
|
package/dist/commands/wt/path.js
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
3
|
+
import { getManagedWorktreesRoot } from '../../lib/global-config.js';
|
|
3
4
|
import { log } from '../../lib/ui.js';
|
|
4
5
|
import { findWorktreeByName, formatWorktreeDisplayPath, getWorktreeBranchName } from '../../lib/worktree.js';
|
|
5
6
|
export async function pathCommand(wtName) {
|
|
6
7
|
try {
|
|
7
8
|
await getRepoRoot();
|
|
8
9
|
const worktrees = await listWorktrees();
|
|
10
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
9
11
|
if (worktrees.length === 0) {
|
|
10
12
|
log.info('No worktrees found.');
|
|
11
13
|
return;
|
|
12
14
|
}
|
|
13
15
|
let targetWt;
|
|
14
16
|
if (wtName) {
|
|
15
|
-
targetWt = findWorktreeByName(worktrees, wtName);
|
|
17
|
+
targetWt = findWorktreeByName(worktrees, wtName, managedRoot);
|
|
16
18
|
if (!targetWt) {
|
|
17
19
|
log.error(`Worktree "${wtName}" not found.`);
|
|
18
20
|
return;
|
|
@@ -22,7 +24,7 @@ export async function pathCommand(wtName) {
|
|
|
22
24
|
// Interactive Selection
|
|
23
25
|
const choices = worktrees.map(wt => {
|
|
24
26
|
const branchName = getWorktreeBranchName(wt);
|
|
25
|
-
const displayPath = formatWorktreeDisplayPath(wt.path);
|
|
27
|
+
const displayPath = formatWorktreeDisplayPath(wt.path, managedRoot);
|
|
26
28
|
return {
|
|
27
29
|
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
28
30
|
value: wt
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { welcome, log } from './lib/ui.js';
|
|
4
|
+
import { formatMainMenuChoice, renderMainMenuIntro, welcome, log, } from './lib/ui.js';
|
|
5
|
+
import { applyHelp, checkoutHelp, createHelp, createMultiHelp, createSandboxHelp, handoffHelp, intentRouterHelp, openHelp, unapplyHelp, } from './lib/help.js';
|
|
5
6
|
import { listCommand } from './commands/wt/list.js';
|
|
6
7
|
import { createCommand } from './commands/wt/create.js';
|
|
7
8
|
import { createCommandNew } from './commands/wt/create-branch.js';
|
|
@@ -16,13 +17,23 @@ import { openCommand } from './commands/wt/open.js';
|
|
|
16
17
|
import { applyCommand } from './commands/wt/apply.js';
|
|
17
18
|
import { unapplyCommand } from './commands/wt/unapply.js';
|
|
18
19
|
import { handoffCommand } from './commands/wt/handoff.js';
|
|
20
|
+
import { configGetCommand, configResetCommand, configSetWorktreeLayoutCommand, configSetWorktreesRootCommand, configUseCommand, } from './commands/config.js';
|
|
19
21
|
import { getVersion } from './lib/version.js';
|
|
20
|
-
import {
|
|
22
|
+
import { checkForUpdate } from './lib/update-check.js';
|
|
21
23
|
import { findSandboxRoot } from './lib/sandbox.js';
|
|
22
24
|
import { bifrostCommand } from './commands/bifrost.js';
|
|
23
25
|
import { thorCommand } from './commands/thor.js';
|
|
24
26
|
const program = new Command();
|
|
25
27
|
const argv = process.argv.map((arg) => arg === '-v' || arg === 'โversion' ? '--version' : arg);
|
|
28
|
+
function mainMenuChoice(value, glyph, label, detail, tone) {
|
|
29
|
+
return {
|
|
30
|
+
name: formatMainMenuChoice({ glyph, label, detail, tone }),
|
|
31
|
+
value,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function mainMenuSeparator(label) {
|
|
35
|
+
return new inquirer.Separator(chalk.dim(` ยท ${label}`));
|
|
36
|
+
}
|
|
26
37
|
function registerWorktreeCommands(parent) {
|
|
27
38
|
parent.command('list')
|
|
28
39
|
.description('List all repo-linked worktrees')
|
|
@@ -35,7 +46,7 @@ function registerWorktreeCommands(parent) {
|
|
|
35
46
|
await listCommand();
|
|
36
47
|
});
|
|
37
48
|
parent.command('create [branch]')
|
|
38
|
-
.description('Create
|
|
49
|
+
.description('Create an official branch-backed task worktree')
|
|
39
50
|
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
40
51
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
41
52
|
.option('--source <type>', 'Base source (local or remote)')
|
|
@@ -45,6 +56,7 @@ function registerWorktreeCommands(parent) {
|
|
|
45
56
|
.option('--enter', 'Enter the worktree sub-shell after creation')
|
|
46
57
|
.option('--no-enter', 'Do not enter the worktree sub-shell after creation')
|
|
47
58
|
.option('--exec <command>', 'Command to execute after creation')
|
|
59
|
+
.addHelpText('after', createHelp)
|
|
48
60
|
.action(async (branch, options) => {
|
|
49
61
|
await createCommandNew({
|
|
50
62
|
...options,
|
|
@@ -52,10 +64,11 @@ function registerWorktreeCommands(parent) {
|
|
|
52
64
|
});
|
|
53
65
|
});
|
|
54
66
|
parent.command('create-multi')
|
|
55
|
-
.description('
|
|
67
|
+
.description('Bulk-create official branch-backed worktrees')
|
|
56
68
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
57
69
|
.option('--source <type>', 'Base source (local or remote)')
|
|
58
70
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
71
|
+
.addHelpText('after', createMultiHelp)
|
|
59
72
|
.action(async (options) => {
|
|
60
73
|
await createCommandMulti(options);
|
|
61
74
|
});
|
|
@@ -71,6 +84,7 @@ function registerWorktreeCommands(parent) {
|
|
|
71
84
|
.addOption(new Option('--enter', 'Enter the worktree sub-shell after checkout/opening').hideHelp())
|
|
72
85
|
.option('--no-enter', 'Do not enter the worktree sub-shell after checkout/opening')
|
|
73
86
|
.option('--exec <command>', 'Command to execute after creation')
|
|
87
|
+
.addHelpText('after', checkoutHelp)
|
|
74
88
|
.action(async (name, ref, options) => {
|
|
75
89
|
await createCommand({
|
|
76
90
|
...options,
|
|
@@ -83,7 +97,7 @@ function registerWorktreeCommands(parent) {
|
|
|
83
97
|
registerWorktreeCheckout('wc', 'Alias for worktree-checkout');
|
|
84
98
|
parent.command('delete')
|
|
85
99
|
.description('Delete managed worktrees')
|
|
86
|
-
.option('-a, --all', 'Include repo-linked worktrees outside
|
|
100
|
+
.option('-a, --all', 'Include repo-linked worktrees outside the managed root (except main/current)')
|
|
87
101
|
.action(async (options) => {
|
|
88
102
|
await deleteCommand(options);
|
|
89
103
|
});
|
|
@@ -92,6 +106,7 @@ function registerWorktreeCommands(parent) {
|
|
|
92
106
|
.option('--tool <command>', 'Editor, app, or terminal command to use (e.g. cursor, code, codex-app, tmux)')
|
|
93
107
|
.option('--enter', 'Enter the worktree sub-shell after opening')
|
|
94
108
|
.addOption(new Option('--no-enter', 'Do not enter the worktree sub-shell after opening').hideHelp())
|
|
109
|
+
.addHelpText('after', openHelp)
|
|
95
110
|
.action(async (worktree, options) => {
|
|
96
111
|
await openCommand(worktree, options);
|
|
97
112
|
});
|
|
@@ -114,21 +129,22 @@ function registerWorktreeCommands(parent) {
|
|
|
114
129
|
await pathCommand(worktree);
|
|
115
130
|
});
|
|
116
131
|
parent.command('create-sandbox')
|
|
117
|
-
.description('Create a
|
|
132
|
+
.description('Create a local-only disposable experiment sandbox')
|
|
118
133
|
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
119
|
-
.option('--carry', '
|
|
120
|
-
.option('--no-carry', 'Do not
|
|
134
|
+
.option('--carry', 'Copy uncommitted changes to sandbox')
|
|
135
|
+
.option('--no-carry', 'Do not copy uncommitted changes')
|
|
121
136
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
122
137
|
.option('--open', 'Open an editor after creation')
|
|
123
138
|
.option('--no-open', 'Skip opening an editor after creation')
|
|
124
139
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
125
140
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
126
141
|
.option('--exec <command>', 'Command to execute after creation')
|
|
142
|
+
.addHelpText('after', createSandboxHelp)
|
|
127
143
|
.action(async (options) => {
|
|
128
144
|
await createSandboxCommand(options);
|
|
129
145
|
});
|
|
130
146
|
parent.command('handoff')
|
|
131
|
-
.description('Carry
|
|
147
|
+
.description('Carry dirty current work into a named sandbox')
|
|
132
148
|
.option('-n, --name <name>', 'Optional handoff name (prompted when omitted)')
|
|
133
149
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
134
150
|
.option('--open', 'Open an editor after creation')
|
|
@@ -136,14 +152,17 @@ function registerWorktreeCommands(parent) {
|
|
|
136
152
|
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
137
153
|
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
138
154
|
.option('--exec <command>', 'Command to execute after creation')
|
|
155
|
+
.addHelpText('after', handoffHelp)
|
|
139
156
|
.action(async (options) => {
|
|
140
157
|
await handoffCommand(options);
|
|
141
158
|
});
|
|
142
159
|
parent.command('apply')
|
|
143
|
-
.description('
|
|
160
|
+
.description('Copy sandbox file changes to the origin checkout')
|
|
161
|
+
.addHelpText('after', applyHelp)
|
|
144
162
|
.action(applyCommand);
|
|
145
163
|
parent.command('unapply')
|
|
146
|
-
.description('
|
|
164
|
+
.description('Restore origin files from sandbox apply backups')
|
|
165
|
+
.addHelpText('after', unapplyHelp)
|
|
147
166
|
.action(unapplyCommand);
|
|
148
167
|
}
|
|
149
168
|
function rejectUnknownTopLevelCommand(args) {
|
|
@@ -162,55 +181,69 @@ program
|
|
|
162
181
|
.description('Interactive CLI for managing git worktrees and configs')
|
|
163
182
|
.version(getVersion())
|
|
164
183
|
.allowExcessArguments(false)
|
|
184
|
+
.addHelpText('after', intentRouterHelp)
|
|
165
185
|
.action(async () => {
|
|
166
|
-
|
|
167
|
-
await welcome();
|
|
168
|
-
await notifyIfUpdateAvailable();
|
|
186
|
+
const update = await checkForUpdate();
|
|
187
|
+
await welcome({ update });
|
|
169
188
|
const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
189
|
+
const dailyChoices = [
|
|
190
|
+
mainMenuChoice('create-smart', 'โ', 'Grow new realm', 'create a branch-backed worktree', 'growth'),
|
|
191
|
+
mainMenuChoice('worktree-checkout', 'โ', 'Check out branch', 'open an existing ref in a new realm', 'travel'),
|
|
192
|
+
mainMenuChoice('handoff', 'โฆ', 'Hand off current work', 'carry dirty work into a named sandbox', 'sandbox'),
|
|
193
|
+
mainMenuChoice('open', 'โ', 'Open realm', 'jump into an existing worktree', 'travel'),
|
|
194
|
+
mainMenuChoice('list', 'โ', 'Survey realms', 'scan active worktrees and PR state', 'tending'),
|
|
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'),
|
|
183
206
|
];
|
|
184
207
|
const sandboxChoices = [
|
|
185
|
-
|
|
186
|
-
|
|
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'),
|
|
187
215
|
];
|
|
188
216
|
const choices = isInSandbox
|
|
189
217
|
? [
|
|
190
218
|
...sandboxChoices,
|
|
191
|
-
|
|
192
|
-
...
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
219
|
+
mainMenuSeparator('Daily routes'),
|
|
220
|
+
...dailyChoices,
|
|
221
|
+
mainMenuSeparator('Growth and experiments'),
|
|
222
|
+
...growthChoices,
|
|
223
|
+
mainMenuSeparator('Tend realms'),
|
|
224
|
+
...tendingChoices,
|
|
225
|
+
mainMenuSeparator('Lore'),
|
|
226
|
+
...loreChoices,
|
|
197
227
|
]
|
|
198
228
|
: [
|
|
199
|
-
...
|
|
200
|
-
|
|
229
|
+
...dailyChoices,
|
|
230
|
+
mainMenuSeparator('Growth and experiments'),
|
|
231
|
+
...growthChoices,
|
|
232
|
+
mainMenuSeparator('Tend realms'),
|
|
233
|
+
...tendingChoices,
|
|
234
|
+
mainMenuSeparator('Sandbox grafts'),
|
|
201
235
|
...sandboxChoices,
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
{ name: `โก Consult Thor ${chalk.dim('(easter egg)')}`, value: 'thor' },
|
|
205
|
-
{ name: `๐ช Leave Yggdrasil ${chalk.dim('(exit)')}`, value: 'exit' },
|
|
236
|
+
mainMenuSeparator('Lore'),
|
|
237
|
+
...loreChoices,
|
|
206
238
|
];
|
|
239
|
+
console.log(renderMainMenuIntro({ isInSandbox }));
|
|
207
240
|
const { action } = await inquirer.prompt([
|
|
208
241
|
{
|
|
209
242
|
type: 'list',
|
|
210
243
|
name: 'action',
|
|
211
|
-
message: '
|
|
244
|
+
message: 'Which route should Yggtree follow?',
|
|
212
245
|
loop: false,
|
|
213
|
-
pageSize:
|
|
246
|
+
pageSize: 14,
|
|
214
247
|
choices,
|
|
215
248
|
},
|
|
216
249
|
]);
|
|
@@ -270,6 +303,7 @@ program
|
|
|
270
303
|
});
|
|
271
304
|
// --- Worktree Commands ---
|
|
272
305
|
const wt = program.command('wt').description('Manage git worktrees');
|
|
306
|
+
wt.addHelpText('after', intentRouterHelp);
|
|
273
307
|
registerWorktreeCommands(program);
|
|
274
308
|
registerWorktreeCommands(wt);
|
|
275
309
|
program.addHelpCommand(true);
|
|
@@ -279,5 +313,21 @@ program.command('bifrost')
|
|
|
279
313
|
program.command('thor')
|
|
280
314
|
.description('Consult the God of Thunder (Easter Egg)')
|
|
281
315
|
.action(thorCommand);
|
|
316
|
+
const config = program.command('config').description('Inspect or update global Yggtree settings');
|
|
317
|
+
config.command('get')
|
|
318
|
+
.description('Show resolved global settings')
|
|
319
|
+
.action(configGetCommand);
|
|
320
|
+
config.command('use <preset>')
|
|
321
|
+
.description('Use a global worktree path preset (default, yggtree, codex)')
|
|
322
|
+
.action(configUseCommand);
|
|
323
|
+
config.command('set-worktrees-root <path>')
|
|
324
|
+
.description('Set the global managed worktrees root')
|
|
325
|
+
.action(configSetWorktreesRootCommand);
|
|
326
|
+
config.command('set-worktree-layout <layout>')
|
|
327
|
+
.description('Set the managed worktree path layout (yggtree or codex)')
|
|
328
|
+
.action(configSetWorktreeLayoutCommand);
|
|
329
|
+
config.command('reset')
|
|
330
|
+
.description('Reset global settings to defaults')
|
|
331
|
+
.action(configResetCommand);
|
|
282
332
|
rejectUnknownTopLevelCommand(argv);
|
|
283
333
|
program.parse(argv);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import { YGG_ROOT } from './paths.js';
|
|
5
|
+
const CONFIG_PATH = path.join(YGG_ROOT, 'config.json');
|
|
6
|
+
const DEFAULT_WORKTREE_LAYOUT = 'yggtree';
|
|
7
|
+
export function expandHome(value) {
|
|
8
|
+
if (value === '~')
|
|
9
|
+
return os.homedir();
|
|
10
|
+
if (value.startsWith('~/'))
|
|
11
|
+
return path.join(os.homedir(), value.slice(2));
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
export function formatHome(value) {
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
if (value === home)
|
|
17
|
+
return '~';
|
|
18
|
+
if (value.startsWith(`${home}${path.sep}`))
|
|
19
|
+
return `~/${path.relative(home, value)}`;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
function normalizeRoot(value) {
|
|
23
|
+
return path.resolve(expandHome(value || YGG_ROOT));
|
|
24
|
+
}
|
|
25
|
+
export function normalizeWorktreesRootInput(value) {
|
|
26
|
+
return path.resolve(expandHome(value));
|
|
27
|
+
}
|
|
28
|
+
function normalizeLayout(value) {
|
|
29
|
+
return value || DEFAULT_WORKTREE_LAYOUT;
|
|
30
|
+
}
|
|
31
|
+
export async function readGlobalConfig() {
|
|
32
|
+
try {
|
|
33
|
+
if (await fs.pathExists(CONFIG_PATH)) {
|
|
34
|
+
const config = await fs.readJSON(CONFIG_PATH);
|
|
35
|
+
return {
|
|
36
|
+
worktreesRoot: typeof config.worktreesRoot === 'string' ? config.worktreesRoot : undefined,
|
|
37
|
+
worktreeLayout: config.worktreeLayout === 'codex' ? 'codex' : undefined,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return {};
|
|
43
|
+
}
|
|
44
|
+
return {};
|
|
45
|
+
}
|
|
46
|
+
export async function writeGlobalConfig(config) {
|
|
47
|
+
await fs.ensureDir(YGG_ROOT);
|
|
48
|
+
await fs.writeJSON(CONFIG_PATH, config, { spaces: 2 });
|
|
49
|
+
}
|
|
50
|
+
export async function getWorktreePathConfig() {
|
|
51
|
+
const config = await readGlobalConfig();
|
|
52
|
+
return {
|
|
53
|
+
root: normalizeRoot(config.worktreesRoot),
|
|
54
|
+
layout: normalizeLayout(config.worktreeLayout),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
export async function getManagedWorktreesRoot() {
|
|
58
|
+
const config = await getWorktreePathConfig();
|
|
59
|
+
return config.root;
|
|
60
|
+
}
|
|
61
|
+
export function buildManagedWorktreePath(repoName, worktreeSlug, config) {
|
|
62
|
+
if (config.layout === 'codex') {
|
|
63
|
+
return path.join(config.root, worktreeSlug, repoName);
|
|
64
|
+
}
|
|
65
|
+
return path.join(config.root, repoName, worktreeSlug);
|
|
66
|
+
}
|
|
67
|
+
export function getPresetConfig(preset) {
|
|
68
|
+
const normalized = preset.trim().toLowerCase();
|
|
69
|
+
if (normalized === 'default' || normalized === 'yggtree') {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
if (normalized === 'codex') {
|
|
73
|
+
return {
|
|
74
|
+
worktreesRoot: '~/.codex/worktrees',
|
|
75
|
+
worktreeLayout: 'codex',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
export function formatGlobalConfig(config, resolved) {
|
|
81
|
+
return [
|
|
82
|
+
`worktreesRoot: ${formatHome(resolved.root)}`,
|
|
83
|
+
`worktreeLayout: ${resolved.layout}`,
|
|
84
|
+
`configFile: ${formatHome(CONFIG_PATH)}`,
|
|
85
|
+
config.worktreesRoot || config.worktreeLayout
|
|
86
|
+
? 'preset: custom'
|
|
87
|
+
: 'preset: default',
|
|
88
|
+
];
|
|
89
|
+
}
|
package/dist/lib/help.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const intentRouterHelp = `
|
|
2
|
+
Choose by intent:
|
|
3
|
+
New official task branch yggtree create feat/name --base main --source remote
|
|
4
|
+
Existing branch/interruption yggtree wc --ref branch-name
|
|
5
|
+
Continue dirty current work yggtree handoff --name task-name
|
|
6
|
+
Disposable local experiment yggtree create-sandbox
|
|
7
|
+
Copy sandbox files to origin yggtree apply # from inside sandbox, not a Git merge
|
|
8
|
+
Bulk official worktrees yggtree create-multi
|
|
9
|
+
|
|
10
|
+
More detail: run yggtree help <command>, for example yggtree help handoff.
|
|
11
|
+
`;
|
|
12
|
+
export const createHelp = `
|
|
13
|
+
Behavior:
|
|
14
|
+
Creates an official branch-backed worktree, publishes the branch to origin, and enters the shell by default.
|
|
15
|
+
Use for real task branches. For disposable experiments, use create-sandbox.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
yggtree create feat/new-flow --base main --source remote
|
|
19
|
+
yggtree create feat/background-task --base main --source remote --no-open --no-enter
|
|
20
|
+
`;
|
|
21
|
+
export const createMultiHelp = `
|
|
22
|
+
Behavior:
|
|
23
|
+
Bulk-creates official branch-backed worktrees.
|
|
24
|
+
This does not share create's open/enter/exec lifecycle. Use create for one task branch.
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
yggtree create-multi --base main --source remote
|
|
28
|
+
`;
|
|
29
|
+
export const checkoutHelp = `
|
|
30
|
+
Behavior:
|
|
31
|
+
Creates or reuses a worktree for an existing branch or ref without disturbing current work.
|
|
32
|
+
Use for interruptions and branch review. Use create for a new official task branch.
|
|
33
|
+
Enters the worktree shell by default; add --no-enter when automation should return.
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
yggtree wc --ref hotfix/payment-timeout
|
|
37
|
+
yggtree wc --ref main --name fresh-main --no-open --no-enter
|
|
38
|
+
`;
|
|
39
|
+
export const openHelp = `
|
|
40
|
+
Behavior:
|
|
41
|
+
Opens an existing worktree in an editor, desktop app, or terminal target.
|
|
42
|
+
Returns by default after opening. Add --enter only when the shell should continue inside the worktree.
|
|
43
|
+
|
|
44
|
+
Examples:
|
|
45
|
+
yggtree open my-feature --tool cursor
|
|
46
|
+
yggtree open my-feature --tool codex-app
|
|
47
|
+
yggtree list --open
|
|
48
|
+
`;
|
|
49
|
+
export const createSandboxHelp = `
|
|
50
|
+
Behavior:
|
|
51
|
+
Creates a local-only disposable experiment from the current branch.
|
|
52
|
+
For continuing current dirty work, prefer handoff instead of teaching create-sandbox --carry.
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
yggtree create-sandbox
|
|
56
|
+
yggtree create-sandbox --name ui-option-a --no-open
|
|
57
|
+
|
|
58
|
+
Related:
|
|
59
|
+
handoff carries current dirty work into a named sandbox.
|
|
60
|
+
apply copies changed sandbox files back to origin.
|
|
61
|
+
`;
|
|
62
|
+
export const handoffHelp = `
|
|
63
|
+
Behavior:
|
|
64
|
+
Carry staged, unstaged, and untracked work into a named sandbox so you can continue there.
|
|
65
|
+
The origin checkout is not cleaned; review or reset it separately if needed.
|
|
66
|
+
|
|
67
|
+
Example:
|
|
68
|
+
yggtree handoff --name continue-auth-refactor
|
|
69
|
+
|
|
70
|
+
Related:
|
|
71
|
+
create-sandbox starts a disposable local experiment.
|
|
72
|
+
apply copies changed sandbox files back to origin.
|
|
73
|
+
`;
|
|
74
|
+
export const applyHelp = `
|
|
75
|
+
Behavior:
|
|
76
|
+
Copies changed files from the current sandbox back to the origin checkout.
|
|
77
|
+
Stores backups in sandbox metadata before overwriting origin files.
|
|
78
|
+
Not a Git merge, rebase, patch, or cherry-pick. Review the origin diff afterward.
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
yggtree apply
|
|
82
|
+
|
|
83
|
+
Related:
|
|
84
|
+
unapply restores from the sandbox backup metadata while the sandbox still exists.
|
|
85
|
+
`;
|
|
86
|
+
export const unapplyHelp = `
|
|
87
|
+
Behavior:
|
|
88
|
+
Restores origin files from sandbox metadata created by apply.
|
|
89
|
+
Only works while the sandbox still exists.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
yggtree unapply
|
|
93
|
+
`;
|
package/dist/lib/paths.js
CHANGED
package/dist/lib/repo-context.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import { getRepoRoot } from './git.js';
|
|
4
|
+
import { getManagedWorktreesRoot } from './global-config.js';
|
|
4
5
|
import { getValidRegisteredRepos } from './registry.js';
|
|
5
6
|
import { log } from './ui.js';
|
|
6
7
|
import { formatWorktreeDisplayPath } from './worktree.js';
|
|
@@ -11,6 +12,7 @@ export async function ensureRepoContext() {
|
|
|
11
12
|
catch {
|
|
12
13
|
const validRepos = await getValidRegisteredRepos();
|
|
13
14
|
const repoEntries = Object.entries(validRepos);
|
|
15
|
+
const managedRoot = await getManagedWorktreesRoot();
|
|
14
16
|
if (repoEntries.length === 0) {
|
|
15
17
|
log.error('Not inside a git repository and no registered realms found.');
|
|
16
18
|
log.dim('Run `yggtree` inside an existing git project first to register it.');
|
|
@@ -34,7 +36,7 @@ export async function ensureRepoContext() {
|
|
|
34
36
|
name: 'selectedRepoPath',
|
|
35
37
|
message: 'Select a realm:',
|
|
36
38
|
choices: repoEntries.map(([name, repoPath]) => ({
|
|
37
|
-
name: `${chalk.bold.yellow(name)} ${chalk.dim(formatWorktreeDisplayPath(repoPath))}`,
|
|
39
|
+
name: `${chalk.bold.yellow(name)} ${chalk.dim(formatWorktreeDisplayPath(repoPath, managedRoot))}`,
|
|
38
40
|
value: repoPath,
|
|
39
41
|
})),
|
|
40
42
|
pageSize: 10,
|
package/dist/lib/ui.js
CHANGED
|
@@ -6,15 +6,80 @@ import path from 'path';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { getVersion } from './version.js';
|
|
8
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const yggAccent = chalk.hex('#DEADED');
|
|
10
|
+
const yggLeaf = chalk.hex('#9AD68B');
|
|
11
|
+
const yggSky = chalk.hex('#7AC7D8');
|
|
12
|
+
const yggBark = chalk.hex('#C99A5B');
|
|
13
|
+
const yggRoot = chalk.hex('#B48AE8');
|
|
14
|
+
const yggWarn = chalk.hex('#E4B85E');
|
|
15
|
+
const yggDanger = chalk.hex('#E06C75');
|
|
16
|
+
const yggMuted = chalk.hex('#8D8D8D');
|
|
17
|
+
export function renderWelcome(options = {}) {
|
|
18
|
+
const rule = yggAccent('โ'.repeat(54));
|
|
12
19
|
const title = figlet.textSync('Yggdrasil', { font: 'Standard' });
|
|
13
|
-
console.log(gradient.mind.multiline(title));
|
|
14
20
|
const version = getVersion();
|
|
15
|
-
|
|
16
|
-
|
|
21
|
+
const updateLines = renderUpdateNotice(options.update);
|
|
22
|
+
return [
|
|
23
|
+
'',
|
|
24
|
+
rule,
|
|
25
|
+
gradient.mind.multiline(title),
|
|
26
|
+
chalk.bold(yggAccent(' Yggdrasil')),
|
|
27
|
+
chalk.dim(` v${version} โข The World Tree Worktree Assistant`),
|
|
28
|
+
...updateLines,
|
|
29
|
+
rule,
|
|
30
|
+
'',
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
export const welcome = async (options = {}) => {
|
|
34
|
+
console.log(renderWelcome(options));
|
|
17
35
|
};
|
|
36
|
+
export function renderMainMenuIntro(options) {
|
|
37
|
+
const detail = options.isInSandbox
|
|
38
|
+
? 'Sandbox tools appear first because this realm can apply or undo grafted changes.'
|
|
39
|
+
: 'Create, enter, inspect, or tend your worktrees.';
|
|
40
|
+
return [
|
|
41
|
+
'Choose the next realm action.',
|
|
42
|
+
yggMuted('Daily routes first. Maintenance and lore wait below.'),
|
|
43
|
+
'',
|
|
44
|
+
`${yggBark('โ')} ${badge('route 01')} ${chalk.bold('Realm')}`,
|
|
45
|
+
`${yggBark('โ')} ${yggMuted(detail)}`,
|
|
46
|
+
].join('\n');
|
|
47
|
+
}
|
|
48
|
+
export function formatMainMenuChoice(options) {
|
|
49
|
+
const tone = menuTone(options.tone);
|
|
50
|
+
const label = options.label.padEnd(24);
|
|
51
|
+
return `${tone(options.glyph)} ${chalk.bold(label)} ${yggMuted(options.detail)}`;
|
|
52
|
+
}
|
|
53
|
+
function renderUpdateNotice(update) {
|
|
54
|
+
if (!update?.updateAvailable)
|
|
55
|
+
return [];
|
|
56
|
+
return [
|
|
57
|
+
'',
|
|
58
|
+
`${yggWarn('Update available')} ${yggMuted(`yggtree ${update.currentVersion} -> ${update.latestVersion}`)}`,
|
|
59
|
+
yggMuted('Run: npm install -g yggtree'),
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
function badge(value) {
|
|
63
|
+
return yggRoot(` ${value} `);
|
|
64
|
+
}
|
|
65
|
+
function menuTone(tone) {
|
|
66
|
+
switch (tone) {
|
|
67
|
+
case 'growth':
|
|
68
|
+
return yggLeaf;
|
|
69
|
+
case 'travel':
|
|
70
|
+
return yggSky;
|
|
71
|
+
case 'sandbox':
|
|
72
|
+
return yggRoot;
|
|
73
|
+
case 'tending':
|
|
74
|
+
return yggBark;
|
|
75
|
+
case 'danger':
|
|
76
|
+
return yggDanger;
|
|
77
|
+
case 'lore':
|
|
78
|
+
return yggAccent;
|
|
79
|
+
case 'exit':
|
|
80
|
+
return yggMuted;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
18
83
|
// --- Logger ---
|
|
19
84
|
export const log = {
|
|
20
85
|
info: (msg) => console.log(chalk.blue('โน'), msg),
|
package/dist/lib/worktree.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { YGG_ROOT } from './paths.js';
|
|
5
5
|
import { getSandboxMetaPath } from './sandbox.js';
|
|
6
6
|
export const WORKTREE_TYPE_ORDER = {
|
|
7
7
|
MAIN: 0,
|
|
@@ -12,19 +12,23 @@ export const WORKTREE_TYPE_ORDER = {
|
|
|
12
12
|
export function getWorktreeBranchName(worktree) {
|
|
13
13
|
return worktree.branch || worktree.HEAD || 'detached';
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function isPathInsideRoot(worktreePath, managedRoot) {
|
|
16
|
+
const relative = path.relative(managedRoot, worktreePath);
|
|
17
|
+
return relative === '' || Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
|
|
17
18
|
}
|
|
18
|
-
export function
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
export function isManagedWorktreePath(worktreePath, managedRoot = YGG_ROOT) {
|
|
20
|
+
return isPathInsideRoot(path.resolve(worktreePath), path.resolve(managedRoot));
|
|
21
|
+
}
|
|
22
|
+
export function formatWorktreeDisplayPath(worktreePath, managedRoot = YGG_ROOT) {
|
|
23
|
+
if (isManagedWorktreePath(worktreePath, managedRoot)) {
|
|
24
|
+
return path.relative(managedRoot, worktreePath);
|
|
21
25
|
}
|
|
22
26
|
return worktreePath.replace(process.env.HOME || '', '~');
|
|
23
27
|
}
|
|
24
|
-
export function findWorktreeByName(worktrees, worktreeName) {
|
|
28
|
+
export function findWorktreeByName(worktrees, worktreeName, managedRoot = YGG_ROOT) {
|
|
25
29
|
return worktrees.find(worktree => {
|
|
26
30
|
const branchName = getWorktreeBranchName(worktree);
|
|
27
|
-
const relativePath = path.relative(
|
|
31
|
+
const relativePath = path.relative(managedRoot, worktree.path);
|
|
28
32
|
const basename = path.basename(worktree.path);
|
|
29
33
|
return branchName === worktreeName ||
|
|
30
34
|
relativePath === worktreeName ||
|
|
@@ -32,8 +36,8 @@ export function findWorktreeByName(worktrees, worktreeName) {
|
|
|
32
36
|
basename === worktreeName;
|
|
33
37
|
});
|
|
34
38
|
}
|
|
35
|
-
export async function detectWorktreeType(worktree, mainWorktreePath) {
|
|
36
|
-
const isManaged = isManagedWorktreePath(worktree.path);
|
|
39
|
+
export async function detectWorktreeType(worktree, mainWorktreePath, managedRoot = YGG_ROOT) {
|
|
40
|
+
const isManaged = isManagedWorktreePath(worktree.path, managedRoot);
|
|
37
41
|
if (!isManaged) {
|
|
38
42
|
return worktree.path === mainWorktreePath ? 'MAIN' : 'LINKED';
|
|
39
43
|
}
|