EPISODE 01 — OPERATION NODE_MODULES

dep-upsurgeon

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.

$npm install -g dep-up-surgeon

▸ 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+

Anime surgeon mascot holding an npm syringe
v4.3.0
NO PATIENTS HARMED ✦︎
ROLLBACK ✓
◉︎ ONE-BY-ONE UPGRADES✚︎ INSTALL + VALIDATE⚕︎ PRE-FLIGHT CHECK⟲︎ AUTO ROLLBACK◇︎ PEER-GRAPH BATCHING⛁︎ WORKSPACE-AWARE⇉︎ PARALLEL TARGETS⛨︎ NPM · PNPM · YARN · BUN▣︎ CATALOG: RANGES⚠︎ CONFLICT REPORTING☰︎ JSON + LAST-RUN↻︎ --RETRY-FAILED▤︎ MD/HTML SUMMARY⎇︎ GIT AUTO-COMMITS◎︎ CI / PR-BOT MODE★︎ INTERACTIVE RECOVERY⛯︎ FRAMEWORK-AGNOSTIC❖︎ SECURITY-FIRST MODE◩︎ TRANSITIVE OVERRIDES⚡︎ BREAKING-CHANGE RADAR⌖︎ BLAST RADIUS§︎ POLICY ENGINE✎︎ CHANGELOG EXCERPTS⇢︎ AUTO-OPEN PR⌕︎ OUTDATED CHECK^ PRESERVES ^ / ~⊞︎ LOCKFILE-AWARE FROM☍︎ PEERS STAY CONTRACTS◉︎ ONE-BY-ONE UPGRADES✚︎ INSTALL + VALIDATE⚕︎ PRE-FLIGHT CHECK⟲︎ AUTO ROLLBACK◇︎ PEER-GRAPH BATCHING⛁︎ WORKSPACE-AWARE⇉︎ PARALLEL TARGETS⛨︎ NPM · PNPM · YARN · BUN▣︎ CATALOG: RANGES⚠︎ CONFLICT REPORTING☰︎ JSON + LAST-RUN↻︎ --RETRY-FAILED▤︎ MD/HTML SUMMARY⎇︎ GIT AUTO-COMMITS◎︎ CI / PR-BOT MODE★︎ INTERACTIVE RECOVERY⛯︎ FRAMEWORK-AGNOSTIC❖︎ SECURITY-FIRST MODE◩︎ TRANSITIVE OVERRIDES⚡︎ BREAKING-CHANGE RADAR⌖︎ BLAST RADIUS§︎ POLICY ENGINE✎︎ CHANGELOG EXCERPTS⇢︎ AUTO-OPEN PR⌕︎ OUTDATED CHECK^ PRESERVES ^ / ~⊞︎ LOCKFILE-AWARE FROM☍︎ PEERS STAY CONTRACTS
// FILE 01 — LIVE DEMO

operation log

A real session: plan → install → validate → rollback on trouble. Hit refresh to replay the surgery.

REC ● LIVE OP
CRT-OS v4.3.0
/dev/tty1
▸ POWER▸ V-HOLD
■︎ dep-up-surgeon
// FILE 02 — TOOLKIT

Sticker-pack features

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.

✚︎

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.

/core/upgrader.ts
◇︎

Peer-graph batching

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.

/core/graph.ts
⚕︎

Pre-flight check

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.

preflight in --json
⚠︎

Generic conflict parser

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.

/core/conflictParser.ts
⛁︎

Workspaces & monorepos

--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.

--workspaces · --install-mode
⇉︎

Parallel target traversal

--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.

--concurrency <n>
♻︎

Fallback strategies

@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.

--fallback-strategy
⛨︎

Auto package-manager

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.

--package-manager auto
★︎

Interactive recovery

--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.

/cli/interactive.ts
☰︎

JSON report + last-run

--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.

--json · last-run.json
↻︎

Resume with --retry-failed

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.

--retry-failed
▤︎

Summary writer

--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.

--summary <md|html>
⎇︎

Git auto-commits

--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.

--git-commit · --git-branch
◎︎

CI / PR-bot mode

--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.

--ci
⟲︎

Safety-first rollback

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.

safety rail
⛯︎

Framework-agnostic

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.

zero stack lock-in
❖︎

Security-first mode

--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.

--security-only · --min-severity
◩︎

Transitive overrides

--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.

--apply-overrides · --override
⟴︎

Peer-range resolver

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.

--resolve-peers · default on
⎌︎

Disaster recovery (undo)

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 undo
✚︎

Doctor subcommand

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.

dep-up-surgeon doctor
⚒︎

Lockfile fix + stale scan

--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".

--fix-lockfile
⇆︎

Parallel installs

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.

--no-parallel-installs
⚡︎

Breaking-change radar

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.

auto when --changelog on
⌖︎

Blast radius

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.

--blast-radius · default on
§︎

Policy engine

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.

policy-as-code
✎︎

Changelog excerpts

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.

--changelog · default on
⇢︎

Auto-open PR

--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.

--open-pr · gh CLI
◐︎

Live progress

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.

/utils/logger.ts
⌕︎

Outdated check

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.

dep-up-surgeon outdated
^︎

Preserves ^ / ~ ranges

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.

default · --pin-exact
⊞︎

Lockfile-aware "from"

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.

installedVersion · lockfile
☍︎

Peers stay contracts

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.

--include-peers
▣︎

Catalog: ranges

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.

catalog: · catalog:<name>
◈︎

Private registries

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.

.npmrc-aware
// FILE 03 — INSTALL

Scrub in,
let's operate.

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.

Prep

CHART 01

▸ install · inspect · plan

▸ 01 — global install
$npm install -g dep-up-surgeon
▸ 02 — check what's actually outdated (lockfile-aware)
$dep-up-surgeon outdated --json
▸ 03 — plan only (no writes)
$dep-up-surgeon --dry-run --json

Operate

CHART 02

▸ bump · validate · commit

▸ 04 — real surgery (preserves ^ / ~ by default)
$dep-up-surgeon --link-groups auto --fallback-strategy major-lines
▸ 05 — monorepo, parallel scan, atomic commits
$dep-up-surgeon --workspaces --concurrency 4 --json --git-commit
▸ 06 — CI / PR-bot (stays green, writes step summary)
$npx dep-up-surgeon --workspaces --ci --git-commit
▸ 07 — resume only the residue from the last run
$dep-up-surgeon --retry-failed --json

Security

CHART 03

▸ CVE · overrides · PR

▸ 08 — CVE-driven: minimal advisory fix first (+ transitive overrides)
$dep-up-surgeon --security-only --min-severity high --apply-overrides
▸ 09 — weekly security sweep → branch → draft PR with summary
$dep-up-surgeon --workspaces --security-only --apply-overrides --git-commit --git-branch deps/security --open-pr --open-pr-draft
▸ 10 — full PR-bot flow with reviewers (policy-aware)
$npx dep-up-surgeon --workspaces --ci --git-commit --git-branch deps/weekly --summary md --open-pr --open-pr-reviewers alice,bob

Recovery

CHART 04

▸ doctor · pin · undo

▸ 11 — pre-flight diagnostic (read-only, CI-gate friendly)
$npx dep-up-surgeon doctor --strict --json
▸ 12 — manual parent-scoped pin (no --security-only needed)
$dep-up-surgeon --override "some-dep>lodash@4.17.21" --override "axios@1.6.0"
▸ 13 — collapse redundant transitives (no package.json diff)
$dep-up-surgeon --fix-lockfile --summary md
▸ 14 — disaster recovery: replay the last run in reverse
$dep-up-surgeon undo
// .dep-up-surgeonrc
{
  "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"
    }
  ]
}
▸ validate accepts a string, { command: "..." }, or { skip: true } · overrides merge with --override CLI flags (CLI wins on chain conflict) · reason flows into --summary + report
# .dep-up-surgeon.policy.yaml
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-*"
▸ policy-as-code · freeze wins · maxVersion caps · allowMajorAfter date-gates majors · requireReviewers / autoMerge are metadata for your PR bot
// FILE 04 — FLAG DOSSIER

Flag reference

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.

// SECTION A

Core run control

▸ 9 flags
--dry-run
Resolve latest versions and print the plan. Does not touch package.json, run installs, or overwrite the last-run report. Pairs beautifully with --json in CI.
--interactive
On failure, prompts for next steps. Single-package: continue / pin / retry. Linked-group: skip / retry / force / freeze. Optionally bulk-adds failed names to .dep-up-surgeonrc. Ignored with --json (prompts would corrupt the report) and --ci; Ctrl+C at a prompt stops the run and rolls back the step in progress.
--forcesharp
Keep a bump even when validation fails, and skip rollback when structured conflicts appear after a successful exit code. Use with care.
--ignore<pkg,pkg,...>
Comma-separated package names to skip. Merged with the ignore list from .dep-up-surgeonrc.
--jsonci-ready
Machine-readable report to stdout — progress, warnings and errors go to stderr, so stdout always parses. Top-level keys: upgraded, skipped, failed, conflicts, unresolved, groups, ignored, project, targets, installMode, concurrency, parallelInstalls, commits, gitCommitMode, preflight, preflightAborted, restoredFiles, transitiveAdvisories, policy, overrides, pullRequest, lockfileFix.
--fallback-strategy<major-lines | minor-lines | none>default: major-lines
After @latest fails: major-lines (default) walks back by major (9.x→8.x→…). minor-lines steps one major.minor at a time. ESM/CJS errors (ERR_REQUIRE_ESM) stop further fallbacks for that package. none only tries @latest.
--link-groups<auto | none>default: auto
auto (default) builds linked batches from the registry peer-graph + your linkedGroups config. none upgrades strictly one dependency per step.
--resolve-peers--no-resolve-peers (opt-out)default: on
When a linked-group bump (or a single-package bump, via an ad-hoc group synthesized from direct-dep blockers) fails with a peer conflict, intersect the members' published peerDependencies across the packument and retry with the newest satisfiable tuple. Graphs of 10+ members switch to a SAT-style AC-3 + MRV-DFS solver. Members may land below `latest`; tagged `peer-resolved from <v>` in --summary / --json.
--pin-exactdefault: preserve ^/~
Write bare exact versions (1.2.3) into package.json. Default preserves the previous range style: ^1.2.3 → ^2.0.0, ~1.2.3 → ~1.5.0. Undo restores the original declared range including caret/tilde.
// SECTION B

Validation

▸ 2 flags
--validate<cmd>
Override the validator command run after every install. Default: <mgr> test if a test script exists (run with CI=true, so watch-mode runners like react-scripts test exit), else <mgr> run build (yarn classic uses yarn build) — plus your lint / typecheck / type-check scripts, so a build that still passes can't hide an upgrade that breaks linting (TypeScript 7 vs typescript-eslint). A script that already fails before the run is compared by exit code: existing lint errors are tolerated, a crash rolls the upgrade back. An explicit --validate is used as-is. Great in monorepos: --validate "tsc -p tsconfig.json --noEmit".
--no-validate
Skip validation entirely. Upgrades are kept regardless of test/build outcome. Different from --force: --force still runs the validator and only keeps a bump when it fails; --no-validate runs no validator at all.
// SECTION C

Workspaces & package managers

▸ 10 flags
--package-manager<auto | npm | pnpm | yarn | bun>default: auto
auto (default) reads packageManager (or devEngines.packageManager), then lockfile (pnpm-lock.yaml → pnpm, yarn.lock → yarn, bun.lock / bun.lockb → bun — a yarn.lock next to them is bun's own — npm-shrinkwrap.json / package-lock.json → npm), then pnpm-workspace.yaml, finally falls back to npm. Drives both <mgr> install and the default validator (bun uses bun run test / bun run build).
--include-workspace-deps
By default, deps whose name matches a local workspace package are skipped (their version comes from the workspace, not the registry). Pass this when those packages are also published to npm.
--cwd<path>
Run against this directory instead of process.cwd(). Same flag on doctor / undo / outdated.
--include-peersdefault: skip peers
Also upgrade peerDependencies entries. Default: skip peers — they are a consumer contract, not a runtime pin. A package listed in both devDependencies and peerDependencies has its dev copy upgraded and its peer range left alone.
--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. Unknown names produce a friendly error listing the known members.
--install-mode<root | filtered>default: root
Workspace install strategy. root (default) runs <mgr> install from the workspace root after every mutation. filtered rewrites per-child installs: npm 7+ → npm install --workspace <name>, pnpm / bun → <mgr> install --filter <name>. yarn (classic and berry) always runs a full root install with a one-time warning: yarn workspaces focus doesn't write yarn.lock, so a kept bump would ship with a stale lockfile.
--concurrency<n>default: 1
Maximum number of workspace targets traversed in parallel (1–16; default 1). Overlaps registry scan + plan across targets while a shared mutex keeps install + validation strictly serialized — the lockfile is shared. In an isolated-lockfile monorepo (independent projects, each with its own lockfile and no shared workspace root) installs + validation ALSO run in parallel. pnpm's shared-workspace-lockfile=false doesn't count: pnpm install still installs every project under pnpm-workspace.yaml. Requires --json so per-target log lines don't interleave.
--no-parallel-installsisolated lockfiles
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.
// SECTION D

Reports & CI

▸ 5 flags
--retry-failed
Read the last-run report (node_modules/.cache/dep-up-surgeon/last-run.json) and freeze last-run successes plus terminal failures (peer, validation-script) per workspace (workspace::name). Re-attempt only non-terminal residue: install, validation-conflicts, versions, unknown. User --ignore / rc lists stay global — a freeze in one member does not skip the same package in another. Retries chain: a --retry-failed run keeps the previous retry's freezes.
--no-persist-report
Do NOT write the last-run report after the run. By default real runs write it — plus the pre-run lockfile backup — to node_modules/.cache/dep-up-surgeon/ (git never sees it) for --retry-failed, undo and CI consumers; --dry-run and runs that abort at pre-flight never write it.
--summary<md | html>default: md
Write a human-friendly summary of the run. Destination is $GITHUB_STEP_SUMMARY if set (appended), otherwise --summary-file <path>, otherwise ./dep-up-surgeon-summary.<ext>. Includes counts, project info, target list, Upgraded/Failed tables, pre-flight status, ignored list.
--summary-file<path>
Override the destination for --summary. Wins over $GITHUB_STEP_SUMMARY.
--ciPR-bot ready
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 + fatal errors exit 1) so per-package conflicts surface in the PR description instead of failing the job.
// SECTION E

Git integration

▸ 6 flags
--git-commit
Commit successful upgrades to git as the run progresses. Refuses to start on a dirty working tree (override with --git-allow-dirty); the tool's own report, summary and backup files don't count. Only stages package.json + the lockfile (+ pnpm-workspace.yaml when a catalog: entry moved, + .pnp.cjs / .yarn/cache in a Yarn zero-install repo) — never git add -A — and skips gitignored lockfiles. Override pins and the --fix-lockfile dedupe land in one extra commit at the end, so the branch matches the report. Skipped silently in --dry-run.
--git-commit-mode<per-success | per-target | all>default: per-success
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. all — one commit at the end with everything. Linked-group upgrades 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 + switch to this branch before any commits. If it already exists, switches to it; invalid branch names are rejected up front. 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-dirtyuse with care
Allow --git-commit to run on a dirty working tree. We still only git add files we touched — but if you also git add your own files manually, they'll land in dep-up-surgeon's commits.
// SECTION F

Security-first mode

▸ 6 flags
--security-onlyCVE-driven · minimal fix
Flip the tool from 'bump everything' to 'bump only packages with known CVEs'. Runs npm / pnpm / yarn / bun audit first (yarn npm audit on Berry), filters by --min-severity, then restricts the upgrade plan to 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 the --summary Security fixes table. Advisories on transitive packages no direct dependency names are listed with a fix hint (npm update <name> or --apply-overrides) and under transitiveAdvisories in --json.
--min-severity<low | moderate | high | critical>default: low
Minimum advisory severity to consider under --security-only. Lower-severity advisories are filtered out before the upgrade plan is built.
--apply-overridestransitive CVEs
After the main upgrade loop, fix transitive CVEs no direct bump could reach. With npm it first tries npm update <name> and writes no pin when the refreshed lockfile is clean. Otherwise it writes a package-manager override: overrides (npm 8.3+, bun), pnpm overrides (pnpm-workspace.yaml when it exists and package.json has no pnpm.overrides block; pnpm 9 and older keep package.json), resolutions (yarn). Only advisories at or above --min-severity are pinned. Install + validator run after each pin; on failure package.json, pnpm-workspace.yaml and the lockfiles are restored byte-for-byte, so an existing pin survives. Requires --security-only.
--override<spec...>parent-scoped
Apply manual override pins — parent-scoped or flat — independent of the audit. Repeatable / comma-separated. 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`). Written to the manager's native nested form and run through the same install + validator + rollback loop as --apply-overrides. Works standalone — no --security-only required.
--override-force
Used with --apply-overrides. Overwrite an existing override whose value conflicts with the audit-recommended version. Default refuses to clobber user-managed pins and records `conflict` in the report.
--fix-lockfilelockfile hygiene
After the main upgrade loop, run the manager's native dedupe (`npm dedupe --no-audit` / `pnpm dedupe` / `yarn dedupe` — berry only) to collapse redundant transitive copies WITHOUT touching package.json, and flag transitives more than a minor or a full major behind registry `latest` (top 250 by installed-copy count). npm-shrinkwrap.json wins over package-lock.json, as it does for npm. Lockfile is snapshotted first; dedupe or post-dedupe validator failure restores it. Yarn classic and bun → `skipped: "unsupported"`; missing lockfile → `skipped: "no-lockfile"`.
// SECTION G

PR automation · changelog · blast radius

▸ 8 flags
--changelog--no-changelog (opt-out)default: on
Fetch release notes (GitHub Releases API first — package-specific tags like create-vite@5.0.0 before v5.0.0, so monorepo packages get their own notes — then the package's CHANGELOG.md from its published tarball) and embed them in commit bodies + --summary. The full notes, plus the first release of each crossed major, are scanned for breaking-change markers and tagged as [breaking] on the commit subject. @mentions and issue refs are wrapped in backticks, so your commits and PRs never ping people or close issues. Default ON when --git-commit or --summary is active. GITHUB_TOKEN / GH_TOKEN lifts the API rate limit to 5000/h; if GitHub starts refusing, lookups stop for the rest of the run.
--blast-radius--no-blast-radius (opt-out)default: on
Scan your project source (.ts/.tsx/.js/.jsx/.mjs/.cjs/.mts/.cts/.vue/.svelte/.astro) to list which files actually import/require each upgraded package. Word-boundary safe so `react` doesn't match `react-dom`, and `@types/x` counts for files importing `x`. Output appears in --json (upgraded[].blastRadius) and as collapsible per-package lists in --summary. Default ON when --summary is active.
--open-prgh CLI required
After --git-commit --git-branch pushes the branch, open a GitHub pull request via the gh CLI. Body is the --summary markdown (falls back to a minimal deterministic body), trimmed to GitHub's size limit. Never fatal — a missing gh binary, auth failure, or push rejection is recorded under pullRequest.error in the JSON report. An open PR for the same head branch is reused (merged / closed ones get a new PR); GitHub Enterprise remotes are supported. Refuses to push when --git-branch is the PR's base branch.
--open-pr-title<title>
Override the PR title. Default is 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 when the breaking-change scanner fires so merge-queue bots don't auto-land it.
--open-pr-base<branch>
Target base branch. Default: the repo's default branch as reported by `gh repo view`.
--open-pr-reviewers<users>
Comma-separated usernames passed straight to `gh pr create --reviewer`.
--open-pr-assignees<users>
Comma-separated usernames passed straight to `gh pr create --assignee`.
// SECTION H

Subcommands · outdated · undo · doctor

▸ 12 flags
dep-up-surgeon outdatedcheck-first
Read-only installed-vs-latest report. Prefers the lockfile version over the declared range floor, so a caret that already resolved to latest is not flagged. Prints name / installed / latest / status. Exit 1 if any package is outdated (CI soft gate), 2 if no package could be checked, 0 otherwise. Pair with `--json` for dashboards.
outdated --include-peers / --json / --cwd
`--include-peers` includes peerDependencies (skipped by default). `--json` emits the structured OutdatedReport. `--cwd` / `--package-manager` mirror the main CLI.
dep-up-surgeon undodisaster recovery
Replay the last-run report in reverse: write every recorded `from` back to package.json, drop (or restore) every override the run added, newest first, restore the exact pre-run lockfile (only when nothing drifted and the lockfile still matches the run), run install once per edited target, then run the validator so you see green/red before committing the revert. Deps and pins that moved since the run are skipped as `drifted`. Exit 1 when install or validation fails or none of the report's packages exist here; 2 when the run report is missing / invalid.
undo --file <path>
Replay a specific run-report file (e.g. from a CI artifact) instead of the one in `node_modules/.cache/dep-up-surgeon/`. Paths are applied under this checkout, never the path recorded on the CI machine.
undo --json / --dry-run
`--json` emits the full structured `UndoResult` to stdout (perfect for CI pipelines). `--dry-run` computes the reverse plan without touching `package.json` or running install — use it for review / CI audit.
undo --skip-install
Write the reverted `from` values back to `package.json` and drop/restore overrides, but DON'T run `<mgr> install` afterwards. Useful when you only want the source edits and plan to install yourself.
undo --no-validate / --validate <cmd>
`--no-validate` skips the validator after the revert install — handy when the project has no test script but you still want the dep ranges rolled back. `--validate "<cmd>"` overrides the validator command for the reverse pass (e.g. `tsc -p tsconfig.json --noEmit`).
undo --package-manager <mgr> / --cwd <path>
Force a specific package manager (`auto` / `npm` / `pnpm` / `yarn` / `bun`) instead of auto-detecting from the run report, or run the undo against a different working directory.
dep-up-surgeon doctorpre-flight gate
Read-only green / yellow / red diagnostic — never mutates anything. Checks node-version (vs engines.node), manager resolution, lockfile parse, dependencies installed (red when node_modules is missing), workspace coherence (declared entries that don't resolve to a member), policy parse, pre-flight validator, peer-dep scan (npm ls / yarn check; skipped on pnpm, Yarn Berry and bun, which have no read-only check — a scan that can't run is yellow), audit severity breakdown, and stale-transitive scan. A missing package.json is red. Exit 0 on all-green (or yellow-only without --strict), 1 on yellow under --strict, 2 on any red.
doctor --strict
Treat yellow checks as failures (exit 1 instead of 0). Use for CI gates that should block on anything non-green.
doctor --json
Emit the full structured `DoctorReport` to stdout instead of the human format. Each check carries a stable ID (`node-version`, `manager`, `lockfile`, `dependencies-installed`, `workspace-coherence`, `policy`, `preflight-validator`, `peer-deps`, `audit`, `stale-transitives`) so dashboards and bots can parse consistently.
doctor --skip-audit / --skip-peer-scan / --skip-stale-scan
Opt out of the slower checks individually. Useful for air-gapped CI (skip-audit), huge trees (skip-peer-scan), or offline dev (skip-stale-scan). The diagnostic also accepts --no-validate, --validate <cmd>, --package-manager <mgr>, and --cwd <path>.

▸ 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