// ZSH-BETTER-NPM-COMPLETION — CACHE-AWARE INSTALL + DEP-AWARE UNINSTALL

zsh plugin · cache-aware completion for npm install · dep-aware completion for npm uninstall · falls back to default npm completion

GitHub Issues
// Color scheme

>_ZSH-BETTER-NPM-COMPLETION

Better completion for npm. npm install <TAB> recommends from npm cache. npm uninstall <TAB> recommends from dependencies / devDependencies. npm run <TAB> shows the script body. Falls back to the default npm completion when none of the above applies.

Install

# Zinit
zinit ice lucid nocompile
zinit load MenkeTechnologies/zsh-better-npm-completion

# Oh My Zsh
git clone https://github.com/MenkeTechnologies/zsh-better-npm-completion \
    "$HOME/.oh-my-zsh/custom/plugins/zsh-better-npm-completion"
# then: plugins+=(zsh-better-npm-completion)

# Antigen
antigen bundle MenkeTechnologies/zsh-better-npm-completion

# Manual
git clone https://github.com/MenkeTechnologies/zsh-better-npm-completion ~/.zsh-better-npm-completion
source ~/.zsh-better-npm-completion/zsh-better-npm-completion.plugin.zsh

Overview

The plugin is two files. zsh-better-npm-completion.plugin.zsh is a 4-line entry that runs the Zsh Plugin Standard 0= path header, then prepends ${0:h}/src to fpath — nothing else. src/_npm is a 153-line compsys completion file beginning with #compdef npm pnpm, so the same completer is registered for both the npm and pnpm binaries.

The top-level _npm dispatcher inspects $words[2] (the npm subcommand) and routes to one of three custom completers. Anything it does not recognise falls through to npm's own bundled completion, so you never lose default behaviour.

$words[2]routed to
i / install__zbnc_npm_install_completion
r / uninstall__zbnc_npm_uninstall_completion
run__zbnc_npm_run_completion
anything else__zbnc_default_npm_completion (shells out to npm completion)

Usage

install from cachenpm install <TAB>
install search (prefix ≥ 2 chars)npm i ab<TAB>
uninstall from package.jsonnpm uninstall <TAB>
run a script (shows body)npm run <TAB>
pnpm (same completer)pnpm run <TAB>
everything elsefalls through to default npm completion

No configuration. Source the plugin, restart zsh (or re-run compinit), hit TAB.

Reference

npm install <TAB>__zbnc_npm_install_completion

  • If the current word starts with - (you are mid-flag, e.g. npm i --save-d), the completer returns immediately and the default completion handles flags.
  • When the prefix is 2 or more characters ((( $#PREFIX >= 2 ))), it runs npm search --no-progress $PREFIX and offers the matching package names with their descriptions via _describe -t npm-search. Results are memoised under the cache key npm_${PREFIX}_cache using zsh _retrieve_cache / _store_cache, so a repeated TAB on the same prefix does not re-hit the network.
  • It also always offers the contents of your local ~/.npm/ cache directory ("$HOME/".npm/*(/:t)) through _wanted npm-cache … compadd -Q — packages you have installed before, available even offline.

npm uninstall <TAB>__zbnc_npm_uninstall_completion

  • If the third word is -g or --global, it returns and lets the default completion list global modules.
  • Otherwise it walks up from $PWD toward / looking for the nearest package.json (__zbnc_recursively_look_for package.json). If none is found it returns.
  • It then offers the union of the dependencies and devDependencies keys from that file via _values — every package you could plausibly remove, regardless of which list it lives in.

npm run <TAB>__zbnc_npm_run_completion

  • Only fires on exactly npm run ? ((( $#words != 3 )) returns early).
  • Walks up for the nearest package.json; returns if none is found.
  • Parses the scripts block and offers each script name with its literal command body as the completion description (_describe), so you see what a script runs before pressing Enter.

Fallback — __zbnc_default_npm_completion

For every other subcommand the dispatcher shells out to npm's own completion engine: npm completion -- "${words[@]}" with COMP_CWORD / COMP_LINE / COMP_POINT set, feeding the result back through compadd. Nothing npm already completes is lost.

Helper functions

__zbnc_npm_command_argechoes $words[3] (used to detect -g/--global on uninstall)
__zbnc_recursively_look_forwalks $PWD up to / looking for a named file
__zbnc_get_package_json_property_objectextracts a top-level JSON object block via two sed passes, emits key=>value
__zbnc_get_package_json_property_object_keyssame, then cut -f1 -d= to keep only the keys
__zbnc_parse_package_json_for_script_suggestionsturns the scripts block into name + body completion candidates
__zbnc_parse_package_json_for_depsemits the keys of dependencies + devDependencies

Configuration

There is no plugin-specific configuration. Two stock zsh mechanisms affect behaviour:

  • Completion cache. The npm search memoisation goes through zsh's standard _retrieve_cache / _store_cache, which obey zstyle ':completion:*' use-cache yes and zstyle ':completion:*' cache-path ~/.zcompcache. Enable caching there to persist results across shell sessions.
  • compinit. The plugin does not call compinit itself — it only adds src/ to fpath. Your .zshrc (or your plugin manager) must run compinit for _npm to be picked up.

Examples

# Offer cached packages plus a live search for the "exp" prefix
npm install exp<TAB>
#   express  expand-tilde  expect  ...   (from npm search, cached as npm_exp_cache)
#   + everything already in ~/.npm/

# Remove a dependency without remembering whether it was prod or dev
npm uninstall <TAB>
#   react  react-dom  typescript  @types/node    (deps + devDeps, unioned)

# See what each script actually runs before invoking it
npm run <TAB>
#   build  -- tsc -p .
#   test   -- jest

# pnpm benefits from the same completer (#compdef npm pnpm)
pnpm run <TAB>

Troubleshooting

  • No completions appear. Confirm compinit runs after the plugin loads and that src/ is on fpath (echo $fpath | tr ' ' '\n' | grep zsh-better-npm-completion).
  • Zinit users: use the nocompile ice. Without it, ${0:h}/src can resolve to the compiled .zwc path instead of the on-disk src/ directory.
  • npm run shows nothing. The scripts block is parsed with sed; a single-line / collapsed package.json defeats the line-based extractor. The standard npm-formatted (multi-line) shape works.
  • Search returns nothing. Search only fires for prefixes of 2+ characters; below that you get only the ~/.npm/ cache directory contents.

Sibling plugins

Part of the MenkeTechnologies zsh plugin family — the MenkeTechnologiesMeta umbrella: