Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/product/cli-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ project show → This directory is not linked to a Prisma Project.
│ project: Not linked

Next steps:
- Link an existing Project: prisma-cli project link <id-or-name>
- Link an existing Project you choose: prisma-cli project link <id-or-name>
- Create a new Project: prisma-cli project create billing-api
```

Expand Down
17 changes: 12 additions & 5 deletions docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,10 @@ Behavior:
- lists projects visible to the active workspace
- does not resolve the current directory
- does not mutate local state
- when the current directory is not linked, human output adds one setup hint after the list
- when the current directory is not linked, human output adds setup hints after the list
- in JSON, unlinked directories include a `user-choice` `nextActions` entry for Project setup
- listed Projects are not marked selected unless durable local binding actually selects one
- listed Projects are candidates only; the user must choose one before `project link <id-or-name>` runs

Examples:

Expand Down Expand Up @@ -425,24 +426,30 @@ prisma-cli project create my-app
prisma-cli project create my-app --json
```

## `prisma-cli project link <id-or-name>`
## `prisma-cli project link [id-or-name]`

Purpose:

- bind the current directory to an existing Prisma Project
- bind the current directory to a Prisma Project

Behavior:

- requires auth
- resolves exactly one Project by id or name in the authenticated workspace
- with `[id-or-name]`, resolves exactly one existing Project by id or name in the authenticated workspace
- without `[id-or-name]` in interactive mode, prompts the user to choose an existing Project, create a new Project, or cancel
- choosing an existing Project writes the local binding and does not create remote resources
- choosing "Create a new Project" prompts for a Project name, creates the Project, and writes the local binding
- without `[id-or-name]` in `--json`, `--no-interactive`, non-TTY, CI, or `--yes` mode, fails with `PROJECT_LINK_TARGET_REQUIRED`
- `--yes` does not choose Project scope
- writes `.prisma/local.json` with Workspace and Project IDs
- ensures `.prisma/` is ignored by Git
- does not create remote resources
- does not create Branch, App, Deployment, database, or Git repository connection state
- fails with `PROJECT_NOT_FOUND` or `PROJECT_AMBIGUOUS` when the Project cannot be selected safely

Examples:

```bash
prisma-cli project link
prisma-cli project link proj_123
prisma-cli project link "Acme Dashboard" --json
```
Expand Down
2 changes: 2 additions & 0 deletions docs/product/error-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ These codes are the minimum stable set for the MVP:
- `USAGE_ERROR`
- `AUTH_REQUIRED`
- `PROJECT_SETUP_REQUIRED`
- `PROJECT_LINK_TARGET_REQUIRED`
- `PROJECT_CREATE_FAILED`
- `PROJECT_NOT_FOUND`
- `PROJECT_AMBIGUOUS`
Expand Down Expand Up @@ -200,6 +201,7 @@ Recommended meanings:
- `USAGE_ERROR`: invalid arguments or invalid command combination
- `AUTH_REQUIRED`: command needs an authenticated session
- `PROJECT_SETUP_REQUIRED`: command needs explicit or durable Project context before it can continue
- `PROJECT_LINK_TARGET_REQUIRED`: `project link` needs the user to choose an existing Project or create a new one
- `PROJECT_CREATE_FAILED`: Project creation failed before deployment or linking could continue
- `PROJECT_NOT_FOUND`: requested project does not exist or is not accessible
- `PROJECT_AMBIGUOUS`: multiple safe project candidates matched
Expand Down
2 changes: 1 addition & 1 deletion docs/product/output-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ project show → This directory is not linked to a Prisma Project.
│ project: Not linked

Next steps:
- Link an existing Project: prisma-cli project link <id-or-name>
- Link an existing Project you choose: prisma-cli project link <id-or-name>
- Create a new Project: prisma-cli project create billing-api
```

Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/project/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,15 @@ function createProjectCreateCommand(runtime: CliRuntime): Command {
function createProjectLinkCommand(runtime: CliRuntime): Command {
const command = attachCommandDescriptor(configureRuntimeCommand(new Command("link"), runtime), "project.link");

command.argument("<id-or-name>", "Project id or name");
command.argument("[id-or-name]", "Project id or name");
addGlobalFlags(command);

command.action(async (projectRef, options) => {
await runCommand<ProjectSetupResult>(
runtime,
"project.link",
options as Record<string, unknown>,
(context) => runProjectLink(context, String(projectRef)),
(context) => runProjectLink(context, typeof projectRef === "string" ? projectRef : undefined),
{
renderHuman: (context, descriptor, result) => renderProjectSetup(context, descriptor, result),
renderJson: (result) => serializeProjectSetup(result),
Expand Down
80 changes: 15 additions & 65 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
type ProjectCandidate,
sortProjects,
} from "../lib/project/resolution";
import { promptForProjectSetupChoice } from "../lib/project/interactive-setup";
import {
bindProjectToDirectory,
formatCommandArgument,
Expand Down Expand Up @@ -2369,75 +2370,32 @@ async function resolveDeployProjectContext(
throw projectSetupRequiredError(projects, suggestedName);
}

type DeployProjectSetupChoice =
| { kind: "project"; project: ProjectCandidate }
| { kind: "create" }
| { kind: "cancel" };

async function resolveInteractiveDeployProjectSetup(
context: CommandContext,
provider: ReturnType<typeof createPreviewAppProvider>,
workspace: AuthWorkspace,
projects: ProjectCandidate[],
): Promise<Omit<ResolvedAppProjectContext, "branch">> {
const sortedProjects = sortProjects(projects);
const choice = await selectPrompt<DeployProjectSetupChoice>({
input: context.runtime.stdin,
output: context.runtime.stderr,
message: "Which Project should this directory use?",
choices: [
...sortedProjects.map((project) => ({
label: project.name,
value: { kind: "project" as const, project },
})),
{ label: "Create a new Project", value: { kind: "create" as const } },
{ label: "Cancel", value: { kind: "cancel" as const } },
],
});

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",
);
}

if (choice.kind === "project") {
return {
workspace,
project: toProjectSummary(choice.project),
resolution: {
projectSource: "prompt",
targetName: choice.project.name,
targetNameSource: "prompt",
},
localPinAction: "linked",
};
}

const suggestedName = await inferTargetName(context.runtime.cwd);
const rawName = await textPrompt({
input: context.runtime.stdin,
output: context.runtime.stderr,
message: "Project name",
placeholder: suggestedName.name,
validate: (value) => validateProjectSetupNameText(value, suggestedName.name),
const setup = await promptForProjectSetupChoice({
context,
projects,
createProject: (projectName) => createProjectForDeploySetup(provider, projectName, workspace),
cancel: {
why: "Deploy needs a Project before it can continue.",
fix: "Choose an existing Project or create a new one, then rerun deploy.",
nextSteps: ["prisma-cli app deploy --project <id-or-name>", "prisma-cli app deploy --create-project <name>"],
},
});
const projectName = rawName.trim() || suggestedName.name;
const created = await createProjectForDeploySetup(provider, projectName, workspace);

return {
workspace,
project: toProjectSummary(created),
project: setup.project,
resolution: {
projectSource: "created",
targetName: projectName,
targetNameSource: rawName.trim() ? "prompt" : suggestedName.source,
projectSource: setup.action === "created" ? "created" : "prompt",
targetName: setup.targetName,
targetNameSource: setup.targetNameSource,
},
localPinAction: "created",
localPinAction: setup.action,
};
}

Expand Down Expand Up @@ -2510,14 +2468,6 @@ function assertExclusiveDeployProjectInputs(options: {
);
}

function validateProjectSetupNameText(value: string | undefined, fallback: string): string | undefined {
if ((value?.trim() || fallback).trim().length > 0) {
return undefined;
}

return "Enter a Project name.";
}

interface ResolvedDeployBranch {
name: string;
annotation: string;
Expand Down
Loading
Loading