@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.40

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.
Files changed (165) hide show
  1. package/README.md +56 -21
  2. package/dist/agent-skill-mongo.md +63 -31
  3. package/dist/agent-skill-postgres.md +1 -1
  4. package/dist/cli-errors-By1iVE3z.mjs +34 -0
  5. package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
  6. package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
  7. package/dist/cli.mjs +123 -15
  8. package/dist/cli.mjs.map +1 -1
  9. package/dist/{client-TG7rbCWT.mjs → client-1JqqkiC7.mjs} +45 -20
  10. package/dist/client-1JqqkiC7.mjs.map +1 -0
  11. package/dist/commands/contract-emit.d.mts.map +1 -1
  12. package/dist/commands/contract-emit.mjs +2 -2
  13. package/dist/commands/contract-infer.d.mts.map +1 -1
  14. package/dist/commands/contract-infer.mjs +2 -2
  15. package/dist/commands/db-init.d.mts.map +1 -1
  16. package/dist/commands/db-init.mjs +10 -9
  17. package/dist/commands/db-init.mjs.map +1 -1
  18. package/dist/commands/db-schema.mjs +5 -5
  19. package/dist/commands/db-sign.mjs +7 -7
  20. package/dist/commands/db-update.mjs +9 -9
  21. package/dist/commands/db-update.mjs.map +1 -1
  22. package/dist/commands/db-verify.mjs +9 -9
  23. package/dist/commands/migration-apply.d.mts +5 -2
  24. package/dist/commands/migration-apply.d.mts.map +1 -1
  25. package/dist/commands/migration-apply.mjs +55 -56
  26. package/dist/commands/migration-apply.mjs.map +1 -1
  27. package/dist/commands/migration-new.d.mts.map +1 -1
  28. package/dist/commands/migration-new.mjs +26 -32
  29. package/dist/commands/migration-new.mjs.map +1 -1
  30. package/dist/commands/migration-plan.d.mts +14 -5
  31. package/dist/commands/migration-plan.d.mts.map +1 -1
  32. package/dist/commands/migration-plan.mjs +45 -48
  33. package/dist/commands/migration-plan.mjs.map +1 -1
  34. package/dist/commands/migration-ref.d.mts +1 -1
  35. package/dist/commands/migration-ref.d.mts.map +1 -1
  36. package/dist/commands/migration-ref.mjs +6 -10
  37. package/dist/commands/migration-ref.mjs.map +1 -1
  38. package/dist/commands/migration-show.d.mts +13 -7
  39. package/dist/commands/migration-show.d.mts.map +1 -1
  40. package/dist/commands/migration-show.mjs +27 -29
  41. package/dist/commands/migration-show.mjs.map +1 -1
  42. package/dist/commands/migration-status.d.mts +23 -5
  43. package/dist/commands/migration-status.d.mts.map +1 -1
  44. package/dist/commands/migration-status.mjs +3 -3
  45. package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
  46. package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
  47. package/dist/config-loader.mjs +1 -1
  48. package/dist/contract-emit-LjzCoicC.mjs +4 -0
  49. package/dist/contract-emit-RZBWzkop.mjs +329 -0
  50. package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
  51. package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
  52. package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
  53. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
  54. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
  55. package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
  56. package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
  57. package/dist/exports/control-api.d.mts +86 -21
  58. package/dist/exports/control-api.d.mts.map +1 -1
  59. package/dist/exports/control-api.mjs +5 -5
  60. package/dist/exports/index.mjs +3 -3
  61. package/dist/exports/init-output.d.mts +39 -0
  62. package/dist/exports/init-output.d.mts.map +1 -0
  63. package/dist/exports/init-output.mjs +3 -0
  64. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
  65. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
  66. package/dist/init-C7dE9KOJ.mjs +2062 -0
  67. package/dist/init-C7dE9KOJ.mjs.map +1 -0
  68. package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
  69. package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
  70. package/dist/migration-cli.d.mts +41 -11
  71. package/dist/migration-cli.d.mts.map +1 -1
  72. package/dist/migration-cli.mjs +308 -84
  73. package/dist/migration-cli.mjs.map +1 -1
  74. package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
  75. package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
  76. package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DoPrFIOQ.mjs} +114 -57
  77. package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
  78. package/dist/{migrations-fU0xoKjS.mjs → migrations-MEoKMiV5.mjs} +42 -21
  79. package/dist/migrations-MEoKMiV5.mjs.map +1 -0
  80. package/dist/output-BpcQrnnq.mjs +103 -0
  81. package/dist/output-BpcQrnnq.mjs.map +1 -0
  82. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
  83. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
  84. package/dist/quick-reference-mongo.md +34 -13
  85. package/dist/quick-reference-postgres.md +11 -9
  86. package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ch6hVnOo.mjs} +35 -93
  87. package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
  88. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
  89. package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
  90. package/dist/{verify-bl__PkXk.mjs → verify-BT9tgCOH.mjs} +2 -2
  91. package/dist/{verify-bl__PkXk.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
  92. package/package.json +23 -17
  93. package/src/cli.ts +32 -6
  94. package/src/commands/contract-emit.ts +67 -163
  95. package/src/commands/contract-infer.ts +7 -20
  96. package/src/commands/db-init.ts +1 -0
  97. package/src/commands/db-update.ts +1 -1
  98. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  99. package/src/commands/init/errors.ts +254 -0
  100. package/src/commands/init/exit-codes.ts +62 -0
  101. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  102. package/src/commands/init/hygiene-gitignore.ts +48 -0
  103. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  104. package/src/commands/init/index.ts +112 -7
  105. package/src/commands/init/init.ts +766 -144
  106. package/src/commands/init/inputs.ts +421 -0
  107. package/src/commands/init/output.ts +147 -0
  108. package/src/commands/init/probe-db.ts +308 -0
  109. package/src/commands/init/reinit-cleanup.ts +83 -0
  110. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  111. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  112. package/src/commands/init/templates/agent-skill.ts +25 -3
  113. package/src/commands/init/templates/code-templates.ts +125 -32
  114. package/src/commands/init/templates/env.ts +80 -0
  115. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  116. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  117. package/src/commands/init/templates/quick-reference.ts +42 -3
  118. package/src/commands/init/templates/tsconfig.ts +167 -5
  119. package/src/commands/inspect-live-schema.ts +10 -5
  120. package/src/commands/migration-apply.ts +84 -63
  121. package/src/commands/migration-new.ts +28 -34
  122. package/src/commands/migration-plan.ts +80 -56
  123. package/src/commands/migration-ref.ts +8 -7
  124. package/src/commands/migration-show.ts +53 -36
  125. package/src/commands/migration-status.ts +194 -58
  126. package/src/config-path-validation.ts +0 -1
  127. package/src/control-api/client.ts +21 -0
  128. package/src/control-api/operations/contract-emit.ts +198 -115
  129. package/src/control-api/operations/db-init.ts +10 -6
  130. package/src/control-api/operations/db-update.ts +10 -6
  131. package/src/control-api/operations/migration-apply.ts +30 -9
  132. package/src/control-api/types.ts +69 -7
  133. package/src/exports/control-api.ts +2 -1
  134. package/src/exports/init-output.ts +10 -0
  135. package/src/migration-cli.ts +445 -122
  136. package/src/utils/cli-errors.ts +49 -2
  137. package/src/utils/command-helpers.ts +45 -23
  138. package/src/utils/emit-queue.ts +26 -0
  139. package/src/utils/formatters/graph-migration-mapper.ts +7 -3
  140. package/src/utils/formatters/migrations.ts +62 -26
  141. package/src/utils/publish-contract-artifact-pair.ts +134 -0
  142. package/dist/cli-errors-DHq6GQGu.mjs +0 -5
  143. package/dist/client-TG7rbCWT.mjs.map +0 -1
  144. package/dist/config-loader-_W4T21X1.mjs.map +0 -1
  145. package/dist/contract-emit-CQfj7xJn.mjs +0 -122
  146. package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
  147. package/dist/contract-emit-DpPjuFy-.mjs +0 -195
  148. package/dist/contract-emit-DpPjuFy-.mjs.map +0 -1
  149. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  150. package/dist/contract-infer-BS4kIX9c.mjs.map +0 -1
  151. package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
  152. package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
  153. package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
  154. package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
  155. package/dist/init-CQfo_4Ro.mjs +0 -430
  156. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  157. package/dist/inspect-live-schema-BsoFVoS1.mjs.map +0 -1
  158. package/dist/migration-status-Ry3TnEya.mjs.map +0 -1
  159. package/dist/migrations-fU0xoKjS.mjs.map +0 -1
  160. package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
  161. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  162. package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
  163. package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
  164. package/src/control-api/operations/extract-operation-statements.ts +0 -14
  165. package/src/control-api/operations/extract-sql-ddl.ts +0 -47
@@ -112,7 +112,7 @@ async function executeDbUpdateCommand(
112
112
  label: op.label,
113
113
  operationClass: op.operationClass,
114
114
  })),
115
- ...ifDefined('sql', result.value.plan.sql),
115
+ ...ifDefined('preview', result.value.plan.preview),
116
116
  },
117
117
  ...ifDefined(
118
118
  'execution',
@@ -0,0 +1,141 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, join } from 'pathe';
3
+
4
+ /**
5
+ * Catalog entry detected in a `pnpm-workspace.yaml` that overrides one of
6
+ * the packages `init` installs. The `version` is the raw value as written
7
+ * in the workspace file (no normalisation), so the warning surfaces the
8
+ * exact text the user can search for if they want to find the override.
9
+ */
10
+ export interface PnpmCatalogOverride {
11
+ readonly name: string;
12
+ readonly version: string;
13
+ }
14
+
15
+ /**
16
+ * Result of scanning for a pnpm workspace catalog that overrides any of
17
+ * the packages `init` is about to install. Returns `null` when no
18
+ * `pnpm-workspace.yaml` is found in `baseDir` or any ancestor; an empty
19
+ * `entries` array means a workspace exists but contains none of our
20
+ * packages.
21
+ */
22
+ export interface PnpmCatalogScanResult {
23
+ /** Absolute path of the `pnpm-workspace.yaml` that was consulted. */
24
+ readonly workspaceFile: string;
25
+ readonly entries: readonly PnpmCatalogOverride[];
26
+ }
27
+
28
+ /**
29
+ * Walks up from `baseDir` looking for `pnpm-workspace.yaml`, then scans
30
+ * its top-level `catalog:` block for entries that match any of `packages`.
31
+ *
32
+ * Implements FR7.3 / Spec Decision 8 (honour-and-warn): when `init` runs
33
+ * inside a pnpm workspace whose catalog overrides one of the packages it
34
+ * installs, surface a structured warning so the user knows the catalog
35
+ * version (not the published `latest`) is what ended up in their
36
+ * `node_modules`. pnpm itself does this silently; the warning closes the
37
+ * "looks fine, must be wrong version six months later" gap.
38
+ *
39
+ * Notes / scope:
40
+ *
41
+ * - We only inspect the unnamed top-level `catalog:` block. pnpm also
42
+ * supports `catalogs:` (plural — *named* catalogs referenced via
43
+ * `catalog:foo` specifiers); those don't apply to a vanilla
44
+ * `pnpm add prisma-next` invocation, so we skip them.
45
+ * - We don't validate YAML syntax exhaustively. The file format pnpm
46
+ * ships is line-oriented and well-known; a minimal regex is more
47
+ * robust than depending on a YAML parser for one warning.
48
+ * - We don't compare against the registry's `latest` — pnpm uses the
49
+ * catalog version regardless, so the warning fires whenever a match
50
+ * exists. The user-facing copy explains how to opt out.
51
+ */
52
+ export function detectPnpmCatalogOverrides(
53
+ baseDir: string,
54
+ packages: readonly string[],
55
+ ): PnpmCatalogScanResult | null {
56
+ const workspaceFile = findNearestPnpmWorkspaceFile(baseDir);
57
+ if (workspaceFile === null) {
58
+ return null;
59
+ }
60
+
61
+ const contents = readFileSync(workspaceFile, 'utf-8');
62
+ const catalog = extractCatalogBlock(contents);
63
+ if (catalog === null) {
64
+ return { workspaceFile, entries: [] };
65
+ }
66
+
67
+ const wanted = new Set(packages);
68
+ const entries: PnpmCatalogOverride[] = [];
69
+ for (const [name, version] of catalog) {
70
+ if (wanted.has(name)) {
71
+ entries.push({ name, version });
72
+ }
73
+ }
74
+ return { workspaceFile, entries };
75
+ }
76
+
77
+ function findNearestPnpmWorkspaceFile(baseDir: string): string | null {
78
+ let dir = baseDir;
79
+ let prev = '';
80
+ while (dir !== prev) {
81
+ const candidate = join(dir, 'pnpm-workspace.yaml');
82
+ if (existsSync(candidate)) {
83
+ return candidate;
84
+ }
85
+ prev = dir;
86
+ dir = dirname(dir);
87
+ }
88
+ return null;
89
+ }
90
+
91
+ /**
92
+ * Returns the entries inside the top-level `catalog:` block as `[name, version]`
93
+ * pairs in document order, or `null` when no `catalog:` block exists.
94
+ *
95
+ * The parser is intentionally minimal: it reads line-by-line, locates the
96
+ * top-level `catalog:` line (no leading whitespace), then collects every
97
+ * subsequent indented line of the form `<key>: <value>` until the next
98
+ * top-level key (or end of file). Quotes around `<key>` and `<value>`
99
+ * are stripped; comments (`#…`) are ignored.
100
+ */
101
+ function extractCatalogBlock(contents: string): Array<[string, string]> | null {
102
+ const lines = contents.split(/\r?\n/);
103
+ const startIdx = lines.findIndex((line) => /^catalog\s*:\s*$/.test(line));
104
+ if (startIdx === -1) {
105
+ return null;
106
+ }
107
+
108
+ const entries: Array<[string, string]> = [];
109
+ for (let i = startIdx + 1; i < lines.length; i++) {
110
+ const raw = lines[i] ?? '';
111
+ if (raw.trim() === '' || /^\s*#/.test(raw)) {
112
+ continue;
113
+ }
114
+ if (!/^\s/.test(raw)) {
115
+ // Hit the next top-level key — catalog block ended.
116
+ break;
117
+ }
118
+ const match = raw.match(/^\s+(?:'([^']+)'|"([^"]+)"|([^:\s'"]+))\s*:\s*(.*?)\s*(?:#.*)?$/);
119
+ if (!match) {
120
+ continue;
121
+ }
122
+ const name = match[1] ?? match[2] ?? match[3];
123
+ if (name === undefined) continue;
124
+ const rawValue = match[4] ?? '';
125
+ const version = stripQuotes(rawValue.trim());
126
+ if (version === '') continue;
127
+ entries.push([name, version]);
128
+ }
129
+ return entries;
130
+ }
131
+
132
+ function stripQuotes(value: string): string {
133
+ if (value.length >= 2) {
134
+ const first = value[0];
135
+ const last = value[value.length - 1];
136
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
137
+ return value.slice(1, -1);
138
+ }
139
+ }
140
+ return value;
141
+ }
@@ -0,0 +1,254 @@
1
+ import { CliStructuredError } from '../../utils/cli-errors';
2
+
3
+ /**
4
+ * No `package.json` / `deno.json` / `deno.jsonc` in the target directory.
5
+ *
6
+ * `init` cannot bootstrap a fresh project from a bare directory (NG1) — that
7
+ * gap is tracked separately. The fix is for the user to run `npm init` (or
8
+ * the equivalent for their package manager) first.
9
+ */
10
+ export function errorInitMissingManifest(): CliStructuredError {
11
+ return new CliStructuredError('5001', 'No project manifest found', {
12
+ domain: 'CLI',
13
+ why: 'No package.json or deno.json found in the target directory. `prisma-next init` requires an existing project to attach to.',
14
+ fix: 'Initialize your project first (e.g. `npm init -y` or `deno init`), then re-run `prisma-next init`.',
15
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
16
+ });
17
+ }
18
+
19
+ /**
20
+ * Re-init in non-interactive mode without `--force`. Distinct from the
21
+ * decline-the-prompt path (which is `errorInitUserAborted`) because here
22
+ * the user was never given the choice — `--force` is the contract.
23
+ */
24
+ export function errorInitReinitNeedsForce(): CliStructuredError {
25
+ return new CliStructuredError('5002', 'Project is already initialized', {
26
+ domain: 'CLI',
27
+ why: 'A `prisma-next.config.ts` already exists in this directory. Re-running `init` would overwrite the scaffolded files; in non-interactive mode `init` will not do that without `--force`.',
28
+ fix: 'Pass `--force` to overwrite the existing scaffold, or run `init` interactively to confirm.',
29
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Non-interactive mode is missing one or more required inputs. Lists every
35
+ * missing flag in the error so an agent / CI script can react without
36
+ * needing to parse English.
37
+ *
38
+ * @param missing — kebab-case flag names without leading dashes
39
+ * @param why — additional context (e.g. "stdin is not a TTY") that helps
40
+ * the user understand why interactive fallback was skipped.
41
+ */
42
+ export function errorInitMissingFlags(options: {
43
+ readonly missing: readonly string[];
44
+ readonly why: string;
45
+ }): CliStructuredError {
46
+ const flagList = options.missing.map((flag) => `--${flag}`).join(', ');
47
+ const fixList = options.missing
48
+ .map((flag) => {
49
+ switch (flag) {
50
+ case 'target':
51
+ return '--target postgres|mongodb';
52
+ case 'authoring':
53
+ return '--authoring psl|typescript';
54
+ case 'schema-path':
55
+ return '--schema-path <path>';
56
+ default:
57
+ return `--${flag} <value>`;
58
+ }
59
+ })
60
+ .join(' ');
61
+ return new CliStructuredError('5003', 'Missing required flags', {
62
+ domain: 'CLI',
63
+ why: `${options.why} Missing required flag(s): ${flagList}.`,
64
+ fix: `Re-run with the missing flag(s) supplied, e.g. \`prisma-next init --yes ${fixList}\`. Use \`prisma-next init --help\` to see every flag.`,
65
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
66
+ meta: { missingFlags: options.missing },
67
+ });
68
+ }
69
+
70
+ /**
71
+ * A flag value was supplied but is not in the allowed set. Lists the
72
+ * allowed values in `meta` for machine-readable consumption.
73
+ */
74
+ export function errorInitInvalidFlagValue(options: {
75
+ readonly flag: string;
76
+ readonly value: string;
77
+ readonly allowed: readonly string[];
78
+ }): CliStructuredError {
79
+ return new CliStructuredError('5004', `Invalid value for --${options.flag}`, {
80
+ domain: 'CLI',
81
+ why: `\`--${options.flag} ${options.value}\` is not one of: ${options.allowed.join(', ')}.`,
82
+ fix: `Use one of: ${options.allowed.map((v) => `--${options.flag} ${v}`).join(', ')}.`,
83
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
84
+ meta: { flag: options.flag, value: options.value, allowed: options.allowed },
85
+ });
86
+ }
87
+
88
+ /**
89
+ * The user cancelled an interactive prompt (Ctrl-C, escape, declined a
90
+ * selection). Distinct from `errorInitReinitNeedsForce` because that path
91
+ * applies to non-interactive mode where the user was never given the
92
+ * choice; this one is the generic "user said no" path. Maps to exit code
93
+ * 3 (USER_ABORTED).
94
+ */
95
+ export function errorInitUserAborted(): CliStructuredError {
96
+ return new CliStructuredError('5006', 'Init cancelled', {
97
+ domain: 'CLI',
98
+ why: 'The interactive prompt was cancelled before all required inputs were supplied. No files were modified.',
99
+ fix: 'Re-run `prisma-next init` and complete the prompts, or pass the required inputs as flags (see `--help`) for a non-interactive run.',
100
+ severity: 'info',
101
+ });
102
+ }
103
+
104
+ /**
105
+ * `--strict-probe` was supplied without `--probe-db`. Per FR8.3 / NFR9
106
+ * (offline-by-default), `--strict-probe` is a no-op without `--probe-db` —
107
+ * but rather than silently ignoring it we tell the user what they probably
108
+ * meant. Without this guard, the flag combination silently does nothing,
109
+ * which is exactly the kind of "looks like it worked" trap that a strict
110
+ * mode is supposed to prevent.
111
+ */
112
+ export function errorInitStrictProbeWithoutProbe(): CliStructuredError {
113
+ return new CliStructuredError('5005', '`--strict-probe` requires `--probe-db`', {
114
+ domain: 'CLI',
115
+ why: '`--strict-probe` only changes how a *failed* probe is reported; without `--probe-db` no probe is attempted in the first place. (`init` is offline-by-default — it never opens a connection to your database without explicit consent.)',
116
+ fix: 'Add `--probe-db` to opt in to the probe, or drop `--strict-probe` if you do not need the version check.',
117
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Dependency installation failed and the pnpm → npm fallback (FR7.2)
123
+ * either did not apply (pm ≠ pnpm or stderr did not match a recognised
124
+ * leak) or also failed. Files scaffolded before the install step are
125
+ * already on disk; `meta.filesWritten` carries the list so a follow-up
126
+ * agent can resume manually. Maps to exit code `4 = INSTALL_FAILED`.
127
+ */
128
+ export function errorInitInstallFailed(options: {
129
+ readonly addCommand: string;
130
+ readonly addDevCommand: string;
131
+ readonly emitCommand: string;
132
+ readonly filesWritten: readonly string[];
133
+ readonly stderrLines: readonly string[];
134
+ }): CliStructuredError {
135
+ const trimmed = options.stderrLines.map((s) => s.trim()).filter(Boolean);
136
+ const why =
137
+ trimmed.length === 0
138
+ ? 'The package manager exited with an error and no recoverable fallback applied.'
139
+ : `The package manager exited with: ${trimmed[0]}`;
140
+ return new CliStructuredError('5007', 'Failed to install dependencies', {
141
+ domain: 'CLI',
142
+ why,
143
+ fix: `Install manually:\n ${options.addCommand}\n ${options.addDevCommand}\nThen run \`${options.emitCommand}\` to emit the contract.`,
144
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
145
+ meta: {
146
+ filesWritten: options.filesWritten,
147
+ stderr: trimmed,
148
+ },
149
+ });
150
+ }
151
+
152
+ /**
153
+ * The user's project manifest (typically `package.json`) failed to parse
154
+ * as JSON. Init reads the manifest to merge `scripts` (FR3.5) and to
155
+ * skip `@types/node` when it is already declared (FR2.1); a malformed
156
+ * file would otherwise surface as an `INTERNAL_ERROR` with a raw
157
+ * `SyntaxError` stack, which violates the FR1.6 contract that every
158
+ * documented failure mode maps to a stable exit code.
159
+ *
160
+ * Maps to exit code `2 = PRECONDITION` — the user can fix the manifest
161
+ * and re-run.
162
+ */
163
+ export function errorInitInvalidManifest(options: {
164
+ readonly path: string;
165
+ readonly cause: string;
166
+ }): CliStructuredError {
167
+ return new CliStructuredError('5010', `Failed to parse ${options.path}`, {
168
+ domain: 'CLI',
169
+ why: `\`${options.path}\` is not valid JSON: ${options.cause}`,
170
+ fix: `Fix the JSON syntax in \`${options.path}\` (a missing comma or unbalanced brace is the most common cause), then re-run \`prisma-next init\`.`,
171
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
172
+ meta: { path: options.path, cause: options.cause },
173
+ });
174
+ }
175
+
176
+ /**
177
+ * The user's existing `tsconfig.json` could not be parsed even with JSONC
178
+ * tolerance (comments + trailing commas) enabled. Init merges the
179
+ * minimum compiler options the scaffolded files need (FR2.2), so an
180
+ * unparseable tsconfig is a hard precondition failure: we cannot
181
+ * faithfully edit a file we cannot read.
182
+ *
183
+ * Init must surface this **before** writing any scaffold file so the
184
+ * user's working tree stays byte-identical (FR6.2 / NFR3) — see
185
+ * `runInit` for the precondition gate.
186
+ *
187
+ * Maps to exit code `2 = PRECONDITION` — the user can fix the file and
188
+ * re-run.
189
+ */
190
+ export function errorInitInvalidTsconfig(options: {
191
+ readonly path: string;
192
+ readonly cause: string;
193
+ }): CliStructuredError {
194
+ return new CliStructuredError('5011', `Failed to parse ${options.path}`, {
195
+ domain: 'CLI',
196
+ why: `\`${options.path}\` is not valid JSON or JSONC: ${options.cause}`,
197
+ fix: `Fix the syntax in \`${options.path}\` and re-run \`prisma-next init\`. \`init\` accepts JSONC (comments and trailing commas) but cannot recover from unbalanced braces or missing commas.`,
198
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
199
+ meta: { path: options.path, cause: options.cause },
200
+ });
201
+ }
202
+
203
+ /**
204
+ * `--probe-db` was supplied along with `--strict-probe` and the probe
205
+ * could not complete (no `DATABASE_URL`, network/auth error, the target
206
+ * driver was not installed, …). Without `--strict-probe` the probe
207
+ * surfaces these as warnings; `--strict-probe` escalates them to
208
+ * fatal so a CI gate can rely on "init exit code 2 means something
209
+ * about the runtime environment is wrong" (FR8.3).
210
+ *
211
+ * Maps to exit code `2 = PRECONDITION`. The caller's project files
212
+ * are already on disk by this point — the probe runs after the write
213
+ * phase — but the install/emit steps may or may not have completed
214
+ * depending on `--no-install` and the exact failure mode; `meta`
215
+ * carries `filesWritten` so a follow-up agent can resume manually.
216
+ */
217
+ export function errorInitProbeFailed(options: {
218
+ readonly cause: string;
219
+ readonly filesWritten: readonly string[];
220
+ }): CliStructuredError {
221
+ return new CliStructuredError('5012', 'Database probe failed', {
222
+ domain: 'CLI',
223
+ why: `\`--probe-db\` could not complete and \`--strict-probe\` was set: ${options.cause}`,
224
+ fix: 'Confirm `DATABASE_URL` points at a reachable server, or drop `--strict-probe` to treat probe failures as warnings.',
225
+ docsUrl: 'https://prisma-next.dev/docs/cli/init',
226
+ meta: {
227
+ filesWritten: options.filesWritten,
228
+ cause: options.cause,
229
+ },
230
+ });
231
+ }
232
+
233
+ /**
234
+ * `prisma-next contract emit` failed after a successful install. Surface
235
+ * the underlying error so the user can fix it and re-run; files and
236
+ * dependencies remain on disk untouched. Maps to exit code
237
+ * `5 = EMIT_FAILED`.
238
+ */
239
+ export function errorInitEmitFailed(options: {
240
+ readonly emitCommand: string;
241
+ readonly filesWritten: readonly string[];
242
+ readonly cause: string;
243
+ }): CliStructuredError {
244
+ return new CliStructuredError('5008', 'Failed to emit contract', {
245
+ domain: 'CLI',
246
+ why: `\`prisma-next contract emit\` failed: ${options.cause}`,
247
+ fix: `Inspect your contract file, fix the underlying issue, then re-run \`${options.emitCommand}\`. Pass \`-v\` for the full error envelope.`,
248
+ docsUrl: 'https://prisma-next.dev/docs/cli/contract-emit',
249
+ meta: {
250
+ filesWritten: options.filesWritten,
251
+ cause: options.cause,
252
+ },
253
+ });
254
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Stable exit codes for the `init` command.
3
+ *
4
+ * These are part of the command's public contract. AI agents and CI scripts
5
+ * branch on them (FR1.6), so the values must remain stable across versions.
6
+ *
7
+ * Codes 0–3 are the CLI-wide reserved values per the [CLI Style Guide
8
+ * Exit Codes section](../../../../../../../docs/CLI%20Style%20Guide.md#exit-codes):
9
+ * `OK = 0`, `INTERNAL_ERROR = 1`, `PRECONDITION = 2`, `USER_ABORTED = 3`.
10
+ * Codes 4 and 5 are command-specific outcomes for `init`'s two fallible
11
+ * side effects (install + emit). Documented in `--help` via
12
+ * `setCommandDescriptions` in `./index.ts`.
13
+ */
14
+
15
+ export const INIT_EXIT_OK = 0;
16
+
17
+ /**
18
+ * Anything we did not anticipate — a bug in prisma-next, not something
19
+ * the caller did wrong. Includes the structured error code `5009`
20
+ * (invalid output document) and any unrecognised internal error code,
21
+ * so callers can distinguish "tool is broken" from "your invocation
22
+ * was wrong" (`PRECONDITION = 2`). Maps to the generic "RUN" error
23
+ * domain.
24
+ */
25
+ export const INIT_EXIT_INTERNAL_ERROR = 1;
26
+
27
+ /**
28
+ * Preconditions not met. The caller asked for something we cannot do
29
+ * without more input or a different environment. Examples:
30
+ * - missing `package.json` / `deno.json`
31
+ * - non-interactive mode without enough flags to proceed
32
+ * - re-init without `--force` in non-interactive mode
33
+ */
34
+ export const INIT_EXIT_PRECONDITION = 2;
35
+
36
+ /**
37
+ * The user actively aborted an interactive prompt (Ctrl-C, declined the
38
+ * re-init confirmation, etc.). Distinct from PRECONDITION because the user
39
+ * was given the choice and made it; no diagnostic is needed.
40
+ */
41
+ export const INIT_EXIT_USER_ABORTED = 3;
42
+
43
+ /**
44
+ * Dependency installation step failed without a recoverable fallback.
45
+ * `init` automatically falls back from `pnpm` to `npm` on a recognised
46
+ * workspace/catalog leak (FR7.2); this code is returned only when the
47
+ * fallback also fails, or when the package manager is not pnpm and the
48
+ * single attempt failed. Files written before the install step (config,
49
+ * schema, db client, etc.) remain on disk so the user can fix the
50
+ * environment and re-run; the error envelope's `meta.filesWritten` lists
51
+ * them.
52
+ */
53
+ export const INIT_EXIT_INSTALL_FAILED = 4;
54
+
55
+ /**
56
+ * Contract emit step failed after a successful install. Files written
57
+ * before emit (including any installed dependencies) are still on disk;
58
+ * the user can fix the underlying issue (typically a contract syntax
59
+ * error or a missing extension pack) and re-run `prisma-next contract
60
+ * emit` manually.
61
+ */
62
+ export const INIT_EXIT_EMIT_FAILED = 5;
@@ -0,0 +1,97 @@
1
+ import type { TargetId } from './templates/code-templates';
2
+
3
+ /**
4
+ * The schema-relative `.gitattributes` entries written for a freshly
5
+ * initialised project (FR3.4). Mirrors the relevant subset of the
6
+ * repo-root [`.gitattributes`](../../../../../../../../.gitattributes):
7
+ *
8
+ * - **Today**: `contract.json`, `contract.d.ts` are emitted on every
9
+ * `prisma-next contract emit`. Marking them `linguist-generated`
10
+ * keeps GitHub's diff stats honest and collapses the file in code
11
+ * review by default.
12
+ * - **Forward-looking**: `end-contract.*`, `start-contract.*`, `ops.json`,
13
+ * `migration.json` are not yet emitted by `init` flows but will be
14
+ * produced by adjacent commands (lower / migration tooling). Adding
15
+ * them now matches Decision 5 (forward-looking subset) so the file
16
+ * does not need to be amended every time a new artefact lands.
17
+ *
18
+ * Patterns are written relative to the schema directory so a user
19
+ * who runs `init --schema-path db/contract.prisma` gets
20
+ * `db/contract.json linguist-generated` — not the workspace-glob form
21
+ * `<glob>/contract.json` (which would over-match any unrelated
22
+ * `contract.json` the user has elsewhere) and not the absolute
23
+ * `prisma/contract.json` (which would silently break for a non-default
24
+ * schema path).
25
+ */
26
+ const ARTEFACT_FILENAMES: readonly string[] = [
27
+ 'contract.json',
28
+ 'contract.d.ts',
29
+ 'end-contract.json',
30
+ 'end-contract.d.ts',
31
+ 'start-contract.json',
32
+ 'start-contract.d.ts',
33
+ 'ops.json',
34
+ 'migration.json',
35
+ ];
36
+
37
+ const ATTRIBUTE = 'linguist-generated';
38
+
39
+ /**
40
+ * Computes the `.gitattributes` lines this scaffold expects to own. Each
41
+ * line has the shape `<path> linguist-generated`. The `target` parameter
42
+ * is currently unused but accepted for symmetry with the other hygiene
43
+ * helpers and to leave room for target-specific entries (e.g. a future
44
+ * Mongo-only artefact) without a signature break.
45
+ */
46
+ export function requiredGitattributesLines(
47
+ schemaDir: string,
48
+ _target: TargetId,
49
+ ): readonly string[] {
50
+ const dir = schemaDir === '.' ? '' : schemaDir.replace(/\/+$/, '');
51
+ const prefix = dir === '' ? '' : `${dir}/`;
52
+ return ARTEFACT_FILENAMES.map((file) => `${prefix}${file} ${ATTRIBUTE}`);
53
+ }
54
+
55
+ /**
56
+ * Idempotent `.gitattributes` merge (FR3.4 / FR9.3). Returns the new file
57
+ * content given the existing content (or `undefined` if the file does
58
+ * not yet exist).
59
+ *
60
+ * Equivalence is exact-line: a user-customised line like
61
+ * `prisma/*.json linguist-generated` is *not* recognised as covering
62
+ * `prisma/contract.json linguist-generated`. We accept that
63
+ * over-specification — preserving the user's broad pattern *and*
64
+ * appending the narrow one — because the narrow lines are what the
65
+ * acceptance criteria pin (FR3.4 AC).
66
+ *
67
+ * Returns `null` when no changes are required (file already contains
68
+ * every required entry).
69
+ */
70
+ export function mergeGitattributes(
71
+ existing: string | undefined,
72
+ required: readonly string[],
73
+ ): string | null {
74
+ if (existing === undefined) {
75
+ return `${required.join('\n')}\n`;
76
+ }
77
+
78
+ const presentLines = new Set(
79
+ existing
80
+ .split('\n')
81
+ .map((line) => line.trim())
82
+ .filter((line) => line.length > 0 && !line.startsWith('#')),
83
+ );
84
+
85
+ const missing = required.filter((line) => !presentLines.has(line));
86
+ if (missing.length === 0) {
87
+ return null;
88
+ }
89
+
90
+ // Mirrors `mergeGitignore`: a zero-byte existing file would otherwise
91
+ // gain a leading blank line, because `''.endsWith('\n')` is false. The
92
+ // empty-file case is uncommon (most projects either don't have a
93
+ // `.gitattributes` or have one with content), but symmetric handling
94
+ // keeps the two mergers' invariants identical.
95
+ const separator = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
96
+ return `${existing}${separator}${missing.join('\n')}\n`;
97
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The minimal `.gitignore` lines a Prisma Next scaffold needs (FR3.3).
3
+ * Order matches what Node tooling typically writes today.
4
+ *
5
+ * `node_modules/` first because it's the byte-largest miss; `dist/`
6
+ * because the scaffolded `tsconfig.json` writes there; `.env` last so
7
+ * the secret-bearing file is the one most-recently visible in any diff
8
+ * (a paranoid-correct ordering — humans skim from the top).
9
+ */
10
+ export const REQUIRED_GITIGNORE_ENTRIES: readonly string[] = ['node_modules/', 'dist/', '.env'];
11
+
12
+ /**
13
+ * Idempotent `.gitignore` merge (FR3.3 / FR9.3). Returns the new file
14
+ * content given the existing content (or `undefined` if the file does
15
+ * not yet exist). Adds only entries that are not already present and
16
+ * never duplicates a line. Existing comments and blank lines are
17
+ * preserved verbatim — `.gitignore` is parsed by `git` without a tree,
18
+ * so any line modification risks changing semantics.
19
+ *
20
+ * Pattern equivalence is line-literal: `node_modules/` and `node_modules`
21
+ * are treated as different entries. This is intentional — `git` treats
22
+ * them differently (the trailing slash restricts the match to
23
+ * directories), and the AC pins the trailing-slash form.
24
+ *
25
+ * Returns `null` when no changes are required (file already contains
26
+ * every required entry). The caller can use this to decide whether to
27
+ * include `.gitignore` in `filesWritten`.
28
+ */
29
+ export function mergeGitignore(existing: string | undefined): string | null {
30
+ if (existing === undefined) {
31
+ return `${REQUIRED_GITIGNORE_ENTRIES.join('\n')}\n`;
32
+ }
33
+
34
+ const present = new Set(
35
+ existing
36
+ .split('\n')
37
+ .map((line) => line.trim())
38
+ .filter((line) => line.length > 0 && !line.startsWith('#')),
39
+ );
40
+
41
+ const missing = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !present.has(entry));
42
+ if (missing.length === 0) {
43
+ return null;
44
+ }
45
+
46
+ const separator = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
47
+ return `${existing}${separator}${missing.join('\n')}\n`;
48
+ }