I try to keep packages up to date with topgrade—the tool that updates everything on your system with one command. Every time it runs, there’s a section for npm packages that vomits a wall of yellow warnings:
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory.
npm warn deprecated har-validator@5.1.5: this library is no longer supported
npm warn deprecated glob@6.0.4: Glob versions prior to v9 are no longer supported
npm warn deprecated cross-spawn-async@2.2.5: cross-spawn no longer requires a build toolchain
npm warn deprecated @oclif/command@1.8.36: Package no longer supported.
npm warn deprecated uuid@3.4.0: Please upgrade to version 7 or higher.
...
changed 923 packages in 18s
Eighteen warnings. Zero context about which project they came from. Is it a global package? A transitive dependency three levels deep? Something I actually installed? No idea.
I thought: there must be a better way to do this. Searched around. Found nothing good. So I built one.
This is Bishop—a lazygit-style TUI for npm package management. (The name comes from my sci-fi naming theme—Bishop is the android from Aliens. Holly, the presence sensor, is the AI from Red Dwarf.) It shows your packages in a terminal interface with color-coded update status, vulnerability indicators, dependency trees, and script runners. Built in about two hours because the tool I wanted didn’t exist.
Why This Exists
The problem: Keeping npm packages updated is either tedious (manually running npm outdated, then npm update, then checking each package) or opaque (letting topgrade handle it and wondering what just happened).
Existing solutions didn’t fit:
- lazynpm: The obvious answer—except it’s been abandoned since April 2020. Last commit was over six years ago. It also requires Git context (won’t work outside a Git repo) and doesn’t support Windows.
- npm-check-updates (ncu): CLI-only, no interactive interface. Great for scripting, not for exploring.
- taze: Similar to ncu—a CLI tool, not a TUI. No visual browsing experience.
- npm itself:
npm outdatedgives you a table.npm updatedoes things. No visibility into what’s happening.
The gap: There’s no modern, actively maintained TUI for npm package management. The lazygit/lazydocker pattern works beautifully for Git and containers—why not for packages?
The goal: A lazygit-style experience for npm. See all your packages, their versions, update status, vulnerabilities. Navigate with keyboard, update with a keypress, drill into dependency trees when needed.
The Plan
Here’s the implementation plan I gave Claude. The research phase took longer than implementation—figuring out what already existed and why it wasn’t good enough.
Plan: encapsulated-toasting-dove.md
npm-explorer Implementation Plan
Overview
A modern TUI for npm package management built with TypeScript and Ink (React for terminals). Fills the gap left by abandoned lazynpm with multi-package-manager support.
Tech Stack
| Tool | Purpose |
|---|---|
| Ink 6.x | React-based TUI framework |
| @inkjs/ui | Pre-built UI components (Select, Spinner, TextInput) |
| Zustand | Lightweight state management |
| Fuse.js | Fuzzy search |
| tsup | Fast TypeScript bundler |
| execa | Shell command execution |
Project Structure
npm-explorer/
├── package.json
├── tsconfig.json
├── tsup.config.ts
├── bin/
│ └── cli.js # npx entry point (shebang)
└── src/
├── cli.tsx # CLI args + app bootstrap
├── app.tsx # Main app, view routing, keyboard
├── core/
│ ├── types.ts # Package, OutdatedInfo, Vulnerability
│ ├── detector.ts # Auto-detect package manager
│ ├── registry.ts # npm registry API client
│ └── adapters/
│ ├── base.ts # IPackageManager interface
│ ├── npm.ts
│ ├── pnpm.ts
│ ├── yarn.ts
│ └── bun.ts
├── hooks/
│ ├── usePackages.ts
│ ├── useOutdated.ts
│ ├── useAudit.ts
│ └── useDependencies.ts
├── store/
│ └── index.ts # Zustand store (UI + package state)
├── components/
│ ├── Layout.tsx # Header + content + footer
│ ├── PackageList.tsx # Scrollable list
│ ├── PackageRow.tsx # Single row with status
│ ├── DetailPanel.tsx # Package info sidebar
│ ├── DependencyTree.tsx # ASCII tree
│ ├── SearchInput.tsx # Fuzzy filter
│ ├── StatusBar.tsx # Keybindings help
│ └── ConfirmDialog.tsx # Update confirmation
├── views/
│ ├── PackagesView.tsx # Main view
│ ├── DependencyView.tsx # Tree drill-down
│ ├── SecurityView.tsx # npm audit
│ └── ScriptsView.tsx # Script runner
└── utils/
├── colors.ts # patch/minor/major coloring
├── format.ts # Size, version formatting
└── tree.ts # ASCII tree rendering
Core Interfaces
// src/core/types.ts
interface Package {
name: string;
version: string;
description?: string;
license?: string;
homepage?: string;
repository?: string;
size?: number;
isDev: boolean;
isPeer: boolean;
}
interface OutdatedInfo {
name: string;
current: string;
wanted: string;
latest: string;
type: 'patch' | 'minor' | 'major';
}
interface Vulnerability {
name: string;
severity: 'low' | 'moderate' | 'high' | 'critical';
fixAvailable: boolean;
}
// src/core/adapters/base.ts
interface IPackageManager {
name: 'npm' | 'pnpm' | 'yarn' | 'bun';
list(global?: boolean): Promise<Package[]>;
outdated(global?: boolean): Promise<OutdatedInfo[]>;
audit(): Promise<Vulnerability[]>;
dependencies(pkg: string): Promise<DependencyNode>;
update(pkg: string, version?: string): Promise<void>;
scripts(): Promise<Record<string, string>>;
runScript(name: string): Promise<void>;
}
Key Decisions
- Adapter pattern for package managers—easy to add new ones
- Zustand for state—minimal boilerplate, works outside React
- tsup for building—zero-config, fast, handles ESM
- Start with npm adapter—most common, pattern for others
Implementation Phases
- Project Setup: Initialize package.json, tsconfig, tsup, bin entry point
- Core Infrastructure: Types, detector, npm adapter, CLI entry
- Basic TUI Shell: Main app with Layout, StatusBar, Zustand store
- Package Listing: usePackages/useOutdated hooks, PackageList, color coding
- Search & Details: Fuse.js integration, DetailPanel, keyboard nav
- Updates: Update action, ConfirmDialog, wire up flow
- Dependency Tree: useDependencies hook, tree components
- Security & Scripts: useAudit hook, SecurityView, ScriptsView
- Other Package Managers: pnpm, yarn, bun adapters
- Polish: Monorepo detection, global packages, error handling
TUI Mockup
┌─────────────────────────────────────────────────────────────────┐
│ npm-explorer [npm] my-project │
├─────────────────────────────────────────────────────────────────┤
│ Filter: _ │
├───────────────────────────────────────┬─────────────────────────┤
│ │ │
│ ▸ express 4.18.2 → 4.21.0 ⬆ │ express v4.18.2 │
│ lodash 4.17.21 ✓ │ │
│ typescript 5.3.3 → 5.7.2 ⬆ │ Fast, unopinionated, │
│ react 18.2.0 → 19.0.0 ⬆ │ minimalist web │
│ eslint 8.56.0 → 9.0.0 ⬆ │ framework for Node.js │
│ │ │
│ │ License: MIT │
│ │ Size: 210 KB │
│ │ │
│ │ [o] Open npm │
│ │ [g] Open GitHub │
├───────────────────────────────────────┴─────────────────────────┤
│ [u]pdate [d]eps [a]udit [s]cripts [/]search [q]uit │
└─────────────────────────────────────────────────────────────────┘
How Claude Code Actually Worked
This was one of the smoothest builds I’ve done. Research took longer than implementation—about 90 minutes figuring out what already existed (lazynpm: dead since 2020, npm-check-updates: CLI only, taze: no TUI) before I was confident there was actually a gap to fill.
Once I described what I wanted—“lazygit but for npm packages”—Claude understood immediately. The TUI mental model is well-established. Architecture took maybe 15 minutes: adapter pattern for multi-PM support, Ink for the framework, Zustand for state. Then breakfast.
The actual coding? Eight minutes from first file to working TUI. My reaction: “Why has no one else done this?” The Ink ecosystem is that good—pre-built components meant most of the UI was assembly, not invention. First build was 80% there. Claude reviewed its own work, fixed edge cases, and we were done.
One catch: comparing the plan against what shipped, I noticed the changelog viewer was missing. Claude had suggested it during planning, I’d agreed it was useful, but it didn’t make it into the initial build. Added it. Classic oversight.
Total: ~2 hours. Research and planning: 90 minutes. Implementation: 36 minutes. Very little steering required—Opus 4.5 just handled it.
Features Built
Package listing with outdated detection: This is the core feature I use most. Shows all dependencies with current vs. latest versions. Color-coded by update type:
- Green (patch): Safe updates, just bug fixes
- Yellow (minor): New features, should be compatible
- Red (major): Breaking changes, review before updating
Security audit view: Runs npm audit and displays vulnerabilities with severity indicators. Click into any vulnerability to see affected packages and available fixes.
Dependency tree visualization: Pick any package and see its full dependency tree. Useful for answering “why do I have this transitive dependency?” (Turns out that’s where most of those topgrade warnings come from—transitive deps I never directly installed.)
Scripts runner: Lists all scripts from package.json, run any with a keypress. No more npm run <tab><tab> to remember script names.
Multi-PM support: Adapter pattern means adding package managers is straightforward. Currently supports:
- npm (primary, most tested)
- pnpm
- yarn
- bun
Global packages toggle: Switch between project dependencies and globally installed packages with g.
Results
Build time: 2 hours total (90 minutes research/planning, 36 minutes implementation)
First run: Worked immediately. Pointed it at this project (daybuilds.ai), listed all 30 dependencies with correct version info and update status.
Already using it: I ran Bishop on this site’s dependencies while writing this article. Found two packages with available patches, updated them. This’ll be a regular part of my workflow now—checking project packages (including the ones we build at work). It’s just so much easier than the npm CLI.
What’s different from my imagined workflow:
Before Bishop: Run npm outdated, scan the table, run npm update package-name individually, check if anything broke.
With Bishop: Open the TUI, arrow to a package, press u to update, see the new version appear. Repeat. Press a to check if I introduced any new vulnerabilities. Done.
Limitations:
- No monorepo support yet (doesn’t detect workspaces)
- Package search from npm registry not implemented
- Windows untested (should work, Ink is cross-platform)
What’s next: Nothing. It works for my use case—managing packages without context-switching to the browser or memorizing npm commands. If I need monorepo support, I’ll add it. Until then, it’s done.
The best part? Next time topgrade throws that wall of deprecation warnings at me, I can actually figure out where they’re coming from.
Tech stack: TypeScript, Ink 6.x, Zustand, Fuse.js, tsup Build time: 2 hours (planning: 90 min, implementation: 36 min) Lines of code: ~2,000 across 25 modules