@prisma/cli 3.0.0-dev.82.1 → 3.0.0-dev.83.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.
@@ -1,8 +1,8 @@
1
1
  import { getAuthFilePath } from "../lib/auth/client.js";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { CredentialsStore } from "@prisma/credentials-store";
5
4
  import { randomUUID } from "node:crypto";
5
+ import { CredentialsStore } from "@prisma/credentials-store";
6
6
  //#region src/adapters/token-storage.ts
7
7
  function findLatestValidTokens(allCredentials) {
8
8
  for (let i = allCredentials.length - 1; i >= 0; i -= 1) {
@@ -49,7 +49,7 @@ var FileTokenStorage = class {
49
49
  const all = await this.credentialsStore.getCredentials();
50
50
  this.signal?.throwIfAborted();
51
51
  return findLatestValidTokens(all);
52
- } catch (error) {
52
+ } catch (_error) {
53
53
  if (this.signal?.aborted) throw this.signal.reason;
54
54
  return null;
55
55
  }
package/dist/cli.js CHANGED
@@ -3,22 +3,22 @@ import { runUpdateDiscoveryWorker } from "./shell/update-check.js";
3
3
  import { runCli } from "./cli2.js";
4
4
  import process from "node:process";
5
5
  //#region src/bin.ts
6
- if (process.env.PRISMA_CLI_RUN_UPDATE_CHECK_WORKER === "1") runUpdateDiscoveryWorker().then(() => {
6
+ if (process.env.PRISMA_CLI_RUN_UPDATE_CHECK_WORKER === "1") {
7
+ await runUpdateDiscoveryWorker();
7
8
  process.exitCode = 0;
8
- });
9
- else {
9
+ } else {
10
10
  const controller = new AbortController();
11
11
  const abortCli = () => {
12
12
  if (!controller.signal.aborted) controller.abort(new DOMException("Command canceled", "AbortError"));
13
13
  };
14
14
  process.once("SIGINT", abortCli);
15
15
  process.once("SIGTERM", abortCli);
16
- runCli({ signal: controller.signal }).then((exitCode) => {
17
- process.exitCode = exitCode;
18
- }).finally(() => {
16
+ try {
17
+ process.exitCode = await runCli({ signal: controller.signal });
18
+ } finally {
19
19
  process.off("SIGINT", abortCli);
20
20
  process.off("SIGTERM", abortCli);
21
- });
21
+ }
22
22
  }
23
23
  //#endregion
24
24
  export {};
package/dist/cli2.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import { CliError } from "./shell/errors.js";
2
- import { createShellUi } from "./shell/ui.js";
3
- import { formatUnexpectedError, writeHumanError, writeJsonError, writeJsonSuccess } from "./shell/output.js";
2
+ import "./shell/prompt.js";
4
3
  import { attachCommandDescriptor } from "./shell/command-meta.js";
5
4
  import { addCompactGlobalFlags } from "./shell/global-flags.js";
5
+ import { createShellUi } from "./shell/ui.js";
6
6
  import { configureRuntimeCommand, createCommandContext } from "./shell/runtime.js";
7
- import "./shell/prompt.js";
7
+ import { formatUnexpectedError, writeHumanError, writeJsonError, writeJsonSuccess } from "./shell/output.js";
8
8
  import { createAppCommand } from "./commands/app/index.js";
9
9
  import { createAuthCommand } from "./commands/auth/index.js";
10
10
  import { createBranchCommand } from "./commands/branch/index.js";
@@ -1,8 +1,8 @@
1
1
  import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
- import { requireComputeAuth } from "../lib/auth/guard.js";
2
+ import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
3
3
  import { formatScopeLabel, parseKeyValuePositional, resolveEnvScope } from "../lib/app/env-config.js";
4
4
  import { readEnvFileAssignments } from "../lib/app/env-file.js";
5
- import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
5
+ import { requireComputeAuth } from "../lib/auth/guard.js";
6
6
  import { readLocalGitBranch } from "../lib/git/local-branch.js";
7
7
  import { requireAuthenticatedAuthState } from "./auth.js";
8
8
  import { listRealWorkspaceProjects } from "./project.js";
@@ -204,12 +204,13 @@ async function requireClientAndProject(context, explicitProject, commandName) {
204
204
  const authState = await requireAuthenticatedAuthState(context);
205
205
  const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
206
206
  if (!client) throw authRequiredError(["prisma-cli auth login"]);
207
- if (!authState.workspace) throw workspaceRequiredError();
207
+ const workspace = authState.workspace;
208
+ if (!workspace) throw workspaceRequiredError();
208
209
  const targetResult = await resolveProjectTarget({
209
210
  context,
210
- workspace: authState.workspace,
211
+ workspace,
211
212
  explicitProject,
212
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace, context.runtime.signal),
213
+ listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
213
214
  commandName
214
215
  });
215
216
  if (targetResult.isErr()) throw projectResolutionErrorToCliError(targetResult.error);
@@ -218,7 +219,7 @@ async function requireClientAndProject(context, explicitProject, commandName) {
218
219
  client,
219
220
  projectId: target.project.id,
220
221
  verboseContext: {
221
- workspace: authState.workspace,
222
+ workspace,
222
223
  project: target.project,
223
224
  resolution: target.resolution
224
225
  }
@@ -1,37 +1,37 @@
1
1
  import { SERVICE_TOKEN_ENV_VAR, getApiBaseUrl } from "../lib/auth/client.js";
2
2
  import { FileTokenStorage } from "../adapters/token-storage.js";
3
3
  import { CliError, authRequiredError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors.js";
4
+ import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt.js";
4
5
  import { renderCommandHeader } from "../shell/ui.js";
5
- import { writeJsonEvent } from "../shell/output.js";
6
6
  import { canPrompt } from "../shell/runtime.js";
7
- import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt.js";
8
- import { requireComputeAuth } from "../lib/auth/guard.js";
9
- import { readAuthState } from "../lib/auth/auth-ops.js";
10
- import { envVarNames, parseEnvInputs } from "../lib/app/env-vars.js";
11
- import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output.js";
12
- import { readBunPackageEntrypoint, readBunPackageJson } from "../lib/app/bun-project.js";
13
- import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.js";
14
- import { formatCommandArgument } from "../shell/command-arguments.js";
15
7
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "../lib/project/local-pin.js";
8
+ import { formatCommandArgument } from "../shell/command-arguments.js";
16
9
  import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, projectResolutionErrorToCliError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
17
10
  import { bindProjectToDirectory, projectCreateFailedError, projectDirectoryBindingErrorToCliError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
18
- import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
19
- import { readLocalGitBranch } from "../lib/git/local-branch.js";
11
+ import { maybeSetupBranchDatabase } from "../lib/app/branch-database-deploy.js";
12
+ import { readBunPackageEntrypoint, readBunPackageJson } from "../lib/app/bun-project.js";
13
+ import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output.js";
14
+ import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
15
+ import { envVarNames, parseEnvInputs } from "../lib/app/env-vars.js";
16
+ import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.js";
20
17
  import { resolveOrCreatePreviewBuildSettings } from "../lib/app/preview-build-settings.js";
21
18
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
22
19
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
23
- import { maybeSetupBranchDatabase } from "../lib/app/branch-database-deploy.js";
24
20
  import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
25
21
  import { PreviewDomainApiError, createPreviewAppProvider } from "../lib/app/preview-provider.js";
26
22
  import { enforceProductionDeployGate } from "../lib/app/production-deploy-gate.js";
27
- import { formatDomainFailureFix } from "../lib/app/domain-guidance.js";
23
+ import { requireComputeAuth } from "../lib/auth/guard.js";
24
+ import { readAuthState } from "../lib/auth/auth-ops.js";
25
+ import { readLocalGitBranch } from "../lib/git/local-branch.js";
26
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
27
+ import { writeJsonEvent } from "../shell/output.js";
28
28
  import { createSelectPromptPort } from "./select-prompt-port.js";
29
29
  import { requireAuthenticatedAuthState } from "./auth.js";
30
30
  import { listRealWorkspaceProjects } from "./project.js";
31
31
  import { access, readFile } from "node:fs/promises";
32
32
  import path from "node:path";
33
- import open from "open";
34
33
  import { Result, matchError } from "better-result";
34
+ import open from "open";
35
35
  //#region src/controllers/app.ts
36
36
  const DEPLOY_FRAMEWORKS = [
37
37
  "nextjs",
@@ -910,51 +910,72 @@ async function confirmDomainRemoval(context, target, hostname) {
910
910
  })) throw usageError("Custom domain removal canceled", "The command was canceled before the domain was detached.", "Rerun the command and confirm removal, or pass --yes.", [`prisma-cli app domain remove ${hostname} --app ${target.app.name} --yes`], "app");
911
911
  }
912
912
  function domainCommandError(command, error, hostname) {
913
- if (error instanceof PreviewDomainApiError) {
914
- if (command === "add" && (error.status === 400 || error.status === 422) && isDomainDnsError(error)) return domainDnsNotConfiguredError(hostname, error);
915
- if (command === "add" && error.status === 400) return new CliError({
916
- code: "DOMAIN_HOSTNAME_INVALID",
917
- domain: "app",
918
- summary: `Invalid custom domain "${hostname}"`,
919
- why: error.message,
920
- fix: "Pass a valid hostname like shop.acme.com and make sure DNS can be verified.",
921
- debug: formatDebugDetails(error),
922
- exitCode: 2,
923
- nextSteps: ["prisma-cli app domain add shop.acme.com"]
924
- });
925
- if (command === "add" && (error.status === 429 || isDomainQuotaError(error))) return new CliError({
926
- code: "DOMAIN_QUOTA_EXCEEDED",
927
- domain: "app",
928
- summary: "Custom domain quota exceeded",
929
- why: error.message,
930
- fix: "Remove an existing custom domain before adding another one.",
931
- debug: formatDebugDetails(error),
932
- exitCode: 1,
933
- nextSteps: ["prisma-cli app domain remove <hostname>"]
934
- });
935
- if (command === "add" && error.status === 409) return domainAlreadyRegisteredError(hostname, error);
936
- if (command === "add" && error.status === 422) return new CliError({
937
- code: "NO_DEPLOYMENTS",
938
- domain: "app",
939
- summary: "Custom domain requires a live production deployment",
940
- why: "The selected production app does not have a promoted version that can receive a custom domain.",
941
- fix: "Deploy the app to the production branch, then rerun the domain command.",
942
- debug: formatDebugDetails(error),
943
- exitCode: 1,
944
- nextSteps: ["prisma-cli app deploy --branch production", `prisma-cli app domain add ${hostname}`]
945
- });
946
- if ((command === "show" || command === "remove" || command === "retry" || command === "wait") && error.status === 404) return domainNotFoundError(hostname);
947
- if (command === "retry" && error.status === 409) return new CliError({
948
- code: "DOMAIN_RETRY_NOT_ELIGIBLE",
949
- domain: "app",
950
- summary: `Custom domain "${hostname}" is not eligible for retry`,
951
- why: error.message,
952
- fix: "Wait for the current verification or TLS step to finish, then rerun retry if the domain fails.",
953
- debug: formatDebugDetails(error),
954
- exitCode: 1,
955
- nextSteps: [`prisma-cli app domain show ${hostname}`]
956
- });
957
- }
913
+ if (error instanceof PreviewDomainApiError) return domainApiCommandError(command, error, hostname);
914
+ return new CliError({
915
+ code: "DEPLOY_FAILED",
916
+ domain: "app",
917
+ summary: `Custom domain ${command} failed`,
918
+ why: error instanceof Error ? error.message : String(error),
919
+ fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
920
+ debug: formatDebugDetails(error),
921
+ exitCode: 1,
922
+ nextSteps: [`prisma-cli app domain show ${hostname}`]
923
+ });
924
+ }
925
+ function domainApiCommandError(command, error, hostname) {
926
+ if (command === "add") return domainAddCommandError(error, hostname);
927
+ if (error.status === 404) return domainNotFoundError(hostname);
928
+ if (command === "retry" && error.status === 409) return domainRetryNotEligibleError(error, hostname);
929
+ return domainGenericCommandError(command, error, hostname);
930
+ }
931
+ function domainAddCommandError(error, hostname) {
932
+ if ((error.status === 400 || error.status === 422) && isDomainDnsError(error)) return domainDnsNotConfiguredError(hostname, error);
933
+ if (error.status === 400) return new CliError({
934
+ code: "DOMAIN_HOSTNAME_INVALID",
935
+ domain: "app",
936
+ summary: `Invalid custom domain "${hostname}"`,
937
+ why: error.message,
938
+ fix: "Pass a valid hostname like shop.acme.com and make sure DNS can be verified.",
939
+ debug: formatDebugDetails(error),
940
+ exitCode: 2,
941
+ nextSteps: ["prisma-cli app domain add shop.acme.com"]
942
+ });
943
+ if (error.status === 429 || isDomainQuotaError(error)) return new CliError({
944
+ code: "DOMAIN_QUOTA_EXCEEDED",
945
+ domain: "app",
946
+ summary: "Custom domain quota exceeded",
947
+ why: error.message,
948
+ fix: "Remove an existing custom domain before adding another one.",
949
+ debug: formatDebugDetails(error),
950
+ exitCode: 1,
951
+ nextSteps: ["prisma-cli app domain remove <hostname>"]
952
+ });
953
+ if (error.status === 409) return domainAlreadyRegisteredError(hostname, error);
954
+ if (error.status === 422) return new CliError({
955
+ code: "NO_DEPLOYMENTS",
956
+ domain: "app",
957
+ summary: "Custom domain requires a live production deployment",
958
+ why: "The selected production app does not have a promoted version that can receive a custom domain.",
959
+ fix: "Deploy the app to the production branch, then rerun the domain command.",
960
+ debug: formatDebugDetails(error),
961
+ exitCode: 1,
962
+ nextSteps: ["prisma-cli app deploy --branch production", `prisma-cli app domain add ${hostname}`]
963
+ });
964
+ return domainGenericCommandError("add", error, hostname);
965
+ }
966
+ function domainRetryNotEligibleError(error, hostname) {
967
+ return new CliError({
968
+ code: "DOMAIN_RETRY_NOT_ELIGIBLE",
969
+ domain: "app",
970
+ summary: `Custom domain "${hostname}" is not eligible for retry`,
971
+ why: error.message,
972
+ fix: "Wait for the current verification or TLS step to finish, then rerun retry if the domain fails.",
973
+ debug: formatDebugDetails(error),
974
+ exitCode: 1,
975
+ nextSteps: [`prisma-cli app domain show ${hostname}`]
976
+ });
977
+ }
978
+ function domainGenericCommandError(command, error, hostname) {
958
979
  return new CliError({
959
980
  code: "DEPLOY_FAILED",
960
981
  domain: "app",
@@ -1356,14 +1377,14 @@ async function requireProviderAndDeployProjectContext(context, explicitProject,
1356
1377
  };
1357
1378
  }
1358
1379
  async function resolveProjectContext(context, client, explicitProject, options) {
1359
- const authState = await requireAuthenticatedAuthState(context);
1360
- if (!authState.workspace) throw workspaceRequiredError();
1380
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
1381
+ if (!workspace) throw workspaceRequiredError();
1361
1382
  const resolvedResult = await resolveProjectTarget({
1362
1383
  context,
1363
- workspace: authState.workspace,
1384
+ workspace,
1364
1385
  explicitProject,
1365
1386
  envProjectId: options?.envProjectId,
1366
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace, context.runtime.signal),
1387
+ listProjects: () => listRealWorkspaceProjects(client, workspace, context.runtime.signal),
1367
1388
  commandName: options?.commandName
1368
1389
  });
1369
1390
  if (resolvedResult.isErr()) throw projectResolutionErrorToCliError(resolvedResult.error);
@@ -1382,8 +1403,16 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1382
1403
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
1383
1404
  if (!workspace) throw workspaceRequiredError();
1384
1405
  const branch = options.branch ?? await resolveDeployBranch(context, void 0);
1385
- const projects = await listRealWorkspaceProjects(client, workspace, context.runtime.signal);
1386
- if (explicitProject) return withRemoteDeployBranch(provider, {
1406
+ return withRemoteDeployBranch(provider, await resolveDeployProjectSetup(context, provider, workspace, await listRealWorkspaceProjects(client, workspace, context.runtime.signal), explicitProject, options), branch, context.runtime.signal);
1407
+ }
1408
+ async function resolveDeployProjectSetup(context, provider, workspace, projects, explicitProject, options) {
1409
+ const selected = await resolveNonInteractiveDeployProjectSetup(context, provider, workspace, projects, explicitProject, options);
1410
+ if (selected) return selected;
1411
+ if (canPrompt(context) && !context.flags.yes) return resolveInteractiveDeployProjectSetup(context, provider, workspace, projects);
1412
+ throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
1413
+ }
1414
+ async function resolveNonInteractiveDeployProjectSetup(context, provider, workspace, projects, explicitProject, options) {
1415
+ if (explicitProject) return {
1387
1416
  workspace,
1388
1417
  project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
1389
1418
  resolution: {
@@ -1392,11 +1421,11 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1392
1421
  targetNameSource: "explicit"
1393
1422
  },
1394
1423
  localPinAction: "linked"
1395
- }, branch, context.runtime.signal);
1424
+ };
1396
1425
  if (options.createProjectName) {
1397
1426
  const projectName = options.createProjectName.trim();
1398
1427
  if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
1399
- return withRemoteDeployBranch(provider, {
1428
+ return {
1400
1429
  workspace,
1401
1430
  project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace, context.runtime.signal)),
1402
1431
  resolution: {
@@ -1405,12 +1434,12 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1405
1434
  targetNameSource: "explicit"
1406
1435
  },
1407
1436
  localPinAction: "created"
1408
- }, branch, context.runtime.signal);
1437
+ };
1409
1438
  }
1410
1439
  if (options.envProjectId) {
1411
1440
  const project = projects.find((candidate) => candidate.id === options.envProjectId);
1412
1441
  if (!project) throw projectNotFoundError(options.envProjectId, workspace);
1413
- return withRemoteDeployBranch(provider, {
1442
+ return {
1414
1443
  workspace,
1415
1444
  project: toProjectSummary(project),
1416
1445
  resolution: {
@@ -1418,14 +1447,14 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1418
1447
  targetName: options.envProjectId,
1419
1448
  targetNameSource: "env"
1420
1449
  }
1421
- }, branch, context.runtime.signal);
1450
+ };
1422
1451
  }
1423
1452
  const localPin = options.localPin;
1424
1453
  if (localPin.kind === "present") {
1425
1454
  if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
1426
1455
  const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
1427
1456
  if (!project) throw localResolutionPinStaleError();
1428
- return withRemoteDeployBranch(provider, {
1457
+ return {
1429
1458
  workspace,
1430
1459
  project: toProjectSummary(project),
1431
1460
  resolution: {
@@ -1433,10 +1462,10 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1433
1462
  targetName: project.name,
1434
1463
  targetNameSource: "local-pin"
1435
1464
  }
1436
- }, branch, context.runtime.signal);
1465
+ };
1437
1466
  }
1438
1467
  const platformMapping = await resolveDurablePlatformMapping();
1439
- if (platformMapping && platformMapping.workspace.id === workspace.id) return withRemoteDeployBranch(provider, {
1468
+ if (platformMapping && platformMapping.workspace.id === workspace.id) return {
1440
1469
  workspace,
1441
1470
  project: toProjectSummary(platformMapping),
1442
1471
  resolution: {
@@ -1444,9 +1473,8 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1444
1473
  targetName: platformMapping.name,
1445
1474
  targetNameSource: "platform-mapping"
1446
1475
  }
1447
- }, branch, context.runtime.signal);
1448
- if (canPrompt(context) && !context.flags.yes) return withRemoteDeployBranch(provider, await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch, context.runtime.signal);
1449
- throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd, context.runtime.signal));
1476
+ };
1477
+ return null;
1450
1478
  }
1451
1479
  async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
1452
1480
  const setup = await promptForProjectSetupChoice({
@@ -1890,41 +1918,7 @@ function deployFailedError(summary, error, nextSteps) {
1890
1918
  function appDeployFailedError(error, progress) {
1891
1919
  const why = error instanceof Error ? error.message : String(error);
1892
1920
  const debug = formatDebugDetails(error);
1893
- if (progress.buildStarted && !progress.buildCompleted) {
1894
- const standaloneOutputFailure = isNextStandaloneOutputFailure(why);
1895
- const fix = standaloneOutputFailure ? "Add output: \"standalone\" to next.config.*, then rerun deploy." : "Inspect the build output above, fix the error, and redeploy.";
1896
- const nextSteps = standaloneOutputFailure ? ["Add output: \"standalone\" to next.config.*, then rerun prisma-cli app deploy"] : [];
1897
- const nextActions = standaloneOutputFailure ? [{
1898
- kind: "edit-file",
1899
- journey: "deploy-app",
1900
- label: "Add Next.js standalone output",
1901
- reason: "Prisma Compute needs Next.js standalone output to build a deployable server artifact."
1902
- }, {
1903
- kind: "run-command",
1904
- journey: "deploy-app",
1905
- label: "Rerun deploy",
1906
- command: "prisma-cli app deploy"
1907
- }] : [];
1908
- return new CliError({
1909
- code: "BUILD_FAILED",
1910
- domain: "app",
1911
- summary: "Build failed locally.",
1912
- why,
1913
- fix,
1914
- debug,
1915
- meta: { phase: "build" },
1916
- humanLines: [
1917
- "Build failed locally.",
1918
- "",
1919
- `✗ Built ${why}`,
1920
- "",
1921
- `Fix: ${fix}`
1922
- ],
1923
- exitCode: 1,
1924
- nextSteps,
1925
- nextActions
1926
- });
1927
- }
1921
+ if (progress.buildStarted && !progress.buildCompleted) return appBuildFailedError(why, debug);
1928
1922
  if (!progress.buildStarted) return deployFailedError("App deploy failed", error, ["prisma-cli app deploy"]);
1929
1923
  const phaseHeadline = progress.containerLive ? "The deployment started, but the app is not ready yet." : "Deploy failed after the build completed.";
1930
1924
  const recoveryLines = progress.versionId ? ["See what happened", `prisma-cli app logs --deployment ${progress.versionId}`] : ["Fix", "Retry the command, or rerun with --trace for more detailed diagnostics."];
@@ -1965,6 +1959,41 @@ function appDeployFailedError(error, progress) {
1965
1959
  nextSteps: []
1966
1960
  });
1967
1961
  }
1962
+ function appBuildFailedError(why, debug) {
1963
+ const standaloneOutputFailure = isNextStandaloneOutputFailure(why);
1964
+ const fix = standaloneOutputFailure ? "Add output: \"standalone\" to next.config.*, then rerun deploy." : "Inspect the build output above, fix the error, and redeploy.";
1965
+ const nextSteps = standaloneOutputFailure ? ["Add output: \"standalone\" to next.config.*, then rerun prisma-cli app deploy"] : [];
1966
+ const nextActions = standaloneOutputFailure ? [{
1967
+ kind: "edit-file",
1968
+ journey: "deploy-app",
1969
+ label: "Add Next.js standalone output",
1970
+ reason: "Prisma Compute needs Next.js standalone output to build a deployable server artifact."
1971
+ }, {
1972
+ kind: "run-command",
1973
+ journey: "deploy-app",
1974
+ label: "Rerun deploy",
1975
+ command: "prisma-cli app deploy"
1976
+ }] : [];
1977
+ return new CliError({
1978
+ code: "BUILD_FAILED",
1979
+ domain: "app",
1980
+ summary: "Build failed locally.",
1981
+ why,
1982
+ fix,
1983
+ debug,
1984
+ meta: { phase: "build" },
1985
+ humanLines: [
1986
+ "Build failed locally.",
1987
+ "",
1988
+ `✗ Built ${why}`,
1989
+ "",
1990
+ `Fix: ${fix}`
1991
+ ],
1992
+ exitCode: 1,
1993
+ nextSteps,
1994
+ nextActions
1995
+ });
1996
+ }
1968
1997
  function localResolutionPinStaleError() {
1969
1998
  return new CliError({
1970
1999
  code: "LOCAL_STATE_STALE",
@@ -1,6 +1,6 @@
1
1
  import { CliError, authRequiredError, workspaceRequiredError } from "../shell/errors.js";
2
- import { requireComputeAuth } from "../lib/auth/guard.js";
3
2
  import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
3
+ import { requireComputeAuth } from "../lib/auth/guard.js";
4
4
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
5
5
  import { requireAuthenticatedAuthState } from "./auth.js";
6
6
  import { listRealWorkspaceProjects } from "./project.js";
@@ -1,6 +1,6 @@
1
1
  import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
- import { requireComputeAuth } from "../lib/auth/guard.js";
3
2
  import { projectResolutionErrorToCliError, resolveProjectTarget } from "../lib/project/resolution.js";
3
+ import { requireComputeAuth } from "../lib/auth/guard.js";
4
4
  import { requireAuthenticatedAuthState } from "./auth.js";
5
5
  import { listFixtureWorkspaceProjects, listRealWorkspaceProjects } from "./project.js";
6
6
  import { createManagementDatabaseProvider, normalizeConnection, normalizeDatabase } from "../lib/database/provider.js";
@@ -1,19 +1,19 @@
1
1
  import { CliError, authRequiredError, featureUnavailableError, usageError, workspaceRequiredError } from "../shell/errors.js";
2
2
  import { renderSummaryLine } from "../shell/ui.js";
3
3
  import { canPrompt } from "../shell/runtime.js";
4
- import { requireComputeAuth } from "../lib/auth/guard.js";
5
- import { formatCommandArgument } from "../shell/command-arguments.js";
6
4
  import { readLocalResolutionPin } from "../lib/project/local-pin.js";
5
+ import { formatCommandArgument } from "../shell/command-arguments.js";
7
6
  import { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectResolutionErrorToCliError, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
8
7
  import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectDirectoryBindingErrorToCliError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
9
- import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
10
8
  import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
9
+ import { requireComputeAuth } from "../lib/auth/guard.js";
10
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
11
11
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
12
12
  import { requireAuthenticatedAuthState } from "./auth.js";
13
13
  import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
14
14
  import { createProjectUseCases } from "../use-cases/project.js";
15
- import open from "open";
16
15
  import { matchError } from "better-result";
16
+ import open from "open";
17
17
  //#region src/controllers/project.ts
18
18
  const GITHUB_INSTALL_POLL_INTERVAL_MS = 2e3;
19
19
  const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
@@ -1,7 +1,7 @@
1
1
  import { CliError, usageError } from "../../shell/errors.js";
2
+ import { confirmPrompt } from "../../shell/prompt.js";
2
3
  import { renderSummaryLine } from "../../shell/ui.js";
3
4
  import { canPrompt } from "../../shell/runtime.js";
4
- import { confirmPrompt } from "../../shell/prompt.js";
5
5
  import { formatCommandArgument } from "../../shell/command-arguments.js";
6
6
  import "../project/setup.js";
7
7
  import { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup } from "./branch-database.js";
@@ -9,6 +9,22 @@ import path from "node:path";
9
9
  //#region src/lib/app/branch-database-deploy.ts
10
10
  async function maybeSetupBranchDatabase(context, provider, projectId, branch, options) {
11
11
  if (options.db === false) return emptyBranchDatabaseSetupOutcome();
12
+ const preflight = branchDatabasePreflight(branch, options);
13
+ if (preflight) return preflight;
14
+ const envState = await inspectBranchDatabaseEnv(provider, projectId, branch, context.runtime.signal);
15
+ const existingEnvOutcome = existingBranchDatabaseEnvOutcome(context, branch, getTargetDatabaseEnvVarKeys(envState), envState, options.db);
16
+ if (existingEnvOutcome) return existingEnvOutcome;
17
+ const localSignal = await inspectBranchDatabaseSignal(context.runtime.cwd, context.runtime.signal);
18
+ if (localSignal.unsupportedSchema) {
19
+ if (options.db === true) throw unsupportedBranchDatabaseSchemaError(localSignal.unsupportedSchema, branch, context);
20
+ return emptyBranchDatabaseSetupOutcome();
21
+ }
22
+ const promptOutcome = await branchDatabasePromptOutcome(context, branch, localSignal, envState, options.db);
23
+ if (promptOutcome) return promptOutcome;
24
+ if (options.db === true && !canPrompt(context) && !context.flags.yes) throw nonInteractiveDatabaseSetupRequiresYesError(branch);
25
+ return setupBranchDatabase(context, provider, projectId, branch, localSignal, envState);
26
+ }
27
+ function branchDatabasePreflight(branch, options) {
12
28
  if (hasProvidedDatabaseEnvVars(options.providedEnvVars)) {
13
29
  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
30
  return emptyBranchDatabaseSetupOutcome();
@@ -17,46 +33,40 @@ async function maybeSetupBranchDatabase(context, provider, projectId, branch, op
17
33
  if (options.db === true) throw productionDatabaseSetupAfterFirstDeployError();
18
34
  return emptyBranchDatabaseSetupOutcome();
19
35
  }
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);
36
+ return null;
37
+ }
38
+ function existingBranchDatabaseEnvOutcome(context, branch, targetEnvVars, envState, requested) {
39
+ if (!hasExistingDatabaseEnvForTarget(branch, envState)) return null;
40
+ const warning = requested === true ? existingDatabaseEnvWarning(branch, targetEnvVars) : null;
41
+ if (warning) emitBranchDatabaseWarning(context, warning);
42
+ return {
43
+ result: requested === true ? {
44
+ status: "skipped",
45
+ reason: existingDatabaseEnvReason(branch),
46
+ envVars: targetEnvVars,
47
+ schema: null
48
+ } : void 0,
49
+ warnings: warning ? [warning] : []
50
+ };
51
+ }
52
+ async function branchDatabasePromptOutcome(context, branch, localSignal, envState, requested) {
53
+ if (requested === true) return null;
54
+ if (!(hasBranchDatabaseSignal(localSignal) || Boolean(envState.inheritedPreviewDatabaseUrl))) return emptyBranchDatabaseSetupOutcome();
55
+ if (!canPrompt(context) || context.flags.yes) {
56
+ const warning = databasePromptSuppressedWarning(branch);
57
+ emitBranchDatabaseWarning(context, warning);
25
58
  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] : []
59
+ result: void 0,
60
+ warnings: [warning]
33
61
  };
34
62
  }
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);
63
+ maybeRenderBranchDatabaseSignal(context, branch, localSignal, envState);
64
+ return await confirmPrompt({
65
+ input: context.runtime.stdin,
66
+ output: context.output.stderr,
67
+ message: databasePromptMessage(branch),
68
+ initialValue: false
69
+ }) ? null : emptyBranchDatabaseSetupOutcome();
60
70
  }
61
71
  async function setupBranchDatabase(context, provider, projectId, branch, signal, envState) {
62
72
  emitBranchDatabaseProgress(context, "pending", "Creating database");
@@ -73,15 +83,16 @@ async function setupBranchDatabase(context, provider, projectId, branch, signal,
73
83
  let schemaSetup = null;
74
84
  const warnings = [];
75
85
  let skippedSchemaWarning = null;
76
- if (signal.schema) {
77
- emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(signal.schema.command)}`);
86
+ const schema = signal.schema;
87
+ if (schema) {
88
+ emitBranchDatabaseProgress(context, "pending", `Applying database schema with ${formatSchemaSetupCommand(schema.command)}`);
78
89
  schemaSetup = await runBranchDatabaseSchemaSetup({
79
90
  context,
80
- schema: signal.schema,
91
+ schema,
81
92
  databaseUrl: database.databaseUrl,
82
93
  directUrl: database.directUrl
83
94
  }).catch((error) => {
84
- throw schemaSetupFailedError(error, signal.schema, branch, context.runtime.cwd);
95
+ throw schemaSetupFailedError(error, schema, branch, context.runtime.cwd);
85
96
  });
86
97
  emitBranchDatabaseProgress(context, "success", "Applied database schema");
87
98
  } else skippedSchemaWarning = "No supported Prisma schema source was found. Database env vars were created, but schema setup was skipped.";
@@ -88,29 +88,41 @@ async function runBranchDatabaseSchemaSetup(options) {
88
88
  async function scanDirectory(cwd, directory, depth, state, signal) {
89
89
  signal.throwIfAborted();
90
90
  if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
91
- let entries;
92
- try {
93
- entries = await readdir(directory, { withFileTypes: true });
94
- } catch (error) {
95
- if (error.code === "ENOENT") return;
96
- throw error;
97
- }
91
+ const entries = await readDirectoryEntries(directory);
92
+ if (!entries) return;
98
93
  entries.sort((left, right) => left.name.localeCompare(right.name));
99
94
  for (const entry of entries) {
100
95
  signal.throwIfAborted();
101
96
  if (state.filesVisited >= MAX_SCAN_FILES) return;
102
- const entryPath = path.join(directory, entry.name);
103
- if (entry.isDirectory()) {
104
- if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
105
- continue;
106
- }
107
- if (!entry.isFile()) continue;
108
- state.filesVisited += 1;
109
- if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
110
- if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
111
- 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);
97
+ await scanDirectoryEntry(cwd, directory, entry, depth, state, signal);
112
98
  }
113
99
  }
100
+ async function readDirectoryEntries(directory) {
101
+ try {
102
+ return await readdir(directory, { withFileTypes: true });
103
+ } catch (error) {
104
+ if (error.code === "ENOENT") return null;
105
+ throw error;
106
+ }
107
+ }
108
+ async function scanDirectoryEntry(cwd, directory, entry, depth, state, signal) {
109
+ const entryPath = path.join(directory, entry.name);
110
+ if (entry.isDirectory()) {
111
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
112
+ return;
113
+ }
114
+ if (!entry.isFile()) return;
115
+ state.filesVisited += 1;
116
+ collectBranchDatabaseCandidate(entryPath, entry.name, state);
117
+ if (await shouldRecordDatabaseUrlReference(entryPath, entry.name, state, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
118
+ }
119
+ function collectBranchDatabaseCandidate(entryPath, entryName, state) {
120
+ if (entryName === "schema.prisma") state.schemaCandidates.push(entryPath);
121
+ if (isPrismaNextConfigFile(entryName)) state.prismaNextConfigCandidates.push(entryPath);
122
+ }
123
+ async function shouldRecordDatabaseUrlReference(entryPath, entryName, state, signal) {
124
+ return state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entryName) && await fileContainsDatabaseUrl(entryPath, signal);
125
+ }
114
126
  async function selectPrismaOrmSchema(cwd, candidates, signal) {
115
127
  const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
116
128
  for (const schemaPath of sorted) {
@@ -143,12 +155,15 @@ async function selectPrismaOrmSchema(cwd, candidates, signal) {
143
155
  };
144
156
  }
145
157
  function selectPrismaNextConfig(cwd, candidates, mode) {
146
- const matches = candidates.filter((candidate) => {
147
- const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
148
- return mode === "supported" ? isSupported : !isSupported;
149
- });
158
+ const matches = candidates.filter(mode === "supported" ? isSupportedPrismaNextConfig : isUnsupportedPrismaNextConfig);
150
159
  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;
151
160
  }
161
+ function isSupportedPrismaNextConfig(candidate) {
162
+ return candidate.target === "postgresql" || candidate.target === "unknown";
163
+ }
164
+ function isUnsupportedPrismaNextConfig(candidate) {
165
+ return !isSupportedPrismaNextConfig(candidate);
166
+ }
152
167
  function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
153
168
  return candidates.map((candidate) => ({
154
169
  absolute: candidate,
@@ -401,23 +401,31 @@ async function normalizeArtifactSymlinks(artifactDir, appPath, signal) {
401
401
  continue;
402
402
  }
403
403
  if (!entry.isSymbolicLink()) continue;
404
- const target = await unsupportedFilesystemBoundary(signal, () => readlink(fullPath));
405
- const resolvedTarget = path.resolve(path.dirname(fullPath), target);
406
- if (isPathWithin(normalizedArtifactDir, resolvedTarget)) continue;
407
- if (!isPathWithin(normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
408
- const targetStat = await unsupportedFilesystemBoundary(signal, () => stat(resolvedTarget));
409
- await unsupportedFilesystemBoundary(signal, () => rm(fullPath, {
410
- force: true,
411
- recursive: true
412
- }));
413
- await unsupportedFilesystemBoundary(signal, () => cp(resolvedTarget, fullPath, {
414
- recursive: targetStat.isDirectory(),
415
- dereference: true
416
- }));
417
- if (targetStat.isDirectory()) await walkDirectory(fullPath);
404
+ if (await materializeArtifactSymlink({
405
+ fullPath,
406
+ normalizedArtifactDir,
407
+ normalizedAppPath,
408
+ signal
409
+ }) === "directory") await walkDirectory(fullPath);
418
410
  }
419
411
  }
420
412
  }
413
+ async function materializeArtifactSymlink(options) {
414
+ const target = await unsupportedFilesystemBoundary(options.signal, () => readlink(options.fullPath));
415
+ const resolvedTarget = path.resolve(path.dirname(options.fullPath), target);
416
+ if (isPathWithin(options.normalizedArtifactDir, resolvedTarget)) return "internal";
417
+ if (!isPathWithin(options.normalizedAppPath, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
418
+ const targetStat = await unsupportedFilesystemBoundary(options.signal, () => stat(resolvedTarget));
419
+ await unsupportedFilesystemBoundary(options.signal, () => rm(options.fullPath, {
420
+ force: true,
421
+ recursive: true
422
+ }));
423
+ await unsupportedFilesystemBoundary(options.signal, () => cp(resolvedTarget, options.fullPath, {
424
+ recursive: targetStat.isDirectory(),
425
+ dereference: true
426
+ }));
427
+ return targetStat.isDirectory() ? "directory" : "file";
428
+ }
421
429
  function isPathWithin(rootPath, candidatePath) {
422
430
  const relativePath = path.relative(rootPath, candidatePath);
423
431
  return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
@@ -509,29 +509,33 @@ async function findAppForDeployment(sdk, deploymentId, signal) {
509
509
  });
510
510
  if (servicesResult.isErr()) throw new Error(servicesResult.error.message);
511
511
  for (const service of servicesResult.value) {
512
- const detailResult = await sdk.showService({
513
- serviceId: service.id,
514
- signal
515
- });
516
- if (detailResult.isErr()) throw new Error(detailResult.error.message);
517
- const app = {
518
- id: detailResult.value.id,
519
- name: detailResult.value.name,
520
- region: detailResult.value.region ?? null,
521
- liveDeploymentId: detailResult.value.latestVersionId ?? null,
522
- liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null)
523
- };
524
- if (app.liveDeploymentId === deploymentId) return app;
525
- const versionsResult = await sdk.listVersions({
526
- serviceId: service.id,
527
- signal
528
- });
529
- if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
530
- if (versionsResult.value.some((version) => version.id === deploymentId)) return app;
512
+ const app = await findServiceAppForDeployment(sdk, service.id, deploymentId, signal);
513
+ if (app) return app;
531
514
  }
532
515
  }
533
516
  return null;
534
517
  }
518
+ async function findServiceAppForDeployment(sdk, serviceId, deploymentId, signal) {
519
+ const detailResult = await sdk.showService({
520
+ serviceId,
521
+ signal
522
+ });
523
+ if (detailResult.isErr()) throw new Error(detailResult.error.message);
524
+ const app = {
525
+ id: detailResult.value.id,
526
+ name: detailResult.value.name,
527
+ region: detailResult.value.region ?? null,
528
+ liveDeploymentId: detailResult.value.latestVersionId ?? null,
529
+ liveUrl: toAbsoluteUrl(detailResult.value.serviceEndpointDomain ?? null)
530
+ };
531
+ if (app.liveDeploymentId === deploymentId) return app;
532
+ const versionsResult = await sdk.listVersions({
533
+ serviceId,
534
+ signal
535
+ });
536
+ if (versionsResult.isErr()) throw new Error(versionsResult.error.message);
537
+ return versionsResult.value.some((version) => version.id === deploymentId) ? app : null;
538
+ }
535
539
  function toAbsoluteUrl(url) {
536
540
  if (!url) return null;
537
541
  return url.startsWith("https://") || url.startsWith("http://") ? url : `https://${url}`;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { canPrompt } from "../../shell/runtime.js";
3
2
  import { confirmPrompt } from "../../shell/prompt.js";
3
+ import { canPrompt } from "../../shell/runtime.js";
4
4
  //#region src/lib/app/production-deploy-gate.ts
5
5
  async function enforceProductionDeployGate(context, provider, options) {
6
6
  if (options.branchKind !== "production") return { firstProductionDeploy: false };
@@ -106,35 +106,42 @@ async function consumePastedCallback(options) {
106
106
  });
107
107
  try {
108
108
  for (;;) {
109
- let answer;
110
- try {
111
- answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
112
- } catch (error) {
113
- if (error?.name === "AbortError") return;
114
- throw error;
115
- }
116
- const trimmed = answer.trim().replace(/^["']|["']$/g, "");
117
- let url;
118
- try {
119
- if (!trimmed) throw new Error("empty input");
120
- url = new URL(trimmed);
121
- } catch {
122
- options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
123
- continue;
124
- }
125
- try {
126
- await options.complete(url);
127
- return;
128
- } catch (error) {
129
- const message = error instanceof Error ? error.message : String(error);
130
- options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
131
- continue;
132
- }
109
+ const url = await readPastedCallbackUrl(rl, options);
110
+ if (url === null) return;
111
+ if (url === void 0) continue;
112
+ if (await tryCompletePastedCallback(url, options)) return;
133
113
  }
134
114
  } finally {
135
115
  rl.close();
136
116
  }
137
117
  }
118
+ async function readPastedCallbackUrl(rl, options) {
119
+ let answer;
120
+ try {
121
+ answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
122
+ } catch (error) {
123
+ if (error?.name === "AbortError") return null;
124
+ throw error;
125
+ }
126
+ const trimmed = answer.trim().replace(/^["']|["']$/g, "");
127
+ try {
128
+ if (!trimmed) throw new Error("empty input");
129
+ return new URL(trimmed);
130
+ } catch {
131
+ options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
132
+ return;
133
+ }
134
+ }
135
+ async function tryCompletePastedCallback(url, options) {
136
+ try {
137
+ await options.complete(url);
138
+ return true;
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
142
+ return false;
143
+ }
144
+ }
138
145
  var LoginState = class {
139
146
  latestVerifier;
140
147
  latestState;
@@ -1,6 +1,6 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
- import { formatCommandArgument } from "../../shell/command-arguments.js";
3
2
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
3
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { Result, TaggedError, matchError } from "better-result";
@@ -1,6 +1,6 @@
1
1
  import { CliError, usageError } from "../../shell/errors.js";
2
- import "../../shell/command-arguments.js";
3
2
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
3
+ import "../../shell/command-arguments.js";
4
4
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
5
5
  import { Result, matchError } from "better-result";
6
6
  //#region src/lib/project/setup.ts
@@ -13,7 +13,8 @@ function validateProjectSetupNameText(value, fallback) {
13
13
  }
14
14
  function resolveProjectForSetup(projectRef, projects, workspace) {
15
15
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
16
- if (matches.length === 1) return matches[0];
16
+ const match = matches[0];
17
+ if (matches.length === 1 && match) return match;
17
18
  if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
18
19
  throw projectNotFoundError(projectRef, workspace);
19
20
  }
@@ -1,5 +1,5 @@
1
- import { maskValue, padDisplay, renderSummaryLine } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { maskValue, padDisplay, renderSummaryLine } from "../shell/ui.js";
3
3
  import stringWidth from "string-width";
4
4
  //#region src/output/patterns.ts
5
5
  function renderList(input, ui) {
@@ -56,7 +56,7 @@ function serializeAppDeploy(result) {
56
56
  };
57
57
  }
58
58
  function renderBranchDatabaseDeploySummary(context, result) {
59
- if (!result.branchDatabase || result.branchDatabase.status !== "created") return [];
59
+ if (result.branchDatabase?.status !== "created") return [];
60
60
  return ["", ...renderDeployOutputRows(context.ui, [
61
61
  {
62
62
  label: "Database",
@@ -1,5 +1,5 @@
1
- import { formatColumns } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { formatColumns } from "../shell/ui.js";
3
3
  import { renderResolvedProjectContextBlock } from "./verbose-context.js";
4
4
  //#region src/presenters/branch.ts
5
5
  function renderBranchList(context, descriptor, result) {
@@ -1,5 +1,5 @@
1
- import { formatColumns, renderSummaryLine } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { formatColumns, renderSummaryLine } from "../shell/ui.js";
3
3
  import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
4
4
  import { renderResolvedProjectContextBlock, stripVerboseContext } from "./verbose-context.js";
5
5
  //#region src/presenters/database.ts
@@ -234,7 +234,7 @@ function renderDatabaseConnectionRemove(context, descriptor, result) {
234
234
  }, context.ui);
235
235
  }
236
236
  function serializeDatabaseConnectionRemove(result) {
237
- return result;
237
+ return { connection: result.connection };
238
238
  }
239
239
  function formatStatus(database) {
240
240
  return database.status ?? (database.isDefault ? "default" : "unknown");
@@ -1,5 +1,5 @@
1
- import { padDisplay, renderNextSteps, renderSummaryLine, renderVerboseBlock } from "../shell/ui.js";
2
1
  import { formatDescriptorLabel } from "../shell/command-meta.js";
2
+ import { padDisplay, renderNextSteps, renderSummaryLine, renderVerboseBlock } from "../shell/ui.js";
3
3
  import { formatCommandArgument } from "../shell/command-arguments.js";
4
4
  import { renderMutate, renderShow, serializeList } from "../output/patterns.js";
5
5
  import { renderResolvedProjectContextBlock } from "./verbose-context.js";
@@ -1,8 +1,8 @@
1
1
  import { CliError, authRequiredError, commandCanceledError } from "./errors.js";
2
- import { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess } from "./output.js";
3
2
  import { getCommandDescriptor } from "./command-meta.js";
4
3
  import { resolveGlobalFlags } from "./global-flags.js";
5
4
  import { createCommandContext } from "./runtime.js";
5
+ import { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess } from "./output.js";
6
6
  import { collectCommandDiagnostics } from "../lib/diagnostics.js";
7
7
  import { renderCommandDiagnostics } from "./diagnostics-output.js";
8
8
  import { AuthError } from "@prisma/management-api-sdk";
@@ -18,44 +18,54 @@ function isCancellationError(error) {
18
18
  return error.name === "AbortError" || error.name === "CancelledError";
19
19
  }
20
20
  async function runCommand(runtime, commandName, options, handler, presenter) {
21
- const flags = resolveGlobalFlags(runtime.argv, options);
22
- const context = await createCommandContext(runtime, flags);
21
+ const context = await createCommandContext(runtime, resolveGlobalFlags(runtime.argv, options));
23
22
  const descriptor = getCommandDescriptor(commandName);
24
23
  const startedAt = Date.now();
25
24
  try {
26
- const success = await handler(context);
27
- if (flags.json) {
28
- writeJsonSuccess(context.output, {
29
- ...success,
30
- result: presenter.renderJson ? presenter.renderJson(success.result) : success.result
31
- });
32
- return;
33
- }
34
- const stdout = presenter.renderStdout?.(context, descriptor, success.result) ?? [];
35
- if (flags.quiet) {
36
- if (stdout.length > 0) context.output.stdout.write(`${stdout.join("\n")}\n`);
37
- return;
38
- }
39
- const rendered = presenter.renderHuman(context, descriptor, success.result);
40
- const diagnostics = await renderBestEffortCommandDiagnostics(context, {
41
- enabled: flags.verbose && rendered.length > 0,
42
- durationMs: Date.now() - startedAt
43
- });
44
- const humanLines = [...rendered, ...diagnostics];
45
- if (stdout.length > 0 && humanLines.length > 0) humanLines.push("");
46
- writeHumanLines(context.output, humanLines);
47
- if (stdout.length > 0) context.output.stdout.write(`${stdout.join("\n")}\n`);
25
+ await writeCommandSuccess(context, descriptor, await handler(context), presenter, Date.now() - startedAt);
48
26
  } catch (error) {
49
27
  const cliError = toCliError(error, runtime);
50
28
  if (cliError) {
51
- if (flags.json) writeJsonError(context.output, commandName, cliError);
52
- else writeHumanError(context.output, context.ui, cliError, { trace: flags.trace });
29
+ writeCommandError(context, commandName, cliError);
53
30
  process.exitCode = cliError.exitCode;
54
31
  return;
55
32
  }
56
33
  throw error;
57
34
  }
58
35
  }
36
+ async function writeCommandSuccess(context, descriptor, success, presenter, durationMs) {
37
+ if (context.flags.json) {
38
+ writeJsonSuccess(context.output, {
39
+ ...success,
40
+ result: presenter.renderJson ? presenter.renderJson(success.result) : success.result
41
+ });
42
+ return;
43
+ }
44
+ const stdout = presenter.renderStdout?.(context, descriptor, success.result) ?? [];
45
+ if (context.flags.quiet) {
46
+ writeStdoutLines(context, stdout);
47
+ return;
48
+ }
49
+ const rendered = presenter.renderHuman(context, descriptor, success.result);
50
+ const diagnostics = await renderBestEffortCommandDiagnostics(context, {
51
+ enabled: context.flags.verbose && rendered.length > 0,
52
+ durationMs
53
+ });
54
+ const humanLines = [...rendered, ...diagnostics];
55
+ if (stdout.length > 0 && humanLines.length > 0) humanLines.push("");
56
+ writeHumanLines(context.output, humanLines);
57
+ writeStdoutLines(context, stdout);
58
+ }
59
+ function writeCommandError(context, commandName, cliError) {
60
+ if (context.flags.json) {
61
+ writeJsonError(context.output, commandName, cliError);
62
+ return;
63
+ }
64
+ writeHumanError(context.output, context.ui, cliError, { trace: context.flags.trace });
65
+ }
66
+ function writeStdoutLines(context, lines) {
67
+ if (lines.length > 0) context.output.stdout.write(`${lines.join("\n")}\n`);
68
+ }
59
69
  async function renderBestEffortCommandDiagnostics(context, options) {
60
70
  if (!options.enabled) return [];
61
71
  try {
@@ -1,6 +1,6 @@
1
- import { createShellUi, padDisplay, wrapText } from "./ui.js";
2
1
  import { formatDescriptorLabel, getDescriptorForCommand } from "./command-meta.js";
3
2
  import { COMPACT_GLOBAL_OPTION_FLAGS, resolveGlobalFlags } from "./global-flags.js";
3
+ import { createShellUi, padDisplay, wrapText } from "./ui.js";
4
4
  //#region src/shell/help.ts
5
5
  function renderHelp(command, runtime) {
6
6
  const descriptor = getDescriptorForCommand(command);
@@ -10,28 +10,38 @@ function renderHelp(command, runtime) {
10
10
  const visibleCommands = command.commands.filter((candidate) => candidate.name() !== "help" && !candidate.hidden);
11
11
  const visibleOptions = command.options.filter((candidate) => !candidate.hidden);
12
12
  if (visibleCommands.length > 0) lines.push(...renderCommandRows(rail, ui, visibleCommands));
13
- if (descriptor.longDescription) {
14
- lines.push(`${rail}`);
15
- const wrapped = wrapText(descriptor.longDescription, Math.max(ui.width - 3, 40));
16
- for (const line of wrapped) lines.push(`${rail} ${line}`);
17
- }
18
- if (visibleOptions.length > 0) {
19
- if (visibleCommands.length > 0) lines.push(`${rail}`);
20
- if (visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags))) lines.push(`${rail} Global options:`);
21
- lines.push(...renderOptionRows(rail, ui, visibleOptions));
22
- }
23
- if (descriptor.examples && descriptor.examples.length > 0) {
24
- lines.push(`${rail}`);
25
- lines.push(`${rail} Examples:`);
26
- for (const example of descriptor.examples) lines.push(`${rail} $ ${example}`);
27
- }
28
- if (descriptor.docsPath) {
29
- lines.push(`${rail}`);
30
- lines.push(`${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(descriptor.docsPath)}`);
31
- }
13
+ lines.push(...renderLongDescription(rail, ui, descriptor.longDescription));
14
+ lines.push(...renderVisibleOptions(rail, ui, visibleCommands, visibleOptions));
15
+ lines.push(...renderExamples(rail, descriptor.examples));
16
+ lines.push(...renderDocsPath(rail, ui, descriptor.docsPath));
32
17
  lines.push("");
33
18
  return `${lines.join("\n")}`;
34
19
  }
20
+ function renderLongDescription(rail, ui, longDescription) {
21
+ if (!longDescription) return [];
22
+ return [`${rail}`, ...wrapText(longDescription, Math.max(ui.width - 3, 40)).map((line) => `${rail} ${line}`)];
23
+ }
24
+ function renderVisibleOptions(rail, ui, visibleCommands, visibleOptions) {
25
+ if (visibleOptions.length === 0) return [];
26
+ const lines = visibleCommands.length > 0 ? [`${rail}`] : [];
27
+ if (shouldLabelGlobalOptions(visibleCommands, visibleOptions)) lines.push(`${rail} Global options:`);
28
+ return [...lines, ...renderOptionRows(rail, ui, visibleOptions)];
29
+ }
30
+ function shouldLabelGlobalOptions(visibleCommands, visibleOptions) {
31
+ return visibleCommands.length > 0 && visibleOptions.every((option) => COMPACT_GLOBAL_OPTION_FLAGS.includes(option.flags));
32
+ }
33
+ function renderExamples(rail, examples) {
34
+ if (!examples || examples.length === 0) return [];
35
+ return [
36
+ `${rail}`,
37
+ `${rail} Examples:`,
38
+ ...examples.map((example) => `${rail} $ ${example}`)
39
+ ];
40
+ }
41
+ function renderDocsPath(rail, ui, docsPath) {
42
+ if (!docsPath) return [];
43
+ return [`${rail}`, `${rail} ${ui.accent(padDisplay("Read more", 16))} ${ui.link(docsPath)}`];
44
+ }
35
45
  function renderCommandRows(rail, ui, commands) {
36
46
  return renderAlignedRows(rail, ui, commands.map((command) => {
37
47
  const descriptor = getDescriptorForCommand(command);
@@ -1,6 +1,6 @@
1
- import { createShellUi } from "./ui.js";
2
1
  import { LocalStateStore } from "../adapters/local-state.js";
3
2
  import { MockApi } from "../adapters/mock-api.js";
3
+ import { createShellUi } from "./ui.js";
4
4
  import { renderHelp } from "./help.js";
5
5
  import path from "node:path";
6
6
  //#region src/shell/runtime.ts
package/dist/shell/ui.js CHANGED
@@ -1,8 +1,9 @@
1
+ import { createColors } from "colorette";
1
2
  import stringWidth from "string-width";
2
3
  import stripAnsi from "strip-ansi";
3
4
  import wrapAnsi from "wrap-ansi";
4
- import { createColors } from "colorette";
5
5
  //#region src/shell/ui.ts
6
+ const URL_CREDENTIALS_PATTERN = /:\/\/[^:@/\s]+:[^@/\s]+@/g;
6
7
  const DEFAULT_WIDTH = 80;
7
8
  function createShellUi(runtime, flags) {
8
9
  const isTTY = Boolean(runtime.stderr.isTTY);
@@ -73,7 +74,23 @@ function padDisplay(text, width) {
73
74
  return `${text}${" ".repeat(padding)}`;
74
75
  }
75
76
  function maskValue(value) {
76
- return value.replace(/([A-Za-z0-9._%+-]{1,})(?=@)/g, "****").replace(/:\/\/[^:@/\s]+:[^@/\s]+@/g, "://****:****@");
77
+ return maskEmailLocalParts(value).replace(URL_CREDENTIALS_PATTERN, "://****:****@");
78
+ }
79
+ function maskEmailLocalParts(value) {
80
+ let masked = "";
81
+ let segmentStart = 0;
82
+ for (let index = 0; index < value.length; index += 1) {
83
+ if (value[index] !== "@") continue;
84
+ let localStart = index;
85
+ while (localStart > segmentStart && isEmailLocalPartChar(value[localStart - 1])) localStart -= 1;
86
+ if (localStart === index) continue;
87
+ masked += `${value.slice(segmentStart, localStart)}****@`;
88
+ segmentStart = index + 1;
89
+ }
90
+ return masked + value.slice(segmentStart);
91
+ }
92
+ function isEmailLocalPartChar(char) {
93
+ return char >= "A" && char <= "Z" || char >= "a" && char <= "z" || char >= "0" && char <= "9" || char === "!" || char === "#" || char === "$" || char === "." || char === "&" || char === "'" || char === "*" || char === "%" || char === "+" || char === "-" || char === "/" || char === "=" || char === "?" || char === "^" || char === "_" || char === "`" || char === "{" || char === "|" || char === "}" || char === "~";
77
94
  }
78
95
  function resolveColorEnabled(runtime, flags, isTTY) {
79
96
  if (flags.color === true) return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.82.1",
3
+ "version": "3.0.0-dev.83.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {