@prisma-next/cli 0.5.0-dev.3 → 0.5.0-dev.5

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 (107) hide show
  1. package/dist/agent-skill-mongo.md +63 -31
  2. package/dist/agent-skill-postgres.md +1 -1
  3. package/dist/cli.mjs +119 -13
  4. package/dist/cli.mjs.map +1 -1
  5. package/dist/{client-TG7rbCWT.mjs → client-CrsnY58k.mjs} +4 -4
  6. package/dist/{client-TG7rbCWT.mjs.map → client-CrsnY58k.mjs.map} +1 -1
  7. package/dist/commands/contract-emit.mjs +2 -2
  8. package/dist/commands/contract-infer.mjs +2 -2
  9. package/dist/commands/db-init.mjs +7 -7
  10. package/dist/commands/db-schema.mjs +5 -5
  11. package/dist/commands/db-sign.mjs +7 -7
  12. package/dist/commands/db-update.mjs +7 -7
  13. package/dist/commands/db-verify.mjs +7 -7
  14. package/dist/commands/migration-apply.mjs +8 -8
  15. package/dist/commands/migration-apply.mjs.map +1 -1
  16. package/dist/commands/migration-new.mjs +5 -5
  17. package/dist/commands/migration-plan.mjs +6 -6
  18. package/dist/commands/migration-ref.d.mts +6 -4
  19. package/dist/commands/migration-ref.d.mts.map +1 -1
  20. package/dist/commands/migration-ref.mjs +29 -34
  21. package/dist/commands/migration-ref.mjs.map +1 -1
  22. package/dist/commands/migration-show.d.mts +1 -1
  23. package/dist/commands/migration-show.mjs +6 -6
  24. package/dist/commands/migration-status.d.mts.map +1 -1
  25. package/dist/commands/migration-status.mjs +2 -2
  26. package/dist/{config-loader-_W4T21X1.mjs → config-loader-C25b63rJ.mjs} +1 -1
  27. package/dist/{config-loader-_W4T21X1.mjs.map → config-loader-C25b63rJ.mjs.map} +1 -1
  28. package/dist/config-loader.mjs +1 -1
  29. package/dist/contract-emit--feXyNd7.mjs +4 -0
  30. package/dist/{contract-emit-CNYyzJwF.mjs → contract-emit-NJ01hiiv.mjs} +8 -8
  31. package/dist/{contract-emit-CNYyzJwF.mjs.map → contract-emit-NJ01hiiv.mjs.map} +1 -1
  32. package/dist/{contract-emit-CQfj7xJn.mjs → contract-emit-V5SSitUT.mjs} +6 -6
  33. package/dist/{contract-emit-CQfj7xJn.mjs.map → contract-emit-V5SSitUT.mjs.map} +1 -1
  34. package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-CAOELa-H.mjs} +1 -1
  35. package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-CAOELa-H.mjs.map} +1 -1
  36. package/dist/{contract-infer-BP3DrGgz.mjs → contract-infer-D9cC3rJm.mjs} +4 -4
  37. package/dist/{contract-infer-BP3DrGgz.mjs.map → contract-infer-D9cC3rJm.mjs.map} +1 -1
  38. package/dist/exports/control-api.mjs +4 -4
  39. package/dist/exports/index.mjs +2 -2
  40. package/dist/exports/init-output.d.mts +39 -0
  41. package/dist/exports/init-output.d.mts.map +1 -0
  42. package/dist/exports/init-output.mjs +3 -0
  43. package/dist/{extract-operation-statements-DZUJNmL3.mjs → extract-operation-statements-DsFfxXVZ.mjs} +2 -2
  44. package/dist/{extract-operation-statements-DZUJNmL3.mjs.map → extract-operation-statements-DsFfxXVZ.mjs.map} +1 -1
  45. package/dist/{extract-sql-ddl-DDMX-9mz.mjs → extract-sql-ddl-D9UbZDyz.mjs} +1 -1
  46. package/dist/{extract-sql-ddl-DDMX-9mz.mjs.map → extract-sql-ddl-D9UbZDyz.mjs.map} +1 -1
  47. package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Cr--XBKy.mjs} +2 -2
  48. package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Cr--XBKy.mjs.map} +1 -1
  49. package/dist/init-C5220SY9.mjs +2062 -0
  50. package/dist/init-C5220SY9.mjs.map +1 -0
  51. package/dist/{inspect-live-schema-DWzf4Q_m.mjs → inspect-live-schema-yrHAvG71.mjs} +6 -6
  52. package/dist/{inspect-live-schema-DWzf4Q_m.mjs.map → inspect-live-schema-yrHAvG71.mjs.map} +1 -1
  53. package/dist/migration-cli.mjs +1 -1
  54. package/dist/{migration-command-scaffold-CLMD302g.mjs → migration-command-scaffold-B3B09et6.mjs} +6 -6
  55. package/dist/{migration-command-scaffold-CLMD302g.mjs.map → migration-command-scaffold-B3B09et6.mjs.map} +1 -1
  56. package/dist/{migration-status-B0HLF7So.mjs → migration-status-DUMiH8_G.mjs} +12 -14
  57. package/dist/{migration-status-B0HLF7So.mjs.map → migration-status-DUMiH8_G.mjs.map} +1 -1
  58. package/dist/{migrations-B0dOQlk0.mjs → migrations-Bo5WtTla.mjs} +2 -2
  59. package/dist/{migrations-B0dOQlk0.mjs.map → migrations-Bo5WtTla.mjs.map} +1 -1
  60. package/dist/output-BpcQrnnq.mjs +103 -0
  61. package/dist/output-BpcQrnnq.mjs.map +1 -0
  62. package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DvQWB1nK.mjs} +1 -1
  63. package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DvQWB1nK.mjs.map} +1 -1
  64. package/dist/quick-reference-mongo.md +34 -13
  65. package/dist/quick-reference-postgres.md +11 -9
  66. package/dist/{result-handler-CIyu0Pdt.mjs → result-handler-Ba3zWQsI.mjs} +5 -78
  67. package/dist/result-handler-Ba3zWQsI.mjs.map +1 -0
  68. package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-C3ZLwQxK.mjs} +76 -2
  69. package/dist/terminal-ui-C3ZLwQxK.mjs.map +1 -0
  70. package/dist/{validate-contract-deps-esa-VQ0h.mjs → validate-contract-deps-B_Cs29TL.mjs} +1 -1
  71. package/dist/{validate-contract-deps-esa-VQ0h.mjs.map → validate-contract-deps-B_Cs29TL.mjs.map} +1 -1
  72. package/dist/{verify-BxiVp50b.mjs → verify-Bkycc-Tf.mjs} +2 -2
  73. package/dist/{verify-BxiVp50b.mjs.map → verify-Bkycc-Tf.mjs.map} +1 -1
  74. package/package.json +21 -16
  75. package/src/commands/init/detect-pnpm-catalog.ts +141 -0
  76. package/src/commands/init/errors.ts +254 -0
  77. package/src/commands/init/exit-codes.ts +62 -0
  78. package/src/commands/init/hygiene-gitattributes.ts +97 -0
  79. package/src/commands/init/hygiene-gitignore.ts +48 -0
  80. package/src/commands/init/hygiene-package-scripts.ts +91 -0
  81. package/src/commands/init/index.ts +112 -7
  82. package/src/commands/init/init.ts +766 -144
  83. package/src/commands/init/inputs.ts +421 -0
  84. package/src/commands/init/output.ts +147 -0
  85. package/src/commands/init/probe-db.ts +308 -0
  86. package/src/commands/init/reinit-cleanup.ts +83 -0
  87. package/src/commands/init/templates/agent-skill-mongo.md +63 -31
  88. package/src/commands/init/templates/agent-skill-postgres.md +1 -1
  89. package/src/commands/init/templates/agent-skill.ts +25 -3
  90. package/src/commands/init/templates/code-templates.ts +125 -32
  91. package/src/commands/init/templates/env.ts +80 -0
  92. package/src/commands/init/templates/quick-reference-mongo.md +34 -13
  93. package/src/commands/init/templates/quick-reference-postgres.md +11 -9
  94. package/src/commands/init/templates/quick-reference.ts +42 -3
  95. package/src/commands/init/templates/tsconfig.ts +167 -5
  96. package/src/commands/migration-apply.ts +3 -3
  97. package/src/commands/migration-ref.ts +32 -47
  98. package/src/commands/migration-status.ts +16 -21
  99. package/src/exports/init-output.ts +10 -0
  100. package/src/utils/command-helpers.ts +3 -3
  101. package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
  102. package/dist/init-CQfo_4Ro.mjs +0 -430
  103. package/dist/init-CQfo_4Ro.mjs.map +0 -1
  104. package/dist/result-handler-CIyu0Pdt.mjs.map +0 -1
  105. package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
  106. /package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-BFYgBH3L.d.mts} +0 -0
  107. /package/dist/{cli-errors-DHq6GQGu.mjs → cli-errors-Cd79vmTH.mjs} +0 -0
@@ -0,0 +1,421 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import * as clack from '@clack/prompts';
3
+ import { extname, join, normalize } from 'pathe';
4
+ import type { GlobalFlags } from '../../utils/global-flags';
5
+ import {
6
+ errorInitInvalidFlagValue,
7
+ errorInitMissingFlags,
8
+ errorInitReinitNeedsForce,
9
+ errorInitStrictProbeWithoutProbe,
10
+ errorInitUserAborted,
11
+ } from './errors';
12
+ import {
13
+ type AuthoringId,
14
+ defaultSchemaPath,
15
+ type TargetId,
16
+ targetLabel,
17
+ targetPackageName,
18
+ } from './templates/code-templates';
19
+
20
+ /**
21
+ * Raw command-line input as Commander.js parses it. `target` here uses the
22
+ * user-facing `mongodb` spelling (matching the flag); the internal
23
+ * `TargetId` uses `mongo`. The mapping happens in `resolveInitInputs`.
24
+ */
25
+ export interface InitFlagOptions {
26
+ readonly target?: string;
27
+ readonly authoring?: string;
28
+ readonly schemaPath?: string;
29
+ readonly force?: boolean;
30
+ readonly writeEnv?: boolean;
31
+ readonly probeDb?: boolean;
32
+ readonly strictProbe?: boolean;
33
+ readonly install?: boolean;
34
+ }
35
+
36
+ /**
37
+ * The fully-resolved set of decisions `runInit` operates on. After this
38
+ * value object is constructed, `runInit` should not need to consult the
39
+ * environment again for any user-visible decision.
40
+ */
41
+ export interface ResolvedInitInputs {
42
+ readonly target: TargetId;
43
+ readonly authoring: AuthoringId;
44
+ readonly schemaPath: string;
45
+ readonly install: boolean;
46
+ readonly writeEnv: boolean;
47
+ readonly probeDb: boolean;
48
+ readonly strictProbe: boolean;
49
+ /**
50
+ * True if the project already has `prisma-next.config.ts` and the user
51
+ * has agreed (or `--force` has been supplied) to overwrite it.
52
+ */
53
+ readonly reinit: boolean;
54
+ /**
55
+ * FR9.2 — set to the **previous** facade package name (e.g.
56
+ * `@prisma-next/postgres`) when re-init is switching targets and the
57
+ * user has consented to remove it from `package.json#dependencies`.
58
+ * `null` when no removal is needed: not a re-init, no previous facade
59
+ * present, the previous facade matches the chosen target, or the user
60
+ * declined the interactive confirm. The chosen-target facade itself
61
+ * is added separately via the install step.
62
+ */
63
+ readonly removePreviousFacade: string | null;
64
+ }
65
+
66
+ const TARGET_ALIASES: ReadonlyMap<string, TargetId> = new Map([
67
+ ['postgres', 'postgres'],
68
+ ['postgresql', 'postgres'],
69
+ ['mongo', 'mongo'],
70
+ ['mongodb', 'mongo'],
71
+ ]);
72
+
73
+ const AUTHORING_VALUES: ReadonlyMap<string, AuthoringId> = new Map([
74
+ ['psl', 'psl'],
75
+ ['typescript', 'typescript'],
76
+ ['ts', 'typescript'],
77
+ ]);
78
+
79
+ /**
80
+ * Resolves every required input for `runInit`. In interactive mode, missing
81
+ * inputs are prompted via clack; in non-interactive mode, missing required
82
+ * inputs throw a structured error listing exactly which flags are missing
83
+ * (FR1.4). Throws `CliStructuredError` on any unrecoverable input issue.
84
+ *
85
+ * `canPrompt` is decoupled from `flags.interactive` so the action handler
86
+ * (`./index.ts`) owns the merge of stdout-TTY (decoration) and stdin-TTY
87
+ * (prompts). `flags.interactive` continues to gate `TerminalUI` decoration
88
+ * — see [Style Guide § Interactivity](../../../../../../../docs/CLI%20Style%20Guide.md#interactivity).
89
+ */
90
+ export async function resolveInitInputs(ctx: {
91
+ readonly baseDir: string;
92
+ readonly options: InitFlagOptions;
93
+ readonly flags: GlobalFlags;
94
+ readonly canPrompt: boolean;
95
+ }): Promise<ResolvedInitInputs> {
96
+ const { baseDir, options, flags, canPrompt } = ctx;
97
+ // `--force` and `--yes` are deliberately separate: `--force` is the
98
+ // contract for "overwrite an existing scaffold" (works in both modes);
99
+ // `--yes` only auto-accepts interactive prompts and never substitutes
100
+ // for the explicit destructive opt-in. In non-interactive mode, `--yes`
101
+ // alone does nothing useful; the user must supply `--target`,
102
+ // `--authoring`, and (for re-init) `--force`.
103
+ const force = Boolean(options.force);
104
+ const autoAcceptPrompts = Boolean(flags.yes);
105
+
106
+ // --strict-probe is a no-op without --probe-db; surface the mistake
107
+ // rather than silently swallowing it (FR8.3 / NFR9).
108
+ if (options.strictProbe && !options.probeDb) {
109
+ throw errorInitStrictProbeWithoutProbe();
110
+ }
111
+
112
+ const reinit = await resolveReinit({ baseDir, force, canPrompt, autoAcceptPrompts });
113
+ const target = resolveTarget(options.target);
114
+ const authoring = resolveAuthoring(options.authoring);
115
+
116
+ // Now collect what's still missing under non-interactive rules.
117
+ const missing: string[] = [];
118
+ if (target === undefined) missing.push('target');
119
+ if (authoring === undefined) missing.push('authoring');
120
+
121
+ if (!canPrompt && missing.length > 0) {
122
+ const reason = process.stdin.isTTY
123
+ ? 'Non-interactive mode is active (`--no-interactive` or stdout is piped).'
124
+ : 'stdin is not a TTY, so `init` cannot prompt interactively.';
125
+ throw errorInitMissingFlags({ missing, why: reason });
126
+ }
127
+
128
+ // Interactive path — fall back to clack for anything still missing.
129
+ const finalTarget = target ?? (await promptTarget());
130
+ const finalAuthoring = authoring ?? (await promptAuthoring());
131
+ const finalSchemaPath =
132
+ options.schemaPath !== undefined
133
+ ? validateSchemaPath(options.schemaPath, finalAuthoring)
134
+ : canPrompt
135
+ ? await promptSchemaPath(finalAuthoring)
136
+ : defaultSchemaPath(finalAuthoring);
137
+
138
+ // FR3.2: `--write-env` is the explicit opt-in for non-interactive
139
+ // mode. Interactive runs additionally get a single confirm — but only
140
+ // when the flag was not already supplied (an explicit `--write-env`
141
+ // suppresses the prompt) and `--yes` did not auto-accept everything
142
+ // (in which case interactive mode is effectively non-interactive and
143
+ // the flag-only contract applies). See Style Guide § Interactivity.
144
+ const writeEnv = await resolveWriteEnv({
145
+ flag: options.writeEnv,
146
+ canPrompt,
147
+ autoAcceptPrompts,
148
+ });
149
+
150
+ // FR9.2 — when re-init switches targets, ask whether to drop the
151
+ // previous facade from `dependencies`. Detection happens here (not in
152
+ // `runInit`) so the prompt sequence stays in one place; the actual
153
+ // edit is applied during `runInit`'s precondition phase alongside the
154
+ // other `package.json` merges.
155
+ const removePreviousFacade = await resolveRemovePreviousFacade({
156
+ baseDir,
157
+ target: finalTarget,
158
+ reinit,
159
+ force,
160
+ canPrompt,
161
+ autoAcceptPrompts,
162
+ });
163
+
164
+ return {
165
+ target: finalTarget,
166
+ authoring: finalAuthoring,
167
+ schemaPath: finalSchemaPath,
168
+ install: options.install !== false,
169
+ writeEnv,
170
+ probeDb: Boolean(options.probeDb),
171
+ strictProbe: Boolean(options.strictProbe),
172
+ reinit,
173
+ removePreviousFacade,
174
+ };
175
+ }
176
+
177
+ async function resolveWriteEnv(opts: {
178
+ readonly flag: boolean | undefined;
179
+ readonly canPrompt: boolean;
180
+ readonly autoAcceptPrompts: boolean;
181
+ }): Promise<boolean> {
182
+ if (opts.flag !== undefined) {
183
+ return Boolean(opts.flag);
184
+ }
185
+ if (!opts.canPrompt || opts.autoAcceptPrompts) {
186
+ return false;
187
+ }
188
+ const result = await clack.confirm({
189
+ message: 'Also write a .env file from .env.example? (gitignored)',
190
+ initialValue: false,
191
+ output: process.stderr,
192
+ });
193
+ if (clack.isCancel(result)) {
194
+ throw errorInitUserAborted();
195
+ }
196
+ return Boolean(result);
197
+ }
198
+
199
+ /**
200
+ * FR9.2 — detects whether re-init is switching targets (the previous
201
+ * facade differs from the chosen target's facade) and resolves the
202
+ * remove-or-keep question.
203
+ *
204
+ * The non-interactive contract is the same as the `--force` re-init
205
+ * gate above: a non-interactive run that reaches this helper always
206
+ * has `--force` (otherwise `resolveReinit` would have thrown 5002), so
207
+ * the removal proceeds without further prompting. Interactive runs see
208
+ * a `clack.confirm` with `initialValue: true` — the destructive default
209
+ * is correct because keeping both facades produces a project that
210
+ * imports from one but pays for both in the lockfile, which is a
211
+ * silent foot-gun the user almost never wants.
212
+ *
213
+ * Returns the previous facade package name when the user consented (or
214
+ * was force-ed) to remove it, otherwise `null`. Parse failures on
215
+ * `package.json` resolve to `null` here — `runInit`'s precondition
216
+ * gate surfaces a structured 5010 error for the same file shortly
217
+ * after, so we avoid double-reporting and keep this helper side-effect
218
+ * free under hostile inputs.
219
+ */
220
+ async function resolveRemovePreviousFacade(opts: {
221
+ readonly baseDir: string;
222
+ readonly target: TargetId;
223
+ readonly reinit: boolean;
224
+ readonly force: boolean;
225
+ readonly canPrompt: boolean;
226
+ readonly autoAcceptPrompts: boolean;
227
+ }): Promise<string | null> {
228
+ if (!opts.reinit) {
229
+ return null;
230
+ }
231
+ const packageJsonPath = join(opts.baseDir, 'package.json');
232
+ if (!existsSync(packageJsonPath)) {
233
+ return null;
234
+ }
235
+ const otherTarget: TargetId = opts.target === 'postgres' ? 'mongo' : 'postgres';
236
+ const otherFacade = targetPackageName(otherTarget);
237
+ let parsed: Record<string, unknown>;
238
+ try {
239
+ parsed = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as Record<string, unknown>;
240
+ } catch {
241
+ return null;
242
+ }
243
+ const deps = parsed['dependencies'];
244
+ if (deps === null || typeof deps !== 'object' || Array.isArray(deps)) {
245
+ return null;
246
+ }
247
+ if (!Object.hasOwn(deps as Record<string, unknown>, otherFacade)) {
248
+ return null;
249
+ }
250
+
251
+ // `--force` (and `--yes` in interactive mode) auto-confirms the
252
+ // removal. The `!canPrompt` branch is unreachable in practice because
253
+ // the FR9.0 reinit gate already required `--force` for non-interactive
254
+ // re-init, but we keep the guard for defence-in-depth.
255
+ if (opts.force || (opts.canPrompt && opts.autoAcceptPrompts)) {
256
+ return otherFacade;
257
+ }
258
+ if (!opts.canPrompt) {
259
+ return otherFacade;
260
+ }
261
+ const result = await clack.confirm({
262
+ message: `Switching from ${targetLabel(otherTarget)} to ${targetLabel(opts.target)} — remove ${otherFacade} from package.json dependencies?`,
263
+ initialValue: true,
264
+ output: process.stderr,
265
+ });
266
+ if (clack.isCancel(result)) {
267
+ throw errorInitUserAborted();
268
+ }
269
+ return result === true ? otherFacade : null;
270
+ }
271
+
272
+ async function resolveReinit(opts: {
273
+ readonly baseDir: string;
274
+ readonly force: boolean;
275
+ readonly canPrompt: boolean;
276
+ readonly autoAcceptPrompts: boolean;
277
+ }): Promise<boolean> {
278
+ const configPath = join(opts.baseDir, 'prisma-next.config.ts');
279
+ if (!existsSync(configPath)) {
280
+ return false;
281
+ }
282
+ if (opts.force) {
283
+ return true;
284
+ }
285
+ if (!opts.canPrompt) {
286
+ throw errorInitReinitNeedsForce();
287
+ }
288
+ // In interactive mode, `--yes` auto-accepts the re-init confirm.
289
+ if (opts.autoAcceptPrompts) {
290
+ return true;
291
+ }
292
+ const result = await clack.confirm({
293
+ message:
294
+ 'This project is already initialized. Re-initialize? This will overwrite all generated files.',
295
+ initialValue: false,
296
+ output: process.stderr,
297
+ });
298
+ if (clack.isCancel(result) || result !== true) {
299
+ throw errorInitUserAborted();
300
+ }
301
+ return true;
302
+ }
303
+
304
+ function resolveTarget(value: string | undefined): TargetId | undefined {
305
+ if (value === undefined) return undefined;
306
+ const mapped = TARGET_ALIASES.get(value.toLowerCase());
307
+ if (mapped === undefined) {
308
+ throw errorInitInvalidFlagValue({
309
+ flag: 'target',
310
+ value,
311
+ allowed: ['postgres', 'mongodb'],
312
+ });
313
+ }
314
+ return mapped;
315
+ }
316
+
317
+ function resolveAuthoring(value: string | undefined): AuthoringId | undefined {
318
+ if (value === undefined) return undefined;
319
+ const mapped = AUTHORING_VALUES.get(value.toLowerCase());
320
+ if (mapped === undefined) {
321
+ throw errorInitInvalidFlagValue({
322
+ flag: 'authoring',
323
+ value,
324
+ allowed: ['psl', 'typescript'],
325
+ });
326
+ }
327
+ return mapped;
328
+ }
329
+
330
+ /**
331
+ * Validates `--schema-path` against the chosen `--authoring` style: PSL
332
+ * authoring requires a `.prisma` file and TypeScript authoring requires a
333
+ * `.ts` file. Mismatched combinations would silently scaffold PSL content
334
+ * into a `.ts` file (or vice versa); this validator surfaces the mistake
335
+ * as a precondition error naming both flags.
336
+ */
337
+ function validateSchemaPath(value: string, authoring: AuthoringId): string {
338
+ const trimmed = value.trim();
339
+ if (trimmed.length === 0) {
340
+ throw errorInitInvalidFlagValue({
341
+ flag: 'schema-path',
342
+ value,
343
+ allowed: ['<non-empty file path with .prisma or .ts extension>'],
344
+ });
345
+ }
346
+ if (trimmed.endsWith('/') || trimmed.endsWith('\\')) {
347
+ throw errorInitInvalidFlagValue({
348
+ flag: 'schema-path',
349
+ value,
350
+ allowed: ['<file path, not a directory>'],
351
+ });
352
+ }
353
+ const ext = extname(trimmed).toLowerCase();
354
+ const expected = authoring === 'typescript' ? '.ts' : '.prisma';
355
+ if (ext !== expected) {
356
+ throw errorInitInvalidFlagValue({
357
+ flag: 'schema-path',
358
+ value,
359
+ allowed: [`<file path ending in ${expected} for --authoring ${authoring}>`],
360
+ });
361
+ }
362
+ return normalize(trimmed);
363
+ }
364
+
365
+ async function promptTarget(): Promise<TargetId> {
366
+ const result = await clack.select({
367
+ message: 'What database are you using?',
368
+ options: [
369
+ { value: 'postgres' as TargetId, label: 'PostgreSQL' },
370
+ { value: 'mongo' as TargetId, label: 'MongoDB' },
371
+ ],
372
+ output: process.stderr,
373
+ });
374
+ if (clack.isCancel(result)) {
375
+ throw errorInitUserAborted();
376
+ }
377
+ return result as TargetId;
378
+ }
379
+
380
+ async function promptAuthoring(): Promise<AuthoringId> {
381
+ const result = await clack.select({
382
+ message: 'How do you want to write your schema?',
383
+ options: [
384
+ { value: 'psl' as AuthoringId, label: 'Prisma Schema Language (.prisma)' },
385
+ { value: 'typescript' as AuthoringId, label: 'TypeScript (.ts)' },
386
+ ],
387
+ output: process.stderr,
388
+ });
389
+ if (clack.isCancel(result)) {
390
+ throw errorInitUserAborted();
391
+ }
392
+ return result as AuthoringId;
393
+ }
394
+
395
+ async function promptSchemaPath(authoring: AuthoringId): Promise<string> {
396
+ const expectedExt = authoring === 'typescript' ? '.ts' : '.prisma';
397
+ const result = await clack.text({
398
+ message: 'Where should the schema file go?',
399
+ initialValue: defaultSchemaPath(authoring),
400
+ validate(value = '') {
401
+ const trimmed = value.trim();
402
+ if (trimmed.length === 0) return 'Path cannot be empty';
403
+ if (trimmed.endsWith('/') || trimmed.endsWith('\\'))
404
+ return 'Path must be a file, not a directory';
405
+ const ext = extname(trimmed).toLowerCase();
406
+ if (ext === '') return 'Path must include a file extension (e.g. .prisma or .ts)';
407
+ if (ext !== expectedExt) {
408
+ return `Schema path must end in ${expectedExt} for --authoring ${authoring} (got ${ext}).`;
409
+ }
410
+ return undefined;
411
+ },
412
+ output: process.stderr,
413
+ });
414
+ if (clack.isCancel(result)) {
415
+ throw errorInitUserAborted();
416
+ }
417
+ // Pipe through `validateSchemaPath` so the final value goes through the
418
+ // same canonicalisation as the flag path — defence-in-depth in case
419
+ // the inline `validate` ever drifts from the flag-mode rules.
420
+ return validateSchemaPath(result as string, authoring);
421
+ }
@@ -0,0 +1,147 @@
1
+ import { type } from 'arktype';
2
+ import type { GlobalFlags } from '../../utils/global-flags';
3
+ import type { TerminalUI } from '../../utils/terminal-ui';
4
+
5
+ /**
6
+ * arktype schema for the structured success document `init --json` writes
7
+ * to stdout (FR1.5). The same shape backs the human-readable outro
8
+ * renderer (FR10), so the two output modes carry identical information.
9
+ *
10
+ * `target` is normalised to the user-facing flag value (`mongodb` rather
11
+ * than the internal `mongo`) so consumers can round-trip the document
12
+ * straight into a follow-up `--target` invocation.
13
+ *
14
+ * The `ok: true` literal is the documented success/error discriminator —
15
+ * see [Style Guide § JSON Semantics](../../../../../../../docs/CLI%20Style%20Guide.md#json-semantics).
16
+ * Error envelopes (`CliErrorEnvelope`) carry `ok: false` so consumers can
17
+ * branch with `if (doc.ok)` without inspecting the rest of the structure.
18
+ */
19
+ export const InitOutputSchema = type({
20
+ ok: 'true',
21
+ target: "'postgres'|'mongodb'",
22
+ authoring: "'psl'|'typescript'",
23
+ schemaPath: 'string',
24
+ filesWritten: 'string[]',
25
+ /**
26
+ * FR9.1 — files removed from disk during this run. Populated only on
27
+ * re-init when previously-emitted contract artefacts (`contract.json`,
28
+ * `contract.d.ts`, `start-/end-contract.*`, `ops.json`,
29
+ * `migration.json`) were left behind by an earlier run. Empty on a
30
+ * green-field init.
31
+ */
32
+ filesDeleted: 'string[]',
33
+ packagesInstalled: {
34
+ skipped: 'boolean',
35
+ deps: 'string[]',
36
+ devDeps: 'string[]',
37
+ },
38
+ contractEmitted: 'boolean',
39
+ nextSteps: 'string[]',
40
+ warnings: 'string[]',
41
+ });
42
+
43
+ export type InitOutput = typeof InitOutputSchema.infer;
44
+
45
+ /**
46
+ * Serialises the output document for `--json`. Sorted keys are not enforced
47
+ * — `JSON.stringify` preserves insertion order, and the schema field order
48
+ * is the documented order, which matches what users will see when they
49
+ * `jq .` the result.
50
+ */
51
+ export function formatInitJson(output: InitOutput): string {
52
+ return JSON.stringify(output, null, 2);
53
+ }
54
+
55
+ /**
56
+ * Renders the human-readable outro on stderr (FR10.1). Re-uses the same
57
+ * data structure as the JSON output so the two stay in lock-step.
58
+ *
59
+ * Warnings come before "Next steps" because they describe state the user
60
+ * needs to be aware of before acting on the next-steps list.
61
+ */
62
+ export function renderInitOutro(ui: TerminalUI, output: InitOutput, flags: GlobalFlags): void {
63
+ if (flags.quiet || flags.json) {
64
+ return;
65
+ }
66
+
67
+ for (const warning of output.warnings) {
68
+ ui.warn(warning);
69
+ }
70
+
71
+ const lines: string[] = [];
72
+ lines.push(`Target: ${output.target}`);
73
+ lines.push(`Authoring: ${output.authoring}`);
74
+ lines.push(`Schema: ${output.schemaPath}`);
75
+ lines.push('');
76
+ lines.push('Files written:');
77
+ for (const file of output.filesWritten) {
78
+ lines.push(` • ${file}`);
79
+ }
80
+
81
+ if (output.filesDeleted.length > 0) {
82
+ lines.push('');
83
+ lines.push('Files deleted (stale contract artefacts):');
84
+ for (const file of output.filesDeleted) {
85
+ lines.push(` • ${file}`);
86
+ }
87
+ }
88
+
89
+ if (!output.packagesInstalled.skipped) {
90
+ lines.push('');
91
+ lines.push('Packages installed:');
92
+ for (const dep of output.packagesInstalled.deps) {
93
+ lines.push(` • ${dep}`);
94
+ }
95
+ for (const dep of output.packagesInstalled.devDeps) {
96
+ lines.push(` • ${dep} (dev)`);
97
+ }
98
+ }
99
+
100
+ lines.push('');
101
+ lines.push('Next steps:');
102
+ for (const step of output.nextSteps) {
103
+ lines.push(` ${step}`);
104
+ }
105
+
106
+ ui.note(lines.join('\n'), 'Done');
107
+ }
108
+
109
+ /**
110
+ * Builds the `nextSteps` array from the resolved scaffold state. Steps are
111
+ * ordered by the workflow a user needs to follow: configure connection →
112
+ * (emit if not yet done) → run a starter query → docs / agent skill.
113
+ *
114
+ * The strings are stable and human-readable; agents wanting to act on them
115
+ * should match on substrings (e.g. "DATABASE_URL") rather than exact text,
116
+ * since copy may evolve.
117
+ */
118
+ export function buildNextSteps(options: {
119
+ readonly target: 'postgres' | 'mongodb';
120
+ readonly contractEmitted: boolean;
121
+ readonly emitCommand: string;
122
+ readonly schemaPath: string;
123
+ }): string[] {
124
+ const steps: string[] = [];
125
+ steps.push('1. Set DATABASE_URL in your environment (export it or add it to .env).');
126
+ if (!options.contractEmitted) {
127
+ steps.push(`2. Emit the contract: \`${options.emitCommand}\``);
128
+ steps.push(`3. Edit your schema at ${options.schemaPath}, then re-run the emit command.`);
129
+ steps.push(
130
+ '4. Open prisma-next.md for a quick reference on how to write your first typed query.',
131
+ );
132
+ steps.push(
133
+ '5. The .agents/skills/prisma-next/SKILL.md file is wired up for AI-coding agents in this project.',
134
+ );
135
+ } else {
136
+ steps.push(
137
+ `2. Edit your schema at ${options.schemaPath}, then re-run \`${options.emitCommand}\`.`,
138
+ );
139
+ steps.push(
140
+ '3. Open prisma-next.md for a quick reference on how to write your first typed query.',
141
+ );
142
+ steps.push(
143
+ '4. The .agents/skills/prisma-next/SKILL.md file is wired up for AI-coding agents in this project.',
144
+ );
145
+ }
146
+ return steps;
147
+ }