@fatdoge/wtree 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +66 -1
- package/README.md +60 -1
- package/api/cli/wtree.ts +218 -78
- package/dist/assets/index-C78PMV-C.js +179 -0
- package/dist/assets/index-CR9jga1C.css +1 -0
- package/dist/index.html +2 -2
- package/dist-node/api/cli/wtree.js +236 -81
- package/package.json +3 -1
- package/skills/wtree/SKILL.md +162 -0
- package/src/App.tsx +2 -2
- package/src/pages/CreateWorktree.tsx +4 -5
- package/src/pages/SettingsPage.tsx +10 -7
- package/src/pages/Worktrees.tsx +13 -29
- package/dist/assets/index-AspflbWf.js +0 -179
- package/dist/assets/index-DXiZ6dVD.css +0 -1
- package/src/components/ToastHost.tsx +0 -41
- package/src/stores/toastStore.ts +0 -29
|
@@ -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
|
+
```
|
package/src/App.tsx
CHANGED
|
@@ -4,7 +4,7 @@ import Worktrees from "@/pages/Worktrees";
|
|
|
4
4
|
import CreateWorktree from "@/pages/CreateWorktree";
|
|
5
5
|
import SettingsPage from "@/pages/SettingsPage";
|
|
6
6
|
import HelpPage from "@/pages/HelpPage";
|
|
7
|
-
import
|
|
7
|
+
import { Toaster } from "sonner";
|
|
8
8
|
import { useThemeStore } from "@/stores/themeStore";
|
|
9
9
|
|
|
10
10
|
export default function App() {
|
|
@@ -16,7 +16,7 @@ export default function App() {
|
|
|
16
16
|
|
|
17
17
|
return (
|
|
18
18
|
<Router>
|
|
19
|
-
<
|
|
19
|
+
<Toaster position="top-right" richColors theme="system" />
|
|
20
20
|
<Routes>
|
|
21
21
|
<Route path="/" element={<Worktrees />} />
|
|
22
22
|
<Route path="/create" element={<CreateWorktree />} />
|
|
@@ -4,9 +4,9 @@ import { Link, useNavigate } from 'react-router-dom'
|
|
|
4
4
|
import { useTranslation } from 'react-i18next'
|
|
5
5
|
import Button from '@/components/Button'
|
|
6
6
|
import Input from '@/components/Input'
|
|
7
|
-
import { useToastStore } from '@/stores/toastStore'
|
|
8
7
|
import { useWorktreeStore } from '@/stores/worktreeStore'
|
|
9
8
|
import type { CreateWorktreeRequest } from '../../shared/wtui-types'
|
|
9
|
+
import { toast } from 'sonner'
|
|
10
10
|
|
|
11
11
|
type Mode = 'existing' | 'new'
|
|
12
12
|
|
|
@@ -16,7 +16,6 @@ export default function CreateWorktree() {
|
|
|
16
16
|
const create = useWorktreeStore((s) => s.create)
|
|
17
17
|
const branches = useWorktreeStore((s) => s.branches)
|
|
18
18
|
const fetchBranches = useWorktreeStore((s) => s.fetchBranches)
|
|
19
|
-
const toast = useToastStore((s) => s.push)
|
|
20
19
|
|
|
21
20
|
useEffect(() => {
|
|
22
21
|
fetchBranches()
|
|
@@ -51,14 +50,14 @@ export default function CreateWorktree() {
|
|
|
51
50
|
}
|
|
52
51
|
const created = await create(payload)
|
|
53
52
|
if (!created) {
|
|
54
|
-
toast(
|
|
53
|
+
toast.error('创建失败')
|
|
55
54
|
return
|
|
56
55
|
}
|
|
57
56
|
setResultPath(created.path)
|
|
58
|
-
toast(
|
|
57
|
+
toast.success('创建成功')
|
|
59
58
|
} catch (e: unknown) {
|
|
60
59
|
const msg = e instanceof Error ? e.message : String(e)
|
|
61
|
-
toast(
|
|
60
|
+
toast.error('创建失败', { description: msg })
|
|
62
61
|
} finally {
|
|
63
62
|
setSubmitting(false)
|
|
64
63
|
}
|
|
@@ -4,15 +4,14 @@ import { Link } from 'react-router-dom'
|
|
|
4
4
|
import { useTranslation } from 'react-i18next'
|
|
5
5
|
import Button from '@/components/Button'
|
|
6
6
|
import Input from '@/components/Input'
|
|
7
|
-
import { useToastStore } from '@/stores/toastStore'
|
|
8
7
|
import { useWorktreeStore } from '@/stores/worktreeStore'
|
|
9
8
|
import { useThemeStore } from '@/stores/themeStore'
|
|
10
9
|
import { apiGet, apiPut } from '@/utils/api'
|
|
11
10
|
import type { WtuiConfig } from '../../shared/wtui-types'
|
|
11
|
+
import { toast } from 'sonner'
|
|
12
12
|
|
|
13
13
|
export default function SettingsPage() {
|
|
14
14
|
const { t, i18n } = useTranslation()
|
|
15
|
-
const toast = useToastStore((s) => s.push)
|
|
16
15
|
const prune = useWorktreeStore((s) => s.prune)
|
|
17
16
|
const { theme, setTheme } = useThemeStore()
|
|
18
17
|
const [cfg, setCfg] = useState<WtuiConfig>({})
|
|
@@ -34,11 +33,11 @@ export default function SettingsPage() {
|
|
|
34
33
|
const r = await apiPut<WtuiConfig, WtuiConfig>('/api/config', cfg)
|
|
35
34
|
if (!r.ok) {
|
|
36
35
|
const msg = (r as { ok: false; error: { message: string } }).error.message
|
|
37
|
-
toast(
|
|
36
|
+
toast.error(t('settings.toast.saveFailed'), { description: msg })
|
|
38
37
|
return
|
|
39
38
|
}
|
|
40
39
|
setCfg(r.data)
|
|
41
|
-
toast(
|
|
40
|
+
toast.success(t('settings.toast.saveSuccess'))
|
|
42
41
|
} finally {
|
|
43
42
|
setLoading(false)
|
|
44
43
|
}
|
|
@@ -51,11 +50,11 @@ export default function SettingsPage() {
|
|
|
51
50
|
const r = await apiPut<WtuiConfig, WtuiConfig>('/api/config', {})
|
|
52
51
|
if (!r.ok) {
|
|
53
52
|
const msg = (r as { ok: false; error: { message: string } }).error.message
|
|
54
|
-
toast(
|
|
53
|
+
toast.error(t('settings.toast.resetFailed', '重置失败'), { description: msg })
|
|
55
54
|
return
|
|
56
55
|
}
|
|
57
56
|
setCfg(r.data)
|
|
58
|
-
toast(
|
|
57
|
+
toast.info(t('settings.toast.resetSuccess', '已重置'))
|
|
59
58
|
} finally {
|
|
60
59
|
setLoading(false)
|
|
61
60
|
}
|
|
@@ -65,7 +64,11 @@ export default function SettingsPage() {
|
|
|
65
64
|
setPruning(true)
|
|
66
65
|
try {
|
|
67
66
|
const ok = await prune()
|
|
68
|
-
|
|
67
|
+
if (ok) {
|
|
68
|
+
toast.success(t('settings.toast.pruneSuccess'))
|
|
69
|
+
} else {
|
|
70
|
+
toast.error(t('settings.toast.pruneFailed'))
|
|
71
|
+
}
|
|
69
72
|
} finally {
|
|
70
73
|
setPruning(false)
|
|
71
74
|
}
|
package/src/pages/Worktrees.tsx
CHANGED
|
@@ -4,8 +4,8 @@ import { Link } from 'react-router-dom'
|
|
|
4
4
|
import { useTranslation } from 'react-i18next'
|
|
5
5
|
import Button from '@/components/Button'
|
|
6
6
|
import Modal from '@/components/Modal'
|
|
7
|
-
import { useToastStore } from '@/stores/toastStore'
|
|
8
7
|
import { useWorktreeStore } from '@/stores/worktreeStore'
|
|
8
|
+
import { toast } from 'sonner'
|
|
9
9
|
|
|
10
10
|
function truncatePath(p: string) {
|
|
11
11
|
if (p.length <= 70) return p
|
|
@@ -23,7 +23,6 @@ export default function Worktrees() {
|
|
|
23
23
|
const open = useWorktreeStore((s) => s.open)
|
|
24
24
|
const lock = useWorktreeStore((s) => s.lock)
|
|
25
25
|
const unlock = useWorktreeStore((s) => s.unlock)
|
|
26
|
-
const toast = useToastStore((s) => s.push)
|
|
27
26
|
|
|
28
27
|
const [removeId, setRemoveId] = useState<string | null>(null)
|
|
29
28
|
const [forceDelete, setForceDelete] = useState(false)
|
|
@@ -47,24 +46,20 @@ export default function Worktrees() {
|
|
|
47
46
|
try {
|
|
48
47
|
const res = await remove(removeId, forceDelete)
|
|
49
48
|
if (res.success) {
|
|
50
|
-
toast(
|
|
49
|
+
toast.success(forceDelete ? t('worktrees.toast.forceDeleteSuccess') : t('worktrees.toast.deleteSuccess'))
|
|
51
50
|
closeModal()
|
|
52
51
|
} else {
|
|
53
52
|
const msg = res.error || ''
|
|
54
53
|
if (msg.toLowerCase().includes('force') || msg.includes('modified') || msg.includes('untracked')) {
|
|
55
54
|
setForceDelete(true)
|
|
56
|
-
toast({
|
|
57
|
-
type: 'error',
|
|
58
|
-
title: t('worktrees.toast.deleteFailed'),
|
|
59
|
-
detail: t('worktrees.toast.deleteWarning'),
|
|
60
|
-
})
|
|
55
|
+
toast.error(t('worktrees.toast.deleteFailed'), { description: t('worktrees.toast.deleteWarning') })
|
|
61
56
|
} else {
|
|
62
|
-
toast(
|
|
57
|
+
toast.error(t('worktrees.toast.deleteFailed'), { description: msg })
|
|
63
58
|
}
|
|
64
59
|
}
|
|
65
60
|
} catch (e: unknown) {
|
|
66
61
|
const msg = e instanceof Error ? e.message : String(e)
|
|
67
|
-
toast(
|
|
62
|
+
toast.error(t('worktrees.toast.deleteFailed'), { description: msg })
|
|
68
63
|
} finally {
|
|
69
64
|
setIsDeleting(false)
|
|
70
65
|
}
|
|
@@ -73,9 +68,9 @@ export default function Worktrees() {
|
|
|
73
68
|
const onCopy = async (text: string) => {
|
|
74
69
|
try {
|
|
75
70
|
await navigator.clipboard.writeText(text)
|
|
76
|
-
toast(
|
|
71
|
+
toast.success(t('worktrees.toast.copySuccess'))
|
|
77
72
|
} catch {
|
|
78
|
-
toast(
|
|
73
|
+
toast.error(t('worktrees.toast.copyFailed'))
|
|
79
74
|
}
|
|
80
75
|
}
|
|
81
76
|
|
|
@@ -177,10 +172,9 @@ export default function Worktrees() {
|
|
|
177
172
|
onClick={(e) => {
|
|
178
173
|
e.stopPropagation()
|
|
179
174
|
open(wt.id).then((ok) =>
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}),
|
|
175
|
+
ok
|
|
176
|
+
? toast.success(t('worktrees.toast.folderSuccess'))
|
|
177
|
+
: toast.error(t('worktrees.toast.folderFailed')),
|
|
184
178
|
)
|
|
185
179
|
}}
|
|
186
180
|
>
|
|
@@ -255,10 +249,7 @@ export default function Worktrees() {
|
|
|
255
249
|
const msg = selected.isLocked ? t('worktrees.toast.unlockSuccess') : t('worktrees.toast.lockSuccess')
|
|
256
250
|
const msgFail = selected.isLocked ? t('worktrees.toast.unlockFailed') : t('worktrees.toast.lockFailed')
|
|
257
251
|
action(selected.id).then((ok) =>
|
|
258
|
-
toast(
|
|
259
|
-
type: ok ? 'success' : 'error',
|
|
260
|
-
title: ok ? msg : msgFail,
|
|
261
|
-
}),
|
|
252
|
+
ok ? toast.success(msg) : toast.error(msgFail),
|
|
262
253
|
)
|
|
263
254
|
}}
|
|
264
255
|
>
|
|
@@ -271,10 +262,7 @@ export default function Worktrees() {
|
|
|
271
262
|
size="sm"
|
|
272
263
|
onClick={() =>
|
|
273
264
|
open(selected.id, 'editor').then((ok) =>
|
|
274
|
-
toast(
|
|
275
|
-
type: ok ? 'success' : 'error',
|
|
276
|
-
title: ok ? t('worktrees.toast.ideSuccess') : t('worktrees.toast.ideFailed'),
|
|
277
|
-
}),
|
|
265
|
+
ok ? toast.success(t('worktrees.toast.ideSuccess')) : toast.error(t('worktrees.toast.ideFailed')),
|
|
278
266
|
)
|
|
279
267
|
}
|
|
280
268
|
>
|
|
@@ -287,10 +275,7 @@ export default function Worktrees() {
|
|
|
287
275
|
size="sm"
|
|
288
276
|
onClick={() =>
|
|
289
277
|
open(selected.id, 'folder').then((ok) =>
|
|
290
|
-
toast(
|
|
291
|
-
type: ok ? 'success' : 'error',
|
|
292
|
-
title: ok ? t('worktrees.toast.folderSuccess') : t('worktrees.toast.folderFailed'),
|
|
293
|
-
}),
|
|
278
|
+
ok ? toast.success(t('worktrees.toast.folderSuccess')) : toast.error(t('worktrees.toast.folderFailed')),
|
|
294
279
|
)
|
|
295
280
|
}
|
|
296
281
|
>
|
|
@@ -351,4 +336,3 @@ export default function Worktrees() {
|
|
|
351
336
|
</div>
|
|
352
337
|
)
|
|
353
338
|
}
|
|
354
|
-
|