@cestoliv/wt 0.1.0 → 0.2.0-pr9.g997ac8d
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 +81 -59
- package/SKILL.md +134 -0
- package/dist/agent-PKCZHGN7.js +428 -0
- package/dist/{create-FV5XL2ON.js → chunk-BYWCGKUP.js} +75 -19
- package/dist/{chunk-25JVZOCI.js → chunk-FSARWOCW.js} +26 -7
- package/dist/{chunk-HIGP6FSR.js → chunk-IQWENLPW.js} +2 -0
- package/dist/cli.js +19 -6
- package/dist/{config-FRLXDMRY.js → config-OSNYQCZL.js} +6 -2
- package/dist/create-5J5NGGGM.js +15 -0
- package/dist/{list-ZKJZEMCV.js → list-5TXRTSIW.js} +20 -3
- package/dist/skill-RR6MNLWH.js +9 -0
- package/package.json +6 -3
package/README.md
CHANGED
|
@@ -1,50 +1,79 @@
|
|
|
1
1
|
# wt
|
|
2
2
|
|
|
3
|
-
A fast
|
|
3
|
+
A fast TUI for git worktrees — browse, create, open, and delete without leaving
|
|
4
|
+
the terminal. Plus **one-command AI agents**:
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
```bash
|
|
7
|
+
wt agent fix-auth "Plan the auth refactor"
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
→ spins up an isolated worktree and launches Claude in Zed, pre-loaded with your
|
|
11
|
+
prompt.
|
|
6
12
|
|
|
7
13
|
## Install
|
|
8
14
|
|
|
9
15
|
```bash
|
|
10
|
-
npm install -g @cestoliv/wt
|
|
16
|
+
npm install -g @cestoliv/wt # also the update command
|
|
11
17
|
```
|
|
12
18
|
|
|
13
19
|
Requires Node.js 20+ and Git. The command is `wt`.
|
|
14
20
|
|
|
15
|
-
|
|
21
|
+
## Let your AI assistant set it up
|
|
16
22
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
```
|
|
23
|
+
Already using an AI coding assistant (Claude Code, Cursor, …)? Paste this prompt
|
|
24
|
+
— it reads `wt skill` to learn the tool, then configures `wt` for you:
|
|
20
25
|
|
|
21
|
-
|
|
26
|
+
```text
|
|
27
|
+
Run `wt skill` to learn how the `wt` CLI works and what its config options are.
|
|
28
|
+
Then configure it for me: choose sensible values for my editor, base branch,
|
|
29
|
+
setup commands, and the `wt agent` settings — ask me about anything you can't
|
|
30
|
+
infer from this project. Write the result to the config file (find its path with
|
|
31
|
+
`wt config --path`), then show me the final config.
|
|
32
|
+
```
|
|
22
33
|
|
|
23
|
-
|
|
24
|
-
dist-tag (the label is removed automatically afterwards):
|
|
34
|
+
## Quick start
|
|
25
35
|
|
|
26
36
|
```bash
|
|
27
|
-
|
|
37
|
+
wt # Browse worktrees (interactive TUI)
|
|
38
|
+
wt create my-feat # New worktree, opens your IDE
|
|
39
|
+
wt agent my-feat "Plan the feature" # New worktree + AI agent in Zed (macOS)
|
|
40
|
+
wt config # Edit config in $EDITOR
|
|
41
|
+
wt skill # Print the skill file (for AI agents)
|
|
28
42
|
```
|
|
29
43
|
|
|
30
|
-
##
|
|
44
|
+
## `wt agent <branch> <plan_prompt>` — the standout
|
|
31
45
|
|
|
32
46
|
```bash
|
|
33
|
-
|
|
34
|
-
wt # Browse worktrees
|
|
35
|
-
wt create my-feat # Create a new worktree and open it in your IDE
|
|
36
|
-
wt config # Edit config in $EDITOR
|
|
47
|
+
wt agent feat/login "Read the codebase, then propose a plan for login."
|
|
37
48
|
```
|
|
38
49
|
|
|
39
|
-
|
|
50
|
+
Creates a worktree exactly like `wt create`, then auto-starts your agent
|
|
51
|
+
(default `claude --permission-mode plan`) in Zed's integrated terminal —
|
|
52
|
+
pre-filled with your prompt and left interactive for you to take over.
|
|
53
|
+
|
|
54
|
+
Under the hood it writes a temporary `.zed/tasks.json`, installs a global Zed
|
|
55
|
+
keymap chord, opens Zed and fires the chord via `osascript`, then removes the
|
|
56
|
+
temp task so the repo stays clean.
|
|
57
|
+
|
|
58
|
+
**Requires** macOS, Zed, and Accessibility permission for the app running `wt`.
|
|
59
|
+
Not granted yet? `wt agent` opens *System Settings → Privacy & Security →
|
|
60
|
+
Accessibility*, waits while you grant it (you may need to quit and reopen the
|
|
61
|
+
app), then retries automatically. On other platforms — or when `ide` isn't
|
|
62
|
+
`zed` — the worktree is still created and opened, just without the agent.
|
|
40
63
|
|
|
41
|
-
|
|
64
|
+
If the path already exists, `wt agent` offers to open it — or open it and start
|
|
65
|
+
the agent — instead of erroring (in a non-interactive shell it exits non-zero).
|
|
42
66
|
|
|
43
|
-
|
|
67
|
+
> **Tip:** trust the parent directory of your worktrees in Claude once, and
|
|
68
|
+
> every worktree created beneath it starts hands-free.
|
|
69
|
+
|
|
70
|
+
## Browse — `wt`
|
|
71
|
+
|
|
72
|
+
An interactive, fuzzy-searchable list of your worktrees:
|
|
44
73
|
|
|
45
74
|
```
|
|
46
75
|
MY-PROJECT
|
|
47
|
-
▶ main
|
|
76
|
+
▶ main ~/dev/my-project
|
|
48
77
|
fix: resolve auth bug (2h ago)
|
|
49
78
|
feat/dashboard ~/dev/my-project-feat-dashboard
|
|
50
79
|
wip: add chart component (1d ago)
|
|
@@ -52,51 +81,46 @@ MY-PROJECT
|
|
|
52
81
|
↕ navigate · Enter open · D delete · C create · Q quit
|
|
53
82
|
```
|
|
54
83
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| `Enter` | Open worktree in IDE |
|
|
59
|
-
| `D` | Delete worktree (with confirm) |
|
|
60
|
-
| `C` | Create new worktree (repo mode) |
|
|
61
|
-
| `Q` | Quit |
|
|
62
|
-
|
|
63
|
-
Type to fuzzy-filter branches instantly.
|
|
64
|
-
|
|
65
|
-
**Repo mode** (inside a git repo): shows worktrees for that repo.
|
|
66
|
-
**Global mode** (outside a repo): shows worktrees across all registered repos.
|
|
84
|
+
Type to fuzzy-filter branches instantly. Inside a repo it shows that repo's
|
|
85
|
+
worktrees (and `C` creates one); run it outside any repo to browse worktrees
|
|
86
|
+
across all registered repos.
|
|
67
87
|
|
|
68
|
-
|
|
88
|
+
## Create — `wt create [branch]`
|
|
69
89
|
|
|
70
90
|
```bash
|
|
71
|
-
wt create feat/login
|
|
72
|
-
wt create
|
|
91
|
+
wt create feat/login # From base branch (origin/main by default)
|
|
92
|
+
wt create # Prompts for a branch name
|
|
73
93
|
```
|
|
74
94
|
|
|
75
|
-
|
|
95
|
+
Creates a worktree as a sibling directory (`../my-project-feat-login`), runs your
|
|
96
|
+
`setup_commands`, and opens it in your IDE. Run it outside a repo to pick from
|
|
97
|
+
registered repos.
|
|
76
98
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
3. Opens the worktree in your IDE
|
|
99
|
+
If the path already exists, `wt create` offers to open it in your IDE instead of
|
|
100
|
+
erroring (in a non-interactive shell it exits non-zero).
|
|
80
101
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
### Edit config — `wt config`
|
|
102
|
+
## Configuration
|
|
84
103
|
|
|
85
|
-
|
|
104
|
+
Edit with `wt config` (`wt config --path` prints the file location —
|
|
105
|
+
`~/Library/Preferences/wt-nodejs/config.json` on macOS).
|
|
86
106
|
|
|
87
|
-
|
|
107
|
+
| Key | Default | Description |
|
|
108
|
+
| --------------------- | --------------------------------- | ------------------------------------------------- |
|
|
109
|
+
| `ide` | `"zed"` | Editor to open worktrees with |
|
|
110
|
+
| `ide_open_args` | `["-n"]` | Extra args passed to the IDE command |
|
|
111
|
+
| `base_branch` | `"origin/main"` | Branch new worktrees are created from |
|
|
112
|
+
| `worktree_path` | `"../"` | Where worktrees are placed (relative to repo) |
|
|
113
|
+
| `setup_commands` | `[]` | Commands to run in new worktrees |
|
|
114
|
+
| `agent_command` | `"claude --permission-mode plan"` | What `wt agent` runs in Zed (prompt appended) |
|
|
115
|
+
| `agent_trigger_chord` | `"ctrl-shift-cmd-c"` | Zed keymap chord `wt agent` installs and presses |
|
|
116
|
+
| `repo_overrides` | `{}` | Per-repo overrides for any key above |
|
|
88
117
|
|
|
89
|
-
|
|
118
|
+
Override any key per repo:
|
|
90
119
|
|
|
91
120
|
```json
|
|
92
121
|
{
|
|
93
|
-
"ide": "code",
|
|
94
|
-
"ide_open_args": ["-n"],
|
|
95
|
-
"base_branch": "origin/main",
|
|
96
|
-
"worktree_path": "../",
|
|
97
|
-
"setup_commands": ["npm install"],
|
|
98
122
|
"repo_overrides": {
|
|
99
|
-
"/path/to/
|
|
123
|
+
"/path/to/repo": {
|
|
100
124
|
"base_branch": "origin/develop",
|
|
101
125
|
"setup_commands": ["pnpm install", "pnpm build"]
|
|
102
126
|
}
|
|
@@ -104,14 +128,12 @@ Config is stored at `~/Library/Preferences/wt-nodejs/config.json` (macOS).
|
|
|
104
128
|
}
|
|
105
129
|
```
|
|
106
130
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
| `setup_commands` | `[]` | Commands to run in new worktrees |
|
|
114
|
-
| `repo_overrides` | `{}` | Per-repo overrides for any of the above |
|
|
131
|
+
## Pre-release builds
|
|
132
|
+
|
|
133
|
+
Add the `publish-dev` label to a PR to publish that branch as a unique, pinned
|
|
134
|
+
prerelease (e.g. `0.1.0-pr12.gabc1234`); the exact install command is posted as a
|
|
135
|
+
PR comment. There's no rolling `dev` channel — each build is a distinct version
|
|
136
|
+
you install explicitly.
|
|
115
137
|
|
|
116
138
|
## License
|
|
117
139
|
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wt-worktree-manager
|
|
3
|
+
description: 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.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# wt — Git Worktree Manager
|
|
7
|
+
|
|
8
|
+
`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.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
|
|
12
|
+
### `wt` (no subcommand)
|
|
13
|
+
|
|
14
|
+
Launch the interactive TUI. Shows worktrees for the current repo (repo mode) or all registered repos (global mode, when run outside a repo).
|
|
15
|
+
|
|
16
|
+
**Keybindings in the TUI:**
|
|
17
|
+
|
|
18
|
+
- Arrow keys / `j`/`k` — navigate
|
|
19
|
+
- `Enter` — open worktree in IDE
|
|
20
|
+
- `d` — delete worktree
|
|
21
|
+
- `c` — create new worktree (repo mode only)
|
|
22
|
+
- `/` — search
|
|
23
|
+
- `q` / `Esc` — quit
|
|
24
|
+
|
|
25
|
+
### `wt create [branch]`
|
|
26
|
+
|
|
27
|
+
Create a new worktree. If `branch` is omitted, prompts interactively.
|
|
28
|
+
|
|
29
|
+
The worktree is created as a sibling directory to the repo: `<parent>/<repo-name>-<branch-name>`.
|
|
30
|
+
|
|
31
|
+
After creation, `wt` runs any configured `setup_commands` and opens the worktree in your IDE.
|
|
32
|
+
|
|
33
|
+
If the worktree path already exists, `wt create` doesn't error — it prompts you
|
|
34
|
+
to **open it in the IDE** or **quit**. (In a non-interactive shell it errors
|
|
35
|
+
with a non-zero exit instead of prompting.)
|
|
36
|
+
|
|
37
|
+
### `wt agent <branch> <plan_prompt>`
|
|
38
|
+
|
|
39
|
+
Create a worktree (same as `wt create`) **and** auto-start an AI agent in Zed's
|
|
40
|
+
integrated terminal, pre-filled with `<plan_prompt>` and left interactive for
|
|
41
|
+
you to take over.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
wt agent feature/login 'Read the codebase, then propose a plan for login.'
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
It writes a temporary `.zed/tasks.json` running
|
|
48
|
+
`<agent_command> '<plan_prompt>'`, ensures a global Zed keymap chord
|
|
49
|
+
(`agent_trigger_chord`) spawns that task, opens Zed, presses the chord via
|
|
50
|
+
`osascript`, then removes the temporary task so the repo is left clean.
|
|
51
|
+
|
|
52
|
+
**macOS + Zed only.** Requires Accessibility permission for the app that runs
|
|
53
|
+
`wt` (Zed itself, when run from its integrated terminal). If it isn't granted,
|
|
54
|
+
`wt agent` opens the *Privacy & Security → Accessibility* settings pane and waits
|
|
55
|
+
for you to grant it and confirm, then retries automatically. On other platforms
|
|
56
|
+
(or when `ide` is not `zed`) the worktree is still created and opened, but the
|
|
57
|
+
agent is not auto-started.
|
|
58
|
+
|
|
59
|
+
If the worktree path already exists, `wt agent` prompts you to **open it in the
|
|
60
|
+
IDE**, **open it and start the agent**, or **quit** — instead of erroring. (In a
|
|
61
|
+
non-interactive shell it errors with a non-zero exit instead of prompting.)
|
|
62
|
+
|
|
63
|
+
### `wt config`
|
|
64
|
+
|
|
65
|
+
Open the global config file in `$EDITOR` (defaults to `nano`).
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
wt config # open in editor
|
|
69
|
+
wt config --path # print config file path only
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### `wt skill`
|
|
73
|
+
|
|
74
|
+
Print this skill file to stdout. Useful for piping to agents or copying to a project.
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
Config is stored as JSON. Get the path with `wt config --path`.
|
|
79
|
+
|
|
80
|
+
### Schema
|
|
81
|
+
|
|
82
|
+
| Key | Type | Default | Description |
|
|
83
|
+
| ---------------- | ---------- | --------------- | ------------------------------------------------------------------------- |
|
|
84
|
+
| `worktree_path` | `string` | `"../"` | Where to place new worktrees, relative to the repo root |
|
|
85
|
+
| `base_branch` | `string` | `"origin/main"` | Branch to base new worktrees on |
|
|
86
|
+
| `setup_commands` | `string[]` | `[]` | Commands to run in a new worktree after creation (e.g. `["npm install"]`) |
|
|
87
|
+
| `ide` | `string` | `"zed"` | IDE command to open worktrees with |
|
|
88
|
+
| `ide_open_args` | `string[]` | `["-n"]` | Arguments passed to the IDE command |
|
|
89
|
+
| `agent_command` | `string` | `"claude --permission-mode plan"` | Command `wt agent` runs in Zed; `<plan_prompt>` is appended single-quoted |
|
|
90
|
+
| `agent_trigger_chord` | `string` | `"ctrl-shift-cmd-c"` | Zed keymap chord `wt agent` installs/presses to spawn the agent task |
|
|
91
|
+
| `repos` | `string[]` | `[]` | Registered repo paths (auto-populated on first use) |
|
|
92
|
+
| `repo_overrides` | `object` | `{}` | Per-repo config overrides (see below) |
|
|
93
|
+
|
|
94
|
+
### Per-repo overrides
|
|
95
|
+
|
|
96
|
+
Override any field (`worktree_path`, `base_branch`, `setup_commands`, `ide`, `ide_open_args`, `agent_command`, `agent_trigger_chord`) for a specific repo:
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"base_branch": "origin/main",
|
|
101
|
+
"ide": "zed",
|
|
102
|
+
"repo_overrides": {
|
|
103
|
+
"/path/to/my-repo": {
|
|
104
|
+
"base_branch": "origin/develop",
|
|
105
|
+
"setup_commands": ["npm install", "npm run build"]
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Common workflows
|
|
112
|
+
|
|
113
|
+
### Create a worktree for a new feature
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
cd /path/to/repo
|
|
117
|
+
wt create feature/my-branch
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Configure setup commands for a repo
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
wt config
|
|
124
|
+
# Then add to the JSON:
|
|
125
|
+
# "repo_overrides": {
|
|
126
|
+
# "/path/to/repo": {
|
|
127
|
+
# "setup_commands": ["npm install"]
|
|
128
|
+
# }
|
|
129
|
+
# }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Browse all worktrees across repos
|
|
133
|
+
|
|
134
|
+
Run `wt` from any directory outside a git repo to see worktrees from all registered repos.
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openConfiguredIde,
|
|
4
|
+
prepareWorktree,
|
|
5
|
+
promptExistingWorktree
|
|
6
|
+
} from "./chunk-BYWCGKUP.js";
|
|
7
|
+
import "./chunk-FSARWOCW.js";
|
|
8
|
+
import "./chunk-IQWENLPW.js";
|
|
9
|
+
|
|
10
|
+
// src/commands/agent.ts
|
|
11
|
+
import * as clack from "@clack/prompts";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
|
|
14
|
+
// src/lib/zed.ts
|
|
15
|
+
import { spawn } from "child_process";
|
|
16
|
+
import {
|
|
17
|
+
existsSync,
|
|
18
|
+
mkdirSync,
|
|
19
|
+
readdirSync,
|
|
20
|
+
readFileSync,
|
|
21
|
+
rmSync,
|
|
22
|
+
writeFileSync
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { homedir } from "os";
|
|
25
|
+
import { dirname, join } from "path";
|
|
26
|
+
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
27
|
+
var AGENT_TASK_LABEL = "wt: agent";
|
|
28
|
+
function buildAgentTask(agentCommand, prompt, label) {
|
|
29
|
+
const escaped = prompt.replace(/'/g, "'\\''");
|
|
30
|
+
return {
|
|
31
|
+
label,
|
|
32
|
+
command: `${agentCommand} '${escaped}'`,
|
|
33
|
+
cwd: "$ZED_WORKTREE_ROOT",
|
|
34
|
+
use_new_terminal: true,
|
|
35
|
+
allow_concurrent_runs: false,
|
|
36
|
+
reveal: "always",
|
|
37
|
+
reveal_target: "dock",
|
|
38
|
+
shell: "system"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function removeTask(tasks, label) {
|
|
42
|
+
return tasks.filter((t) => t.label !== label);
|
|
43
|
+
}
|
|
44
|
+
function buildKeymapBinding(chord, label) {
|
|
45
|
+
return {
|
|
46
|
+
context: "Workspace",
|
|
47
|
+
bindings: { [chord]: ["task::Spawn", { task_name: label }] }
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function upsertKeymapBinding(keymap, chord, label) {
|
|
51
|
+
const target = buildKeymapBinding(chord, label);
|
|
52
|
+
const targetValue = JSON.stringify(target.bindings[chord]);
|
|
53
|
+
const already = keymap.some(
|
|
54
|
+
(e) => e.context === "Workspace" && e.bindings != null && JSON.stringify(e.bindings[chord]) === targetValue
|
|
55
|
+
);
|
|
56
|
+
if (already) return keymap;
|
|
57
|
+
return [...keymap, target];
|
|
58
|
+
}
|
|
59
|
+
function hasConflictingChord(keymap, chord, label) {
|
|
60
|
+
const ours = JSON.stringify(buildKeymapBinding(chord, label).bindings[chord]);
|
|
61
|
+
return keymap.some(
|
|
62
|
+
(e) => e.bindings != null && e.bindings[chord] !== void 0 && JSON.stringify(e.bindings[chord]) !== ours
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
var MODIFIER_MAP = {
|
|
66
|
+
ctrl: "control down",
|
|
67
|
+
control: "control down",
|
|
68
|
+
shift: "shift down",
|
|
69
|
+
cmd: "command down",
|
|
70
|
+
command: "command down",
|
|
71
|
+
super: "command down",
|
|
72
|
+
alt: "option down",
|
|
73
|
+
opt: "option down",
|
|
74
|
+
option: "option down"
|
|
75
|
+
};
|
|
76
|
+
var KEY_CODE_MAP = {
|
|
77
|
+
space: 49,
|
|
78
|
+
tab: 48,
|
|
79
|
+
return: 36,
|
|
80
|
+
enter: 36,
|
|
81
|
+
escape: 53,
|
|
82
|
+
esc: 53,
|
|
83
|
+
delete: 51,
|
|
84
|
+
backspace: 51,
|
|
85
|
+
forwarddelete: 117,
|
|
86
|
+
up: 126,
|
|
87
|
+
down: 125,
|
|
88
|
+
left: 123,
|
|
89
|
+
right: 124,
|
|
90
|
+
home: 115,
|
|
91
|
+
end: 119,
|
|
92
|
+
pageup: 116,
|
|
93
|
+
pagedown: 121,
|
|
94
|
+
f1: 122,
|
|
95
|
+
f2: 120,
|
|
96
|
+
f3: 99,
|
|
97
|
+
f4: 118,
|
|
98
|
+
f5: 96,
|
|
99
|
+
f6: 97,
|
|
100
|
+
f7: 98,
|
|
101
|
+
f8: 100,
|
|
102
|
+
f9: 101,
|
|
103
|
+
f10: 109,
|
|
104
|
+
f11: 103,
|
|
105
|
+
f12: 111
|
|
106
|
+
};
|
|
107
|
+
function parseChord(chord) {
|
|
108
|
+
const parts = chord.split("-").map((p) => p.trim().toLowerCase()).filter(Boolean);
|
|
109
|
+
if (parts.length === 0) {
|
|
110
|
+
throw new Error(`Invalid chord: "${chord}"`);
|
|
111
|
+
}
|
|
112
|
+
const key = parts[parts.length - 1];
|
|
113
|
+
const modifiers = parts.slice(0, -1).map((m) => {
|
|
114
|
+
const mapped = MODIFIER_MAP[m];
|
|
115
|
+
if (!mapped) {
|
|
116
|
+
throw new Error(`Unknown modifier "${m}" in chord "${chord}"`);
|
|
117
|
+
}
|
|
118
|
+
return mapped;
|
|
119
|
+
});
|
|
120
|
+
return { key, modifiers };
|
|
121
|
+
}
|
|
122
|
+
function buildOsascript(chord, opts = {}) {
|
|
123
|
+
const { loadDelay = 3, activateDelay = 0.8 } = opts;
|
|
124
|
+
const { key, modifiers } = parseChord(chord);
|
|
125
|
+
const using = modifiers.length > 0 ? ` using {${modifiers.join(", ")}}` : "";
|
|
126
|
+
let press;
|
|
127
|
+
if (key.length === 1) {
|
|
128
|
+
const escaped = key.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
129
|
+
press = `keystroke "${escaped}"`;
|
|
130
|
+
} else {
|
|
131
|
+
const code = KEY_CODE_MAP[key];
|
|
132
|
+
if (code === void 0) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
`Unsupported key "${key}" in chord "${chord}". Use a single character or one of: ${Object.keys(KEY_CODE_MAP).join(", ")}.`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
press = `key code ${code}`;
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
`delay ${loadDelay}`,
|
|
141
|
+
'tell application "Zed" to activate',
|
|
142
|
+
`delay ${activateDelay}`,
|
|
143
|
+
`tell application "System Events" to ${press}${using}`
|
|
144
|
+
].join("\n");
|
|
145
|
+
}
|
|
146
|
+
function parseTasks(raw) {
|
|
147
|
+
const errors = [];
|
|
148
|
+
const parsed = parse(raw, errors, { allowTrailingComma: true });
|
|
149
|
+
if (errors.length > 0 || !Array.isArray(parsed)) return null;
|
|
150
|
+
return parsed;
|
|
151
|
+
}
|
|
152
|
+
function readTasks(tasksPath) {
|
|
153
|
+
try {
|
|
154
|
+
return parseTasks(readFileSync(tasksPath, "utf8")) ?? [];
|
|
155
|
+
} catch {
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function writeAgentTask(worktreePath, task) {
|
|
160
|
+
const zedDir = join(worktreePath, ".zed");
|
|
161
|
+
const tasksPath = join(zedDir, "tasks.json");
|
|
162
|
+
const createdDir = !existsSync(zedDir);
|
|
163
|
+
if (createdDir) mkdirSync(zedDir, { recursive: true });
|
|
164
|
+
const createdFile = !existsSync(tasksPath);
|
|
165
|
+
if (createdFile) {
|
|
166
|
+
writeFileSync(tasksPath, `${JSON.stringify([task], null, 2)}
|
|
167
|
+
`);
|
|
168
|
+
return { createdDir, createdFile };
|
|
169
|
+
}
|
|
170
|
+
const raw = readFileSync(tasksPath, "utf8");
|
|
171
|
+
const tasks = parseTasks(raw);
|
|
172
|
+
if (tasks === null) {
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
`
|
|
175
|
+
Warning: could not parse ${tasksPath}; not modifying it. The agent task was not added.
|
|
176
|
+
`
|
|
177
|
+
);
|
|
178
|
+
return { createdDir, createdFile };
|
|
179
|
+
}
|
|
180
|
+
const idx = tasks.findIndex((t) => t.label === task.label);
|
|
181
|
+
const edits = modify(raw, idx === -1 ? [tasks.length] : [idx], task, {
|
|
182
|
+
isArrayInsertion: idx === -1,
|
|
183
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
184
|
+
});
|
|
185
|
+
const updated = applyEdits(raw, edits);
|
|
186
|
+
writeFileSync(tasksPath, updated.endsWith("\n") ? updated : `${updated}
|
|
187
|
+
`);
|
|
188
|
+
return { createdDir, createdFile };
|
|
189
|
+
}
|
|
190
|
+
function cleanupAgentTask(worktreePath, label, created) {
|
|
191
|
+
const zedDir = join(worktreePath, ".zed");
|
|
192
|
+
const tasksPath = join(zedDir, "tasks.json");
|
|
193
|
+
if (existsSync(tasksPath)) {
|
|
194
|
+
try {
|
|
195
|
+
if (created.createdFile) {
|
|
196
|
+
const next = removeTask(readTasks(tasksPath), label);
|
|
197
|
+
if (next.length === 0) {
|
|
198
|
+
rmSync(tasksPath, { force: true });
|
|
199
|
+
} else {
|
|
200
|
+
writeFileSync(tasksPath, `${JSON.stringify(next, null, 2)}
|
|
201
|
+
`);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
const raw = readFileSync(tasksPath, "utf8");
|
|
205
|
+
const tasks = parseTasks(raw);
|
|
206
|
+
const idx = tasks?.findIndex((t) => t.label === label) ?? -1;
|
|
207
|
+
if (idx !== -1) {
|
|
208
|
+
const edits = modify(raw, [idx], void 0, {});
|
|
209
|
+
const updated = applyEdits(raw, edits);
|
|
210
|
+
writeFileSync(
|
|
211
|
+
tasksPath,
|
|
212
|
+
updated.endsWith("\n") ? updated : `${updated}
|
|
213
|
+
`
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch {
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (created.createdDir && existsSync(zedDir)) {
|
|
221
|
+
try {
|
|
222
|
+
if (readdirSync(zedDir).length === 0) {
|
|
223
|
+
rmSync(zedDir, { recursive: true, force: true });
|
|
224
|
+
}
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function defaultKeymapPath() {
|
|
230
|
+
return join(homedir(), ".config", "zed", "keymap.json");
|
|
231
|
+
}
|
|
232
|
+
function ensureKeymap(chord, label, keymapPath = defaultKeymapPath()) {
|
|
233
|
+
const binding = buildKeymapBinding(chord, label);
|
|
234
|
+
if (!existsSync(keymapPath)) {
|
|
235
|
+
mkdirSync(dirname(keymapPath), { recursive: true });
|
|
236
|
+
writeFileSync(keymapPath, `${JSON.stringify([binding], null, 2)}
|
|
237
|
+
`);
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
const raw = readFileSync(keymapPath, "utf8");
|
|
241
|
+
const errors = [];
|
|
242
|
+
const parsed = parse(raw, errors, {
|
|
243
|
+
allowTrailingComma: true,
|
|
244
|
+
disallowComments: false
|
|
245
|
+
});
|
|
246
|
+
if (errors.length > 0 || !Array.isArray(parsed)) {
|
|
247
|
+
process.stderr.write(
|
|
248
|
+
`
|
|
249
|
+
Warning: could not parse ${keymapPath}; not modifying it. Add this binding manually:
|
|
250
|
+
${JSON.stringify(binding, null, 2)}
|
|
251
|
+
`
|
|
252
|
+
);
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
const keymap = parsed;
|
|
256
|
+
if (upsertKeymapBinding(keymap, chord, label) === keymap) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
if (hasConflictingChord(keymap, chord, label)) {
|
|
260
|
+
process.stderr.write(
|
|
261
|
+
`
|
|
262
|
+
Note: "${chord}" is already bound in ${keymapPath}; wt's agent binding will take precedence.
|
|
263
|
+
`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const edits = modify(raw, [keymap.length], binding, {
|
|
267
|
+
isArrayInsertion: true,
|
|
268
|
+
formattingOptions: { insertSpaces: true, tabSize: 2 }
|
|
269
|
+
});
|
|
270
|
+
const updated = applyEdits(raw, edits);
|
|
271
|
+
writeFileSync(keymapPath, updated.endsWith("\n") ? updated : `${updated}
|
|
272
|
+
`);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
var ACCESSIBILITY_SETTINGS_URL = "x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility";
|
|
276
|
+
function defaultRunner(script) {
|
|
277
|
+
return new Promise((resolve) => {
|
|
278
|
+
const child = spawn("osascript", ["-e", script], {
|
|
279
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
280
|
+
});
|
|
281
|
+
let stderr = "";
|
|
282
|
+
child.stderr?.on("data", (d) => {
|
|
283
|
+
stderr += d.toString();
|
|
284
|
+
});
|
|
285
|
+
child.on("error", (err) => resolve({ code: null, stderr: err.message }));
|
|
286
|
+
child.on("close", (code) => resolve({ code, stderr }));
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function isAccessibilityError(stderr) {
|
|
290
|
+
return /\b1002\b/.test(stderr) || /-1719\b/.test(stderr) || /not allowed to send keystrokes/i.test(stderr) || /not allowed assistive access/i.test(stderr);
|
|
291
|
+
}
|
|
292
|
+
async function triggerChord(chord, opts = {}) {
|
|
293
|
+
if (process.platform !== "darwin") {
|
|
294
|
+
return { ok: false, reason: "unsupported" };
|
|
295
|
+
}
|
|
296
|
+
const { runner = defaultRunner, loadDelay, activateDelay } = opts;
|
|
297
|
+
const { code, stderr } = await runner(
|
|
298
|
+
buildOsascript(chord, { loadDelay, activateDelay })
|
|
299
|
+
);
|
|
300
|
+
if (code === 0) return { ok: true };
|
|
301
|
+
if (isAccessibilityError(stderr)) {
|
|
302
|
+
return { ok: false, reason: "accessibility", message: stderr.trim() };
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
ok: false,
|
|
306
|
+
reason: "error",
|
|
307
|
+
message: stderr.trim() || `osascript exited with code ${code}`
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function defaultOpen(url) {
|
|
311
|
+
const child = spawn("open", [url], { detached: true, stdio: "ignore" });
|
|
312
|
+
child.on("error", () => {
|
|
313
|
+
});
|
|
314
|
+
child.unref();
|
|
315
|
+
}
|
|
316
|
+
function openAccessibilitySettings(open = defaultOpen) {
|
|
317
|
+
if (process.platform !== "darwin") return;
|
|
318
|
+
open(ACCESSIBILITY_SETTINGS_URL);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/commands/agent.ts
|
|
322
|
+
var CLEANUP_DELAY_MS = 2e4;
|
|
323
|
+
var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
324
|
+
async function createAgentWorktree(branch, planPrompt, options = {}) {
|
|
325
|
+
const prepared = await prepareWorktree(branch, options);
|
|
326
|
+
if (!prepared) return;
|
|
327
|
+
const { status, config, worktreePath } = prepared;
|
|
328
|
+
if (status === "exists") {
|
|
329
|
+
const prompt = options.existingWorktreePrompt ?? promptExistingWorktree;
|
|
330
|
+
const action = await prompt(worktreePath, { allowAgent: true });
|
|
331
|
+
if (action === "quit") return;
|
|
332
|
+
if (action === "open") {
|
|
333
|
+
await openConfiguredIde(config, worktreePath);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
await startAgentInWorktree(config, worktreePath, planPrompt);
|
|
338
|
+
}
|
|
339
|
+
async function startAgentInWorktree(config, worktreePath, planPrompt) {
|
|
340
|
+
if (config.ide !== "zed") {
|
|
341
|
+
console.warn(
|
|
342
|
+
pc.yellow(
|
|
343
|
+
`\u26A0 Agent auto-start requires Zed (ide is "${config.ide}"). Opening without starting the agent.`
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
await openConfiguredIde(config, worktreePath);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!config.agent_command) {
|
|
350
|
+
console.error(
|
|
351
|
+
pc.red("No agent_command configured. Set it with `wt config`.")
|
|
352
|
+
);
|
|
353
|
+
await openConfiguredIde(config, worktreePath);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
const task = buildAgentTask(
|
|
357
|
+
config.agent_command,
|
|
358
|
+
planPrompt,
|
|
359
|
+
AGENT_TASK_LABEL
|
|
360
|
+
);
|
|
361
|
+
const created = writeAgentTask(worktreePath, task);
|
|
362
|
+
const keymapOk = ensureKeymap(config.agent_trigger_chord, AGENT_TASK_LABEL);
|
|
363
|
+
const opened = await openConfiguredIde(config, worktreePath);
|
|
364
|
+
if (!opened) {
|
|
365
|
+
console.error(pc.red("\u2717 Could not open Zed."));
|
|
366
|
+
cleanupAgentTask(worktreePath, AGENT_TASK_LABEL, created);
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
if (!keymapOk) {
|
|
370
|
+
console.warn(
|
|
371
|
+
pc.yellow(
|
|
372
|
+
`\u26A0 Could not install the Zed keybinding (see above). In Zed, press ${config.agent_trigger_chord} to start the agent manually.`
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
console.log(pc.dim("Starting agent in Zed\u2026"));
|
|
378
|
+
let result = await triggerChord(config.agent_trigger_chord);
|
|
379
|
+
while (!result.ok && result.reason === "accessibility" && process.stdin.isTTY) {
|
|
380
|
+
console.warn(
|
|
381
|
+
pc.yellow(
|
|
382
|
+
"\u26A0 macOS Accessibility permission is required to send the keystroke that starts the agent."
|
|
383
|
+
)
|
|
384
|
+
);
|
|
385
|
+
openAccessibilitySettings();
|
|
386
|
+
const proceed = await clack.confirm({
|
|
387
|
+
message: "Grant Accessibility to the app running wt (e.g. Zed) in the panel that opened, then confirm to retry. (If that app was already running, you may need to quit and reopen it for the grant to take effect.)"
|
|
388
|
+
});
|
|
389
|
+
if (clack.isCancel(proceed) || !proceed) break;
|
|
390
|
+
result = await triggerChord(config.agent_trigger_chord, {
|
|
391
|
+
loadDelay: 0,
|
|
392
|
+
activateDelay: 0.5
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
if (!result.ok) {
|
|
396
|
+
reportTriggerFailure(result, config.agent_trigger_chord);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
console.log(pc.green("\u2713 Agent started"));
|
|
400
|
+
await delay(CLEANUP_DELAY_MS);
|
|
401
|
+
cleanupAgentTask(worktreePath, AGENT_TASK_LABEL, created);
|
|
402
|
+
}
|
|
403
|
+
function reportTriggerFailure(result, chord) {
|
|
404
|
+
if (result.reason === "unsupported") {
|
|
405
|
+
console.warn(
|
|
406
|
+
pc.yellow(
|
|
407
|
+
`\u26A0 Agent auto-start is only supported on macOS. The worktree and Zed are open; press ${chord} in Zed to start the agent.`
|
|
408
|
+
)
|
|
409
|
+
);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (result.reason === "accessibility") {
|
|
413
|
+
console.warn(
|
|
414
|
+
pc.yellow(
|
|
415
|
+
`\u26A0 Agent not started (Accessibility not granted). In Zed, press ${chord} to start it manually.`
|
|
416
|
+
)
|
|
417
|
+
);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
console.warn(
|
|
421
|
+
pc.yellow(
|
|
422
|
+
`\u26A0 Could not auto-start the agent${result.message ? `: ${result.message}` : ""}. In Zed, press ${chord} to start it manually.`
|
|
423
|
+
)
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
export {
|
|
427
|
+
createAgentWorktree
|
|
428
|
+
};
|
|
@@ -2,18 +2,20 @@
|
|
|
2
2
|
import {
|
|
3
3
|
addWorktree,
|
|
4
4
|
branchExists,
|
|
5
|
+
fetchRemote,
|
|
5
6
|
getRegisteredRepos,
|
|
6
7
|
getRepoRoot,
|
|
8
|
+
listWorktrees,
|
|
7
9
|
openIde,
|
|
8
10
|
registerRepo,
|
|
9
11
|
resolveWorktreePath,
|
|
10
12
|
runBranchInput,
|
|
11
13
|
runRepoPicker
|
|
12
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-FSARWOCW.js";
|
|
13
15
|
import {
|
|
14
16
|
createStore,
|
|
15
17
|
getEffectiveConfig
|
|
16
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-IQWENLPW.js";
|
|
17
19
|
|
|
18
20
|
// src/commands/create.ts
|
|
19
21
|
import { existsSync } from "fs";
|
|
@@ -48,7 +50,7 @@ function runCommand(command, cwd) {
|
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
// src/commands/create.ts
|
|
51
|
-
async function
|
|
53
|
+
async function prepareWorktree(branch, options = {}) {
|
|
52
54
|
const {
|
|
53
55
|
cwd = process.cwd(),
|
|
54
56
|
store = createStore(),
|
|
@@ -66,7 +68,7 @@ async function createWorktree(branch, options = {}) {
|
|
|
66
68
|
"No repos registered. cd into a repo and run wt create to get started."
|
|
67
69
|
)
|
|
68
70
|
);
|
|
69
|
-
return;
|
|
71
|
+
return null;
|
|
70
72
|
}
|
|
71
73
|
if (!process.stdin.isTTY) {
|
|
72
74
|
console.error(
|
|
@@ -77,21 +79,21 @@ async function createWorktree(branch, options = {}) {
|
|
|
77
79
|
process.exit(1);
|
|
78
80
|
}
|
|
79
81
|
const picked = await repoPicker(repos);
|
|
80
|
-
if (!picked) return;
|
|
82
|
+
if (!picked) return null;
|
|
81
83
|
repoRoot = picked;
|
|
82
84
|
if (!branch) {
|
|
83
85
|
const entered = await branchInput(repoRoot);
|
|
84
|
-
if (!entered) return;
|
|
86
|
+
if (!entered) return null;
|
|
85
87
|
branch = entered;
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
|
-
if (!repoRoot) return;
|
|
90
|
+
if (!repoRoot) return null;
|
|
89
91
|
if (!branch) {
|
|
90
92
|
const input = await clack.text({
|
|
91
93
|
message: "Branch name:",
|
|
92
|
-
validate: (v) => v.length === 0 ? "Required" : void 0
|
|
94
|
+
validate: (v) => !v || v.length === 0 ? "Required" : void 0
|
|
93
95
|
});
|
|
94
|
-
if (clack.isCancel(input)) return;
|
|
96
|
+
if (clack.isCancel(input)) return null;
|
|
95
97
|
branch = input;
|
|
96
98
|
}
|
|
97
99
|
registerRepo(repoRoot, store);
|
|
@@ -102,7 +104,31 @@ async function createWorktree(branch, options = {}) {
|
|
|
102
104
|
branch
|
|
103
105
|
);
|
|
104
106
|
if (existsSync(worktreePath)) {
|
|
105
|
-
|
|
107
|
+
const isWorktree = listWorktrees(repoRoot).some(
|
|
108
|
+
(wt) => wt.path === worktreePath
|
|
109
|
+
);
|
|
110
|
+
if (!isWorktree) {
|
|
111
|
+
console.error(
|
|
112
|
+
pc.red(
|
|
113
|
+
`Path already exists but is not a git worktree: ${worktreePath}`
|
|
114
|
+
)
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
return { status: "exists", repoRoot, worktreePath, config };
|
|
119
|
+
}
|
|
120
|
+
const parts = config.base_branch.split("/", 2);
|
|
121
|
+
if (parts.length === 2) {
|
|
122
|
+
const remote = parts[0];
|
|
123
|
+
try {
|
|
124
|
+
fetchRemote(repoRoot, remote);
|
|
125
|
+
} catch (err) {
|
|
126
|
+
console.warn(
|
|
127
|
+
pc.yellow(
|
|
128
|
+
`\u26A0 Could not fetch from ${remote} \u2014 using local state${err instanceof Error ? ` (${err.message})` : ""}`
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
}
|
|
106
132
|
}
|
|
107
133
|
const exists = branchExists(repoRoot, branch);
|
|
108
134
|
if (exists) {
|
|
@@ -124,17 +150,47 @@ async function createWorktree(branch, options = {}) {
|
|
|
124
150
|
process.exit(1);
|
|
125
151
|
}
|
|
126
152
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
133
|
-
if (opened) {
|
|
134
|
-
console.log(pc.green(`\u2713 Opened ${config.ide}`));
|
|
135
|
-
}
|
|
153
|
+
return { status: "created", repoRoot, worktreePath, config };
|
|
154
|
+
}
|
|
155
|
+
async function promptExistingWorktree(worktreePath, opts) {
|
|
156
|
+
if (!process.stdin.isTTY) {
|
|
157
|
+
console.error(pc.red(`Worktree path already exists: ${worktreePath}`));
|
|
158
|
+
process.exit(1);
|
|
136
159
|
}
|
|
160
|
+
const choice = await clack.select({
|
|
161
|
+
message: `Worktree already exists at ${worktreePath}.`,
|
|
162
|
+
options: [
|
|
163
|
+
{ value: "open", label: "Open in IDE" },
|
|
164
|
+
...opts.allowAgent ? [{ value: "agent", label: "Open and start agent" }] : [],
|
|
165
|
+
{ value: "quit", label: "Ignore and quit" }
|
|
166
|
+
]
|
|
167
|
+
});
|
|
168
|
+
if (clack.isCancel(choice)) return "quit";
|
|
169
|
+
return choice;
|
|
137
170
|
}
|
|
171
|
+
async function openConfiguredIde(config, worktreePath) {
|
|
172
|
+
if (!config.ide) return false;
|
|
173
|
+
const opened = await openIde(config.ide, config.ide_open_args, worktreePath);
|
|
174
|
+
if (opened) {
|
|
175
|
+
console.log(pc.green(`\u2713 Opened ${config.ide}`));
|
|
176
|
+
}
|
|
177
|
+
return opened;
|
|
178
|
+
}
|
|
179
|
+
async function createWorktree(branch, options = {}) {
|
|
180
|
+
const prepared = await prepareWorktree(branch, options);
|
|
181
|
+
if (!prepared) return;
|
|
182
|
+
const { status, config, worktreePath } = prepared;
|
|
183
|
+
if (status === "exists") {
|
|
184
|
+
const prompt = options.existingWorktreePrompt ?? promptExistingWorktree;
|
|
185
|
+
const action = await prompt(worktreePath, { allowAgent: false });
|
|
186
|
+
if (action !== "open") return;
|
|
187
|
+
}
|
|
188
|
+
await openConfiguredIde(config, worktreePath);
|
|
189
|
+
}
|
|
190
|
+
|
|
138
191
|
export {
|
|
192
|
+
prepareWorktree,
|
|
193
|
+
promptExistingWorktree,
|
|
194
|
+
openConfiguredIde,
|
|
139
195
|
createWorktree
|
|
140
196
|
};
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import {
|
|
3
3
|
createStore,
|
|
4
4
|
getGlobalConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-IQWENLPW.js";
|
|
6
6
|
|
|
7
7
|
// src/lib/git.ts
|
|
8
8
|
import { execFileSync } from "child_process";
|
|
9
|
-
import { realpathSync } from "fs";
|
|
9
|
+
import { existsSync, realpathSync, rmSync } from "fs";
|
|
10
10
|
import path from "path";
|
|
11
11
|
function getRepoRoot(cwd = process.cwd()) {
|
|
12
12
|
try {
|
|
@@ -75,11 +75,22 @@ function addWorktree(repoRoot, worktreePath, branch, baseBranch) {
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
function removeWorktree(repoRoot, worktreePath, force = false) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
try {
|
|
79
|
+
execFileSync(
|
|
80
|
+
"git",
|
|
81
|
+
["worktree", "remove", ...force ? ["--force"] : [], worktreePath],
|
|
82
|
+
{ cwd: repoRoot, stdio: "pipe" }
|
|
83
|
+
);
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (!force) throw err;
|
|
86
|
+
if (existsSync(worktreePath)) {
|
|
87
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
execFileSync("git", ["worktree", "prune"], {
|
|
90
|
+
cwd: repoRoot,
|
|
91
|
+
stdio: "pipe"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
83
94
|
}
|
|
84
95
|
function listWorktreeDirtyFiles(worktreePath) {
|
|
85
96
|
try {
|
|
@@ -113,6 +124,13 @@ function branchExists(repoRoot, branch) {
|
|
|
113
124
|
return false;
|
|
114
125
|
}
|
|
115
126
|
}
|
|
127
|
+
function fetchRemote(repoRoot, remote = "origin") {
|
|
128
|
+
execFileSync("git", ["fetch", remote], {
|
|
129
|
+
cwd: repoRoot,
|
|
130
|
+
stdio: "pipe",
|
|
131
|
+
timeout: 3e4
|
|
132
|
+
});
|
|
133
|
+
}
|
|
116
134
|
function resolveWorktreePath(repoRoot, worktreePath, branch) {
|
|
117
135
|
const repoName = path.basename(repoRoot);
|
|
118
136
|
const safeBranch = branch.replace(/\//g, "-");
|
|
@@ -480,6 +498,7 @@ export {
|
|
|
480
498
|
removeWorktree,
|
|
481
499
|
listWorktreeDirtyFiles,
|
|
482
500
|
branchExists,
|
|
501
|
+
fetchRemote,
|
|
483
502
|
resolveWorktreePath,
|
|
484
503
|
openIde,
|
|
485
504
|
registerRepo,
|
package/dist/cli.js
CHANGED
|
@@ -3,16 +3,29 @@
|
|
|
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.
|
|
7
|
-
const { runList } = await import("./list-
|
|
6
|
+
program.name("wt").description("Git worktree manager").version("0.2.0-pr9.g997ac8d").action(async () => {
|
|
7
|
+
const { runList } = await import("./list-5TXRTSIW.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-
|
|
11
|
+
const { createWorktree } = await import("./create-5J5NGGGM.js");
|
|
12
12
|
await createWorktree(branch);
|
|
13
13
|
});
|
|
14
|
-
program.command("
|
|
15
|
-
const {
|
|
16
|
-
|
|
14
|
+
program.command("agent <branch> <plan_prompt>").description("Create a worktree and auto-start an AI agent in Zed (macOS)").action(async (branch, planPrompt) => {
|
|
15
|
+
const { createAgentWorktree } = await import("./agent-PKCZHGN7.js");
|
|
16
|
+
await createAgentWorktree(branch, planPrompt);
|
|
17
|
+
});
|
|
18
|
+
program.command("config").description("Open the config file in $EDITOR").option("--path", "Print the config file path and exit").action(async (options) => {
|
|
19
|
+
if (options.path) {
|
|
20
|
+
const { printConfigPath } = await import("./config-OSNYQCZL.js");
|
|
21
|
+
printConfigPath();
|
|
22
|
+
} else {
|
|
23
|
+
const { openConfig } = await import("./config-OSNYQCZL.js");
|
|
24
|
+
openConfig();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
program.command("skill").description("Print the wt skill file to stdout").action(async () => {
|
|
28
|
+
const { printSkill } = await import("./skill-RR6MNLWH.js");
|
|
29
|
+
printSkill();
|
|
17
30
|
});
|
|
18
31
|
await program.parseAsync(process.argv);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createStore
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-IQWENLPW.js";
|
|
5
5
|
|
|
6
6
|
// src/commands/config.ts
|
|
7
7
|
import { spawn } from "child_process";
|
|
8
8
|
function getConfigPath(store = createStore()) {
|
|
9
9
|
return store.path;
|
|
10
10
|
}
|
|
11
|
+
function printConfigPath(store = createStore()) {
|
|
12
|
+
console.log(store.path);
|
|
13
|
+
}
|
|
11
14
|
function openConfig(store = createStore()) {
|
|
12
15
|
const configPath = store.path;
|
|
13
16
|
console.log(`Config: ${configPath}`);
|
|
@@ -21,5 +24,6 @@ function openConfig(store = createStore()) {
|
|
|
21
24
|
}
|
|
22
25
|
export {
|
|
23
26
|
getConfigPath,
|
|
24
|
-
openConfig
|
|
27
|
+
openConfig,
|
|
28
|
+
printConfigPath
|
|
25
29
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
createWorktree,
|
|
4
|
+
openConfiguredIde,
|
|
5
|
+
prepareWorktree,
|
|
6
|
+
promptExistingWorktree
|
|
7
|
+
} from "./chunk-BYWCGKUP.js";
|
|
8
|
+
import "./chunk-FSARWOCW.js";
|
|
9
|
+
import "./chunk-IQWENLPW.js";
|
|
10
|
+
export {
|
|
11
|
+
createWorktree,
|
|
12
|
+
openConfiguredIde,
|
|
13
|
+
prepareWorktree,
|
|
14
|
+
promptExistingWorktree
|
|
15
|
+
};
|
|
@@ -8,11 +8,11 @@ import {
|
|
|
8
8
|
registerRepo,
|
|
9
9
|
removeWorktree,
|
|
10
10
|
runInteractiveList
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-FSARWOCW.js";
|
|
12
12
|
import {
|
|
13
13
|
createStore,
|
|
14
14
|
getEffectiveConfig
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-IQWENLPW.js";
|
|
16
16
|
|
|
17
17
|
// src/commands/list.ts
|
|
18
18
|
import * as clack from "@clack/prompts";
|
|
@@ -66,6 +66,23 @@ async function runList(options = {}) {
|
|
|
66
66
|
return true;
|
|
67
67
|
} catch (err) {
|
|
68
68
|
const msg = String(err);
|
|
69
|
+
if (msg.includes("cannot be moved or removed")) {
|
|
70
|
+
clack.log.warn(
|
|
71
|
+
"Worktree contains git submodules, which prevent standard removal."
|
|
72
|
+
);
|
|
73
|
+
const force = await clack.confirm({
|
|
74
|
+
message: `Force delete ${pc.bold(item.branch)}? The worktree directory will be removed directly.`
|
|
75
|
+
});
|
|
76
|
+
if (clack.isCancel(force) || !force) return false;
|
|
77
|
+
try {
|
|
78
|
+
removeWorktree(item.repoRoot, item.path, true);
|
|
79
|
+
console.log(pc.green(`\u2713 Force-removed ${item.branch}`));
|
|
80
|
+
return true;
|
|
81
|
+
} catch (err2) {
|
|
82
|
+
console.error(pc.red(`\u2717 Failed to force-remove: ${String(err2)}`));
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
69
86
|
if (msg.includes("modified or untracked files")) {
|
|
70
87
|
const dirty = listWorktreeDirtyFiles(item.path);
|
|
71
88
|
if (dirty.length > 0) {
|
|
@@ -93,7 +110,7 @@ ${dirty.map((f) => ` ${f}`).join("\n")}`
|
|
|
93
110
|
},
|
|
94
111
|
onCreate: async () => {
|
|
95
112
|
if (repoRoot) {
|
|
96
|
-
const { createWorktree } = await import("./create-
|
|
113
|
+
const { createWorktree } = await import("./create-5J5NGGGM.js");
|
|
97
114
|
await createWorktree(void 0, { cwd: repoRoot, store });
|
|
98
115
|
}
|
|
99
116
|
}
|
|
@@ -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 / `j`/`k` \u2014 navigate\n- `Enter` \u2014 open worktree in IDE\n- `d` \u2014 delete worktree\n- `c` \u2014 create new worktree (repo mode only)\n- `/` \u2014 search\n- `q` / `Esc` \u2014 quit\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>`\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.\'\n```\n\nIt writes a temporary `.zed/tasks.json` running\n`<agent_command> \'<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\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| `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"` | Command `wt agent` runs in Zed; `<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`, `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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cestoliv/wt",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0-pr9.g997ac8d",
|
|
4
|
+
"description": "Fast, interactive TUI to browse, create, open, and delete git worktrees across repos.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/cestoliv/worktrees.git"
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"wt": "./dist/cli.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
|
-
"dist"
|
|
17
|
+
"dist",
|
|
18
|
+
"SKILL.md"
|
|
18
19
|
],
|
|
19
20
|
"publishConfig": {
|
|
20
21
|
"access": "public"
|
|
@@ -25,6 +26,7 @@
|
|
|
25
26
|
"prepublishOnly": "npm run build",
|
|
26
27
|
"test": "vitest run",
|
|
27
28
|
"test:watch": "vitest",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
28
30
|
"lint": "biome check .",
|
|
29
31
|
"format": "biome format --write ."
|
|
30
32
|
},
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"commander": "^14.0.3",
|
|
34
36
|
"conf": "^15.1.0",
|
|
35
37
|
"fuse.js": "^7.1.0",
|
|
38
|
+
"jsonc-parser": "^3.3.1",
|
|
36
39
|
"picocolors": "^1.1.1"
|
|
37
40
|
},
|
|
38
41
|
"devDependencies": {
|