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:
- Shell wrapper function - Intercepts
pm cdto enable directory navigation - 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, callspm cd --completionsto 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_WORDSarray instead of zsh’s$words - Uses
compgenfor completion generation - Registers with the
completebuiltin 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:
-
Check if the shell wrapper is loaded:
type pmShould show:
pm is a shell function(notpm is /path/to/pm) -
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"> shell config>
Symptoms: Typing pm cd <TAB> doesn’t show your workspace packages.
Diagnosis:
-
Test completions directly:
pm cd --completionsShould list all workspace package names.
-
Check if you’re in a monorepo:
pm plsShould 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
compinitruns after the activation line
< printing a p title="Conflicts with other pm tools"> printing a p>
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 syntaxOr 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 jumpingautojump- Smart directory navigationj- 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 interceptspm cdzshCompletions(lines 24-34): Zsh-specific completion enhancementbashCompletions(lines 36-44): Bash-specific completion enhancementactivateCmd(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).