// ZSH-BETTER-NPM-COMPLETION — ENGINEERING REPORT

#compdef npm pnpm · npm i <tab> from ~/.npm/ cache · npm uninstall <tab> from dependencies + devDependencies · npm run <tab> with script bodies

>_EXECUTIVE SUMMARY

zsh-better-npm-completion intercepts npm and pnpm tab-completion with three context-aware completers. npm install <tab> recommends from the local ~/.npm/ on-disk cache (or fires npm search with _retrieve_cache / _store_cache memoization when the prefix is 2+ chars). npm uninstall <tab> reads the nearest package.json via a walk-up directory search and emits the union of its dependencies + devDependencies keys. npm run <tab> emits every script in scripts alongside its literal body as the completion description (so the user sees what each script runs before pressing Enter). For anything else (npm publish, npm version, npm whoami, ...), it falls through to npm's own bundled completion. Total surface: 157 zsh lines — a 4-line entry + a 153-line _npm. Pinned by 46 @test blocks across 6 zunit files, including 4 package.json fixtures covering scripts-only, deps-only, mixed, and full shapes.

157
Zsh Lines
153
_npm Lines
46
@test Blocks (zunit)
6
Test Files
4
Fixture package.json
2
Compdef Targets (npm/pnpm)
3
Custom Completers
8
Helper Fns

~ARCHITECTURE

Smallest of the five MenkeTechnologies zsh-plugin reports — total surface is one tiny entry + one self-contained compdef file.

FileLinesRole
zsh-better-npm-completion.plugin.zsh 4 The smallest plugin entry in the umbrella. Runs the Zsh Plugin Standard 0= path header, then prepends ${0:h}/src to fpath. No aliases, no widgets, no setopt, no autoloadcompinit does the rest.
src/_npm 153 The entire completion. Starts with #compdef npm pnpm (single file registers both binaries). Top-level dispatcher _npm case-switches on $words[2]: i / install__zbnc_npm_install_completion; r / uninstall__zbnc_npm_uninstall_completion; run__zbnc_npm_run_completion; default → __zbnc_default_npm_completion (which shells out to npm completion for the canonical surface). Helper functions: __zbnc_npm_command_arg, __zbnc_recursively_look_for (walk-up dir search), __zbnc_get_package_json_property_object (sed-based JSON section extractor), __zbnc_get_package_json_property_object_keys, __zbnc_parse_package_json_for_script_suggestions, __zbnc_parse_package_json_for_deps.
Total source 157 2 files · zero external runtime deps

#TEST COVERAGE

46 @test blocks across 6 zunit files, plus 4 tests/fixtures/*.json mock package.json files driving the parser.

FileTestsPins
t-plugin.zsh21Plugin + completion contract pins. fpath augmentation uses ${0:h}/src; #compdef npm pnpm as the literal first line; the package.json parser correctly extracts keys from each of the 4 fixture files; npm install / uninstall / run dispatch to the correct internal completer based on $words[2].
t-syntax.zsh2zsh -n over plugin entry + _npm.
t-contract.zsh7Plugin shape pins: entrypoint stem matches dir basename; entrypoint parses; every _* file leads with #compdef; entry is shorter than 20 lines (catches accidental config-bloat); fpath augmentation runs at source time, not in a function.
t-contract2.zsh6fpath uses ${0:h}/src (plugin-manager portable); compdef target line includes both npm AND pnpm; default fallback calls __zbnc_default_npm_completion; ZSH Plugin Standard header present.
t-contract3.zsh5Zinit install path; nocompile ice honored; no compinit call inside the plugin (defer to the user's compinit).
t-contract4.zsh5OMZ install path; double-source is idempotent; no side effects on first source (no setopt, no autoload, no typeset).
Total466 zunit files · 4 package.json fixtures

Fixtures: tests/fixtures/scripts_only.json (a scripts block, no deps); tests/fixtures/prod_and_dev.json (split dependencies + devDependencies); tests/fixtures/scripts_and_deps.json (both); tests/fixtures/full.json (every npm package.json field).


/INTEGRATION

Zinit (recommended)

zinit ice lucid nocompile
zinit load MenkeTechnologies/zsh-better-npm-completion
nocompile is important — without it, ${0:h}/src would resolve to the compiled .zwc path instead of the on-disk src/.

npm + pnpm in one compdef

#compdef npm pnpm on line 1 of src/_npm. compinit registers the same _npm dispatcher for both binaries — pnpm install, pnpm uninstall, pnpm run all benefit from the same script-aware / dep-aware completion logic.

Walk-up package.json search

__zbnc_recursively_look_for "package.json" walks $PWD up to / looking for the file. Same behavior as npm itself — the completion works whether the user is in the package root or a deeply nested subdirectory.

Fallback to npm completion

Anything outside the 3 custom branches falls through to __zbnc_default_npm_completion, which shells out to npm completion -- "${words[@]}". The user never loses npm's bundled completion behavior for subcommands the custom path doesn't handle.

Cache memoization on npm search

Each npm search $PREFIX result is keyed as npm_${PREFIX}_cache via _retrieve_cache / _store_cache. First TAB against a 2+ char prefix fires the search; subsequent TABs on the same prefix hit the cache. TTL is governed by zstyle ':completion::complete:*' use-cache yes.

No compinit inside the plugin

Deliberately. compinit is the user's responsibility (and most modern plugin managers handle it). The plugin just adds to fpath and lets the user-level compinit pick up _npm on the next prompt.


!DESIGN DECISIONS

sed JSON parser, not jq dependency

__zbnc_get_package_json_property_object uses two sed calls to extract the scripts / dependencies / devDependencies blocks. Costs the plugin some robustness against pathological JSON formatting (one-line collapsed JSON would defeat it), gains it zero external-dependency installs — the plugin works in CI sandboxes that have only npm + zsh and nothing else. The 4 fixture files in tests/fixtures/ pin the parser against the common shapes.

Script body as completion description

npm's bundled completion shows script names. zsh-better-npm-completion shows "name — literal body" via _describe. npm run build <tab> reveals what the script actually runs (e.g. build — webpack --mode production) BEFORE the user invokes it — catches the "which one is the linter, which one is the typechecker" question every time.

Cache prefix length floor

(( $#PREFIX >= 2 )) — below 2 chars, the install completer skips npm search entirely and only emits the ~/.npm/ cache directory contents. Prevents 50k+ candidate dumps on npm i <tab> with no prefix.

Flag-prefix guard

[[ $PREFIX == -* ]] && return at the top of __zbnc_npm_install_completion. When the user is mid-flag (npm i --save-d), the completer doesn't try to interpret the flag as a package name and doesn't fire npm search. Falls through to default.

Union of dependencies + devDependencies

npm uninstall completion offers BOTH prod and dev deps. Users don't have to remember which list a package came from — npm uninstall <tab> shows every dep that could plausibly be removed.

Tiny plugin entry — 4 lines

The entry doesn't even define functions. The compsys file under src/_npm is fully self-contained. Sourcing the plugin is constant-time regardless of npm install size; the heavy work happens inside compinit when the user actually requests completion.


@FOOTPRINT