No description
  • TypeScript 95.6%
  • JavaScript 4.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Morten Olsen 37291d95e0
fix wup list to discover worktrees whose branch names contain a slash
`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>
2026-07-01 12:39:42 +02:00
bin init 2026-05-13 07:48:30 +02:00
src fix wup list to discover worktrees whose branch names contain a slash 2026-07-01 12:39:42 +02:00
.gitignore init 2026-05-13 07:48:30 +02:00
.prettierignore init 2026-05-13 07:48:30 +02:00
.prettierrc init 2026-05-13 07:48:30 +02:00
CLAUDE.md init 2026-05-13 07:48:30 +02:00
eslint.config.mjs init 2026-05-13 07:48:30 +02:00
mise.toml init 2026-05-13 07:48:30 +02:00
package.json init 2026-05-13 07:48:30 +02:00
pnpm-lock.yaml init 2026-05-13 07:48:30 +02:00
README.md land wup repo switch at the project root instead of the default-branch worktree 2026-07-01 10:50:52 +02:00
Taskfile.yml init 2026-05-13 07:48:30 +02:00
tsconfig.json init 2026-05-13 07:48:30 +02:00
vitest.config.ts init 2026-05-13 07:48:30 +02:00

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 zsh is 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:
    1. Worktree already exists → cd into it.
    2. Local branch exists but no worktree → create the worktree, cd into it.
    3. Remote branch exists → create a worktree tracking it, cd into it.
    4. 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 a wup.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; use wup switch from there to enter a worktree.
  • wup repo (no subcommand) — shorthand for wup 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:

  1. git fetch origin.
  2. 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).
  3. Removes every non-default worktree.
  4. Hard-resets the default-branch worktree to origin/<defaultBranch>.
  5. 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's wup.toml doesn'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. Run gh auth login once.

Status

Early development. The commands above are the v1 surface; everything else is intentionally left out for later.