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.
This page is the schema-by-field reference. For the lpm add consumer flow, see lpm add.
Editor autocomplete
LPM publishes a JSON Schema for lpm.config.json at https://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://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.
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://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. - Agent skills installed under
.lpm/skills/<package>/. Skills are intentionally non-fatal (best-effort writes, swallowed errors) and run after the main transaction commits.
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 configlpm schema— emit either schema to stdout or a file