Skip to content

Inspect, call, dev and build

Declare callable operations, understand command effects, and locate failures with structured CLI diagnostics.

This page documents published @lenso/cli@0.19.0 with Core/Engine 0.2.0, audited against source 549b987. Check the installed launcher’s lenso help --json when diagnosing a different version.

Choose a command by what it runs

CommandMain effect
help --jsonLists commands, flags, error codes and exit codes; imports no app config.
inspect [plugin-id [method]] --jsonImports canonical trusted lenso.config.ts; describes declarations without setup.
check --jsonRuns trusted Engine setup/discovery and validates assembly; no application setup.
generate --jsonRuns Engine and writes owned .lenso files.
build [--entry file] --jsonGenerates, then bundles into dist.
call plugin-id methodValidates input, starts the full app, invokes one explicit operation, then stops.
dev [--entry file]Watches and supervises owned Engine/runtime processes. --json is unsupported.

All commands accept --root directory. --entry is available only for build/dev. Inspection is not a sandbox: imports and trusted schema converters execute top-level code even though plugin setup and configuration source reads do not run.

Declare operations alongside the application

lenso.config.ts default-exports defineApp({ plugins }). Its separate named operations export is the CLI invocation allowlist. Named mcpOperations independently selects MCP operations; if omitted, MCP falls back to operations, while an explicit empty list disables that fallback. Named operationBinding belongs to the CLI entry. These exports must not be properties of the default defineApp declaration. No returned service method is exposed automatically.

Notes uses a companion factory returning { plugin, operations, manage }. A declaration inside that factory is:

const operations = [  defineOperation({    plugin,    method: "list",    context: true,    input: notesListInput,    effect: "read",    destructive: false,    retry: "safe",    cancellation: "none",    outputDescription: "The authenticated user's private notes.",    source,    description: "List the authenticated user's private notes.",  }),];

The actual file imports defineOperation from @lenso/engine/operations; @lenso/cli also re-exports it. The factory creates plugin once and uses its shared contracts. The config installs notesOperations.plugin and selects CLI/MCP subsets from its declarations. selectManageOperations returns the original Operations; Manage does not add another handler or grant permission. That exact installed object must appear in every declaration.

defineOperation checks the service method's input type against the Standard Schema v1 output. Invocation requires an own callable method on the returned service and calls it with the service as this. Single-input methods remain supported. Methods that need request evidence or an Auth-produced actor can declare context: true; the context type is inferred from the real method’s second parameter. The trusted entry supplies that argument separately from business JSON.

Notes passes trusted entry evidence through NotesOperationContext, verifies it with Auth on each call and keeps owner checks in the Notes service. actor, subjectId or a permission flag in business JSON is not evidence. A declared operation grants exposure; it does not grant permission.

Bind trusted context after startup

Add a typed named binding beside the selected operations in the existing Notes config:

import type { OperationBinding } from "@lenso/cli";
export const operationBinding: OperationBinding<typeof operations[number]> =  () => ({ context: { evidence: process.env.NOTES_SESSION ?? null } });

OperationBinding<O> receives (operation, validatedInput, running). CLI validates the raw business input once, including schema transforms, then starts the full app, then awaits the binding. OperationContext<O> recovers the method's context type, and OperationBoundOptions<O> requires context for a context: true declaration. Types and context presence do not authenticate anyone: Notes verifies the supplied evidence through authentication.for(...).required(...) before service policy checks.

OperationInvocationOptions contains context, maxOutputBytes, confirm and approve. It has no top-level signal option. Notes' real context optionally carries signal, which its method explicitly passes to Auth; neither a binding nor cancellation metadata automatically cancels every business effect.

inspect reports contextRequired, confirmation and approval but never invokes the binding or infers a context schema. MCP uses only its explicit trusted launch StdioOptions.binding, never the CLI operationBinding. Manage adapters supply their own per-call binding and borrow a running app.

Enforce confirmation and approval separately

An Operation may declare confirmation: "required" and/or approval: "required". After context binding and before the service call, the shared invocation path requires the corresponding trusted confirm/approve callback to resolve to exactly true. The entry must implement a real flow that verifies this invocation's user confirmation or configured approval owner. A constant callback or a flag from business JSON does not establish that evidence.

Missing context yields missing-context-binding; missing or unsuccessful gates yield confirmation-required / approval-required. These checks can fail after full app setup, so the CLI still awaits resource cleanup. Allowlisting, Auth permission, confirmation and approval are independent requirements. destructive: true alone triggers none of them.

Inspect first, call with one input source

From a prepared, version-matched framework checkout, the real Notes operation can be inspected without opening its database or Auth connection:

bun run cli inspect notes-operations list --root examples/notes --json

After explicit migrations and the Notes authentication setup are complete, call it using the existing authorized launch environment:

printf '%s\n' '{}' |  bun run cli call notes-operations list --root examples/notes --stdin --json

These commands use the repository's existing launcher. An external app uses its installed lenso binary. Notes call requires the example's selected database/storage setup and a valid NOTES_SESSION; inspection does not prove those runtime prerequisites.

Choose exactly one input source: nonsensitive inline JSON, --stdin, or --input-file file. Omitting all three uses {}. Invalid JSON/input and unknown operations fail before startup. A valid call resolves every installed configured instance and starts all installed plugins, even when the selected operation is a read.

Each call owns a fresh app. In-memory state is not shared with an already running HTTP server or a previous call. Both invocation and cleanup failures are preserved. An output or cleanup failure can follow a committed business write; query authorized state before repeating an uncertain mutation.

Treat metadata as descriptions

effect is read, write or unknown. destructive, retry and cancellation describe operation semantics for users and adapters. They do not authorize a call, make it idempotent, retry it, pass an AbortSignal or roll back an effect. Omitted retry/cancellation are unknown; omitted destructive/output metadata are null.

outputDescription is prose, not an output validator. Return finite, acyclic plain JSON, with explicit null or a result object instead of undefined. Dates, functions, streams, symbol-keyed objects and other unsupported values cause serialization-failed. The shared invocation path checks the original result before redaction, then checks the safe JSON again. Its default output budget is 1 MiB; trusted bindings can set maxOutputBytes, while MCP applies its configured host limit. Oversized output fails with output-too-large before the CLI stops the app; it does not undo the business effect.

Input inspection uses the validator's Standard JSON Schema converter when available. Otherwise it reports schemaAvailability: "runtime-validation-only" and inputSchema: null; runtime validation still applies. Defaults/examples are omitted. Field names and constraints remain discoverable, so never embed secrets in schemas or metadata.

Read phase, code and ordered causes

Finite commands with --json emit one envelope on stdout: { schemaVersion: 1, ok: true, data } or { schemaVersion: 1, ok: false, error }. Logs use stderr.

ExitMeaningFirst boundary to check
0SuccessInspect the command's returned data.
2Arguments/inputFlags, JSON source, shared input schema.
3Discovery/assemblyConfig import, exact instances, operation declarations.
1Runtime/build/outputConfig preflight, setup, invoke, cleanup, bundling or JSON serialization.

A trusted config import failure differs from runtime configuration preflight. Configuration preflight failures use phase config and top-level config-invalid, with ordered safe causes naming instance, field path and source. Setup/invoke/cleanup can each fail independently; preserve invocation-and-cleanup-failed causes instead of choosing one.

Unknown application error text, raw input and stacks are omitted because they can contain secrets. Console methods are routed to stderr with redaction, but arbitrary direct stdout writes and secrets under innocuous names are not reliably isolated. Keep stdout protocol-only and avoid logging sensitive values.

Start dev after actual startup is reported

In an existing server entry, call reportDevReady({ urls: [actualUrl], capabilities }) from @lenso/engine/dev-ready after app startup and binding succeed. Service-only entries can omit URLs. The helper is a no-op outside supervised dev; no hand-written IPC is needed.

Run lenso dev --root . or choose an existing entry with --entry. Failed starts keep watching. build writes bundles; it does not publish npm packages, provision a database or deploy Workers. Custom generation/targets and file ownership are documented in Engine.

For programmatic use, inspect(root, pluginId?, method?), call(root, pluginId, method, input) and invoke(appDefinition, pluginId, method, input, binding?) are public @lenso/cli exports. Programmatic invoke defaults to appDefinition.operationBinding; an explicit fifth argument selects its binding. call loads the named binding from canonical config and has no binding argument. Use Engine for programmatic build stages. See CLI contract and parser implementation.