LPM CLI

lpm run

Run package.json scripts — parallel, cached, watchable, workspace-aware.

lpm run <scripts...> [-- script-args...]

Runs one or more scripts entries from package.json. With multiple independent scripts, sequential mode preserves the command-line order. Dependencies always run before their dependents. Pass -p to run independent tasks in parallel.

lpm.json is optional. Without it, the command uses package.json scripts and standard environment files.

It also uses a Node selector in .nvmrc or .node-version. Task dependencies and task caching require lpm.json.

Shorthand: lpm <script-or-bin> without run

lpm falls through to the same task runner as lpm run <name> when the first argument isn't a known subcommand. That means plain shorthand script invocations pick up lpm.json meta-tasks, dependsOn expansion, caching, and the normal script/task resolution rules.

lpm build              # equivalent to lpm run build
lpm typecheck          # equivalent to lpm run typecheck
lpm verify             # equivalent to lpm run verify (including lpm.json meta-tasks)
lpm jest --watch       # runs node_modules/.bin/jest --watch if no script/task named "jest" exists

The lookup only kicks in for unknown subcommand names — built-in commands always win. So lpm test runs the Vitest/Jest forwarder (built-in lpm test), not your "test" script entry. To force the script entry when a built-in conflicts, use lpm run <name> explicitly.

If no package.json script or lpm.json task matches the shorthand name, LPM CLI tries a project-local binary with the same resolver as lpm exec. That fallback searches only node_modules/.bin directories for the project/workspace; it does not run arbitrary system PATH binaries and it does not use a shell. Scripts and tasks still win over same-named local binaries.

Path-like invocations such as lpm scripts/seed.ts use the source-file runner instead of the shorthand lookup.

Use lpm run when you need runner flags. lpm build --filter web forwards --filter web to your script; lpm run build --filter web applies LPM CLI's workspace selection.

Hidden package.json scripts

Package scripts whose name starts with . are hidden helper scripts:

package.json
{
  "scripts": {
    "build": "lpm run .build",
    ".build": "tsc -p tsconfig.build.json"
  }
}

lpm run .build and the shorthand lpm .build are rejected when invoked directly from your shell. Hidden scripts are also omitted from missing-script suggestions and script listings.

Visible scripts may call hidden scripts, and lpm.json task dependencies may reference them:

lpm.json
{
  "tasks": {
    "build": {
      "dependsOn": [".build"]
    }
  }
}

Use this for internal building blocks you do not want to expose as top-level project commands.

Examples

lpm run build
lpm run lint test                 # sequential
lpm run -p lint test typecheck    # parallel
lpm run dev -- --port 4000        # forward args to the script
lpm run build --filter web        # workspaces: build only the web member
lpm run build --all               # build every workspace member
lpm run test --affected           # only members affected by recent changes
lpm run test --filter web --no-bail  # keep running selected members after a failure
lpm run build --filter './packages/*' --workspace-concurrency 2
lpm run test --watch              # re-run on file changes
lpm run --env=staging start       # load .env.staging before running

Forwarding arguments

Anything after -- is passed to the script as argument data:

lpm run dev -- --port 4000 --host 0.0.0.0
# runs: <whatever "dev" expands to> --port 4000 --host 0.0.0.0

Shell metacharacters in an argument do not become another shell command. On Windows, LPM CLI rejects arguments that contain line breaks.

cmd.exe cannot receive these arguments safely.

Parallel execution

lpm run -p lint test typecheck

Runs all three scripts concurrently, respecting any task dependencies declared in lpm.json. Output is buffered per task by default — pass --stream for live prefixed output. With --no-bail, the runner keeps going past failed tasks and reports the failures at the end.

Workspaces

lpm run build --all                    # every member, topological order
lpm run build --filter web             # one member
lpm run build --filter web --filter api # two members (union)
lpm run build --filter-prod ...shared  # prod graph closure only
lpm run test --affected                # members touched since base branch
lpm run test --affected --base develop # change base branch (default: main)
lpm run test --affected --changed-files-ignore-pattern '**/README.md'
lpm run test --affected --test-pattern '**/*.test.js'

--filter accepts the filter grammar: exact name (foo), glob (@scope/*), combined name/path (@scope/*{./apps/web}), path glob (./apps/*), forward closure (foo...), reverse closure (...foo), exclusion (!foo).

Full grammar also includes path exact ({./apps/web}), git ref ([origin/main]), and the dependency/dependent-closure variants foo^... and ...^foo.

--filter-prod <EXPR> accepts the same grammar, but closure operators ignore devDependencies edges. Use it when a production deploy/build/test selection should include runtime workspace dependents without pulling in test-only packages.

--all is mutually exclusive with --filter, --filter-prod, and --affected; filters compose with --affected (the affected set is unioned with the filter result).

--all, filters, and --affected require a workspace.

--filter core does not match @babel/core — substring matching is not supported. Use --filter '*/core' for that.

--fail-if-no-match makes a typo'd filter exit non-zero — recommended in CI.

--no-bail continues independent selected members after a member fails. A member does not run if its explicit ^task dependency fails. The command exits non-zero if a selected member fails.

If a selected task uses ^task, LPM CLI also runs that task in the selected member's upstream workspace dependencies. This expansion covers the complete dependency chain. It can include packages outside the original filter. The command checks the expanded graph before it starts a task process.

--workspace-concurrency <N> caps how many selected workspace members run at once within each topological level. The command reads persistent defaults from lpm.toml > [workspace].concurrency, then ~/.lpm/config.toml > workspace-concurrency, then falls back to the host's available parallelism.

--changed-files-ignore-pattern <glob> drops matching paths before [git-ref] filters or --affected map changed files to workspace members. It also reads project defaults from lpm.toml > [workspace].changed-files-ignore-pattern.

--test-pattern <glob> marks matching changed files as test-only. Directly changed packages still run, but those packages do not seed dependent fan-out when all their changes match the test pattern. It also reads project defaults from lpm.toml > [workspace].test-pattern.

Caching

Task results are cached only when the task opts in through lpm.json. The task needs cache: true and non-empty outputs.

The key includes script arguments, the child environment, runtime identities, and selected Node and Bun executable fingerprints.

A cached task with unchanged inputs replays without execution. Bypass the cache with --no-cache.

Single-script human output prints a compact stderr header before the script's own output:

 Running build
    cache    miss
    command  next build

 build · success in 4.82s

On a cache hit, LPM CLI replays the captured stdout/stderr and reports that the task was restored from cache.

Buffered, streamed-with-prefix, and cache-replayed task output is sanitized before LPM renders it. A child process that receives stdout/stderr directly retains raw terminal ownership so interactive programs and terminal-aware tools keep their normal behavior. See Terminal output security for the full boundary, including JSON semantics.

The local cache lives under ~/.lpm/cache/tasks/. With lpm.json > remoteCache.enabled, LPM CLI checks the hosted cache after the local cache and uploads successful task outputs after the local cache write. Remote outages, 404s, bad signatures, and corrupt artifacts are misses — they do not fail a successful local build.

Manage cache state with lpm cache. See Task runner — Remote cache for setup.

JSON output

lpm run build --json
lpm run build --filter web --json

lpm run --json emits an LPM CLI metadata envelope, even for a single script in a single package. Single-package task output is captured into the task result instead of being passed through as raw stdout. The envelope includes success, task counts (total, passed, failed), and tasks[] entries with each task name, status, exit code, duration, cache-hit state, and skip reason.

Workspace selections emit success, packages, succeeded, and duration_ms. packages counts each scheduled member after ^task expansion. This count excludes selected members that do not define the task. succeeded counts the members that completed successfully.

If a workspace filter matches nothing and --fail-if-no-match is not set, JSON mode emits a successful zero-package envelope.

Environment files

lpm run start --env=staging

With --env=staging, LPM CLI loads .env, .env.local, .env.staging, then .env.staging.local. Without --env, it loads .env, then .env.local.

The environment order is --env, tasks.<name>.env, env.<name>, then the default files. Thus, the command-line mode overrides all lpm.json mappings.

Vault-backed env secrets override file values after dotenv loading.

An environment file must resolve inside the project directory. LPM CLI rejects a configured path or symlink that resolves outside the project.

Runtime-hook env names such as NODE_OPTIONS, LD_PRELOAD, LD_AUDIT, BASH_ENV, and DYLD_INSERT_LIBRARIES are ignored when they come from env files, env secrets, or env-schema defaults. LPM CLI also strips inherited values for these names before spawning the script.

LPM CLI also strips inherited credential names such as LPM_TOKEN and NPM_TOKEN.

It strips names that end in _TOKEN, _SECRET, or _PASSWORD. Project-resolved values still use the environment policy.

--no-env-check skips environment-variable schema validation if you've declared a schema.

Watch mode

lpm run dev --watch

Re-runs one script in one package on file changes.

If the task declares inputs globs in lpm.json, only matching file changes retrigger the run. Otherwise, relevant project-file changes retrigger it.

--watch is single-package today: it does not support multiple script names or workspace-selection flags like --all, --filter, --filter-prod, or --affected. Watch runs fresh on every change; the task cache is not used.

Flags

FlagEffect
-p, --parallelRun scripts concurrently (respects task deps from lpm.json)
--no-bailKeep running remaining tasks or selected workspace members after a failure
--streamLive prefixed output instead of buffered per-task in task-graph / parallel runs
--allRun in every workspace member (topological order); mutually exclusive with filters / --affected
--filter <EXPR>Filter workspace members (repeatable; unions)
--filter-prod <EXPR>Filter workspace members with production-only dependency closures
--fail-if-no-matchExit non-zero if the selected workspace set is empty
--workspace-concurrency <N>Limit concurrent workspace members for --all, --filter, or --affected runs
--affectedOnly members affected by changes vs --base
--base <REF>Git base ref for --affected (default: main)
--changed-files-ignore-pattern <glob>Ignore matching git-diff paths for --affected / [git-ref] filters
--test-pattern <glob>Treat matching git-diff paths as test-only for --affected / [git-ref] fan-out decisions
--no-cacheBypass the task cache for cache-enabled tasks; ignored in watch mode
--no-env-checkSkip env-var schema validation
--env=<MODE>Load .env, .env.local, .env.<MODE>, and .env.<MODE>.local
--watchRe-run one script in one package on file changes

Plus the global flags.

See also