@cestoliv/wt 0.5.0-pr24.g7ccdc37 → 0.5.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.md CHANGED
@@ -37,8 +37,7 @@ infer from this project. Write the result to the config file (find its path with
37
37
  wt # Browse worktrees (interactive TUI)
38
38
  wt create my-feat # New worktree, opens your IDE
39
39
  wt agent my-feat "Plan the feature" # New worktree + AI agent in Zed (macOS)
40
- wt agent fix-bug "Fix bug" --mode auto # Use auto mode instead of the default
41
- wt prune # Remove merged worktrees (per-branch confirm)
40
+ wt agent fix-bug "Fix bug" --mode auto # Use auto mode instead of plan
42
41
  wt config # Edit config in $EDITOR
43
42
  wt skill # Print the skill file (for AI agents)
44
43
  ```
@@ -52,15 +51,14 @@ wt agent refactor "Refactor API layer" --mode default
52
51
  ```
53
52
 
54
53
  Creates a worktree exactly like `wt create`, then auto-starts your agent
55
- (default `claude`, run with `--permission-mode default`) in Zed's integrated
56
- terminal — pre-filled with your prompt and left interactive for you to take over.
54
+ (default `claude --permission-mode plan`) in Zed's integrated terminal —
55
+ pre-filled with your prompt and left interactive for you to take over.
57
56
 
58
- **Available modes** (`--mode`, defaults to `default`; change the default with
59
- the `agent_mode` config key):
57
+ **Available modes** (`--mode`, defaults to `plan`):
60
58
 
61
- - `default` — Standard interactive mode with approval for each action (default)
59
+ - `default` — Standard interactive mode with approval for each action
62
60
  - `acceptEdits` — Allow file changes but keep command execution controlled
63
- - `plan` — Architecture-first mode with no surprise mutations
61
+ - `plan` — Architecture-first mode with no surprise mutations (default)
64
62
  - `auto` — Claude's safety model makes decisions instead of prompting
65
63
  - `dontAsk` — Minimal interruptions in trusted environments
66
64
  - `bypassPermissions` — Skip all permission checks (dangerous, CI/sandbox only)
@@ -87,12 +85,12 @@ An interactive, fuzzy-searchable list of your worktrees:
87
85
 
88
86
  ```
89
87
  MY-PROJECT
90
- ▶ main (main) ~/dev/my-project
88
+ ▶ main ~/dev/my-project
91
89
  fix: resolve auth bug (2h ago)
92
90
  feat/dashboard ~/dev/my-project-feat-dashboard
93
91
  wip: add chart component (1d ago)
94
92
 
95
- ↕ navigate · Enter open · D delete · P prune · C create · A agent · Q quit
93
+ ↕ navigate · Enter open · D delete · C create · A agent · Q quit
96
94
  ```
97
95
 
98
96
  Type to fuzzy-filter branches instantly. Inside a repo it shows that repo's
@@ -106,12 +104,8 @@ and a permission mode — **worktree (repo → branch) → plan prompt → permi
106
104
  mode**. Pressing `Esc` steps back to the previous question (answers preserved),
107
105
  or back to the list from the first step. After creating, the list **refreshes
108
106
  and stays open** (preserving your search and cursor) instead of exiting — only
109
- `Enter` and `Q`/`Esc` leave the TUI. `P` prunes every worktree whose branch has
110
- already been merged (see [`wt prune`](#prune--wt-prune) below). Note that
111
- `a`/`c`/`d`/`p` are command keys, so they can't be typed into the search box.
112
-
113
- The main worktree is tagged `(main)` and is protected — `D` only removes linked
114
- worktrees, never the main repository.
107
+ `Enter` and `Q`/`Esc` leave the TUI. Note that `a`/`c`/`d` are command keys, so
108
+ they can't be typed into the search box.
115
109
 
116
110
  ## Create — `wt create [branch]`
117
111
 
@@ -127,29 +121,6 @@ registered repos.
127
121
  If the path already exists, `wt create` offers to open it in your IDE instead of
128
122
  erroring (in a non-interactive shell it exits non-zero).
129
123
 
130
- ## Prune — `wt prune`
131
-
132
- ```bash
133
- wt prune # remove every merged worktree, one confirmation per branch
134
- ```
135
-
136
- Cleans up the worktrees you're done with: it finds every worktree whose branch
137
- has already been merged into the base branch (`base_branch`, default
138
- `origin/main`) and removes it — **always confirming each branch individually**,
139
- and force-confirming when git refuses (submodules or uncommitted changes), just
140
- like a manual `D` delete. The branch itself stays; only the worktree is removed.
141
- Your `teardown_commands` run before each removal.
142
-
143
- Merge detection covers both **fast-forward / merge-commit** merges (the branch
144
- tip is an ancestor of the base branch) and patch-id matches (via `git cherry`,
145
- so a single-commit branch **squash-merged** through a PR is still detected); a
146
- worktree still sitting exactly on the base commit is never offered. `wt prune`
147
- best-effort fetches
148
- the remote first; if the base ref can't be resolved (offline, missing), it
149
- removes nothing. The TUI exposes the same action under the `P` key. Works in
150
- repo mode and across all registered repos in global mode (each against its own
151
- `base_branch`).
152
-
153
124
  ## Configuration
154
125
 
155
126
  Edit with `wt config` (`wt config --path` prints the file location —
@@ -163,10 +134,8 @@ Edit with `wt config` (`wt config --path` prints the file location —
163
134
  | `worktree_path` | `"../"` | Where worktrees are placed (relative to repo) |
164
135
  | `setup_commands` | `[]` | Commands to run in new worktrees |
165
136
  | `teardown_commands` | `[]` | Commands to run in a worktree just before it is deleted (e.g. `["docker compose down -v"]`) |
166
- | `agent_command` | `"claude"` | Base command; `--permission-mode <mode>` injected, then prompt appended |
167
- | `agent_mode` | `"default"` | Default permission mode for `wt agent` (overridden by `--mode`) |
137
+ | `agent_command` | `"claude --permission-mode plan"` | Base command; `--permission-mode` replaced by `--mode` option, then prompt appended |
168
138
  | `agent_trigger_chord` | `"ctrl-shift-cmd-c"` | Zed keymap chord `wt agent` installs and presses |
169
- | `auto_refresh_minutes`| `5` | How often the interactive list re-fetches worktrees (shows a "last refreshed" header); `0` disables it |
170
139
  | `repo_overrides` | `{}` | Per-repo overrides for any key above |
171
140
 
172
141
  Override any key per repo:
package/SKILL.md CHANGED
@@ -17,8 +17,7 @@ Launch the interactive TUI. Shows worktrees for the current repo (repo mode) or
17
17
 
18
18
  - Arrow keys — navigate
19
19
  - `Enter` — open worktree in IDE (exits the TUI)
20
- - `D` — delete worktree (the main worktree is tagged `(main)` and cannot be deleted — only linked worktrees can)
21
- - `P` — prune all merged worktrees (per-branch confirmation)
20
+ - `D` — delete worktree
22
21
  - `C` — create a new worktree (works in both repo and global mode)
23
22
  - `A` — create a worktree and start an AI agent in it (works in both modes)
24
23
  - type to search · `Backspace` — edit search
@@ -39,8 +38,8 @@ After a create or agent action the TUI **refreshes and stays open** on the list
39
38
  (your search and cursor are preserved) rather than exiting — only `Enter` (open)
40
39
  and `Q`/`Esc` exit.
41
40
 
42
- Because `a`/`A`, `c`/`C`, `d`/`D`, and `p`/`P` are reserved as command keys,
43
- those letters can't be typed into the search box.
41
+ Because `a`/`A`, `c`/`C`, and `d`/`D` are reserved as command keys, those
42
+ letters can't be typed into the search box.
44
43
 
45
44
  ### `wt create [branch]`
46
45
 
@@ -66,12 +65,11 @@ wt agent feature/fix 'Fix the bug in payment processing' --mode auto
66
65
  wt agent refactor/api 'Refactor the API layer' --mode default
67
66
  ```
68
67
 
69
- The `--mode` flag sets Claude Code's permission mode (defaults to `default`;
70
- change the default with the `agent_mode` config key):
68
+ The `--mode` flag sets Claude Code's permission mode (defaults to `plan`):
71
69
 
72
- - `default` — Standard interactive mode with approval for each action (default)
70
+ - `default` — Standard interactive mode with approval for each action
73
71
  - `acceptEdits` — Allow file changes but keep command execution controlled
74
- - `plan` — Architecture-first mode with no surprise mutations
72
+ - `plan` — Architecture-first mode with no surprise mutations (default)
75
73
  - `auto` — Claude's safety model makes decisions instead of prompting
76
74
  - `dontAsk` — Minimal interruptions in trusted environments
77
75
  - `bypassPermissions` — Skip all permission checks (dangerous, CI/sandbox only)
@@ -99,29 +97,6 @@ If the worktree path already exists, `wt agent` prompts you to **open it in the
99
97
  IDE**, **open it and start the agent**, or **quit** — instead of erroring. (In a
100
98
  non-interactive shell it errors with a non-zero exit instead of prompting.)
101
99
 
102
- ### `wt prune`
103
-
104
- Remove every worktree whose branch has already been merged into the base
105
- branch (`base_branch`, default `origin/main`). Each candidate is confirmed
106
- individually — and force-confirmed when git refuses (submodules / uncommitted
107
- changes), exactly like a manual `d` delete. The branch itself is left intact;
108
- only the worktree is removed.
109
-
110
- ```bash
111
- wt prune # review and remove merged worktrees, one prompt per branch
112
- ```
113
-
114
- Merge detection covers two cases: a branch whose tip is an ancestor of the base
115
- branch (a **fast-forward / merge-commit** merge — its commits live verbatim in
116
- base), and a branch whose diff already exists in base by patch id (via
117
- `git cherry`, so a single-commit branch **squash-merged** through a PR is still
118
- recognized). A worktree still sitting exactly on the base commit (no work yet) is
119
- never offered. It also best-effort fetches the remote first so detection sees
120
- up-to-date refs; if the
121
- base ref can't be resolved (e.g. offline), nothing is removed. Works in repo
122
- mode (current repo) and global mode (all registered repos, each against its own
123
- `base_branch`). The TUI exposes the same action under the `p` key.
124
-
125
100
  ### `wt config`
126
101
 
127
102
  Open the global config file in `$EDITOR` (defaults to `nano`).
@@ -149,16 +124,14 @@ Config is stored as JSON. Get the path with `wt config --path`.
149
124
  | `teardown_commands` | `string[]` | `[]` | Commands to run in a worktree just before it is deleted (e.g. `["docker compose down -v"]`); on failure you are prompted whether to delete anyway |
150
125
  | `ide` | `string` | `"zed"` | IDE command to open worktrees with |
151
126
  | `ide_open_args` | `string[]` | `["-n"]` | Arguments passed to the IDE command |
152
- | `agent_command` | `string` | `"claude"` | Base command `wt agent` runs in Zed; `--permission-mode <mode>` is injected (any existing one replaced), then `<plan_prompt>` is appended single-quoted |
153
- | `agent_mode` | `string` | `"default"` | Default Claude Code permission mode for `wt agent`; the `--mode` flag overrides it. One of `default`, `acceptEdits`, `plan`, `auto`, `dontAsk`, `bypassPermissions` |
127
+ | `agent_command` | `string` | `"claude --permission-mode plan"` | Base command `wt agent` runs in Zed; any `--permission-mode` flag is replaced by the `--mode` option (defaults to `plan`), then `<plan_prompt>` is appended single-quoted |
154
128
  | `agent_trigger_chord` | `string` | `"ctrl-shift-cmd-c"` | Zed keymap chord `wt agent` installs/presses to spawn the agent task |
155
- | `auto_refresh_minutes`| `number` | `5` | How often the interactive list (`wt`) re-fetches worktrees and updates the "last refreshed" header; `0` disables auto-refresh |
156
129
  | `repos` | `string[]` | `[]` | Registered repo paths (auto-populated on first use) |
157
130
  | `repo_overrides` | `object` | `{}` | Per-repo config overrides (see below) |
158
131
 
159
132
  ### Per-repo overrides
160
133
 
161
- Override any field (`worktree_path`, `base_branch`, `setup_commands`, `teardown_commands`, `ide`, `ide_open_args`, `agent_command`, `agent_mode`, `agent_trigger_chord`, `auto_refresh_minutes`) for a specific repo:
134
+ Override any field (`worktree_path`, `base_branch`, `setup_commands`, `teardown_commands`, `ide`, `ide_open_args`, `agent_command`, `agent_trigger_chord`) for a specific repo:
162
135
 
163
136
  ```json
164
137
  {
@@ -3,9 +3,9 @@ import {
3
3
  openConfiguredIde,
4
4
  prepareWorktree,
5
5
  promptExistingWorktree
6
- } from "./chunk-S75RK5RZ.js";
7
- import "./chunk-47P7FKFX.js";
8
- import "./chunk-OUWQ6NIV.js";
6
+ } from "./chunk-QGSJG72F.js";
7
+ import "./chunk-GHYUCETL.js";
8
+ import "./chunk-FNAMNRUH.js";
9
9
 
10
10
  // src/commands/agent.ts
11
11
  import * as clack from "@clack/prompts";
@@ -424,30 +424,19 @@ var VALID_MODES = [
424
424
  "dontAsk",
425
425
  "bypassPermissions"
426
426
  ];
427
- var isValidMode = (mode) => VALID_MODES.includes(mode);
428
427
  var CLEANUP_DELAY_MS = 2e4;
429
428
  var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
430
429
  async function createAgentWorktree(branch, planPrompt, options = {}) {
431
- if (options.mode !== void 0 && !isValidMode(options.mode)) {
430
+ const mode = options.mode ?? "plan";
431
+ if (!VALID_MODES.includes(mode)) {
432
432
  console.error(
433
- pc.red(
434
- `Invalid mode "${options.mode}". Valid modes: ${VALID_MODES.join(", ")}`
435
- )
433
+ pc.red(`Invalid mode "${mode}". Valid modes: ${VALID_MODES.join(", ")}`)
436
434
  );
437
435
  process.exit(1);
438
436
  }
439
437
  const prepared = await prepareWorktree(branch, options);
440
438
  if (!prepared) return;
441
439
  const { status, config, worktreePath } = prepared;
442
- let mode = options.mode ?? config.agent_mode ?? "default";
443
- if (!isValidMode(mode)) {
444
- console.warn(
445
- pc.yellow(
446
- `\u26A0 Invalid agent_mode "${mode}" in config; using "default". Valid modes: ${VALID_MODES.join(", ")}`
447
- )
448
- );
449
- mode = "default";
450
- }
451
440
  if (status === "exists") {
452
441
  const prompt = options.existingWorktreePrompt ?? promptExistingWorktree;
453
442
  const action = await prompt(worktreePath, { allowAgent: true });
@@ -68,10 +68,8 @@ var DEFAULT_CONFIG = {
68
68
  teardown_commands: [],
69
69
  ide: "zed",
70
70
  ide_open_args: ["-n"],
71
- agent_command: "claude",
71
+ agent_command: "claude --permission-mode plan",
72
72
  agent_trigger_chord: "ctrl-shift-cmd-c",
73
- auto_refresh_minutes: 5,
74
- agent_mode: "default",
75
73
  repos: [],
76
74
  repo_overrides: {}
77
75
  };
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  createStore,
4
4
  getGlobalConfig
5
- } from "./chunk-OUWQ6NIV.js";
5
+ } from "./chunk-FNAMNRUH.js";
6
6
 
7
7
  // src/lib/git.ts
8
8
  import { execFileSync } from "child_process";
@@ -48,7 +48,7 @@ function listWorktrees(repoRoot, cwd = process.cwd()) {
48
48
  }));
49
49
  }
50
50
  function parseWorktreeList(output, repoRoot, cwd) {
51
- return output.trim().split("\n\n").map((block, index) => {
51
+ return output.trim().split("\n\n").map((block) => {
52
52
  const lines = block.trim().split("\n");
53
53
  const wtPath = lines[0].slice("worktree ".length);
54
54
  const branchLine = lines.find((l) => l.startsWith("branch "));
@@ -57,8 +57,6 @@ function parseWorktreeList(output, repoRoot, cwd) {
57
57
  path: wtPath,
58
58
  branch,
59
59
  isCurrent: cwd === wtPath || cwd.startsWith(wtPath + path.sep),
60
- // The main worktree is always the first entry of `git worktree list`.
61
- isMain: index === 0,
62
60
  repoRoot
63
61
  };
64
62
  });
@@ -79,16 +77,6 @@ function addWorktree(repoRoot, worktreePath, branch, baseBranch) {
79
77
  }
80
78
  }
81
79
  function removeWorktree(repoRoot, worktreePath, force = false) {
82
- const resolve = (p) => {
83
- try {
84
- return realpathSync(p);
85
- } catch {
86
- return p;
87
- }
88
- };
89
- if (resolve(worktreePath) === resolve(repoRoot)) {
90
- throw new Error("Refusing to remove the main worktree");
91
- }
92
80
  try {
93
81
  execFileSync(
94
82
  "git",
@@ -138,40 +126,6 @@ function branchExists(repoRoot, branch) {
138
126
  return false;
139
127
  }
140
128
  }
141
- function isAncestor(repoRoot, ancestor, descendant) {
142
- try {
143
- execFileSync("git", ["merge-base", "--is-ancestor", ancestor, descendant], {
144
- cwd: repoRoot,
145
- stdio: "pipe"
146
- });
147
- return true;
148
- } catch {
149
- return false;
150
- }
151
- }
152
- function isBranchMerged(repoRoot, branch, baseBranch) {
153
- try {
154
- const revParse = (ref) => execFileSync("git", ["rev-parse", ref], {
155
- cwd: repoRoot,
156
- encoding: "utf8",
157
- stdio: "pipe"
158
- }).trim();
159
- const baseTip = revParse(baseBranch);
160
- const tip = revParse(branch);
161
- if (tip !== baseTip && isAncestor(repoRoot, branch, baseBranch)) {
162
- return true;
163
- }
164
- const out = execFileSync("git", ["cherry", baseBranch, branch], {
165
- cwd: repoRoot,
166
- encoding: "utf8",
167
- stdio: "pipe"
168
- });
169
- const lines = out.split("\n").filter((l) => l.trim().length > 0);
170
- return lines.length > 0 && lines.every((l) => l.startsWith("-"));
171
- } catch {
172
- return false;
173
- }
174
- }
175
129
  function setUpstreamTracking(worktreePath, branch, remote = "origin") {
176
130
  try {
177
131
  execFileSync(
@@ -299,24 +253,8 @@ function shortenPath(p) {
299
253
  const home = process.env.HOME ?? "";
300
254
  return home && p.startsWith(home) ? `~${p.slice(home.length)}` : p;
301
255
  }
302
- function formatRefreshStatus(lastRefresh, intervalMinutes) {
303
- const time = lastRefresh.toLocaleTimeString();
304
- return `\u27F3 Last refreshed ${time} \xB7 every ${intervalMinutes}m`;
305
- }
306
- function reconcileSelectedIndex(items, prevPath, prevIndex) {
307
- if (items.length === 0) return 0;
308
- if (prevPath) {
309
- const found = items.findIndex((w) => w.path === prevPath);
310
- if (found !== -1) return found;
311
- }
312
- return Math.min(Math.max(0, prevIndex), items.length - 1);
313
- }
314
- function buildListLayout(items, selectedIndex, query, mode, lastRefresh = null, intervalMinutes = 0) {
256
+ function buildListLayout(items, selectedIndex, query, mode) {
315
257
  const header = [];
316
- if (lastRefresh && intervalMinutes > 0) {
317
- header.push(pc.dim(formatRefreshStatus(lastRefresh, intervalMinutes)));
318
- header.push("");
319
- }
320
258
  if (mode === "global") {
321
259
  header.push(
322
260
  pc.dim("\u2139 Not in a git repository \u2014 showing all registered worktrees")
@@ -333,7 +271,7 @@ function buildListLayout(items, selectedIndex, query, mode, lastRefresh = null,
333
271
  for (const item of groupItems) {
334
272
  const start = body.length;
335
273
  const cursor = i === selectedIndex ? pc.cyan("\u25B6") : " ";
336
- const branchLabel = item.isCurrent ? pc.dim(`${item.branch} (current)`) : item.isMain ? pc.dim(`${item.branch} (main)`) : pc.white(item.branch);
274
+ const branchLabel = item.isCurrent ? pc.dim(`${item.branch} (current)`) : pc.white(item.branch);
337
275
  const pathLabel = pc.dim(shortenPath(item.path));
338
276
  body.push(` ${cursor} ${branchLabel} ${pathLabel}`);
339
277
  if (item.lastCommit) {
@@ -344,33 +282,28 @@ function buildListLayout(items, selectedIndex, query, mode, lastRefresh = null,
344
282
  }
345
283
  }
346
284
  const footer = [
347
- pc.dim(
348
- "\u2195 navigate \xB7 Enter open \xB7 D delete \xB7 P prune \xB7 C create \xB7 A agent \xB7 Q quit"
349
- )
285
+ pc.dim("\u2195 navigate \xB7 Enter open \xB7 D delete \xB7 C create \xB7 A agent \xB7 Q quit")
350
286
  ];
351
287
  return { header, body, footer, itemSpans };
352
288
  }
353
- function clampScroll(offset, span, viewportHeight2, bodyLength) {
354
- const maxOffset = Math.max(0, bodyLength - viewportHeight2);
289
+ function clampScroll(offset, span, viewportHeight, bodyLength) {
290
+ const maxOffset = Math.max(0, bodyLength - viewportHeight);
355
291
  let next = offset;
356
292
  if (span.start < next) next = span.start;
357
- if (span.end >= next + viewportHeight2) next = span.end - viewportHeight2 + 1;
293
+ if (span.end >= next + viewportHeight) next = span.end - viewportHeight + 1;
358
294
  return Math.max(0, Math.min(next, maxOffset));
359
295
  }
360
- function composeView(layout, offset, viewportHeight2) {
296
+ function composeView(layout, offset, viewportHeight) {
361
297
  const { header, body, footer } = layout;
362
- const visible = body.slice(offset, offset + viewportHeight2);
363
- while (visible.length < viewportHeight2) visible.push("");
298
+ const visible = body.slice(offset, offset + viewportHeight);
299
+ while (visible.length < viewportHeight) visible.push("");
364
300
  const topSlot = offset > 0 ? pc.dim(" \u2191 more") : "";
365
- const bottomSlot = offset + viewportHeight2 < body.length ? pc.dim(" \u2193 more") : "";
301
+ const bottomSlot = offset + viewportHeight < body.length ? pc.dim(" \u2193 more") : "";
366
302
  return [...header, topSlot, ...visible, bottomSlot, ...footer].join("\n");
367
303
  }
368
304
  function fixedHeight(layout) {
369
305
  return layout.header.length + layout.footer.length + 2;
370
306
  }
371
- function viewportHeight(layout, rows) {
372
- return Math.max(1, rows - fixedHeight(layout) - 1);
373
- }
374
307
  function setupRawMode() {
375
308
  process.stdin.setRawMode(true);
376
309
  process.stdin.resume();
@@ -381,17 +314,6 @@ function cleanupRawMode() {
381
314
  process.stdin.pause();
382
315
  process.stdout.write("\x1B[2J\x1B[H");
383
316
  }
384
- function waitForKeypress() {
385
- return new Promise((resolve) => {
386
- process.stdin.setRawMode(true);
387
- process.stdin.resume();
388
- process.stdin.once("data", () => {
389
- process.stdin.setRawMode(false);
390
- process.stdin.pause();
391
- resolve();
392
- });
393
- });
394
- }
395
317
  function renderRepoPicker(repos, selectedIndex, query) {
396
318
  const lines = [];
397
319
  lines.push(pc.dim("\u2139 Not in a git repository \u2014 select a repo to create in"));
@@ -540,25 +462,15 @@ async function runWizard(steps) {
540
462
  }
541
463
  return true;
542
464
  }
543
- async function runInteractiveList(allItems, mode, handlers, options = {}) {
544
- const { autoRefreshMinutes = 0 } = options;
545
- const refreshEnabled = Number.isFinite(autoRefreshMinutes) && autoRefreshMinutes > 0;
465
+ async function runInteractiveList(allItems, mode, handlers) {
546
466
  let query = "";
547
467
  let selectedIndex = 0;
548
468
  let scrollOffset = 0;
549
469
  let filtered = allItems;
550
- let lastRefresh = refreshEnabled ? /* @__PURE__ */ new Date() : null;
551
470
  const render = () => {
552
471
  const rows = process.stdout.rows ?? 24;
553
- const layout = buildListLayout(
554
- filtered,
555
- selectedIndex,
556
- query,
557
- mode,
558
- lastRefresh,
559
- autoRefreshMinutes
560
- );
561
- const viewport = viewportHeight(layout, rows);
472
+ const layout = buildListLayout(filtered, selectedIndex, query, mode);
473
+ const viewport = Math.max(1, rows - fixedHeight(layout));
562
474
  const span = layout.itemSpans[selectedIndex] ?? { start: 0, end: 0 };
563
475
  scrollOffset = clampScroll(
564
476
  scrollOffset,
@@ -573,9 +485,6 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
573
485
  render();
574
486
  return new Promise((resolve, reject) => {
575
487
  let listenerActive = false;
576
- let interacting = false;
577
- let refreshing = false;
578
- let refreshTimer = null;
579
488
  const attachListener = () => {
580
489
  if (!listenerActive) {
581
490
  process.stdin.on("data", onData);
@@ -588,35 +497,9 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
588
497
  process.stdout.removeListener("resize", render);
589
498
  listenerActive = false;
590
499
  };
591
- const stopRefresh = () => {
592
- if (refreshTimer !== null) {
593
- clearInterval(refreshTimer);
594
- refreshTimer = null;
595
- }
596
- };
597
- const tick = async () => {
598
- if (interacting || refreshing) return;
599
- refreshing = true;
600
- try {
601
- const prevPath = filtered[selectedIndex]?.path;
602
- allItems = await handlers.refreshItems();
603
- filtered = filterItems(allItems, query);
604
- selectedIndex = reconcileSelectedIndex(
605
- filtered,
606
- prevPath,
607
- selectedIndex
608
- );
609
- lastRefresh = /* @__PURE__ */ new Date();
610
- if (!interacting) render();
611
- } catch {
612
- } finally {
613
- refreshing = false;
614
- }
615
- };
616
500
  const onData = async (key) => {
617
501
  try {
618
502
  if (key === "" || key === "q" || key === "Q" || key === "\x1B") {
619
- stopRefresh();
620
503
  detachListener();
621
504
  cleanupRawMode();
622
505
  resolve();
@@ -629,7 +512,6 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
629
512
  } else if (key === "\r") {
630
513
  const item = filtered[selectedIndex];
631
514
  if (item) {
632
- stopRefresh();
633
515
  detachListener();
634
516
  cleanupRawMode();
635
517
  handlers.onOpen(item);
@@ -638,21 +520,12 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
638
520
  } else if (key === "d" || key === "D") {
639
521
  const item = filtered[selectedIndex];
640
522
  if (item) {
641
- if (item.isMain) {
642
- process.stdout.write(
643
- pc.red(
644
- "\nCannot delete the main repository \u2014 only worktrees can be removed.\n"
645
- )
646
- );
647
- return;
648
- }
649
523
  if (item.isCurrent) {
650
524
  process.stdout.write(
651
525
  pc.red("\nCannot delete the worktree you are currently in.\n")
652
526
  );
653
527
  return;
654
528
  }
655
- interacting = true;
656
529
  detachListener();
657
530
  cleanupRawMode();
658
531
  const confirmed = await handlers.onDelete(item);
@@ -666,32 +539,9 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
666
539
  );
667
540
  setupRawMode();
668
541
  attachListener();
669
- interacting = false;
670
542
  render();
671
543
  }
672
- } else if (key === "p" || key === "P") {
673
- interacting = true;
674
- detachListener();
675
- cleanupRawMode();
676
- const removed = await handlers.onWipe(allItems);
677
- if (removed.length > 0) {
678
- const removedSet = new Set(removed);
679
- allItems = allItems.filter((w) => !removedSet.has(w));
680
- filtered = filtered.filter((w) => !removedSet.has(w));
681
- } else {
682
- process.stdout.write(pc.dim("\nPress any key to continue\u2026"));
683
- await waitForKeypress();
684
- }
685
- selectedIndex = Math.min(
686
- selectedIndex,
687
- Math.max(0, filtered.length - 1)
688
- );
689
- setupRawMode();
690
- attachListener();
691
- interacting = false;
692
- render();
693
544
  } else if (key === "c" || key === "C") {
694
- interacting = true;
695
545
  detachListener();
696
546
  cleanupRawMode();
697
547
  await handlers.onCreate();
@@ -701,13 +551,10 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
701
551
  selectedIndex,
702
552
  Math.max(0, filtered.length - 1)
703
553
  );
704
- lastRefresh = refreshEnabled ? /* @__PURE__ */ new Date() : lastRefresh;
705
554
  setupRawMode();
706
555
  attachListener();
707
- interacting = false;
708
556
  render();
709
557
  } else if (key === "a" || key === "A") {
710
- interacting = true;
711
558
  detachListener();
712
559
  cleanupRawMode();
713
560
  await handlers.onAgent();
@@ -717,10 +564,8 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
717
564
  selectedIndex,
718
565
  Math.max(0, filtered.length - 1)
719
566
  );
720
- lastRefresh = refreshEnabled ? /* @__PURE__ */ new Date() : lastRefresh;
721
567
  setupRawMode();
722
568
  attachListener();
723
- interacting = false;
724
569
  render();
725
570
  } else if (key === "\x7F") {
726
571
  query = query.slice(0, -1);
@@ -736,16 +581,12 @@ async function runInteractiveList(allItems, mode, handlers, options = {}) {
736
581
  render();
737
582
  }
738
583
  } catch (err) {
739
- stopRefresh();
740
584
  detachListener();
741
585
  cleanupRawMode();
742
586
  reject(err);
743
587
  }
744
588
  };
745
589
  attachListener();
746
- if (refreshEnabled) {
747
- refreshTimer = setInterval(tick, autoRefreshMinutes * 6e4);
748
- }
749
590
  });
750
591
  }
751
592
 
@@ -756,7 +597,6 @@ export {
756
597
  removeWorktree,
757
598
  listWorktreeDirtyFiles,
758
599
  branchExists,
759
- isBranchMerged,
760
600
  setUpstreamTracking,
761
601
  fetchRemote,
762
602
  resolveWorktreePath,
@@ -13,11 +13,11 @@ import {
13
13
  runCommands,
14
14
  runRepoPicker,
15
15
  setUpstreamTracking
16
- } from "./chunk-47P7FKFX.js";
16
+ } from "./chunk-GHYUCETL.js";
17
17
  import {
18
18
  createStore,
19
19
  getEffectiveConfig
20
- } from "./chunk-OUWQ6NIV.js";
20
+ } from "./chunk-FNAMNRUH.js";
21
21
 
22
22
  // src/commands/create.ts
23
23
  import { existsSync } from "fs";
package/dist/cli.js CHANGED
@@ -3,40 +3,35 @@
3
3
  // src/cli.ts
4
4
  import { Command } from "commander";
5
5
  var program = new Command();
6
- program.name("wt").description("Git worktree manager").version("0.5.0-pr24.g7ccdc37").action(async () => {
7
- const { runList } = await import("./list-TTNX2HBM.js");
6
+ program.name("wt").description("Git worktree manager").version("0.5.0").action(async () => {
7
+ const { runList } = await import("./list-XHV4ODXW.js");
8
8
  await runList();
9
9
  });
10
10
  program.command("create [branch]").description("Create a new worktree").action(async (branch) => {
11
- const { createWorktree } = await import("./create-L3JURY7V.js");
11
+ const { createWorktree } = await import("./create-XKF574AL.js");
12
12
  await createWorktree(branch);
13
13
  });
14
14
  program.command("agent <branch> <plan_prompt>").description("Create a worktree and auto-start an AI agent in Zed (macOS)").option(
15
15
  "--mode <mode>",
16
- "Claude Code permission mode (default, plan, auto, etc.); overrides the configured agent_mode"
16
+ "Claude Code permission mode (default, plan, auto, etc.)",
17
+ "plan"
17
18
  ).action(
18
19
  async (branch, planPrompt, options) => {
19
- const { createAgentWorktree } = await import("./agent-CSBQ4HUU.js");
20
+ const { createAgentWorktree } = await import("./agent-Z3YCY245.js");
20
21
  await createAgentWorktree(branch, planPrompt, { mode: options.mode });
21
22
  }
22
23
  );
23
- program.command("prune").description(
24
- "Remove worktrees whose branch has been merged into the base branch"
25
- ).action(async () => {
26
- const { runPrune } = await import("./prune-NAGNNGQ5.js");
27
- await runPrune();
28
- });
29
24
  program.command("config").description("Open the config file in $EDITOR").option("--path", "Print the config file path and exit").action(async (options) => {
30
25
  if (options.path) {
31
- const { printConfigPath } = await import("./config-QSYG3JDC.js");
26
+ const { printConfigPath } = await import("./config-RFATE2PF.js");
32
27
  printConfigPath();
33
28
  } else {
34
- const { openConfig } = await import("./config-QSYG3JDC.js");
29
+ const { openConfig } = await import("./config-RFATE2PF.js");
35
30
  openConfig();
36
31
  }
37
32
  });
38
33
  program.command("skill").description("Print the wt skill file to stdout").action(async () => {
39
- const { printSkill } = await import("./skill-GZVGKD5Z.js");
34
+ const { printSkill } = await import("./skill-MVKLVB5V.js");
40
35
  printSkill();
41
36
  });
42
37
  await program.parseAsync(process.argv);
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  getConfigFilePath
4
- } from "./chunk-OUWQ6NIV.js";
4
+ } from "./chunk-FNAMNRUH.js";
5
5
 
6
6
  // src/commands/config.ts
7
7
  import { spawn } from "child_process";
@@ -4,9 +4,9 @@ import {
4
4
  openConfiguredIde,
5
5
  prepareWorktree,
6
6
  promptExistingWorktree
7
- } from "./chunk-S75RK5RZ.js";
8
- import "./chunk-47P7FKFX.js";
9
- import "./chunk-OUWQ6NIV.js";
7
+ } from "./chunk-QGSJG72F.js";
8
+ import "./chunk-GHYUCETL.js";
9
+ import "./chunk-FNAMNRUH.js";
10
10
  export {
11
11
  createWorktree,
12
12
  openConfiguredIde,
@@ -0,0 +1,204 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getRegisteredRepos,
4
+ getRepoRoot,
5
+ listWorktreeDirtyFiles,
6
+ listWorktrees,
7
+ openIde,
8
+ registerRepo,
9
+ removeWorktree,
10
+ runBranchInput,
11
+ runCommands,
12
+ runInteractiveList,
13
+ runRepoPicker,
14
+ runWizard
15
+ } from "./chunk-GHYUCETL.js";
16
+ import {
17
+ createStore,
18
+ getEffectiveConfig
19
+ } from "./chunk-FNAMNRUH.js";
20
+
21
+ // src/commands/list.ts
22
+ import * as clack from "@clack/prompts";
23
+ import pc from "picocolors";
24
+ function buildWorktreeSteps(repoRoot, store, state) {
25
+ const steps = [];
26
+ if (!repoRoot) {
27
+ const repos = getRegisteredRepos(store);
28
+ steps.push(async () => {
29
+ const picked = await runRepoPicker(repos, state.pickedRepo);
30
+ if (!picked) return false;
31
+ state.pickedRepo = picked;
32
+ return true;
33
+ });
34
+ }
35
+ steps.push(async () => {
36
+ const entered = await runBranchInput(
37
+ state.pickedRepo,
38
+ state.branch ?? ""
39
+ );
40
+ if (!entered) return false;
41
+ state.branch = entered;
42
+ return true;
43
+ });
44
+ return steps;
45
+ }
46
+ async function prepareListItems(options = {}) {
47
+ const { cwd = process.cwd(), store = createStore() } = options;
48
+ let repoRoot = null;
49
+ try {
50
+ repoRoot = getRepoRoot(cwd);
51
+ } catch {
52
+ }
53
+ if (repoRoot) {
54
+ registerRepo(repoRoot, store);
55
+ const items2 = listWorktrees(repoRoot, cwd);
56
+ return { items: items2, mode: "repo", repoRoot };
57
+ }
58
+ const repos = getRegisteredRepos(store);
59
+ const items = repos.flatMap((repo) => {
60
+ try {
61
+ return listWorktrees(repo, cwd);
62
+ } catch {
63
+ return [];
64
+ }
65
+ });
66
+ return { items, mode: "global", repoRoot: null };
67
+ }
68
+ async function runList(options = {}) {
69
+ const { store = createStore(), cwd = process.cwd() } = options;
70
+ const { items, mode, repoRoot } = await prepareListItems({ cwd, store });
71
+ if (items.length === 0 && mode === "global") {
72
+ console.log(
73
+ pc.dim(
74
+ "No repos registered. Run `wt create` inside a repo to get started."
75
+ )
76
+ );
77
+ return;
78
+ }
79
+ await runInteractiveList(items, mode, {
80
+ onOpen: (item) => {
81
+ const config = getEffectiveConfig(item.repoRoot, store);
82
+ openIde(config.ide, config.ide_open_args, item.path);
83
+ },
84
+ onDelete: async (item) => {
85
+ const confirmed = await clack.confirm({
86
+ message: `Remove worktree ${pc.bold(item.branch)}? This cannot be undone.`
87
+ });
88
+ if (clack.isCancel(confirmed) || !confirmed) return false;
89
+ const config = getEffectiveConfig(item.repoRoot, store);
90
+ if (config.teardown_commands.length > 0) {
91
+ console.log(pc.dim("Running teardown commands..."));
92
+ const result = await runCommands(config.teardown_commands, item.path);
93
+ if (!result.success) {
94
+ clack.log.warn(
95
+ `Teardown command failed: ${result.failedCommand} (exit code ${result.exitCode})`
96
+ );
97
+ const proceed = await clack.confirm({
98
+ message: `Delete ${pc.bold(item.branch)} anyway?`
99
+ });
100
+ if (clack.isCancel(proceed) || !proceed) return false;
101
+ }
102
+ }
103
+ try {
104
+ removeWorktree(item.repoRoot, item.path);
105
+ console.log(pc.green(`\u2713 Removed ${item.branch}`));
106
+ return true;
107
+ } catch (err) {
108
+ const msg = String(err);
109
+ if (msg.includes("cannot be moved or removed")) {
110
+ clack.log.warn(
111
+ "Worktree contains git submodules, which prevent standard removal."
112
+ );
113
+ const force = await clack.confirm({
114
+ message: `Force delete ${pc.bold(item.branch)}? The worktree directory will be removed directly.`
115
+ });
116
+ if (clack.isCancel(force) || !force) return false;
117
+ try {
118
+ removeWorktree(item.repoRoot, item.path, true);
119
+ console.log(pc.green(`\u2713 Force-removed ${item.branch}`));
120
+ return true;
121
+ } catch (err2) {
122
+ console.error(pc.red(`\u2717 Failed to force-remove: ${String(err2)}`));
123
+ return false;
124
+ }
125
+ }
126
+ if (msg.includes("modified or untracked files")) {
127
+ const dirty = listWorktreeDirtyFiles(item.path);
128
+ if (dirty.length > 0) {
129
+ clack.log.warn(
130
+ `Worktree has uncommitted changes:
131
+ ${dirty.map((f) => ` ${f}`).join("\n")}`
132
+ );
133
+ }
134
+ const force = await clack.confirm({
135
+ message: `Force delete ${pc.bold(item.branch)}? All changes will be lost.`
136
+ });
137
+ if (clack.isCancel(force) || !force) return false;
138
+ try {
139
+ removeWorktree(item.repoRoot, item.path, true);
140
+ console.log(pc.green(`\u2713 Force-removed ${item.branch}`));
141
+ return true;
142
+ } catch (err2) {
143
+ console.error(pc.red(`\u2717 Failed to force-remove: ${String(err2)}`));
144
+ return false;
145
+ }
146
+ }
147
+ console.error(pc.red(`\u2717 Failed to remove: ${msg}`));
148
+ return false;
149
+ }
150
+ },
151
+ onCreate: async () => {
152
+ const state = { pickedRepo: repoRoot ?? void 0 };
153
+ const steps = buildWorktreeSteps(repoRoot, store, state);
154
+ if (!await runWizard(steps)) return;
155
+ if (state.pickedRepo === void 0 || state.branch === void 0) return;
156
+ const { createWorktree } = await import("./create-XKF574AL.js");
157
+ await createWorktree(state.branch, { cwd: state.pickedRepo, store });
158
+ },
159
+ onAgent: async () => {
160
+ const { createAgentWorktree, VALID_MODES } = await import("./agent-Z3YCY245.js");
161
+ const state = {
162
+ pickedRepo: repoRoot ?? void 0,
163
+ mode: "plan"
164
+ };
165
+ const steps = buildWorktreeSteps(repoRoot, store, state);
166
+ steps.push(async () => {
167
+ const entered = await clack.text({
168
+ message: "Plan prompt for the agent:",
169
+ initialValue: state.plan,
170
+ validate: (v) => !v || v.length === 0 ? "Required" : void 0
171
+ });
172
+ if (clack.isCancel(entered)) return false;
173
+ state.plan = entered;
174
+ return true;
175
+ });
176
+ steps.push(async () => {
177
+ const chosen = await clack.select({
178
+ message: "Permission mode:",
179
+ initialValue: state.mode,
180
+ options: VALID_MODES.map((m) => ({ value: String(m), label: m }))
181
+ });
182
+ if (clack.isCancel(chosen)) return false;
183
+ state.mode = chosen;
184
+ return true;
185
+ });
186
+ if (!await runWizard(steps)) return;
187
+ if (state.pickedRepo === void 0 || state.branch === void 0 || state.plan === void 0)
188
+ return;
189
+ await createAgentWorktree(state.branch, state.plan, {
190
+ cwd: state.pickedRepo,
191
+ store,
192
+ mode: state.mode
193
+ });
194
+ },
195
+ refreshItems: async () => {
196
+ const refreshed = await prepareListItems({ cwd, store });
197
+ return refreshed.items;
198
+ }
199
+ });
200
+ }
201
+ export {
202
+ prepareListItems,
203
+ runList
204
+ };
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/commands/skill.ts
4
+ function printSkill() {
5
+ console.log('---\nname: wt-worktree-manager\ndescription: Use the wt CLI to create, browse, open, and delete git worktrees across repos. Use when the user asks to manage worktrees, create isolated branches, or configure worktree defaults.\n---\n\n# wt \u2014 Git Worktree Manager\n\n`wt` is a CLI for managing git worktrees. It provides an interactive TUI to browse, create, open in your IDE, and delete worktrees across multiple repos.\n\n## Commands\n\n### `wt` (no subcommand)\n\nLaunch the interactive TUI. Shows worktrees for the current repo (repo mode) or all registered repos (global mode, when run outside a repo).\n\n**Keybindings in the TUI:**\n\n- Arrow keys \u2014 navigate\n- `Enter` \u2014 open worktree in IDE (exits the TUI)\n- `D` \u2014 delete worktree\n- `C` \u2014 create a new worktree (works in both repo and global mode)\n- `A` \u2014 create a worktree and start an AI agent in it (works in both modes)\n- type to search \xB7 `Backspace` \u2014 edit search\n- `Q` / `Esc` \u2014 quit\n\n`C` and `A` are step-by-step wizards. In global mode (run from outside a repo /\n"home") they start by prompting for the repo (picker), then the branch; in repo\nmode the repo is fixed so they start at the branch. `A` then adds two more\nsteps:\n\n- `C` \u2014 **worktree (repo \u2192 branch)**\n- `A` \u2014 **worktree (repo \u2192 branch) \u2192 plan prompt \u2192 permission mode**\n\nPressing `Esc` at any step goes back to the previous step (your earlier answers\nare preserved); pressing `Esc` on the first step returns to the list.\n\nAfter a create or agent action the TUI **refreshes and stays open** on the list\n(your search and cursor are preserved) rather than exiting \u2014 only `Enter` (open)\nand `Q`/`Esc` exit.\n\nBecause `a`/`A`, `c`/`C`, and `d`/`D` are reserved as command keys, those\nletters can\'t be typed into the search box.\n\n### `wt create [branch]`\n\nCreate a new worktree. If `branch` is omitted, prompts interactively.\n\nThe worktree is created as a sibling directory to the repo: `<parent>/<repo-name>-<branch-name>`.\n\nAfter creation, `wt` runs any configured `setup_commands` and opens the worktree in your IDE.\n\nIf the worktree path already exists, `wt create` doesn\'t error \u2014 it prompts you\nto **open it in the IDE** or **quit**. (In a non-interactive shell it errors\nwith a non-zero exit instead of prompting.)\n\n### `wt agent <branch> <plan_prompt> [--mode <mode>]`\n\nCreate a worktree (same as `wt create`) **and** auto-start an AI agent in Zed\'s\nintegrated terminal, pre-filled with `<plan_prompt>` and left interactive for\nyou to take over.\n\n```bash\nwt agent feature/login \'Read the codebase, then propose a plan for login.\'\nwt agent feature/fix \'Fix the bug in payment processing\' --mode auto\nwt agent refactor/api \'Refactor the API layer\' --mode default\n```\n\nThe `--mode` flag sets Claude Code\'s permission mode (defaults to `plan`):\n\n- `default` \u2014 Standard interactive mode with approval for each action\n- `acceptEdits` \u2014 Allow file changes but keep command execution controlled\n- `plan` \u2014 Architecture-first mode with no surprise mutations (default)\n- `auto` \u2014 Claude\'s safety model makes decisions instead of prompting\n- `dontAsk` \u2014 Minimal interruptions in trusted environments\n- `bypassPermissions` \u2014 Skip all permission checks (dangerous, CI/sandbox only)\n\nIt writes a temporary `.zed/tasks.json` running\n`<agent_command> --permission-mode <mode> \'<plan_prompt>\'`, ensures a global Zed keymap chord\n(`agent_trigger_chord`) spawns that task, opens Zed, presses the chord via\n`osascript`, then removes the temporary task so the repo is left clean.\n\n**macOS + Zed only.** Requires Accessibility permission for the app that runs\n`wt` (Zed itself, when run from its integrated terminal). If it isn\'t granted,\n`wt agent` opens the _Privacy & Security \u2192 Accessibility_ settings pane and waits\nfor you to grant it and confirm, then retries automatically. On other platforms\n(or when `ide` is not `zed`) the worktree is still created and opened, but the\nagent is not auto-started.\n\nOver SSH it still works, provided the same user has an active graphical login on\nthe Mac: the keystroke is run inside the GUI session via Launch Services\n(`open -a Terminal` briefly flashes a Terminal window). Grant Accessibility to\nTerminal (not Zed) the first time. With no one logged in graphically there is\nnothing to drive, so it falls back to the manual "press the chord in Zed"\nmessage.\n\nIf the worktree path already exists, `wt agent` prompts you to **open it in the\nIDE**, **open it and start the agent**, or **quit** \u2014 instead of erroring. (In a\nnon-interactive shell it errors with a non-zero exit instead of prompting.)\n\n### `wt config`\n\nOpen the global config file in `$EDITOR` (defaults to `nano`).\n\n```bash\nwt config # open in editor\nwt config --path # print config file path only\n```\n\n### `wt skill`\n\nPrint this skill file to stdout. Useful for piping to agents or copying to a project.\n\n## Configuration\n\nConfig is stored as JSON. Get the path with `wt config --path`.\n\n### Schema\n\n| Key | Type | Default | Description |\n| --------------------- | ---------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `worktree_path` | `string` | `"../"` | Where to place new worktrees, relative to the repo root |\n| `base_branch` | `string` | `"origin/main"` | Branch to base new worktrees on |\n| `setup_commands` | `string[]` | `[]` | Commands to run in a new worktree after creation (e.g. `["npm install"]`) |\n| `teardown_commands` | `string[]` | `[]` | Commands to run in a worktree just before it is deleted (e.g. `["docker compose down -v"]`); on failure you are prompted whether to delete anyway |\n| `ide` | `string` | `"zed"` | IDE command to open worktrees with |\n| `ide_open_args` | `string[]` | `["-n"]` | Arguments passed to the IDE command |\n| `agent_command` | `string` | `"claude --permission-mode plan"` | Base command `wt agent` runs in Zed; any `--permission-mode` flag is replaced by the `--mode` option (defaults to `plan`), then `<plan_prompt>` is appended single-quoted |\n| `agent_trigger_chord` | `string` | `"ctrl-shift-cmd-c"` | Zed keymap chord `wt agent` installs/presses to spawn the agent task |\n| `repos` | `string[]` | `[]` | Registered repo paths (auto-populated on first use) |\n| `repo_overrides` | `object` | `{}` | Per-repo config overrides (see below) |\n\n### Per-repo overrides\n\nOverride any field (`worktree_path`, `base_branch`, `setup_commands`, `teardown_commands`, `ide`, `ide_open_args`, `agent_command`, `agent_trigger_chord`) for a specific repo:\n\n```json\n{\n "base_branch": "origin/main",\n "ide": "zed",\n "repo_overrides": {\n "/path/to/my-repo": {\n "base_branch": "origin/develop",\n "setup_commands": ["npm install", "npm run build"]\n }\n }\n}\n```\n\n## Common workflows\n\n### Create a worktree for a new feature\n\n```bash\ncd /path/to/repo\nwt create feature/my-branch\n```\n\n### Configure setup commands for a repo\n\n```bash\nwt config\n# Then add to the JSON:\n# "repo_overrides": {\n# "/path/to/repo": {\n# "setup_commands": ["npm install"]\n# }\n# }\n```\n\n### Browse all worktrees across repos\n\nRun `wt` from any directory outside a git repo to see worktrees from all registered repos.\n');
6
+ }
7
+ export {
8
+ printSkill
9
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cestoliv/wt",
3
- "version": "0.5.0-pr24.g7ccdc37",
3
+ "version": "0.5.0",
4
4
  "description": "Fast, interactive TUI to browse, create, open, and delete git worktrees across repos.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,271 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- fetchRemote,
4
- getRegisteredRepos,
5
- getRepoRoot,
6
- isBranchMerged,
7
- listWorktreeDirtyFiles,
8
- listWorktrees,
9
- openIde,
10
- registerRepo,
11
- removeWorktree,
12
- runBranchInput,
13
- runCommands,
14
- runInteractiveList,
15
- runRepoPicker,
16
- runWizard
17
- } from "./chunk-47P7FKFX.js";
18
- import {
19
- createStore,
20
- getEffectiveConfig,
21
- getGlobalConfig
22
- } from "./chunk-OUWQ6NIV.js";
23
-
24
- // src/commands/list.ts
25
- import * as clack from "@clack/prompts";
26
- import pc from "picocolors";
27
- function buildWorktreeSteps(repoRoot, store, state) {
28
- const steps = [];
29
- if (!repoRoot) {
30
- const repos = getRegisteredRepos(store);
31
- steps.push(async () => {
32
- const picked = await runRepoPicker(repos, state.pickedRepo);
33
- if (!picked) return false;
34
- state.pickedRepo = picked;
35
- return true;
36
- });
37
- }
38
- steps.push(async () => {
39
- const entered = await runBranchInput(
40
- state.pickedRepo,
41
- state.branch ?? ""
42
- );
43
- if (!entered) return false;
44
- state.branch = entered;
45
- return true;
46
- });
47
- return steps;
48
- }
49
- async function prepareListItems(options = {}) {
50
- const { cwd = process.cwd(), store = createStore() } = options;
51
- let repoRoot = null;
52
- try {
53
- repoRoot = getRepoRoot(cwd);
54
- } catch {
55
- }
56
- if (repoRoot) {
57
- registerRepo(repoRoot, store);
58
- const items2 = listWorktrees(repoRoot, cwd);
59
- return { items: items2, mode: "repo", repoRoot };
60
- }
61
- const repos = getRegisteredRepos(store);
62
- const items = repos.flatMap((repo) => {
63
- try {
64
- return listWorktrees(repo, cwd);
65
- } catch {
66
- return [];
67
- }
68
- });
69
- return { items, mode: "global", repoRoot: null };
70
- }
71
- async function runList(options = {}) {
72
- const { store = createStore(), cwd = process.cwd() } = options;
73
- const { items, mode, repoRoot } = await prepareListItems({ cwd, store });
74
- if (items.length === 0 && mode === "global") {
75
- console.log(
76
- pc.dim(
77
- "No repos registered. Run `wt create` inside a repo to get started."
78
- )
79
- );
80
- return;
81
- }
82
- const autoRefreshMinutes = mode === "repo" && repoRoot ? getEffectiveConfig(repoRoot, store).auto_refresh_minutes : getGlobalConfig(store).auto_refresh_minutes;
83
- await runInteractiveList(
84
- items,
85
- mode,
86
- {
87
- onOpen: (item) => {
88
- const config = getEffectiveConfig(item.repoRoot, store);
89
- openIde(config.ide, config.ide_open_args, item.path);
90
- },
91
- onDelete: (item) => deleteWorktree(item, store),
92
- onWipe: (items2) => wipeWorktrees(items2, store, { fetch: true }),
93
- onCreate: async () => {
94
- const state = { pickedRepo: repoRoot ?? void 0 };
95
- const steps = buildWorktreeSteps(repoRoot, store, state);
96
- if (!await runWizard(steps)) return;
97
- if (state.pickedRepo === void 0 || state.branch === void 0)
98
- return;
99
- const { createWorktree } = await import("./create-L3JURY7V.js");
100
- await createWorktree(state.branch, { cwd: state.pickedRepo, store });
101
- },
102
- onAgent: async () => {
103
- const { createAgentWorktree, VALID_MODES } = await import("./agent-CSBQ4HUU.js");
104
- const state = {
105
- pickedRepo: repoRoot ?? void 0
106
- };
107
- const steps = buildWorktreeSteps(repoRoot, store, state);
108
- steps.push(async () => {
109
- const entered = await clack.text({
110
- message: "Plan prompt for the agent:",
111
- initialValue: state.plan,
112
- validate: (v) => !v || v.length === 0 ? "Required" : void 0
113
- });
114
- if (clack.isCancel(entered)) return false;
115
- state.plan = entered;
116
- return true;
117
- });
118
- steps.push(async () => {
119
- const configuredMode = state.pickedRepo ? getEffectiveConfig(state.pickedRepo, store).agent_mode : void 0;
120
- const chosen = await clack.select({
121
- message: "Permission mode:",
122
- initialValue: state.mode ?? configuredMode,
123
- options: VALID_MODES.map((m) => ({ value: String(m), label: m }))
124
- });
125
- if (clack.isCancel(chosen)) return false;
126
- state.mode = chosen;
127
- return true;
128
- });
129
- if (!await runWizard(steps)) return;
130
- if (state.pickedRepo === void 0 || state.branch === void 0 || state.plan === void 0)
131
- return;
132
- await createAgentWorktree(state.branch, state.plan, {
133
- cwd: state.pickedRepo,
134
- store,
135
- mode: state.mode
136
- });
137
- },
138
- refreshItems: async () => {
139
- const refreshed = await prepareListItems({ cwd, store });
140
- return refreshed.items;
141
- }
142
- },
143
- { autoRefreshMinutes }
144
- );
145
- }
146
- async function deleteWorktree(item, store) {
147
- const confirmed = await clack.confirm({
148
- message: `Remove worktree ${pc.bold(item.branch)}? This cannot be undone.`
149
- });
150
- if (clack.isCancel(confirmed) || !confirmed) return false;
151
- const config = getEffectiveConfig(item.repoRoot, store);
152
- if (config.teardown_commands.length > 0) {
153
- console.log(pc.dim("Running teardown commands..."));
154
- const result = await runCommands(config.teardown_commands, item.path);
155
- if (!result.success) {
156
- clack.log.warn(
157
- `Teardown command failed: ${result.failedCommand} (exit code ${result.exitCode})`
158
- );
159
- const proceed = await clack.confirm({
160
- message: `Delete ${pc.bold(item.branch)} anyway?`
161
- });
162
- if (clack.isCancel(proceed) || !proceed) return false;
163
- }
164
- }
165
- try {
166
- removeWorktree(item.repoRoot, item.path);
167
- console.log(pc.green(`\u2713 Removed ${item.branch}`));
168
- return true;
169
- } catch (err) {
170
- const msg = String(err);
171
- if (msg.includes("cannot be moved or removed")) {
172
- clack.log.warn(
173
- "Worktree contains git submodules, which prevent standard removal."
174
- );
175
- const force = await clack.confirm({
176
- message: `Force delete ${pc.bold(item.branch)}? The worktree directory will be removed directly.`
177
- });
178
- if (clack.isCancel(force) || !force) return false;
179
- try {
180
- removeWorktree(item.repoRoot, item.path, true);
181
- console.log(pc.green(`\u2713 Force-removed ${item.branch}`));
182
- return true;
183
- } catch (err2) {
184
- console.error(pc.red(`\u2717 Failed to force-remove: ${String(err2)}`));
185
- return false;
186
- }
187
- }
188
- if (msg.includes("modified or untracked files")) {
189
- const dirty = listWorktreeDirtyFiles(item.path);
190
- if (dirty.length > 0) {
191
- clack.log.warn(
192
- `Worktree has uncommitted changes:
193
- ${dirty.map((f) => ` ${f}`).join("\n")}`
194
- );
195
- }
196
- const force = await clack.confirm({
197
- message: `Force delete ${pc.bold(item.branch)}? All changes will be lost.`
198
- });
199
- if (clack.isCancel(force) || !force) return false;
200
- try {
201
- removeWorktree(item.repoRoot, item.path, true);
202
- console.log(pc.green(`\u2713 Force-removed ${item.branch}`));
203
- return true;
204
- } catch (err2) {
205
- console.error(pc.red(`\u2717 Failed to force-remove: ${String(err2)}`));
206
- return false;
207
- }
208
- }
209
- console.error(pc.red(`\u2717 Failed to remove: ${msg}`));
210
- return false;
211
- }
212
- }
213
- function selectWipeCandidates(items, isMerged) {
214
- return items.filter(
215
- (wt) => !wt.isCurrent && !wt.isMain && wt.branch !== "(detached)" && isMerged(wt)
216
- );
217
- }
218
- function buildMergedPredicate(store) {
219
- return (wt) => {
220
- const config = getEffectiveConfig(wt.repoRoot, store);
221
- const base = config.base_branch;
222
- const baseLocal = base.split("/", 2).slice(1).join("/") || base;
223
- if (wt.branch === base || wt.branch === baseLocal) return false;
224
- return isBranchMerged(wt.repoRoot, wt.branch, base);
225
- };
226
- }
227
- async function wipeWorktrees(items, store, options = {}) {
228
- if (options.fetch) {
229
- const seen = /* @__PURE__ */ new Set();
230
- for (const wt of items) {
231
- if (seen.has(wt.repoRoot)) continue;
232
- seen.add(wt.repoRoot);
233
- const parts = getEffectiveConfig(wt.repoRoot, store).base_branch.split(
234
- "/",
235
- 2
236
- );
237
- if (parts.length !== 2) continue;
238
- const remote = parts[0] || "origin";
239
- try {
240
- fetchRemote(wt.repoRoot, remote);
241
- } catch (err) {
242
- console.warn(
243
- pc.yellow(
244
- `\u26A0 Could not fetch from ${remote} \u2014 using local state${err instanceof Error ? ` (${err.message})` : ""}`
245
- )
246
- );
247
- }
248
- }
249
- }
250
- const candidates = selectWipeCandidates(items, buildMergedPredicate(store));
251
- if (candidates.length === 0) {
252
- console.log(pc.dim("No merged worktrees to wipe."));
253
- return [];
254
- }
255
- const removed = [];
256
- for (const candidate of candidates) {
257
- if (await deleteWorktree(candidate, store)) {
258
- removed.push(candidate);
259
- }
260
- }
261
- return removed;
262
- }
263
-
264
- export {
265
- prepareListItems,
266
- runList,
267
- deleteWorktree,
268
- selectWipeCandidates,
269
- buildMergedPredicate,
270
- wipeWorktrees
271
- };
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- buildMergedPredicate,
4
- deleteWorktree,
5
- prepareListItems,
6
- runList,
7
- selectWipeCandidates,
8
- wipeWorktrees
9
- } from "./chunk-63BGDDDM.js";
10
- import "./chunk-47P7FKFX.js";
11
- import "./chunk-OUWQ6NIV.js";
12
- export {
13
- buildMergedPredicate,
14
- deleteWorktree,
15
- prepareListItems,
16
- runList,
17
- selectWipeCandidates,
18
- wipeWorktrees
19
- };
@@ -1,31 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- prepareListItems,
4
- wipeWorktrees
5
- } from "./chunk-63BGDDDM.js";
6
- import "./chunk-47P7FKFX.js";
7
- import {
8
- createStore
9
- } from "./chunk-OUWQ6NIV.js";
10
-
11
- // src/commands/prune.ts
12
- import pc from "picocolors";
13
- async function runPrune(options = {}) {
14
- const { cwd = process.cwd(), store = createStore() } = options;
15
- const { items, mode } = await prepareListItems({ cwd, store });
16
- if (items.length === 0) {
17
- console.log(
18
- pc.dim(
19
- mode === "global" ? "No repos registered. Run `wt create` inside a repo to get started." : "No worktrees found."
20
- )
21
- );
22
- return;
23
- }
24
- const removed = await wipeWorktrees(items, store, { fetch: true });
25
- if (removed.length > 0) {
26
- console.log(pc.green(`\u2713 Pruned ${removed.length} worktree(s).`));
27
- }
28
- }
29
- export {
30
- runPrune
31
- };
@@ -1,9 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/commands/skill.ts
4
- function printSkill() {
5
- console.log('---\nname: wt-worktree-manager\ndescription: Use the wt CLI to create, browse, open, and delete git worktrees across repos. Use when the user asks to manage worktrees, create isolated branches, or configure worktree defaults.\n---\n\n# wt \u2014 Git Worktree Manager\n\n`wt` is a CLI for managing git worktrees. It provides an interactive TUI to browse, create, open in your IDE, and delete worktrees across multiple repos.\n\n## Commands\n\n### `wt` (no subcommand)\n\nLaunch the interactive TUI. Shows worktrees for the current repo (repo mode) or all registered repos (global mode, when run outside a repo).\n\n**Keybindings in the TUI:**\n\n- Arrow keys \u2014 navigate\n- `Enter` \u2014 open worktree in IDE (exits the TUI)\n- `D` \u2014 delete worktree (the main worktree is tagged `(main)` and cannot be deleted \u2014 only linked worktrees can)\n- `P` \u2014 prune all merged worktrees (per-branch confirmation)\n- `C` \u2014 create a new worktree (works in both repo and global mode)\n- `A` \u2014 create a worktree and start an AI agent in it (works in both modes)\n- type to search \xB7 `Backspace` \u2014 edit search\n- `Q` / `Esc` \u2014 quit\n\n`C` and `A` are step-by-step wizards. In global mode (run from outside a repo /\n"home") they start by prompting for the repo (picker), then the branch; in repo\nmode the repo is fixed so they start at the branch. `A` then adds two more\nsteps:\n\n- `C` \u2014 **worktree (repo \u2192 branch)**\n- `A` \u2014 **worktree (repo \u2192 branch) \u2192 plan prompt \u2192 permission mode**\n\nPressing `Esc` at any step goes back to the previous step (your earlier answers\nare preserved); pressing `Esc` on the first step returns to the list.\n\nAfter a create or agent action the TUI **refreshes and stays open** on the list\n(your search and cursor are preserved) rather than exiting \u2014 only `Enter` (open)\nand `Q`/`Esc` exit.\n\nBecause `a`/`A`, `c`/`C`, `d`/`D`, and `p`/`P` are reserved as command keys,\nthose letters can\'t be typed into the search box.\n\n### `wt create [branch]`\n\nCreate a new worktree. If `branch` is omitted, prompts interactively.\n\nThe worktree is created as a sibling directory to the repo: `<parent>/<repo-name>-<branch-name>`.\n\nAfter creation, `wt` runs any configured `setup_commands` and opens the worktree in your IDE.\n\nIf the worktree path already exists, `wt create` doesn\'t error \u2014 it prompts you\nto **open it in the IDE** or **quit**. (In a non-interactive shell it errors\nwith a non-zero exit instead of prompting.)\n\n### `wt agent <branch> <plan_prompt> [--mode <mode>]`\n\nCreate a worktree (same as `wt create`) **and** auto-start an AI agent in Zed\'s\nintegrated terminal, pre-filled with `<plan_prompt>` and left interactive for\nyou to take over.\n\n```bash\nwt agent feature/login \'Read the codebase, then propose a plan for login.\'\nwt agent feature/fix \'Fix the bug in payment processing\' --mode auto\nwt agent refactor/api \'Refactor the API layer\' --mode default\n```\n\nThe `--mode` flag sets Claude Code\'s permission mode (defaults to `default`;\nchange the default with the `agent_mode` config key):\n\n- `default` \u2014 Standard interactive mode with approval for each action (default)\n- `acceptEdits` \u2014 Allow file changes but keep command execution controlled\n- `plan` \u2014 Architecture-first mode with no surprise mutations\n- `auto` \u2014 Claude\'s safety model makes decisions instead of prompting\n- `dontAsk` \u2014 Minimal interruptions in trusted environments\n- `bypassPermissions` \u2014 Skip all permission checks (dangerous, CI/sandbox only)\n\nIt writes a temporary `.zed/tasks.json` running\n`<agent_command> --permission-mode <mode> \'<plan_prompt>\'`, ensures a global Zed keymap chord\n(`agent_trigger_chord`) spawns that task, opens Zed, presses the chord via\n`osascript`, then removes the temporary task so the repo is left clean.\n\n**macOS + Zed only.** Requires Accessibility permission for the app that runs\n`wt` (Zed itself, when run from its integrated terminal). If it isn\'t granted,\n`wt agent` opens the _Privacy & Security \u2192 Accessibility_ settings pane and waits\nfor you to grant it and confirm, then retries automatically. On other platforms\n(or when `ide` is not `zed`) the worktree is still created and opened, but the\nagent is not auto-started.\n\nOver SSH it still works, provided the same user has an active graphical login on\nthe Mac: the keystroke is run inside the GUI session via Launch Services\n(`open -a Terminal` briefly flashes a Terminal window). Grant Accessibility to\nTerminal (not Zed) the first time. With no one logged in graphically there is\nnothing to drive, so it falls back to the manual "press the chord in Zed"\nmessage.\n\nIf the worktree path already exists, `wt agent` prompts you to **open it in the\nIDE**, **open it and start the agent**, or **quit** \u2014 instead of erroring. (In a\nnon-interactive shell it errors with a non-zero exit instead of prompting.)\n\n### `wt prune`\n\nRemove every worktree whose branch has already been merged into the base\nbranch (`base_branch`, default `origin/main`). Each candidate is confirmed\nindividually \u2014 and force-confirmed when git refuses (submodules / uncommitted\nchanges), exactly like a manual `d` delete. The branch itself is left intact;\nonly the worktree is removed.\n\n```bash\nwt prune # review and remove merged worktrees, one prompt per branch\n```\n\nMerge detection covers two cases: a branch whose tip is an ancestor of the base\nbranch (a **fast-forward / merge-commit** merge \u2014 its commits live verbatim in\nbase), and a branch whose diff already exists in base by patch id (via\n`git cherry`, so a single-commit branch **squash-merged** through a PR is still\nrecognized). A worktree still sitting exactly on the base commit (no work yet) is\nnever offered. It also best-effort fetches the remote first so detection sees\nup-to-date refs; if the\nbase ref can\'t be resolved (e.g. offline), nothing is removed. Works in repo\nmode (current repo) and global mode (all registered repos, each against its own\n`base_branch`). The TUI exposes the same action under the `p` key.\n\n### `wt config`\n\nOpen the global config file in `$EDITOR` (defaults to `nano`).\n\n```bash\nwt config # open in editor\nwt config --path # print config file path only\n```\n\n### `wt skill`\n\nPrint this skill file to stdout. Useful for piping to agents or copying to a project.\n\n## Configuration\n\nConfig is stored as JSON. Get the path with `wt config --path`.\n\n### Schema\n\n| Key | Type | Default | Description |\n| --------------------- | ---------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `worktree_path` | `string` | `"../"` | Where to place new worktrees, relative to the repo root |\n| `base_branch` | `string` | `"origin/main"` | Branch to base new worktrees on |\n| `setup_commands` | `string[]` | `[]` | Commands to run in a new worktree after creation (e.g. `["npm install"]`) |\n| `teardown_commands` | `string[]` | `[]` | Commands to run in a worktree just before it is deleted (e.g. `["docker compose down -v"]`); on failure you are prompted whether to delete anyway |\n| `ide` | `string` | `"zed"` | IDE command to open worktrees with |\n| `ide_open_args` | `string[]` | `["-n"]` | Arguments passed to the IDE command |\n| `agent_command` | `string` | `"claude"` | Base command `wt agent` runs in Zed; `--permission-mode <mode>` is injected (any existing one replaced), then `<plan_prompt>` is appended single-quoted |\n| `agent_mode` | `string` | `"default"` | Default Claude Code permission mode for `wt agent`; the `--mode` flag overrides it. One of `default`, `acceptEdits`, `plan`, `auto`, `dontAsk`, `bypassPermissions` |\n| `agent_trigger_chord` | `string` | `"ctrl-shift-cmd-c"` | Zed keymap chord `wt agent` installs/presses to spawn the agent task |\n| `auto_refresh_minutes`| `number` | `5` | How often the interactive list (`wt`) re-fetches worktrees and updates the "last refreshed" header; `0` disables auto-refresh |\n| `repos` | `string[]` | `[]` | Registered repo paths (auto-populated on first use) |\n| `repo_overrides` | `object` | `{}` | Per-repo config overrides (see below) |\n\n### Per-repo overrides\n\nOverride any field (`worktree_path`, `base_branch`, `setup_commands`, `teardown_commands`, `ide`, `ide_open_args`, `agent_command`, `agent_mode`, `agent_trigger_chord`, `auto_refresh_minutes`) for a specific repo:\n\n```json\n{\n "base_branch": "origin/main",\n "ide": "zed",\n "repo_overrides": {\n "/path/to/my-repo": {\n "base_branch": "origin/develop",\n "setup_commands": ["npm install", "npm run build"]\n }\n }\n}\n```\n\n## Common workflows\n\n### Create a worktree for a new feature\n\n```bash\ncd /path/to/repo\nwt create feature/my-branch\n```\n\n### Configure setup commands for a repo\n\n```bash\nwt config\n# Then add to the JSON:\n# "repo_overrides": {\n# "/path/to/repo": {\n# "setup_commands": ["npm install"]\n# }\n# }\n```\n\n### Browse all worktrees across repos\n\nRun `wt` from any directory outside a git repo to see worktrees from all registered repos.\n');
6
- }
7
- export {
8
- printSkill
9
- };