dep-up-surgeon 2.2.1 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +95 -13
  2. package/dist/cli/doctor.d.ts +52 -0
  3. package/dist/cli/doctor.d.ts.map +1 -0
  4. package/dist/cli/doctor.js +616 -0
  5. package/dist/cli/doctor.js.map +1 -0
  6. package/dist/cli/doctorCommand.d.ts +2 -0
  7. package/dist/cli/doctorCommand.d.ts.map +1 -0
  8. package/dist/cli/doctorCommand.js +71 -0
  9. package/dist/cli/doctorCommand.js.map +1 -0
  10. package/dist/cli/doctorRenderer.d.ts +21 -0
  11. package/dist/cli/doctorRenderer.d.ts.map +1 -0
  12. package/dist/cli/doctorRenderer.js +79 -0
  13. package/dist/cli/doctorRenderer.js.map +1 -0
  14. package/dist/cli/overrideFlow.d.ts +37 -0
  15. package/dist/cli/overrideFlow.d.ts.map +1 -1
  16. package/dist/cli/overrideFlow.js +110 -69
  17. package/dist/cli/overrideFlow.js.map +1 -1
  18. package/dist/cli/report.d.ts +2 -0
  19. package/dist/cli/report.d.ts.map +1 -1
  20. package/dist/cli/report.js +4 -0
  21. package/dist/cli/report.js.map +1 -1
  22. package/dist/cli/summary.d.ts.map +1 -1
  23. package/dist/cli/summary.js +7 -4
  24. package/dist/cli/summary.js.map +1 -1
  25. package/dist/cli.js +53 -14
  26. package/dist/cli.js.map +1 -1
  27. package/dist/core/audit.d.ts +18 -0
  28. package/dist/core/audit.d.ts.map +1 -1
  29. package/dist/core/audit.js +29 -0
  30. package/dist/core/audit.js.map +1 -1
  31. package/dist/core/peerResolver.d.ts +64 -0
  32. package/dist/core/peerResolver.d.ts.map +1 -1
  33. package/dist/core/peerResolver.js +225 -2
  34. package/dist/core/peerResolver.js.map +1 -1
  35. package/dist/core/peerResolverAdHoc.d.ts +42 -0
  36. package/dist/core/peerResolverAdHoc.d.ts.map +1 -0
  37. package/dist/core/peerResolverAdHoc.js +226 -0
  38. package/dist/core/peerResolverAdHoc.js.map +1 -0
  39. package/dist/core/upgrader.d.ts +30 -7
  40. package/dist/core/upgrader.d.ts.map +1 -1
  41. package/dist/core/upgrader.js +227 -18
  42. package/dist/core/upgrader.js.map +1 -1
  43. package/dist/core/workspaces.d.ts +22 -0
  44. package/dist/core/workspaces.d.ts.map +1 -1
  45. package/dist/core/workspaces.js +52 -0
  46. package/dist/core/workspaces.js.map +1 -1
  47. package/dist/types.d.ts +35 -4
  48. package/dist/types.d.ts.map +1 -1
  49. package/dist/utils/concurrency.d.ts +36 -0
  50. package/dist/utils/concurrency.d.ts.map +1 -1
  51. package/dist/utils/concurrency.js +45 -0
  52. package/dist/utils/concurrency.js.map +1 -1
  53. package/dist/utils/overrides.d.ts +81 -7
  54. package/dist/utils/overrides.d.ts.map +1 -1
  55. package/dist/utils/overrides.js +344 -30
  56. package/dist/utils/overrides.js.map +1 -1
  57. package/package.json +3 -3
package/README.md CHANGED
@@ -62,7 +62,8 @@ dep-up-surgeon [options]
62
62
  | `--workspaces-only` | Like `--workspaces` but **skips** the root `package.json`. Only workspace members are traversed. |
63
63
  | `--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. |
64
64
  | `--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>`. |
65
- | `--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. |
65
+ | `--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. |
66
+ | `--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. |
66
67
  | `--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. |
67
68
  | `--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. |
68
69
  | `--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>`. |
@@ -78,8 +79,9 @@ dep-up-surgeon [options]
78
79
  | `--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. |
79
80
  | `--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. |
80
81
  | `--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. |
81
- | `--resolve-peers` / `--no-resolve-peers` | When a linked-group bump (e.g. `react` + `react-dom` + `@types/react`) 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`). **Default ON**. See **Peer-range intersection resolver** below. |
82
+ | `--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. |
82
83
  | `--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. |
84
+ | `--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. |
83
85
  | `--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. |
84
86
  | `--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. |
85
87
  | `--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. |
@@ -234,6 +236,8 @@ npx dep-up-surgeon --workspaces --security-only --min-severity high \
234
236
  --git-branch "deps/security-$(date +%Y-%m-%d)"
235
237
  ```
236
238
 
239
+ 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).
240
+
237
241
  ### Policy engine (policy-as-code)
238
242
 
239
243
  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.
@@ -295,16 +299,21 @@ Linked-group bumps (e.g. `react` + `react-dom` + `@types/react`, or the Jest / T
295
299
  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.
296
300
  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.
297
301
  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`).
298
- 5. 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 }`.
299
- 6. 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).
302
+ 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.
303
+ 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.
304
+ 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).
305
+
306
+ **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).
300
307
 
301
308
  Guard rails that keep it safe:
302
309
 
303
- - **Bounded search** — capped at 400 tuples explored per batch. Past that the resolver gives up silently instead of hanging the run on pathological inputs.
310
+ - **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.
304
311
  - **Optional peers** (`peerDependenciesMeta[name].optional === true`) are ignored. An unsatisfied optional peer isn't a hard conflict.
305
312
  - **Deprecated versions** never appear in the domain. We'd rather fail to find a solution than auto-suggest a known-bad version.
313
+ - **Ad-hoc group size cap** — default 6 members (primary + up to 5 direct-dep blockers). Prevents registry fetch storms on pathological peer graphs.
314
+ - **Ad-hoc never adds dependencies** — a peer on a transitive that isn't already a direct dep is ignored rather than introduced.
306
315
  - **`--force` bypasses the resolver** — the user has explicitly opted into barreling through peer conflicts.
307
- - **`--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.
316
+ - **`--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.
308
317
 
309
318
  Where it shows up:
310
319
 
@@ -313,7 +322,7 @@ Where it shows up:
313
322
  - **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.
314
323
  - **`--json`**: `upgraded[].resolvedPeer = { originalTarget, reason, tuplesExplored }` plus `upgraded[].requestedLatest` still reflects the pre-resolver target so downstream tools can diff them.
315
324
 
316
- ### Transitive overrides (`--apply-overrides`)
325
+ ### Transitive overrides (`--apply-overrides` / `--override`)
317
326
 
318
327
  `--security-only` by itself can only fix vulnerabilities reachable from a direct dependency. For CVEs that live in transitives (very common — `lodash@4.17.20` 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.
319
328
 
@@ -322,14 +331,45 @@ Where it shows up:
322
331
  - **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.
323
332
  - **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.
324
333
  - **Where it shows up**:
325
- - **`--summary`**: dedicated `Overrides applied` table with `Package / Pinned to / Severity / Advisory`.
326
- - **`--json`**: `overrides.field` + `overrides.attempts[]` with the full decision trail (`ok`, `skipped`, `reason`, `previous`, `applied`, `installLog`, `rolledBack`).
334
+ - **`--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.
335
+ - **`--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"`.
336
+
337
+ #### Parent-scoped pins (`--override`)
338
+
339
+ `--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 `foo@1.2.3` under `some-dep` while the rest of the tree uses `foo@2.x`), use `--override` to write a **parent-scoped** selector. Works standalone — no `--security-only` required.
340
+
341
+ Syntax: `<chain>@<range>`. The chain supports three forms, all normalized internally:
342
+
343
+ - **Flat**: `--override lodash@4.17.21` → same shape as a classic flat override.
344
+ - **pnpm-style**: `--override "some-dep>foo@1.2.3"` — pin `foo` only when nested under `some-dep`. Chains of any depth (`a>b>c>d@1.0.0`) are supported.
345
+ - **yarn-style**: `--override "parent/child@1.2.3"` — `/` separator; `@scope/pkg` stays intact as a single chain segment.
346
+
347
+ Each selector is written to the **manager's native nested encoding**:
348
+
349
+ | Manager | Shape written |
350
+ | --- | --- |
351
+ | npm | `{ "overrides": { "some-dep": { "foo": "1.2.3" } } }` — nested object; an existing flat pin for the parent is preserved via npm's `"."` self-selector. |
352
+ | pnpm | `{ "pnpm": { "overrides": { "some-dep>foo": "1.2.3" } } }` — pnpm's `>`-chain keys, deep chains supported. |
353
+ | yarn | `{ "resolutions": { "some-dep/foo": "1.2.3" } }` — `/`-chain keys. |
354
+
355
+ 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.
356
+
357
+ ```bash
358
+ # Pin `lodash@4.17.21` ONLY when it's a transitive of `some-dep`, and pin `axios@1.6.0`
359
+ # globally. Both live in the same run; one failing never touches the other.
360
+ npx dep-up-surgeon \
361
+ --override "some-dep>lodash@4.17.21" \
362
+ --override "axios@1.6.0" \
363
+ --validate "npm test"
364
+ ```
327
365
 
328
366
  ```bash
329
- # Weekly security sweep: direct bumps first, then transitive overrides, then a draft PR.
367
+ # Weekly security sweep: direct bumps first, then transitive overrides (audit-driven +
368
+ # one manual pin), then a draft PR.
330
369
  npx dep-up-surgeon --workspaces \
331
370
  --security-only --min-severity high \
332
371
  --apply-overrides \
372
+ --override "@babel/core>@babel/traverse@7.23.2" \
333
373
  --git-commit --git-commit-mode per-success --git-branch "deps/security-$(date +%Y-%m-%d)" \
334
374
  --summary md \
335
375
  --open-pr --open-pr-draft
@@ -362,6 +402,50 @@ Where it shows up:
362
402
  npx dep-up-surgeon --security-only --apply-overrides --fix-lockfile --summary md
363
403
  ```
364
404
 
405
+ ### Doctor subcommand (`dep-up-surgeon doctor`)
406
+
407
+ `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.
408
+
409
+ What it checks, in order (stable IDs for `--json` consumers):
410
+
411
+ 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.
412
+ 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.
413
+ 3. **`lockfile`** — the lockfile is parseable. Yellow on npm v1 shape (upgrades to v2 recommended); red on unreadable / corrupt files.
414
+ 4. **`workspace-coherence`** — declared workspace members resolve on disk with their own `package.json`.
415
+ 5. **`policy`** — `.dep-up-surgeon.policy.{yaml,json}` (when present) parses without warnings.
416
+ 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.
417
+ 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.
418
+ 8. **`audit`** — `<mgr> audit` dry-run with severity breakdown. Red on any high/critical advisory, yellow on low/moderate.
419
+ 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.
420
+
421
+ Exit codes:
422
+
423
+ - `0` — all checks green (or yellow-only without `--strict`)
424
+ - `1` — any yellow under `--strict`
425
+ - `2` — any red
426
+
427
+ Options are focused (no entanglement with the 70+ upgrade-flow flags):
428
+
429
+ | Option | Description |
430
+ |--------|-------------|
431
+ | `--json` | Emit the full `DoctorReport` as JSON on stdout instead of the human format. |
432
+ | `--strict` | Treat yellow checks as failures (exit 1 instead of 0). Use for CI gates. |
433
+ | `--no-validate` | Skip the pre-flight validator check. |
434
+ | `--validate <cmd>` | Override the validator command used by the pre-flight check. |
435
+ | `--skip-audit` | Skip the audit dry-run. Use for air-gapped CI / offline dev. |
436
+ | `--skip-peer-scan` | Skip the peer-dep scan (slow on huge trees). |
437
+ | `--skip-stale-scan` | Skip the registry-backed stale-transitive scan. |
438
+ | `--package-manager <mgr>` | Override detected manager: `auto`, `npm`, `pnpm`, `yarn`. |
439
+ | `--cwd <path>` | Run against a different directory. |
440
+
441
+ ```bash
442
+ # Quick CI pre-check
443
+ npx dep-up-surgeon doctor --strict --json
444
+
445
+ # Local "should I trust the upgrade loop?" check
446
+ npx dep-up-surgeon doctor
447
+ ```
448
+
365
449
  ### Auto-opening a PR (`--open-pr`)
366
450
 
367
451
  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`)](https://cli.github.com/). Uses your existing `gh auth`; the tool handles nothing sensitive.
@@ -397,7 +481,7 @@ npx dep-up-surgeon --workspaces --summary md \
397
481
  - **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
398
482
 
399
483
  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.
400
- - **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. **Installs and validations stay serialized** under a shared async mutex because they all touch the same root lockfile and `node_modules`; running them in parallel would corrupt the lockfile. 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.
484
+ - **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.
401
485
 
402
486
  The detected manager + members are surfaced under `project` in `--json` output:
403
487
 
@@ -560,8 +644,6 @@ The compiled entry is `dist/cli.js` (see `"bin"` in `package.json`).
560
644
  ## Future work (tracked in code)
561
645
 
562
646
  - GitLab / Bitbucket auto-PR providers (today `--open-pr` is GitHub-only via `gh`)
563
- - Nested / parent-scoped override rules beyond the flat `name → version` form (`overrides: { "foo": { ">=2 <3": "3.0.0" } }` in npm, deep pnpm selectors)
564
- - Resolver sharpening: SAT-backed peer-range intersection for very large linked graphs (10+ members) where the current 400-tuple backtracking budget gives up; peer-range intersection across **non-linked** packages too (today the resolver only intervenes on linked-group failures)
565
647
  - Renovate-style scheduling helpers (cron / day-of-week filters, grouping rules)
566
648
  - True parallel installs in monorepos that don't share a root lockfile (e.g. nohoist setups), going beyond today's parallel scan + serial install model
567
649
  - AI-assisted failure explanation: feed `install.lastLines` + `validation.lastLines` to an LLM and attach a one-sentence "why this broke" note to failed records
@@ -0,0 +1,52 @@
1
+ import type { PackageManager } from '../core/workspaces.js';
2
+ export type DoctorStatus = 'green' | 'yellow' | 'red';
3
+ export interface DoctorCheck {
4
+ /** Stable id — used as the JSON key so consumers can branch on it reliably. */
5
+ id: string;
6
+ /** Short label shown in the human-readable output. */
7
+ label: string;
8
+ status: DoctorStatus;
9
+ /** One-line human summary of the outcome. */
10
+ message: string;
11
+ /** Optional remediation hint shown under the message for non-green entries. */
12
+ hint?: string;
13
+ /**
14
+ * Free-form structured payload included in `--json` output. Kept check-specific so the human
15
+ * renderer can stay dumb and downstream consumers can parse what they care about.
16
+ */
17
+ data?: Record<string, unknown>;
18
+ }
19
+ export interface DoctorReport {
20
+ cwd: string;
21
+ toolVersion: string;
22
+ /** Per-check results in stable order. */
23
+ checks: DoctorCheck[];
24
+ /** Aggregate — worst status across all checks. */
25
+ overall: DoctorStatus;
26
+ /** Count of checks per status. */
27
+ counts: Record<DoctorStatus, number>;
28
+ }
29
+ export interface RunDoctorOptions {
30
+ cwd: string;
31
+ toolVersion: string;
32
+ /** When true, skip the pre-flight validator even if a command is configured. */
33
+ skipValidator?: boolean;
34
+ /** User-provided validator command (overrides the default `<mgr> test` fallback). */
35
+ validatorCommand?: string;
36
+ /** When true, skip `<mgr> audit` (air-gapped CI, offline dev). */
37
+ skipAudit?: boolean;
38
+ /** When true, skip `<mgr> ls --all` (slow on huge trees). */
39
+ skipPeerScan?: boolean;
40
+ /** When true, skip the registry-backed stale-transitive scan. */
41
+ skipStaleScan?: boolean;
42
+ /** Override package manager (mirrors the top-level `--package-manager` flag). */
43
+ manager?: PackageManager | 'auto';
44
+ }
45
+ /**
46
+ * Public entry point. Runs every enabled check in sequence (checks are cheap enough that
47
+ * serial keeps the log predictable; we don't gain anything by racing them). Never throws —
48
+ * a check that explodes returns `red` with the error as the `message` so downstream renderers
49
+ * still see a complete report.
50
+ */
51
+ export declare function runDoctor(opts: RunDoctorOptions): Promise<DoctorReport>;
52
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/cli/doctor.ts"],"names":[],"mappings":"AAyCA,OAAO,KAAK,EAAE,cAAc,EAAe,MAAM,uBAAuB,CAAC;AAUzE,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAEtD,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,yCAAyC;IACzC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,kDAAkD;IAClD,OAAO,EAAE,YAAY,CAAC;IACtB,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC;CACtC;AAED,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,gFAAgF;IAChF,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qFAAqF;IACrF,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kEAAkE;IAClE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iEAAiE;IACjE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iFAAiF;IACjF,OAAO,CAAC,EAAE,cAAc,GAAG,MAAM,CAAC;CACnC;AAED;;;;;GAKG;AACH,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAkC7E"}