@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 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 a worktree (interactive selection, force deletion supported)
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
- console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`))
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
- console.info(chalk.green(`\n准备创建 Worktree:`))
436
- console.info(` 分支: ${targetBranch}`)
437
- console.info(` 目录: ${targetDir}`)
438
- console.info(` 来源: ${baseRef || 'Existing Local'}`)
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
- const { createNew } = await inquirer.prompt([
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
- process.exit(1)
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
- const { dirName } = await inquirer.prompt([
618
- {
619
- type: 'input',
620
- name: 'dirName',
621
- message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
622
- default: defaultDirName,
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 worktrees = getWorktreeList(ctx.rootDir)
734
- const choices = getDeletableWorktrees(ctx.rootDir, worktrees)
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
- console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'))
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
- const { targetPaths } = await inquirer.prompt([
741
- {
742
- type: 'checkbox',
743
- name: 'targetPaths',
744
- message: '请选择要删除的 Worktree:',
745
- choices,
746
- validate: (answer: string[]) => (answer.length > 0 ? true : '请至少选择一个'),
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(ctx.rootDir, targetPath)
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
- console.info(chalk.green(`成功删除: ${targetPath}`))
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
- console.error(chalk.red(`删除失败: ${errMsg(e)}`))
788
- const { force } = await inquirer.prompt([
789
- {
790
- type: 'confirm',
791
- name: 'force',
792
- message: '删除失败 (可能有未提交的更改). 强制删除吗?',
793
- default: false,
794
- },
795
- ])
796
- if (!force) return
797
- try {
798
- gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE')
799
- console.info(chalk.green(`成功强制删除: ${targetPath}`))
800
- } catch (forceErr: unknown) {
801
- console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`))
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
- console.info(chalk.blue(`检测到git repo根目录 ${rootDir},将在这里运行git命令`));
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
- console.info(chalk.green(`\n准备创建 Worktree:`));
385
- console.info(` 分支: ${targetBranch}`);
386
- console.info(` 目录: ${targetDir}`);
387
- console.info(` 来源: ${baseRef || 'Existing Local'}`);
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
- const { createNew } = await inquirer.prompt([
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
- process.exit(1);
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
- const { dirName } = await inquirer.prompt([
545
- {
546
- type: 'input',
547
- name: 'dirName',
548
- message: `请输入 Worktree 目录路径 (相对于 Git 根目录, 默认: ${defaultDirName}):`,
549
- default: defaultDirName,
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 worktrees = getWorktreeList(ctx.rootDir);
649
- const choices = getDeletableWorktrees(ctx.rootDir, worktrees);
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
- console.warn(chalk.yellow('没有可删除的 Worktree (除了主 Worktree)'));
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
- const { targetPaths } = await inquirer.prompt([
655
- {
656
- type: 'checkbox',
657
- name: 'targetPaths',
658
- message: '请选择要删除的 Worktree:',
659
- choices,
660
- validate: (answer) => (answer.length > 0 ? true : '请至少选择一个'),
661
- },
662
- ]);
663
- const { confirmDelete } = await inquirer.prompt([
664
- {
665
- type: 'confirm',
666
- name: 'confirmDelete',
667
- message: `确定要删除这 ${targetPaths.length} 个 Worktree 吗?`,
668
- default: false,
669
- },
670
- ]);
671
- if (!confirmDelete)
672
- return;
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(ctx.rootDir, targetPath);
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
- console.info(chalk.green(`成功删除: ${targetPath}`));
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
- console.error(chalk.red(`删除失败: ${errMsg(e)}`));
700
- const { force } = await inquirer.prompt([
701
- {
702
- type: 'confirm',
703
- name: 'force',
704
- message: '删除失败 (可能有未提交的更改). 强制删除吗?',
705
- default: false,
706
- },
707
- ]);
708
- if (!force)
709
- return;
710
- try {
711
- gitOrThrow(rootDir, ['worktree', 'remove', '--force', targetPath], 'WORKTREE_REMOVE_FORCE');
712
- console.info(chalk.green(`成功强制删除: ${targetPath}`));
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
- catch (forceErr) {
715
- console.error(chalk.red(`强制删除也失败了: ${errMsg(forceErr)}`));
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.1.10",
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
+ ```