npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dep-up-surgeon

v2.2.8

Published

Upgrade npm dependencies one-by-one with validation, rollback, and conflict reporting.

Readme

dep-up-surgeon

npm version npm downloads npm license npm unpacked size Node.js engines TypeScript GitHub stars GitHub forks GitHub issues GitHub pull requests GitHub contributors Last commit Commit activity Libraries.io release Libraries.io dependents Website

Website: https://dep-up-surgeon.netlify.app/

Production-oriented CLI that upgrades npm dependencies with npm install + validation after each change, and rolls back on failure. It is framework-agnostic: grouping and conflict handling come from registry metadata and parsed npm output, not hardcoded stacks (React, Angular, etc.).

Install

npm install -g dep-up-surgeon

Or run locally after cloning:

npm install
npm run build
npx dep-up-surgeon --help

Usage

From your project root (where package.json lives):

dep-up-surgeon [options]

Options

| Option | Description | |--------|-------------| | --dry-run | Resolve latest versions and print the plan; does not change package.json or run installs. | | --interactive | On failure, prompts for next steps (see Interactive mode). After the run, optionally bulk-add failed names to .dep-up-surgeonrc. | | --force | Keep a version bump even when validation fails; also skips rollback when structured conflicts are detected in npm output after a successful exit code (use with care). | | --ignore <pkgs> | Comma-separated package names to skip (merged with .dep-up-surgeonrc). | | --json | Machine-readable report on stdout (see JSON report). | | --fallback-strategy <mode> | major-lines (default), minor-lines, or none. After @latest fails, major-lines tries the best stable version per major (e.g. 9.x8.x7.x …). minor-lines steps one major.minor line at a time. If npm output looks like ESM vs CommonJS (ERR_REQUIRE_ESM), further fallbacks for that package stop. none only attempts @latest. | | --link-groups <mode> | auto (default) or none. auto builds linked batches from the registry graph and optional linkedGroups. none upgrades one dependency per step. | | --validate <cmd> | Override the validator command run after every install. Defaults to <manager> test if a test script exists, else <manager> run build (yarn classic uses yarn build), else nothing. Useful in monorepos where the default build is heavy or fragile (e.g. --validate "tsc -p tsconfig.json --noEmit"). | | --no-validate | Skip validation entirely. Upgrades are kept regardless of test/build outcome. Different from --force: --force runs the validator and only keeps the bump when it fails, --no-validate doesn’t run a validator at all. | | --package-manager <mgr> | auto (default), npm, pnpm, or yarn. auto reads the packageManager field, then falls back to lockfile detection (pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm), then pnpm-workspace.yaml, then npm. The chosen manager drives both the install command (<mgr> install) and the default validator (<mgr> test / <mgr> run build). | | --include-workspace-deps | By default, dependencies whose name matches a local workspace package (resolved via workspaces in package.json or pnpm-workspace.yaml) are skipped — their version comes from the local workspace, not the registry. Pass this flag to upgrade them anyway (e.g. when local workspace packages also publish to the registry). | | --workspaces | Traverse the root package.json and every workspace member (one engine pass per package.json). Install + validation always run from the workspace root so the lockfile and validator see the whole monorepo. | | --workspaces-only | Like --workspaces but skips the root package.json. Only workspace members are traversed. | | --workspace <names> | Comma-separated workspace member names (the name field from each child package.json) to traverse. Pass root to also include the root. Example: --workspace "@org/core,@org/web,root". Unknown names produce a friendly error listing the known members. | | --install-mode <mode> | Workspace install strategy. root (default) always runs <mgr> install from the workspace root after every mutation — the safest option, supported by every package manager. filtered rewrites per-child installs to their workspace-scoped form: npm 7+ uses npm install --workspace <name>, pnpm uses pnpm install --filter <name>, yarn berry (v2+) with @yarnpkg/plugin-workspace-tools uses yarn workspaces focus <name>, and yarn classic / berry without the plugin falls back to a full root install with a one-time warning explaining the upgrade path. The capability is auto-detected at startup (yarn version + plugin probe) and reported as project.yarnMajorVersion + project.yarnSupportsFocus in --json. Only meaningful with --workspaces / --workspaces-only / --workspace <names>. | | --concurrency <n> | Maximum number of workspace targets to traverse in parallel (1–16; default 1). Higher values overlap registry scan + plan phases across targets while a shared mutex keeps install + validation strictly serialized — the workspace lockfile is shared, so concurrent installs would corrupt it. The default in-process registry cache also deduplicates pacote.manifest / pacote.packument calls across targets, so even at concurrency 1 you get a speedup when the same dep appears in many workspaces. Requires --json so per-target log lines don't interleave; non-JSON mode silently downgrades to 1 with a warning. In an isolated-lockfile monorepo (pnpm shared-workspace-lockfile=false, or every workspace member shipping its own lockfile) installs + validation are ALSO run in parallel — see Parallel installs below. | | --no-parallel-installs | Force installs + validation to stay serialized even when an isolated-lockfile monorepo is detected. Useful when debugging a flaky install step (parallel installs mask the ordering) or when a per-workspace postinstall script touches shared state outside its workspace. | | --retry-failed | Read .dep-up-surgeon.last-run.json from the previous run and only re-attempt entries that failed for non-terminal reasons (install, validation-conflicts, versions, unknown). Successful upgrades + terminal failures (peer, validation-script) from the last run are added to the ignore list automatically. See Persisted last-run report below. | | --no-persist-report | Do not write .dep-up-surgeon.last-run.json after the run. By default the structured report is written next to the workspace root for --retry-failed and CI consumers. | | --summary <format> | Write a human-friendly summary of the run as md (default) or html. Destination is $GITHUB_STEP_SUMMARY if set (appended), otherwise --summary-file <path>, otherwise ./dep-up-surgeon-summary.<ext>. | | --summary-file <path> | Override the destination for --summary. Wins over $GITHUB_STEP_SUMMARY. | | --ci | Convenience flag for CI / bot use. Disables --interactive, auto-enables --summary md (great with $GITHUB_STEP_SUMMARY), and exits 0 even when individual upgrades fail (only pre-flight failures and fatal errors exit 1) so per-package conflicts surface in the PR description instead of failing the job. | | --git-commit | Commit successful upgrades to git as the run progresses. Refuses to start on a dirty working tree (override with --git-allow-dirty). Only stages package.json + the lockfile — never git add -A, so unrelated WIP, generated files, and prepare/postinstall side effects are never accidentally swept into a commit. Skipped silently in --dry-run. | | --git-commit-mode <mode> | How to group commits: per-success (default, one commit per upgrade — best for review and git revert-friendly), per-target (one commit per workspace target with all its successes squashed), or all (one commit at the end with everything). Linked-group upgrades (e.g. react + react-dom) always land in a single commit regardless of mode. | | --git-commit-prefix <prefix> | String prepended to every commit message (default "deps: "). Use "chore(deps): " for Conventional Commits or set it to your team's preferred convention. | | --git-branch <name> | Create + checkout this branch before any commits. If the branch already exists, switches to it. Pairs nicely with --ci for PR-bot workflows (e.g. --git-branch "deps/auto-$(date +%Y-%m-%d)"). | | --git-sign | Pass --gpg-sign to every commit. Requires a signing key configured in git (user.signingkey + gpg.format). Failed signatures are recorded as failed commits in the JSON report rather than aborting the run. | | --git-allow-dirty | Allow --git-commit to run on a dirty working tree. We still only git add files we touched, so your WIP isn't swept up — but if you also git add your own files manually, they'll land in dep-up-surgeon's commits. | | --changelog / --no-changelog | Fetch the bumped package's release notes (GitHub Releases first, then its published CHANGELOG.md) and include them in commit bodies + --summary. Default ON when --git-commit or --summary is active. Network failures are non-fatal — missing changelogs are silently skipped. See Changelog excerpts below. | | --security-only | Run npm audit (or pnpm/yarn equivalent) first, then upgrade only the packages with open advisories. Every successful bump carries the advisory severity + ID into its commit subject ([security:high]) and into the summary's Security fixes table. Pairs well with --git-commit-mode per-success to produce one PR per CVE. See Security-first mode below. | | --min-severity <level> | Minimum advisory severity to consider under --security-only: low (default), moderate, high, or critical. Lower-severity advisories are filtered out before the upgrade plan is built. | | --blast-radius / --no-blast-radius | Scan project source files to list which files actually import/require each upgraded package, and surface the list in --json + --summary. Default ON when --summary is active. See Blast radius below. | | --resolve-peers / --no-resolve-peers | When a linked-group bump (e.g. react + react-dom + @types/react) or a single-package bump fails with a peer-dependency conflict, compute the intersection of peer ranges across the registry packument and retry with a satisfiable version tuple (members may land below latest). Linked graphs with 10+ members automatically use a SAT-style AC-3 solver; single-package failures synthesize an ad-hoc group from direct-dep blockers named in the install output. Default ON. See Peer-range intersection resolver below. | | --apply-overrides | After the main upgrade loop, fix transitive CVEs that no direct bump could reach by writing a package-manager override (overrides for npm, pnpm.overrides for pnpm, resolutions for yarn) pinning each vulnerable transitive to its audit-recommended safe version. Runs install + validator after each pin and rolls back automatically when the validator fails. Requires --security-only. See Transitive overrides below. | | --override <spec...> | Apply one or more manual override pins independent of the audit. Repeatable and also accepts comma-separated values. Syntax: <chain>@<range>, where <chain> is a bare name (lodash), a pnpm-style chain (some-dep>foo, any depth), or a yarn-style chain (parent/child). Scoped names (@scope/pkg) are preserved as single chain segments. Written to the manager-native nested form (npm object, pnpm >-keys, yarn /-keys) and run through the same install + validator + rollback loop as --apply-overrides. Works standalone — --security-only is not required. See Transitive overrides below. | | --override-force | Used with --apply-overrides. Overwrite an existing override entry whose value conflicts with the audit-recommended version. By default we refuse to clobber user-managed pins and record conflict in the report. | | --fix-lockfile | After the main upgrade loop, run the package manager's native dedupe command (npm dedupe / pnpm dedupe / yarn dedupe) to collapse redundant transitive copies without touching package.json, and flag transitives more than a minor or a full major behind registry latest. Lockfile is backed up before dedupe and restored if dedupe OR the post-dedupe validator fails. Yarn classic (v1) has no dedupe subcommand — recorded as skipped: "unsupported". See Lockfile fix below. | | --open-pr | After --git-commit --git-branch pushes the branch, open a GitHub PR with the --summary markdown as the body (falls back to a deterministic minimal body). Uses the gh CLI (must be installed + authenticated); never fatal — a missing binary, auth failure, or push rejection is recorded as pullRequest.error in the JSON report without aborting the run. See Auto-opening a PR below. | | --open-pr-title <title> | Override the PR title. Default: derived from the upgrade counts, e.g. deps: [breaking+security] bump 3 packages. | | --open-pr-draft | Open the PR as a draft. Recommended with --force or on Fridays so merge-queue bots don't auto-land it. | | --open-pr-base <branch> | Target base branch. Default: the repo default branch as reported by gh repo view. | | --open-pr-reviewers <users> / --open-pr-assignees <users> | Comma-separated usernames passed straight to gh pr create --reviewer / --assignee. |

Exit code 1 when any upgrade could not be kept (unless --force). The CLI also exits 1 when the pre-flight validator (run on the unchanged tree) fails — see Pre-flight check below. Fatal errors also exit 1.

Pre-flight check

Before mutating any dependency, the CLI runs the resolved validator command once against the unchanged tree:

  • If it passes, the run continues normally.
  • If it fails, the run aborts immediately with an error containing the validator command, exit code, and last ~40 lines of output. This prevents the common failure mode where every per-group rollback looks identical because the project build was already broken before the run.
  • To proceed anyway, use --validate "<cmd>" to swap the validator, --no-validate to skip it, or --force to ignore the pre-flight failure.

The pre-flight outcome is also surfaced under preflight / preflightAborted in --json output.

Persisted last-run report

After every CLI run the structured report is written to .dep-up-surgeon.last-run.json next to the workspace root (set --no-persist-report to opt out). The file mirrors the --json output and adds a small header (finishedAt, toolVersion, cwd, dryRun) so CI dashboards / bots can pick it up without re-running the tool. Add it to your .gitignore if you don't want it tracked.

Retry-failed mode (--retry-failed)

Pass --retry-failed to resume the previous run instead of starting from scratch:

  • dep-up-surgeon reads .dep-up-surgeon.last-run.json and freezes every package that either:
    • succeeded in the last run (no need to redo work), or
    • failed for a terminal reason: peer (real peer-dep conflict; bumping the same package alone almost always fails the same way) or validation-script (the project's own test/build script crashed; re-running won't help without a code change).
  • It then re-attempts only the residue: failures classified as install, validation-conflicts, versions, or unknown. These are the cases where another dependency move during the new run can plausibly unblock them.
  • Linked-group failures (name === '[group:<id>]') are expanded to every member of the group via the persisted groups field, so freezing a peer-failed group correctly freezes every package in it.
  • If .dep-up-surgeon.last-run.json is missing the CLI exits 1 with a friendly message; pass --retry-failed only after at least one prior run.

Typical workflow:

dep-up-surgeon --workspaces           # first pass: lots of moves, some failures
# fix the script that caused a `validation-script` failure (or accept it)
dep-up-surgeon --retry-failed         # second pass: only retries install/conflict residue

Summary writer (--summary <md|html>)

Pass --summary md (or --summary html) to render a human-friendly report alongside the normal output:

  • GitHub Actions: when GITHUB_STEP_SUMMARY is set, the Markdown summary is appended to that file — it shows up in the job summary tab without any extra workflow plumbing.
  • Explicit destination: --summary-file <path> overrides everything (wins over $GITHUB_STEP_SUMMARY).
  • Default: ./dep-up-surgeon-summary.<md|html>.

The summary contains: counts (upgraded / failed / skipped), detected project info, target list, an Upgraded table (Package | Workspace | From | To | Notes), a Failed table (Package | Workspace | Reason | Attempted | Detail), pre-flight status when it aborted, and the ignored list. HTML output escapes all dynamic content. Designed to be ~40 lines of code on the producer side and easy to embed in PR comments / dashboards.

The HTML output is self-contained and styled: an inline <style> block scoped to .dep-up-surgeon-report gives severity chips (critical / high / moderate / low), breaking / peer-resolved / fallback / forced badges on the Upgraded table, clickable advisory IDs linking to github.com/advisories/..., and responsive tables. No external CSS or web-font requests — the file opens cleanly in any browser, and when the style tag is stripped (GitHub step-summary sanitizer) the tables still render as plain HTML. Release notes and Blast radius per-package blocks are folded inside <details> elements so the summary stays scannable.

CI / bot mode (--ci)

--ci is a convenience flag for unattended runs (GitHub Actions, GitLab CI, Renovate-style bots). It:

  • Disables --interactive unconditionally — never blocks on stdin.
  • Auto-enables --summary md so a Markdown report lands in $GITHUB_STEP_SUMMARY (or ./dep-up-surgeon-summary.md outside Actions). Pass an explicit --summary html if you'd rather have HTML.
  • Remaps the exit code: per-package failures (peer conflicts, install crashes, validation script errors) are recorded in the report and the run still exits 0, so the bot's PR carries the diagnostic instead of the job failing red. Pre-flight failures and fatal errors still exit 1 — those mean the project itself is broken before any upgrade and a human needs to look.

Typical GitHub Actions step:

- name: dep-up-surgeon
  run: npx dep-up-surgeon --workspaces --ci

The job stays green; the Summary tab shows the upgraded / failed tables; .dep-up-surgeon.last-run.json is committed (or uploaded as an artifact) so a follow-up --retry-failed job can resume the residue.

Git integration (--git-commit)

Pair dep-up-surgeon with git so every successful upgrade lands as its own atomic commit — perfect for code-review-friendly auto-update PRs.

# One commit per upgrade (best for review).
npx dep-up-surgeon --workspaces --git-commit

# One commit per workspace target (squashed) on a fresh branch.
npx dep-up-surgeon --workspaces \
  --git-commit --git-commit-mode per-target \
  --git-branch "deps/auto-$(date +%Y-%m-%d)"

# CI bot: per-success commits, Conventional Commits prefix, signed.
npx dep-up-surgeon --workspaces --ci \
  --git-commit \
  --git-commit-prefix "chore(deps): " \
  --git-sign

Three commit modes:

  • per-success (default) — one commit per upgrade. Each commit contains exactly the package.json + lockfile diff for one dependency. Trivial to revert any single bump (git revert <sha>) and trivially reviewable in a PR. Linked-group upgrades (e.g. react + react-dom) still land as one commit since they were a single install.
  • per-target — one commit per workspace target, listing every successful upgrade in the commit body. Useful for monorepos where you want each member's bumps grouped.
  • all — one commit at the end with everything. Good for tiny single-package projects; avoid in monorepos.

Safety:

  • Refuses to start on a dirty working tree unless you pass --git-allow-dirty. We don't want to accidentally commit your WIP.
  • Only stages package.json + the lockfile — never git add -A. Files modified by prepare/postinstall hooks (e.g. .husky/) or other side effects of npm install stay uncommitted.
  • Errors out cleanly when not in a git repo (instead of silently skipping).
  • Skipped silently in --dry-run (no upgrades happen → nothing to commit).
  • A failed git commit (signing rejected, pre-commit hook refused, etc.) is recorded as commits[].ok === false in the JSON report with the git stderr — the upgrade itself is never rolled back because of a commit failure.

Concurrency-safe. --git-commit works fine with --workspaces --concurrency 8: the same async mutex that serializes installs also serializes git invocations, so two targets can't race the index.

Structured report. Every commit attempt (success or failure) appears under commits in --json output:

{
  "gitCommitMode": "per-success",
  "commits": [
    {
      "ok": true,
      "sha": "a1b2c3d",
      "message": "deps: bump axios from ^1.6.0 to ^1.7.2",
      "files": ["package.json", "package-lock.json"],
      "workspace": "root"
    }
  ]
}

Changelog excerpts

Every successful upgrade can be annotated with the package's release notes so reviewers don't have to open five GitHub tabs per PR. Enabled by default when --git-commit or --summary is set; disable with --no-changelog.

  • Source. First preference is the GitHub Releases API (GET /repos/:owner/:repo/releases/tags/:tag), resolved from the package's repository field in its package.json. Fallback is the CHANGELOG.md extracted from the published tarball via pacote.extract — the matching version section is parsed out with a Markdown-aware heading scanner (handles ## 1.2.3, ## [1.2.3] - 2024-..., ## v1.2.3, etc.).
  • Where it shows up. In --git-commit-mode per-success, the excerpt is embedded directly in the commit body. In per-target / all modes it collapses to a compact See: <release-url> footer so the commit doesn't balloon. --summary md / --summary html renders each excerpt in a collapsible <details> block — clean in PR bodies, compact in GitHub's Job Summary.
  • Caching & resilience. A run-local cache deduplicates fetches across workspaces. Network errors, missing tags, private repos, and malformed CHANGELOG.md files are all silently skipped — a missing excerpt never fails a commit.
  • GitHub auth. Anonymous GitHub API requests are rate-limited to 60/hour. Set GITHUB_TOKEN (or GH_TOKEN) in the environment to lift that to 5,000/hour — dep-up-surgeon uses it automatically for changelog fetches and nothing else.

Security-first mode

--security-only flips the tool from "bump everything safely" to "bump only packages with known CVEs". Competes directly with Dependabot's security-alert surface, but runs locally and respects your validator / policy / link groups.

  1. Runs npm audit --json (or pnpm audit --json / yarn audit depending on the detected manager) before the upgrade plan is built.
  2. Filters the audit to advisories at or above --min-severity <low|moderate|high|critical>.
  3. Builds a restrictToNames set from the vulnerable package names and passes it to the engine — every other dependency gets added to the ignore list automatically (visible as reason: "ignored" in the report).
  4. Attaches the severity + advisory ID + title to every upgraded record's security field, which the CLI then propagates into:
    • Commit subjects: deps: [security:high] bump axios from 1.6.0 to 1.7.2
    • Commit bodies: full advisory ID, URL, and title
    • --summary: a prominent Security fixes table above the normal upgraded table
    • --json: upgraded[].security = { severity, ids, url, title, vulnerableRange, recommendedVersion }
# Only critical + high; one commit per CVE on a dedicated branch.
npx dep-up-surgeon --workspaces --security-only --min-severity high \
  --git-commit --git-commit-mode per-success \
  --git-branch "deps/security-$(date +%Y-%m-%d)"

The whole path is covered by test/unit/security-only.test.mjs — a hermetic regression harness that drives runAudit with a canned npm audit --json blob, asserts --min-severity filters at every tier, and exercises the full runUpgradeFlow → install → validator → rollback cycle without touching the registry (via the UpgradeFlowOptions.installer injection point).

Policy engine (policy-as-code)

Drop a .dep-up-surgeon.policy.yaml (or .json) in the repo root to encode upgrade rules that survive across runs and humans. Loaded automatically on startup; violations are reported per-package and the engine skips the offending bumps instead of failing.

# .dep-up-surgeon.policy.yaml
freeze:
  - pattern: react               # never touch it
    reason: "React 18 pinned until Q3 refactor"
  - pattern: "@types/*"          # wildcard — freezes every @types/* scope
maxVersion:
  - pattern: next
    range: "<=14"                # refuse anything outside this semver range
allowMajorAfter:
  - pattern: eslint
    date: "2026-06-01"           # patch/minor OK now, majors blocked until the date
requireReviewers: 2              # metadata: surfaced in --summary / --json for your bot to consume
autoMerge: false                 # metadata: ditto

How rules interact

  • freeze always wins. Exact names go straight into the ignore list; wildcards are matched against the scanned deps inside the engine so rules like @types/* don't have to be unrolled by hand. Freezes produce a reason: "policy" skip record with the originating pattern.
  • maxVersion caps the candidate list. If no candidate satisfies the range, the package is skipped with reason: "policy" — it won't degrade to a no-op install.
  • allowMajorAfter blocks cross-major bumps until the specified date (checked against Date.now()), demoting the candidate to the newest in-major version. Patch/minor still flow through normally.
  • requireReviewers and autoMerge are metadata only — attached to the policy block of --json + --summary for downstream automation (GitHub Actions PR-opener, the SaaS bot, etc.) to consume.

Every applied rule appears in the Policy section of --summary and under policy.applied / policy.frozen / policy.warnings in --json, so audits show exactly which rule blocked which package.

Blast radius

Before handing the PR to a reviewer, dep-up-surgeon can list which of your own source files actually import each upgraded package. Surfaced automatically under --summary; attach it to --json too with --blast-radius.

  • Scans: .ts, .tsx, .js, .jsx, .mjs, .cjs, .mts, .cts, .vue, .svelte, .astro.
  • Skips: node_modules, dist, build, coverage, .git, .next, .turbo, .vercel, .cache, .parcel-cache, out, .output.
  • Detects: ES imports (import x from '<pkg>'), re-exports (export … from '<pkg>'), CommonJS require('<pkg>'), dynamic import('<pkg>'), and subpath imports (from '<pkg>/sub' still counts as a hit on <pkg>). Word-boundary safe — looking for react does not falsely match react-dom; looking for @types/node does not match @types/node-ipc.
  • Output: per-package { total, truncated, files[] } entries in upgraded[].blastRadius, plus a collapsible per-package list in the Markdown / HTML summary. Caps at 20 file paths per package by default; total keeps counting past the cap so the summary can honestly say "used in 134 files".
  • Cost: a single pass over the tree, at most 1 MB read per file, parallel I/O (default concurrency 8). Failures are non-fatal — a broken symlink never aborts the run. Turn it off in huge monorepos with --no-blast-radius.

Breaking-change detection

Whenever a changelog excerpt is fetched, dep-up-surgeon scans it for breaking-change markers and flags the upgrade so reviewers catch them before clicking merge. Works alongside --changelog (enabled by default with --git-commit / --summary) with no extra flags.

  • What we match: BREAKING CHANGE: / BREAKING CHANGES: footers (Conventional Commits), the 💥 and ⚠️ BREAKING emoji conventions used by Changesets / tsup / Vitest, explicit Node-version drops (drop support for Node 16, requires Node >= 20), API-removal bullets (- Removed the …), and no longer supported / renamed … to … phrasing. Deprecation notices alone do not trip the scan.
  • Where it shows up:
    • Commit subjects gain a [breaking] tag (emitted BEFORE [security:<sev>] when both apply): deps: [breaking][security:high] bump axios from 1.6.0 to 2.0.0.
    • Commit bodies get a Breaking changes detected: section listing the exact matched lines, capped at 5 per package.
    • --summary md|html renders a prominent ⚠️ Breaking changes detected section ABOVE the upgraded table, plus a ⚠️ breaking badge in the Notes column.
    • --jsonupgraded[].changelog.breaking = { hasBreaking, matchedLines[], reasons[] } (only present when the scan matched).
  • Never fatal, never noisy: absence of a changelog means no scan, which means no flag. The scan caps matches at 10 per package and dedupes identical lines so verbose changelogs don't drown out the signal.

Peer-range intersection resolver

Linked-group bumps (e.g. react + react-dom + @types/react, or the Jest / Testing-Library / Vitest families) frequently fail because one member's latest demands a peer version another member can't yet satisfy. Without help, the whole batch rolls back and the user has to figure out the right tuple by hand.

--resolve-peers (default ON) turns this into an automated constraint-satisfaction problem:

  1. The first batch attempt runs exactly like today — every linked member goes to its registry latest.
  2. If the install fails with a peer conflict, the resolver fetches each linked package's full registry packument (cached — one call per package per run) and reads every published version's peerDependencies block.
  3. Each member gets a candidate domain: every version between currentRange's minVersion and the originally-requested target, sorted newest-first, minus deprecated / pre-release versions.
  4. A newest-first backtracking search enumerates version tuples (variable = one package, domain = its candidate versions). For each partial assignment, every peer constraint that has become knowable is checked; peers on packages inside the linked group are checked against the chosen version, peers on packages OUTSIDE the group are checked against that package's range in the current package.json (via semver.minVersion).
  5. For large linked graphs (≥ 10 members) — where the 400-tuple backtracking budget can be burned before the solver escapes the first variable's domain — the resolver automatically switches to a SAT-style path (arc-consistency + least-constraining-value DFS). It pre-prunes every member-version that can't be satisfied against external peers, runs up to 128 AC-3 rounds across every ordered member pair until the pruned domains reach a fixed point, then does an MRV-ordered (smallest domain first) newest-first DFS on whatever survived. For monorepo link groups up to ~50 members × ~30 recent versions this finishes in milliseconds where plain DFS would return undefined. When the SAT path fails the dispatcher falls back to the plain backtracker automatically.
  6. The first complete tuple to satisfy every constraint is also the least-downgrade one. The engine rewrites the batch's target versions and retries the install + validator. On success, every affected row is tagged with resolvedPeer = { originalTarget, reason, tuplesExplored }reason carries a [backtracking] or [sat] method tag so reviewers can tell which solver path produced the tuple.
  7. If the resolver can't find a satisfiable tuple, or the retried install still fails, the batch falls back to the pre-resolver behavior (rollback + kind: 'peer' failure row).

Ad-hoc resolver for non-linked bumps. Single-package upgrades that fail with a peer conflict used to be rolled back unconditionally — the resolver was linked-groups-only. Now we synthesize an ad-hoc group from the parsed install output: the primary + every blocker named in the peer-conflict lines that's already a direct dep of the workspace (peers on unknown transitives stay out of scope). The same resolver (and the same SAT fallback) runs on that synthesized group. On success the engine writes a small batch: the primary at whatever version the resolver picked, plus any blocker the resolver wants moved within its current pinned range (the ad-hoc path is allowed to downgrade the primary, never to silently bump a blocker past its pin).

Guard rails that keep it safe:

  • Bounded search — capped at 400 tuples explored per batch (small graphs) or 400 × members tuples for the SAT path's DFS phase. Past that the resolver gives up silently instead of hanging the run on pathological inputs.
  • Optional peers (peerDependenciesMeta[name].optional === true) are ignored. An unsatisfied optional peer isn't a hard conflict.
  • Deprecated versions never appear in the domain. We'd rather fail to find a solution than auto-suggest a known-bad version.
  • Ad-hoc group size cap — default 6 members (primary + up to 5 direct-dep blockers). Prevents registry fetch storms on pathological peer graphs.
  • Ad-hoc never adds dependencies — a peer on a transitive that isn't already a direct dep is ignored rather than introduced.
  • --force bypasses the resolver — the user has explicitly opted into barreling through peer conflicts.
  • --no-resolve-peers keeps the old behavior when you WANT peer failures to surface so a human resolves them instead of the tool silently nudging versions off latest. Applies to both the linked-group and ad-hoc paths.

Where it shows up:

  • Console output: upgraded: react-dom → 18.3.1 (group react-pair) [peer-resolved from 19.0.0].
  • --summary md|html: a dedicated Peer-range resolutions table (package / group / requested / installed / tuples explored) above the upgraded table, plus a peer-resolved from <v> badge in the upgraded row's Notes column.
  • Commit subjects: [peer-resolved] tag sits between [breaking] and [security:<sev>] (stable order). The body gets a Peer-range resolutions (kept linked group satisfiable): footer listing each pinned member.
  • --json: upgraded[].resolvedPeer = { originalTarget, reason, tuplesExplored } plus upgraded[].requestedLatest still reflects the pre-resolver target so downstream tools can diff them.

Transitive overrides (--apply-overrides / --override)

--security-only by itself can only fix vulnerabilities reachable from a direct dependency. For CVEs that live in transitives (very common — [email protected] buried six levels deep under a toolchain package), pair --security-only with --apply-overrides and the tool will write a package-manager override to pin the vulnerable transitive to its safe version.

  • Which field: overrides for npm (>=8.3), pnpm.overrides for pnpm, resolutions for yarn (classic + berry).
  • How it picks the pin: uses the audit's own fixAvailable.version when present; otherwise minVersion of the first safe range the manager reported.
  • Rollback on failure: after each override, the tool runs a full install and then the validator. If either fails, the override is removed, install re-runs to restore the starting state, and the next advisory is still attempted. A failed override never strands the workspace — report.overrides.attempts[].rolledBack === true appears in the JSON and the summary.
  • Conflict protection: when the user already has a manual override with a value that conflicts with the audit recommendation, we refuse to clobber by default (reason: "conflicts with target ..."). Pass --override-force to overwrite explicitly.
  • Where it shows up:
    • --summary: dedicated Overrides applied table with Package / Pinned to / Source / Severity / Advisory. Parent-scoped pins render as a › b › c so the chain is visible at a glance.
    • --json: overrides.field + overrides.attempts[] with the full decision trail (ok, skipped, reason, previous, applied, installLog, rolledBack, chain, source). Parent-scoped pins carry chain: ["parent", "child"]; source distinguishes "advisory" from "manual".

Parent-scoped pins (--override)

--apply-overrides only writes the flat name → version form — every occurrence of the package gets pinned. When you need to pin a transitive only when it appears under a specific parent (e.g. you want [email protected] under some-dep while the rest of the tree uses [email protected]), use --override to write a parent-scoped selector. Works standalone — no --security-only required.

Syntax: <chain>@<range>. The chain supports three forms, all normalized internally:

Each selector is written to the manager's native nested encoding:

| Manager | Shape written | | --- | --- | | npm | { "overrides": { "some-dep": { "foo": "1.2.3" } } } — nested object; an existing flat pin for the parent is preserved via npm's "." self-selector. | | pnpm | { "pnpm": { "overrides": { "some-dep>foo": "1.2.3" } } } — pnpm's >-chain keys, deep chains supported. | | yarn | { "resolutions": { "some-dep/foo": "1.2.3" } }/-chain keys. |

Every pin runs through the same install + validator + rollback loop as advisory-driven pins. A failed manual pin is rolled back (only that specific slot) and the rest of the run continues; a flat pin and a parent-scoped pin with the same leaf name coexist as separate entries.

# Pin `[email protected]` ONLY when it's a transitive of `some-dep`, and pin `[email protected]`
# globally. Both live in the same run; one failing never touches the other.
npx dep-up-surgeon \
  --override "some-dep>[email protected]" \
  --override "[email protected]" \
  --validate "npm test"
# Weekly security sweep: direct bumps first, then transitive overrides (audit-driven +
# one manual pin), then a draft PR.
npx dep-up-surgeon --workspaces \
  --security-only --min-severity high \
  --apply-overrides \
  --override "@babel/core>@babel/[email protected]" \
  --git-commit --git-commit-mode per-success --git-branch "deps/security-$(date +%Y-%m-%d)" \
  --summary md \
  --open-pr --open-pr-draft

Override policy file (.dep-up-surgeonrc overrides)

Re-typing --override "parent>[email protected]" on every CI run gets old fast. Commit the pins to .dep-up-surgeonrc instead and they'll apply on every run the same way ignore does — merging with any CLI --override flags on the same invocation (CLI wins on chain conflict). The committed form supports a reason string that flows straight into the report + summary so reviewers can see why each transitive is pinned (CVE ID, vendor guidance, upstream PR link) without grepping commit history.

Two input shapes are accepted:

{
  "overrides": [
    // Structured: explicit chain + range. `chain: "lodash"` is shorthand for the flat case.
    { "chain": ["some-dep", "foo"], "range": "1.2.3", "reason": "CVE-2025-1234" },

    // Selector form: same syntax as the `--override` CLI flag.
    { "selector": "@babel/core>@babel/[email protected]", "reason": "upstream PR #16012 pending" },
    { "selector": "[email protected]" }
  ]
}

Behavior:

  • Merge + dedupe: entries are merged with CLI --override selectors by exact chain. A CLI pin for the same chain replaces the rc entry (including the reason), so one-off ad-hoc overrides always win over committed policy.
  • Malformed CLI selectors are warnings, not fatal: a typo in one --override flag won't prevent committed rc pins from applying. rc entries with a bad shape produce per-entry warnings in .dep-up-surgeonrc.warnings and the run continues.
  • Same lifecycle as --override: every pin goes through the install + validator + rollback loop. Failures are per-pin — one bad pin never strands the rest.
  • reason surfaces in the report: the Overrides applied table in --summary adds a Reason column whenever at least one attempt carries one; --json ships overrides.attempts[].policyReason verbatim for bots.
# Run the committed overrides policy. No CLI flags needed; `overrides: [...]` in
# `.dep-up-surgeonrc` is enough to trigger the flow.
npx dep-up-surgeon --summary md

# Add a one-off pin on top of the committed set for this run only:
npx dep-up-surgeon --override "[email protected]" --summary md

Disaster recovery (dep-up-surgeon undo)

Every run writes .dep-up-surgeon.last-run.json — a structured record of what the tool did (previous ranges, to values, every override attempt with applied/previous, workspace targets). dep-up-surgeon undo replays that record in reverse:

  1. For every successful upgrade row, write the recorded from back to package.json (in whatever section currently holds the dep, across the root and every workspace target).
  2. For every successful override attempt, drop the pin we added — or, when the attempt recorded a previous value (the run replaced an existing pin), restore that previous value.
  3. Run <manager> install once per edited target so the lockfile re-converges to the reverted package.json.
  4. Run the validator so you see green/red before you commit the revert.

Drift protection:

  • If the current package.json value for a dep doesn't match the to the run landed on (another run, or a human edit, moved it), the row is skipped with reason: 'drifted'. Undo never rewrites state we don't recognize.
  • When the recorded run was --dry-run, undo is a no-op.
  • Missing / unparseable run report → the command exits with status 2 and a clear error message.
# Replay the newest recorded run in this directory.
npx dep-up-surgeon undo

# Compute the reverse plan WITHOUT touching the disk. Use for review/CI dry-runs.
npx dep-up-surgeon undo --dry-run

# Replay a specific run file (e.g. from a CI artifact).
npx dep-up-surgeon undo --file ./ci-logs/2026-04-18-upgrade.last-run.json

# Skip the validator — handy when the project has no test script but you still want the
# dep ranges rolled back.
npx dep-up-surgeon undo --no-validate

Exit codes: 0 = reverse pass succeeded (or was a no-op); 1 = install or validator failed during the reverse pass (the JSON report still explains which rows moved); 2 = the run report was missing / invalid. Pair with --json for CI pipelines — the full UndoResult is emitted on stdout.

Lockfile fix (--fix-lockfile)

--fix-lockfile improves the lockfile's dependency graph without touching package.json. It's the counterpart to --apply-overrides: where overrides fix vulnerable transitives, --fix-lockfile collapses redundant ones and surfaces the stale-but-not-vulnerable tail that a direct upgrade loop can never reach.

What it does:

  • Dedupe: runs the package manager's native dedupe command — npm dedupe --no-audit / pnpm dedupe / yarn dedupe (berry only). These commands collapse multiple copies of the same package when semver ranges allow it.
  • Stale-transitive scan: for the top 250 packages in the lockfile (by installed-copy count), cross-references registry latest and flags any package whose highest installed version is more than one minor OR a full major behind. Trivial drift (a patch or a single minor) is filtered out — only the drifted-by-6-months cases show up.
  • Backup + rollback: lockfile is snapshotted before dedupe. If dedupe exits non-zero OR the post-dedupe validator fails, the snapshot is restored and the manager is re-run to reconcile node_modules. The worst case is the same tree you started with.

Guard rails:

  • Yarn classic (v1) has no dedupe subcommand — recorded as skipped: "unsupported" and the rest of the run continues normally.
  • No lockfile on diskskipped: "no-lockfile". Nothing to dedupe when the install has never been run.
  • Runs after --apply-overrides so the final tree includes security pins before dedupe.

Where it shows up:

  • Console: one-line summary — --fix-lockfile: npm dedupe ... succeeded (12 packages deduped/updated, 3 stale transitives flagged).
  • --summary md / --summary html: a dedicated Lockfile fix section with a merged/updated diff table, a collapsible Stale transitives details block, and the last lines of the dedupe/validator output on failure.
  • --json: lockfileFix: { status, manager, lockfile, command, dedupeChanges[], stale[], ... } with the full structured diff.
# Post-security-sweep cleanup: dedupe the tree and surface stale transitives in the PR body.
npx dep-up-surgeon --security-only --apply-overrides --fix-lockfile --summary md

Doctor subcommand (dep-up-surgeon doctor)

doctor is a read-only diagnostic that answers one question: "is this project in good shape for an upgrade pass right now?". Run it before trusting an upgrade loop (or as a CI pre-check); it never mutates anything. Output is a traffic-light report — green/yellow/red per check with a remediation hint on anything non-green.

What it checks, in order (stable IDs for --json consumers):

  1. node-version — current Node satisfies engines.node (if set). Red when a mismatched Node would tear down peer-dep resolution in ways that look like CVE-driven failures later.
  2. manager — a single package manager was resolved cleanly. Yellow when multiple lockfiles coexist or the tool had to fall back to the npm default without any signal.
  3. lockfile — the lockfile is parseable. Yellow on npm v1 shape (upgrades to v2 recommended); red on unreadable / corrupt files.
  4. workspace-coherence — declared workspace members resolve on disk with their own package.json.
  5. policy.dep-up-surgeon.policy.{yaml,json} (when present) parses without warnings.
  6. preflight-validator — your <mgr> test / <mgr> run build (or --validate <cmd>) passes right now, before any upgrade. Red here means the project is broken before the upgrade loop — fix that first or every failure downstream will look like a regression.
  7. peer-deps — existing peer / missing dep warnings (via npm ls --all, pnpm install --frozen-lockfile --offline, or yarn check). Catches "already broken before you touched it" cases.
  8. audit<mgr> audit dry-run with severity breakdown. Red on any high/critical advisory, yellow on low/moderate.
  9. stale-transitives — up to 100 transitives scanned against registry latest; yellow when any are more than a minor or a full major behind. Informational (never red); run --fix-lockfile to clean up the easy ones.

Exit codes:

  • 0 — all checks green (or yellow-only without --strict)
  • 1 — any yellow under --strict
  • 2 — any red

Options are focused (no entanglement with the 70+ upgrade-flow flags):

| Option | Description | |--------|-------------| | --json | Emit the full DoctorReport as JSON on stdout instead of the human format. | | --strict | Treat yellow checks as failures (exit 1 instead of 0). Use for CI gates. | | --no-validate | Skip the pre-flight validator check. | | --validate <cmd> | Override the validator command used by the pre-flight check. | | --skip-audit | Skip the audit dry-run. Use for air-gapped CI / offline dev. | | --skip-peer-scan | Skip the peer-dep scan (slow on huge trees). | | --skip-stale-scan | Skip the registry-backed stale-transitive scan. | | --package-manager <mgr> | Override detected manager: auto, npm, pnpm, yarn. | | --cwd <path> | Run against a different directory. |

# Quick CI pre-check
npx dep-up-surgeon doctor --strict --json

# Local "should I trust the upgrade loop?" check
npx dep-up-surgeon doctor

Auto-opening a PR (--open-pr)

When you've already paid the cost of running --git-commit --git-branch, --open-pr closes the loop by pushing the branch and opening a GitHub pull request via the GitHub CLI (gh). Uses your existing gh auth; the tool handles nothing sensitive.

  • Body: the Markdown --summary file when one was written, otherwise a deterministic minimal body listing upgraded packages. gh pr create --body-file - is used so the body is piped via stdin (no argv quoting hell for multi-KB Markdown).
  • Title: derived from the upgrade counts — e.g. deps: [breaking+security] bump 3 packages — or any string you pass via --open-pr-title.
  • Base branch: resolved from gh repo view when not explicitly given; respects your default branch setting.
  • Reuses existing PRs: if a PR already exists for the same head branch, we return { reused: true } instead of erroring.
  • Never fatal: a missing gh binary, an unauthenticated session, a rejected push, or a 4xx from the API is recorded as pullRequest.error in the JSON report and printed to stderr — the upgrade commits are still on disk, and a subsequent manual gh pr create or git push will work normally.
  • Draft mode: pass --open-pr-draft to open as a draft (recommended with --force or when the breaking-change badge fires).
# Full "open a proper PR" flow with reviewers + draft mode.
npx dep-up-surgeon --workspaces --summary md \
  --git-commit --git-commit-mode per-success --git-branch deps/weekly \
  --open-pr --open-pr-draft \
  --open-pr-reviewers alice,bob --open-pr-base main

Workspaces & package managers

dep-up-surgeon is workspace-aware:

  • Detection. On startup the tool resolves the package manager (npm / pnpm / yarn) by reading, in order: the --package-manager flag, the packageManager field in package.json, the lockfile (pnpm-lock.yaml → pnpm, yarn.lock → yarn, package-lock.json → npm), the presence of pnpm-workspace.yaml, and finally falls back to npm. Workspace globs are read from workspaces (npm/yarn — both array and { packages: [...] } forms are supported) or pnpm-workspace.yaml (packages: list).

  • Install + validator follow the manager. <mgr> install runs after each bump and the default validator becomes <mgr> test<mgr> run build (yarn classic uses yarn build). Override with --validate "<cmd>" if you need something different (e.g. pnpm -r build).

  • Workspace-internal deps are skipped automatically. If a dependency name matches a local workspace package, the tool does not try to resolve it from the npm registry — it appears in the report as skipped with detail: "workspace-internal dep …". Pass --include-workspace-deps to override (useful when those packages are also published).

  • Workspace child traversal (--workspaces / --workspaces-only / --workspace <names>). By default only the root package.json is mutated. With --workspaces, the tool also scans every member's package.json (one engine pass per file), but install + validation always run from the workspace root so the lockfile resolves correctly and the validator sees the entire monorepo. Pre-flight runs once at the workspace root regardless of how many targets are traversed. Every upgraded / failed row in the report is tagged with a workspace field ("root" or the member's package name) so you can tell at a glance which package.json produced each change.

  • Install mode (--install-mode root|filtered). Default is root: every per-child mutation triggers a full <mgr> install from the workspace root — slow on large monorepos but supported by every package manager and impossible to misconfigure. Pass --install-mode filtered to rewrite per-child installs to their workspace-scoped form so only the affected member is resolved/linked:

    • npm 7+npm install --workspace <name>
    • pnpmpnpm install --filter <name>
    • yarn berry (v2+) with @yarnpkg/plugin-workspace-toolsyarn workspaces focus <name> (install the plugin once with yarn plugin import workspace-tools)
    • yarn classic (v1.x) → falls back to a full root install with a one-time warning suggesting an upgrade to yarn berry
    • yarn berry without the plugin → falls back to a full root install with a one-time warning telling you the exact yarn plugin import command to fix it

    The yarn capability is auto-probed at startup (yarn --version + yarn workspaces focus --help) and surfaced as project.yarnMajorVersion and project.yarnSupportsFocus in --json. The mode actually used is recorded as installMode in the report, and the exact filtered command appears under failed[].install.command when an upgrade rolls back.

  • Parallel target traversal (--concurrency <n>). With more than one target, pass --concurrency 4 (or up to 16) to run target scans + plans concurrently. Registry IO (pacote.manifest / pacote.packument) is the slow part of each engine pass and is fully parallel-safe — overlapping it across targets gives a real wall-clock speedup on monorepos with many workspaces. In a shared-lockfile monorepo (the common case) installs and validations stay serialized under a keyed async mutex because they all touch the same root lockfile and node_modules; running them in parallel would corrupt the lockfile. In an isolated-lockfile monorepo (pnpm shared-workspace-lockfile=false, or every workspace member shipping its own lockfile) the installs and validations ALSO run in parallel — the keyed mutex keys off each target's install directory, so same-dir operations still serialize while different-dir operations unlock. The detection is automatic and surfaced as project.isolatedLockfiles + parallelInstalls: true in --json; pass --no-parallel-installs to force the old serialized behavior. An in-process registry cache (always on) also deduplicates fetches so the same dependency name in many workspaces only hits the network once. The effective concurrency is reported as concurrency in --json output. Parallelism requires --json; non-JSON mode silently downgrades to 1 to keep per-target log lines legible.

The detected manager + members are surfaced under project in --json output:

"project": {
  "manager": "pnpm",
  "managerVersion": "9.10.0",
  "managerSource": "package.json:packageManager",
  "lockfile": "pnpm-lock.yaml",
  "hasWorkspaces": true,
  "workspaceGlobs": ["packages/*", "apps/*"],
  "workspaceMembers": [
    { "name": "@org/core", "dir": "/path/to/repo/packages/core" }
  ]
}

What gets scanned

Direct entries in dependencies, devDependencies, peerDependencies, and optionalDependencies are considered. Non-registry ranges (workspace:, link:, file:, git: …) are skipped for upgrades.

How linked groups are chosen (--link-groups auto)

There are no framework-specific lists. Groups are derived from your direct dependency names and published package metadata:

  1. Custom groups from .dep-up-surgeonrc linkedGroups are applied first (exact package names).
  2. For each remaining registry dependency, the tool fetches the published manifest (pacote, cached in-memory for the run) and reads peerDependencies only. (Runtime dependencies / optionalDependencies are not used for clustering: they tend to connect unrelated packages through hubs like typescript, eslint, or rxjs, producing one giant batch.)
  3. An undirected edge is added between two project packages A and B when B appears in A’s published peerDependencies (and both are direct registry deps in your project). Connected components become one upgrade batch each (single package.json write + one npm install + one validation).
  4. @types/<pkg> is linked to <pkg> when both are direct dependencies (types often move with the runtime package).
  5. Isolated packages are upgraded alone (singleton groups).

Caveats

  • Packages only batch if the registry exposes edges between them. If two packages must move together but are not linked in metadata, add a linkedGroups entry.
  • SDK-style tooling (e.g. Expo) may expect npx expo install for channel alignment; this tool automates semver bumps when the graph is visible to npm.
  • Prerelease / canary: @latest may not match your channel — pin or ignore as needed.

Performance: manifests are fetched with bounded concurrency (parallel batches); responses are cached for the duration of the run.

Interactive mode (--interactive)

  • Single package failures: prompt to continue, pin (ignore) that package, or retry once.
  • Linked group failures: prompt to skip the group, retry (same targets; several attempts allowed), force (same as --force for that batch), or freeze (add all packages in the group to .dep-up-surgeonrc ignore). Attempts are capped higher when interactive so you can recover without rerunning the whole CLI.

Conflict detection

After each npm install, output is passed through a generic conflict parser (regex-based, no hardcoded package names). Lines that only refer to the root package.json name (for example npm’s While resolving: [email protected]) are filtered out so they do not appear as fake registry-package conflicts. Structured conflicts are classified (e.g. peer mismatch, missing peer, version range, engine, unresolved tree). If npm install exits successfully but conflicts are still detected in the log, the tool rolls back the bump (unless --force). Failed runs also attach parsed conflicts to the report where possible.

Why not only “latest”?

latest may not be adoptable yet (e.g. ESM-only majors, or a TypeScript major that breaks your build). The default strategy tries @latest first, then walks older release lines when fallbacks are enabled.

Live progress (spinner + elapsed timer)

Long phases — pre-flight validation, <mgr> install, the post-install test / build script, and rollbacks — no longer go silent. While each phase is running the CLI prints a single-line status that updates in place with an elapsed-seconds counter, so you can tell at a glance whether the tool is working or genuinely hung.

Typical output during a run:

⠸ Pre-flight: running `npm test` on unchanged tree... (4s)
✔ Pre-flight ok: `npm test` (12s)

Upgrading axios: 1.6.0 → latest 1.7.2 …
⠹ Installing [email protected] with npm... (6s)
⠋ Validating [email protected]: `npm test`... (18s)
✔ upgraded: axios → 1.7.2

On failure the spinner switches to the rollback phase before the final error line so you see the recovery happen:

⠹ Installing [email protected] with npm... (9s)
⠸ Rolling back react: `npm test` failed (exit 1)... (23s)
✖ skipped: react — validator (npm test) failed; this is not a dependency conflict

For linked-group upgrades the status line shows a compact batch label (first three members, then …+N more for larger groups):

Linked group [react-pair]: react → 19.0.0; react-dom → 19.0.0; @types/react → 19.0.0 …
⠙ Installing batch (3 pkgs) with npm: [email protected], [email protected], @types/[email protected]... (7s)
⠧ Validating batch (3 pkgs): `npm test`... (21s)
✔ upgraded: react → 19.0.0 (group react-pair)
✔ upgraded: react-dom → 19.0.0 (group react-pair)
✔ upgraded: @types/react → 19.0.0 (group react-pair)

Environment handling:

  • TTY (local dev) — animated braille spinner updated in place with a live elapsed timer.
  • Non-TTY (CI logs, piped output, tee-ed runs) — auto-degrades to plain › <phase> lines, one per phase transition, no ANSI escapes. Jenkins / GitHub Actions logs stay clean and every phase remains visible in the scrollback.
  • --json / --ci — completely silent. Machine output is untouched so JSON consumers never see progress noise.
  • --concurrency > 1 — already requires --json, so the spinner stays off in parallel runs. Serial runs keep a single clean status line regardless of how many workspace targets are traversed.

Configuration

Create .dep-up-surgeonrc in the project root:

{
  "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": "[email protected]", "reason": "awaiting upstream PR #42" }
  ]
}

Ignored packages are never upgraded. The CLI --ignore list is merged with this file.

linkedGroups defines forced batches before the dynamic graph runs (exact npm package names).

validate overrides the validator command. Accepts either a shell string ("tsc --noEmit") or an object: { "command": "pnpm -r build" } or { "skip": true }. CLI flags (--validate, --no-validate) always win over this file.

overrides defines persistent parent-scoped / flat override pins applied on every run. Merges with CLI --override flags (CLI wins on exact-chain conflict). Each entry's reason flows into report.overrides.attempts[].policyReason and the summary's Reason column. See Override policy file for the full schema.

JSON report (--json)

Stdout is a single JSON object including:

  • upgraded, skipped, failed, conflicts (parsed from npm output), unresolved (failed entries), groups (planned linked groups: ids and package names), and ignored.
  • preflight (when not skipped): { ok, command, exitCode, lastLines, source } for the unchanged-tree validator run.
  • preflightAborted: true if the run aborted before any upgrade.
  • For each failed entry caused by the validator, a validation block with { command, exitCode, lastLines, source } so you can tell at a glance whether the failure was a project-side script crash or an actual dependency conflict.
  • For every failed entry, an install block with { command, exitCode, lastLines, ok } capturing the install step that triggered the failure. `ok: