yggtree 1.0.0 โ 1.1.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/README.md +388 -39
- package/dist/commands/wt/bootstrap.js +8 -5
- package/dist/commands/wt/create-branch.js +52 -20
- package/dist/commands/wt/create-multi.js +23 -12
- package/dist/commands/wt/create.js +50 -20
- package/dist/commands/wt/delete.js +38 -19
- package/dist/commands/wt/enter.js +110 -0
- package/dist/commands/wt/exec.js +93 -0
- package/dist/commands/wt/leave.js +13 -0
- package/dist/commands/wt/list.js +13 -7
- package/dist/commands/wt/path.js +62 -0
- package/dist/commands/wt/prune.js +4 -1
- package/dist/index.js +57 -7
- package/dist/lib/config.js +44 -25
- package/dist/lib/git.js +13 -0
- package/dist/lib/paths.js +2 -2
- package/dist/lib/ui.js +14 -1
- package/dist/lib/version.js +17 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,11 +8,15 @@ import { createCommandMulti } from './commands/wt/create-multi.js';
|
|
|
8
8
|
import { deleteCommand } from './commands/wt/delete.js';
|
|
9
9
|
import { bootstrapCommand } from './commands/wt/bootstrap.js';
|
|
10
10
|
import { pruneCommand } from './commands/wt/prune.js';
|
|
11
|
+
import { execCommand } from './commands/wt/exec.js';
|
|
12
|
+
import { enterCommand } from './commands/wt/enter.js';
|
|
13
|
+
import { pathCommand } from './commands/wt/path.js';
|
|
14
|
+
import { getVersion } from './lib/version.js';
|
|
11
15
|
const program = new Command();
|
|
12
16
|
program
|
|
13
17
|
.name('yggtree')
|
|
14
18
|
.description('Interactive CLI for managing git worktrees and configs')
|
|
15
|
-
.version(
|
|
19
|
+
.version(getVersion())
|
|
16
20
|
.action(async () => {
|
|
17
21
|
// Interactive Menu if no command is provided
|
|
18
22
|
await welcome();
|
|
@@ -30,6 +34,9 @@ program
|
|
|
30
34
|
{ name: '๐๏ธ Delete worktree', value: 'delete' },
|
|
31
35
|
{ name: '๐ Bootstrap worktree', value: 'bootstrap' },
|
|
32
36
|
{ name: '๐งน Prune stale worktrees', value: 'prune' },
|
|
37
|
+
{ name: '๐ Exec command', value: 'exec' },
|
|
38
|
+
{ name: '๐ช Enter worktree', value: 'enter' },
|
|
39
|
+
{ name: '๐ Get worktree path', value: 'path' },
|
|
33
40
|
new inquirer.Separator(),
|
|
34
41
|
{ name: '๐ช Exit', value: 'exit' },
|
|
35
42
|
],
|
|
@@ -57,6 +64,15 @@ program
|
|
|
57
64
|
case 'prune':
|
|
58
65
|
await pruneCommand();
|
|
59
66
|
break;
|
|
67
|
+
case 'exec':
|
|
68
|
+
await execCommand();
|
|
69
|
+
break;
|
|
70
|
+
case 'enter':
|
|
71
|
+
await enterCommand();
|
|
72
|
+
break;
|
|
73
|
+
case 'path':
|
|
74
|
+
await pathCommand();
|
|
75
|
+
break;
|
|
60
76
|
case 'exit':
|
|
61
77
|
log.info('Bye! ๐');
|
|
62
78
|
process.exit(0);
|
|
@@ -67,28 +83,43 @@ const wt = program.command('wt').description('Manage git worktrees');
|
|
|
67
83
|
wt.command('list')
|
|
68
84
|
.description('List all worktrees')
|
|
69
85
|
.action(listCommand);
|
|
70
|
-
wt.command('create')
|
|
86
|
+
wt.command('create [branch]')
|
|
71
87
|
.description('Create a new worktree (Smart branch detection)')
|
|
72
88
|
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
73
89
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
90
|
+
.option('--source <type>', 'Base source (local or remote)')
|
|
74
91
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
75
|
-
.
|
|
76
|
-
|
|
92
|
+
.option('--enter', 'Enter sub-shell after creation')
|
|
93
|
+
.option('--no-enter', 'Skip entering sub-shell')
|
|
94
|
+
.option('--exec <command>', 'Command to execute after creation')
|
|
95
|
+
.action(async (branch, options) => {
|
|
96
|
+
await createCommandNew({
|
|
97
|
+
...options,
|
|
98
|
+
branch: branch || options.branch
|
|
99
|
+
});
|
|
77
100
|
});
|
|
78
101
|
wt.command('create-multi')
|
|
79
102
|
.description('Create multiple worktrees (Smart branch detection)')
|
|
80
103
|
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
104
|
+
.option('--source <type>', 'Base source (local or remote)')
|
|
81
105
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
82
106
|
.action(async (options) => {
|
|
83
107
|
await createCommandMulti(options);
|
|
84
108
|
});
|
|
85
|
-
wt.command('create-slug')
|
|
109
|
+
wt.command('create-slug [name] [ref]')
|
|
86
110
|
.description('Create a new worktree (Manual slug/ref)')
|
|
87
111
|
.option('-n, --name <slug>', 'Worktree name (slug)')
|
|
88
112
|
.option('-r, --ref <ref>', 'Existing branch or ref')
|
|
89
113
|
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
90
|
-
.
|
|
91
|
-
|
|
114
|
+
.option('--enter', 'Enter sub-shell after creation')
|
|
115
|
+
.option('--no-enter', 'Skip entering sub-shell')
|
|
116
|
+
.option('--exec <command>', 'Command to execute after creation')
|
|
117
|
+
.action(async (name, ref, options) => {
|
|
118
|
+
await createCommand({
|
|
119
|
+
...options,
|
|
120
|
+
name: name || options.name,
|
|
121
|
+
ref: ref || options.ref
|
|
122
|
+
});
|
|
92
123
|
});
|
|
93
124
|
wt.command('delete')
|
|
94
125
|
.description('Delete a managed worktree')
|
|
@@ -99,4 +130,23 @@ wt.command('bootstrap')
|
|
|
99
130
|
wt.command('prune')
|
|
100
131
|
.description('Prune stale worktree information')
|
|
101
132
|
.action(pruneCommand);
|
|
133
|
+
wt.command('exec')
|
|
134
|
+
.description('Execute a command in a worktree')
|
|
135
|
+
.argument('[worktree]', 'Worktree name or path')
|
|
136
|
+
.argument('[command...]', 'Command and arguments to execute')
|
|
137
|
+
.action(async (worktree, command) => {
|
|
138
|
+
await execCommand(worktree, command);
|
|
139
|
+
});
|
|
140
|
+
wt.command('enter')
|
|
141
|
+
.description('Enter a worktree sub-shell')
|
|
142
|
+
.argument('[worktree]', 'Worktree name or path')
|
|
143
|
+
.option('--exec <command>', 'Command to execute before entering')
|
|
144
|
+
.action(async (worktree, options) => {
|
|
145
|
+
await enterCommand(worktree, options);
|
|
146
|
+
});
|
|
147
|
+
wt.command('path [worktree]')
|
|
148
|
+
.description('Show the cd command for a specific worktree')
|
|
149
|
+
.action(async (worktree) => {
|
|
150
|
+
await pathCommand(worktree);
|
|
151
|
+
});
|
|
102
152
|
program.parse(process.argv);
|
package/dist/lib/config.js
CHANGED
|
@@ -2,35 +2,41 @@ import path from 'path';
|
|
|
2
2
|
import fs from 'fs-extra';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { log, createSpinner } from './ui.js';
|
|
5
|
-
export async function getBootstrapCommands(repoRoot) {
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
export async function getBootstrapCommands(repoRoot, wtPath) {
|
|
6
|
+
const searchPaths = [];
|
|
7
|
+
if (wtPath)
|
|
8
|
+
searchPaths.push(wtPath);
|
|
9
|
+
searchPaths.push(repoRoot);
|
|
10
|
+
for (const searchPath of searchPaths) {
|
|
11
|
+
const configPath = path.join(searchPath, 'yggtree-worktree.json');
|
|
12
|
+
const cursorConfigPath = path.join(searchPath, '.cursor', 'worktrees.json');
|
|
13
|
+
if (await fs.pathExists(configPath)) {
|
|
14
|
+
try {
|
|
15
|
+
const config = await fs.readJSON(configPath);
|
|
16
|
+
if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
|
|
17
|
+
return config['setup-worktree'];
|
|
18
|
+
}
|
|
13
19
|
}
|
|
14
|
-
|
|
15
|
-
|
|
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'];
|
|
20
|
+
catch (e) {
|
|
21
|
+
log.warning(`Failed to parse ${configPath}.`);
|
|
24
22
|
}
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
if (await fs.pathExists(cursorConfigPath)) {
|
|
25
|
+
try {
|
|
26
|
+
const config = await fs.readJSON(cursorConfigPath);
|
|
27
|
+
if (config['setup-worktree'] && Array.isArray(config['setup-worktree'])) {
|
|
28
|
+
return config['setup-worktree'];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch (e) {
|
|
32
|
+
log.warning(`Failed to parse ${cursorConfigPath}.`);
|
|
33
|
+
}
|
|
28
34
|
}
|
|
29
35
|
}
|
|
30
36
|
return null;
|
|
31
37
|
}
|
|
32
38
|
export async function runBootstrap(wtPath, repoRoot) {
|
|
33
|
-
const customCommands = await getBootstrapCommands(repoRoot);
|
|
39
|
+
const customCommands = await getBootstrapCommands(repoRoot, wtPath);
|
|
34
40
|
if (customCommands) {
|
|
35
41
|
log.info('Using custom bootstrap commands from config...');
|
|
36
42
|
for (const cmd of customCommands) {
|
|
@@ -42,7 +48,10 @@ export async function runBootstrap(wtPath, repoRoot) {
|
|
|
42
48
|
}
|
|
43
49
|
catch (e) {
|
|
44
50
|
spinner.fail(`Failed: ${cmd}`);
|
|
45
|
-
log.
|
|
51
|
+
log.actionableError(e.message, cmd, wtPath, [
|
|
52
|
+
`Try running the command manually: cd ${wtPath} && ${cmd}`,
|
|
53
|
+
'Check your configuration in yggtree-worktree.json'
|
|
54
|
+
]);
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
}
|
|
@@ -59,7 +68,10 @@ export async function runBootstrap(wtPath, repoRoot) {
|
|
|
59
68
|
}
|
|
60
69
|
catch (e) {
|
|
61
70
|
installSpinner.fail('npm install failed.');
|
|
62
|
-
log.
|
|
71
|
+
log.actionableError(e.message, 'npm install', wtPath, [
|
|
72
|
+
`cd ${wtPath} && npm install`,
|
|
73
|
+
'Check your network connection and package-lock.json'
|
|
74
|
+
]);
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
catch {
|
|
@@ -74,8 +86,15 @@ export async function runBootstrap(wtPath, repoRoot) {
|
|
|
74
86
|
}
|
|
75
87
|
catch (e) {
|
|
76
88
|
subSpinner.fail('Submodule sync failed.');
|
|
77
|
-
|
|
78
|
-
|
|
89
|
+
const isAuthError = /permission denied|authentication|publickey/i.test(e.message);
|
|
90
|
+
const steps = [
|
|
91
|
+
'git submodule sync --recursive',
|
|
92
|
+
'git submodule update --init --recursive'
|
|
93
|
+
];
|
|
94
|
+
if (isAuthError) {
|
|
95
|
+
steps.unshift('ssh -T git@github.com', 'ssh-add --apple-use-keychain ~/.ssh/id_ed25519');
|
|
96
|
+
}
|
|
97
|
+
log.actionableError(e.message, 'git submodule update --init --recursive', wtPath, steps);
|
|
79
98
|
}
|
|
80
99
|
}
|
|
81
100
|
}
|
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']);
|
|
@@ -70,3 +74,12 @@ export async function syncSubmodules(cwd) {
|
|
|
70
74
|
await execa('git', ['submodule', 'sync', '--recursive'], { cwd });
|
|
71
75
|
await execa('git', ['submodule', 'update', '--init', '--recursive'], { cwd });
|
|
72
76
|
}
|
|
77
|
+
export async function isGitClean(cwd) {
|
|
78
|
+
try {
|
|
79
|
+
const { stdout } = await execa('git', ['status', '--porcelain'], { cwd });
|
|
80
|
+
return stdout.trim().length === 0;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
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;
|
package/dist/lib/ui.js
CHANGED
|
@@ -4,13 +4,15 @@ import gradient from 'gradient-string';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import path from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
+
import { getVersion } from './version.js';
|
|
7
8
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
// --- Personality & Branding ---
|
|
9
10
|
export const welcome = async () => {
|
|
10
11
|
console.log('');
|
|
11
12
|
const title = figlet.textSync('Yggdrasil', { font: 'Standard' });
|
|
12
13
|
console.log(gradient.mind.multiline(title));
|
|
13
|
-
|
|
14
|
+
const version = getVersion();
|
|
15
|
+
console.log(chalk.dim(` v${version} โข The World Tree Worktree Assistant`));
|
|
14
16
|
console.log('');
|
|
15
17
|
};
|
|
16
18
|
// --- Logger ---
|
|
@@ -21,6 +23,17 @@ export const log = {
|
|
|
21
23
|
error: (msg) => console.log(chalk.red('โ'), msg),
|
|
22
24
|
dim: (msg) => console.log(chalk.dim(msg)),
|
|
23
25
|
header: (msg) => console.log(chalk.bold.hex('#DEADED')(`\n${msg}\n`)),
|
|
26
|
+
actionableError: (error, command, worktreePath, nextSteps = []) => {
|
|
27
|
+
console.log(chalk.red('\nโ Error:'), error);
|
|
28
|
+
console.log(chalk.dim(' Command:'), chalk.white(command));
|
|
29
|
+
if (worktreePath)
|
|
30
|
+
console.log(chalk.dim(' Path: '), chalk.cyan(worktreePath));
|
|
31
|
+
if (nextSteps.length > 0) {
|
|
32
|
+
console.log(chalk.bold('\n Next steps:'));
|
|
33
|
+
nextSteps.forEach(step => console.log(chalk.yellow(` - ${step}`)));
|
|
34
|
+
}
|
|
35
|
+
console.log('');
|
|
36
|
+
},
|
|
24
37
|
};
|
|
25
38
|
// --- Initial Spinners ---
|
|
26
39
|
export const createSpinner = (text) => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = path.dirname(__filename);
|
|
6
|
+
export function getVersion() {
|
|
7
|
+
try {
|
|
8
|
+
// In dist, this file is in dist/lib/version.js
|
|
9
|
+
// package.json is in the root (../../package.json)
|
|
10
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
11
|
+
const pkg = fs.readJsonSync(packageJsonPath);
|
|
12
|
+
return pkg.version || '1.0.0';
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
return '1.0.0';
|
|
16
|
+
}
|
|
17
|
+
}
|