yggtree 1.0.0
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/LICENSE +21 -0
- package/README.md +93 -0
- package/bin/yggtree +2 -0
- package/dist/commands/wt/bootstrap.js +41 -0
- package/dist/commands/wt/create-branch.js +138 -0
- package/dist/commands/wt/create-multi.js +109 -0
- package/dist/commands/wt/create.js +124 -0
- package/dist/commands/wt/delete.js +52 -0
- package/dist/commands/wt/list.js +32 -0
- package/dist/commands/wt/prune.js +20 -0
- package/dist/index.js +102 -0
- package/dist/lib/config.js +81 -0
- package/dist/lib/git.js +72 -0
- package/dist/lib/paths.js +4 -0
- package/dist/lib/ui.js +39 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Leonardo Dias
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 🌳 Yggdrasil Worktree (yggtree)
|
|
2
|
+
|
|
3
|
+
**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
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🚀 Quick Start
|
|
8
|
+
|
|
9
|
+
### Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Clone the repository
|
|
13
|
+
git clone https://github.com/leoreisdias/yggtree.git
|
|
14
|
+
cd yggtree
|
|
15
|
+
|
|
16
|
+
# Install dependencies and build
|
|
17
|
+
npm install
|
|
18
|
+
npm run build
|
|
19
|
+
|
|
20
|
+
# Link the CLI globally
|
|
21
|
+
npm link
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Usage
|
|
25
|
+
|
|
26
|
+
Simply run `yggtree` to open the interactive menu:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
yggtree
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or use specific commands:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
yggtree wt create # Smart branch-based creation
|
|
36
|
+
yggtree wt list # View all managed worktrees
|
|
37
|
+
yggtree wt prune # Clean up stale worktree data
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ✨ Key Features
|
|
43
|
+
|
|
44
|
+
### 🌿 Smart Branch Creation (`wt create`)
|
|
45
|
+
The primary way to start working. Instead of worrying about folder names, just tell Yggdrasil which branch you want to work on.
|
|
46
|
+
- **Auto-Slug**: Converts `feat/eng-123-ui` to a clean folder name like `feat-eng-123-ui`.
|
|
47
|
+
- **Auto-Branching**: If the branch doesn't exist, Yggdrasil creates it for you from a base branch.
|
|
48
|
+
- **Remote Awareness**: Seamlessly base your work on `origin/main` or local refs.
|
|
49
|
+
|
|
50
|
+
### 🌳 Batch Creation (`wt create-multi`)
|
|
51
|
+
Need to spin up multiple features? Provide branch names separated by spaces, and Yggdrasil will provision all of them in one go.
|
|
52
|
+
|
|
53
|
+
### 🚀 Custom Bootstrapping
|
|
54
|
+
Configure your environment automatically using an `anvil-worktree.json` (also compatible with `.cursor/worktrees.json`) file in your project root.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## ⚙️ Configuration
|
|
59
|
+
|
|
60
|
+
Yggdrasil looks for setup instructions in your project root:
|
|
61
|
+
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"setup-worktree": [
|
|
65
|
+
"npm install",
|
|
66
|
+
"git submodule sync --recursive",
|
|
67
|
+
"git submodule update --init --recursive",
|
|
68
|
+
"npm run build",
|
|
69
|
+
"echo '🌳 The realm is ready!'"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🛠️ Commands Reference
|
|
77
|
+
|
|
78
|
+
| Command | Description |
|
|
79
|
+
| :--- | :--- |
|
|
80
|
+
| `yggtree` | Open the interactive main menu. |
|
|
81
|
+
| `yggtree wt create` | Create a worktree by branch name (Recommended). |
|
|
82
|
+
| `yggtree wt create-multi` | Create multiple worktrees in a single command. |
|
|
83
|
+
| `yggtree wt create-slug` | Manually specify both folder name and branch ref. |
|
|
84
|
+
| `yggtree wt list` | List all managed worktrees and their status. |
|
|
85
|
+
| `yggtree wt delete` | Interactively select and remove a worktree. |
|
|
86
|
+
| `yggtree wt bootstrap` | Re-run the setup commands for an existing worktree. |
|
|
87
|
+
| `yggtree wt prune` | Clean up Git's internal data for worktrees. |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## 📄 License
|
|
92
|
+
|
|
93
|
+
MIT License.
|
package/bin/yggtree
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
5
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
6
|
+
import { log } from '../../lib/ui.js';
|
|
7
|
+
import { runBootstrap } from '../../lib/config.js';
|
|
8
|
+
export async function bootstrapCommand() {
|
|
9
|
+
try {
|
|
10
|
+
const _ = await getRepoRoot();
|
|
11
|
+
const worktrees = await listWorktrees();
|
|
12
|
+
// 1. Select Worktree
|
|
13
|
+
// Filter managed or just show all? The prompt said "managed", let's prioritize managed but maybe allow all if needed?
|
|
14
|
+
// User requirements say "managed", sticking to that for consistency.
|
|
15
|
+
const managedWts = worktrees.filter(wt => wt.path.startsWith(WORKTREES_ROOT));
|
|
16
|
+
if (managedWts.length === 0) {
|
|
17
|
+
log.info('No managed worktrees found to bootstrap.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const choices = managedWts.map(wt => ({
|
|
21
|
+
name: `${chalk.bold(path.basename(wt.path))} (${chalk.dim(wt.branch || wt.HEAD)})`,
|
|
22
|
+
value: wt.path,
|
|
23
|
+
}));
|
|
24
|
+
const { selectedPath } = await inquirer.prompt([
|
|
25
|
+
{
|
|
26
|
+
type: 'list',
|
|
27
|
+
name: 'selectedPath',
|
|
28
|
+
message: 'Select worktree to bootstrap:',
|
|
29
|
+
choices: choices,
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
const wtPath = selectedPath;
|
|
33
|
+
const repoRoot = await getRepoRoot();
|
|
34
|
+
await runBootstrap(wtPath, repoRoot);
|
|
35
|
+
log.success('Bootstrap completed!');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
log.error(error.message);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getRepoRoot, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
5
|
+
import { runBootstrap } from '../../lib/config.js';
|
|
6
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
|
+
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import { spawn } from 'child_process';
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
export async function createCommandNew(options) {
|
|
12
|
+
try {
|
|
13
|
+
const repoRoot = await getRepoRoot();
|
|
14
|
+
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
15
|
+
// 1. Gather inputs
|
|
16
|
+
const currentBranch = await getCurrentBranch();
|
|
17
|
+
const answers = await inquirer.prompt([
|
|
18
|
+
{
|
|
19
|
+
type: 'input',
|
|
20
|
+
name: 'branch',
|
|
21
|
+
message: 'Branch name (e.g. feat/new-thing):',
|
|
22
|
+
default: options.branch,
|
|
23
|
+
when: !options.branch,
|
|
24
|
+
validate: (input) => input.trim().length > 0 || 'Branch name is required',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: 'input',
|
|
28
|
+
name: 'base',
|
|
29
|
+
message: 'Base branch name:',
|
|
30
|
+
default: options.base || currentBranch,
|
|
31
|
+
when: !options.base,
|
|
32
|
+
validate: (input) => input.trim().length > 0 || 'Base ref is required',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: 'list',
|
|
36
|
+
name: 'source',
|
|
37
|
+
message: 'Base on:',
|
|
38
|
+
loop: false,
|
|
39
|
+
choices: [
|
|
40
|
+
{ name: 'Remote (origin)', value: 'remote' },
|
|
41
|
+
{ name: 'Local', value: 'local' },
|
|
42
|
+
],
|
|
43
|
+
default: 'remote',
|
|
44
|
+
when: !options.base,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
type: 'confirm',
|
|
48
|
+
name: 'bootstrap',
|
|
49
|
+
message: 'Run bootstrap? (npm install + submodules)',
|
|
50
|
+
default: true,
|
|
51
|
+
when: options.bootstrap !== false,
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
let shouldEnter = false;
|
|
55
|
+
if (!options.branch) {
|
|
56
|
+
const finalAnswer = await inquirer.prompt([{
|
|
57
|
+
type: 'confirm',
|
|
58
|
+
name: 'shouldEnter',
|
|
59
|
+
message: 'Do you want to enter the new worktree now?',
|
|
60
|
+
default: true
|
|
61
|
+
}]);
|
|
62
|
+
shouldEnter = finalAnswer.shouldEnter;
|
|
63
|
+
}
|
|
64
|
+
const branchName = options.branch || answers.branch;
|
|
65
|
+
let baseRef = options.base || answers.base;
|
|
66
|
+
// Append origin/ if remote is selected and not already present
|
|
67
|
+
if (!options.base && answers.source === 'remote' && !baseRef.startsWith('origin/')) {
|
|
68
|
+
baseRef = `origin/${baseRef}`;
|
|
69
|
+
}
|
|
70
|
+
const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
|
|
71
|
+
// Convert branch name to slug (friendly folder name)
|
|
72
|
+
// e.g. feat/eng-2222-new-button -> feat-eng-2222-new-button
|
|
73
|
+
const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
|
|
74
|
+
const wtPath = path.join(WORKTREES_ROOT, slug);
|
|
75
|
+
// 2. Validation
|
|
76
|
+
if (!slug)
|
|
77
|
+
throw new Error('Invalid name');
|
|
78
|
+
if (!baseRef)
|
|
79
|
+
throw new Error('Invalid base ref');
|
|
80
|
+
// 3. Execution
|
|
81
|
+
const spinner = createSpinner('Fetching...').start();
|
|
82
|
+
await fetchAll();
|
|
83
|
+
spinner.text = 'Verifying base ref...';
|
|
84
|
+
const baseExists = await verifyRef(baseRef);
|
|
85
|
+
if (!baseExists) {
|
|
86
|
+
spinner.fail(`Base ref not found: ${baseRef}`);
|
|
87
|
+
log.warning(`Tip: try checking if the branch exists on remote.`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
spinner.text = `Creating worktree at ${ui.path(wtPath)}...`;
|
|
91
|
+
// Check if target branch already exists
|
|
92
|
+
const targetBranchExists = await verifyRef(branchName);
|
|
93
|
+
// If branch doesn't exist, we create it from base
|
|
94
|
+
// If it does exist, we just check it out
|
|
95
|
+
const createBranchFlag = targetBranchExists ? '' : `-b ${branchName}`;
|
|
96
|
+
try {
|
|
97
|
+
await fs.ensureDir(path.dirname(wtPath));
|
|
98
|
+
// slightly different logic for creating new branch vs existing
|
|
99
|
+
if (targetBranchExists) {
|
|
100
|
+
await execa('git', ['worktree', 'add', wtPath, branchName]);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
await execa('git', ['worktree', 'add', '-b', branchName, wtPath, baseRef]);
|
|
104
|
+
}
|
|
105
|
+
spinner.succeed('Worktree created.');
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
spinner.fail('Failed to create worktree.');
|
|
109
|
+
log.error(e.message);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// 4. Bootstrap
|
|
113
|
+
if (shouldBootstrap) {
|
|
114
|
+
await runBootstrap(wtPath, repoRoot);
|
|
115
|
+
}
|
|
116
|
+
// 5. Final Output
|
|
117
|
+
log.success('Worktree ready!');
|
|
118
|
+
if (shouldEnter) {
|
|
119
|
+
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
120
|
+
log.dim('Type "exit" to return to the main terminal.');
|
|
121
|
+
const shell = process.env.SHELL || 'zsh';
|
|
122
|
+
const child = spawn(shell, [], {
|
|
123
|
+
cwd: wtPath,
|
|
124
|
+
stdio: 'inherit',
|
|
125
|
+
});
|
|
126
|
+
child.on('close', () => {
|
|
127
|
+
log.info('Exited sub-shell.');
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
log.header(`cd "${wtPath}"`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
log.error(error.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getRepoRoot, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
5
|
+
import { runBootstrap } from '../../lib/config.js';
|
|
6
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
|
+
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
8
|
+
import { execa } from 'execa';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
export async function createCommandMulti(options) {
|
|
11
|
+
try {
|
|
12
|
+
const repoRoot = await getRepoRoot();
|
|
13
|
+
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
14
|
+
// 1. Gather inputs
|
|
15
|
+
const currentBranch = await getCurrentBranch();
|
|
16
|
+
const answers = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'base',
|
|
20
|
+
message: 'Base branch name:',
|
|
21
|
+
default: options.base || currentBranch,
|
|
22
|
+
when: !options.base,
|
|
23
|
+
validate: (input) => input.trim().length > 0 || 'Base ref is required',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'list',
|
|
27
|
+
name: 'source',
|
|
28
|
+
message: 'Base on:',
|
|
29
|
+
loop: false,
|
|
30
|
+
choices: [
|
|
31
|
+
{ name: 'Remote (origin)', value: 'remote' },
|
|
32
|
+
{ name: 'Local', value: 'local' },
|
|
33
|
+
],
|
|
34
|
+
default: 'remote',
|
|
35
|
+
when: !options.base,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
type: 'input',
|
|
39
|
+
name: 'branches',
|
|
40
|
+
message: 'Enter branch names (separated by space):',
|
|
41
|
+
validate: (input) => input.trim().length > 0 || 'At least one branch name is required',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
type: 'confirm',
|
|
45
|
+
name: 'bootstrap',
|
|
46
|
+
message: 'Run bootstrap for all worktrees? (npm install + submodules)',
|
|
47
|
+
default: true,
|
|
48
|
+
when: options.bootstrap !== false,
|
|
49
|
+
},
|
|
50
|
+
]);
|
|
51
|
+
let baseRef = options.base || answers.base;
|
|
52
|
+
if (!options.base && answers.source === 'remote' && !baseRef.startsWith('origin/')) {
|
|
53
|
+
baseRef = `origin/${baseRef}`;
|
|
54
|
+
}
|
|
55
|
+
const branchNames = answers.branches.split(/\s+/).filter((b) => b.length > 0);
|
|
56
|
+
const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
|
|
57
|
+
// 2. Validation of base ref
|
|
58
|
+
const spinner = createSpinner('Fetching...').start();
|
|
59
|
+
await fetchAll();
|
|
60
|
+
spinner.text = 'Verifying base ref...';
|
|
61
|
+
const baseExists = await verifyRef(baseRef);
|
|
62
|
+
if (!baseExists) {
|
|
63
|
+
spinner.fail(`Base ref not found: ${baseRef}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
spinner.succeed(`Base ref ${chalk.cyan(baseRef)} verified.`);
|
|
67
|
+
const createdWorktrees = [];
|
|
68
|
+
// 3. Execution for each branch
|
|
69
|
+
for (const branchName of branchNames) {
|
|
70
|
+
const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
|
|
71
|
+
const wtPath = path.join(WORKTREES_ROOT, slug);
|
|
72
|
+
log.header(`Processing: ${branchName}`);
|
|
73
|
+
const wtSpinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
|
|
74
|
+
try {
|
|
75
|
+
// Check if target branch already exists
|
|
76
|
+
const targetBranchExists = await verifyRef(branchName);
|
|
77
|
+
await fs.ensureDir(path.dirname(wtPath));
|
|
78
|
+
if (targetBranchExists) {
|
|
79
|
+
await execa('git', ['worktree', 'add', wtPath, branchName]);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
await execa('git', ['worktree', 'add', '-b', branchName, wtPath, baseRef]);
|
|
83
|
+
}
|
|
84
|
+
wtSpinner.succeed(`Worktree for ${chalk.cyan(branchName)} created.`);
|
|
85
|
+
createdWorktrees.push(wtPath);
|
|
86
|
+
// 4. Bootstrap
|
|
87
|
+
if (shouldBootstrap) {
|
|
88
|
+
await runBootstrap(wtPath, repoRoot);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
wtSpinner.fail(`Failed to create worktree for ${branchName}.`);
|
|
93
|
+
log.error(error.message);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// 5. Final Output
|
|
97
|
+
log.success(`${createdWorktrees.length} worktrees ready!`);
|
|
98
|
+
if (createdWorktrees.length > 0) {
|
|
99
|
+
log.info('You can access them with:');
|
|
100
|
+
createdWorktrees.forEach(wtPath => {
|
|
101
|
+
log.header(`cd "${wtPath}"`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
log.error(error.message);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getRepoRoot, verifyRef, createWorktree, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
5
|
+
import { runBootstrap } from '../../lib/config.js';
|
|
6
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
|
+
import { log, ui, createSpinner } from '../../lib/ui.js';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import fs from 'fs-extra';
|
|
10
|
+
export async function createCommand(options) {
|
|
11
|
+
try {
|
|
12
|
+
const repoRoot = await getRepoRoot();
|
|
13
|
+
log.info(`Repo: ${chalk.dim(repoRoot)}`);
|
|
14
|
+
// 1. Gather inputs
|
|
15
|
+
const currentBranch = await getCurrentBranch();
|
|
16
|
+
const answers = await inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: 'input',
|
|
19
|
+
name: 'name',
|
|
20
|
+
message: 'Worktree name (slug):',
|
|
21
|
+
default: options.name,
|
|
22
|
+
when: !options.name,
|
|
23
|
+
validate: (input) => input.trim().length > 0 || 'Name is required',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'input',
|
|
27
|
+
name: 'ref',
|
|
28
|
+
message: 'Base branch name:',
|
|
29
|
+
default: options.ref || currentBranch,
|
|
30
|
+
when: !options.ref,
|
|
31
|
+
validate: (input) => input.trim().length > 0 || 'Ref is required',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
type: 'list',
|
|
35
|
+
name: 'source',
|
|
36
|
+
message: 'Base on:',
|
|
37
|
+
loop: false,
|
|
38
|
+
choices: [
|
|
39
|
+
{ name: 'Remote (origin)', value: 'remote' },
|
|
40
|
+
{ name: 'Local', value: 'local' },
|
|
41
|
+
],
|
|
42
|
+
default: 'remote',
|
|
43
|
+
when: !options.ref,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
type: 'confirm',
|
|
47
|
+
name: 'bootstrap',
|
|
48
|
+
message: 'Run bootstrap? (npm install + submodules)',
|
|
49
|
+
default: true,
|
|
50
|
+
when: options.bootstrap !== false,
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
let shouldEnter = false;
|
|
54
|
+
if (!options.ref) {
|
|
55
|
+
const finalAnswer = await inquirer.prompt([{
|
|
56
|
+
type: 'confirm',
|
|
57
|
+
name: 'shouldEnter',
|
|
58
|
+
message: 'Do you want to enter the new worktree now?',
|
|
59
|
+
default: true
|
|
60
|
+
}]);
|
|
61
|
+
shouldEnter = finalAnswer.shouldEnter;
|
|
62
|
+
}
|
|
63
|
+
const name = options.name || answers.name;
|
|
64
|
+
let ref = options.ref || answers.ref;
|
|
65
|
+
// Append origin/ if remote is selected and not already present
|
|
66
|
+
if (!options.ref && answers.source === 'remote' && !ref.startsWith('origin/')) {
|
|
67
|
+
ref = `origin/${ref}`;
|
|
68
|
+
}
|
|
69
|
+
const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
|
|
70
|
+
const slug = name.replace(/\s+/g, '-');
|
|
71
|
+
const wtPath = path.join(WORKTREES_ROOT, slug);
|
|
72
|
+
// 2. Validation
|
|
73
|
+
if (!slug)
|
|
74
|
+
throw new Error('Invalid name');
|
|
75
|
+
if (!ref)
|
|
76
|
+
throw new Error('Invalid ref');
|
|
77
|
+
// 3. Execution
|
|
78
|
+
const spinner = createSpinner('Fetching...').start();
|
|
79
|
+
await fetchAll();
|
|
80
|
+
spinner.text = 'Verifying ref...';
|
|
81
|
+
const exists = await verifyRef(ref);
|
|
82
|
+
if (!exists) {
|
|
83
|
+
spinner.fail(`Ref not found: ${ref}`);
|
|
84
|
+
log.warning(`Tip: try 'origin/${ref}' or check if the branch exists.`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
spinner.text = `Creating worktree at ${ui.path(wtPath)}...`;
|
|
88
|
+
try {
|
|
89
|
+
await fs.ensureDir(path.dirname(wtPath));
|
|
90
|
+
await createWorktree(wtPath, ref);
|
|
91
|
+
spinner.succeed('Worktree created.');
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
spinner.fail('Failed to create worktree.');
|
|
95
|
+
log.error(e.message);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// 4. Bootstrap
|
|
99
|
+
if (shouldBootstrap) {
|
|
100
|
+
await runBootstrap(wtPath, repoRoot);
|
|
101
|
+
}
|
|
102
|
+
// 5. Final Output
|
|
103
|
+
log.success('Worktree ready!');
|
|
104
|
+
if (shouldEnter) {
|
|
105
|
+
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
106
|
+
log.dim('Type "exit" to return to the main terminal.');
|
|
107
|
+
const shell = process.env.SHELL || 'zsh';
|
|
108
|
+
const child = spawn(shell, [], {
|
|
109
|
+
cwd: wtPath,
|
|
110
|
+
stdio: 'inherit',
|
|
111
|
+
});
|
|
112
|
+
child.on('close', () => {
|
|
113
|
+
log.info('Exited sub-shell.');
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
log.header(`cd "${wtPath}"`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
log.error(error.message);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { listWorktrees, removeWorktree, getRepoRoot } from '../../lib/git.js';
|
|
5
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
6
|
+
import { log, createSpinner } from '../../lib/ui.js';
|
|
7
|
+
export async function deleteCommand() {
|
|
8
|
+
try {
|
|
9
|
+
const _ = await getRepoRoot();
|
|
10
|
+
const worktrees = await listWorktrees();
|
|
11
|
+
// Filter only managed worktrees
|
|
12
|
+
const managedWts = worktrees.filter(wt => wt.path.startsWith(WORKTREES_ROOT));
|
|
13
|
+
if (managedWts.length === 0) {
|
|
14
|
+
log.info('No managed worktrees found to delete.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const choices = managedWts.map(wt => ({
|
|
18
|
+
name: `${chalk.bold(path.basename(wt.path))} (${chalk.dim(wt.branch || wt.HEAD)})`,
|
|
19
|
+
value: wt.path,
|
|
20
|
+
}));
|
|
21
|
+
const { selectedPath } = await inquirer.prompt([
|
|
22
|
+
{
|
|
23
|
+
type: 'list',
|
|
24
|
+
name: 'selectedPath',
|
|
25
|
+
message: 'Select worktree to delete:',
|
|
26
|
+
choices: choices,
|
|
27
|
+
},
|
|
28
|
+
]);
|
|
29
|
+
const worktreeName = path.basename(selectedPath);
|
|
30
|
+
const { confirm } = await inquirer.prompt([
|
|
31
|
+
{
|
|
32
|
+
type: 'input',
|
|
33
|
+
name: 'confirm',
|
|
34
|
+
message: `Type "${chalk.bold(worktreeName)}" to confirm deletion:`,
|
|
35
|
+
validate: (input) => input === worktreeName || 'Incorrect name, deletion aborted.',
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
|
|
39
|
+
try {
|
|
40
|
+
await removeWorktree(selectedPath);
|
|
41
|
+
spinner.succeed(`Deleted worktree: ${worktreeName}`);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
spinner.fail(`Failed to delete ${worktreeName}`);
|
|
45
|
+
log.error(e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
log.error(error.message);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
3
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
4
|
+
import { log } from '../../lib/ui.js';
|
|
5
|
+
export async function listCommand() {
|
|
6
|
+
try {
|
|
7
|
+
const _ = await getRepoRoot(); // Verify we are in a git repo
|
|
8
|
+
const worktrees = await listWorktrees();
|
|
9
|
+
if (worktrees.length === 0) {
|
|
10
|
+
log.info('No worktrees found.');
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
console.log(chalk.bold('\n Active Worktrees:\n'));
|
|
14
|
+
// Header
|
|
15
|
+
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('BRANCH')} ${chalk.dim('PATH')}`);
|
|
16
|
+
console.log(chalk.dim(' ' + '-'.repeat(60)));
|
|
17
|
+
for (const wt of worktrees) {
|
|
18
|
+
const isManaged = wt.path.startsWith(WORKTREES_ROOT);
|
|
19
|
+
const isMain = !isManaged; // Simplification: assume main repo is not in managed dir
|
|
20
|
+
const type = isManaged ? chalk.green('MANAGED') : chalk.blue('MAIN ');
|
|
21
|
+
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
22
|
+
const displayPath = wt.path.replace(process.env.HOME || '', '~');
|
|
23
|
+
const colorPath = isManaged ? chalk.cyan(displayPath) : chalk.dim(displayPath);
|
|
24
|
+
console.log(` ${type} ${chalk.yellow(branchName.padEnd(18))} ${colorPath}`);
|
|
25
|
+
}
|
|
26
|
+
console.log('');
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
log.error(error.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { pruneWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
2
|
+
import { log, createSpinner } from '../../lib/ui.js';
|
|
3
|
+
export async function pruneCommand() {
|
|
4
|
+
try {
|
|
5
|
+
const _ = await getRepoRoot();
|
|
6
|
+
const spinner = createSpinner('Pruning stale worktrees...').start();
|
|
7
|
+
try {
|
|
8
|
+
await pruneWorktrees();
|
|
9
|
+
spinner.succeed('Worktrees pruned.');
|
|
10
|
+
}
|
|
11
|
+
catch (e) {
|
|
12
|
+
spinner.fail('Failed to prune worktrees.');
|
|
13
|
+
log.error(e.message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
log.error(error.message);
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { welcome, log } from './lib/ui.js';
|
|
4
|
+
import { listCommand } from './commands/wt/list.js';
|
|
5
|
+
import { createCommand } from './commands/wt/create.js';
|
|
6
|
+
import { createCommandNew } from './commands/wt/create-branch.js';
|
|
7
|
+
import { createCommandMulti } from './commands/wt/create-multi.js';
|
|
8
|
+
import { deleteCommand } from './commands/wt/delete.js';
|
|
9
|
+
import { bootstrapCommand } from './commands/wt/bootstrap.js';
|
|
10
|
+
import { pruneCommand } from './commands/wt/prune.js';
|
|
11
|
+
const program = new Command();
|
|
12
|
+
program
|
|
13
|
+
.name('yggtree')
|
|
14
|
+
.description('Interactive CLI for managing git worktrees and configs')
|
|
15
|
+
.version('1.0.0')
|
|
16
|
+
.action(async () => {
|
|
17
|
+
// Interactive Menu if no command is provided
|
|
18
|
+
await welcome();
|
|
19
|
+
const { action } = await inquirer.prompt([
|
|
20
|
+
{
|
|
21
|
+
type: 'list',
|
|
22
|
+
name: 'action',
|
|
23
|
+
message: 'What would you like to do?',
|
|
24
|
+
loop: false,
|
|
25
|
+
choices: [
|
|
26
|
+
{ name: '🌿 Create new worktree (Smart Branch)', value: 'create-smart' },
|
|
27
|
+
{ name: '🌳 Create multiple worktrees', value: 'create-multi' },
|
|
28
|
+
{ name: '🌱 Create new worktree (Manual Slug)', value: 'create-slug' },
|
|
29
|
+
{ name: '📋 List worktrees', value: 'list' },
|
|
30
|
+
{ name: '🗑️ Delete worktree', value: 'delete' },
|
|
31
|
+
{ name: '🚀 Bootstrap worktree', value: 'bootstrap' },
|
|
32
|
+
{ name: '🧹 Prune stale worktrees', value: 'prune' },
|
|
33
|
+
new inquirer.Separator(),
|
|
34
|
+
{ name: '🚪 Exit', value: 'exit' },
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
]);
|
|
38
|
+
switch (action) {
|
|
39
|
+
case 'create-smart':
|
|
40
|
+
await createCommandNew({ bootstrap: true });
|
|
41
|
+
break;
|
|
42
|
+
case 'create-multi':
|
|
43
|
+
await createCommandMulti({ bootstrap: true });
|
|
44
|
+
break;
|
|
45
|
+
case 'create-slug':
|
|
46
|
+
await createCommand({ bootstrap: true });
|
|
47
|
+
break;
|
|
48
|
+
case 'list':
|
|
49
|
+
await listCommand();
|
|
50
|
+
break;
|
|
51
|
+
case 'delete':
|
|
52
|
+
await deleteCommand();
|
|
53
|
+
break;
|
|
54
|
+
case 'bootstrap':
|
|
55
|
+
await bootstrapCommand();
|
|
56
|
+
break;
|
|
57
|
+
case 'prune':
|
|
58
|
+
await pruneCommand();
|
|
59
|
+
break;
|
|
60
|
+
case 'exit':
|
|
61
|
+
log.info('Bye! 👋');
|
|
62
|
+
process.exit(0);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
// --- Worktree Commands ---
|
|
66
|
+
const wt = program.command('wt').description('Manage git worktrees');
|
|
67
|
+
wt.command('list')
|
|
68
|
+
.description('List all worktrees')
|
|
69
|
+
.action(listCommand);
|
|
70
|
+
wt.command('create')
|
|
71
|
+
.description('Create a new worktree (Smart branch detection)')
|
|
72
|
+
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
73
|
+
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
74
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
75
|
+
.action(async (options) => {
|
|
76
|
+
await createCommandNew(options);
|
|
77
|
+
});
|
|
78
|
+
wt.command('create-multi')
|
|
79
|
+
.description('Create multiple worktrees (Smart branch detection)')
|
|
80
|
+
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
81
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
82
|
+
.action(async (options) => {
|
|
83
|
+
await createCommandMulti(options);
|
|
84
|
+
});
|
|
85
|
+
wt.command('create-slug')
|
|
86
|
+
.description('Create a new worktree (Manual slug/ref)')
|
|
87
|
+
.option('-n, --name <slug>', 'Worktree name (slug)')
|
|
88
|
+
.option('-r, --ref <ref>', 'Existing branch or ref')
|
|
89
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
90
|
+
.action(async (options) => {
|
|
91
|
+
await createCommand(options);
|
|
92
|
+
});
|
|
93
|
+
wt.command('delete')
|
|
94
|
+
.description('Delete a managed worktree')
|
|
95
|
+
.action(deleteCommand);
|
|
96
|
+
wt.command('bootstrap')
|
|
97
|
+
.description('Bootstrap dependencies in a worktree')
|
|
98
|
+
.action(bootstrapCommand);
|
|
99
|
+
wt.command('prune')
|
|
100
|
+
.description('Prune stale worktree information')
|
|
101
|
+
.action(pruneCommand);
|
|
102
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { log, createSpinner } from './ui.js';
|
|
5
|
+
export async function getBootstrapCommands(repoRoot) {
|
|
6
|
+
const configPath = path.join(repoRoot, 'anvil-worktree.json');
|
|
7
|
+
const cursorConfigPath = path.join(repoRoot, '.cursor', 'worktrees.json');
|
|
8
|
+
if (await fs.pathExists(configPath)) {
|
|
9
|
+
try {
|
|
10
|
+
const config = await fs.readJSON(configPath);
|
|
11
|
+
if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
|
|
12
|
+
return config['setup-worktree'];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch (e) {
|
|
16
|
+
log.warning(`Failed to parse ${configPath}.`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (await fs.pathExists(cursorConfigPath)) {
|
|
20
|
+
try {
|
|
21
|
+
const config = await fs.readJSON(cursorConfigPath);
|
|
22
|
+
if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
|
|
23
|
+
return config['setup-worktree'];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (e) {
|
|
27
|
+
log.warning(`Failed to parse ${cursorConfigPath}.`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
export async function runBootstrap(wtPath, repoRoot) {
|
|
33
|
+
const customCommands = await getBootstrapCommands(repoRoot);
|
|
34
|
+
if (customCommands) {
|
|
35
|
+
log.info('Using custom bootstrap commands from config...');
|
|
36
|
+
for (const cmd of customCommands) {
|
|
37
|
+
const spinner = createSpinner(`Running: ${cmd}`).start();
|
|
38
|
+
try {
|
|
39
|
+
// Split command and arguments properly if needed, or run via shell
|
|
40
|
+
await execa(cmd, { cwd: wtPath, shell: true });
|
|
41
|
+
spinner.succeed(`Finished: ${cmd}`);
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
spinner.fail(`Failed: ${cmd}`);
|
|
45
|
+
log.error(e.message);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
// Fallback to default behavior
|
|
51
|
+
log.info('Running default bootstrap (npm install + submodules)...');
|
|
52
|
+
// npm install
|
|
53
|
+
try {
|
|
54
|
+
await execa('npm', ['--version']);
|
|
55
|
+
const installSpinner = createSpinner('Running npm install...').start();
|
|
56
|
+
try {
|
|
57
|
+
await execa('npm', ['install'], { cwd: wtPath });
|
|
58
|
+
installSpinner.succeed('Dependencies installed.');
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
installSpinner.fail('npm install failed.');
|
|
62
|
+
log.error(e.message);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
log.warning('npm not found, skipping install.');
|
|
67
|
+
}
|
|
68
|
+
// Submodules
|
|
69
|
+
const subSpinner = createSpinner('Syncing submodules...').start();
|
|
70
|
+
try {
|
|
71
|
+
await execa('git', ['submodule', 'sync', '--recursive'], { cwd: wtPath });
|
|
72
|
+
await execa('git', ['submodule', 'update', '--init', '--recursive'], { cwd: wtPath });
|
|
73
|
+
subSpinner.succeed('Submodules synced.');
|
|
74
|
+
}
|
|
75
|
+
catch (e) {
|
|
76
|
+
subSpinner.fail('Submodule sync failed.');
|
|
77
|
+
log.error(e.message);
|
|
78
|
+
log.warning('Tip: If auth failed, try adding your key to the agent.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/dist/lib/git.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
export async function getRepoRoot() {
|
|
5
|
+
try {
|
|
6
|
+
const { stdout } = await execa('git', ['rev-parse', '--show-toplevel']);
|
|
7
|
+
return stdout.trim();
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
throw new Error('Not a git repository');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export async function getCurrentBranch() {
|
|
14
|
+
try {
|
|
15
|
+
const { stdout } = await execa('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
16
|
+
return stdout.trim();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function verifyRef(ref) {
|
|
23
|
+
try {
|
|
24
|
+
await execa('git', ['rev-parse', '--verify', ref]);
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function fetchAll() {
|
|
32
|
+
await execa('git', ['fetch', '--all', '--prune']);
|
|
33
|
+
}
|
|
34
|
+
export async function createWorktree(wtPath, ref) {
|
|
35
|
+
await fs.ensureDir(path.dirname(wtPath));
|
|
36
|
+
await execa('git', ['worktree', 'add', wtPath, ref]);
|
|
37
|
+
}
|
|
38
|
+
export async function removeWorktree(wtPath) {
|
|
39
|
+
await execa('git', ['worktree', 'remove', wtPath, '--force']);
|
|
40
|
+
}
|
|
41
|
+
export async function listWorktrees() {
|
|
42
|
+
const { stdout } = await execa('git', ['worktree', 'list', '--porcelain']);
|
|
43
|
+
const worktrees = [];
|
|
44
|
+
let currentWt = {};
|
|
45
|
+
for (const line of stdout.split('\n')) {
|
|
46
|
+
if (!line) {
|
|
47
|
+
if (currentWt.path)
|
|
48
|
+
worktrees.push(currentWt);
|
|
49
|
+
currentWt = {};
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const [key, ...rest] = line.split(' ');
|
|
53
|
+
const value = rest.join(' ');
|
|
54
|
+
if (key === 'worktree')
|
|
55
|
+
currentWt.path = value;
|
|
56
|
+
if (key === 'HEAD')
|
|
57
|
+
currentWt.HEAD = value;
|
|
58
|
+
if (key === 'branch')
|
|
59
|
+
currentWt.branch = value.replace('refs/heads/', '');
|
|
60
|
+
}
|
|
61
|
+
// Push the last one if active
|
|
62
|
+
if (currentWt.path)
|
|
63
|
+
worktrees.push(currentWt);
|
|
64
|
+
return worktrees;
|
|
65
|
+
}
|
|
66
|
+
export async function pruneWorktrees() {
|
|
67
|
+
await execa('git', ['worktree', 'prune']);
|
|
68
|
+
}
|
|
69
|
+
export async function syncSubmodules(cwd) {
|
|
70
|
+
await execa('git', ['submodule', 'sync', '--recursive'], { cwd });
|
|
71
|
+
await execa('git', ['submodule', 'update', '--init', '--recursive'], { cwd });
|
|
72
|
+
}
|
package/dist/lib/ui.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import figlet from 'figlet';
|
|
3
|
+
import gradient from 'gradient-string';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
// --- Personality & Branding ---
|
|
9
|
+
export const welcome = async () => {
|
|
10
|
+
console.log('');
|
|
11
|
+
const title = figlet.textSync('Yggdrasil', { font: 'Standard' });
|
|
12
|
+
console.log(gradient.mind.multiline(title));
|
|
13
|
+
console.log(chalk.dim(' v1.0.0 • The World Tree Worktree Assistant'));
|
|
14
|
+
console.log('');
|
|
15
|
+
};
|
|
16
|
+
// --- Logger ---
|
|
17
|
+
export const log = {
|
|
18
|
+
info: (msg) => console.log(chalk.blue('ℹ'), msg),
|
|
19
|
+
success: (msg) => console.log(chalk.green('✔'), msg),
|
|
20
|
+
warning: (msg) => console.log(chalk.yellow('⚠'), msg),
|
|
21
|
+
error: (msg) => console.log(chalk.red('✖'), msg),
|
|
22
|
+
dim: (msg) => console.log(chalk.dim(msg)),
|
|
23
|
+
header: (msg) => console.log(chalk.bold.hex('#DEADED')(`\n${msg}\n`)),
|
|
24
|
+
};
|
|
25
|
+
// --- Initial Spinners ---
|
|
26
|
+
export const createSpinner = (text) => {
|
|
27
|
+
return ora({
|
|
28
|
+
text,
|
|
29
|
+
color: 'cyan',
|
|
30
|
+
spinner: 'dots',
|
|
31
|
+
});
|
|
32
|
+
};
|
|
33
|
+
// --- Prompts helpers ---
|
|
34
|
+
// Ensure consistency in UI
|
|
35
|
+
export const ui = {
|
|
36
|
+
docLink: (url) => chalk.underline.cyan(url),
|
|
37
|
+
code: (cmd) => chalk.bgBlack.white(` ${cmd} `),
|
|
38
|
+
path: (p) => chalk.cyan(p),
|
|
39
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "yggtree",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Interactive CLI for managing git worktrees and configs",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"yggtree": "./bin/yggtree"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"bin",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"start": "node dist/index.js",
|
|
18
|
+
"dev": "tsc --watch",
|
|
19
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"cli",
|
|
23
|
+
"git",
|
|
24
|
+
"worktree",
|
|
25
|
+
"productivity"
|
|
26
|
+
],
|
|
27
|
+
"author": "Leonardo Dias",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"commander": "^12.0.0",
|
|
32
|
+
"execa": "^8.0.1",
|
|
33
|
+
"figlet": "^1.7.0",
|
|
34
|
+
"fs-extra": "^11.2.0",
|
|
35
|
+
"gradient-string": "^2.0.2",
|
|
36
|
+
"inquirer": "^9.2.14",
|
|
37
|
+
"ora": "^8.0.1",
|
|
38
|
+
"zod": "^3.22.4"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/figlet": "^1.5.8",
|
|
42
|
+
"@types/fs-extra": "^11.0.4",
|
|
43
|
+
"@types/gradient-string": "^1.1.6",
|
|
44
|
+
"@types/inquirer": "^9.0.7",
|
|
45
|
+
"@types/node": "^20.11.19",
|
|
46
|
+
"typescript": "^5.3.3"
|
|
47
|
+
}
|
|
48
|
+
}
|