@prisma-next/cli 0.3.0-dev.16 → 0.3.0-dev.162
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/LICENSE +201 -0
- package/README.md +381 -128
- package/dist/cli-errors-BDCYR5ap.mjs +4 -0
- package/dist/cli-errors-Dzs7Oxz7.d.mts +3 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.js +1 -2671
- package/dist/cli.mjs +245 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/client-yYtotiSX.mjs +1063 -0
- package/dist/client-yYtotiSX.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts +7 -0
- package/dist/commands/contract-emit.d.mts.map +1 -0
- package/dist/commands/contract-emit.mjs +8 -0
- package/dist/commands/contract-infer.d.mts +7 -0
- package/dist/commands/contract-infer.d.mts.map +1 -0
- package/dist/commands/contract-infer.mjs +9 -0
- package/dist/commands/db-init.d.mts +7 -0
- package/dist/commands/db-init.d.mts.map +1 -0
- package/dist/commands/db-init.mjs +125 -0
- package/dist/commands/db-init.mjs.map +1 -0
- package/dist/commands/db-schema.d.mts +7 -0
- package/dist/commands/db-schema.d.mts.map +1 -0
- package/dist/commands/db-schema.mjs +55 -0
- package/dist/commands/db-schema.mjs.map +1 -0
- package/dist/commands/db-sign.d.mts +7 -0
- package/dist/commands/db-sign.d.mts.map +1 -0
- package/dist/commands/db-sign.mjs +136 -0
- package/dist/commands/db-sign.mjs.map +1 -0
- package/dist/commands/db-update.d.mts +7 -0
- package/dist/commands/db-update.d.mts.map +1 -0
- package/dist/commands/db-update.mjs +122 -0
- package/dist/commands/db-update.mjs.map +1 -0
- package/dist/commands/db-verify.d.mts +7 -0
- package/dist/commands/db-verify.d.mts.map +1 -0
- package/dist/commands/db-verify.mjs +322 -0
- package/dist/commands/db-verify.mjs.map +1 -0
- package/dist/commands/migration-apply.d.mts +36 -0
- package/dist/commands/migration-apply.d.mts.map +1 -0
- package/dist/commands/migration-apply.mjs +244 -0
- package/dist/commands/migration-apply.mjs.map +1 -0
- package/dist/commands/migration-new.d.mts +8 -0
- package/dist/commands/migration-new.d.mts.map +1 -0
- package/dist/commands/migration-new.mjs +151 -0
- package/dist/commands/migration-new.mjs.map +1 -0
- package/dist/commands/migration-plan.d.mts +47 -0
- package/dist/commands/migration-plan.d.mts.map +1 -0
- package/dist/commands/migration-plan.mjs +312 -0
- package/dist/commands/migration-plan.mjs.map +1 -0
- package/dist/commands/migration-ref.d.mts +43 -0
- package/dist/commands/migration-ref.d.mts.map +1 -0
- package/dist/commands/migration-ref.mjs +194 -0
- package/dist/commands/migration-ref.mjs.map +1 -0
- package/dist/commands/migration-show.d.mts +28 -0
- package/dist/commands/migration-show.d.mts.map +1 -0
- package/dist/commands/migration-show.mjs +139 -0
- package/dist/commands/migration-show.mjs.map +1 -0
- package/dist/commands/migration-status.d.mts +86 -0
- package/dist/commands/migration-status.d.mts.map +1 -0
- package/dist/commands/migration-status.mjs +8 -0
- package/dist/commands/migration-verify.d.mts +16 -0
- package/dist/commands/migration-verify.d.mts.map +1 -0
- package/dist/commands/migration-verify.mjs +109 -0
- package/dist/commands/migration-verify.mjs.map +1 -0
- package/dist/config-loader-C4VXKl8f.mjs +43 -0
- package/dist/config-loader-C4VXKl8f.mjs.map +1 -0
- package/dist/{config-loader.d.ts → config-loader.d.mts} +8 -3
- package/dist/config-loader.d.mts.map +1 -0
- package/dist/config-loader.mjs +3 -0
- package/dist/contract-emit-Bk_eEDKu.mjs +187 -0
- package/dist/contract-emit-Bk_eEDKu.mjs.map +1 -0
- package/dist/contract-infer-suMDmFSG.mjs +89 -0
- package/dist/contract-infer-suMDmFSG.mjs.map +1 -0
- package/dist/exports/config-types.d.mts +2 -0
- package/dist/exports/config-types.mjs +3 -0
- package/dist/exports/control-api.d.mts +624 -0
- package/dist/exports/control-api.d.mts.map +1 -0
- package/dist/exports/control-api.mjs +109 -0
- package/dist/exports/control-api.mjs.map +1 -0
- package/dist/{load-ts-contract.d.ts → exports/index.d.mts} +12 -7
- package/dist/exports/index.d.mts.map +1 -0
- package/dist/exports/index.mjs +141 -0
- package/dist/exports/index.mjs.map +1 -0
- package/dist/extract-operation-statements-BVlb3jxp.mjs +13 -0
- package/dist/extract-operation-statements-BVlb3jxp.mjs.map +1 -0
- package/dist/extract-sql-ddl-6EVSOThm.mjs +26 -0
- package/dist/extract-sql-ddl-6EVSOThm.mjs.map +1 -0
- package/dist/framework-components-BAsliT4V.mjs +59 -0
- package/dist/framework-components-BAsliT4V.mjs.map +1 -0
- package/dist/inspect-live-schema-HMutsJYh.mjs +91 -0
- package/dist/inspect-live-schema-HMutsJYh.mjs.map +1 -0
- package/dist/migration-command-scaffold-Dg7CKKCg.mjs +105 -0
- package/dist/migration-command-scaffold-Dg7CKKCg.mjs.map +1 -0
- package/dist/migration-status-BqfVmC0w.mjs +1582 -0
- package/dist/migration-status-BqfVmC0w.mjs.map +1 -0
- package/dist/migrations-Bv8oeiY_.mjs +173 -0
- package/dist/migrations-Bv8oeiY_.mjs.map +1 -0
- package/dist/progress-adapter-D4x8SbJa.mjs +43 -0
- package/dist/progress-adapter-D4x8SbJa.mjs.map +1 -0
- package/dist/terminal-ui-N5tR-ob5.mjs +967 -0
- package/dist/terminal-ui-N5tR-ob5.mjs.map +1 -0
- package/dist/verify-WARh5TjK.mjs +385 -0
- package/dist/verify-WARh5TjK.mjs.map +1 -0
- package/package.json +88 -42
- package/src/cli.ts +113 -58
- package/src/commands/contract-emit.ts +237 -144
- package/src/commands/contract-infer-paths.ts +32 -0
- package/src/commands/contract-infer.ts +143 -0
- package/src/commands/db-init.ts +97 -219
- package/src/commands/db-schema.ts +77 -0
- package/src/commands/db-sign.ts +208 -229
- package/src/commands/db-update.ts +236 -0
- package/src/commands/db-verify.ts +504 -184
- package/src/commands/inspect-live-schema.ts +170 -0
- package/src/commands/migration-apply.ts +427 -0
- package/src/commands/migration-new.ts +260 -0
- package/src/commands/migration-plan.ts +519 -0
- package/src/commands/migration-ref.ts +305 -0
- package/src/commands/migration-show.ts +246 -0
- package/src/commands/migration-status.ts +864 -0
- package/src/commands/migration-verify.ts +180 -0
- package/src/config-loader.ts +13 -3
- package/src/control-api/client.ts +424 -72
- package/src/control-api/contract-enrichment.ts +119 -0
- package/src/control-api/errors.ts +9 -0
- package/src/control-api/operations/contract-emit.ts +174 -0
- package/src/control-api/operations/db-init.ts +53 -49
- package/src/control-api/operations/db-update.ts +220 -0
- package/src/control-api/operations/extract-operation-statements.ts +14 -0
- package/src/control-api/operations/extract-sql-ddl.ts +47 -0
- package/src/control-api/operations/migration-apply.ts +191 -0
- package/src/control-api/operations/migration-helpers.ts +49 -0
- package/src/control-api/types.ts +388 -18
- package/src/exports/config-types.ts +4 -3
- package/src/exports/control-api.ts +21 -2
- package/src/load-ts-contract.ts +30 -19
- package/src/utils/cli-errors.ts +14 -8
- package/src/utils/command-helpers.ts +302 -3
- package/src/utils/formatters/emit.ts +67 -0
- package/src/utils/formatters/errors.ts +82 -0
- package/src/utils/formatters/graph-migration-mapper.ts +240 -0
- package/src/utils/formatters/graph-render.ts +1323 -0
- package/src/utils/formatters/graph-types.ts +120 -0
- package/src/utils/formatters/help.ts +380 -0
- package/src/utils/formatters/helpers.ts +28 -0
- package/src/utils/formatters/migrations.ts +346 -0
- package/src/utils/formatters/styled.ts +212 -0
- package/src/utils/formatters/verify.ts +621 -0
- package/src/utils/framework-components.ts +13 -10
- package/src/utils/global-flags.ts +41 -23
- package/src/utils/migration-command-scaffold.ts +184 -0
- package/src/utils/migration-types.ts +12 -0
- package/src/utils/progress-adapter.ts +18 -29
- package/src/utils/result-handler.ts +12 -13
- package/src/utils/shutdown.ts +92 -0
- package/src/utils/suggest-command.ts +31 -0
- package/src/utils/terminal-ui.ts +276 -0
- package/dist/chunk-5MPKZYVI.js +0 -47
- package/dist/chunk-5MPKZYVI.js.map +0 -1
- package/dist/chunk-6EPKRATC.js +0 -91
- package/dist/chunk-6EPKRATC.js.map +0 -1
- package/dist/chunk-74IELXRA.js +0 -371
- package/dist/chunk-74IELXRA.js.map +0 -1
- package/dist/chunk-HWYQOCAJ.js +0 -47
- package/dist/chunk-HWYQOCAJ.js.map +0 -1
- package/dist/chunk-U6QI3AZ3.js +0 -133
- package/dist/chunk-U6QI3AZ3.js.map +0 -1
- package/dist/chunk-VI2YETW7.js +0 -38
- package/dist/chunk-VI2YETW7.js.map +0 -1
- package/dist/chunk-ZG5T6OB5.js +0 -923
- package/dist/chunk-ZG5T6OB5.js.map +0 -1
- package/dist/cli.d.ts +0 -2
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/commands/contract-emit.d.ts +0 -3
- package/dist/commands/contract-emit.d.ts.map +0 -1
- package/dist/commands/contract-emit.js +0 -11
- package/dist/commands/contract-emit.js.map +0 -1
- package/dist/commands/db-init.d.ts +0 -3
- package/dist/commands/db-init.d.ts.map +0 -1
- package/dist/commands/db-init.js +0 -302
- package/dist/commands/db-init.js.map +0 -1
- package/dist/commands/db-introspect.d.ts +0 -3
- package/dist/commands/db-introspect.d.ts.map +0 -1
- package/dist/commands/db-introspect.js +0 -185
- package/dist/commands/db-introspect.js.map +0 -1
- package/dist/commands/db-schema-verify.d.ts +0 -3
- package/dist/commands/db-schema-verify.d.ts.map +0 -1
- package/dist/commands/db-schema-verify.js +0 -163
- package/dist/commands/db-schema-verify.js.map +0 -1
- package/dist/commands/db-sign.d.ts +0 -3
- package/dist/commands/db-sign.d.ts.map +0 -1
- package/dist/commands/db-sign.js +0 -198
- package/dist/commands/db-sign.js.map +0 -1
- package/dist/commands/db-verify.d.ts +0 -3
- package/dist/commands/db-verify.d.ts.map +0 -1
- package/dist/commands/db-verify.js +0 -172
- package/dist/commands/db-verify.js.map +0 -1
- package/dist/config-loader.d.ts.map +0 -1
- package/dist/config-loader.js +0 -7
- package/dist/config-loader.js.map +0 -1
- package/dist/control-api/client.d.ts +0 -13
- package/dist/control-api/client.d.ts.map +0 -1
- package/dist/control-api/operations/db-init.d.ts +0 -29
- package/dist/control-api/operations/db-init.d.ts.map +0 -1
- package/dist/control-api/types.d.ts +0 -256
- package/dist/control-api/types.d.ts.map +0 -1
- package/dist/exports/config-types.d.ts +0 -3
- package/dist/exports/config-types.d.ts.map +0 -1
- package/dist/exports/config-types.js +0 -6
- package/dist/exports/config-types.js.map +0 -1
- package/dist/exports/control-api.d.ts +0 -13
- package/dist/exports/control-api.d.ts.map +0 -1
- package/dist/exports/control-api.js +0 -9
- package/dist/exports/control-api.js.map +0 -1
- package/dist/exports/index.d.ts +0 -4
- package/dist/exports/index.d.ts.map +0 -1
- package/dist/exports/index.js +0 -177
- package/dist/exports/index.js.map +0 -1
- package/dist/load-ts-contract.d.ts.map +0 -1
- package/dist/utils/action.d.ts +0 -16
- package/dist/utils/action.d.ts.map +0 -1
- package/dist/utils/cli-errors.d.ts +0 -7
- package/dist/utils/cli-errors.d.ts.map +0 -1
- package/dist/utils/command-helpers.d.ts +0 -12
- package/dist/utils/command-helpers.d.ts.map +0 -1
- package/dist/utils/framework-components.d.ts +0 -70
- package/dist/utils/framework-components.d.ts.map +0 -1
- package/dist/utils/global-flags.d.ts +0 -25
- package/dist/utils/global-flags.d.ts.map +0 -1
- package/dist/utils/output.d.ts +0 -142
- package/dist/utils/output.d.ts.map +0 -1
- package/dist/utils/progress-adapter.d.ts +0 -26
- package/dist/utils/progress-adapter.d.ts.map +0 -1
- package/dist/utils/result-handler.d.ts +0 -15
- package/dist/utils/result-handler.d.ts.map +0 -1
- package/dist/utils/spinner.d.ts +0 -29
- package/dist/utils/spinner.d.ts.map +0 -1
- package/src/commands/db-introspect.ts +0 -254
- package/src/commands/db-schema-verify.ts +0 -231
- package/src/utils/action.ts +0 -43
- package/src/utils/output.ts +0 -1471
- package/src/utils/spinner.ts +0 -67
package/src/load-ts-contract.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { existsSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { tmpdir } from 'node:os';
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import type { Contract } from '@prisma-next/contract/types';
|
|
5
5
|
import type { Plugin } from 'esbuild';
|
|
6
6
|
import { build } from 'esbuild';
|
|
7
|
+
import { join } from 'pathe';
|
|
7
8
|
|
|
8
9
|
export interface LoadTsContractOptions {
|
|
9
10
|
readonly allowlist?: ReadonlyArray<string>;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
const DEFAULT_ALLOWLIST = ['@prisma-next/*'];
|
|
13
|
+
const DEFAULT_ALLOWLIST = ['@prisma-next/*', 'node:crypto'];
|
|
13
14
|
|
|
14
15
|
function isAllowedImport(importPath: string, allowlist: ReadonlyArray<string>): boolean {
|
|
15
16
|
for (const pattern of allowlist) {
|
|
@@ -18,6 +19,11 @@ function isAllowedImport(importPath: string, allowlist: ReadonlyArray<string>):
|
|
|
18
19
|
if (importPath === prefix || importPath.startsWith(`${prefix}/`)) {
|
|
19
20
|
return true;
|
|
20
21
|
}
|
|
22
|
+
} else if (pattern.endsWith('*')) {
|
|
23
|
+
const prefix = pattern.slice(0, -1);
|
|
24
|
+
if (importPath.startsWith(prefix)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
21
27
|
} else if (importPath === pattern) {
|
|
22
28
|
return true;
|
|
23
29
|
}
|
|
@@ -30,26 +36,31 @@ function validatePurity(value: unknown): void {
|
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
32
38
|
|
|
33
|
-
const
|
|
39
|
+
const path = new WeakSet();
|
|
40
|
+
|
|
34
41
|
function check(value: unknown): void {
|
|
35
42
|
if (value === null || typeof value !== 'object') {
|
|
36
43
|
return;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
|
-
if (
|
|
46
|
+
if (path.has(value)) {
|
|
40
47
|
throw new Error('Contract export contains circular references');
|
|
41
48
|
}
|
|
42
|
-
|
|
49
|
+
path.add(value);
|
|
43
50
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
try {
|
|
52
|
+
for (const key in value) {
|
|
53
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
54
|
+
if (descriptor && (descriptor.get || descriptor.set)) {
|
|
55
|
+
throw new Error(`Contract export contains getter/setter at key "${key}"`);
|
|
56
|
+
}
|
|
57
|
+
if (descriptor && typeof descriptor.value === 'function') {
|
|
58
|
+
throw new Error(`Contract export contains function at key "${key}"`);
|
|
59
|
+
}
|
|
60
|
+
check((value as Record<string, unknown>)[key]);
|
|
51
61
|
}
|
|
52
|
-
|
|
62
|
+
} finally {
|
|
63
|
+
path.delete(value);
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
|
|
@@ -92,7 +103,7 @@ function createImportAllowlistPlugin(allowlist: ReadonlyArray<string>, entryPath
|
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
/**
|
|
95
|
-
* Loads a contract from a TypeScript file and returns it as
|
|
106
|
+
* Loads a contract from a TypeScript file and returns it as Contract.
|
|
96
107
|
*
|
|
97
108
|
* **Responsibility: Parsing Only**
|
|
98
109
|
* This function loads and parses a TypeScript contract file. It does NOT normalize the contract.
|
|
@@ -103,13 +114,13 @@ function createImportAllowlistPlugin(allowlist: ReadonlyArray<string>, entryPath
|
|
|
103
114
|
*
|
|
104
115
|
* @param entryPath - Path to the TypeScript contract file
|
|
105
116
|
* @param options - Optional configuration (import allowlist)
|
|
106
|
-
* @returns The contract as
|
|
117
|
+
* @returns The contract as Contract (should already be normalized)
|
|
107
118
|
* @throws Error if the contract cannot be loaded or is not JSON-serializable
|
|
108
119
|
*/
|
|
109
120
|
export async function loadContractFromTs(
|
|
110
121
|
entryPath: string,
|
|
111
122
|
options?: LoadTsContractOptions,
|
|
112
|
-
): Promise<
|
|
123
|
+
): Promise<Contract> {
|
|
113
124
|
const allowlist = options?.allowlist ?? DEFAULT_ALLOWLIST;
|
|
114
125
|
|
|
115
126
|
if (!existsSync(entryPath)) {
|
|
@@ -175,7 +186,7 @@ export async function loadContractFromTs(
|
|
|
175
186
|
}
|
|
176
187
|
writeFileSync(tempFile, bundleContent, 'utf-8');
|
|
177
188
|
|
|
178
|
-
const module = (await import(
|
|
189
|
+
const module = (await import(/* @vite-ignore */ pathToFileURL(tempFile).href)) as {
|
|
179
190
|
default?: unknown;
|
|
180
191
|
contract?: unknown;
|
|
181
192
|
};
|
|
@@ -199,7 +210,7 @@ export async function loadContractFromTs(
|
|
|
199
210
|
|
|
200
211
|
validatePurity(contract);
|
|
201
212
|
|
|
202
|
-
return contract as
|
|
213
|
+
return contract as Contract;
|
|
203
214
|
} catch (error) {
|
|
204
215
|
try {
|
|
205
216
|
if (tempFile) {
|
package/src/utils/cli-errors.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Re-export all domain error factories from
|
|
2
|
+
* Re-export all domain error factories from @prisma-next/errors for convenience.
|
|
3
3
|
* CLI-specific errors (e.g., Commander.js argument validation) can be added here if needed.
|
|
4
4
|
*/
|
|
5
|
-
export type { CliErrorConflict, CliErrorEnvelope } from '@prisma-next/
|
|
5
|
+
export type { CliErrorConflict, CliErrorEnvelope } from '@prisma-next/errors/control';
|
|
6
6
|
export {
|
|
7
7
|
CliStructuredError,
|
|
8
8
|
errorConfigFileNotFound,
|
|
@@ -14,13 +14,19 @@ export {
|
|
|
14
14
|
errorDriverRequired,
|
|
15
15
|
errorFamilyReadMarkerSqlRequired,
|
|
16
16
|
errorFileNotFound,
|
|
17
|
-
errorHashMismatch,
|
|
18
|
-
errorJsonFormatNotSupported,
|
|
19
|
-
errorMarkerMissing,
|
|
20
17
|
errorMigrationPlanningFailed,
|
|
21
18
|
errorQueryRunnerFactoryRequired,
|
|
22
|
-
errorRuntime,
|
|
23
19
|
errorTargetMigrationNotSupported,
|
|
24
|
-
errorTargetMismatch,
|
|
25
20
|
errorUnexpected,
|
|
26
|
-
} from '@prisma-next/
|
|
21
|
+
} from '@prisma-next/errors/control';
|
|
22
|
+
export {
|
|
23
|
+
ERROR_CODE_DESTRUCTIVE_CHANGES,
|
|
24
|
+
errorDestructiveChanges,
|
|
25
|
+
errorHashMismatch,
|
|
26
|
+
errorMarkerMissing,
|
|
27
|
+
errorMarkerRequired,
|
|
28
|
+
errorRunnerFailed,
|
|
29
|
+
errorRuntime,
|
|
30
|
+
errorSchemaVerificationFailed,
|
|
31
|
+
errorTargetMismatch,
|
|
32
|
+
} from '@prisma-next/errors/execution';
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import type { ControlTargetDescriptor } from '@prisma-next/framework-components/control';
|
|
3
|
+
import { hasMigrations } from '@prisma-next/framework-components/control';
|
|
4
|
+
import type { PathDecision } from '@prisma-next/migration-tools/dag';
|
|
5
|
+
import { reconstructGraph } from '@prisma-next/migration-tools/dag';
|
|
6
|
+
import { readMigrationsDir } from '@prisma-next/migration-tools/io';
|
|
7
|
+
import type {
|
|
8
|
+
AttestedMigrationBundle,
|
|
9
|
+
DraftMigrationBundle,
|
|
10
|
+
MigrationGraph,
|
|
11
|
+
} from '@prisma-next/migration-tools/types';
|
|
12
|
+
import { isAttested, isDraft } from '@prisma-next/migration-tools/types';
|
|
13
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
1
14
|
import type { Command } from 'commander';
|
|
15
|
+
import { relative, resolve } from 'pathe';
|
|
16
|
+
import { formatCommandHelp } from './formatters/help';
|
|
17
|
+
import type { CommonCommandOptions } from './global-flags';
|
|
18
|
+
import { parseGlobalFlags } from './global-flags';
|
|
19
|
+
|
|
20
|
+
const longDescriptions = new WeakMap<Command, string>();
|
|
21
|
+
const commandExamples = new WeakMap<Command, readonly string[]>();
|
|
2
22
|
|
|
3
23
|
/**
|
|
4
24
|
* Sets both short and long descriptions for a command.
|
|
@@ -12,15 +32,294 @@ export function setCommandDescriptions(
|
|
|
12
32
|
): Command {
|
|
13
33
|
command.description(shortDescription);
|
|
14
34
|
if (longDescription) {
|
|
15
|
-
|
|
16
|
-
(command as Command & { _longDescription?: string })._longDescription = longDescription;
|
|
35
|
+
longDescriptions.set(command, longDescription);
|
|
17
36
|
}
|
|
18
37
|
return command;
|
|
19
38
|
}
|
|
20
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Sets copy-pastable examples for a command, shown in help text.
|
|
42
|
+
*/
|
|
43
|
+
export function setCommandExamples(command: Command, examples: readonly string[]): Command {
|
|
44
|
+
commandExamples.set(command, examples);
|
|
45
|
+
return command;
|
|
46
|
+
}
|
|
47
|
+
|
|
21
48
|
/**
|
|
22
49
|
* Gets the long description from a command if it was set via setCommandDescriptions.
|
|
23
50
|
*/
|
|
24
51
|
export function getLongDescription(command: Command): string | undefined {
|
|
25
|
-
return (command
|
|
52
|
+
return longDescriptions.get(command);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Gets examples from a command if set via setCommandExamples.
|
|
57
|
+
*/
|
|
58
|
+
export function getCommandExamples(command: Command): readonly string[] | undefined {
|
|
59
|
+
return commandExamples.get(command);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Shared CLI options interface for migration commands (db init, db update).
|
|
64
|
+
* These are the Commander.js parsed options common to both commands.
|
|
65
|
+
*/
|
|
66
|
+
export interface MigrationCommandOptions extends CommonCommandOptions {
|
|
67
|
+
readonly db?: string;
|
|
68
|
+
readonly config?: string;
|
|
69
|
+
readonly dryRun?: boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Resolves the absolute path to contract.json from the config.
|
|
74
|
+
* Centralises the fallback logic shared by every command that reads the contract.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveContractPath(config: { contract?: { output?: string } }): string {
|
|
77
|
+
return config.contract?.output
|
|
78
|
+
? resolve(config.contract.output)
|
|
79
|
+
: resolve('src/prisma/contract.json');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Resolves the migrations directory and config path from CLI options.
|
|
84
|
+
* Shared by migration-apply, migration-plan, and migration-status.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveMigrationPaths(
|
|
87
|
+
configOption: string | undefined,
|
|
88
|
+
config: { migrations?: { dir?: string } },
|
|
89
|
+
): {
|
|
90
|
+
configPath: string;
|
|
91
|
+
migrationsDir: string;
|
|
92
|
+
migrationsRelative: string;
|
|
93
|
+
refsPath: string;
|
|
94
|
+
} {
|
|
95
|
+
const configPath = configOption
|
|
96
|
+
? relative(process.cwd(), resolve(configOption))
|
|
97
|
+
: 'prisma-next.config.ts';
|
|
98
|
+
const migrationsDir = resolve(
|
|
99
|
+
configOption ? resolve(configOption, '..') : process.cwd(),
|
|
100
|
+
config.migrations?.dir ?? 'migrations',
|
|
101
|
+
);
|
|
102
|
+
const migrationsRelative = relative(process.cwd(), migrationsDir);
|
|
103
|
+
const refsPath = resolve(migrationsDir, 'refs.json');
|
|
104
|
+
return { configPath, migrationsDir, migrationsRelative, refsPath };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Slim representation of a PathDecision for CLI JSON output.
|
|
109
|
+
* Strips internal fields (createdAt, labels) from path entries.
|
|
110
|
+
*/
|
|
111
|
+
export interface PathDecisionResult {
|
|
112
|
+
readonly fromHash: string;
|
|
113
|
+
readonly toHash: string;
|
|
114
|
+
readonly alternativeCount: number;
|
|
115
|
+
readonly tieBreakReasons: readonly string[];
|
|
116
|
+
readonly refName?: string;
|
|
117
|
+
readonly selectedPath: readonly {
|
|
118
|
+
readonly dirName: string;
|
|
119
|
+
readonly migrationId: string;
|
|
120
|
+
readonly from: string;
|
|
121
|
+
readonly to: string;
|
|
122
|
+
}[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Maps a PathDecision to the slim CLI output representation.
|
|
127
|
+
*/
|
|
128
|
+
export function toPathDecisionResult(decision: PathDecision): PathDecisionResult {
|
|
129
|
+
return {
|
|
130
|
+
fromHash: decision.fromHash,
|
|
131
|
+
toHash: decision.toHash,
|
|
132
|
+
alternativeCount: decision.alternativeCount,
|
|
133
|
+
tieBreakReasons: decision.tieBreakReasons,
|
|
134
|
+
...ifDefined('refName', decision.refName),
|
|
135
|
+
selectedPath: decision.selectedPath.map((entry) => ({
|
|
136
|
+
dirName: entry.dirName,
|
|
137
|
+
migrationId: entry.migrationId,
|
|
138
|
+
from: entry.from,
|
|
139
|
+
to: entry.to,
|
|
140
|
+
})),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function targetSupportsMigrations(target: ControlTargetDescriptor<string, string>): boolean {
|
|
145
|
+
return hasMigrations(target);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getTargetMigrations(target: ControlTargetDescriptor<string, string>) {
|
|
149
|
+
return hasMigrations(target) ? target.migrations : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reads the migrations directory, filters to attested bundles, and builds
|
|
154
|
+
* the migration graph. Throws on I/O or graph errors — callers handle
|
|
155
|
+
* error mapping.
|
|
156
|
+
*/
|
|
157
|
+
export async function loadMigrationBundles(migrationsDir: string): Promise<{
|
|
158
|
+
bundles: readonly AttestedMigrationBundle[];
|
|
159
|
+
graph: MigrationGraph;
|
|
160
|
+
}> {
|
|
161
|
+
const allBundles = await readMigrationsDir(migrationsDir);
|
|
162
|
+
const bundles = allBundles.filter(isAttested);
|
|
163
|
+
const graph = reconstructGraph(bundles);
|
|
164
|
+
return { bundles, graph };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface MigrationBundleSet {
|
|
168
|
+
readonly attested: readonly AttestedMigrationBundle[];
|
|
169
|
+
readonly drafts: readonly DraftMigrationBundle[];
|
|
170
|
+
readonly graph: MigrationGraph;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function loadAllBundles(migrationsDir: string): Promise<MigrationBundleSet> {
|
|
174
|
+
const all = await readMigrationsDir(migrationsDir);
|
|
175
|
+
const attested = all.filter(isAttested);
|
|
176
|
+
const drafts = all.filter(isDraft);
|
|
177
|
+
const graph = reconstructGraph(attested);
|
|
178
|
+
return { attested, drafts, graph };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* The subset of the emitted contract.json that the framework layer can
|
|
183
|
+
* safely type. The emitter adds these fields on top of the family-specific
|
|
184
|
+
* storage/models/relations. Other fields exist in the JSON but are opaque
|
|
185
|
+
* at this layer — the index signature preserves them for downstream
|
|
186
|
+
* consumers that operate at the family level (e.g., the control client).
|
|
187
|
+
*/
|
|
188
|
+
export interface ContractEnvelope {
|
|
189
|
+
readonly storageHash: string;
|
|
190
|
+
readonly schemaVersion: string;
|
|
191
|
+
readonly target: string;
|
|
192
|
+
readonly targetFamily: string;
|
|
193
|
+
readonly profileHash?: string;
|
|
194
|
+
readonly [key: string]: unknown;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Reads and parses contract.json, validating the framework-level envelope
|
|
199
|
+
* fields (storageHash, schemaVersion, target, targetFamily).
|
|
200
|
+
*
|
|
201
|
+
* Family-specific validation (storage structure, codec mappings, etc.)
|
|
202
|
+
* happens downstream in the control client via the family instance.
|
|
203
|
+
*/
|
|
204
|
+
export async function readContractEnvelope(config: {
|
|
205
|
+
contract?: { output?: string };
|
|
206
|
+
}): Promise<ContractEnvelope> {
|
|
207
|
+
const contractPath = resolveContractPath(config);
|
|
208
|
+
const content = await readFile(contractPath, 'utf-8');
|
|
209
|
+
const json = JSON.parse(content) as Record<string, unknown>;
|
|
210
|
+
|
|
211
|
+
const { schemaVersion, target, targetFamily, profileHash } = json;
|
|
212
|
+
const storage = json['storage'] as Record<string, unknown> | undefined;
|
|
213
|
+
const storageHash = storage?.['storageHash'];
|
|
214
|
+
|
|
215
|
+
if (typeof storageHash !== 'string') {
|
|
216
|
+
throw new Error(
|
|
217
|
+
`Contract at ${relative(process.cwd(), contractPath)} is missing a valid storage.storageHash. Run \`prisma-next contract emit\` to regenerate.`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (typeof schemaVersion !== 'string') {
|
|
221
|
+
throw new Error(
|
|
222
|
+
`Contract at ${relative(process.cwd(), contractPath)} is missing schemaVersion.`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
if (typeof target !== 'string') {
|
|
226
|
+
throw new Error(`Contract at ${relative(process.cwd(), contractPath)} is missing target.`);
|
|
227
|
+
}
|
|
228
|
+
if (typeof targetFamily !== 'string') {
|
|
229
|
+
throw new Error(
|
|
230
|
+
`Contract at ${relative(process.cwd(), contractPath)} is missing targetFamily.`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
...json,
|
|
236
|
+
storageHash,
|
|
237
|
+
schemaVersion,
|
|
238
|
+
target,
|
|
239
|
+
targetFamily,
|
|
240
|
+
...(typeof profileHash === 'string' ? { profileHash } : {}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Masks credentials in a database connection URL.
|
|
246
|
+
* Handles standard URLs (username + password + query params) and libpq-style key=value strings.
|
|
247
|
+
*/
|
|
248
|
+
export function maskConnectionUrl(url: string): string {
|
|
249
|
+
try {
|
|
250
|
+
const parsed = new URL(url);
|
|
251
|
+
if (parsed.username) {
|
|
252
|
+
parsed.username = '****';
|
|
253
|
+
}
|
|
254
|
+
if (parsed.password) {
|
|
255
|
+
parsed.password = '****';
|
|
256
|
+
}
|
|
257
|
+
// Also mask password in query parameters (e.g., ?password=secret, ?sslpassword=secret)
|
|
258
|
+
for (const key of [...parsed.searchParams.keys()]) {
|
|
259
|
+
if (/password/i.test(key)) {
|
|
260
|
+
parsed.searchParams.set(key, '****');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return parsed.toString();
|
|
264
|
+
} catch {
|
|
265
|
+
// Fallback for libpq-style key=value connection strings (e.g., "host=localhost password=secret user=admin")
|
|
266
|
+
return url
|
|
267
|
+
.replace(/password\s*=\s*\S+/gi, 'password=****')
|
|
268
|
+
.replace(/user\s*=\s*\S+/gi, 'user=****');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Strips raw connection URL fragments from an error message to prevent credential leakage.
|
|
274
|
+
* Call this before surfacing driver errors to the user.
|
|
275
|
+
*/
|
|
276
|
+
export function sanitizeErrorMessage(message: string, connectionUrl?: string): string {
|
|
277
|
+
if (!connectionUrl) {
|
|
278
|
+
return message;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const parsed = new URL(connectionUrl);
|
|
282
|
+
// Replace the full URL (with and without trailing slash)
|
|
283
|
+
let sanitized = message;
|
|
284
|
+
sanitized = sanitized.replaceAll(connectionUrl, maskConnectionUrl(connectionUrl));
|
|
285
|
+
// Also replace the password and username individually if they appear
|
|
286
|
+
if (parsed.password) {
|
|
287
|
+
sanitized = sanitized.replaceAll(parsed.password, '****');
|
|
288
|
+
}
|
|
289
|
+
if (parsed.username) {
|
|
290
|
+
sanitized = sanitized.replaceAll(parsed.username, '****');
|
|
291
|
+
}
|
|
292
|
+
return sanitized;
|
|
293
|
+
} catch {
|
|
294
|
+
// For libpq-style strings, mask password and user values in the message
|
|
295
|
+
return message
|
|
296
|
+
.replace(/password\s*=\s*\S+/gi, 'password=****')
|
|
297
|
+
.replace(/user\s*=\s*\S+/gi, 'user=****');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Registers the global CLI options shared by every command:
|
|
303
|
+
* --json, -q/--quiet, -v/--verbose, --trace, --color, --no-color,
|
|
304
|
+
* --interactive, --no-interactive, -y/--yes.
|
|
305
|
+
*
|
|
306
|
+
* Also sets up the styled help formatter.
|
|
307
|
+
*/
|
|
308
|
+
export function addGlobalOptions(command: Command): Command {
|
|
309
|
+
return command
|
|
310
|
+
.configureHelp({
|
|
311
|
+
formatHelp: (cmd) => {
|
|
312
|
+
const flags = parseGlobalFlags({});
|
|
313
|
+
return formatCommandHelp({ command: cmd, flags });
|
|
314
|
+
},
|
|
315
|
+
})
|
|
316
|
+
.option('--json', 'Output as JSON')
|
|
317
|
+
.option('-q, --quiet', 'Quiet mode: errors only')
|
|
318
|
+
.option('-v, --verbose', 'Verbose output: debug info, timings')
|
|
319
|
+
.option('--trace', 'Trace output: deep internals, stack traces')
|
|
320
|
+
.option('--color', 'Force color output')
|
|
321
|
+
.option('--no-color', 'Disable color output')
|
|
322
|
+
.option('--interactive', 'Force interactive mode')
|
|
323
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
324
|
+
.option('-y, --yes', 'Auto-accept prompts');
|
|
26
325
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { ifDefined } from '@prisma-next/utils/defined';
|
|
2
|
+
import { relative } from 'pathe';
|
|
3
|
+
|
|
4
|
+
import type { GlobalFlags } from '../global-flags';
|
|
5
|
+
import { isVerbose } from './helpers';
|
|
6
|
+
|
|
7
|
+
// EmitContractResult type for CLI output formatting (includes file paths)
|
|
8
|
+
export interface EmitContractResult {
|
|
9
|
+
readonly storageHash: string;
|
|
10
|
+
readonly executionHash?: string;
|
|
11
|
+
readonly profileHash: string;
|
|
12
|
+
readonly outDir: string;
|
|
13
|
+
readonly files: {
|
|
14
|
+
readonly json: string;
|
|
15
|
+
readonly dts: string;
|
|
16
|
+
};
|
|
17
|
+
readonly timings: {
|
|
18
|
+
readonly total: number;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Formats human-readable output for contract emit.
|
|
24
|
+
*/
|
|
25
|
+
export function formatEmitOutput(result: EmitContractResult, flags: GlobalFlags): string {
|
|
26
|
+
if (flags.quiet) {
|
|
27
|
+
return '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const lines: string[] = [];
|
|
31
|
+
|
|
32
|
+
// Convert absolute paths to relative paths from cwd
|
|
33
|
+
const jsonPath = relative(process.cwd(), result.files.json);
|
|
34
|
+
const dtsPath = relative(process.cwd(), result.files.dts);
|
|
35
|
+
|
|
36
|
+
lines.push(`✔ Emitted contract.json → ${jsonPath}`);
|
|
37
|
+
lines.push(`✔ Emitted contract.d.ts → ${dtsPath}`);
|
|
38
|
+
lines.push(` storageHash: ${result.storageHash}`);
|
|
39
|
+
if (result.executionHash) {
|
|
40
|
+
lines.push(` executionHash: ${result.executionHash}`);
|
|
41
|
+
}
|
|
42
|
+
if (result.profileHash) {
|
|
43
|
+
lines.push(` profileHash: ${result.profileHash}`);
|
|
44
|
+
}
|
|
45
|
+
if (isVerbose(flags, 1)) {
|
|
46
|
+
lines.push(` Total time: ${result.timings.total}ms`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return lines.join('\n');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Formats JSON output for contract emit.
|
|
54
|
+
*/
|
|
55
|
+
export function formatEmitJson(result: EmitContractResult): string {
|
|
56
|
+
const output = {
|
|
57
|
+
ok: true,
|
|
58
|
+
storageHash: result.storageHash,
|
|
59
|
+
...ifDefined('executionHash', result.executionHash),
|
|
60
|
+
...(result.profileHash ? { profileHash: result.profileHash } : {}),
|
|
61
|
+
outDir: result.outDir,
|
|
62
|
+
files: result.files,
|
|
63
|
+
timings: result.timings,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return JSON.stringify(output, null, 2);
|
|
67
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { red } from 'colorette';
|
|
2
|
+
|
|
3
|
+
import type { CliErrorConflict, CliErrorEnvelope } from '../cli-errors';
|
|
4
|
+
import type { GlobalFlags } from '../global-flags';
|
|
5
|
+
import { createColorFormatter, formatDim, isVerbose } from './helpers';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formats error output for human-readable display.
|
|
9
|
+
*/
|
|
10
|
+
export function formatErrorOutput(error: CliErrorEnvelope, flags: GlobalFlags): string {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
|
|
13
|
+
const useColor = flags.color !== false;
|
|
14
|
+
const formatRed = createColorFormatter(useColor, red);
|
|
15
|
+
const formatDimText = (text: string) => formatDim(useColor, text);
|
|
16
|
+
|
|
17
|
+
lines.push(`${formatRed('✖')} ${error.summary} (${error.code})`);
|
|
18
|
+
|
|
19
|
+
if (error.why) {
|
|
20
|
+
lines.push(`${formatDimText(` Why: ${error.why}`)}`);
|
|
21
|
+
}
|
|
22
|
+
if (error.fix) {
|
|
23
|
+
lines.push(`${formatDimText(` Fix: ${error.fix}`)}`);
|
|
24
|
+
}
|
|
25
|
+
if (error.where?.path) {
|
|
26
|
+
const whereLine = error.where.line
|
|
27
|
+
? `${error.where.path}:${error.where.line}`
|
|
28
|
+
: error.where.path;
|
|
29
|
+
lines.push(`${formatDimText(` Where: ${whereLine}`)}`);
|
|
30
|
+
}
|
|
31
|
+
// Show conflicts list if present (always show a short list; show full list when verbose)
|
|
32
|
+
if (error.meta?.['conflicts']) {
|
|
33
|
+
const conflicts = error.meta['conflicts'] as readonly CliErrorConflict[];
|
|
34
|
+
if (conflicts.length > 0) {
|
|
35
|
+
const maxToShow = isVerbose(flags, 1) ? conflicts.length : Math.min(3, conflicts.length);
|
|
36
|
+
const header = isVerbose(flags, 1)
|
|
37
|
+
? ' Conflicts:'
|
|
38
|
+
: ` Conflicts (showing ${maxToShow} of ${conflicts.length}):`;
|
|
39
|
+
lines.push(`${formatDimText(header)}`);
|
|
40
|
+
for (const conflict of conflicts.slice(0, maxToShow)) {
|
|
41
|
+
lines.push(`${formatDimText(` - [${conflict.kind}] ${conflict.summary}`)}`);
|
|
42
|
+
}
|
|
43
|
+
if (!isVerbose(flags, 1) && conflicts.length > maxToShow) {
|
|
44
|
+
lines.push(`${formatDimText(' Re-run with -v/--verbose to see all conflicts')}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Show issues list if present (always show a short list; show full list when verbose)
|
|
49
|
+
if (error.meta?.['issues']) {
|
|
50
|
+
const issues = error.meta['issues'] as readonly { kind?: string; message?: string }[];
|
|
51
|
+
if (issues.length > 0) {
|
|
52
|
+
const maxToShow = isVerbose(flags, 1) ? issues.length : Math.min(3, issues.length);
|
|
53
|
+
const header = isVerbose(flags, 1)
|
|
54
|
+
? ' Issues:'
|
|
55
|
+
: ` Issues (showing ${maxToShow} of ${issues.length}):`;
|
|
56
|
+
lines.push(`${formatDimText(header)}`);
|
|
57
|
+
for (const issue of issues.slice(0, maxToShow)) {
|
|
58
|
+
const kind = issue.kind ?? 'issue';
|
|
59
|
+
const message = issue.message ?? '';
|
|
60
|
+
lines.push(`${formatDimText(` - [${kind}] ${message}`)}`);
|
|
61
|
+
}
|
|
62
|
+
if (!isVerbose(flags, 1) && issues.length > maxToShow) {
|
|
63
|
+
lines.push(`${formatDimText(' Re-run with -v/--verbose to see all issues')}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (error.docsUrl && isVerbose(flags, 1)) {
|
|
68
|
+
lines.push(formatDimText(error.docsUrl));
|
|
69
|
+
}
|
|
70
|
+
if (isVerbose(flags, 2) && error.meta) {
|
|
71
|
+
lines.push(`${formatDimText(` Meta: ${JSON.stringify(error.meta, null, 2)}`)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return lines.join('\n');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Formats error output as JSON.
|
|
79
|
+
*/
|
|
80
|
+
export function formatErrorJson(error: CliErrorEnvelope): string {
|
|
81
|
+
return JSON.stringify(error, null, 2);
|
|
82
|
+
}
|