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
|
@@ -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';
|
|
@@ -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();
|
|
@@ -66,14 +68,15 @@ export async function createCommandMulti(options) {
|
|
|
66
68
|
spinner.succeed(`Base ref ${chalk.cyan(baseRef)} verified.`);
|
|
67
69
|
const createdWorktrees = [];
|
|
68
70
|
// 3. Execution for each branch
|
|
71
|
+
const repoName = await getRepoName();
|
|
69
72
|
for (const branchName of branchNames) {
|
|
70
73
|
const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
|
|
71
|
-
const wtPath = path.join(WORKTREES_ROOT, slug);
|
|
74
|
+
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
72
75
|
log.header(`Processing: ${branchName}`);
|
|
73
76
|
const wtSpinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
|
|
77
|
+
// Check if target branch already exists
|
|
78
|
+
const targetBranchExists = await verifyRef(branchName);
|
|
74
79
|
try {
|
|
75
|
-
// Check if target branch already exists
|
|
76
|
-
const targetBranchExists = await verifyRef(branchName);
|
|
77
80
|
await fs.ensureDir(path.dirname(wtPath));
|
|
78
81
|
if (targetBranchExists) {
|
|
79
82
|
await execa('git', ['worktree', 'add', wtPath, branchName]);
|
|
@@ -90,7 +93,15 @@ export async function createCommandMulti(options) {
|
|
|
90
93
|
}
|
|
91
94
|
catch (error) {
|
|
92
95
|
wtSpinner.fail(`Failed to create worktree for ${branchName}.`);
|
|
93
|
-
|
|
96
|
+
const cmd = targetBranchExists
|
|
97
|
+
? `git worktree add ${wtPath} ${branchName}`
|
|
98
|
+
: `git worktree add -b ${branchName} ${wtPath} ${baseRef}`;
|
|
99
|
+
log.actionableError(error.message, cmd, wtPath, [
|
|
100
|
+
'Check if the folder already exists: ls ' + wtPath,
|
|
101
|
+
'Check if the branch is already used: git worktree list',
|
|
102
|
+
'Try pruning stale worktrees: yggtree wt prune',
|
|
103
|
+
`Run manually: ${cmd}`
|
|
104
|
+
]);
|
|
94
105
|
}
|
|
95
106
|
}
|
|
96
107
|
// 5. Final Output
|
|
@@ -103,7 +114,7 @@ export async function createCommandMulti(options) {
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
catch (error) {
|
|
106
|
-
log.
|
|
117
|
+
log.actionableError(error.message, 'yggtree wt create-multi');
|
|
107
118
|
process.exit(1);
|
|
108
119
|
}
|
|
109
120
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
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';
|
|
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,35 +41,43 @@ 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
|
-
const
|
|
79
|
+
const repoName = await getRepoName();
|
|
80
|
+
const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
|
|
72
81
|
// 2. Validation
|
|
73
82
|
if (!slug)
|
|
74
83
|
throw new Error('Invalid name');
|
|
@@ -92,14 +101,35 @@ export async function createCommand(options) {
|
|
|
92
101
|
}
|
|
93
102
|
catch (e) {
|
|
94
103
|
spinner.fail('Failed to create worktree.');
|
|
95
|
-
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
|
+
]);
|
|
96
110
|
return;
|
|
97
111
|
}
|
|
98
|
-
// 4. Bootstrap
|
|
99
112
|
if (shouldBootstrap) {
|
|
100
113
|
await runBootstrap(wtPath, repoRoot);
|
|
101
114
|
}
|
|
102
|
-
// 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
|
|
103
133
|
log.success('Worktree ready!');
|
|
104
134
|
if (shouldEnter) {
|
|
105
135
|
log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
|
|
@@ -118,7 +148,7 @@ export async function createCommand(options) {
|
|
|
118
148
|
}
|
|
119
149
|
}
|
|
120
150
|
catch (error) {
|
|
121
|
-
log.
|
|
151
|
+
log.actionableError(error.message, 'yggtree wt create');
|
|
122
152
|
process.exit(1);
|
|
123
153
|
}
|
|
124
154
|
}
|
|
@@ -14,35 +14,54 @@ 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
|
-
|
|
21
|
-
|
|
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
|
+
});
|
|
24
|
+
const { selectedPaths } = await inquirer.prompt([
|
|
22
25
|
{
|
|
23
|
-
type: '
|
|
24
|
-
name: '
|
|
25
|
-
message: 'Select
|
|
26
|
+
type: 'checkbox',
|
|
27
|
+
name: 'selectedPaths',
|
|
28
|
+
message: 'Select worktrees to delete:',
|
|
26
29
|
choices: choices,
|
|
27
30
|
},
|
|
28
31
|
]);
|
|
29
|
-
|
|
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));
|
|
30
38
|
const { confirm } = await inquirer.prompt([
|
|
31
39
|
{
|
|
32
|
-
type: '
|
|
40
|
+
type: 'confirm',
|
|
33
41
|
name: 'confirm',
|
|
34
|
-
message: `
|
|
35
|
-
|
|
42
|
+
message: `Are you sure you want to delete ${count > 1 ? `${count} worktrees` : `"${names[0]}"`}?`,
|
|
43
|
+
default: false,
|
|
36
44
|
},
|
|
37
45
|
]);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
spinner.succeed(`Deleted worktree: ${worktreeName}`);
|
|
46
|
+
if (!confirm) {
|
|
47
|
+
log.info('Deletion aborted.');
|
|
48
|
+
return;
|
|
42
49
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
+
}
|
|
46
65
|
}
|
|
47
66
|
}
|
|
48
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,5 +1,6 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { listWorktrees, getRepoRoot, isGitClean } from '../../lib/git.js';
|
|
3
4
|
import { WORKTREES_ROOT } from '../../lib/paths.js';
|
|
4
5
|
import { log } from '../../lib/ui.js';
|
|
5
6
|
export async function listCommand() {
|
|
@@ -12,21 +13,26 @@ export async function listCommand() {
|
|
|
12
13
|
}
|
|
13
14
|
console.log(chalk.bold('\n Active Worktrees:\n'));
|
|
14
15
|
// Header
|
|
15
|
-
console.log(` ${chalk.dim('TYPE')} ${chalk.dim('BRANCH')} ${chalk.dim('PATH')}`);
|
|
16
|
-
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)));
|
|
17
18
|
for (const wt of worktrees) {
|
|
18
19
|
const isManaged = wt.path.startsWith(WORKTREES_ROOT);
|
|
19
|
-
const isMain = !isManaged; // Simplification: assume main repo is not in managed dir
|
|
20
20
|
const type = isManaged ? chalk.green('MANAGED') : chalk.blue('MAIN ');
|
|
21
21
|
const branchName = wt.branch || wt.HEAD || 'detached';
|
|
22
|
-
|
|
22
|
+
let displayPath = wt.path.replace(process.env.HOME || '', '~');
|
|
23
|
+
if (isManaged) {
|
|
24
|
+
displayPath = path.relative(WORKTREES_ROOT, wt.path);
|
|
25
|
+
}
|
|
23
26
|
const colorPath = isManaged ? chalk.cyan(displayPath) : chalk.dim(displayPath);
|
|
24
|
-
|
|
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}`);
|
|
25
31
|
}
|
|
26
32
|
console.log('');
|
|
27
33
|
}
|
|
28
34
|
catch (error) {
|
|
29
|
-
log.
|
|
35
|
+
log.actionableError(error.message, 'yggtree wt list');
|
|
30
36
|
process.exit(1);
|
|
31
37
|
}
|
|
32
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) {
|