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.
@@ -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: 'ide', aliases: ['cursor'] },
14
- { id: 'code', name: 'VS Code', command: 'code', kind: 'ide', aliases: ['vscode', 'code'] },
15
- { id: 'windsurf', name: 'Windsurf', command: 'windsurf', kind: 'ide', aliases: ['windsurf'] },
16
- { id: 'zed', name: 'Zed', command: 'zed', kind: 'ide', aliases: ['zed'] },
17
- { id: 'agy', name: 'Agy', command: 'agy', kind: 'ide', aliases: ['agy'] },
18
- { id: 'idea', name: 'IntelliJ IDEA', command: 'idea', kind: 'ide', aliases: ['idea', 'intellij'] },
19
- { id: 'webstorm', name: 'WebStorm', command: 'webstorm', kind: 'ide', aliases: ['webstorm'] },
20
- { id: 'subl', name: 'Sublime Text', command: 'subl', kind: 'ide', aliases: ['subl', 'sublime'] },
21
- { id: 'claude', name: 'Claude Code', command: 'claude', kind: 'agent', aliases: ['claude', 'claude-code'] },
22
- { id: 'codex', name: 'Codex', command: 'codex', kind: 'agent', aliases: ['codex'] },
23
- { id: 'gemini', name: 'Gemini CLI', command: 'gemini', kind: 'agent', aliases: ['gemini'] },
24
- { id: 'opencode', name: 'OpenCode', command: 'opencode', kind: 'agent', aliases: ['opencode'] },
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: await commandExists(tool.command),
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 isAgentTool(tool) {
102
- return tool.kind === 'agent';
103
- }
104
- export function buildAgentExecCommand(tool) {
105
- return tool.command;
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 === 'ide')
163
+ .filter(tool => tool.kind === 'editor')
110
164
  .map(tool => ({
111
- name: `${tool.name} ${chalk.dim(`(${tool.command})`)}`,
165
+ name: formatOpenToolChoice(tool),
112
166
  value: tool,
113
167
  }));
114
- const agentChoices = installedTools
115
- .filter(tool => tool.kind === 'agent')
168
+ const appChoices = installedTools
169
+ .filter(tool => tool.kind === 'app')
116
170
  .map(tool => ({
117
- name: `${tool.name} ${chalk.dim(`(${tool.command})`)}`,
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(chalk.dim('── IDEs ──')));
176
+ choices.push(new inquirer.Separator(openSectionLabel('Editors & IDEs')));
123
177
  choices.push(...ideChoices);
124
178
  }
125
- if (agentChoices.length > 0) {
126
- if (choices.length > 0)
127
- choices.push(new inquirer.Separator(" "));
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 async function launchOpenTool(tool, wtPath) {
144
- if (isAgentTool(tool)) {
145
- await enterCommand(wtPath, { exec: buildAgentExecCommand(tool) });
146
- return;
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
- await execa(tool.command, [wtPath], {
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
- chosenTool = resolveOpenToolOption(requestedTool, installedTools);
215
- if (!chosenTool && await commandExists(requestedTool)) {
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 open tool command was detected in PATH.');
233
- log.dim('Try: yggtree wt open --tool <command> (e.g. --tool cursor, --tool claude)');
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
- chosenTool = await promptOpenToolSelection(installedTools);
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.info(`Opening ${ui.path(targetWt.path)} in ${chalk.cyan(chosenTool.name)}...`);
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 Tool ${chalk.dim('(open worktree in IDE/agent)')}`, value: 'open' },
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
- wt.command('list')
136
- .description('List all repo-linked worktrees')
137
- .option('--open', 'Open a worktree in an IDE/agent tool instead of listing')
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
- program.parse(process.argv);
265
+ rejectUnknownTopLevelCommand(argv);
266
+ program.parse(argv);