For the CLI command, install
prisma-next. The publicprisma-nextpackage ships theprisma-nextbinary and nothing else — it has no library exports.This package (
@prisma-next/cli) is both the CLI's implementation and the documented programmatic-API import target. Authors of build integrations, extension packs, and advanced config wiring import from@prisma-next/cli/config-types,@prisma-next/cli/control-api,@prisma-next/cli/commands/*, and@prisma-next/config-loader. These subpaths are less stable than the facade packages (@prisma-next/postgres/config,@prisma-next/mongo/config); prefer those for application-level config.This README is architecture and internal documentation for contributors; the user-facing README lives in the
prisma-nextpackage.
Command-line interface for Prisma Next contract emission and management.
The CLI provides commands for emitting canonical contract.json and contract.d.ts files from TypeScript-authored contracts. It enforces import allowlists and validates contract purity to ensure deterministic, reproducible artifacts. Generated files include metadata and warning headers to indicate they're generated artifacts and should not be edited manually.
Provide a command-line interface that:
- Loads TypeScript-authored contracts using esbuild with import allowlisting
- Validates contract purity (JSON-serializable, no functions/getters)
- Invokes the emitter to produce canonical artifacts
- Handles all file I/O operations (CLI handles I/O; emitter returns strings)
- TS Contract Loading: Bundle and load TypeScript contract files with import allowlist enforcement
- CLI Command Interface: Parse arguments and route to command handlers using commander
- File I/O: Read TS contracts, write emitted artifacts (
contract.json,contract.d.ts) - Extension Pack Descriptor Assembly: Collect adapter and extension descriptors for emission
- Help Output Formatting: Custom styled help output with command trees and formatted descriptions
- Config Management: Load and validate
prisma-next.config.tsfiles using Arktype validation - CLI Binary Compatibility: Build emits
dist/cli.mjsand also writes a compatibility shim atdist/cli.js
The CLI performs wiring validation at the composition boundary: it ensures the emitted contract artifacts are compatible with the descriptors wired in prisma-next.config.ts.
This prevents runtime mismatches (for example: a contract that declares extension packs, but a config that doesn’t provide the matching descriptors).
Commands that enforce wiring validation:
db verifydb signdb initdb update
If you hit a wiring validation error: add the required descriptors to config.extensionPacks (matched by descriptor id) and re-run the command.
Note: Control plane domain actions (database verification, contract emission) are implemented in @prisma-next/emitter and @prisma-next/framework-components/control. The CLI uses the control plane domain actions programmatically but does not define control plane types itself.
Commands use separate short and long descriptions via setCommandDescriptions():
- Short description: One-liner used in command trees and headers (e.g., "Emit signed contract artifacts")
- Long description: Multiline text shown at the bottom of help output with detailed context
See src/utils/command-helpers.ts for setCommandDescriptions() and getLongDescription().
Emit contract.json and contract.d.ts from config.contract.
Canonical command:
prisma-next contract emit [--config <path>] [--json] [-v] [-q] [--color/--no-color]Config File Requirements:
The contract emit command does not require a driver in the config since it doesn't connect to a database:
import { defineConfig } from '@prisma-next/cli/config-types';
import { typescriptContract } from '@prisma-next/sql-contract-ts/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgres from '@prisma-next/target-postgres/control';
import sql from '@prisma-next/family-sql/control';
import { contract } from './prisma/contract';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
extensionPacks: [],
contract: typescriptContract(contract, 'src/prisma/contract.json'),
});Options:
--config <path>: Optional. Path toprisma-next.config.ts(defaults to./prisma-next.config.tsif present)--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)-vv, --trace: Trace output (deep internals, stack traces)--color/--no-color: Force/disable color output
Examples:
# Use config defaults
prisma-next contract emit
# JSON output
prisma-next contract emit --json
# Verbose output
prisma-next contract emit -vVerify that a database instance matches the emitted contract by checking the marker first and, by default, the live schema second.
Command:
prisma-next db verify [--db <url>] [--config <path>] [--marker-only | --schema-only] [--strict] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (optional; defaults toconfig.db.connectionif set)--config <path>: Optional. Path toprisma-next.config.ts(defaults to./prisma-next.config.tsif present)--marker-only: Skip schema verification and only check the database marker--schema-only: Skip marker verification and only check whether the live schema satisfies the contract--strict: When schema verification runs, schema elements not present in the contract are considered an error--marker-onlycannot be combined with--schema-onlyor--strict(exit code 2,PN-CLI-4012).--schema-only --strictis valid.--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)-vv, --trace: Trace output (deep internals, stack traces)--color/--no-color: Force/disable color output
Examples:
# Use config defaults
prisma-next db verify
# Specify database URL
prisma-next db verify --db postgresql://user:pass@localhost/db
# Marker-only verification when callers accept the trade-off
prisma-next db verify --db postgresql://user:pass@localhost/db --marker-only
# Schema-only verification without relying on marker state
prisma-next db verify --db postgresql://user:pass@localhost/db --schema-only
# Strict schema verification (extras fail)
prisma-next db verify --db postgresql://user:pass@localhost/db --strict
# JSON output
prisma-next db verify --json
# Verbose output
prisma-next db verify -vConfig File Requirements:
The db verify command requires a driver in the config to connect to the database:
import { defineConfig } from '@prisma-next/cli/config-types';
import { typescriptContract } from '@prisma-next/sql-contract-ts/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgresDriver from '@prisma-next/driver-postgres/control';
import postgres from '@prisma-next/target-postgres/control';
import sql from '@prisma-next/family-sql/control';
import { contract } from './prisma/contract';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensionPacks: [],
contract: typescriptContract(contract, 'src/prisma/contract.json'),
db: {
connection: process.env.DATABASE_URL, // Optional: can also use --db flag
},
});Verification Process:
- Load Contract: Reads the emitted
contract.jsonfromconfig.contract.output - Connect to Database: Uses
config.driver.create(url)to create a driver - Create Family Instance: Creates a
ControlStackviacreateControlStack()and passes it toconfig.family.create(stack)to create a family instance - Verify Marker: Calls
familyInstance.verify()which:- Reads the contract marker from the database
- Compares marker presence: Returns
PN-RUN-3001if marker is missing - Compares target compatibility: Returns
PN-RUN-3003if contract target doesn't match config target - Compares storage hash: Returns
PN-RUN-3002ifstorageHashdoesn't match - Compares profile hash: Returns
PN-RUN-3002ifprofileHashdoesn't match (when present) - Checks codec coverage (optional): Compares contract column types against supported codec types and reports missing codecs
- Verify Schema (default): Unless
--marker-onlyis provided, callsfamilyInstance.schemaVerify()to catch schema mismatches such as missing tables or columns after manual DDL. By default this runs in tolerant mode;--stricttreats schema elements not present in the contract as an error. - Schema-only mode:
--schema-onlyskips marker verification entirely and runs onlyschemaVerify(). This is useful for brownfield adoption and corrupt-marker diagnosis.
Output Format (TTY):
Success:
✔ Database marker and schema match contract
verification: marker + schema
storageHash: sha256:abc123...
profileHash: sha256:def456...
Marker-only success:
✔ Database marker matches contract
verification: marker only (--marker-only)
storageHash: sha256:abc123...
profileHash: sha256:def456...
⚠ Schema verification skipped because --marker-only was provided
Marker failure:
✖ Marker missing (PN-RUN-3001)
Why: Contract marker not found in database
Fix: Run `prisma-next db sign --db <url>` to create marker
Schema drift failure:
db verify prints the schema verification tree / JSON payload and exits with code 1.
Output Format (JSON):
{
"ok": true,
"summary": "Database marker and schema match contract",
"mode": "full",
"contract": {
"storageHash": "sha256:abc123...",
"profileHash": "sha256:def456..."
},
"marker": {
"storageHash": "sha256:abc123...",
"profileHash": "sha256:def456..."
},
"target": {
"expected": "postgres"
},
"missingCodecs": [],
"schema": {
"summary": "Database schema satisfies contract",
"counts": {
"pass": 12,
"warn": 0,
"fail": 0,
"totalNodes": 12
},
"strict": false
},
"meta": {
"configPath": "/path/to/prisma-next.config.ts",
"contractPath": "/path/to/src/prisma/contract.json",
"schemaVerification": "performed"
},
"timings": {
"total": 42
}
}Error Codes:
PN-CLI-4010: Missing driver in config — provide a driver descriptorPN-RUN-3001: Marker missing - Contract marker not found in databasePN-RUN-3002: Hash mismatch - Contract hash does not match database markerPN-RUN-3003: Target mismatch - Contract target does not match config target- Exit code 1 with schema verification payload: Schema does not match the contract (default mode or
--schema-only)
Family Requirements:
The family must provide a create() method in the family descriptor that accepts a ControlStack and returns a ControlFamilyInstance with a verify() method:
interface ControlFamilyDescriptor<TFamilyId, TFamilyInstance> {
create<TTargetId extends string>(
stack: ControlStack<TFamilyId, TTargetId>,
): TFamilyInstance;
}
interface ControlStack<TFamilyId, TTargetId> {
readonly target: ControlTargetDescriptor<TFamilyId, TTargetId>;
readonly adapter: ControlAdapterDescriptor<TFamilyId, TTargetId>;
readonly driver: ControlDriverDescriptor<TFamilyId, TTargetId> | undefined;
readonly extensionPacks: readonly ControlExtensionDescriptor<TFamilyId, TTargetId>[];
}
interface ControlFamilyInstance {
verify(options: {
driver: ControlDriverInstance;
contract: Contract;
expectedTargetId: string;
contractPath: string;
configPath?: string;
}): Promise<VerifyDatabaseResult>;
}Use createControlStack() from @prisma-next/framework-components/control to create the stack with sensible defaults (driver defaults to undefined, extensionPacks defaults to []).
The SQL family provides this via @prisma-next/family-sql/control. The verify() method handles marker checks, full db verify follows it with schemaVerify(), --marker-only skips that schema step, and --schema-only runs schemaVerify() without marker checks.
Inspect the live database schema and display it as a human-readable tree or machine-consumable JSON. This command is read-only and never writes files.
Command:
prisma-next db schema [--db <url>] [--config <path>] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (optional; defaults toconfig.db.connectionif set)--config <path>: Optional. Path toprisma-next.config.ts(defaults to./prisma-next.config.tsif present)--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)-vv, --trace: Trace output (deep internals, stack traces)--color/--no-color: Force/disable color output
Examples:
# Use config defaults
prisma-next db schema
# Specify database URL
prisma-next db schema --db postgresql://user:pass@localhost/db
# JSON output
prisma-next db schema --json
# Verbose output
prisma-next db schema -vInspect the live database schema and write an inferred PSL contract to disk. Use this for brownfield adoption when you want a starting contract.prisma before running contract emit and db sign.
Command:
prisma-next contract infer [--db <url>] [--config <path>] [--output <path>] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (optional; defaults toconfig.db.connectionif set)--config <path>: Optional. Path toprisma-next.config.ts(defaults to./prisma-next.config.tsif present)--output <path>: Write the inferred PSL contract to the specified path--json: Output a JSON result envelope (includespsl.path)-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)-vv, --trace: Trace output (deep internals, stack traces)--color/--no-color: Force/disable color output
Examples:
# Infer contract.prisma next to the configured contract.json output
prisma-next contract infer
# Specify database URL
prisma-next contract infer --db postgresql://user:pass@localhost/db
# Override the output path
prisma-next contract infer --output ./prisma/contract.prisma
# JSON output
prisma-next contract infer --jsonBy default, contract infer writes to:
--output <path>, if providedcontract.prismanext toconfig.contract.outputcontract.prismain the current working directory
Config File Requirements:
Both db schema and contract infer require a driver in the config to connect to the database:
import { defineConfig } from '@prisma-next/cli/config-types';
import { typescriptContract } from '@prisma-next/sql-contract-ts/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgresDriver from '@prisma-next/driver-postgres/control';
import postgres from '@prisma-next/target-postgres/control';
import sql from '@prisma-next/family-sql/control';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensionPacks: [],
db: {
connection: process.env.DATABASE_URL, // Optional: can also use --db flag
},
});Introspection Process:
- Connect to Database: Uses
config.driver.create(url)to create a driver - Create Family Instance: Creates a
ControlStackviacreateControlStack()and passes it toconfig.family.create(stack)to create a family instance - Introspect: Calls
familyInstance.introspect()which:- Queries the database catalog to discover schema structure
- Returns a family-specific schema IR (e.g.,
SqlSchemaIRfor SQL family)
- Transform to Schema View: Calls
familyInstance.toSchemaView()to project the schema IR into aCoreSchemaViewfor display - Format Output: Formats the schema view as a human-readable tree or JSON envelope
Output Format (TTY):
Human-readable schema tree:
sql schema (tables: 2)
├─ table user
│ ├─ id: int4 (not null)
│ ├─ email: text (not null)
│ └─ unique user_email_key
├─ table post
│ ├─ id: int4 (not null)
│ ├─ title: text (not null)
│ └─ userId: int4 (not null)
├─ extension plpgsql
└─ extension vector
Output Format (JSON):
{
"ok": true,
"summary": "Schema introspected successfully",
"schema": {
"root": {
"kind": "root",
"id": "sql-schema",
"label": "sql schema (tables: 2)",
"children": [
{
"kind": "entity",
"id": "table-user",
"label": "table user",
"children": [
{
"kind": "field",
"id": "column-user-id",
"label": "id: int4 (not null)",
"meta": {
"nativeType": "int4",
"nullable": false
}
}
]
}
]
}
},
"meta": {
"configPath": "/path/to/prisma-next.config.ts",
"dbUrl": "postgresql://user:pass@localhost/db"
},
"timings": {
"total": 42
}
}Error Codes:
PN-CLI-4010: Missing driver in config — provide a driver descriptorPN-CLI-4005: Missing database connection — provide--db <url>or setdb.connectionin config
Family Requirements:
The family must provide:
- A
create()method in the family descriptor that returns aControlFamilyInstancewith anintrospect()method - An optional
toSchemaView()method on theControlFamilyInstanceto project family-specific schema IR intoCoreSchemaView
interface ControlFamilyInstance {
introspect(options: {
driver: ControlDriverInstance;
contract?: Contract;
schema?: string;
}): Promise<FamilySchemaIR>;
toSchemaView?(schema: FamilySchemaIR): CoreSchemaView;
}The SQL family provides this via @prisma-next/family-sql/control. The introspect() method queries the database catalog and returns SqlSchemaIR, and toSchemaView() projects it into a CoreSchemaView for display.
Note: The introspection output displays native database types (e.g., int4, text, timestamptz) rather than mapped codec IDs (e.g., pg/int4@1). This reflects the actual database state, which may be enriched with type mappings later.
Mark the database as matching the emitted contract by writing or updating the contract marker. This command verifies that the database schema satisfies the contract before signing, ensuring the marker is only written when the database is fully aligned.
Command:
prisma-next db sign [--db <url>] [--config <path>] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (optional; defaults toconfig.db.connectionif set)--config <path>: Optional. Path toprisma-next.config.ts(defaults to./prisma-next.config.tsif present)--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)-vv, --trace: Trace output (deep internals, stack traces)--color/--no-color: Force/disable color output
Examples:
# Use config defaults
prisma-next db sign
# Specify database URL
prisma-next db sign --db postgresql://user:pass@localhost/db
# JSON output
prisma-next db sign --json
# Verbose output
prisma-next db sign -vConfig File Requirements:
The db sign command requires a driver in the config to connect to the database and a contract.output path to locate the emitted contract:
import { defineConfig } from '@prisma-next/cli/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgresDriver from '@prisma-next/driver-postgres/control';
import postgres from '@prisma-next/target-postgres/control';
import sql from '@prisma-next/family-sql/control';
import { contract } from './prisma/contract';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensionPacks: [],
contract: typescriptContract(contract, 'src/prisma/contract.json'),
db: {
connection: process.env.DATABASE_URL, // Optional: can also use --db flag
},
});Signing Process:
- Load Contract: Reads the emitted
contract.jsonfromconfig.contract.output - Connect to Database: Uses
config.driver.create(url)to create a driver - Create Family Instance: Creates a
ControlStackviacreateControlStack()and passes it toconfig.family.create(stack)to create a family instance - Schema Verification (Precondition): Calls
familyInstance.schemaVerify()to verify the database schema matches the contract:- If verification fails: Prints schema verification output and exits with code 1 (marker is not written)
- If verification passes: Proceeds to marker signing
- Sign: Calls
familyInstance.sign()which:- Ensures the marker schema and table exist
- Reads any existing marker from the database
- Compares contract hashes with existing marker:
- If marker is missing: Inserts a new marker row
- If hashes differ: Updates the existing marker row
- If hashes match: No-op (idempotent)
Output Format (TTY):
Success (new marker):
✔ Database signed (marker created)
storageHash: sha256:abc123...
profileHash: sha256:def456...
Total time: 42ms
Success (updated marker):
✔ Database signed (marker updated from sha256:old-hash)
storageHash: sha256:abc123...
profileHash: sha256:def456...
previous storageHash: sha256:old-hash
Total time: 42ms
Success (already up-to-date):
✔ Database already signed with this contract
storageHash: sha256:abc123...
profileHash: sha256:def456...
Total time: 42ms
Failure (schema mismatch):
✖ Schema verification failed
[Schema verification tree output]
Output Format (JSON):
{
"ok": true,
"summary": "Database signed (marker created)",
"contract": {
"storageHash": "sha256:abc123...",
"profileHash": "sha256:def456..."
},
"target": {
"expected": "postgres",
"actual": "postgres"
},
"marker": {
"created": true,
"updated": false
},
"meta": {
"configPath": "/path/to/prisma-next.config.ts",
"contractPath": "/path/to/src/prisma/contract.json"
},
"timings": {
"total": 42
}
}For updated markers:
{
"ok": true,
"summary": "Database signed (marker updated from sha256:old-hash)",
"contract": {
"storageHash": "sha256:abc123...",
"profileHash": "sha256:def456..."
},
"target": {
"expected": "postgres",
"actual": "postgres"
},
"marker": {
"created": false,
"updated": true,
"previous": {
"storageHash": "sha256:old-hash",
"profileHash": "sha256:old-profile-hash"
}
},
"meta": {
"configPath": "/path/to/prisma-next.config.ts",
"contractPath": "/path/to/src/prisma/contract.json"
},
"timings": {
"total": 42
}
}Error Codes:
PN-CLI-4010: Missing driver in config — provide a driver descriptorPN-CLI-4005: Missing database connection — provide--db <url>or setdb.connectionin config- Exit code 1: Schema verification failed — database schema does not match contract (marker is not written)
Relationship to Other Commands:
db verify:db verifychecks that the marker exists and matches the contract, then runs schema verification by default.db signwrites the marker thatdb verifychecks. Usedb verify --marker-onlyfor marker-only verification anddb verify --schema-onlyto inspect only the live schema.
Idempotency:
The db sign command is idempotent and safe to run multiple times:
- If the marker already matches the contract (same hashes), no database changes are made
- The command reports success in all cases (new marker, updated marker, or already up-to-date)
- Safe to run in CI/deployment pipelines
Family Requirements:
The family must provide a create() method in the family descriptor that returns a ControlFamilyInstance with schemaVerify() and sign() methods:
interface ControlFamilyInstance {
schemaVerify(options: {
driver: ControlDriverInstance;
contract: Contract;
strict: boolean;
contractPath: string;
configPath?: string;
}): Promise<VerifyDatabaseSchemaResult>;
sign(options: {
driver: ControlDriverInstance;
contract: Contract;
contractPath: string;
configPath?: string;
}): Promise<SignDatabaseResult>;
}The SQL family provides this via @prisma-next/family-sql/control. The sign() method handles ensuring the marker schema/table exist, reading existing markers, comparing hashes, and writing/updating markers internally.
Initialize a database schema from the contract. This command plans and applies additive-only operations (create missing tables/columns/constraints/indexes) until the database satisfies the contract, then writes the contract marker.
Command:
prisma-next db init [--db <url>] [--config <path>] [--dry-run] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (optional; defaults toconfig.db.connectionif set)--config <path>: Optional. Path toprisma-next.config.ts(defaults to./prisma-next.config.tsif present)--dry-run: Only show the migration plan, do not apply it--json [format]: Output as JSON (objectonly;ndjsonis not supported for this command)-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)-vv, --trace: Trace output (deep internals, stack traces)--color/--no-color: Force/disable color output
Examples:
# Initialize database with config defaults
prisma-next db init
# Preview migration plan without applying
prisma-next db init --dry-run
# Specify database URL
prisma-next db init --db postgresql://user:pass@localhost/db
# JSON output
prisma-next db init --jsonConfig File Requirements:
The db init command requires a driver in the config to connect to the database:
import { defineConfig } from '@prisma-next/cli/config-types';
import { typescriptContract } from '@prisma-next/sql-contract-ts/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgresDriver from '@prisma-next/driver-postgres/control';
import postgres from '@prisma-next/target-postgres/control';
import sql from '@prisma-next/family-sql/control';
import { contract } from './prisma/contract';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensionPacks: [],
contract: typescriptContract(contract, 'src/prisma/contract.json'),
db: {
connection: process.env.DATABASE_URL, // Optional: can also use --db flag
},
});Initialization Process:
- Load Contract: Reads the emitted
contract.jsonfromconfig.contract.output - Connect to Database: Uses
config.driver.create(url)to create a driver - Create Family Instance: Creates a
ControlStackviacreateControlStack()and passes it toconfig.family.create(stack)to create a family instance - Introspect Schema: Calls
familyInstance.introspect()to get the current database schema IR - Validate wiring: Ensures the contract is compatible with the CLI config:
contract.targetFamilymatchesconfig.family.familyIdcontract.targetmatchesconfig.target.targetIdcontract.extensionPacks(if present) are provided byconfig.extensionPacks(matched by descriptorid)
- Create Planner/Runner: Uses
config.target.migrations.createPlanner()andconfig.target.migrations.createRunner() - Plan Migration: Calls
planner.plan()with the contract, schema IR, additive-only policy, andframeworkComponents(the active target/adapter/extension descriptors)- On conflict: Returns a structured failure with conflict list
- On success: Returns a migration plan with operations
- Apply Migration (if not
--dry-run):- Calls
runner.execute()to apply the plan - After execution, verifies schema matches contract
- Writes contract marker (and records a ledger entry via the target runner)
- Calls
Output Format (TTY - Plan Mode):
prisma-next db init ➜ Bootstrap a database to match the current contract
config: prisma-next.config.ts
contract: src/prisma/contract.json
mode: plan (dry run)
✔ Planned 4 operation(s)
│
├─ Create table user [additive]
├─ Add unique constraint user_email_key on user [additive]
├─ Create index user_email_idx on user [additive]
└─ Add foreign key post_userId_fkey on post [additive]
Destination hash: sha256:abc123...
This is a dry run. No changes were applied.
Run without --dry-run to apply changes.
Output Format (TTY - Apply Mode):
prisma-next db init ➜ Bootstrap a database to match the current contract
config: prisma-next.config.ts
contract: src/prisma/contract.json
Applying migration plan and verifying schema...
→ Create table user...
→ Add unique constraint user_email_key on user...
→ Create index user_email_idx on user...
→ Add foreign key post_userId_fkey on post...
✔ Applied 4 operation(s)
Marker written: sha256:abc123...
Output Format (JSON):
{
"ok": true,
"mode": "apply",
"plan": {
"targetId": "postgres",
"destination": {
"storageHash": "sha256:abc123..."
},
"operations": [
{
"id": "table.user",
"label": "Create table user",
"operationClass": "additive"
}
]
},
"execution": {
"operationsPlanned": 4,
"operationsExecuted": 4
},
"marker": {
"storageHash": "sha256:abc123..."
}
}Error Codes:
PN-CLI-4004: Contract file not foundPN-CLI-4005: Missing database URLPN-CLI-4008: Unsupported JSON format (--json ndjsonis rejected fordb init)PN-CLI-4010: Missing driver in configPN-CLI-4020: Migration planning failed (conflicts)PN-CLI-4021: Target does not support migrationsPN-RUN-3000: Runtime error (includes marker mismatch failures)
Behavior Notes:
- If the database already has a marker that matches the destination contract,
db initsucceeds as a noop (0 operations planned/executed). - If the database has a marker that does not match the destination contract,
db initfails (including in--dry-runmode). Usedb initfor bootstrapping; use your migration workflow to reconcile existing databases.
Update your database schema to match the currently emitted contract.
db update differs from db init:
- Works on any database, whether or not it has been initialized with
db init(creates the signature table if missing) - Allows
additive,widening, anddestructiveoperation classes where supported by planner/runner - Disables per-operation runner execution checks by default (precheck/postcheck/idempotency)
- In
--dry-runmode for SQL targets, prints a DDL preview derived from planned operations - In interactive mode, destructive plans require confirmation before apply
- In non-interactive mode, destructive plans fail unless
-y, --yesis provided
Command:
prisma-next db update [--db <url>] [--config <path>] [--dry-run] [-y|--yes] [--interactive|--no-interactive] [--json] [-v] [-q] [--color/--no-color]Error codes (additional to shared CLI/runtime codes):
RUNNER_FAILED: runner rejected apply (origin mismatch, failed checks, policy failures, or execution errors)
Config File (prisma-next.config.ts):
The CLI uses a config file to specify the target family, target, adapter, extensionPacks, and contract.
Config Discovery:
--config <path>: Explicit path (relative or absolute)- Default:
./prisma-next.config.tsin current working directory - No upward search (stays in CWD)
Note: The CLI uses c12 for config loading, but constrains it to the current working directory (no upward search) to match the style guide's discovery precedence.
import { defineConfig } from '@prisma-next/cli/config-types';
import { typescriptContract } from '@prisma-next/sql-contract-ts/config-types';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgres from '@prisma-next/target-postgres/control';
import sql from '@prisma-next/family-sql/control';
import { contract } from './prisma/contract';
export default defineConfig({
family: sql,
target: postgres,
adapter: postgresAdapter,
extensionPacks: [],
contract: typescriptContract(contract, 'src/prisma/contract.json'),
});Prefer helper utilities for authoring mode selection:
typescriptContract(contract, outputPath?)from@prisma-next/sql-contract-ts/config-typesfor TS-authored contractsprismaContract(schemaPath, { output?, target? })from@prisma-next/sql-contract-psl/providerfor PSL-authored providers- Provider failures are returned as structured diagnostics for CLI rendering
The contract.output field specifies the path to contract.json. This is the canonical location where other CLI commands can find the contract JSON artifact. Defaults to 'src/prisma/contract.json' if not specified.
contract.d.ts is always colocated with contract.json and derived from contract.output (contract.json → contract.d.ts).
Output:
contract.json: Includes_generatedmetadata field indicating it's a generated artifact (excluded from canonicalization/hashing)contract.d.ts: Includes warning header comments indicating it's a generated file
Plan a migration from contract changes. Compares a starting contract against a destination contract and produces a new migration package with the required operations. No database connection is needed — fully offline.
prisma-next migration plan [--config <path>] [--name <slug>] [--from <contract>] [--to <contract>] [--json] [-v] [-q] [--color/--no-color]Options:
--config <path>: Path toprisma-next.config.ts--name <slug>: Name slug for the migration directory (default:migration)--from <contract>: Starting contract reference (hash, prefix, ref name, migration directory,<dir>^, or filesystem path). Defaults to thedbref (greenfield when absent).--to <contract>: Destination contract reference (same grammar as--from). Defaults to the emittedcontract.json. Use--to <migration-dir>^to plan a rollback toward a predecessor state.--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)
What it does:
- Loads config and resolves the destination contract:
--to <contract>if provided, otherwisecontract.json - Reads existing migrations from
config.migrations.dir(default:migrations/) - Determines the starting point:
--from <contract>if provided, otherwise thedbref (greenfield when absent) - Diffs the starting contract against the destination using the target's migration planner
- Scaffolds a new migration package:
migration.ts(containingplaceholder(...)lambdas for any data transforms),migration.json(with a content-addressedmigrationHashover the planned ops, or over[]when the planner could not lower any calls because of placeholders),ops.json(the planned ops, or[]in the placeholder-blocked case), and contract bookends. The package is always fully attested — there is no draft state on disk. - If the plan has unfilled
placeholder(...)slots, the command returns a successfulpendingPlaceholdersenvelope (a warning, not a failure) asking the developer to fill in the slots before re-emitting. The on-diskops.jsonis[]andmigrationHashis the hash of(metadata, []), so applying the migration as-written will not advance the storage hash to the intended destination — the runner's destination-hash post-check surfaces this as a state mismatch. After filling in the placeholders, runnode migrations/<dir>/migration.tsto re-emitops.jsonand the correspondingmigrationHash.PN-MIG-2001is raised only at self-emit time when a slot is still unfilled.
Outputs:
migrations/<dir>/migration.ts— editable migration source (withplaceholder(...)slots when the planner inserted them)migrations/<dir>/migration.json— fully attested metadata (migrationHash: string, never null)migrations/<dir>/ops.json— planned operations (empty list[]if placeholders blocked the planner)migrations/<dir>/start-contract.{json,d.ts}— bookend from the "from" side (when applicable)migrations/<dir>/end-contract.{json,d.ts}— bookend from the "to" side
Branching with --from and --to: Use --from to create a migration edge from a specific contract hash instead of the default starting point. Use --to to plan toward any resolved contract — including a rollback via <migration-dir>^ — instead of the emitted contract. This enables branched migration graphs and arbitrary-target (including reverse) edges without editing contract source.
Display a migration package's operations, DDL preview, and metadata. Accepts a directory path, a hash prefix (git-style matching against migrationHash), or defaults to the latest migration.
prisma-next migration show [target] [--config <path>] [--json] [-v] [-q] [--color/--no-color]Options:
[target]: Migration directory path ormigrationHashprefix (defaults to latest)--config <path>: Path toprisma-next.config.ts--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output
What it does:
- If
targetis a path (contains/or\), reads that directory directly - If
targetis a hash prefix, scans all attested migrations and matches againstmigrationHash - If no target, defaults to the latest migration
- Displays operations with operation class badges, destructive warnings, and DDL preview
Destructive warnings: When a migration contains destructive operations (e.g., DROP TABLE, ALTER COLUMN TYPE), the output includes a prominent ⚠ warning about potential data loss.
Show the migration graph and applied status. Adapts based on context:
- With DB connection: Shows applied/pending markers and "you are here" indicators
- Without DB connection: Shows the graph structure from disk only
- With
--ref: Targets a specific ref instead of the contract hash; all refs fromrefs.jsonare rendered on the graph
prisma-next migration status [--db <url>] [--ref <name>] [--config <path>] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (enables online mode)--ref <name>: Target a named ref frommigrations/refs.jsoninstead of the current contract hash--config <path>: Path toprisma-next.config.ts--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output
What it does:
- Reads migration packages from disk and reconstructs the migration graph
- Loads all refs from
migrations/refs.json(if present) and renders them on the graph - If
--refis provided, uses the ref's hash as the target instead of the contract hash; the active ref is highlighted in bold, other refs are dimmed - If a DB connection is available, reads the marker to determine applied/pending status and shows distance from the ref target (e.g., "2 edge(s) behind ref")
- Displays the graph with
◄ DB,◄ Contract, and◄ ref:<name>markers - Shows operation summaries with destructive operation highlighting
- In
--refmode, theCONTRACT.AHEADwarning is suppressed — contract being ahead of a ref target is expected in multi-environment workflows
Branched graphs: When the migration graph has multiple branches (divergence), status reports an AMBIGUOUS_TARGET error with the divergence point and branch details. Use --ref to target a specific branch.
Apply planned migrations to the database. Executes previously planned migrations (created by migration plan). Compares the database marker against the migration graph to determine which migrations are pending, then executes them sequentially. Each migration runs in its own transaction. Does not plan new migrations — run migration plan first.
prisma-next migrate [--db <url>] [--to <contract>] [--config <path>] [--json] [-v] [-q] [--color/--no-color]Options:
--db <url>: Database connection string (optional; defaults toconfig.db.connection)--to <contract>: Target contract reference (hash, prefix, ref name, migration directory,<dir>^, or filesystem path). When omitted, applies toward the emittedcontract.json. When--toresolves to an on-disk graph node, verification and apply use that bundle'send-contract.json— so a planned rollback or other arbitrary-target edge applies without editing contract source.--ref <name>: Target a named ref frommigrations/refs.jsoninstead of the current contract hash--config <path>: Path toprisma-next.config.ts--json: Output as JSON object-q, --quiet: Quiet mode (errors only)-v, --verbose: Verbose output (debug info, timings)
What it does:
- Reads migration packages from
config.migrations.dir. Every package is attested — there is no on-disk draft state. The loader (readMigrationPackagein@prisma-next/migration-tools/io) rehashes(metadata, ops)for eachMigrationPackageit returns and confirms the result matches the storedmigrationHash. If a package has been hand-edited or partially written since emit, the load fails withMIGRATION.HASH_MISMATCHpointing at the offending directory and asks the developer to re-runnode migrations/<dir>/migration.ts(or restore from version control). - Reconstructs the migration graph from all loaded packages
- Determines the destination hash and apply contract: from
--to/--ref, or fromcontract.jsonwhen neither is supplied - Connects to the database and reads the current marker hash
- Finds the shortest path from the marker hash to the destination using graph pathfinding
- Executes each pending migration in order using the target's
MigrationRunner - Each migration runs in its own transaction with prechecks, postchecks, and idempotency checks enabled
- After each migration, the runner runs the migration's post-checks and verifies the resulting state matches the target contract's storage hash, then updates the marker/ledger
Rollback workflow: When no on-disk edge reaches the target (for example migrate --to <migration-dir>^), the command refuses with MIGRATION.PATH_UNREACHABLE and suggests planning the missing edge with migration plan --from <current> --to <target> --name <slug>, then re-running migrate --to <target>. No contract-source edit is required.
Config requirements: Requires driver and db.connection (or --db). migrations.dir is optional and defaults to migrations/.
Resume semantics: If a migration fails, previously applied migrations are preserved. Re-running migrate resumes from the last successful migration.
Ref-based routing: With --ref, apply targets the ref's hash instead of the contract hash. This enables multi-environment workflows where staging and production track different points in the migration graph.
There is no dedicated CLI command for emitting a migration — migrations
self-emit. After scaffolding (via migration plan or migration new),
run migration.ts directly with Node to produce ops.json and attest
migration.json:
node migrations/<dir>/migration.tsThe scaffolded migration.ts calls MigrationCLI.run(import.meta.url, ...) from @prisma-next/cli/migration-cli when invoked as the entrypoint. (Postgres and SQLite scaffolds re-export MigrationCLI through @prisma-next/postgres/migration or @prisma-next/sqlite/migration so a migration.ts only needs the single facade import; Mongo scaffolds still pull from @prisma-next/cli/migration-cli directly.) The CLI entrypoint loads prisma-next.config.ts, assembles a ControlStack, instantiates the migration with that stack (so dataTransform and other adapter-aware helpers can materialize a real adapter), and serializes operations to ops.json while writing the content-addressed migrationHash into migration.json. If migration.ts contains unfilled placeholder() slots, the script exits with PN-MIG-2001 and reports the slot to fill in.
MigrationCLI.run accepts an optional third argument { argv?, stdout?, stderr? } for in-process testability (default: process.argv / process.stdout / process.stderr) and returns the exit code as a Promise<number>. The flag surface is --help / --dry-run / --config <path>, parsed by clipanion. The main multi-command surface (prisma-next contract emit, db verify, etc.) uses Commander; the per-migration MigrationCLI.run entrypoint uses clipanion to keep authored migration files lightweight and in-process testable.
Manage named refs in migrations/refs.json. Refs map logical environment names (e.g., staging, production) to contract hashes, enabling multi-environment migration workflows where different environments track different points in the migration graph.
prisma-next ref set <name> <contract> # Set a ref to a contract (hash, ref, dir, ...)
prisma-next ref list # List all refs (use `ref list` and filter for one ref)
prisma-next ref delete <name> # Delete a refOptions (all subcommands):
--config <path>: Path toprisma-next.config.ts--json: Output as JSON object-q, --quiet: Quiet mode (errors only)
Ref naming rules: Lowercase alphanumeric with hyphens or forward slashes (e.g., staging, prod/us-east). No . or .. segments.
Ref values: Must be valid contract hashes (sha256:<64 hex chars> or sha256:empty).
Atomic writes: refs.json is written atomically via temp file + rename to prevent corruption from concurrent writes.
flowchart TD
CLI[CLI Entry Point]
CMD_EMIT[Emit Command]
CMD_DB[DB Commands]
CMD_MIG[Migration Commands]
EXEC_EMIT[executeContractEmit]
PUBLISH[publishContractArtifactPair]
EMIT[Emitter]
CTRL[Control Client]
MIG_TOOLS["@prisma-next/migration-tools"]
FS[File System]
VITE["@prisma-next/vite-plugin-contract-emit"]
CLI --> CMD_EMIT
CLI --> CMD_DB
CLI --> CMD_MIG
CMD_EMIT --> EXEC_EMIT
VITE --> EXEC_EMIT
EXEC_EMIT --> EMIT
EXEC_EMIT --> PUBLISH
PUBLISH --> FS
CMD_DB --> CTRL
CMD_MIG --> CTRL
CMD_MIG --> MIG_TOOLS
MIG_TOOLS --> FS
CTRL --> FS
For agents/contributors:
executeContractEmitis the SINGLE publication path forcontract.json+contract.d.ts. The CLI command (prisma-next contract emit) and the Vite plugin (@prisma-next/vite-plugin-contract-emit) both call into it. Do NOT re-implement the load → emit → publish dance in a new caller; if you need additional behavior, extendContractEmitOptions/ContractEmitResultand updateexecuteContractEmititself.
How it composes:
- The whole flow (load config → resolve source → emit bytes → publish) is
serialized per output JSON path via
queueEmitByOutput(src/utils/emit-queue.ts). Concurrent calls for the same output line up FIFO; concurrent calls for distinct outputs run in parallel. Last submission wins on disk. - Within a single emit,
publishContractArtifactPair(src/utils/publish-contract-artifact-pair.ts) stages temp files, renamescontract.d.tsbeforecontract.json, and attempts to restore the previous pair if either rename fails — so type-only consumers never observe a mismatched pair. - Long-lived hosts (Vite dev server, watch CLIs) must call
disposeEmitQueueon shutdown to drop the per-output queue state, otherwise the module-global queue map leaks one entry per unique output path.
The validateContractDeps warning is returned in ContractEmitResult.validationWarning
rather than written to stderr by the operation — callers (CLI, Vite plugin) decide
how to render it (ui.warn, plugin logger, etc.).
The defineConfig() function validates and normalizes configs using Arktype:
- Validation: Validates config structure using Arktype schemas
- Normalization: Applies default values (e.g.,
contract.outputdefaults to'src/prisma/contract.json') - Error Messages: Provides clear, actionable error messages on validation failure
See .cursor/rules/config-validation-and-normalization.mdc for detailed patterns.
- Main entry point using commander
- Parses arguments and routes to command handlers
- Handles global flags (
--help,--version) - Exit codes: 0 (success), 1 (runtime error), 2 (usage/config error)
- Error Handling: Uses
exitOverride()to catch unhandled errors (non-structured errors that fail fast) and print stack traces. Commands handle structured errors themselves viaprocess.exit(). - Command Taxonomy: Groups commands by domain/plane (e.g.,
contract emit) - Help Formatting: Uses
configureHelp()to customize help output with styled format matching normal command output. Root help shows "prisma-next" title with command tree; command help shows "prisma-next ➜ " with options and docs URLs. Seeutils/formatters/help.tsfor help formatters. - Command Descriptions: See the “Command Descriptions” section above for
setCommandDescriptions()usage.
- Canonical command implementation using commander
- Supports global flags (JSON, verbosity, color, interactive, yes)
- Error Handling: Uses structured errors (
CliStructuredError), Result pattern, andprocess.exit(). Commands returnResult<T, CliStructuredError>, process results withhandleResult(), and callprocess.exit(exitCode)directly. See.cursor/rules/cli-error-handling.mdcfor details. - Loads the user's config module (
prisma-next.config.ts) - Resolves contract from provider:
- Calls
config.contract.source.load(context)—context.resolvedInputscarries the absolute paths the CLI loader resolved fromsource.inputs— and expectsResult<Contract, ContractSourceDiagnostics> - Source-specific parsing/loading stays inside providers
- Provider diagnostics are surfaced as actionable CLI failures
- Throws error if
config.contractis missing
- Calls
- Uses artifact path from
config.contract.output(already normalized bydefineConfig()with defaults applied) - Creates family instance via
config.family.create()(assembles operation registry, type imports, extension IDs) - Calls
familyInstance.emitContract()with raw contract (instance handles stripping mappings and validation internally) - Outputs human-readable or JSON format based on flags
emitContract(options): Programmatic API for emitting contracts- Accepts resolved contract, output paths, and assembly data
- Caller is responsible for loading the contract and resolving paths
- Returns result with hashes, file paths, and timings
- Used by CLI command internally
- Structured Errors: Call sites throw
CliStructuredErrorinstances with full context (why, fix, docsUrl, etc.) - Result Pattern: Commands return
Result<T, CliStructuredError>and usehandleResult()for output and exit codes - Error Conversion:
CliStructuredError.toEnvelope()converts errors to envelopes for output formatting - Result Processing:
handleResult()processes Results, formats output, and returns exit codes - Exit Codes:
- Usage/config errors (PN-CLI-4001-4007) → exit code 2
- Runtime errors (PN-RUN-3xxx) → exit code 1
- Success → exit code 0
- Fail Fast: Non-structured errors propagate and are caught by Commander.js's
exitOverride()with stack traces - See
.cursor/rules/cli-error-handling.mdcfor detailed patterns
- Family instances now handle pack assembly internally. The CLI creates a family instance via
config.family.create()and reads assembly data (operation registry, type imports, extension IDs) from the instance. - Removed:
pack-assembly.tshas been removed. Pack assembly is now handled by family instances. For SQL family, tests can import pack-based helpers directly frompackages/2-sql/3-tooling/family/src/core/assembly.tsusing relative paths. - Assembly logic is family-specific and owned by each family's instance implementation (e.g.,
createSqlFamilyInstancein@prisma-next/family-sql).
- Command Output Formatters: Format human-readable output for commands (emit, verify, etc.)
- Paths are shown as relative paths from current working directory (using
relative(process.cwd(), path)) - Success indicators use consistent checkmark (✔) throughout
- Paths are shown as relative paths from current working directory (using
- Error Output Formatters: Format error output for human-readable and JSON display
- Styled Headers:
formatStyledHeader()creates styled headers for command output with "prisma-next ➜ " format- Parameter labels include colons (e.g.,
config:,contract:) - Uses fixed 20-character left column width for consistent alignment
- Parameter labels include colons (e.g.,
- Help Formatters:
formatRootHelp()- Formats root help with "prisma-next" title, command tree, and multiline descriptionformatCommandHelp()- Formats command help with "prisma-next ➜ ", options, subcommands, docs URLs, and multiline descriptionrenderCommandTree()- Shared function to render hierarchical command trees with tree characters (├─, └─, │)- Fixed-Width Formatting: All two-column output (help, styled headers) uses fixed 20-character left column width
- Text Wrapping: Right column wraps at 90 characters using
wrap-ansifor ANSI-aware wrapping - Default Values: Options with default values display
default: <value>on the following line (dimmed) - ANSI-Aware Padding: Uses
string-widthandstrip-ansito measure and pad text correctly with ANSI codes - Help formatters use the same styling system as normal command output (colors, dim text, badges)
- Short descriptions appear in command trees and headers; long descriptions appear at the bottom of help output
- Help formatting is configured via
configureHelp()incli.tsto apply to all commands
- The SQL family (and other families) provide:
create(options)- Creates a family instance that implements domain actionshook- Target family hook for contract emission
- Family instances provide:
deserializeContract(contractJson)- Validates and normalizes contract, returnsContractwithout mappingsemitContract(options)- Emits contract (handles stripping mappings and validation internally)verify(options)- Verifies database marker against contractschemaVerify(options)- Verifies database schema against contractintrospect(options)- Introspects database schema
- Families expose component descriptors (target, adapter, driver, extensions) as plain TypeScript objects. Each descriptor includes declarative fields: metadata that describes what the component provides (independent of its runtime implementation), and that the CLI can safely copy into emitted artifacts.
- Common declarative keys:
version: Component version included in emitted metadata (useful for debugging and reproducibility).capabilities: Feature flags the component contributes (e.g., adapter/runtime lowering requirements). Typically namespaced by target (e.g.,{ postgres: { returning: true } }) so contracts can be validated against the active target.types: Type import specs and type IDs contributed by the component. Common examples:types.codecTypes.import: Where to import codec type mappings forcontract.d.ts.types.queryOperationTypes.import: Where to import flat query-builder operation type signatures forcontract.d.ts(adapters/extensions).types.storage: Storage type bindings (typeId,nativeType, etc.) used in authoring/emission.
operations: Operation signatures the component contributes (extensions), used for type generation and (optionally) validation/lowering.- Component-specific metadata:
- Extensions may also include control-plane-only metadata like
contractSpace(used by verify, planning, and migration flows and not required at runtime).
- Extensions may also include control-plane-only metadata like
- Common declarative keys:
Unlike the older manifest-based IR approach (separate JSON manifests + a parsing/validation step to build an IR), descriptors are imported directly from packages (e.g., @prisma-next/*/control). This removes a file-format boundary and keeps the data and its types co-located.
- Benefits: fewer moving parts (no JSON parsing), easier refactors (TypeScript catches drift), and clearer ownership (the package exports the canonical descriptor object).
- Trade-offs: descriptors must be available as build-time imports (less dynamic discovery vs scanning arbitrary manifest files).
Illustrative example (descriptor object):
import type { SqlControlExtensionDescriptor } from '@prisma-next/family-sql/control';
const exampleExtension: SqlControlExtensionDescriptor<'postgres'> = {
kind: 'extension',
id: 'example',
version: '1.0.0',
familyId: 'sql',
targetId: 'postgres',
capabilities: { postgres: { 'example/feature': true } },
types: {
queryOperationTypes: {
import: {
package: '@prisma-next/extension-example/operation-types',
named: 'QueryOperationTypes',
alias: 'ExampleQueryOperationTypes',
},
},
},
operations: [],
create: () => ({ familyId: 'sql', targetId: 'postgres' }),
};
export default exampleExtension;How CLI consumers import/use it:
- Config imports descriptors directly and passes them to
defineConfig()(see “Config File Requirements” underprisma-next contract emitabove; also see “Entrypoints” below for the@prisma-next/*/controlsubpaths):
import { defineConfig } from '@prisma-next/cli/config-types';
import exampleExtension from '@prisma-next/extension-example/control';
export default defineConfig({
// family/target/adapter/driver omitted for brevity
extensionPacks: [exampleExtension],
});commander: CLI argument parsing and command routingesbuild: Bundling TypeScript contract files with import allowlisting@prisma-next/emitter: Contract emission engine (returns strings)@prisma-next/migration-tools: On-disk migration I/O, hash verification, and history reconstruction@prisma-next/framework-components: Control plane types, migration operation types, control stack (via./control)@prisma-next/errors: Error types and factories (via./control)
- Import Allowlist: Only
@prisma-next/*packages allowed (MVP). Expand later if needed. - Utility Separation: TS contract loading is a utility function, not a command. Commands use utilities.
- CLI Framework: Use
commanderlibrary for robust CLI argument parsing. - File I/O: CLI handles all I/O; emitter returns strings (no file operations in emitter).
- Generated File Metadata: Adds
_generatedmetadata field tocontract.jsonto indicate it's a generated artifact. This field is excluded from canonicalization/hashing to ensure determinism. Thecontract.d.tsfile includes warning header comments generated by the emitter hook.
The CLI package includes unit tests, integration tests, and e2e tests:
- Unit tests: Test individual functions and utilities in isolation
- Integration tests: Test component interactions (e.g., config loading, pack assembly)
- E2E tests: Test complete command execution with real config files
E2E tests use a shared fixture app pattern to ensure proper module resolution:
- Shared fixture app:
test/cli-e2e-test-app/contains a staticpackage.jsonwith dependencies - Fixture organization: Fixtures are organized by command in subdirectories (e.g.,
fixtures/emit/,fixtures/db-verify/) - Ephemeral test directories: Each test creates an isolated directory with files copied from fixtures
- No package.json in test directories: Test directories inherit workspace dependencies from the parent
package.jsonat the root - Helper function:
setupTestDirectoryFromFixtures()handles directory setup and returns a cleanup function - Cleanup responsibility: Each test must clean up its own directory (use
afterEachhooks orfinallyblocks)
Example:
import { setupTestDirectoryFromFixtures } from './utils/test-helpers';
const fixtureSubdir = 'emit';
it('test description', async () => {
const testSetup = setupTestDirectoryFromFixtures(
fixtureSubdir,
'prisma-next.config.emit.ts',
);
const cleanupDir = testSetup.cleanup;
try {
// ... test code ...
} finally {
cleanupDir(); // Each test cleans up its own directory
}
});See .cursor/rules/cli-e2e-test-patterns.mdc for detailed patterns and examples.
Run tests:
pnpm test # Run all tests
pnpm test:unit # Run unit tests only
pnpm test:integration # Run integration tests only
pnpm test:e2e # Run e2e tests onlyThe CLI package provides a programmatic control client for running control-plane operations without using the command line. This is useful for:
- Integration with build tools and CI pipelines
- Custom orchestration workflows
- Test automation
- Programmatic database management
import { createControlClient } from '@prisma-next/cli/control-api';
import sql from '@prisma-next/family-sql/control';
import postgres from '@prisma-next/target-postgres/control';
import postgresAdapter from '@prisma-next/adapter-postgres/control';
import postgresDriver from '@prisma-next/driver-postgres/control';
// Create a control client with framework component descriptors
const client = createControlClient({
family: sql,
target: postgres,
adapter: postgresAdapter,
driver: postgresDriver,
extensionPacks: [],
});
try {
// Connect to database
await client.connect(databaseUrl);
// Run operations
const verifyResult = await client.verify({ contract });
const initResult = await client.dbInit({ contract, mode: 'apply' });
const updateResult = await client.dbUpdate({ contract, mode: 'apply' });
const introspectResult = await client.introspect();
} finally {
// Clean up
await client.close();
}| Method | Description |
|---|---|
connect(url) |
Establishes database connection |
close() |
Closes connection (idempotent) |
readMarker() |
Reads contract marker from database (null if none) |
verify(options) |
Verifies database marker matches contract |
schemaVerify(options) |
Verifies database schema satisfies contract |
sign(options) |
Writes contract marker to database |
dbInit(options) |
Initializes database schema from contract |
dbUpdate(options) |
Updates database schema to match contract |
migrate(options) |
Advances the database to the target contract via the migration graph |
introspect(options) |
Introspects database schema |
Operations return structured result types:
readMarker()→ContractMarkerRecord | nullverify()→VerifyDatabaseResultschemaVerify()→VerifyDatabaseSchemaResultsign()→SignDatabaseResultdbInit()→Result<DbInitSuccess, DbInitFailure>(uses Result pattern)dbUpdate()→Result<DbUpdateSuccess, DbUpdateFailure>(uses Result pattern)migrate()→Result<MigrateSuccess, MigrateFailure>(uses Result pattern)introspect()→ Schema IR (family-specific)
- Connection errors: Thrown as exceptions from
connect() - Not connected errors: Thrown if operations called before
connect() - Driver not configured: Thrown if driver is not provided in options
- Operation failures: Returned as structured results (not thrown)
| Aspect | CLI | Control API |
|---|---|---|
| Config | Reads prisma-next.config.ts |
Accepts descriptors directly |
| File I/O | Reads contract.json from disk | Accepts contract directly |
| Output | Formats for console | Returns structured data |
| Exit codes | Uses process.exit() |
Returns results/throws |
The CLI package exports several subpaths for different use cases:
@prisma-next/cli(main export): ExportsloadContractFromTsandcreateContractEmitCommand@prisma-next/cli/config-types: ExportsdefineConfigand config types@prisma-next/cli/control-api: ExportscreateControlClientand control API types@prisma-next/cli/commands/db-init: ExportscreateDbInitCommand@prisma-next/cli/commands/db-update: ExportscreateDbUpdateCommand@prisma-next/cli/commands/db-schema: ExportscreateDbSchemaCommand@prisma-next/cli/commands/db-sign: ExportscreateDbSignCommand@prisma-next/cli/commands/db-verify: ExportscreateDbVerifyCommand@prisma-next/cli/commands/contract-emit: ExportscreateContractEmitCommand@prisma-next/cli/commands/contract-infer: ExportscreateContractInferCommand@prisma-next/cli/commands/migration-plan: ExportscreateMigrationPlanCommand@prisma-next/cli/commands/migration-show: ExportscreateMigrationShowCommand@prisma-next/cli/commands/migration-status: ExportscreateMigrationStatusCommand@prisma-next/cli/commands/migrate: ExportscreateMigrateCommand@prisma-next/config-loader: ExportsloadConfig
Important: loadContractFromTs is exported from the main package (@prisma-next/cli). See .cursor/rules/cli-package-exports.mdc for import patterns.
This package is part of the framework domain, tooling layer, migration plane:
- Domain: framework (target-agnostic)
- Layer: tooling
- Plane: migration
- Path:
packages/1-framework/3-tooling/cli
@prisma-next/emitter- Contract emission engine- Project Brief — CLI Support for Extension Packs:
docs/briefs/complete/20-CLI-Support-for-Extension-Packs.md