yggtree 1.0.0 → 1.0.1
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 +161 -8
- package/dist/commands/wt/bootstrap.js +7 -4
- package/dist/commands/wt/create-branch.js +3 -2
- package/dist/commands/wt/create-multi.js +3 -2
- package/dist/commands/wt/create.js +3 -2
- package/dist/commands/wt/delete.js +8 -5
- package/dist/commands/wt/list.js +5 -1
- package/dist/lib/git.js +4 -0
- package/dist/lib/paths.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# 🌳 Yggdrasil Worktree (yggtree)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/yggtree)
|
|
4
|
+
[](https://www.npmjs.com/package/yggtree)
|
|
5
|
+
|
|
3
6
|
**Yggdrasil Worktree** (invoked as `yggtree`) is a powerful, interactive CLI designed to streamline your Git worktree workflow. Like the mythical world tree connecting the realms, Yggdrasil connects your branches into isolated, manageable worktrees.
|
|
4
7
|
|
|
5
8
|
---
|
|
@@ -8,17 +11,16 @@
|
|
|
8
11
|
|
|
9
12
|
### Installation
|
|
10
13
|
|
|
14
|
+
You can run it directly without installing:
|
|
15
|
+
|
|
11
16
|
```bash
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
cd yggtree
|
|
17
|
+
npx yggtree
|
|
18
|
+
```
|
|
15
19
|
|
|
16
|
-
|
|
17
|
-
npm install
|
|
18
|
-
npm run build
|
|
20
|
+
Or install it globally for convenience:
|
|
19
21
|
|
|
20
|
-
|
|
21
|
-
npm
|
|
22
|
+
```bash
|
|
23
|
+
npm install -g yggtree
|
|
22
24
|
```
|
|
23
25
|
|
|
24
26
|
### Usage
|
|
@@ -53,6 +55,140 @@ Need to spin up multiple features? Provide branch names separated by spaces, and
|
|
|
53
55
|
### 🚀 Custom Bootstrapping
|
|
54
56
|
Configure your environment automatically using an `anvil-worktree.json` (also compatible with `.cursor/worktrees.json`) file in your project root.
|
|
55
57
|
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## 🤔 Why Yggdrasil Worktree?
|
|
61
|
+
|
|
62
|
+
Git worktrees are incredibly powerful, but they are also **painfully manual** once you go beyond a single task.
|
|
63
|
+
|
|
64
|
+
Modern development isn’t sequential anymore. You’re constantly switching contexts, juggling experiments, fixes, reviews, and now **AI agents running in parallel**.
|
|
65
|
+
|
|
66
|
+
Yggdrasil exists to solve three modern problems at once:
|
|
67
|
+
|
|
68
|
+
1. **Parallel work without context collision**
|
|
69
|
+
2. **Fast environment setup per task**
|
|
70
|
+
3. **Agent-friendly isolation for AI workflows**
|
|
71
|
+
|
|
72
|
+
Instead of juggling branches, folders, and bootstrap scripts by hand, Yggdrasil gives you a repeatable, intentional workflow.
|
|
73
|
+
Each worktree becomes its own **small realm** — isolated, focused, and safe to explore.
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 🧠 Parallel Development, Done Right
|
|
78
|
+
|
|
79
|
+
Traditional branching assumes *sequential* work.
|
|
80
|
+
|
|
81
|
+
Reality looks more like this:
|
|
82
|
+
|
|
83
|
+
* You’re fixing a bug
|
|
84
|
+
* Reviewing a PR
|
|
85
|
+
* Prototyping a feature
|
|
86
|
+
* Letting an AI agent explore refactors
|
|
87
|
+
* Letting another agent generate tests
|
|
88
|
+
|
|
89
|
+
All at the same time.
|
|
90
|
+
|
|
91
|
+
Yggdrasil lets each task live in its **own isolated worktree** — a separate realm where ideas can evolve without colliding.
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
yggtree wt create feat/eng-2581-state-selection
|
|
95
|
+
yggtree wt create fix/eng-2610-validation
|
|
96
|
+
yggtree wt create chore/cleanup-api
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Each command spins up:
|
|
100
|
+
|
|
101
|
+
* A clean folder
|
|
102
|
+
* A dedicated branch
|
|
103
|
+
* A fully bootstrapped environment
|
|
104
|
+
|
|
105
|
+
No context bleeding.
|
|
106
|
+
No stash juggling.
|
|
107
|
+
No “what branch am I on?” moments.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 🤖 Built for AI-Assisted Workflows
|
|
112
|
+
|
|
113
|
+
Yggdrasil shines when combined with AI agents.
|
|
114
|
+
|
|
115
|
+
Instead of running agents against the same working directory, you can:
|
|
116
|
+
|
|
117
|
+
* Assign one worktree per agent
|
|
118
|
+
* Let each agent explore independently
|
|
119
|
+
* Compare results safely
|
|
120
|
+
* Merge only what makes sense
|
|
121
|
+
|
|
122
|
+
Example workflow:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Prepare isolated worktrees
|
|
126
|
+
yggtree wt create feat/ai-refactor-a
|
|
127
|
+
yggtree wt create feat/ai-refactor-b
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Each agent operates in its own realm, free to experiment without side effects.
|
|
131
|
+
|
|
132
|
+
This enables patterns like:
|
|
133
|
+
|
|
134
|
+
* Model A refactors architecture
|
|
135
|
+
* Model B focuses on tests
|
|
136
|
+
* Model C explores performance improvements
|
|
137
|
+
|
|
138
|
+
All in parallel.
|
|
139
|
+
All reviewable.
|
|
140
|
+
All under your control.
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## ⚡ Zero-Friction Bootstrapping
|
|
145
|
+
|
|
146
|
+
Every worktree can auto-configure itself.
|
|
147
|
+
|
|
148
|
+
Yggdrasil reads your project’s setup file and prepares the realm immediately:
|
|
149
|
+
|
|
150
|
+
```json
|
|
151
|
+
{
|
|
152
|
+
"setup-worktree": [
|
|
153
|
+
"npm install",
|
|
154
|
+
"git submodule sync --recursive",
|
|
155
|
+
"git submodule update --init --recursive"
|
|
156
|
+
]
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
That means:
|
|
161
|
+
|
|
162
|
+
* No manual installs
|
|
163
|
+
* No forgotten submodules
|
|
164
|
+
* No “works on main but not here” surprises
|
|
165
|
+
|
|
166
|
+
You enter a worktree and it’s ready to work.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## 🌱 When Should You Use Yggdrasil?
|
|
171
|
+
|
|
172
|
+
Yggdrasil is ideal when:
|
|
173
|
+
|
|
174
|
+
* You work on multiple tasks in parallel
|
|
175
|
+
* You use AI agents for exploration or execution
|
|
176
|
+
* You want isolation without duplication
|
|
177
|
+
* You value repeatable, scripted setup
|
|
178
|
+
* You’re tired of fighting your own repo
|
|
179
|
+
|
|
180
|
+
If your workflow has outgrown `git checkout`, Yggdrasil is the missing layer.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## 🌍 Philosophy
|
|
185
|
+
|
|
186
|
+
Branches are ideas.
|
|
187
|
+
Worktrees are realities.
|
|
188
|
+
|
|
189
|
+
Yggdrasil helps you keep each idea grounded in its own world — until you decide which ones deserve to merge.
|
|
190
|
+
|
|
191
|
+
|
|
56
192
|
---
|
|
57
193
|
|
|
58
194
|
## ⚙️ Configuration
|
|
@@ -88,6 +224,23 @@ Yggdrasil looks for setup instructions in your project root:
|
|
|
88
224
|
|
|
89
225
|
---
|
|
90
226
|
|
|
227
|
+
## 🛠️ Development
|
|
228
|
+
|
|
229
|
+
If you'd like to contribute or run the latest development version:
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
# Clone the repository
|
|
233
|
+
git clone https://github.com/leoreisdias/yggdrasil-cli.git
|
|
234
|
+
cd yggdrasil-cli
|
|
235
|
+
|
|
236
|
+
# Install dependencies and build
|
|
237
|
+
npm install
|
|
238
|
+
npm run build
|
|
239
|
+
|
|
240
|
+
# Link the CLI locally
|
|
241
|
+
npm link
|
|
242
|
+
```
|
|
243
|
+
|
|
91
244
|
## 📄 License
|
|
92
245
|
|
|
93
246
|
MIT License.
|
|
@@ -17,10 +17,13 @@ export async function bootstrapCommand() {
|
|
|
17
17
|
log.info('No managed worktrees found to bootstrap.');
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
|
-
const choices = managedWts.map(wt =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
const choices = managedWts.map(wt => {
|
|
21
|
+
const relative = path.relative(WORKTREES_ROOT, wt.path);
|
|
22
|
+
return {
|
|
23
|
+
name: `${chalk.bold(relative)} (${chalk.dim(wt.branch || wt.HEAD)})`,
|
|
24
|
+
value: wt.path,
|
|
25
|
+
};
|
|
26
|
+
});
|
|
24
27
|
const { selectedPath } = await inquirer.prompt([
|
|
25
28
|
{
|
|
26
29
|
type: 'list',
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { getRepoRoot, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
4
|
+
import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
5
5
|
import { runBootstrap } from '../../lib/config.js';
|
|
6
6
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
7
|
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
@@ -71,7 +71,8 @@ export async function createCommandNew(options) {
|
|
|
71
71
|
// Convert branch name to slug (friendly folder name)
|
|
72
72
|
// e.g. feat/eng-2222-new-button -> feat-eng-2222-new-button
|
|
73
73
|
const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
|
|
74
|
-
const
|
|
74
|
+
const repoName = await getRepoName();
|
|
75
|
+
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
75
76
|
// 2. Validation
|
|
76
77
|
if (!slug)
|
|
77
78
|
throw new Error('Invalid name');
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { getRepoRoot, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
4
|
+
import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
5
5
|
import { runBootstrap } from '../../lib/config.js';
|
|
6
6
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
7
|
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
@@ -66,9 +66,10 @@ export async function createCommandMulti(options) {
|
|
|
66
66
|
spinner.succeed(`Base ref ${chalk.cyan(baseRef)} verified.`);
|
|
67
67
|
const createdWorktrees = [];
|
|
68
68
|
// 3. Execution for each branch
|
|
69
|
+
const repoName = await getRepoName();
|
|
69
70
|
for (const branchName of branchNames) {
|
|
70
71
|
const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
|
|
71
|
-
const wtPath = path.join(WORKTREES_ROOT, slug);
|
|
72
|
+
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
72
73
|
log.header(`Processing: ${branchName}`);
|
|
73
74
|
const wtSpinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
|
|
74
75
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import path from 'path';
|
|
4
|
-
import { getRepoRoot, verifyRef, createWorktree, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
4
|
+
import { getRepoRoot, getRepoName, verifyRef, createWorktree, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
5
5
|
import { runBootstrap } from '../../lib/config.js';
|
|
6
6
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
7
|
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
@@ -68,7 +68,8 @@ export async function createCommand(options) {
|
|
|
68
68
|
}
|
|
69
69
|
const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
|
|
70
70
|
const slug = name.replace(/\s+/g, '-');
|
|
71
|
-
const
|
|
71
|
+
const repoName = await getRepoName();
|
|
72
|
+
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
72
73
|
// 2. Validation
|
|
73
74
|
if (!slug)
|
|
74
75
|
throw new Error('Invalid name');
|
|
@@ -14,10 +14,13 @@ export async function deleteCommand() {
|
|
|
14
14
|
log.info('No managed worktrees found to delete.');
|
|
15
15
|
return;
|
|
16
16
|
}
|
|
17
|
-
const choices = managedWts.map(wt =>
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const choices = managedWts.map(wt => {
|
|
18
|
+
const relative = path.relative(WORKTREES_ROOT, wt.path);
|
|
19
|
+
return {
|
|
20
|
+
name: `${chalk.bold(relative)} (${chalk.dim(wt.branch || wt.HEAD)})`,
|
|
21
|
+
value: wt.path,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
21
24
|
const { selectedPath } = await inquirer.prompt([
|
|
22
25
|
{
|
|
23
26
|
type: 'list',
|
|
@@ -26,7 +29,7 @@ export async function deleteCommand() {
|
|
|
26
29
|
choices: choices,
|
|
27
30
|
},
|
|
28
31
|
]);
|
|
29
|
-
const worktreeName = path.
|
|
32
|
+
const worktreeName = path.relative(WORKTREES_ROOT, selectedPath);
|
|
30
33
|
const { confirm } = await inquirer.prompt([
|
|
31
34
|
{
|
|
32
35
|
type: 'input',
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
3
4
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
4
5
|
import { log } from '../../lib/ui.js';
|
|
@@ -19,7 +20,10 @@ export async function listCommand() {
|
|
|
19
20
|
const isMain = !isManaged; // Simplification: assume main repo is not in managed dir
|
|
20
21
|
const type = isManaged ? chalk.green('MANAGED') : chalk.blue('MAIN ');
|
|
21
22
|
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
22
|
-
|
|
23
|
+
let displayPath = wt.path.replace(process.env.HOME || '', '~');
|
|
24
|
+
if (isManaged) {
|
|
25
|
+
displayPath = path.relative(WORKTREES_ROOT, wt.path);
|
|
26
|
+
}
|
|
23
27
|
const colorPath = isManaged ? chalk.cyan(displayPath) : chalk.dim(displayPath);
|
|
24
28
|
console.log(` ${type} ${chalk.yellow(branchName.padEnd(18))} ${colorPath}`);
|
|
25
29
|
}
|
package/dist/lib/git.js
CHANGED
|
@@ -10,6 +10,10 @@ export async function getRepoRoot() {
|
|
|
10
10
|
throw new Error('Not a git repository');
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
|
+
export async function getRepoName() {
|
|
14
|
+
const root = await getRepoRoot();
|
|
15
|
+
return path.basename(root);
|
|
16
|
+
}
|
|
13
17
|
export async function getCurrentBranch() {
|
|
14
18
|
try {
|
|
15
19
|
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
package/dist/lib/paths.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import os from 'os';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
export const
|
|
4
|
-
export const WORKTREES_ROOT =
|
|
3
|
+
export const YGG_ROOT = path.join(os.homedir(), '.yggtree');
|
|
4
|
+
export const WORKTREES_ROOT = YGG_ROOT;
|