@fatdoge/wtree 0.1.10 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +66 -1
- package/README.md +60 -1
- package/api/cli/wtree.ts +248 -78
- package/dist-node/api/cli/wtree.js +266 -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
|
@@ -6,6 +6,7 @@ import inquirer from 'inquirer'
|
|
|
6
6
|
import chalk from 'chalk'
|
|
7
7
|
import { execSync } from 'node:child_process'
|
|
8
8
|
import fs from 'node:fs'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
9
10
|
import { getRepoRoot } from '../core/git.js'
|
|
10
11
|
import { git, gitOrThrow } from '../core/git.js'
|
|
11
12
|
import { listWorktrees, parseWorktreePorcelain } from '../core/worktree.js'
|
|
@@ -13,8 +14,25 @@ import { openPath } from '../core/open.js'
|
|
|
13
14
|
import { readConfig, writeConfig, getConfigPaths } from '../core/config.js'
|
|
14
15
|
import { startUiDevServer } from '../ui/startUiDev.js'
|
|
15
16
|
|
|
17
|
+
type ParsedFlags = {
|
|
18
|
+
ui: boolean
|
|
19
|
+
noOpen: boolean
|
|
20
|
+
repo: string
|
|
21
|
+
port: number | undefined
|
|
22
|
+
json: boolean
|
|
23
|
+
yes: boolean
|
|
24
|
+
force: boolean
|
|
25
|
+
dir: string
|
|
26
|
+
base: string
|
|
27
|
+
editor: string | undefined
|
|
28
|
+
noEditor: boolean
|
|
29
|
+
noInstall: boolean
|
|
30
|
+
version: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
type Ctx = {
|
|
17
34
|
rootDir: string
|
|
35
|
+
flags: ParsedFlags
|
|
18
36
|
}
|
|
19
37
|
|
|
20
38
|
type SourceSelection =
|
|
@@ -27,17 +45,45 @@ type SourceSelection =
|
|
|
27
45
|
type SourceType = SourceSelection['type']
|
|
28
46
|
type CommandType = 'list' | 'create' | 'delete' | 'open' | 'config' | 'help' | 'interactive' | 'prune' | 'lock' | 'unlock'
|
|
29
47
|
|
|
48
|
+
function getVersion(): string {
|
|
49
|
+
try {
|
|
50
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
51
|
+
// 从 api/cli/ 或 dist-node/api/cli/ 向上查找 package.json
|
|
52
|
+
let dir = __dirname
|
|
53
|
+
for (let i = 0; i < 5; i++) {
|
|
54
|
+
const pkgPath = path.join(dir, 'package.json')
|
|
55
|
+
if (fs.existsSync(pkgPath)) {
|
|
56
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'))
|
|
57
|
+
return pkg.version || 'unknown'
|
|
58
|
+
}
|
|
59
|
+
dir = path.dirname(dir)
|
|
60
|
+
}
|
|
61
|
+
return 'unknown'
|
|
62
|
+
} catch {
|
|
63
|
+
return 'unknown'
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
30
67
|
function errMsg(e: unknown) {
|
|
31
68
|
return e instanceof Error ? e.message : String(e)
|
|
32
69
|
}
|
|
33
70
|
|
|
34
71
|
function parseArgs(argv: string[]) {
|
|
35
72
|
const args = [...argv]
|
|
36
|
-
const flags = {
|
|
73
|
+
const flags: ParsedFlags = {
|
|
37
74
|
ui: false,
|
|
38
75
|
noOpen: false,
|
|
39
76
|
repo: '',
|
|
40
77
|
port: undefined as number | undefined,
|
|
78
|
+
json: false,
|
|
79
|
+
yes: false,
|
|
80
|
+
force: false,
|
|
81
|
+
dir: '',
|
|
82
|
+
base: '',
|
|
83
|
+
editor: undefined,
|
|
84
|
+
noEditor: false,
|
|
85
|
+
noInstall: false,
|
|
86
|
+
version: false,
|
|
41
87
|
}
|
|
42
88
|
const positional: string[] = []
|
|
43
89
|
|
|
@@ -60,6 +106,15 @@ function parseArgs(argv: string[]) {
|
|
|
60
106
|
if (Number.isFinite(v)) flags.port = v
|
|
61
107
|
continue
|
|
62
108
|
}
|
|
109
|
+
if (a === '--json') { flags.json = true; continue }
|
|
110
|
+
if (a === '--yes' || a === '-y') { flags.yes = true; continue }
|
|
111
|
+
if (a === '--force' || a === '-f') { flags.force = true; continue }
|
|
112
|
+
if (a === '--dir') { flags.dir = String(args.shift() || ''); continue }
|
|
113
|
+
if (a === '--base') { flags.base = String(args.shift() || ''); continue }
|
|
114
|
+
if (a === '--editor') { flags.editor = String(args.shift() || ''); continue }
|
|
115
|
+
if (a === '--no-editor') { flags.noEditor = true; continue }
|
|
116
|
+
if (a === '--no-install') { flags.noInstall = true; continue }
|
|
117
|
+
if (a === '--version' || a === '-v') { flags.version = true; continue }
|
|
63
118
|
if (a.startsWith('--')) continue
|
|
64
119
|
positional.push(a)
|
|
65
120
|
}
|
|
@@ -99,8 +154,12 @@ function parseCommand(positional: string[]) {
|
|
|
99
154
|
return { command: 'interactive' as CommandType, rest: positional }
|
|
100
155
|
}
|
|
101
156
|
|
|
102
|
-
function printWorktreeList(rootDir: string) {
|
|
157
|
+
function printWorktreeList(rootDir: string, json = false) {
|
|
103
158
|
const items = listWorktrees(rootDir)
|
|
159
|
+
if (json) {
|
|
160
|
+
console.info(JSON.stringify(items, null, 2))
|
|
161
|
+
return
|
|
162
|
+
}
|
|
104
163
|
if (items.length === 0) {
|
|
105
164
|
console.info('未读取到 worktree。')
|
|
106
165
|
return
|
|
@@ -118,7 +177,7 @@ function printHelp() {
|
|
|
118
177
|
console.info(' wtree')
|
|
119
178
|
console.info(' wtree list')
|
|
120
179
|
console.info(' wtree create [branch]')
|
|
121
|
-
console.info(' wtree delete')
|
|
180
|
+
console.info(' wtree delete [branch|path ...]')
|
|
122
181
|
console.info(' wtree open [path|branch]')
|
|
123
182
|
console.info(' wtree lock [path|branch]')
|
|
124
183
|
console.info(' wtree unlock [path|branch]')
|
|
@@ -127,8 +186,25 @@ function printHelp() {
|
|
|
127
186
|
console.info(' wtree config get <key>')
|
|
128
187
|
console.info(' wtree config set <key> <value>')
|
|
129
188
|
console.info(' wtree --ui [--repo <path>] [--no-open] [--port <number>]')
|
|
189
|
+
console.info(' wtree --version, -v')
|
|
190
|
+
console.info('')
|
|
191
|
+
console.info('选项:')
|
|
192
|
+
console.info(' --json 以 JSON 格式输出 (适合脚本/agent 使用)')
|
|
193
|
+
console.info(' --yes, -y 自动确认所有提示')
|
|
194
|
+
console.info(' --force, -f 强制操作 (如强制删除有未提交更改的 worktree)')
|
|
195
|
+
console.info(' --dir <path> 指定 worktree 目录路径 (相对于 git 根目录)')
|
|
196
|
+
console.info(' --base <ref> 创建新分支时的基准引用 (如 main, origin/main)')
|
|
197
|
+
console.info(' --editor <name> 创建后使用指定编辑器打开 (trae, cursor, code, none)')
|
|
198
|
+
console.info(' --no-editor 创建后不打开编辑器')
|
|
199
|
+
console.info(' --no-install 创建后不自动安装依赖')
|
|
130
200
|
console.info('')
|
|
131
201
|
console.info('可用配置 key: baseDir, openCommand, editorCommand')
|
|
202
|
+
console.info('')
|
|
203
|
+
console.info('非交互示例:')
|
|
204
|
+
console.info(' wtree list --json')
|
|
205
|
+
console.info(' wtree create feat/x --yes --no-editor --no-install --json')
|
|
206
|
+
console.info(' wtree create feat/new --base main --yes --dir worktrees/feat-new --json')
|
|
207
|
+
console.info(' wtree delete feat/old --yes --force --json')
|
|
132
208
|
}
|
|
133
209
|
|
|
134
210
|
function resolveWorktree(rootDir: string, key: string) {
|
|
@@ -291,6 +367,12 @@ async function pruneWorktrees(rootDir: string) {
|
|
|
291
367
|
|
|
292
368
|
async function main() {
|
|
293
369
|
const { flags, positional } = parseArgs(process.argv.slice(2))
|
|
370
|
+
|
|
371
|
+
if (flags.version || positional[0] === 'version') {
|
|
372
|
+
console.info(getVersion())
|
|
373
|
+
return
|
|
374
|
+
}
|
|
375
|
+
|
|
294
376
|
const cwd = flags.repo ? path.resolve(flags.repo) : process.cwd()
|
|
295
377
|
const rootDir = getRepoRoot(cwd)
|
|
296
378
|
|
|
@@ -312,21 +394,23 @@ async function main() {
|
|
|
312
394
|
return
|
|
313
395
|
}
|
|
314
396
|
|
|
315
|
-
|
|
397
|
+
if (!flags.json) {
|
|
398
|
+
console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`))
|
|
399
|
+
}
|
|
316
400
|
|
|
317
401
|
const { command, rest } = parseCommand(positional)
|
|
318
402
|
if (command === 'list') {
|
|
319
|
-
printWorktreeList(rootDir)
|
|
403
|
+
printWorktreeList(rootDir, flags.json)
|
|
320
404
|
return
|
|
321
405
|
}
|
|
322
406
|
|
|
323
407
|
if (command === 'create') {
|
|
324
|
-
await createWorktree({ rootDir }, rest[0])
|
|
408
|
+
await createWorktree({ rootDir, flags }, rest[0])
|
|
325
409
|
return
|
|
326
410
|
}
|
|
327
411
|
|
|
328
412
|
if (command === 'delete') {
|
|
329
|
-
await deleteWorktree({ rootDir })
|
|
413
|
+
await deleteWorktree({ rootDir, flags }, rest)
|
|
330
414
|
return
|
|
331
415
|
}
|
|
332
416
|
|
|
@@ -376,7 +460,7 @@ async function main() {
|
|
|
376
460
|
const directBranch = rest[0]
|
|
377
461
|
|
|
378
462
|
const action = await getUserAction(directBranch)
|
|
379
|
-
const ctx: Ctx = { rootDir }
|
|
463
|
+
const ctx: Ctx = { rootDir, flags }
|
|
380
464
|
if (action === 'create') {
|
|
381
465
|
await createWorktree(ctx, directBranch)
|
|
382
466
|
} else if (action === 'delete') {
|
|
@@ -416,7 +500,7 @@ async function getUserAction(directBranch?: string) {
|
|
|
416
500
|
}
|
|
417
501
|
|
|
418
502
|
async function createWorktree(ctx: Ctx, directBranch?: string) {
|
|
419
|
-
const { rootDir } = ctx
|
|
503
|
+
const { rootDir, flags } = ctx
|
|
420
504
|
const defaultBranch =
|
|
421
505
|
git(rootDir, ['symbolic-ref', '--short', 'refs/remotes/origin/HEAD']).stdout
|
|
422
506
|
.replace(/^origin\//, '')
|
|
@@ -429,13 +513,16 @@ async function createWorktree(ctx: Ctx, directBranch?: string) {
|
|
|
429
513
|
selection,
|
|
430
514
|
directBranch,
|
|
431
515
|
defaultBranch,
|
|
516
|
+
flags,
|
|
432
517
|
)
|
|
433
|
-
const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch)
|
|
518
|
+
const { targetDir, dirName } = await selectTargetDir(rootDir, targetBranch, flags)
|
|
434
519
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
520
|
+
if (!flags.json) {
|
|
521
|
+
console.info(chalk.green(`\n准备创建 Worktree:`))
|
|
522
|
+
console.info(` 分支: ${targetBranch}`)
|
|
523
|
+
console.info(` 目录: ${targetDir}`)
|
|
524
|
+
console.info(` 来源: ${baseRef || 'Existing Local'}`)
|
|
525
|
+
}
|
|
439
526
|
|
|
440
527
|
await createGitWorktree(
|
|
441
528
|
rootDir,
|
|
@@ -448,8 +535,14 @@ async function createWorktree(ctx: Ctx, directBranch?: string) {
|
|
|
448
535
|
)
|
|
449
536
|
|
|
450
537
|
await setupWorktreeEnv(rootDir, targetDir, dirName)
|
|
451
|
-
await installDependencies(targetDir)
|
|
452
|
-
await openInIDE(targetDir)
|
|
538
|
+
await installDependencies(targetDir, flags.noInstall)
|
|
539
|
+
await openInIDE(targetDir, flags)
|
|
540
|
+
|
|
541
|
+
if (flags.json) {
|
|
542
|
+
const items = listWorktrees(rootDir)
|
|
543
|
+
const created = items.find(x => path.resolve(x.path) === path.resolve(targetDir))
|
|
544
|
+
console.info(JSON.stringify({ ok: true, data: created || null }))
|
|
545
|
+
}
|
|
453
546
|
}
|
|
454
547
|
|
|
455
548
|
async function selectSource(rootDir: string, directBranch: string | undefined, defaultBranch: string) {
|
|
@@ -503,6 +596,7 @@ async function resolveBranchInfo(
|
|
|
503
596
|
selection: SourceSelection,
|
|
504
597
|
directBranch: string | undefined,
|
|
505
598
|
defaultBranch: string,
|
|
599
|
+
flags: ParsedFlags,
|
|
506
600
|
) {
|
|
507
601
|
let targetBranch = ''
|
|
508
602
|
let baseRef = ''
|
|
@@ -541,19 +635,24 @@ async function resolveBranchInfo(
|
|
|
541
635
|
baseRef = `origin/${targetBranch}`
|
|
542
636
|
isNewBranch = true
|
|
543
637
|
} else {
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
type: 'confirm',
|
|
547
|
-
name: 'createNew',
|
|
548
|
-
message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
|
|
549
|
-
default: true,
|
|
550
|
-
},
|
|
551
|
-
])
|
|
552
|
-
if (createNew) {
|
|
553
|
-
baseRef = defaultBranch
|
|
638
|
+
if (flags.yes) {
|
|
639
|
+
baseRef = flags.base || defaultBranch
|
|
554
640
|
isNewBranch = true
|
|
555
641
|
} else {
|
|
556
|
-
|
|
642
|
+
const { createNew } = await inquirer.prompt([
|
|
643
|
+
{
|
|
644
|
+
type: 'confirm',
|
|
645
|
+
name: 'createNew',
|
|
646
|
+
message: `分支 ${targetBranch} 不存在。是否基于 ${defaultBranch} 创建新分支?`,
|
|
647
|
+
default: true,
|
|
648
|
+
},
|
|
649
|
+
])
|
|
650
|
+
if (createNew) {
|
|
651
|
+
baseRef = defaultBranch
|
|
652
|
+
isNewBranch = true
|
|
653
|
+
} else {
|
|
654
|
+
process.exit(1)
|
|
655
|
+
}
|
|
557
656
|
}
|
|
558
657
|
}
|
|
559
658
|
}
|
|
@@ -612,16 +711,25 @@ async function resolveBranchInfo(
|
|
|
612
711
|
return { targetBranch, baseRef, isNewBranch }
|
|
613
712
|
}
|
|
614
713
|
|
|
615
|
-
async function selectTargetDir(rootDir: string, targetBranch: string) {
|
|
714
|
+
async function selectTargetDir(rootDir: string, targetBranch: string, flags: ParsedFlags) {
|
|
616
715
|
const defaultDirName = `worktrees/${targetBranch.split('/').join('-')}`
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
716
|
+
|
|
717
|
+
let dirName: string
|
|
718
|
+
if (flags.dir) {
|
|
719
|
+
dirName = flags.dir
|
|
720
|
+
} else if (flags.yes) {
|
|
721
|
+
dirName = defaultDirName
|
|
722
|
+
} else {
|
|
723
|
+
const result = await inquirer.prompt([
|
|
724
|
+
{
|
|
725
|
+
type: 'input',
|
|
726
|
+
name: 'dirName',
|
|
727
|
+
message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
|
|
728
|
+
default: defaultDirName,
|
|
729
|
+
},
|
|
730
|
+
])
|
|
731
|
+
dirName = result.dirName
|
|
732
|
+
}
|
|
625
733
|
|
|
626
734
|
const targetDir = path.resolve(rootDir, dirName)
|
|
627
735
|
if (fs.existsSync(targetDir)) {
|
|
@@ -682,7 +790,8 @@ async function setupWorktreeEnv(rootDir: string, targetDir: string, dirName: str
|
|
|
682
790
|
}
|
|
683
791
|
}
|
|
684
792
|
|
|
685
|
-
async function installDependencies(targetDir: string) {
|
|
793
|
+
async function installDependencies(targetDir: string, skip = false) {
|
|
794
|
+
if (skip) return
|
|
686
795
|
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return
|
|
687
796
|
try {
|
|
688
797
|
execSync('pnpm --version', { stdio: 'ignore' })
|
|
@@ -702,7 +811,18 @@ function hasCommand(cmd: string) {
|
|
|
702
811
|
}
|
|
703
812
|
}
|
|
704
813
|
|
|
705
|
-
async function openInIDE(targetDir: string) {
|
|
814
|
+
async function openInIDE(targetDir: string, flags: ParsedFlags) {
|
|
815
|
+
if (flags.noEditor) return
|
|
816
|
+
if (flags.editor !== undefined) {
|
|
817
|
+
if (flags.editor === 'none' || flags.editor === '') return
|
|
818
|
+
try {
|
|
819
|
+
execSync(`${flags.editor} "${targetDir}"`, { stdio: 'ignore' })
|
|
820
|
+
} catch (e: unknown) {
|
|
821
|
+
void e
|
|
822
|
+
}
|
|
823
|
+
return
|
|
824
|
+
}
|
|
825
|
+
|
|
706
826
|
const editors: { name: string; value: string }[] = []
|
|
707
827
|
if (hasCommand('trae')) editors.push({ name: `在 Trae 中打开 (trae ${targetDir})`, value: 'trae' })
|
|
708
828
|
if (hasCommand('cursor')) editors.push({ name: `在 Cursor 中打开 (cursor ${targetDir})`, value: 'cursor' })
|
|
@@ -729,35 +849,64 @@ async function openInIDE(targetDir: string) {
|
|
|
729
849
|
}
|
|
730
850
|
}
|
|
731
851
|
|
|
732
|
-
async function deleteWorktree(ctx: Ctx) {
|
|
733
|
-
const
|
|
734
|
-
const
|
|
852
|
+
async function deleteWorktree(ctx: Ctx, targets: string[] = []) {
|
|
853
|
+
const { rootDir, flags } = ctx
|
|
854
|
+
const worktrees = getWorktreeList(rootDir)
|
|
855
|
+
const choices = getDeletableWorktrees(rootDir, worktrees)
|
|
735
856
|
if (choices.length === 0) {
|
|
736
|
-
|
|
857
|
+
if (flags.json) {
|
|
858
|
+
console.info(JSON.stringify({ ok: true, data: [], message: 'No deletable worktrees' }))
|
|
859
|
+
} else {
|
|
860
|
+
console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'))
|
|
861
|
+
}
|
|
737
862
|
return
|
|
738
863
|
}
|
|
739
864
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
865
|
+
let targetPaths: string[]
|
|
866
|
+
|
|
867
|
+
if (targets.length > 0) {
|
|
868
|
+
// Non-interactive: resolve each target to a worktree path
|
|
869
|
+
targetPaths = []
|
|
870
|
+
for (const key of targets) {
|
|
871
|
+
const wt = resolveWorktree(rootDir, key)
|
|
872
|
+
if (!wt) {
|
|
873
|
+
console.error(chalk.red(`未找到 worktree: ${key}`))
|
|
874
|
+
process.exit(1)
|
|
875
|
+
}
|
|
876
|
+
if (path.resolve(wt.path) === path.resolve(rootDir)) {
|
|
877
|
+
console.error(chalk.red(`不能删除主 worktree: ${key}`))
|
|
878
|
+
process.exit(1)
|
|
879
|
+
}
|
|
880
|
+
targetPaths.push(wt.path)
|
|
881
|
+
}
|
|
882
|
+
} else {
|
|
883
|
+
// Interactive: checkbox prompt
|
|
884
|
+
const result = await inquirer.prompt([
|
|
885
|
+
{
|
|
886
|
+
type: 'checkbox',
|
|
887
|
+
name: 'targetPaths',
|
|
888
|
+
message: '请选择要删除的 Worktree:',
|
|
889
|
+
choices,
|
|
890
|
+
validate: (answer: string[]) => (answer.length > 0 ? true : '请至少选择一个'),
|
|
891
|
+
},
|
|
892
|
+
])
|
|
893
|
+
targetPaths = result.targetPaths
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
if (!flags.yes) {
|
|
897
|
+
const { confirmDelete } = await inquirer.prompt([
|
|
898
|
+
{
|
|
899
|
+
type: 'confirm',
|
|
900
|
+
name: 'confirmDelete',
|
|
901
|
+
message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
|
|
902
|
+
default: false,
|
|
903
|
+
},
|
|
904
|
+
])
|
|
905
|
+
if (!confirmDelete) return
|
|
906
|
+
}
|
|
749
907
|
|
|
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
908
|
for (const targetPath of targetPaths) {
|
|
760
|
-
await deleteSingleWorktree(
|
|
909
|
+
await deleteSingleWorktree(rootDir, targetPath, flags)
|
|
761
910
|
}
|
|
762
911
|
}
|
|
763
912
|
|
|
@@ -779,26 +928,47 @@ function getDeletableWorktrees(rootDir: string, worktrees: { path: string; branc
|
|
|
779
928
|
})
|
|
780
929
|
}
|
|
781
930
|
|
|
782
|
-
async function deleteSingleWorktree(rootDir: string, targetPath: string) {
|
|
931
|
+
async function deleteSingleWorktree(rootDir: string, targetPath: string, flags: ParsedFlags) {
|
|
783
932
|
try {
|
|
784
933
|
gitOrThrow(rootDir, ['worktree', 'remove', targetPath], 'WORKTREE_REMOVE')
|
|
785
|
-
|
|
934
|
+
if (flags.json) {
|
|
935
|
+
console.info(JSON.stringify({ ok: true, removed: targetPath }))
|
|
936
|
+
} else {
|
|
937
|
+
console.info(chalk.green(`成功删除: ${targetPath}`))
|
|
938
|
+
}
|
|
786
939
|
} catch (e: unknown) {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
940
|
+
if (flags.force) {
|
|
941
|
+
try {
|
|
942
|
+
gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE')
|
|
943
|
+
if (flags.json) {
|
|
944
|
+
console.info(JSON.stringify({ ok: true, removed: targetPath, forced: true }))
|
|
945
|
+
} else {
|
|
946
|
+
console.info(chalk.green(`成功强制删除: ${targetPath}`))
|
|
947
|
+
}
|
|
948
|
+
} catch (forceErr: unknown) {
|
|
949
|
+
if (flags.json) {
|
|
950
|
+
console.error(JSON.stringify({ ok: false, error: errMsg(forceErr), path: targetPath }))
|
|
951
|
+
} else {
|
|
952
|
+
console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`))
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
} else {
|
|
956
|
+
console.error(chalk.red(`删除失败: ${errMsg(e)}`))
|
|
957
|
+
const { force } = await inquirer.prompt([
|
|
958
|
+
{
|
|
959
|
+
type: 'confirm',
|
|
960
|
+
name: 'force',
|
|
961
|
+
message: '删除失败 (可能有未提交的更改). 强制删除吗?',
|
|
962
|
+
default: false,
|
|
963
|
+
},
|
|
964
|
+
])
|
|
965
|
+
if (!force) return
|
|
966
|
+
try {
|
|
967
|
+
gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE')
|
|
968
|
+
console.info(chalk.green(`成功强制删除: ${targetPath}`))
|
|
969
|
+
} catch (forceErr: unknown) {
|
|
970
|
+
console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`))
|
|
971
|
+
}
|
|
802
972
|
}
|
|
803
973
|
}
|
|
804
974
|
}
|