@prisma-next/cli 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,840 @@
1
+ # @prisma-next/cli
2
+
3
+ Command-line interface for Prisma Next contract emission and management.
4
+
5
+ ## Overview
6
+
7
+ 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.
8
+
9
+ ## Purpose
10
+
11
+ Provide a command-line interface that:
12
+ - Loads TypeScript-authored contracts using esbuild with import allowlisting
13
+ - Validates contract purity (JSON-serializable, no functions/getters)
14
+ - Invokes the emitter to produce canonical artifacts
15
+ - Handles all file I/O operations (CLI handles I/O; emitter returns strings)
16
+
17
+ ## Responsibilities
18
+
19
+ - **TS Contract Loading**: Bundle and load TypeScript contract files with import allowlist enforcement
20
+ - **CLI Command Interface**: Parse arguments and route to command handlers using commander
21
+ - **File I/O**: Read TS contracts, write emitted artifacts (`contract.json`, `contract.d.ts`)
22
+ - **Extension Pack Loading**: Load adapter and extension pack manifests for emission
23
+ - **Help Output Formatting**: Custom styled help output with command trees and formatted descriptions
24
+ - **Config Management**: Load and validate `prisma-next.config.ts` files using Arktype validation
25
+
26
+ **Note**: Control plane domain actions (database verification, contract emission) are implemented in `@prisma-next/core-control-plane`. The CLI uses the control plane domain actions programmatically but does not define control plane types itself.
27
+
28
+ ## Command Descriptions
29
+
30
+ Commands use separate short and long descriptions via `setCommandDescriptions()`:
31
+
32
+ - **Short description**: One-liner used in command trees and headers (e.g., "Emit signed contract artifacts")
33
+ - **Long description**: Multiline text shown at the bottom of help output with detailed context
34
+
35
+ See `.cursor/rules/cli-command-descriptions.mdc` for details.
36
+
37
+ ## Commands
38
+
39
+ ### `prisma-next contract emit` (canonical)
40
+
41
+ Emit `contract.json` and `contract.d.ts` from `config.contract`.
42
+
43
+ **Canonical command:**
44
+ ```bash
45
+ prisma-next contract emit [--config <path>] [--json] [-v] [-q] [--timestamps] [--color/--no-color]
46
+ ```
47
+
48
+ **Config File Requirements:**
49
+
50
+ The `contract emit` command requires a `driver` in the config (even though it doesn't use it) because `ControlFamilyDescriptor.create()` requires it for consistency:
51
+
52
+ ```typescript
53
+ import { defineConfig } from '@prisma-next/cli/config-types';
54
+ import postgresAdapter from '@prisma-next/adapter-postgres/control';
55
+ import postgresDriver from '@prisma-next/driver-postgres/control';
56
+ import postgres from '@prisma-next/targets-postgres/control';
57
+ import sql from '@prisma-next/family-sql/control';
58
+ import { contract } from './prisma/contract';
59
+
60
+ export default defineConfig({
61
+ family: sql,
62
+ target: postgres,
63
+ adapter: postgresAdapter,
64
+ driver: postgresDriver, // Required even though emit doesn't use it
65
+ extensions: [],
66
+ contract: {
67
+ source: contract,
68
+ output: 'src/prisma/contract.json',
69
+ types: 'src/prisma/contract.d.ts',
70
+ },
71
+ });
72
+ ```
73
+
74
+ Options:
75
+ - `--config <path>`: Optional. Path to `prisma-next.config.ts` (defaults to `./prisma-next.config.ts` if present)
76
+ - `--json`: Output as JSON object
77
+ - `-q, --quiet`: Quiet mode (errors only)
78
+ - `-v, --verbose`: Verbose output (debug info, timings)
79
+ - `-vv, --trace`: Trace output (deep internals, stack traces)
80
+ - `--timestamps`: Add timestamps to output
81
+ - `--color/--no-color`: Force/disable color output
82
+
83
+ Examples:
84
+ ```bash
85
+ # Use config defaults
86
+ prisma-next contract emit
87
+
88
+ # JSON output
89
+ prisma-next contract emit --json
90
+
91
+ # Verbose output with timestamps
92
+ prisma-next contract emit -v --timestamps
93
+ ```
94
+
95
+ ### `prisma-next db verify`
96
+
97
+ Verify that a database instance matches the emitted contract by checking marker presence, hash equality, and target compatibility.
98
+
99
+ **Command:**
100
+ ```bash
101
+ prisma-next db verify [--db <url>] [--config <path>] [--json] [-v] [-q] [--timestamps] [--color/--no-color]
102
+ ```
103
+
104
+ Options:
105
+ - `--db <url>`: Database connection string (optional, falls back to `config.db.url` or `DATABASE_URL` environment variable)
106
+ - `--config <path>`: Optional. Path to `prisma-next.config.ts` (defaults to `./prisma-next.config.ts` if present)
107
+ - `--json`: Output as JSON object
108
+ - `-q, --quiet`: Quiet mode (errors only)
109
+ - `-v, --verbose`: Verbose output (debug info, timings)
110
+ - `-vv, --trace`: Trace output (deep internals, stack traces)
111
+ - `--timestamps`: Add timestamps to output
112
+ - `--color/--no-color`: Force/disable color output
113
+
114
+ Examples:
115
+ ```bash
116
+ # Use config defaults
117
+ prisma-next db verify
118
+
119
+ # Specify database URL
120
+ prisma-next db verify --db postgresql://user:pass@localhost/db
121
+
122
+ # JSON output
123
+ prisma-next db verify --json
124
+
125
+ # Verbose output with timestamps
126
+ prisma-next db verify -v --timestamps
127
+ ```
128
+
129
+ **Config File Requirements:**
130
+
131
+ The `db verify` command requires a `driver` in the config to connect to the database:
132
+
133
+ ```typescript
134
+ import { defineConfig } from '@prisma-next/cli/config-types';
135
+ import postgresAdapter from '@prisma-next/adapter-postgres/control';
136
+ import postgresDriver from '@prisma-next/driver-postgres/control';
137
+ import postgres from '@prisma-next/targets-postgres/control';
138
+ import sql from '@prisma-next/family-sql/control';
139
+ import { contract } from './prisma/contract';
140
+
141
+ export default defineConfig({
142
+ family: sql,
143
+ target: postgres,
144
+ adapter: postgresAdapter,
145
+ driver: postgresDriver,
146
+ extensions: [],
147
+ contract: {
148
+ source: contract,
149
+ output: 'src/prisma/contract.json',
150
+ types: 'src/prisma/contract.d.ts',
151
+ },
152
+ db: {
153
+ url: process.env.DATABASE_URL, // Optional: can also use --db flag
154
+ },
155
+ });
156
+ ```
157
+
158
+ **Verification Process:**
159
+
160
+ 1. **Load Contract**: Reads the emitted `contract.json` from `config.contract.output`
161
+ 2. **Connect to Database**: Uses `config.driver.create(url)` to create a driver
162
+ 3. **Create Family Instance**: Calls `config.family.create()` with target, adapter, driver, and extensions to create a family instance
163
+ 4. **Verify**: Calls `familyInstance.verify()` which:
164
+ - Reads the contract marker from the database
165
+ - Compares marker presence: Returns `PN-RTM-3001` if marker is missing
166
+ - Compares target compatibility: Returns `PN-RTM-3003` if contract target doesn't match config target
167
+ - Compares core hash: Returns `PN-RTM-3002` if `coreHash` doesn't match
168
+ - Compares profile hash: Returns `PN-RTM-3002` if `profileHash` doesn't match (when present)
169
+ - Checks codec coverage (optional): Compares contract column types against supported codec types and reports missing codecs
170
+
171
+ **Output Format (TTY):**
172
+
173
+ Success:
174
+ ```
175
+ ✔ Database matches contract
176
+ coreHash: sha256:abc123...
177
+ profileHash: sha256:def456...
178
+ ```
179
+
180
+ Failure:
181
+ ```
182
+ ✖ Marker missing (PN-RTM-3001)
183
+ Why: Contract marker not found in database
184
+ Fix: Run `prisma-next db sign --db <url>` to create marker
185
+ ```
186
+
187
+ **Output Format (JSON):**
188
+
189
+ ```json
190
+ {
191
+ "ok": true,
192
+ "summary": "Database matches contract",
193
+ "contract": {
194
+ "coreHash": "sha256:abc123...",
195
+ "profileHash": "sha256:def456..."
196
+ },
197
+ "marker": {
198
+ "coreHash": "sha256:abc123...",
199
+ "profileHash": "sha256:def456..."
200
+ },
201
+ "target": {
202
+ "expected": "postgres"
203
+ },
204
+ "missingCodecs": [],
205
+ "meta": {
206
+ "configPath": "/path/to/prisma-next.config.ts",
207
+ "contractPath": "/path/to/src/prisma/contract.json"
208
+ },
209
+ "timings": {
210
+ "total": 42
211
+ }
212
+ }
213
+ ```
214
+
215
+ **Error Codes:**
216
+
217
+ - `PN-CLI-4010`: Missing driver in config — provide a driver descriptor
218
+ - `PN-RTM-3001`: Marker missing - Contract marker not found in database
219
+ - `PN-RTM-3002`: Hash mismatch - Contract hash does not match database marker
220
+ - `PN-RTM-3003`: Target mismatch - Contract target does not match config target
221
+
222
+ **Family Requirements:**
223
+
224
+ The family must provide a `create()` method in the family descriptor that returns a `FamilyInstance` with a `verify()` method:
225
+
226
+ ```typescript
227
+ interface FamilyDescriptor {
228
+ create(options: {
229
+ target: TargetDescriptor;
230
+ adapter: AdapterDescriptor;
231
+ extensions: ExtensionDescriptor[];
232
+ }): FamilyInstance;
233
+ }
234
+
235
+ interface FamilyInstance {
236
+ verify(options: {
237
+ driver: ControlPlaneDriver;
238
+ contractIR: ContractIR;
239
+ expectedTargetId: string;
240
+ contractPath: string;
241
+ configPath?: string;
242
+ }): Promise<VerifyDatabaseResult>;
243
+ }
244
+ ```
245
+
246
+ The SQL family provides this via `@prisma-next/family-sql/control`. The `verify()` method handles reading the marker, comparing hashes, and checking codec coverage internally.
247
+
248
+ ### `prisma-next db introspect`
249
+
250
+ Inspect the live database schema and display it as a human-readable tree or machine-consumable JSON.
251
+
252
+ **Command:**
253
+ ```bash
254
+ prisma-next db introspect [--db <url>] [--config <path>] [--json] [-v] [-q] [--timestamps] [--color/--no-color]
255
+ ```
256
+
257
+ Options:
258
+ - `--db <url>`: Database connection string (optional, falls back to `config.db.url` or `DATABASE_URL` environment variable)
259
+ - `--config <path>`: Optional. Path to `prisma-next.config.ts` (defaults to `./prisma-next.config.ts` if present)
260
+ - `--json`: Output as JSON object
261
+ - `-q, --quiet`: Quiet mode (errors only)
262
+ - `-v, --verbose`: Verbose output (debug info, timings)
263
+ - `-vv, --trace`: Trace output (deep internals, stack traces)
264
+ - `--timestamps`: Add timestamps to output
265
+ - `--color/--no-color`: Force/disable color output
266
+
267
+ Examples:
268
+ ```bash
269
+ # Use config defaults
270
+ prisma-next db introspect
271
+
272
+ # Specify database URL
273
+ prisma-next db introspect --db postgresql://user:pass@localhost/db
274
+
275
+ # JSON output
276
+ prisma-next db introspect --json
277
+
278
+ # Verbose output with timestamps
279
+ prisma-next db introspect -v --timestamps
280
+ ```
281
+
282
+ **Config File Requirements:**
283
+
284
+ The `db introspect` command requires a `driver` in the config to connect to the database:
285
+
286
+ ```typescript
287
+ import { defineConfig } from '@prisma-next/cli/config-types';
288
+ import postgresAdapter from '@prisma-next/adapter-postgres/control';
289
+ import postgresDriver from '@prisma-next/driver-postgres/control';
290
+ import postgres from '@prisma-next/targets-postgres/control';
291
+ import sql from '@prisma-next/family-sql/control';
292
+
293
+ export default defineConfig({
294
+ family: sql,
295
+ target: postgres,
296
+ adapter: postgresAdapter,
297
+ driver: postgresDriver,
298
+ extensions: [],
299
+ db: {
300
+ url: process.env.DATABASE_URL, // Optional: can also use --db flag
301
+ },
302
+ });
303
+ ```
304
+
305
+ **Introspection Process:**
306
+
307
+ 1. **Connect to Database**: Uses `config.driver.create(url)` to create a driver
308
+ 2. **Create Family Instance**: Calls `config.family.create()` with target, adapter, driver, and extensions to create a family instance
309
+ 3. **Introspect**: Calls `familyInstance.introspect()` which:
310
+ - Queries the database catalog to discover schema structure
311
+ - Returns a family-specific schema IR (e.g., `SqlSchemaIR` for SQL family)
312
+ 4. **Transform to Schema View**: Calls `familyInstance.toSchemaView()` to project the schema IR into a `CoreSchemaView` for display
313
+ 5. **Format Output**: Formats the schema view as a human-readable tree or JSON envelope
314
+
315
+ **Output Format (TTY):**
316
+
317
+ Human-readable schema tree:
318
+ ```
319
+ sql schema (tables: 2)
320
+ ├─ table user
321
+ │ ├─ id: int4 (not null)
322
+ │ ├─ email: text (not null)
323
+ │ └─ unique user_email_key
324
+ ├─ table post
325
+ │ ├─ id: int4 (not null)
326
+ │ ├─ title: text (not null)
327
+ │ └─ userId: int4 (not null)
328
+ ├─ extension plpgsql
329
+ └─ extension vector
330
+ ```
331
+
332
+ **Output Format (JSON):**
333
+
334
+ ```json
335
+ {
336
+ "ok": true,
337
+ "summary": "Schema introspected successfully",
338
+ "schema": {
339
+ "root": {
340
+ "kind": "root",
341
+ "id": "sql-schema",
342
+ "label": "sql schema (tables: 2)",
343
+ "children": [
344
+ {
345
+ "kind": "entity",
346
+ "id": "table-user",
347
+ "label": "table user",
348
+ "children": [
349
+ {
350
+ "kind": "field",
351
+ "id": "column-user-id",
352
+ "label": "id: int4 (not null)",
353
+ "meta": {
354
+ "typeId": "pg/int4@1",
355
+ "nullable": false,
356
+ "nativeType": "int4"
357
+ }
358
+ }
359
+ ]
360
+ }
361
+ ]
362
+ }
363
+ },
364
+ "meta": {
365
+ "configPath": "/path/to/prisma-next.config.ts",
366
+ "dbUrl": "postgresql://user:pass@localhost/db"
367
+ },
368
+ "timings": {
369
+ "total": 42
370
+ }
371
+ }
372
+ ```
373
+
374
+ **Error Codes:**
375
+ - `PN-CLI-4010`: Missing driver in config — provide a driver descriptor
376
+ - `PN-CLI-4011`: Missing database URL — provide `--db` flag or `config.db.url` or `DATABASE_URL` environment variable
377
+
378
+ **Family Requirements:**
379
+
380
+ The family must provide:
381
+ 1. A `create()` method in the family descriptor that returns a `FamilyInstance` with an `introspect()` method
382
+ 2. An optional `toSchemaView()` method on the `FamilyInstance` to project family-specific schema IR into `CoreSchemaView`
383
+
384
+ ```typescript
385
+ interface FamilyInstance {
386
+ introspect(options: {
387
+ driver: ControlDriverInstance;
388
+ contractIR?: ContractIR;
389
+ schema?: string;
390
+ }): Promise<FamilySchemaIR>;
391
+
392
+ toSchemaView?(schema: FamilySchemaIR): CoreSchemaView;
393
+ }
394
+ ```
395
+
396
+ 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.
397
+
398
+ **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.
399
+
400
+ ### `prisma-next db sign`
401
+
402
+ 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.
403
+
404
+ **Command:**
405
+ ```bash
406
+ prisma-next db sign [--db <url>] [--config <path>] [--json] [-v] [-q] [--timestamps] [--color/--no-color]
407
+ ```
408
+
409
+ Options:
410
+ - `--db <url>`: Database connection string (optional, falls back to `config.db.url` or `DATABASE_URL` environment variable)
411
+ - `--config <path>`: Optional. Path to `prisma-next.config.ts` (defaults to `./prisma-next.config.ts` if present)
412
+ - `--json`: Output as JSON object
413
+ - `-q, --quiet`: Quiet mode (errors only)
414
+ - `-v, --verbose`: Verbose output (debug info, timings)
415
+ - `-vv, --trace`: Trace output (deep internals, stack traces)
416
+ - `--timestamps`: Add timestamps to output
417
+ - `--color/--no-color`: Force/disable color output
418
+
419
+ Examples:
420
+ ```bash
421
+ # Use config defaults
422
+ prisma-next db sign
423
+
424
+ # Specify database URL
425
+ prisma-next db sign --db postgresql://user:pass@localhost/db
426
+
427
+ # JSON output
428
+ prisma-next db sign --json
429
+
430
+ # Verbose output with timestamps
431
+ prisma-next db sign -v --timestamps
432
+ ```
433
+
434
+ **Config File Requirements:**
435
+
436
+ 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:
437
+
438
+ ```typescript
439
+ import { defineConfig } from '@prisma-next/cli/config-types';
440
+ import postgresAdapter from '@prisma-next/adapter-postgres/control';
441
+ import postgresDriver from '@prisma-next/driver-postgres/control';
442
+ import postgres from '@prisma-next/targets-postgres/control';
443
+ import sql from '@prisma-next/family-sql/control';
444
+ import { contract } from './prisma/contract';
445
+
446
+ export default defineConfig({
447
+ family: sql,
448
+ target: postgres,
449
+ adapter: postgresAdapter,
450
+ driver: postgresDriver,
451
+ extensions: [],
452
+ contract: {
453
+ source: contract,
454
+ output: 'src/prisma/contract.json',
455
+ types: 'src/prisma/contract.d.ts',
456
+ },
457
+ db: {
458
+ url: process.env.DATABASE_URL, // Optional: can also use --db flag
459
+ },
460
+ });
461
+ ```
462
+
463
+ **Signing Process:**
464
+
465
+ 1. **Load Contract**: Reads the emitted `contract.json` from `config.contract.output`
466
+ 2. **Connect to Database**: Uses `config.driver.create(url)` to create a driver
467
+ 3. **Create Family Instance**: Calls `config.family.create()` with target, adapter, driver, and extensions to create a family instance
468
+ 4. **Schema Verification (Precondition)**: Calls `familyInstance.schemaVerify()` to verify the database schema matches the contract:
469
+ - If verification fails: Prints schema verification output and exits with code 1 (marker is not written)
470
+ - If verification passes: Proceeds to marker signing
471
+ 5. **Sign**: Calls `familyInstance.sign()` which:
472
+ - Ensures the marker schema and table exist
473
+ - Reads any existing marker from the database
474
+ - Compares contract hashes with existing marker:
475
+ - If marker is missing: Inserts a new marker row
476
+ - If hashes differ: Updates the existing marker row
477
+ - If hashes match: No-op (idempotent)
478
+
479
+ **Output Format (TTY):**
480
+
481
+ Success (new marker):
482
+ ```
483
+ ✔ Database signed (marker created)
484
+ coreHash: sha256:abc123...
485
+ profileHash: sha256:def456...
486
+ Total time: 42ms
487
+ ```
488
+
489
+ Success (updated marker):
490
+ ```
491
+ ✔ Database signed (marker updated from sha256:old-hash)
492
+ coreHash: sha256:abc123...
493
+ profileHash: sha256:def456...
494
+ previous coreHash: sha256:old-hash
495
+ Total time: 42ms
496
+ ```
497
+
498
+ Success (already up-to-date):
499
+ ```
500
+ ✔ Database already signed with this contract
501
+ coreHash: sha256:abc123...
502
+ profileHash: sha256:def456...
503
+ Total time: 42ms
504
+ ```
505
+
506
+ Failure (schema mismatch):
507
+ ```
508
+ ✖ Schema verification failed
509
+ [Schema verification tree output]
510
+ ```
511
+
512
+ **Output Format (JSON):**
513
+
514
+ ```json
515
+ {
516
+ "ok": true,
517
+ "summary": "Database signed (marker created)",
518
+ "contract": {
519
+ "coreHash": "sha256:abc123...",
520
+ "profileHash": "sha256:def456..."
521
+ },
522
+ "target": {
523
+ "expected": "postgres",
524
+ "actual": "postgres"
525
+ },
526
+ "marker": {
527
+ "created": true,
528
+ "updated": false
529
+ },
530
+ "meta": {
531
+ "configPath": "/path/to/prisma-next.config.ts",
532
+ "contractPath": "/path/to/src/prisma/contract.json"
533
+ },
534
+ "timings": {
535
+ "total": 42
536
+ }
537
+ }
538
+ ```
539
+
540
+ For updated markers:
541
+ ```json
542
+ {
543
+ "ok": true,
544
+ "summary": "Database signed (marker updated from sha256:old-hash)",
545
+ "contract": {
546
+ "coreHash": "sha256:abc123...",
547
+ "profileHash": "sha256:def456..."
548
+ },
549
+ "target": {
550
+ "expected": "postgres",
551
+ "actual": "postgres"
552
+ },
553
+ "marker": {
554
+ "created": false,
555
+ "updated": true,
556
+ "previous": {
557
+ "coreHash": "sha256:old-hash",
558
+ "profileHash": "sha256:old-profile-hash"
559
+ }
560
+ },
561
+ "meta": {
562
+ "configPath": "/path/to/prisma-next.config.ts",
563
+ "contractPath": "/path/to/src/prisma/contract.json"
564
+ },
565
+ "timings": {
566
+ "total": 42
567
+ }
568
+ }
569
+ ```
570
+
571
+ **Error Codes:**
572
+ - `PN-CLI-4010`: Missing driver in config — provide a driver descriptor
573
+ - `PN-CLI-4011`: Missing database URL — provide `--db` flag or `config.db.url` or `DATABASE_URL` environment variable
574
+ - Exit code 1: Schema verification failed — database schema does not match contract (marker is not written)
575
+
576
+ **Relationship to Other Commands:**
577
+ - **`db schema-verify`**: `db sign` calls `schemaVerify` as a precondition before writing the marker. If schema verification fails, `db sign` exits without writing the marker.
578
+ - **`db verify`**: `db verify` checks that the marker exists and matches the contract. `db sign` writes the marker that `db verify` checks.
579
+
580
+ **Idempotency:**
581
+ The `db sign` command is idempotent and safe to run multiple times:
582
+ - If the marker already matches the contract (same hashes), no database changes are made
583
+ - The command reports success in all cases (new marker, updated marker, or already up-to-date)
584
+ - Safe to run in CI/deployment pipelines
585
+
586
+ **Family Requirements:**
587
+ The family must provide a `create()` method in the family descriptor that returns a `FamilyInstance` with `schemaVerify()` and `sign()` methods:
588
+
589
+ ```typescript
590
+ interface FamilyInstance {
591
+ schemaVerify(options: {
592
+ driver: ControlDriverInstance;
593
+ contractIR: ContractIR;
594
+ strict: boolean;
595
+ contractPath: string;
596
+ configPath?: string;
597
+ }): Promise<VerifyDatabaseSchemaResult>;
598
+
599
+ sign(options: {
600
+ driver: ControlDriverInstance;
601
+ contractIR: ContractIR;
602
+ contractPath: string;
603
+ configPath?: string;
604
+ }): Promise<SignDatabaseResult>;
605
+ }
606
+ ```
607
+
608
+ 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.
609
+
610
+ **Config File (`prisma-next.config.ts`):**
611
+
612
+ The CLI uses a config file to specify the target family, target, adapter, extensions, and contract.
613
+
614
+ **Config Discovery:**
615
+ - `--config <path>`: Explicit path (relative or absolute)
616
+ - Default: `./prisma-next.config.ts` in current working directory
617
+ - No upward search (stays in CWD)
618
+
619
+ **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.
620
+
621
+ ```typescript
622
+ import { defineConfig } from '@prisma-next/cli/config-types';
623
+ import postgresAdapter from '@prisma-next/adapter-postgres/control';
624
+ import postgres from '@prisma-next/targets-postgres/control';
625
+ import sql from '@prisma-next/family-sql/control';
626
+ import { contract } from './prisma/contract';
627
+
628
+ export default defineConfig({
629
+ family: sql,
630
+ target: postgres,
631
+ adapter: postgresAdapter,
632
+ extensions: [],
633
+ contract: {
634
+ source: contract, // Can be a value or a function: () => import('./contract').then(m => m.contract)
635
+ output: 'src/prisma/contract.json', // Optional: defaults to 'src/prisma/contract.json'
636
+ types: 'src/prisma/contract.d.ts', // Optional: defaults to output with .d.ts extension
637
+ },
638
+ });
639
+ ```
640
+
641
+ The `contract.source` field can be:
642
+ - A direct value: `source: contract`
643
+ - A synchronous function: `source: () => contract`
644
+ - An asynchronous function: `source: () => import('./contract').then(m => m.contract)`
645
+
646
+ 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.
647
+
648
+ The `contract.types` field specifies the path to `contract.d.ts`. Defaults to `output` with `.d.ts` extension (replaces `.json` with `.d.ts` if output ends with `.json`, otherwise appends `contract.d.ts` to the directory containing output).
649
+
650
+ **Output:**
651
+ - `contract.json`: Includes `_generated` metadata field indicating it's a generated artifact (excluded from canonicalization/hashing)
652
+ - `contract.d.ts`: Includes warning header comments indicating it's a generated file
653
+
654
+ ## Architecture
655
+
656
+ ```mermaid
657
+ flowchart TD
658
+ CLI[CLI Entry Point]
659
+ CMD[Emit Command]
660
+ LOAD[TS Contract Loader]
661
+ EMIT[Emitter]
662
+ FS[File System]
663
+
664
+ CLI --> CMD
665
+ CMD --> LOAD
666
+ LOAD --> EMIT
667
+ EMIT --> CMD
668
+ CMD --> FS
669
+ ```
670
+
671
+ ## Config Validation and Normalization
672
+
673
+ The `defineConfig()` function validates and normalizes configs using Arktype:
674
+
675
+ - **Validation**: Validates config structure using Arktype schemas
676
+ - **Normalization**: Applies default values (e.g., `contract.output` defaults to `'src/prisma/contract.json'`)
677
+ - **Error Messages**: Provides clear, actionable error messages on validation failure
678
+
679
+ See `.cursor/rules/config-validation-and-normalization.mdc` for detailed patterns.
680
+
681
+ ## Components
682
+
683
+ ### CLI Entry Point (`cli.ts`)
684
+ - Main entry point using commander
685
+ - Parses arguments and routes to command handlers
686
+ - Handles global flags (`--help`, `--version`)
687
+ - Exit codes: 0 (success), 1 (runtime error), 2 (usage/config error)
688
+ - **Error Handling**: Uses `exitOverride()` to catch unhandled errors (non-structured errors that fail fast) and print stack traces. Commands handle structured errors themselves via `process.exit()`.
689
+ - **Command Taxonomy**: Groups commands by domain/plane (e.g., `contract emit`)
690
+ - **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 <command> ➜ <description>" with options and docs URLs. See `utils/output.ts` for help formatters.
691
+ - **Command Descriptions**: Commands use `setCommandDescriptions()` to set separate short and long descriptions. See `utils/command-helpers.ts` and `.cursor/rules/cli-command-descriptions.mdc`.
692
+
693
+ ### Contract Emit Command (`commands/contract-emit.ts`)
694
+ - Canonical command implementation using commander
695
+ - Supports global flags (JSON, verbosity, color, timestamps)
696
+ - **Error Handling**: Uses structured errors (`CliStructuredError`), Result pattern (`performAction`), and `process.exit()`. Commands wrap logic in `performAction()`, process results with `handleResult()`, and call `process.exit(exitCode)` directly. See `.cursor/rules/cli-error-handling.mdc` for details.
697
+ - Loads the user's config module (`prisma-next.config.ts`)
698
+ - Resolves contract from config:
699
+ - Uses `config.contract.source` (supports sync and async functions)
700
+ - User's config is responsible for loading the contract (can use `loadContractFromTs` or any other method)
701
+ - Throws error if `config.contract` is missing
702
+ - Uses artifact paths from `config.contract.output/types` (already normalized by `defineConfig()` with defaults applied)
703
+ - Creates family instance via `config.family.create()` (assembles operation registry, type imports, extension IDs)
704
+ - Calls `familyInstance.emitContract()` with raw contract (instance handles stripping mappings and validation internally)
705
+ - Outputs human-readable or JSON format based on flags
706
+
707
+ ### Programmatic API (`api/emit-contract.ts`)
708
+ - **`emitContract(options)`**: Programmatic API for emitting contracts
709
+ - Accepts resolved contract IR, output paths, and assembly data
710
+ - Caller is responsible for loading the contract and resolving paths
711
+ - Returns result with hashes, file paths, and timings
712
+ - Used by CLI command internally
713
+
714
+ ### Error Handling (`utils/errors.ts`, `utils/cli-errors.ts`, `utils/result.ts`, `utils/result-handler.ts`)
715
+ - **Structured Errors**: Call sites throw `CliStructuredError` instances with full context (why, fix, docsUrl, etc.)
716
+ - **Result Pattern**: Commands wrap logic in `performAction()` which only catches `CliStructuredError` instances
717
+ - **Error Conversion**: `CliStructuredError.toEnvelope()` converts errors to envelopes for output formatting
718
+ - **Result Processing**: `handleResult()` processes Results, formats output, and returns exit codes
719
+ - **Exit Codes**:
720
+ - Usage/config errors (PN-CLI-4001-4007) → exit code 2
721
+ - Runtime errors (PN-RTM-3xxx) → exit code 1
722
+ - Success → exit code 0
723
+ - **Fail Fast**: Non-structured errors propagate and are caught by Commander.js's `exitOverride()` with stack traces
724
+ - See `.cursor/rules/cli-error-handling.mdc` for detailed patterns
725
+
726
+ ### Pack Assembly
727
+ - **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.
728
+ - **Removed**: `pack-assembly.ts` has been removed. Pack assembly is now handled by family instances. For SQL family, tests can import pack-based helpers directly from `packages/sql/family/src/core/assembly.ts` using relative paths.
729
+ - Assembly logic is family-specific and owned by each family's instance implementation (e.g., `createSqlFamilyInstance` in `@prisma-next/family-sql`).
730
+
731
+ ### Output Formatting (`utils/output.ts`)
732
+ - **Command Output Formatters**: Format human-readable output for commands (emit, verify, etc.)
733
+ - Paths are shown as relative paths from current working directory (using `relative(process.cwd(), path)`)
734
+ - Success indicators use consistent checkmark (✔) throughout
735
+ - **Error Output Formatters**: Format error output for human-readable and JSON display
736
+ - **Styled Headers**: `formatStyledHeader()` creates styled headers for command output with "prisma-next <command> ➜ <description>" format
737
+ - Parameter labels include colons (e.g., `config:`, `contract:`)
738
+ - Uses fixed 20-character left column width for consistent alignment
739
+ - **Help Formatters**:
740
+ - `formatRootHelp()` - Formats root help with "prisma-next" title, command tree, and multiline description
741
+ - `formatCommandHelp()` - Formats command help with "prisma-next <command> ➜ <description>", options, subcommands, docs URLs, and multiline description
742
+ - `renderCommandTree()` - Shared function to render hierarchical command trees with tree characters (├─, └─, │)
743
+ - **Fixed-Width Formatting**: All two-column output (help, styled headers) uses fixed 20-character left column width
744
+ - **Text Wrapping**: Right column wraps at 90 characters using `wrap-ansi` for ANSI-aware wrapping
745
+ - **Default Values**: Options with default values display `default: <value>` on the following line (dimmed)
746
+ - **ANSI-Aware Padding**: Uses `string-width` and `strip-ansi` to measure and pad text correctly with ANSI codes
747
+ - Help formatters use the same styling system as normal command output (colors, dim text, badges)
748
+ - Short descriptions appear in command trees and headers; long descriptions appear at the bottom of help output
749
+ - Help formatting is configured via `configureHelp()` in `cli.ts` to apply to all commands
750
+
751
+ ### Family Descriptor (provided by family /cli entrypoint)
752
+ - The SQL family (and other families) provide:
753
+ - `create(options)` - Creates a family instance that implements domain actions
754
+ - `hook` - Target family hook for contract emission
755
+ - Family instances provide:
756
+ - `validateContractIR(contractJson)` - Validates and normalizes contract, returns ContractIR without mappings
757
+ - `emitContract(options)` - Emits contract (handles stripping mappings and validation internally)
758
+ - `verify(options)` - Verifies database marker against contract
759
+ - `schemaVerify(options)` - Verifies database schema against contract
760
+ - `introspect(options)` - Introspects database schema
761
+
762
+ ### Pack Manifest Types (IR)
763
+ - Families define their manifest IR and related types under their own tooling packages. CLI treats manifests as opaque data.
764
+
765
+ ## Dependencies
766
+
767
+ - **`commander`**: CLI argument parsing and command routing
768
+ - **`esbuild`**: Bundling TypeScript contract files with import allowlisting
769
+ - **`@prisma-next/emitter`**: Contract emission engine (returns strings)
770
+
771
+ ## Design Decisions
772
+
773
+ 1. **Import Allowlist**: Only `@prisma-next/*` packages allowed (MVP). Expand later if needed.
774
+ 2. **Utility Separation**: TS contract loading is a utility function, not a command. Commands use utilities.
775
+ 3. **CLI Framework**: Use `commander` library for robust CLI argument parsing.
776
+ 4. **File I/O**: CLI handles all I/O; emitter returns strings (no file operations in emitter).
777
+ 5. **Generated File Metadata**: Adds `_generated` metadata field to `contract.json` to indicate it's a generated artifact. This field is excluded from canonicalization/hashing to ensure determinism. The `contract.d.ts` file includes warning header comments generated by the emitter hook.
778
+
779
+ ## Testing
780
+
781
+ The CLI package includes unit tests, integration tests, and e2e tests:
782
+
783
+ - **Unit tests**: Test individual functions and utilities in isolation
784
+ - **Integration tests**: Test component interactions (e.g., config loading, pack assembly)
785
+ - **E2E tests**: Test complete command execution with real config files
786
+
787
+ ### E2E Test Patterns
788
+
789
+ E2E tests use a shared fixture app pattern to ensure proper module resolution:
790
+
791
+ - **Shared fixture app**: `test/cli-e2e-test-app/` contains a static `package.json` with dependencies
792
+ - **Fixture organization**: Fixtures are organized by command in subdirectories (e.g., `fixtures/emit/`, `fixtures/db-verify/`)
793
+ - **Ephemeral test directories**: Each test creates an isolated directory with files copied from fixtures
794
+ - **No package.json in test directories**: Test directories inherit workspace dependencies from the parent `package.json` at the root
795
+ - **Helper function**: `setupTestDirectoryFromFixtures()` handles directory setup and returns a cleanup function
796
+ - **Cleanup responsibility**: Each test must clean up its own directory (use `afterEach` hooks or `finally` blocks)
797
+
798
+ **Example:**
799
+ ```typescript
800
+ import { setupTestDirectoryFromFixtures } from './utils/test-helpers';
801
+
802
+ const fixtureSubdir = 'emit';
803
+
804
+ it('test description', async () => {
805
+ const testSetup = setupTestDirectoryFromFixtures(
806
+ fixtureSubdir,
807
+ 'prisma-next.config.emit.ts',
808
+ );
809
+ const cleanupDir = testSetup.cleanup;
810
+
811
+ try {
812
+ // ... test code ...
813
+ } finally {
814
+ cleanupDir(); // Each test cleans up its own directory
815
+ }
816
+ });
817
+ ```
818
+
819
+ See `.cursor/rules/cli-e2e-test-patterns.mdc` for detailed patterns and examples.
820
+
821
+ Run tests:
822
+ ```bash
823
+ pnpm test # Run all tests
824
+ pnpm test:unit # Run unit tests only
825
+ pnpm test:integration # Run integration tests only
826
+ pnpm test:e2e # Run e2e tests only
827
+ ```
828
+
829
+ ## Package Location
830
+
831
+ This package is part of the **framework domain**, **tooling layer**, **migration plane**:
832
+ - **Domain**: framework (target-agnostic)
833
+ - **Layer**: tooling
834
+ - **Plane**: migration
835
+ - **Path**: `packages/framework/tooling/cli`
836
+
837
+ ## See Also
838
+
839
+ - [`@prisma-next/emitter`](../emitter/README.md) - Contract emission engine
840
+ - Project Brief — CLI Support for Extension Packs: `docs/briefs/complete/20-CLI-Support-for-Extension-Packs.md`