From 351f2d48953663961020a4052af48b3282595301 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 19 Aug 2026 12:48:26 +0530 Subject: [PATCH 1/6] feat: improve Prisma deployment completion --- bun.lock | 14 +- package.json | 2 +- src/commands/create.ts | 6 +- src/tasks/deploy-with-composer.ts | 212 +++++++++++++++++++++++------ src/tasks/setup-prisma.ts | 54 +++++++- tests/deploy-with-composer.test.ts | 66 ++++++--- 6 files changed, 279 insertions(+), 75 deletions(-) diff --git a/bun.lock b/bun.lock index 4ad777f..c333548 100644 --- a/bun.lock +++ b/bun.lock @@ -5,7 +5,7 @@ "": { "name": "create-prisma", "dependencies": { - "@clack/prompts": "^1.0.1", + "@clack/prompts": "^1.7.0", "@orpc/server": "^1.13.5", "execa": "^9.6.1", "fs-extra": "^11.3.3", @@ -39,9 +39,9 @@ "@babel/types": ["@babel/types@8.0.0-rc.1", "", { "dependencies": { "@babel/helper-string-parser": "^8.0.0-rc.1", "@babel/helper-validator-identifier": "^8.0.0-rc.1" } }, "sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ=="], - "@clack/core": ["@clack/core@1.0.1", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-WKeyK3NOBwDOzagPR5H08rFk9D/WuN705yEbuZvKqlkmoLM2woKtXb10OO2k1NoSU4SFG947i2/SCYh+2u5e4g=="], + "@clack/core": ["@clack/core@1.4.3", "", { "dependencies": { "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ=="], - "@clack/prompts": ["@clack/prompts@1.0.1", "", { "dependencies": { "@clack/core": "1.0.1", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-/42G73JkuYdyWZ6m8d/CJtBrGl1Hegyc7Fy78m5Ob+jF85TOUmLR5XLce/U3LxYAw0kJ8CT5aI99RIvPHcGp/Q=="], + "@clack/prompts": ["@clack/prompts@1.7.0", "", { "dependencies": { "@clack/core": "1.4.3", "fast-string-width": "^3.0.2", "fast-wrap-ansi": "^0.2.0", "sisteransi": "^1.0.5" } }, "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A=="], "@electric-sql/pglite": ["@electric-sql/pglite@0.4.3", "", {}, "sha512-ichuWTgtd4mOM1G4SpyGJa5trT03lWbMypDV0fUXUCXg5hiHqVAz/bZyV68NqmkLB7WcYmj1RMJVSp8HV/v/ZQ=="], @@ -343,8 +343,14 @@ "fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], @@ -497,8 +503,6 @@ "perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], "pkg-dir": ["pkg-dir@4.2.0", "", { "dependencies": { "find-up": "^4.0.0" } }, "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ=="], diff --git a/package.json b/package.json index a6a2693..5017900 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "release-notes": "bunx changelogithub" }, "dependencies": { - "@clack/prompts": "^1.0.1", + "@clack/prompts": "^1.7.0", "@orpc/server": "^1.13.5", "execa": "^9.6.1", "fs-extra": "^11.3.3", diff --git a/src/commands/create.ts b/src/commands/create.ts index 27dc4f8..5d96b69 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -330,7 +330,7 @@ async function executeCreateContext( log.success("Starter files scaffolded."); } } catch (error) { - createSpinner?.stop("Could not create Prisma 8 project."); + createSpinner?.error("Could not create Prisma 8 project."); return { ok: false, stage: "scaffold_template", @@ -345,7 +345,7 @@ async function executeCreateContext( projectDir: context.targetDirectory, }); } catch (error) { - createSpinner?.stop("Could not create Prisma 8 project."); + createSpinner?.error("Could not create Prisma 8 project."); return { ok: false, stage: "scaffold_template", @@ -391,7 +391,7 @@ async function executeCreateContext( }; } } catch (error) { - createSpinner?.stop("Could not create Prisma 8 project."); + createSpinner?.error("Could not create Prisma 8 project."); return { ok: false, stage: "prisma_setup", diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index bc5a9c7..baa4e19 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -10,10 +10,45 @@ import { getRunScriptCommand, } from "../utils/package-manager"; -type PrismaCliEnvelope = { +type PrismaCliEnvelope = { ok: boolean; - result?: unknown; - error?: { summary?: string; message?: string }; + result?: Result; + error?: { summary?: string; message?: string; why?: string }; +}; + +type PrismaWorkspace = { + id: string; + name: string | null; +}; + +type WhoamiResult = { + authenticated: boolean; + workspace: PrismaWorkspace | null; +}; + +type ProjectShowResult = { + workspace: PrismaWorkspace; + project: { id: string; name: string } | null; +}; + +type ComposerDeployCommandResult = { + summary: { + app: string; + nodes: Array<{ + entities: Array<{ kind: string; id: string; url?: string }>; + }>; + } | null; +}; + +export type ComposerDeployResult = { + appName: string; + appUrl?: string; + workspace?: PrismaWorkspace; + project: { + id?: string; + name: string; + consoleUrl?: string; + }; }; function redactSecrets(message: string): string { @@ -30,7 +65,9 @@ function getErrorMessage(error: unknown): string { return redactSecrets(String(error)); } -export function parsePrismaCliEnvelope(output: string): PrismaCliEnvelope { +export function parsePrismaCliEnvelope( + output: string, +): PrismaCliEnvelope { const lines = output .split(/\r?\n/) .map((line) => line.trim()) @@ -43,50 +80,72 @@ export function parsePrismaCliEnvelope(output: string): PrismaCliEnvelope { const candidate = parsed.kind === "result" ? parsed.envelope : parsed; if (typeof candidate !== "object" || candidate === null) continue; if (typeof Reflect.get(candidate, "ok") !== "boolean") continue; - return candidate as PrismaCliEnvelope; + return candidate as PrismaCliEnvelope; } catch { - // The CLI may print progress lines before its final JSON envelope. + // The CLI may print progress frames before its final JSON envelope. } } throw new Error("Prisma CLI returned output that is not a valid result envelope."); } -export function extractDeploymentUrl(output: string): string | undefined { - return output - .match(/https:\/\/[a-z0-9.-]+\.prisma\.build\/?/gi) - ?.at(-1) - ?.replace(/\/$/, ""); -} - function getPrismaCliArgs(packageManager: PackageManager, args: string[]) { return getPackageExecutionArgs(packageManager, [PRISMA_PLATFORM_CLI_PACKAGE, ...args]); } -async function isAuthenticated(packageManager: PackageManager, projectDir: string) { - const invocation = getPrismaCliArgs(packageManager, [ - "auth", - "whoami", +async function runPrismaJsonCommand(options: { + packageManager: PackageManager; + projectDir: string; + args: string[]; + forwardStderr?: boolean; +}): Promise { + const invocation = getPrismaCliArgs(options.packageManager, [ + ...options.args, "--json", "--no-interactive", ]); const result = await execa(invocation.command, invocation.args, { - cwd: projectDir, + cwd: options.projectDir, env: process.env, reject: false, }); - const envelope = parsePrismaCliEnvelope(result.stdout); - if (result.exitCode !== 0 || !envelope.ok) { + + if (options.forwardStderr && result.stderr) { + process.stderr.write(result.stderr.endsWith("\n") ? result.stderr : `${result.stderr}\n`); + } + + let envelope: PrismaCliEnvelope; + try { + envelope = parsePrismaCliEnvelope(result.stdout); + } catch (error) { + if (result.exitCode !== 0 && result.stderr.trim()) throw new Error(result.stderr.trim()); + throw error; + } + + if (result.exitCode !== 0 || !envelope.ok || envelope.result === undefined) { + const summary = envelope.error?.summary ?? envelope.error?.message; throw new Error( - envelope.error?.summary ?? envelope.error?.message ?? "Prisma authentication check failed.", + [summary, envelope.error?.why].filter(Boolean).join(": ") || + result.stderr.trim() || + "Prisma CLI command failed.", ); } - if (typeof envelope.result !== "object" || envelope.result === null) return false; - return Reflect.get(envelope.result, "authenticated") === true; + return envelope.result; } -async function ensureAuthentication(packageManager: PackageManager, projectDir: string) { - if (await isAuthenticated(packageManager, projectDir)) return; +async function ensureAuthentication( + packageManager: PackageManager, + projectDir: string, +): Promise { + const whoami = () => + runPrismaJsonCommand({ + packageManager, + projectDir, + args: ["auth", "whoami"], + }); + + const authState = await whoami(); + if (authState.authenticated) return authState; const loginCommand = getPackageExecutionCommand(packageManager, [ PRISMA_PLATFORM_CLI_PACKAGE, @@ -107,44 +166,109 @@ async function ensureAuthentication(packageManager: PackageManager, projectDir: stdio: "inherit", }); - if (!(await isAuthenticated(packageManager, projectDir))) { + const authenticatedState = await whoami(); + if (!authenticatedState.authenticated) { throw new Error("Prisma sign-in completed without an active workspace session."); } + return authenticatedState; +} + +export function parseComposerDeployResult(result: ComposerDeployCommandResult): + | { + appName: string; + appUrl?: string; + } + | undefined { + const summary = result.summary; + if (!summary) return; + const computeService = summary.nodes + .flatMap((node) => node.entities) + .find((entity) => entity.kind === "compute-service"); + return { + appName: summary.app, + ...(computeService?.url ? { appUrl: computeService.url.replace(/\/$/, "") } : {}), + }; +} + +async function getProjectDetails(options: { + packageManager: PackageManager; + projectDir: string; + appName: string; +}): Promise | undefined> { + try { + const result = await runPrismaJsonCommand({ + packageManager: options.packageManager, + projectDir: options.projectDir, + args: ["project", "show", "--project", options.appName], + }); + if (!result.project) return; + return { + workspace: result.workspace, + project: { + id: result.project.id, + name: result.project.name, + consoleUrl: `https://console.prisma.io/${encodeURIComponent( + result.workspace.id, + )}/${encodeURIComponent(result.project.id)}`, + }, + }; + } catch { + // Metadata enrichment must not turn a successful deploy into a failure. + return; + } } export async function deployWithComposer(options: { + appName: string; packageManager: PackageManager; projectDir: string; verbose: boolean; -}): Promise { +}): Promise { const progress = options.verbose ? undefined : spinner(); try { - await ensureAuthentication(options.packageManager, options.projectDir); - progress?.start("Deploying to Prisma..."); + const authState = await ensureAuthentication(options.packageManager, options.projectDir); - const command = getRunScriptArgs(options.packageManager, "deploy"); - const result = await execa(command.command, command.args, { + progress?.start("Building for deployment..."); + if (options.verbose) log.step("Building for deployment."); + const build = getRunScriptArgs(options.packageManager, "build"); + await execa(build.command, build.args, { cwd: options.projectDir, env: process.env, stdio: options.verbose ? "inherit" : "pipe", }); + progress?.message("Deploying to Prisma..."); + if (options.verbose) log.step("Deploying to Prisma."); + const deployment = parseComposerDeployResult( + await runPrismaJsonCommand({ + packageManager: options.packageManager, + projectDir: options.projectDir, + args: ["composer", "deploy", "module.ts"], + forwardStderr: options.verbose, + }), + ); + const appName = deployment?.appName ?? options.appName; + + progress?.message("Loading deployment details..."); + const details = await getProjectDetails({ + packageManager: options.packageManager, + projectDir: options.projectDir, + appName, + }); + progress?.stop("Deployed to Prisma."); - if (options.verbose) { - log.success("Deployed to Prisma."); - } else { - const deploymentUrl = extractDeploymentUrl( - [result.stdout, result.stderr] - .filter((value): value is string => typeof value === "string") - .join("\n"), - ); - if (deploymentUrl) log.info(`App: ${deploymentUrl}`); - } - return true; + if (options.verbose) log.success("Deployed to Prisma."); + const workspace = details?.workspace ?? authState.workspace ?? undefined; + return { + appName, + ...(deployment?.appUrl ? { appUrl: deployment.appUrl } : {}), + ...(workspace ? { workspace } : {}), + project: details?.project ?? { name: appName }, + }; } catch (error) { - progress?.stop("Deployment failed."); + progress?.error("Deployment failed."); log.error(`Deploy failed: ${getErrorMessage(error)}`); - return false; + return; } } diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index 49e84f5..2c00c3a 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -21,7 +21,7 @@ import { getPackageExecutionArgs, getRunScriptCommand, } from "../utils/package-manager"; -import { deployWithComposer } from "./deploy-with-composer"; +import { deployWithComposer, type ComposerDeployResult } from "./deploy-with-composer"; import { installProjectDependencies, writePrismaDependencies } from "./install"; const DEFAULT_DATABASE_PROVIDER: DatabaseProvider = "postgres"; @@ -262,6 +262,42 @@ function formatNextSteps(steps: NextStep[]): string { return steps.map((step) => `${step.command}\n ${step.description}`).join("\n\n"); } +function formatPlatformTarget(name: string | null, id: string): string { + return name ? `${name} (${id})` : id; +} + +function formatProjectSummary(options: { + createdProjectPath?: string; + deployment?: ComposerDeployResult; +}): string { + const lines: string[] = []; + if (options.createdProjectPath) { + lines.push(`Path: ${path.resolve(options.createdProjectPath)}`); + } + if (options.deployment?.workspace) { + lines.push( + `Workspace: ${formatPlatformTarget( + options.deployment.workspace.name, + options.deployment.workspace.id, + )}`, + ); + } + if (options.deployment) { + lines.push( + `Project: ${ + options.deployment.project.id + ? formatPlatformTarget(options.deployment.project.name, options.deployment.project.id) + : options.deployment.project.name + }`, + ); + lines.push(`App: ${options.deployment.appUrl ?? options.deployment.appName}`); + if (options.deployment.project.consoleUrl) { + lines.push(`Console: ${options.deployment.project.consoleUrl}`); + } + } + return lines.join("\n"); +} + function buildNextSteps(context: PrismaSetupContext, options: PrismaSetupRunOptions): NextStep[] { const nextSteps = [...(options.prependNextSteps ?? [])]; if (context.databaseProvider === "mongo") { @@ -326,22 +362,28 @@ export async function executePrismaSetupContext( await emitContract(context, projectDir); progress?.stop("Prisma 8 project ready."); } catch (error) { - progress?.stop("Could not create Prisma 8 project."); + progress?.error("Could not create Prisma 8 project."); cancel(getCommandErrorMessage(error)); return false; } + let deployment: ComposerDeployResult | undefined; if (context.shouldDeploy) { - const didDeploy = await deployWithComposer({ + deployment = await deployWithComposer({ + appName: projectName, packageManager: context.packageManager, projectDir, verbose: context.verbose, }); - if (!didDeploy) return false; + if (!deployment) return false; } - if (options.createdProjectPath) note(path.resolve(options.createdProjectPath), "Project path"); + const projectSummary = formatProjectSummary({ + createdProjectPath: options.createdProjectPath, + deployment, + }); + if (projectSummary) note(projectSummary, context.shouldDeploy ? "Deployment" : "Project"); note(formatNextSteps(buildNextSteps(context, options)), "Next steps"); - outro(context.shouldDeploy ? "Prisma 8 app deployed." : "Prisma 8 setup complete."); + outro(context.shouldDeploy ? "Prisma 8 app deployed." : "Prisma 8 project ready."); return true; } diff --git a/tests/deploy-with-composer.test.ts b/tests/deploy-with-composer.test.ts index 7ffd8ce..b7b5c0a 100644 --- a/tests/deploy-with-composer.test.ts +++ b/tests/deploy-with-composer.test.ts @@ -1,27 +1,61 @@ import { describe, expect, test } from "bun:test"; -import { extractDeploymentUrl } from "../src/tasks/deploy-with-composer"; +import { + parseComposerDeployResult, + parsePrismaCliEnvelope, +} from "../src/tasks/deploy-with-composer"; -describe("extractDeploymentUrl", () => { - test("returns the deployed Prisma Compute URL", () => { +describe("parsePrismaCliEnvelope", () => { + test("reads the terminal result after progress frames", () => { expect( - extractDeploymentUrl(` -app compute-service cps_abc123 - https://abc123.ewr.prisma.build -Done: 22 succeeded -`), - ).toBe("https://abc123.ewr.prisma.build"); + parsePrismaCliEnvelope( + [ + '{"kind":"progress","message":"Deploying"}', + '{"kind":"result","envelope":{"ok":true,"result":{"summary":null}}}', + ].join("\n"), + ), + ).toEqual({ ok: true, result: { summary: null } }); }); +}); - test("uses the final deployed URL and removes a trailing slash", () => { +describe("parseComposerDeployResult", () => { + test("reads the official Composer deployment summary", () => { expect( - extractDeploymentUrl( - "Previous: https://old.ewr.prisma.build\nCurrent: https://new.fra.prisma.build/", - ), - ).toBe("https://new.fra.prisma.build"); + parseComposerDeployResult({ + summary: { + app: "my-app", + nodes: [ + { + address: "app", + entities: [ + { + kind: "compute-service", + id: "cps_abc123", + url: "https://abc123.ewr.prisma.build/", + }, + ], + }, + ], + }, + }), + ).toEqual({ + appName: "my-app", + appUrl: "https://abc123.ewr.prisma.build", + }); + }); + + test("keeps the app name when no compute URL was reported", () => { + expect( + parseComposerDeployResult({ + summary: { + app: "worker", + nodes: [{ address: "database", entities: [] }], + }, + }), + ).toEqual({ appName: "worker" }); }); - test("returns undefined when deploy output has no Compute URL", () => { - expect(extractDeploymentUrl("Done: 22 succeeded")).toBeUndefined(); + test("returns undefined when Composer has no deployment summary", () => { + expect(parseComposerDeployResult({ summary: null })).toBeUndefined(); }); }); From d052076be1937ac85940e3ef1b82357aadd566c2 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 19 Aug 2026 12:59:06 +0530 Subject: [PATCH 2/6] feat: support Prisma workspace selection --- README.md | 7 +- src/tasks/deploy-with-composer.ts | 111 +++++++++++++++++++++++++++++- src/tasks/setup-prisma.ts | 6 ++ src/types.ts | 6 ++ tests/setup-prisma.test.ts | 34 +++++++++ tests/telemetry.test.ts | 1 + 6 files changed, 162 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38f9a1c..2a42ae1 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ bunx create-prisma@next my-app The CLI initializes Prisma 8 with `prisma@next`, installs dependencies, emits the contract, and generates a deployable Composer app. PostgreSQL projects use Composer's native Prisma Postgres provider, including migrations and a typed runtime client. -The only Composer prompt is: +The deployment prompt is: ```text Deploy to Prisma now? @@ -23,6 +23,10 @@ Deploy to Prisma now? Choose no to deploy later with the generated `deploy` script. +When multiple Prisma workspace sessions are available, the CLI asks which workspace should receive +the deployment. For unattended usage, pass `--workspace ` or omit it to use the active +workspace. Choosing another workspace also updates the Prisma CLI's active workspace session. + ## Templates - `minimal` @@ -45,6 +49,7 @@ PostgreSQL and MongoDB are supported with PSL or TypeScript contract authoring. - `--authoring psl|typescript` - `--package-manager npm|pnpm|yarn|bun` - `--deploy` / `--no-deploy` +- `--workspace ` - `--yes` - `--force` - `--verbose` diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index baa4e19..65331e0 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -1,4 +1,4 @@ -import { log, spinner } from "@clack/prompts"; +import { cancel, isCancel, log, select, spinner } from "@clack/prompts"; import { execa } from "execa"; import { PRISMA_PLATFORM_CLI_PACKAGE } from "../constants/dependencies"; @@ -24,6 +24,19 @@ type PrismaWorkspace = { type WhoamiResult = { authenticated: boolean; workspace: PrismaWorkspace | null; + source: "stored" | "environment" | null; +}; + +type WorkspaceListResult = { + items: Array<{ + workspaceId: string; + workspaceName: string | null; + current: boolean; + }>; +}; + +type WorkspaceUseResult = { + workspace: PrismaWorkspace; }; type ProjectShowResult = { @@ -173,6 +186,90 @@ async function ensureAuthentication( return authenticatedState; } +function workspaceLabel(workspace: PrismaWorkspace): string { + return workspace.name ?? workspace.id; +} + +async function useWorkspace(options: { + packageManager: PackageManager; + projectDir: string; + workspace: string; + activeWorkspace: PrismaWorkspace; +}): Promise { + const result = await runPrismaJsonCommand({ + packageManager: options.packageManager, + projectDir: options.projectDir, + args: ["auth", "workspace", "use", options.workspace], + }); + if (result.workspace.id !== options.activeWorkspace.id) { + log.info(`Prisma CLI workspace switched to ${workspaceLabel(result.workspace)}.`); + } + return result.workspace; +} + +async function selectDeploymentWorkspace(options: { + packageManager: PackageManager; + projectDir: string; + shouldPrompt: boolean; + workspace?: string; + authState: WhoamiResult; +}): Promise { + const activeWorkspace = options.authState.workspace; + if (!activeWorkspace) { + throw new Error("The active Prisma credential does not specify a workspace."); + } + + if (options.workspace) { + if (options.authState.source === "environment") { + if (options.workspace === activeWorkspace.id || options.workspace === activeWorkspace.name) { + return activeWorkspace; + } + throw new Error( + `The environment credential is fixed to workspace ${workspaceLabel( + activeWorkspace, + )}. Unset it before using --workspace ${options.workspace}.`, + ); + } + return useWorkspace({ + packageManager: options.packageManager, + projectDir: options.projectDir, + workspace: options.workspace, + activeWorkspace, + }); + } + + if (!options.shouldPrompt || process.stdin.isTTY !== true) return activeWorkspace; + + const available = await runPrismaJsonCommand({ + packageManager: options.packageManager, + projectDir: options.projectDir, + args: ["auth", "workspace", "list"], + }); + if (available.items.length <= 1) return activeWorkspace; + + const selectedWorkspaceId = await select({ + message: "Select Prisma workspace for deployment", + initialValue: activeWorkspace.id, + options: available.items.map((workspace) => ({ + value: workspace.workspaceId, + label: workspace.workspaceName ?? workspace.workspaceId, + hint: workspace.current ? `${workspace.workspaceId}, current` : workspace.workspaceId, + })), + }); + if (isCancel(selectedWorkspaceId)) { + cancel("Operation cancelled."); + return; + } + if (selectedWorkspaceId === activeWorkspace.id) return activeWorkspace; + + return useWorkspace({ + packageManager: options.packageManager, + projectDir: options.projectDir, + workspace: selectedWorkspaceId, + activeWorkspace, + }); +} + export function parseComposerDeployResult(result: ComposerDeployCommandResult): | { appName: string; @@ -222,12 +319,22 @@ export async function deployWithComposer(options: { appName: string; packageManager: PackageManager; projectDir: string; + shouldPromptForWorkspace: boolean; verbose: boolean; + workspace?: string; }): Promise { const progress = options.verbose ? undefined : spinner(); try { const authState = await ensureAuthentication(options.packageManager, options.projectDir); + const selectedWorkspace = await selectDeploymentWorkspace({ + packageManager: options.packageManager, + projectDir: options.projectDir, + shouldPrompt: options.shouldPromptForWorkspace, + authState, + ...(options.workspace ? { workspace: options.workspace } : {}), + }); + if (!selectedWorkspace) return; progress?.start("Building for deployment..."); if (options.verbose) log.step("Building for deployment."); @@ -259,7 +366,7 @@ export async function deployWithComposer(options: { progress?.stop("Deployed to Prisma."); if (options.verbose) log.success("Deployed to Prisma."); - const workspace = details?.workspace ?? authState.workspace ?? undefined; + const workspace = details?.workspace ?? selectedWorkspace; return { appName, ...(deployment?.appUrl ? { appUrl: deployment.appUrl } : {}), diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index 2c00c3a..bc9556b 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -49,6 +49,8 @@ export type PrismaSetupContext = { authoring: AuthoringStyle; packageManager: PackageManager; shouldDeploy: boolean; + shouldPromptForWorkspace: boolean; + workspace?: string; }; async function promptForDatabaseProvider(): Promise { @@ -155,6 +157,8 @@ export async function collectPrismaSetupContext( authoring, packageManager, shouldDeploy, + shouldPromptForWorkspace: !useDefaults, + ...(input.workspace ? { workspace: input.workspace } : {}), }; } @@ -373,7 +377,9 @@ export async function executePrismaSetupContext( appName: projectName, packageManager: context.packageManager, projectDir, + shouldPromptForWorkspace: context.shouldPromptForWorkspace, verbose: context.verbose, + ...(context.workspace ? { workspace: context.workspace } : {}), }); if (!deployment) return false; } diff --git a/src/types.ts b/src/types.ts index ea50fb4..c259164 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,6 +56,12 @@ export const PrismaSetupOptionsSchema = z.object({ "Package manager used for dependency installation", ), deploy: z.boolean().optional().describe("Deploy the generated app to Prisma immediately"), + workspace: z + .string() + .trim() + .min(1, "Please enter a valid workspace id or name") + .optional() + .describe("Prisma workspace id or name to deploy into"), }); export const PrismaSetupCommandInputSchema = CommonCommandOptionsSchema.extend( diff --git a/tests/setup-prisma.test.ts b/tests/setup-prisma.test.ts index c2f46f2..9b1649c 100644 --- a/tests/setup-prisma.test.ts +++ b/tests/setup-prisma.test.ts @@ -27,6 +27,7 @@ describe("collectPrismaSetupContext", () => { authoring: "psl", packageManager: "bun", shouldDeploy: false, + shouldPromptForWorkspace: false, }); }); }); @@ -41,6 +42,39 @@ describe("collectPrismaSetupContext", () => { }); }); + test("preserves an explicit workspace for unattended deployment", async () => { + await withTempProject(async (projectDir) => { + const context = await collectPrismaSetupContext( + { + yes: true, + packageManager: "pnpm", + deploy: true, + workspace: "workspace_123", + }, + { projectDir }, + ); + expect(context).toMatchObject({ + shouldPromptForWorkspace: false, + workspace: "workspace_123", + }); + }); + }); + + test("allows workspace selection during an interactive deployment", async () => { + await withTempProject(async (projectDir) => { + const context = await collectPrismaSetupContext( + { + provider: "postgres", + authoring: "psl", + packageManager: "bun", + deploy: true, + }, + { projectDir }, + ); + expect(context.shouldPromptForWorkspace).toBe(true); + }); + }); + test("keeps MongoDB as an explicit provider option", async () => { await withTempProject(async (projectDir) => { const context = await collectPrismaSetupContext( diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index e725894..7b9d977 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -28,6 +28,7 @@ const createContext: CreatePromptContext = { authoring: "psl", packageManager: "bun", shouldDeploy: true, + shouldPromptForWorkspace: false, }, }; From ca91936e61a73aa7bfd4c10fc6aec60041627aa1 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 19 Aug 2026 13:06:21 +0530 Subject: [PATCH 3/6] fix: type Prisma config environment access --- templates/create/_shared/prisma.config.ts.hbs | 4 +++- tests/install.test.ts | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/templates/create/_shared/prisma.config.ts.hbs b/templates/create/_shared/prisma.config.ts.hbs index 4fdf962..d5adaea 100644 --- a/templates/create/_shared/prisma.config.ts.hbs +++ b/templates/create/_shared/prisma.config.ts.hbs @@ -1,3 +1,5 @@ +import { env } from "node:process"; + import { definePrismaConfig } from "@prisma/cli-engine"; import { defineConfig as ormConfig } from "@prisma/orm-{{#if (eq provider "postgres")}}postgres{{else}}mongo{{/if}}/config"; @@ -8,7 +10,7 @@ export default definePrismaConfig({ output: "./src/prisma/generated", {{/if}} db: { - connection: process.env.{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}!, + connection: env.{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}!, }, }), composer: { diff --git a/tests/install.test.ts b/tests/install.test.ts index 60ca790..3b788b2 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -176,7 +176,8 @@ describe("generated templates", () => { if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); expect(moduleSource).toContain('config: "./prisma.config.ts"'); - expect(prismaConfig).toContain("connection: process.env.DATABASE_URL!"); + expect(prismaConfig).toContain('import { env } from "node:process"'); + expect(prismaConfig).toContain("connection: env.DATABASE_URL!"); expect(seedSource).toContain("conflictOn: { email: user.email }"); const composerSource = await readFile( path.join(projectDir, "src/prisma/composer.ts"), @@ -197,7 +198,8 @@ describe("generated templates", () => { } } else { expect(moduleSource).toContain('envSecret("MONGODB_URL")'); - expect(prismaConfig).toContain("connection: process.env.MONGODB_URL!"); + expect(prismaConfig).toContain('import { env } from "node:process"'); + expect(prismaConfig).toContain("connection: env.MONGODB_URL!"); expect(seedSource).not.toContain(".prisma-composer"); } if (authoring === "typescript") { From aa33e8e3a574af66c963ea0271b54f8dace35d61 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 19 Aug 2026 13:10:08 +0530 Subject: [PATCH 4/6] fix: use Console route project IDs --- src/tasks/deploy-with-composer.ts | 17 ++++++++++++++--- tests/deploy-with-composer.test.ts | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index 65331e0..1f456ee 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -78,6 +78,19 @@ function getErrorMessage(error: unknown): string { return redactSecrets(String(error)); } +function stripResourcePrefix(id: string, prefix: "proj" | "wksp"): string { + const marker = `${prefix}_`; + return id.startsWith(marker) ? id.slice(marker.length) : id; +} + +export function getConsoleProjectUrl(workspaceId: string, projectId: string): string { + const consoleWorkspaceId = stripResourcePrefix(workspaceId, "wksp"); + const consoleProjectId = stripResourcePrefix(projectId, "proj"); + return `https://console.prisma.io/${encodeURIComponent( + consoleWorkspaceId, + )}/${encodeURIComponent(consoleProjectId)}`; +} + export function parsePrismaCliEnvelope( output: string, ): PrismaCliEnvelope { @@ -304,9 +317,7 @@ async function getProjectDetails(options: { project: { id: result.project.id, name: result.project.name, - consoleUrl: `https://console.prisma.io/${encodeURIComponent( - result.workspace.id, - )}/${encodeURIComponent(result.project.id)}`, + consoleUrl: getConsoleProjectUrl(result.workspace.id, result.project.id), }, }; } catch { diff --git a/tests/deploy-with-composer.test.ts b/tests/deploy-with-composer.test.ts index b7b5c0a..017c94d 100644 --- a/tests/deploy-with-composer.test.ts +++ b/tests/deploy-with-composer.test.ts @@ -1,10 +1,25 @@ import { describe, expect, test } from "bun:test"; import { + getConsoleProjectUrl, parseComposerDeployResult, parsePrismaCliEnvelope, } from "../src/tasks/deploy-with-composer"; +describe("getConsoleProjectUrl", () => { + test("converts Management API resource ids to Console route ids", () => { + expect(getConsoleProjectUrl("wksp_workspace123", "proj_project123")).toBe( + "https://console.prisma.io/workspace123/project123", + ); + }); + + test("preserves raw workspace and project ids", () => { + expect(getConsoleProjectUrl("workspace123", "project123")).toBe( + "https://console.prisma.io/workspace123/project123", + ); + }); +}); + describe("parsePrismaCliEnvelope", () => { test("reads the terminal result after progress frames", () => { expect( From 94df6eb3b65651f53b5258bcbb455bbab3f3d389 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 19 Aug 2026 13:16:30 +0530 Subject: [PATCH 5/6] fix: declare Node types in generated templates --- templates/create/_shared/prisma.config.ts.hbs | 4 +--- templates/create/astro/tsconfig.json | 3 +++ templates/create/elysia/tsconfig.json | 3 ++- templates/create/hono/tsconfig.json | 3 ++- templates/create/minimal/tsconfig.json | 3 ++- templates/create/nest/tsconfig.json | 3 ++- templates/create/next/tsconfig.json | 1 + templates/create/nuxt/tsconfig.json | 3 +++ templates/create/svelte/tsconfig.json | 3 ++- templates/create/tanstack-start/tsconfig.json | 2 +- tests/install.test.ts | 8 ++++---- 11 files changed, 23 insertions(+), 13 deletions(-) diff --git a/templates/create/_shared/prisma.config.ts.hbs b/templates/create/_shared/prisma.config.ts.hbs index d5adaea..4fdf962 100644 --- a/templates/create/_shared/prisma.config.ts.hbs +++ b/templates/create/_shared/prisma.config.ts.hbs @@ -1,5 +1,3 @@ -import { env } from "node:process"; - import { definePrismaConfig } from "@prisma/cli-engine"; import { defineConfig as ormConfig } from "@prisma/orm-{{#if (eq provider "postgres")}}postgres{{else}}mongo{{/if}}/config"; @@ -10,7 +8,7 @@ export default definePrismaConfig({ output: "./src/prisma/generated", {{/if}} db: { - connection: env.{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}!, + connection: process.env.{{#if (eq provider "postgres")}}DATABASE_URL{{else}}MONGODB_URL{{/if}}!, }, }), composer: { diff --git a/templates/create/astro/tsconfig.json b/templates/create/astro/tsconfig.json index 8bf91d3..7bb947c 100644 --- a/templates/create/astro/tsconfig.json +++ b/templates/create/astro/tsconfig.json @@ -1,5 +1,8 @@ { "extends": "astro/tsconfigs/strict", + "compilerOptions": { + "types": ["astro/client", "node"] + }, "include": [".astro/types.d.ts", "**/*"], "exclude": ["dist"] } diff --git a/templates/create/elysia/tsconfig.json b/templates/create/elysia/tsconfig.json index 03edb06..f85f59d 100644 --- a/templates/create/elysia/tsconfig.json +++ b/templates/create/elysia/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "dist", - "rootDir": "." + "rootDir": ".", + "types": ["node"] } } diff --git a/templates/create/hono/tsconfig.json b/templates/create/hono/tsconfig.json index 03edb06..f85f59d 100644 --- a/templates/create/hono/tsconfig.json +++ b/templates/create/hono/tsconfig.json @@ -10,6 +10,7 @@ "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "outDir": "dist", - "rootDir": "." + "rootDir": ".", + "types": ["node"] } } diff --git a/templates/create/minimal/tsconfig.json b/templates/create/minimal/tsconfig.json index 42e3bae..b4edbd2 100644 --- a/templates/create/minimal/tsconfig.json +++ b/templates/create/minimal/tsconfig.json @@ -8,7 +8,8 @@ "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "types": ["node"] }, "include": ["src/**/*.ts"] } diff --git a/templates/create/nest/tsconfig.json b/templates/create/nest/tsconfig.json index 99ab532..3fa1273 100644 --- a/templates/create/nest/tsconfig.json +++ b/templates/create/nest/tsconfig.json @@ -11,7 +11,8 @@ "forceConsistentCasingInFileNames": true, "emitDecoratorMetadata": true, "experimentalDecorators": true, - "outDir": "dist" + "outDir": "dist", + "types": ["node"] }, "include": ["src/**/*.ts"] } diff --git a/templates/create/next/tsconfig.json b/templates/create/next/tsconfig.json index 470837e..9cb38fb 100644 --- a/templates/create/next/tsconfig.json +++ b/templates/create/next/tsconfig.json @@ -14,6 +14,7 @@ "isolatedModules": true, "jsx": "react-jsx", "incremental": true, + "types": ["node"], "plugins": [ { "name": "next" diff --git a/templates/create/nuxt/tsconfig.json b/templates/create/nuxt/tsconfig.json index 6ae5970..2c89a86 100644 --- a/templates/create/nuxt/tsconfig.json +++ b/templates/create/nuxt/tsconfig.json @@ -1,4 +1,7 @@ { + "compilerOptions": { + "types": ["node"] + }, "files": [], "references": [ { diff --git a/templates/create/svelte/tsconfig.json b/templates/create/svelte/tsconfig.json index c7b9df5..eca22f6 100644 --- a/templates/create/svelte/tsconfig.json +++ b/templates/create/svelte/tsconfig.json @@ -10,7 +10,8 @@ "skipLibCheck": true, "sourceMap": true, "strict": true, - "moduleResolution": "bundler" + "moduleResolution": "bundler", + "types": ["node"] } // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files diff --git a/templates/create/tanstack-start/tsconfig.json b/templates/create/tanstack-start/tsconfig.json index abdd2d4..0448467 100644 --- a/templates/create/tanstack-start/tsconfig.json +++ b/templates/create/tanstack-start/tsconfig.json @@ -17,7 +17,7 @@ "@/*": ["./src/*"] }, "lib": ["ES2022", "DOM", "DOM.Iterable"], - "types": ["vite/client"] + "types": ["vite/client", "node"] }, "include": ["src/**/*.ts", "src/**/*.tsx", "vite.config.ts"] } diff --git a/tests/install.test.ts b/tests/install.test.ts index 3b788b2..43dd0a1 100644 --- a/tests/install.test.ts +++ b/tests/install.test.ts @@ -137,12 +137,14 @@ describe("generated templates", () => { path.join(projectDir, "prisma.config.ts"), "utf8", ); + const tsconfig = await readFile(path.join(projectDir, "tsconfig.json"), "utf8"); expect(packageJson.scripts?.deploy).toBeDefined(); expect(packageJson.dependencies).toHaveProperty("@prisma/composer"); expect(packageJson.dependencies).toHaveProperty("alchemy"); expect(prismaConfig).toContain("orm: ormConfig({"); expect(prismaConfig).toContain('configPath: "./prisma-composer.config.ts"'); + expect(tsconfig).toContain('"node"'); expect(serviceSource).toContain("compute({"); expect(dbSource).toContain("export function connectDatabase()"); expect(seedSource).toContain("await connectDatabase()"); @@ -176,8 +178,7 @@ describe("generated templates", () => { if (provider === "postgres") { expect(moduleSource).toContain("pnPostgres({"); expect(moduleSource).toContain('config: "./prisma.config.ts"'); - expect(prismaConfig).toContain('import { env } from "node:process"'); - expect(prismaConfig).toContain("connection: env.DATABASE_URL!"); + expect(prismaConfig).toContain("connection: process.env.DATABASE_URL!"); expect(seedSource).toContain("conflictOn: { email: user.email }"); const composerSource = await readFile( path.join(projectDir, "src/prisma/composer.ts"), @@ -198,8 +199,7 @@ describe("generated templates", () => { } } else { expect(moduleSource).toContain('envSecret("MONGODB_URL")'); - expect(prismaConfig).toContain('import { env } from "node:process"'); - expect(prismaConfig).toContain("connection: env.MONGODB_URL!"); + expect(prismaConfig).toContain("connection: process.env.MONGODB_URL!"); expect(seedSource).not.toContain(".prisma-composer"); } if (authoring === "typescript") { From 54d7543e36c09c522f76b774ed2ae6e5760c1611 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Wed, 19 Aug 2026 13:20:21 +0530 Subject: [PATCH 6/6] fix: show deployment preparation progress --- src/tasks/deploy-with-composer.ts | 48 ++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/tasks/deploy-with-composer.ts b/src/tasks/deploy-with-composer.ts index 1f456ee..e0a7347 100644 --- a/src/tasks/deploy-with-composer.ts +++ b/src/tasks/deploy-with-composer.ts @@ -162,6 +162,7 @@ async function runPrismaJsonCommand(options: { async function ensureAuthentication( packageManager: PackageManager, projectDir: string, + beforeInteractiveLogin?: () => void, ): Promise { const whoami = () => runPrismaJsonCommand({ @@ -184,6 +185,7 @@ async function ensureAuthentication( ); } + beforeInteractiveLogin?.(); log.info("Sign in to Prisma to deploy."); const login = getPrismaCliArgs(packageManager, ["auth", "login"]); await execa(login.command, login.args, { @@ -207,16 +209,12 @@ async function useWorkspace(options: { packageManager: PackageManager; projectDir: string; workspace: string; - activeWorkspace: PrismaWorkspace; }): Promise { const result = await runPrismaJsonCommand({ packageManager: options.packageManager, projectDir: options.projectDir, args: ["auth", "workspace", "use", options.workspace], }); - if (result.workspace.id !== options.activeWorkspace.id) { - log.info(`Prisma CLI workspace switched to ${workspaceLabel(result.workspace)}.`); - } return result.workspace; } @@ -226,6 +224,8 @@ async function selectDeploymentWorkspace(options: { shouldPrompt: boolean; workspace?: string; authState: WhoamiResult; + beforePrompt?: () => void; + afterPrompt?: () => void; }): Promise { const activeWorkspace = options.authState.workspace; if (!activeWorkspace) { @@ -247,7 +247,6 @@ async function selectDeploymentWorkspace(options: { packageManager: options.packageManager, projectDir: options.projectDir, workspace: options.workspace, - activeWorkspace, }); } @@ -260,6 +259,7 @@ async function selectDeploymentWorkspace(options: { }); if (available.items.length <= 1) return activeWorkspace; + options.beforePrompt?.(); const selectedWorkspaceId = await select({ message: "Select Prisma workspace for deployment", initialValue: activeWorkspace.id, @@ -273,13 +273,13 @@ async function selectDeploymentWorkspace(options: { cancel("Operation cancelled."); return; } + options.afterPrompt?.(); if (selectedWorkspaceId === activeWorkspace.id) return activeWorkspace; return useWorkspace({ packageManager: options.packageManager, projectDir: options.projectDir, workspace: selectedWorkspaceId, - activeWorkspace, }); } @@ -335,19 +335,45 @@ export async function deployWithComposer(options: { workspace?: string; }): Promise { const progress = options.verbose ? undefined : spinner(); + let progressRunning = false; + const showProgress = (message: string) => { + if (!progress) return; + if (progressRunning) { + progress.message(message); + } else { + progress.start(message); + progressRunning = true; + } + }; + const clearProgress = () => { + if (!progress || !progressRunning) return; + progress.clear(); + progressRunning = false; + }; try { - const authState = await ensureAuthentication(options.packageManager, options.projectDir); + showProgress("Checking Prisma account..."); + if (options.verbose) log.step("Checking Prisma account."); + const authState = await ensureAuthentication( + options.packageManager, + options.projectDir, + clearProgress, + ); + + showProgress("Checking Prisma workspace..."); + if (options.verbose) log.step("Checking Prisma workspace."); const selectedWorkspace = await selectDeploymentWorkspace({ packageManager: options.packageManager, projectDir: options.projectDir, shouldPrompt: options.shouldPromptForWorkspace, authState, + beforePrompt: clearProgress, + afterPrompt: () => showProgress("Selecting Prisma workspace..."), ...(options.workspace ? { workspace: options.workspace } : {}), }); if (!selectedWorkspace) return; - progress?.start("Building for deployment..."); + showProgress("Building for deployment..."); if (options.verbose) log.step("Building for deployment."); const build = getRunScriptArgs(options.packageManager, "build"); await execa(build.command, build.args, { @@ -356,7 +382,7 @@ export async function deployWithComposer(options: { stdio: options.verbose ? "inherit" : "pipe", }); - progress?.message("Deploying to Prisma..."); + showProgress("Deploying to Prisma..."); if (options.verbose) log.step("Deploying to Prisma."); const deployment = parseComposerDeployResult( await runPrismaJsonCommand({ @@ -368,7 +394,7 @@ export async function deployWithComposer(options: { ); const appName = deployment?.appName ?? options.appName; - progress?.message("Loading deployment details..."); + showProgress("Loading deployment details..."); const details = await getProjectDetails({ packageManager: options.packageManager, projectDir: options.projectDir, @@ -376,6 +402,7 @@ export async function deployWithComposer(options: { }); progress?.stop("Deployed to Prisma."); + progressRunning = false; if (options.verbose) log.success("Deployed to Prisma."); const workspace = details?.workspace ?? selectedWorkspace; return { @@ -386,6 +413,7 @@ export async function deployWithComposer(options: { }; } catch (error) { progress?.error("Deployment failed."); + progressRunning = false; log.error(`Deploy failed: ${getErrorMessage(error)}`); return; }