# lpm.config.json (/docs/reference/lpm-config-json)



`lpm.config.json` is the **package author's** declaration of what `lpm add` should do when delivering this package's source into a consumer project. It lives at the tarball root and turns `lpm add` from a plain copy into a configurable installer — interactive prompts, conditional file copying, conditional dependency injection, import-path rewriting.

The file is **optional**. Without it, `lpm add` is a plain source copy of every file in the tarball. With it, `lpm add` becomes the installer the package author designed.

LPM CLI reads `lpm.config.json` with a 16 MiB limit enforced before JSON parsing. A missing file retains the plain-copy path; an oversized file fails instead of being treated as absent, so it cannot silently change file selection or dependency injection. See [local configuration size limits](/docs/project-setup#local-configuration-size-limits).

This page is the schema-by-field reference. For the `lpm add` consumer flow, see [`lpm add`](/docs/packages/add).

## Editor autocomplete [#editor-autocomplete]

LPM CLI publishes a JSON Schema for `lpm.config.json` at [`https://cli.lpm.dev/schemas/lpm.config.json`](https://cli.lpm.dev/schemas/lpm.config.json). Add a `$schema` line to get inline validation and field completion in any editor that supports JSON Schema:

```json title="lpm.config.json"
{
  "$schema": "https://cli.lpm.dev/schemas/lpm.config.json",
  "ecosystem": "js"
}
```

Or emit the schema yourself:

```bash
lpm schema lpm.config.json                       # to stdout
lpm schema lpm.config.json -o config.schema.json # to a file
```

The schema is hand-authored against the consumer code in the CLI (since the file's surface is partly dynamic — packages declare their own configurable fields). A corpus of real-world fixtures is validated against it on every CLI build, so the schema can't silently drift from the runtime.

The older `https://lpm.dev/schemas/lpm.config.json` URL remains a compatibility alias for existing packages.

When stdout is an interactive terminal, schema output may use syntax color. Redirected or piped output remains plain pretty-printed JSON, including when color is forced globally.

## Top-level shape [#top-level-shape]

```json title="lpm.config.json"
{
  "$schema":      "https://cli.lpm.dev/schemas/lpm.config.json",
  "ecosystem":    "js",
  "importAlias":  "@/components",
  "configSchema": { "<field>": { /* ConfigField */ } },
  "defaultConfig": { "<field>": "<value>" },
  "files": [ { "src": "...", "dest": "...", "include": "always" } ],
  "dependencies": { "<configKey>": { "<value>": ["dep1", "dep2"] } }
}
```

Every top-level field is optional. The minimal valid file is `{}`.

## `ecosystem` [#ecosystem]

```json
{ "ecosystem": "js" }
```

| Value            | Meaning                                                                                                                                                     |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `"js"` (default) | JavaScript / TypeScript source. Default install dir resolves via framework detection (Next.js → `components/`, Vite → `src/components/`, etc.).             |
| `"swift"`        | Swift source. Default install dir is `Sources/<target>/` for SPM projects, or `Packages/LPMComponents/Sources/<target>/` when an Xcode project is detected. |

Drives the default install-dir resolution. Override via `lpm add --path <dir>`.

## `importAlias` [#importalias]

```json
{ "importAlias": "@/components" }
```

The author's import alias prefix used in the source files. `lpm add` rewrites occurrences in the copied source to match the consumer project's `tsconfig.json > paths` or the explicit `--alias` flag. Without this field no rewriting happens; imports are copied verbatim.

## `configSchema` [#configschema]

```json
{
  "configSchema": {
    "component": {
      "type": "select",
      "label": "Which component?",
      "options": ["dialog", "popover", "menu"],
      "default": "dialog",
      "required": true
    },
    "withTests": {
      "type": "boolean",
      "label": "Include tests?",
      "default": true
    }
  }
}
```

Map of configurable field name → field specification. Each entry drives an interactive prompt during `lpm add` and feeds the inline-config map used by `files[].condition` and `dependencies` lookups.

| Field         | Type                                | Notes                                                                                                                                                                                             |
| ------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `type`        | `"string" \| "boolean" \| "select"` | Default `"string"`. `boolean` produces a confirm prompt; `select` produces a single- or multi-choice list.                                                                                        |
| `label`       | string                              | Prompt label. Defaults to the field name.                                                                                                                                                         |
| `default`     | string \| boolean \| number         | Initial value. Native JSON types are accepted; legacy stringified values (`"true"`, `"false"`) are still honored for back-compat.                                                                 |
| `required`    | boolean                             | When `true`, `lpm add --yes` fills the field with its `default` instead of skipping it.                                                                                                           |
| `multiSelect` | boolean                             | Only meaningful when `type` is `select`. When `true`, the prompt accepts multiple values; the resolved inline-config value is comma-joined.                                                       |
| `options`     | array                               | Selectable options. Plain strings (`["dialog", "menu"]`) are shorthand for `{value, label}` pairs where both equal the string. Object form: `[{"value": "dialog", "label": "Dialog component"}]`. |

## `defaultConfig` [#defaultconfig]

```json
{ "defaultConfig": { "withTests": true, "component": "dialog" } }
```

Map of configurable field name → default value, overriding any `default` declared in the matching `configSchema` entry. Useful for sharing common defaults across packages without duplicating the field spec. Same value-type rules as `configSchema.<field>.default` (native booleans, numbers, and strings; legacy stringified values still accepted).

## `files` [#files]

```json
{
  "files": [
    { "src": "src/index.ts" },
    { "src": "src/styles.css", "dest": "styles/" },
    {
      "src": "src/test.ts",
      "include": "when",
      "condition": { "withTests": true }
    },
    { "src": "src/private/internal.ts", "include": "never" }
  ]
}
```

Ordered list of file copy rules. When `files` is omitted, `lpm add` copies every file in the tarball.

| Field       | Type                            | Notes                                                                                                                                                                                                                                                                               |
| ----------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `src`       | string (required)               | Source path or glob (relative to the tarball root). Trailing `/**` and `/*` are supported.                                                                                                                                                                                          |
| `dest`      | string                          | Destination path (relative to the consumer project's resolved install dir). Trailing `/` treats the value as a directory; the source filename is preserved. Omit to keep the source-relative path.                                                                                  |
| `include`   | `"always" \| "when" \| "never"` | Default `"always"`. `"when"` requires every entry in `condition` to match. `"never"` skips.                                                                                                                                                                                         |
| `condition` | object                          | Required when `include` is `"when"`. Map of config field name → expected value. Native booleans and numbers are accepted alongside strings. Comma-separated multi-select values match if any element equals the expected value. Missing keys default to "include" (all-by-default). |

## `dependencies` [#dependencies]

```json
{
  "dependencies": {
    "icons": {
      "lucide": ["lucide-react"],
      "heroicons": ["@heroicons/react"]
    },
    "withMotion": {
      "true": ["framer-motion"]
    }
  }
}
```

Conditional dependency injection map. Outer keys match `configSchema` field names; inner keys match the user's selected value(s) for that field; values are arrays of **dependency entries** added to the consumer's `package.json`. Each entry is either a bare name or a `name@range` spec:

```json
{
  "dependencies": {
    "icons": {
      "lucide": [
        "lucide-react",                  // bare → ^resolvedLatest
        "lucide-react@^0.400.0",         // explicit range, preserved verbatim
        "lodash@4.17.21",                // explicit exact, preserved verbatim
        "react@latest",                  // dist-tag → resolved + saved per policy
        "@lpm.dev/owner.helpers@^1"      // any registry, range preserved
      ]
    }
  }
}
```

The save-spec policy mirrors `lpm install <pkg>`:

| Entry shape                            | What lands in consumer's `package.json`                                                              |              |
| -------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------ |
| Bare name (e.g., `"react"`)            | `^resolvedLatest` (e.g., `"^18.3.1"`), respecting the consumer's \`\~/.lpm/config.toml > save-prefix | save-exact\` |
| `name@^range` / `name@~range` etc.     | Preserved verbatim                                                                                   |              |
| `name@1.2.3` (exact)                   | Preserved verbatim                                                                                   |              |
| `name@latest` / `name@beta` (dist-tag) | Stable resolved → caret default; prerelease resolved → exact (no caret widening)                     |              |
| `name@*` (explicit wildcard)           | Preserved as `"*"` (the only path that lands `*` — never the default)                                |              |

### Registry agnostic [#registry-agnostic]

Names from any registry are accepted — npm-published, private-registry (declared in `.npmrc`), and `@lpm.dev/*` packages all flow through the same path. The package manager `lpm add` was invoked with (`--pm <lpm|npm|pnpm|yarn|bun|auto>`, where `auto` detects the project's existing package manager; default `lpm`) runs the install.

### Resolve-then-write, with rollback [#resolve-then-write-with-rollback]

`lpm add` resolves every bare/dist-tag entry against the registry **before** mutating `package.json`. Auth or access errors (a private package the user lacks a token for, an `@lpm.dev` marketplace package without a seat, a misspelled name) abort the whole `lpm add` cleanly — the manifest stays untouched. Without this fail-fast posture, an unresolvable entry would leave a stranded `*` range that the trailing install can't recover.

If resolution succeeds but a later step fails — file copy, manifest mutation, or trailing install (any `--pm`) — `lpm add` rolls back atomically and exits with the underlying error. The transaction snapshots:

| Path                                                                                                     | Rollback behavior                                                          |
| -------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| Project `package.json`                                                                                   | Bytes restored to pre-`lpm add` state                                      |
| `lpm.lock`, `lpm.lockb`                                                                                  | Bytes restored, or removed if absent before                                |
| Selected PM's lockfile (`package-lock.json` / `pnpm-lock.yaml` / `yarn.lock` / `bun.lock` + `bun.lockb`) | Bytes restored, or removed if absent before                                |
| Every source file `lpm add` copied into the project                                                      | Pre-existing files restored to original bytes; newly-created files deleted |
| `.lpm/install-hash`                                                                                      | Always invalidated so the next `lpm install` re-derives clean state        |

What's outside the rollback surface, by design:

* **The install directory itself**, plus any parent directories materialized to host the copied source files. `lpm add` resolves `target_dir` and creates it (and any canonical parents on the resolution chain) before the transaction opens — that mkdir is what lets the transaction snapshot canonical-pinned dest paths in the first place. A rolled-back failure leaves these directories on disk; the files inside them were rolled back, so the directories may end up empty.
* **Recursive Swift `lpm add`** for SE-0292 source-package dependencies. Each recursive call owns its own transaction; the outer `lpm add` commits before triggering recursion so a Swift-dep failure doesn't roll back the root package's already-applied mutations.
* **Package-published agent skills** reconciled under `.lpm/skills/<package>/`. Fetch, validation, and write errors fail the install instead of leaving a partial or stale package-owned skill set.

If `lpm add` fails inside the rollback boundary, every file and lockfile under the transaction snaps back to its pre-`lpm add` state. The remaining residue is empty directories under the install target (cheap to clean up; `rm -rf` the directory if you don't want it). Re-run `lpm add` after fixing the underlying error to converge.

### Author opt-out of legacy fallback [#author-opt-out-of-legacy-fallback]

Declaring `dependencies` — even with conditional branches that don't match the consumer's config — opts out of the legacy fallback below. An empty match is the author's deliberate signal "no deps for this configuration," not a request to fall back.

### Legacy fallback [#legacy-fallback]

When `dependencies` is omitted entirely, `lpm add` falls back to reading `dependencies` + `peerDependencies` from the package's own `package.json` (legacy path). Each entry's declared range carries through and is preserved verbatim. The same registry-agnostic policy applies — every entry, including `@lpm.dev/*`, is installed.

## See also [#see-also]

* [`lpm add`](/docs/packages/add) — the consumer-side flow
* [`lpm.json`](./lpm-json) — runtime / task / publish config
* [JSON Schemas](/docs/reference/schemas) — published schema URLs and `lpm schema`
