Skip to content

CLI Reference

mantle — Serverless TypeScript framework CLI.

bash
npx mantle <command> [flags]
# or install globally: pnpm add -g @j0nathan-ll0yd/cli

Build & Deploy

mantle build

Bundle Lambda functions with esbuild.

FlagDescription
-w, --watchWatch for changes and rebuild
-f, --function <name>Build a specific function only
--analyzeGenerate bundle analysis metafiles
--sourcemapGenerate source maps
--no-minifySkip 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.

bash
mantle deploy --stage staging
mantle deploy --stage production
FlagDescription
--stage <env>Target stage — required (dev, staging, production)

Runs tofu init, tofu plan, tofu apply. Requires AWS credentials. Never run without --stagedev 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).

FlagDescription
--dry-runRun tofu plan without applying
--allow-empty-statePermit 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.

bash
AWS_PROFILE=mantle-MyApp mantle bootstrap-state

Applies: 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.

FlagDescription
--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.

typescript
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.

FlagDefaultDescription
--entity-dir <dir>src/entities/queriesEntity queries directory
--output-dir <dir>buildBuild output directory
--terraform-dir <dir>infraTerraform output directory
--permissions-dir <dir>permissionsPermissions 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.

FlagDefaultDescription
--output <path>openapi.jsonOutput file path. Extension controls format — .json emits JSON, .yaml/.yml emits YAML.
--title <title>APIAPI title
--version <version>1.0.0API 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).
--verbosefalsePrint 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 modeStatus codes emitted
none (or unset)200, 400, 500
bearer200, 400, 401, 500
session200, 400, 401, 500
authorizer200, 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:

  • 400ErrorResponse
  • 401UnauthorizedError
  • 403ForbiddenError
  • 500InternalServerError

Per-handler error override

Any handler may override the auth-derived defaults via openapi.errors:

typescript
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 all surface every exported Zod schema in the module, so barrel files like src/types/api-schema/schemas.ts expose 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 to export const at 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-package instanceof mismatches between different Zod copies. Do not import { 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's z.string().datetime() natively emits format: "date-time" via toJSONSchema(). 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.

FlagDefaultDescription
--trigger <type>apiTrigger type: api, sqs, s3, schedule, authorizer

mantle generate entity <name>

Scaffold a Drizzle entity schema.

FlagDescription
--fields <fields>Comma-separated name:type pairs (types: string, number, boolean, timestamp, uuid)
--queriesAlso 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).

FlagDescription
--checkFreshness 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.

FlagDescription
--no-pre-commitSkip pre-commit hook
--no-pre-pushSkip pre-push hook
--no-worktree-provisioningSkip .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.

FlagDefaultDescription
--folder <path>./migrationsMigrations folder
--provider <type>auto-detectedaurora-dsql, aurora-serverless-v2, neon
--endpoint <url>DSQL_ENDPOINT envDSQL cluster endpoint
--connection-string <url>DATABASE_URL envConnection string (non-DSQL)
--region <region>AWS_REGION envAWS region
--no-lockSkip 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).

FlagDescription
--name <name>Migration file name
--customGenerate empty migration for manual SQL
--prefix <type>File prefix: index, timestamp, unix, none

mantle db check-dsql

Classify migration statements for Aurora DSQL compatibility.

FlagDefaultDescription
--migrations-dir <path>./migrationsMigrations 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.

FlagDefaultDescription
--folder <path>./permissionsPermissions folder
--stage <stage>Stage whose IAM roles the grants target; sets RESOURCE_PREFIX
--endpoint <url>DSQL_ENDPOINT envDSQL endpoint (auto-detected from Terraform state when omitted)
--region <region>AWS_REGION envAWS 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:

bash
AWS_PROFILE=mantle-{InstanceName} npx mantle db apply-permissions --stage staging

Explicit 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.

FlagDescription
--fullAdd 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).

FlagDescription
--fastCRITICAL 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.

FlagDefaultDescription
--threshold <bytes>5000000Max bundle size in bytes
--jsonJSON 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.

FlagDescription
--cwd <dir>Project root to check (cross-repo runs, e.g. against an instance)
--blockingExit 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):

text
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.length

The 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:

FieldTypeDefaultEffect
mode'cost-optimized' | 'per-function''cost-optimized'cost-optimized limits Lambda alarms to criticalFunctions; per-function alarms all discovered Lambdas
criticalFunctionsstring[][]Lambda names that get per-function Errors + Throttles alarms in cost-optimized mode (2 alarm-metrics each)
errorLogNotifierbooleantrue in cost-optimized, false in per-functionEnables 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.

VerdictExitMeaning
CLEAN0The declared version is published and the payloads match
PENDING_PUBLISH0/2Bump is ahead of the registry and the payload differs; blocking on --lane=post-publish only
BUMP_NOT_NEEDED0Bump is ahead but the payload is identical — the bump would publish a byte-identical artifact
NEVER_PUBLISHED0/2Packument 404; blocking on --lane=post-publish only
DRIFT2The declared version IS published and the payloads differ — changeset publish will silently skip
VERSION_REGRESSION2The declared version is below the newest published one
LEAKED_ARTIFACT2An untracked, non-build-output path entered the tarball
INDETERMINATE3Registry unreachable, no token, or integrity mismatch — explicitly not a pass
BUILD_FAILED4The build exited non-zero, or left a declared output directory absent or empty
SKIPPED0private: true, or publishing somewhere other than GitHub Packages
NO_PUBLISHABLE_PACKAGES3Discovery 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.