Surgical one-by-one
Every upgrade is its own operation: bump → install → validate. If a step fails, package.json, the lockfile and any catalog file are put back exactly as they were. No mass updates, no mystery regressions.
Upgrade npm, pnpm, yarn & bun deps one-by-one — with pre-flight + install + validation, automatic rollback, a peer-range resolver that backtracks through the packument (SAT / AC-3 for big graphs), --security-only with minimal advisory fixes + transitive overrides, outdated lockfile-aware checks, preserved ^ / ~ ranges, --fix-lockfile dedupe, breaking-change radar, policy-as-code, atomic git commits, auto-opened PRs, and undo + doctor subcommands.
▸ framework-agnostic · peerDeps graph · peer resolver · monorepo-ready · ci-mode · security-first · outdated · preserves ^/~ · policy-as-code · auto-PR · undo · doctor · bun · catalogs · node 22.22+ / 24.15+ / 26+

A real session: plan → install → validate → rollback on trouble. Hit refresh to replay the surgery.
Peel, slap onto your CI pipeline, press firmly. Thirty-five capabilities in one binary — workspace-aware, git-aware, CI-aware, policy-aware, security-aware, PR-aware, and with outdated, undo + doctor subcommands for check-first workflows, disaster recovery, and pre-flight diagnostics. No plugins, no bolt-ons, no framework lock-in.
Every upgrade is its own operation: bump → install → validate. If a step fails, package.json, the lockfile and any catalog file are put back exactly as they were. No mass updates, no mystery regressions.
With --link-groups auto, the tool reads each package's published peerDependencies and builds a graph. Connected components become one batch → one write → one install → one validation. @types/* pair with their runtime automatically.
Before mutating any dependency, the resolved validator runs once on the unchanged tree. If the project build was already broken, the run aborts with the validator command, exit code, and last ~40 lines — instead of pretending every rollback is a real conflict. It also records the exit code of your lint / typecheck scripts, so an upgrade that breaks linting while the build still passes gets rolled back.
A regex-based parser classifies peer mismatches, missing peers, engine mismatches, and unresolved trees — from real npm / pnpm / yarn / bun output. Engine warnings and routine peer warnings never roll back an install that succeeded, and root-only mentions are filtered so your own app never looks like a conflict. After every install it also checks peer ranges between installed direct dependencies — npm with a lockfile and bun don't — and hands new conflicts to the peer resolver.
--workspaces traverses the root + every member; --workspace <names> picks specific packages. Install + validation run from the workspace root so the lockfile is always coherent. --install-mode filtered uses npm install --workspace or pnpm/bun --filter; yarn keeps a full install, because yarn workspaces focus doesn't write yarn.lock.
--concurrency up to 16 overlaps registry scan + plan across workspace targets while a shared mutex keeps install + validation strictly serialized — your shared lockfile is never raced. An in-process manifest cache deduplicates pacote calls across the whole run.
@latest first, then walk back: major-lines (default) tries the best stable per major; minor-lines steps by major.minor. Linked groups get the same idea after peer-resolve fails — members step back release lines together (bounded). ERR_REQUIRE_ESM halts further tries for that package. Set none to lock to @latest only.
Detects npm / pnpm / yarn / bun from the packageManager field (or devEngines.packageManager), then the lockfile (including bun.lock / bun.lockb and npm-shrinkwrap.json — a yarn.lock next to bun.lock is bun's own), then pnpm-workspace.yaml. The chosen manager drives both install and the default validator (<mgr> test → <mgr> run build; bun uses bun run test / bun run build). The frozen-lockfile defaults pnpm and Yarn Berry switch on under CI are turned off for its installs. A second lockfile the run won't update gets a warning up front.
--interactive splits prompts by phase. Linked-group conflict: skip group / retry / force / freeze-all. Single-package failure: continue / pin / retry. After the run, optionally bulk-add every failed name to the .dep-up-surgeonrc ignore list so the next pass converges. Ctrl+C at a prompt stops the run and rolls back the step in progress; prompts stay off with --json and --ci.
--json emits one structured object on stdout — progress, warnings and errors go to stderr, so it always parses: upgraded, skipped, failed, conflicts, groups, project, targets, installMode, concurrency, commits. Real runs persist it to node_modules/.cache/dep-up-surgeon/last-run.json — out of git's sight — for CI dashboards, --retry-failed and undo; --dry-run never overwrites it.
Reads the persisted last-run report and freezes successes plus terminal failures (peer, validation-script) per workspace (workspace::name). Only non-terminal residue is retried: install, validation-conflicts, versions, unknown. A freeze in one member does not skip the same package in another. Retries chain — a retry keeps the previous retry's freezes.
--summary md|html renders Upgraded / Failed tables, pre-flight status, ignored list, and target counts. On GitHub Actions it's appended to $GITHUB_STEP_SUMMARY automatically — perfect for PR-bot diagnostics without extra workflow plumbing.
--git-commit lands every successful upgrade as its own atomic commit (per-success / per-target / all). Only stages package.json + the lockfile (+ pnpm-workspace.yaml when a catalog: entry moved) — never git add -A — and skips lockfiles you gitignore. A commit refused by a hook is unstaged so it never rides along with the next one. Override pins and the --fix-lockfile dedupe land in one extra commit, so the branch matches the report. Pair --git-branch + --git-sign for review-ready PR-bot runs.
--ci disables --interactive, auto-enables --summary md, and remaps the exit code: per-package failures stay green so your bot's PR carries the diagnostic. Pre-flight + fatal errors still exit 1 — those mean a human needs to look.
Every install attempt snapshots package.json, the lockfile and any catalog file; a failure puts them back byte-for-byte, formatting included. Ctrl+C / SIGTERM undoes the step in progress while upgrades that already passed validation stay. Even when the installer exits 0 but reports real peer conflicts, the bump is rolled back — unless you --force it. A failed git commit never rolls back the upgrade. If the reinstall after a rollback fails, the run stops. At the end, tracked files inside the project that the run rewrote (tsconfig.json from next build…) are restored, after a backup copy goes to node_modules/.cache/dep-up-surgeon/side-effects/ — only package.json and the lockfile stay changed.
No hardcoded React / Angular / Vue / Expo lists. Grouping and conflict handling come from registry metadata and parsed installer output. Custom batches via .dep-up-surgeonrc linkedGroups when the registry graph is invisible.
--security-only flips the tool into CVE-driven mode. It runs npm / pnpm / yarn / bun audit (yarn npm audit on Berry), filters by --min-severity, then upgrades only the vulnerable names — preferring the lowest published version outside every vulnerable range, never a downgrade, before climbing toward @latest. A failed audit exits 1 instead of looking clean. Every bump carries its advisory ID + severity into commit subjects ([security:high]) and into a dedicated Security fixes table in --summary. Advisories on transitive packages are listed with a fix hint.
--apply-overrides fixes vulnerable transitives no direct bump could reach. With npm it first tries npm update <name> and skips the pin when the refreshed lockfile is clean; otherwise it writes overrides (npm 8.3+, bun), pnpm overrides (pnpm-workspace.yaml or package.json, wherever yours live), or resolutions (yarn). --override accepts manual parent-scoped pins (some-dep>foo@1.2.3 / parent/child@1.2.3) and works standalone. Install + validator run after each pin; a failure restores package.json, pnpm-workspace.yaml and the lockfiles exactly, so a pin you already had survives.
When a linked-group bump hits a peer conflict, the resolver reads every member's published peerDependencies and backtracks to the newest tuple that satisfies them all. Graphs of 10+ members switch to a SAT-style AC-3 solver; single-package failures synthesize an ad-hoc group from direct-dep blockers — never ones you ignored or froze, and never below their installed version. --no-resolve-peers restores the old fail-fast behavior.
dep-up-surgeon undo replays the last-run report in reverse: writes every recorded from back to package.json, drops (or restores) every override it added, newest first, restores the exact pre-run lockfile, then runs install + validator so you see green/red before you commit. Drift-protected — deps and pins that moved since the run are skipped as 'drifted' instead of being rewritten.
dep-up-surgeon doctor is a read-only green / yellow / red pre-flight: Node engine match, manager resolution, lockfile parse, dependencies installed, workspace coherence, policy parse, validator pre-flight, peer-dep scan (npm / yarn classic — it never runs an install), audit severity breakdown, and stale-transitive scan. Exit 2 on any red, 1 on yellow under --strict. The CI gate you run before trusting the upgrade loop.
--fix-lockfile runs `npm dedupe` / `pnpm dedupe` / `yarn dedupe` (berry only) to collapse redundant copies without touching package.json, and flags transitives more than a minor or full major behind latest (top 250 by installed-copy count). Lockfile is snapshotted first — restored on dedupe or post-dedupe validator failure. Yarn classic and bun → skipped:"unsupported"; missing lockfile → skipped:"no-lockfile".
In an isolated-lockfile monorepo — a folder of independent projects, each with its own lockfile and no shared workspace root — installs + validation run in parallel too: the keyed async mutex unlocks different-dir operations while same-dir ones still serialize. A shared root doesn't count: npm, yarn and bun install from a root workspaces field, and pnpm install still installs every project under pnpm-workspace.yaml, even with shared-workspace-lockfile=false. Reported as project.isolatedLockfiles + top-level parallelInstalls in --json. --no-parallel-installs forces the serialized path.
Every fetched changelog is scanned in full — plus the first release of each crossed major — for breaking-change markers: Conventional-Commit footers, 💥 / ⚠ BREAKING emoji, Node-version drops, public API removals. Negated mentions ("no breaking changes"), empty Breaking Changes headings and internal cleanups are ignored. Matches add a [breaking] tag to the commit subject, a Breaking changes detected section to the body, and a prominent banner above the --summary upgraded table.
Before you hand the PR to a reviewer, --blast-radius scans your source (.ts/.tsx/.js/.jsx/.mjs/.cjs/.mts/.cts/.vue/.svelte/.astro) to list which files actually import each upgraded package. Word-boundary safe, and @types/x counts for files importing x. Surfaces as collapsible per-package lists in --summary and upgraded[].blastRadius in --json.
Drop a .dep-up-surgeon.policy.yaml in the repo and encode upgrade rules that survive runs: freeze patterns (wildcards like @types/* supported), maxVersion semver caps, allowMajorAfter date gates, plus requireReviewers / autoMerge metadata keyed by major / minor / patch for your PR bot. Caps hold everywhere a version is picked: single bumps, linked groups, fallbacks, the peer resolver and --dry-run plans. Every applied rule appears in --summary and under policy.* in --json.
Every successful upgrade is annotated with release notes (GitHub Releases API first — one request per package, package-specific tags before v<version> so monorepo packages get their own notes — then the package's CHANGELOG.md from its tarball). Embedded in commit bodies and rendered as collapsible <details> blocks in --summary, with @mentions and issue refs wrapped in backticks so nobody gets pinged. Set GITHUB_TOKEN / GH_TOKEN to lift the API rate limit from 60/h to 5000/h; if GitHub starts refusing, lookups stop for the rest of the run.
--open-pr closes the loop: after --git-commit --git-branch pushes the branch, it pipes the --summary markdown to gh pr create --body-file -. Reuses an open PR for the same head (merged or closed ones get a fresh PR), trims oversized bodies to GitHub's limit, and works with GitHub Enterprise remotes. --open-pr-draft for merge-queue safety, --open-pr-reviewers / --open-pr-assignees / --open-pr-base for team wiring. Refuses to push the PR's base branch. Never fatal — a missing gh never aborts the run.
TTY-aware spinner (⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏) with (Ns) / (NmNs) elapsed timer on the pre-flight, single-package, and linked-group phases. Status flips through Installing … → Validating <cmd> … → Rolling back … as the engine moves. Non-TTY / CI degrades to plain › … log lines; --json and --ci suppress the spinner entirely so machine-parseable output stays clean. Auto-disabled when --concurrency > 1 in non-JSON mode so per-target lines never interleave.
dep-up-surgeon outdated is a read-only installed-vs-latest report. It prefers the lockfile (what's actually in node_modules) over the declared range floor, so ^1.0.0 with lockfile already at 1.9.0 does not look "outdated". Exit 1 when anything lags, 2 when no package could be checked — perfect as a CI soft gate before you run upgrades.
Successful bumps rewrite package.json with the same range style you already use: ^1.2.3 → ^2.0.0, ~1.2.3 → ~1.5.0. Exact pins stay exact. Pass --pin-exact only when you want bare versions. Undo restores the original declared range, caret and all.
Skip / dry-run / outdated decisions use the version the lockfile resolved for that package.json's own dependency (per workspace member) — not semver.coerce of the declared floor, and not a nested copy somewhere else in the tree. Packages already at (or ahead of) @latest are skipped instead of rewriting package.json for no reason.
peerDependencies are skipped by default — they are a consumer contract, not a runtime pin. Pass --include-peers when you intentionally want to bump them. A library that lists the same package in devDependencies and peerDependencies gets its dev copy upgraded and its peer range left alone.
pnpm workspace catalogs (catalog: and catalog:default alike) and Bun workspaces.catalog are upgraded by rewriting the catalog entry in pnpm-workspace.yaml or the root package.json. The catalog: pointer in each package.json stays intact. Other non-registry specs (workspace:, link:, file:, git:, GitHub shorthands like user/repo#tag, tarballs, local paths) are skipped — never swapped for a same-named registry package.
Registry lookups honour your .npmrc — project, user and global, plus npm_config_* env vars: the default registry, @scope:registry mappings, per-registry auth tokens, TLS settings and proxies. Corporate mirrors and private scoped packages resolve exactly as they do for npm itself.
Install globally or run locally after cloning. From your project root (where package.json lives), start with outdated or --dry-run — then operate. Range style (^ / ~) is preserved unless you pass --pin-exact.
▸ install · inspect · plan
$npm install -g dep-up-surgeon$dep-up-surgeon outdated --json$dep-up-surgeon --dry-run --json▸ bump · validate · commit
$dep-up-surgeon --link-groups auto --fallback-strategy major-lines$dep-up-surgeon --workspaces --concurrency 4 --json --git-commit$npx dep-up-surgeon --workspaces --ci --git-commit$dep-up-surgeon --retry-failed --json▸ CVE · overrides · PR
$dep-up-surgeon --security-only --min-severity high --apply-overrides$dep-up-surgeon --workspaces --security-only --apply-overrides --git-commit --git-branch deps/security --open-pr --open-pr-draft$npx dep-up-surgeon --workspaces --ci --git-commit --git-branch deps/weekly --summary md --open-pr --open-pr-reviewers alice,bob▸ doctor · pin · undo
$npx dep-up-surgeon doctor --strict --json$dep-up-surgeon --override "some-dep>lodash@4.17.21" --override "axios@1.6.0"$dep-up-surgeon --fix-lockfile --summary md$dep-up-surgeon undo{
"ignore": ["some-legacy-package"],
"linkedGroups": [
{
"id": "my-batch",
"packages": ["package-a", "package-b"]
}
],
"validate": "tsc -p tsconfig.json --noEmit",
"overrides": [
{
"chain": ["some-dep", "foo"],
"range": "1.2.3",
"reason": "CVE-2025-1234"
},
{
"selector": "lodash@4.17.21",
"reason": "awaiting upstream PR #42"
}
]
}freeze:
- pattern: react
reason: "pinned until Q3 refactor"
- pattern: "@types/*"
maxVersion:
- pattern: next
range: "<=14"
allowMajorAfter:
- pattern: eslint
date: "2026-06-01"
requireReviewers:
major: 2
minor: 1
patch: 0
autoMerge:
patch: true
minor: false
include:
- "eslint-plugin-*"The full control panel — 58 flags & subcommands across eight sections: core run control, validation, workspaces, reports & CI, git integration, security-first mode, PR automation, and the outdated / doctor / undo subcommands. Exit code 1 when any upgrade could not be kept (unless --force) or when the pre-flight validator fails. Under --ci, per-package failures stay green.
--dry-run--interactive--forcesharp--ignore<pkg,pkg,...>--jsonci-ready--fallback-strategy<major-lines | minor-lines | none>default: major-lines--link-groups<auto | none>default: auto--resolve-peers--no-resolve-peers (opt-out)default: on--pin-exactdefault: preserve ^/~--validate<cmd>--no-validate--package-manager<auto | npm | pnpm | yarn | bun>default: auto--include-workspace-deps--cwd<path>--include-peersdefault: skip peers--workspaces--workspaces-only--workspace<names>--install-mode<root | filtered>default: root--concurrency<n>default: 1--no-parallel-installsisolated lockfiles--retry-failed--no-persist-report--summary<md | html>default: md--summary-file<path>--ciPR-bot ready--git-commit--git-commit-mode<per-success | per-target | all>default: per-success--git-commit-prefix<prefix>--git-branch<name>--git-sign--git-allow-dirtyuse with care--security-onlyCVE-driven · minimal fix--min-severity<low | moderate | high | critical>default: low--apply-overridestransitive CVEs--override<spec...>parent-scoped--override-force--fix-lockfilelockfile hygiene--changelog--no-changelog (opt-out)default: on--blast-radius--no-blast-radius (opt-out)default: on--open-prgh CLI required--open-pr-title<title>--open-pr-draft--open-pr-base<branch>--open-pr-reviewers<users>--open-pr-assignees<users>dep-up-surgeon outdatedcheck-firstoutdated --include-peers / --json / --cwddep-up-surgeon undodisaster recoveryundo --file <path>undo --json / --dry-runundo --skip-installundo --no-validate / --validate <cmd>undo --package-manager <mgr> / --cwd <path>dep-up-surgeon doctorpre-flight gatedoctor --strictdoctor --jsondoctor --skip-audit / --skip-peer-scan / --skip-stale-scan▸ note — non-registry specs (workspace:, link:, file:, git:, git+, github:, user/repo#ref, ssh:, http:, https:, npm:, jsr:, portal:, patch:, tarballs, local paths) are skipped for upgrades · catalog: / catalog:<name> (pnpm + bun) are upgraded by rewriting the catalog entry · the lockfile's version for each dependency beats the declared range floor for “already latest” · registry lookups honour .npmrc (registries, scopes, auth) · pre-flight runs once at the workspace root before any mutation