- TypeScript 95.6%
- JavaScript 4.4%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
`list()` did a flat `readdirSync` of `.trees/` and skipped any directory without a `metadata.json` at its root, so `feat/project-manager` (nested as `.trees/feat/project-manager/`) never showed up in `wup list` or the interactive `wup switch` picker. Walk the tree recursively instead, treating directories without a metadata file as namespaces. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
| bin | ||
| src | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc | ||
| CLAUDE.md | ||
| eslint.config.mjs | ||
| mise.toml | ||
| package.json | ||
| pnpm-lock.yaml | ||
| README.md | ||
| Taskfile.yml | ||
| tsconfig.json | ||
| vitest.config.ts | ||
wup
wup (short for workedup) is a small CLI for working with multiple AI coding agents in parallel. It manages git worktrees so each agent can work on its own branch in its own directory — and gives you fast verbs for the things you do dozens of times a day: jump between worktrees, commit with an AI-written message, ship a PR.
If you've ever ended up with eight checkouts of the same repo named myproject, myproject-2, myproject-claude, myproject-bugfix, this is for you.
How it works
A wup project looks like this on disk:
<project>/
├── wup.toml ← project marker + config
├── .bare/ ← the git repository (bare — no working tree of its own)
└── .trees/
├── main/
│ ├── repo/ ← worktree for `main`
│ └── metadata.json
├── feature-x/
│ ├── repo/ ← worktree for `feature-x`
│ └── metadata.json
└── bugfix-y/
├── repo/
└── metadata.json
Every branch you work on is a worktree under .trees/. The repo itself lives in .bare/ and is shared across all worktrees, so there's no duplication of git history and git fetch from any worktree updates them all. There's no "main checkout" — even main is just another worktree. The rule is simple: you always work inside a worktree, never at the top level.
Install
wup is not on npm yet. To install from source:
git clone https://code.olsen.cloud/utils/workedup.git
cd workedup
pnpm install
npm link
npm link adds the wup binary to your PATH, pointing at this checkout — so git pull is all you need to update.
Shell setup (required)
wup switch (and friends) need to change your shell's working directory, which a subprocess can't do on its own. Add this to your ~/.zshrc:
command -v wup >/dev/null 2>&1 && eval "$(wup setup zsh)"
Then start a new shell. From now on, wup switch foo will actually cd you into the worktree.
Only
zshis supported today. Bash and Fish wrappers will follow when there's demand.
Quick start
# Clone an existing repo as a wup project
wup clone git@github.com:you/myproject.git
cd myproject
# See what's there (just the default branch initially)
wup list
# Start working on something new — creates the branch + worktree, cd's you in
wup switch -c feature-x
# ...edit files, run tests, etc...
# Ship it — commits with an AI-written message, pushes, opens a PR
wup ship
Commands
wup setup <shell>
Prints the shell function you need to source. Use as eval "$(wup setup zsh)" in your shell rc file.
wup clone <url>
Clones a repository as a wup project. Creates the wup.toml marker, the bare repository in .bare/, and an initial worktree for the default branch. After cloning, you'll be inside the new project's default worktree.
wup init
Converts an existing git checkout (run from inside it) into a wup project: the current .git becomes the bare repository, your current branch becomes the first worktree, and your untracked/ignored files (like .env, node_modules) move with it.
wup init refuses if you have uncommitted changes — commit or stash them first.
wup list
Lists all worktrees in the current project with their status:
- clean — nothing to do
- dirty — uncommitted changes
- ahead — local commits not pushed
- behind — remote has commits you don't
- diverged — both
wup switch [branch] [-c] [--from <base>] (alias: wup s)
The verb you'll use most.
wup switch(no argument) — interactive picker.wup switch <branch>— jump to that worktree. Behaviour depends on what exists:- Worktree already exists → cd into it.
- Local branch exists but no worktree → create the worktree, cd into it.
- Remote branch exists → create a worktree tracking it, cd into it.
- Branch doesn't exist anywhere → prompts you to create it (
[y/N]).
wup switch -c <branch>— skip the prompt; create if needed.wup switch -c <branch> --from <base>— fork from a specific branch instead of the default.
When forking from the default branch (no --from), wup runs git fetch origin first and bases the new branch on origin/<defaultBranch> — so you never fork from a stale local copy. Pass --from to opt out (the value is used literally).
wup commit
Stages all changes (if nothing is staged), drafts a commit message with the Claude Agent SDK, shows it to you for confirmation, then commits.
Pass -y to skip the confirmation.
wup ship
The "I'm done, send it out" verb. Walks through whatever needs doing:
| Your branch is... | ship does... |
|---|---|
| Dirty (uncommitted changes) | AI-commits (asks first), pushes, opens PR if none |
| Clean, unpushed commits | Pushes, opens PR if none |
| Pushed but no PR | Opens PR |
| PR exists, new local commits | Pushes (PR updates automatically) |
| PR exists, fully synced | Prints the PR URL, exits |
Pass --force to skip the commit-message confirmation.
PR titles and bodies are AI-generated. You can customise the instructions used to generate them by editing ~/.config/wup/pr-instructions.md.
wup rm <branch>
Removes a worktree and its envelope. Refuses if the worktree is dirty or has unpushed commits unless you pass --force.
wup repo (alias: wup r)
Manage a per-user list of wup repos so you can jump between projects the way wup switch jumps between worktrees. The registry lives in ~/.config/wup/repos.json.
wup repo add [path]— register the current wup project (or the given path). Requires awup.toml. No-op if already registered.wup repo remove [name|path]— unregister. With no argument, removes the current project.wup repo list— print all registered repos with their names and paths.wup repo switch [name](alias:wup repo s) — jump to a repo. No argument opens an interactive picker. Lands at the project root; usewup switchfrom there to enter a worktree.wup repo(no subcommand) — shorthand forwup repo switch.
Names are derived from wup.toml's name field, falling back to the project folder name.
wup reset
Wipes every worktree except the default branch and hard-resets that worktree to origin/<defaultBranch> — the "back to a clean slate" verb after a round of agent experiments.
Steps:
git fetch origin.- Refuses (and lists the offenders) if anything is unsafe: dirty changes, unpushed commits, or a branch with no upstream. The default-branch worktree is unsafe if it's dirty (a hard reset would lose those changes).
- Removes every non-default worktree.
- Hard-resets the default-branch worktree to
origin/<defaultBranch>. - cd's you to the default-branch worktree.
Pass --force (or -f) to skip the safety checks and discard everything.
Configuration
Per-project config lives in wup.toml at the project root:
# Optional — short name used by ${REPO_NAME} in tmux templates.
# Falls back to the project folder name.
name = "myproject"
# Optional — override the default branch detected from origin/HEAD.
defaultBranch = "main"
# Optional — shell commands to run inside a newly created worktree.
# Runs for every `wup switch` path that produces a new worktree (new branch,
# existing local branch checkout, remote branch tracking). Each command runs
# via `sh -c`, sequentially, with stdio inherited. A non-zero exit stops the
# sequence and `wup` exits 1 (the worktree itself stays).
[hooks]
postCreate = ["pnpm install"]
# Optional — when inside tmux ($TMUX is set), rename the current session /
# window / pane after every switch. Each entry is independent and optional.
# Template variables: ${REPO_NAME}, ${BRANCH_NAME}. Tmux failures are
# swallowed so a missing tmux binary can't break the switch.
[tmux]
session = "${REPO_NAME}"
window = "${REPO_NAME} - ${BRANCH_NAME}"
pane = "${BRANCH_NAME}"
User-level config lives in ~/.config/wup/:
pr-instructions.md— extra context appended to the prompt that generates PR titles/bodies.config.toml— defaults for[hooks]and[tmux], used when the project'swup.tomldoesn't define them.
# ~/.config/wup/config.toml — applied to every wup project
[hooks]
postCreate = ["pnpm install"]
[tmux]
session = "${REPO_NAME}"
window = "${REPO_NAME} - ${BRANCH_NAME}"
pane = "${BRANCH_NAME}"
Project config replaces global config per section. If wup.toml defines [tmux] at all, the global [tmux] is ignored entirely (same for [hooks]) — so you can't, for example, set tmux.session globally and tmux.window per-project. Define the whole section in the place that needs it.
Requirements
- Node 24+
- Git 2.40+ (worktree support)
- The GitHub CLI (
gh) — used for authentication when opening PRs. Rungh auth loginonce.
Status
Early development. The commands above are the v1 surface; everything else is intentionally left out for later.