yggtree 1.3.0 → 1.4.1
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 +114 -10
- package/dist/commands/bifrost.js +187 -0
- package/dist/commands/thor.js +39 -0
- package/dist/commands/wt/create-branch.js +26 -6
- package/dist/commands/wt/create-sandbox.js +63 -10
- package/dist/commands/wt/create.js +157 -57
- package/dist/commands/wt/delete.js +43 -18
- package/dist/commands/wt/enter.js +43 -31
- package/dist/commands/wt/exec.js +4 -17
- package/dist/commands/wt/list.js +20 -10
- package/dist/commands/wt/open.js +245 -0
- package/dist/commands/wt/path.js +4 -17
- package/dist/index.js +84 -25
- package/dist/lib/prompt.js +9 -0
- package/dist/lib/sandbox.js +14 -0
- package/dist/lib/worktree.js +52 -0
- package/package.json +2 -1
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 `
|
|
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.
|
|
181
|
-
2. `yggtree-worktree.json` in the repo root
|
|
182
|
-
3. `.cursor/worktrees.json`
|
|
183
|
-
4.
|
|
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
|
|
338
|
+
* TYPE (`MAIN`, `MANAGED`, `LINKED`, `SANDBOX`)
|
|
282
339
|
* STATE (clean / dirty)
|
|
340
|
+
* LAST ACTIVE
|
|
283
341
|
* BRANCH
|
|
284
|
-
|
|
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
|
|
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 `
|
|
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`.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import gradient from 'gradient-string';
|
|
3
|
+
import figlet from 'figlet';
|
|
4
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
export async function bifrostCommand() {
|
|
6
|
+
console.clear();
|
|
7
|
+
process.stdout.write('\x1b[?25l'); // Hide cursor
|
|
8
|
+
// Animated clouds blowing across the sky
|
|
9
|
+
const cloudArt = ' ☁️ ☁️ ☁️ ';
|
|
10
|
+
const skyWidth = 40;
|
|
11
|
+
console.log(chalk.dim('\n The sky darkens...\n'));
|
|
12
|
+
// Print a placeholder for the animated line so we can cursor-up over it
|
|
13
|
+
console.log('');
|
|
14
|
+
for (let i = 0; i < 20; i++) {
|
|
15
|
+
// Shift the clouds by slicing from an ever-changing offset of a repeated string
|
|
16
|
+
const offset = i % cloudArt.length;
|
|
17
|
+
const visibleClouds = (cloudArt.repeat(3)).substring(offset, offset + skyWidth);
|
|
18
|
+
process.stdout.write('\x1b[1A'); // Move cursor up 1 line
|
|
19
|
+
process.stdout.write('\r ' + chalk.gray(visibleClouds) + '\n');
|
|
20
|
+
await sleep(100);
|
|
21
|
+
}
|
|
22
|
+
console.clear();
|
|
23
|
+
// 6-Frame Cutscene: The Bifrost Opening (Bigger & Stronger)
|
|
24
|
+
const bifrostFrames = [
|
|
25
|
+
// Frame 1: Empty sky
|
|
26
|
+
chalk.gray(`
|
|
27
|
+
☁️ ☁️
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
`),
|
|
39
|
+
// Frame 2: Small beam of light
|
|
40
|
+
chalk.cyan(`
|
|
41
|
+
☁️ | ☁️
|
|
42
|
+
|
|
|
43
|
+
|
|
|
44
|
+
|
|
|
45
|
+
|
|
|
46
|
+
|
|
|
47
|
+
|
|
|
48
|
+
|
|
|
49
|
+
|
|
|
50
|
+
|
|
|
51
|
+
v
|
|
52
|
+
`),
|
|
53
|
+
// Frame 3: Wider beam reaching the ground
|
|
54
|
+
gradient.rainbow(`
|
|
55
|
+
☁️ ||| ☁️
|
|
56
|
+
|||
|
|
57
|
+
|||
|
|
58
|
+
|||
|
|
59
|
+
|||
|
|
60
|
+
|||
|
|
61
|
+
|||
|
|
62
|
+
|||
|
|
63
|
+
|||
|
|
64
|
+
|||
|
|
65
|
+
---------
|
|
66
|
+
`),
|
|
67
|
+
// Frame 4: Giant beam hitting the ground
|
|
68
|
+
gradient.rainbow(`
|
|
69
|
+
☁️ ||||||||||| ☁️
|
|
70
|
+
|||||||||||
|
|
71
|
+
|||||||||||
|
|
72
|
+
|||||||||||
|
|
73
|
+
|||||||||||
|
|
74
|
+
|||||||||||
|
|
75
|
+
|||||||||||
|
|
76
|
+
|||||||||||
|
|
77
|
+
|||||||||||
|
|
78
|
+
|||||||||||
|
|
79
|
+
-----------------
|
|
80
|
+
`),
|
|
81
|
+
// Frame 5: MASSIVE beam
|
|
82
|
+
gradient.rainbow(`
|
|
83
|
+
☁️ ||||||||||||||||||||| ☁️
|
|
84
|
+
|||||||||||||||||||||
|
|
85
|
+
|||||||||||||||||||||
|
|
86
|
+
|||||||||||||||||||||
|
|
87
|
+
|||||||||||||||||||||
|
|
88
|
+
|||||||||||||||||||||
|
|
89
|
+
|||||||||||||||||||||
|
|
90
|
+
|||||||||||||||||||||
|
|
91
|
+
|||||||||||||||||||||
|
|
92
|
+
|||||||||||||||||||||
|
|
93
|
+
---------------------------
|
|
94
|
+
`),
|
|
95
|
+
// Frame 6: Silhouette of Thor in the massive beam
|
|
96
|
+
gradient.rainbow(`
|
|
97
|
+
☁️ ||||||||||||||||||||| ☁️
|
|
98
|
+
|||||||||||||||||||||
|
|
99
|
+
|||||||||||||||||||||
|
|
100
|
+
|||||||||||||||||||||
|
|
101
|
+
|||||||||||||||||||||
|
|
102
|
+
|||||||||||||||||||||
|
|
103
|
+
|||||||||||||||||||||
|
|
104
|
+
|||||||||||||||||||||
|
|
105
|
+
|||||||||||||||||||||
|
|
106
|
+
||||||||| 𖨆 |||||||||
|
|
107
|
+
---------------------------
|
|
108
|
+
`)
|
|
109
|
+
];
|
|
110
|
+
// Give the user more time to digest each step of the expanding Bifrost
|
|
111
|
+
const frameDelays = [400, 300, 300, 300, 400, 1000];
|
|
112
|
+
for (let i = 0; i < bifrostFrames.length; i++) {
|
|
113
|
+
console.clear();
|
|
114
|
+
console.log(chalk.bold.italic('\n Opening the Bifrost...\n'));
|
|
115
|
+
console.log(bifrostFrames[i]);
|
|
116
|
+
await sleep(frameDelays[i]); // Custom timing per frame for better impact
|
|
117
|
+
}
|
|
118
|
+
await sleep(200);
|
|
119
|
+
// Lightning Flash
|
|
120
|
+
// We use standard ANSI terminal invert colors to simulate lightning
|
|
121
|
+
process.stdout.write('\x1b[?5h'); // Inverse Video On
|
|
122
|
+
console.log(chalk.yellow.bold('\n\n ⚡ \n ⚡ ⚡\n ⚡\n\n'));
|
|
123
|
+
await sleep(150);
|
|
124
|
+
process.stdout.write('\x1b[?5l'); // Inverse Video Off
|
|
125
|
+
console.clear();
|
|
126
|
+
const frames = [
|
|
127
|
+
chalk.gray.bold(`
|
|
128
|
+
___________
|
|
129
|
+
| |
|
|
130
|
+
| MJÖLNIR |
|
|
131
|
+
|___________|
|
|
132
|
+
| |
|
|
133
|
+
| |
|
|
134
|
+
_|_|_
|
|
135
|
+
`),
|
|
136
|
+
chalk.gray.bold(`
|
|
137
|
+
|
|
138
|
+
_____________
|
|
139
|
+
======| MJÖLNIR |
|
|
140
|
+
-------------
|
|
141
|
+
|
|
142
|
+
`),
|
|
143
|
+
chalk.gray.bold(`
|
|
144
|
+
_|_|_
|
|
145
|
+
| |
|
|
146
|
+
| |
|
|
147
|
+
___________
|
|
148
|
+
| |
|
|
149
|
+
| MJÖLNIR |
|
|
150
|
+
|___________|
|
|
151
|
+
`),
|
|
152
|
+
chalk.gray.bold(`
|
|
153
|
+
|
|
154
|
+
_____________
|
|
155
|
+
| MJÖLNIR |======
|
|
156
|
+
-------------
|
|
157
|
+
|
|
158
|
+
`)
|
|
159
|
+
];
|
|
160
|
+
// Let the hammer fall down and spin from right to left
|
|
161
|
+
const animationSteps = 15;
|
|
162
|
+
for (let i = 0; i < animationSteps; i++) {
|
|
163
|
+
console.clear();
|
|
164
|
+
console.log('\x1b[?25l'); // Hide cursor
|
|
165
|
+
const yPadding = '\n'.repeat(i);
|
|
166
|
+
const xPaddingLength = Math.max(0, (animationSteps - i) * 6);
|
|
167
|
+
const xPadding = ' '.repeat(xPaddingLength);
|
|
168
|
+
const frame = frames[i % frames.length];
|
|
169
|
+
const paddedFrame = frame.split('\n').map(line => xPadding + line).join('\n');
|
|
170
|
+
console.log(yPadding + paddedFrame);
|
|
171
|
+
await sleep(70);
|
|
172
|
+
}
|
|
173
|
+
// Final pose upright!
|
|
174
|
+
console.clear();
|
|
175
|
+
const finalYPadding = '\n'.repeat(animationSteps);
|
|
176
|
+
console.log(finalYPadding + frames[0]);
|
|
177
|
+
await sleep(200);
|
|
178
|
+
// THOOM text
|
|
179
|
+
const thoomArt = figlet.textSync('THOOM!', {
|
|
180
|
+
font: 'Standard',
|
|
181
|
+
horizontalLayout: 'default',
|
|
182
|
+
verticalLayout: 'default',
|
|
183
|
+
});
|
|
184
|
+
console.log(gradient.fruit(thoomArt));
|
|
185
|
+
console.log(chalk.yellow.bold('\n ⚡ The son of Odin has entered the realm ⚡ \n'));
|
|
186
|
+
console.log('\x1b[?25h'); // Show cursor
|
|
187
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3
|
+
// A collection of legendary quotes (mix of Poetic Edda and developer humor)
|
|
4
|
+
const quotes = [
|
|
5
|
+
"I say thee NAY to unhandled promises!",
|
|
6
|
+
"Even the All-Father tests in production sometimes.",
|
|
7
|
+
"A wise developer commits early, and commits often.",
|
|
8
|
+
"Bugs are just frost giants hiding in your codebase.",
|
|
9
|
+
"May your builds be as swift as Mjölnir's flight.",
|
|
10
|
+
"Do not trust a tree without deep roots, nor code without tests.",
|
|
11
|
+
"Yggdrasil connects all realms; git connects all branches.",
|
|
12
|
+
"A clean working tree is acceptable to the gods.",
|
|
13
|
+
"The Bifrost is open, but did you remember to push?",
|
|
14
|
+
"Verily, thou shalt not force push to main."
|
|
15
|
+
];
|
|
16
|
+
export async function thorCommand() {
|
|
17
|
+
console.clear();
|
|
18
|
+
const randomQuote = quotes[Math.floor(Math.random() * quotes.length)];
|
|
19
|
+
// Static Thor ASCII art
|
|
20
|
+
const thorArt = `
|
|
21
|
+
/ \\
|
|
22
|
+
/___\\
|
|
23
|
+
(o_o) "Hello from Asgard!"
|
|
24
|
+
/|||||\\
|
|
25
|
+
| | | |
|
|
26
|
+
\\=====//
|
|
27
|
+
| |
|
|
28
|
+
| |
|
|
29
|
+
_|_|_
|
|
30
|
+
`;
|
|
31
|
+
console.log(chalk.gray.bold(thorArt));
|
|
32
|
+
// Typewriter effect for the quote
|
|
33
|
+
process.stdout.write(chalk.cyan('Thor says: '));
|
|
34
|
+
for (let i = 0; i < randomQuote.length; i++) {
|
|
35
|
+
process.stdout.write(chalk.bold.yellow(randomQuote.charAt(i)));
|
|
36
|
+
await sleep(30); // Typing speed
|
|
37
|
+
}
|
|
38
|
+
console.log('\n');
|
|
39
|
+
}
|
|
@@ -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: '
|
|
62
|
-
name: '
|
|
63
|
-
message: '
|
|
64
|
-
default:
|
|
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
|
-
|
|
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.
|
|
24
|
-
const
|
|
25
|
-
|
|
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: '
|
|
51
|
-
name: '
|
|
52
|
-
message: '
|
|
53
|
-
default:
|
|
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
|
-
|
|
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.`);
|