@prisma/cli 3.0.0-alpha.6 → 3.0.0-alpha.8

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.
@@ -4,21 +4,34 @@ import { CliError, authRequiredError, featureUnavailableError, usageError, works
4
4
  import { renderCommandHeader } from "../shell/ui.js";
5
5
  import { writeJsonEvent } from "../shell/output.js";
6
6
  import { canPrompt } from "../shell/runtime.js";
7
- import { textPrompt } from "../shell/prompt.js";
7
+ import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt.js";
8
8
  import { requireComputeAuth } from "../lib/auth/guard.js";
9
9
  import { readAuthState } from "../lib/auth/auth-ops.js";
10
10
  import { parseEnvAssignments } from "../lib/app/env-vars.js";
11
+ import { renderDeployOutputRows } from "../lib/app/deploy-output.js";
12
+ import { readBunPackageJson } from "../lib/app/bun-project.js";
11
13
  import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.js";
12
- import { resolveProjectTarget } from "../lib/project/resolution.js";
14
+ import { inferTargetName, projectNotFoundError, resolveProjectTarget } from "../lib/project/resolution.js";
15
+ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, readLocalResolutionPin, writeLocalResolutionPin } from "../lib/project/local-pin.js";
13
16
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
14
- import { PREVIEW_DEFAULT_REGION, createPreviewDeployInteraction } from "../lib/app/preview-interaction.js";
15
- import { createPreviewDeployProgress, createPreviewPromoteProgress, createPreviewUpdateEnvProgress } from "../lib/app/preview-progress.js";
17
+ import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
18
+ import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress, createPreviewUpdateEnvProgress } from "../lib/app/preview-progress.js";
16
19
  import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
17
20
  import { createSelectPromptPort } from "./select-prompt-port.js";
18
21
  import { requireAuthenticatedAuthState } from "./auth.js";
19
22
  import { listRealWorkspaceProjects } from "./project.js";
23
+ import { access, readFile } from "node:fs/promises";
24
+ import path from "node:path";
20
25
  import open from "open";
21
26
  //#region src/controllers/app.ts
27
+ const DEPLOY_FRAMEWORKS = [
28
+ "nextjs",
29
+ "hono",
30
+ "tanstack-start"
31
+ ];
32
+ const FRAMEWORK_DEFAULT_HTTP_PORT = 3e3;
33
+ const PRISMA_PROJECT_ID_ENV_VAR = "PRISMA_PROJECT_ID";
34
+ const PRISMA_APP_ID_ENV_VAR = "PRISMA_APP_ID";
22
35
  function isRealMode(context) {
23
36
  return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
24
37
  }
@@ -80,15 +93,75 @@ async function runAppRun(context, entrypoint, requestedBuildType, requestedPort)
80
93
  }
81
94
  async function runAppDeploy(context, appName, options) {
82
95
  ensurePreviewAppMode(context);
83
- const buildType = normalizeBuildType(options?.buildType);
84
- assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
85
- const portMapping = parseDeployPortMapping(options?.httpPort);
96
+ const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
97
+ const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
98
+ const skipLocalPin = Boolean(envProjectId || envAppId);
99
+ const localPin = skipLocalPin ? { kind: "missing" } : await readLocalResolutionPin(context.runtime.cwd);
100
+ if (!skipLocalPin && localPin.kind === "invalid") throw localResolutionPinStaleError();
101
+ const explicitBuildType = Boolean(options?.buildType && options.buildType !== "auto");
102
+ const branch = await resolveDeployBranch(context, options?.branchName);
103
+ if (options?.httpPort) parseDeployHttpPort(options.httpPort);
104
+ let framework = await resolveDeployFramework(context, {
105
+ requestedFramework: options?.framework,
106
+ requestedBuildType: options?.buildType,
107
+ explicitBuildType
108
+ });
109
+ let runtime = resolveDeployRuntime(options?.httpPort, framework);
110
+ assertSupportedEntrypoint(framework.buildType, options?.entrypoint, "deploy");
86
111
  const envVars = toOptionalEnvVars(parseEnvAssignments(options?.envAssignments, { commandName: "deploy" }));
87
- const { provider, target, projectId } = await requireProviderAndProjectContext(context, options?.projectRef, { allowCreate: true });
88
- const selectedApp = await resolveDeploySelection(context, projectId, await listApps(context, provider, projectId), appName);
112
+ const firstDeploy = !skipLocalPin && localPin.kind === "missing";
113
+ const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
114
+ allowCreate: true,
115
+ branch,
116
+ envProjectId,
117
+ localPin
118
+ });
119
+ const selectedApp = await resolveDeployAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), {
120
+ explicitAppName: appName,
121
+ explicitAppId: envAppId,
122
+ firstDeploy,
123
+ inferName: () => inferTargetName(context.runtime.cwd)
124
+ });
125
+ await maybeRenderDeploySetupBlock(context, {
126
+ firstDeploy: selectedApp.firstDeploy,
127
+ workspaceName: target.workspace.name,
128
+ projectName: target.project.name,
129
+ projectAnnotation: annotationForProjectResolution(target.resolution),
130
+ branchName: target.branch.name,
131
+ branchAnnotation: branch.annotation,
132
+ appName: selectedApp.displayName,
133
+ appAnnotation: selectedApp.annotation,
134
+ framework,
135
+ runtime
136
+ });
137
+ const customized = await maybeCustomizeDeploySettings(context, {
138
+ framework,
139
+ runtime,
140
+ firstDeploy: selectedApp.firstDeploy,
141
+ explicitFramework: Boolean(options?.framework),
142
+ explicitBuildType,
143
+ explicitHttpPort: Boolean(options?.httpPort)
144
+ });
145
+ framework = customized.framework;
146
+ runtime = customized.runtime;
147
+ const buildType = framework.buildType;
148
+ assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
149
+ const portMapping = parseDeployPortMapping(String(runtime.port));
150
+ const shouldWriteLocalPin = firstDeploy && !skipLocalPin;
151
+ if (shouldWriteLocalPin) {
152
+ await writeLocalResolutionPin(context.runtime.cwd, {
153
+ workspaceId: target.workspace.id,
154
+ projectId: target.project.id
155
+ });
156
+ await ensureLocalResolutionPinGitignore(context.runtime.cwd);
157
+ maybeRenderLocalPinBound(context, target.project.name);
158
+ }
159
+ const progressState = createPreviewDeployProgressState();
160
+ const deployStartedAt = Date.now();
89
161
  const deployResult = await provider.deployApp({
90
162
  cwd: context.runtime.cwd,
91
163
  projectId,
164
+ branchName: target.branch.name,
92
165
  appId: selectedApp.appId,
93
166
  appName: selectedApp.appName,
94
167
  region: selectedApp.region,
@@ -96,11 +169,12 @@ async function runAppDeploy(context, appName, options) {
96
169
  buildType,
97
170
  portMapping,
98
171
  envVars,
99
- interaction: selectedApp.useInteractiveSelection ? createPreviewDeployInteraction(context) : void 0,
100
- progress: createPreviewDeployProgress(context.output.stderr, !context.flags.json && !context.flags.quiet)
172
+ interaction: void 0,
173
+ progress: createPreviewDeployProgress(context.output.stderr, context.ui, !context.flags.json && !context.flags.quiet, progressState)
101
174
  }).catch((error) => {
102
- throw deployFailedError("App deploy failed", error, ["prisma-cli app list-deploys"]);
175
+ throw appDeployFailedError(error, progressState);
103
176
  });
177
+ const deployDurationMs = Date.now() - deployStartedAt;
104
178
  await context.stateStore.setSelectedApp(projectId, {
105
179
  id: deployResult.app.id,
106
180
  name: deployResult.app.name
@@ -117,7 +191,12 @@ async function runAppDeploy(context, appName, options) {
117
191
  id: deployResult.app.id,
118
192
  name: deployResult.app.name
119
193
  },
120
- deployment: deployResult.deployment
194
+ deployment: deployResult.deployment,
195
+ durationMs: deployDurationMs,
196
+ localPin: shouldWriteLocalPin ? {
197
+ path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
198
+ written: true
199
+ } : void 0
121
200
  },
122
201
  warnings: [],
123
202
  nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${deployResult.deployment.id}`]
@@ -130,8 +209,8 @@ async function runAppUpdateEnv(context, appName, envAssignments, projectRef) {
130
209
  commandName: "update-env",
131
210
  requireAtLeastOne: true
132
211
  });
133
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
134
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
212
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
213
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
135
214
  if (!selectedApp) throw noDeploymentsError("No deployments available to update environment variables", "The resolved project does not have any deployed app yet.");
136
215
  const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
137
216
  throw deployFailedError("Failed to inspect app deployments", error, ["prisma-cli app list-deploys"]);
@@ -168,8 +247,8 @@ async function runAppUpdateEnv(context, appName, envAssignments, projectRef) {
168
247
  async function runAppListEnv(context, appName, projectRef) {
169
248
  ensurePreviewAppMode(context);
170
249
  emitLegacyEnvDeprecationWarning(context, "app list-env", "project env list");
171
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
172
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
250
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
251
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
173
252
  if (!selectedApp) return {
174
253
  command: "app.list-env",
175
254
  result: {
@@ -255,8 +334,8 @@ async function runAppListEnv(context, appName, projectRef) {
255
334
  }
256
335
  async function runAppListDeploys(context, appName, projectRef) {
257
336
  ensurePreviewAppMode(context);
258
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
259
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
337
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
338
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
260
339
  if (!selectedApp) return {
261
340
  command: "app.list-deploys",
262
341
  result: {
@@ -292,8 +371,8 @@ async function runAppListDeploys(context, appName, projectRef) {
292
371
  }
293
372
  async function runAppShow(context, appName, projectRef) {
294
373
  ensurePreviewAppMode(context);
295
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
296
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
374
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
375
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
297
376
  if (!selectedApp) return {
298
377
  command: "app.show",
299
378
  result: {
@@ -368,8 +447,8 @@ async function runAppShowDeploy(context, deploymentId) {
368
447
  }
369
448
  async function runAppOpen(context, appName, projectRef) {
370
449
  ensurePreviewAppMode(context);
371
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
372
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
450
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
451
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName);
373
452
  if (!selectedApp) throw noDeploymentsError("No deployments available to open", "The resolved project does not have any deployed app yet.");
374
453
  const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
375
454
  throw deployFailedError("Failed to resolve app URL", error, ["prisma-cli app show"]);
@@ -402,8 +481,8 @@ async function runAppOpen(context, appName, projectRef) {
402
481
  }
403
482
  async function runAppLogs(context, appName, deploymentId, projectRef) {
404
483
  ensurePreviewAppMode(context);
405
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
406
- const target = deploymentId ? await resolveExplicitLogDeployment(context, provider, projectId, appName, deploymentId) : await resolveLiveLogDeployment(context, provider, projectId, appName);
484
+ const { provider, target: resolvedTarget, projectId } = await requireProviderAndProjectContext(context, projectRef);
485
+ const target = deploymentId ? await resolveExplicitLogDeployment(context, provider, projectId, resolvedTarget.branch.name, appName, deploymentId) : await resolveLiveLogDeployment(context, provider, projectId, resolvedTarget.branch.name, appName);
407
486
  if (!context.flags.json && !context.flags.quiet) {
408
487
  const lines = renderCommandHeader(context.ui, {
409
488
  commandLabel: "app logs",
@@ -433,9 +512,9 @@ async function runAppLogs(context, appName, deploymentId, projectRef) {
433
512
  throw deployFailedError("Failed to stream app logs", error, [`prisma-cli app show-deploy ${target.deployment.id}`, "prisma-cli app list-deploys"]);
434
513
  });
435
514
  }
436
- async function resolveExplicitLogDeployment(context, provider, projectId, appName, deploymentId) {
515
+ async function resolveExplicitLogDeployment(context, provider, projectId, branchName, appName, deploymentId) {
437
516
  if (appName) {
438
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
517
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, branchName), appName);
439
518
  if (!selectedApp) throw noDeploymentsError("No deployments available to stream logs", "The resolved project does not have any deployed app yet.");
440
519
  const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
441
520
  throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
@@ -471,7 +550,7 @@ async function resolveExplicitLogDeployment(context, provider, projectId, appNam
471
550
  exitCode: 1,
472
551
  nextSteps: ["prisma-cli app list-deploys"]
473
552
  });
474
- const resolvedProjectApp = (await listApps(context, provider, projectId)).find((app) => app.id === shown.app?.id);
553
+ const resolvedProjectApp = (await listApps(context, provider, projectId, branchName)).find((app) => app.id === shown.app?.id);
475
554
  if (!resolvedProjectApp) throw new CliError({
476
555
  code: "DEPLOYMENT_NOT_FOUND",
477
556
  domain: "app",
@@ -490,8 +569,8 @@ async function resolveExplicitLogDeployment(context, provider, projectId, appNam
490
569
  deployment: shown.deployment
491
570
  };
492
571
  }
493
- async function resolveLiveLogDeployment(context, provider, projectId, appName) {
494
- const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
572
+ async function resolveLiveLogDeployment(context, provider, projectId, branchName, appName) {
573
+ const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId, branchName), appName);
495
574
  if (!selectedApp) throw noDeploymentsError("No deployments available to stream logs", "The resolved project does not have any deployed app yet.");
496
575
  const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
497
576
  throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
@@ -526,8 +605,8 @@ function writeLogRecord(context, record) {
526
605
  }
527
606
  async function runAppPromote(context, deploymentId, appName, projectRef) {
528
607
  ensurePreviewAppMode(context);
529
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
530
- const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId), appName, "promote");
608
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
609
+ const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName, "promote");
531
610
  const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
532
611
  throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
533
612
  });
@@ -566,8 +645,8 @@ async function runAppPromote(context, deploymentId, appName, projectRef) {
566
645
  }
567
646
  async function runAppRollback(context, appName, deploymentId, projectRef) {
568
647
  ensurePreviewAppMode(context);
569
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
570
- const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId), appName, "rollback");
648
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
649
+ const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName, "rollback");
571
650
  const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
572
651
  throw deployFailedError("Failed to list app deployments", error, ["prisma-cli app list-deploys"]);
573
652
  });
@@ -608,8 +687,8 @@ async function runAppRollback(context, appName, deploymentId, projectRef) {
608
687
  }
609
688
  async function runAppRemove(context, appName, projectRef) {
610
689
  ensurePreviewAppMode(context);
611
- const { provider, projectId } = await requireProviderAndProjectContext(context, projectRef);
612
- const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId), appName, "remove");
690
+ const { provider, target, projectId } = await requireProviderAndProjectContext(context, projectRef);
691
+ const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), appName, "remove");
613
692
  await confirmAppRemoval(context, selectedApp);
614
693
  const removedApp = await provider.removeApp(selectedApp.id).catch((error) => {
615
694
  throw removeFailedError("Failed to remove app", error, ["prisma-cli app show", "prisma-cli app list-deploys"]);
@@ -629,30 +708,104 @@ async function runAppRemove(context, appName, projectRef) {
629
708
  nextSteps: ["prisma-cli app deploy", "prisma-cli app list-deploys"]
630
709
  };
631
710
  }
632
- async function resolveDeploySelection(context, projectId, apps, explicitAppName) {
633
- if (explicitAppName) {
634
- const matched = findAppByName(apps, explicitAppName);
711
+ async function resolveDeployAppSelection(context, projectId, apps, options) {
712
+ if (options.explicitAppName) {
713
+ const matches = findAppsByName(apps, options.explicitAppName);
714
+ if (matches.length > 1) return resolveAmbiguousDeployApp(context, matches, options.explicitAppName, options.firstDeploy);
715
+ const matched = matches[0];
635
716
  if (matched) return {
636
717
  appId: matched.id,
637
- useInteractiveSelection: false
718
+ displayName: matched.name,
719
+ annotation: "set by --app",
720
+ firstDeploy: options.firstDeploy
638
721
  };
639
722
  return {
640
- appName: explicitAppName,
723
+ appName: options.explicitAppName,
641
724
  region: PREVIEW_DEFAULT_REGION,
642
- useInteractiveSelection: false
725
+ displayName: options.explicitAppName,
726
+ annotation: "set by --app",
727
+ firstDeploy: options.firstDeploy
643
728
  };
644
729
  }
645
- const savedSelection = await context.stateStore.readSelectedApp(projectId);
646
- if (savedSelection) {
647
- const matched = apps.find((app) => app.id === savedSelection.id) ?? findAppByName(apps, savedSelection.name);
648
- if (matched) return {
730
+ if (options.explicitAppId) {
731
+ const matched = apps.find((app) => app.id === options.explicitAppId);
732
+ if (!matched) throw usageError("Selected app does not exist in the resolved project", `The app "${options.explicitAppId}" from ${PRISMA_APP_ID_ENV_VAR} could not be found in resolved project "${projectId}".`, `Unset ${PRISMA_APP_ID_ENV_VAR}, pass --app <name>, or choose an app from prisma-cli app list-deploys.`, ["prisma-cli app list-deploys"], "app");
733
+ return {
649
734
  appId: matched.id,
650
- useInteractiveSelection: false
735
+ displayName: matched.name,
736
+ annotation: `from ${PRISMA_APP_ID_ENV_VAR}`,
737
+ firstDeploy: options.firstDeploy
738
+ };
739
+ }
740
+ const inferredName = await options.inferName();
741
+ const matches = findAppsByName(apps, inferredName.name);
742
+ if (matches.length > 1) return resolveAmbiguousDeployApp(context, matches, inferredName.name, options.firstDeploy);
743
+ const matched = matches[0];
744
+ if (matched) return {
745
+ appId: matched.id,
746
+ displayName: matched.name,
747
+ annotation: "existing app on this branch",
748
+ firstDeploy: options.firstDeploy
749
+ };
750
+ return {
751
+ appName: inferredName.name,
752
+ region: PREVIEW_DEFAULT_REGION,
753
+ displayName: inferredName.name,
754
+ annotation: inferredName.source === "package-name" ? "created from package.json" : "created from directory name",
755
+ firstDeploy: options.firstDeploy
756
+ };
757
+ }
758
+ async function resolveAmbiguousDeployApp(context, matches, targetName, firstDeploy) {
759
+ if (canPrompt(context)) {
760
+ const createNew = "__create_new_app__";
761
+ const cancel = "__cancel__";
762
+ const selected = await selectPrompt({
763
+ input: context.runtime.stdin,
764
+ output: context.runtime.stderr,
765
+ message: `Multiple apps are named "${targetName}"`,
766
+ choices: [
767
+ ...sortApps(matches).map((app) => ({
768
+ label: `${app.name} (${app.id})`,
769
+ value: app
770
+ })),
771
+ {
772
+ label: `Create a new app named "${targetName}"`,
773
+ value: createNew
774
+ },
775
+ {
776
+ label: "Cancel",
777
+ value: cancel
778
+ }
779
+ ]
780
+ });
781
+ if (selected === cancel) throw usageError("App selection canceled", "The command was canceled before an app was selected.", "Re-run the command and choose an app, or pass --app <name>.", ["prisma-cli app deploy --app <name>"], "app");
782
+ if (selected === createNew) return {
783
+ appName: targetName,
784
+ region: PREVIEW_DEFAULT_REGION,
785
+ displayName: targetName,
786
+ annotation: "created from package.json",
787
+ firstDeploy
788
+ };
789
+ return {
790
+ appId: selected.id,
791
+ displayName: selected.name,
792
+ annotation: "selected by you",
793
+ firstDeploy
651
794
  };
652
- if (!canPrompt(context)) throw usageError("Saved app selection is no longer available", "The locally selected app could not be found in the resolved project.", "Pass --app <name>, or rerun prisma-cli app deploy in a TTY to choose or create an app again.", ["prisma-cli app deploy"], "app");
653
795
  }
654
- if (!canPrompt(context)) throw usageError("App deploy requires an app selection in non-interactive mode", "This command cannot choose or create an app in the current mode.", "Pass --app <name>, or rerun prisma-cli app deploy in a TTY to choose or create an app.", ["prisma-cli app deploy --app hello-world"], "app");
655
- return { useInteractiveSelection: true };
796
+ throw new CliError({
797
+ code: "APP_AMBIGUOUS",
798
+ domain: "app",
799
+ summary: "App resolution is ambiguous",
800
+ why: `Multiple apps matched "${targetName}".`,
801
+ fix: "Pass --app <name> to choose the app explicitly.",
802
+ meta: { candidates: matches.map((app) => ({
803
+ id: app.id,
804
+ name: app.name
805
+ })) },
806
+ exitCode: 2,
807
+ nextSteps: ["prisma-cli app deploy --app <name>"]
808
+ });
656
809
  }
657
810
  async function resolveExistingAppSelection(context, projectId, apps, explicitAppName) {
658
811
  if (explicitAppName) {
@@ -767,8 +920,8 @@ function resolveRollbackTarget(deployments, currentLiveDeploymentId) {
767
920
  nextSteps: ["prisma-cli app deploy", "prisma-cli app list-deploys"]
768
921
  });
769
922
  }
770
- async function listApps(context, provider, projectId) {
771
- return provider.listApps(projectId).then(sortApps).catch((error) => {
923
+ async function listApps(context, provider, projectId, branchName) {
924
+ return provider.listApps(projectId, { branchName }).then(sortApps).catch((error) => {
772
925
  if (isMissingProjectError(error)) throw new CliError({
773
926
  code: "PROJECT_NOT_FOUND",
774
927
  domain: "project",
@@ -819,6 +972,16 @@ async function requireProviderAndProjectContext(context, explicitProject, option
819
972
  projectId: target.project.id
820
973
  };
821
974
  }
975
+ async function requireProviderAndDeployProjectContext(context, explicitProject, options) {
976
+ const { client, provider } = await requirePreviewAppProviderWithClient(context);
977
+ const target = await resolveDeployProjectContext(context, client, provider, explicitProject, options);
978
+ return {
979
+ client,
980
+ provider,
981
+ target,
982
+ projectId: target.project.id
983
+ };
984
+ }
822
985
  async function resolveProjectContext(context, client, provider, explicitProject, options) {
823
986
  const authState = await requireAuthenticatedAuthState(context);
824
987
  if (!authState.workspace) throw workspaceRequiredError();
@@ -845,17 +1008,404 @@ async function resolveProjectContext(context, client, provider, explicitProject,
845
1008
  prompt: createSelectPromptPort(context),
846
1009
  remember: true
847
1010
  });
848
- const branchName = await context.stateStore.read().then((state) => state.branch.active);
1011
+ const branch = options?.branch ?? await resolveDeployBranch(context, void 0);
849
1012
  return {
850
1013
  ...resolved,
851
1014
  branch: {
852
- name: branchName,
853
- kind: toBranchKind(branchName)
1015
+ name: branch.name,
1016
+ kind: toBranchKind(branch.name)
854
1017
  }
855
1018
  };
856
1019
  }
1020
+ async function resolveDeployProjectContext(context, client, provider, explicitProject, options) {
1021
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
1022
+ if (!workspace) throw workspaceRequiredError();
1023
+ const branch = options.branch ?? await resolveDeployBranch(context, void 0);
1024
+ const projects = await listRealWorkspaceProjects(client, workspace);
1025
+ const createProject = options.allowCreate ? async (name) => {
1026
+ const project = await provider.createProject({ name }).catch((error) => {
1027
+ throw createProjectOnFirstDeployError({
1028
+ error,
1029
+ inferredName: name,
1030
+ workspaceName: workspace.name
1031
+ });
1032
+ });
1033
+ return {
1034
+ id: project.id,
1035
+ name: project.name,
1036
+ workspace
1037
+ };
1038
+ } : void 0;
1039
+ if (explicitProject) return withDeployBranch(await resolveProjectTarget({
1040
+ context,
1041
+ workspace,
1042
+ explicitProject,
1043
+ listProjects: async () => projects,
1044
+ createProject,
1045
+ allowCreate: options.allowCreate,
1046
+ prompt: createSelectPromptPort(context),
1047
+ remember: true
1048
+ }), branch);
1049
+ if (options.envProjectId) {
1050
+ const project = projects.find((candidate) => candidate.id === options.envProjectId);
1051
+ if (!project) throw projectNotFoundError(options.envProjectId, workspace);
1052
+ return withDeployBranch({
1053
+ workspace,
1054
+ project: toProjectSummary(project),
1055
+ resolution: {
1056
+ projectSource: "env",
1057
+ targetName: options.envProjectId,
1058
+ targetNameSource: "env"
1059
+ }
1060
+ }, branch);
1061
+ }
1062
+ const localPin = options.localPin;
1063
+ if (localPin.kind === "present") {
1064
+ if (localPin.pin.workspaceId !== workspace.id) throw localResolutionPinStaleError();
1065
+ const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
1066
+ if (!project) throw localResolutionPinStaleError();
1067
+ return withDeployBranch({
1068
+ workspace,
1069
+ project: toProjectSummary(project),
1070
+ resolution: {
1071
+ projectSource: "local-pin",
1072
+ targetName: project.name,
1073
+ targetNameSource: "local-pin"
1074
+ }
1075
+ }, branch);
1076
+ }
1077
+ return withDeployBranch(await resolveProjectTarget({
1078
+ context,
1079
+ workspace,
1080
+ listProjects: async () => projects,
1081
+ createProject,
1082
+ allowCreate: options.allowCreate,
1083
+ prompt: createSelectPromptPort(context),
1084
+ remember: true
1085
+ }), branch);
1086
+ }
1087
+ function withDeployBranch(target, branch) {
1088
+ return {
1089
+ ...target,
1090
+ branch: {
1091
+ name: branch.name,
1092
+ kind: toBranchKind(branch.name)
1093
+ }
1094
+ };
1095
+ }
1096
+ function toProjectSummary(project) {
1097
+ return {
1098
+ id: project.id,
1099
+ name: project.name
1100
+ };
1101
+ }
857
1102
  function toBranchKind(name) {
858
- return name === "production" ? "production" : "preview";
1103
+ return name === "production" || name === "main" ? "production" : "preview";
1104
+ }
1105
+ async function resolveDeployBranch(context, explicitBranchName) {
1106
+ if (explicitBranchName) return {
1107
+ name: explicitBranchName,
1108
+ annotation: "set by --branch"
1109
+ };
1110
+ const gitBranch = await readLocalGitBranch(context.runtime.cwd);
1111
+ if (gitBranch) return {
1112
+ name: gitBranch,
1113
+ annotation: "from local Git branch"
1114
+ };
1115
+ return {
1116
+ name: "main",
1117
+ annotation: "default"
1118
+ };
1119
+ }
1120
+ async function readLocalGitBranch(cwd) {
1121
+ const headPath = await resolveGitHeadPath(path.join(cwd, ".git"));
1122
+ if (!headPath) return null;
1123
+ try {
1124
+ const head = (await readFile(headPath, "utf8")).trim();
1125
+ if (head.startsWith("ref: refs/heads/")) return head.slice(16);
1126
+ } catch {
1127
+ return null;
1128
+ }
1129
+ return null;
1130
+ }
1131
+ async function resolveGitHeadPath(gitPath) {
1132
+ try {
1133
+ const raw = await readFile(gitPath, "utf8");
1134
+ if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
1135
+ } catch {}
1136
+ try {
1137
+ await access(path.join(gitPath, "HEAD"));
1138
+ return path.join(gitPath, "HEAD");
1139
+ } catch {
1140
+ return null;
1141
+ }
1142
+ }
1143
+ async function resolveDeployFramework(context, options) {
1144
+ if (options.requestedFramework) return frameworkFromUserFacingValue(options.requestedFramework, "set by --framework");
1145
+ if (options.explicitBuildType) {
1146
+ const buildType = normalizeBuildType(options.requestedBuildType);
1147
+ if (buildType !== "auto") return {
1148
+ key: buildType,
1149
+ buildType,
1150
+ displayName: formatBuildTypeName(buildType),
1151
+ annotation: "set by --build-type"
1152
+ };
1153
+ }
1154
+ const detected = await detectDeployFramework(context.runtime.cwd);
1155
+ if (detected) return detected;
1156
+ throw frameworkNotDetectedError(context.runtime.cwd);
1157
+ }
1158
+ function resolveDeployRuntime(requestedHttpPort, framework) {
1159
+ if (requestedHttpPort) return {
1160
+ port: parseDeployHttpPort(requestedHttpPort),
1161
+ annotation: "set by --http-port"
1162
+ };
1163
+ return {
1164
+ port: FRAMEWORK_DEFAULT_HTTP_PORT,
1165
+ annotation: `${framework.displayName} default`
1166
+ };
1167
+ }
1168
+ async function detectDeployFramework(cwd) {
1169
+ const packageJson = await readBunPackageJson(cwd);
1170
+ const nextConfig = await detectNextConfig(cwd);
1171
+ if (nextConfig.exists || hasPackageDependency(packageJson, "next")) return {
1172
+ key: "nextjs",
1173
+ buildType: "nextjs",
1174
+ displayName: "Next.js",
1175
+ annotation: nextConfig.standalone ? "standalone output detected" : nextConfig.exists ? "detected from next.config" : "detected from package.json"
1176
+ };
1177
+ if (hasPackageDependency(packageJson, "hono")) return {
1178
+ key: "hono",
1179
+ buildType: "bun",
1180
+ displayName: "Hono",
1181
+ annotation: "detected from package.json"
1182
+ };
1183
+ if (hasPackageDependency(packageJson, "@tanstack/start")) return {
1184
+ key: "tanstack-start",
1185
+ buildType: "tanstack-start",
1186
+ displayName: "TanStack Start",
1187
+ annotation: "detected from package.json"
1188
+ };
1189
+ return null;
1190
+ }
1191
+ async function detectNextConfig(cwd) {
1192
+ for (const candidate of [
1193
+ "next.config.js",
1194
+ "next.config.mjs",
1195
+ "next.config.cjs",
1196
+ "next.config.ts"
1197
+ ]) {
1198
+ const filePath = path.join(cwd, candidate);
1199
+ try {
1200
+ const content = await readFile(filePath, "utf8");
1201
+ return {
1202
+ exists: true,
1203
+ standalone: /\boutput\s*:\s*["'`]standalone["'`]/.test(content)
1204
+ };
1205
+ } catch (error) {
1206
+ if (error.code !== "ENOENT") throw error;
1207
+ }
1208
+ }
1209
+ return {
1210
+ exists: false,
1211
+ standalone: false
1212
+ };
1213
+ }
1214
+ function hasPackageDependency(packageJson, dependencyName) {
1215
+ return hasDependency(packageJson?.dependencies, dependencyName) || hasDependency(packageJson?.devDependencies, dependencyName);
1216
+ }
1217
+ function hasDependency(dependencies, dependencyName) {
1218
+ return Boolean(dependencies && typeof dependencies === "object" && dependencyName in dependencies);
1219
+ }
1220
+ function frameworkFromUserFacingValue(value, annotation) {
1221
+ switch (value.trim().toLowerCase()) {
1222
+ case "next":
1223
+ case "next.js":
1224
+ case "nextjs": return {
1225
+ key: "nextjs",
1226
+ buildType: "nextjs",
1227
+ displayName: "Next.js",
1228
+ annotation
1229
+ };
1230
+ case "hono": return {
1231
+ key: "hono",
1232
+ buildType: "bun",
1233
+ displayName: "Hono",
1234
+ annotation
1235
+ };
1236
+ case "tanstack":
1237
+ case "tanstack-start":
1238
+ case "@tanstack/start": return {
1239
+ key: "tanstack-start",
1240
+ buildType: "tanstack-start",
1241
+ displayName: "TanStack Start",
1242
+ annotation
1243
+ };
1244
+ default: throw frameworkNotDetectedError(void 0, value);
1245
+ }
1246
+ }
1247
+ function frameworkNotDetectedError(cwd, requestedFramework) {
1248
+ const supported = "Next.js, Hono, TanStack Start";
1249
+ const directory = cwd ? ` in ${formatDeployDirectory(cwd)}` : "";
1250
+ return new CliError({
1251
+ code: "FRAMEWORK_NOT_DETECTED",
1252
+ domain: "app",
1253
+ summary: requestedFramework ? `Unsupported framework "${requestedFramework}"` : `Cannot detect a supported framework${directory}`,
1254
+ why: `Supported Beta frameworks: ${supported}.`,
1255
+ fix: "Add one of these frameworks as a dependency, or pass --framework <nextjs|hono|tanstack-start>.",
1256
+ exitCode: 2,
1257
+ nextSteps: [
1258
+ "prisma-cli app deploy --framework nextjs",
1259
+ "prisma-cli app deploy --framework hono",
1260
+ "prisma-cli app deploy --framework tanstack-start"
1261
+ ]
1262
+ });
1263
+ }
1264
+ async function maybeRenderDeploySetupBlock(context, details) {
1265
+ if (context.flags.json || context.flags.quiet) return;
1266
+ const directory = formatDeployDirectory(context.runtime.cwd);
1267
+ if (!details.firstDeploy) {
1268
+ context.output.stderr.write(`Deploying ${directory} to ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`);
1269
+ return;
1270
+ }
1271
+ const title = `Setting up your local directory ${formatLocalDirectory(context.runtime.cwd, context.runtime.env)}`;
1272
+ const rows = details.firstDeploy ? [
1273
+ {
1274
+ label: "Workspace",
1275
+ value: details.workspaceName
1276
+ },
1277
+ {
1278
+ label: "Project",
1279
+ value: details.projectName,
1280
+ origin: details.projectAnnotation
1281
+ },
1282
+ {
1283
+ label: "Branch",
1284
+ value: details.branchName,
1285
+ origin: details.branchAnnotation
1286
+ },
1287
+ {
1288
+ label: "App",
1289
+ value: details.appName,
1290
+ origin: details.appAnnotation
1291
+ },
1292
+ {
1293
+ label: "Framework",
1294
+ value: details.framework.displayName,
1295
+ origin: details.framework.annotation
1296
+ },
1297
+ {
1298
+ label: "Runtime",
1299
+ value: `HTTP ${details.runtime.port}`,
1300
+ origin: details.runtime.annotation
1301
+ }
1302
+ ] : [];
1303
+ const lines = [
1304
+ title,
1305
+ "",
1306
+ ...renderDeployOutputRows(context.ui, rows),
1307
+ ""
1308
+ ];
1309
+ context.output.stderr.write(`${lines.join("\n")}\n`);
1310
+ }
1311
+ function maybeRenderLocalPinBound(context, projectName) {
1312
+ if (context.flags.json || context.flags.quiet) return;
1313
+ context.output.stderr.write(`This directory is now linked to project ${projectName}.\n\n`);
1314
+ }
1315
+ async function maybeCustomizeDeploySettings(context, options) {
1316
+ if (!options.firstDeploy || context.flags.yes || options.explicitFramework || options.explicitBuildType || options.explicitHttpPort || !canPrompt(context)) return {
1317
+ framework: options.framework,
1318
+ runtime: options.runtime
1319
+ };
1320
+ if (!await confirmPrompt({
1321
+ input: context.runtime.stdin,
1322
+ output: context.runtime.stderr,
1323
+ message: "Customize settings?",
1324
+ initialValue: false
1325
+ })) return {
1326
+ framework: options.framework,
1327
+ runtime: options.runtime
1328
+ };
1329
+ const framework = frameworkFromUserFacingValue(await selectPrompt({
1330
+ input: context.runtime.stdin,
1331
+ output: context.runtime.stderr,
1332
+ message: `Framework (${options.framework.displayName})`,
1333
+ choices: DEPLOY_FRAMEWORKS.map((framework) => ({
1334
+ label: frameworkDisplayName(framework),
1335
+ value: framework
1336
+ }))
1337
+ }), "set by you");
1338
+ const requestedPort = await textPrompt({
1339
+ input: context.runtime.stdin,
1340
+ output: context.runtime.stderr,
1341
+ message: `HTTP port (${options.runtime.port})`,
1342
+ placeholder: String(options.runtime.port),
1343
+ validate: validateDeployHttpPortText
1344
+ });
1345
+ const runtime = {
1346
+ port: requestedPort.trim() ? parseDeployHttpPort(requestedPort) : options.runtime.port,
1347
+ annotation: "set by you"
1348
+ };
1349
+ const changedRows = [framework.key !== options.framework.key ? {
1350
+ label: "Framework",
1351
+ value: framework.displayName,
1352
+ annotation: framework.annotation
1353
+ } : null, runtime.port !== options.runtime.port ? {
1354
+ label: "Runtime",
1355
+ value: `HTTP ${runtime.port}`,
1356
+ annotation: runtime.annotation
1357
+ } : null].filter((row) => Boolean(row));
1358
+ if (changedRows.length > 0 && !context.flags.quiet && !context.flags.json) context.output.stderr.write(`${renderDeployOutputRows(context.ui, changedRows.map((row) => ({
1359
+ label: row.label,
1360
+ value: row.value,
1361
+ origin: row.annotation
1362
+ }))).join("\n")}\n\n`);
1363
+ return {
1364
+ framework,
1365
+ runtime
1366
+ };
1367
+ }
1368
+ function annotationForProjectResolution(resolution) {
1369
+ switch (resolution.projectSource) {
1370
+ case "explicit": return "set by --project";
1371
+ case "env": return `from ${PRISMA_PROJECT_ID_ENV_VAR}`;
1372
+ case "local-pin": return "from local pin";
1373
+ case "created": return resolution.targetNameSource === "directory-name" ? "created from directory name" : "created from package.json";
1374
+ case "package-name":
1375
+ case "directory-name": return "linked to existing project";
1376
+ case "platform-mapping":
1377
+ case "remembered-local": return "linked to existing project";
1378
+ case "prompt": return "selected by you";
1379
+ }
1380
+ }
1381
+ function frameworkDisplayName(framework) {
1382
+ switch (framework) {
1383
+ case "nextjs": return "Next.js";
1384
+ case "hono": return "Hono";
1385
+ case "tanstack-start": return "TanStack Start";
1386
+ }
1387
+ }
1388
+ function validateDeployHttpPortText(value) {
1389
+ if (!value?.trim()) return;
1390
+ try {
1391
+ parseDeployHttpPort(value);
1392
+ return;
1393
+ } catch (error) {
1394
+ return error instanceof CliError ? error.summary : String(error);
1395
+ }
1396
+ }
1397
+ function formatDeployDirectory(cwd) {
1398
+ const basename = path.basename(cwd);
1399
+ return basename ? `./${basename}` : ".";
1400
+ }
1401
+ function formatLocalDirectory(cwd, env) {
1402
+ const resolved = path.resolve(cwd);
1403
+ const home = env.HOME ? path.resolve(env.HOME) : null;
1404
+ if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
1405
+ const relative = path.relative(home, resolved);
1406
+ return relative ? `~/${relative}` : "~";
1407
+ }
1408
+ return resolved;
859
1409
  }
860
1410
  async function readCurrentWorkspaceId(context) {
861
1411
  const state = await context.stateStore.read();
@@ -891,9 +1441,12 @@ function parseLocalPort(requestedPort) {
891
1441
  }
892
1442
  function parseDeployPortMapping(requestedPort) {
893
1443
  if (!requestedPort) return;
1444
+ return { http: parseDeployHttpPort(requestedPort) };
1445
+ }
1446
+ function parseDeployHttpPort(requestedPort) {
894
1447
  const port = Number.parseInt(requestedPort, 10);
895
1448
  if (!Number.isInteger(port) || port <= 0 || port > 65535) throw usageError(`Invalid HTTP port "${requestedPort}"`, "HTTP port must be an integer between 1 and 65535.", "Pass --http-port <number> with a valid port value.", ["prisma-cli app deploy --http-port 3000"], "app");
896
- return { http: port };
1449
+ return port;
897
1450
  }
898
1451
  function ensurePreviewAppMode(context) {
899
1452
  if (isRealMode(context)) return;
@@ -911,6 +1464,83 @@ function deployFailedError(summary, error, nextSteps) {
911
1464
  nextSteps
912
1465
  });
913
1466
  }
1467
+ function appDeployFailedError(error, progress) {
1468
+ const why = error instanceof Error ? error.message : String(error);
1469
+ const debug = formatDebugDetails(error);
1470
+ if (progress.buildStarted && !progress.buildCompleted) return new CliError({
1471
+ code: "BUILD_FAILED",
1472
+ domain: "app",
1473
+ summary: "Build failed locally.",
1474
+ why,
1475
+ fix: "Inspect the build output above, fix the error, and redeploy.",
1476
+ debug,
1477
+ meta: { phase: "build" },
1478
+ humanLines: [
1479
+ "Build failed locally.",
1480
+ "",
1481
+ `✗ Built ${why}`,
1482
+ "",
1483
+ "Fix: Inspect the build output above, fix the error, and redeploy."
1484
+ ],
1485
+ exitCode: 1,
1486
+ nextSteps: []
1487
+ });
1488
+ if (!progress.buildStarted) return deployFailedError("App deploy failed", error, ["prisma-cli app deploy"]);
1489
+ const phaseHeadline = progress.containerLive ? "The deployment started, but the app is not ready yet." : "Deploy failed after the build completed.";
1490
+ 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."];
1491
+ const urlLines = progress.deploymentUrl ? [
1492
+ "",
1493
+ "URL",
1494
+ progress.deploymentUrl
1495
+ ] : [];
1496
+ const humanLines = progress.containerLive ? [
1497
+ phaseHeadline,
1498
+ "",
1499
+ "This is usually a missing env var, a failed DB connection,",
1500
+ "or a crash on startup.",
1501
+ "",
1502
+ ...recoveryLines,
1503
+ ...urlLines
1504
+ ] : [
1505
+ phaseHeadline,
1506
+ "",
1507
+ progress.uploadCompleted ? "The artifact uploaded, but the deployment did not start." : progress.archiveReady ? "The app built locally, but the artifact did not finish uploading." : "The app built locally, but the deployment did not start.",
1508
+ "",
1509
+ ...recoveryLines
1510
+ ];
1511
+ return new CliError({
1512
+ code: "DEPLOY_FAILED",
1513
+ domain: "app",
1514
+ summary: phaseHeadline,
1515
+ why,
1516
+ fix: progress.versionId ? `Inspect logs with prisma-cli app logs --deployment ${progress.versionId}.` : "Retry the command, or rerun with --trace for more detailed diagnostics.",
1517
+ debug,
1518
+ meta: {
1519
+ phase: progress.containerLive ? "runtime_ready" : "deploy",
1520
+ deploymentId: progress.versionId,
1521
+ deploymentUrl: progress.deploymentUrl
1522
+ },
1523
+ humanLines,
1524
+ exitCode: 1,
1525
+ nextSteps: []
1526
+ });
1527
+ }
1528
+ function localResolutionPinStaleError() {
1529
+ return new CliError({
1530
+ code: "LOCAL_STATE_STALE",
1531
+ domain: "project",
1532
+ summary: "Local project binding is stale",
1533
+ why: `The target recorded in ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} is no longer available in the selected workspace.`,
1534
+ fix: `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} and re-run to re-bootstrap.`,
1535
+ meta: { pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH },
1536
+ exitCode: 1,
1537
+ nextSteps: ["prisma-cli app deploy"]
1538
+ });
1539
+ }
1540
+ function readDeployEnvOverride(context, name) {
1541
+ const value = context.runtime.env[name]?.trim();
1542
+ return value ? value : void 0;
1543
+ }
914
1544
  /**
915
1545
  * `app deploy` falls into "create a new project on first deploy" when no
916
1546
  * existing project matches the package.json name (or the cwd basename as a
@@ -1032,6 +1662,9 @@ function isMissingProjectError(error) {
1032
1662
  function findAppByName(apps, name) {
1033
1663
  return apps.find((app) => app.name === name);
1034
1664
  }
1665
+ function findAppsByName(apps, name) {
1666
+ return apps.filter((app) => app.name === name);
1667
+ }
1035
1668
  function sortApps(apps) {
1036
1669
  return apps.slice().sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
1037
1670
  }