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
3 changes: 2 additions & 1 deletion docs/product/command-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,8 @@ Behavior:
- resolves or creates app context inside the resolved branch from `--app`, `PRISMA_APP_ID`, `package.json#name`, or current directory name
- does not prompt when there is no real choice; zero matching apps creates the inferred app
- writes `.prisma/local.json` after Project binding succeeds and before build/deploy starts, so retries after a failed deploy do not repeat setup
- asks `Customize settings? (y/N)` only while binding the directory for the first time, and only asks for Framework and HTTP port when the user opts in
- before asking `Customize build settings? (y/N)`, previews the detected framework and runtime so the user can see the defaults they are accepting or changing
- asks `Customize build settings? (y/N)` only while binding the directory for the first time, and only asks for Framework and HTTP port when the user opts in
- after setup, deploy prints `Deploying to <Project> / <Branch> / <App>`; later deploys print a compact target header such as `Deploying ./j1 to j1 / main / j1`
- deploy progress uses short stage copy (`Building locally...`, `Built <size>`, `Uploading...`, `Uploaded`, `Deploying...`, `Deployed`) and never prints `Status: running` or `Deployment is running at ...`
- success human output prints `Live in <duration>`, the URL on its own line, and `Logs prisma-cli app logs`
Expand Down
6 changes: 6 additions & 0 deletions docs/product/output-conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,12 @@ After setup, keep the confirmation compact:
Saved .prisma/local.json

Deploying to Acme Dashboard / feat-login / my-app

Detected Next.js
│ framework: Next.js
│ runtime: HTTP 3000

? Customize build settings? No
```

Deploy progress should describe phases without claiming runtime success before
Expand Down
29 changes: 27 additions & 2 deletions packages/cli/src/controllers/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ import { requireComputeAuth } from "../lib/auth/guard";
import { readAuthState } from "../lib/auth/auth-ops";
import { getApiBaseUrl, SERVICE_TOKEN_ENV_VAR } from "../lib/auth/client";
import { parseEnvAssignments } from "../lib/app/env-vars";
import { renderDeployOutputRows } from "../lib/app/deploy-output";
import { renderDeployOutputRows, renderDeploySettingsPreview } from "../lib/app/deploy-output";
import {
DEFAULT_LOCAL_DEV_PORT,
resolveLocalBuildType,
Expand Down Expand Up @@ -2842,10 +2842,15 @@ async function maybeCustomizeDeploySettings(
};
}

maybeRenderDeploySettingsPreview(context, {
framework: options.framework,
runtime: options.runtime,
});

const shouldCustomize = await confirmPrompt({
input: context.runtime.stdin,
output: context.runtime.stderr,
message: "Customize settings?",
message: "Customize build settings?",
initialValue: false,
});

Expand Down Expand Up @@ -2900,6 +2905,26 @@ async function maybeCustomizeDeploySettings(
};
}

function maybeRenderDeploySettingsPreview(
context: CommandContext,
options: {
framework: ResolvedDeployFramework;
runtime: ResolvedDeployRuntime;
},
): void {
if (context.flags.quiet || context.flags.json) {
return;
}

context.output.stderr.write(
`Detected ${options.framework.displayName}\n`
+ `${renderDeploySettingsPreview(context.ui, [
{ key: "framework", value: options.framework.displayName },
{ key: "runtime", value: `HTTP ${options.runtime.port}` },
]).join("\n")}\n\n`,
);
}

function frameworkDisplayName(framework: DeployFramework): string {
switch (framework) {
case "nextjs":
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/src/lib/app/deploy-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@ export interface DeployOutputRow {
origin?: string;
}

export interface DeploySettingsPreviewRow {
key: string;
value: string;
}

const DEPLOY_OUTPUT_MIN_LABEL_WIDTH = "Framework".length;
const DEPLOY_OUTPUT_MIN_VALUE_WIDTH = "HTTP 3000".length;
const DEPLOY_SETTINGS_MIN_KEY_WIDTH = "framework:".length;

export function renderDeployOutputRows(ui: ShellUi, rows: DeployOutputRow[]): string[] {
if (rows.length === 0) {
Expand All @@ -28,3 +34,17 @@ export function renderDeployOutputRows(ui: ShellUi, rows: DeployOutputRow[]): st
return ` ${label} ${value}${origin}`.trimEnd();
});
}

export function renderDeploySettingsPreview(ui: ShellUi, rows: DeploySettingsPreviewRow[]): string[] {
if (rows.length === 0) {
return [];
}

const keyWidth = Math.max(DEPLOY_SETTINGS_MIN_KEY_WIDTH, ...rows.map((row) => `${row.key}:`.length));
const rail = ui.dim("│");

return rows.map((row) => {
const key = ui.accent(padDisplay(`${row.key}:`, keyWidth));
return `${rail} ${key} ${ui.strong(row.value)}`;
});
}
78 changes: 78 additions & 0 deletions packages/cli/tests/app-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1733,6 +1733,84 @@ describe("app controller", () => {
expect(stderr.buffer).toContain(`Linked "./${path.basename(cwd)}" to Project "Acme Dashboard"`);
});

it("interactive first deploy previews detected framework and runtime before the customization prompt", async () => {
const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient());
const createProject = vi.fn();
const listApps = vi.fn().mockResolvedValue([]);
const deployApp = vi.fn().mockResolvedValue({
projectId: "proj_123",
app: {
id: "app_new",
name: "hello-world",
region: "eu-central-1",
liveDeploymentId: "dep_123",
},
deployment: {
id: "dep_123",
status: "running",
url: "https://hello-world.prisma.app",
},
});

vi.doMock("../src/lib/auth/guard", () => ({
requireComputeAuth,
}));
vi.doMock("../src/lib/app/preview-provider", () => ({
createPreviewAppProvider: vi.fn(() => ({
createProject,
listApps,
deployApp,
listDeployments: vi.fn(),
showDeployment: vi.fn(),
})),
}));

const { createTempCwd, createTestCommandContext } = await import("./helpers");
const { runAppDeploy } = await import("../src/controllers/app");
const cwd = await createTempCwd();
await writePackageJson(cwd, {
name: "hello-world",
dependencies: {
next: "15.0.0",
},
});
const stateDir = path.join(cwd, ".state");
const { context, stderr } = await createTestCommandContext({
cwd,
stateDir,
isTTY: true,
stdinText: "\r\r",
env: {
...process.env,
PRISMA_CLI_TEST_REMEMBER_PROJECT_ID: "",
PRISMA_CLI_MOCK_FIXTURE_PATH: undefined,
},
});

await runAppDeploy(context, "hello-world");

expect(deployApp).toHaveBeenCalledWith(
expect.objectContaining({
buildType: "nextjs",
portMapping: { http: 3000 },
}),
);

const targetIndex = stderr.buffer.indexOf("Deploying to Acme Dashboard / main / hello-world");
const detectedIndex = stderr.buffer.indexOf("Detected Next.js");
const promptIndex = stderr.buffer.indexOf("Customize build settings?");

expect(targetIndex).toBeGreaterThanOrEqual(0);
expect(detectedIndex).toBeGreaterThan(targetIndex);
expect(stderr.buffer).toContain("framework:");
expect(stderr.buffer).toContain("runtime:");
expect(stderr.buffer).toContain("Next.js");
expect(stderr.buffer).toContain("HTTP 3000");
expect(stderr.buffer).not.toContain("Using deploy settings:");
expect(stderr.buffer).not.toContain("build:");
expect(promptIndex).toBeGreaterThan(detectedIndex);
});

it("interactive first deploy can create a new Project from an editable suggested name", async () => {
const requireComputeAuth = vi.fn().mockResolvedValue(createProjectClient());
const createProject = vi.fn().mockResolvedValue({
Expand Down
Loading