diff --git a/docs/product/command-spec.md b/docs/product/command-spec.md index 96aea2e..3f2fbb2 100644 --- a/docs/product/command-spec.md +++ b/docs/product/command-spec.md @@ -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 / / `; later deploys print a compact target header such as `Deploying ./j1 to j1 / main / j1` - deploy progress uses short stage copy (`Building locally...`, `Built `, `Uploading...`, `Uploaded`, `Deploying...`, `Deployed`) and never prints `Status: running` or `Deployment is running at ...` - success human output prints `Live in `, the URL on its own line, and `Logs prisma-cli app logs` diff --git a/docs/product/output-conventions.md b/docs/product/output-conventions.md index ed9010e..b611bed 100644 --- a/docs/product/output-conventions.md +++ b/docs/product/output-conventions.md @@ -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 diff --git a/packages/cli/src/controllers/app.ts b/packages/cli/src/controllers/app.ts index 045e603..bc9bde8 100644 --- a/packages/cli/src/controllers/app.ts +++ b/packages/cli/src/controllers/app.ts @@ -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, @@ -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, }); @@ -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": diff --git a/packages/cli/src/lib/app/deploy-output.ts b/packages/cli/src/lib/app/deploy-output.ts index 3aa811f..ffb780c 100644 --- a/packages/cli/src/lib/app/deploy-output.ts +++ b/packages/cli/src/lib/app/deploy-output.ts @@ -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) { @@ -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)}`; + }); +} diff --git a/packages/cli/tests/app-controller.test.ts b/packages/cli/tests/app-controller.test.ts index ea6ba6a..7f6e84e 100644 --- a/packages/cli/tests/app-controller.test.ts +++ b/packages/cli/tests/app-controller.test.ts @@ -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({