@soprog_/cdwt
v0.1.3
Published
Interactive git worktree switcher with fzf integration
Readme
cdwt
Interactive git worktree switcher for zsh. Pick an existing worktree, jump
back to the default branch worktree, create a new worktree from the default
branch, check out a GitHub PR into a worktree, or delete one — and cd into
the result.
Written in TypeScript, distributed as an npx-installable CLI plus a small
zsh function that performs the cd.
Requirements
- Node.js 20+
gitzshfzfrecommended — without it the selector falls back to a numbered promptghoptional — enables thegithub prsection
Install
From npm
Try it once:
npx @soprog_/cdwt root
npx @soprog_/cdwt new feature/awesome
npx @soprog_/cdwtWithout the shell wrapper, npx prints the destination path instead of
changing your current shell directory.
Install for daily use:
npm i -g @soprog_/cdwt # or: pnpm add -g @soprog_/cdwt
cdwt install # writes shell function + sources it from ~/.zshrc
exec zsh -lnpm i -g only installs the cdwt executable. Run cdwt install once so
zsh loads the shell function that performs the actual cd.
From source
git clone https://github.com/souta0104/cdwt.git
cd cdwt
pnpm install
pnpm build
pnpm link --global
cdwt install
exec zsh -lcdwt install writes ~/.local/share/cdwt/cdwt.zsh and adds
source "$HOME/.local/share/cdwt/cdwt.zsh" to ~/.zshrc (skipped if already
present). The shell function is required because a child process can't change
the parent shell's directory: cdwt prints the destination path on stdout
and the function cds into it.
Usage
cdwtOpens a single picker with rows tagged by section (in this order):
| Glyph + Tag | Action on enter |
| ---------------- | ----------------------------------------------------------- |
| ★ [main] | cd into the main worktree |
| ● [worktree] | cd into an existing linked worktree |
| ◆ [PR] | cd into the PR's worktree (creates a detached one if new) |
| ○ [branch] | runs git worktree add for that local branch and cds in |
Filled glyphs (★ ●) mark rows whose worktree already exists on disk; open
glyphs (○ ◆) mark rows that will create a new worktree on enter. Each
section also has its own color so worktrees and branches are visually distinct.
/new <branch> creates a new worktree from the default branch; ctrl-d on
a [worktree] row deletes that worktree (with a confirmation prompt).
cdwt root # skip the picker, jump to the main worktree
cdwt new feature/awesome # create a new worktree from the default branch
cdwt pr 42 # skip the picker, cd into PR #42's worktree
cdwt pr # open the picker pre-filtered to PRs
cdwt rm feature/awesome # delete the worktree for that branch
cdwt -h # show help (bypasses the shell wrapper)cdwt root jumps to the main worktree (the one that holds the
non-bare .git directory), not literally to a worktree of origin/HEAD.
In a typical setup these are the same; if you've checked out a different
branch in the main worktree, that's what you'll land on.
cdwt pr <number> skips the picker: it cds into <repo-parent>/<repo-name>-pr-<number>
if it already exists, otherwise it creates that worktree and runs
gh pr checkout <number> inside it. Without a number, cdwt pr opens the picker
pre-filtered to PRs.
cdwt rm <target> deletes the worktree matching that branch name or path,
prompting for confirmation and refusing the main worktree. Without a target it
opens the picker, where ctrl-d deletes the highlighted worktree.
New worktree paths
new worktree and local branch create the worktree at:
<repo-parent>/<repo-name>-<branch-slug><branch-slug> replaces /, spaces, and any non-[A-Za-z0-9._-] character
with -, then trims leading/trailing dashes (e.g. feature/awesome →
repo-feature-awesome).
github pr for a branch without a local worktree creates:
<repo-parent>/<repo-name>-pr-<pr-number>…then runs gh pr checkout <pr-number> inside it.
Selector keys
With fzf:
enter—cdinto the highlighted entryesc— canceltab/shift-tab— cycle the filter (all / worktree / branch / pr)ctrl-d— delete the highlighted worktree (confirmation prompt)?— show the help overlay (includes a row legend)/— slash commands (/new <branch>,/main,/pr,/refresh,/help)
Without fzf: numbered prompt; type a number to jump, d <number> to
delete that entry, or one of the slash commands above.
Configuration
.cdwt/settings.json controls which Git-ignored files get copied into newly
created worktrees:
{
"copyIgnored": {
"paths": [".claude/settings.local.json"],
"patterns": [".claude/**", "CLAUDE.md", "*.local.json"]
}
}paths— repo-relative file or directory paths copied verbatimpatterns— glob patterns matched against repo-relative paths- a pattern containing
/matches the whole path - a pattern without
/matches any file or directory of that name
- a pattern containing
Only files Git considers ignored are copied. Patterns and paths that escape
the worktree (.., absolute, …) are rejected.
Config resolution order (weak → strong)
Later files override matching keys; missing keys leave earlier values intact. An explicit empty array clears the inherited value.
$HOME/.cdwt/settings.json.cdwt/settings.jsonwalking from/down to the cwd (or to the main worktree if cwd is outside it)
Set CDWT_CONFIG=/path/to/settings.json or pass --config <file> to read
only that file.
Development
pnpm install
pnpm test # vitest
pnpm typecheck # tsc --noEmit
pnpm lint # eslint
pnpm format # prettier --write .
pnpm build # tsup → dist/cli.js
pnpm dev -- rootLayout:
src/
cli.ts commander entry
commands/ select / install / actions (confirm + git ops)
core/ pure functions (paths, config merge, sections, ...)
io/ git, gh, fs, repo context
ui/ fzf, prompts, selector flow
shell/cdwt.zsh zsh wrapper that cd's into stdout
tests/ vitest (pure + integration against a temp git repo)Uninstall
npm rm -g cdwt # or: pnpm remove -g cdwt
rm -f ~/.local/share/cdwt/cdwt.zshThen remove this line from ~/.zshrc:
source "$HOME/.local/share/cdwt/cdwt.zsh"