Skip to content
Open
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
14 changes: 7 additions & 7 deletions docs/product/cli-style-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,15 +64,14 @@ Recommended symbols:
Human-oriented command output in TTY mode should usually start with a compact header:

```text
project show → No Project linked to this directory.
project show → This directory is not linked to a Prisma Project.

│ workspace: Acme Inc
│ project: unbound
│ suggested: billing-api (package name)
│ match: Billing API
│ next: prisma-cli project link <id-or-name>
│ Read more docs/product/command-spec.md#prisma-cli-project-show
│ project: Not linked

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

Rules:
Expand All @@ -84,6 +83,7 @@ Rules:
- prefer display labels in default human output and keep opaque ids in JSON unless a later verbose mode explicitly asks for them
- mask sensitive values rather than omitting their presence entirely when the value matters to the flow
- include only rows that are actually known for the current command
- use human labels such as `Not linked` instead of internal resolution terms such as `unbound`
- include a `Read more` row that points to the source-of-truth repo doc or anchor until a stable public docs URL exists
- leave one blank line between the header block and the body

Expand Down
7 changes: 5 additions & 2 deletions docs/product/command-principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,15 @@ The CLI must work well for both humans and agents.

That means:

- default output is human-readable
- structured output is explicit
- default output is concise, human-readable, and calm
- structured output is explicit enough for agents to make safe choices
- human output and JSON output describe the same truth, but may render it differently
- targeting rules are deterministic
- risky actions are surfaced clearly
- nouns and verbs stay stable across help, docs, and output
- shared UX rules stay centralized rather than reinvented per command group
- local metadata may suggest defaults, but must not be presented as target selection
- missing context ends with clear next actions, not implicit resolution

## No Dead Ends

Expand Down
7 changes: 6 additions & 1 deletion docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ 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
- 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

Examples:

Expand All @@ -387,7 +390,8 @@ Behavior:
- does not mutate local state
- `--project <id-or-name>` resolves only the explicit project
- when bound, returns Workspace, Project, and `resolution.projectSource`
- when unbound, exits successfully with `project: null`, `resolution.projectSource: "unbound"`, a suggested Project name, matching Project candidates, and recovery commands
- when unbound, human output says `project: Not linked` and shows link/create next steps
- when unbound, JSON exits successfully with `project: null`, `localBinding.status: "not-linked"`, `resolution.projectSource: "unbound"`, a suggested Project name, matching Project candidates, recovery commands, and `user-choice` `nextActions`
- package names and directory names only power unbound suggestions
- fails with `PROJECT_NOT_FOUND`, `PROJECT_AMBIGUOUS`, or `LOCAL_STATE_STALE` when explicit or durable binding validation cannot continue safely

Expand Down Expand Up @@ -617,6 +621,7 @@ Behavior:

- when "Create a new Project" is selected, prompts for a Project name with the package/directory name as a suggestion
- when no Project is resolved in `--json` / `--no-interactive` mode, fails with `PROJECT_SETUP_REQUIRED`
- `PROJECT_SETUP_REQUIRED` preserves readable recovery commands in `nextSteps` and includes structured `nextActions` for choosing, linking, creating, or retrying with an explicit Project
- `--yes` alone does not choose Project scope; use `--project` or `--create-project`
- `--project` and `--create-project` are mutually exclusive with each other and with `PRISMA_PROJECT_ID`
- resolves or creates branch context from `--branch`, local Git branch, or `main`
Expand Down
5 changes: 3 additions & 2 deletions docs/product/error-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,8 @@ Commands run with `--json` should emit this envelope on failure:
"docsUrl": null
},
"warnings": [],
"nextSteps": []
"nextSteps": [],
"nextActions": []
}
```

Expand All @@ -150,7 +151,7 @@ Rules:
- `error.where` points to the relevant location when applicable
- `error.meta` is structured, not free-form prose
- `error.docsUrl` may be `null` when no per-code doc exists yet
- `warnings` and `nextSteps` are always present
- `warnings`, `nextSteps`, and `nextActions` are always present
- agents and CI should branch on structured error fields, not prose strings

## MVP Error Codes
Expand Down
48 changes: 39 additions & 9 deletions docs/product/output-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,15 +220,14 @@ Human output should:
Recommended header shape:

```text
project show → No Project linked to this directory.
project show → This directory is not linked to a Prisma Project.

│ workspace: Acme Inc
│ project: unbound
│ suggested: billing-api (package name)
│ match: Billing API
│ next: prisma-cli project link <id-or-name>
│ Read more docs/product/command-spec.md#prisma-cli-project-show
│ project: Not linked

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

Rules:
Expand All @@ -238,6 +237,7 @@ Rules:
- show only metadata that is relevant to the current invocation
- include `Read more` when a stable repo doc reference exists
- prefer display labels in default human output and keep opaque ids in JSON unless a later verbose mode explicitly asks for them
- do not expose agent-only reasoning in human output when a clear status and next step is enough

Recommended summary lines:

Expand Down Expand Up @@ -341,7 +341,8 @@ Recommended envelope:
"command": "app.deploy",
"result": {},
"warnings": [],
"nextSteps": []
"nextSteps": [],
"nextActions": []
}
```

Expand All @@ -352,8 +353,27 @@ Required conventions:
- `result` holds command-specific data
- `warnings` is always present
- `nextSteps` is always present, even if empty
- `nextActions` is always present, even if empty
- human-readable guidance that matters to automation should also be represented in structured fields, not only on stderr

`nextSteps` remains a compatibility surface for readable commands. `nextActions`
is the structured surface for agents and automation:

```ts
type NextAction = {
kind: "run-command" | "user-choice" | "edit-file" | "done"
journey: "project-setup" | "deploy-app" | "inspect" | "recover"
label: string
command?: string
commands?: string[]
reason?: string
}
```

Use `user-choice` when the next move requires the caller or user to choose a
target. Do not encode local package names, directory names, or nearby matches as
a selected target; put them in command-specific suggestion fields instead.

## Streaming JSON Shape

True streaming commands should emit newline-delimited JSON events in `--json` mode.
Expand Down Expand Up @@ -417,6 +437,14 @@ context, status, decoration, and errors stay on stderr.
"nextSteps": [
"prisma-cli app list-deploys --app hello-world",
"prisma-cli app show-deploy dep_045"
],
"nextActions": [
{
"kind": "run-command",
"journey": "inspect",
"label": "View deployment logs",
"command": "prisma-cli app logs"
}
]
}
```
Expand All @@ -427,4 +455,6 @@ Human output and JSON output should describe the same underlying model.

The CLI should never require users or agents to learn different meanings for the same command.

Human output may be richer in layout and interaction, but not richer in meaning.
Human output should stay optimized for people. JSON should carry the same model
with enough explicit structure for agents to recognize stop points, user-choice
moments, and safe recovery actions.
41 changes: 38 additions & 3 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
} from "../lib/app/local-dev";
import { readBunPackageEntrypoint, readBunPackageJson, type BunPackageJsonLike } from "../lib/app/bun-project";
import {
buildProjectSetupNextActions,
inferTargetName,
projectNotFoundError,
resolveDurablePlatformMapping,
Expand Down Expand Up @@ -3152,23 +3153,48 @@ function appDeployFailedError(error: unknown, progress: PreviewDeployProgressSta
const debug = formatDebugDetails(error);

if (progress.buildStarted && !progress.buildCompleted) {
const standaloneOutputFailure = isNextStandaloneOutputFailure(why);
const fix = standaloneOutputFailure
? "Add output: \"standalone\" to next.config.*, then rerun deploy."
: "Inspect the build output above, fix the error, and redeploy.";
const nextSteps = standaloneOutputFailure
? ["Add output: \"standalone\" to next.config.*, then rerun prisma-cli app deploy"]
: [];
const nextActions = standaloneOutputFailure
? [
{
kind: "edit-file" as const,
journey: "deploy-app" as const,
label: "Add Next.js standalone output",
reason: "Prisma Compute needs Next.js standalone output to build a deployable server artifact.",
},
{
kind: "run-command" as const,
journey: "deploy-app" as const,
label: "Rerun deploy",
command: "prisma-cli app deploy",
},
]
: [];

return new CliError({
code: "BUILD_FAILED",
domain: "app",
summary: "Build failed locally.",
why,
fix: "Inspect the build output above, fix the error, and redeploy.",
fix,
debug,
meta: { phase: "build" },
humanLines: [
"Build failed locally.",
"",
`✗ Built ${why}`,
"",
"Fix: Inspect the build output above, fix the error, and redeploy.",
`Fix: ${fix}`,
],
exitCode: 1,
nextSteps: [],
nextSteps,
nextActions,
});
}

Expand Down Expand Up @@ -3278,9 +3304,18 @@ function projectSetupRequiredError(
"prisma-cli app deploy --project <id-or-name>",
createCommand,
],
nextActions: buildProjectSetupNextActions({
commandName: "app deploy",
createCommand,
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.",
}),
});
}

function isNextStandaloneOutputFailure(message: string): boolean {
return /next\.?js/i.test(message) && /standalone output/i.test(message);
}

function noDeploymentsError(summary: string, why: string): CliError {
return new CliError({
code: "NO_DEPLOYMENTS",
Expand Down
51 changes: 49 additions & 2 deletions packages/cli/src/controllers/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import {
} from "../adapters/git";
import { requireComputeAuth } from "../lib/auth/guard";
import {
buildProjectSetupNextActions,
inspectProjectBinding,
resolveProjectTarget,
sortProjects,
type ProjectCandidate,
type ResolvedProjectTarget,
} from "../lib/project/resolution";
import { readLocalResolutionPin } from "../lib/project/local-pin";
import {
bindProjectToDirectory,
isValidProjectSetupName,
Expand Down Expand Up @@ -54,6 +56,23 @@ function isRealMode(context: CommandContext): boolean {
return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
}

async function readProjectListLocalBinding(
cwd: string,
workspace: AuthWorkspace,
projects: Array<Pick<ProjectCandidate, "id">>,
): Promise<ProjectListResult["localBinding"]> {
const pin = await readLocalResolutionPin(cwd);
if (pin.kind === "present") {
return pin.pin.workspaceId === workspace.id && projects.some((project) => project.id === pin.pin.projectId)
? { status: "linked" }
: { status: "invalid" };
}
if (pin.kind === "invalid") {
return { status: "invalid" };
}
return { status: "not-linked" };
}

export async function runProjectList(context: CommandContext): Promise<CommandSuccess<ProjectListResult>> {
const authState = await requireAuthenticatedAuthState(context);
const workspace = authState.workspace;
Expand All @@ -66,29 +85,50 @@ export async function runProjectList(context: CommandContext): Promise<CommandSu
if (!client) {
throw authRequiredError();
}
const projects = sortProjects(await listRealWorkspaceProjects(client, workspace));
const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, projects);
const nextActions = buildProjectListNextActions(localBinding);

return {
command: "project.list",
result: {
workspace,
projects: sortProjects(await listRealWorkspaceProjects(client, workspace)).map(toProjectSummary),
projects: projects.map(toProjectSummary),
localBinding,
},
warnings: [],
nextSteps: [],
nextActions,
};
}

const projectUseCases = createProjectUseCases(createCliUseCaseGateways(context));
const result = await projectUseCases.list(authState);
const localBinding = await readProjectListLocalBinding(context.runtime.cwd, workspace, result.projects);
const nextActions = buildProjectListNextActions(localBinding);

return {
command: "project.list",
result,
result: {
...result,
localBinding,
},
warnings: [],
nextSteps: [],
nextActions,
};
}

function buildProjectListNextActions(localBinding: ProjectListResult["localBinding"]) {
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.",
});
}

export async function runProjectShow(
context: CommandContext,
explicitProject: string | undefined,
Expand All @@ -108,6 +148,13 @@ export async function runProjectShow(
result,
warnings: [],
nextSteps: [],
nextActions: result.project === null
? buildProjectSetupNextActions({
commandName: "project show",
suggestedProjectName: result.suggestedProjectName,
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.",
})
: [],
};
}

Expand Down
Loading
Loading