Skip to content
mihai-valentinPublic

About

Tiny bash CLI that replaces boring cd navigation

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

cdp — change-dir-project

CI License: MIT

Tiny bash CLI that replaces cd /long/path/to/project with cdp <label>. Per-project macros (defined in an SSH-style config file) let you bundle a jump with the commands you usually type after it: cdp myproject deploy jumps and runs the deploy steps.

# Jump to a project
cdp myproject

# Jump and run a macro defined for that project
cdp myproject deploy

# Add / list / remove projects
cdp add myproject /home/user/myproject
cdp ls
cdp rm myproject

No npm, no composer, no Go binary, no Python. Bash 4+, coreutils, and an flock from util-linux are all you need at runtime. tmux is required only if you use Tmux blocks; macros and bare jumps work without it.

Install

From source (recommended)

git clone https://github.com/mihai-valentin/cdp.git
cd cdp
make install                       # installs to $HOME/.local
# Add this to your ~/.bashrc or ~/.zshrc:
eval "$(~/.local/bin/cdp init bash)"

make install puts cdp in $PREFIX/bin, scripts in $PREFIX/libexec/cdp/, and helper libs in $PREFIX/lib/cdp/. Override the prefix with make install PREFIX=/usr/local.

Without make

./install.sh

install.sh probes your PATH and picks $HOME/.local or /usr/local accordingly. Override with ./install.sh --prefix=<path>.

From a release tarball

VERSION=1.11.0
curl -sLO https://github.com/mihai-valentin/cdp/releases/download/v${VERSION}/cdp-${VERSION}.tar.gz
curl -sLO https://github.com/mihai-valentin/cdp/releases/download/v${VERSION}/cdp-${VERSION}.tar.gz.sha256
sha256sum -c cdp-${VERSION}.tar.gz.sha256
tar -xzf cdp-${VERSION}.tar.gz
cd cdp-${VERSION} && ./install.sh

Uninstall

make uninstall

Usage

Jump

cdp <label>

cdp myproject resolves the myproject project's Path and cds your shell into it. The shim function emitted by cdp init performs the actual cd — a regular binary cannot mutate its parent's cwd.

Jump + macro

cdp <label> <macro>

cdp myproject deploy jumps to myproject and then runs the deploy macro's Run lines, in order, in your interactive shell. Macros aren't sandboxed — export FOO=bar inside a macro affects your shell, by design.

Jump + ad-hoc command (exec)

cdp <label> exec <command>…                  # jump, then run <command>
cdp <label> exec --save-macro=<name> <cmd>…  # …and save it as a macro

cdp api exec git status jumps to api and runs git status there — a one-shot macro you didn't have to define. The words after exec are joined with spaces and run in your live shell, so it composes exactly like a macro (env changes stick; interactive commands like git commit or a pager open on your terminal). Quote the command to use shell features that would otherwise be eaten by the outer shell: cdp api exec 'git log --oneline | head'.

Add --save-macro=<name> (right after exec) to persist the command as a Macro <name> under the project and run it now:

cdp api exec --save-macro=build npm run build
# → runs `npm run build` in api, and writes:
#     Macro build
#         Run npm run build
# thereafter reachable as: cdp api build

The name must be a valid identifier, must not be a reserved subcommand or exec, and must not already name an action of the project (no silent overwrite — edit the config to change an existing macro). The save is atomic and re-verified before your config is replaced; the confirmation prints to stderr, so the command's own output is unaffected.

Manage projects

cdp add <label> <path>      # add an entry
cdp add -g <group> <path>   # add an entry into a group
cdp rm <label>              # remove an entry
cdp path <label>            # print the project's absolute path
cdp path -x <label>         # print it and copy it to the clipboard
cdp ls                      # list projects (readable on a terminal)
cdp ls -l                   # force the readable layout when piping
cdp edit                    # open the config in $VISUAL / $EDITOR
cdp check                   # parse the config and report validity

cdp add derives the project label from the directory's basename when you don't pass one, so the common case is just cdp add from inside the project. The path argument accepts ., a relative path, ~, or an absolute path and is canonicalized to an absolute path before it's written. If the basename isn't a valid label (e.g. it contains a .), pass one explicitly: cdp add <label> <path>. Forms: cdp add (current dir), cdp add <path>, cdp add <label> <path>.

Pass -g/--group <group> to attach the new project to a group — it's inserted as the group's last member, so it inherits the group's macros (and root, though the written Path is always absolute). The group must already exist; add --create-group to create it (a new Group block appended to the file) on the fly. The flag works in any position: cdp add -g work api ~/src/api or cdp add ~/src/api -g work. If the group is missing and you didn't pass --create-group, the add fails and the config is left untouched. As a safety measure cdp add re-parses the file it produced and verifies the new project resolves to the intended path and group before replacing your config.

On a terminal, cdp ls prints a readable per-project block — the path on its own line, and actions grouped by kind (macros, tmux, inherited):

cdp
  path       /home/user/xlnf/cdp
  inherited  claude (from xlnf)

xlnf
  path       /home/user/xlnf
  macros     deploy, logs
  tmux       dev

When the output is piped (not a terminal), cdp ls switches to a stable, awk-friendly LABEL\tPATH\tACTIONS line per project — each entry in ACTIONS is <name>:<kind> (<kind> is macro or tmux) in source order, with macros inherited from a Group (see below) carrying a trailing @<group> suffix (e.g. claude:macro@xlnf). Pass -l/--long to force the readable layout even when piping (e.g. cdp ls -l | less). Macros and tmux blocks have no dedicated subcommand — use cdp edit (or any editor) to add or modify them.

cdp path <label> writes the project's absolute path to stdout and nothing else, so it composes in command substitution — cd "$(cdp path api)", tar -C "$(cdp path api)" …, etc. (It's a pure lookup and does not require the directory to exist on disk.) Pass -x (or --clip) to also copy the path to the system clipboard; the path still prints, and a short confirmation goes to stderr. The clipboard backend is auto-detected in order — pbcopy (macOS), wl-copy (Wayland), xclip / xsel (X11), clip.exe (WSL) — or set $CDP_CLIPBOARD to a command of your own (it receives the path on stdin, e.g. CDP_CLIPBOARD='xsel -b'). If -x is given but no backend is found, the command prints the path and exits non-zero.

cdp edit resolves the editor in the standard $VISUAL → $EDITOR → vi chain. If the config file does not yet exist, the parent directory is created so the editor opens at the resolved path; the file is materialized on save.

cdp check runs the parser over the resolved config and reports whether it is valid — exit 0 and a brief OK line on stderr on success, exit 2 if the file is missing or unreadable, exit 65 with the parser's config:<line>: <message> on a parse error. Pairs naturally with cdp edit for an edit-then-validate loop, and is safe to use as a precondition: cdp check && cdp <label>.

Tmux integration

cdp <label> <tmux-name> materializes a per-project tmux pane layout (or attaches if the session already exists). Each Tmux block names its layout with a small DSL — h:[…] for side-by-side splits, v:[…] for stacked splits, with arbitrary nesting — and one Pane <name> block per pane carrying the commands to send.

Project myapp
    Path /home/user/myapp
    Tmux dev
        Layout h:[main | v:[test | logs]]
        Pane main
            Run pnpm dev
        Pane test
            Run pnpm test --watch
        Pane logs
            Run tail -f var/log/app.log

Then cdp myapp dev builds the layout (one main pane on the left; test over logs on the right) and attaches you. If the myapp-dev tmux session already exists, the user's running panes are kept as-is and cdp just attaches.

Direction convention follows tmux: h = horizontal divider (panes side-by-side), v = vertical divider (panes stacked). The full grammar, walk semantics, and protocol live in docs/specs/tmux-layout.md.

Shell shim

cdp init bash      # prints shim source for bash
cdp init zsh       # prints shim source for zsh (identical body in V1)

Source it once per interactive shell, typically by adding eval "$(cdp init bash)" to your ~/.bashrc (or ~/.zshrc).

Tab completion

The same eval "$(cdp init bash)" line also installs tab completion — no extra setup. Re-source your rc file (or open a new shell) after upgrading to pick it up.

cdp <TAB>              # subcommands + project labels
cdp web <TAB>          # web's actions: macros, tmux blocks, inherited group macros, exec
cdp path <TAB>         # project labels
cdp path -<TAB>        # -x / --clip
cdp init <TAB>         # bash / zsh
cdp add <TAB>          # directories

Candidates come from a hidden cdp __complete helper that reads your config, so completion always reflects the current file. zsh: completion is provided through bash-style bashcompinit, which needs zsh's own compinit to have run — if your ~/.zshrc doesn't already call compinit, add it before the eval "$(cdp init zsh)" line (cdp also attempts to bootstrap it defensively).

Config file

The config file lives at $CDP_CONFIG, or $XDG_CONFIG_HOME/cdp/config, or $HOME/.config/cdp/config — searched in that order.

Project myproject
    Path /home/user/myproject
    Macro deploy
        Run cd ./static-pages
        Run ./scripts/publish.sh
    Macro logs
        Run tail -f /var/log/myproject.log

Project api
    Path /home/user/projects/api
    Macro dev
        Run pnpm install
        Run pnpm --filter web dev

The formal grammar lives in docs/specs/config-format.md. Highlights: indentation-based blocks, case-insensitive keywords, # line comments only (no trailing comments), tilde expansion at parse time.

Groups (shared macros)

Wrap a set of related projects under a Group block to share Macros across them. A group's macros are inherited by every nested project; a project's own macro of the same name shadows the inherited one.

Group xlnf
    Macro claude
        Run ../xlnfclaude -c

    Project xlnf
        Path /home/user/xlnf
        Macro claude              # shadows the group's claude
            Run ./xlnfclaude -c

    Project cdp
        Path /home/user/xlnf/cdp
                                  # inherits claude from the group

    Project shawarma
        Path /home/user/xlnf/shawarma
                                  # inherits claude

Project nexus                     # column 0 — outside any group
    Path /home/user/nexus
    Macro claude
        Run claude -c

Then cdp cdp claude runs ../xlnfclaude -c (inherited), cdp xlnf claude runs ./xlnfclaude -c (override), and cdp nexus claude runs claude -c (no group affiliation). cdp ls shows the inheritance:

cdp     /home/user/xlnf/cdp     claude:macro@xlnf
nexus   /home/user/nexus        claude:macro
xlnf    /home/user/xlnf         claude:macro

Group rules in brief: nesting determines membership (one group per project); a Group carries Macros and, optionally, one Path root (see below) — Tmux and nested Group are parse errors; project-local always wins on name collision.

Group workspace root

Add a single Path line directly under a Group (before its projects) to set a workspace root. Member projects are then resolved against it:

Group xlnf
    Path /home/user/xlnf          # the group's workspace root
    Project xlnf                  # no Path  → /home/user/xlnf  (the root itself)
    Project cdp
        Path cdp                  # relative → /home/user/xlnf/cdp
    Project notes
        Path /opt/notes           # absolute → /opt/notes  (wins, escapes the root)

A relative member Path is joined onto the root; an absolute member Path wins (escapes the root); a member with no Path resolves to the root itself. The group root is tilde-expanded and must be absolute. A Group without a Path root behaves as before — its members must each declare an absolute Path.

Shell support

  • bash 4+ and zsh are tested daily.
  • macOS ships bash 3.2 by default. Install a modern bash via Homebrew (brew install bash) or use zsh (the system default on recent macOS).
  • fish and other shells are not supported in V1.

Troubleshooting

cdp: command not found — the eval line isn't in your rc file, or your rc file isn't being read by your shell. Confirm with type cdp; if it returns "not found", add the eval line and start a fresh shell.

cdp: requires bash 4 or newer — you're on macOS's default bash 3.2. Install bash 4+ (brew install bash) and exec it as your shell, or switch to zsh.

cdp: config file not found at ... — the config file doesn't exist yet. Run cdp add <label> <path> to bootstrap it; the parent directory will be created automatically.

cdp myproject runs but my cwd doesn't change — your shell's cdp is being shadowed by an alias or another function. Run type cdp; if it shows an alias, unalias cdp and re-source your rc file.

Status

v1.11.0 — cdp <label> exec <command>… runs an ad-hoc command in a project (a one-shot macro), with stdin restored to the terminal so interactive commands work; --save-macro=<name> also persists the command as a Macro and runs it (save-and-run), atomically re-verified before the config is replaced. Built on the v1.10.x cdp add -g / v1.9.x tab completion / v1.8.x cdp path / v1.7.x readable cdp ls block / v1.6.x Group Path root / v1.5.x cdp add ergonomics / v1.4.x Group blocks / v1.3.x cdp check / v1.2.x cdp edit / v1.1.x tmux-integration line. The roadmap and open items are tracked as GitHub issues; the formal grammar and protocol live under docs/specs/.

Contributing

See CONTRIBUTING.md.

License

MIT — see LICENSE.

About

Tiny bash CLI that replaces boring cd navigation

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages