LPM CLI

Migrating from pnpm

Convert a pnpm-lock.yaml project to LPM CLI, isolated layout preserved.

lpm migrate converts a pnpm project to LPM CLI. It writes a version 12 staging lockfile, then runs an online install by default.

The install writes version 13 with exact package-instance identities. The command creates backups before it changes files.

The migration shape is the same as migrating from npm. This page covers the pnpm-specific differences.

What carries over cleanly

  • Isolated node_modules layout for workspaces and peer conflicts. Workspace projects auto-flip to LPM CLI's isolated layout (symlinks into a virtual store) — same shape pnpm uses, so your imports keep working unchanged and you get the same phantom-dep protection. Single-package projects start hoisted, but default installs auto-switch to isolated when resolution detects incompatible peer requirements. Force isolated with lpm install --linker=isolated or package.json > lpm > linker = "isolated" if you want pnpm's shape there for every install.
  • .pnpmfile.cjs is unaffected. LPM CLI doesn't use it, but doesn't delete it either. If you have one, decide whether you still need it.
  • Workspaces and catalogs. LPM CLI reads package.json > workspaces first; if it's absent, it falls back to pnpm-workspace.yaml for member globs. It also reads pnpm-style pnpm-workspace.yaml > catalog and catalogs for default and named catalog entries, so catalog: dependencies keep resolving without moving catalog data into package.json. If cleanupUnusedCatalogs: true is set in pnpm-workspace.yaml, LPM CLI prunes unused catalog entries after successful installs.
  • .npmrc-declared private registries carry over verbatim. LPM CLI honors .npmrc for routing.

Run the migration

lpm migrate --dry-run   # preview
lpm migrate             # do it

Same flow as the npm path:

  1. Detect pnpm, parse pnpm-lock.yaml, convert.
  2. Write a version 12 staging lpm.lock. If the graph fits the binary format, write lpm.lockb.
  3. Touch .npmrc (with --no-npmrc to skip), backing up the original.
  4. Run an online lpm install. This step writes version 13 and removes the staging lpm.lockb.
  5. Run build + test to verify (skip with --skip-verify).
  6. Print summary + CI hint.

Optional flags

FlagEffect
--dry-runParse + convert only, write nothing
--forceOverwrite an existing lpm.lock
-y, --yesReserved. The flow is non-interactive today, so this flag is a no-op. It does NOT imply --force.
--no-installConvert lockfile only, skip install
--skip-verifySkip build + test verification
--no-npmrcDon't touch .npmrc
--ciGenerate a CI workflow template
--no-ciSuppress CI hint
--rollbackRestore from .backup files

Verify and commit

If you used --no-install, run lpm install before the offline command.

lpm install --offline       # confirm reproducibility
lpm test
lpm lint
lpm fmt --check
git add lpm.lock .npmrc package.json
test ! -f lpm.lockb || git add lpm.lockb
git rm pnpm-lock.yaml pnpm-lock.yaml.backup .npmrc.backup
git commit -m "Migrate to LPM CLI"

What lpm migrate translates from package.json

  • pnpm.overrides is auto-translated to package.json > lpm.overrides with the same selector grammar. lpm install does not use the original pnpm.overrides block as resolution input. Successful entries land in lpm.overrides, and the original block stays in place for parallel pnpm use. The translation validates every entry before it changes a file. Invalid values and merge conflicts stop the migration without a partial update.

  • pnpm.patchedDependencies is auto-translated to package.json > lpm.patchedDependencies with originalIntegrity resolved from the migrated lockfile. Each patch file is copied to LPM CLI's canonical patches/<safe_key>.patch location (or treated as a validated no-op if the source is already at that path), and lpm.lock > [patches] records the patch path + SHA-256 so the first LPM CLI install can verify the bytes it replays. The plan validates everything up front: missing source files, paths outside the project root, paths through symlinked ancestors, directory targets, and any package whose lockfile entry is missing or has no integrity hash all abort the migration with a structured error before any disk mutation. Like the overrides block, the original pnpm.patchedDependencies is left in place after migration.

  • pnpm.peerDependencyRules is auto-translated to package.json > lpm.peerDependencyRules with the same shape verbatim — ignoreMissing, allowedVersions, and allowAny all carry over. List entries (ignoreMissing / allowAny) are unioned with any pre-existing LPM CLI-side entries; map entries (allowedVersions) merge selector-by-selector. Same selector with the same range is a no-op idempotent merge; same selector with a different range is a hard conflict that aborts the migration before any disk mutation. Range strings that don't parse as valid semver, and selector keys that don't match LPM CLI's grammar, are surfaced up-front as parse errors with the offending entry named. allowedVersions selectors mirror lpm.overrides: bare peer names ("react"), scoped peers ("@scope/foo"), and parent-scoped forms ("foo>react", "foo@^2>react", "@scope/foo@^2>react") all carry over verbatim; multi-segment paths and bare-name-with-version forms ("foo@2" without >) are rejected on both surfaces. Pattern semantics (@scope/*, *-suffix, etc.) work for ignoreMissing and allowAny. The same parser runs at install time, so hand-edits to lpm.peerDependencyRules after migration fail loud (LpmError::Script) instead of silently no-op'ing.

After migration, lpm install is silent when the LPM CLI fields cover every pnpm entry. Later drift causes a human-output warning that suggests lpm migrate.

The --json flag suppresses that stderr warning. Use lpm doctor --json to detect drift through pnpm_overrides_drift, pnpm_patches_drift, and pnpm_peer_rules_drift.

pnpm-specific gotchas

  • pnpm.overrides is migration input, not install input. Run lpm migrate after each pnpm-side change, or update lpm.overrides directly.
  • pnpm-workspace.yaml is read as a fallback when package.json > workspaces is absent, and its catalog / catalogs blocks are read as root catalogs. If the same catalog package entry exists in both package.json > catalogs and pnpm-workspace.yaml, LPM CLI uses the package.json value. cleanupUnusedCatalogs: true is honored unless package.json > lpm.cleanupUnusedCatalogs explicitly says otherwise.
  • shamefully-hoist in .npmrc is pnpm-specific. LPM CLI's closest equivalent is lpm install --linker=hoisted or package.json > lpm > linker = "hoisted"; hoisted mode still uses LPM CLI's v2 virtual-store layout instead of flattening every transitive dependency to the project root.

Rollback

lpm migrate --rollback

Restores pnpm-lock.yaml, .npmrc (if touched), .gitattributes, and package.json (rolled back when the pnpm.* translations mutated it) to pre-migration state. Files the migration newly created — lpm.lock, lpm.lockb, any new .gitattributes, and any patch files copied to patches/ — are removed.

Lifecycle scripts

Bare lpm install runs your root project lifecycle, including prepare. Dependency lifecycle scripts (postinstall, etc.) remain under script-policy: deny by default. After the migration's lpm install step, run:

lpm rebuild

For packages that pnpm allowed to run scripts but LPM CLI hasn't trusted yet, lpm approve-scripts walks the blocked set interactively.

If your project depends on postinstall for a tight inner loop and you're confident about the trust set:

lpm install --auto-build

Auto-runs lpm rebuild for trusted packages right after install. If a trusted lifecycle script fails, install exits non-zero. Or set package.json > lpm > scripts.autoBuild: true to make it sticky.

See also