A context-aware cd for monorepos.
cdx 3 # up three directories
cdx :repo # the git root
cdx +monorepo # wherever your pnpm-workspace.yaml lives
cdx @acme/webapp # that package, from anywhere in the repo
cdx @acme/webapp/src/componentsWith tab completion for every form.
cd ../../../ is fine until you are five levels deep in a monorepo and the thing you want is a sibling package. cdx resolves short expressions against where you are standing and against tags you define, then takes you there.
It does not do frecency ranking or fuzzy history. zoxide already does that well, and the two coexist happily.
./install.shCopies the binary to ~/.local/bin and adds one line to your shell rc inside a marker-delimited block:
eval "$(cdx init zsh)"./uninstall.sh reverses it.
An argument is an anchor plus an optional path suffix. The suffix is appended once the anchor resolves, so cdx @acme/webapp/src lands in that package's src.
cdx 3 # ../../..
cdx 3/packages # ../../../packagesThe cdup replacement.
Built in, always available, never configurable.
| tag | resolves to |
|---|---|
:root |
/ |
:user |
your home directory |
:repo |
nearest ancestor containing .git |
Tags you define, in a cdx.conf:
{
"tags": {
"logs": "/var/log",
"api": "./apps/api",
"monorepo": { "findUp": "pnpm-workspace.yaml" }
}
}cdx +api
cdx +monorepoThree ways to write a destination:
- Absolute path (starts with
/), used as-is. - Relative path, resolved against the directory holding that
cdx.conf, never against your cwd. A tag defined at the repo root means the same thing everywhere beneath it. findUp, which searches upward from your cwd for the named file and lands on the directory containing it. The search stops at thecdx.confthat declared the tag, so a tag can never take you outside the tree its config governs.
cdx @acme/webappSearches from the git root for a package whose declared name matches, reading package.json, go.mod, and Cargo.toml. Directory names are not matched; the name in the manifest is the name.
Gitignored paths are skipped, and the search goes at most 5 levels below the git root.
Because # and $ do not survive the shell. # starts a comment and $name is parameter expansion, so cdx #repo and cdx $tag reach the binary as zero arguments, in zsh and bash alike. : and + pass through untouched, as does @.
cdx src
cdx /tmpFalls through to ordinary path behavior, so cdx is a drop-in replacement for cd.
cdx walks up from your cwd collecting every cdx.conf it finds, stopping at $HOME. All of them merge, with the nearest file winning on conflicts.
So a ~/cdx.conf gives you tags that work everywhere, a repo-root cdx.conf adds repo-specific ones, and a leaf cdx.conf can override a tag for one subtree. Each tag keeps resolving relative to its own config file, so merging never changes what a relative path means.
A compiled binary cannot change its parent shell's directory, so cdx is a binary plus a shell function. The binary prints a path; the function runs cd:
cdx() { local d; d="$(command cdx resolve "$@")" && cd "$d"; }If resolution fails, nothing is printed, the exit code is nonzero, and you stay exactly where you were.
There is no cache and no background process. Each invocation asks git for the repository's non-ignored manifests and reads them. That means a package you added a second ago completes correctly, with no daemon to restart and no stale index to clear.
git- Go 1.22+ to build
go build ./...
go test ./...