yggtree 1.3.0 → 1.4.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 CHANGED
@@ -162,7 +162,7 @@ Sometimes you don't want to "commit to a branch" yet. You just want to try somet
162
162
 
163
163
  **Sandboxes** are temporary, local-only worktrees designed for this:
164
164
 
165
- 1. **Create**: `yggtree wt create-sandbox` (creates `branch_qes2`).
165
+ 1. **Create**: `yggtree wt create-sandbox` (creates something like `sandbox-a3f2_feature-branch`).
166
166
  2. **Experiment**: Change files, run tests, try that risky refactor.
167
167
  3. **Apply**: `yggtree wt apply` to "push" those file changes back to your origin directory.
168
168
  4. **Unapply**: Don't like it? `yggtree wt unapply` restores your origin to exactly how it was before.
@@ -177,10 +177,13 @@ Yggdrasil automatically prepares each worktree.
177
177
 
178
178
  Resolution order:
179
179
 
180
- 1. `yggtree-worktree.json` inside the worktree
181
- 2. `yggtree-worktree.json` in the repo root
182
- 3. `.cursor/worktrees.json`
183
- 4. Fallback: `npm install` + submodules
180
+ 1. `.yggtree/worktree-setup.json` in the repo root
181
+ 2. `yggtree-worktree.json` in the repo root (legacy fallback)
182
+ 3. `.cursor/worktrees.json` in the repo root (legacy fallback)
183
+ 4. `.yggtree/worktree-setup.json` inside the worktree (per-worktree fallback)
184
+ 5. `yggtree-worktree.json` inside the worktree (legacy fallback)
185
+ 6. `.cursor/worktrees.json` inside the worktree (legacy fallback)
186
+ 7. Fallback: `npm install` + submodules
184
187
 
185
188
  ### Example configuration
186
189
 
@@ -211,12 +214,18 @@ Create a worktree from a branch.
211
214
 
212
215
  Options:
213
216
 
217
+ * `-b, --branch <name>`
214
218
  * `--base <ref>`
215
219
  * `--source local|remote`
216
220
  * `--no-bootstrap`
217
221
  * `--enter / --no-enter`
218
222
  * `--exec "<command>"`
219
223
 
224
+ Interactive flow:
225
+
226
+ * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open a tool after creation (IDE or agent CLI).
227
+ * `--exec` remains available as an advanced explicit override.
228
+
220
229
  <details>
221
230
  <summary>Example</summary>
222
231
 
@@ -228,17 +237,59 @@ yggtree wt create feat/new-ui --base main --exec "cursor ."
228
237
 
229
238
  ---
230
239
 
240
+ ### `yggtree wt worktree-checkout [name] [ref]`
241
+
242
+ Create a checkout-style worktree from an existing branch.
243
+
244
+ Behavior:
245
+
246
+ * Prompts a searchable branch picker (type to filter in real time).
247
+ * Attaches the new worktree directly to the selected branch (checkout-style).
248
+ * If you select a remote-only branch (`origin/*`), yggtree creates the local branch in the new worktree automatically.
249
+ * If that branch already has an active yggtree-managed worktree, yggtree falls back to entering that worktree instead of creating a duplicate.
250
+
251
+ Options:
252
+
253
+ * `-n, --name <slug>`
254
+ * `-r, --ref <ref>`: skip picker and use a specific branch (`feature/x` or `origin/feature/x`)
255
+ * `--no-bootstrap`
256
+ * `--enter / --no-enter`
257
+ * `--exec "<command>"`
258
+
259
+ Interactive flow:
260
+
261
+ * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open a tool after creation (IDE or agent CLI).
262
+ * `--exec` remains available as an advanced explicit override.
263
+
264
+ <details>
265
+ <summary>Example</summary>
266
+
267
+ ```bash
268
+ yggtree wt worktree-checkout -n hotfix-auth -r main --no-enter
269
+ ```
270
+
271
+ </details>
272
+
273
+ ---
274
+
231
275
  ### `yggtree wt create-sandbox`
232
276
 
233
277
  Create a temporary sandbox from your current local branch.
234
278
 
235
279
  Options:
236
280
 
281
+ * `-n, --name <name>`: Optional sandbox name (auto-generated if omitted).
237
282
  * `--carry / --no-carry`: Bring uncommitted changes (staged/unstaged/untracked) with you.
238
283
  * `--no-bootstrap`
239
284
  * `--enter / --no-enter`
240
285
  * `--exec "<command>"`
241
286
 
287
+ Interactive flow:
288
+
289
+ * Prompts for an optional sandbox name (leave empty to auto-generate one from current branch).
290
+ * Instead of asking for a free-form `exec` command, yggtree now asks if you want to open a tool after creation (IDE or agent CLI).
291
+ * `--exec` remains available as an advanced explicit override.
292
+
242
293
  ---
243
294
 
244
295
  ### `yggtree wt apply`
@@ -261,6 +312,12 @@ Undo a previous `apply` operation.
261
312
 
262
313
  Create multiple worktrees at once.
263
314
 
315
+ Options:
316
+
317
+ * `--base <ref>`
318
+ * `--source local|remote`
319
+ * `--no-bootstrap`
320
+
264
321
  <details>
265
322
  <summary>Example</summary>
266
323
 
@@ -274,14 +331,21 @@ yggtree wt create-multi --base main
274
331
 
275
332
  ### `yggtree wt list`
276
333
 
277
- List all worktrees with state.
334
+ List all repo-linked worktrees with state.
278
335
 
279
336
  Columns:
280
337
 
281
- * TYPE (MAIN / MANAGED)
338
+ * TYPE (`MAIN`, `MANAGED`, `LINKED`, `SANDBOX`)
282
339
  * STATE (clean / dirty)
340
+ * LAST ACTIVE
283
341
  * BRANCH
284
- * PATH
342
+
343
+ Notes:
344
+
345
+ * Entries are grouped by `TYPE`.
346
+ * `SANDBOX` and `MANAGED` are worktrees inside `~/.yggtree`.
347
+ * External worktrees are labeled `LINKED`.
348
+ * Use `--open` to switch this flow into "pick and open in tool" mode.
285
349
 
286
350
  ---
287
351
 
@@ -307,6 +371,35 @@ yggtree wt enter feat/new-ui --exec "npm test"
307
371
 
308
372
  ---
309
373
 
374
+ ### `yggtree wt open [worktree]`
375
+
376
+ Open a worktree in an IDE or agent CLI.
377
+
378
+ Behavior:
379
+
380
+ * If `[worktree]` is omitted, you can pick from the worktree list with type-to-filter search.
381
+ * Detects available tool commands in your `PATH` (for example: IDEs like `cursor`, `code`, `zed`; agents like `claude`, `codex`, `gemini`, `opencode`).
382
+ * Lets you choose one interactively, or pass `--tool`.
383
+ * If an agent CLI is selected, yggtree opens a sub-shell and launches it there.
384
+
385
+ Options:
386
+
387
+ * `--tool <command>`
388
+
389
+ <details>
390
+ <summary>Examples</summary>
391
+
392
+ ```bash
393
+ yggtree wt open
394
+ yggtree wt open feat/new-ui --tool cursor
395
+ yggtree wt open feat/new-ui --tool claude
396
+ yggtree wt list --open
397
+ ```
398
+
399
+ </details>
400
+
401
+ ---
402
+
310
403
  ### `yggtree wt exec [worktree] -- <command>`
311
404
 
312
405
  Run a command inside a worktree **without entering**.
@@ -338,7 +431,18 @@ Re‑run bootstrap commands for a worktree.
338
431
 
339
432
  ### `yggtree wt delete`
340
433
 
341
- Interactively delete managed worktrees.
434
+ Interactively delete worktrees.
435
+
436
+ Behavior:
437
+
438
+ * Default flow targets managed worktrees.
439
+ * In interactive mode, yggtree asks whether to include external linked worktrees.
440
+ * In direct CLI usage, `--all` includes external linked worktrees (main/current are still excluded for safety).
441
+ * The delete selector shows 6 items per page.
442
+
443
+ Optional:
444
+
445
+ * `--all` includes linked worktrees outside `~/.yggtree` (main/current worktree is excluded for safety)
342
446
 
343
447
  ---
344
448
 
@@ -488,7 +592,7 @@ yggtree wt create-sandbox --carry
488
592
  **Scenario:**
489
593
 
490
594
  1. You have 5 files changed in your main repo but aren't sure about the direction.
491
- 2. Run `create-sandbox --carry` to move those changes into an isolated `current-branch_a1b2` folder.
595
+ 2. Run `create-sandbox --carry` to move those changes into an isolated `sandbox-a3f2_feature-branch` folder.
492
596
  3. Experiment freely.
493
597
  4. If it works: `yggtree wt apply`.
494
598
  5. If it fails: Just delete the sandbox or `unapply`.
@@ -5,6 +5,7 @@ import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensure
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 { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
8
9
  import { execa } from 'execa';
9
10
  import { spawn } from 'child_process';
10
11
  import fs from 'fs-extra';
@@ -58,19 +59,29 @@ export async function createCommandNew(options) {
58
59
  when: options.enter === undefined,
59
60
  },
60
61
  {
61
- type: 'input',
62
- name: 'exec',
63
- message: 'Command to run after creation (optional):',
64
- default: options.exec,
62
+ type: 'confirm',
63
+ name: 'shouldOpenTool',
64
+ message: 'Open a tool after creation? (IDE or agent CLI)',
65
+ default: false,
65
66
  when: options.exec === undefined,
66
- }
67
+ },
67
68
  ]);
68
69
  const branchName = options.branch || answers.branch;
69
70
  let baseRef = options.base || answers.base;
70
71
  const source = options.source || answers.source;
71
72
  const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
72
73
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
73
- const execCommandStr = options.exec || answers.exec;
74
+ let selectedTool;
75
+ if (options.exec === undefined && answers.shouldOpenTool) {
76
+ const installedTools = await detectInstalledOpenTools();
77
+ if (installedTools.length === 0) {
78
+ log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
79
+ }
80
+ else {
81
+ selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
82
+ }
83
+ }
84
+ const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
74
85
  // Append origin/ if remote is selected and not already present
75
86
  if (!options.base && source === 'remote' && !baseRef.startsWith('origin/')) {
76
87
  baseRef = `origin/${baseRef}`;
@@ -159,6 +170,15 @@ export async function createCommandNew(options) {
159
170
  ]);
160
171
  }
161
172
  }
173
+ if (selectedTool && !isAgentTool(selectedTool)) {
174
+ try {
175
+ log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
176
+ await launchOpenTool(selectedTool, wtPath);
177
+ }
178
+ catch (error) {
179
+ log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
180
+ }
181
+ }
162
182
  // 6. Final Output
163
183
  log.success('Worktree ready!');
164
184
  if (shouldEnter) {
@@ -5,7 +5,8 @@ import { getRepoRoot, getRepoName, 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 { generateSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
8
+ import { generateSandboxName, normalizeSandboxName, writeSandboxMeta } from '../../lib/sandbox.js';
9
+ import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
9
10
  import { execa } from 'execa';
10
11
  import { spawn } from 'child_process';
11
12
  import fs from 'fs-extra';
@@ -20,11 +21,32 @@ export async function createSandboxCommand(options = {}) {
20
21
  return;
21
22
  }
22
23
  log.info(`Current branch: ${chalk.cyan(currentBranch)}`);
23
- // 2. Generate random sandbox name
24
- const sandboxName = generateSandboxName(currentBranch);
25
- log.info(`Sandbox name: ${chalk.yellow(sandboxName)}`);
24
+ // 2. Build default + optional custom name validation
25
+ const generatedSandboxName = generateSandboxName(currentBranch);
26
+ const validateSandboxName = async (input) => {
27
+ if (!input.trim())
28
+ return true;
29
+ const normalized = normalizeSandboxName(input);
30
+ if (!normalized) {
31
+ return 'Please provide at least one valid character.';
32
+ }
33
+ try {
34
+ await execa('git', ['check-ref-format', '--branch', normalized], { cwd: repoRoot });
35
+ return true;
36
+ }
37
+ catch {
38
+ return 'Sandbox name is not a valid git branch name.';
39
+ }
40
+ };
26
41
  // 3. Gather remaining inputs
27
42
  const answers = await inquirer.prompt([
43
+ {
44
+ type: 'input',
45
+ name: 'name',
46
+ message: `Sandbox name (optional, leave empty for auto: ${generatedSandboxName}):`,
47
+ when: options.name === undefined,
48
+ validate: validateSandboxName,
49
+ },
28
50
  {
29
51
  type: 'confirm',
30
52
  name: 'carry',
@@ -47,17 +69,39 @@ export async function createSandboxCommand(options = {}) {
47
69
  when: options.enter === undefined,
48
70
  },
49
71
  {
50
- type: 'input',
51
- name: 'exec',
52
- message: 'Command to run after creation (optional):',
53
- default: options.exec,
72
+ type: 'confirm',
73
+ name: 'shouldOpenTool',
74
+ message: 'Open a tool after creation? (IDE or agent CLI)',
75
+ default: false,
54
76
  when: options.exec === undefined,
55
- }
77
+ },
56
78
  ]);
79
+ const requestedSandboxName = options.name !== undefined ? options.name : answers.name || '';
80
+ const nameValidation = await validateSandboxName(requestedSandboxName);
81
+ if (nameValidation !== true) {
82
+ log.error(nameValidation);
83
+ return;
84
+ }
85
+ const normalizedCustomName = normalizeSandboxName(requestedSandboxName);
86
+ const sandboxName = normalizedCustomName || generatedSandboxName;
87
+ if (requestedSandboxName.trim() && normalizedCustomName !== requestedSandboxName.trim()) {
88
+ log.dim(`Normalized sandbox name: ${chalk.yellow(sandboxName)}`);
89
+ }
90
+ log.info(`Sandbox name: ${chalk.yellow(sandboxName)}`);
57
91
  const shouldCarry = options.carry !== undefined ? options.carry : answers.carry;
58
92
  const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
59
93
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
60
- const execCommandStr = options.exec || answers.exec;
94
+ let selectedTool;
95
+ if (options.exec === undefined && answers.shouldOpenTool) {
96
+ const installedTools = await detectInstalledOpenTools();
97
+ if (installedTools.length === 0) {
98
+ log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
99
+ }
100
+ else {
101
+ selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
102
+ }
103
+ }
104
+ const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
61
105
  // 4. Detect uncommitted changes before creating worktree
62
106
  let changedFiles = [];
63
107
  let submodulePaths = [];
@@ -159,6 +203,15 @@ export async function createSandboxCommand(options = {}) {
159
203
  ]);
160
204
  }
161
205
  }
206
+ if (selectedTool && !isAgentTool(selectedTool)) {
207
+ try {
208
+ log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
209
+ await launchOpenTool(selectedTool, wtPath);
210
+ }
211
+ catch (error) {
212
+ log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
213
+ }
214
+ }
162
215
  // 10. Final output
163
216
  log.success('Sandbox ready!');
164
217
  log.info(`Use ${chalk.cyan('yggtree wt apply')} to apply changes to origin.`);
@@ -1,48 +1,138 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import path from 'path';
4
- import { getRepoRoot, getRepoName, verifyRef, createWorktree, fetchAll, getCurrentBranch } from '../../lib/git.js';
4
+ import { getRepoRoot, getRepoName, createWorktree, fetchAll, listWorktrees } 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 { ensureAutocompletePrompt } from '../../lib/prompt.js';
9
+ import { enterCommand } from './enter.js';
10
+ import { buildAgentExecCommand, detectInstalledOpenTools, isAgentTool, launchOpenTool, promptOpenToolSelection, } from './open.js';
8
11
  import { execa } from 'execa';
9
12
  import { spawn } from 'child_process';
10
13
  import fs from 'fs-extra';
14
+ function toSlug(value) {
15
+ return value
16
+ .trim()
17
+ .replace(/[\/\\]/g, '-')
18
+ .replace(/\s+/g, '-');
19
+ }
20
+ async function listBranchCandidates() {
21
+ const [localRefs, remoteRefs] = await Promise.all([
22
+ execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads']),
23
+ execa('git', ['for-each-ref', '--format=%(refname:short)', 'refs/remotes/origin']),
24
+ ]);
25
+ const localBranches = localRefs.stdout
26
+ .split('\n')
27
+ .map(line => line.trim())
28
+ .filter(Boolean);
29
+ const remoteBranches = remoteRefs.stdout
30
+ .split('\n')
31
+ .map(line => line.trim())
32
+ .filter(Boolean)
33
+ .filter(ref => ref !== 'origin/HEAD' && !ref.endsWith('/HEAD'))
34
+ .map(ref => ref.replace(/^origin\//, ''));
35
+ const localSet = new Set(localBranches);
36
+ const remoteOnly = [...new Set(remoteBranches)].filter(branch => !localSet.has(branch));
37
+ const localCandidates = localBranches.map(branchName => ({
38
+ branchName,
39
+ checkoutRef: branchName,
40
+ createLocalBranch: false,
41
+ source: 'local',
42
+ }));
43
+ const remoteCandidates = remoteOnly.map(branchName => ({
44
+ branchName,
45
+ checkoutRef: `origin/${branchName}`,
46
+ createLocalBranch: true,
47
+ source: 'remote',
48
+ }));
49
+ return [...localCandidates, ...remoteCandidates]
50
+ .sort((a, b) => a.branchName.localeCompare(b.branchName));
51
+ }
52
+ function resolveCandidateFromRef(ref, candidates) {
53
+ const trimmedRef = ref.trim();
54
+ if (!trimmedRef)
55
+ return undefined;
56
+ if (trimmedRef.startsWith('origin/')) {
57
+ const branchName = trimmedRef.replace(/^origin\//, '');
58
+ return candidates.find(candidate => candidate.branchName === branchName);
59
+ }
60
+ return candidates.find(candidate => candidate.branchName === trimmedRef ||
61
+ candidate.checkoutRef === trimmedRef);
62
+ }
11
63
  export async function createCommand(options) {
12
64
  try {
13
65
  const repoRoot = await getRepoRoot();
14
66
  log.info(`Repo: ${chalk.dim(repoRoot)}`);
15
- // 1. Gather inputs
16
- const currentBranch = await getCurrentBranch();
67
+ // 1. Load branches
68
+ const loadingSpinner = createSpinner('Fetching branches...').start();
69
+ await fetchAll();
70
+ const candidates = await listBranchCandidates();
71
+ if (candidates.length === 0) {
72
+ loadingSpinner.fail('No branches found.');
73
+ log.warning('Create a branch first, then run worktree-checkout again.');
74
+ return;
75
+ }
76
+ loadingSpinner.succeed(`Loaded ${candidates.length} branches.`);
77
+ // 2. Select branch
78
+ ensureAutocompletePrompt();
79
+ let selectedBranch;
80
+ if (options.ref) {
81
+ selectedBranch = resolveCandidateFromRef(options.ref, candidates);
82
+ if (!selectedBranch) {
83
+ log.error(`Branch "${options.ref}" not found.`);
84
+ log.warning('Tip: use a local branch name or origin/<branch>.');
85
+ return;
86
+ }
87
+ }
88
+ else {
89
+ const { branchChoice } = await inquirer.prompt([
90
+ {
91
+ type: 'autocomplete',
92
+ name: 'branchChoice',
93
+ message: 'Select branch (type to filter):',
94
+ source: async (_answers, input = '') => {
95
+ const term = input.trim().toLowerCase();
96
+ const filtered = term
97
+ ? candidates.filter(candidate => candidate.branchName.toLowerCase().includes(term))
98
+ : candidates;
99
+ return filtered.map(candidate => ({
100
+ name: `${chalk.yellow(candidate.branchName)} ${chalk.dim(`(${candidate.source})`)}`,
101
+ value: candidate,
102
+ }));
103
+ },
104
+ pageSize: 10,
105
+ },
106
+ ]);
107
+ selectedBranch = branchChoice;
108
+ }
109
+ if (!selectedBranch) {
110
+ log.error('No branch selected.');
111
+ return;
112
+ }
113
+ const existingWorktrees = await listWorktrees();
114
+ const existingManagedWorktree = existingWorktrees.find(wt => wt.branch === selectedBranch.branchName && wt.path.startsWith(WORKTREES_ROOT));
115
+ if (existingManagedWorktree) {
116
+ log.info(`Branch ${chalk.cyan(selectedBranch.branchName)} is already active in ${ui.path(existingManagedWorktree.path)}.`);
117
+ if (options.enter === false) {
118
+ log.header(`cd "${existingManagedWorktree.path}"`);
119
+ return;
120
+ }
121
+ log.info('Opening existing worktree instead of creating a duplicate...');
122
+ await enterCommand(existingManagedWorktree.path, { exec: options.exec ?? '' });
123
+ return;
124
+ }
125
+ // 3. Gather remaining inputs
126
+ const defaultSlug = toSlug(selectedBranch.branchName);
17
127
  const answers = await inquirer.prompt([
18
128
  {
19
129
  type: 'input',
20
130
  name: 'name',
21
131
  message: 'Worktree name (slug):',
22
- default: options.name,
132
+ default: options.name || defaultSlug,
23
133
  when: !options.name,
24
134
  validate: (input) => input.trim().length > 0 || 'Name is required',
25
135
  },
26
- {
27
- type: 'input',
28
- name: 'ref',
29
- message: 'Base branch name:',
30
- default: options.ref || currentBranch,
31
- when: !options.ref,
32
- validate: (input) => input.trim().length > 0 || 'Ref is required',
33
- },
34
- {
35
- type: 'list',
36
- name: 'source',
37
- message: 'Base on:',
38
- loop: false,
39
- choices: [
40
- { name: 'Remote (origin)', value: 'remote' },
41
- { name: 'Local', value: 'local' },
42
- ],
43
- default: 'remote',
44
- when: !options.ref && !options.source,
45
- },
46
136
  {
47
137
  type: 'confirm',
48
138
  name: 'bootstrap',
@@ -58,61 +148,62 @@ export async function createCommand(options) {
58
148
  when: options.enter === undefined,
59
149
  },
60
150
  {
61
- type: 'input',
62
- name: 'exec',
63
- message: 'Command to run after creation (optional):',
64
- default: options.exec,
151
+ type: 'confirm',
152
+ name: 'shouldOpenTool',
153
+ message: 'Open a tool after creation? (IDE or agent CLI)',
154
+ default: false,
65
155
  when: options.exec === undefined,
66
- }
156
+ },
67
157
  ]);
68
158
  const name = options.name || answers.name;
69
- let ref = options.ref || answers.ref;
70
- const source = options.source || answers.source;
71
159
  const shouldEnter = options.enter !== undefined ? options.enter : answers.shouldEnter;
72
160
  const shouldBootstrap = options.bootstrap !== undefined ? options.bootstrap : answers.bootstrap;
73
- const execCommandStr = options.exec || answers.exec;
74
- // Append origin/ if remote is selected and not already present
75
- if (!options.ref && source === 'remote' && !ref.startsWith('origin/')) {
76
- ref = `origin/${ref}`;
161
+ let selectedTool;
162
+ if (options.exec === undefined && answers.shouldOpenTool) {
163
+ const installedTools = await detectInstalledOpenTools();
164
+ if (installedTools.length === 0) {
165
+ log.warning('No IDE/agent tool detected in PATH. Skipping open step.');
166
+ }
167
+ else {
168
+ selectedTool = await promptOpenToolSelection(installedTools, 'Select tool to open:');
169
+ }
77
170
  }
78
- const slug = name.replace(/\s+/g, '-');
171
+ const execCommandStr = options.exec || (selectedTool && isAgentTool(selectedTool) ? buildAgentExecCommand(selectedTool) : undefined);
172
+ const slug = toSlug(name);
79
173
  const repoName = await getRepoName();
80
174
  const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
81
- // 2. Validation
175
+ // 4. Validation
82
176
  if (!slug)
83
177
  throw new Error('Invalid name');
84
- if (!ref)
85
- throw new Error('Invalid ref');
86
- // 3. Execution
87
- const spinner = createSpinner('Fetching...').start();
88
- await fetchAll();
89
- spinner.text = 'Verifying ref...';
90
- const exists = await verifyRef(ref);
91
- if (!exists) {
92
- spinner.fail(`Ref not found: ${ref}`);
93
- log.warning(`Tip: try 'origin/${ref}' or check if the branch exists.`);
94
- return;
95
- }
96
- spinner.text = `Creating worktree at ${ui.path(wtPath)}...`;
178
+ // 5. Execution (checkout-style: attach to selected branch)
179
+ const spinner = createSpinner(`Creating worktree at ${ui.path(wtPath)}...`).start();
97
180
  try {
98
181
  await fs.ensureDir(path.dirname(wtPath));
99
- await createWorktree(wtPath, ref);
182
+ if (selectedBranch.createLocalBranch) {
183
+ await execa('git', ['worktree', 'add', '-b', selectedBranch.branchName, wtPath, selectedBranch.checkoutRef]);
184
+ }
185
+ else {
186
+ await createWorktree(wtPath, selectedBranch.checkoutRef);
187
+ }
100
188
  spinner.succeed('Worktree created.');
101
189
  }
102
190
  catch (e) {
103
191
  spinner.fail('Failed to create worktree.');
104
- log.actionableError(e.message, `git worktree add ${wtPath} ${ref}`, wtPath, [
192
+ const command = selectedBranch.createLocalBranch
193
+ ? `git worktree add -b ${selectedBranch.branchName} ${wtPath} ${selectedBranch.checkoutRef}`
194
+ : `git worktree add ${wtPath} ${selectedBranch.checkoutRef}`;
195
+ log.actionableError(e.message, command, wtPath, [
105
196
  'Check if the folder already exists: ls ' + wtPath,
106
- 'Check if the branch is already used: git worktree list',
197
+ `Check if branch "${selectedBranch.branchName}" is already checked out in another worktree: git worktree list`,
107
198
  'Try pruning stale worktrees: yggtree wt prune',
108
- `Run manually: git worktree add ${wtPath} ${ref}`
199
+ `Run manually: ${command}`
109
200
  ]);
110
201
  return;
111
202
  }
112
203
  if (shouldBootstrap) {
113
204
  await runBootstrap(wtPath, repoRoot);
114
205
  }
115
- // 5. Exec Command
206
+ // 6. Exec Command
116
207
  if (execCommandStr && execCommandStr.trim()) {
117
208
  log.info(`Executing: ${execCommandStr} in ${ui.path(wtPath)}`);
118
209
  try {
@@ -129,7 +220,16 @@ export async function createCommand(options) {
129
220
  ]);
130
221
  }
131
222
  }
132
- // 6. Final Output
223
+ if (selectedTool && !isAgentTool(selectedTool)) {
224
+ try {
225
+ log.info(`Opening ${ui.path(wtPath)} in ${chalk.cyan(selectedTool.name)}...`);
226
+ await launchOpenTool(selectedTool, wtPath);
227
+ }
228
+ catch (error) {
229
+ log.warning(`Could not open ${selectedTool.name}: ${error.message}`);
230
+ }
231
+ }
232
+ // 7. Final Output
133
233
  log.success('Worktree ready!');
134
234
  if (shouldEnter) {
135
235
  log.info(`Spawning sub-shell in ${ui.path(wtPath)}...`);
@@ -148,7 +248,7 @@ export async function createCommand(options) {
148
248
  }
149
249
  }
150
250
  catch (error) {
151
- log.actionableError(error.message, 'yggtree wt create');
251
+ log.actionableError(error.message, 'yggtree wt worktree-checkout');
152
252
  process.exit(1);
153
253
  }
154
254
  }