use clap::{Args, Subcommand};
#[derive(Args)]
pub struct CommitArgs {
#[arg(short, long, add = crate::completion::worktree_only_completer())]
pub(crate) branch: Option<String>,
#[arg(short, long, help_heading = "Automation")]
pub(crate) yes: bool,
#[arg(long = "no-hooks", action = clap::ArgAction::SetFalse, default_value_t = true, help_heading = "Automation")]
pub(crate) verify: bool,
#[arg(long = "no-verify", hide = true)]
pub(crate) no_verify_deprecated: bool,
#[arg(long)]
pub(crate) stage: Option<crate::commands::commit::StageMode>,
#[arg(long)]
pub(crate) show_prompt: bool,
}
#[derive(Args)]
pub struct SquashArgs {
#[arg(add = crate::completion::branch_value_completer())]
pub(crate) target: Option<String>,
#[arg(short, long, help_heading = "Automation")]
pub(crate) yes: bool,
#[arg(long = "no-hooks", action = clap::ArgAction::SetFalse, default_value_t = true, help_heading = "Automation")]
pub(crate) verify: bool,
#[arg(long = "no-verify", hide = true)]
pub(crate) no_verify_deprecated: bool,
#[arg(long)]
pub(crate) stage: Option<crate::commands::commit::StageMode>,
#[arg(long)]
pub(crate) show_prompt: bool,
}
#[derive(Subcommand)]
#[command(allow_external_subcommands = true)]
pub enum StepCommand {
#[command(
after_long_help = r#"See [LLM-generated commit messages](@/llm-commits.md) for configuration and prompt customization.
## Options
### `--stage`
Controls what to stage before committing:
| Value | Behavior |
|-------|----------|
| `all` | Stage all changes including untracked files (default) |
| `tracked` | Stage only modified tracked files |
| `none` | Don't stage anything, commit only what's already staged |
```console
$ wt step commit --stage=tracked
```
Configure the default in user config:
```toml
[commit]
stage = "tracked"
```
### `--show-prompt`
Output the rendered LLM prompt to stdout without running the command. Useful for inspecting prompt templates or piping to other tools:
```console
# Inspect the rendered prompt
$ wt step commit --show-prompt | less
# Pipe to a different LLM
$ wt step commit --show-prompt | llm -m gpt-5-nano
```
"#
)]
Commit(CommitArgs),
#[command(
after_long_help = r#"See [LLM-generated commit messages](@/llm-commits.md) for configuration and prompt customization.
## Options
### `--stage`
Controls what to stage before squashing:
| Value | Behavior |
|-------|----------|
| `all` | Stage all changes including untracked files (default) |
| `tracked` | Stage only modified tracked files |
| `none` | Don't stage anything, squash only committed changes |
```console
$ wt step squash --stage=none
```
Configure the default in user config:
```toml
[commit]
stage = "tracked"
```
### `--show-prompt`
Output the rendered LLM prompt to stdout without running the command. Useful for inspecting prompt templates or piping to other tools:
```console
$ wt step squash --show-prompt | less
```
"#
)]
Squash(SquashArgs),
#[command(
after_long_help = r#"Updates the local target branch (e.g., `main`) to include current commits.
## Examples
```console
$ wt step push # Fast-forward main to current branch
$ wt step push develop # Fast-forward develop instead
```
Similar to `git push . HEAD:<target>`, but uses `receive.denyCurrentBranch=updateInstead` internally.
"#
)]
Push {
#[arg(add = crate::completion::branch_value_completer())]
target: Option<String>,
#[arg(long = "no-ff", overrides_with = "ff")]
no_ff: bool,
#[arg(long, overrides_with = "no_ff", hide = true)]
ff: bool,
},
#[command(
after_long_help = r#"Rebases the current branch onto the target branch. Conflicts abort immediately; use `git rebase --abort` to recover.
## Examples
```console
$ wt step rebase # Rebase onto default branch
$ wt step rebase develop # Rebase onto develop
```
"#
)]
Rebase {
#[arg(add = crate::completion::branch_value_completer())]
target: Option<String>,
},
#[command(
after_long_help = r#"This is what `wt merge` would include — a single diff against the merge base.
## Extra git diff arguments
Arguments after `--` are forwarded to `git diff`:
```console
$ wt step diff -- --stat
$ wt step diff -- --name-only
$ wt step diff -- -- '*.rs'
```
The diff is pipeable to tools like `delta`:
```console
$ wt step diff | delta
```
## How it works
Equivalent to:
```console
$ cp "$(git rev-parse --git-dir)/index" /tmp/idx
$ GIT_INDEX_FILE=/tmp/idx git add --intent-to-add .
$ GIT_INDEX_FILE=/tmp/idx git diff $(git merge-base HEAD $(wt config state default-branch))
```
`git diff` ignores untracked files. `git add --intent-to-add .` registers them in the index without staging their content, making them visible to `git diff`. This runs against a copy of the real index so the original is never modified.
"#
)]
Diff {
#[arg(add = crate::completion::branch_value_completer())]
target: Option<String>,
#[arg(last = true)]
extra_args: Vec<String>,
},
#[command(after_long_help = r#"## Setup
Add to the project config:
```toml
# .config/wt.toml
[post-start]
copy = "wt step copy-ignored"
```
## What gets copied
All gitignored files are copied by default, except for built-in excluded directories: VCS metadata (`.bzr/`, `.hg/`, `.jj/`, `.pijul/`, `.sl/`, `.svn/`) and tool-state (`.conductor/`, `.entire/`, `.pi/`, `.worktrees/`). Tracked files are never touched.
To limit what gets copied further, create `.worktreeinclude` with gitignore-style patterns. Files must be **both** gitignored **and** in `.worktreeinclude`:
```text
# .worktreeinclude
.env
node_modules/
target/
```
After `.worktreeinclude` selects entries, you can add more gitignore-style excludes in user config, per-project user overrides, or project config:
```toml
[step.copy-ignored]
exclude = [".cache/", ".turbo/"]
```
## Common patterns
| Type | Patterns |
|------|----------|
| Dependencies | `node_modules/`, `.venv/`, `target/`, `vendor/`, `Pods/` |
| Build caches | `.cache/`, `.next/`, `.parcel-cache/`, `.turbo/` |
| Generated assets | Images, ML models, binaries too large for git |
| Environment files | `.env` (if not generated per-worktree) |
## Features
- Uses copy-on-write (reflink) when available for space-efficient copies
- Handles nested `.gitignore` files, global excludes, and `.git/info/exclude`
- Skips existing files by default (safe to re-run)
- `--force` overwrites existing files in the destination
- Always skips built-in excluded directories — VCS metadata (`.bzr/`, `.hg/`, `.jj/`, `.pijul/`, `.sl/`, `.svn/`) and tool-state (`.conductor/`, `.entire/`, `.pi/`, `.worktrees/`) — and nested worktrees
## Performance
Reflink copies share disk blocks until modified — no data is actually copied. For a 14GB `target/` directory:
| Command | Time |
|---------|------|
| `cp -R` (full copy) | 2m |
| `cp -Rc` / `wt step copy-ignored` | 20s |
Uses per-file reflink (like `cp -Rc`) — copy time scales with file count. On Unix, the process is automatically reniced to lowest priority (nice 19) so it yields to interactive work.
Use the `post-start` hook so the copy runs in the background. Use `pre-start` instead if subsequent hooks or `--execute` command need the copied files immediately.
## Language-specific notes
### Rust
The `target/` directory is huge (often 1-10GB). Copying with reflink cuts first build from ~68s to ~3s by reusing compiled dependencies.
### Node.js
`node_modules/` is large but mostly static. If the project has no native dependencies, symlinks are even faster:
```toml
[pre-start]
deps = "ln -sf {{ primary_worktree_path }}/node_modules ."
```
### Python
Virtual environments contain absolute paths and can't be copied. Use `uv sync` instead — it's fast enough that copying isn't worth it.
## Behavior vs Claude Code on desktop
The `.worktreeinclude` pattern is shared with [Claude Code on desktop](https://code.claude.com/docs/en/desktop), which copies matching files when creating worktrees. Differences:
- worktrunk copies all gitignored files by default; Claude Code requires `.worktreeinclude`
- worktrunk uses copy-on-write for large directories like `target/` — potentially 30x faster on macOS, 6x on Linux
- worktrunk runs as a configurable hook in the worktree lifecycle
"#)]
CopyIgnored {
#[arg(long, add = crate::completion::worktree_only_completer())]
from: Option<String>,
#[arg(long, add = crate::completion::worktree_only_completer())]
to: Option<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
force: bool,
},
#[command(
after_long_help = r#"All [hook template variables and filters](@/hook.md#template-variables) are available.
## Examples
Get the port for the current branch:
```console
$ wt step eval '{{ branch | hash_port }}'
16066
```
Use in shell substitution:
```console
$ curl http://localhost:$(wt step eval '{{ branch | hash_port }}')/health
```
Combine multiple values:
```console
$ wt step eval '{{ branch | hash_port }},{{ ("supabase-api-" ~ branch) | hash_port }}'
16066,16739
```
Use conditionals and filters:
```console
$ wt step eval '{{ branch | sanitize_db }}'
feature_auth_oauth2_a1b
```
Show available template variables:
```console
$ wt step eval --dry-run '{{ branch }}'
branch=feature/auth-oauth2
worktree_path=/home/user/projects/myapp-feature-auth-oauth2
...
Result: feature/auth-oauth2
```
Note: This command is experimental and may change in future versions.
"#
)]
Eval {
template: String,
#[arg(long)]
dry_run: bool,
},
#[command(
after_long_help = r#"A summary of successes and failures is shown at the end. Context JSON is piped to stdin for scripts that need structured data.
## Template variables
All variables are shell-escaped. See [`wt hook` template variables](@/hook.md#template-variables) for the complete list and filters.
## Examples
Check status across all worktrees:
```console
$ wt step for-each -- git status --short
```
Run npm install in all worktrees:
```console
$ wt step for-each -- npm install
```
Use branch name in command:
```console
$ wt step for-each -- "echo Branch: {{ branch }}"
```
Pull updates in worktrees with upstreams (skips others):
```console
$ git fetch --prune && wt step for-each -- '[ "$(git rev-parse @{u} 2>/dev/null)" ] || exit 0; git pull --autostash'
```
Note: This command is experimental and may change in future versions.
"#
)]
ForEach {
#[arg(long, default_value = "text", help_heading = "Automation")]
format: crate::cli::SwitchFormat,
#[arg(required = true, last = true, num_args = 1..)]
args: Vec<String>,
},
#[command(
after_long_help = r#"**Experimental.** Use promote for temporary testing when the main worktree has special significance (Docker Compose, IDE configs, heavy build artifacts anchored to project root), and hooks & tools aren't yet set up to run on arbitrary worktrees. The idiomatic Worktrunk workflow does not use `promote`; instead each worktree has a full environment. `promote` is the only Worktrunk command which changes a branch in an existing worktree.
## Example
```console
# from ~/project (main worktree)
$ wt step promote feature
```
Before:
```
Branch Path
@ main ~/project
+ feature ~/project.feature
```
After:
```
Branch Path
@ feature ~/project
+ main ~/project.feature
```
To restore: `wt step promote main` from anywhere, or just `wt step promote` from the main worktree.
Without an argument, promotes the current branch — or restores the default branch if run from the main worktree.
## Requirements
- Both worktrees must be clean
- The branch must have an existing worktree
## Gitignored files
Gitignored files (build artifacts, `node_modules/`, `.env`) are swapped along with the branches so each worktree keeps the artifacts that belong to its branch. Files are discovered using the same mechanism as [`copy-ignored`](#wt-step-copy-ignored) and can be filtered with `.worktreeinclude`.
The swap uses `rename()` for each entry — fast regardless of entry size, since only filesystem metadata changes. If the worktree is on a different filesystem from `.git/`, it falls back to reflink copy.
"#
)]
Promote {
#[arg(add = crate::completion::worktree_only_completer())]
branch: Option<String>,
},
#[command(
after_long_help = r#"Bulk-removes worktrees and branches that are integrated into the default branch, using the same criteria as `wt remove`'s branch cleanup. Stale worktree entries are cleaned up too.
In `wt list`, candidates show `_` (same commit) or `⊂` (content integrated). Run `--dry-run` to preview. See `wt remove --help` for the full integration criteria.
Locked worktrees and the main worktree are always skipped. The current worktree is removed last, triggering cd to the primary worktree. Pre-remove and post-remove hooks run for each removal.
## Min-age guard
Worktrees younger than `--min-age` (default: 1 hour) are skipped. This prevents removing a worktree just created from the default branch — it looks "merged" because its branch points at the same commit.
```console
$ wt step prune --min-age=0s # no age guard
$ wt step prune --min-age=2d # skip worktrees younger than 2 days
```
## Examples
Preview what would be removed:
```console
$ wt step prune --dry-run
```
Remove all merged worktrees:
```console
$ wt step prune
```
"#
)]
Prune {
#[arg(long)]
dry_run: bool,
#[arg(short, long, help_heading = "Automation")]
yes: bool,
#[arg(long, default_value = "1h")]
min_age: String,
#[arg(long)]
foreground: bool,
#[arg(long, default_value = "text", help_heading = "Automation")]
format: crate::cli::SwitchFormat,
},
#[command(after_long_help = r#"## Examples
Preview what would be moved:
```console
$ wt step relocate --dry-run
```
Move all mismatched worktrees:
```console
$ wt step relocate
```
Auto-commit and clobber blockers (never fails):
```console
$ wt step relocate --commit --clobber
```
Move specific worktrees:
```console
$ wt step relocate feature bugfix
```
## Swap handling
When worktrees are at each other's expected locations (e.g., `alpha` at
`repo.beta` and `beta` at `repo.alpha`), relocate automatically resolves
this by using a temporary location.
## Clobbering
With `--clobber`, non-worktree paths at target locations are moved to
`<path>.bak-<timestamp>` before relocating.
## Main worktree behavior
The main worktree can't be moved with `git worktree move`. Instead, relocate
switches it to the default branch and creates a new linked worktree at the
expected path. Untracked and gitignored files remain at the original location.
## Skipped worktrees
- **Dirty** (without `--commit`) — use `--commit` to auto-commit first
- **Locked** — unlock with `git worktree unlock`
- **Target blocked** (without `--clobber`) — use `--clobber` to backup blocker
- **Detached HEAD** — no branch to compute expected path
Note: This command is experimental and may change in future versions.
"#)]
Relocate {
#[arg(add = crate::completion::worktree_only_completer())]
branches: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(long)]
commit: bool,
#[arg(long)]
clobber: bool,
},
#[command(external_subcommand)]
External(Vec<String>),
}