@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.30

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 (99) hide show
  1. package/README.md +6 -15
  2. package/dist/adapters/local-state.js +15 -4
  3. package/dist/adapters/mock-api.js +244 -0
  4. package/dist/adapters/token-storage.js +335 -34
  5. package/dist/cli.js +7 -7
  6. package/dist/cli2.js +24 -5
  7. package/dist/commands/agent/index.js +60 -0
  8. package/dist/commands/app/index.js +91 -61
  9. package/dist/commands/auth/index.js +55 -2
  10. package/dist/commands/branch/index.js +2 -27
  11. package/dist/commands/bucket/index.js +123 -0
  12. package/dist/commands/build/index.js +29 -0
  13. package/dist/commands/database/index.js +249 -0
  14. package/dist/commands/env.js +8 -4
  15. package/dist/commands/feedback/index.js +20 -0
  16. package/dist/commands/git/index.js +1 -1
  17. package/dist/commands/init/index.js +33 -0
  18. package/dist/commands/project/index.js +54 -5
  19. package/dist/controllers/agent-setup.js +52 -0
  20. package/dist/controllers/agent.js +228 -0
  21. package/dist/controllers/app-env-api.js +55 -0
  22. package/dist/controllers/app-env-file.js +181 -0
  23. package/dist/controllers/app-env.js +227 -104
  24. package/dist/controllers/app.js +746 -306
  25. package/dist/controllers/auth.js +247 -3
  26. package/dist/controllers/branch.js +78 -48
  27. package/dist/controllers/bucket.js +278 -0
  28. package/dist/controllers/build.js +88 -0
  29. package/dist/controllers/database.js +567 -0
  30. package/dist/controllers/feedback.js +86 -0
  31. package/dist/controllers/init.js +753 -0
  32. package/dist/controllers/project.js +377 -22
  33. package/dist/controllers/select-prompt-port.js +1 -0
  34. package/dist/lib/agent/cli-command.js +20 -0
  35. package/dist/lib/agent/constants.js +12 -0
  36. package/dist/lib/agent/package-manager.js +99 -0
  37. package/dist/lib/agent/setup-status.js +83 -0
  38. package/dist/lib/app/{preview-provider.js → app-provider.js} +137 -88
  39. package/dist/lib/app/branch-database-api.js +102 -0
  40. package/dist/lib/app/branch-database-deploy.js +326 -0
  41. package/dist/lib/app/branch-database.js +216 -0
  42. package/dist/lib/app/build-settings.js +93 -0
  43. package/dist/lib/app/build.js +83 -0
  44. package/dist/lib/app/bun-project.js +3 -4
  45. package/dist/lib/app/compute-config.js +145 -0
  46. package/dist/lib/app/deploy-plan.js +59 -0
  47. package/dist/lib/app/{preview-progress.js → deploy-progress.js} +12 -12
  48. package/dist/lib/app/env-config.js +1 -1
  49. package/dist/lib/app/env-file.js +82 -0
  50. package/dist/lib/app/env-vars.js +28 -2
  51. package/dist/lib/app/local-dev.js +3 -60
  52. package/dist/lib/app/production-deploy-gate.js +162 -0
  53. package/dist/lib/app/read-branch.js +30 -0
  54. package/dist/lib/auth/auth-ops.js +10 -4
  55. package/dist/lib/auth/guard.js +4 -1
  56. package/dist/lib/auth/login.js +33 -26
  57. package/dist/lib/auth/recipient.js +42 -0
  58. package/dist/lib/bucket/provider.js +139 -0
  59. package/dist/lib/database/provider.js +378 -0
  60. package/dist/lib/diagnostics.js +15 -0
  61. package/dist/lib/fs/home-path.js +24 -0
  62. package/dist/lib/git/local-branch.js +53 -0
  63. package/dist/lib/git/local-status.js +57 -0
  64. package/dist/lib/project/interactive-setup.js +5 -4
  65. package/dist/lib/project/local-pin.js +171 -41
  66. package/dist/lib/project/provider.js +92 -0
  67. package/dist/lib/project/resolution.js +199 -48
  68. package/dist/lib/project/setup.js +67 -20
  69. package/dist/output/patterns.js +1 -1
  70. package/dist/presenters/agent.js +74 -0
  71. package/dist/presenters/app-env.js +149 -14
  72. package/dist/presenters/app.js +208 -27
  73. package/dist/presenters/auth.js +99 -2
  74. package/dist/presenters/branch.js +37 -102
  75. package/dist/presenters/bucket.js +174 -0
  76. package/dist/presenters/database.js +448 -0
  77. package/dist/presenters/feedback.js +26 -0
  78. package/dist/presenters/init.js +30 -0
  79. package/dist/presenters/project.js +139 -27
  80. package/dist/presenters/verbose-context.js +64 -0
  81. package/dist/shell/cli-command.js +12 -0
  82. package/dist/shell/command-arguments.js +7 -1
  83. package/dist/shell/command-meta.js +458 -17
  84. package/dist/shell/command-runner.js +58 -18
  85. package/dist/shell/diagnostics-output.js +57 -0
  86. package/dist/shell/errors.js +56 -1
  87. package/dist/shell/help.js +31 -20
  88. package/dist/shell/output.js +72 -1
  89. package/dist/shell/prompt.js +12 -5
  90. package/dist/shell/runtime.js +8 -4
  91. package/dist/shell/ui.js +42 -3
  92. package/dist/shell/update-check.js +2 -2
  93. package/dist/use-cases/auth.js +68 -1
  94. package/dist/use-cases/branch.js +20 -68
  95. package/dist/use-cases/create-cli-gateways.js +2 -17
  96. package/dist/use-cases/project.js +2 -1
  97. package/package.json +21 -4
  98. package/dist/lib/app/preview-build.js +0 -312
  99. package/dist/lib/app/preview-interaction.js +0 -5
@@ -0,0 +1,753 @@
1
+ import { detectPackageManagerSync } from "../lib/agent/package-manager.js";
2
+ import { resolvePrismaCliPackageCommandFormatterSync } from "../lib/agent/cli-command.js";
3
+ import { CliError, usageError } from "../shell/errors.js";
4
+ import { canPrompt } from "../shell/runtime.js";
5
+ import { confirmPrompt, isPromptCancelError, selectPrompt, textPrompt } from "../shell/prompt.js";
6
+ import { readLocalResolutionPin } from "../lib/project/local-pin.js";
7
+ import { readBunPackageEntrypoint, readBunPackageJson } from "../lib/app/bun-project.js";
8
+ import { maybePromptForAgentSetup } from "./agent-setup.js";
9
+ import { runProjectLink } from "./project.js";
10
+ import { detectDeployFramework } from "./app.js";
11
+ import { execa } from "execa";
12
+ import path from "node:path";
13
+ import { readFile, rm, writeFile } from "node:fs/promises";
14
+ import { COMPUTE_CONFIG_FILENAME, COMPUTE_CONFIG_JSON_FILENAME, COMPUTE_REGIONS, FRAMEWORKS, defaultHttpPortForBuildType, findComputeConfigCandidates, findComputeConfigDir, frameworkByKey, frameworkFromAlias, normalizeComputeConfig, serializeComputeConfig, serializeComputeConfigJson } from "@prisma/compute-sdk/config";
15
+ //#region src/controllers/init.ts
16
+ async function runInit(context, flags) {
17
+ const cwd = context.runtime.cwd;
18
+ const signal = context.runtime.signal;
19
+ const formatCommand = resolvePrismaCliPackageCommandFormatterSync(cwd);
20
+ const format = parseInitFormat(flags.format, formatCommand);
21
+ if (format.value === "json" && flags.install === true) throw usageError("--install does not apply to the JSON config format", `${COMPUTE_CONFIG_JSON_FILENAME} is a dependency-free static config; the ${COMPUTE_SDK_PACKAGE} devDependency exists only for ${COMPUTE_CONFIG_FILENAME} editor types.`, "Drop --install, or use the TypeScript format.", [formatCommand([
22
+ "init",
23
+ "--format",
24
+ "json"
25
+ ])], "app");
26
+ const existingConfig = await findExistingComputeConfig(cwd, signal);
27
+ if (existingConfig) {
28
+ const solePath = existingConfig.candidates.length === 1 ? existingConfig.candidates[0] : void 0;
29
+ const soleIsJson = solePath !== void 0 && path.extname(solePath) === ".json";
30
+ if (soleIsJson && format.value === "typescript" && format.explicit) {
31
+ rejectConversionResolutionFlags(flags, formatCommand);
32
+ return runInitConversion(context, flags, solePath, formatCommand);
33
+ }
34
+ if (solePath && !soleIsJson && format.value === "json") throw initConvertUnsupportedError(solePath);
35
+ throw initConfigExistsError(existingConfig.candidates[0] ?? existingConfig.directory);
36
+ }
37
+ const region = parseInitRegion(flags.region, formatCommand);
38
+ let framework = await resolveInitFramework(context, flags, formatCommand);
39
+ const name = await resolveInitAppName(cwd, flags.name, signal, formatCommand);
40
+ let httpPort = parseInitHttpPort(flags.httpPort, formatCommand) ?? {
41
+ value: defaultHttpPortForBuildType(frameworkByKey(framework.key).buildType),
42
+ source: "framework default"
43
+ };
44
+ const adjusted = await maybeAdjustSettings(context, framework, httpPort, { portExplicit: flags.httpPort !== void 0 });
45
+ framework = adjusted.framework;
46
+ httpPort = adjusted.httpPort;
47
+ if (format.value === "json" && framework.key === "custom") throw usageError("Custom framework requires the TypeScript config format", "The custom framework needs build.outputDirectory and build.entrypoint, which init does not collect; the TypeScript format includes a commented build stub to complete, and strict JSON cannot carry it.", `Rerun without --format json and fill in the build stub, or write ${COMPUTE_CONFIG_JSON_FILENAME} by hand with a build object.`, [formatCommand([
48
+ "init",
49
+ "--framework",
50
+ "custom"
51
+ ])], "app");
52
+ const entry = await resolveInitEntry(cwd, framework, flags.entry, signal);
53
+ const settings = [
54
+ {
55
+ key: "app",
56
+ value: name.value,
57
+ source: name.source
58
+ },
59
+ {
60
+ key: "framework",
61
+ value: framework.displayName,
62
+ source: framework.source
63
+ },
64
+ ...entry ? [{
65
+ key: "entry",
66
+ value: entry.value,
67
+ source: entry.source
68
+ }] : [],
69
+ {
70
+ key: "http port",
71
+ value: String(httpPort.value),
72
+ source: httpPort.source
73
+ },
74
+ ...region ? [{
75
+ key: "region",
76
+ value: region,
77
+ source: "flag"
78
+ }] : []
79
+ ];
80
+ renderInitSettingsPreview(context, settings);
81
+ const config = { app: {
82
+ name: name.value,
83
+ framework: framework.key,
84
+ httpPort: httpPort.value,
85
+ ...entry ? { entry: entry.value } : {},
86
+ ...region ? { region } : {}
87
+ } };
88
+ const configFilename = format.value === "json" ? COMPUTE_CONFIG_JSON_FILENAME : COMPUTE_CONFIG_FILENAME;
89
+ const configPath = path.join(cwd, configFilename);
90
+ let source;
91
+ if (format.value === "json") source = serializeComputeConfigJson(config);
92
+ else {
93
+ source = serializeComputeConfig(config);
94
+ if (framework.key === "custom") source += CUSTOM_BUILD_STUB;
95
+ }
96
+ signal.throwIfAborted();
97
+ try {
98
+ await writeFile(configPath, source, {
99
+ encoding: "utf8",
100
+ flag: "wx"
101
+ });
102
+ } catch (error) {
103
+ if (error.code === "EEXIST") throw initConfigExistsError(configPath);
104
+ throw error;
105
+ }
106
+ const warnings = [];
107
+ const types = format.value === "json" ? {
108
+ status: "skipped",
109
+ package: COMPUTE_SDK_PACKAGE,
110
+ installCommand: null
111
+ } : await resolveInitTypes(context, flags, { onWarning: (message) => warnings.push(message) });
112
+ const link = await resolveInitLink(context, flags, {
113
+ onWarning: (message) => warnings.push(message),
114
+ formatCommand
115
+ });
116
+ warnings.push(...await maybePromptForAgentSetup(context, cwd));
117
+ const unlinked = link.status !== "linked" && link.status !== "already-linked";
118
+ const typesMissing = types.status !== "installed" && types.status !== "already-installed";
119
+ return {
120
+ command: "init",
121
+ result: {
122
+ configPath: configFilename,
123
+ format: format.value,
124
+ converted: false,
125
+ directory: formatInitDirectory(cwd),
126
+ app: {
127
+ name: name.value,
128
+ framework: framework.key,
129
+ httpPort: httpPort.value,
130
+ ...entry ? { entry: entry.value } : {},
131
+ ...region ? { region } : {}
132
+ },
133
+ settings,
134
+ types,
135
+ link
136
+ },
137
+ warnings,
138
+ nextSteps: [
139
+ ...typesMissing && types.installCommand ? [types.installCommand] : [],
140
+ formatCommand(["app", "deploy"]),
141
+ ...unlinked ? [formatCommand(["project", "link"])] : []
142
+ ]
143
+ };
144
+ }
145
+ /** Dev dependency that provides editor types for the generated config. */
146
+ const COMPUTE_SDK_PACKAGE = "@prisma/compute-sdk";
147
+ function packageAddCommand(packageManager) {
148
+ switch (packageManager) {
149
+ case "pnpm": return [
150
+ "pnpm",
151
+ "add",
152
+ "-D",
153
+ COMPUTE_SDK_PACKAGE
154
+ ];
155
+ case "bun": return [
156
+ "bun",
157
+ "add",
158
+ "-d",
159
+ COMPUTE_SDK_PACKAGE
160
+ ];
161
+ case "yarn": return [
162
+ "yarn",
163
+ "add",
164
+ "-D",
165
+ COMPUTE_SDK_PACKAGE
166
+ ];
167
+ case "npm": return [
168
+ "npm",
169
+ "install",
170
+ "-D",
171
+ COMPUTE_SDK_PACKAGE
172
+ ];
173
+ }
174
+ }
175
+ /**
176
+ * Offers to install `@prisma/compute-sdk` as a devDependency so the generated
177
+ * config's typed import resolves in the editor. Deploy resolves the import
178
+ * without a local install, so every outcome short of success is a hint, never
179
+ * a failure.
180
+ */
181
+ async function resolveInitTypes(context, flags, hooks) {
182
+ const cwd = context.runtime.cwd;
183
+ let packageJson;
184
+ try {
185
+ packageJson = await readBunPackageJson(cwd, context.runtime.signal);
186
+ } catch (error) {
187
+ if (context.runtime.signal.aborted) throw error;
188
+ hooks.onWarning(`Skipped the ${COMPUTE_SDK_PACKAGE} types install: package.json could not be read (${error instanceof Error ? error.message.split("\n")[0] : String(error)}).`);
189
+ return {
190
+ status: "skipped",
191
+ package: COMPUTE_SDK_PACKAGE,
192
+ installCommand: null
193
+ };
194
+ }
195
+ if (hasComputeSdkDependency(packageJson)) return {
196
+ status: "already-installed",
197
+ package: COMPUTE_SDK_PACKAGE,
198
+ installCommand: null
199
+ };
200
+ const installCommand = packageAddCommand(detectPackageManagerSync(cwd) ?? "npm");
201
+ const installCommandText = installCommand.join(" ");
202
+ const state = (status) => ({
203
+ status,
204
+ package: COMPUTE_SDK_PACKAGE,
205
+ installCommand: installCommandText
206
+ });
207
+ if (!packageJson) return state("skipped");
208
+ if (flags.install === false) return state("skipped");
209
+ let shouldInstall = flags.install === true;
210
+ if (!shouldInstall) {
211
+ if (!canPrompt(context) || context.flags.yes) return state("skipped");
212
+ try {
213
+ shouldInstall = await confirmPrompt({
214
+ input: context.runtime.stdin,
215
+ output: context.output.stderr,
216
+ signal: context.runtime.signal,
217
+ message: `Install ${COMPUTE_SDK_PACKAGE} for config types? (${installCommandText})`,
218
+ initialValue: true
219
+ });
220
+ } catch (error) {
221
+ if (isPromptCancelError(error)) return state("declined");
222
+ throw error;
223
+ }
224
+ if (!shouldInstall) return state("declined");
225
+ }
226
+ const command = resolveInitInstallCommandOverride(context) ?? installCommand;
227
+ if (!context.flags.quiet && !context.flags.json) context.output.stderr.write(`Installing ${COMPUTE_SDK_PACKAGE}...\n`);
228
+ try {
229
+ const [executable, ...args] = command;
230
+ await execa(executable, args, {
231
+ cwd,
232
+ env: context.runtime.env,
233
+ cancelSignal: context.runtime.signal,
234
+ stdin: "ignore"
235
+ });
236
+ return state("installed");
237
+ } catch (error) {
238
+ if (context.runtime.signal.aborted) throw error;
239
+ const detail = error instanceof Error ? error.message.split("\n")[0] : String(error);
240
+ hooks.onWarning(`Installing ${COMPUTE_SDK_PACKAGE} failed: ${detail}. Install it later with ${installCommandText}.`);
241
+ return state("failed");
242
+ }
243
+ }
244
+ function hasComputeSdkDependency(packageJson) {
245
+ for (const group of [packageJson?.dependencies, packageJson?.devDependencies]) if (group && typeof group === "object" && COMPUTE_SDK_PACKAGE in group) return true;
246
+ return false;
247
+ }
248
+ /** Test hook: JSON array command that replaces the real package-manager install. */
249
+ function resolveInitInstallCommandOverride(context) {
250
+ const raw = context.runtime.env.PRISMA_CLI_INIT_INSTALL_COMMAND;
251
+ if (!raw) return null;
252
+ try {
253
+ const parsed = JSON.parse(raw);
254
+ return Array.isArray(parsed) && parsed.every((p) => typeof p === "string") ? parsed : null;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+ const CUSTOM_BUILD_STUB = `
260
+ // framework "custom" deploys a prebuilt artifact. Add its build settings:
261
+ // build: {
262
+ // command: "npm run build",
263
+ // outputDirectory: "dist",
264
+ // entrypoint: "server.js",
265
+ // },
266
+ `;
267
+ /**
268
+ * Nearest existing compute config, searching from `cwd` up to the source
269
+ * root. Init routes on this: refuse, convert, or proceed fresh.
270
+ */
271
+ async function findExistingComputeConfig(cwd, signal) {
272
+ const configDir = await findComputeConfigDir(cwd, signal);
273
+ if (!configDir) return null;
274
+ return {
275
+ directory: configDir,
276
+ candidates: await findComputeConfigCandidates(configDir, signal)
277
+ };
278
+ }
279
+ function parseInitFormat(value, formatCommand) {
280
+ if (value === void 0) return {
281
+ value: "typescript",
282
+ explicit: false
283
+ };
284
+ const normalized = value.trim().toLowerCase();
285
+ if (normalized === "ts" || normalized === "typescript") return {
286
+ value: "typescript",
287
+ explicit: true
288
+ };
289
+ if (normalized === "json") return {
290
+ value: "json",
291
+ explicit: true
292
+ };
293
+ throw usageError("Unknown config format", `"${value}" is not a supported config format.`, "Pass --format ts or --format json.", [formatCommand([
294
+ "init",
295
+ "--format",
296
+ "json"
297
+ ])], "app");
298
+ }
299
+ /**
300
+ * Conversion transports the existing config's values; it never re-resolves
301
+ * settings. Refusing resolution flags beats silently ignoring them.
302
+ */
303
+ function rejectConversionResolutionFlags(flags, formatCommand) {
304
+ const passed = [
305
+ flags.framework !== void 0 ? "--framework" : null,
306
+ flags.entry !== void 0 ? "--entry" : null,
307
+ flags.httpPort !== void 0 ? "--http-port" : null,
308
+ flags.name !== void 0 ? "--name" : null,
309
+ flags.region !== void 0 ? "--region" : null
310
+ ].filter((flag) => flag !== null);
311
+ if (passed.length === 0) return;
312
+ throw usageError(`${passed.join(", ")} ${passed.length === 1 ? "does" : "do"} not apply when converting an existing config`, `--format ts with an existing ${COMPUTE_CONFIG_JSON_FILENAME} converts it as-is; settings are transported, never re-resolved.`, `Convert first, then edit ${COMPUTE_CONFIG_FILENAME} directly.`, [formatCommand([
313
+ "init",
314
+ "--format",
315
+ "ts"
316
+ ])], "app");
317
+ }
318
+ function initConvertIncompleteError(jsonConfigPath, tsConfigPath) {
319
+ return new CliError({
320
+ code: "INIT_CONVERT_INCOMPLETE",
321
+ domain: "app",
322
+ summary: "Conversion left two config files behind",
323
+ why: `${path.basename(tsConfigPath)} was written but ${path.basename(jsonConfigPath)} could not be deleted, and rolling back the write also failed. Commands refuse to load a directory with two config files.`,
324
+ fix: `Delete one file by hand: keep ${path.basename(tsConfigPath)} to finish the conversion, or keep ${path.basename(jsonConfigPath)} to undo it.`,
325
+ exitCode: 1,
326
+ nextSteps: [],
327
+ meta: {
328
+ jsonConfigPath,
329
+ tsConfigPath
330
+ }
331
+ });
332
+ }
333
+ function initConvertUnsupportedError(existingPath) {
334
+ return new CliError({
335
+ code: "INIT_CONVERT_UNSUPPORTED",
336
+ domain: "app",
337
+ summary: "TypeScript configs do not convert to JSON",
338
+ why: `${existingPath} may contain imports, expressions, or comments that the static ${COMPUTE_CONFIG_JSON_FILENAME} format cannot express, so an automatic conversion would be lossy.`,
339
+ fix: `If the config is fully static, rewrite it by hand as ${COMPUTE_CONFIG_JSON_FILENAME} and delete ${path.basename(existingPath)}.`,
340
+ exitCode: 1,
341
+ nextSteps: [],
342
+ meta: { existingConfigPath: existingPath }
343
+ });
344
+ }
345
+ /**
346
+ * The graduation path: an explicit `--format ts` with an existing
347
+ * `prisma.compute.json` rewrites the same config as `prisma.compute.ts` and
348
+ * deletes the JSON file, so a static config can grow into a programmatic one.
349
+ * The values are transported, never re-resolved.
350
+ */
351
+ async function runInitConversion(context, flags, jsonConfigPath, formatCommand) {
352
+ const cwd = context.runtime.cwd;
353
+ const signal = context.runtime.signal;
354
+ let parsed;
355
+ try {
356
+ parsed = JSON.parse(await readFile(jsonConfigPath, "utf8"));
357
+ } catch (error) {
358
+ if (signal.aborted) throw error;
359
+ throw initConvertInvalidError(jsonConfigPath, [error instanceof Error ? error.message.split("\n")[0] : String(error)]);
360
+ }
361
+ const config = stripJsonSchemaKey(parsed);
362
+ const normalized = normalizeComputeConfig(config, jsonConfigPath);
363
+ if (normalized.isErr()) throw initConvertInvalidError(jsonConfigPath, normalized.error.issues);
364
+ const loaded = normalized.value;
365
+ const tsConfigPath = path.join(loaded.configDir, COMPUTE_CONFIG_FILENAME);
366
+ const source = serializeComputeConfig(config);
367
+ signal.throwIfAborted();
368
+ try {
369
+ await writeFile(tsConfigPath, source, {
370
+ encoding: "utf8",
371
+ flag: "wx"
372
+ });
373
+ } catch (error) {
374
+ if (error.code === "EEXIST") throw initConfigExistsError(tsConfigPath);
375
+ throw error;
376
+ }
377
+ try {
378
+ await rm(jsonConfigPath);
379
+ } catch (error) {
380
+ try {
381
+ await rm(tsConfigPath, { force: true });
382
+ } catch {
383
+ throw initConvertIncompleteError(jsonConfigPath, tsConfigPath);
384
+ }
385
+ throw error;
386
+ }
387
+ const settings = conversionSettings(loaded);
388
+ renderInitSettingsPreview(context, settings);
389
+ const warnings = [];
390
+ const stepContext = withRuntimeCwd(context, loaded.configDir);
391
+ const types = await resolveInitTypes(stepContext, flags, { onWarning: (message) => warnings.push(message) });
392
+ const link = await resolveInitLink(stepContext, flags, {
393
+ onWarning: (message) => warnings.push(message),
394
+ formatCommand
395
+ });
396
+ warnings.push(...await maybePromptForAgentSetup(stepContext, loaded.configDir));
397
+ const unlinked = link.status !== "already-linked" && link.status !== "linked";
398
+ const typesMissing = types.status !== "installed" && types.status !== "already-installed";
399
+ return {
400
+ command: "init",
401
+ result: {
402
+ configPath: path.relative(cwd, tsConfigPath) || COMPUTE_CONFIG_FILENAME,
403
+ format: "typescript",
404
+ converted: true,
405
+ directory: formatInitDirectory(loaded.configDir),
406
+ app: conversionApp(loaded),
407
+ settings,
408
+ types,
409
+ link
410
+ },
411
+ warnings,
412
+ nextSteps: [
413
+ ...typesMissing && types.installCommand ? [types.installCommand] : [],
414
+ formatCommand(["app", "deploy"]),
415
+ ...unlinked ? [formatCommand(["project", "link"])] : []
416
+ ]
417
+ };
418
+ }
419
+ /** The same command context, acting from `cwd` instead of the invocation directory. */
420
+ function withRuntimeCwd(context, cwd) {
421
+ if (path.resolve(context.runtime.cwd) === path.resolve(cwd)) return context;
422
+ return {
423
+ ...context,
424
+ runtime: {
425
+ ...context.runtime,
426
+ cwd
427
+ }
428
+ };
429
+ }
430
+ function stripJsonSchemaKey(parsed) {
431
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
432
+ const { $schema: _schema, ...config } = parsed;
433
+ return config;
434
+ }
435
+ function initConvertInvalidError(jsonConfigPath, issues) {
436
+ return new CliError({
437
+ code: "COMPUTE_CONFIG_INVALID",
438
+ domain: "app",
439
+ summary: `Invalid ${path.basename(jsonConfigPath)}`,
440
+ why: issues.join(" "),
441
+ fix: `Fix ${path.basename(jsonConfigPath)} and rerun the conversion.`,
442
+ where: jsonConfigPath,
443
+ meta: {
444
+ configPath: jsonConfigPath,
445
+ issues
446
+ },
447
+ exitCode: 2,
448
+ nextSteps: []
449
+ });
450
+ }
451
+ /** Preview rows for a conversion; every value is sourced from the JSON file. */
452
+ function conversionSettings(loaded) {
453
+ const target = loaded.kind === "single" ? loaded.targets[0] : void 0;
454
+ if (!target) return [];
455
+ const source = COMPUTE_CONFIG_JSON_FILENAME;
456
+ return [
457
+ ...target.name ? [{
458
+ key: "app",
459
+ value: target.name,
460
+ source
461
+ }] : [],
462
+ ...target.framework ? [{
463
+ key: "framework",
464
+ value: frameworkByKey(target.framework).displayName,
465
+ source
466
+ }] : [],
467
+ ...target.entry ? [{
468
+ key: "entry",
469
+ value: target.entry,
470
+ source
471
+ }] : [],
472
+ ...target.httpPort !== null ? [{
473
+ key: "http port",
474
+ value: String(target.httpPort),
475
+ source
476
+ }] : [],
477
+ ...target.region ? [{
478
+ key: "region",
479
+ value: target.region,
480
+ source
481
+ }] : []
482
+ ];
483
+ }
484
+ /**
485
+ * App identity for the conversion result. Configs written by init pin all of
486
+ * name, framework, and httpPort; hand-written configs that omit any of them
487
+ * (or define multiple apps) report null instead of a partial identity.
488
+ */
489
+ function conversionApp(loaded) {
490
+ const target = loaded.kind === "single" ? loaded.targets[0] : void 0;
491
+ if (!target?.name || !target.framework || target.httpPort === null) return null;
492
+ return {
493
+ name: target.name,
494
+ framework: target.framework,
495
+ httpPort: target.httpPort,
496
+ ...target.entry ? { entry: target.entry } : {},
497
+ ...target.region ? { region: target.region } : {}
498
+ };
499
+ }
500
+ function initConfigExistsError(existingPath) {
501
+ return new CliError({
502
+ code: "INIT_CONFIG_EXISTS",
503
+ domain: "app",
504
+ summary: "A compute config already exists",
505
+ why: `${existingPath} already defines this repository's compute config, and init never overwrites or merges.`,
506
+ fix: "Edit the existing config instead, or delete it first if you want init to regenerate it.",
507
+ exitCode: 1,
508
+ nextSteps: [],
509
+ meta: { existingConfigPath: existingPath }
510
+ });
511
+ }
512
+ async function resolveInitFramework(context, flags, formatCommand) {
513
+ if (flags.framework) {
514
+ const framework = frameworkFromAlias(flags.framework.trim());
515
+ if (!framework) throw usageError("Unknown framework", `"${flags.framework}" is not a supported framework.`, `Pass one of: ${FRAMEWORKS.map((candidate) => candidate.key).join(", ")}.`, [formatCommand([
516
+ "init",
517
+ "--framework",
518
+ "hono"
519
+ ])], "app");
520
+ return {
521
+ key: framework.key,
522
+ displayName: framework.displayName,
523
+ source: "flag"
524
+ };
525
+ }
526
+ const detected = await detectDeployFramework(context.runtime.cwd, context.runtime.signal);
527
+ if (detected) return {
528
+ key: detected.key,
529
+ displayName: detected.displayName,
530
+ source: detected.annotation
531
+ };
532
+ if (canPrompt(context) && !context.flags.yes) {
533
+ const key = await selectPrompt({
534
+ input: context.runtime.stdin,
535
+ output: context.output.stderr,
536
+ signal: context.runtime.signal,
537
+ message: "Which framework does this app use?",
538
+ choices: FRAMEWORKS.map((framework) => ({
539
+ label: framework.displayName,
540
+ value: framework.key
541
+ }))
542
+ });
543
+ return {
544
+ key,
545
+ displayName: frameworkByKey(key).displayName,
546
+ source: "selected"
547
+ };
548
+ }
549
+ throw new CliError({
550
+ code: "INIT_DETECTION_FAILED",
551
+ domain: "app",
552
+ summary: "No supported framework detected",
553
+ why: "The directory has none of the framework signals init detects from, and no --framework was passed.",
554
+ fix: `Pass --framework with one of: ${FRAMEWORKS.map((framework) => framework.key).join(", ")}.`,
555
+ exitCode: 1,
556
+ nextSteps: FRAMEWORKS.slice(0, 3).map((framework) => formatCommand([
557
+ "init",
558
+ "--framework",
559
+ framework.key
560
+ ])),
561
+ meta: { frameworks: FRAMEWORKS.map((framework) => framework.key) }
562
+ });
563
+ }
564
+ async function resolveInitAppName(cwd, explicitName, signal, formatCommand) {
565
+ const trimmed = explicitName?.trim();
566
+ if (explicitName !== void 0 && !trimmed) throw usageError("App name required", "--name needs a non-empty value.", "Pass a non-empty app name.", [formatCommand([
567
+ "init",
568
+ "--name",
569
+ "api"
570
+ ])], "app");
571
+ if (trimmed) return {
572
+ value: trimmed,
573
+ source: "flag"
574
+ };
575
+ const packageJson = await readBunPackageJson(cwd, signal);
576
+ const packageName = typeof packageJson?.name === "string" ? packageJson.name.trim() : "";
577
+ if (packageName) return {
578
+ value: packageName,
579
+ source: "package.json"
580
+ };
581
+ return {
582
+ value: path.basename(cwd),
583
+ source: "directory name"
584
+ };
585
+ }
586
+ function parseInitHttpPort(value, formatCommand) {
587
+ if (value === void 0) return;
588
+ const port = Number(value.trim());
589
+ if (!Number.isInteger(port) || port < 1 || port > 65535) throw usageError("Invalid HTTP port", "--http-port must be an integer between 1 and 65535.", "Pass a valid port.", [formatCommand([
590
+ "init",
591
+ "--http-port",
592
+ "3000"
593
+ ])], "app");
594
+ return {
595
+ value: port,
596
+ source: "flag"
597
+ };
598
+ }
599
+ function parseInitRegion(value, formatCommand) {
600
+ if (value === void 0) return;
601
+ const trimmed = value.trim();
602
+ if (COMPUTE_REGIONS.includes(trimmed)) return trimmed;
603
+ throw usageError("Unknown region", `"${value}" is not a supported Compute region.`, `Pass one of: ${COMPUTE_REGIONS.join(", ")}.`, [formatCommand([
604
+ "init",
605
+ "--region",
606
+ "us-east-1"
607
+ ])], "app");
608
+ }
609
+ async function resolveInitEntry(cwd, resolvedFramework, explicitEntry, signal) {
610
+ const framework = frameworkByKey(resolvedFramework.key);
611
+ const trimmed = explicitEntry?.trim();
612
+ if (!framework.usesEntrypoint) {
613
+ if (trimmed) throw usageError("--entry is not supported for this framework", `${resolvedFramework.displayName} derives its entrypoint from build output; --entry applies only to frameworks that run a source entrypoint (Bun, Hono).`, "Drop --entry, or pass an entrypoint framework with --framework.", [], "app");
614
+ return;
615
+ }
616
+ if (trimmed) return {
617
+ value: trimmed,
618
+ source: "flag"
619
+ };
620
+ const packageEntrypoint = readBunPackageEntrypoint(await readBunPackageJson(cwd, signal));
621
+ if (packageEntrypoint) return {
622
+ value: packageEntrypoint,
623
+ source: "package.json"
624
+ };
625
+ }
626
+ async function maybeAdjustSettings(context, framework, httpPort, options) {
627
+ if (!canPrompt(context) || context.flags.yes) return {
628
+ framework,
629
+ httpPort
630
+ };
631
+ if (!await confirmPrompt({
632
+ input: context.runtime.stdin,
633
+ output: context.output.stderr,
634
+ signal: context.runtime.signal,
635
+ message: `Adjust these settings? (${framework.displayName}, HTTP ${httpPort.value})`,
636
+ initialValue: false
637
+ })) return {
638
+ framework,
639
+ httpPort
640
+ };
641
+ const key = await selectPrompt({
642
+ input: context.runtime.stdin,
643
+ output: context.output.stderr,
644
+ signal: context.runtime.signal,
645
+ message: "Framework",
646
+ choices: FRAMEWORKS.map((candidate) => ({
647
+ label: candidate.key === framework.key ? `${candidate.displayName} (current)` : candidate.displayName,
648
+ value: candidate.key
649
+ }))
650
+ });
651
+ const nextFramework = key === framework.key ? framework : {
652
+ key,
653
+ displayName: frameworkByKey(key).displayName,
654
+ source: "selected"
655
+ };
656
+ const defaultPort = options.portExplicit ? httpPort.value : defaultHttpPortForBuildType(frameworkByKey(key).buildType);
657
+ const portText = await textPrompt({
658
+ input: context.runtime.stdin,
659
+ output: context.output.stderr,
660
+ signal: context.runtime.signal,
661
+ message: "HTTP port",
662
+ placeholder: String(defaultPort),
663
+ validate: (value) => {
664
+ if (!value?.trim()) return;
665
+ const port = Number(value.trim());
666
+ return Number.isInteger(port) && port >= 1 && port <= 65535 ? void 0 : "Enter a port between 1 and 65535.";
667
+ }
668
+ });
669
+ return {
670
+ framework: nextFramework,
671
+ httpPort: portText.trim() ? {
672
+ value: Number(portText.trim()),
673
+ source: "selected"
674
+ } : {
675
+ value: defaultPort,
676
+ source: options.portExplicit ? httpPort.source : "framework default"
677
+ }
678
+ };
679
+ }
680
+ function renderInitSettingsPreview(context, settings) {
681
+ if (context.flags.quiet || context.flags.json) return;
682
+ const keyWidth = Math.max(...settings.map((row) => row.key.length));
683
+ const valueWidth = Math.max(...settings.map((row) => row.value.length));
684
+ const lines = settings.map((row) => ` ${row.key.padEnd(keyWidth)} ${row.value.padEnd(valueWidth)} ${context.ui.dim(row.source)}`);
685
+ context.output.stderr.write(`${lines.join("\n")}\n\n`);
686
+ }
687
+ async function resolveInitLink(context, flags, hooks) {
688
+ const pin = await readLocalResolutionPin(context.runtime.cwd, context.runtime.signal);
689
+ if (pin.isOk() && pin.value.kind === "present") return {
690
+ status: "already-linked",
691
+ project: null
692
+ };
693
+ if (flags.link === false) return {
694
+ status: "skipped",
695
+ project: null
696
+ };
697
+ const explicitProject = flags.project?.trim();
698
+ let shouldLink = Boolean(explicitProject) || flags.link === true;
699
+ if (!shouldLink) {
700
+ if (!canPrompt(context) || context.flags.yes) return {
701
+ status: "skipped",
702
+ project: null
703
+ };
704
+ try {
705
+ shouldLink = await confirmPrompt({
706
+ input: context.runtime.stdin,
707
+ output: context.output.stderr,
708
+ signal: context.runtime.signal,
709
+ message: "Link this directory to a Prisma Project now?",
710
+ initialValue: true
711
+ });
712
+ } catch (error) {
713
+ if (isPromptCancelError(error)) return {
714
+ status: "declined",
715
+ project: null
716
+ };
717
+ throw error;
718
+ }
719
+ if (!shouldLink) return {
720
+ status: "declined",
721
+ project: null
722
+ };
723
+ }
724
+ try {
725
+ const linked = await runProjectLink(context, explicitProject || void 0);
726
+ return {
727
+ status: "linked",
728
+ project: {
729
+ id: linked.result.project.id,
730
+ name: linked.result.project.name
731
+ }
732
+ };
733
+ } catch (error) {
734
+ if (error instanceof CliError) {
735
+ if (isPromptCancelError(error)) return {
736
+ status: "declined",
737
+ project: null
738
+ };
739
+ hooks.onWarning(`Project link failed: ${error.summary}. Link later with ${hooks.formatCommand(["project", "link"])}.`);
740
+ return {
741
+ status: "failed",
742
+ project: null
743
+ };
744
+ }
745
+ throw error;
746
+ }
747
+ }
748
+ function formatInitDirectory(cwd) {
749
+ const basename = path.basename(cwd);
750
+ return basename ? `./${basename}` : ".";
751
+ }
752
+ //#endregion
753
+ export { runInit };