yggtree 1.2.1 → 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
@@ -118,8 +118,8 @@ Use the interactive UI or drive everything through commands and flags.
118
118
  ## 🧠 Parallel Development, Done Right
119
119
 
120
120
  ```bash
121
- yggtree wt create feat/eng-2581-state-selection
122
- yggtree wt create fix/eng-2610-validation
121
+ yggtree wt create feat/city-selection
122
+ yggtree wt create fix/validation
123
123
  yggtree wt create chore/cleanup-api
124
124
  ```
125
125
 
@@ -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
 
@@ -373,11 +477,12 @@ yggtree wt create feat/login-flow
373
477
 
374
478
  **What happens:**
375
479
 
376
- * Creates a new branch if it doesn’t exist
480
+ * Creates a new branch if it doesn’t exist (without inheriting base tracking), then publishes it to `origin` when possible
377
481
  * Creates a dedicated worktree
378
482
  * Runs bootstrap if enabled
379
483
  * Drops you into a sub-shell inside the worktree
380
484
 
485
+
381
486
  </details>
382
487
  ---
383
488
 
@@ -487,7 +592,7 @@ yggtree wt create-sandbox --carry
487
592
  **Scenario:**
488
593
 
489
594
  1. You have 5 files changed in your main repo but aren't sure about the direction.
490
- 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.
491
596
  3. Experiment freely.
492
597
  4. If it works: `yggtree wt apply`.
493
598
  5. If it fails: Just delete the sandbox or `unapply`.
@@ -4,7 +4,7 @@ import path from 'path';
4
4
  import fs from 'fs-extra';
5
5
  import { execa } from 'execa';
6
6
  import { log, ui, createSpinner } from '../../lib/ui.js';
7
- import { findSandboxRoot, readSandboxMeta, writeSandboxMeta, SANDBOX_META_FILE } from '../../lib/sandbox.js';
7
+ import { findSandboxRoot, readSandboxMeta, writeSandboxMeta, YGGTREE_DIR } from '../../lib/sandbox.js';
8
8
  export async function applyCommand() {
9
9
  try {
10
10
  const cwd = process.cwd();
@@ -35,8 +35,12 @@ export async function applyCommand() {
35
35
  ...stagedFiles.split('\n').filter(Boolean),
36
36
  ...untrackedFiles.split('\n').filter(Boolean)
37
37
  ]);
38
- // Exclude internal sandbox metadata file
39
- allChanges.delete(SANDBOX_META_FILE);
38
+ // Exclude internal .yggtree/ directory (config, sandbox metadata, etc.)
39
+ for (const file of allChanges) {
40
+ if (file.startsWith(`${YGGTREE_DIR}/`)) {
41
+ allChanges.delete(file);
42
+ }
43
+ }
40
44
  changedFiles = [...allChanges];
41
45
  }
42
46
  catch (e) {
@@ -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, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensureCorrectUpstream } from '../../lib/git.js';
4
+ import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensureCorrectUpstream, publishBranch } 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 { 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,25 +59,35 @@ 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}`;
77
88
  }
78
89
  // Convert branch name to slug (friendly folder name)
79
- // e.g. feat/eng-2222-new-button -> feat-eng-2222-new-button
90
+ // e.g. feat/new-button -> feat-new-button
80
91
  const slug = branchName.replace(/[\/\\]/g, '-').replace(/\s+/g, '-');
81
92
  const repoName = await getRepoName();
82
93
  const wtPath = path.join(WORKTREES_ROOT, repoName, slug);
@@ -98,24 +109,23 @@ export async function createCommandNew(options) {
98
109
  spinner.text = `Creating worktree at ${ui.path(wtPath)}...`;
99
110
  // Check if target branch already exists
100
111
  const targetBranchExists = await verifyRef(branchName);
101
- // If branch doesn't exist, we create it from base
102
- // If it does exist, we just check it out
103
- const createBranchFlag = targetBranchExists ? '' : `-b ${branchName}`;
104
112
  try {
105
113
  await fs.ensureDir(path.dirname(wtPath));
106
- // slightly different logic for creating new branch vs existing
107
114
  if (targetBranchExists) {
115
+ // Branch exists — just attach the worktree
108
116
  await execa('git', ['worktree', 'add', wtPath, branchName]);
109
117
  }
110
118
  else {
111
- await execa('git', ['worktree', 'add', '-b', branchName, wtPath, baseRef]);
119
+ // Create branch WITHOUT tracking the base, then attach worktree
120
+ await execa('git', ['branch', '--no-track', branchName, baseRef]);
121
+ await execa('git', ['worktree', 'add', wtPath, branchName]);
112
122
  }
113
123
  }
114
124
  catch (e) {
115
125
  spinner.fail('Failed to create worktree.');
116
126
  const cmd = targetBranchExists
117
127
  ? `git worktree add ${wtPath} ${branchName}`
118
- : `git worktree add -b ${branchName} ${wtPath} ${baseRef}`;
128
+ : `git branch --no-track ${branchName} ${baseRef} && git worktree add ${wtPath} ${branchName}`;
119
129
  log.actionableError(e.message, cmd, wtPath, [
120
130
  'Check if the folder already exists: ls ' + wtPath,
121
131
  'Check if the branch is already used: git worktree list',
@@ -124,20 +134,21 @@ export async function createCommandNew(options) {
124
134
  ]);
125
135
  return;
126
136
  }
137
+ // Safety net: ensure no incorrect upstream was inherited
138
+ spinner.text = 'Verifying upstream safety...';
139
+ await ensureCorrectUpstream(wtPath, branchName);
140
+ // Auto-publish: push to origin and set correct tracking
127
141
  try {
128
- // Strong Safety Mode: Ensure upstream is origin/<branchName> and publish
129
- spinner.text = 'Safely publishing branch...';
130
- await ensureCorrectUpstream(wtPath, branchName);
142
+ spinner.text = 'Publishing branch...';
143
+ await publishBranch(wtPath, branchName);
131
144
  spinner.succeed('Worktree created and branch published.');
132
145
  }
133
146
  catch (e) {
134
- spinner.fail('Worktree created, but branch publication failed.');
147
+ spinner.succeed('Worktree created (publish failed push manually later).');
135
148
  log.actionableError(e.message, 'git push -u origin HEAD', wtPath, [
136
149
  `cd ${wtPath}`,
137
150
  'Attempt to push manually: git push -u origin HEAD',
138
- 'Check if the remote branch already exists or if you have push permissions'
139
151
  ]);
140
- // We don't return here because the worktree IS created, we just failed to publish
141
152
  }
142
153
  if (shouldBootstrap) {
143
154
  await runBootstrap(wtPath, repoRoot);
@@ -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) {
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
3
  import path from 'path';
4
- import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensureCorrectUpstream } from '../../lib/git.js';
4
+ import { getRepoRoot, getRepoName, verifyRef, fetchAll, getCurrentBranch, ensureCorrectUpstream, publishBranch } 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';
@@ -82,14 +82,16 @@ export async function createCommandMulti(options) {
82
82
  await execa('git', ['worktree', 'add', wtPath, branchName]);
83
83
  }
84
84
  else {
85
- await execa('git', ['worktree', 'add', '-b', branchName, wtPath, baseRef]);
85
+ // Create branch WITHOUT tracking the base, then attach worktree
86
+ await execa('git', ['branch', '--no-track', branchName, baseRef]);
87
+ await execa('git', ['worktree', 'add', wtPath, branchName]);
86
88
  }
87
89
  }
88
90
  catch (error) {
89
91
  wtSpinner.fail(`Failed to create worktree for ${branchName}.`);
90
92
  const cmd = targetBranchExists
91
93
  ? `git worktree add ${wtPath} ${branchName}`
92
- : `git worktree add -b ${branchName} ${wtPath} ${baseRef}`;
94
+ : `git branch --no-track ${branchName} ${baseRef} && git worktree add ${wtPath} ${branchName}`;
93
95
  log.actionableError(error.message, cmd, wtPath, [
94
96
  'Check if the folder already exists: ls ' + wtPath,
95
97
  'Check if the branch is already used: git worktree list',
@@ -98,22 +100,23 @@ export async function createCommandMulti(options) {
98
100
  ]);
99
101
  continue;
100
102
  }
103
+ // Safety net: ensure no incorrect upstream was inherited
104
+ wtSpinner.text = `Verifying upstream safety for ${branchName}...`;
105
+ await ensureCorrectUpstream(wtPath, branchName);
106
+ // Auto-publish: push to origin and set correct tracking
101
107
  try {
102
- // Strong Safety Mode: Ensure upstream is origin/<branchName> and publish
103
- wtSpinner.text = `Safely publishing branch ${branchName}...`;
104
- await ensureCorrectUpstream(wtPath, branchName);
108
+ wtSpinner.text = `Publishing branch ${branchName}...`;
109
+ await publishBranch(wtPath, branchName);
105
110
  wtSpinner.succeed(`Worktree for ${chalk.cyan(branchName)} created and published.`);
106
- createdWorktrees.push(wtPath);
107
111
  }
108
112
  catch (error) {
109
- wtSpinner.fail(`Worktree for ${branchName} created, but publication failed.`);
113
+ wtSpinner.succeed(`Worktree for ${chalk.cyan(branchName)} created (publish failed — push manually later).`);
110
114
  log.actionableError(error.message, 'git push -u origin HEAD', wtPath, [
111
115
  `cd ${wtPath}`,
112
116
  'Attempt to push manually: git push -u origin HEAD',
113
- 'Check if the remote branch already exists or if you have push permissions'
114
117
  ]);
115
- createdWorktrees.push(wtPath); // Still added to list since wt exists
116
118
  }
119
+ createdWorktrees.push(wtPath);
117
120
  // 4. Bootstrap
118
121
  if (shouldBootstrap) {
119
122
  await runBootstrap(wtPath, repoRoot);
@@ -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.`);