Built-in tools
How LPM CLI ships Oxlint, Biome, and Rolldown, plus how `lpm check` and `lpm pack` pick their backends.
LPM CLI ships a small set of built-in tools — Oxlint for lpm lint, Biome for lpm fmt, a type-check wrapper for lpm check, a package-build wrapper for lpm pack, and a bundler wrapper for lpm bundle. Oxlint, Biome, and Rolldown are managed through lpm plugin: lazy-downloaded on first use, cached, and pinned per-project. lpm check --engine tsgo uses a separate internal managed engine cache under ~/.lpm/engines.
This page covers the design — how the plugin system works, where binaries land, how versions are resolved, and the security model.
Why lazy-download
Bundling 50+ MB of toolchains into the LPM CLI binary would bloat every install and waste disk for users who don't lint. Bundling nothing and using npx adds hundreds of milliseconds of resolution overhead per invocation. The middle path: download tools on first use, cache them globally, hash-verify, share across every project.
Measured cost: lpm lint is 3 ms (vs npx oxlint 273 ms). lpm fmt is 3 ms (vs npx biome 340 ms). The win is not the tool itself — it's the resolution overhead npx pays per invocation.
What ships as a managed plugin today
| Plugin | Backs | Source | Format |
|---|---|---|---|
oxlint | lpm lint | oxc-project/oxc GitHub Releases | .tar.gz archive |
biome | lpm fmt | biomejs/biome GitHub Releases | Direct binary |
rolldown | lpm bundle | npm registry | Verified package graph |
Other tool backends sit beside the plugin system:
tscshells out through the same PATH-injection mechanismlpm runuses. PATH-injection prependsnode_modules/.binand falls back to the systemPATH, so a globally-installedtsccan run too — but the project-local install is the right answer.lpm doctorflags the system-only case astypescript_missing_for_tsconfig(warn).tsdownstays project-local.lpm packwalksnode_modules/.binfrom the current package up to the workspace root and requires a reachabletsdownthere. LPM CLI owns the workspace selection,--jsonenvelope, and stable top-level flags; the backend version still comes from the repo's ownpackage.json.tsgois an internal managed engine. LPM CLI downloads the pinned platform tarball on first use, verifies its SRI integrity, preserves the full extracted layout, and reuses it from~/.lpm/engines/tsgo/<version>/<platform>/on later runs.rolldownis user-facing as a managed plugin, even though its internal cache uses the managed engine layout.lpm bundleinstalls the rootrolldownpackage,@rolldown/pluginutils,@oxc-project/types, and the current platform binding into one verified cached layout under~/.lpm/engines/rolldown/<version>/<platform>/.
That split is intentional: tsc and tsdown preserve repo-local version ownership, tsgo remains an internal engine choice, and Rolldown gets the public plugin update UX while keeping the package-graph layout it needs internally.
Managed engine layout
~/.lpm/engines/
├── tsgo/
│ └── 7.0.0-dev.20260525.1/
│ └── darwin-arm64/
│ ├── lib/tsgo
│ ├── lib/lib.d.ts
│ └── .lpm-engine.json
└── rolldown/
└── 1.0.2/
└── darwin-arm64/
├── bin/cli.mjs
├── node_modules/@rolldown/pluginutils/
├── node_modules/@oxc-project/types/
├── node_modules/@rolldown/binding-darwin-arm64/
└── .lpm-engine.jsonThe engine sidecar records the engine identity, platform, entry path, every package's install subdir and tarball metadata, per-package SHA-256s, and a hash of the extracted layout. Reuse re-hashes the installed layout before the engine runs, so a stray binary without a sidecar, a tampered lib/*.d.ts file, or a stale Rolldown package set is treated as a cache miss instead of trusted state.
On-disk layout
~/.lpm/plugins/
├── oxlint/
│ └── 1.58.0/
│ └── darwin-arm64/
│ ├── oxlint ← downloaded binary
│ └── .lpm-plugin.json ← sidecar (verification metadata)
├── biome/
│ └── 2.4.10/
│ └── linux-x64/
│ ├── biome
│ └── .lpm-plugin.json
└── .version-cache.json ← cached "latest version" mapEach plugin gets a directory keyed by {version}/{platform}. The platform segment is required: a $LPM_HOME shared across architectures (cross-arch CI runners, NFS, Docker bind mounts) would otherwise let one host install a binary the next host can't exec.
Multiple versions can coexist; multiple platforms within a version can coexist too. The plugin .version-cache.json is the install-selection cache: each entry is the highest version that has been successfully verified-and-installed via lpm plugin update. Sticky by design — entries never expire by time, and the file is only written after a successful verified install (so a transient missing upstream .sha256, parse failure, or download failure cannot poison it and route every future invocation down a permanently-failing install path).
The .lpm-plugin.json sidecar records the plugin name, version, platform, asset URL, asset and on-disk binary checksums, and how the install was verified (bundled, upstream, or unverified-override). It is the source of truth for whether a cached binary may be reused — a bare binary with no sidecar, or a sidecar that fails any check, is treated as a cache miss and re-verified.
Version resolution
The lookup order, when lpm lint, lpm fmt, or lpm bundle starts:
- Pinned version in
lpm.json > tools.<name>— exact match. max(hardcoded, approved-cache)— whichever is newer, the version baked into the LPM CLI binary or one a previous successfullpm plugin updateapproved.- Auto-download if the resolved version isn't installed for the current platform yet.
Normal tool execution never touches the network for upstream-latest discovery — it only reads the bundled floor and sticky approved cache. To check upstream for newer releases, run lpm plugin outdated. To approve a newer version, run lpm plugin update <name>; that command runs the discovery probe, prints the download and verification phases for human runs, and only then approves the verified new version into the cache.
Pin a version per-project:
{
"tools": {
"oxlint": "1.57.0",
"biome": "2.4.8",
"rolldown": "1.1.3"
}
}Update to the latest (sticky — the new "latest" is cached for future invocations):
lpm plugin update # installed managed plugins
lpm plugin update oxlint # one plugin
lpm plugin update rolldown # Rolldown package graphForce a fresh download of an existing version (useful for corrupted installs):
LPM_FORCE_TOOL_INSTALL=1 lpm lintChecksum verification
Every install — whether the registry's pinned latest_version, a lpm.json-pinned version, or a freshly-fetched lpm plugin update result — must pass verification before it can be reused. Oxlint and Biome pass a SHA-256 checksum gate. Rolldown verifies npm SRI integrity for each package tarball, records per-package SHA-256s, and hashes the extracted layout.
For Oxlint and Biome, the checksum gate tries three sources in order:
- Bundled checksum — SHA-256 baked into the LPM CLI binary at build time. Used when the requested version equals the registry's hardcoded
latest_version. The fast path; no network round-trip. - Upstream sidecar — SHA-256 fetched from
<asset_url>.sha256(oxlint via cargo-dist, biome publishes per-asset sidecars). Used for pinned versions andlpm plugin updateresults. - Unverified override — only if
LPM_ALLOW_UNVERIFIED_PLUGINS=1is set. Recordsverification-source: unverified-overridein the sidecar; reuse requires the same env var on every subsequent invocation.
A mismatch at any stage is a hard error and the partial download is removed.
If neither bundled nor upstream verification is available — typically when the upstream sidecar is unreachable for an old or yanked release — and LPM_ALLOW_UNVERIFIED_PLUGINS is not set, the install fails with an actionable message:
refusing to install oxlint 1.42.0 for darwin-arm64: no bundled checksum and
upstream sidecar https://.../oxlint-aarch64-apple-darwin.tar.gz.sha256 is
unavailable. Update LPM CLI (newer releases ship pinned checksums for newer plugin
versions), pin a different version, or set LPM_ALLOW_UNVERIFIED_PLUGINS=1
to install without verification.The bundled-checksum table is declared inline in the registry:
PluginDef {
name: "oxlint",
latest_version: "1.58.0",
// ...
checksums: &[
("darwin-arm64", "422756416c840b77212c673ae4aa88c8ef27e0e09b8ae51aeed21a2cef6b7191"),
("darwin-x64", "f4d49bb4a636c8a0810e4c5a56adb02be9cf448570292a102d8a8835f7ba1980"),
// ...
],
}Reuse from cache
The sidecar — not bare file existence — gates reuse. Every cache hit re-validates:
schema_versionmatches the current LPM CLI binary,plugin_nameandversionmatch the request,platformmatches the current host,verification_sourceis compatible with the current trust posture (unverified-overrideis only honored whenLPM_ALLOW_UNVERIFIED_PLUGINS=1is set in the consuming process),- the binary's on-disk SHA-256 matches
binary_sha256recorded at install time (tamper detection).
Anything that fails gets treated as a cache miss and re-verified through the full download pipeline.
Supported platforms
Each plugin definition declares which platforms get a binary:
| Plugin | Platforms |
|---|---|
oxlint | darwin-arm64, darwin-x64, linux-x64, linux-arm64, win-x64 |
biome | darwin-arm64, darwin-x64, linux-x64, linux-arm64, win-x64 |
LPM CLI detects the current platform and downloads the matching asset. Unsupported platforms get an immediate error rather than a download attempt.
Manage installed plugins
lpm plugin list # installed plugins and latest locally approved versions
lpm plugin outdated # live upstream current vs latest check
lpm plugin update # pull latest for every installed managed plugin
lpm plugin update oxlint # one plugin
lpm plugin update rolldown # Rolldown package graph
lpm plugin remove biome # delete cached binariesSee lpm plugin for the full subcommand surface.
Why the registry is hardcoded
The plugin registry ships inside the LPM CLI binary, not as a runtime-fetched config. New plugins land in an LPM CLI release — there's no add-your-own-plugin path. This is deliberately conservative: tool distribution bugs ship with the LPM CLI binary and pass through the team's QA, instead of a registry config that could change between two lpm lint invocations on the same machine.
If you want to use a tool that isn't in the plugin set today, install it as a normal dep and run it through your package.json scripts:
lpm install -D eslint
lpm run lint # runs the package.json `lint` scriptCI behavior
In CI, plugins behave the same way as locally — first invocation downloads, subsequent invocations reuse. Cache ~/.lpm/plugins/ in your CI to skip the download step:
- name: Cache LPM CLI plugins
uses: actions/cache@v4
with:
path: ~/.lpm/plugins
key: lpm-plugins-${{ hashFiles('lpm.json') }}The cache key is the lpm.json hash — when tools versions change, the cache invalidates and re-downloads.
See also
lpm lint— backed by oxlintlpm fmt— backed by biomelpm check— project-localtscby default, managedtsgowhen selectedlpm pack— project-local tsdown with LPM CLI workspace orchestrationlpm bundle— managed Rolldown pluginlpm plugin— manage installed pluginslpm.jsontools — version pinning per project