A brand-neutral dependency-inventory core + CLI for JS/TS monorepos. Scan every workspace's declared dependencies, enrich them with the latest published version, flag what's genuinely outdated, get a one-line recommendation for each, and export the whole thing to CSV.
npm i -D @joinhottub/depwatch # or: pnpm add -D @joinhottub/depwatch
npx depwatch --outdated # the installed binary is `depwatch`NAME DECLARED LATEST STATUS
@types/node ^20.11.0 22.10.5 Match your runtime
react ^18.2.0 19.0.0 Hold — plan a migration
tsup ^7.0.0 8.3.5 Bump when convenient
zod ^3.22.0 3.24.1 Safe update
42 dependencies · 4 with a newer version available
Most "outdated" tools answer "is there a newer version?" — which floods you with noise, because a newer version inside your declared range is not something you need to act on. depwatch answers the more useful question: "is there an update that sits outside what an install would already pull, and what should I do about it?" It's semver-aware, monorepo-aware, and gives you a reason for every recommendation instead of a wall of red.
It's also deliberately small and dependency-light (one runtime dep: semver),
with a clean data-in/data-out core you can build your own dashboard or cron on top
of.
depwatch [rootDir] [options]
rootDir Repo root to scan (default: current directory)
--csv Output RFC-4180 CSV instead of a table
-o, --out <file> Write to a file instead of stdout
--outdated Only show deps with a newer version outside the declared range
--offline Skip the npm registry (no network)
--concurrency <n> Parallel registry lookups (default: 12)
-h, --help Show help
Workspaces are auto-detected from package.json "workspaces" (npm/yarn/bun) and
from pnpm-workspace.yaml. With no config it falls back to apps/* + packages/*.
Everything is plain functions over plain data — no globals, no config files, no network unless you ask for it.
import {
scanWorkspaceDependencies,
fetchLatestVersion,
isOutdated,
recommend,
dependenciesToCsv,
type EnrichedDependency,
} from '@joinhottub/depwatch';
// 1. Pure filesystem scan — no network.
const records = scanWorkspaceDependencies(process.cwd());
// 2. Enrich with registry data (fail-soft: a blip returns null, never throws).
const enriched: EnrichedDependency[] = await Promise.all(
records.map(async (r) => {
const latestVersion = r.internal ? null : await fetchLatestVersion(r.name);
return { ...r, latestVersion, outdated: isOutdated(r.ranges, latestVersion) };
}),
);
// 3. Decide what to do, and serialize.
for (const dep of enriched) {
const { level, action, why } = recommend(dep);
console.log(dep.name, level, action, '—', why);
}
const csv = dependenciesToCsv(enriched);The core is intentionally just the reusable, brand-neutral bits. Persistence (a DB table), scheduling (a cron), auth, and any dashboard UI are deliberately left to the host application so this stays a small, embeddable library rather than a framework.
| Export | What it does |
|---|---|
scanWorkspaceDependencies(root, opts?) |
De-duplicated dependency inventory across the workspace. |
detectWorkspaceGlobs(root) |
The repo's workspace globs from package.json / pnpm-workspace.yaml. |
fetchLatestVersion(name, opts?) |
Best-effort latest published version (fail-soft → null). |
isOutdated(ranges, latest) |
Does latest satisfy none of the declared ranges? |
recommend(dep) |
Rule-based { level, action, why } for a dependency. |
dependenciesToCsv(rows) |
RFC-4180 CSV with proper escaping. |
fetchLatestVersion takes an injectable fetchImpl, so it's trivial to unit-test
or point at a private registry.
isOutdated(ranges, latest) is true only when a valid latest exists and it
satisfies none of the declared ranges. So ^1.2.0 with latest 1.9.0 is not
outdated (an install already gets it); with latest 2.0.0 it is. Workspace
(workspace:*) deps and unparseable ranges are never outdated.
The recommend() rules then add judgment: runtime majors → hold and plan a
migration; dev-only majors → bump when convenient; @types/* → match your
runtime, not the newest types; minor/patch ahead → safe update.
MIT © Hottub, Inc.