yggtree 1.0.1 → 1.1.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 +308 -112
- package/dist/commands/wt/bootstrap.js +1 -1
- package/dist/commands/wt/create-branch.js +65 -20
- package/dist/commands/wt/create-multi.js +42 -17
- package/dist/commands/wt/create.js +47 -18
- package/dist/commands/wt/delete.js +31 -15
- 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 +8 -6
- package/dist/commands/wt/path.js +62 -0
- package/dist/commands/wt/prune.js +4 -1
- package/dist/index.js +58 -8
- package/dist/lib/config.js +44 -25
- package/dist/lib/git.js +42 -0
- package/dist/lib/ui.js +14 -1
- package/dist/lib/version.js +17 -0
- package/package.json +1 -1
|
@@ -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, getRepoName, verifyRef, fetchAll, getCurrentBranch } from '../../lib/git.js';
|
|
4
|
+
import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensureCorrectUpstream } 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';
|
|
@@ -32,7 +32,7 @@ export async function createCommandMulti(options) {
|
|
|
32
32
|
{ name: 'Local', value: 'local' },
|
|
33
33
|
],
|
|
34
34
|
default: 'remote',
|
|
35
|
-
when: !options.base,
|
|
35
|
+
when: !options.base && !options.source,
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
type: 'input',
|
|
@@ -45,15 +45,17 @@ export async function createCommandMulti(options) {
|
|
|
45
45
|
name: 'bootstrap',
|
|
46
46
|
message: 'Run bootstrap for all worktrees? (npm install + submodules)',
|
|
47
47
|
default: true,
|
|
48
|
-
when: options.bootstrap !== false,
|
|
49
|
-
}
|
|
48
|
+
when: options.bootstrap !== false && options.bootstrap !== true,
|
|
49
|
+
}
|
|
50
50
|
]);
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
const baseRefRaw = options.base || answers.base;
|
|
52
|
+
const source = options.source || answers.source;
|
|
53
|
+
let baseRef = baseRefRaw;
|
|
54
|
+
if (!options.base && source === 'remote' && !baseRef.startsWith('origin/')) {
|
|
53
55
|
baseRef = `origin/${baseRef}`;
|
|
54
56
|
}
|
|
55
57
|
const branchNames = answers.branches.split(/\s+/).filter((b) => b.length > 0);
|
|
56
|
-
const shouldBootstrap = options.bootstrap
|
|
58
|
+
const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
57
59
|
// 2. Validation of base ref
|
|
58
60
|
const spinner = createSpinner('Fetching...').start();
|
|
59
61
|
await fetchAll();
|
|
@@ -72,9 +74,9 @@ export async function createCommandMulti(options) {
|
|
|
72
74
|
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
73
75
|
log.header(`Processing: ${branchName}`);
|
|
74
76
|
const wtSpinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
|
|
77
|
+
// Check if target branch already exists
|
|
78
|
+
const targetBranchExists = await verifyRef(branchName);
|
|
75
79
|
try {
|
|
76
|
-
// Check if target branch already exists
|
|
77
|
-
const targetBranchExists = await verifyRef(branchName);
|
|
78
80
|
await fs.ensureDir(path.dirname(wtPath));
|
|
79
81
|
if (targetBranchExists) {
|
|
80
82
|
await execa('git', ['worktree', 'add', wtPath, branchName]);
|
|
@@ -82,16 +84,39 @@ export async function createCommandMulti(options) {
|
|
|
82
84
|
else {
|
|
83
85
|
await execa('git', ['worktree', 'add', '-b', branchName, wtPath, baseRef]);
|
|
84
86
|
}
|
|
85
|
-
wtSpinner.succeed(`Worktree for ${chalk.cyan(branchName)} created.`);
|
|
86
|
-
createdWorktrees.push(wtPath);
|
|
87
|
-
// 4. Bootstrap
|
|
88
|
-
if (shouldBootstrap) {
|
|
89
|
-
await runBootstrap(wtPath, repoRoot);
|
|
90
|
-
}
|
|
91
87
|
}
|
|
92
88
|
catch (error) {
|
|
93
89
|
wtSpinner.fail(`Failed to create worktree for ${branchName}.`);
|
|
94
|
-
|
|
90
|
+
const cmd = targetBranchExists
|
|
91
|
+
? `git worktree add ${wtPath} ${branchName}`
|
|
92
|
+
: `git worktree add -b ${branchName} ${wtPath} ${baseRef}`;
|
|
93
|
+
log.actionableError(error.message, cmd, wtPath, [
|
|
94
|
+
'Check if the folder already exists: ls ' + wtPath,
|
|
95
|
+
'Check if the branch is already used: git worktree list',
|
|
96
|
+
'Try pruning stale worktrees: yggtree wt prune',
|
|
97
|
+
`Run manually: ${cmd}`
|
|
98
|
+
]);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
// Strong Safety Mode: Ensure upstream is origin/<branchName> and publish
|
|
103
|
+
wtSpinner.text = `Safely publishing branch ${branchName}...`;
|
|
104
|
+
await ensureCorrectUpstream(wtPath, branchName);
|
|
105
|
+
wtSpinner.succeed(`Worktree for ${chalk.cyan(branchName)} created and published.`);
|
|
106
|
+
createdWorktrees.push(wtPath);
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
wtSpinner.fail(`Worktree for ${branchName} created, but publication failed.`);
|
|
110
|
+
log.actionableError(error.message, 'git push -u origin HEAD', wtPath, [
|
|
111
|
+
`cd ${wtPath}`,
|
|
112
|
+
'Attempt to push manually: git push -u origin HEAD',
|
|
113
|
+
'Check if the remote branch already exists or if you have push permissions'
|
|
114
|
+
]);
|
|
115
|
+
createdWorktrees.push(wtPath); // Still added to list since wt exists
|
|
116
|
+
}
|
|
117
|
+
// 4. Bootstrap
|
|
118
|
+
if (shouldBootstrap) {
|
|
119
|
+
await runBootstrap(wtPath, repoRoot);
|
|
95
120
|
}
|
|
96
121
|
}
|
|
97
122
|
// 5. Final Output
|
|
@@ -104,7 +129,7 @@ export async function createCommandMulti(options) {
|
|
|
104
129
|
}
|
|
105
130
|
}
|
|
106
131
|
catch (error) {
|
|
107
|
-
log.
|
|
132
|
+
log.actionableError(error.message, 'yggtree wt create-multi');
|
|
108
133
|
process.exit(1);
|
|
109
134
|
}
|
|
110
135
|
}
|
|
@@ -5,6 +5,7 @@ import { getRepoRoot, getRepoName, verifyRef, createWorktree, fetchAll, getCurre
|
|
|
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';
|
|
8
|
+
import { execa } from 'execa';
|
|
8
9
|
import { spawn } from 'child_process';
|
|
9
10
|
import fs from 'fs-extra';
|
|
10
11
|
export async function createCommand(options) {
|
|
@@ -40,33 +41,40 @@ export async function createCommand(options) {
|
|
|
40
41
|
{ name: 'Local', value: 'local' },
|
|
41
42
|
],
|
|
42
43
|
default: 'remote',
|
|
43
|
-
when: !options.ref,
|
|
44
|
+
when: !options.ref && !options.source,
|
|
44
45
|
},
|
|
45
46
|
{
|
|
46
47
|
type: 'confirm',
|
|
47
48
|
name: 'bootstrap',
|
|
48
49
|
message: 'Run bootstrap? (npm install + submodules)',
|
|
49
50
|
default: true,
|
|
50
|
-
when: options.bootstrap !== false,
|
|
51
|
+
when: options.bootstrap !== false && options.bootstrap !== true,
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
type: 'confirm',
|
|
55
|
+
name: 'shouldEnter',
|
|
56
|
+
message: 'Do you want to enter the new worktree now?',
|
|
57
|
+
default: true,
|
|
58
|
+
when: options.enter === undefined,
|
|
51
59
|
},
|
|
60
|
+
{
|
|
61
|
+
type: 'input',
|
|
62
|
+
name: 'exec',
|
|
63
|
+
message: 'Command to run after creation (optional):',
|
|
64
|
+
default: options.exec,
|
|
65
|
+
when: options.exec === undefined,
|
|
66
|
+
}
|
|
52
67
|
]);
|
|
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
68
|
const name = options.name || answers.name;
|
|
64
69
|
let ref = options.ref || answers.ref;
|
|
70
|
+
const source = options.source || answers.source;
|
|
71
|
+
const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
|
|
72
|
+
const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
|
|
73
|
+
const execCommandStr = options.exec || answers.exec;
|
|
65
74
|
// Append origin/ if remote is selected and not already present
|
|
66
|
-
if (!options.ref &&
|
|
75
|
+
if (!options.ref && source === 'remote' && !ref.startsWith('origin/')) {
|
|
67
76
|
ref = `origin/${ref}`;
|
|
68
77
|
}
|
|
69
|
-
const shouldBootstrap = options.bootstrap === false ? false : answers.bootstrap;
|
|
70
78
|
const slug = name.replace(/\s+/g, '-');
|
|
71
79
|
const repoName = await getRepoName();
|
|
72
80
|
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
@@ -93,14 +101,35 @@ export async function createCommand(options) {
|
|
|
93
101
|
}
|
|
94
102
|
catch (e) {
|
|
95
103
|
spinner.fail('Failed to create worktree.');
|
|
96
|
-
log.
|
|
104
|
+
log.actionableError(e.message, `git worktree add ${wtPath} ${ref}`, wtPath, [
|
|
105
|
+
'Check if the folder already exists: ls ' + wtPath,
|
|
106
|
+
'Check if the branch is already used: git worktree list',
|
|
107
|
+
'Try pruning stale worktrees: yggtree wt prune',
|
|
108
|
+
`Run manually: git worktree add ${wtPath} ${ref}`
|
|
109
|
+
]);
|
|
97
110
|
return;
|
|
98
111
|
}
|
|
99
|
-
// 4. Bootstrap
|
|
100
112
|
if (shouldBootstrap) {
|
|
101
113
|
await runBootstrap(wtPath, repoRoot);
|
|
102
114
|
}
|
|
103
|
-
// 5.
|
|
115
|
+
// 5. Exec Command
|
|
116
|
+
if (execCommandStr && execCommandStr.trim()) {
|
|
117
|
+
log.info(`Executing: ${execCommandStr} in ${ui.path(wtPath)}`);
|
|
118
|
+
try {
|
|
119
|
+
await execa(execCommandStr, {
|
|
120
|
+
cwd: wtPath,
|
|
121
|
+
stdio: 'inherit',
|
|
122
|
+
shell: true
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
log.actionableError(error.message, execCommandStr, wtPath, [
|
|
127
|
+
`cd ${wtPath} && ${execCommandStr}`,
|
|
128
|
+
'Check your command syntax and environment variables'
|
|
129
|
+
]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// 6. Final Output
|
|
104
133
|
log.success('Worktree ready!');
|
|
105
134
|
if (shouldEnter) {
|
|
106
135
|
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
@@ -119,7 +148,7 @@ export async function createCommand(options) {
|
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
catch (error) {
|
|
122
|
-
log.
|
|
151
|
+
log.actionableError(error.message, 'yggtree wt create');
|
|
123
152
|
process.exit(1);
|
|
124
153
|
}
|
|
125
154
|
}
|
|
@@ -21,31 +21,47 @@ export async function deleteCommand() {
|
|
|
21
21
|
value: wt.path,
|
|
22
22
|
};
|
|
23
23
|
});
|
|
24
|
-
const {
|
|
24
|
+
const { selectedPaths } = await inquirer.prompt([
|
|
25
25
|
{
|
|
26
|
-
type: '
|
|
27
|
-
name: '
|
|
28
|
-
message: 'Select
|
|
26
|
+
type: 'checkbox',
|
|
27
|
+
name: 'selectedPaths',
|
|
28
|
+
message: 'Select worktrees to delete:',
|
|
29
29
|
choices: choices,
|
|
30
30
|
},
|
|
31
31
|
]);
|
|
32
|
-
|
|
32
|
+
if (!selectedPaths || selectedPaths.length === 0) {
|
|
33
|
+
log.info('No worktrees selected.');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const count = selectedPaths.length;
|
|
37
|
+
const names = selectedPaths.map((p) => path.relative(WORKTREES_ROOT, p));
|
|
33
38
|
const { confirm } = await inquirer.prompt([
|
|
34
39
|
{
|
|
35
|
-
type: '
|
|
40
|
+
type: 'confirm',
|
|
36
41
|
name: 'confirm',
|
|
37
|
-
message: `
|
|
38
|
-
|
|
42
|
+
message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
|
|
43
|
+
default: false,
|
|
39
44
|
},
|
|
40
45
|
]);
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
spinner.succeed(`Deleted worktree: ${worktreeName}`);
|
|
46
|
+
if (!confirm) {
|
|
47
|
+
log.info('Deletion aborted.');
|
|
48
|
+
return;
|
|
45
49
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
for (const wtPath of selectedPaths) {
|
|
51
|
+
const worktreeName = path.relative(WORKTREES_ROOT, wtPath);
|
|
52
|
+
const spinner = createSpinner(`Deleting ${worktreeName}...`).start();
|
|
53
|
+
try {
|
|
54
|
+
await removeWorktree(wtPath);
|
|
55
|
+
spinner.succeed(`Deleted worktree: ${worktreeName}`);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
spinner.fail(`Failed to delete ${worktreeName}`);
|
|
59
|
+
log.actionableError(e.message, `git worktree remove ${wtPath} --force`, wtPath, [
|
|
60
|
+
`Try running manually: git worktree remove ${wtPath} --force`,
|
|
61
|
+
'Check if any files in the worktree are open or locked',
|
|
62
|
+
'Try running yggtree wt prune to clean up git metadata'
|
|
63
|
+
]);
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
catch (error) {
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { spawn } from 'child_process';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
6
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
7
|
+
import { log, ui } from '../../lib/ui.js';
|
|
8
|
+
export async function enterCommand(wtName, options = {}) {
|
|
9
|
+
try {
|
|
10
|
+
await getRepoRoot();
|
|
11
|
+
const worktrees = await listWorktrees();
|
|
12
|
+
if (worktrees.length === 0) {
|
|
13
|
+
log.info('No worktrees found.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
let targetWt;
|
|
17
|
+
if (wtName) {
|
|
18
|
+
// Find worktree by name (branch name, relative path, or slug/basename)
|
|
19
|
+
targetWt = worktrees.find(wt => {
|
|
20
|
+
const branchName = wt.branch || wt.HEAD || '';
|
|
21
|
+
const relativePath = path.relative(WORKTREES_ROOT, wt.path);
|
|
22
|
+
const basename = path.basename(wt.path);
|
|
23
|
+
return branchName === wtName ||
|
|
24
|
+
relativePath === wtName ||
|
|
25
|
+
wt.path === wtName ||
|
|
26
|
+
basename === wtName;
|
|
27
|
+
});
|
|
28
|
+
if (!targetWt) {
|
|
29
|
+
log.error(`Worktree "${wtName}" not found.`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Interactive Selection
|
|
35
|
+
const choices = worktrees.map(wt => {
|
|
36
|
+
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
37
|
+
const isManaged = wt.path.startsWith(WORKTREES_ROOT);
|
|
38
|
+
const displayPath = isManaged
|
|
39
|
+
? path.relative(WORKTREES_ROOT, wt.path)
|
|
40
|
+
: wt.path.replace(process.env.HOME || '', '~');
|
|
41
|
+
return {
|
|
42
|
+
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
43
|
+
value: wt
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
const { selectedWt } = await inquirer.prompt([
|
|
47
|
+
{
|
|
48
|
+
type: 'list',
|
|
49
|
+
name: 'selectedWt',
|
|
50
|
+
message: 'Select a worktree to enter:',
|
|
51
|
+
choices,
|
|
52
|
+
loop: false
|
|
53
|
+
}
|
|
54
|
+
]);
|
|
55
|
+
targetWt = selectedWt;
|
|
56
|
+
}
|
|
57
|
+
const { execCommandStr } = await inquirer.prompt([
|
|
58
|
+
{
|
|
59
|
+
type: 'input',
|
|
60
|
+
name: 'execCommandStr',
|
|
61
|
+
message: 'Command to run before entering (optional):',
|
|
62
|
+
default: options.exec,
|
|
63
|
+
when: options.exec === undefined,
|
|
64
|
+
}
|
|
65
|
+
]);
|
|
66
|
+
const finalExec = options.exec || execCommandStr;
|
|
67
|
+
if (finalExec && finalExec.trim()) {
|
|
68
|
+
log.info(`Executing: ${finalExec} in ${ui.path(targetWt?.path || '')}`);
|
|
69
|
+
try {
|
|
70
|
+
await execa(finalExec, {
|
|
71
|
+
cwd: targetWt?.path,
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
shell: true
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
log.error(`Command failed: ${error.message}`);
|
|
78
|
+
// Ask if still want to enter?
|
|
79
|
+
const { stillEnter } = await inquirer.prompt([{
|
|
80
|
+
type: 'confirm',
|
|
81
|
+
name: 'stillEnter',
|
|
82
|
+
message: 'Command failed. Do you still want to enter the sub-shell?',
|
|
83
|
+
default: true
|
|
84
|
+
}]);
|
|
85
|
+
if (!stillEnter)
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
log.info(`Spawning sub-shell in ${ui.path(targetWt?.path || '')}...`);
|
|
90
|
+
log.dim('Type "exit" to return to the main terminal.');
|
|
91
|
+
const shell = process.env.SHELL || 'zsh';
|
|
92
|
+
const child = spawn(shell, [], {
|
|
93
|
+
cwd: targetWt?.path,
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
env: {
|
|
96
|
+
...process.env,
|
|
97
|
+
YGGTREE_SHELL: 'true'
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
child.on('exit', (code) => {
|
|
101
|
+
if (code !== 0 && code !== null) {
|
|
102
|
+
// Command failed with non-zero exit code
|
|
103
|
+
}
|
|
104
|
+
log.info('Exited sub-shell.');
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
log.error(error.message);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import { execa } from 'execa';
|
|
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
|
+
export async function execCommand(wtName, commandArgs) {
|
|
8
|
+
let targetWt;
|
|
9
|
+
let command = '';
|
|
10
|
+
let args = [];
|
|
11
|
+
try {
|
|
12
|
+
await getRepoRoot();
|
|
13
|
+
const worktrees = await listWorktrees();
|
|
14
|
+
if (worktrees.length === 0) {
|
|
15
|
+
log.info('No worktrees found.');
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (wtName) {
|
|
19
|
+
// Find worktree by name (branch name, relative path, or slug/basename)
|
|
20
|
+
targetWt = worktrees.find(wt => {
|
|
21
|
+
const branchName = wt.branch || wt.HEAD || '';
|
|
22
|
+
const relativePath = path.relative(WORKTREES_ROOT, wt.path);
|
|
23
|
+
const basename = path.basename(wt.path);
|
|
24
|
+
return branchName === wtName ||
|
|
25
|
+
relativePath === wtName ||
|
|
26
|
+
wt.path === wtName ||
|
|
27
|
+
basename === wtName;
|
|
28
|
+
});
|
|
29
|
+
if (!targetWt) {
|
|
30
|
+
log.error(`Worktree "${wtName}" not found.`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
// Interactive Selection
|
|
36
|
+
const choices = worktrees.map(wt => {
|
|
37
|
+
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
38
|
+
const isManaged = wt.path.startsWith(WORKTREES_ROOT);
|
|
39
|
+
const displayPath = isManaged
|
|
40
|
+
? path.relative(WORKTREES_ROOT, wt.path)
|
|
41
|
+
: wt.path.replace(process.env.HOME || '', '~');
|
|
42
|
+
return {
|
|
43
|
+
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
44
|
+
value: wt
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
const { selectedWt } = await inquirer.prompt([
|
|
48
|
+
{
|
|
49
|
+
type: 'list',
|
|
50
|
+
name: 'selectedWt',
|
|
51
|
+
message: 'Select a worktree:',
|
|
52
|
+
choices,
|
|
53
|
+
loop: false
|
|
54
|
+
}
|
|
55
|
+
]);
|
|
56
|
+
targetWt = selectedWt;
|
|
57
|
+
}
|
|
58
|
+
if (commandArgs && commandArgs.length > 0) {
|
|
59
|
+
command = commandArgs[0];
|
|
60
|
+
args = commandArgs.slice(1);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
const { inputCommand } = await inquirer.prompt([
|
|
64
|
+
{
|
|
65
|
+
type: 'input',
|
|
66
|
+
name: 'inputCommand',
|
|
67
|
+
message: `Enter command to run in ${targetWt?.path}:`,
|
|
68
|
+
validate: (input) => input.trim().length > 0 || 'Command cannot be empty'
|
|
69
|
+
}
|
|
70
|
+
]);
|
|
71
|
+
// Basic shell-like parsing for the interactive input
|
|
72
|
+
const parts = inputCommand.trim().split(/\s+/);
|
|
73
|
+
command = parts[0];
|
|
74
|
+
args = parts.slice(1);
|
|
75
|
+
}
|
|
76
|
+
log.info(`Executing: ${command} ${args.join(' ')} in ${targetWt?.path}`);
|
|
77
|
+
await execa(command, args, {
|
|
78
|
+
cwd: targetWt?.path,
|
|
79
|
+
stdio: 'inherit',
|
|
80
|
+
shell: false
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error.exitCode !== undefined) {
|
|
85
|
+
// Command failed, but it already printed its error to inherited stdio
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
log.actionableError(error.message, `${command} ${args.join(' ')}`, targetWt?.path, [
|
|
89
|
+
`cd ${targetWt?.path} && ${command} ${args.join(' ')}`,
|
|
90
|
+
'Check if the command exists and is in your PATH'
|
|
91
|
+
]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { log } from '../../lib/ui.js';
|
|
2
|
+
export async function leaveCommand() {
|
|
3
|
+
if (process.env.YGGTREE_SHELL === 'true') {
|
|
4
|
+
log.info('Leaving worktree sub-shell...');
|
|
5
|
+
// SIGHUP is standard for closing shells
|
|
6
|
+
process.kill(process.ppid, 'SIGHUP');
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
log.warning('You are not in a yggtree sub-shell.');
|
|
10
|
+
log.dim('Try using "yggtree wt enter" first.');
|
|
11
|
+
process.exit(0);
|
|
12
|
+
}
|
|
13
|
+
}
|
package/dist/commands/wt/list.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
3
|
+
import { listWorktrees, getRepoRoot, isGitClean } from '../../lib/git.js';
|
|
4
4
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
5
5
|
import { log } from '../../lib/ui.js';
|
|
6
6
|
export async function listCommand() {
|
|
@@ -13,11 +13,10 @@ export async function listCommand() {
|
|
|
13
13
|
}
|
|
14
14
|
console.log(chalk.bold('\n Active Worktrees:\n'));
|
|
15
15
|
// Header
|
|
16
|
-
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('BRANCH')} ${chalk.dim('PATH')}`);
|
|
17
|
-
console.log(chalk.dim(' ' + '-'.repeat(
|
|
16
|
+
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('STATE')} ${chalk.dim('BRANCH')} ${chalk.dim('PATH')}`);
|
|
17
|
+
console.log(chalk.dim(' ' + '-'.repeat(75)));
|
|
18
18
|
for (const wt of worktrees) {
|
|
19
19
|
const isManaged = wt.path.startsWith(WORKTREES_ROOT);
|
|
20
|
-
const isMain = !isManaged; // Simplification: assume main repo is not in managed dir
|
|
21
20
|
const type = isManaged ? chalk.green('MANAGED') : chalk.blue('MAIN ');
|
|
22
21
|
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
23
22
|
let displayPath = wt.path.replace(process.env.HOME || '', '~');
|
|
@@ -25,12 +24,15 @@ export async function listCommand() {
|
|
|
25
24
|
displayPath = path.relative(WORKTREES_ROOT, wt.path);
|
|
26
25
|
}
|
|
27
26
|
const colorPath = isManaged ? chalk.cyan(displayPath) : chalk.dim(displayPath);
|
|
28
|
-
|
|
27
|
+
const isClean = await isGitClean(wt.path);
|
|
28
|
+
const stateLabel = (isClean ? 'clean' : 'dirty').padEnd(8);
|
|
29
|
+
const stateText = isClean ? chalk.green(stateLabel) : chalk.yellow(stateLabel);
|
|
30
|
+
console.log(` ${type} ${stateText} ${chalk.yellow(branchName.padEnd(18))} ${colorPath}`);
|
|
29
31
|
}
|
|
30
32
|
console.log('');
|
|
31
33
|
}
|
|
32
34
|
catch (error) {
|
|
33
|
-
log.
|
|
35
|
+
log.actionableError(error.message, 'yggtree wt list');
|
|
34
36
|
process.exit(1);
|
|
35
37
|
}
|
|
36
38
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import inquirer from 'inquirer';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { listWorktrees, getRepoRoot } from '../../lib/git.js';
|
|
4
|
+
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
5
|
+
import { log } from '../../lib/ui.js';
|
|
6
|
+
export async function pathCommand(wtName) {
|
|
7
|
+
try {
|
|
8
|
+
await getRepoRoot();
|
|
9
|
+
const worktrees = await listWorktrees();
|
|
10
|
+
if (worktrees.length === 0) {
|
|
11
|
+
log.info('No worktrees found.');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
let targetWt;
|
|
15
|
+
if (wtName) {
|
|
16
|
+
// Find worktree by name (branch name, relative path, or slug/basename)
|
|
17
|
+
targetWt = worktrees.find(wt => {
|
|
18
|
+
const branchName = wt.branch || wt.HEAD || '';
|
|
19
|
+
const relativePath = path.relative(WORKTREES_ROOT, wt.path);
|
|
20
|
+
const basename = path.basename(wt.path);
|
|
21
|
+
return branchName === wtName ||
|
|
22
|
+
relativePath === wtName ||
|
|
23
|
+
wt.path === wtName ||
|
|
24
|
+
basename === wtName;
|
|
25
|
+
});
|
|
26
|
+
if (!targetWt) {
|
|
27
|
+
log.error(`Worktree "${wtName}" not found.`);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// Interactive Selection
|
|
33
|
+
const choices = worktrees.map(wt => {
|
|
34
|
+
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
35
|
+
const isManaged = wt.path.startsWith(WORKTREES_ROOT);
|
|
36
|
+
const displayPath = isManaged
|
|
37
|
+
? path.relative(WORKTREES_ROOT, wt.path)
|
|
38
|
+
: wt.path.replace(process.env.HOME || '', '~');
|
|
39
|
+
return {
|
|
40
|
+
name: `${branchName.padEnd(20)} ${displayPath}`,
|
|
41
|
+
value: wt
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
const { selectedWt } = await inquirer.prompt([
|
|
45
|
+
{
|
|
46
|
+
type: 'list',
|
|
47
|
+
name: 'selectedWt',
|
|
48
|
+
message: 'Select a worktree to get path:',
|
|
49
|
+
choices,
|
|
50
|
+
loop: false
|
|
51
|
+
}
|
|
52
|
+
]);
|
|
53
|
+
targetWt = selectedWt;
|
|
54
|
+
}
|
|
55
|
+
if (targetWt) {
|
|
56
|
+
console.log(`cd "${targetWt.path}"`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
log.error(error.message);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -10,7 +10,10 @@ export async function pruneCommand() {
|
|
|
10
10
|
}
|
|
11
11
|
catch (e) {
|
|
12
12
|
spinner.fail('Failed to prune worktrees.');
|
|
13
|
-
log.
|
|
13
|
+
log.actionableError(e.message, 'git worktree prune', undefined, [
|
|
14
|
+
'Try running manually: git worktree prune',
|
|
15
|
+
'Check if any worktree folders were deleted manually without using git'
|
|
16
|
+
]);
|
|
14
17
|
}
|
|
15
18
|
}
|
|
16
19
|
catch (error) {
|