yggtree 1.4.1 → 1.4.3
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 +131 -87
- package/dist/commands/wt/create-branch.js +21 -35
- package/dist/commands/wt/create-multi.js +11 -2
- package/dist/commands/wt/create-sandbox.js +21 -35
- package/dist/commands/wt/create.js +109 -58
- package/dist/commands/wt/enter.js +3 -2
- package/dist/commands/wt/list.js +23 -6
- package/dist/commands/wt/open.js +207 -53
- package/dist/index.js +133 -111
- package/dist/lib/env-files.js +72 -0
- package/dist/lib/git.js +89 -6
- package/dist/lib/registry.js +81 -0
- package/dist/lib/repo-context.js +46 -0
- package/dist/lib/update-check.js +55 -0
- package/dist/lib/worktree.js +12 -0
- package/package.json +12 -3
package/dist/commands/wt/open.js
CHANGED
|
@@ -9,20 +9,26 @@ import { log, ui } from '../../lib/ui.js';
|
|
|
9
9
|
import { ensureAutocompletePrompt } from '../../lib/prompt.js';
|
|
10
10
|
import { detectWorktreeType, findWorktreeByName, formatWorktreeDisplayPath, formatWorktreeType, getWorktreeBranchName, } from '../../lib/worktree.js';
|
|
11
11
|
import { enterCommand } from './enter.js';
|
|
12
|
-
const OPEN_TOOL_CANDIDATES = [
|
|
13
|
-
{ id: 'cursor', name: 'Cursor', command: 'cursor', kind: '
|
|
14
|
-
{ id: 'code', name: 'VS Code', command: 'code', kind: '
|
|
15
|
-
{ id: 'windsurf', name: 'Windsurf', command: 'windsurf', kind: '
|
|
16
|
-
{ id: 'zed', name: 'Zed', command: 'zed', kind: '
|
|
17
|
-
{ id: '
|
|
18
|
-
{ id: '
|
|
19
|
-
{ id: '
|
|
20
|
-
{
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
12
|
+
export const OPEN_TOOL_CANDIDATES = [
|
|
13
|
+
{ id: 'cursor', name: 'Cursor', command: 'cursor', kind: 'editor', aliases: ['cursor'] },
|
|
14
|
+
{ id: 'code', name: 'VS Code', command: 'code', kind: 'editor', aliases: ['vscode', 'code'] },
|
|
15
|
+
{ id: 'windsurf', name: 'Windsurf', command: 'windsurf', kind: 'editor', aliases: ['windsurf'] },
|
|
16
|
+
{ id: 'zed', name: 'Zed', command: 'zed', kind: 'editor', aliases: ['zed'] },
|
|
17
|
+
{ id: 'idea', name: 'IntelliJ IDEA', command: 'idea', kind: 'editor', aliases: ['idea', 'intellij'] },
|
|
18
|
+
{ id: 'webstorm', name: 'WebStorm', command: 'webstorm', kind: 'editor', aliases: ['webstorm'] },
|
|
19
|
+
{ id: 'subl', name: 'Sublime Text', command: 'subl', kind: 'editor', aliases: ['subl', 'sublime'] },
|
|
20
|
+
{
|
|
21
|
+
id: 'codex-app',
|
|
22
|
+
name: 'Codex App',
|
|
23
|
+
command: 'codex-app',
|
|
24
|
+
kind: 'app',
|
|
25
|
+
aliases: ['codex', 'codex-app'],
|
|
26
|
+
bundleId: 'com.openai.codex',
|
|
27
|
+
},
|
|
25
28
|
];
|
|
29
|
+
const OTHER_COMMAND_ACTION = '__other_command__';
|
|
30
|
+
const NO_OPEN_ACTION = '__no_open__';
|
|
31
|
+
const yggtreeAccent = chalk.hex('#DEADED');
|
|
26
32
|
function truncateEnd(value, maxLen) {
|
|
27
33
|
if (maxLen <= 0)
|
|
28
34
|
return '';
|
|
@@ -52,6 +58,29 @@ function formatWorktreeChoiceLabel(type, branchName, displayPath, terminalColumn
|
|
|
52
58
|
const typeText = formatWorktreeType(type);
|
|
53
59
|
return `${typeText} ${chalk.yellow(branchText)} ${chalk.cyan(pathText)}`;
|
|
54
60
|
}
|
|
61
|
+
function openRail() {
|
|
62
|
+
return yggtreeAccent('│');
|
|
63
|
+
}
|
|
64
|
+
function openSectionLabel(label) {
|
|
65
|
+
return `${yggtreeAccent('┌')} ${chalk.bold(label)}`;
|
|
66
|
+
}
|
|
67
|
+
function formatOpenColumns(name, command, detail) {
|
|
68
|
+
const paddedName = name.padEnd(16);
|
|
69
|
+
const paddedCommand = command.padEnd(13);
|
|
70
|
+
const row = `${openRail()} ${chalk.bold(paddedName)} ${chalk.cyan(paddedCommand)}`;
|
|
71
|
+
return detail ? `${row} ${chalk.dim(detail)}` : row;
|
|
72
|
+
}
|
|
73
|
+
export function formatOpenToolChoice(tool) {
|
|
74
|
+
return formatOpenColumns(tool.name, tool.command, '');
|
|
75
|
+
}
|
|
76
|
+
export function formatOpenSpecialChoice(value, allowShellAction) {
|
|
77
|
+
if (value === OTHER_COMMAND_ACTION) {
|
|
78
|
+
return formatOpenColumns('Other command', 'custom', 'run first, then stay in shell');
|
|
79
|
+
}
|
|
80
|
+
return allowShellAction
|
|
81
|
+
? formatOpenColumns('Nothing', 'skip', 'just enter the shell')
|
|
82
|
+
: formatOpenColumns('Do not open', 'skip', 'return after selection');
|
|
83
|
+
}
|
|
55
84
|
export async function commandExists(command) {
|
|
56
85
|
if (!command)
|
|
57
86
|
return false;
|
|
@@ -80,10 +109,29 @@ export async function commandExists(command) {
|
|
|
80
109
|
}
|
|
81
110
|
return false;
|
|
82
111
|
}
|
|
112
|
+
export async function macOSAppBundleExists(bundleId) {
|
|
113
|
+
if (process.platform !== 'darwin')
|
|
114
|
+
return false;
|
|
115
|
+
try {
|
|
116
|
+
const result = await execa('/usr/bin/mdfind', [`kMDItemCFBundleIdentifier == '${bundleId}'`]);
|
|
117
|
+
if (result.stdout.trim().length > 0)
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
// Fall back to common application roots when Spotlight is unavailable.
|
|
122
|
+
}
|
|
123
|
+
const commonAppPaths = [
|
|
124
|
+
'/Applications/Codex.app',
|
|
125
|
+
path.join(process.env.HOME || '', 'Applications', 'Codex.app'),
|
|
126
|
+
];
|
|
127
|
+
return commonAppPaths.some(appPath => fs.existsSync(appPath));
|
|
128
|
+
}
|
|
83
129
|
export async function detectInstalledOpenTools() {
|
|
84
130
|
const checks = await Promise.all(OPEN_TOOL_CANDIDATES.map(async (tool) => ({
|
|
85
131
|
tool,
|
|
86
|
-
exists:
|
|
132
|
+
exists: tool.bundleId
|
|
133
|
+
? await macOSAppBundleExists(tool.bundleId)
|
|
134
|
+
: await commandExists(tool.command),
|
|
87
135
|
})));
|
|
88
136
|
return checks
|
|
89
137
|
.filter(check => check.exists)
|
|
@@ -98,35 +146,39 @@ export function resolveOpenToolOption(input, installed) {
|
|
|
98
146
|
tool.command.toLowerCase() === normalized ||
|
|
99
147
|
tool.aliases?.some(alias => alias.toLowerCase() === normalized));
|
|
100
148
|
}
|
|
101
|
-
export function
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
149
|
+
export async function resolveOpenToolAction(input, installed) {
|
|
150
|
+
let chosenTool = resolveOpenToolOption(input, installed);
|
|
151
|
+
if (!chosenTool && await commandExists(input)) {
|
|
152
|
+
chosenTool = {
|
|
153
|
+
id: 'custom',
|
|
154
|
+
name: input,
|
|
155
|
+
command: input,
|
|
156
|
+
kind: 'editor',
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return chosenTool ? { type: 'tool', tool: chosenTool } : undefined;
|
|
106
160
|
}
|
|
107
161
|
export async function promptOpenToolSelection(installedTools, message = 'Select tool to open:') {
|
|
108
162
|
const ideChoices = installedTools
|
|
109
|
-
.filter(tool => tool.kind === '
|
|
163
|
+
.filter(tool => tool.kind === 'editor')
|
|
110
164
|
.map(tool => ({
|
|
111
|
-
name:
|
|
165
|
+
name: formatOpenToolChoice(tool),
|
|
112
166
|
value: tool,
|
|
113
167
|
}));
|
|
114
|
-
const
|
|
115
|
-
.filter(tool => tool.kind === '
|
|
168
|
+
const appChoices = installedTools
|
|
169
|
+
.filter(tool => tool.kind === 'app')
|
|
116
170
|
.map(tool => ({
|
|
117
|
-
name:
|
|
171
|
+
name: formatOpenToolChoice(tool),
|
|
118
172
|
value: tool,
|
|
119
173
|
}));
|
|
120
174
|
const choices = [];
|
|
121
175
|
if (ideChoices.length > 0) {
|
|
122
|
-
choices.push(new inquirer.Separator(
|
|
176
|
+
choices.push(new inquirer.Separator(openSectionLabel('Editors & IDEs')));
|
|
123
177
|
choices.push(...ideChoices);
|
|
124
178
|
}
|
|
125
|
-
if (
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
choices.push(new inquirer.Separator(chalk.dim('── Agent CLIs ──')));
|
|
129
|
-
choices.push(...agentChoices);
|
|
179
|
+
if (appChoices.length > 0) {
|
|
180
|
+
choices.push(new inquirer.Separator(openSectionLabel('Apps')));
|
|
181
|
+
choices.push(...appChoices);
|
|
130
182
|
}
|
|
131
183
|
const { selectedTool } = await inquirer.prompt([
|
|
132
184
|
{
|
|
@@ -140,16 +192,121 @@ export async function promptOpenToolSelection(installedTools, message = 'Select
|
|
|
140
192
|
]);
|
|
141
193
|
return selectedTool;
|
|
142
194
|
}
|
|
143
|
-
export
|
|
144
|
-
if (
|
|
145
|
-
|
|
146
|
-
|
|
195
|
+
export function buildOpenActionsFromSelection(selectedValues, installedTools, otherCommand) {
|
|
196
|
+
if (selectedValues.includes(NO_OPEN_ACTION))
|
|
197
|
+
return [];
|
|
198
|
+
const actions = selectedValues
|
|
199
|
+
.filter(value => value.startsWith('tool:'))
|
|
200
|
+
.map(value => installedTools.find(tool => `tool:${tool.id}` === value))
|
|
201
|
+
.filter((tool) => Boolean(tool))
|
|
202
|
+
.map(tool => ({ type: 'tool', tool }));
|
|
203
|
+
if (selectedValues.includes(OTHER_COMMAND_ACTION) && otherCommand?.trim()) {
|
|
204
|
+
actions.push({ type: 'other-command', command: otherCommand.trim() });
|
|
205
|
+
}
|
|
206
|
+
return actions;
|
|
207
|
+
}
|
|
208
|
+
export function validateOpenActionSelection(selectedValues) {
|
|
209
|
+
if (selectedValues.includes(NO_OPEN_ACTION) && selectedValues.length > 1) {
|
|
210
|
+
return 'Choose either "Nothing" or one or more actions, not both.';
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
export async function promptOpenActions(installedTools, options = {}) {
|
|
215
|
+
const allowShellAction = options.allowShellAction ?? true;
|
|
216
|
+
const choices = [];
|
|
217
|
+
if (installedTools.length > 0) {
|
|
218
|
+
const editorTools = installedTools.filter(tool => tool.kind === 'editor');
|
|
219
|
+
const appTools = installedTools.filter(tool => tool.kind === 'app');
|
|
220
|
+
if (editorTools.length > 0) {
|
|
221
|
+
choices.push(new inquirer.Separator(openSectionLabel('Editors & IDEs')));
|
|
222
|
+
choices.push(...editorTools.map(tool => ({
|
|
223
|
+
name: formatOpenToolChoice(tool),
|
|
224
|
+
value: `tool:${tool.id}`,
|
|
225
|
+
})));
|
|
226
|
+
}
|
|
227
|
+
if (appTools.length > 0) {
|
|
228
|
+
choices.push(new inquirer.Separator(openSectionLabel('Apps')));
|
|
229
|
+
choices.push(...appTools.map(tool => ({
|
|
230
|
+
name: formatOpenToolChoice(tool),
|
|
231
|
+
value: `tool:${tool.id}`,
|
|
232
|
+
})));
|
|
233
|
+
}
|
|
147
234
|
}
|
|
148
|
-
|
|
235
|
+
if (allowShellAction) {
|
|
236
|
+
if (choices.length > 0)
|
|
237
|
+
choices.push(new inquirer.Separator(' '));
|
|
238
|
+
choices.push(new inquirer.Separator(openSectionLabel('Shell')));
|
|
239
|
+
choices.push({
|
|
240
|
+
name: formatOpenSpecialChoice(OTHER_COMMAND_ACTION, allowShellAction),
|
|
241
|
+
value: OTHER_COMMAND_ACTION,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (choices.length > 0)
|
|
245
|
+
choices.push(new inquirer.Separator(' '));
|
|
246
|
+
choices.push({
|
|
247
|
+
name: formatOpenSpecialChoice(NO_OPEN_ACTION, allowShellAction),
|
|
248
|
+
value: NO_OPEN_ACTION,
|
|
249
|
+
});
|
|
250
|
+
const { selectedValues } = await inquirer.prompt([
|
|
251
|
+
{
|
|
252
|
+
type: 'checkbox',
|
|
253
|
+
name: 'selectedValues',
|
|
254
|
+
message: options.message || 'Open anything before entering the shell?',
|
|
255
|
+
choices,
|
|
256
|
+
loop: false,
|
|
257
|
+
pageSize: 10,
|
|
258
|
+
validate: validateOpenActionSelection,
|
|
259
|
+
},
|
|
260
|
+
]);
|
|
261
|
+
let otherCommand;
|
|
262
|
+
if (selectedValues.includes(OTHER_COMMAND_ACTION)) {
|
|
263
|
+
const answer = await inquirer.prompt([
|
|
264
|
+
{
|
|
265
|
+
type: 'input',
|
|
266
|
+
name: 'otherCommand',
|
|
267
|
+
message: 'Command to run in the worktree shell:',
|
|
268
|
+
validate: (input) => input.trim().length > 0 || 'Command is required',
|
|
269
|
+
},
|
|
270
|
+
]);
|
|
271
|
+
otherCommand = answer.otherCommand;
|
|
272
|
+
}
|
|
273
|
+
return buildOpenActionsFromSelection(selectedValues, installedTools, otherCommand);
|
|
274
|
+
}
|
|
275
|
+
export async function launchOpenTool(tool, wtPath) {
|
|
276
|
+
const { executable, args } = buildOpenToolLaunchCommand(tool, wtPath);
|
|
277
|
+
await execa(executable, args, {
|
|
149
278
|
cwd: wtPath,
|
|
150
279
|
stdio: 'ignore',
|
|
151
280
|
});
|
|
152
281
|
}
|
|
282
|
+
export function buildOpenToolLaunchCommand(tool, wtPath) {
|
|
283
|
+
if (tool.bundleId) {
|
|
284
|
+
return {
|
|
285
|
+
executable: '/usr/bin/open',
|
|
286
|
+
args: ['-b', tool.bundleId, wtPath],
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
executable: tool.command,
|
|
291
|
+
args: [wtPath],
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
export async function runOpenActions(actions, wtPath, options = {}) {
|
|
295
|
+
for (const action of actions) {
|
|
296
|
+
if (action.type === 'tool' && action.tool) {
|
|
297
|
+
log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(action.tool.name)}...`);
|
|
298
|
+
await launchOpenTool(action.tool, wtPath);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const shellAction = actions.find(action => action.type === 'other-command');
|
|
302
|
+
if (shellAction?.command) {
|
|
303
|
+
await enterCommand(wtPath, { exec: shellAction.command });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (options.enter !== false) {
|
|
307
|
+
await enterCommand(wtPath);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
153
310
|
export async function openCommand(wtName, options = {}) {
|
|
154
311
|
try {
|
|
155
312
|
await getRepoRoot();
|
|
@@ -208,36 +365,33 @@ export async function openCommand(wtName, options = {}) {
|
|
|
208
365
|
return;
|
|
209
366
|
}
|
|
210
367
|
const installedTools = await detectInstalledOpenTools();
|
|
211
|
-
let chosenTool;
|
|
212
368
|
const requestedTool = options.tool;
|
|
369
|
+
const shouldEnter = options.enter === true;
|
|
213
370
|
if (requestedTool) {
|
|
214
|
-
|
|
215
|
-
if (!
|
|
216
|
-
chosenTool = {
|
|
217
|
-
id: 'custom',
|
|
218
|
-
name: requestedTool,
|
|
219
|
-
command: requestedTool,
|
|
220
|
-
kind: 'ide',
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
if (!chosenTool) {
|
|
371
|
+
const toolAction = await resolveOpenToolAction(requestedTool, installedTools);
|
|
372
|
+
if (!toolAction) {
|
|
224
373
|
const available = installedTools.map(tool => tool.command).join(', ') || 'none detected';
|
|
225
374
|
log.error(`Tool "${requestedTool}" not found.`);
|
|
226
375
|
log.dim(`Detected tool commands: ${available}`);
|
|
227
376
|
return;
|
|
228
377
|
}
|
|
378
|
+
await runOpenActions([toolAction], targetWt.path, { enter: shouldEnter });
|
|
379
|
+
log.success('Worktree opened.');
|
|
380
|
+
return;
|
|
229
381
|
}
|
|
230
382
|
else {
|
|
231
|
-
if (installedTools.length === 0) {
|
|
232
|
-
log.error('No supported
|
|
233
|
-
log.dim('Try: yggtree
|
|
383
|
+
if (installedTools.length === 0 && !shouldEnter) {
|
|
384
|
+
log.error('No supported editor command or app was detected.');
|
|
385
|
+
log.dim('Try: yggtree open --tool <command> (e.g. --tool cursor or --tool codex-app), or yggtree open --enter');
|
|
234
386
|
return;
|
|
235
387
|
}
|
|
236
|
-
|
|
388
|
+
const actions = await promptOpenActions(installedTools, {
|
|
389
|
+
allowShellAction: shouldEnter,
|
|
390
|
+
message: shouldEnter ? 'Open anything before entering the shell?' : 'Open anything?',
|
|
391
|
+
});
|
|
392
|
+
await runOpenActions(actions, targetWt.path, { enter: shouldEnter });
|
|
237
393
|
}
|
|
238
|
-
log.
|
|
239
|
-
await launchOpenTool(chosenTool, targetWt.path);
|
|
240
|
-
log.success(isAgentTool(chosenTool) ? 'Agent launched.' : 'IDE launched.');
|
|
394
|
+
log.success('Worktree opened.');
|
|
241
395
|
}
|
|
242
396
|
catch (error) {
|
|
243
397
|
log.error(error.message);
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
1
|
+
import { Command, Option } from 'commander';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { welcome, log } from './lib/ui.js';
|
|
@@ -11,23 +11,148 @@ import { deleteCommand } from './commands/wt/delete.js';
|
|
|
11
11
|
import { bootstrapCommand } from './commands/wt/bootstrap.js';
|
|
12
12
|
import { pruneCommand } from './commands/wt/prune.js';
|
|
13
13
|
import { execCommand } from './commands/wt/exec.js';
|
|
14
|
-
import { enterCommand } from './commands/wt/enter.js';
|
|
15
14
|
import { pathCommand } from './commands/wt/path.js';
|
|
16
15
|
import { openCommand } from './commands/wt/open.js';
|
|
17
16
|
import { applyCommand } from './commands/wt/apply.js';
|
|
18
17
|
import { unapplyCommand } from './commands/wt/unapply.js';
|
|
19
18
|
import { getVersion } from './lib/version.js';
|
|
19
|
+
import { notifyIfUpdateAvailable } from './lib/update-check.js';
|
|
20
20
|
import { findSandboxRoot } from './lib/sandbox.js';
|
|
21
21
|
import { bifrostCommand } from './commands/bifrost.js';
|
|
22
22
|
import { thorCommand } from './commands/thor.js';
|
|
23
23
|
const program = new Command();
|
|
24
|
+
const argv = process.argv.map((arg) => arg === '-v' || arg === '—version' ? '--version' : arg);
|
|
25
|
+
function registerWorktreeCommands(parent) {
|
|
26
|
+
parent.command('list')
|
|
27
|
+
.description('List all repo-linked worktrees')
|
|
28
|
+
.option('--open', 'Open a worktree in an IDE/agent tool instead of listing')
|
|
29
|
+
.action(async (options) => {
|
|
30
|
+
if (options.open) {
|
|
31
|
+
await openCommand();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await listCommand();
|
|
35
|
+
});
|
|
36
|
+
parent.command('create [branch]')
|
|
37
|
+
.description('Create a new worktree (Smart branch detection)')
|
|
38
|
+
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
39
|
+
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
40
|
+
.option('--source <type>', 'Base source (local or remote)')
|
|
41
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
42
|
+
.option('--open', 'Open an editor after creation')
|
|
43
|
+
.option('--no-open', 'Skip opening an editor after creation')
|
|
44
|
+
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
45
|
+
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
46
|
+
.option('--exec <command>', 'Command to execute after creation')
|
|
47
|
+
.action(async (branch, options) => {
|
|
48
|
+
await createCommandNew({
|
|
49
|
+
...options,
|
|
50
|
+
branch: branch || options.branch
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
parent.command('create-multi')
|
|
54
|
+
.description('Create multiple worktrees (Smart branch detection)')
|
|
55
|
+
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
56
|
+
.option('--source <type>', 'Base source (local or remote)')
|
|
57
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
await createCommandMulti(options);
|
|
60
|
+
});
|
|
61
|
+
const registerWorktreeCheckout = (commandName, description) => {
|
|
62
|
+
parent.command(`${commandName} [name] [ref]`)
|
|
63
|
+
.description(description)
|
|
64
|
+
.option('-n, --name <slug>', 'Worktree name (slug)')
|
|
65
|
+
.option('-r, --ref <ref>', 'Existing branch or ref')
|
|
66
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
67
|
+
.option('--open', 'Open editors or run a startup command before entering')
|
|
68
|
+
.option('--no-open', 'Skip opening editors or startup commands before entering')
|
|
69
|
+
.option('--tool <command>', 'Editor or app command to open after checkout (skips open prompt)')
|
|
70
|
+
.addOption(new Option('--enter', 'Enter the worktree sub-shell after checkout/opening').hideHelp())
|
|
71
|
+
.option('--no-enter', 'Do not enter the worktree sub-shell after checkout/opening')
|
|
72
|
+
.option('--exec <command>', 'Command to execute after creation')
|
|
73
|
+
.action(async (name, ref, options) => {
|
|
74
|
+
await createCommand({
|
|
75
|
+
...options,
|
|
76
|
+
name: name || options.name,
|
|
77
|
+
ref: ref || options.ref
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
registerWorktreeCheckout('worktree-checkout', 'Create a checkout-style worktree from an existing branch');
|
|
82
|
+
registerWorktreeCheckout('wc', 'Alias for worktree-checkout');
|
|
83
|
+
parent.command('delete')
|
|
84
|
+
.description('Delete managed worktrees')
|
|
85
|
+
.option('-a, --all', 'Include repo-linked worktrees outside ~/.yggtree (except main/current)')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
await deleteCommand(options);
|
|
88
|
+
});
|
|
89
|
+
parent.command('open [worktree]')
|
|
90
|
+
.description('Open a worktree in an editor or supported app')
|
|
91
|
+
.option('--tool <command>', 'Editor or app command to use (e.g. cursor, code, codex-app)')
|
|
92
|
+
.option('--enter', 'Enter the worktree sub-shell after opening')
|
|
93
|
+
.addOption(new Option('--no-enter', 'Do not enter the worktree sub-shell after opening').hideHelp())
|
|
94
|
+
.action(async (worktree, options) => {
|
|
95
|
+
await openCommand(worktree, options);
|
|
96
|
+
});
|
|
97
|
+
parent.command('bootstrap')
|
|
98
|
+
.description('Bootstrap dependencies in a worktree')
|
|
99
|
+
.action(bootstrapCommand);
|
|
100
|
+
parent.command('prune')
|
|
101
|
+
.description('Prune stale worktree information')
|
|
102
|
+
.action(pruneCommand);
|
|
103
|
+
parent.command('exec')
|
|
104
|
+
.description('Execute a command in a worktree')
|
|
105
|
+
.argument('[worktree]', 'Worktree name or path')
|
|
106
|
+
.argument('[command...]', 'Command and arguments to execute')
|
|
107
|
+
.action(async (worktree, command) => {
|
|
108
|
+
await execCommand(worktree, command);
|
|
109
|
+
});
|
|
110
|
+
parent.command('path [worktree]')
|
|
111
|
+
.description('Show the cd command for a specific worktree')
|
|
112
|
+
.action(async (worktree) => {
|
|
113
|
+
await pathCommand(worktree);
|
|
114
|
+
});
|
|
115
|
+
parent.command('create-sandbox')
|
|
116
|
+
.description('Create a sandbox worktree from current branch')
|
|
117
|
+
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
118
|
+
.option('--carry', 'Carry uncommitted changes to sandbox')
|
|
119
|
+
.option('--no-carry', 'Do not carry uncommitted changes')
|
|
120
|
+
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
121
|
+
.option('--open', 'Open an editor after creation')
|
|
122
|
+
.option('--no-open', 'Skip opening an editor after creation')
|
|
123
|
+
.addOption(new Option('--enter', 'Deprecated alias for --open').hideHelp())
|
|
124
|
+
.addOption(new Option('--no-enter', 'Deprecated alias for --no-open').hideHelp())
|
|
125
|
+
.option('--exec <command>', 'Command to execute after creation')
|
|
126
|
+
.action(async (options) => {
|
|
127
|
+
await createSandboxCommand(options);
|
|
128
|
+
});
|
|
129
|
+
parent.command('apply')
|
|
130
|
+
.description('Apply sandbox changes to origin directory')
|
|
131
|
+
.action(applyCommand);
|
|
132
|
+
parent.command('unapply')
|
|
133
|
+
.description('Undo applied sandbox changes in origin')
|
|
134
|
+
.action(unapplyCommand);
|
|
135
|
+
}
|
|
136
|
+
function rejectUnknownTopLevelCommand(args) {
|
|
137
|
+
const commandArg = args.slice(2).find(arg => !arg.startsWith('-'));
|
|
138
|
+
if (!commandArg)
|
|
139
|
+
return;
|
|
140
|
+
if (commandArg === 'help')
|
|
141
|
+
return;
|
|
142
|
+
const knownCommands = new Set(program.commands.flatMap(command => [command.name(), ...command.aliases()]));
|
|
143
|
+
if (!knownCommands.has(commandArg)) {
|
|
144
|
+
program.error(`error: unknown command '${commandArg}'`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
24
147
|
program
|
|
25
148
|
.name('yggtree')
|
|
26
149
|
.description('Interactive CLI for managing git worktrees and configs')
|
|
27
150
|
.version(getVersion())
|
|
151
|
+
.allowExcessArguments(false)
|
|
28
152
|
.action(async () => {
|
|
29
153
|
// Interactive Menu if no command is provided
|
|
30
154
|
await welcome();
|
|
155
|
+
await notifyIfUpdateAvailable();
|
|
31
156
|
const isInSandbox = Boolean(await findSandboxRoot(process.cwd()));
|
|
32
157
|
const realmChoices = [
|
|
33
158
|
{ name: `🌿 Grow New Realm ${chalk.dim('(create worktree)')}`, value: 'create-smart' },
|
|
@@ -35,12 +160,11 @@ program
|
|
|
35
160
|
{ name: `🌳 Grow Many Realms ${chalk.dim('(create multiple worktrees)')}`, value: 'create-multi' },
|
|
36
161
|
{ name: `🧪 Forge Sandbox Realm ${chalk.dim('(create sandbox worktree)')}`, value: 'create-sandbox' },
|
|
37
162
|
{ name: `🗺️ Survey Realms ${chalk.dim('(list worktrees)')}`, value: 'list' },
|
|
38
|
-
{ name: `🧭 Open Realm in
|
|
163
|
+
{ name: `🧭 Open Realm in Editor ${chalk.dim('(open worktree in editor)')}`, value: 'open' },
|
|
39
164
|
{ name: `🪓 Fell a Realm ${chalk.dim('(delete worktree)')}`, value: 'delete' },
|
|
40
165
|
{ name: `🚀 Bless Realm Setup ${chalk.dim('(bootstrap worktree)')}`, value: 'bootstrap' },
|
|
41
166
|
{ name: `🧹 Prune Withered Realms ${chalk.dim('(prune stale worktrees)')}`, value: 'prune' },
|
|
42
167
|
{ name: `🐚 Cast a Command ${chalk.dim('(exec command in worktree)')}`, value: 'exec' },
|
|
43
|
-
{ name: `🚪 Enter Realm Shell ${chalk.dim('(enter worktree)')}`, value: 'enter' },
|
|
44
168
|
{ name: `📍 Reveal Realm Path ${chalk.dim('(show worktree path)')}`, value: 'path' },
|
|
45
169
|
];
|
|
46
170
|
const sandboxChoices = [
|
|
@@ -104,9 +228,6 @@ program
|
|
|
104
228
|
case 'exec':
|
|
105
229
|
await execCommand();
|
|
106
230
|
break;
|
|
107
|
-
case 'enter':
|
|
108
|
-
await enterCommand();
|
|
109
|
-
break;
|
|
110
231
|
case 'path':
|
|
111
232
|
await pathCommand();
|
|
112
233
|
break;
|
|
@@ -132,113 +253,14 @@ program
|
|
|
132
253
|
});
|
|
133
254
|
// --- Worktree Commands ---
|
|
134
255
|
const wt = program.command('wt').description('Manage git worktrees');
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.action(async (options) => {
|
|
139
|
-
if (options.open) {
|
|
140
|
-
await openCommand();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
await listCommand();
|
|
144
|
-
});
|
|
145
|
-
wt.command('create [branch]')
|
|
146
|
-
.description('Create a new worktree (Smart branch detection)')
|
|
147
|
-
.option('-b, --branch <name>', 'Branch name (e.g. feat/new-ui)')
|
|
148
|
-
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
149
|
-
.option('--source <type>', 'Base source (local or remote)')
|
|
150
|
-
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
151
|
-
.option('--enter', 'Enter sub-shell after creation')
|
|
152
|
-
.option('--no-enter', 'Skip entering sub-shell')
|
|
153
|
-
.option('--exec <command>', 'Command to execute after creation')
|
|
154
|
-
.action(async (branch, options) => {
|
|
155
|
-
await createCommandNew({
|
|
156
|
-
...options,
|
|
157
|
-
branch: branch || options.branch
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
wt.command('create-multi')
|
|
161
|
-
.description('Create multiple worktrees (Smart branch detection)')
|
|
162
|
-
.option('--base <ref>', 'Base ref (e.g. main)')
|
|
163
|
-
.option('--source <type>', 'Base source (local or remote)')
|
|
164
|
-
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
165
|
-
.action(async (options) => {
|
|
166
|
-
await createCommandMulti(options);
|
|
167
|
-
});
|
|
168
|
-
wt.command('worktree-checkout [name] [ref]')
|
|
169
|
-
.description('Create a checkout-style worktree from an existing branch')
|
|
170
|
-
.option('-n, --name <slug>', 'Worktree name (slug)')
|
|
171
|
-
.option('-r, --ref <ref>', 'Existing branch or ref')
|
|
172
|
-
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
173
|
-
.option('--enter', 'Enter sub-shell after creation')
|
|
174
|
-
.option('--no-enter', 'Skip entering sub-shell')
|
|
175
|
-
.option('--exec <command>', 'Command to execute after creation')
|
|
176
|
-
.action(async (name, ref, options) => {
|
|
177
|
-
await createCommand({
|
|
178
|
-
...options,
|
|
179
|
-
name: name || options.name,
|
|
180
|
-
ref: ref || options.ref
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
wt.command('delete')
|
|
184
|
-
.description('Delete managed worktrees')
|
|
185
|
-
.option('-a, --all', 'Include repo-linked worktrees outside ~/.yggtree (except main/current)')
|
|
186
|
-
.action(async (options) => {
|
|
187
|
-
await deleteCommand(options);
|
|
188
|
-
});
|
|
189
|
-
wt.command('open [worktree]')
|
|
190
|
-
.description('Open a worktree in an IDE or agent CLI')
|
|
191
|
-
.option('--tool <command>', 'Tool command to use (e.g. cursor, code, claude, codex)')
|
|
192
|
-
.action(async (worktree, options) => {
|
|
193
|
-
await openCommand(worktree, options);
|
|
194
|
-
});
|
|
195
|
-
wt.command('bootstrap')
|
|
196
|
-
.description('Bootstrap dependencies in a worktree')
|
|
197
|
-
.action(bootstrapCommand);
|
|
198
|
-
wt.command('prune')
|
|
199
|
-
.description('Prune stale worktree information')
|
|
200
|
-
.action(pruneCommand);
|
|
201
|
-
wt.command('exec')
|
|
202
|
-
.description('Execute a command in a worktree')
|
|
203
|
-
.argument('[worktree]', 'Worktree name or path')
|
|
204
|
-
.argument('[command...]', 'Command and arguments to execute')
|
|
205
|
-
.action(async (worktree, command) => {
|
|
206
|
-
await execCommand(worktree, command);
|
|
207
|
-
});
|
|
208
|
-
wt.command('enter')
|
|
209
|
-
.description('Enter a worktree sub-shell')
|
|
210
|
-
.argument('[worktree]', 'Worktree name or path')
|
|
211
|
-
.option('--exec <command>', 'Command to execute before entering')
|
|
212
|
-
.action(async (worktree, options) => {
|
|
213
|
-
await enterCommand(worktree, options);
|
|
214
|
-
});
|
|
215
|
-
wt.command('path [worktree]')
|
|
216
|
-
.description('Show the cd command for a specific worktree')
|
|
217
|
-
.action(async (worktree) => {
|
|
218
|
-
await pathCommand(worktree);
|
|
219
|
-
});
|
|
220
|
-
wt.command('create-sandbox')
|
|
221
|
-
.description('Create a sandbox worktree from current branch')
|
|
222
|
-
.option('-n, --name <name>', 'Optional sandbox name (auto-generated when omitted)')
|
|
223
|
-
.option('--carry', 'Carry uncommitted changes to sandbox')
|
|
224
|
-
.option('--no-carry', 'Do not carry uncommitted changes')
|
|
225
|
-
.option('--no-bootstrap', 'Skip bootstrap (npm install + submodules)')
|
|
226
|
-
.option('--enter', 'Enter sub-shell after creation')
|
|
227
|
-
.option('--no-enter', 'Skip entering sub-shell')
|
|
228
|
-
.option('--exec <command>', 'Command to execute after creation')
|
|
229
|
-
.action(async (options) => {
|
|
230
|
-
await createSandboxCommand(options);
|
|
231
|
-
});
|
|
232
|
-
wt.command('apply')
|
|
233
|
-
.description('Apply sandbox changes to origin directory')
|
|
234
|
-
.action(applyCommand);
|
|
235
|
-
wt.command('unapply')
|
|
236
|
-
.description('Undo applied sandbox changes in origin')
|
|
237
|
-
.action(unapplyCommand);
|
|
256
|
+
registerWorktreeCommands(program);
|
|
257
|
+
registerWorktreeCommands(wt);
|
|
258
|
+
program.addHelpCommand(true);
|
|
238
259
|
program.command('bifrost')
|
|
239
260
|
.description('Summon the Bifrost (Easter Egg)')
|
|
240
261
|
.action(bifrostCommand);
|
|
241
262
|
program.command('thor')
|
|
242
263
|
.description('Consult the God of Thunder (Easter Egg)')
|
|
243
264
|
.action(thorCommand);
|
|
244
|
-
|
|
265
|
+
rejectUnknownTopLevelCommand(argv);
|
|
266
|
+
program.parse(argv);
|