Skip to main content
Shell integration unlocks Better PM’s full potential by enabling the pm cd command to actually change your shell’s working directory, rather than just printing a path.

Quick Setup

Add this line to your shell configuration file:
eval "$(pm activate zsh)"
Then reload your shell:
source ~/.zshrc  # or ~/.bashrc

What Gets Activated

The pm activate command outputs two things:
  1. Shell wrapper function - Intercepts pm cd to enable directory navigation
  2. Enhanced completions - Adds package name completions for pm cd

The Shell Wrapper

Here’s what the wrapper function does (from src/commands/activate.ts):
Shell wrapper (simplified)
pm() {
  if [ "$1" = "cd" ]; then
    # Special handling for pm cd
    shift;
    for arg in "$@"; do
      case "$arg" in
        -*) command pm cd "$@"; return;;  # If flags, just print
      esac;
    done;
    local dir;
    dir=$(command pm cd "$@");  # Get directory from CLI
    if [ $? -eq 0 ] && [ -d "$dir" ]; then
      builtin cd "$dir";  # Actually change directory
    fi;
  else
    command pm "$@";  # Pass through all other commands
  fi;
};
How it works:
1

Intercepts pm cd

When you run pm cd @myapp/web, the shell function catches it before the actual CLI runs.
2

Executes the CLI command

Calls command pm cd @myapp/web to get the package directory path from the Better PM CLI.
3

Changes directory

If the CLI succeeds and returns a valid directory, uses builtin cd to navigate there.
4

Handles flags correctly

If you pass flags like --completions, it just executes the command normally without changing directory.

Enhanced Completions

The activation also sets up intelligent completions that list your workspace packages when you type:
pm cd <TAB>
This shows all available workspace package names, making navigation much faster.

Zsh Integration

For zsh, the activation adds custom completion logic:
Zsh completions
eval "$(command pm --completions zsh)";
if (( $+functions[_pm_zsh_completions] )); then
  functions[_pm_zsh_completions_base]=$functions[_pm_zsh_completions];
  _pm_zsh_completions() {
    if [[ $words[2] == cd ]] && (( CURRENT == 3 )); then
      compadd -- ${(f)"$(command pm cd --completions 2>/dev/null)"};
    else
      _pm_zsh_completions_base "$@";
    fi;
  };
fi;
What this does:
  • First loads the base completions generated by Better PM’s CLI framework
  • Wraps the completion function to add custom behavior for pm cd
  • When completing the first argument after pm cd, calls pm cd --completions to get package names
  • For all other commands, delegates to the base completion function
The --completions flag is handled by src/commands/cd.ts and outputs one package name per line.

Bash Integration

Bash uses a similar but slightly different approach:
Bash completions
eval "$(command pm --completions bash)";
_pm_custom_completions() {
  if [[ "${COMP_WORDS[1]}" == "cd" ]] && [[ $COMP_CWORD -eq 2 ]]; then
    COMPREPLY=($(compgen -W "$(command pm cd --completions 2>/dev/null)" -- "${COMP_WORDS[$COMP_CWORD]}"));
    return;
  fi;
  _pm_bash_completions;
};
complete -F _pm_custom_completions -o nosort -o bashdefault -o default pm;
Key differences from zsh:
  • Uses COMP_WORDS array instead of zsh’s $words
  • Uses compgen for completion generation
  • Registers with the complete builtin instead of zsh’s completion system

Using pm cd

Once shell integration is active, you can navigate your workspace:
pm cd @myapp/web
# Now in: ~/my-monorepo/apps/web/

Without Shell Integration

If you haven’t activated shell integration, pm cd still works but only prints the path:
pm cd @myapp/web
# Prints: /home/user/my-monorepo/apps/web
# (but doesn't change directory)
You can use this with command substitution:
cd $(pm cd @myapp/web)
Or use the -p / --path flag explicitly:
cd $(pm cd -p @myapp/web)

Troubleshooting

Symptoms: Running pm cd @myapp/web prints the path but doesn’t navigate.Diagnosis:
  1. Check if the shell wrapper is loaded:
    type pm
    
    Should show: pm is a shell function (not pm is /path/to/pm)
  2. Verify activation line is in your shell config:
    grep "pm activate" ~/.zshrc  # or ~/.bashrc
    
Solution:
  • Add eval "$(pm activate zsh)" to your ~/.zshrc (or bash equivalent)
  • Reload: source ~/.zshrc
Symptoms: Typing pm cd <TAB> doesn’t show your workspace packages.Diagnosis:
  1. Test completions directly:
    pm cd --completions
    
    Should list all workspace package names.
  2. Check if you’re in a monorepo:
    pm pls
    
    Should list your packages. If not, you may not be in a workspace.
Solution:
  • Ensure you have workspace configuration (see Monorepo Setup)
  • Reload shell after adding activation line
  • For zsh, ensure compinit runs after the activation line
Symptoms: The shell wrapper interferes with other tools named pm.Solution: You can selectively disable the wrapper for specific commands:
command pm cd @myapp/web  # Bypass wrapper, just print path
\pm cd @myapp/web         # Alternative bypass syntax
Or rename the Better PM binary during installation if conflicts are severe.
Symptoms: Tab completion for pm cd takes several seconds.Cause: The CLI is being invoked synchronously to generate completions.Mitigation:
  • Install via Homebrew (recommended): Uses native binary, completions resolve in ~60ms
  • If using npm install: Completions invoke Node.js which is slower
  • Large monorepos (100+ packages) may still see slight delays
Note: This is listed in README as a known trade-off of the npm installation method.

Advanced Usage

Multiple Shells

If you use both zsh and bash, add activation to both config files:
~/.zshrc
eval "$(pm activate zsh)"
~/.bashrc
eval "$(pm activate bash)"
The pm activate command outputs shell-specific code, so each shell gets the correct syntax.

Custom Prompts

The shell wrapper doesn’t interfere with prompt customization. You can still use tools like:
  • Starship
  • Oh My Zsh themes
  • Powerline
  • Custom PS1/PROMPT variables
Just ensure the activation line comes before prompt setup in your config.

Integration with Directory Jumpers

Better PM works alongside tools like:
  • z / zoxide - Frecency-based jumping
  • autojump - Smart directory navigation
  • j - Jump to recent directories
Example workflow:
# Use pm cd for exact workspace navigation
pm cd @myapp/web

# Use z/zoxide for fuzzy navigation
z api  # Jump to ~/my-monorepo/apps/api if visited before

Scripting with pm cd

In scripts, use the --path flag to avoid shell wrapper complications:
Script example
#!/bin/bash

# Get package directory programmatically
WEB_DIR=$(pm cd --path @myapp/web)
cd "$WEB_DIR"

# Now in the package directory
npm run build
This works even without shell integration activated.

Performance Considerations

The shell wrapper adds minimal overhead:
  • Function check: ~0.1ms (checks if command is cd)
  • CLI invocation: 60ms (Homebrew) or 200-500ms (npm install)
  • Directory change: <1ms
Optimization tip: Use Homebrew installation for fastest performance:
brew install fdarian/tap/better-pm
The Homebrew version installs a native binary compiled from the Effect CLI framework, while npm installation runs through Node.js.

Source Code Reference

The shell integration is implemented in /src/commands/activate.ts (lines 6-66):
  • shellWrapper (lines 6-22): The bash/zsh function that intercepts pm cd
  • zshCompletions (lines 24-34): Zsh-specific completion enhancement
  • bashCompletions (lines 36-44): Bash-specific completion enhancement
  • activateCmd (lines 57-66): Command that outputs the combined activation code
The pm cd --completions flag is handled in /src/commands/cd.ts (lines 31-36).