en
v1.0 · MIT

One NuGet store, byte-identical builds, no new manifest format.

PCPM is a content-addressable package manager for .NET, built on Central Package Management. It cuts the on-disk NuGet footprint by 70-90%, pins every transitive to a content hash, and ships a workspace doctor + security audit out of the box.

~/my-dotnet-project
$ # From an empty directory…
$ pcpm init
 Created pcpm.json
 Created Directory.Packages.props

$ pcpm add Newtonsoft.Json
 Resolved Newtonsoft.Json@13.0.3
 Wrote Directory.Packages.props
 Hardlinked to ~/.nuget/packages

$ pcpm install
 Resolving 12 packages…
 11 served from store (0 KB)
 1 new package (387 KB) written to store
 dotnet restore completed in 1.4s

$ pcpm doctor
 CPM enabled, no floating versions
 Lockfile in sync, no known CVEs
 Workspace is clean

What it does for you, concretely

Not a marketing pitch — the four things people switch to PCPM for.

  • 70-90% less disk on the NuGet cache

    Every distinct .nupkg is stored once in a content-addressable store and hardlinked into ~/.nuget/packages. The store does not grow with the number of projects — adding a 31st project that uses the same 200 packages adds zero bytes.

  • Reproducible builds, enforced

    pcpm.lock records the resolved version and the sha256 of the .nupkg for every entry. pcpm ci refuses to proceed if the lockfile is stale, missing, or has hashes that don't match the store — a clean CI run that resolves a new transitive becomes a build failure, not a silent regression.

  • Clean CI restores that aren't slow

    First install downloads the world. Every subsequent install — including on a clean CI runner with a warm cache — only fetches the deltas. The hardlink step is metadata-only, and dotnet restore sees a standard ~/.nuget/packages layout.

  • Tells you who pulled in what

    pcpm why <package> walks the lockfile and shows the full chain of dependents that brought a transitive into your build. When a surprise bump shows up in CI, you know exactly which direct dependency to negotiate with.

Workspace health, before CI does it for you

PCPM ships two real diagnostic tools — a doctor for CPM hygiene, and an audit for security and licensing.

pcpm doctor

exit 1/2/0

Runs a battery of checks against the current workspace and exits non-zero on real problems. Wire it into your build gate.

  • ManagePackageVersionsCentrally is set
  • No floating versions (1.0.*, *) in Directory.Packages.props
  • No <PackageReference Version=…> left over from a pre-CPM era
  • No orphaned CPM entries (declared, unused)
  • No missing CPM entries (referenced, undeclared)
  • Lockfile is in sync with the workspace
  • Known CVEs from the NuGet feed (opt-out with --no-cve)

Exit 1 = errors, 2 = warnings, 0 = clean.

pcpm audit

SBOM

Scans the resolved graph against vulnerability advisories and license metadata. --output <dir> writes a CycloneDX SBOM plus a license report.

  • Vulnerability scan against the NuGet advisory feed
  • License detection and report (SPDX identifiers)
  • CycloneDX SBOM output (--no-sbom to skip)
  • JSON output mode for CI piping

Already have a .NET solution? Migrate in one command.

pcpm convert walks your solution and rewrites it in place: hoists every per-project Version= into <PackageVersion />, adopts an existing Directory.Packages.props, writes pcpm.json and pcpm.lock.

  • Adopts existing Directory.Packages.props — never overwrites
  • Strips Version= from <PackageReference /> in every .csproj
  • --dry-run previews the diff before any change
  • --revert undoes a previous convert
  • --workspace covers every project in the monorepo
pcpm convert
$ pcpm convert --dry-run
 Will write Directory.Packages.props (new)
 Will strip Version= from 14 .csproj files
 Will write pcpm.json + pcpm-workspace.yaml

$ pcpm convert
 Created Directory.Packages.props
 Stripped 14 Version= attributes
 Wrote pcpm.json
 pcpm install completed in 3.1s
 Done. Roll back with: pcpm convert --revert

Read the migration guide →

Everything you need, nothing you don't

Twelve verbs. No 80-flag flags, no plugin sprawl.

Command What it does
pcpm init Initialise a workspace (pcpm.json, Directory.Packages.props, pcpm-workspace.yaml for monorepos).
pcpm add <pkg> Add a direct dependency. --version <v> to pin, --project <path> to target one csproj, --no-install to skip the implicit install.
pcpm install Resolve the transitive graph, materialise packages into the store, hardlink to ~/.nuget/packages, run dotnet restore.
pcpm remove <pkg> Remove from CPM and every referencing .csproj.
pcpm list Pretty-print pcpm.lock as a table.
pcpm why <pkg> Show the chains of dependents that pull <pkg> into the lockfile.
pcpm outdated Query the feed for newer versions, report bump type (major / minor / patch).
pcpm doctor CPM hygiene, floating versions, hardcoded Version=, orphans, missing entries, lockfile sync, CVE feed.
pcpm audit Vulnerability scan, license report, CycloneDX SBOM (--output <dir>, --no-sbom).
pcpm convert Migrate an existing solution to CPM + pcpm. --dry-run, --revert, --workspace.
pcpm store status Disk usage of the global content-addressable store.
pcpm ci Strict, fail-fast install for CI — refuses if pcpm.lock is stale or hashes don't match.

See the full command reference →

An architecture you can reason about

Three layers, each with a single responsibility, all wired through constructor injection. Zero I/O in the core, so the domain rules are testable in isolation.

CLI

pcpm.Cli

Spectre.Console.Cli commands (one per verb) — thin orchestration only, no business logic.

Core

pcpm.Core

Domain models, abstractions, pure logic — DependencyResolver, version parsing, conflict detection. No I/O.

Infrastructure

pcpm.Infrastructure

Implementations: NuGetFeed (raw HTTP), PackageStore (content-addressable), HardlinkCreator (Win32 P/Invoke), CpmFileService, LockfileService, ConvertService, …

Ready to give PCPM a spin?

MIT-licensed, open to contributions. The repo builds with a single dotnet build; the docs cover installation, migration, CI integration, and the full command reference.