@prisma/cli 3.0.0-dev.119.1 → 3.0.0-dev.121.1

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.
@@ -8,7 +8,7 @@ import { Command, Option } from "commander";
8
8
  //#region src/commands/init/index.ts
9
9
  function createInitCommand(runtime) {
10
10
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("init"), runtime), "init");
11
- command.addOption(new Option("--framework <framework>", "Framework override; detected when omitted")).addOption(new Option("--entry <path>", "Source entrypoint for entrypoint frameworks (Bun, Hono)")).addOption(new Option("--http-port <port>", "HTTP port the app listens on")).addOption(new Option("--region <region>", "Region used when deploy creates the app")).addOption(new Option("--name <app-name>", "App name")).addOption(new Option("--link", "Link this directory to a Project")).addOption(new Option("--no-link", "Skip the Project link step")).addOption(new Option("--project <id-or-name>", "Project to link this directory to")).addOption(new Option("--install", "Install @prisma/compute-sdk for config types")).addOption(new Option("--no-install", "Skip the types install step"));
11
+ command.addOption(new Option("--framework <framework>", "Framework override; detected when omitted")).addOption(new Option("--entry <path>", "Source entrypoint for entrypoint frameworks (Bun, Hono)")).addOption(new Option("--http-port <port>", "HTTP port the app listens on")).addOption(new Option("--region <region>", "Region used when deploy creates the app")).addOption(new Option("--name <app-name>", "App name")).addOption(new Option("--link", "Link this directory to a Project")).addOption(new Option("--no-link", "Skip the Project link step")).addOption(new Option("--project <id-or-name>", "Project to link this directory to")).addOption(new Option("--install", "Install @prisma/compute-sdk for config types")).addOption(new Option("--no-install", "Skip the types install step")).addOption(new Option("--format <ts|json>", "Config file format: ts (default) writes prisma.compute.ts, json writes dependency-free prisma.compute.json; --format ts converts an existing prisma.compute.json"));
12
12
  addGlobalFlags(command);
13
13
  command.action(async (options) => {
14
14
  const flags = options;
@@ -20,7 +20,8 @@ function createInitCommand(runtime) {
20
20
  name: flags.name,
21
21
  link: flags.link,
22
22
  project: flags.project,
23
- install: flags.install
23
+ install: flags.install,
24
+ format: flags.format
24
25
  }), {
25
26
  renderHuman: (context, descriptor, result) => renderInit(context, descriptor, result),
26
27
  renderJson: (result) => serializeInit(result)
@@ -9,14 +9,30 @@ import { runProjectLink } from "./project.js";
9
9
  import { detectDeployFramework } from "./app.js";
10
10
  import { execa } from "execa";
11
11
  import path from "node:path";
12
- import { writeFile } from "node:fs/promises";
13
- import { COMPUTE_CONFIG_FILENAME, COMPUTE_REGIONS, FRAMEWORKS, defaultHttpPortForBuildType, findComputeConfigCandidates, findComputeConfigDir, frameworkByKey, frameworkFromAlias, serializeComputeConfig } from "@prisma/compute-sdk/config";
12
+ import { readFile, rm, writeFile } from "node:fs/promises";
13
+ import { COMPUTE_CONFIG_FILENAME, COMPUTE_CONFIG_JSON_FILENAME, COMPUTE_REGIONS, FRAMEWORKS, defaultHttpPortForBuildType, findComputeConfigCandidates, findComputeConfigDir, frameworkByKey, frameworkFromAlias, normalizeComputeConfig, serializeComputeConfig, serializeComputeConfigJson } from "@prisma/compute-sdk/config";
14
14
  //#region src/controllers/init.ts
15
15
  async function runInit(context, flags) {
16
16
  const cwd = context.runtime.cwd;
17
17
  const signal = context.runtime.signal;
18
18
  const formatCommand = resolvePrismaCliPackageCommandFormatterSync(cwd);
19
- await requireNoExistingComputeConfig(cwd, signal);
19
+ const format = parseInitFormat(flags.format, formatCommand);
20
+ 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([
21
+ "init",
22
+ "--format",
23
+ "json"
24
+ ])], "app");
25
+ const existingConfig = await findExistingComputeConfig(cwd, signal);
26
+ if (existingConfig) {
27
+ const solePath = existingConfig.candidates.length === 1 ? existingConfig.candidates[0] : void 0;
28
+ const soleIsJson = solePath !== void 0 && path.extname(solePath) === ".json";
29
+ if (soleIsJson && format.value === "typescript" && format.explicit) {
30
+ rejectConversionResolutionFlags(flags, formatCommand);
31
+ return runInitConversion(context, flags, solePath, formatCommand);
32
+ }
33
+ if (solePath && !soleIsJson && format.value === "json") throw initConvertUnsupportedError(solePath);
34
+ throw initConfigExistsError(existingConfig.candidates[0] ?? existingConfig.directory);
35
+ }
20
36
  const region = parseInitRegion(flags.region, formatCommand);
21
37
  let framework = await resolveInitFramework(context, flags, formatCommand);
22
38
  const name = await resolveInitAppName(cwd, flags.name, signal, formatCommand);
@@ -27,6 +43,11 @@ async function runInit(context, flags) {
27
43
  const adjusted = await maybeAdjustSettings(context, framework, httpPort, { portExplicit: flags.httpPort !== void 0 });
28
44
  framework = adjusted.framework;
29
45
  httpPort = adjusted.httpPort;
46
+ 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([
47
+ "init",
48
+ "--framework",
49
+ "custom"
50
+ ])], "app");
30
51
  const entry = await resolveInitEntry(cwd, framework, flags.entry, signal);
31
52
  const settings = [
32
53
  {
@@ -63,9 +84,14 @@ async function runInit(context, flags) {
63
84
  ...entry ? { entry: entry.value } : {},
64
85
  ...region ? { region } : {}
65
86
  } };
66
- const configPath = path.join(cwd, COMPUTE_CONFIG_FILENAME);
67
- let source = serializeComputeConfig(config);
68
- if (framework.key === "custom") source += CUSTOM_BUILD_STUB;
87
+ const configFilename = format.value === "json" ? COMPUTE_CONFIG_JSON_FILENAME : COMPUTE_CONFIG_FILENAME;
88
+ const configPath = path.join(cwd, configFilename);
89
+ let source;
90
+ if (format.value === "json") source = serializeComputeConfigJson(config);
91
+ else {
92
+ source = serializeComputeConfig(config);
93
+ if (framework.key === "custom") source += CUSTOM_BUILD_STUB;
94
+ }
69
95
  signal.throwIfAborted();
70
96
  try {
71
97
  await writeFile(configPath, source, {
@@ -77,7 +103,11 @@ async function runInit(context, flags) {
77
103
  throw error;
78
104
  }
79
105
  const warnings = [];
80
- const types = await resolveInitTypes(context, flags, { onWarning: (message) => warnings.push(message) });
106
+ const types = format.value === "json" ? {
107
+ status: "skipped",
108
+ package: COMPUTE_SDK_PACKAGE,
109
+ installCommand: null
110
+ } : await resolveInitTypes(context, flags, { onWarning: (message) => warnings.push(message) });
81
111
  const link = await resolveInitLink(context, flags, {
82
112
  onWarning: (message) => warnings.push(message),
83
113
  formatCommand
@@ -87,7 +117,9 @@ async function runInit(context, flags) {
87
117
  return {
88
118
  command: "init",
89
119
  result: {
90
- configPath: COMPUTE_CONFIG_FILENAME,
120
+ configPath: configFilename,
121
+ format: format.value,
122
+ converted: false,
91
123
  directory: formatInitDirectory(cwd),
92
124
  app: {
93
125
  name: name.value,
@@ -230,10 +262,237 @@ const CUSTOM_BUILD_STUB = `
230
262
  // entrypoint: "server.js",
231
263
  // },
232
264
  `;
233
- async function requireNoExistingComputeConfig(cwd, signal) {
265
+ /**
266
+ * Nearest existing compute config, searching from `cwd` up to the source
267
+ * root. Init routes on this: refuse, convert, or proceed fresh.
268
+ */
269
+ async function findExistingComputeConfig(cwd, signal) {
234
270
  const configDir = await findComputeConfigDir(cwd, signal);
235
- if (!configDir) return;
236
- throw initConfigExistsError((await findComputeConfigCandidates(configDir, signal))[0] ?? configDir);
271
+ if (!configDir) return null;
272
+ return {
273
+ directory: configDir,
274
+ candidates: await findComputeConfigCandidates(configDir, signal)
275
+ };
276
+ }
277
+ function parseInitFormat(value, formatCommand) {
278
+ if (value === void 0) return {
279
+ value: "typescript",
280
+ explicit: false
281
+ };
282
+ const normalized = value.trim().toLowerCase();
283
+ if (normalized === "ts" || normalized === "typescript") return {
284
+ value: "typescript",
285
+ explicit: true
286
+ };
287
+ if (normalized === "json") return {
288
+ value: "json",
289
+ explicit: true
290
+ };
291
+ throw usageError("Unknown config format", `"${value}" is not a supported config format.`, "Pass --format ts or --format json.", [formatCommand([
292
+ "init",
293
+ "--format",
294
+ "json"
295
+ ])], "app");
296
+ }
297
+ /**
298
+ * Conversion transports the existing config's values; it never re-resolves
299
+ * settings. Refusing resolution flags beats silently ignoring them.
300
+ */
301
+ function rejectConversionResolutionFlags(flags, formatCommand) {
302
+ const passed = [
303
+ flags.framework !== void 0 ? "--framework" : null,
304
+ flags.entry !== void 0 ? "--entry" : null,
305
+ flags.httpPort !== void 0 ? "--http-port" : null,
306
+ flags.name !== void 0 ? "--name" : null,
307
+ flags.region !== void 0 ? "--region" : null
308
+ ].filter((flag) => flag !== null);
309
+ if (passed.length === 0) return;
310
+ 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([
311
+ "init",
312
+ "--format",
313
+ "ts"
314
+ ])], "app");
315
+ }
316
+ function initConvertIncompleteError(jsonConfigPath, tsConfigPath) {
317
+ return new CliError({
318
+ code: "INIT_CONVERT_INCOMPLETE",
319
+ domain: "app",
320
+ summary: "Conversion left two config files behind",
321
+ 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.`,
322
+ fix: `Delete one file by hand: keep ${path.basename(tsConfigPath)} to finish the conversion, or keep ${path.basename(jsonConfigPath)} to undo it.`,
323
+ exitCode: 1,
324
+ nextSteps: [],
325
+ meta: {
326
+ jsonConfigPath,
327
+ tsConfigPath
328
+ }
329
+ });
330
+ }
331
+ function initConvertUnsupportedError(existingPath) {
332
+ return new CliError({
333
+ code: "INIT_CONVERT_UNSUPPORTED",
334
+ domain: "app",
335
+ summary: "TypeScript configs do not convert to JSON",
336
+ 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.`,
337
+ fix: `If the config is fully static, rewrite it by hand as ${COMPUTE_CONFIG_JSON_FILENAME} and delete ${path.basename(existingPath)}.`,
338
+ exitCode: 1,
339
+ nextSteps: [],
340
+ meta: { existingConfigPath: existingPath }
341
+ });
342
+ }
343
+ /**
344
+ * The graduation path: an explicit `--format ts` with an existing
345
+ * `prisma.compute.json` rewrites the same config as `prisma.compute.ts` and
346
+ * deletes the JSON file, so a static config can grow into a programmatic one.
347
+ * The values are transported, never re-resolved.
348
+ */
349
+ async function runInitConversion(context, flags, jsonConfigPath, formatCommand) {
350
+ const cwd = context.runtime.cwd;
351
+ const signal = context.runtime.signal;
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse(await readFile(jsonConfigPath, "utf8"));
355
+ } catch (error) {
356
+ if (signal.aborted) throw error;
357
+ throw initConvertInvalidError(jsonConfigPath, [error instanceof Error ? error.message.split("\n")[0] : String(error)]);
358
+ }
359
+ const config = stripJsonSchemaKey(parsed);
360
+ const normalized = normalizeComputeConfig(config, jsonConfigPath);
361
+ if (normalized.isErr()) throw initConvertInvalidError(jsonConfigPath, normalized.error.issues);
362
+ const loaded = normalized.value;
363
+ const tsConfigPath = path.join(loaded.configDir, COMPUTE_CONFIG_FILENAME);
364
+ const source = serializeComputeConfig(config);
365
+ signal.throwIfAborted();
366
+ try {
367
+ await writeFile(tsConfigPath, source, {
368
+ encoding: "utf8",
369
+ flag: "wx"
370
+ });
371
+ } catch (error) {
372
+ if (error.code === "EEXIST") throw initConfigExistsError(tsConfigPath);
373
+ throw error;
374
+ }
375
+ try {
376
+ await rm(jsonConfigPath);
377
+ } catch (error) {
378
+ try {
379
+ await rm(tsConfigPath, { force: true });
380
+ } catch {
381
+ throw initConvertIncompleteError(jsonConfigPath, tsConfigPath);
382
+ }
383
+ throw error;
384
+ }
385
+ const settings = conversionSettings(loaded);
386
+ renderInitSettingsPreview(context, settings);
387
+ const warnings = [];
388
+ const stepContext = withRuntimeCwd(context, loaded.configDir);
389
+ const types = await resolveInitTypes(stepContext, flags, { onWarning: (message) => warnings.push(message) });
390
+ const link = await resolveInitLink(stepContext, flags, {
391
+ onWarning: (message) => warnings.push(message),
392
+ formatCommand
393
+ });
394
+ const unlinked = link.status !== "already-linked" && link.status !== "linked";
395
+ const typesMissing = types.status !== "installed" && types.status !== "already-installed";
396
+ return {
397
+ command: "init",
398
+ result: {
399
+ configPath: path.relative(cwd, tsConfigPath) || COMPUTE_CONFIG_FILENAME,
400
+ format: "typescript",
401
+ converted: true,
402
+ directory: formatInitDirectory(loaded.configDir),
403
+ app: conversionApp(loaded),
404
+ settings,
405
+ types,
406
+ link
407
+ },
408
+ warnings,
409
+ nextSteps: [
410
+ ...typesMissing && types.installCommand ? [types.installCommand] : [],
411
+ formatCommand(["app", "deploy"]),
412
+ ...unlinked ? [formatCommand(["project", "link"])] : []
413
+ ]
414
+ };
415
+ }
416
+ /** The same command context, acting from `cwd` instead of the invocation directory. */
417
+ function withRuntimeCwd(context, cwd) {
418
+ if (path.resolve(context.runtime.cwd) === path.resolve(cwd)) return context;
419
+ return {
420
+ ...context,
421
+ runtime: {
422
+ ...context.runtime,
423
+ cwd
424
+ }
425
+ };
426
+ }
427
+ function stripJsonSchemaKey(parsed) {
428
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return parsed;
429
+ const { $schema: _schema, ...config } = parsed;
430
+ return config;
431
+ }
432
+ function initConvertInvalidError(jsonConfigPath, issues) {
433
+ return new CliError({
434
+ code: "COMPUTE_CONFIG_INVALID",
435
+ domain: "app",
436
+ summary: `Invalid ${path.basename(jsonConfigPath)}`,
437
+ why: issues.join(" "),
438
+ fix: `Fix ${path.basename(jsonConfigPath)} and rerun the conversion.`,
439
+ where: jsonConfigPath,
440
+ meta: {
441
+ configPath: jsonConfigPath,
442
+ issues
443
+ },
444
+ exitCode: 2,
445
+ nextSteps: []
446
+ });
447
+ }
448
+ /** Preview rows for a conversion; every value is sourced from the JSON file. */
449
+ function conversionSettings(loaded) {
450
+ const target = loaded.kind === "single" ? loaded.targets[0] : void 0;
451
+ if (!target) return [];
452
+ const source = COMPUTE_CONFIG_JSON_FILENAME;
453
+ return [
454
+ ...target.name ? [{
455
+ key: "app",
456
+ value: target.name,
457
+ source
458
+ }] : [],
459
+ ...target.framework ? [{
460
+ key: "framework",
461
+ value: frameworkByKey(target.framework).displayName,
462
+ source
463
+ }] : [],
464
+ ...target.entry ? [{
465
+ key: "entry",
466
+ value: target.entry,
467
+ source
468
+ }] : [],
469
+ ...target.httpPort !== null ? [{
470
+ key: "http port",
471
+ value: String(target.httpPort),
472
+ source
473
+ }] : [],
474
+ ...target.region ? [{
475
+ key: "region",
476
+ value: target.region,
477
+ source
478
+ }] : []
479
+ ];
480
+ }
481
+ /**
482
+ * App identity for the conversion result. Configs written by init pin all of
483
+ * name, framework, and httpPort; hand-written configs that omit any of them
484
+ * (or define multiple apps) report null instead of a partial identity.
485
+ */
486
+ function conversionApp(loaded) {
487
+ const target = loaded.kind === "single" ? loaded.targets[0] : void 0;
488
+ if (!target?.name || !target.framework || target.httpPort === null) return null;
489
+ return {
490
+ name: target.name,
491
+ framework: target.framework,
492
+ httpPort: target.httpPort,
493
+ ...target.entry ? { entry: target.entry } : {},
494
+ ...target.region ? { region: target.region } : {}
495
+ };
237
496
  }
238
497
  function initConfigExistsError(existingPath) {
239
498
  return new CliError({
@@ -1,10 +1,11 @@
1
1
  import { resolvePrismaCliPackageCommandFormatterSync } from "../lib/agent/cli-command.js";
2
2
  import { renderNextSteps, renderSummaryLine } from "../shell/ui.js";
3
+ import { COMPUTE_CONFIG_JSON_FILENAME } from "@prisma/compute-sdk/config";
3
4
  //#region src/presenters/init.ts
4
5
  function renderInit(context, _descriptor, result) {
5
6
  const ui = context.ui;
6
7
  const formatCommand = resolvePrismaCliPackageCommandFormatterSync(context.runtime.cwd);
7
- const lines = [renderSummaryLine(ui, "success", `Wrote ${result.configPath}`)];
8
+ const lines = [renderSummaryLine(ui, "success", result.converted ? `Converted ${COMPUTE_CONFIG_JSON_FILENAME} to ${result.configPath}` : `Wrote ${result.configPath}`)];
8
9
  if (result.types.status === "installed") lines.push(renderSummaryLine(ui, "success", `Installed ${result.types.package} (config types)`));
9
10
  else if ((result.types.status === "skipped" || result.types.status === "declined") && result.types.installCommand) lines.push(` ${ui.dim(`For editor types: ${result.types.installCommand}`)}`);
10
11
  switch (result.link.status) {
@@ -23,7 +23,7 @@ const DESCRIPTORS = [
23
23
  {
24
24
  id: "init",
25
25
  path: ["prisma", "init"],
26
- description: "Write a committed prisma.compute.ts for this app",
26
+ description: "Write a committed compute config for this app",
27
27
  examples: (runtime) => agentCommandExamples(runtime, [
28
28
  ["init"],
29
29
  [
@@ -33,6 +33,11 @@ const DESCRIPTORS = [
33
33
  "--entry",
34
34
  "src/index.ts"
35
35
  ],
36
+ [
37
+ "init",
38
+ "--format",
39
+ "json"
40
+ ],
36
41
  ["init", "--no-link"]
37
42
  ])
38
43
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.119.1",
3
+ "version": "3.0.0-dev.121.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "dependencies": {
46
46
  "@clack/prompts": "^1.5.0",
47
- "@prisma/compute-sdk": "0.31.0",
47
+ "@prisma/compute-sdk": "0.33.0",
48
48
  "@prisma/credentials-store": "^7.8.0",
49
49
  "@prisma/management-api-sdk": "^1.46.0",
50
50
  "better-result": "^2.9.2",