workforest 0.2.3__tar.gz → 0.4.0__tar.gz
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.
- workforest-0.4.0/.claude/rules/architecture.md +16 -0
- workforest-0.4.0/.claude/rules/conventions.md +12 -0
- workforest-0.4.0/.claude/rules/tests.md +17 -0
- {workforest-0.2.3 → workforest-0.4.0}/PKG-INFO +104 -26
- {workforest-0.2.3 → workforest-0.4.0}/README.md +103 -25
- workforest-0.4.0/completions/_workforest +44 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/__init__.py +1 -1
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/cli.py +59 -35
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/commands.py +80 -28
- workforest-0.4.0/src/workforest/completions.py +96 -0
- workforest-0.4.0/src/workforest/config.py +313 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/errors.py +1 -1
- workforest-0.4.0/src/workforest/examples/config.yaml +179 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/gitutil.py +26 -23
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/hooks.py +5 -2
- workforest-0.4.0/src/workforest/integrations/__init__.py +2 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/integrations/claude.py +19 -8
- workforest-0.4.0/src/workforest/launch.py +285 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/output.py +22 -7
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/shell/completion.bash +3 -1
- workforest-0.4.0/src/workforest/shell/completion.zsh +67 -0
- workforest-0.4.0/src/workforest/shell/workforest.sh +18 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/tui.py +26 -13
- {workforest-0.2.3 → workforest-0.4.0}/tests/conftest.py +11 -6
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_claude.py +2 -2
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_cli.py +18 -5
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_commands.py +77 -4
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_config.py +160 -31
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_gitutil.py +20 -7
- workforest-0.4.0/tests/test_launch.py +431 -0
- workforest-0.4.0/tests/test_output.py +56 -0
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_shell.py +68 -9
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_tui.py +22 -6
- workforest-0.2.3/completions/_workforest +0 -23
- workforest-0.2.3/src/workforest/completions.py +0 -76
- workforest-0.2.3/src/workforest/config.py +0 -188
- workforest-0.2.3/src/workforest/examples/config.yaml +0 -141
- workforest-0.2.3/src/workforest/integrations/__init__.py +0 -2
- workforest-0.2.3/src/workforest/launch.py +0 -204
- workforest-0.2.3/src/workforest/shell/completion.zsh +0 -38
- workforest-0.2.3/src/workforest/shell/workforest.sh +0 -16
- workforest-0.2.3/tests/test_launch.py +0 -270
- {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/ci.yml +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/publish_aur.yml +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/publish_homebrew.yml +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/publish_pypi.yml +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/.github/workflows/release.yml +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/.gitignore +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/LICENSE +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/Makefile +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/packaging/AUR/PKGBUILD.template +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/packaging/homebrew/workforest.rb.template +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/pyproject.toml +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/__main__.py +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/src/workforest/shellinit.py +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/tests/__init__.py +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_harness.py +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/tests/test_hooks.py +0 -0
- {workforest-0.2.3 → workforest-0.4.0}/uv.lock +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Architecture invariants
|
|
2
|
+
|
|
3
|
+
- `gitutil.py` is the only module that spawns git. Consumers get typed
|
|
4
|
+
results; worktree data comes from `--porcelain -z` output, never from
|
|
5
|
+
parsing the human-readable form.
|
|
6
|
+
- `cli.py` is the sole stdout writer. Commands return
|
|
7
|
+
`ShellAction | str | None`; stdout carries the shell-wrapper cd protocol,
|
|
8
|
+
so nothing else may print there (hook/script stdout is diverted to stderr).
|
|
9
|
+
- `tui.py`: everything except the fzf subprocess is pure and unit-tested;
|
|
10
|
+
fzf is the only sanctioned external tool there.
|
|
11
|
+
- `completions.py` must never break the shell: any error yields an empty
|
|
12
|
+
candidate list, and output stays plain `NAME<TAB>ANNOTATION` lines.
|
|
13
|
+
- `integrations/claude.py` is experimental: it reads Claude Code's private
|
|
14
|
+
on-disk state. Session lines are rewritten by JSON parsing, never by
|
|
15
|
+
string substitution.
|
|
16
|
+
- README.md is the project reference; there is no separate design document.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Code conventions
|
|
2
|
+
|
|
3
|
+
- Python 3.14, uv-managed. Verify changes with `make check` (ruff lint +
|
|
4
|
+
format check, mypy, pytest with a 90% coverage floor). Run
|
|
5
|
+
`uv run ruff format .` rather than hand-formatting.
|
|
6
|
+
- When a return value or constant bundles fields whose positions carry
|
|
7
|
+
meaning, use a small named dataclass — `@dataclass(slots=True,
|
|
8
|
+
frozen=True)` — not an anonymous tuple or nested dict. Plain dicts are for
|
|
9
|
+
genuine key→value lookups only (env maps, branch→remotes).
|
|
10
|
+
- Pre-1.0 with zero users: on breaking changes keep the clean design and
|
|
11
|
+
state what to re-run (e.g. re-eval shell-init). Never add
|
|
12
|
+
backward-compatibility shims.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "tests/**/*.py"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test conventions
|
|
7
|
+
|
|
8
|
+
- Isolation contract (see `tests/conftest.py`): no test may read or write
|
|
9
|
+
the real environment. The autouse `isolated_env` fixture redirects HOME,
|
|
10
|
+
XDG_CONFIG_HOME, and git config, and pins SHELL=/bin/sh, EDITOR, and
|
|
11
|
+
NO_COLOR — rely on it instead of patching these per test.
|
|
12
|
+
- Build repos through the `repo` / `make_repo` fixtures and the `Repo`
|
|
13
|
+
helper methods (`add_branch`, `add_remote`, `write_project_config`,
|
|
14
|
+
`make_dirty`) rather than raw git calls.
|
|
15
|
+
- Coverage floor is 90% (`--cov-fail-under=90` in pyproject.toml); pure
|
|
16
|
+
helpers are expected to be unit-tested, subprocess/terminal glue may be
|
|
17
|
+
`# pragma: no cover`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: workforest
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Git worktree forest management: create, open, and clean up per-branch worktrees with project-defined setup hooks
|
|
5
5
|
Project-URL: Homepage, https://github.com/ArkadyBuryakov/workforest
|
|
6
6
|
Author-email: Arkady Buryakov <arkady@buryakov.pro>
|
|
@@ -20,6 +20,14 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
|
|
21
21
|
# Workforest
|
|
22
22
|
|
|
23
|
+
## Elevator pitch
|
|
24
|
+
|
|
25
|
+
Your repo has one working directory; your AI agents want five. Workforest
|
|
26
|
+
gives every feature — and every agent — its own disposable git worktree, so
|
|
27
|
+
parallel work on the same repo never collides.
|
|
28
|
+
|
|
29
|
+
## About
|
|
30
|
+
|
|
23
31
|
Git worktree forest management: one main checkout plus any number of
|
|
24
32
|
disposable, per-branch worktrees in a predictable location — created, set up,
|
|
25
33
|
opened, and cleaned up with one command.
|
|
@@ -36,8 +44,8 @@ opened, and cleaned up with one command.
|
|
|
36
44
|
- **Create** a worktree for any branch (local, remote, or brand new) and have
|
|
37
45
|
it set up automatically: symlinks for untracked assets (`node_modules`,
|
|
38
46
|
`.env`, …) and project-defined setup scripts.
|
|
39
|
-
- **Open** it in your editor — in
|
|
40
|
-
|
|
47
|
+
- **Open** it in your editor — in your terminal, in a new terminal window or
|
|
48
|
+
multiplexer pane, or in a GUI app, all from a small shell-command config.
|
|
41
49
|
- **Run** named project scripts with well-known `WF_*` environment variables.
|
|
42
50
|
- **Delete** worktrees safely, or **checkout**: collapse one back into the
|
|
43
51
|
main checkout.
|
|
@@ -98,27 +106,77 @@ Layered, YAML or JSON; later layers override earlier ones:
|
|
|
98
106
|
| project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
|
|
99
107
|
| project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
|
|
100
108
|
|
|
101
|
-
Scalars and lists replace; the `scripts
|
|
102
|
-
(`null` removes an entry). `workforest config` shows the
|
|
103
|
-
where each layer came from; `workforest init` scaffolds a
|
|
104
|
-
(`--local` for a personal one).
|
|
109
|
+
Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
|
|
110
|
+
merge per key (`null` removes an entry). `workforest config` shows the
|
|
111
|
+
merged result and where each layer came from; `workforest init` scaffolds a
|
|
112
|
+
project file (`--local` for a personal one). Nothing in the environment
|
|
113
|
+
changes the result — files and flags only.
|
|
105
114
|
|
|
106
115
|
All keys, with defaults:
|
|
107
116
|
|
|
108
117
|
```yaml
|
|
109
118
|
worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
|
|
110
|
-
opener: "" # default opener
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
119
|
+
opener: "" # default opener: an `openers` name or a shell command;
|
|
120
|
+
# "" → $VISUAL → $EDITOR
|
|
121
|
+
openers: {} # name -> what `-o NAME` runs, and where
|
|
122
|
+
wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
|
|
114
123
|
symlinks: [] # untracked assets linked from main into new worktrees
|
|
115
124
|
setup_scripts: [] # shell snippets run in a fresh worktree
|
|
116
125
|
scripts: {} # name -> snippet for `wf run NAME`
|
|
117
126
|
```
|
|
118
127
|
|
|
119
|
-
Openers
|
|
120
|
-
|
|
121
|
-
|
|
128
|
+
### Openers
|
|
129
|
+
|
|
130
|
+
An opener is a shell command that runs with the worktree root as working
|
|
131
|
+
directory, either **in your terminal** (the default: the `wf` wrapper does
|
|
132
|
+
`cd` there and runs it) or **in the background** (`background: true`:
|
|
133
|
+
spawned detached, `wf` returns immediately — for GUI apps and commands that
|
|
134
|
+
hand off to a daemon or multiplexer). Optionally it runs **through a
|
|
135
|
+
wrapper**, a command that receives it as `$WF_COMMAND` and takes it
|
|
136
|
+
somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
|
|
137
|
+
no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
opener: win # what `wf create` / `wf open` run by default
|
|
141
|
+
|
|
142
|
+
wrappers: # get the opener command as $WF_COMMAND
|
|
143
|
+
kitty:
|
|
144
|
+
command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
|
|
145
|
+
background: true
|
|
146
|
+
tmux: # the tmux server has its own environment: WF_ENV re-creates ours
|
|
147
|
+
command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
|
|
148
|
+
background: true
|
|
149
|
+
|
|
150
|
+
openers:
|
|
151
|
+
edit: $EDITOR "$WF_TARGET" # in your terminal
|
|
152
|
+
code: # GUI app: detached
|
|
153
|
+
command: code "$WF_WORKTREE"
|
|
154
|
+
background: true
|
|
155
|
+
kitty: # edit's command, in a new kitty window
|
|
156
|
+
from: edit
|
|
157
|
+
wrap: kitty
|
|
158
|
+
tmux: # edit's command, in a new tmux window
|
|
159
|
+
from: edit
|
|
160
|
+
wrap: tmux
|
|
161
|
+
git: lazygit
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
An entry is a shell command string, or a mapping with either `command` (a
|
|
165
|
+
shell command, never a name) or `from` (another opener's command — one
|
|
166
|
+
level: the target has a `command` of its own — inheriting its `background`
|
|
167
|
+
unless the entry sets one), plus optionally `background: true` or
|
|
168
|
+
`wrap: NAME`. `wrap` and `background` never sit on the same entry: the
|
|
169
|
+
wrapper decides where the whole thing runs, via its own `background` flag.
|
|
170
|
+
`-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
|
|
171
|
+
`-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
|
|
172
|
+
none). Wrappers are environment-specific, so they belong in the per-machine
|
|
173
|
+
user config — a host without your terminal emulator simply has none.
|
|
174
|
+
`from` and `wrap` names are checked when the config loads, so `wf config`
|
|
175
|
+
reports a misspelled one.
|
|
176
|
+
|
|
177
|
+
All of them are plain shell commands, run via `$SHELL -c` with one variable
|
|
178
|
+
family in the environment — the same family the launched process and every
|
|
179
|
+
script receive:
|
|
122
180
|
|
|
123
181
|
| Variable | Value |
|
|
124
182
|
|---|---|
|
|
@@ -129,16 +187,21 @@ environment variables:
|
|
|
129
187
|
| `WF_BRANCH` | its branch (empty if detached) |
|
|
130
188
|
| `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
|
|
131
189
|
| `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
`$
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
190
|
+
| `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
|
|
191
|
+
| `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
|
|
192
|
+
|
|
193
|
+
Standard shell rules apply — there is no workforest template syntax:
|
|
194
|
+
`"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
|
|
195
|
+
braces, pipes, and `&&` mean whatever your shell says they mean (a
|
|
196
|
+
misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
|
|
197
|
+
reaches the wrapper unexpanded, so run it through a shell of its own for its
|
|
198
|
+
`$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
|
|
199
|
+
runs somewhere this environment is not inherited — a tmux server, an ssh
|
|
200
|
+
host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
|
|
201
|
+
re-parsed on the far side, quoting intact. Background
|
|
202
|
+
processes shed activation state inherited from the invoking shell (Python
|
|
203
|
+
venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
|
|
204
|
+
environment it cannot deactivate.
|
|
142
205
|
|
|
143
206
|
Fully commented reference configs:
|
|
144
207
|
[`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
|
|
@@ -177,8 +240,8 @@ scripts:
|
|
|
177
240
|
## Commands
|
|
178
241
|
|
|
179
242
|
```
|
|
180
|
-
workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
|
|
181
|
-
workforest open [NAME] [-o OPENER] [-p PATH]
|
|
243
|
+
workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
|
|
244
|
+
workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
|
|
182
245
|
workforest list [--porcelain]
|
|
183
246
|
workforest delete NAME... [--force] [--delete-branch | --keep-branch]
|
|
184
247
|
workforest checkout NAME [--force]
|
|
@@ -187,8 +250,23 @@ workforest tui [MODE]
|
|
|
187
250
|
workforest init [--local]
|
|
188
251
|
workforest config [--json]
|
|
189
252
|
workforest shell-init [bash|zsh]
|
|
253
|
+
workforest claude copy-session SESSION_ID # experimental
|
|
190
254
|
```
|
|
191
255
|
|
|
256
|
+
`open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
|
|
257
|
+
worktree you are standing in.
|
|
258
|
+
|
|
259
|
+
`create` resolves BRANCH in order: existing local branch, then a branch on
|
|
260
|
+
exactly one remote (checked out tracking it), then a brand-new branch.
|
|
261
|
+
`REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
|
|
262
|
+
carry the same branch name; if that local name is already taken, `create`
|
|
263
|
+
prompts for a different one.
|
|
264
|
+
|
|
265
|
+
`workforest claude` (shown only when `~/.claude` exists) copies a Claude
|
|
266
|
+
Code session from the main worktree into the current one. It is
|
|
267
|
+
**experimental**: it manipulates Claude Code's private on-disk state,
|
|
268
|
+
which is not a stable interface, so any Claude Code update may break it.
|
|
269
|
+
|
|
192
270
|
Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
|
|
193
271
|
Human messages go to stderr; stdout carries only machine output (`cd`
|
|
194
272
|
directives for the `wf` wrapper, `--porcelain` listings, dumps).
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Workforest
|
|
2
2
|
|
|
3
|
+
## Elevator pitch
|
|
4
|
+
|
|
5
|
+
Your repo has one working directory; your AI agents want five. Workforest
|
|
6
|
+
gives every feature — and every agent — its own disposable git worktree, so
|
|
7
|
+
parallel work on the same repo never collides.
|
|
8
|
+
|
|
9
|
+
## About
|
|
10
|
+
|
|
3
11
|
Git worktree forest management: one main checkout plus any number of
|
|
4
12
|
disposable, per-branch worktrees in a predictable location — created, set up,
|
|
5
13
|
opened, and cleaned up with one command.
|
|
@@ -16,8 +24,8 @@ opened, and cleaned up with one command.
|
|
|
16
24
|
- **Create** a worktree for any branch (local, remote, or brand new) and have
|
|
17
25
|
it set up automatically: symlinks for untracked assets (`node_modules`,
|
|
18
26
|
`.env`, …) and project-defined setup scripts.
|
|
19
|
-
- **Open** it in your editor — in
|
|
20
|
-
|
|
27
|
+
- **Open** it in your editor — in your terminal, in a new terminal window or
|
|
28
|
+
multiplexer pane, or in a GUI app, all from a small shell-command config.
|
|
21
29
|
- **Run** named project scripts with well-known `WF_*` environment variables.
|
|
22
30
|
- **Delete** worktrees safely, or **checkout**: collapse one back into the
|
|
23
31
|
main checkout.
|
|
@@ -78,27 +86,77 @@ Layered, YAML or JSON; later layers override earlier ones:
|
|
|
78
86
|
| project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
|
|
79
87
|
| project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
|
|
80
88
|
|
|
81
|
-
Scalars and lists replace; the `scripts
|
|
82
|
-
(`null` removes an entry). `workforest config` shows the
|
|
83
|
-
where each layer came from; `workforest init` scaffolds a
|
|
84
|
-
(`--local` for a personal one).
|
|
89
|
+
Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
|
|
90
|
+
merge per key (`null` removes an entry). `workforest config` shows the
|
|
91
|
+
merged result and where each layer came from; `workforest init` scaffolds a
|
|
92
|
+
project file (`--local` for a personal one). Nothing in the environment
|
|
93
|
+
changes the result — files and flags only.
|
|
85
94
|
|
|
86
95
|
All keys, with defaults:
|
|
87
96
|
|
|
88
97
|
```yaml
|
|
89
98
|
worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
|
|
90
|
-
opener: "" # default opener
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
99
|
+
opener: "" # default opener: an `openers` name or a shell command;
|
|
100
|
+
# "" → $VISUAL → $EDITOR
|
|
101
|
+
openers: {} # name -> what `-o NAME` runs, and where
|
|
102
|
+
wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
|
|
94
103
|
symlinks: [] # untracked assets linked from main into new worktrees
|
|
95
104
|
setup_scripts: [] # shell snippets run in a fresh worktree
|
|
96
105
|
scripts: {} # name -> snippet for `wf run NAME`
|
|
97
106
|
```
|
|
98
107
|
|
|
99
|
-
Openers
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
### Openers
|
|
109
|
+
|
|
110
|
+
An opener is a shell command that runs with the worktree root as working
|
|
111
|
+
directory, either **in your terminal** (the default: the `wf` wrapper does
|
|
112
|
+
`cd` there and runs it) or **in the background** (`background: true`:
|
|
113
|
+
spawned detached, `wf` returns immediately — for GUI apps and commands that
|
|
114
|
+
hand off to a daemon or multiplexer). Optionally it runs **through a
|
|
115
|
+
wrapper**, a command that receives it as `$WF_COMMAND` and takes it
|
|
116
|
+
somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
|
|
117
|
+
no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
opener: win # what `wf create` / `wf open` run by default
|
|
121
|
+
|
|
122
|
+
wrappers: # get the opener command as $WF_COMMAND
|
|
123
|
+
kitty:
|
|
124
|
+
command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
|
|
125
|
+
background: true
|
|
126
|
+
tmux: # the tmux server has its own environment: WF_ENV re-creates ours
|
|
127
|
+
command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
|
|
128
|
+
background: true
|
|
129
|
+
|
|
130
|
+
openers:
|
|
131
|
+
edit: $EDITOR "$WF_TARGET" # in your terminal
|
|
132
|
+
code: # GUI app: detached
|
|
133
|
+
command: code "$WF_WORKTREE"
|
|
134
|
+
background: true
|
|
135
|
+
kitty: # edit's command, in a new kitty window
|
|
136
|
+
from: edit
|
|
137
|
+
wrap: kitty
|
|
138
|
+
tmux: # edit's command, in a new tmux window
|
|
139
|
+
from: edit
|
|
140
|
+
wrap: tmux
|
|
141
|
+
git: lazygit
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
An entry is a shell command string, or a mapping with either `command` (a
|
|
145
|
+
shell command, never a name) or `from` (another opener's command — one
|
|
146
|
+
level: the target has a `command` of its own — inheriting its `background`
|
|
147
|
+
unless the entry sets one), plus optionally `background: true` or
|
|
148
|
+
`wrap: NAME`. `wrap` and `background` never sit on the same entry: the
|
|
149
|
+
wrapper decides where the whole thing runs, via its own `background` flag.
|
|
150
|
+
`-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
|
|
151
|
+
`-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
|
|
152
|
+
none). Wrappers are environment-specific, so they belong in the per-machine
|
|
153
|
+
user config — a host without your terminal emulator simply has none.
|
|
154
|
+
`from` and `wrap` names are checked when the config loads, so `wf config`
|
|
155
|
+
reports a misspelled one.
|
|
156
|
+
|
|
157
|
+
All of them are plain shell commands, run via `$SHELL -c` with one variable
|
|
158
|
+
family in the environment — the same family the launched process and every
|
|
159
|
+
script receive:
|
|
102
160
|
|
|
103
161
|
| Variable | Value |
|
|
104
162
|
|---|---|
|
|
@@ -109,16 +167,21 @@ environment variables:
|
|
|
109
167
|
| `WF_BRANCH` | its branch (empty if detached) |
|
|
110
168
|
| `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
|
|
111
169
|
| `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
`$
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
170
|
+
| `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
|
|
171
|
+
| `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
|
|
172
|
+
|
|
173
|
+
Standard shell rules apply — there is no workforest template syntax:
|
|
174
|
+
`"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
|
|
175
|
+
braces, pipes, and `&&` mean whatever your shell says they mean (a
|
|
176
|
+
misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
|
|
177
|
+
reaches the wrapper unexpanded, so run it through a shell of its own for its
|
|
178
|
+
`$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
|
|
179
|
+
runs somewhere this environment is not inherited — a tmux server, an ssh
|
|
180
|
+
host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
|
|
181
|
+
re-parsed on the far side, quoting intact. Background
|
|
182
|
+
processes shed activation state inherited from the invoking shell (Python
|
|
183
|
+
venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
|
|
184
|
+
environment it cannot deactivate.
|
|
122
185
|
|
|
123
186
|
Fully commented reference configs:
|
|
124
187
|
[`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
|
|
@@ -157,8 +220,8 @@ scripts:
|
|
|
157
220
|
## Commands
|
|
158
221
|
|
|
159
222
|
```
|
|
160
|
-
workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
|
|
161
|
-
workforest open [NAME] [-o OPENER] [-p PATH]
|
|
223
|
+
workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
|
|
224
|
+
workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
|
|
162
225
|
workforest list [--porcelain]
|
|
163
226
|
workforest delete NAME... [--force] [--delete-branch | --keep-branch]
|
|
164
227
|
workforest checkout NAME [--force]
|
|
@@ -167,8 +230,23 @@ workforest tui [MODE]
|
|
|
167
230
|
workforest init [--local]
|
|
168
231
|
workforest config [--json]
|
|
169
232
|
workforest shell-init [bash|zsh]
|
|
233
|
+
workforest claude copy-session SESSION_ID # experimental
|
|
170
234
|
```
|
|
171
235
|
|
|
236
|
+
`open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
|
|
237
|
+
worktree you are standing in.
|
|
238
|
+
|
|
239
|
+
`create` resolves BRANCH in order: existing local branch, then a branch on
|
|
240
|
+
exactly one remote (checked out tracking it), then a brand-new branch.
|
|
241
|
+
`REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
|
|
242
|
+
carry the same branch name; if that local name is already taken, `create`
|
|
243
|
+
prompts for a different one.
|
|
244
|
+
|
|
245
|
+
`workforest claude` (shown only when `~/.claude` exists) copies a Claude
|
|
246
|
+
Code session from the main worktree into the current one. It is
|
|
247
|
+
**experimental**: it manipulates Claude Code's private on-disk state,
|
|
248
|
+
which is not a stable interface, so any Claude Code update may break it.
|
|
249
|
+
|
|
172
250
|
Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
|
|
173
251
|
Human messages go to stderr; stdout carries only machine output (`cd`
|
|
174
252
|
directives for the `wf` wrapper, `--porcelain` listings, dumps).
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#compdef workforest wf
|
|
2
|
+
# zsh completion for workforest / wf — installed to zsh site-functions.
|
|
3
|
+
# Dynamic candidates are delegated to `workforest --complete TOPIC`.
|
|
4
|
+
|
|
5
|
+
local topic cmd line name rest kind desc
|
|
6
|
+
local -a items cmds
|
|
7
|
+
cmd="${words[2]:-}"
|
|
8
|
+
if (( CURRENT == 2 )); then
|
|
9
|
+
topic=commands
|
|
10
|
+
else
|
|
11
|
+
case "$cmd" in
|
|
12
|
+
create) topic=branches ;;
|
|
13
|
+
open|delete|checkout) topic=worktrees ;;
|
|
14
|
+
run) topic=scripts ;;
|
|
15
|
+
claude) topic=claude-sessions ;;
|
|
16
|
+
tui|list|init|config|shell-init) topic=none ;;
|
|
17
|
+
*) topic=worktrees ;;
|
|
18
|
+
esac
|
|
19
|
+
fi
|
|
20
|
+
if [[ "$topic" != none ]]; then
|
|
21
|
+
items=(${(f)"$(workforest --complete "$topic" 2>/dev/null)"})
|
|
22
|
+
if [[ "$topic" == commands ]]; then
|
|
23
|
+
# NAME<TAB>KIND<TAB>DESCRIPTION → described candidates; one group
|
|
24
|
+
# (kind spelled out in the description) so list order, alignment,
|
|
25
|
+
# and the command/opener distinction survive fzf-tab's merging.
|
|
26
|
+
# Cyan for openers, but only under fzf-tab (fzf renders ANSI with
|
|
27
|
+
# --ansi; plain complist would print the escapes literally).
|
|
28
|
+
local pre="" post=""
|
|
29
|
+
if (( ${+functions[fzf-tab-complete]} )); then
|
|
30
|
+
pre=$'\e[36m' post=$'\e[0m'
|
|
31
|
+
fi
|
|
32
|
+
for line in "${items[@]}"; do
|
|
33
|
+
name="${line%%$'\t'*}"
|
|
34
|
+
rest="${line#*$'\t'}"
|
|
35
|
+
kind="${rest%%$'\t'*}"
|
|
36
|
+
desc="${rest#*$'\t'}"
|
|
37
|
+
[[ "$kind" == opener ]] && desc="${pre}opener: ${desc}${post}"
|
|
38
|
+
cmds+=("${name//:/\\:}:${desc}")
|
|
39
|
+
done
|
|
40
|
+
(( ${#cmds} )) && _describe -t commands 'workforest command' cmds
|
|
41
|
+
else
|
|
42
|
+
(( ${#items} )) && compadd -a items
|
|
43
|
+
fi
|
|
44
|
+
fi
|