@fatdoge/wtree 0.1.10 → 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-node/api/cli/wtree.js +236 -81
- package/dist-node/api/cli/wtui.js +0 -0
- package/package.json +16 -15
- package/skills/wtree/SKILL.md +162 -0
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
|
}
|
|
@@ -21,6 +21,14 @@ function parseArgs(argv) {
|
|
|
21
21
|
noOpen: false,
|
|
22
22
|
repo: '',
|
|
23
23
|
port: undefined,
|
|
24
|
+
json: false,
|
|
25
|
+
yes: false,
|
|
26
|
+
force: false,
|
|
27
|
+
dir: '',
|
|
28
|
+
base: '',
|
|
29
|
+
editor: undefined,
|
|
30
|
+
noEditor: false,
|
|
31
|
+
noInstall: false,
|
|
24
32
|
};
|
|
25
33
|
const positional = [];
|
|
26
34
|
while (args.length) {
|
|
@@ -43,6 +51,38 @@ function parseArgs(argv) {
|
|
|
43
51
|
flags.port = v;
|
|
44
52
|
continue;
|
|
45
53
|
}
|
|
54
|
+
if (a === '--json') {
|
|
55
|
+
flags.json = true;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (a === '--yes' || a === '-y') {
|
|
59
|
+
flags.yes = true;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (a === '--force' || a === '-f') {
|
|
63
|
+
flags.force = true;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (a === '--dir') {
|
|
67
|
+
flags.dir = String(args.shift() || '');
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (a === '--base') {
|
|
71
|
+
flags.base = String(args.shift() || '');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (a === '--editor') {
|
|
75
|
+
flags.editor = String(args.shift() || '');
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (a === '--no-editor') {
|
|
79
|
+
flags.noEditor = true;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (a === '--no-install') {
|
|
83
|
+
flags.noInstall = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
46
86
|
if (a.startsWith('--'))
|
|
47
87
|
continue;
|
|
48
88
|
positional.push(a);
|
|
@@ -80,8 +120,12 @@ function parseCommand(positional) {
|
|
|
80
120
|
}
|
|
81
121
|
return { command: 'interactive', rest: positional };
|
|
82
122
|
}
|
|
83
|
-
function printWorktreeList(rootDir) {
|
|
123
|
+
function printWorktreeList(rootDir, json = false) {
|
|
84
124
|
const items = listWorktrees(rootDir);
|
|
125
|
+
if (json) {
|
|
126
|
+
console.info(JSON.stringify(items, null, 2));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
85
129
|
if (items.length === 0) {
|
|
86
130
|
console.info('未读取到 worktree。');
|
|
87
131
|
return;
|
|
@@ -98,7 +142,7 @@ function printHelp() {
|
|
|
98
142
|
console.info(' wtree');
|
|
99
143
|
console.info(' wtree list');
|
|
100
144
|
console.info(' wtree create [branch]');
|
|
101
|
-
console.info(' wtree delete');
|
|
145
|
+
console.info(' wtree delete [branch|path ...]');
|
|
102
146
|
console.info(' wtree open [path|branch]');
|
|
103
147
|
console.info(' wtree lock [path|branch]');
|
|
104
148
|
console.info(' wtree unlock [path|branch]');
|
|
@@ -108,7 +152,23 @@ function printHelp() {
|
|
|
108
152
|
console.info(' wtree config set <key> <value>');
|
|
109
153
|
console.info(' wtree --ui [--repo <path>] [--no-open] [--port <number>]');
|
|
110
154
|
console.info('');
|
|
155
|
+
console.info('选项:');
|
|
156
|
+
console.info(' --json 以 JSON 格式输出 (适合脚本/agent 使用)');
|
|
157
|
+
console.info(' --yes, -y 自动确认所有提示');
|
|
158
|
+
console.info(' --force, -f 强制操作 (如强制删除有未提交更改的 worktree)');
|
|
159
|
+
console.info(' --dir <path> 指定 worktree 目录路径 (相对于 git 根目录)');
|
|
160
|
+
console.info(' --base <ref> 创建新分支时的基准引用 (如 main, origin/main)');
|
|
161
|
+
console.info(' --editor <name> 创建后使用指定编辑器打开 (trae, cursor, code, none)');
|
|
162
|
+
console.info(' --no-editor 创建后不打开编辑器');
|
|
163
|
+
console.info(' --no-install 创建后不自动安装依赖');
|
|
164
|
+
console.info('');
|
|
111
165
|
console.info('可用配置 key: baseDir, openCommand, editorCommand');
|
|
166
|
+
console.info('');
|
|
167
|
+
console.info('非交互示例:');
|
|
168
|
+
console.info(' wtree list --json');
|
|
169
|
+
console.info(' wtree create feat/x --yes --no-editor --no-install --json');
|
|
170
|
+
console.info(' wtree create feat/new --base main --yes --dir worktrees/feat-new --json');
|
|
171
|
+
console.info(' wtree delete feat/old --yes --force --json');
|
|
112
172
|
}
|
|
113
173
|
function resolveWorktree(rootDir, key) {
|
|
114
174
|
const items = listWorktrees(rootDir);
|
|
@@ -276,18 +336,20 @@ async function main() {
|
|
|
276
336
|
process.on('SIGTERM', close);
|
|
277
337
|
return;
|
|
278
338
|
}
|
|
279
|
-
|
|
339
|
+
if (!flags.json) {
|
|
340
|
+
console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`));
|
|
341
|
+
}
|
|
280
342
|
const { command, rest } = parseCommand(positional);
|
|
281
343
|
if (command === 'list') {
|
|
282
|
-
printWorktreeList(rootDir);
|
|
344
|
+
printWorktreeList(rootDir, flags.json);
|
|
283
345
|
return;
|
|
284
346
|
}
|
|
285
347
|
if (command === 'create') {
|
|
286
|
-
await createWorktree({ rootDir }, rest[0]);
|
|
348
|
+
await createWorktree({ rootDir, flags }, rest[0]);
|
|
287
349
|
return;
|
|
288
350
|
}
|
|
289
351
|
if (command === 'delete') {
|
|
290
|
-
await deleteWorktree({ rootDir });
|
|
352
|
+
await deleteWorktree({ rootDir, flags }, rest);
|
|
291
353
|
return;
|
|
292
354
|
}
|
|
293
355
|
if (command === 'open') {
|
|
@@ -329,7 +391,7 @@ async function main() {
|
|
|
329
391
|
}
|
|
330
392
|
const directBranch = rest[0];
|
|
331
393
|
const action = await getUserAction(directBranch);
|
|
332
|
-
const ctx = { rootDir };
|
|
394
|
+
const ctx = { rootDir, flags };
|
|
333
395
|
if (action === 'create') {
|
|
334
396
|
await createWorktree(ctx, directBranch);
|
|
335
397
|
}
|
|
@@ -374,21 +436,28 @@ async function getUserAction(directBranch) {
|
|
|
374
436
|
return action;
|
|
375
437
|
}
|
|
376
438
|
async function createWorktree(ctx, directBranch) {
|
|
377
|
-
const { rootDir } = ctx;
|
|
439
|
+
const { rootDir, flags } = ctx;
|
|
378
440
|
const defaultBranch = git(rootDir, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']).stdout
|
|
379
441
|
.replace(/^origin\//, '')
|
|
380
442
|
.trim() || 'master';
|
|
381
443
|
const { sourceType, selection } = await selectSource(rootDir, directBranch, defaultBranch);
|
|
382
|
-
const { targetBranch, baseRef, isNewBranch } = await resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch);
|
|
383
|
-
const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
444
|
+
const { targetBranch, baseRef, isNewBranch } = await resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch, flags);
|
|
445
|
+
const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch, flags);
|
|
446
|
+
if (!flags.json) {
|
|
447
|
+
console.info(chalk.green(`\n准备创建 Worktree:`));
|
|
448
|
+
console.info(` 分支: ${targetBranch}`);
|
|
449
|
+
console.info(` 目录: ${targetDir}`);
|
|
450
|
+
console.info(` 来源: ${baseRef || 'Existing Local'}`);
|
|
451
|
+
}
|
|
388
452
|
await createGitWorktree(rootDir, targetDir, targetBranch, baseRef, isNewBranch, sourceType, defaultBranch);
|
|
389
453
|
await setupWorktreeEnv(rootDir, targetDir, dirName);
|
|
390
|
-
await installDependencies(targetDir);
|
|
391
|
-
await openInIDE(targetDir);
|
|
454
|
+
await installDependencies(targetDir, flags.noInstall);
|
|
455
|
+
await openInIDE(targetDir, flags);
|
|
456
|
+
if (flags.json) {
|
|
457
|
+
const items = listWorktrees(rootDir);
|
|
458
|
+
const created = items.find(x => path.resolve(x.path) === path.resolve(targetDir));
|
|
459
|
+
console.info(JSON.stringify({ ok: true, data: created || null }));
|
|
460
|
+
}
|
|
392
461
|
}
|
|
393
462
|
async function selectSource(rootDir, directBranch, defaultBranch) {
|
|
394
463
|
let sourceType;
|
|
@@ -430,7 +499,7 @@ async function selectSource(rootDir, directBranch, defaultBranch) {
|
|
|
430
499
|
}
|
|
431
500
|
return { sourceType, selection };
|
|
432
501
|
}
|
|
433
|
-
async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch) {
|
|
502
|
+
async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, defaultBranch, flags) {
|
|
434
503
|
let targetBranch = '';
|
|
435
504
|
let baseRef = '';
|
|
436
505
|
let isNewBranch = false;
|
|
@@ -470,20 +539,26 @@ async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, d
|
|
|
470
539
|
isNewBranch = true;
|
|
471
540
|
}
|
|
472
541
|
else {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
type: 'confirm',
|
|
476
|
-
name: 'createNew',
|
|
477
|
-
message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
|
|
478
|
-
default: true,
|
|
479
|
-
},
|
|
480
|
-
]);
|
|
481
|
-
if (createNew) {
|
|
482
|
-
baseRef = defaultBranch;
|
|
542
|
+
if (flags.yes) {
|
|
543
|
+
baseRef = flags.base || defaultBranch;
|
|
483
544
|
isNewBranch = true;
|
|
484
545
|
}
|
|
485
546
|
else {
|
|
486
|
-
|
|
547
|
+
const { createNew } = await inquirer.prompt([
|
|
548
|
+
{
|
|
549
|
+
type: 'confirm',
|
|
550
|
+
name: 'createNew',
|
|
551
|
+
message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
|
|
552
|
+
default: true,
|
|
553
|
+
},
|
|
554
|
+
]);
|
|
555
|
+
if (createNew) {
|
|
556
|
+
baseRef = defaultBranch;
|
|
557
|
+
isNewBranch = true;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
487
562
|
}
|
|
488
563
|
}
|
|
489
564
|
}
|
|
@@ -539,16 +614,26 @@ async function resolveBranchInfo(rootDir, sourceType, selection, directBranch, d
|
|
|
539
614
|
}
|
|
540
615
|
return { targetBranch, baseRef, isNewBranch };
|
|
541
616
|
}
|
|
542
|
-
async function selectTargetDir(rootDir, targetBranch) {
|
|
617
|
+
async function selectTargetDir(rootDir, targetBranch, flags) {
|
|
543
618
|
const defaultDirName = `worktrees/${targetBranch.split('/').join('-')}`;
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
619
|
+
let dirName;
|
|
620
|
+
if (flags.dir) {
|
|
621
|
+
dirName = flags.dir;
|
|
622
|
+
}
|
|
623
|
+
else if (flags.yes) {
|
|
624
|
+
dirName = defaultDirName;
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
const result = await inquirer.prompt([
|
|
628
|
+
{
|
|
629
|
+
type: 'input',
|
|
630
|
+
name: 'dirName',
|
|
631
|
+
message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
|
|
632
|
+
default: defaultDirName,
|
|
633
|
+
},
|
|
634
|
+
]);
|
|
635
|
+
dirName = result.dirName;
|
|
636
|
+
}
|
|
552
637
|
const targetDir = path.resolve(rootDir, dirName);
|
|
553
638
|
if (fs.existsSync(targetDir)) {
|
|
554
639
|
console.error(chalk.red(`目录 ${targetDir} 已存在!`));
|
|
@@ -594,7 +679,9 @@ async function setupWorktreeEnv(rootDir, targetDir, dirName) {
|
|
|
594
679
|
}
|
|
595
680
|
}
|
|
596
681
|
}
|
|
597
|
-
async function installDependencies(targetDir) {
|
|
682
|
+
async function installDependencies(targetDir, skip = false) {
|
|
683
|
+
if (skip)
|
|
684
|
+
return;
|
|
598
685
|
if (!fs.existsSync(path.join(targetDir, 'package.json')))
|
|
599
686
|
return;
|
|
600
687
|
try {
|
|
@@ -615,7 +702,20 @@ function hasCommand(cmd) {
|
|
|
615
702
|
return false;
|
|
616
703
|
}
|
|
617
704
|
}
|
|
618
|
-
async function openInIDE(targetDir) {
|
|
705
|
+
async function openInIDE(targetDir, flags) {
|
|
706
|
+
if (flags.noEditor)
|
|
707
|
+
return;
|
|
708
|
+
if (flags.editor !== undefined) {
|
|
709
|
+
if (flags.editor === 'none' || flags.editor === '')
|
|
710
|
+
return;
|
|
711
|
+
try {
|
|
712
|
+
execSync(`${flags.editor} "${targetDir}"`, { stdio: 'ignore' });
|
|
713
|
+
}
|
|
714
|
+
catch (e) {
|
|
715
|
+
void e;
|
|
716
|
+
}
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
619
719
|
const editors = [];
|
|
620
720
|
if (hasCommand('trae'))
|
|
621
721
|
editors.push({ name: `在 Trae 中打开 (trae ${targetDir})`, value: 'trae' });
|
|
@@ -644,34 +744,63 @@ async function openInIDE(targetDir) {
|
|
|
644
744
|
void e;
|
|
645
745
|
}
|
|
646
746
|
}
|
|
647
|
-
async function deleteWorktree(ctx) {
|
|
648
|
-
const
|
|
649
|
-
const
|
|
747
|
+
async function deleteWorktree(ctx, targets = []) {
|
|
748
|
+
const { rootDir, flags } = ctx;
|
|
749
|
+
const worktrees = getWorktreeList(rootDir);
|
|
750
|
+
const choices = getDeletableWorktrees(rootDir, worktrees);
|
|
650
751
|
if (choices.length === 0) {
|
|
651
|
-
|
|
752
|
+
if (flags.json) {
|
|
753
|
+
console.info(JSON.stringify({ ok: true, data: [], message: 'No deletable worktrees' }));
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'));
|
|
757
|
+
}
|
|
652
758
|
return;
|
|
653
759
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
760
|
+
let targetPaths;
|
|
761
|
+
if (targets.length > 0) {
|
|
762
|
+
// Non-interactive: resolve each target to a worktree path
|
|
763
|
+
targetPaths = [];
|
|
764
|
+
for (const key of targets) {
|
|
765
|
+
const wt = resolveWorktree(rootDir, key);
|
|
766
|
+
if (!wt) {
|
|
767
|
+
console.error(chalk.red(`未找到 worktree: ${key}`));
|
|
768
|
+
process.exit(1);
|
|
769
|
+
}
|
|
770
|
+
if (path.resolve(wt.path) === path.resolve(rootDir)) {
|
|
771
|
+
console.error(chalk.red(`不能删除主 worktree: ${key}`));
|
|
772
|
+
process.exit(1);
|
|
773
|
+
}
|
|
774
|
+
targetPaths.push(wt.path);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
else {
|
|
778
|
+
// Interactive: checkbox prompt
|
|
779
|
+
const result = await inquirer.prompt([
|
|
780
|
+
{
|
|
781
|
+
type: 'checkbox',
|
|
782
|
+
name: 'targetPaths',
|
|
783
|
+
message: '请选择要删除的 Worktree:',
|
|
784
|
+
choices,
|
|
785
|
+
validate: (answer) => (answer.length > 0 ? true : '请至少选择一个'),
|
|
786
|
+
},
|
|
787
|
+
]);
|
|
788
|
+
targetPaths = result.targetPaths;
|
|
789
|
+
}
|
|
790
|
+
if (!flags.yes) {
|
|
791
|
+
const { confirmDelete } = await inquirer.prompt([
|
|
792
|
+
{
|
|
793
|
+
type: 'confirm',
|
|
794
|
+
name: 'confirmDelete',
|
|
795
|
+
message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
|
|
796
|
+
default: false,
|
|
797
|
+
},
|
|
798
|
+
]);
|
|
799
|
+
if (!confirmDelete)
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
673
802
|
for (const targetPath of targetPaths) {
|
|
674
|
-
await deleteSingleWorktree(
|
|
803
|
+
await deleteSingleWorktree(rootDir, targetPath, flags);
|
|
675
804
|
}
|
|
676
805
|
}
|
|
677
806
|
function getWorktreeList(rootDir) {
|
|
@@ -690,29 +819,55 @@ function getDeletableWorktrees(rootDir, worktrees) {
|
|
|
690
819
|
return { name: `${wt.branch || 'HEAD'} (${relativePath})`, value: wt.path };
|
|
691
820
|
});
|
|
692
821
|
}
|
|
693
|
-
async function deleteSingleWorktree(rootDir, targetPath) {
|
|
822
|
+
async function deleteSingleWorktree(rootDir, targetPath, flags) {
|
|
694
823
|
try {
|
|
695
824
|
gitOrThrow(rootDir, ['worktree', 'remove', targetPath], 'WORKTREE_REMOVE');
|
|
696
|
-
|
|
825
|
+
if (flags.json) {
|
|
826
|
+
console.info(JSON.stringify({ ok: true, removed: targetPath }));
|
|
827
|
+
}
|
|
828
|
+
else {
|
|
829
|
+
console.info(chalk.green(`成功删除: ${targetPath}`));
|
|
830
|
+
}
|
|
697
831
|
}
|
|
698
832
|
catch (e) {
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
833
|
+
if (flags.force) {
|
|
834
|
+
try {
|
|
835
|
+
gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE');
|
|
836
|
+
if (flags.json) {
|
|
837
|
+
console.info(JSON.stringify({ ok: true, removed: targetPath, forced: true }));
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
console.info(chalk.green(`成功强制删除: ${targetPath}`));
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
catch (forceErr) {
|
|
844
|
+
if (flags.json) {
|
|
845
|
+
console.error(JSON.stringify({ ok: false, error: errMsg(forceErr), path: targetPath }));
|
|
846
|
+
}
|
|
847
|
+
else {
|
|
848
|
+
console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`));
|
|
849
|
+
}
|
|
850
|
+
}
|
|
713
851
|
}
|
|
714
|
-
|
|
715
|
-
console.error(chalk.red(
|
|
852
|
+
else {
|
|
853
|
+
console.error(chalk.red(`删除失败: ${errMsg(e)}`));
|
|
854
|
+
const { force } = await inquirer.prompt([
|
|
855
|
+
{
|
|
856
|
+
type: 'confirm',
|
|
857
|
+
name: 'force',
|
|
858
|
+
message: '删除失败 (可能有未提交的更改). 强制删除吗?',
|
|
859
|
+
default: false,
|
|
860
|
+
},
|
|
861
|
+
]);
|
|
862
|
+
if (!force)
|
|
863
|
+
return;
|
|
864
|
+
try {
|
|
865
|
+
gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE');
|
|
866
|
+
console.info(chalk.green(`成功强制删除: ${targetPath}`));
|
|
867
|
+
}
|
|
868
|
+
catch (forceErr) {
|
|
869
|
+
console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`));
|
|
870
|
+
}
|
|
716
871
|
}
|
|
717
872
|
}
|
|
718
873
|
}
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fatdoge/wtree",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "CLI + UI tool for managing git worktrees",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"git",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"api",
|
|
31
31
|
"src",
|
|
32
32
|
"shared",
|
|
33
|
+
"skills",
|
|
33
34
|
"index.html",
|
|
34
35
|
"vite.config.ts",
|
|
35
36
|
"tailwind.config.js",
|
|
@@ -37,6 +38,19 @@
|
|
|
37
38
|
"README.md",
|
|
38
39
|
"LICENSE"
|
|
39
40
|
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"client:dev": "vite",
|
|
43
|
+
"build:ui": "vite build",
|
|
44
|
+
"build:cli": "tsc -p tsconfig.node.json",
|
|
45
|
+
"build": "pnpm run build:cli && pnpm run build:ui",
|
|
46
|
+
"lint": "eslint .",
|
|
47
|
+
"preview": "vite preview",
|
|
48
|
+
"check": "tsc --noEmit && tsc -p tsconfig.node.json --noEmit",
|
|
49
|
+
"server:dev": "nodemon",
|
|
50
|
+
"dev": "concurrently \"pnpm run client:dev\" \"pnpm run server:dev\"",
|
|
51
|
+
"wtree": "tsx api/cli/wtree.ts",
|
|
52
|
+
"test": "vitest run"
|
|
53
|
+
},
|
|
40
54
|
"dependencies": {
|
|
41
55
|
"@vitejs/plugin-react": "^4.4.1",
|
|
42
56
|
"autoprefixer": "^10.4.21",
|
|
@@ -82,18 +96,5 @@
|
|
|
82
96
|
"typescript": "~5.8.3",
|
|
83
97
|
"typescript-eslint": "^8.30.1",
|
|
84
98
|
"vitest": "^2.1.9"
|
|
85
|
-
},
|
|
86
|
-
"scripts": {
|
|
87
|
-
"client:dev": "vite",
|
|
88
|
-
"build:ui": "vite build",
|
|
89
|
-
"build:cli": "tsc -p tsconfig.node.json",
|
|
90
|
-
"build": "pnpm run build:cli && pnpm run build:ui",
|
|
91
|
-
"lint": "eslint .",
|
|
92
|
-
"preview": "vite preview",
|
|
93
|
-
"check": "tsc --noEmit && tsc -p tsconfig.node.json --noEmit",
|
|
94
|
-
"server:dev": "nodemon",
|
|
95
|
-
"dev": "concurrently \"pnpm run client:dev\" \"pnpm run server:dev\"",
|
|
96
|
-
"wtree": "tsx api/cli/wtree.ts",
|
|
97
|
-
"test": "vitest run"
|
|
98
99
|
}
|
|
99
|
-
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wtree
|
|
3
|
+
description: "Manage git worktrees using the wtree CLI. Use when the user wants to create, list, delete, open, lock, unlock, or prune git worktrees, or work with multiple branches simultaneously."
|
|
4
|
+
allowed-tools: Bash
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# wtree - Git Worktree Manager
|
|
9
|
+
|
|
10
|
+
You manage git worktrees using the `wtree` CLI tool. Always use non-interactive flags so commands complete without user input.
|
|
11
|
+
|
|
12
|
+
## Prerequisites
|
|
13
|
+
|
|
14
|
+
The `wtree` CLI must be installed globally (`npm install -g @fatdoge/wtree`) or run via `npx @fatdoge/wtree`. If working inside the wtree project itself, use `node dist-node/api/cli/wtree.js`.
|
|
15
|
+
|
|
16
|
+
## Important Rules
|
|
17
|
+
|
|
18
|
+
1. **Always use `--json` flag** when you need to parse output programmatically
|
|
19
|
+
2. **Always use `--yes`** to skip all confirmation prompts
|
|
20
|
+
3. **Always use `--no-editor`** unless the user explicitly asks to open an editor
|
|
21
|
+
4. **Always use `--no-install`** unless the user explicitly asks to install dependencies
|
|
22
|
+
5. **Use `--repo <path>`** if the current working directory is not inside the target git repository
|
|
23
|
+
6. **Never run `wtree` without a subcommand** — that enters interactive mode
|
|
24
|
+
|
|
25
|
+
## Commands Reference
|
|
26
|
+
|
|
27
|
+
### List Worktrees
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
wtree list --json
|
|
31
|
+
# With specific repo:
|
|
32
|
+
wtree list --json --repo /path/to/repo
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Returns a JSON array of worktree objects:
|
|
36
|
+
```json
|
|
37
|
+
[
|
|
38
|
+
{
|
|
39
|
+
"id": "<base64url-encoded-path>",
|
|
40
|
+
"path": "/absolute/path/to/worktree",
|
|
41
|
+
"head": "<commit-sha>",
|
|
42
|
+
"branch": "branch-name",
|
|
43
|
+
"isMain": true,
|
|
44
|
+
"isLocked": false
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Create Worktree
|
|
50
|
+
|
|
51
|
+
For an **existing** local or remote branch:
|
|
52
|
+
```bash
|
|
53
|
+
wtree create <branch-name> --yes --no-editor --no-install --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a **new branch** based on a reference:
|
|
57
|
+
```bash
|
|
58
|
+
wtree create <new-branch-name> --base <base-ref> --yes --no-editor --no-install --json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
With a specific target directory:
|
|
62
|
+
```bash
|
|
63
|
+
wtree create <branch> --dir <relative-path> --yes --no-editor --no-install --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
- `<branch-name>` (positional, required): The branch to check out or create
|
|
68
|
+
- `--dir <path>`: Worktree directory, relative to repo root (default: `worktrees/<branch-sanitized>`)
|
|
69
|
+
- `--base <ref>`: Base reference for new branch creation (e.g., `main`, `origin/main`)
|
|
70
|
+
- `--yes`: Auto-confirm new branch creation and accept default directory
|
|
71
|
+
- `--editor <name>`: Open in specific editor after creation (`trae`, `cursor`, `code`)
|
|
72
|
+
- `--no-editor`: Do not open any editor
|
|
73
|
+
- `--no-install`: Skip automatic dependency installation
|
|
74
|
+
- `--json`: Output result as JSON
|
|
75
|
+
|
|
76
|
+
Returns on success:
|
|
77
|
+
```json
|
|
78
|
+
{"ok": true, "data": {"id": "...", "path": "...", "head": "...", "branch": "...", "isMain": false, "isLocked": false}}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Delete Worktree
|
|
82
|
+
|
|
83
|
+
Delete one or more worktrees by branch name or path:
|
|
84
|
+
```bash
|
|
85
|
+
wtree delete <branch-or-path> --yes --json
|
|
86
|
+
wtree delete <branch1> <branch2> --yes --json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Force delete (even with uncommitted changes):
|
|
90
|
+
```bash
|
|
91
|
+
wtree delete <branch> --yes --force --json
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Parameters:
|
|
95
|
+
- Positional args: Worktree identifiers (branch name, path, or directory basename)
|
|
96
|
+
- `--yes`: Skip deletion confirmation
|
|
97
|
+
- `--force`: Force-delete even if there are uncommitted changes
|
|
98
|
+
- `--json`: Output result as JSON
|
|
99
|
+
|
|
100
|
+
### Open Worktree
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
wtree open <branch-or-path>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Opens the worktree directory in the system file manager.
|
|
107
|
+
|
|
108
|
+
### Lock / Unlock Worktree
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
wtree lock <branch-or-path>
|
|
112
|
+
wtree unlock <branch-or-path>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Prune Invalid Worktrees
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
wtree prune
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Removes worktree records for directories that no longer exist.
|
|
122
|
+
|
|
123
|
+
### Configuration
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
wtree config # Show all config as JSON
|
|
127
|
+
wtree config get <key> # Get a single config value
|
|
128
|
+
wtree config set <key> <value> # Set a config value
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Available config keys: `baseDir`, `openCommand`, `editorCommand`
|
|
132
|
+
|
|
133
|
+
## Workflow Examples
|
|
134
|
+
|
|
135
|
+
### Create a worktree for a new feature branch
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# 1. List existing worktrees
|
|
139
|
+
wtree list --json
|
|
140
|
+
|
|
141
|
+
# 2. Create worktree with new branch from main
|
|
142
|
+
wtree create feature/my-feature --base main --yes --no-editor --no-install --json
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Clean up old worktrees
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# 1. List all worktrees
|
|
149
|
+
wtree list --json
|
|
150
|
+
|
|
151
|
+
# 2. Delete unwanted ones
|
|
152
|
+
wtree delete feature/old-branch --yes --json
|
|
153
|
+
|
|
154
|
+
# 3. Prune stale records
|
|
155
|
+
wtree prune
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Create worktree for an existing remote branch
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
wtree create feature/existing-branch --yes --no-editor --no-install --json
|
|
162
|
+
```
|