LPM CLI

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:

lpm.config.json
{
  "$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 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

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": "js" }
ValueMeaning
"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.

FieldTypeNotes
type"string" | "boolean" | "select"Default "string". boolean produces a confirm prompt; select produces a single- or multi-choice list.
labelstringPrompt label. Defaults to the field name.
defaultstring | boolean | numberInitial value. Native JSON types are accepted; legacy stringified values ("true", "false") are still honored for back-compat.
requiredbooleanWhen true, lpm add --yes fills the field with its default instead of skipping it.
multiSelectbooleanOnly meaningful when type is select. When true, the prompt accepts multiple values; the resolved inline-config value is comma-joined.
optionsarraySelectable 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.

FieldTypeNotes
srcstring (required)Source path or glob (relative to the tarball root). Trailing /** and /* are supported.
deststringDestination 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.
conditionobjectRequired 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 shapeWhat 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:

PathRollback behavior
Project package.jsonBytes restored to pre-lpm add state
lpm.lock, lpm.lockbBytes 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 projectPre-existing files restored to original bytes; newly-created files deleted
.lpm/install-hashAlways 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

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 flow
  • lpm.json — runtime / task / publish config
  • JSON Schemas — published schema URLs and lpm schema