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 +118 -13
- package/dist/commands/wt/apply.js +7 -3
- package/dist/commands/wt/create-branch.js +40 -20
- package/dist/commands/wt/create-multi.js +13 -10
- package/dist/commands/wt/create-sandbox.js +63 -10
- package/dist/commands/wt/create.js +157 -57
- package/dist/commands/wt/delete.js +45 -17
- package/dist/commands/wt/enter.js +43 -31
- package/dist/commands/wt/exec.js +4 -17
- package/dist/commands/wt/list.js +29 -19
- package/dist/commands/wt/open.js +245 -0
- package/dist/commands/wt/path.js +4 -17
- package/dist/index.js +66 -25
- package/dist/lib/config.js +17 -4
- package/dist/lib/git.js +63 -13
- package/dist/lib/prompt.js +9 -0
- package/dist/lib/sandbox.js +19 -3
- package/dist/lib/ui.js +20 -0
- package/dist/lib/worktree.js +52 -0
- package/package.json +2 -1
- package/dist/commands/wt/leave.js +0 -13
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/
|
|
122
|
-
yggtree wt create fix/
|
|
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 `
|
|
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
|
|
|
@@ -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 `
|
|
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,
|
|
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
|
|
39
|
-
allChanges
|
|
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: '
|
|
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}`;
|
|
77
88
|
}
|
|
78
89
|
// Convert branch name to slug (friendly folder name)
|
|
79
|
-
// e.g. feat/
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
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.
|
|
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.
|
|
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.`);
|