@prisma/cli 3.0.0-beta.1 → 3.0.0-beta.11

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