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.

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:

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

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://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.
  • 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 flow
  • lpm.json — runtime / task / publish config
  • lpm schema — emit either schema to stdout or a file