lpm.config.json
Schema reference for the lpm.config.json file shipped at a tarball root to control `lpm add`.
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.
This page is the schema-by-field reference. For the lpm add consumer flow, see lpm add.
Editor autocomplete
LPM CLI publishes a JSON Schema for lpm.config.json at 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:
{
"$schema": "https://cli.lpm.dev/schemas/lpm.config.json",
"ecosystem": "js"
}Or emit the schema yourself:
lpm schema lpm.config.json # to stdout
lpm schema lpm.config.json -o config.schema.json # to a fileThe 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
{
"$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": "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": "@/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": {
"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": { "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": [
{ "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": {
"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:
{
"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 |
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
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
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 addresolvestarget_dirand 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 addfor SE-0292 source-package dependencies. Each recursive call owns its own transaction; the outerlpm addcommits 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
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
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
lpm add— the consumer-side flowlpm.json— runtime / task / publish config- JSON Schemas — published schema URLs and
lpm schema