workforest 0.2.2__tar.gz → 0.3.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/.claude/rules/architecture.md +16 -0
- workforest-0.3.0/.claude/rules/conventions.md +12 -0
- workforest-0.3.0/.claude/rules/tests.md +17 -0
- {workforest-0.2.2 → workforest-0.3.0}/.github/workflows/publish_aur.yml +4 -25
- {workforest-0.2.2 → workforest-0.3.0}/.github/workflows/publish_homebrew.yml +19 -34
- {workforest-0.2.2 → workforest-0.3.0}/.gitignore +4 -1
- {workforest-0.2.2 → workforest-0.3.0}/PKG-INFO +48 -20
- {workforest-0.2.2 → workforest-0.3.0}/README.md +47 -19
- workforest-0.3.0/completions/_workforest +44 -0
- workforest-0.2.2/packaging/AUR/PKGBUILD → workforest-0.3.0/packaging/AUR/PKGBUILD.template +6 -2
- workforest-0.2.2/packaging/homebrew/workforest.rb → workforest-0.3.0/packaging/homebrew/workforest.rb.template +7 -5
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/__init__.py +1 -1
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/cli.py +51 -34
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/commands.py +76 -28
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/completions.py +30 -10
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/config.py +41 -29
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/errors.py +1 -1
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/examples/config.yaml +34 -27
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/gitutil.py +26 -23
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/hooks.py +5 -2
- workforest-0.3.0/src/workforest/integrations/__init__.py +2 -0
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/integrations/claude.py +19 -8
- workforest-0.3.0/src/workforest/launch.py +235 -0
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/output.py +22 -7
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/shell/completion.bash +3 -1
- workforest-0.3.0/src/workforest/shell/completion.zsh +67 -0
- workforest-0.3.0/src/workforest/shell/workforest.sh +18 -0
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/tui.py +26 -13
- {workforest-0.2.2 → workforest-0.3.0}/tests/conftest.py +13 -7
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_claude.py +2 -2
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_cli.py +6 -5
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_commands.py +77 -4
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_config.py +2 -2
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_gitutil.py +20 -7
- workforest-0.3.0/tests/test_launch.py +302 -0
- workforest-0.3.0/tests/test_output.py +56 -0
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_shell.py +58 -6
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_tui.py +10 -4
- workforest-0.2.2/completions/_workforest +0 -23
- workforest-0.2.2/packaging/AUR/.SRCINFO +0 -19
- workforest-0.2.2/src/workforest/integrations/__init__.py +0 -2
- workforest-0.2.2/src/workforest/launch.py +0 -162
- workforest-0.2.2/src/workforest/shell/completion.zsh +0 -38
- workforest-0.2.2/src/workforest/shell/workforest.sh +0 -16
- workforest-0.2.2/tests/test_launch.py +0 -204
- {workforest-0.2.2 → workforest-0.3.0}/.github/workflows/ci.yml +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/.github/workflows/publish_pypi.yml +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/.github/workflows/release.yml +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/LICENSE +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/Makefile +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/pyproject.toml +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/__main__.py +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/src/workforest/shellinit.py +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/tests/__init__.py +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_harness.py +0 -0
- {workforest-0.2.2 → workforest-0.3.0}/tests/test_hooks.py +0 -0
- {workforest-0.2.2 → workforest-0.3.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`.
|
|
@@ -12,7 +12,7 @@ jobs:
|
|
|
12
12
|
name: aur
|
|
13
13
|
url: https://aur.archlinux.org/packages/workforest
|
|
14
14
|
permissions:
|
|
15
|
-
contents:
|
|
15
|
+
contents: read
|
|
16
16
|
steps:
|
|
17
17
|
- name: Install system dependencies
|
|
18
18
|
run: >
|
|
@@ -21,15 +21,15 @@ jobs:
|
|
|
21
21
|
|
|
22
22
|
- uses: actions/checkout@v4
|
|
23
23
|
|
|
24
|
-
- name:
|
|
24
|
+
- name: Render PKGBUILD from template
|
|
25
25
|
run: |
|
|
26
26
|
version="${GITHUB_REF_NAME#v}"
|
|
27
27
|
cd packaging/AUR
|
|
28
|
-
sed
|
|
29
|
-
sed -i "s/^pkgrel=.*/pkgrel=1/" PKGBUILD
|
|
28
|
+
sed "s/@VERSION@/$version/g" PKGBUILD.template > PKGBUILD
|
|
30
29
|
# makepkg refuses to run as root
|
|
31
30
|
useradd -m builder
|
|
32
31
|
chown -R builder .
|
|
32
|
+
# updpkgsums replaces the @SHA256@ placeholder with the real checksum
|
|
33
33
|
runuser -u builder -- updpkgsums
|
|
34
34
|
runuser -u builder -- sh -c 'makepkg --printsrcinfo > .SRCINFO'
|
|
35
35
|
|
|
@@ -70,24 +70,3 @@ jobs:
|
|
|
70
70
|
git commit -m "Update to $version"
|
|
71
71
|
git push origin HEAD:master
|
|
72
72
|
fi
|
|
73
|
-
|
|
74
|
-
- name: Commit generated PKGBUILD back to main
|
|
75
|
-
run: |
|
|
76
|
-
version="${GITHUB_REF_NAME#v}"
|
|
77
|
-
cp packaging/AUR/PKGBUILD packaging/AUR/.SRCINFO /tmp/
|
|
78
|
-
# The workspace is created by the host runner under a different UID
|
|
79
|
-
# than the container's root, so git rejects it without this.
|
|
80
|
-
git config --global --add safe.directory "$GITHUB_WORKSPACE"
|
|
81
|
-
# The workflow runs detached at the release tag; move to main to commit.
|
|
82
|
-
# -f discards the in-place edits we just copied to /tmp; build
|
|
83
|
-
# artifacts in packaging/AUR are untracked and never git-added.
|
|
84
|
-
git fetch origin main
|
|
85
|
-
git checkout -f -B main origin/main
|
|
86
|
-
cp /tmp/PKGBUILD /tmp/.SRCINFO packaging/AUR/
|
|
87
|
-
git add packaging/AUR/PKGBUILD packaging/AUR/.SRCINFO
|
|
88
|
-
if git diff --cached --quiet; then
|
|
89
|
-
echo "Repo copy already up to date."
|
|
90
|
-
else
|
|
91
|
-
git commit -m "Update PKGBUILD to $version"
|
|
92
|
-
git push origin main
|
|
93
|
-
fi
|
|
@@ -3,32 +3,43 @@ name: Publish to Homebrew tap
|
|
|
3
3
|
on:
|
|
4
4
|
release:
|
|
5
5
|
types: [published]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
inputs:
|
|
8
|
+
version:
|
|
9
|
+
description: "Release version to publish (e.g. 0.2.1)"
|
|
10
|
+
required: true
|
|
6
11
|
|
|
7
12
|
jobs:
|
|
8
13
|
publish:
|
|
9
14
|
runs-on: macos-latest
|
|
15
|
+
env:
|
|
16
|
+
RELEASE_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.version || github.ref_name }}
|
|
10
17
|
environment:
|
|
11
18
|
name: homebrew-tap
|
|
12
19
|
url: https://github.com/ArkadyBuryakov/homebrew-tap
|
|
13
20
|
permissions:
|
|
14
|
-
contents:
|
|
21
|
+
contents: read
|
|
15
22
|
steps:
|
|
16
23
|
- uses: actions/checkout@v4
|
|
17
24
|
|
|
18
|
-
- name:
|
|
25
|
+
- name: Render formula from template
|
|
19
26
|
run: |
|
|
20
|
-
version="${
|
|
27
|
+
version="${RELEASE_VERSION#v}"
|
|
21
28
|
url="https://github.com/ArkadyBuryakov/workforest/archive/v$version.tar.gz"
|
|
22
29
|
curl -fsSL "$url" -o /tmp/workforest.tar.gz
|
|
23
30
|
sha256=$(shasum -a 256 /tmp/workforest.tar.gz | cut -d' ' -f1)
|
|
24
31
|
cd packaging/homebrew
|
|
25
|
-
sed -
|
|
26
|
-
|
|
32
|
+
sed -e "s/@VERSION@/$version/g" -e "s/@SHA256@/$sha256/g" \
|
|
33
|
+
workforest.rb.template > workforest.rb
|
|
27
34
|
|
|
28
35
|
- name: Build, test, and smoke test
|
|
29
36
|
run: |
|
|
30
|
-
|
|
31
|
-
|
|
37
|
+
# Homebrew >= 4.6.4 refuses to install from a bare file path —
|
|
38
|
+
# formulas must live in a tap, so stage ours in a throwaway one.
|
|
39
|
+
brew tap-new --no-git arkadyburyakov/test
|
|
40
|
+
cp packaging/homebrew/workforest.rb "$(brew --repository arkadyburyakov/test)/Formula/"
|
|
41
|
+
brew install --build-from-source arkadyburyakov/test/workforest
|
|
42
|
+
brew test arkadyburyakov/test/workforest
|
|
32
43
|
workforest --version
|
|
33
44
|
wf --version
|
|
34
45
|
workforest shell-init bash | bash -n
|
|
@@ -38,7 +49,7 @@ jobs:
|
|
|
38
49
|
env:
|
|
39
50
|
TAP_SSH_PRIVATE_KEY: ${{ secrets.TAP_SSH_PRIVATE_KEY }}
|
|
40
51
|
run: |
|
|
41
|
-
version="${
|
|
52
|
+
version="${RELEASE_VERSION#v}"
|
|
42
53
|
# Host keys are pinned rather than keyscanned (same policy as the
|
|
43
54
|
# AUR workflow); these are GitHub's published SSH fingerprints.
|
|
44
55
|
mkdir -p /tmp/ssh
|
|
@@ -63,29 +74,3 @@ jobs:
|
|
|
63
74
|
git commit -m "workforest $version"
|
|
64
75
|
git push origin main
|
|
65
76
|
fi
|
|
66
|
-
|
|
67
|
-
- name: Commit generated formula back to main
|
|
68
|
-
run: |
|
|
69
|
-
version="${GITHUB_REF_NAME#v}"
|
|
70
|
-
cp packaging/homebrew/workforest.rb /tmp/workforest.rb
|
|
71
|
-
git config --global user.name "Arkady Buryakov"
|
|
72
|
-
git config --global user.email "arkady@buryakov.pro"
|
|
73
|
-
# The workflow runs detached at the release tag; move to main to
|
|
74
|
-
# commit. Retry because the AUR publisher pushes to main from the
|
|
75
|
-
# same release event and the two can race.
|
|
76
|
-
for attempt in 1 2 3; do
|
|
77
|
-
git fetch origin main
|
|
78
|
-
git checkout -f -B main origin/main
|
|
79
|
-
cp /tmp/workforest.rb packaging/homebrew/workforest.rb
|
|
80
|
-
git add packaging/homebrew/workforest.rb
|
|
81
|
-
if git diff --cached --quiet; then
|
|
82
|
-
echo "Repo copy already up to date."
|
|
83
|
-
break
|
|
84
|
-
fi
|
|
85
|
-
git commit -m "Update Homebrew formula to $version"
|
|
86
|
-
if git push origin main; then
|
|
87
|
-
break
|
|
88
|
-
fi
|
|
89
|
-
echo "Push raced with another workflow, retrying ($attempt)..."
|
|
90
|
-
sleep 10
|
|
91
|
-
done
|
|
@@ -13,9 +13,12 @@ build/
|
|
|
13
13
|
.coverage
|
|
14
14
|
htmlcov/
|
|
15
15
|
|
|
16
|
-
# Packaging artifacts
|
|
16
|
+
# Packaging artifacts (rendered from *.template at release time)
|
|
17
17
|
*.pkg.tar.zst
|
|
18
18
|
pkg/
|
|
19
|
+
packaging/AUR/PKGBUILD
|
|
20
|
+
packaging/AUR/.SRCINFO
|
|
21
|
+
packaging/homebrew/workforest.rb
|
|
19
22
|
|
|
20
23
|
# Tooling config
|
|
21
24
|
.workforest.yaml
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: workforest
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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.
|
|
@@ -108,17 +116,17 @@ All keys, with defaults:
|
|
|
108
116
|
```yaml
|
|
109
117
|
worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
|
|
110
118
|
opener: "" # default opener; "" → $VISUAL → $EDITOR
|
|
111
|
-
openers: {} # name -> command
|
|
112
|
-
window_command: "" # "" → current shell; or e.g.
|
|
113
|
-
# "
|
|
119
|
+
openers: {} # name -> shell command, e.g. edit: '$EDITOR "$WF_TARGET"'
|
|
120
|
+
window_command: "" # "" → current shell; or e.g. kitty --title "$WF_TITLE"
|
|
121
|
+
# --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
|
|
114
122
|
symlinks: [] # untracked assets linked from main into new worktrees
|
|
115
123
|
setup_scripts: [] # shell snippets run in a fresh worktree
|
|
116
124
|
scripts: {} # name -> snippet for `wf run NAME`
|
|
117
125
|
```
|
|
118
126
|
|
|
119
|
-
Openers and `window_command` are
|
|
120
|
-
family
|
|
121
|
-
|
|
127
|
+
Openers and `window_command` are plain shell commands, run via `$SHELL -c`
|
|
128
|
+
with one variable family in the environment — the same family the launched
|
|
129
|
+
process and every script receive:
|
|
122
130
|
|
|
123
131
|
| Variable | Value |
|
|
124
132
|
|---|---|
|
|
@@ -130,13 +138,17 @@ environment variables:
|
|
|
130
138
|
| `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
|
|
131
139
|
| `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
|
|
132
140
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
`$
|
|
141
|
+
Standard shell rules apply — there is no workforest template syntax:
|
|
142
|
+
`"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
|
|
143
|
+
braces, pipes, and `&&` mean whatever your shell says they mean (a
|
|
144
|
+
misspelled `$WF_VAR` expands to empty, as in any shell). Openers run with
|
|
145
|
+
the worktree root as working directory. In `window_command` the resolved
|
|
146
|
+
opener command is additionally available as `$WF_COMMAND` — still
|
|
147
|
+
unexpanded, so run it through a shell of its own for its `$WF_*` references
|
|
148
|
+
to resolve: `$SHELL -c "$WF_COMMAND"`. Spawned windows shed activation
|
|
149
|
+
state inherited from the invoking shell (Python venv, conda, nvm, rvm) so
|
|
150
|
+
the new session starts clean instead of carrying an environment it cannot
|
|
151
|
+
deactivate.
|
|
140
152
|
|
|
141
153
|
Fully commented reference configs:
|
|
142
154
|
[`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
|
|
@@ -185,8 +197,23 @@ workforest tui [MODE]
|
|
|
185
197
|
workforest init [--local]
|
|
186
198
|
workforest config [--json]
|
|
187
199
|
workforest shell-init [bash|zsh]
|
|
200
|
+
workforest claude copy-session SESSION_ID # experimental
|
|
188
201
|
```
|
|
189
202
|
|
|
203
|
+
`open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
|
|
204
|
+
worktree you are standing in.
|
|
205
|
+
|
|
206
|
+
`create` resolves BRANCH in order: existing local branch, then a branch on
|
|
207
|
+
exactly one remote (checked out tracking it), then a brand-new branch.
|
|
208
|
+
`REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
|
|
209
|
+
carry the same branch name; if that local name is already taken, `create`
|
|
210
|
+
prompts for a different one.
|
|
211
|
+
|
|
212
|
+
`workforest claude` (shown only when `~/.claude` exists) copies a Claude
|
|
213
|
+
Code session from the main worktree into the current one. It is
|
|
214
|
+
**experimental**: it manipulates Claude Code's private on-disk state,
|
|
215
|
+
which is not a stable interface, so any Claude Code update may break it.
|
|
216
|
+
|
|
190
217
|
Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
|
|
191
218
|
Human messages go to stderr; stdout carries only machine output (`cd`
|
|
192
219
|
directives for the `wf` wrapper, `--porcelain` listings, dumps).
|
|
@@ -200,12 +227,13 @@ make install # install this checkout as a uv tool (~/.local/bin/workforest)
|
|
|
200
227
|
make uninstall # remove it again
|
|
201
228
|
```
|
|
202
229
|
|
|
203
|
-
Packaging
|
|
204
|
-
manager: `packaging/AUR/`, `packaging/homebrew/`)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
Packaging templates live under `packaging/` (one directory per package
|
|
231
|
+
manager: `packaging/AUR/`, `packaging/homebrew/`); the `@VERSION@` and
|
|
232
|
+
`@SHA256@` placeholders are filled in at release time.
|
|
233
|
+
Release: bump `__version__` and push to main — CI tags the release,
|
|
234
|
+
renders the templates, and publishes to PyPI, the AUR, and the
|
|
235
|
+
[Homebrew tap](https://github.com/ArkadyBuryakov/homebrew-tap). The
|
|
236
|
+
published AUR package and tap are the only places rendered recipes exist.
|
|
209
237
|
|
|
210
238
|
## License
|
|
211
239
|
|
|
@@ -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.
|
|
@@ -88,17 +96,17 @@ All keys, with defaults:
|
|
|
88
96
|
```yaml
|
|
89
97
|
worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME" # where the forest lives
|
|
90
98
|
opener: "" # default opener; "" → $VISUAL → $EDITOR
|
|
91
|
-
openers: {} # name -> command
|
|
92
|
-
window_command: "" # "" → current shell; or e.g.
|
|
93
|
-
# "
|
|
99
|
+
openers: {} # name -> shell command, e.g. edit: '$EDITOR "$WF_TARGET"'
|
|
100
|
+
window_command: "" # "" → current shell; or e.g. kitty --title "$WF_TITLE"
|
|
101
|
+
# --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
|
|
94
102
|
symlinks: [] # untracked assets linked from main into new worktrees
|
|
95
103
|
setup_scripts: [] # shell snippets run in a fresh worktree
|
|
96
104
|
scripts: {} # name -> snippet for `wf run NAME`
|
|
97
105
|
```
|
|
98
106
|
|
|
99
|
-
Openers and `window_command` are
|
|
100
|
-
family
|
|
101
|
-
|
|
107
|
+
Openers and `window_command` are plain shell commands, run via `$SHELL -c`
|
|
108
|
+
with one variable family in the environment — the same family the launched
|
|
109
|
+
process and every script receive:
|
|
102
110
|
|
|
103
111
|
| Variable | Value |
|
|
104
112
|
|---|---|
|
|
@@ -110,13 +118,17 @@ environment variables:
|
|
|
110
118
|
| `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
|
|
111
119
|
| `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |
|
|
112
120
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
`$
|
|
121
|
+
Standard shell rules apply — there is no workforest template syntax:
|
|
122
|
+
`"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
|
|
123
|
+
braces, pipes, and `&&` mean whatever your shell says they mean (a
|
|
124
|
+
misspelled `$WF_VAR` expands to empty, as in any shell). Openers run with
|
|
125
|
+
the worktree root as working directory. In `window_command` the resolved
|
|
126
|
+
opener command is additionally available as `$WF_COMMAND` — still
|
|
127
|
+
unexpanded, so run it through a shell of its own for its `$WF_*` references
|
|
128
|
+
to resolve: `$SHELL -c "$WF_COMMAND"`. Spawned windows shed activation
|
|
129
|
+
state inherited from the invoking shell (Python venv, conda, nvm, rvm) so
|
|
130
|
+
the new session starts clean instead of carrying an environment it cannot
|
|
131
|
+
deactivate.
|
|
120
132
|
|
|
121
133
|
Fully commented reference configs:
|
|
122
134
|
[`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
|
|
@@ -165,8 +177,23 @@ workforest tui [MODE]
|
|
|
165
177
|
workforest init [--local]
|
|
166
178
|
workforest config [--json]
|
|
167
179
|
workforest shell-init [bash|zsh]
|
|
180
|
+
workforest claude copy-session SESSION_ID # experimental
|
|
168
181
|
```
|
|
169
182
|
|
|
183
|
+
`open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
|
|
184
|
+
worktree you are standing in.
|
|
185
|
+
|
|
186
|
+
`create` resolves BRANCH in order: existing local branch, then a branch on
|
|
187
|
+
exactly one remote (checked out tracking it), then a brand-new branch.
|
|
188
|
+
`REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
|
|
189
|
+
carry the same branch name; if that local name is already taken, `create`
|
|
190
|
+
prompts for a different one.
|
|
191
|
+
|
|
192
|
+
`workforest claude` (shown only when `~/.claude` exists) copies a Claude
|
|
193
|
+
Code session from the main worktree into the current one. It is
|
|
194
|
+
**experimental**: it manipulates Claude Code's private on-disk state,
|
|
195
|
+
which is not a stable interface, so any Claude Code update may break it.
|
|
196
|
+
|
|
170
197
|
Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
|
|
171
198
|
Human messages go to stderr; stdout carries only machine output (`cd`
|
|
172
199
|
directives for the `wf` wrapper, `--porcelain` listings, dumps).
|
|
@@ -180,12 +207,13 @@ make install # install this checkout as a uv tool (~/.local/bin/workforest)
|
|
|
180
207
|
make uninstall # remove it again
|
|
181
208
|
```
|
|
182
209
|
|
|
183
|
-
Packaging
|
|
184
|
-
manager: `packaging/AUR/`, `packaging/homebrew/`)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
210
|
+
Packaging templates live under `packaging/` (one directory per package
|
|
211
|
+
manager: `packaging/AUR/`, `packaging/homebrew/`); the `@VERSION@` and
|
|
212
|
+
`@SHA256@` placeholders are filled in at release time.
|
|
213
|
+
Release: bump `__version__` and push to main — CI tags the release,
|
|
214
|
+
renders the templates, and publishes to PyPI, the AUR, and the
|
|
215
|
+
[Homebrew tap](https://github.com/ArkadyBuryakov/homebrew-tap). The
|
|
216
|
+
published AUR package and tap are the only places rendered recipes exist.
|
|
189
217
|
|
|
190
218
|
## License
|
|
191
219
|
|
|
@@ -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
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Maintainer: Arkady Buryakov <arkady@buryakov.pro>
|
|
2
|
+
#
|
|
3
|
+
# Template — not a buildable PKGBUILD. On release the publish_aur workflow
|
|
4
|
+
# substitutes @VERSION@, fills sha256sums via updpkgsums, generates .SRCINFO,
|
|
5
|
+
# and pushes the rendered files to the AUR; nothing is committed back here.
|
|
2
6
|
pkgname=workforest
|
|
3
|
-
pkgver
|
|
7
|
+
pkgver=@VERSION@
|
|
4
8
|
pkgrel=1
|
|
5
9
|
pkgdesc="Git worktree forest management: per-branch worktrees with project-defined setup hooks"
|
|
6
10
|
arch=(any)
|
|
@@ -10,7 +14,7 @@ depends=(python python-yaml git)
|
|
|
10
14
|
makedepends=(python-build python-installer python-wheel python-hatchling)
|
|
11
15
|
optdepends=('fzf: interactive TUI (workforest tui)')
|
|
12
16
|
source=("$pkgname-$pkgver.tar.gz::$url/archive/v$pkgver.tar.gz")
|
|
13
|
-
sha256sums=('
|
|
17
|
+
sha256sums=('@SHA256@')
|
|
14
18
|
|
|
15
19
|
build() {
|
|
16
20
|
cd "$pkgname-$pkgver"
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
#
|
|
2
|
-
#
|
|
3
|
-
#
|
|
1
|
+
# Template — not an installable formula. On release the publish_homebrew
|
|
2
|
+
# workflow substitutes @VERSION@ and @SHA256@ and pushes the rendered
|
|
3
|
+
# Formula/workforest.rb to the ArkadyBuryakov/homebrew-tap repo; nothing is
|
|
4
|
+
# committed back here. This copy is the source of truth for everything else
|
|
5
|
+
# (deps, completions, caveats, test).
|
|
4
6
|
class Workforest < Formula
|
|
5
7
|
include Language::Python::Virtualenv
|
|
6
8
|
|
|
7
9
|
desc "Git worktree forest management with per-branch setup hooks"
|
|
8
10
|
homepage "https://github.com/ArkadyBuryakov/workforest"
|
|
9
|
-
url "https://github.com/ArkadyBuryakov/workforest/archive/
|
|
10
|
-
sha256 "
|
|
11
|
+
url "https://github.com/ArkadyBuryakov/workforest/archive/v@VERSION@.tar.gz"
|
|
12
|
+
sha256 "@SHA256@"
|
|
11
13
|
license "MIT"
|
|
12
14
|
head "https://github.com/ArkadyBuryakov/workforest.git", branch: "main"
|
|
13
15
|
|