Skip to content
Better PM
Esc
↑↓navigate↵open⌘Jpreview

Shell Integration

Enable directory navigation and enhanced completions with Better PM shell integration

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)"
eval "$(pm activate bash)"

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):

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:

Intercepts pm cd

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

Executes the CLI command

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

Changes directory

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

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:

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

Bash Integration

Bash uses a similar but slightly different approach:

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/
pm cd
# Now in: ~/my-monorepo/
pm cd @my<TAB>
# Completes to: pm cd @myapp/
# Continue: pm cd @myapp/w<TAB>
# Completes to: pm cd @myapp/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

pm cd doesn't change directory

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

  • < shell config title="Completions don't show package names">

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

  • < printing a p title="Conflicts with other pm tools">

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.

Slow completions in large monorepos

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:

eval "$(pm activate zsh)"
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:

#!/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).

Was this page helpful?