# Built-in tools (/docs/dev/builtin-tools)



LPM CLI ships a small set of built-in tools — [Oxlint](https://oxc.rs) for [`lpm lint`](/docs/dev/lint), [Biome](https://biomejs.dev) for [`lpm fmt`](/docs/dev/fmt), a type-check wrapper for [`lpm check`](/docs/dev/check), a package-build wrapper for [`lpm pack`](/docs/dev/pack), and a bundler wrapper for [`lpm bundle`](/docs/dev/bundle). Oxlint, Biome, and Rolldown are managed through [`lpm plugin`](/docs/dev/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 [#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 [#what-ships-as-a-managed-plugin-today]

| Plugin     | Backs                            | Source                            | Format                 |
| ---------- | -------------------------------- | --------------------------------- | ---------------------- |
| `oxlint`   | [`lpm lint`](/docs/dev/lint)     | `oxc-project/oxc` GitHub Releases | `.tar.gz` archive      |
| `biome`    | [`lpm fmt`](/docs/dev/fmt)       | `biomejs/biome` GitHub Releases   | Direct binary          |
| `rolldown` | [`lpm bundle`](/docs/dev/bundle) | npm registry                      | Verified package graph |

Other tool backends sit beside the plugin system:

* `tsc` shells out through the same PATH-injection mechanism `lpm run` uses. PATH-injection prepends `node_modules/.bin` and falls back to the system `PATH`, so a globally-installed `tsc` can run too — but the project-local install is the right answer. [`lpm doctor`](/docs/infra/doctor) flags the system-only case as `typescript_missing_for_tsconfig` (warn).
* `tsdown` stays project-local. [`lpm pack`](/docs/dev/pack) walks `node_modules/.bin` from the current package up to the workspace root and requires a reachable `tsdown` there. LPM CLI owns the workspace selection, `--json` envelope, and stable top-level flags; the backend version still comes from the repo's own `package.json`.
* `tsgo` is 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.
* `rolldown` is user-facing as a managed plugin, even though its internal cache uses the managed engine layout. [`lpm bundle`](/docs/dev/bundle) installs the root `rolldown` package, `@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 [#managed-engine-layout]

```text
~/.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.json
```

The 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 [#on-disk-layout]

```text
~/.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" map
```

Each 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 [#version-resolution]

The lookup order, when [`lpm lint`](/docs/dev/lint), [`lpm fmt`](/docs/dev/fmt), or [`lpm bundle`](/docs/dev/bundle) starts:

1. **Pinned version** in `lpm.json > tools.<name>` — exact match.
2. **`max(hardcoded, approved-cache)`** — whichever is newer, the version baked into the LPM CLI binary or one a previous successful `lpm plugin update` approved.
3. **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:

```json title="lpm.json"
{
  "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):

```bash
lpm plugin update                # installed managed plugins
lpm plugin update oxlint         # one plugin
lpm plugin update rolldown       # Rolldown package graph
```

Force a fresh download of an existing version (useful for corrupted installs):

```bash
LPM_FORCE_TOOL_INSTALL=1 lpm lint
```

## Checksum verification [#checksum-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:

1. **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.
2. **Upstream sidecar** — SHA-256 fetched from `<asset_url>.sha256` (oxlint via cargo-dist, biome publishes per-asset sidecars). Used for pinned versions and `lpm plugin update` results.
3. **Unverified override** — only if `LPM_ALLOW_UNVERIFIED_PLUGINS=1` is set. Records `verification-source: unverified-override` in 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:

```text
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:

```rust
PluginDef {
    name: "oxlint",
    latest_version: "1.58.0",
    // ...
    checksums: &[
        ("darwin-arm64", "422756416c840b77212c673ae4aa88c8ef27e0e09b8ae51aeed21a2cef6b7191"),
        ("darwin-x64",   "f4d49bb4a636c8a0810e4c5a56adb02be9cf448570292a102d8a8835f7ba1980"),
        // ...
    ],
}
```

### Reuse from cache [#reuse-from-cache]

The sidecar — not bare file existence — gates reuse. Every cache hit re-validates:

* `schema_version` matches the current LPM CLI binary,
* `plugin_name` and `version` match the request,
* `platform` matches the current host,
* `verification_source` is compatible with the current trust posture (`unverified-override` is only honored when `LPM_ALLOW_UNVERIFIED_PLUGINS=1` is set in the consuming process),
* the binary's on-disk SHA-256 matches `binary_sha256` recorded at install time (tamper detection).

Anything that fails gets treated as a cache miss and re-verified through the full download pipeline.

## Supported platforms [#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 [#manage-installed-plugins]

```bash
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 binaries
```

See [`lpm plugin`](/docs/dev/plugin) for the full subcommand surface.

## Why the registry is hardcoded [#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:

```bash
lpm install -D eslint
lpm run lint           # runs the package.json `lint` script
```

## CI behavior [#ci-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:

```yaml
- 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 [#see-also]

* [`lpm lint`](/docs/dev/lint) — backed by oxlint
* [`lpm fmt`](/docs/dev/fmt) — backed by biome
* [`lpm check`](/docs/dev/check) — project-local `tsc` by default, managed `tsgo` when selected
* [`lpm pack`](/docs/dev/pack) — project-local tsdown with LPM CLI workspace orchestration
* [`lpm bundle`](/docs/dev/bundle) — managed Rolldown plugin
* [`lpm plugin`](/docs/dev/plugin) — manage installed plugins
* [`lpm.json` tools](/docs/reference/lpm-json#tools) — version pinning per project
