workforest 0.3.0__tar.gz → 0.5.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.3.0 → workforest-0.5.0}/.claude/rules/architecture.md +5 -0
- {workforest-0.3.0 → workforest-0.5.0}/.github/workflows/ci.yml +6 -2
- {workforest-0.3.0 → workforest-0.5.0}/.github/workflows/publish_aur.yml +2 -1
- {workforest-0.3.0 → workforest-0.5.0}/.github/workflows/publish_homebrew.yml +6 -0
- workforest-0.5.0/PKG-INFO +369 -0
- workforest-0.5.0/README.md +349 -0
- workforest-0.5.0/completions/_workforest +35 -0
- workforest-0.5.0/man/wf.1 +1 -0
- workforest-0.5.0/man/wf.5 +1 -0
- workforest-0.5.0/man/workforest.1 +535 -0
- workforest-0.5.0/man/workforest.5 +398 -0
- {workforest-0.3.0 → workforest-0.5.0}/packaging/AUR/PKGBUILD.template +1 -0
- {workforest-0.3.0 → workforest-0.5.0}/packaging/homebrew/workforest.rb.template +9 -0
- {workforest-0.3.0 → workforest-0.5.0}/pyproject.toml +9 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/__init__.py +1 -1
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/cli.py +32 -15
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/commands.py +30 -3
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/completions.py +12 -13
- workforest-0.5.0/src/workforest/config.py +352 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/errors.py +8 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/examples/config.yaml +86 -47
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/gitutil.py +8 -0
- workforest-0.5.0/src/workforest/hooks.py +493 -0
- workforest-0.5.0/src/workforest/jobs.py +234 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/launch.py +99 -49
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/shell/completion.bash +7 -4
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/shell/completion.zsh +7 -24
- workforest-0.5.0/src/workforest/shellinit.py +58 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_cli.py +54 -9
- workforest-0.5.0/tests/test_config.py +475 -0
- workforest-0.5.0/tests/test_hooks.py +630 -0
- workforest-0.5.0/tests/test_jobs.py +310 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_launch.py +187 -58
- workforest-0.5.0/tests/test_man.py +103 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_shell.py +80 -18
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_tui.py +13 -3
- workforest-0.3.0/PKG-INFO +0 -240
- workforest-0.3.0/README.md +0 -220
- workforest-0.3.0/completions/_workforest +0 -44
- workforest-0.3.0/src/workforest/config.py +0 -200
- workforest-0.3.0/src/workforest/hooks.py +0 -149
- workforest-0.3.0/src/workforest/shellinit.py +0 -27
- workforest-0.3.0/tests/test_config.py +0 -256
- workforest-0.3.0/tests/test_hooks.py +0 -185
- {workforest-0.3.0 → workforest-0.5.0}/.claude/rules/conventions.md +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/.claude/rules/tests.md +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/.github/workflows/publish_pypi.yml +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/.github/workflows/release.yml +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/.gitignore +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/LICENSE +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/Makefile +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/__main__.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/integrations/__init__.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/integrations/claude.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/output.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/shell/workforest.sh +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/src/workforest/tui.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/__init__.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/conftest.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_claude.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_commands.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_gitutil.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_harness.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/tests/test_output.py +0 -0
- {workforest-0.3.0 → workforest-0.5.0}/uv.lock +0 -0
|
@@ -14,3 +14,8 @@
|
|
|
14
14
|
on-disk state. Session lines are rewritten by JSON parsing, never by
|
|
15
15
|
string substitution.
|
|
16
16
|
- README.md is the project reference; there is no separate design document.
|
|
17
|
+
The man pages under `man/` (`workforest.1`, `workforest.5`) are its
|
|
18
|
+
installed counterpart: any change to commands, options, config keys,
|
|
19
|
+
environment variables, or exit codes updates both README.md and the
|
|
20
|
+
affected page in the same change. `tests/test_man.py` catches drift from
|
|
21
|
+
`cli.py`, not from prose — keep the wording in sync by hand.
|
|
@@ -11,7 +11,7 @@ jobs:
|
|
|
11
11
|
container: archlinux:latest
|
|
12
12
|
steps:
|
|
13
13
|
- name: Install system dependencies
|
|
14
|
-
run: pacman -Syu --noconfirm git python uv make zsh
|
|
14
|
+
run: pacman -Syu --noconfirm git python uv make zsh groff
|
|
15
15
|
|
|
16
16
|
- uses: actions/checkout@v4
|
|
17
17
|
|
|
@@ -30,7 +30,7 @@ jobs:
|
|
|
30
30
|
container: archlinux:latest
|
|
31
31
|
steps:
|
|
32
32
|
- name: Install system dependencies
|
|
33
|
-
run: pacman -Syu --noconfirm git python python-build python-installer python-wheel python-hatchling
|
|
33
|
+
run: pacman -Syu --noconfirm git python python-build python-installer python-wheel python-hatchling man-db
|
|
34
34
|
|
|
35
35
|
- uses: actions/checkout@v4
|
|
36
36
|
|
|
@@ -45,3 +45,7 @@ jobs:
|
|
|
45
45
|
/tmp/venv/bin/wf --version
|
|
46
46
|
/tmp/venv/bin/workforest shell-init bash | bash -n
|
|
47
47
|
/tmp/venv/bin/workforest shell-init zsh | head -1 | grep -q Workforest
|
|
48
|
+
# man pages ride along in the wheel's share/man data, and shell-init
|
|
49
|
+
# puts that directory on $MANPATH for venv installs
|
|
50
|
+
MANPATH=/tmp/venv/share/man man -w workforest wf 5 workforest wf
|
|
51
|
+
/tmp/venv/bin/workforest shell-init bash | grep -q 'MANPATH=/tmp/venv/share/man'
|
|
@@ -16,7 +16,7 @@ jobs:
|
|
|
16
16
|
steps:
|
|
17
17
|
- name: Install system dependencies
|
|
18
18
|
run: >
|
|
19
|
-
pacman -Syu --noconfirm base-devel git openssh pacman-contrib namcap
|
|
19
|
+
pacman -Syu --noconfirm base-devel git openssh pacman-contrib namcap man-db
|
|
20
20
|
python python-yaml python-build python-installer python-wheel python-hatchling
|
|
21
21
|
|
|
22
22
|
- uses: actions/checkout@v4
|
|
@@ -41,6 +41,7 @@ jobs:
|
|
|
41
41
|
pacman -U --noconfirm workforest-*.pkg.tar.zst
|
|
42
42
|
workforest --version
|
|
43
43
|
wf --version
|
|
44
|
+
man -w workforest wf 5 workforest wf
|
|
44
45
|
|
|
45
46
|
- name: Push to AUR
|
|
46
47
|
env:
|
|
@@ -44,6 +44,12 @@ jobs:
|
|
|
44
44
|
wf --version
|
|
45
45
|
workforest shell-init bash | bash -n
|
|
46
46
|
workforest shell-init zsh | head -1 | grep -q Workforest
|
|
47
|
+
test -f "$(brew --prefix)/share/man/man1/workforest.1"
|
|
48
|
+
test -f "$(brew --prefix)/share/man/man1/wf.1"
|
|
49
|
+
test -f "$(brew --prefix)/share/man/man5/workforest.5"
|
|
50
|
+
test -f "$(brew --prefix)/share/man/man5/wf.5"
|
|
51
|
+
# the formula relocates them, so shell-init must not touch MANPATH
|
|
52
|
+
! workforest shell-init bash | grep -q MANPATH
|
|
47
53
|
|
|
48
54
|
- name: Push to tap
|
|
49
55
|
env:
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: workforest
|
|
3
|
+
Version: 0.5.0
|
|
4
|
+
Summary: Git worktree forest management: create, open, and clean up per-branch worktrees with project-defined setup hooks
|
|
5
|
+
Project-URL: Homepage, https://github.com/ArkadyBuryakov/workforest
|
|
6
|
+
Author-email: Arkady Buryakov <arkady@buryakov.pro>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: cli,developer-tools,git,worktree
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Requires-Dist: pyyaml>=6.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Workforest
|
|
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
|
+
|
|
31
|
+
Git worktree forest management: one main checkout plus any number of
|
|
32
|
+
disposable, per-branch worktrees in a predictable location — created, set up,
|
|
33
|
+
opened, and cleaned up with one command.
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
~/dev/
|
|
37
|
+
├── api/ # main checkout
|
|
38
|
+
└── worktrees/
|
|
39
|
+
└── api/
|
|
40
|
+
├── feature-x/ # wf create feature-x
|
|
41
|
+
└── fix-y/
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- **Create** a worktree for any branch (local, remote, or brand new) and have
|
|
45
|
+
it set up automatically: symlinks for untracked assets (`node_modules`,
|
|
46
|
+
`.env`, …) and project-defined setup scripts.
|
|
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.
|
|
49
|
+
- **Run** named project scripts with well-known `WF_*` environment variables.
|
|
50
|
+
- **Delete** worktrees safely, or **checkout**: collapse one back into the
|
|
51
|
+
main checkout.
|
|
52
|
+
- Drive everything from an interactive fzf **TUI** (`wf` with no arguments).
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```sh
|
|
57
|
+
# Arch Linux
|
|
58
|
+
yay -S workforest # AUR
|
|
59
|
+
|
|
60
|
+
# macOS (or Linux with Homebrew)
|
|
61
|
+
brew install arkadyburyakov/tap/workforest
|
|
62
|
+
|
|
63
|
+
# anywhere else
|
|
64
|
+
uv tool install workforest # or: pipx install workforest
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
This installs two commands: `workforest` and its alias `wf`. Then add one
|
|
68
|
+
line to your `~/.bashrc` / `~/.zshrc`:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
eval "$(workforest shell-init)"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This upgrades `wf` to a shell function (needed so `wf open` can change your
|
|
75
|
+
shell's directory — a plain binary cannot), registers completions, and — for
|
|
76
|
+
`uv tool`/`pipx` installs, whose files live in a venv — puts the man pages on
|
|
77
|
+
`$MANPATH`. Without it everything still works, but "open in current shell"
|
|
78
|
+
prints the `cd` command instead of performing it.
|
|
79
|
+
|
|
80
|
+
Reference: `man workforest` (commands) and `man 5 workforest` (the
|
|
81
|
+
configuration files); `wf` works in place of `workforest` for both.
|
|
82
|
+
|
|
83
|
+
Requirements: Linux or macOS, git ≥ 2.36, Python ≥ 3.14 (the AUR and
|
|
84
|
+
Homebrew packages bring their own). Optional: `fzf` for the TUI.
|
|
85
|
+
|
|
86
|
+
## Quick start
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
wf create feature/login # create worktree + run hooks + open in $EDITOR
|
|
90
|
+
wf list # what's in the forest
|
|
91
|
+
wf open login -o 'lazygit' # open with any command instead
|
|
92
|
+
wf run test # run a named script from config
|
|
93
|
+
wf run -b backend # detached; `wf stop backend` ends it
|
|
94
|
+
wf run make check -j2 # extra args are appended to the script command
|
|
95
|
+
wf checkout login # fold the branch back into the main checkout
|
|
96
|
+
wf delete fix-y # remove a worktree (asks about dirty changes)
|
|
97
|
+
wf # interactive TUI (fzf)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Configuration
|
|
101
|
+
|
|
102
|
+
Layered, YAML or JSON; later layers override earlier ones:
|
|
103
|
+
|
|
104
|
+
| Layer | Location | Typical content |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| system | `/etc/workforest/config.yaml` | org-wide defaults |
|
|
107
|
+
| user | `~/.config/workforest/config.yaml` | your terminal/editor setup |
|
|
108
|
+
| project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
|
|
109
|
+
| project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |
|
|
110
|
+
|
|
111
|
+
Scalars and lists replace; the `openers`/`wrappers`/`scripts` mappings
|
|
112
|
+
merge per key (`null` removes an entry). `workforest config` shows the
|
|
113
|
+
merged result and where each layer came from; `workforest init` scaffolds a
|
|
114
|
+
project file (`--local` for a personal one). Nothing in the environment
|
|
115
|
+
changes the result — files and flags only.
|
|
116
|
+
|
|
117
|
+
All keys, with defaults:
|
|
118
|
+
|
|
119
|
+
```yaml
|
|
120
|
+
worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
|
|
121
|
+
opener: "" # default opener: an `openers` name or a shell command;
|
|
122
|
+
# "" → $VISUAL → $EDITOR
|
|
123
|
+
openers: {} # name -> what `-o NAME` runs, and where
|
|
124
|
+
wrappers: {} # name -> command that runs $WF_COMMAND elsewhere (window, pane, direnv)
|
|
125
|
+
symlinks: [] # untracked assets linked from main into new worktrees
|
|
126
|
+
setup_scripts: [] # shell snippets run in a fresh worktree
|
|
127
|
+
scripts: {} # name -> command for `wf run NAME` (or a mapping, see Scripts)
|
|
128
|
+
stop_timeout: 30 # seconds a stopped script gets after SIGTERM before SIGKILL
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Openers
|
|
132
|
+
|
|
133
|
+
An opener is a shell command that runs with the worktree root as working
|
|
134
|
+
directory, either **in your terminal** (the default: the `wf` wrapper does
|
|
135
|
+
`cd` there and runs it) or **in the background** (`background: true`:
|
|
136
|
+
spawned detached, `wf` returns immediately — for GUI apps and commands that
|
|
137
|
+
hand off to a daemon or multiplexer). Optionally it runs **through a
|
|
138
|
+
wrapper**, a command that receives it as `$WF_COMMAND` and takes it
|
|
139
|
+
somewhere else: a new terminal window, a tmux window, a `direnv exec`. With
|
|
140
|
+
no config at all, `wf` runs `$VISUAL`/`$EDITOR` in your terminal.
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
opener: win # what `wf create` / `wf open` run by default
|
|
144
|
+
|
|
145
|
+
wrappers: # get the opener command as $WF_COMMAND
|
|
146
|
+
kitty:
|
|
147
|
+
command: kitty --title "$WF_TITLE" --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
|
|
148
|
+
background: true
|
|
149
|
+
tmux: # the tmux server has its own environment: WF_ENV re-creates ours
|
|
150
|
+
command: tmux new-window -n "$WF_TITLE" -c "$WF_WORKTREE" "export $WF_ENV; $WF_COMMAND"
|
|
151
|
+
background: true
|
|
152
|
+
|
|
153
|
+
openers:
|
|
154
|
+
edit: $EDITOR "$WF_TARGET" # in your terminal
|
|
155
|
+
code: # GUI app: detached
|
|
156
|
+
command: code "$WF_WORKTREE"
|
|
157
|
+
background: true
|
|
158
|
+
kitty: # edit's command, in a new kitty window
|
|
159
|
+
from: edit
|
|
160
|
+
wrap: kitty
|
|
161
|
+
tmux: # edit's command, in a new tmux window
|
|
162
|
+
from: edit
|
|
163
|
+
wrap: tmux
|
|
164
|
+
git: lazygit
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
An entry is a shell command string, or a mapping with either `command` (a
|
|
168
|
+
shell command, never a name) or `from` (another opener's command — one
|
|
169
|
+
level: the target has a `command` of its own — inheriting its `background`
|
|
170
|
+
unless the entry sets one), plus optionally `background: true` or
|
|
171
|
+
`wrap: NAME`. `wrap` and `background` never sit on the same entry: the
|
|
172
|
+
wrapper decides where the whole thing runs, via its own `background` flag.
|
|
173
|
+
`-o VALUE` (and `opener:`) is an `openers` name, else a shell command;
|
|
174
|
+
`-w NAME` on `create`/`open` overrides the opener's wrapper (`-w ''` for
|
|
175
|
+
none). Wrappers are environment-specific, so they belong in the per-machine
|
|
176
|
+
user config — a host without your terminal emulator simply has none.
|
|
177
|
+
`from` and `wrap` names are checked when the config loads, so `wf config`
|
|
178
|
+
reports a misspelled one.
|
|
179
|
+
|
|
180
|
+
All of them are plain shell commands, run via `$SHELL -c` with one variable
|
|
181
|
+
family in the environment — the same family the launched process and every
|
|
182
|
+
script receive:
|
|
183
|
+
|
|
184
|
+
| Variable | Value |
|
|
185
|
+
|---|---|
|
|
186
|
+
| `WF_MAIN` | main worktree path, `/home/user/Projects/project_name` |
|
|
187
|
+
| `WF_NAME` | repo name, `project_name` |
|
|
188
|
+
| `WF_WORKTREES_DIR` | resolved worktrees directory |
|
|
189
|
+
| `WF_WORKTREE` | this worktree's path |
|
|
190
|
+
| `WF_BRANCH` | its branch (empty if detached) |
|
|
191
|
+
| `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
|
|
192
|
+
| `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
|
|
193
|
+
| `WF_ENV` | all of the above as shell-quoted `NAME=value` assignments (launch-only) |
|
|
194
|
+
| `WF_COMMAND` | in a wrapper: the opener command, unexpanded |
|
|
195
|
+
|
|
196
|
+
Standard shell rules apply — there is no workforest template syntax:
|
|
197
|
+
`"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
|
|
198
|
+
braces, pipes, and `&&` mean whatever your shell says they mean (a
|
|
199
|
+
misspelled `$WF_VAR` expands to empty, as in any shell). `$WF_COMMAND`
|
|
200
|
+
reaches the wrapper unexpanded, so run it through a shell of its own for its
|
|
201
|
+
`$WF_*` references to resolve: `$SHELL -c "$WF_COMMAND"`. When that shell
|
|
202
|
+
runs somewhere this environment is not inherited — a tmux server, an ssh
|
|
203
|
+
host — hand the family over as text: `"export $WF_ENV; $WF_COMMAND"` is
|
|
204
|
+
re-parsed on the far side, quoting intact. Background
|
|
205
|
+
processes shed activation state inherited from the invoking shell (Python
|
|
206
|
+
venv, conda, nvm, rvm) so a new window starts clean instead of carrying an
|
|
207
|
+
environment it cannot deactivate.
|
|
208
|
+
|
|
209
|
+
Fully commented reference configs:
|
|
210
|
+
[`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
|
|
211
|
+
[`.workforest.yaml`](src/workforest/examples/.workforest.yaml) (project) —
|
|
212
|
+
installed to `/usr/share/doc/workforest/examples/` by the Arch package.
|
|
213
|
+
|
|
214
|
+
### Scripts
|
|
215
|
+
|
|
216
|
+
`wf run NAME [ARGS...]` runs a `scripts` entry from the root of the current
|
|
217
|
+
worktree, with any extra arguments shell-quoted and appended. An entry is a
|
|
218
|
+
shell command string, or a mapping:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
scripts:
|
|
222
|
+
test: npm test # shorthand for {command: npm test}
|
|
223
|
+
frontend:
|
|
224
|
+
command: cd frontend && npm run dev
|
|
225
|
+
exclusive: true # at most one instance per project: it owns the port
|
|
226
|
+
backend:
|
|
227
|
+
command: docker compose up
|
|
228
|
+
exclusive: true
|
|
229
|
+
background: true # detached, output in a log file
|
|
230
|
+
cleanup: docker compose down # runs after the command ends, however it ended
|
|
231
|
+
stop_timeout: 60 # give it a minute to come down before SIGKILL
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The command runs in a process group of its own, in the foreground of your
|
|
235
|
+
terminal, so Ctrl-C and Ctrl-Z reach it directly; a signal sent to `wf`
|
|
236
|
+
itself (SIGINT, SIGTERM, SIGHUP) is forwarded to the whole group. `cleanup`
|
|
237
|
+
then runs — after a normal exit, a failure, or a kill — in the same
|
|
238
|
+
worktree with the same `WF_*` variables. `wf run` fails with the command's
|
|
239
|
+
status, or `128+N` when the command was killed by signal N; a command
|
|
240
|
+
killed by SIGINT ends `wf` the way Ctrl-C would, so a shell loop around it
|
|
241
|
+
aborts.
|
|
242
|
+
|
|
243
|
+
A `background` script (or `wf run -b NAME`) is detached instead: it runs
|
|
244
|
+
under a supervisor of its own — the same process-group and cleanup
|
|
245
|
+
handling, minus the terminal — with its output in
|
|
246
|
+
`.git/workforest/logs/NAME/WORKTREE.log` of the main checkout, and `wf`
|
|
247
|
+
returns as soon as it is clearly running (a command that dies right away
|
|
248
|
+
is reported with the tail of its log). That way several long-running
|
|
249
|
+
scripts can be started from one terminal. `wf stop NAME` stops this
|
|
250
|
+
worktree's instance, foreground or background, from any terminal;
|
|
251
|
+
`--all` stops every worktree's. A script runs once per worktree: starting
|
|
252
|
+
one that is already running there fails (`wf stop` it first).
|
|
253
|
+
|
|
254
|
+
An `exclusive` script runs at most once per project: starting it stops the
|
|
255
|
+
running instance in any worktree (this one included), waits for that
|
|
256
|
+
instance's cleanup to finish, and only then starts. Stopping — by `wf
|
|
257
|
+
stop` or by preemption — is SIGTERM, then SIGKILL after `stop_timeout`
|
|
258
|
+
seconds (30 by default; the top-level key changes it for every script, an
|
|
259
|
+
entry's own `stop_timeout` for that one). The stopped instance's `wf run`
|
|
260
|
+
reports ``killed by SIGTERM (stopped by `wf run backend` in 'feat-x')``
|
|
261
|
+
and exits 143. Running instances are recorded under
|
|
262
|
+
`.git/workforest/running/` in the main checkout. A record is a hint, not
|
|
263
|
+
the truth: pid, process group, and boot are verified before anything is
|
|
264
|
+
signalled, so a crash, a reboot, or a `kill -9`'d `wf` leaves nothing to
|
|
265
|
+
tidy by hand. A command still running after its `wf run` is gone (an
|
|
266
|
+
orphan) is stopped and cleaned up by whoever stops it when `/proc`
|
|
267
|
+
confirms the pid was not recycled; elsewhere it is reported and left
|
|
268
|
+
alone.
|
|
269
|
+
|
|
270
|
+
### Script environment
|
|
271
|
+
|
|
272
|
+
`setup_scripts`, `scripts`, and hooks run via `$SHELL -c` with:
|
|
273
|
+
|
|
274
|
+
| Variable | Value |
|
|
275
|
+
|---|---|
|
|
276
|
+
| `WF_MAIN` | main worktree path |
|
|
277
|
+
| `WF_NAME` | repo name (main checkout directory name) |
|
|
278
|
+
| `WF_WORKTREE` | current/new worktree path |
|
|
279
|
+
| `WF_WORKTREES_DIR` | resolved worktrees directory |
|
|
280
|
+
| `WF_BRANCH` | branch of the current/new worktree |
|
|
281
|
+
|
|
282
|
+
`worktrees_dir` is a template using the same naming pattern: `$WF_MAIN` and
|
|
283
|
+
`$WF_NAME` (plus regular environment variables like `$HOME`) expand there —
|
|
284
|
+
the per-worktree variables don't, since no worktree exists yet when the base
|
|
285
|
+
directory is resolved.
|
|
286
|
+
|
|
287
|
+
### Example project config
|
|
288
|
+
|
|
289
|
+
```yaml
|
|
290
|
+
# .workforest.yaml — committed to the repo
|
|
291
|
+
symlinks: [node_modules, .env]
|
|
292
|
+
setup_scripts:
|
|
293
|
+
- npm install --prefer-offline
|
|
294
|
+
scripts:
|
|
295
|
+
test: npm test
|
|
296
|
+
migrate: npm run db:migrate
|
|
297
|
+
frontend:
|
|
298
|
+
command: cd frontend && npm run dev
|
|
299
|
+
exclusive: true
|
|
300
|
+
backend:
|
|
301
|
+
command: docker compose up
|
|
302
|
+
exclusive: true
|
|
303
|
+
background: true
|
|
304
|
+
cleanup: docker compose down
|
|
305
|
+
stop_timeout: 60
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Commands
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
workforest create [BRANCH] [-o OPENER] [-w WRAPPER] [-p PATH] [--no-hooks] [--no-open]
|
|
312
|
+
workforest open [NAME] [-o OPENER] [-w WRAPPER] [-p PATH]
|
|
313
|
+
workforest list [--porcelain]
|
|
314
|
+
workforest delete NAME... [--force] [--delete-branch | --keep-branch]
|
|
315
|
+
workforest checkout NAME [--force]
|
|
316
|
+
workforest run [-b] SCRIPT [ARGS...]
|
|
317
|
+
workforest stop SCRIPT [--all]
|
|
318
|
+
workforest tui [MODE]
|
|
319
|
+
workforest init [--local]
|
|
320
|
+
workforest config [--json]
|
|
321
|
+
workforest shell-init [bash|zsh]
|
|
322
|
+
workforest claude copy-session SESSION_ID # experimental
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
`open` without NAME opens the worktree you are standing in.
|
|
326
|
+
|
|
327
|
+
`create` resolves BRANCH in order: existing local branch, then a branch on
|
|
328
|
+
exactly one remote (checked out tracking it), then a brand-new branch.
|
|
329
|
+
`REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
|
|
330
|
+
carry the same branch name; if that local name is already taken, `create`
|
|
331
|
+
prompts for a different one.
|
|
332
|
+
|
|
333
|
+
`workforest claude` (shown only when `~/.claude` exists) copies a Claude
|
|
334
|
+
Code session from the main worktree into the current one. It is
|
|
335
|
+
**experimental**: it manipulates Claude Code's private on-disk state,
|
|
336
|
+
which is not a stable interface, so any Claude Code update may break it.
|
|
337
|
+
|
|
338
|
+
Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error
|
|
339
|
+
· `128+N` the `run` command was killed by signal N.
|
|
340
|
+
Human messages go to stderr; stdout carries only machine output (`cd`
|
|
341
|
+
directives for the `wf` wrapper, `--porcelain` listings, dumps).
|
|
342
|
+
|
|
343
|
+
## Development
|
|
344
|
+
|
|
345
|
+
```sh
|
|
346
|
+
uv sync # venv + dev dependencies (uv.lock)
|
|
347
|
+
make check # ruff + mypy --strict + pytest (coverage gate ≥ 90%)
|
|
348
|
+
make install # install this checkout as a uv tool (~/.local/bin/workforest)
|
|
349
|
+
make uninstall # remove it again
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Man pages are hand-written roff under `man/` (`workforest.1` and
|
|
353
|
+
`workforest.5`, plus `wf.1`/`wf.5` links to them); `tests/test_man.py` fails when they drift from
|
|
354
|
+
`cli.py`. They ship as `share/man` data in the wheel, which is how every
|
|
355
|
+
package gets them: the Arch package installs the wheel to `/usr`, the
|
|
356
|
+
Homebrew formula relocates them out of its venv, and `uv tool`/`pipx` keep
|
|
357
|
+
them in the venv where `workforest shell-init` points `$MANPATH`.
|
|
358
|
+
|
|
359
|
+
Packaging templates live under `packaging/` (one directory per package
|
|
360
|
+
manager: `packaging/AUR/`, `packaging/homebrew/`); the `@VERSION@` and
|
|
361
|
+
`@SHA256@` placeholders are filled in at release time.
|
|
362
|
+
Release: bump `__version__` and push to main — CI tags the release,
|
|
363
|
+
renders the templates, and publishes to PyPI, the AUR, and the
|
|
364
|
+
[Homebrew tap](https://github.com/ArkadyBuryakov/homebrew-tap). The
|
|
365
|
+
published AUR package and tap are the only places rendered recipes exist.
|
|
366
|
+
|
|
367
|
+
## License
|
|
368
|
+
|
|
369
|
+
[MIT](./LICENSE)
|