@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.
@@ -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 ToastHost from "@/components/ToastHost";
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
- <ToastHost />
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({ type: 'error', title: '创建失败' })
53
+ toast.error('创建失败')
55
54
  return
56
55
  }
57
56
  setResultPath(created.path)
58
- toast({ type: 'success', title: '创建成功' })
57
+ toast.success('创建成功')
59
58
  } catch (e: unknown) {
60
59
  const msg = e instanceof Error ? e.message : String(e)
61
- toast({ type: 'error', title: '创建失败', detail: msg })
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({ type: 'error', title: t('settings.toast.saveFailed'), detail: msg })
36
+ toast.error(t('settings.toast.saveFailed'), { description: msg })
38
37
  return
39
38
  }
40
39
  setCfg(r.data)
41
- toast({ type: 'success', title: t('settings.toast.saveSuccess') })
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({ type: 'error', title: t('settings.toast.resetFailed', '重置失败'), detail: msg })
53
+ toast.error(t('settings.toast.resetFailed', '重置失败'), { description: msg })
55
54
  return
56
55
  }
57
56
  setCfg(r.data)
58
- toast({ type: 'info', title: t('settings.toast.resetSuccess', '已重置') })
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
- toast({ type: ok ? 'success' : 'error', title: ok ? t('settings.toast.pruneSuccess') : t('settings.toast.pruneFailed') })
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
  }
@@ -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({ type: 'success', title: forceDelete ? t('worktrees.toast.forceDeleteSuccess') : t('worktrees.toast.deleteSuccess') })
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({ type: 'error', title: t('worktrees.toast.deleteFailed'), detail: msg })
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({ type: 'error', title: t('worktrees.toast.deleteFailed'), detail: msg })
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({ type: 'success', title: t('worktrees.toast.copySuccess') })
71
+ toast.success(t('worktrees.toast.copySuccess'))
77
72
  } catch {
78
- toast({ type: 'error', title: t('worktrees.toast.copyFailed') })
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
- toast({
181
- type: ok ? 'success' : 'error',
182
- title: ok ? t('worktrees.toast.folderSuccess') : t('worktrees.toast.folderFailed'),
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
-