@prisma/cli 3.0.0-dev.41.1 → 3.0.0-dev.43.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
  });
@@ -11,9 +11,11 @@ import { parseEnvAssignments } from "../lib/app/env-vars.js";
11
11
  import { renderDeployOutputRows } 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
+ import { formatCommandArgument } from "../shell/command-arguments.js";
14
15
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "../lib/project/local-pin.js";
15
- import { inferTargetName, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
16
- import { bindProjectToDirectory, formatCommandArgument, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
16
+ import { buildProjectSetupNextActions, inferTargetName, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
17
+ import { bindProjectToDirectory, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
18
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
17
19
  import { PREVIEW_BUILD_TYPES, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild } from "../lib/app/preview-build.js";
18
20
  import { PREVIEW_DEFAULT_REGION } from "../lib/app/preview-interaction.js";
19
21
  import { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress } from "../lib/app/preview-progress.js";
@@ -1361,58 +1363,25 @@ async function resolveDeployProjectContext(context, client, provider, explicitPr
1361
1363
  throw projectSetupRequiredError(projects, await inferTargetName(context.runtime.cwd));
1362
1364
  }
1363
1365
  async function resolveInteractiveDeployProjectSetup(context, provider, workspace, projects) {
1364
- const sortedProjects = sortProjects(projects);
1365
- const choice = await selectPrompt({
1366
- input: context.runtime.stdin,
1367
- output: context.runtime.stderr,
1368
- message: "Which Project should this directory use?",
1369
- choices: [
1370
- ...sortedProjects.map((project) => ({
1371
- label: project.name,
1372
- value: {
1373
- kind: "project",
1374
- project
1375
- }
1376
- })),
1377
- {
1378
- label: "Create a new Project",
1379
- value: { kind: "create" }
1380
- },
1381
- {
1382
- label: "Cancel",
1383
- value: { kind: "cancel" }
1384
- }
1385
- ]
1386
- });
1387
- 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");
1388
- if (choice.kind === "project") return {
1389
- workspace,
1390
- project: toProjectSummary(choice.project),
1391
- resolution: {
1392
- projectSource: "prompt",
1393
- targetName: choice.project.name,
1394
- targetNameSource: "prompt"
1395
- },
1396
- localPinAction: "linked"
1397
- };
1398
- const suggestedName = await inferTargetName(context.runtime.cwd);
1399
- const rawName = await textPrompt({
1400
- input: context.runtime.stdin,
1401
- output: context.runtime.stderr,
1402
- message: "Project name",
1403
- placeholder: suggestedName.name,
1404
- 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
+ }
1405
1375
  });
1406
- const projectName = rawName.trim() || suggestedName.name;
1407
1376
  return {
1408
1377
  workspace,
1409
- project: toProjectSummary(await createProjectForDeploySetup(provider, projectName, workspace)),
1378
+ project: setup.project,
1410
1379
  resolution: {
1411
- projectSource: "created",
1412
- targetName: projectName,
1413
- targetNameSource: rawName.trim() ? "prompt" : suggestedName.source
1380
+ projectSource: setup.action === "created" ? "created" : "prompt",
1381
+ targetName: setup.targetName,
1382
+ targetNameSource: setup.targetNameSource
1414
1383
  },
1415
- localPinAction: "created"
1384
+ localPinAction: setup.action
1416
1385
  };
1417
1386
  }
1418
1387
  async function createProjectForDeploySetup(provider, projectName, workspace) {
@@ -1458,10 +1427,6 @@ function assertExclusiveDeployProjectInputs(options) {
1458
1427
  `unset ${PRISMA_PROJECT_ID_ENV_VAR}`
1459
1428
  ], "project");
1460
1429
  }
1461
- function validateProjectSetupNameText(value, fallback) {
1462
- if ((value?.trim() || fallback).trim().length > 0) return;
1463
- return "Enter a Project name.";
1464
- }
1465
1430
  async function resolveDeployBranch(context, explicitBranchName) {
1466
1431
  if (explicitBranchName) return {
1467
1432
  name: explicitBranchName,
@@ -1796,24 +1761,41 @@ function deployFailedError(summary, error, nextSteps) {
1796
1761
  function appDeployFailedError(error, progress) {
1797
1762
  const why = error instanceof Error ? error.message : String(error);
1798
1763
  const debug = formatDebugDetails(error);
1799
- if (progress.buildStarted && !progress.buildCompleted) return new CliError({
1800
- code: "BUILD_FAILED",
1801
- domain: "app",
1802
- summary: "Build failed locally.",
1803
- why,
1804
- fix: "Inspect the build output above, fix the error, and redeploy.",
1805
- debug,
1806
- meta: { phase: "build" },
1807
- humanLines: [
1808
- "Build failed locally.",
1809
- "",
1810
- `✗ Built ${why}`,
1811
- "",
1812
- "Fix: Inspect the build output above, fix the error, and redeploy."
1813
- ],
1814
- exitCode: 1,
1815
- nextSteps: []
1816
- });
1764
+ if (progress.buildStarted && !progress.buildCompleted) {
1765
+ const standaloneOutputFailure = isNextStandaloneOutputFailure(why);
1766
+ const fix = standaloneOutputFailure ? "Add output: \"standalone\" to next.config.*, then rerun deploy." : "Inspect the build output above, fix the error, and redeploy.";
1767
+ const nextSteps = standaloneOutputFailure ? ["Add output: \"standalone\" to next.config.*, then rerun prisma-cli app deploy"] : [];
1768
+ const nextActions = standaloneOutputFailure ? [{
1769
+ kind: "edit-file",
1770
+ journey: "deploy-app",
1771
+ label: "Add Next.js standalone output",
1772
+ reason: "Prisma Compute needs Next.js standalone output to build a deployable server artifact."
1773
+ }, {
1774
+ kind: "run-command",
1775
+ journey: "deploy-app",
1776
+ label: "Rerun deploy",
1777
+ command: "prisma-cli app deploy"
1778
+ }] : [];
1779
+ return new CliError({
1780
+ code: "BUILD_FAILED",
1781
+ domain: "app",
1782
+ summary: "Build failed locally.",
1783
+ why,
1784
+ fix,
1785
+ debug,
1786
+ meta: { phase: "build" },
1787
+ humanLines: [
1788
+ "Build failed locally.",
1789
+ "",
1790
+ `✗ Built ${why}`,
1791
+ "",
1792
+ `Fix: ${fix}`
1793
+ ],
1794
+ exitCode: 1,
1795
+ nextSteps,
1796
+ nextActions
1797
+ });
1798
+ }
1817
1799
  if (!progress.buildStarted) return deployFailedError("App deploy failed", error, ["prisma-cli app deploy"]);
1818
1800
  const phaseHeadline = progress.containerLive ? "The deployment started, but the app is not ready yet." : "Deploy failed after the build completed.";
1819
1801
  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."];
@@ -1896,9 +1878,17 @@ function projectSetupRequiredError(projects, suggestedName) {
1896
1878
  "prisma-cli project list",
1897
1879
  "prisma-cli app deploy --project <id-or-name>",
1898
1880
  createCommand
1899
- ]
1881
+ ],
1882
+ nextActions: buildProjectSetupNextActions({
1883
+ commandName: "app deploy",
1884
+ createCommand,
1885
+ reason: "This directory is not linked to a Prisma Project. Ask the user which Project to use before deploying; package and directory names are setup suggestions only."
1886
+ })
1900
1887
  });
1901
1888
  }
1889
+ function isNextStandaloneOutputFailure(message) {
1890
+ return /next\.?js/i.test(message) && /standalone output/i.test(message);
1891
+ }
1902
1892
  function noDeploymentsError(summary, why) {
1903
1893
  return new CliError({
1904
1894
  code: "NO_DEPLOYMENTS",
@@ -2,8 +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 { inspectProjectBinding, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
5
+ import { formatCommandArgument } from "../shell/command-arguments.js";
6
+ import { readLocalResolutionPin } from "../lib/project/local-pin.js";
7
+ import { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, resolveProjectTarget, sortProjects } from "../lib/project/resolution.js";
6
8
  import { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary } from "../lib/project/setup.js";
9
+ import { promptForProjectSetupChoice } from "../lib/project/interactive-setup.js";
7
10
  import { createPreviewAppProvider } from "../lib/app/preview-provider.js";
8
11
  import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
9
12
  import { requireAuthenticatedAuthState } from "./auth.js";
@@ -16,6 +19,12 @@ const GITHUB_INSTALL_POLL_TIMEOUT_MS = 12e4;
16
19
  function isRealMode(context) {
17
20
  return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
18
21
  }
22
+ async function readProjectListLocalBinding(cwd, workspace, projects) {
23
+ const pin = await readLocalResolutionPin(cwd);
24
+ if (pin.kind === "present") return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId) ? { status: "linked" } : { status: "invalid" };
25
+ if (pin.kind === "invalid") return { status: "invalid" };
26
+ return { status: "not-linked" };
27
+ }
19
28
  async function runProjectList(context) {
20
29
  const authState = await requireAuthenticatedAuthState(context);
21
30
  const workspace = authState.workspace;
@@ -23,31 +32,55 @@ async function runProjectList(context) {
23
32
  if (isRealMode(context)) {
24
33
  const client = await requireComputeAuth(context.runtime.env);
25
34
  if (!client) throw authRequiredError();
35
+ const projects = sortProjects(await listRealWorkspaceProjects(client, workspace));
36
+ const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, projects);
37
+ const nextActions = buildProjectListNextActions(localBinding);
26
38
  return {
27
39
  command: "project.list",
28
40
  result: {
29
41
  workspace,
30
- projects: sortProjects(await listRealWorkspaceProjects(client, workspace)).map(toProjectSummary)
42
+ projects: projects.map(toProjectSummary),
43
+ localBinding
31
44
  },
32
45
  warnings: [],
33
- nextSteps: []
46
+ nextSteps: [],
47
+ nextActions
34
48
  };
35
49
  }
50
+ const result = await createProjectUseCases(createCliUseCaseGateways(context)).list(authState);
51
+ const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, result.projects);
52
+ const nextActions = buildProjectListNextActions(localBinding);
36
53
  return {
37
54
  command: "project.list",
38
- result: await createProjectUseCases(createCliUseCaseGateways(context)).list(authState),
55
+ result: {
56
+ ...result,
57
+ localBinding
58
+ },
39
59
  warnings: [],
40
- nextSteps: []
60
+ nextSteps: [],
61
+ nextActions
41
62
  };
42
63
  }
64
+ function buildProjectListNextActions(localBinding) {
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
+ });
69
+ }
43
70
  async function runProjectShow(context, explicitProject) {
44
71
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
45
72
  if (!workspace) throw workspaceRequiredError();
73
+ const result = isRealMode(context) ? await resolveProjectShowInRealMode(context, workspace, explicitProject) : await resolveProjectShowInFixtureMode(context, workspace, explicitProject);
46
74
  return {
47
75
  command: "project.show",
48
- result: isRealMode(context) ? await resolveProjectShowInRealMode(context, workspace, explicitProject) : await resolveProjectShowInFixtureMode(context, workspace, explicitProject),
76
+ result,
49
77
  warnings: [],
50
- nextSteps: []
78
+ nextSteps: [],
79
+ nextActions: result.project === null ? buildProjectSetupNextActions({
80
+ commandName: "project show",
81
+ suggestedProjectName: result.suggestedProjectName,
82
+ reason: "This directory is not linked to a Prisma Project. Package and directory names can suggest setup defaults, but they do not select a Project."
83
+ }) : []
51
84
  };
52
85
  }
53
86
  async function runProjectCreate(context, projectName) {
@@ -79,15 +112,84 @@ async function runProjectCreate(context, projectName) {
79
112
  async function runProjectLink(context, projectRef) {
80
113
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
81
114
  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);
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);
84
127
  return {
85
128
  command: "project.link",
86
- result: await bindProjectToDirectory(context, workspace, toProjectSummary(resolveProjectForSetup(projectRef.trim(), projects, workspace)), "linked"),
129
+ result,
87
130
  warnings: [],
88
131
  nextSteps: ["prisma-cli app deploy"]
89
132
  };
90
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
+ }
91
193
  async function runGitConnect(context, gitUrl, options = {}) {
92
194
  const workspace = (await requireAuthenticatedAuthState(context)).workspace;
93
195
  if (!workspace) throw workspaceRequiredError();
@@ -214,11 +316,6 @@ async function resolveRequiredProjectInRealMode(context, workspace, explicitProj
214
316
  commandName
215
317
  });
216
318
  }
217
- async function listRealProjectsForLink(context, workspace) {
218
- const client = await requireComputeAuth(context.runtime.env);
219
- if (!client) throw authRequiredError();
220
- return listRealWorkspaceProjects(client, workspace);
221
- }
222
319
  async function resolveProjectShowInFixtureMode(context, workspace, explicitProject) {
223
320
  return inspectProjectBinding({
224
321
  context,
@@ -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 };
@@ -1,4 +1,5 @@
1
1
  import { CliError } from "../../shell/errors.js";
2
+ import { formatCommandArgument } from "../../shell/command-arguments.js";
2
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
3
4
  import { readFile } from "node:fs/promises";
4
5
  import path from "node:path";
@@ -20,6 +21,7 @@ async function inspectProjectBinding(options) {
20
21
  return {
21
22
  workspace: options.workspace,
22
23
  project: null,
24
+ localBinding: { status: "not-linked" },
23
25
  resolution: { projectSource: "unbound" },
24
26
  ...await buildProjectSetupSuggestion({
25
27
  cwd: options.context.runtime.cwd,
@@ -89,8 +91,45 @@ async function projectSetupRequiredError(options) {
89
91
  fix: "Link the directory to an existing Project, or pass --project <id-or-name> for this command.",
90
92
  meta: { ...suggestion },
91
93
  exitCode: 1,
92
- nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands]
94
+ nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands],
95
+ nextActions: buildProjectSetupNextActions({
96
+ commandName: options.commandName,
97
+ suggestedProjectName: suggestion.suggestedProjectName
98
+ })
99
+ });
100
+ }
101
+ function buildProjectSetupNextActions(options = {}) {
102
+ const recoveryCommands = buildProjectRecoveryCommands(options.commandName);
103
+ const linkCommand = recoveryCommands[0] ?? "prisma-cli project link <id-or-name>";
104
+ const retryCommand = recoveryCommands[1];
105
+ const actions = [{
106
+ kind: "user-choice",
107
+ journey: "project-setup",
108
+ label: "Ask the user whether to link an existing Project or create a new one",
109
+ commands: ["prisma-cli project list", ...recoveryCommands],
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
+ }, {
112
+ kind: "run-command",
113
+ journey: "project-setup",
114
+ label: "Link the chosen Project",
115
+ command: linkCommand,
116
+ reason: "Linking writes the durable local Project binding for this directory."
117
+ }];
118
+ const createCommand = options.createCommand ?? (options.suggestedProjectName ? `prisma-cli project create ${formatCommandArgument(options.suggestedProjectName)}` : void 0);
119
+ if (createCommand) actions.push({
120
+ kind: "run-command",
121
+ journey: "project-setup",
122
+ label: "Create and link a new Project",
123
+ command: createCommand,
124
+ reason: "Use this when the user wants a new Prisma Project instead of an existing one."
125
+ });
126
+ if (options.commandName) actions.push({
127
+ kind: "run-command",
128
+ journey: "recover",
129
+ label: "Retry with an explicit Project",
130
+ command: retryCommand ?? `prisma-cli ${options.commandName} --project <id-or-name>`
93
131
  });
132
+ return actions;
94
133
  }
95
134
  async function readPackageName(cwd) {
96
135
  try {
@@ -187,4 +226,4 @@ function toProjectSummary(project) {
187
226
  };
188
227
  }
189
228
  //#endregion
190
- export { inferTargetName, inspectProjectBinding, projectAmbiguousError, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
229
+ export { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectAmbiguousError, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
@@ -1,10 +1,15 @@
1
1
  import { CliError, usageError } from "../../shell/errors.js";
2
+ import "../../shell/command-arguments.js";
2
3
  import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
3
4
  import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
4
5
  //#region src/lib/project/setup.ts
5
6
  function isValidProjectSetupName(projectName) {
6
7
  return projectName.trim().length > 0;
7
8
  }
9
+ function validateProjectSetupNameText(value, fallback) {
10
+ if ((value?.trim() || fallback).trim().length > 0) return;
11
+ return "Enter a Project name.";
12
+ }
8
13
  function resolveProjectForSetup(projectRef, projects, workspace) {
9
14
  const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
10
15
  if (matches.length === 1) return matches[0];
@@ -60,9 +65,6 @@ function projectCreateFailedError(error, projectName, workspace, options) {
60
65
  nextSteps: options.nextSteps
61
66
  });
62
67
  }
63
- function formatCommandArgument(value) {
64
- return /^[A-Za-z0-9._/-]+$/.test(value) ? value : JSON.stringify(value);
65
- }
66
68
  function formatSetupDirectory(cwd) {
67
69
  const basename = cwd.split(/[\\/]/).filter(Boolean).pop();
68
70
  return basename ? `./${basename}` : ".";
@@ -83,4 +85,4 @@ function formatDebugDetails(error) {
83
85
  return typeof error === "string" ? error : null;
84
86
  }
85
87
  //#endregion
86
- export { bindProjectToDirectory, formatCommandArgument, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary };
88
+ export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary, validateProjectSetupNameText };
@@ -1,8 +1,9 @@
1
- import { renderSummaryLine } from "../shell/ui.js";
1
+ import { renderNextSteps, renderSummaryLine } from "../shell/ui.js";
2
+ import { formatCommandArgument } from "../shell/command-arguments.js";
2
3
  import { renderList, renderMutate, renderShow, serializeList } from "../output/patterns.js";
3
4
  //#region src/presenters/project.ts
4
5
  function renderProjectList(context, descriptor, result) {
5
- return renderList({
6
+ const lines = renderList({
6
7
  title: "Listing projects for the authenticated workspace.",
7
8
  descriptor,
8
9
  parentContext: {
@@ -17,46 +18,40 @@ function renderProjectList(context, descriptor, result) {
17
18
  })),
18
19
  emptyMessage: "No projects found."
19
20
  }, context.ui);
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
+ return lines;
20
23
  }
21
24
  function serializeProjectList(result) {
22
- return serializeList({
23
- context: { workspace: result.workspace.name },
24
- items: result.projects.map((project) => ({
25
- noun: "project",
26
- label: project.name,
27
- id: project.id,
28
- status: null
29
- }))
30
- });
25
+ return {
26
+ ...serializeList({
27
+ context: { workspace: result.workspace.name },
28
+ items: result.projects.map((project) => ({
29
+ noun: "project",
30
+ label: project.name,
31
+ id: project.id,
32
+ status: null
33
+ }))
34
+ }),
35
+ localBinding: result.localBinding ?? null
36
+ };
31
37
  }
32
38
  function renderProjectShow(context, descriptor, result) {
33
- if (result.project === null) return renderShow({
34
- title: "No Project linked to this directory.",
35
- descriptor,
36
- fields: [
37
- {
39
+ if (result.project === null) {
40
+ const lines = renderShow({
41
+ title: "This directory is not linked to a Prisma Project.",
42
+ descriptor,
43
+ fields: [{
38
44
  key: "workspace",
39
45
  value: result.workspace.name
40
- },
41
- {
46
+ }, {
42
47
  key: "project",
43
- value: "unbound",
48
+ value: "Not linked",
44
49
  tone: "warning"
45
- },
46
- {
47
- key: "suggested",
48
- value: `${result.suggestedProjectName} (${formatSuggestionSource(result.suggestedProjectNameSource)})`
49
- },
50
- {
51
- key: "match",
52
- value: formatCandidateList(result.candidates)
53
- },
54
- {
55
- key: "next",
56
- value: result.recoveryCommands[0] ?? "prisma-cli project link <id-or-name>"
57
- }
58
- ]
59
- }, context.ui);
50
+ }]
51
+ }, context.ui);
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
+ return lines;
54
+ }
60
55
  return renderShow({
61
56
  title: "Showing this directory's Project binding.",
62
57
  descriptor,
@@ -149,16 +144,6 @@ function formatProjectSource(source) {
149
144
  case "unbound": return "unbound";
150
145
  }
151
146
  }
152
- function formatSuggestionSource(source) {
153
- switch (source) {
154
- case "package-name": return "package name";
155
- case "directory-name": return "directory name";
156
- }
157
- }
158
- function formatCandidateList(candidates) {
159
- if (candidates.length === 0) return "none";
160
- return candidates.map((project) => project.name).join(", ");
161
- }
162
147
  function formatGitConnectionDetail(status) {
163
148
  switch (status) {
164
149
  case "active": return "GitHub branch automation is active for this project.";
@@ -0,0 +1,6 @@
1
+ //#region src/shell/command-arguments.ts
2
+ function formatCommandArgument(value) {
3
+ return /^[A-Za-z0-9._/-]+$/.test(value) && !value.startsWith("-") ? value : `'${value.replace(/'/g, "'\\''")}'`;
4
+ }
5
+ //#endregion
6
+ export { formatCommandArgument };
@@ -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",
@@ -47,7 +47,8 @@ async function runStreamingCommand(runtime, commandName, options, handler) {
47
47
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
48
48
  result: null,
49
49
  warnings: [],
50
- nextSteps: []
50
+ nextSteps: [],
51
+ nextActions: []
51
52
  });
52
53
  } catch (error) {
53
54
  const cliError = toCliError(error);
@@ -58,7 +59,8 @@ async function runStreamingCommand(runtime, commandName, options, handler) {
58
59
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
59
60
  error: cliErrorToJson(cliError),
60
61
  warnings: [],
61
- nextSteps: cliError.nextSteps
62
+ nextSteps: cliError.nextSteps,
63
+ nextActions: cliError.nextActions
62
64
  });
63
65
  else writeHumanError(context.output, context.ui, cliError, { trace: flags.trace });
64
66
  process.exitCode = cliError.exitCode;
@@ -12,6 +12,7 @@ var CliError = class extends Error {
12
12
  docsUrl;
13
13
  exitCode;
14
14
  nextSteps;
15
+ nextActions;
15
16
  humanLines;
16
17
  constructor(options) {
17
18
  super(options.summary);
@@ -28,6 +29,7 @@ var CliError = class extends Error {
28
29
  this.docsUrl = options.docsUrl ?? null;
29
30
  this.exitCode = options.exitCode ?? 1;
30
31
  this.nextSteps = options.nextSteps ?? [];
32
+ this.nextActions = options.nextActions ?? [];
31
33
  this.humanLines = options.humanLines && options.humanLines.length > 0 ? [...options.humanLines] : null;
32
34
  }
33
35
  };
@@ -3,6 +3,7 @@ import { renderNextSteps, renderSummaryLine } from "./ui.js";
3
3
  function writeJsonSuccess(output, success) {
4
4
  output.stdout.write(`${JSON.stringify({
5
5
  ok: true,
6
+ nextActions: [],
6
7
  ...success
7
8
  }, null, 2)}\n`);
8
9
  }
@@ -28,7 +29,8 @@ function writeJsonError(output, command, error) {
28
29
  command,
29
30
  error: cliErrorToJson(error),
30
31
  warnings: [],
31
- nextSteps: error.nextSteps
32
+ nextSteps: error.nextSteps,
33
+ nextActions: error.nextActions
32
34
  }, null, 2)}\n`);
33
35
  }
34
36
  function writeHumanLines(output, lines) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prisma/cli",
3
- "version": "3.0.0-dev.41.1",
3
+ "version": "3.0.0-dev.43.1",
4
4
  "description": "Command-line interface for the Prisma Developer Platform.",
5
5
  "type": "module",
6
6
  "bin": {