@prisma/cli 3.0.0-beta.0 → 3.0.0-beta.10

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 (65) hide show
  1. package/README.md +18 -3
  2. package/dist/adapters/git.js +8 -3
  3. package/dist/adapters/local-state.js +12 -4
  4. package/dist/adapters/mock-api.js +81 -2
  5. package/dist/adapters/token-storage.js +63 -22
  6. package/dist/cli.js +17 -2
  7. package/dist/cli2.js +7 -3
  8. package/dist/commands/app/index.js +26 -13
  9. package/dist/commands/branch/index.js +2 -27
  10. package/dist/commands/database/index.js +159 -0
  11. package/dist/commands/env.js +8 -4
  12. package/dist/commands/project/index.js +28 -2
  13. package/dist/controllers/app-env-api.js +54 -0
  14. package/dist/controllers/app-env-file.js +181 -0
  15. package/dist/controllers/app-env.js +284 -132
  16. package/dist/controllers/app.js +493 -344
  17. package/dist/controllers/auth.js +8 -8
  18. package/dist/controllers/branch.js +78 -48
  19. package/dist/controllers/database.js +318 -0
  20. package/dist/controllers/project.js +302 -75
  21. package/dist/lib/app/branch-database-deploy.js +373 -0
  22. package/dist/lib/app/branch-database.js +316 -0
  23. package/dist/lib/app/bun-project.js +12 -5
  24. package/dist/lib/app/deploy-output.js +10 -1
  25. package/dist/lib/app/env-config.js +1 -1
  26. package/dist/lib/app/env-file.js +82 -0
  27. package/dist/lib/app/env-vars.js +28 -2
  28. package/dist/lib/app/local-dev.js +34 -18
  29. package/dist/lib/app/preview-branch-database.js +102 -0
  30. package/dist/lib/app/preview-build-settings.js +385 -0
  31. package/dist/lib/app/preview-build.js +272 -81
  32. package/dist/lib/app/preview-provider.js +163 -54
  33. package/dist/lib/app/production-deploy-gate.js +161 -0
  34. package/dist/lib/auth/auth-ops.js +69 -19
  35. package/dist/lib/auth/guard.js +3 -2
  36. package/dist/lib/auth/login.js +109 -14
  37. package/dist/lib/database/provider.js +167 -0
  38. package/dist/lib/diagnostics.js +15 -0
  39. package/dist/lib/git/local-branch.js +41 -0
  40. package/dist/lib/git/local-status.js +57 -0
  41. package/dist/lib/project/interactive-setup.js +56 -0
  42. package/dist/lib/project/local-pin.js +182 -33
  43. package/dist/lib/project/resolution.js +287 -105
  44. package/dist/lib/project/setup.js +132 -0
  45. package/dist/presenters/app-env.js +149 -14
  46. package/dist/presenters/app.js +170 -20
  47. package/dist/presenters/auth.js +19 -6
  48. package/dist/presenters/branch.js +37 -102
  49. package/dist/presenters/database.js +274 -0
  50. package/dist/presenters/project.js +100 -47
  51. package/dist/presenters/verbose-context.js +64 -0
  52. package/dist/shell/command-arguments.js +6 -0
  53. package/dist/shell/command-meta.js +139 -16
  54. package/dist/shell/command-runner.js +38 -8
  55. package/dist/shell/diagnostics-output.js +63 -0
  56. package/dist/shell/errors.js +14 -1
  57. package/dist/shell/output.js +13 -2
  58. package/dist/shell/runtime.js +3 -3
  59. package/dist/shell/ui.js +23 -1
  60. package/dist/shell/update-check.js +247 -0
  61. package/dist/use-cases/auth.js +15 -4
  62. package/dist/use-cases/branch.js +20 -68
  63. package/dist/use-cases/create-cli-gateways.js +2 -17
  64. package/dist/use-cases/project.js +2 -1
  65. package/package.json +12 -10
@@ -0,0 +1,373 @@
1
+ import { CliError, usageError } from "../../shell/errors.js";
2
+ import { renderSummaryLine } from "../../shell/ui.js";
3
+ import { canPrompt } from "../../shell/runtime.js";
4
+ import { confirmPrompt } from "../../shell/prompt.js";
5
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
6
+ import "../project/setup.js";
7
+ import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup } from "./branch-database.js";
8
+ import path from "node:path";
9
+ //#region src/lib/app/branch-database-deploy.ts
10
+ async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
11
+ if (options.db === false) return emptyBranchDatabaseSetupOutcome();
12
+ if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
13
+ if (options.db === true) throw usageError("Database setup cannot be combined with provided database env vars", "The deploy command received --db and a DATABASE_URL or DIRECT_URL value from --env.", "Remove the --env database value to let --db create and wire a database, or remove --db to deploy with the provided value.", ["prisma-cli app deploy --db", "prisma-cli app deploy --env DATABASE_URL=postgresql://example"], "app");
14
+ return emptyBranchDatabaseSetupOutcome();
15
+ }
16
+ if (branch.kind === "production" && !options.firstProductionDeploy) {
17
+ if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
18
+ return emptyBranchDatabaseSetupOutcome();
19
+ }
20
+ const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
21
+ const targetEnvVars = getTargetDatabaseEnvVarKeys(envState);
22
+ if (hasExistingDatabaseEnvForTarget(branch, envState)) {
23
+ const warning = options.db === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
24
+ if (warning) emitBranchDatabaseWarning(context, warning);
25
+ return {
26
+ result: options.db === true ? {
27
+ status: "skipped",
28
+ reason: existingDatabaseEnvReason(branch),
29
+ envVars: targetEnvVars,
30
+ schema: null
31
+ } : void 0,
32
+ warnings: warning ? [warning] : []
33
+ };
34
+ }
35
+ const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
36
+ if (localSignal.unsupportedSchema) {
37
+ if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
38
+ return emptyBranchDatabaseSetupOutcome();
39
+ }
40
+ const hasSignal = hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl);
41
+ if (options.db !== true) {
42
+ if (!hasSignal) return emptyBranchDatabaseSetupOutcome();
43
+ if (!canPrompt(context) || context.flags.yes) {
44
+ const warning = databasePromptSuppressedWarning(branch);
45
+ emitBranchDatabaseWarning(context, warning);
46
+ return {
47
+ result: void 0,
48
+ warnings: [warning]
49
+ };
50
+ }
51
+ maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
52
+ if (!await confirmPrompt({
53
+ input: context.runtime.stdin,
54
+ output: context.output.stderr,
55
+ message: databasePromptMessage(branch),
56
+ initialValue: false
57
+ })) return emptyBranchDatabaseSetupOutcome();
58
+ } else if (!canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
59
+ return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
60
+ }
61
+ async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
62
+ emitBranchDatabaseProgress(context, "pending", "Creating database");
63
+ const database = await provider.createBranchDatabase({
64
+ projectId,
65
+ branchId: branch.id,
66
+ branchName: branch.name,
67
+ signal: context.runtime.signal
68
+ }).catch((error) => {
69
+ throw branchDatabaseSetupFailedError("Failed to create database", error, branch);
70
+ });
71
+ emitBranchDatabaseProgress(context, "success", "Created database");
72
+ try {
73
+ let schemaSetup = null;
74
+ const warnings = [];
75
+ let skippedSchemaWarning = null;
76
+ if (signal.schema) {
77
+ emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`);
78
+ schemaSetup = await runBranchDatabaseSchemaSetup({
79
+ context,
80
+ schema: signal.schema,
81
+ databaseUrl: database.databaseUrl,
82
+ directUrl: database.directUrl
83
+ }).catch((error) => {
84
+ throw schemaSetupFailedError(error, signal.schema, branch, context.runtime.cwd);
85
+ });
86
+ emitBranchDatabaseProgress(context, "success", "Applied database schema");
87
+ } else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
88
+ const envVars = await upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState);
89
+ emitBranchDatabaseProgress(context, "success", `Added ${envScopeLabel(branch)} env var${envVars.length === 1 ? "" : "s"} ${envVars.join(", ")}`);
90
+ if (skippedSchemaWarning) {
91
+ emitBranchDatabaseWarning(context, skippedSchemaWarning);
92
+ warnings.push(skippedSchemaWarning);
93
+ }
94
+ return {
95
+ result: {
96
+ status: "created",
97
+ database: {
98
+ id: database.id,
99
+ name: database.name
100
+ },
101
+ envVars,
102
+ schema: schemaSetup ? {
103
+ command: schemaSetup.command,
104
+ source: schemaSetup.source,
105
+ path: schemaSetup.schemaPath
106
+ } : null
107
+ },
108
+ warnings
109
+ };
110
+ } catch (error) {
111
+ throw await cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error);
112
+ }
113
+ }
114
+ async function upsertBranchDatabaseEnvVars(context, provider, projectId, branch, database, envState) {
115
+ const scope = envScopeForBranch(branch);
116
+ const written = [];
117
+ await upsertBranchDatabaseEnvVar(context, provider, {
118
+ projectId,
119
+ ...scope,
120
+ key: "DATABASE_URL",
121
+ value: database.databaseUrl,
122
+ existing: envState.targetDatabaseUrl,
123
+ branch
124
+ });
125
+ written.push("DATABASE_URL");
126
+ if (database.directUrl) {
127
+ await upsertBranchDatabaseEnvVar(context, provider, {
128
+ projectId,
129
+ ...scope,
130
+ key: "DIRECT_URL",
131
+ value: database.directUrl,
132
+ existing: envState.targetDirectUrl,
133
+ branch
134
+ });
135
+ written.push("DIRECT_URL");
136
+ } else if (branch.kind === "preview" && envState.targetDirectUrl) await provider.deleteEnvironmentVariable({
137
+ envVarId: envState.targetDirectUrl.id,
138
+ signal: context.runtime.signal
139
+ }).catch((error) => {
140
+ throw branchDatabaseSetupFailedError("Failed to remove stale DIRECT_URL", error, branch);
141
+ });
142
+ return written;
143
+ }
144
+ async function upsertBranchDatabaseEnvVar(context, provider, options) {
145
+ if (options.existing) {
146
+ await provider.updateEnvironmentVariable({
147
+ envVarId: options.existing.id,
148
+ value: options.value,
149
+ signal: context.runtime.signal
150
+ }).catch((error) => {
151
+ throw branchDatabaseSetupFailedError(`Failed to update ${options.key}`, error, options.branch);
152
+ });
153
+ return;
154
+ }
155
+ await provider.createEnvironmentVariable({
156
+ projectId: options.projectId,
157
+ className: options.className,
158
+ key: options.key,
159
+ value: options.value,
160
+ ...options.branchId ? { branchId: options.branchId } : {},
161
+ signal: context.runtime.signal
162
+ }).catch((error) => {
163
+ throw branchDatabaseSetupFailedError(`Failed to write ${options.key}`, error, options.branch);
164
+ });
165
+ }
166
+ async function inspectBranchDatabaseEnv(provider, projectId, branch, signal) {
167
+ const scope = envScopeForBranch(branch);
168
+ const [databaseUrlRows, directUrlRows] = await Promise.all([provider.listEnvironmentVariables({
169
+ projectId,
170
+ className: scope.className,
171
+ key: "DATABASE_URL",
172
+ signal
173
+ }), provider.listEnvironmentVariables({
174
+ projectId,
175
+ className: scope.className,
176
+ key: "DIRECT_URL",
177
+ signal
178
+ })]);
179
+ const targetBranchId = branch.kind === "preview" ? branch.id : null;
180
+ return {
181
+ targetDatabaseUrl: findEnvVar(databaseUrlRows, { branchId: targetBranchId }),
182
+ targetDirectUrl: findEnvVar(directUrlRows, { branchId: targetBranchId }),
183
+ inheritedPreviewDatabaseUrl: branch.kind === "preview" ? findEnvVar(databaseUrlRows, { branchId: null }) : null
184
+ };
185
+ }
186
+ function findEnvVar(rows, options) {
187
+ return rows.find((row) => row.branchId === options.branchId) ?? null;
188
+ }
189
+ function hasProvidedDatabaseEnvVars(envVars) {
190
+ return Boolean(envVars && ("DATABASE_URL" in envVars || "DIRECT_URL" in envVars));
191
+ }
192
+ function envScopeForBranch(branch) {
193
+ return branch.kind === "production" ? { className: "production" } : {
194
+ className: "preview",
195
+ branchId: branch.id
196
+ };
197
+ }
198
+ function envScopeLabel(branch) {
199
+ return branch.kind === "production" ? "production" : "branch";
200
+ }
201
+ function getTargetDatabaseEnvVarKeys(envState) {
202
+ return [envState.targetDatabaseUrl, envState.targetDirectUrl].filter((variable) => Boolean(variable)).map((variable) => variable.key).sort();
203
+ }
204
+ function hasExistingDatabaseEnvForTarget(branch, envState) {
205
+ if (branch.kind === "production") return Boolean(envState.targetDatabaseUrl || envState.targetDirectUrl);
206
+ return Boolean(envState.targetDatabaseUrl);
207
+ }
208
+ function existingDatabaseEnvReason(branch) {
209
+ return branch.kind === "production" ? "production-env-exists" : "branch-env-exists";
210
+ }
211
+ function existingDatabaseEnvWarning(branch, envVars) {
212
+ if (branch.kind === "production") return `Production already has ${envVars.join(" and ")}. Treating it as BYO database configuration and leaving env vars unchanged.`;
213
+ return `Branch "${branch.name}" already has DATABASE_URL. Leaving branch database env vars unchanged.`;
214
+ }
215
+ function databasePromptSuppressedWarning(branch) {
216
+ if (branch.kind === "production") return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db --yes to create and wire a Prisma Postgres database for this first production deploy.";
217
+ return "This app appears to use DATABASE_URL. Run prisma-cli app deploy --db to create an isolated database for this preview branch.";
218
+ }
219
+ function databasePromptMessage(branch) {
220
+ return branch.kind === "production" ? "Create a Prisma Postgres database for production?" : `Create an isolated database for branch "${branch.name}"?`;
221
+ }
222
+ function maybeRenderBranchDatabaseSignal(context, branch, signal, envState) {
223
+ if (context.flags.json || context.flags.quiet) return;
224
+ const rows = [
225
+ signal.schema ? ` Schema ${path.relative(context.runtime.cwd, signal.schema.path) || defaultSchemaSourcePath(signal.schema)}` : null,
226
+ signal.databaseUrlReferences.length > 0 ? ` Code ${signal.databaseUrlReferences.slice(0, 3).join(", ")}` : null,
227
+ envState.inheritedPreviewDatabaseUrl ? " Env preview DATABASE_URL is inherited by this branch" : null
228
+ ].filter((row) => Boolean(row));
229
+ context.output.stderr.write(`Database signal found for ${databaseTargetLabel(branch)}\n${rows.join("\n")}\n\n`);
230
+ }
231
+ function databaseTargetLabel(branch) {
232
+ return branch.kind === "production" ? `production branch "${branch.name}"` : `branch "${branch.name}"`;
233
+ }
234
+ function emitBranchDatabaseProgress(context, status, message) {
235
+ if (context.flags.json || context.flags.quiet) return;
236
+ const line = status === "pending" ? `${context.ui.warning("◇")} ${message}...` : renderSummaryLine(context.ui, "success", message);
237
+ context.output.stderr.write(`${line}\n`);
238
+ }
239
+ function emitBranchDatabaseWarning(context, warning) {
240
+ if (context.flags.json || context.flags.quiet) return;
241
+ context.output.stderr.write(`${renderSummaryLine(context.ui, "warning", warning)}\n`);
242
+ }
243
+ function emptyBranchDatabaseSetupOutcome() {
244
+ return {
245
+ result: void 0,
246
+ warnings: []
247
+ };
248
+ }
249
+ function productionDatabaseSetupAfterFirstDeployError() {
250
+ return usageError("Database setup is only available during the first production deploy", "The selected production app already has a live deployment.", "Use project env commands to manage production DATABASE_URL, or deploy a preview branch with --db.", ["prisma-cli project env add DATABASE_URL=<value> --role production", "prisma-cli app deploy --branch feature/db --db"], "app");
251
+ }
252
+ function nonInteractiveDatabaseSetupRequiresYesError(branch) {
253
+ return usageError("Database setup requires --yes in non-interactive mode", "The deploy command received --db, but prompts are not available and --yes was not passed.", "Pass --yes together with --db to confirm non-interactive database creation.", [branch.kind === "production" ? "prisma-cli app deploy --prod --db --yes" : `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db --yes`], "app");
254
+ }
255
+ function formatSchemaSetupCommand(command) {
256
+ switch (command) {
257
+ case "migrate-deploy": return "prisma migrate deploy";
258
+ case "db-push": return "prisma db push";
259
+ case "prisma-next-db-init": return "prisma-next db init";
260
+ }
261
+ }
262
+ function branchDatabaseSetupFailedError(summary, error, branch) {
263
+ return new CliError({
264
+ code: "BRANCH_DATABASE_SETUP_FAILED",
265
+ domain: "app",
266
+ summary,
267
+ why: error instanceof Error ? error.message : String(error),
268
+ fix: "Retry the command, or create the database and env vars manually with project env commands.",
269
+ debug: formatDebugDetails(error),
270
+ meta: { branch: branch.name },
271
+ exitCode: 1,
272
+ nextSteps: [formatAppDeployWithDbNextStep(branch), formatProjectEnvListNextStep(branch)]
273
+ });
274
+ }
275
+ async function cleanupCreatedBranchDatabaseAfterFailure(context, provider, database, branch, error) {
276
+ const setupError = error instanceof CliError ? error : branchDatabaseSetupFailedError("Database setup failed", error, branch);
277
+ emitBranchDatabaseProgress(context, "pending", "Removing database after setup failed");
278
+ try {
279
+ await provider.deleteBranchDatabase({
280
+ databaseId: database.id,
281
+ signal: context.runtime.signal
282
+ });
283
+ emitBranchDatabaseProgress(context, "success", "Removed database after setup failed");
284
+ } catch (cleanupError) {
285
+ return branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch);
286
+ }
287
+ return setupError;
288
+ }
289
+ function branchDatabaseCleanupFailedError(setupError, cleanupError, database, branch) {
290
+ const cleanupWhy = cleanupError instanceof Error ? cleanupError.message : String(cleanupError);
291
+ const setupWhy = setupError.why ?? "Database setup failed.";
292
+ return new CliError({
293
+ code: setupError.code,
294
+ domain: setupError.domain,
295
+ summary: setupError.summary,
296
+ why: `${setupWhy} Prisma could not delete the created database "${database.name}" (${database.id}): ${cleanupWhy}`,
297
+ fix: "Delete the created database from Console or contact Prisma support, then rerun deploy with --db.",
298
+ debug: formatCombinedDebugDetails(setupError, cleanupError),
299
+ meta: {
300
+ ...setupError.meta,
301
+ branch: branch.name,
302
+ databaseId: database.id,
303
+ databaseName: database.name,
304
+ cleanupFailed: true
305
+ },
306
+ exitCode: setupError.exitCode,
307
+ nextSteps: []
308
+ });
309
+ }
310
+ function schemaSetupFailedError(error, schema, branch, cwd) {
311
+ return new CliError({
312
+ code: "SCHEMA_SETUP_FAILED",
313
+ domain: "app",
314
+ summary: "Database schema setup failed",
315
+ why: error instanceof Error ? error.message : String(error),
316
+ fix: "Fix the Prisma schema or migrations, then rerun deploy with --db.",
317
+ debug: formatDebugDetails(error),
318
+ meta: {
319
+ branch: branch.name,
320
+ schemaPath: schema.path,
321
+ source: schema.kind,
322
+ command: schema.command
323
+ },
324
+ exitCode: 1,
325
+ nextSteps: [...formatSchemaSetupNextSteps(schema, cwd), formatAppDeployWithDbNextStep(branch)]
326
+ });
327
+ }
328
+ function unsupportedBranchDatabaseSchemaError(schema, branch, context) {
329
+ return usageError("Database setup is not available for this Prisma schema", `${path.relative(context.runtime.cwd, schema.path) || defaultUnsupportedSchemaSourcePath(schema)} targets ${formatUnsupportedSchemaTarget(schema.target)}, but --db creates Prisma Postgres databases.`, "Use project env commands to provide a database URL, or switch the Prisma schema source to PostgreSQL before using --db.", [formatProjectEnvAddNextStep(branch), `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)}`], "app");
330
+ }
331
+ function formatAppDeployWithDbNextStep(branch) {
332
+ return `prisma-cli app deploy --branch ${formatCommandArgument(branch.name)} --db`;
333
+ }
334
+ function formatProjectEnvListNextStep(branch) {
335
+ return branch.kind === "production" ? "prisma-cli project env list --role production" : `prisma-cli project env list --branch ${formatCommandArgument(branch.name)}`;
336
+ }
337
+ function formatProjectEnvAddNextStep(branch) {
338
+ return branch.kind === "production" ? "prisma-cli project env add DATABASE_URL=<value> --role production" : `prisma-cli project env add DATABASE_URL=<value> --branch ${formatCommandArgument(branch.name)}`;
339
+ }
340
+ function formatSchemaSetupNextSteps(schema, cwd) {
341
+ const sourcePath = path.relative(cwd, schema.path) || defaultSchemaSourcePath(schema);
342
+ switch (schema.command) {
343
+ case "migrate-deploy": return [`npx --no-install prisma migrate deploy --schema ${formatCommandArgument(sourcePath)}`];
344
+ case "db-push": return [`npx --no-install prisma db push --schema ${formatCommandArgument(sourcePath)}`];
345
+ case "prisma-next-db-init": return [`npx --no-install prisma-next contract emit --config ${formatCommandArgument(sourcePath)}`, `npx --no-install prisma-next db init --config ${formatCommandArgument(sourcePath)} --db <DATABASE_URL>`];
346
+ }
347
+ }
348
+ function defaultSchemaSourcePath(schema) {
349
+ return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
350
+ }
351
+ function defaultUnsupportedSchemaSourcePath(schema) {
352
+ return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
353
+ }
354
+ function formatUnsupportedSchemaTarget(target) {
355
+ switch (target) {
356
+ case "cockroachdb": return "CockroachDB";
357
+ case "mongodb": return "MongoDB";
358
+ case "mysql": return "MySQL";
359
+ case "sqlite": return "SQLite";
360
+ case "sqlserver": return "SQL Server";
361
+ }
362
+ }
363
+ function formatDebugDetails(error) {
364
+ if (error instanceof Error) return error.stack ?? error.message;
365
+ return typeof error === "string" ? error : null;
366
+ }
367
+ function formatCombinedDebugDetails(setupError, cleanupError) {
368
+ const setupDebug = setupError.debug ?? setupError.stack ?? setupError.message;
369
+ const cleanupDebug = formatDebugDetails(cleanupError);
370
+ return [setupDebug ? `Setup error:\n${setupDebug}` : null, cleanupDebug ? `Cleanup error:\n${cleanupDebug}` : null].filter((line) => Boolean(line)).join("\n\n") || null;
371
+ }
372
+ //#endregion
373
+ export { maybeSetupBranchDatabase };
@@ -0,0 +1,316 @@
1
+ import { access, readFile, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ //#region src/lib/app/branch-database.ts
5
+ const SKIPPED_DIRECTORIES = new Set([
6
+ ".git",
7
+ ".next",
8
+ ".nuxt",
9
+ ".output",
10
+ ".prisma",
11
+ ".turbo",
12
+ ".vercel",
13
+ ".wrangler",
14
+ "build",
15
+ "coverage",
16
+ "dist",
17
+ "node_modules",
18
+ "out"
19
+ ]);
20
+ const DATABASE_URL_SCAN_EXTENSIONS = new Set([
21
+ ".cjs",
22
+ ".cts",
23
+ ".env",
24
+ ".js",
25
+ ".json",
26
+ ".jsx",
27
+ ".mjs",
28
+ ".mts",
29
+ ".prisma",
30
+ ".ts",
31
+ ".tsx"
32
+ ]);
33
+ const MAX_SCAN_DEPTH = 6;
34
+ const MAX_SCAN_FILES = 1e3;
35
+ const MAX_DATABASE_URL_REFERENCE_FILES = 10;
36
+ const MAX_TEXT_FILE_BYTES = 1024 * 1024;
37
+ async function inspectBranchDatabaseSignal(cwd, signal) {
38
+ const state = {
39
+ filesVisited: 0,
40
+ schemaCandidates: [],
41
+ prismaNextConfigCandidates: [],
42
+ databaseUrlReferences: []
43
+ };
44
+ await scanDirectory(cwd, cwd, 0, state, signal);
45
+ const prismaNextConfigs = await Promise.all(state.prismaNextConfigCandidates.map((configPath) => classifyPrismaNextConfig(configPath, signal)));
46
+ const supportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "supported");
47
+ const unsupportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "unsupported");
48
+ const selectedPrismaOrmSchema = await selectPrismaOrmSchema(cwd, state.schemaCandidates, signal);
49
+ const schema = supportedPrismaNextConfig ? {
50
+ kind: "prisma-next",
51
+ path: supportedPrismaNextConfig.path,
52
+ hasMigrations: false,
53
+ command: "prisma-next-db-init",
54
+ target: supportedPrismaNextConfig.target
55
+ } : selectedPrismaOrmSchema.schema;
56
+ return {
57
+ schema,
58
+ unsupportedSchema: schema ? null : unsupportedPrismaNextConfig ? {
59
+ kind: "prisma-next",
60
+ path: unsupportedPrismaNextConfig.path,
61
+ target: unsupportedPrismaNextConfig.target
62
+ } : selectedPrismaOrmSchema.unsupportedSchema,
63
+ databaseUrlReferences: state.databaseUrlReferences
64
+ };
65
+ }
66
+ function hasBranchDatabaseSignal(signal) {
67
+ if (signal.unsupportedSchema) return false;
68
+ return Boolean(signal.schema || signal.databaseUrlReferences.length > 0);
69
+ }
70
+ async function runBranchDatabaseSchemaSetup(options) {
71
+ const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
72
+ const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl);
73
+ for (const command of commands) await runPrismaCommand({
74
+ context: options.context,
75
+ ...command,
76
+ env: {
77
+ DATABASE_URL: options.databaseUrl,
78
+ ...options.directUrl ? { DIRECT_URL: options.directUrl } : {}
79
+ }
80
+ });
81
+ return {
82
+ command: options.schema.command,
83
+ source: options.schema.kind,
84
+ schemaPath
85
+ };
86
+ }
87
+ async function scanDirectory(cwd, directory, depth, state, signal) {
88
+ signal.throwIfAborted();
89
+ if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
90
+ let entries;
91
+ try {
92
+ entries = await readdir(directory, { withFileTypes: true });
93
+ } catch (error) {
94
+ if (error.code === "ENOENT") return;
95
+ throw error;
96
+ }
97
+ entries.sort((left, right) => left.name.localeCompare(right.name));
98
+ for (const entry of entries) {
99
+ signal.throwIfAborted();
100
+ if (state.filesVisited >= MAX_SCAN_FILES) return;
101
+ const entryPath = path.join(directory, entry.name);
102
+ if (entry.isDirectory()) {
103
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
104
+ continue;
105
+ }
106
+ if (!entry.isFile()) continue;
107
+ state.filesVisited += 1;
108
+ if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
109
+ if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
110
+ if (state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entry.name) && await fileContainsDatabaseUrl(entryPath, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
111
+ }
112
+ }
113
+ async function selectPrismaOrmSchema(cwd, candidates, signal) {
114
+ const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
115
+ for (const schemaPath of sorted) {
116
+ const target = await classifyPrismaOrmSchemaTarget(schemaPath, signal);
117
+ if (target === "postgresql" || target === "unknown") {
118
+ const hasMigrations = await hasMigrationsDirectory(path.dirname(schemaPath), signal);
119
+ return {
120
+ schema: {
121
+ kind: "prisma-orm",
122
+ path: schemaPath,
123
+ hasMigrations,
124
+ command: hasMigrations ? "migrate-deploy" : "db-push",
125
+ target
126
+ },
127
+ unsupportedSchema: null
128
+ };
129
+ }
130
+ return {
131
+ schema: null,
132
+ unsupportedSchema: {
133
+ kind: "prisma-orm",
134
+ path: schemaPath,
135
+ target
136
+ }
137
+ };
138
+ }
139
+ return {
140
+ schema: null,
141
+ unsupportedSchema: null
142
+ };
143
+ }
144
+ function selectPrismaNextConfig(cwd, candidates, mode) {
145
+ const matches = candidates.filter((candidate) => {
146
+ const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
147
+ return mode === "supported" ? isSupported : !isSupported;
148
+ });
149
+ return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts").map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)).find((candidate) => Boolean(candidate)) ?? null;
150
+ }
151
+ function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
152
+ return candidates.map((candidate) => ({
153
+ absolute: candidate,
154
+ relative: path.relative(cwd, candidate) || preferredRootFile
155
+ })).sort((left, right) => {
156
+ if (left.relative === preferredRootFile) return -1;
157
+ if (right.relative === preferredRootFile) return 1;
158
+ return left.relative.length - right.relative.length || left.relative.localeCompare(right.relative);
159
+ }).map((candidate) => candidate.absolute);
160
+ }
161
+ async function hasMigrationsDirectory(schemaDirectory, signal) {
162
+ signal.throwIfAborted();
163
+ const migrationsPath = path.join(schemaDirectory, "migrations");
164
+ try {
165
+ await access(migrationsPath);
166
+ return (await readdir(migrationsPath)).length > 0;
167
+ } catch (error) {
168
+ if (error.code === "ENOENT") return false;
169
+ throw error;
170
+ }
171
+ }
172
+ async function classifyPrismaNextConfig(configPath, signal) {
173
+ const content = await readTextFileIfSmall(configPath, signal);
174
+ if (!content) return {
175
+ path: configPath,
176
+ target: "unknown"
177
+ };
178
+ if (content.includes("@prisma-next/postgres/config")) return {
179
+ path: configPath,
180
+ target: "postgresql"
181
+ };
182
+ if (content.includes("@prisma-next/mongo/config")) return {
183
+ path: configPath,
184
+ target: "mongodb"
185
+ };
186
+ if (content.includes("@prisma-next/sqlite/config")) return {
187
+ path: configPath,
188
+ target: "sqlite"
189
+ };
190
+ return {
191
+ path: configPath,
192
+ target: "unknown"
193
+ };
194
+ }
195
+ async function classifyPrismaOrmSchemaTarget(schemaPath, signal) {
196
+ switch ((await readTextFileIfSmall(schemaPath, signal))?.match(/\bprovider\s*=\s*"([^"]+)"/)?.[1] ?? null) {
197
+ case "postgresql": return "postgresql";
198
+ case "mongodb": return "mongodb";
199
+ case "mysql": return "mysql";
200
+ case "sqlite": return "sqlite";
201
+ case "sqlserver": return "sqlserver";
202
+ case "cockroachdb": return "cockroachdb";
203
+ default: return "unknown";
204
+ }
205
+ }
206
+ function shouldScanForDatabaseUrl(fileName) {
207
+ if (fileName === ".env" || fileName.startsWith(".env.")) return true;
208
+ return DATABASE_URL_SCAN_EXTENSIONS.has(path.extname(fileName));
209
+ }
210
+ function isPrismaNextConfigFile(fileName) {
211
+ if (!fileName.startsWith("prisma-next.config.")) return false;
212
+ return [
213
+ ".cjs",
214
+ ".cts",
215
+ ".js",
216
+ ".mjs",
217
+ ".mts",
218
+ ".ts"
219
+ ].some((extension) => fileName.endsWith(extension));
220
+ }
221
+ async function fileContainsDatabaseUrl(filePath, signal) {
222
+ return (await readTextFileIfSmall(filePath, signal))?.includes("DATABASE_URL") ?? false;
223
+ }
224
+ async function readTextFileIfSmall(filePath, signal) {
225
+ signal.throwIfAborted();
226
+ if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return null;
227
+ return readFile(filePath, {
228
+ encoding: "utf8",
229
+ signal
230
+ });
231
+ }
232
+ function buildSchemaSetupCommands(schema, schemaPath, databaseUrl) {
233
+ if (schema.command === "migrate-deploy") return [{
234
+ args: [
235
+ "--no-install",
236
+ "prisma",
237
+ "migrate",
238
+ "deploy",
239
+ "--schema",
240
+ schemaPath
241
+ ],
242
+ displayCommand: "npx --no-install prisma migrate deploy"
243
+ }];
244
+ if (schema.command === "db-push") return [{
245
+ args: [
246
+ "--no-install",
247
+ "prisma",
248
+ "db",
249
+ "push",
250
+ "--schema",
251
+ schemaPath
252
+ ],
253
+ displayCommand: "npx --no-install prisma db push"
254
+ }];
255
+ return [{
256
+ args: [
257
+ "--no-install",
258
+ "prisma-next",
259
+ "contract",
260
+ "emit",
261
+ "--config",
262
+ schemaPath
263
+ ],
264
+ displayCommand: "npx --no-install prisma-next contract emit"
265
+ }, {
266
+ args: [
267
+ "--no-install",
268
+ "prisma-next",
269
+ "db",
270
+ "init",
271
+ "--config",
272
+ schemaPath,
273
+ "--db",
274
+ databaseUrl
275
+ ],
276
+ displayCommand: "npx --no-install prisma-next db init"
277
+ }];
278
+ }
279
+ function defaultSchemaSourcePath(schema) {
280
+ return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
281
+ }
282
+ async function runPrismaCommand(options) {
283
+ const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet;
284
+ const child = spawn("npx", options.args, {
285
+ cwd: options.context.runtime.cwd,
286
+ env: {
287
+ ...options.context.runtime.env,
288
+ ...options.env
289
+ },
290
+ signal: options.context.runtime.signal,
291
+ stdio: shouldPipeOutput ? [
292
+ "ignore",
293
+ "pipe",
294
+ "pipe"
295
+ ] : [
296
+ "ignore",
297
+ "ignore",
298
+ "ignore"
299
+ ]
300
+ });
301
+ if (shouldPipeOutput) {
302
+ child.stdout?.pipe(options.context.output.stderr, { end: false });
303
+ child.stderr?.pipe(options.context.output.stderr, { end: false });
304
+ }
305
+ const exit = await new Promise((resolve, reject) => {
306
+ child.once("error", reject);
307
+ child.once("close", (code, signal) => resolve({
308
+ code,
309
+ signal
310
+ }));
311
+ });
312
+ if (exit.signal) throw new Error(`${options.displayCommand} was terminated by ${exit.signal}.`);
313
+ if (exit.code !== 0) throw new Error(`${options.displayCommand} exited with code ${exit.code ?? 1}.`);
314
+ }
315
+ //#endregion
316
+ export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup };