@prisma/cli 3.0.0-dev.42.1 → 3.0.0-dev.46.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.
@@ -31,10 +31,10 @@ function createProjectCreateCommand(runtime) {
31
31
  }
32
32
  function createProjectLinkCommand(runtime) {
33
33
  const command = attachCommandDescriptor(configureRuntimeCommand(new Command("link"), runtime), "project.link");
34
- command.argument("<id-or-name>", "Project id or name");
34
+ command.argument("[id-or-name]", "Project id or name");
35
35
  addGlobalFlags(command);
36
36
  command.action(async (projectRef, options) => {
37
- await runCommand(runtime, "project.link", options, (context) => runProjectLink(context, String(projectRef)), {
37
+ await runCommand(runtime, "project.link", options, (context) => runProjectLink(context, typeof projectRef === "string" ? projectRef : void 0), {
38
38
  renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result),
39
39
  renderJson: (result) => serializeProjectSetup(result)
40
40
  });
@@ -8,13 +8,14 @@ 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";
11
+ import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output.js";
12
12
  import { readBunPackageEntrypoint, readBunPackageJson } from "../lib/app/bun-project.js";
13
13
  import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.js";
14
14
  import { formatCommandArgument } from "../shell/command-arguments.js";
15
15
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "../lib/project/local-pin.js";
16
16
  import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
17
17
  import { bindProjectToDirectory, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
18
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
18
19
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
19
20
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
20
21
  import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
@@ -1362,58 +1363,25 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1362
1363
  throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd));
1363
1364
  }
1364
1365
  async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
1365
- const sortedProjects = sortProjects(projects);
1366
- const choice = await selectPrompt({
1367
- input: context.runtime.stdin,
1368
- output: context.runtime.stderr,
1369
- message: "Which Project should this directory use?",
1370
- choices: [
1371
- ...sortedProjects.map((project) => ({
1372
- label: project.name,
1373
- value: {
1374
- kind: "project",
1375
- project
1376
- }
1377
- })),
1378
- {
1379
- label: "Create a new Project",
1380
- value: { kind: "create" }
1381
- },
1382
- {
1383
- label: "Cancel",
1384
- value: { kind: "cancel" }
1385
- }
1386
- ]
1387
- });
1388
- 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");
1389
- if (choice.kind === "project") return {
1390
- workspace,
1391
- project: toProjectSummary(choice.project),
1392
- resolution: {
1393
- projectSource: "prompt",
1394
- targetName: choice.project.name,
1395
- targetNameSource: "prompt"
1396
- },
1397
- localPinAction: "linked"
1398
- };
1399
- const suggestedName = await inferTargetName(context.runtime.cwd);
1400
- const rawName = await textPrompt({
1401
- input: context.runtime.stdin,
1402
- output: context.runtime.stderr,
1403
- message: "Project name",
1404
- placeholder: suggestedName.name,
1405
- validate: (value) => validateProjectSetupNameText(value, suggestedName.name)
1366
+ const setup = await promptForProjectSetupChoice({
1367
+ context,
1368
+ projects,
1369
+ createProject: (projectName) => createProjectForDeploySetup(provider, projectName, workspace),
1370
+ cancel: {
1371
+ why: "Deploy needs a Project before it can continue.",
1372
+ fix: "Choose an existing Project or create a new one, then rerun deploy.",
1373
+ nextSteps: ["prisma-cli app deploy --project <id-or-name>", "prisma-cli app deploy --create-project <name>"]
1374
+ }
1406
1375
  });
1407
- const projectName = rawName.trim() || suggestedName.name;
1408
1376
  return {
1409
1377
  workspace,
1410
- project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace)),
1378
+ project: setup.project,
1411
1379
  resolution: {
1412
- projectSource: "created",
1413
- targetName: projectName,
1414
- targetNameSource: rawName.trim() ? "prompt" : suggestedName.source
1380
+ projectSource: setup.action === "created" ? "created" : "prompt",
1381
+ targetName: setup.targetName,
1382
+ targetNameSource: setup.targetNameSource
1415
1383
  },
1416
- localPinAction: "created"
1384
+ localPinAction: setup.action
1417
1385
  };
1418
1386
  }
1419
1387
  async function createProjectForDeploySetup(provider, projectName, workspace) {
@@ -1459,10 +1427,6 @@ function assertExclusiveDeployProjectInputs(options) {
1459
1427
  `unset ${PRISMA_PROJECT_ID_ENV_VAR}`
1460
1428
  ], "project");
1461
1429
  }
1462
- function validateProjectSetupNameText(value, fallback) {
1463
- if ((value?.trim() || fallback).trim().length > 0) return;
1464
- return "Enter a Project name.";
1465
- }
1466
1430
  async function resolveDeployBranch(context, explicitBranchName) {
1467
1431
  if (explicitBranchName) return {
1468
1432
  name: explicitBranchName,
@@ -1665,10 +1629,14 @@ async function maybeCustomizeDeploySettings(context, options) {
1665
1629
  framework: options.framework,
1666
1630
  runtime: options.runtime
1667
1631
  };
1632
+ maybeRenderDeploySettingsPreview(context, {
1633
+ framework: options.framework,
1634
+ runtime: options.runtime
1635
+ });
1668
1636
  if (!await confirmPrompt({
1669
1637
  input: context.runtime.stdin,
1670
1638
  output: context.runtime.stderr,
1671
- message: "Customize settings?",
1639
+ message: "Customize build settings?",
1672
1640
  initialValue: false
1673
1641
  })) return {
1674
1642
  framework: options.framework,
@@ -1713,6 +1681,16 @@ async function maybeCustomizeDeploySettings(context, options) {
1713
1681
  runtime
1714
1682
  };
1715
1683
  }
1684
+ function maybeRenderDeploySettingsPreview(context, options) {
1685
+ if (context.flags.quiet || context.flags.json) return;
1686
+ context.output.stderr.write(`Detected ${options.framework.displayName}\n${renderDeploySettingsPreview(context.ui, [{
1687
+ key: "framework",
1688
+ value: options.framework.displayName
1689
+ }, {
1690
+ key: "runtime",
1691
+ value: `HTTP ${options.runtime.port}`
1692
+ }]).join("\n")}\n\n`);
1693
+ }
1716
1694
  function frameworkDisplayName(framework) {
1717
1695
  switch (framework) {
1718
1696
  case "nextjs": return "Next.js";
@@ -2,9 +2,11 @@ import { CliError, authRequiredError, featureUnavailableError, usageError, works
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
+ import { formatCommandArgument } from "../shell/command-arguments.js";
5
6
  import { readLocalResolutionPin } from "../lib/project/local-pin.js";
6
- import { buildProjectSetupNextActions, inspectProjectBinding, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
7
+ import { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
7
8
  import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
9
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
8
10
  import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
9
11
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
10
12
  import { requireAuthenticatedAuthState } from "./auth.js";
@@ -60,7 +62,10 @@ async function runProjectList(context) {
60
62
  };
61
63
  }
62
64
  function buildProjectListNextActions(localBinding) {
63
- return localBinding?.status === "linked" ? [] : buildProjectSetupNextActions({ reason: localBinding?.status === "invalid" ? "This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands." : "This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory." });
65
+ return localBinding?.status === "linked" ? [] : buildProjectSetupNextActions({
66
+ createCommand: "prisma-cli project create <name>",
67
+ reason: localBinding?.status === "invalid" ? "This directory has an invalid local Project binding. Ask the user which Prisma Project to link before running Project-scoped commands." : "This directory is not linked to a Prisma Project. Project list shows available Projects, but none is selected for this directory."
68
+ });
64
69
  }
65
70
  async function runProjectShow(context, explicitProject) {
66
71
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
@@ -107,15 +112,84 @@ async function runProjectCreate(context, projectName) {
107
112
  async function runProjectLink(context, projectRef) {
108
113
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
109
114
  if (!workspace) throw workspaceRequiredError();
110
- 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");
111
- const projects = isRealMode(context) ? await listRealProjectsForLink(context, workspace) : listFixtureWorkspaceProjects(context, workspace);
115
+ let provider = null;
116
+ let projects;
117
+ if (isRealMode(context)) {
118
+ const client = await requireComputeAuth(context.runtime.env);
119
+ if (!client) throw authRequiredError();
120
+ provider = createPreviewAppProvider(client);
121
+ projects = await listRealWorkspaceProjects(client, workspace);
122
+ } else projects = listFixtureWorkspaceProjects(context, workspace);
123
+ let result;
124
+ if (projectRef?.trim()) result = await bindProjectToDirectory(context, workspace, toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace)), "linked");
125
+ else if (canPrompt(context) && !context.flags.yes) result = await resolveInteractiveProjectLinkSetup(context, workspace, projects, provider);
126
+ else throw await projectLinkTargetRequiredError(context, projects);
112
127
  return {
113
128
  command: "project.link",
114
- result: await bindProjectToDirectory(context, workspace, toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace)), "linked"),
129
+ result,
115
130
  warnings: [],
116
131
  nextSteps: ["prisma-cli app deploy"]
117
132
  };
118
133
  }
134
+ async function resolveInteractiveProjectLinkSetup(context, workspace, projects, provider) {
135
+ const setup = await promptForProjectSetupChoice({
136
+ context,
137
+ projects,
138
+ createProject: (projectName) => {
139
+ if (!provider) 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");
140
+ return createProjectForLinkSetup(provider, projectName, workspace);
141
+ },
142
+ cancel: {
143
+ why: "Project link needs a Project before it can continue.",
144
+ fix: "Choose an existing Project or create a new one, then rerun project link.",
145
+ nextSteps: ["prisma-cli project link <id-or-name>", "prisma-cli project create <name>"]
146
+ }
147
+ });
148
+ return bindProjectToDirectory(context, workspace, setup.project, setup.action);
149
+ }
150
+ async function createProjectForLinkSetup(provider, projectName, workspace) {
151
+ const created = await provider.createProject({ name: projectName }).catch((error) => {
152
+ throw projectCreateFailedError(error, projectName, workspace, {
153
+ nextSteps: [
154
+ "prisma-cli project list",
155
+ "prisma-cli project link <id-or-name>",
156
+ `prisma-cli project create ${formatCommandArgument(projectName)}`
157
+ ],
158
+ permissionFix: "Grant the token permission to create Projects in this workspace, or link an existing Project.",
159
+ fallbackFix: "Retry the command, or choose an existing Project with prisma-cli project link <id-or-name>."
160
+ });
161
+ });
162
+ return {
163
+ id: created.id,
164
+ name: created.name,
165
+ workspace
166
+ };
167
+ }
168
+ async function projectLinkTargetRequiredError(context, projects) {
169
+ const suggestedName = await inferTargetName(context.runtime.cwd);
170
+ const createCommand = `prisma-cli project create ${formatCommandArgument(suggestedName.name)}`;
171
+ const recoveryCommands = ["prisma-cli project link <id-or-name>", createCommand];
172
+ return new CliError({
173
+ code: "PROJECT_LINK_TARGET_REQUIRED",
174
+ domain: "project",
175
+ summary: "Choose a Project to link this directory",
176
+ why: "This directory is not linked to a Prisma Project. Existing Projects are candidates until the user chooses one, and package or directory names are suggestions only.",
177
+ fix: "Run prisma-cli project link in a TTY to choose from the setup list, pass a Project id or name, or create a new Project.",
178
+ meta: {
179
+ suggestedProjectName: suggestedName.name,
180
+ suggestedProjectNameSource: suggestedName.source,
181
+ candidates: sortProjects(projects).map(toProjectSummary),
182
+ recoveryCommands
183
+ },
184
+ exitCode: 2,
185
+ nextSteps: ["prisma-cli project list", ...recoveryCommands],
186
+ nextActions: buildProjectSetupNextActions({
187
+ suggestedProjectName: suggestedName.name,
188
+ createCommand,
189
+ reason: "Project link needs the user to choose an existing Project or create a new one. Existing Projects, package names, and directory names are candidates only, not selections."
190
+ })
191
+ });
192
+ }
119
193
  async function runGitConnect(context, gitUrl, options = {}) {
120
194
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
121
195
  if (!workspace) throw workspaceRequiredError();
@@ -242,11 +316,6 @@ async function resolveRequiredProjectInRealMode(context, workspace, explicitProj
242
316
  commandName
243
317
  });
244
318
  }
245
- async function listRealProjectsForLink(context, workspace) {
246
- const client = await requireComputeAuth(context.runtime.env);
247
- if (!client) throw authRequiredError();
248
- return listRealWorkspaceProjects(client, workspace);
249
- }
250
319
  async function resolveProjectShowInFixtureMode(context, workspace, explicitProject) {
251
320
  return inspectProjectBinding({
252
321
  context,
@@ -2,6 +2,7 @@ import { padDisplay } from "../../shell/ui.js";
2
2
  //#region src/lib/app/deploy-output.ts
3
3
  const DEPLOY_OUTPUT_MIN_LABEL_WIDTH = 9;
4
4
  const DEPLOY_OUTPUT_MIN_VALUE_WIDTH = 9;
5
+ const DEPLOY_SETTINGS_MIN_KEY_WIDTH = 10;
5
6
  function renderDeployOutputRows(ui, rows) {
6
7
  if (rows.length === 0) return [];
7
8
  const labelWidth = Math.max(DEPLOY_OUTPUT_MIN_LABEL_WIDTH, ...rows.map((row) => row.label.length));
@@ -11,5 +12,13 @@ function renderDeployOutputRows(ui, rows) {
11
12
  return ` ${padDisplay(row.label, labelWidth)} ${padDisplay(ui.strong(row.value), valueWidth)}${row.origin ? ` ${ui.dim(`· ${row.origin}`)}` : ""}`.trimEnd();
12
13
  });
13
14
  }
15
+ function renderDeploySettingsPreview(ui, rows) {
16
+ if (rows.length === 0) return [];
17
+ const keyWidth = Math.max(DEPLOY_SETTINGS_MIN_KEY_WIDTH, ...rows.map((row) => `${row.key}:`.length));
18
+ const rail = ui.dim("│");
19
+ return rows.map((row) => {
20
+ return `${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${ui.strong(row.value)}`;
21
+ });
22
+ }
14
23
  //#endregion
15
- export { renderDeployOutputRows };
24
+ export { renderDeployOutputRows, renderDeploySettingsPreview };
@@ -0,0 +1,56 @@
1
+ import { usageError } from "../../shell/errors.js";
2
+ import { selectPrompt, textPrompt } from "../../shell/prompt.js";
3
+ import { inferTargetName, sortProjects } from "./resolution.js";
4
+ import { toProjectSummary, validateProjectSetupNameText } from "./setup.js";
5
+ //#region src/lib/project/interactive-setup.ts
6
+ async function promptForProjectSetupChoice(options) {
7
+ const sortedProjects = sortProjects(options.projects);
8
+ const projectNames = sortedProjects.map((project) => project.name);
9
+ const duplicateNames = new Set(projectNames.filter((name, index) => projectNames.indexOf(name) !== index));
10
+ const choice = await selectPrompt({
11
+ input: options.context.runtime.stdin,
12
+ output: options.context.runtime.stderr,
13
+ message: "Which Project should this directory use?",
14
+ choices: [
15
+ ...sortedProjects.map((project) => ({
16
+ label: duplicateNames.has(project.name) ? `${project.name} (${project.id})` : project.name,
17
+ value: {
18
+ kind: "project",
19
+ project
20
+ }
21
+ })),
22
+ {
23
+ label: "Create a new Project",
24
+ value: { kind: "create" }
25
+ },
26
+ {
27
+ label: "Cancel",
28
+ value: { kind: "cancel" }
29
+ }
30
+ ]
31
+ });
32
+ if (choice.kind === "cancel") throw usageError("Project setup canceled", options.cancel.why, options.cancel.fix, options.cancel.nextSteps, "project");
33
+ if (choice.kind === "project") return {
34
+ project: toProjectSummary(choice.project),
35
+ action: "linked",
36
+ targetName: choice.project.name,
37
+ targetNameSource: "prompt"
38
+ };
39
+ const suggestedName = await inferTargetName(options.context.runtime.cwd);
40
+ const rawName = await textPrompt({
41
+ input: options.context.runtime.stdin,
42
+ output: options.context.runtime.stderr,
43
+ message: "Project name",
44
+ placeholder: suggestedName.name,
45
+ validate: (value) => validateProjectSetupNameText(value, suggestedName.name)
46
+ });
47
+ const projectName = rawName.trim() || suggestedName.name;
48
+ return {
49
+ project: toProjectSummary(await options.createProject(projectName)),
50
+ action: "created",
51
+ targetName: projectName,
52
+ targetNameSource: rawName.trim() ? "prompt" : suggestedName.source
53
+ };
54
+ }
55
+ //#endregion
56
+ export { promptForProjectSetupChoice };
@@ -105,7 +105,7 @@ function buildProjectSetupNextActions(options = {}) {
105
105
  const actions = [{
106
106
  kind: "user-choice",
107
107
  journey: "project-setup",
108
- label: "Ask the user which Prisma Project this directory should use",
108
+ label: "Ask the user whether to link an existing Project or create a new one",
109
109
  commands: ["prisma-cli project list", ...recoveryCommands],
110
110
  reason: options.reason ?? "This directory is not linked to a Prisma Project. Package and directory names are suggestions only, not a safe Project selection."
111
111
  }, {
@@ -6,6 +6,10 @@ import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
6
6
  function isValidProjectSetupName(projectName) {
7
7
  return projectName.trim().length > 0;
8
8
  }
9
+ function validateProjectSetupNameText(value, fallback) {
10
+ if ((value?.trim() || fallback).trim().length > 0) return;
11
+ return "Enter a Project name.";
12
+ }
9
13
  function resolveProjectForSetup(projectRef, projects, workspace) {
10
14
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
11
15
  if (matches.length === 1) return matches[0];
@@ -81,4 +85,4 @@ function formatDebugDetails(error) {
81
85
  return typeof error === "string" ? error : null;
82
86
  }
83
87
  //#endregion
84
- export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary };
88
+ export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary, validateProjectSetupNameText };
@@ -18,7 +18,7 @@ function renderProjectList(context, descriptor, result) {
18
18
  })),
19
19
  emptyMessage: "No projects found."
20
20
  }, context.ui);
21
- if (result.localBinding?.status === "not-linked" || result.localBinding?.status === "invalid") lines.push(...renderNextSteps(["Link the chosen Project: prisma-cli project link <id-or-name>"]));
21
+ if (result.localBinding?.status === "not-linked" || result.localBinding?.status === "invalid") lines.push(...renderNextSteps(["Link an existing Project you choose: prisma-cli project link <id-or-name>", "Create a new Project: prisma-cli project create <name>"]));
22
22
  return lines;
23
23
  }
24
24
  function serializeProjectList(result) {
@@ -49,7 +49,7 @@ function renderProjectShow(context, descriptor, result) {
49
49
  tone: "warning"
50
50
  }]
51
51
  }, context.ui);
52
- lines.push(...renderNextSteps(["Link an existing Project: prisma-cli project link <id-or-name>", `Create a new Project: prisma-cli project create ${formatCommandArgument(result.suggestedProjectName)}`]));
52
+ lines.push(...renderNextSteps(["Link an existing Project you choose: prisma-cli project link <id-or-name>", `Create a new Project: prisma-cli project create ${formatCommandArgument(result.suggestedProjectName)}`]));
53
53
  return lines;
54
54
  }
55
55
  return renderShow({
@@ -115,8 +115,12 @@ const DESCRIPTORS = [
115
115
  "project",
116
116
  "link"
117
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"]
118
+ description: "Link this directory to a Project",
119
+ examples: [
120
+ "prisma-cli project link",
121
+ "prisma-cli project link proj_123",
122
+ "prisma-cli project link \"Acme Dashboard\" --json"
123
+ ]
120
124
  },
121
125
  {
122
126
  id: "git.connect",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.42.1",
3
+ "version": "3.0.0-dev.46.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {