@prisma/cli 3.0.0-dev.38.1 → 3.0.0-dev.39.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.
@@ -59,7 +59,7 @@ function createRunCommand(runtime) {
59
59
  }
60
60
  function createDeployCommand(runtime) {
61
61
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("deploy"), runtime), "app.deploy");
62
- command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name")).addOption(new Option("--branch <name>", "Branch name")).addOption(new Option("--framework <name>", "Framework to deploy").choices([
62
+ command.addOption(new Option("--app <name>", "App name")).addOption(new Option("--project <id-or-name>", "Project id or name")).addOption(new Option("--create-project <name>", "Create and link a new Project before deploying")).addOption(new Option("--branch <name>", "Branch name")).addOption(new Option("--framework <name>", "Framework to deploy").choices([
63
63
  "nextjs",
64
64
  "hono",
65
65
  "tanstack-start"
@@ -74,8 +74,10 @@ function createDeployCommand(runtime) {
74
74
  const httpPort = options.httpPort;
75
75
  const envAssignments = options.env;
76
76
  const projectRef = options.project;
77
+ const createProjectName = options.createProject;
77
78
  await runCommand(runtime, "app.deploy", options, (context) => runAppDeploy(context, appName, {
78
79
  projectRef,
80
+ createProjectName,
79
81
  branchName,
80
82
  entrypoint: entry,
81
83
  buildType,
@@ -1,9 +1,9 @@
1
1
  import { attachCommandDescriptor } from "../../shell/command-meta.js";
2
2
  import { addCompactGlobalFlags, addGlobalFlags } from "../../shell/global-flags.js";
3
3
  import { configureRuntimeCommand } from "../../shell/runtime.js";
4
- import { runProjectList, runProjectShow } from "../../controllers/project.js";
4
+ import { runProjectCreate, runProjectLink, runProjectList, runProjectShow } from "../../controllers/project.js";
5
5
  import { runCommand } from "../../shell/command-runner.js";
6
- import { renderProjectList, renderProjectShow, serializeProjectList, serializeProjectShow } from "../../presenters/project.js";
6
+ import { renderProjectList, renderProjectSetup, renderProjectShow, serializeProjectList, serializeProjectSetup, serializeProjectShow } from "../../presenters/project.js";
7
7
  import { createEnvCommand } from "../env.js";
8
8
  import { Command } from "commander";
9
9
  //#region src/commands/project/index.ts
@@ -12,9 +12,35 @@ function createProjectCommand(runtime) {
12
12
  addCompactGlobalFlags(project);
13
13
  project.addCommand(createProjectListCommand(runtime));
14
14
  project.addCommand(createProjectShowCommand(runtime));
15
+ project.addCommand(createProjectCreateCommand(runtime));
16
+ project.addCommand(createProjectLinkCommand(runtime));
15
17
  project.addCommand(createEnvCommand(runtime));
16
18
  return project;
17
19
  }
20
+ function createProjectCreateCommand(runtime) {
21
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("create"), runtime), "project.create");
22
+ command.argument("<name>", "Project name");
23
+ addGlobalFlags(command);
24
+ command.action(async (name, options) => {
25
+ await runCommand(runtime, "project.create", options, (context) => runProjectCreate(context, String(name)), {
26
+ renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result),
27
+ renderJson: (result) => serializeProjectSetup(result)
28
+ });
29
+ });
30
+ return command;
31
+ }
32
+ function createProjectLinkCommand(runtime) {
33
+ const command = attachCommandDescriptor(configureRuntimeCommand(new Command("link"), runtime), "project.link");
34
+ command.argument("<id-or-name>", "Project id or name");
35
+ addGlobalFlags(command);
36
+ command.action(async (projectRef, options) => {
37
+ await runCommand(runtime, "project.link", options, (context) => runProjectLink(context, String(projectRef)), {
38
+ renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result),
39
+ renderJson: (result) => serializeProjectSetup(result)
40
+ });
41
+ });
42
+ return command;
43
+ }
18
44
  function createProjectListCommand(runtime) {
19
45
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("list"), runtime), "project.list");
20
46
  addGlobalFlags(command);
@@ -11,8 +11,9 @@ import { parseEnvAssignments } from "../lib/app/env-vars.js";
11
11
  import { renderDeployOutputRows } from "../lib/app/deploy-output.js";
12
12
  import { readBunPackageJson } from "../lib/app/bun-project.js";
13
13
  import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.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";
14
+ import { inferTargetName, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
15
+ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "../lib/project/local-pin.js";
16
+ import { bindProjectToDirectory, formatCommandArgument, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
16
17
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
17
18
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
18
19
  import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
@@ -96,44 +97,54 @@ async function runAppDeploy(context, appName, options) {
96
97
  ensurePreviewAppMode(context);
97
98
  const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
98
99
  const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
99
- const skipLocalPin = Boolean(envProjectId || envAppId);
100
+ assertExclusiveDeployProjectInputs({
101
+ projectRef: options?.projectRef,
102
+ createProjectName: options?.createProjectName,
103
+ envProjectId
104
+ });
105
+ const skipLocalPin = Boolean(envProjectId || options?.projectRef || options?.createProjectName);
100
106
  const localPin = skipLocalPin ? { kind: "missing" } : await readLocalResolutionPin(context.runtime.cwd);
101
107
  if (!skipLocalPin && localPin.kind === "invalid") throw localResolutionPinStaleError();
102
108
  const explicitBuildType = Boolean(options?.buildType && options.buildType !== "auto");
103
- const branch = await resolveDeployBranch(context, options?.branchName);
104
109
  if (options?.httpPort) parseDeployHttpPort(options.httpPort);
105
- let framework = await resolveDeployFramework(context, {
110
+ assertSupportedEntrypointForRequestedDeployShape({
106
111
  requestedFramework: options?.framework,
107
112
  requestedBuildType: options?.buildType,
108
- explicitBuildType
113
+ explicitBuildType,
114
+ entrypoint: options?.entrypoint
109
115
  });
110
- let runtime = resolveDeployRuntime(options?.httpPort, framework);
111
- assertSupportedEntrypoint(framework.buildType, options?.entrypoint, "deploy");
112
- const envVars = toOptionalEnvVars(parseEnvAssignments(options?.envAssignments, { commandName: "deploy" }));
113
- const firstDeploy = !skipLocalPin && localPin.kind === "missing";
116
+ const branch = await resolveDeployBranch(context, options?.branchName);
114
117
  const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
115
- allowCreate: true,
116
118
  branch,
119
+ createProjectName: options?.createProjectName,
117
120
  envProjectId,
118
121
  localPin
119
122
  });
123
+ let localPinResult;
124
+ if (target.localPinAction) {
125
+ const setupResult = await bindProjectToDirectory(context, target.workspace, target.project, target.localPinAction);
126
+ localPinResult = setupResult.localPin;
127
+ maybeRenderProjectLinked(context, setupResult.directory, setupResult.project.name, setupResult.localPin.path);
128
+ }
129
+ let framework = await resolveDeployFramework(context, {
130
+ requestedFramework: options?.framework,
131
+ requestedBuildType: options?.buildType,
132
+ explicitBuildType
133
+ });
134
+ let runtime = resolveDeployRuntime(options?.httpPort, framework);
135
+ assertSupportedEntrypoint(framework.buildType, options?.entrypoint, "deploy");
136
+ const envVars = toOptionalEnvVars(parseEnvAssignments(options?.envAssignments, { commandName: "deploy" }));
120
137
  const selectedApp = await resolveDeployAppSelection(context, projectId, await listApps(context, provider, projectId, target.branch.name), {
121
138
  explicitAppName: appName,
122
139
  explicitAppId: envAppId,
123
- firstDeploy,
140
+ firstDeploy: Boolean(target.localPinAction),
124
141
  inferName: () => inferTargetName(context.runtime.cwd)
125
142
  });
126
143
  await maybeRenderDeploySetupBlock(context, {
127
- firstDeploy: selectedApp.firstDeploy,
128
- workspaceName: target.workspace.name,
144
+ includeDirectory: !target.localPinAction,
129
145
  projectName: target.project.name,
130
- projectAnnotation: annotationForProjectResolution(target.resolution),
131
146
  branchName: target.branch.name,
132
- branchAnnotation: branch.annotation,
133
- appName: selectedApp.displayName,
134
- appAnnotation: selectedApp.annotation,
135
- framework,
136
- runtime
147
+ appName: selectedApp.displayName
137
148
  });
138
149
  const customized = await maybeCustomizeDeploySettings(context, {
139
150
  framework,
@@ -148,15 +159,6 @@ async function runAppDeploy(context, appName, options) {
148
159
  const buildType = framework.buildType;
149
160
  assertSupportedEntrypoint(buildType, options?.entrypoint, "deploy");
150
161
  const portMapping = parseDeployPortMapping(String(runtime.port));
151
- const shouldWriteLocalPin = firstDeploy && !skipLocalPin;
152
- if (shouldWriteLocalPin) {
153
- await writeLocalResolutionPin(context.runtime.cwd, {
154
- workspaceId: target.workspace.id,
155
- projectId: target.project.id
156
- });
157
- await ensureLocalResolutionPinGitignore(context.runtime.cwd);
158
- maybeRenderLocalPinBound(context, target.project.name);
159
- }
160
162
  const progressState = createPreviewDeployProgressState();
161
163
  const deployStartedAt = Date.now();
162
164
  const deployResult = await provider.deployApp({
@@ -194,10 +196,7 @@ async function runAppDeploy(context, appName, options) {
194
196
  },
195
197
  deployment: deployResult.deployment,
196
198
  durationMs: deployDurationMs,
197
- localPin: shouldWriteLocalPin ? {
198
- path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
199
- written: true
200
- } : void 0
199
+ localPin: localPinResult
201
200
  },
202
201
  warnings: [],
203
202
  nextSteps: ["prisma-cli app list-deploys", `prisma-cli app show-deploy ${deployResult.deployment.id}`]
@@ -719,11 +718,10 @@ async function resolveAppDomainTarget(context, options) {
719
718
  });
720
719
  const envProjectId = readDeployEnvOverride(context, PRISMA_PROJECT_ID_ENV_VAR);
721
720
  const envAppId = readDeployEnvOverride(context, PRISMA_APP_ID_ENV_VAR);
722
- const skipLocalPin = Boolean(envProjectId || envAppId);
721
+ const skipLocalPin = Boolean(envProjectId);
723
722
  const localPin = skipLocalPin ? { kind: "missing" } : await readLocalResolutionPin(context.runtime.cwd);
724
723
  if (!skipLocalPin && localPin.kind === "invalid") throw localResolutionPinStaleError();
725
724
  const { provider, target, projectId } = await requireProviderAndDeployProjectContext(context, options?.projectRef, {
726
- allowCreate: false,
727
725
  branch,
728
726
  envProjectId,
729
727
  localPin
@@ -1258,7 +1256,7 @@ function createPreviewLogAuthOptions(env) {
1258
1256
  }
1259
1257
  async function requireProviderAndProjectContext(context, explicitProject, options) {
1260
1258
  const { client, provider } = await requirePreviewAppProviderWithClient(context);
1261
- const target = await resolveProjectContext(context, client, provider, explicitProject, options);
1259
+ const target = await resolveProjectContext(context, client, explicitProject, options);
1262
1260
  return {
1263
1261
  client,
1264
1262
  provider,
@@ -1276,7 +1274,7 @@ async function requireProviderAndDeployProjectContext(context, explicitProject,
1276
1274
  projectId: target.project.id
1277
1275
  };
1278
1276
  }
1279
- async function resolveProjectContext(context, client, provider, explicitProject, options) {
1277
+ async function resolveProjectContext(context, client, explicitProject, options) {
1280
1278
  const authState = await requireAuthenticatedAuthState(context);
1281
1279
  if (!authState.workspace) throw workspaceRequiredError();
1282
1280
  const resolved = await resolveProjectTarget({
@@ -1284,22 +1282,6 @@ async function resolveProjectContext(context, client, provider, explicitProject,
1284
1282
  workspace: authState.workspace,
1285
1283
  explicitProject,
1286
1284
  listProjects: () => listRealWorkspaceProjects(client, authState.workspace),
1287
- createProject: options?.allowCreate ? async (name) => {
1288
- const project = await provider.createProject({ name }).catch((error) => {
1289
- throw createProjectOnFirstDeployError({
1290
- error,
1291
- inferredName: name,
1292
- workspaceName: authState.workspace.name
1293
- });
1294
- });
1295
- return {
1296
- id: project.id,
1297
- name: project.name,
1298
- workspace: authState.workspace
1299
- };
1300
- } : void 0,
1301
- allowCreate: options?.allowCreate,
1302
- prompt: createSelectPromptPort(context),
1303
1285
  remember: true
1304
1286
  });
1305
1287
  const branch = options?.branch ?? await resolveDeployBranch(context, void 0);
@@ -1316,30 +1298,30 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1316
1298
  if (!workspace) throw workspaceRequiredError();
1317
1299
  const branch = options.branch ?? await resolveDeployBranch(context, void 0);
1318
1300
  const projects = await listRealWorkspaceProjects(client, workspace);
1319
- const createProject = options.allowCreate ? async (name) => {
1320
- const project = await provider.createProject({ name }).catch((error) => {
1321
- throw createProjectOnFirstDeployError({
1322
- error,
1323
- inferredName: name,
1324
- workspaceName: workspace.name
1325
- });
1326
- });
1327
- return {
1328
- id: project.id,
1329
- name: project.name,
1330
- workspace
1331
- };
1332
- } : void 0;
1333
- if (explicitProject) return withDeployBranch(await resolveProjectTarget({
1334
- context,
1301
+ if (explicitProject) return withDeployBranch({
1335
1302
  workspace,
1336
- explicitProject,
1337
- listProjects: async () => projects,
1338
- createProject,
1339
- allowCreate: options.allowCreate,
1340
- prompt: createSelectPromptPort(context),
1341
- remember: true
1342
- }), branch);
1303
+ project: toProjectSummary(resolveProjectForSetup(explicitProject, projects, workspace)),
1304
+ resolution: {
1305
+ projectSource: "explicit",
1306
+ targetName: explicitProject,
1307
+ targetNameSource: "explicit"
1308
+ },
1309
+ localPinAction: "linked"
1310
+ }, branch);
1311
+ if (options.createProjectName) {
1312
+ const projectName = options.createProjectName.trim();
1313
+ if (!projectName) throw projectSetupNameRequiredError("app deploy --create-project");
1314
+ return withDeployBranch({
1315
+ workspace,
1316
+ project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace)),
1317
+ resolution: {
1318
+ projectSource: "created",
1319
+ targetName: projectName,
1320
+ targetNameSource: "explicit"
1321
+ },
1322
+ localPinAction: "created"
1323
+ }, branch);
1324
+ }
1343
1325
  if (options.envProjectId) {
1344
1326
  const project = projects.find((candidate) => candidate.id === options.envProjectId);
1345
1327
  if (!project) throw projectNotFoundError(options.envProjectId, workspace);
@@ -1368,15 +1350,91 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1368
1350
  }
1369
1351
  }, branch);
1370
1352
  }
1371
- return withDeployBranch(await resolveProjectTarget({
1372
- context,
1353
+ const platformMapping = await resolveDurablePlatformMapping();
1354
+ if (platformMapping && platformMapping.workspace.id === workspace.id) return withDeployBranch({
1373
1355
  workspace,
1374
- listProjects: async () => projects,
1375
- createProject,
1376
- allowCreate: options.allowCreate,
1377
- prompt: createSelectPromptPort(context),
1378
- remember: true
1379
- }), branch);
1356
+ project: toProjectSummary(platformMapping),
1357
+ resolution: {
1358
+ projectSource: "platform-mapping",
1359
+ targetName: platformMapping.name,
1360
+ targetNameSource: "platform-mapping"
1361
+ }
1362
+ }, branch);
1363
+ if (canPrompt(context) && !context.flags.yes) return withDeployBranch(await resolveInteractiveDeployProjectSetup(context, provider, workspace, projects), branch);
1364
+ throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd));
1365
+ }
1366
+ async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
1367
+ const sortedProjects = sortProjects(projects);
1368
+ const choice = await selectPrompt({
1369
+ input: context.runtime.stdin,
1370
+ output: context.runtime.stderr,
1371
+ message: "Which Project should this directory use?",
1372
+ choices: [
1373
+ ...sortedProjects.map((project) => ({
1374
+ label: project.name,
1375
+ value: {
1376
+ kind: "project",
1377
+ project
1378
+ }
1379
+ })),
1380
+ {
1381
+ label: "Create a new Project",
1382
+ value: { kind: "create" }
1383
+ },
1384
+ {
1385
+ label: "Cancel",
1386
+ value: { kind: "cancel" }
1387
+ }
1388
+ ]
1389
+ });
1390
+ if (choice.kind === "cancel") throw usageError("Project setup canceled", "Deploy needs a Project before it can continue.", "Choose an existing Project or create a new one, then rerun deploy.", ["prisma-cli app deploy --project <id-or-name>", "prisma-cli app deploy --create-project <name>"], "project");
1391
+ if (choice.kind === "project") return {
1392
+ workspace,
1393
+ project: toProjectSummary(choice.project),
1394
+ resolution: {
1395
+ projectSource: "prompt",
1396
+ targetName: choice.project.name,
1397
+ targetNameSource: "prompt"
1398
+ },
1399
+ localPinAction: "linked"
1400
+ };
1401
+ const suggestedName = await inferTargetName(context.runtime.cwd);
1402
+ const rawName = await textPrompt({
1403
+ input: context.runtime.stdin,
1404
+ output: context.runtime.stderr,
1405
+ message: "Project name",
1406
+ placeholder: suggestedName.name,
1407
+ validate: (value) => validateProjectSetupNameText(value, suggestedName.name)
1408
+ });
1409
+ const projectName = rawName.trim() || suggestedName.name;
1410
+ return {
1411
+ workspace,
1412
+ project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace)),
1413
+ resolution: {
1414
+ projectSource: "created",
1415
+ targetName: projectName,
1416
+ targetNameSource: rawName.trim() ? "prompt" : suggestedName.source
1417
+ },
1418
+ localPinAction: "created"
1419
+ };
1420
+ }
1421
+ async function createProjectForDeploySetup(provider, projectName, workspace) {
1422
+ const created = await provider.createProject({ name: projectName }).catch((error) => {
1423
+ throw projectCreateFailedError(error, projectName, workspace, {
1424
+ nextSteps: [
1425
+ "prisma-cli project list",
1426
+ "prisma-cli app deploy --project <id-or-name>",
1427
+ `prisma-cli app deploy --create-project ${formatCommandArgument(projectName)}`
1428
+ ],
1429
+ permissionFix: "Choose an existing Project with --project, or grant the token permission to create Projects in this workspace.",
1430
+ fallbackFix: "Choose an existing Project with --project, or retry after addressing the platform error above."
1431
+ });
1432
+ });
1433
+ return {
1434
+ id: created.id,
1435
+ name: created.name,
1436
+ workspace
1437
+ };
1380
1438
  }
1381
1439
  function withDeployBranch(target, branch) {
1382
1440
  return {
@@ -1387,15 +1445,26 @@ function withDeployBranch(target, branch) {
1387
1445
  }
1388
1446
  };
1389
1447
  }
1390
- function toProjectSummary(project) {
1391
- return {
1392
- id: project.id,
1393
- name: project.name
1394
- };
1395
- }
1396
1448
  function toBranchKind(name) {
1397
1449
  return name === "production" || name === "main" ? "production" : "preview";
1398
1450
  }
1451
+ function assertExclusiveDeployProjectInputs(options) {
1452
+ const provided = [
1453
+ options.projectRef ? "--project" : null,
1454
+ options.createProjectName ? "--create-project" : null,
1455
+ options.envProjectId ? PRISMA_PROJECT_ID_ENV_VAR : null
1456
+ ].filter((value) => Boolean(value));
1457
+ if (provided.length <= 1) return;
1458
+ throw usageError("Project selection is ambiguous", `${provided.join(", ")} cannot be used together.`, "Choose exactly one Project source for this deploy.", [
1459
+ "prisma-cli app deploy --project <id-or-name>",
1460
+ "prisma-cli app deploy --create-project <name>",
1461
+ `unset ${PRISMA_PROJECT_ID_ENV_VAR}`
1462
+ ], "project");
1463
+ }
1464
+ function validateProjectSetupNameText(value, fallback) {
1465
+ if ((value?.trim() || fallback).trim().length > 0) return;
1466
+ return "Enter a Project name.";
1467
+ }
1399
1468
  async function resolveDeployBranch(context, explicitBranchName) {
1400
1469
  if (explicitBranchName) return {
1401
1470
  name: explicitBranchName,
@@ -1459,6 +1528,14 @@ function resolveDeployRuntime(requestedHttpPort, framework) {
1459
1528
  annotation: `${framework.displayName} default`
1460
1529
  };
1461
1530
  }
1531
+ function assertSupportedEntrypointForRequestedDeployShape(options) {
1532
+ if (options.requestedFramework) {
1533
+ assertSupportedEntrypoint(frameworkFromUserFacingValue(options.requestedFramework, "set by --framework").buildType, options.entrypoint, "deploy");
1534
+ return;
1535
+ }
1536
+ if (!options.explicitBuildType) return;
1537
+ assertSupportedEntrypoint(normalizeBuildType(options.requestedBuildType), options.entrypoint, "deploy");
1538
+ }
1462
1539
  async function detectDeployFramework(cwd) {
1463
1540
  const packageJson = await readBunPackageJson(cwd);
1464
1541
  const nextConfig = await detectNextConfig(cwd);
@@ -1558,53 +1635,12 @@ function frameworkNotDetectedError(cwd, requestedFramework) {
1558
1635
  async function maybeRenderDeploySetupBlock(context, details) {
1559
1636
  if (context.flags.json || context.flags.quiet) return;
1560
1637
  const directory = formatDeployDirectory(context.runtime.cwd);
1561
- if (!details.firstDeploy) {
1562
- context.output.stderr.write(`Deploying ${directory} to ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`);
1563
- return;
1564
- }
1565
- const title = `Setting up your local directory ${formatLocalDirectory(context.runtime.cwd, context.runtime.env)}`;
1566
- const rows = details.firstDeploy ? [
1567
- {
1568
- label: "Workspace",
1569
- value: details.workspaceName
1570
- },
1571
- {
1572
- label: "Project",
1573
- value: details.projectName,
1574
- origin: details.projectAnnotation
1575
- },
1576
- {
1577
- label: "Branch",
1578
- value: details.branchName,
1579
- origin: details.branchAnnotation
1580
- },
1581
- {
1582
- label: "App",
1583
- value: details.appName,
1584
- origin: details.appAnnotation
1585
- },
1586
- {
1587
- label: "Framework",
1588
- value: details.framework.displayName,
1589
- origin: details.framework.annotation
1590
- },
1591
- {
1592
- label: "Runtime",
1593
- value: `HTTP ${details.runtime.port}`,
1594
- origin: details.runtime.annotation
1595
- }
1596
- ] : [];
1597
- const lines = [
1598
- title,
1599
- "",
1600
- ...renderDeployOutputRows(context.ui, rows),
1601
- ""
1602
- ];
1603
- context.output.stderr.write(`${lines.join("\n")}\n`);
1638
+ const prefix = details.includeDirectory ? `Deploying ${directory} to` : "Deploying to";
1639
+ context.output.stderr.write(`${prefix} ${details.projectName} / ${details.branchName} / ${details.appName}\n\n`);
1604
1640
  }
1605
- function maybeRenderLocalPinBound(context, projectName) {
1641
+ function maybeRenderProjectLinked(context, directory, projectName, localPinPath) {
1606
1642
  if (context.flags.json || context.flags.quiet) return;
1607
- context.output.stderr.write(`This directory is now linked to project ${projectName}.\n\n`);
1643
+ context.output.stderr.write(`${context.ui.success("✔")} Linked "${directory}" to Project "${projectName}"\nSaved ${localPinPath}\n\n`);
1608
1644
  }
1609
1645
  async function maybeCustomizeDeploySettings(context, options) {
1610
1646
  if (!options.firstDeploy || context.flags.yes || options.explicitFramework || options.explicitBuildType || options.explicitHttpPort || !canPrompt(context)) return {
@@ -1659,19 +1695,6 @@ async function maybeCustomizeDeploySettings(context, options) {
1659
1695
  runtime
1660
1696
  };
1661
1697
  }
1662
- function annotationForProjectResolution(resolution) {
1663
- switch (resolution.projectSource) {
1664
- case "explicit": return "set by --project";
1665
- case "env": return `from ${PRISMA_PROJECT_ID_ENV_VAR}`;
1666
- case "local-pin": return "from local pin";
1667
- case "created": return resolution.targetNameSource === "directory-name" ? "created from directory name" : "created from package.json";
1668
- case "package-name":
1669
- case "directory-name": return "linked to existing project";
1670
- case "platform-mapping":
1671
- case "remembered-local": return "linked to existing project";
1672
- case "prompt": return "selected by you";
1673
- }
1674
- }
1675
1698
  function frameworkDisplayName(framework) {
1676
1699
  switch (framework) {
1677
1700
  case "nextjs": return "Next.js";
@@ -1692,15 +1715,6 @@ function formatDeployDirectory(cwd) {
1692
1715
  const basename = path.basename(cwd);
1693
1716
  return basename ? `./${basename}` : ".";
1694
1717
  }
1695
- function formatLocalDirectory(cwd, env) {
1696
- const resolved = path.resolve(cwd);
1697
- const home = env.HOME ? path.resolve(env.HOME) : null;
1698
- if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
1699
- const relative = path.relative(home, resolved);
1700
- return relative ? `~/${relative}` : "~";
1701
- }
1702
- return resolved;
1703
- }
1704
1718
  async function readCurrentWorkspaceId(context) {
1705
1719
  const state = await context.stateStore.read();
1706
1720
  if (state.auth?.workspaceId) return state.auth.workspaceId;
@@ -1835,52 +1849,31 @@ function readDeployEnvOverride(context, name) {
1835
1849
  const value = context.runtime.env[name]?.trim();
1836
1850
  return value ? value : void 0;
1837
1851
  }
1838
- /**
1839
- * `app deploy` falls into "create a new project on first deploy" when no
1840
- * existing project matches the package.json name (or the cwd basename as a
1841
- * fallback). When the create call fails the user often doesn't realise the
1842
- * CLI was attempting to create a project at all — they thought the deploy
1843
- * would find an existing project. Surface that context, and recommend the
1844
- * explicit `--project` flag as the unambiguous way out.
1845
- */
1846
- function createProjectOnFirstDeployError(options) {
1847
- const { error, inferredName, workspaceName } = options;
1848
- const status = extractHttpStatus(error);
1849
- const errorMessage = error instanceof Error ? error.message : String(error);
1850
- const inferredContext = `No existing project matched the package.json name \`${inferredName}\`, so the CLI attempted to create one.`;
1851
- const nextSteps = ["prisma-cli project list", "prisma-cli app deploy --project <id-or-name>"];
1852
- if (status === 401 || status === 403) return new CliError({
1853
- code: "AUTH_FORBIDDEN",
1854
- domain: "auth",
1855
- summary: "Could not create a new project for this deploy",
1856
- why: `${inferredContext} The platform rejected the create (HTTP ${status}).`,
1857
- fix: `Pass --project <id-or-name> to deploy into an existing project, or grant the service token project-create permission on workspace \`${workspaceName}\`.`,
1858
- debug: formatDebugDetails(error),
1859
- exitCode: 1,
1860
- nextSteps
1861
- });
1852
+ function projectSetupRequiredError(projects, suggestedName) {
1853
+ const createCommand = `prisma-cli app deploy --create-project ${formatCommandArgument(suggestedName.name)}`;
1862
1854
  return new CliError({
1863
- code: "DEPLOY_FAILED",
1864
- domain: "app",
1865
- summary: "Could not create a new project for this deploy",
1866
- why: `${inferredContext} ${errorMessage}`.trim(),
1867
- fix: "Pass --project <id-or-name> to deploy into an existing project, or retry after addressing the platform error above.",
1868
- debug: formatDebugDetails(error),
1855
+ code: "PROJECT_SETUP_REQUIRED",
1856
+ domain: "project",
1857
+ summary: "Choose a Project before deploying this directory",
1858
+ why: "This directory is not linked to a Prisma Project, and deploy will not choose or create one implicitly.",
1859
+ fix: "Choose an existing Project with --project, create one with --create-project, or rerun interactively to pick from the setup list.",
1860
+ meta: {
1861
+ candidates: sortProjects(projects).map((project) => ({
1862
+ id: project.id,
1863
+ name: project.name
1864
+ })),
1865
+ suggestedProjectName: suggestedName.name,
1866
+ suggestedProjectNameSource: suggestedName.source,
1867
+ recoveryCommands: ["prisma-cli app deploy --project <id-or-name>", createCommand]
1868
+ },
1869
1869
  exitCode: 1,
1870
- nextSteps
1870
+ nextSteps: [
1871
+ "prisma-cli project list",
1872
+ "prisma-cli app deploy --project <id-or-name>",
1873
+ createCommand
1874
+ ]
1871
1875
  });
1872
1876
  }
1873
- function extractHttpStatus(error) {
1874
- if (!error || typeof error !== "object") return null;
1875
- const candidate = error;
1876
- if (typeof candidate.statusCode === "number") return candidate.statusCode;
1877
- if (typeof candidate.status === "number") return candidate.status;
1878
- if (typeof candidate.message === "string") {
1879
- const match = /\(HTTP (\d{3})\)/.exec(candidate.message);
1880
- if (match) return Number.parseInt(match[1], 10);
1881
- }
1882
- return null;
1883
- }
1884
1877
  function noDeploymentsError(summary, why) {
1885
1878
  return new CliError({
1886
1879
  code: "NO_DEPLOYMENTS",
@@ -1,8 +1,10 @@
1
- import { CliError, authRequiredError, usageError, workspaceRequiredError } from "../shell/errors.js";
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
4
  import { requireComputeAuth } from "../lib/auth/guard.js";
5
5
  import { resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
6
+ import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup } from "../lib/project/setup.js";
7
+ import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
6
8
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
7
9
  import { requireAuthenticatedAuthState } from "./auth.js";
8
10
  import { parseGitHubRepositoryUrl, readGitOriginRemote } from "../adapters/git.js";
@@ -48,6 +50,44 @@ async function runProjectShow(context, explicitProject) {
48
50
  nextSteps: []
49
51
  };
50
52
  }
53
+ async function runProjectCreate(context, projectName) {
54
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
55
+ if (!workspace) throw workspaceRequiredError();
56
+ if (!isValidProjectSetupName(projectName)) throw projectSetupNameRequiredError("project create");
57
+ if (!isRealMode(context)) throw featureUnavailableError("Project create is not available in fixture mode", "Creating Projects requires live platform integration.", "Rerun without fixture mode enabled to create a Project.", ["prisma-cli auth login"], "project");
58
+ const client = await requireComputeAuth(context.runtime.env);
59
+ if (!client) throw authRequiredError();
60
+ const provider = createPreviewAppProvider(client);
61
+ const name = projectName.trim();
62
+ const created = await provider.createProject({ name }).catch((error) => {
63
+ throw projectCreateFailedError(error, name, workspace, {
64
+ nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"],
65
+ permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
66
+ fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
67
+ });
68
+ });
69
+ return {
70
+ command: "project.create",
71
+ result: await bindProjectToDirectory(context, workspace, {
72
+ id: created.id,
73
+ name: created.name
74
+ }, "created"),
75
+ warnings: [],
76
+ nextSteps: ["prisma-cli app deploy"]
77
+ };
78
+ }
79
+ async function runProjectLink(context, projectRef) {
80
+ const workspace = (await requireAuthenticatedAuthState(context)).workspace;
81
+ if (!workspace) throw workspaceRequiredError();
82
+ if (!projectRef || !projectRef.trim()) throw usageError("Project link requires a Project id or name", "The command cannot choose a Project without an explicit id or name.", "Pass the Project id or name as the first argument.", ["prisma-cli project link proj_123"], "project");
83
+ const projects = isRealMode(context) ? await listRealProjectsForLink(context, workspace) : listFixtureWorkspaceProjects(context, workspace);
84
+ return {
85
+ command: "project.link",
86
+ result: await bindProjectToDirectory(context, workspace, toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace)), "linked"),
87
+ warnings: [],
88
+ nextSteps: ["prisma-cli app deploy"]
89
+ };
90
+ }
51
91
  async function runGitConnect(context, gitUrl, options = {}) {
52
92
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
53
93
  if (!workspace) throw workspaceRequiredError();
@@ -163,6 +203,11 @@ async function resolveProjectShowInRealMode(context, workspace, explicitProject)
163
203
  remember: false
164
204
  });
165
205
  }
206
+ async function listRealProjectsForLink(context, workspace) {
207
+ const client = await requireComputeAuth(context.runtime.env);
208
+ if (!client) throw authRequiredError();
209
+ return listRealWorkspaceProjects(client, workspace);
210
+ }
166
211
  async function resolveProjectShowInFixtureMode(context, workspace, explicitProject) {
167
212
  return resolveProjectTarget({
168
213
  context,
@@ -501,4 +546,4 @@ function toProjectSummary(project) {
501
546
  };
502
547
  }
503
548
  //#endregion
504
- export { listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectList, runProjectShow };
549
+ export { listRealWorkspaceProjects, runGitConnect, runGitDisconnect, runProjectCreate, runProjectLink, runProjectList, runProjectShow };
@@ -198,4 +198,4 @@ function toProjectSummary(project) {
198
198
  };
199
199
  }
200
200
  //#endregion
201
- export { inferTargetName, projectNotFoundError, resolveProjectTarget, sortProjects };
201
+ export { inferTargetName, projectAmbiguousError, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
@@ -0,0 +1,86 @@
1
+ import { CliError, usageError } from "../../shell/errors.js";
2
+ import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
3
+ import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
4
+ //#region src/lib/project/setup.ts
5
+ function isValidProjectSetupName(projectName) {
6
+ return projectName.trim().length > 0;
7
+ }
8
+ function resolveProjectForSetup(projectRef, projects, workspace) {
9
+ const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
10
+ if (matches.length === 1) return matches[0];
11
+ if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
12
+ throw projectNotFoundError(projectRef, workspace);
13
+ }
14
+ async function bindProjectToDirectory(context, workspace, project, action) {
15
+ await writeLocalResolutionPin(context.runtime.cwd, {
16
+ workspaceId: workspace.id,
17
+ projectId: project.id
18
+ });
19
+ await ensureLocalResolutionPinGitignore(context.runtime.cwd);
20
+ return {
21
+ workspace,
22
+ project,
23
+ directory: formatSetupDirectory(context.runtime.cwd),
24
+ localPin: {
25
+ path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
26
+ written: true
27
+ },
28
+ action
29
+ };
30
+ }
31
+ function toProjectSummary(project) {
32
+ return {
33
+ id: project.id,
34
+ name: project.name
35
+ };
36
+ }
37
+ function projectSetupNameRequiredError(command) {
38
+ return usageError("Project create requires a name", "The project name must be a non-empty value.", "Pass a Project name explicitly.", [`prisma-cli ${command} my-app`], "project");
39
+ }
40
+ function projectCreateFailedError(error, projectName, workspace, options) {
41
+ const status = extractHttpStatus(error);
42
+ if (status === 401 || status === 403) return new CliError({
43
+ code: "PROJECT_CREATE_FAILED",
44
+ domain: "project",
45
+ summary: `Could not create Project "${projectName}"`,
46
+ why: `The platform rejected the Project create in workspace "${workspace.name}" (HTTP ${status}).`,
47
+ fix: options.permissionFix,
48
+ debug: formatDebugDetails(error),
49
+ exitCode: 1,
50
+ nextSteps: options.nextSteps
51
+ });
52
+ return new CliError({
53
+ code: "PROJECT_CREATE_FAILED",
54
+ domain: "project",
55
+ summary: `Could not create Project "${projectName}"`,
56
+ why: error instanceof Error ? error.message : String(error),
57
+ fix: options.fallbackFix,
58
+ debug: formatDebugDetails(error),
59
+ exitCode: 1,
60
+ nextSteps: options.nextSteps
61
+ });
62
+ }
63
+ function formatCommandArgument(value) {
64
+ return /^[A-Za-z0-9._/-]+$/.test(value) ? value : JSON.stringify(value);
65
+ }
66
+ function formatSetupDirectory(cwd) {
67
+ const basename = cwd.split(/[\\/]/).filter(Boolean).pop();
68
+ return basename ? `./${basename}` : ".";
69
+ }
70
+ function extractHttpStatus(error) {
71
+ if (!error || typeof error !== "object") return null;
72
+ const candidate = error;
73
+ if (typeof candidate.statusCode === "number") return candidate.statusCode;
74
+ if (typeof candidate.status === "number") return candidate.status;
75
+ if (typeof candidate.message === "string") {
76
+ const match = /\(HTTP (\d{3})\)/.exec(candidate.message);
77
+ if (match) return Number.parseInt(match[1], 10);
78
+ }
79
+ return null;
80
+ }
81
+ function formatDebugDetails(error) {
82
+ if (error instanceof Error) return error.stack ?? error.message;
83
+ return typeof error === "string" ? error : null;
84
+ }
85
+ //#endregion
86
+ export { bindProjectToDirectory, formatCommandArgument, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary };
@@ -1,3 +1,4 @@
1
+ import { renderSummaryLine } from "../shell/ui.js";
1
2
  import { renderList, renderMutate, renderShow, serializeList } from "../output/patterns.js";
2
3
  //#region src/presenters/project.ts
3
4
  function renderProjectList(context, descriptor, result) {
@@ -51,6 +52,14 @@ function renderProjectShow(context, descriptor, result) {
51
52
  function serializeProjectShow(result) {
52
53
  return result;
53
54
  }
55
+ function renderProjectSetup(context, _descriptor, result) {
56
+ const lines = result.action === "created" ? [renderSummaryLine(context.ui, "success", `Created Project "${result.project.name}"`)] : [];
57
+ lines.push(renderSummaryLine(context.ui, "success", `Linked "${result.directory}" to Project "${result.project.name}"`), `Saved ${result.localPin.path}`);
58
+ return lines;
59
+ }
60
+ function serializeProjectSetup(result) {
61
+ return result;
62
+ }
54
63
  function renderGitConnect(context, descriptor, result) {
55
64
  const connection = result.repositoryConnection;
56
65
  return renderMutate({
@@ -124,4 +133,4 @@ function formatGitConnectionDetail(status) {
124
133
  }
125
134
  }
126
135
  //#endregion
127
- export { renderGitConnect, renderGitDisconnect, renderProjectList, renderProjectShow, serializeProjectList, serializeProjectShow };
136
+ export { renderGitConnect, renderGitDisconnect, renderProjectList, renderProjectSetup, renderProjectShow, serializeProjectList, serializeProjectSetup, serializeProjectShow };
@@ -54,7 +54,11 @@ const DESCRIPTORS = [
54
54
  id: "project",
55
55
  path: ["prisma", "project"],
56
56
  description: "Manage and inspect your Prisma projects",
57
- examples: ["prisma-cli project list", "prisma-cli project show"]
57
+ examples: [
58
+ "prisma-cli project list",
59
+ "prisma-cli project link proj_123",
60
+ "prisma-cli project create my-app"
61
+ ]
58
62
  },
59
63
  {
60
64
  id: "app",
@@ -94,6 +98,26 @@ const DESCRIPTORS = [
94
98
  description: "Show which project is active for this directory",
95
99
  examples: ["prisma-cli project show", "prisma-cli project show --project proj_123 --json"]
96
100
  },
101
+ {
102
+ id: "project.create",
103
+ path: [
104
+ "prisma",
105
+ "project",
106
+ "create"
107
+ ],
108
+ description: "Create a Project and link this directory",
109
+ examples: ["prisma-cli project create my-app", "prisma-cli project create my-app --json"]
110
+ },
111
+ {
112
+ id: "project.link",
113
+ path: [
114
+ "prisma",
115
+ "project",
116
+ "link"
117
+ ],
118
+ description: "Link this directory to an existing Project",
119
+ examples: ["prisma-cli project link proj_123", "prisma-cli project link \"Acme Dashboard\" --json"]
120
+ },
97
121
  {
98
122
  id: "git.connect",
99
123
  path: [
@@ -183,6 +207,8 @@ const DESCRIPTORS = [
183
207
  longDescription: "Agent skills for guided Next.js deploys are available from the Prisma CLI skill cluster.",
184
208
  examples: [
185
209
  "prisma-cli app deploy",
210
+ "prisma-cli app deploy --project proj_123",
211
+ "prisma-cli app deploy --create-project my-app --yes",
186
212
  "prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
187
213
  "prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
188
214
  "prisma-cli app deploy --branch feat-login --framework hono",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.38.1",
3
+ "version": "3.0.0-dev.39.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {