@fatdoge/wtree 0.1.9 → 0.2.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.en.md +66 -1
- package/README.md +60 -1
- package/api/cli/wtree.ts +218 -78
- package/dist/assets/index-C78PMV-C.js +179 -0
- package/dist/assets/index-CR9jga1C.css +1 -0
- package/dist/index.html +2 -2
- package/dist-node/api/cli/wtree.js +236 -81
- package/package.json +3 -1
- package/skills/wtree/SKILL.md +162 -0
- package/src/App.tsx +2 -2
- package/src/pages/CreateWorktree.tsx +4 -5
- package/src/pages/SettingsPage.tsx +10 -7
- package/src/pages/Worktrees.tsx +13 -29
- package/dist/assets/index-AspflbWf.js +0 -179
- package/dist/assets/index-DXiZ6dVD.css +0 -1
- package/src/components/ToastHost.tsx +0 -41
- package/src/stores/toastStore.ts +0 -29
package/README.en.md
CHANGED
|
@@ -11,8 +11,21 @@ English | [简体中文](https://github.com/FatDoge/wtree/blob/main/README.md)
|
|
|
11
11
|
- Support for creating worktrees from new branches or existing branches/commits
|
|
12
12
|
- Open worktrees instantly in your system file manager or preferred IDEs (Trae, Cursor, VS Code)
|
|
13
13
|
- Support for Locking, Unlocking, and Pruning invalid worktrees
|
|
14
|
+
- Non-interactive mode: fully automated operations via CLI flags, ideal for scripts and AI Agents
|
|
15
|
+
- Provides Agent Skill for AI coding tools like Trae, Cursor, and Claude Code
|
|
14
16
|
- Local API executes git commands securely on your machine, data never leaves your computer
|
|
15
17
|
|
|
18
|
+
## Screenshots
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/home.jpg" alt="Home" width="48%" />
|
|
22
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/new.jpg" alt="Create" width="48%" />
|
|
23
|
+
</p>
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/settings.jpg" alt="Settings" width="48%" />
|
|
26
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/help.jpg" alt="Help" width="48%" />
|
|
27
|
+
</p>
|
|
28
|
+
|
|
16
29
|
## Installation
|
|
17
30
|
|
|
18
31
|
Install globally via npm (specify the public registry if you are using a private one):
|
|
@@ -68,7 +81,7 @@ wtree --ui --port 0
|
|
|
68
81
|
- `wtree`: Interactive main menu (Create/Delete/List/Open/Lock/Unlock/Prune)
|
|
69
82
|
- `wtree list`: Print worktree list
|
|
70
83
|
- `wtree create [branch]`: Create a worktree (interactive selection supported)
|
|
71
|
-
- `wtree delete`: Delete
|
|
84
|
+
- `wtree delete [branch|path ...]`: Delete worktrees (interactive selection or specify targets directly, force deletion supported)
|
|
72
85
|
- `wtree open [path|branch]`: Open a worktree
|
|
73
86
|
- `wtree lock [path|branch]`: Lock a specific worktree to prevent it from being moved or deleted
|
|
74
87
|
- `wtree unlock [path|branch]`: Unlock a specific worktree
|
|
@@ -78,6 +91,58 @@ wtree --ui --port 0
|
|
|
78
91
|
- `wtree config set <key> <value>`: Set a configuration item
|
|
79
92
|
- `wtree help`: View help information
|
|
80
93
|
|
|
94
|
+
## CLI Flags
|
|
95
|
+
|
|
96
|
+
- `--ui`: Launch the local UI
|
|
97
|
+
- `--repo <path>`: Specify the repository path (defaults to the current directory)
|
|
98
|
+
- `--no-open`: Do not automatically open the browser
|
|
99
|
+
- `--port <number>`: Specify the UI port (`0` for auto-assign)
|
|
100
|
+
- `--json`: Output in JSON format (ideal for scripts and AI Agents)
|
|
101
|
+
- `--yes, -y`: Auto-confirm all prompts
|
|
102
|
+
- `--force, -f`: Force the operation (e.g., force-delete worktrees with uncommitted changes)
|
|
103
|
+
- `--dir <path>`: Specify worktree directory path (relative to the git root)
|
|
104
|
+
- `--base <ref>`: Base reference for new branch creation (e.g., `main`, `origin/main`)
|
|
105
|
+
- `--editor <name>`: Open in a specific editor after creation (`trae`, `cursor`, `code`, `none`)
|
|
106
|
+
- `--no-editor`: Do not open any editor after creation
|
|
107
|
+
- `--no-install`: Skip automatic dependency installation after creation
|
|
108
|
+
|
|
109
|
+
## Non-Interactive Mode
|
|
110
|
+
|
|
111
|
+
All commands support fully non-interactive execution via CLI flags, suitable for scripts and AI Agents:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# List worktrees (JSON output)
|
|
115
|
+
wtree list --json
|
|
116
|
+
|
|
117
|
+
# Create a worktree for an existing branch
|
|
118
|
+
wtree create feature/my-branch --yes --no-editor --no-install --json
|
|
119
|
+
|
|
120
|
+
# Create a worktree with a new branch based on main
|
|
121
|
+
wtree create feature/new-thing --base main --yes --dir worktrees/new-thing --no-editor --no-install --json
|
|
122
|
+
|
|
123
|
+
# Delete a specific worktree
|
|
124
|
+
wtree delete feature/old-branch --yes --json
|
|
125
|
+
|
|
126
|
+
# Force delete (even with uncommitted changes)
|
|
127
|
+
wtree delete feature/dirty --yes --force --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Agent Skill
|
|
131
|
+
|
|
132
|
+
`wtree` provides an Agent Skill that enables AI coding tools (Trae, Cursor, Claude Code, etc.) to manage git worktrees directly.
|
|
133
|
+
|
|
134
|
+
### Install Skill
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# Install to current project
|
|
138
|
+
npx skills add FatDoge/wtree --skill wtree
|
|
139
|
+
|
|
140
|
+
# Install globally (available in all projects)
|
|
141
|
+
npx skills add FatDoge/wtree --skill wtree -g
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Once installed, the AI Agent will automatically recognize the `wtree` skill and invoke it when you need to manage worktrees.
|
|
145
|
+
|
|
81
146
|
## Configuration
|
|
82
147
|
|
|
83
148
|
The UI settings page saves configuration to a local file:
|
package/README.md
CHANGED
|
@@ -11,8 +11,21 @@
|
|
|
11
11
|
- 支持创建新分支、从已有分支/提交创建 worktree
|
|
12
12
|
- 支持在系统文件管理器或常用 IDE (Trae, Cursor, VS Code) 中一键打开
|
|
13
13
|
- 支持锁定 (Lock) / 解锁 (Unlock) 以及清理 (Prune) 无效的 worktree
|
|
14
|
+
- 非交互模式:支持通过命令行参数完全自动化操作,适合脚本和 AI Agent 调用
|
|
15
|
+
- 提供 Agent Skill,可被 Trae / Cursor / Claude Code 等 AI 编码工具直接使用
|
|
14
16
|
- 本地 API 执行 git 命令,数据不出机器
|
|
15
17
|
|
|
18
|
+
## UI 截图
|
|
19
|
+
|
|
20
|
+
<p align="center">
|
|
21
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/home.jpg" alt="首页" width="48%" />
|
|
22
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/new.jpg" alt="创建页面" width="48%" />
|
|
23
|
+
</p>
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/settings.jpg" alt="设置页面" width="48%" />
|
|
26
|
+
<img src="https://raw.githubusercontent.com/FatDoge/wtree/main/docs/screenshots/help.jpg" alt="帮助页面" width="48%" />
|
|
27
|
+
</p>
|
|
28
|
+
|
|
16
29
|
## 安装
|
|
17
30
|
|
|
18
31
|
可以通过 npm 全局安装(如果使用了私有源,请指定官方源):
|
|
@@ -69,13 +82,21 @@ wtree --ui --port 0
|
|
|
69
82
|
- `--repo <path>`:指定仓库路径(默认使用当前目录)
|
|
70
83
|
- `--no-open`:不自动打开浏览器
|
|
71
84
|
- `--port <number>`:指定 UI 端口(`0` 表示自动分配)
|
|
85
|
+
- `--json`:以 JSON 格式输出(适合脚本和 Agent 使用)
|
|
86
|
+
- `--yes, -y`:自动确认所有提示
|
|
87
|
+
- `--force, -f`:强制操作(如强制删除有未提交更改的 worktree)
|
|
88
|
+
- `--dir <path>`:指定 worktree 目录路径(相对于 git 根目录)
|
|
89
|
+
- `--base <ref>`:创建新分支时的基准引用(如 `main`、`origin/main`)
|
|
90
|
+
- `--editor <name>`:创建后使用指定编辑器打开(`trae`、`cursor`、`code`、`none`)
|
|
91
|
+
- `--no-editor`:创建后不打开编辑器
|
|
92
|
+
- `--no-install`:创建后不自动安装依赖
|
|
72
93
|
|
|
73
94
|
## CLI 命令
|
|
74
95
|
|
|
75
96
|
- `wtree`:交互式主菜单 (创建/删除/列表/打开/锁定/解锁/清理)
|
|
76
97
|
- `wtree list`:打印 worktree 列表
|
|
77
98
|
- `wtree create [branch]`:创建 worktree(支持交互式选择)
|
|
78
|
-
- `wtree delete`:删除 worktree
|
|
99
|
+
- `wtree delete [branch|path ...]`:删除 worktree(支持交互式选择和直接指定目标,支持强制删除)
|
|
79
100
|
- `wtree open [path|branch]`:打开 worktree
|
|
80
101
|
- `wtree lock [path|branch]`:锁定指定的 worktree,防止被移动或删除
|
|
81
102
|
- `wtree unlock [path|branch]`:解锁指定的 worktree
|
|
@@ -85,6 +106,43 @@ wtree --ui --port 0
|
|
|
85
106
|
- `wtree config set <key> <value>`:设置配置项
|
|
86
107
|
- `wtree help`:查看帮助
|
|
87
108
|
|
|
109
|
+
## 非交互模式
|
|
110
|
+
|
|
111
|
+
所有命令都支持通过参数完全非交互地执行,适合在脚本或 AI Agent 中使用:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# 查看 worktree 列表(JSON 输出)
|
|
115
|
+
wtree list --json
|
|
116
|
+
|
|
117
|
+
# 为已有分支创建 worktree
|
|
118
|
+
wtree create feature/my-branch --yes --no-editor --no-install --json
|
|
119
|
+
|
|
120
|
+
# 基于 main 创建新分支的 worktree
|
|
121
|
+
wtree create feature/new-thing --base main --yes --dir worktrees/new-thing --no-editor --no-install --json
|
|
122
|
+
|
|
123
|
+
# 删除指定 worktree
|
|
124
|
+
wtree delete feature/old-branch --yes --json
|
|
125
|
+
|
|
126
|
+
# 强制删除(即使有未提交更改)
|
|
127
|
+
wtree delete feature/dirty --yes --force --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Agent Skill
|
|
131
|
+
|
|
132
|
+
`wtree` 提供了 Agent Skill,可以让 AI 编码工具(Trae、Cursor、Claude Code 等)直接管理 git worktree。
|
|
133
|
+
|
|
134
|
+
### 安装 Skill
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# 安装到当前项目
|
|
138
|
+
npx skills add FatDoge/wtree --skill wtree
|
|
139
|
+
|
|
140
|
+
# 全局安装(所有项目可用)
|
|
141
|
+
npx skills add FatDoge/wtree --skill wtree -g
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
安装后,AI Agent 会自动识别 `wtree` skill,在你需要管理 worktree 时自动调用。
|
|
145
|
+
|
|
88
146
|
## 配置
|
|
89
147
|
|
|
90
148
|
UI 设置页会将配置写入本地文件:
|
|
@@ -137,3 +195,4 @@ pnpm run check
|
|
|
137
195
|
- `api/`:CLI + 本地 API(git 执行与配置读写)
|
|
138
196
|
- `src/`:UI(React + Vite + Tailwind)
|
|
139
197
|
- `shared/`:前后端共享类型
|
|
198
|
+
- `skills/`:Agent Skill 定义
|
package/api/cli/wtree.ts
CHANGED
|
@@ -13,8 +13,24 @@ import { openPath } from '../core/open.js'
|
|
|
13
13
|
import { readConfig, writeConfig, getConfigPaths } from '../core/config.js'
|
|
14
14
|
import { startUiDevServer } from '../ui/startUiDev.js'
|
|
15
15
|
|
|
16
|
+
type ParsedFlags = {
|
|
17
|
+
ui: boolean
|
|
18
|
+
noOpen: boolean
|
|
19
|
+
repo: string
|
|
20
|
+
port: number | undefined
|
|
21
|
+
json: boolean
|
|
22
|
+
yes: boolean
|
|
23
|
+
force: boolean
|
|
24
|
+
dir: string
|
|
25
|
+
base: string
|
|
26
|
+
editor: string | undefined
|
|
27
|
+
noEditor: boolean
|
|
28
|
+
noInstall: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
type Ctx = {
|
|
17
32
|
rootDir: string
|
|
33
|
+
flags: ParsedFlags
|
|
18
34
|
}
|
|
19
35
|
|
|
20
36
|
type SourceSelection =
|
|
@@ -33,11 +49,19 @@ function errMsg(e: unknown) {
|
|
|
33
49
|
|
|
34
50
|
function parseArgs(argv: string[]) {
|
|
35
51
|
const args = [...argv]
|
|
36
|
-
const flags = {
|
|
52
|
+
const flags: ParsedFlags = {
|
|
37
53
|
ui: false,
|
|
38
54
|
noOpen: false,
|
|
39
55
|
repo: '',
|
|
40
56
|
port: undefined as number | undefined,
|
|
57
|
+
json: false,
|
|
58
|
+
yes: false,
|
|
59
|
+
force: false,
|
|
60
|
+
dir: '',
|
|
61
|
+
base: '',
|
|
62
|
+
editor: undefined,
|
|
63
|
+
noEditor: false,
|
|
64
|
+
noInstall: false,
|
|
41
65
|
}
|
|
42
66
|
const positional: string[] = []
|
|
43
67
|
|
|
@@ -60,6 +84,14 @@ function parseArgs(argv: string[]) {
|
|
|
60
84
|
if (Number.isFinite(v)) flags.port = v
|
|
61
85
|
continue
|
|
62
86
|
}
|
|
87
|
+
if (a === '--json') { flags.json = true; continue }
|
|
88
|
+
if (a === '--yes' || a === '-y') { flags.yes = true; continue }
|
|
89
|
+
if (a === '--force' || a === '-f') { flags.force = true; continue }
|
|
90
|
+
if (a === '--dir') { flags.dir = String(args.shift() || ''); continue }
|
|
91
|
+
if (a === '--base') { flags.base = String(args.shift() || ''); continue }
|
|
92
|
+
if (a === '--editor') { flags.editor = String(args.shift() || ''); continue }
|
|
93
|
+
if (a === '--no-editor') { flags.noEditor = true; continue }
|
|
94
|
+
if (a === '--no-install') { flags.noInstall = true; continue }
|
|
63
95
|
if (a.startsWith('--')) continue
|
|
64
96
|
positional.push(a)
|
|
65
97
|
}
|
|
@@ -99,8 +131,12 @@ function parseCommand(positional: string[]) {
|
|
|
99
131
|
return { command: 'interactive' as CommandType, rest: positional }
|
|
100
132
|
}
|
|
101
133
|
|
|
102
|
-
function printWorktreeList(rootDir: string) {
|
|
134
|
+
function printWorktreeList(rootDir: string, json = false) {
|
|
103
135
|
const items = listWorktrees(rootDir)
|
|
136
|
+
if (json) {
|
|
137
|
+
console.info(JSON.stringify(items, null, 2))
|
|
138
|
+
return
|
|
139
|
+
}
|
|
104
140
|
if (items.length === 0) {
|
|
105
141
|
console.info('未读取到 worktree。')
|
|
106
142
|
return
|
|
@@ -118,7 +154,7 @@ function printHelp() {
|
|
|
118
154
|
console.info(' wtree')
|
|
119
155
|
console.info(' wtree list')
|
|
120
156
|
console.info(' wtree create [branch]')
|
|
121
|
-
console.info(' wtree delete')
|
|
157
|
+
console.info(' wtree delete [branch|path ...]')
|
|
122
158
|
console.info(' wtree open [path|branch]')
|
|
123
159
|
console.info(' wtree lock [path|branch]')
|
|
124
160
|
console.info(' wtree unlock [path|branch]')
|
|
@@ -128,7 +164,23 @@ function printHelp() {
|
|
|
128
164
|
console.info(' wtree config set <key> <value>')
|
|
129
165
|
console.info(' wtree --ui [--repo <path>] [--no-open] [--port <number>]')
|
|
130
166
|
console.info('')
|
|
167
|
+
console.info('选项:')
|
|
168
|
+
console.info(' --json 以 JSON 格式输出 (适合脚本/agent 使用)')
|
|
169
|
+
console.info(' --yes, -y 自动确认所有提示')
|
|
170
|
+
console.info(' --force, -f 强制操作 (如强制删除有未提交更改的 worktree)')
|
|
171
|
+
console.info(' --dir <path> 指定 worktree 目录路径 (相对于 git 根目录)')
|
|
172
|
+
console.info(' --base <ref> 创建新分支时的基准引用 (如 main, origin/main)')
|
|
173
|
+
console.info(' --editor <name> 创建后使用指定编辑器打开 (trae, cursor, code, none)')
|
|
174
|
+
console.info(' --no-editor 创建后不打开编辑器')
|
|
175
|
+
console.info(' --no-install 创建后不自动安装依赖')
|
|
176
|
+
console.info('')
|
|
131
177
|
console.info('可用配置 key: baseDir, openCommand, editorCommand')
|
|
178
|
+
console.info('')
|
|
179
|
+
console.info('非交互示例:')
|
|
180
|
+
console.info(' wtree list --json')
|
|
181
|
+
console.info(' wtree create feat/x --yes --no-editor --no-install --json')
|
|
182
|
+
console.info(' wtree create feat/new --base main --yes --dir worktrees/feat-new --json')
|
|
183
|
+
console.info(' wtree delete feat/old --yes --force --json')
|
|
132
184
|
}
|
|
133
185
|
|
|
134
186
|
function resolveWorktree(rootDir: string, key: string) {
|
|
@@ -312,21 +364,23 @@ async function main() {
|
|
|
312
364
|
return
|
|
313
365
|
}
|
|
314
366
|
|
|
315
|
-
|
|
367
|
+
if (!flags.json) {
|
|
368
|
+
console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`))
|
|
369
|
+
}
|
|
316
370
|
|
|
317
371
|
const { command, rest } = parseCommand(positional)
|
|
318
372
|
if (command === 'list') {
|
|
319
|
-
printWorktreeList(rootDir)
|
|
373
|
+
printWorktreeList(rootDir, flags.json)
|
|
320
374
|
return
|
|
321
375
|
}
|
|
322
376
|
|
|
323
377
|
if (command === 'create') {
|
|
324
|
-
await createWorktree({ rootDir }, rest[0])
|
|
378
|
+
await createWorktree({ rootDir, flags }, rest[0])
|
|
325
379
|
return
|
|
326
380
|
}
|
|
327
381
|
|
|
328
382
|
if (command === 'delete') {
|
|
329
|
-
await deleteWorktree({ rootDir })
|
|
383
|
+
await deleteWorktree({ rootDir, flags }, rest)
|
|
330
384
|
return
|
|
331
385
|
}
|
|
332
386
|
|
|
@@ -376,7 +430,7 @@ async function main() {
|
|
|
376
430
|
const directBranch = rest[0]
|
|
377
431
|
|
|
378
432
|
const action = await getUserAction(directBranch)
|
|
379
|
-
const ctx: Ctx = { rootDir }
|
|
433
|
+
const ctx: Ctx = { rootDir, flags }
|
|
380
434
|
if (action === 'create') {
|
|
381
435
|
await createWorktree(ctx, directBranch)
|
|
382
436
|
} else if (action === 'delete') {
|
|
@@ -416,7 +470,7 @@ async function getUserAction(directBranch?: string) {
|
|
|
416
470
|
}
|
|
417
471
|
|
|
418
472
|
async function createWorktree(ctx: Ctx, directBranch?: string) {
|
|
419
|
-
const { rootDir } = ctx
|
|
473
|
+
const { rootDir, flags } = ctx
|
|
420
474
|
const defaultBranch =
|
|
421
475
|
git(rootDir, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']).stdout
|
|
422
476
|
.replace(/^origin\//, '')
|
|
@@ -429,13 +483,16 @@ async function createWorktree(ctx: Ctx, directBranch?: string) {
|
|
|
429
483
|
selection,
|
|
430
484
|
directBranch,
|
|
431
485
|
defaultBranch,
|
|
486
|
+
flags,
|
|
432
487
|
)
|
|
433
|
-
const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch)
|
|
488
|
+
const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch, flags)
|
|
434
489
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
490
|
+
if (!flags.json) {
|
|
491
|
+
console.info(chalk.green(`\n准备创建 Worktree:`))
|
|
492
|
+
console.info(` 分支: ${targetBranch}`)
|
|
493
|
+
console.info(` 目录: ${targetDir}`)
|
|
494
|
+
console.info(` 来源: ${baseRef || 'Existing Local'}`)
|
|
495
|
+
}
|
|
439
496
|
|
|
440
497
|
await createGitWorktree(
|
|
441
498
|
rootDir,
|
|
@@ -448,8 +505,14 @@ async function createWorktree(ctx: Ctx, directBranch?: string) {
|
|
|
448
505
|
)
|
|
449
506
|
|
|
450
507
|
await setupWorktreeEnv(rootDir, targetDir, dirName)
|
|
451
|
-
await installDependencies(targetDir)
|
|
452
|
-
await openInIDE(targetDir)
|
|
508
|
+
await installDependencies(targetDir, flags.noInstall)
|
|
509
|
+
await openInIDE(targetDir, flags)
|
|
510
|
+
|
|
511
|
+
if (flags.json) {
|
|
512
|
+
const items = listWorktrees(rootDir)
|
|
513
|
+
const created = items.find(x => path.resolve(x.path) === path.resolve(targetDir))
|
|
514
|
+
console.info(JSON.stringify({ ok: true, data: created || null }))
|
|
515
|
+
}
|
|
453
516
|
}
|
|
454
517
|
|
|
455
518
|
async function selectSource(rootDir: string, directBranch: string | undefined, defaultBranch: string) {
|
|
@@ -503,6 +566,7 @@ async function resolveBranchInfo(
|
|
|
503
566
|
selection: SourceSelection,
|
|
504
567
|
directBranch: string | undefined,
|
|
505
568
|
defaultBranch: string,
|
|
569
|
+
flags: ParsedFlags,
|
|
506
570
|
) {
|
|
507
571
|
let targetBranch = ''
|
|
508
572
|
let baseRef = ''
|
|
@@ -541,19 +605,24 @@ async function resolveBranchInfo(
|
|
|
541
605
|
baseRef = `origin/${targetBranch}`
|
|
542
606
|
isNewBranch = true
|
|
543
607
|
} else {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
type: 'confirm',
|
|
547
|
-
name: 'createNew',
|
|
548
|
-
message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
|
|
549
|
-
default: true,
|
|
550
|
-
},
|
|
551
|
-
])
|
|
552
|
-
if (createNew) {
|
|
553
|
-
baseRef = defaultBranch
|
|
608
|
+
if (flags.yes) {
|
|
609
|
+
baseRef = flags.base || defaultBranch
|
|
554
610
|
isNewBranch = true
|
|
555
611
|
} else {
|
|
556
|
-
|
|
612
|
+
const { createNew } = await inquirer.prompt([
|
|
613
|
+
{
|
|
614
|
+
type: 'confirm',
|
|
615
|
+
name: 'createNew',
|
|
616
|
+
message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
|
|
617
|
+
default: true,
|
|
618
|
+
},
|
|
619
|
+
])
|
|
620
|
+
if (createNew) {
|
|
621
|
+
baseRef = defaultBranch
|
|
622
|
+
isNewBranch = true
|
|
623
|
+
} else {
|
|
624
|
+
process.exit(1)
|
|
625
|
+
}
|
|
557
626
|
}
|
|
558
627
|
}
|
|
559
628
|
}
|
|
@@ -612,16 +681,25 @@ async function resolveBranchInfo(
|
|
|
612
681
|
return { targetBranch, baseRef, isNewBranch }
|
|
613
682
|
}
|
|
614
683
|
|
|
615
|
-
async function selectTargetDir(rootDir: string, targetBranch: string) {
|
|
684
|
+
async function selectTargetDir(rootDir: string, targetBranch: string, flags: ParsedFlags) {
|
|
616
685
|
const defaultDirName = `worktrees/${targetBranch.split('/').join('-')}`
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
686
|
+
|
|
687
|
+
let dirName: string
|
|
688
|
+
if (flags.dir) {
|
|
689
|
+
dirName = flags.dir
|
|
690
|
+
} else if (flags.yes) {
|
|
691
|
+
dirName = defaultDirName
|
|
692
|
+
} else {
|
|
693
|
+
const result = await inquirer.prompt([
|
|
694
|
+
{
|
|
695
|
+
type: 'input',
|
|
696
|
+
name: 'dirName',
|
|
697
|
+
message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
|
|
698
|
+
default: defaultDirName,
|
|
699
|
+
},
|
|
700
|
+
])
|
|
701
|
+
dirName = result.dirName
|
|
702
|
+
}
|
|
625
703
|
|
|
626
704
|
const targetDir = path.resolve(rootDir, dirName)
|
|
627
705
|
if (fs.existsSync(targetDir)) {
|
|
@@ -682,7 +760,8 @@ async function setupWorktreeEnv(rootDir: string, targetDir: string, dirName: str
|
|
|
682
760
|
}
|
|
683
761
|
}
|
|
684
762
|
|
|
685
|
-
async function installDependencies(targetDir: string) {
|
|
763
|
+
async function installDependencies(targetDir: string, skip = false) {
|
|
764
|
+
if (skip) return
|
|
686
765
|
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return
|
|
687
766
|
try {
|
|
688
767
|
execSync('pnpm --version', { stdio: 'ignore' })
|
|
@@ -702,7 +781,18 @@ function hasCommand(cmd: string) {
|
|
|
702
781
|
}
|
|
703
782
|
}
|
|
704
783
|
|
|
705
|
-
async function openInIDE(targetDir: string) {
|
|
784
|
+
async function openInIDE(targetDir: string, flags: ParsedFlags) {
|
|
785
|
+
if (flags.noEditor) return
|
|
786
|
+
if (flags.editor !== undefined) {
|
|
787
|
+
if (flags.editor === 'none' || flags.editor === '') return
|
|
788
|
+
try {
|
|
789
|
+
execSync(`${flags.editor} "${targetDir}"`, { stdio: 'ignore' })
|
|
790
|
+
} catch (e: unknown) {
|
|
791
|
+
void e
|
|
792
|
+
}
|
|
793
|
+
return
|
|
794
|
+
}
|
|
795
|
+
|
|
706
796
|
const editors: { name: string; value: string }[] = []
|
|
707
797
|
if (hasCommand('trae')) editors.push({ name: `在 Trae 中打开 (trae ${targetDir})`, value: 'trae' })
|
|
708
798
|
if (hasCommand('cursor')) editors.push({ name: `在 Cursor 中打开 (cursor ${targetDir})`, value: 'cursor' })
|
|
@@ -729,35 +819,64 @@ async function openInIDE(targetDir: string) {
|
|
|
729
819
|
}
|
|
730
820
|
}
|
|
731
821
|
|
|
732
|
-
async function deleteWorktree(ctx: Ctx) {
|
|
733
|
-
const
|
|
734
|
-
const
|
|
822
|
+
async function deleteWorktree(ctx: Ctx, targets: string[] = []) {
|
|
823
|
+
const { rootDir, flags } = ctx
|
|
824
|
+
const worktrees = getWorktreeList(rootDir)
|
|
825
|
+
const choices = getDeletableWorktrees(rootDir, worktrees)
|
|
735
826
|
if (choices.length === 0) {
|
|
736
|
-
|
|
827
|
+
if (flags.json) {
|
|
828
|
+
console.info(JSON.stringify({ ok: true, data: [], message: 'No deletable worktrees' }))
|
|
829
|
+
} else {
|
|
830
|
+
console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'))
|
|
831
|
+
}
|
|
737
832
|
return
|
|
738
833
|
}
|
|
739
834
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
835
|
+
let targetPaths: string[]
|
|
836
|
+
|
|
837
|
+
if (targets.length > 0) {
|
|
838
|
+
// Non-interactive: resolve each target to a worktree path
|
|
839
|
+
targetPaths = []
|
|
840
|
+
for (const key of targets) {
|
|
841
|
+
const wt = resolveWorktree(rootDir, key)
|
|
842
|
+
if (!wt) {
|
|
843
|
+
console.error(chalk.red(`未找到 worktree: ${key}`))
|
|
844
|
+
process.exit(1)
|
|
845
|
+
}
|
|
846
|
+
if (path.resolve(wt.path) === path.resolve(rootDir)) {
|
|
847
|
+
console.error(chalk.red(`不能删除主 worktree: ${key}`))
|
|
848
|
+
process.exit(1)
|
|
849
|
+
}
|
|
850
|
+
targetPaths.push(wt.path)
|
|
851
|
+
}
|
|
852
|
+
} else {
|
|
853
|
+
// Interactive: checkbox prompt
|
|
854
|
+
const result = await inquirer.prompt([
|
|
855
|
+
{
|
|
856
|
+
type: 'checkbox',
|
|
857
|
+
name: 'targetPaths',
|
|
858
|
+
message: '请选择要删除的 Worktree:',
|
|
859
|
+
choices,
|
|
860
|
+
validate: (answer: string[]) => (answer.length > 0 ? true : '请至少选择一个'),
|
|
861
|
+
},
|
|
862
|
+
])
|
|
863
|
+
targetPaths = result.targetPaths
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (!flags.yes) {
|
|
867
|
+
const { confirmDelete } = await inquirer.prompt([
|
|
868
|
+
{
|
|
869
|
+
type: 'confirm',
|
|
870
|
+
name: 'confirmDelete',
|
|
871
|
+
message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
|
|
872
|
+
default: false,
|
|
873
|
+
},
|
|
874
|
+
])
|
|
875
|
+
if (!confirmDelete) return
|
|
876
|
+
}
|
|
749
877
|
|
|
750
|
-
const { confirmDelete } = await inquirer.prompt([
|
|
751
|
-
{
|
|
752
|
-
type: 'confirm',
|
|
753
|
-
name: 'confirmDelete',
|
|
754
|
-
message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
|
|
755
|
-
default: false,
|
|
756
|
-
},
|
|
757
|
-
])
|
|
758
|
-
if (!confirmDelete) return
|
|
759
878
|
for (const targetPath of targetPaths) {
|
|
760
|
-
await deleteSingleWorktree(
|
|
879
|
+
await deleteSingleWorktree(rootDir, targetPath, flags)
|
|
761
880
|
}
|
|
762
881
|
}
|
|
763
882
|
|
|
@@ -779,26 +898,47 @@ function getDeletableWorktrees(rootDir: string, worktrees: { path: string; branc
|
|
|
779
898
|
})
|
|
780
899
|
}
|
|
781
900
|
|
|
782
|
-
async function deleteSingleWorktree(rootDir: string, targetPath: string) {
|
|
901
|
+
async function deleteSingleWorktree(rootDir: string, targetPath: string, flags: ParsedFlags) {
|
|
783
902
|
try {
|
|
784
903
|
gitOrThrow(rootDir, ['worktree', 'remove', targetPath], 'WORKTREE_REMOVE')
|
|
785
|
-
|
|
904
|
+
if (flags.json) {
|
|
905
|
+
console.info(JSON.stringify({ ok: true, removed: targetPath }))
|
|
906
|
+
} else {
|
|
907
|
+
console.info(chalk.green(`成功删除: ${targetPath}`))
|
|
908
|
+
}
|
|
786
909
|
} catch (e: unknown) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
910
|
+
if (flags.force) {
|
|
911
|
+
try {
|
|
912
|
+
gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE')
|
|
913
|
+
if (flags.json) {
|
|
914
|
+
console.info(JSON.stringify({ ok: true, removed: targetPath, forced: true }))
|
|
915
|
+
} else {
|
|
916
|
+
console.info(chalk.green(`成功强制删除: ${targetPath}`))
|
|
917
|
+
}
|
|
918
|
+
} catch (forceErr: unknown) {
|
|
919
|
+
if (flags.json) {
|
|
920
|
+
console.error(JSON.stringify({ ok: false, error: errMsg(forceErr), path: targetPath }))
|
|
921
|
+
} else {
|
|
922
|
+
console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`))
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
console.error(chalk.red(`删除失败: ${errMsg(e)}`))
|
|
927
|
+
const { force } = await inquirer.prompt([
|
|
928
|
+
{
|
|
929
|
+
type: 'confirm',
|
|
930
|
+
name: 'force',
|
|
931
|
+
message: '删除失败 (可能有未提交的更改). 强制删除吗?',
|
|
932
|
+
default: false,
|
|
933
|
+
},
|
|
934
|
+
])
|
|
935
|
+
if (!force) return
|
|
936
|
+
try {
|
|
937
|
+
gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE')
|
|
938
|
+
console.info(chalk.green(`成功强制删除: ${targetPath}`))
|
|
939
|
+
} catch (forceErr: unknown) {
|
|
940
|
+
console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`))
|
|
941
|
+
}
|
|
802
942
|
}
|
|
803
943
|
}
|
|
804
944
|
}
|