CLI Reference
mantle — Serverless TypeScript framework CLI.
npx mantle <command> [flags]
# or install globally: pnpm add -g @j0nathan-ll0yd/cliBuild & Deploy
mantle build
Bundle Lambda functions with esbuild.
| Flag | Description |
|---|---|
-w, --watch | Watch for changes and rebuild |
-f, --function <name> | Build a specific function only |
--analyze | Generate bundle analysis metafiles |
--sourcemap | Generate source maps |
--no-minify | Skip minification |
Scans src/lambdas/, outputs build/lambdas/<Name>/index.mjs. Target: Node.js 24 / ESM.
mantle dev
Start a local development server with hot reload. Watches src/lambdas/, simulates API Gateway routing locally.
mantle deploy
Deploy via OpenTofu.
mantle deploy --stage staging
mantle deploy --stage production| Flag | Description |
|---|---|
--stage <env> | Target stage — required (dev, staging, production) |
Runs tofu init, tofu plan, tofu apply. Requires AWS credentials. Never run without --stage — dev has no tfvars and prompts interactively.
Guards: when allowedStages is set in mantle.config.ts, any other --stage is hard-refused before any work (dry-run included, not bypassable with --yes). An apply whose initialized state lists zero addresses — an empty local file or an empty remote key (e.g. a stale checkout pointing at a pre-rename backend key) — is refused when the account already has that stage's Lambdas (--allow-empty-state overrides for a genuine first bootstrap).
| Flag | Description |
|---|---|
--dry-run | Run tofu plan without applying |
--allow-empty-state | Permit an apply from empty local state (genuine first bootstrap only) |
mantle bootstrap-state
Create and harden the S3 Terraform state bucket declared in backend.s3 (idempotent; safe to re-run). Solves the chicken-and-egg problem: the state bucket cannot be managed by the state it stores.
AWS_PROFILE=mantle-MyApp mantle bootstrap-stateApplies: bucket creation, versioning, AES256 encryption, public-access-block, TLS-deny bucket policy, and a 90-day noncurrent-version expiry lifecycle. Verifies versioning is Enabled before reporting success. Fails hard if the bucket name is taken by another account.
State key naming convention: infra-<stage>.tfstate (e.g. infra-staging.tfstate) in a per-instance bucket named mantle-<name>-tfstate.
Generate
Alias: mantle g <subcommand>
mantle generate infra
Generate OpenTofu configuration from project structure and mantle.config.ts.
| Flag | Description |
|---|---|
--modules-path <path> | Path to mantle modules (auto-detected if omitted) |
Scans src/lambdas/, derives API routes from filesystem, auto-wires env vars from getRequiredEnv() / getOptionalEnv() calls. Safe to run repeatedly — skips ejected files.
Observability alarms (observability.tf)
Opt-in. When mantle.config.ts sets an observability.alerts block, generate infra emits observability.tf sourcing modules/observability: an SNS alerts topic with email subscription(s) plus CloudWatch alarms on free native metrics only — per-function Lambda Errors/Throttles, DLQ ApproximateNumberOfMessagesVisible for every queue with a DLQ, and API Gateway 5XXError when the project exposes an API. Alarm actions are wired to the SNS topic. With no alerts block, no file is generated (zero cost, zero behavior change) and any previously generated observability.tf is removed.
observability: {
alerts: {
email: "ops@example.com", // string or string[]; validated at load time
dashboard: false, // optional; opt-in CloudWatch dashboard (~$3/mo once >50 metrics), default false
},
}Alarm count and estimated monthly cost ($0.10/alarm beyond the first 10 free) are written into the generated file's header comment for review.
Reconciliation with per-resource DLQ alarm flags: the enableDlqAlarm (queues) and dlqAlarm (eventbridge) flags were set to false across the instances during the April 2026 alarm removal because there was no SNS fan-out target. Opting into observability.alerts supplies that target, so observability assumes ownership of those previously-silenced DLQ alarms. Setting a flag to true keeps ownership in the resource's own module, and observability defers on that DLQ to avoid a duplicate alarm.
mantle generate permissions
Extract @RequiresTable metadata and generate least-privilege DSQL permissions.
| Flag | Default | Description |
|---|---|---|
--entity-dir <dir> | src/entities/queries | Entity queries directory |
--output-dir <dir> | build | Build output directory |
--terraform-dir <dir> | infra | Terraform output directory |
--permissions-dir <dir> | permissions | Permissions output directory |
mantle generate openapi
Generate an OpenAPI 3.1 specification from defineApiHandler metadata. Resolves Zod request/response schemas end-to-end via esbuild bundling and sibling extraction, producing fully-typed components/schemas with $ref operation bodies. Error responses are derived from each handler's auth mode and emitted as $refs to Models.ErrorResponse / Models.UnauthorizedError / Models.ForbiddenError / Models.InternalServerError when those schemas are sibling-extracted.
| Flag | Default | Description |
|---|---|---|
--output <path> | openapi.json | Output file path. Extension controls format — .json emits JSON, .yaml/.yml emits YAML. |
--title <title> | API | API title |
--version <version> | 1.0.0 | API version |
--server-url <url> | — | Optional server URL in info.servers[0].url |
--schema-prefix <prefix> | — | Prepend a namespace to every component name (e.g., Models.). The Schema suffix is stripped first, so fileListResponseSchema becomes Models.FileListResponse. Preserves TypeSpec-era naming for iOS/Swift consumers. |
--html <path> | — | Also write a Redoc HTML docs bundle to the given path. Uses @redocly/cli build-docs under the hood (auto-installed via npx). |
--verbose | false | Print per-schema resolution diagnostics — which schemas resolved, which fell back to placeholders, and which inline handler-body schemas were promoted. |
Default error response behavior
buildOperation computes the default error set from the handler's auth mode:
| Auth mode | Status codes emitted |
|---|---|
none (or unset) | 200, 400, 500 |
bearer | 200, 400, 401, 500 |
session | 200, 400, 401, 500 |
authorizer | 200, 400, 401, 403, 500 |
Each error code is only emitted when its matching component schema is present in the resolved schema set, so instances without an api-schema/schemas.ts barrel degrade gracefully to 200-only responses instead of producing broken $refs. The component name mapping is fixed:
400→ErrorResponse401→UnauthorizedError403→ForbiddenError500→InternalServerError
Per-handler error override
Any handler may override the auth-derived defaults via openapi.errors:
const api = defineApiHandler({
auth: "bearer",
openapi: {
summary: "Fetch by ID",
errors: [400, 404, 500], // omits 401 even though auth is bearer
},
});The extractor parses the array literal via ts-morph; only integer literals are supported. Unknown codes (those without a ERROR_RESPONSES mapping) are silently dropped — useful for documenting non-standard codes in prose without producing broken refs.
Query parameters
defineApiHandler({ querySchema: MyQuerySchema }) emits OpenAPI parameters: [{in: 'query', ...}] — not requestBody. Each top-level property of the resolved query schema becomes a separate query parameter entry with its constraints (type, enum, minLength, etc.). Query parameters are appended after any path parameters on the same operation.
$ref promotion
After resolving all schemas to JSON Schema, the generator promotes inlined sub-schemas to $ref pointers when they are structurally identical to a top-level component. This produces clean Swift types (e.g., [HealthQuantity] instead of [HealthSyncRequestPayload.QuantitiesItem]). Self-matches are skipped — a component never $refs itself.
Schema resolution notes
- Sibling extraction: schemas imported via
import * as allsurface every exported Zod schema in the module, so barrel files likesrc/types/api-schema/schemas.tsexpose sub-schemas as named components even when no handler references them directly. - Inline handler schemas:
const Schema = z.object(...)declared inside a handler body is promoted toexport constat bundle time and resolved as a top-level component. - Zod identity: the generator bundles each schema together with its
toJSONSchema()call via esbuild to avoid cross-packageinstanceofmismatches between different Zod copies. Do notimport { z } from 'zod'directly in handler files — use@j0nathan-ll0yd/validation's re-export so all schemas share one Zod instance. format: date-time: Zod 4'sz.string().datetime()natively emitsformat: "date-time"viatoJSONSchema(). Use.datetime()on timestamp schema fields to get proper date formatting in the spec.
mantle generate handler <Name>
Scaffold a new Lambda handler with matching test file.
| Flag | Default | Description |
|---|---|---|
--trigger <type> | api | Trigger type: api, sqs, s3, schedule, authorizer |
mantle generate entity <name>
Scaffold a Drizzle entity schema.
| Flag | Description |
|---|---|
--fields <fields> | Comma-separated name:type pairs (types: string, number, boolean, timestamp, uuid) |
--queries | Also generate entity query class with @RequiresTable methods |
mantle generate migration <name>
Create a timestamped migration SQL file in migrations/.
mantle generate graph
Generate file-level dependency graph to build/graph.json. Auto-run during mantle build.
mantle generate graph:knowledge
Generate GraphRAG knowledge graph to graphrag/knowledge-graph.json.
mantle generate inventory
Inject the generated package/module inventory between <!-- BEGIN generated:inventory --> / <!-- END generated:inventory --> markers in ARCHITECTURE.md, docs/public/llms.txt, and openspec/project.md. Counts are derived from the filesystem, so the inventory is correct by construction. Graceful no-op in directories without markers (e.g. instance roots).
| Flag | Description |
|---|---|
--check | Freshness mode: exit 1 if regeneration would change any surface (CI gate; runs in the framework-fitness job and the inventory-freshness instance CI step) |
mantle generate hooks
Generate Husky git hooks and a safe linked-worktree provisioner. The provisioner copies only missing files from the primary checkout (including .claude/settings.local.json, local env files, and local Terraform var files), then installs pnpm dependencies. It never copies directories or overwrites files.
| Flag | Description |
|---|---|
--no-pre-commit | Skip pre-commit hook |
--no-pre-push | Skip pre-push hook |
--no-worktree-provisioning | Skip .husky/post-checkout and scripts/worktree-setup.sh |
Existing hooks and setup scripts are left unchanged. Use WORKTREE_SKIP_INSTALL=1 git worktree add … when a fresh worktree should skip the dependency install. For an existing instance, commit the generated files before using git worktree add, so the new checkout contains its post-checkout hook.
mantle generate ci-templates
Generate GitHub Actions CI/CD workflow files (.github/workflows/).
Takes no flags. The generated workflows resolve the Node.js version from .nvmrc via actions/setup-node's node-version-file.
mantle generate agents
Generate AGENTS.md for AI assistants. Preserves content between <!-- CUSTOM:START --> and <!-- CUSTOM:END --> markers.
mantle generate repomix
Generate repomix.config.json for AI context packing (XML format, tree-sitter compression).
mantle generate deps-config
Generate .dependency-cruiser.cjs with Mantle architecture rules.
mantle generate integration-tests
Generate integration test scaffold with Docker Compose (LocalStack + PostgreSQL).
mantle generate knip-config
Generate knip.config.ts with Mantle-aware entry points for dead code detection.
Database
mantle db migrate
Run pending migrations using the Mantle migration runner.
| Flag | Default | Description |
|---|---|---|
--folder <path> | ./migrations | Migrations folder |
--provider <type> | auto-detected | aurora-dsql, aurora-serverless-v2, neon |
--endpoint <url> | DSQL_ENDPOINT env | DSQL cluster endpoint |
--connection-string <url> | DATABASE_URL env | Connection string (non-DSQL) |
--region <region> | AWS_REGION env | AWS region |
--no-lock | — | Skip migration lock |
Auto-detects endpoint and region from Terraform state when infra/ is initialized.
mantle db generate
Generate migration file from schema diff (wraps drizzle-kit generate).
| Flag | Description |
|---|---|
--name <name> | Migration file name |
--custom | Generate empty migration for manual SQL |
--prefix <type> | File prefix: index, timestamp, unix, none |
mantle db check-dsql
Classify migration statements for Aurora DSQL compatibility.
| Flag | Default | Description |
|---|---|---|
--migrations-dir <path> | ./migrations | Migrations directory |
Output labels: OK (compatible), INDEX (auto-converted to ASYNC), STRIP (skipped — DSQL-unsupported), RECREATION (requires table recreation).
mantle db apply-permissions
Apply DSQL role permissions from permissions/ folder.
| Flag | Default | Description |
|---|---|---|
--folder <path> | ./permissions | Permissions folder |
--stage <stage> | — | Stage whose IAM roles the grants target; sets RESOURCE_PREFIX |
--endpoint <url> | DSQL_ENDPOINT env | DSQL endpoint (auto-detected from Terraform state when omitted) |
--region <region> | AWS_REGION env | AWS region (inferred from the endpoint when omitted) |
The generated SQL's AWS IAM GRANT ARNs reference ${AWS_ACCOUNT_ID} and ${RESOURCE_PREFIX} placeholders. Both are resolved automatically — RESOURCE_PREFIX from --stage, AWS_ACCOUNT_ID from the caller's STS identity — so the typical invocation is just:
AWS_PROFILE=mantle-{InstanceName} npx mantle db apply-permissions --stage stagingExplicit environment variables always take precedence; any placeholder that cannot be resolved fails fast with an aggregated list before anything is applied.
mantle db push
Push schema directly to the database (dev only, wraps drizzle-kit push).
mantle db studio
Open Drizzle Studio (wraps drizzle-kit studio).
Quality
mantle ci
Run the full local CI pipeline.
| Flag | Description |
|---|---|
--full | Add integration tests and extended quality checks |
--step <name> | Run one step by its stable name (for example, typecheck) |
Phases: Setup → Build → Validate → Test → Quality → Integration (full only). Exits non-zero on any CRITICAL or HIGH failure. Missing tools (ShellCheck, tofu) are skipped gracefully.
mantle check
Validate codebase against 69 validation rules (67 ts-morph rules + 2 auto-fixes).
| Flag | Description |
|---|---|
--fast | CRITICAL rules only |
--severity <level> | Minimum severity: CRITICAL, HIGH, MEDIUM |
--rule <name> | Run one specific rule |
mantle check deps
Enforce dependency architecture rules (no handler-to-handler imports, no circular deps, entity/service isolation).
mantle check bundles
Check Lambda bundle sizes against thresholds.
| Flag | Default | Description |
|---|---|---|
--threshold <bytes> | 5000000 | Max bundle size in bytes |
--json | — | JSON output for CI |
mantle check dead-code
Detect unused exports via Knip. Generate config first with mantle generate knip-config.
mantle check test-output [file]
Validate test output for problematic patterns (EMF metrics on stdout, unhandled rejections, deprecation warnings). Reads from stdin if no file given.
mantle check openspec
Verify the OpenSpec drift tether: every ### Requirement: in openspec/specs/**/spec.md has ≥1 covering test (line-leading // covers: <capability>#<Requirement Name> annotation), every annotation points at a real requirement, no spec restates a Zod/TypeScript shape, and no stray hand-authored package/module counts exist outside the generated inventory snippet.
| Flag | Description |
|---|---|
--cwd <dir> | Project root to check (cross-repo runs, e.g. against an instance) |
--blocking | Exit 1 on any finding (CI gate mode — used by the framework's framework-fitness job). Default is advisory: findings print as warnings, exit 0 |
mantle check observability
Verify infra/observability.tf stays within the 10 free-tier CloudWatch alarm-metrics (C144). Parses the file on disk — catches ejected or hand-edited files that bypass the generator throw.
The alarm-metric count formula (single source of truth shared with the generator):
alarmMetrics = 2 × lambda_function_names.length
+ sqs_dlq_names.length
+ (api_gateway_name ? 1 : 0)
+ 2 × eventbridge_rule_names.length
+ sqs_age_queue_names.length
+ custom_alarms.lengthThe 2 × lambda_function_names term holds regardless of enable_per_function_lambda_alarms — CloudWatch metric-math alarms bill per referenced metric, not per alarm object.
Exit 1 when alarmMetrics > 10. No-op (exit 0) when infra/observability.tf is absent.
Config fields that affect the alarm budget:
| Field | Type | Default | Effect |
|---|---|---|---|
mode | 'cost-optimized' | 'per-function' | 'cost-optimized' | cost-optimized limits Lambda alarms to criticalFunctions; per-function alarms all discovered Lambdas |
criticalFunctions | string[] | [] | Lambda names that get per-function Errors + Throttles alarms in cost-optimized mode (2 alarm-metrics each) |
errorLogNotifier | boolean | true in cost-optimized, false in per-function | Enables the Tier B account-level subscription filter → LogNotifier Lambda → SNS email for comprehensive error coverage at $0 |
See mantle/docs/reference/observability-alerting.md for architecture details and the billing trap explanation (C145).
mantle check package-boundary
Enforce the framework's 7-tier package dependency DAG (e.g. database must never import auth). Reads each packages/*/package.json's runtime dependencies; a package may only depend on its own tier or lower. Blocking on tier violations; warns on unregistered packages. No-op outside a framework root. Known limitation: same-tier cycles are not detected.
mantle check package-versions
Publish-payload drift gate (C147). For every publishable workspace package it asks one question: does the payload this checkout would publish differ from the payload already published under the version this checkout declares?
The reference is the registry, never a historical commit. That makes the gate checkout-independent: a shallow clone, a rewritten history, a detached worktree and a missing origin/main all produce identical verdicts.
How it works: enumerate packages from the UNION of two sources — the workspace globs a package manager declares (pnpm-workspace.yaml packages:, or the root manifest's workspaces) and a plain directory scan for package.json, minus anything git ignores — → resolve a registry token → fetch each packument with plain fetch() (never npm view, which serves ~/.npm/_cacache even with the registry down) → build the workspace once → pnpm pack each package (never npm pack, which does not rewrite workspace:*) → screen packed files that are neither git-tracked, nor under a declared turbo.json build output, nor npm-injected → compare canonical digests against the reference tarball, verified against its own dist.integrity.
| Verdict | Exit | Meaning |
|---|---|---|
CLEAN | 0 | The declared version is published and the payloads match |
PENDING_PUBLISH | 0/2 | Bump is ahead of the registry and the payload differs; blocking on --lane=post-publish only |
BUMP_NOT_NEEDED | 0 | Bump is ahead but the payload is identical — the bump would publish a byte-identical artifact |
NEVER_PUBLISHED | 0/2 | Packument 404; blocking on --lane=post-publish only |
DRIFT | 2 | The declared version IS published and the payloads differ — changeset publish will silently skip |
VERSION_REGRESSION | 2 | The declared version is below the newest published one |
LEAKED_ARTIFACT | 2 | An untracked, non-build-output path entered the tarball |
INDETERMINATE | 3 | Registry unreachable, no token, or integrity mismatch — explicitly not a pass |
BUILD_FAILED | 4 | The build exited non-zero, or left a declared output directory absent or empty |
SKIPPED | 0 | private: true, or publishing somewhere other than GitHub Packages |
NO_PUBLISHABLE_PACKAGES | 3 | Discovery inventoried nothing publishable — whole-workspace, and explicitly not a pass |
The process exit code is the worst row, ordered 4 > 3 > 2 > 0.
Flags: --lane <pre-push|branch|post-publish> (changes only the exit code of PENDING_PUBLISH/NEVER_PUBLISHED, never a verdict), --json, --strict-maps (include source maps no consumer can resolve), --registry <url>, --self-test.
Discovery is deliberately not tied to one package manager. It used to be pnpm list -r --depth -1 --json alone, and in mantle-LifegamesPortal — whose pnpm-workspace.yaml is settings-only and carries no packages: key — that returns the private root and nothing else. The gate reported 1 SKIPPED, exit 0, on a repo with a real published package in packages/portal-contract. The empty-set guard closes the same hole from the other side: zero publishable packages is NO_PUBLISHABLE_PACKAGES and exit 3, never a silent pass.
Token resolution, first hit wins: DRIFT_REGISTRY_TOKEN, NODE_AUTH_TOKEN, GITHUB_TOKEN, //npm.pkg.github.com/:_authToken read directly from ~/.npmrc, gh auth token. GitHub Packages rejects anonymous reads even for public packages, so there is no offline mode; git push --no-verify is the documented escape.
--self-test builds a throwaway git repository and an in-process npm registry and drives the full pipeline through fourteen scenarios with no seam stubbed. It runs in the package-version-drift CI job, which is the stable status context to require on main.
The other half of A2b — proving the suite can still FAIL — lives in the test suite rather than the binary: mutation.test.ts patches the gate's own source text (asserting each anchor matches exactly once), imports the patched module and asserts every one of nine deliberate defects turns that ladder red. Mutants are deliberately NOT selectable at runtime; a flag the shipped binary can read is a test-only branch in production, and it makes the mutant exercise a different expression than the one production runs. cli-exit.test.ts spawns the real built binary and asserts the actual process exit status — the one boundary every other suite skips.
The digest scheme itself is not defined here. It is defined by atlas/contracts/package-digest/reference.mjs; its generated conformance fixture, checksum sidecar and shared runner are vendored verbatim into packages/cli/test/fixtures/ and asserted on every run, so this implementation, design-system's and Atlas's cannot diverge unnoticed. SPEC_VERSION means one thing: same number iff byte-identical normalization.
mantle check permissions-fresh
Verify permissions/permissions.sql is up to date with the current @RequiresTable/defineQuery annotations and Lambda inventory (C111): regenerates in-memory and diffs against the committed file. Exit 1 on drift or if the committed file is missing. Runs in instance CI/pre-push via the permissions-freshness step (triggered by changes matching db/schema|entities/|permissions/). Fix drift with mantle generate permissions.
MCP Server
mantle mcp-server
Start the Model Context Protocol server for AI-assisted development (Claude Code, Cursor, etc.).
Provides 19 tools across Validation, Infrastructure, Reference, Workflow, Data Queries, and Performance categories.