Metadata-Version: 2.5
Name: workforest
Version: 0.3.0
Summary: Git worktree forest management: create, open, and clean up per-branch worktrees with project-defined setup hooks
Project-URL: Homepage, https://github.com/ArkadyBuryakov/workforest
Author-email: Arkady Buryakov <arkady@buryakov.pro>
License-Expression: MIT
License-File: LICENSE
Keywords: cli,developer-tools,git,worktree
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Version Control :: Git
Requires-Python: >=3.14
Requires-Dist: pyyaml>=6.0
Description-Content-Type: text/markdown

# Workforest

## Elevator pitch

Your repo has one working directory; your AI agents want five. Workforest
gives every feature — and every agent — its own disposable git worktree, so
parallel work on the same repo never collides.

## About

Git worktree forest management: one main checkout plus any number of
disposable, per-branch worktrees in a predictable location — created, set up,
opened, and cleaned up with one command.

```
~/dev/
├── api/                  # main checkout
└── worktrees/
    └── api/
        ├── feature-x/    # wf create feature-x
        └── fix-y/
```

- **Create** a worktree for any branch (local, remote, or brand new) and have
  it set up automatically: symlinks for untracked assets (`node_modules`,
  `.env`, …) and project-defined setup scripts.
- **Open** it in your editor — in the current shell, or in a new terminal
  window via a configurable command template.
- **Run** named project scripts with well-known `WF_*` environment variables.
- **Delete** worktrees safely, or **checkout**: collapse one back into the
  main checkout.
- Drive everything from an interactive fzf **TUI** (`wf` with no arguments).

## Install

```sh
# Arch Linux
yay -S workforest        # AUR

# macOS (or Linux with Homebrew)
brew install arkadyburyakov/tap/workforest

# anywhere else
uv tool install workforest   # or: pipx install workforest
```

This installs two commands: `workforest` and its alias `wf`. Then add one
line to your `~/.bashrc` / `~/.zshrc`:

```sh
eval "$(workforest shell-init)"
```

This upgrades `wf` to a shell function (needed so `wf open` can change your
shell's directory — a plain binary cannot) and registers completions. Without
it everything still works, but "open in current shell" prints the `cd`
command instead of performing it.

Requirements: Linux or macOS, git ≥ 2.36, Python ≥ 3.14 (the AUR and
Homebrew packages bring their own). Optional: `fzf` for the TUI.

## Quick start

```sh
wf create feature/login     # create worktree + run hooks + open in $EDITOR
wf list                     # what's in the forest
wf open login -o 'lazygit'  # open with any command instead
wf run test                 # run a named script from config
wf run make check -j2       # extra args are appended to the script command
wf checkout login           # fold the branch back into the main checkout
wf delete fix-y             # remove a worktree (asks about dirty changes)
wf                          # interactive TUI (fzf)
```

Any unknown first word is an opener shortcut: `wf edit api` ≡
`wf open api -o edit`.

## Configuration

Layered, YAML or JSON; later layers override earlier ones:

| Layer | Location | Typical content |
|---|---|---|
| system | `/etc/workforest/config.yaml` | org-wide defaults |
| user | `~/.config/workforest/config.yaml` | your terminal/editor setup |
| project (shared) | `.workforest.yaml` in the repo root | repo policy, committed |
| project (local) | `.vscode/` or `.idea/` `.workforest.yaml` | personal overrides, untracked |

Scalars and lists replace; the `scripts`/`openers` mappings merge per key
(`null` removes an entry). `workforest config` shows the merged result and
where each layer came from; `workforest init` scaffolds a project file
(`--local` for a personal one).

All keys, with defaults:

```yaml
worktrees_dir: "$WF_MAIN/../worktrees/$WF_NAME"  # where the forest lives
opener: ""              # default opener; "" → $VISUAL → $EDITOR
openers: {}             # name -> shell command, e.g. edit: '$EDITOR "$WF_TARGET"'
window_command: ""      # "" → current shell; or e.g. kitty --title "$WF_TITLE"
                        #   --directory "$WF_WORKTREE" $SHELL -c "$WF_COMMAND"
symlinks: []            # untracked assets linked from main into new worktrees
setup_scripts: []       # shell snippets run in a fresh worktree
scripts: {}             # name -> snippet for `wf run NAME`
```

Openers and `window_command` are plain shell commands, run via `$SHELL -c`
with one variable family in the environment — the same family the launched
process and every script receive:

| Variable | Value |
|---|---|
| `WF_MAIN` | main worktree path, `/home/user/Projects/project_name` |
| `WF_NAME` | repo name, `project_name` |
| `WF_WORKTREES_DIR` | resolved worktrees directory |
| `WF_WORKTREE` | this worktree's path |
| `WF_BRANCH` | its branch (empty if detached) |
| `WF_TARGET` | the `-p` argument, default `.` (launch-only) |
| `WF_TITLE` | window label, `project_name: feat-x` (launch-only) |

Standard shell rules apply — there is no workforest template syntax:
`"$WF_X"` is exactly one argument, bare `$WF_X` word-splits, and `$$`,
braces, pipes, and `&&` mean whatever your shell says they mean (a
misspelled `$WF_VAR` expands to empty, as in any shell). Openers run with
the worktree root as working directory. In `window_command` the resolved
opener command is additionally available as `$WF_COMMAND` — still
unexpanded, so run it through a shell of its own for its `$WF_*` references
to resolve: `$SHELL -c "$WF_COMMAND"`. Spawned windows shed activation
state inherited from the invoking shell (Python venv, conda, nvm, rvm) so
the new session starts clean instead of carrying an environment it cannot
deactivate.

Fully commented reference configs:
[`config.yaml`](src/workforest/examples/config.yaml) (user/system) and
[`.workforest.yaml`](src/workforest/examples/.workforest.yaml) (project) —
installed to `/usr/share/doc/workforest/examples/` by the Arch package.

### Script environment

`setup_scripts`, `scripts`, and hooks run via `$SHELL -c` with:

| Variable | Value |
|---|---|
| `WF_MAIN` | main worktree path |
| `WF_NAME` | repo name (main checkout directory name) |
| `WF_WORKTREE` | current/new worktree path |
| `WF_WORKTREES_DIR` | resolved worktrees directory |
| `WF_BRANCH` | branch of the current/new worktree |

`worktrees_dir` is a template using the same naming pattern: `$WF_MAIN` and
`$WF_NAME` (plus regular environment variables like `$HOME`) expand there —
the per-worktree variables don't, since no worktree exists yet when the base
directory is resolved.

### Example project config

```yaml
# .workforest.yaml — committed to the repo
symlinks: [node_modules, .env]
setup_scripts:
  - npm install --prefer-offline
scripts:
  test: npm test
  migrate: npm run db:migrate
```

## Commands

```
workforest create [BRANCH] [-o OPENER] [-p PATH] [--no-hooks] [--no-open]
workforest open   [NAME]   [-o OPENER] [-p PATH]
workforest list   [--porcelain]
workforest delete NAME...  [--force] [--delete-branch | --keep-branch]
workforest checkout NAME   [--force]
workforest run    SCRIPT [ARGS...]
workforest tui    [MODE]
workforest init   [--local]
workforest config [--json]
workforest shell-init [bash|zsh]
workforest claude copy-session SESSION_ID   # experimental
```

`open` (and the opener shortcut, e.g. `wf edit`) without NAME opens the
worktree you are standing in.

`create` resolves BRANCH in order: existing local branch, then a branch on
exactly one remote (checked out tracking it), then a brand-new branch.
`REMOTE/BRANCH` picks the remote explicitly — needed when several remotes
carry the same branch name; if that local name is already taken, `create`
prompts for a different one.

`workforest claude` (shown only when `~/.claude` exists) copies a Claude
Code session from the main worktree into the current one. It is
**experimental**: it manipulates Claude Code's private on-disk state,
which is not a stable interface, so any Claude Code update may break it.

Exit codes: `0` ok · `1` error · `2` usage · `3` cancelled · `4` config error.
Human messages go to stderr; stdout carries only machine output (`cd`
directives for the `wf` wrapper, `--porcelain` listings, dumps).

## Development

```sh
uv sync           # venv + dev dependencies (uv.lock)
make check        # ruff + mypy --strict + pytest (coverage gate ≥ 90%)
make install      # install this checkout as a uv tool (~/.local/bin/workforest)
make uninstall    # remove it again
```

Packaging templates live under `packaging/` (one directory per package
manager: `packaging/AUR/`, `packaging/homebrew/`); the `@VERSION@` and
`@SHA256@` placeholders are filled in at release time.
Release: bump `__version__` and push to main — CI tags the release,
renders the templates, and publishes to PyPI, the AUR, and the
[Homebrew tap](https://github.com/ArkadyBuryakov/homebrew-tap). The
published AUR package and tap are the only places rendered recipes exist.

## License

[MIT](./LICENSE)
