LPM-cli

lpm store

Verify integrity, print the path, or wipe the global content-addressable package store.

lpm store <action>

Maintenance commands for the global content-addressable store at ~/.lpm/store/. Every lpm install deduplicates packages into this store and then links them into project node_modules trees — so the store is shared across every project on your machine.

For reachability-aware orphan cleanup, use lpm cache prune. lpm store covers integrity, the store path, and the blunt full wipe.

Examples

lpm store path                          # print the store root
lpm store verify                        # check integrity (fast)
lpm store verify --deep                 # also parse package.json + check name/version match
lpm store verify --fix                  # auto-fix what it can (e.g., refresh stale caches)
lpm store clean                         # wipe the entire store (rare)

Subcommands

SubcommandEffect
pathPrint the store root path
verifyIntegrity-check entries against their declared SRI
cleanBlunt wipe — removes everything under the store root

Locking model

Store operations coordinate through a writer-preference reader/writer protocol built on three advisory lock files:

  • Data lock at ~/.lpm/store/.gc.lock — the access lock. Shared by readers in the body, exclusive by writers in the body.
  • Writer-intent gate at ~/.lpm/store/.gc.lock.writer-intent — derived automatically. Held exclusive by the writer that's currently running, so new readers admitted at the gate are blocked until the writer finishes.
  • Writer-queue baton at ~/.lpm/store/.gc.lock.writer-queue — derived automatically. Held shared by every queued writer (including the one in the body). Readers probe it with a non-blocking exclusive try before attempting the gate; if the probe WouldBlocks, at least one writer is queued and the reader backs off. This closes the multi-writer leapfrog hole the gate alone leaves open — between writer W1 releasing the gate and writer W2 polling for it, the kernel could otherwise let queued readers grab gate-shared first and starve W2.

Surface:

  • Shared (multi-reader): lpm install, lpm patch, lpm rebuild, lpm approve-scripts, lpm store verify, and lpm cache prune (dry-run) always acquire the shared lock for the duration of the command. lpm audit and lpm query acquire it only when the project has LPM-store-backed packages, and they narrow the lock to just the store-touching slice (audit's behavioral-analysis pass; query's inventory load + lifecycle/build-state loop) — discovery, registry calls, and OSV calls run unlocked. Multiple readers run concurrently — two lpm install invocations in different shells share without contention.
  • Exclusive (single-writer): lpm store clean and lpm cache prune --apply. They take the queue baton shared (signaling "writer queued"), then the gate exclusive (blocking new reader admissions), then wait for in-flight readers to release the data lock.

Writer preference. Once a writer is queued, no new reader is admitted until every queued writer has run. The protocol holds even with multiple writers in line — W2 stays at the head of the queue across W1's release boundary because W2's queue-shared keeps the readers' probes blocked. Bias is intentional: writes are rare; install/query is common.

If contention lasts longer than one second, the blocked command emits a single Waiting for another lpm store operation to finish... line on stderr — no PID claim (the lock primitive doesn't expose owner introspection), but enough to confirm you aren't stuck. The hint fires once per acquisition, not on every poll iteration.

Locks release on normal exit and on panic — there's no stale-lock recovery to run by hand.

Verify

lpm store verify          # fast pass
lpm store verify --deep   # also parse package.json + lockfile cross-check
lpm store verify --fix    # regenerate stale behavioral-analysis caches

The fast pass walks every store entry and confirms three things: the directory exists, package.json is present, and the directory is non-empty. For v2 entries, it also checks that the link sidecar's source hash maps to the recorded object path and that the referenced object is present. Mismatches surface as corrupted entries with a remediation hint.

--deep extends the pass with two extra checks per entry:

  • Parses package.json and confirms the on-disk name and version match the directory's coordinates.
  • Cross-checks the integrity hash against lpm.lock in the current directory. Mismatches surface as integrity mismatch errors — they catch a re-published version with different bytes, accidental edits, or partial extraction. Run from a project root for this to do anything; without a lockfile, the cross-check is a no-op. If lpm.lock exists but cannot be read or parsed, deep verification exits non-zero instead of silently skipping the cross-check.

--fix writes one specific thing: missing or stale .lpm-security.json behavioral-analysis caches alongside each package. It implies the deep/cache-analysis path even when --deep is omitted. If a cache cannot be written, the command exits non-zero and the failed .lpm-security.json write appears in issues[]. It does not repair integrity mismatches, restore missing directories, or fix invalid package.json — those are reported as corrupted regardless. Use lpm store clean for the blunt repair.

The output distinguishes entries (one per directory on disk; may double-count during a v1↔v2 migration or under cross-project graph-key splits where the same name@version legitimately lives at multiple paths) from unique coordinates (deduped on name@version). Both counts appear in human + JSON output so an in-flight migration doesn't look like an inflated package count.

Clean (full wipe)

lpm store clean

Removes ~/.lpm/store/v1/ and ~/.lpm/store/v2/ in their entirety — every extracted package, every link entry, every cached integrity sidecar. The outer ~/.lpm/store/ directory plus the .gc.lock* control files stay so the next operation doesn't have to reinitialize them. The next lpm install rebuilds whatever it needs.

This is the "I want a from-scratch state" escape hatch. For normal maintenance use lpm cache prune, which is reference-aware and won't evict entries a project still needs.

JSON output

lpm store path --json     # { "success": true, "path": "/Users/you/.lpm/store" }
lpm store verify --json   # entry counts, unique coords, corrupted list
lpm store clean --json    # bytes freed across v1 + v2 subdirectories

verify envelope:

FieldMeaning
successfalse when any integrity/sidecar/lockfile/cache-write issue prevents a clean verify
check_kindpresence for the fast pass, lockfile_marker_consistency for --deep or --fix
bytes_integrity_recomputedAlways false today — verify checks markers/metadata, not a Merkle hash of extracted bytes
entries_verifiedNumber of store entries walked (one per directory on disk)
verifiedLegacy alias of entries_verified for older consumers
unique_coordsDeduped count over name@version — different from entries_verified during v1↔v2 migration
duplicated_entriesentries_verified − unique_coords
corruptedNumber of entries with at least one failed check
issues[]Per-entry human-readable issue strings (e.g., "sharp@0.34.4 — integrity mismatch: stored 'sha512-aaa...' != lockfile 'sha512-bbb...'")
securityMismatches / securityReanalyzedPresent on deep/fix runs; cache mismatches found vs. caches successfully refreshed

clean envelope:

FieldMeaning
removed_bytes / removedTotal bytes / formatted total across v1 + v2
v1_removed_bytes / v2_removed_bytesPer-subdirectory breakdown
v1_path / v2_pathAbsolute paths that were removed
pathLegacy alias of v1_path retained for pre-v2 consumers — prefer v1_path / v2_path

Flags

FlagApplies toEffect
--deepverifyAlso parse package.json, validate name/version, and cross-check lockfile integrity markers
--fixverifyRefresh stale/missing security caches; implies the deep/cache-analysis path

Plus the global flags.

See also