Stage 0
Stage 1 — Headless installer
Make pacquet install --frozen-lockfile feature-complete with pnpm install --frozen-lockfile.
Pacquet does not resolve dependencies in this stage. The user runs pnpm install (or pnpm install --lockfile-only) to produce pnpm-lock.yaml, then pacquet install --frozen-lockfile materializes node_modules from it. The lockfile is the contract between the two tools.
This mirrors pnpm's internal @pnpm/headless boundary, so the seam is natural.
Completed
TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Correct snapshot filtering and lockfile loading; nothing downstream is sound until these are in place.
Tier 2 — Real-world install enablers. Each unlocks a large slice of real pnpm repos and is largely independent of the others.
Tier 3 — New resolution types and layout modes. Each is a self-contained extension to the install pipeline; the ordering inside this tier is mostly arbitrary.
Tier 4 — Consistency, polish, auxiliary. Quality-of-life fixes that close the parity gap once the structural work is in place.
Integration milestone (between Stage 1 and Stage 2)
Once the headless installer is solid, integrate pacquet as an opt-in install backend for pnpm itself — for example via an install-backend=pacquet setting, or a Node N-API addon hooked at the @pnpm/headless seam. This ships Stage 1 value to every pnpm user, not just those who install pacquet directly, and provides a feedback firehose for parity validation before Stage 2 is built.
Stage 2 — Implementing resolution
Make pacquet install feature-complete with pnpm install (no --frozen-lockfile). Pacquet reads package.json + pnpm-workspace.yaml, resolves the dependency graph, writes a pnpm-lock.yaml that round-trips byte-identically with pnpm's, and materializes node_modules. This unlocks the install subcommands that today only work via pnpm: install (default), install --lockfile-only, add, update, remove, outdated, why.
Pacquet already has scaffolding from Stage 1: crates/registry, crates/resolving-npm-resolver (metadata fetch + cache, named registries, mirroring, trust checks), crates/resolving-resolver-base, and a full lockfile writer (crates/lockfile/save_lockfile). Stage 2 fills in the parts that turn a manifest into a resolved graph.
TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Nothing downstream is sound until these land.
Tier 2 — Per-protocol resolvers. Each unlocks a class of spec. Largely independent.
Tier 3 — Resolution policies & constraints. Behaviors layered on top of the recursion that change which version wins or whether the install proceeds.
Tier 4 — Hooks & custom resolution. Both require running user JavaScript inside pacquet — design call needed.
Tier 5 — Commands unlocked by resolution. Stage 1 only delivers pacquet install --frozen-lockfile. Once resolution lands, each of these is a CLI surface port that follows the underlying capability.
Tier 6 — Consistency, polish, auxiliary. Close the parity gap once the structural work is in place.
Integration milestone (between Stage 2 and Stage 3)
Once resolution is solid, extend the Stage-1 → Stage-2 opt-in seam (install-backend=pacquet / N-API) to route everyday pnpm install calls through pacquet end-to-end — not just --frozen-lockfile. Stage 3 then targets the non-install command surface (publish, audit, dlx, exec, run, store).
Stage 3 — Non-install command surface
Stages 1–2 cover the dependency-management surface: install (incl. --frozen-lockfile / --lockfile-only), add, update, remove, outdated, why. Stage 3 tracks every remaining pnpm command. Pacquet's crates/cli already implements a handful of these beyond install; the rest are unported.
Status legend: [x] = a handler exists in pacquet's CLI today (a parity audit against pnpm's flags/output is still required before it can be called complete); [ ] = not ported.
Tracking
Grouped by the pnpm package that owns each command (see the imports at the top of pnpm/src/cmd/index.ts). Aliases are listed after the primary name.
Script & process — @pnpm/exec.commands
Store & cache — @pnpm/store.commands, @pnpm/cache.commands
Project scaffolding — @pnpm/workspace.commands
Workspace install helpers — @pnpm/installing.commands
Inspection & query — @pnpm/deps.inspection.commands
Compliance — @pnpm/deps.compliance.commands
Build / lifecycle — @pnpm/building.commands
Patching — @pnpm/patching.commands (taken by @spencer17x)
Publishing & releasing — @pnpm/releasing.commands
Registry access — @pnpm/registry-access.commands, @pnpm/auth.commands
Config & manifest — @pnpm/config.commands, @pnpm/pkg-manifest.commands
Engine & self-management — @pnpm/engine.pm.commands, @pnpm/engine.runtime.commands
Completion — @pnpm/cli.commands
pnpm CLI built-ins — pnpm/src/cmd
Not tracked: access, edit, issues, prefix, profile, team, token, xmas — these are stubs in pnpm itself (they throw NOT_IMPLEMENTED and defer to the npm CLI), so there is nothing to port.
Stage 3 tracking section drafted by an agent (Claude Code, claude-opus-4-8).
Stage 2 plan drafted by an agent (Claude Code, claude-opus-4-7).
Stage 0
Stage 1 — Headless installer
Make
pacquet install --frozen-lockfilefeature-complete withpnpm install --frozen-lockfile.Pacquet does not resolve dependencies in this stage. The user runs
pnpm install(orpnpm install --lockfile-only) to producepnpm-lock.yaml, thenpacquet install --frozen-lockfilematerializesnode_modulesfrom it. The lockfile is the contract between the two tools.This mirrors pnpm's internal
@pnpm/headlessboundary, so the seam is natural.Completed
.modules.yamlwrite and verify pacquet#331 (.modules.yamlwrite and verify)@pnpm/core-loggersschema (Run post install scripts after all dependencies were installed #345). Visual parity via@pnpm/cli.default-reporteris tracked under Tier 4 below (EACCESS error on runas #344).TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Correct snapshot filtering and lockfile loading; nothing downstream is sound until these are in place.
optionalDependenciessupport (umbrella). Without correctos/cpu/libcfiltering, every install over-fetches and may break on platforms other than the build host. Subsumes Install respects every snapshot — no os/cpu/libc filter for optional deps pacquet#266.crates/lockfile/src/resolution.rsuses#[serde(deny_unknown_fields)]and is missingTarballResolution.gitHosted,BinaryResolution, andVariationsResolution. A real v11pnpm-lock.yamlcontaining a github-tarball, a git-hosted package, or a runtime entry fails serde before the install dispatcher runs. Types covered piecemeal in Install git-hosted packages frompnpm-lock.yaml(frozen-lockfile) pacquet#436 and Install runtime dependencies (node@runtime:,deno@runtime:,bun@runtime:) frompnpm-lock.yamlpacquet#437; land them as a single unblocker so loading is correct before the install logic for each shape ships.package.jsondependencies don't match the lockfile importer entries, mirroring upstream'sallProjectsAreUpToDate. Today pacquet installs regardless of staleness, which silently masks lockfile/manifest drift in CI.Tier 2 — Real-world install enablers. Each unlocks a large slice of real pnpm repos and is largely independent of the others.
pacquet install --frozen-lockfilepacquet#431 — Support workspaces. Most real pnpm repos are workspaces; until this lands, pacquet can't install them at all.--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots pacquet#433 — Partial install with--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots. Closes the warm-install perf gap.hoistPatternandpublicHoistPatternpacquet#435 — Hoisting (hoistPattern,publicHoistPattern). Needed for ecosystem packages that rely on phantom dependencies and for editor tooling (eslint/prettier).Tier 3 — New resolution types and layout modes. Each is a self-contained extension to the install pipeline; the ordering inside this tier is mostly arbitrary.
pacquet install --frozen-lockfilepacquet#432 — Support the global virtual store dir (Stage 1: frozen-lockfile install + per-importer registry). Follow-ups: Implementpacquet store prunefor the global virtual store (#432 follow-up) pacquet#458 (prune sweep), Engine-agnostic GVS hash gating viaallowBuilds-drivenbuilt_dep_paths(#432 follow-up) pacquet#459 (engine-agnostic hash gating); CI auto-detect / CONFIG_CONFLICT validation / e2eglobalVirtualStore.tsport remain as bullets on Add global virtual store support topacquet install --frozen-lockfilepacquet#432.pnpm-lock.yaml(frozen-lockfile) pacquet#436 — Install git-hosted packages frompnpm-lock.yaml(frozen-lockfile).node@runtime:,deno@runtime:,bun@runtime:) frompnpm-lock.yamlpacquet#437 — Install runtime dependencies (node@runtime:,deno@runtime:,bun@runtime:).nodeLinker: 'hoisted'(umbrella). Alternative installer; orthogonal to Add hoisting support:hoistPatternandpublicHoistPatternpacquet#435. Landed via thereal-hoistcrate +link_hoisted_modules.rs; popularity-based ident preference completed in feat(pacquet): cover the gap innodeLinker: 'hoisted'#12510.Tier 4 — Consistency, polish, auxiliary. Quality-of-life fixes that close the parity gap once the structural work is in place.
.pnpm/that no longer appear in the lockfile (upstream'spruneVirtualStore). Sister to Partial install with--frozen-lockfile: read+writenode_modules/.pnpm/lock.yamland skip unchanged snapshots pacquet#433 — partial install decides what to add; this decides what to remove. Implemented as the mark-and-sweepStoreDir::pruneincrates/store-dir/src/prune.rs(reporter wiring still tracked under Use pnpm's @pnpm/cli.default-reporter for terminal output (via NDJSON) pacquet#344)..modules.yamlvalidation triggering re-install on layout change. Upstream'svalidateModulesforces a full re-import whennodeLinker/hoistPattern/publicHoistPattern/ etc. differ between runs. Implemented asmodules_consistent_withincrates/package-manager/src/install.rs, which gates the frozen-lockfile no-op fast path onlayoutVersion/nodeLinker/hoistPattern/publicHoistPattern/virtualStoreDirMaxLength/storeDir/virtualStoreDir/includedmatching.@pnpm/cli.default-reporterfor terminal output (via NDJSON). Brings visual parity withpnpm installon top of the reporting engine landed in feat(reporter): implement the reporting engine (NDJSON + Reporter trait) pacquet#345.Integration milestone (between Stage 1 and Stage 2)
Once the headless installer is solid, integrate pacquet as an opt-in install backend for pnpm itself — for example via an
install-backend=pacquetsetting, or a Node N-API addon hooked at the@pnpm/headlessseam. This ships Stage 1 value to every pnpm user, not just those who install pacquet directly, and provides a feedback firehose for parity validation before Stage 2 is built.Stage 2 — Implementing resolution
Make
pacquet installfeature-complete withpnpm install(no--frozen-lockfile). Pacquet readspackage.json+pnpm-workspace.yaml, resolves the dependency graph, writes apnpm-lock.yamlthat round-trips byte-identically with pnpm's, and materializesnode_modules. This unlocks the install subcommands that today only work via pnpm:install(default),install --lockfile-only,add,update,remove,outdated,why.Pacquet already has scaffolding from Stage 1:
crates/registry,crates/resolving-npm-resolver(metadata fetch + cache, named registries, mirroring, trust checks),crates/resolving-resolver-base, and a full lockfile writer (crates/lockfile/save_lockfile). Stage 2 fills in the parts that turn a manifest into a resolved graph.TODO
Ordered by implementation dependency — items lower in the list generally build on items above.
Tier 1 — Foundations. Nothing downstream is sound until these land.
@pnpm/resolving.parse-wanted-dependency(npm aliaspnpm:foo@npm:bar, scoped names, tag vs. semver) and the protocol-routing dispatcher at the top ofresolving/default-resolver/src/index.ts. Slots into the existingresolving-resolver-basecrate.installing/deps-resolver/src/resolveDependencyTree.tsandresolveDependencies.ts(~2.3k LoC combined). Owns dedupe by name (pkgIdsByName),preferredVersions, direct-vs-transitive context,resolutionsByDepPathcache,pendingNodes, linked-workspace tracking. Heart of this stage. Landed incrates/resolving-deps-resolver(resolve_workspace/resolve_importer/resolve_dependency_tree).save_lockfileround-trips Stage 1 inputs; confirm it serializes newpackages/snapshots/importers/catalogs/overrides/patchedDependenciesbyte-identically with pnpm sopnpm installafterpacquet installproduces no diff.crates/lockfile/save_lockfilenow writes all of these sections from a fresh resolution (env-document prefix preserved).Tier 2 — Per-protocol resolvers. Each unlocks a class of spec. Largely independent.
pickPackage.ts/pickPackageFromMeta.ts: semver matching, tag handling,allowedDeprecatedVersions,minimumReleaseAge. Metadata fetch already exists inresolving-npm-resolver.resolving/git-resolver:git+ssh,github:owner/repo#ref,#semver:^1selectors, GitHub-tarball preference. Pairs with the Stage-1 git-fetcher crate.resolving/tarball-resolver: directhttps://…/foo.tgzresolution + integrity verify.resolving/local-resolver:file:../foo,link:../foo, injected deps. The Stage-1directory-fetchercrate handles the fetch side.resolving/jsr-specifier-parser; wires into the JSR path already drafted inresolving-npm-resolver.node@runtime:/deno@runtime:/bun@runtime:. Stage 1 already loads them from the lockfile; resolution still has to choose a version and emit the entry.catalog:default/catalog:react18throughpnpm-workspace.yaml'scatalog(s):, then dispatch to npm. Snapshot the selections into the lockfile'scatalogs:block.workspace:*/workspace:^/workspace:~/workspace:1.2.3→link:lockfile entry + publish-time spec rewrite. Pacquet already loads workspaces (Add workspace support topacquet install --frozen-lockfilepacquet#431).Tier 3 — Resolution policies & constraints. Behaviors layered on top of the recursion that change which version wins or whether the install proceeds.
resolvePeers.ts(~1k LoC). Produces dep paths with peer hashes (foo@1.0.0(react@18.0.0)). Heaviest single port in this stage.pnpm.overridesfrompnpm-workspace.yaml. Apply atparseWantedDependencytime; record in the lockfile'soverrides:block.allowedDeprecatedVersions+ deprecation warning emission. Wire to the same NDJSONdeprecationchannel pnpm uses so@pnpm/cli.default-reporterformats them identically. Partial: theallowedDeprecatedVersionsconfig field is parsed, but nopnpm:deprecationevent is emitted yet (so the allow-list has nothing to suppress).blockExoticSubdeps. Reject git/tarball/file specs in transitive deps.patchedDependenciesresolution-side. Hash the patch and attachpkgIdWithPatchHashto dep paths so the installer picks the patched store entry.crates/lockfile/pkg_id_with_patch_hash.rsalready exists.allowNonAppliedPatches+verifyPatches. Partial:verify_patches(with anallow_unused_patchesparameter) and theERR_PNPM_UNUSED_PATCHdiagnostic exist incrates/patching, and the resolver collectsapplied_patches, but noallowNonAppliedPatchesconfig field is wired andverify_patchesis not yet invoked from the install flow.minimumReleaseAgeat resolve time. Today it gates global installs only; extend it so new transitive versions are also rejected, mirroring upstream's resolve-path enforcement.Tier 4 — Hooks & custom resolution. Both require running user JavaScript inside pacquet — design call needed.
.pnpmfile.cjs/pnpmfile.cjsexecution. Port thereadPackage,afterAllResolved,preResolution,filterLoghook surface fromhooks/pnpmfile. Implemented incrates/hooksby shelling out tonodevia a worker-pool runtime (node_runtime.rs);readPackage/preResolution/afterAllResolved/updateConfigare wired into the resolution pipeline. (filterLogtrait method exists but is not yet consumed by the reporter.)hooks.types.CustomResolver). Extracted from the pnpmfile and prepended to the resolver chain viaCustomResolverAdapter; covered bycrates/cli/tests/custom_resolvers.rs.Tier 5 — Commands unlocked by resolution. Stage 1 only delivers
pacquet install --frozen-lockfile. Once resolution lands, each of these is a CLI surface port that follows the underlying capability.pacquet install(default, non-frozen).pacquet install --lockfile-only.pacquet add(incl.-D,--save-peer,--save-exact,--save-prefix).pacquet update(incl.--latest,updateMatching,-i).pacquet remove.pacquet outdated.pacquet why.Tier 6 — Consistency, polish, auxiliary. Close the parity gap once the structural work is in place.
pnpm-lock.yaml(key ordering, anchor reuse, no spurious churn).add/remove/update. PortupdateProjectManifest.ts: respectsave-prefix,save-exact,save-workspace-protocol.PackageManifestadd/remove/update +crates/workspace-manifest-writer(format-preserving catalog writes) handle this.afterAllResolved/preResolutionlog-event parity so@pnpm/cli.default-reporterformats them identically. Partial:readPackage/afterAllResolvedcontext.log()calls emitpnpm:hookevents;preResolution'sinfo/warnlogging is not yet routed topnpm:channels.pnpm/test/install.ts,add.ts,update.ts,remove.ts,outdated.tsinto pacquet's test layout as each capability lands. Each command now has acrates/cli/tests/*.rssuite (install/add/update/remove/outdated/why + ~30 more); ongoing parity backfill tracked inpacquet/plans/TEST_PORTING.md.Integration milestone (between Stage 2 and Stage 3)
Once resolution is solid, extend the Stage-1 → Stage-2 opt-in seam (
install-backend=pacquet/ N-API) to route everydaypnpm installcalls through pacquet end-to-end — not just--frozen-lockfile. Stage 3 then targets the non-install command surface (publish,audit,dlx,exec,run,store).Stage 3 — Non-install command surface
Stages 1–2 cover the dependency-management surface:
install(incl.--frozen-lockfile/--lockfile-only),add,update,remove,outdated,why. Stage 3 tracks every remaining pnpm command. Pacquet'scrates/clialready implements a handful of these beyond install; the rest are unported.Status legend:
[x]= a handler exists in pacquet's CLI today (a parity audit against pnpm's flags/output is still required before it can be called complete);[ ]= not ported.Tracking
Grouped by the pnpm package that owns each command (see the imports at the top of
pnpm/src/cmd/index.ts). Aliases are listed after the primary name.Script & process —
@pnpm/exec.commandsrun/run-script(incl.test,startscript aliases)execdlxcreatetakenrestart(+stop)Store & cache —
@pnpm/store.commands,@pnpm/cache.commandsstore(+ subcommands)cat-filecat-indexfind-hashcacheProject scaffolding —
@pnpm/workspace.commandsinitWorkspace install helpers —
@pnpm/installing.commandsimportdedupeprunefetchlink/lnunlink/dislinkInspection & query —
@pnpm/deps.inspection.commandslist/lsll/lapeersview/info/show/v@Safortbugsrepodocs/homeCompliance —
@pnpm/deps.compliance.commandsaudit@zkochanlicensessbomBuild / lifecycle —
@pnpm/building.commandsrebuild/rb@zkochanapprove-builds@zkochanignored-builds@zkochanPatching —
@pnpm/patching.commands(taken by @spencer17x)patchpatch-commitpatch-removePublishing & releasing —
@pnpm/releasing.commandspublish(taken by @KSXGitHub)pack(taken by @KSXGitHub)pack-app@zkochandeploy@zkochanstageversion(taken by @Safort)Registry access —
@pnpm/registry-access.commands,@pnpm/auth.commandslogin/adduser(taken by @KSXGitHub)logout(taken by @KSXGitHub)whoami(taken by @Safort)ping@Safortsearch/s/se/finddeprecateundeprecateunpublishdist-tag/dist-tags@zkochanowner/ownersstarstarsunstarConfig & manifest —
@pnpm/config.commands,@pnpm/pkg-manifest.commandsconfig/c(+get,set) @zkochanpkgset-script/ss(taken by @Safort)Engine & self-management —
@pnpm/engine.pm.commands,@pnpm/engine.runtime.commandsself-update@zkochansetup@zkochanwith@zkochanruntime/rt@zkochanCompletion —
@pnpm/cli.commandscompletion@spencer17xpnpm CLI built-ins —
pnpm/src/cmdbin(taken by @exidniy)root(taken by @exidniy)clean/purgeci/clean-install/ic/install-cleaninstall-test/itrecursive/multi/mNot tracked:
access,edit,issues,prefix,profile,team,token,xmas— these are stubs in pnpm itself (they throwNOT_IMPLEMENTEDand defer to the npm CLI), so there is nothing to port.Stage 3 tracking section drafted by an agent (Claude Code, claude-opus-4-8).
Stage 2 plan drafted by an agent (Claude Code, claude-opus-4-7).