From dd3177b8196c70827519f4806e8d2e40e02cca17 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 27 Aug 2026 20:14:12 +0530 Subject: [PATCH 1/2] fix: classify create telemetry outcomes --- src/commands/create.ts | 109 ++++++++++++++++++------- src/create-outcome.ts | 80 ++++++++++++++++++ src/result.ts | 4 +- src/tasks/deploy-with-composer.ts | 121 ++++++++++++++++++++-------- src/tasks/setup-prisma.ts | 109 +++++++++++++++++++------ src/telemetry/create.ts | 29 +++++-- src/telemetry/index.ts | 2 + tests/deploy-with-composer.test.ts | 35 ++++++++ tests/e2e/create-prisma.e2e.test.ts | 2 +- tests/telemetry.test.ts | 40 +++++++-- 10 files changed, 429 insertions(+), 102 deletions(-) create mode 100644 src/create-outcome.ts diff --git a/src/commands/create.ts b/src/commands/create.ts index 5ad25e7..112f3b9 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -3,17 +3,19 @@ import fs from "fs-extra"; import path from "node:path"; import type { Writable } from "node:stream"; +import { + CreateCancellationError, + getCreateFailureReason, + type CreateFailureReason, + type CreateFailureStage, +} from "../create-outcome"; import { CREATE_PRISMA_RESULT_SCHEMA_VERSION, createCommandFailureResult, type CreateCommandResult, type CreateProjectResult, } from "../result"; -import { - trackCreateCompleted, - trackCreateFailed, - type CreateTelemetryFailureStage, -} from "../telemetry"; +import { trackCreateCancelled, trackCreateCompleted, trackCreateFailed } from "../telemetry"; import { scaffoldCreateFrameworkTemplate } from "../templates/render-create-template"; import { writeCreateTemplateDependencies } from "../tasks/install"; import type { PrismaSetupContext } from "../tasks/setup-prisma"; @@ -51,14 +53,22 @@ type ExecuteCreateContextResult = | { ok: true; result: CreateCommandResult } | { ok: false; - stage: CreateTelemetryFailureStage; + cancelled: true; + stage: "select_workspace"; + errorReported?: boolean; + } + | { + ok: false; + cancelled?: false; + stage: CreateFailureStage; + reason: CreateFailureReason; error?: unknown; errorReported?: boolean; }; type CollectCreateContextResult = | { ok: true; context: CreatePromptContext } - | { ok: false; message: string }; + | { ok: false; message: string; reason: CreateFailureReason }; function toPackageName(projectName: string): string { return ( @@ -102,7 +112,7 @@ function getProjectResult(context: CreatePromptContext): CreateProjectResult { }; } -async function promptForProjectName(output: Writable): Promise { +async function promptForProjectName(output: Writable): Promise { const projectName = await text({ message: "Project name", placeholder: DEFAULT_PROJECT_NAME, @@ -113,13 +123,13 @@ async function promptForProjectName(output: Writable): Promise { +async function promptForCreateTemplate(output: Writable): Promise { const template = await select({ message: "Select template", initialValue: DEFAULT_TEMPLATE, @@ -175,7 +185,7 @@ async function promptForCreateTemplate(output: Writable): Promise project.id).join(", "); - throw new Error( + throw new ClassifiedCreateError( + "project_name_collision", `A Prisma project named "${options.appName}" already exists in workspace ${workspaceLabel( options.workspace, )} (${options.workspace.id}). Choose a different project name or delete the existing project ` + @@ -216,7 +238,8 @@ async function ensureAuthentication(options: { "login", ]); if (!options.allowInteractiveLogin || process.stdin.isTTY !== true) { - throw new Error( + throw new ClassifiedCreateError( + "not_authenticated", `Sign in first with ${loginCommand}, then run ${getRunScriptCommand(options.packageManager, "deploy")}.`, ); } @@ -232,7 +255,10 @@ async function ensureAuthentication(options: { const authenticatedState = await whoami(); if (!authenticatedState.authenticated) { - throw new Error("Prisma sign-in completed without an active workspace session."); + throw new ClassifiedCreateError( + "authentication_failed", + "Prisma sign-in completed without an active workspace session.", + ); } return authenticatedState; } @@ -263,38 +289,47 @@ async function selectDeploymentWorkspace(options: { beforePrompt?: () => void; afterPrompt?: () => void; output: Writable; -}): Promise { +}): Promise { const activeWorkspace = options.authState.workspace; if (!activeWorkspace) { - throw new Error("The active Prisma credential does not specify a workspace."); + throw new ClassifiedCreateError( + "workspace_missing", + "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; + return { ok: true, workspace: activeWorkspace }; } - throw new Error( + throw new ClassifiedCreateError( + "workspace_mismatch", `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, - }); + return { + ok: true, + workspace: await useWorkspace({ + packageManager: options.packageManager, + projectDir: options.projectDir, + workspace: options.workspace, + }), + }; } - if (!options.shouldPrompt || process.stdin.isTTY !== true) return activeWorkspace; + if (!options.shouldPrompt || process.stdin.isTTY !== true) { + return { ok: true, workspace: activeWorkspace }; + } const available = await runPrismaJsonCommand({ packageManager: options.packageManager, projectDir: options.projectDir, args: ["auth", "workspace", "list"], }); - if (available.items.length <= 1) return activeWorkspace; + if (available.items.length <= 1) return { ok: true, workspace: activeWorkspace }; options.beforePrompt?.(); const selectedWorkspaceId = await select({ @@ -309,16 +344,21 @@ async function selectDeploymentWorkspace(options: { }); if (isCancel(selectedWorkspaceId)) { cancel("Operation cancelled.", { output: options.output }); - return; + return { ok: false, cancelled: true }; } options.afterPrompt?.(); - if (selectedWorkspaceId === activeWorkspace.id) return activeWorkspace; + if (selectedWorkspaceId === activeWorkspace.id) { + return { ok: true, workspace: activeWorkspace }; + } - return useWorkspace({ - packageManager: options.packageManager, - projectDir: options.projectDir, - workspace: selectedWorkspaceId, - }); + return { + ok: true, + workspace: await useWorkspace({ + packageManager: options.packageManager, + projectDir: options.projectDir, + workspace: selectedWorkspaceId, + }), + }; } export function parseComposerDeployResult(result: ComposerDeployCommandResult): @@ -379,13 +419,14 @@ export async function deployNewProjectWithComposer(options: { output?: Writable; allowInteractiveLogin?: boolean; json?: boolean; - throwOnError?: boolean; workspace?: string; -}): Promise { +}): Promise { const output = options.output ?? process.stdout; const progress = options.verbose ? undefined : spinner({ output }); let deploymentLog: ReturnType | undefined; let progressRunning = false; + let failureStage: CreateFailureStage = "authenticate"; + let failureReason: CreateFailureReason = "prisma_auth_command_failed"; const showProgress = (message: string) => { if (!progress) return; if (progressRunning) { @@ -413,8 +454,10 @@ export async function deployNewProjectWithComposer(options: { }); showProgress("Checking Prisma workspace..."); + failureStage = "select_workspace"; + failureReason = "workspace_selection_failed"; if (options.verbose) log.step("Checking Prisma workspace.", { output }); - const selectedWorkspace = await selectDeploymentWorkspace({ + const workspaceResult = await selectDeploymentWorkspace({ packageManager: options.packageManager, projectDir: options.projectDir, shouldPrompt: options.shouldPromptForWorkspace, @@ -424,9 +467,12 @@ export async function deployNewProjectWithComposer(options: { afterPrompt: () => showProgress("Selecting Prisma workspace..."), ...(options.workspace ? { workspace: options.workspace } : {}), }); - if (!selectedWorkspace) return; + if (!workspaceResult.ok) return { ok: false, cancelled: true, stage: "select_workspace" }; + const selectedWorkspace = workspaceResult.workspace; showProgress("Checking Prisma project name..."); + failureStage = "check_project_name"; + failureReason = "project_lookup_failed"; if (options.verbose) log.step("Checking Prisma project name.", { output }); await ensureProjectNameAvailable({ appName: options.appName, @@ -436,6 +482,8 @@ export async function deployNewProjectWithComposer(options: { }); showProgress("Building for deployment..."); + failureStage = "build"; + failureReason = "build_failed"; if (options.verbose) log.step("Building for deployment.", { output }); const build = getRunScriptArgs(options.packageManager, "build"); await runSetupCommand({ @@ -448,6 +496,8 @@ export async function deployNewProjectWithComposer(options: { }); clearProgress(); + failureStage = "composer_deploy"; + failureReason = "composer_deploy_failed"; const deployCommand = getPackageExecutionCommand(options.packageManager, [ PRISMA_PLATFORM_CLI_PACKAGE, "deploy", @@ -493,11 +543,14 @@ export async function deployNewProjectWithComposer(options: { if (options.verbose) log.success("Deployed to Prisma.", { output }); const workspace = details?.workspace ?? selectedWorkspace; return { - appName, - ...(deployment?.appUrl ? { appUrl: deployment.appUrl } : {}), - ...(deployment?.serviceId ? { serviceId: deployment.serviceId } : {}), - ...(workspace ? { workspace } : {}), - project: details?.project ?? { name: appName }, + ok: true, + deployment: { + appName, + ...(deployment?.appUrl ? { appUrl: deployment.appUrl } : {}), + ...(deployment?.serviceId ? { serviceId: deployment.serviceId } : {}), + ...(workspace ? { workspace } : {}), + project: details?.project ?? { name: appName }, + }, }; } catch (error) { if (deploymentLog) { @@ -508,7 +561,11 @@ export async function deployNewProjectWithComposer(options: { } progressRunning = false; log.error(`Deploy failed: ${getErrorMessage(error)}`, { output }); - if (options.throwOnError) throw error; - return; + return { + ok: false, + stage: failureStage, + reason: getCreateFailureReason(error, failureReason), + error, + }; } } diff --git a/src/tasks/setup-prisma.ts b/src/tasks/setup-prisma.ts index 6bd1f24..4ace2e3 100644 --- a/src/tasks/setup-prisma.ts +++ b/src/tasks/setup-prisma.ts @@ -4,6 +4,13 @@ import path from "node:path"; import type { Writable } from "node:stream"; import { PRISMA_DENO_CLI_PACKAGE, PRISMA_PLATFORM_CLI_PACKAGE } from "../constants/dependencies"; +import { + ClassifiedCreateError, + CreateCancellationError, + getCreateFailureReason, + type CreateFailureReason, + type CreateFailureStage, +} from "../create-outcome"; import type { CreateNextStep } from "../result"; import { scaffoldCreateSharedTemplates } from "../templates/render-create-template"; import { @@ -65,9 +72,22 @@ export type PrismaSetupExecutionResult = gitInitialization?: GitInitializationResult; warnings: string[]; } - | { ok: false; error?: unknown; errorReported?: boolean }; - -async function promptForDatabaseProvider(output: Writable): Promise { + | { + ok: false; + cancelled: true; + stage: "select_workspace"; + errorReported?: boolean; + } + | { + ok: false; + cancelled?: false; + stage: CreateFailureStage; + reason: CreateFailureReason; + error?: unknown; + errorReported?: boolean; + }; + +async function promptForDatabaseProvider(output: Writable): Promise { const databaseProvider = await select({ message: "Select your database", initialValue: DEFAULT_DATABASE_PROVIDER, @@ -79,12 +99,12 @@ async function promptForDatabaseProvider(output: Writable): Promise { +async function promptForAuthoringStyle(output: Writable): Promise { const authoring = await select({ message: "Choose contract authoring style", initialValue: DEFAULT_AUTHORING, @@ -96,7 +116,7 @@ async function promptForAuthoringStyle(output: Writable): Promise { +): Promise { const packageManager = await select({ message: "Choose package manager", initialValue: detected, @@ -128,12 +148,12 @@ async function promptForPackageManager( }); if (isCancel(packageManager)) { cancel("Operation cancelled.", { output }); - return; + throw new CreateCancellationError("package_manager"); } return PackageManagerSchema.parse(packageManager); } -async function promptForDeployment(output: Writable): Promise { +async function promptForDeployment(output: Writable): Promise { const shouldDeploy = await confirm({ message: "Deploy to Prisma now?", initialValue: true, @@ -141,7 +161,7 @@ async function promptForDeployment(output: Writable): Promise { +): Promise { const projectDir = path.resolve(options.projectDir ?? process.cwd()); const { json, output, useDefaults } = resolveExecutionSettings(input); const databaseProvider = input.provider ?? (useDefaults ? DEFAULT_DATABASE_PROVIDER : await promptForDatabaseProvider(output)); - if (!databaseProvider) return; - const authoring = input.authoring ?? (useDefaults ? DEFAULT_AUTHORING : await promptForAuthoringStyle(output)); - if (!authoring) return; - const detectedPackageManager = await detectPackageManager(projectDir); const packageManager = input.packageManager ?? (useDefaults ? detectedPackageManager : await promptForPackageManager(detectedPackageManager, output)); - if (!packageManager) return; - if (packageManager === "deno" && databaseProvider !== "postgres") { - throw new Error("Deno support currently requires PostgreSQL."); + throw new ClassifiedCreateError( + "unsupported_configuration", + "Deno support currently requires PostgreSQL.", + ); } if (packageManager === "deno" && options.template && options.template !== "minimal") { - throw new Error("Deno support currently requires the minimal template."); + throw new ClassifiedCreateError( + "unsupported_configuration", + "Deno support currently requires the minimal template.", + ); } if (packageManager === "deno" && input.deploy === true) { - throw new Error("Prisma Compute does not support Deno deployments yet. Use --no-deploy."); + throw new ClassifiedCreateError( + "unsupported_configuration", + "Prisma Compute does not support Deno deployments yet. Use --no-deploy.", + ); } const shouldDeploy = packageManager === "deno" ? false : (input.deploy ?? (json ? true : useDefaults ? false : await promptForDeployment(output))); - if (shouldDeploy === undefined) return; - return { projectDir, verbose: input.verbose === true, @@ -433,12 +454,16 @@ export async function executePrismaSetupContext( : (options.progressSpinner ?? spinner({ output: context.output })); const ownsProgress = progress !== undefined && !options.progressSpinner; let gitInitialization: GitInitializationResult | undefined; + let setupStage: CreateFailureStage = "initialize_prisma"; + let setupReason: CreateFailureReason = "prisma_init_failed"; if (ownsProgress) progress.start("Creating Prisma 8 project..."); try { progress?.message("Preparing Prisma 8 project files..."); await runPrismaInit(context, projectDir); + setupStage = "configure_project"; + setupReason = "project_configuration_failed"; await scaffoldCreateSharedTemplates({ projectDir, projectName, @@ -463,24 +488,34 @@ export async function executePrismaSetupContext( progress?.message( `Installing dependencies with ${getInstallCommand(context.packageManager)}...`, ); + setupStage = "install_dependencies"; + setupReason = "dependency_install_failed"; await installProjectDependencies(context.packageManager, projectDir, { verbose: context.verbose, json: context.json, }); progress?.message("Installing Prisma agent skills..."); + setupStage = "initialize_agent_skills"; + setupReason = "agent_skills_init_failed"; await initializeAgentSkills(context, projectDir); progress?.message("Generating Prisma 8 contract artifacts..."); + setupStage = "emit_contract"; + setupReason = "contract_emit_failed"; await emitContract(context, projectDir); if (context.databaseProvider === "postgres") { progress?.message("Authoring the baseline migration..."); + setupStage = "plan_migration"; + setupReason = "migration_plan_failed"; await planBaselineMigration(context, projectDir); } if (options.initializeGit) { progress?.message("Initializing Git repository..."); + setupStage = "initialize_git"; + setupReason = "git_initialization_failed"; gitInitialization = await initializeGitRepository(projectDir); } progress?.stop("Prisma 8 project ready."); @@ -494,12 +529,18 @@ export async function executePrismaSetupContext( } catch (error) { progress?.error("Could not create Prisma 8 project."); cancel(getErrorMessage(error), { output: context.output }); - return { ok: false, error, errorReported: true }; + return { + ok: false, + stage: setupStage, + reason: getCreateFailureReason(error, setupReason), + error, + errorReported: true, + }; } let deployment: ComposerDeployResult | undefined; if (context.shouldDeploy) { - deployment = await deployNewProjectWithComposer({ + const deploymentResult = await deployNewProjectWithComposer({ appName: projectName, packageManager: context.packageManager, projectDir, @@ -508,10 +549,26 @@ export async function executePrismaSetupContext( output: context.output, allowInteractiveLogin: !context.json, json: context.json, - throwOnError: context.json, ...(context.workspace ? { workspace: context.workspace } : {}), }); - if (!deployment) return { ok: false, errorReported: true }; + if (!deploymentResult.ok) { + if (deploymentResult.cancelled) { + return { + ok: false, + cancelled: true, + stage: deploymentResult.stage, + errorReported: true, + }; + } + return { + ok: false, + stage: deploymentResult.stage, + reason: deploymentResult.reason, + error: deploymentResult.error, + errorReported: true, + }; + } + deployment = deploymentResult.deployment; } const nextSteps = buildNextSteps(context, options); diff --git a/src/telemetry/create.ts b/src/telemetry/create.ts index ce666ab..f8d8c9d 100644 --- a/src/telemetry/create.ts +++ b/src/telemetry/create.ts @@ -1,17 +1,18 @@ import type { CreatePromptContext } from "../commands/create"; +import type { + CreateCancellationStage, + CreateFailureReason, + CreateFailureStage, +} from "../create-outcome"; import type { CreateCommandInput } from "../types"; import { trackCliTelemetry } from "./client"; export const CREATE_PRISMA_NEXT_COMPLETED_EVENT = "cli:create_prisma_next_command_completed"; export const CREATE_PRISMA_NEXT_FAILED_EVENT = "cli:create_prisma_next_command_failed"; +export const CREATE_PRISMA_NEXT_CANCELLED_EVENT = "cli:create_prisma_next_command_cancelled"; -export type CreateTelemetryFailureStage = - | "validate_input" - | "collect_context" - | "scaffold_template" - | "prisma_setup" - | "unknown"; +export type CreateTelemetryFailureStage = CreateFailureStage; function getTargetDirectoryState(context: CreatePromptContext): string { if (!context.targetPathState.exists) { @@ -30,6 +31,7 @@ function getBaseCreateProperties( context?: CreatePromptContext, ): Record { return { + "telemetry-schema-version": 2, command: "create", "uses-defaults": input.yes === true || input.json === true, json: input.json === true, @@ -83,12 +85,27 @@ export async function trackCreateFailed(params: { durationMs: number; error?: unknown; stage: CreateTelemetryFailureStage; + reason: CreateFailureReason; }): Promise { await trackCliTelemetry(CREATE_PRISMA_NEXT_FAILED_EVENT, { ...getBaseCreateProperties(params.input, params.context), "duration-ms": params.durationMs, "failure-stage": params.stage, + "failure-reason": params.reason, "error-name": getErrorName(params.error), "error-code": getErrorCode(params.error), }); } + +export async function trackCreateCancelled(params: { + input: CreateCommandInput; + context?: CreatePromptContext; + durationMs: number; + stage: CreateCancellationStage; +}): Promise { + await trackCliTelemetry(CREATE_PRISMA_NEXT_CANCELLED_EVENT, { + ...getBaseCreateProperties(params.input, params.context), + "duration-ms": params.durationMs, + "cancellation-stage": params.stage, + }); +} diff --git a/src/telemetry/index.ts b/src/telemetry/index.ts index 847483b..ca6bb9d 100644 --- a/src/telemetry/index.ts +++ b/src/telemetry/index.ts @@ -1,5 +1,7 @@ export { + trackCreateCancelled, trackCreateCompleted, trackCreateFailed, type CreateTelemetryFailureStage, } from "./create"; +export type { CreateCancellationStage, CreateFailureReason } from "../create-outcome"; diff --git a/tests/deploy-with-composer.test.ts b/tests/deploy-with-composer.test.ts index 00607ae..66f5130 100644 --- a/tests/deploy-with-composer.test.ts +++ b/tests/deploy-with-composer.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { PassThrough } from "node:stream"; import { + deployNewProjectWithComposer, findProjectNameCollisions, getConsoleProjectUrl, parseComposerDeployResult, @@ -133,3 +135,36 @@ describe("parseComposerDeployResult", () => { expect(parseComposerDeployResult({ summary: null })).toBeUndefined(); }); }); + +describe("deployNewProjectWithComposer", () => { + test("returns the authentication failure instead of swallowing it", async () => { + const originalPath = process.env.PATH; + process.env.PATH = ""; + try { + const result = await deployNewProjectWithComposer({ + appName: "test-app", + packageManager: "npm", + projectDir: process.cwd(), + shouldPromptForWorkspace: false, + verbose: false, + output: new PassThrough(), + allowInteractiveLogin: false, + json: true, + }); + + expect(result).toMatchObject({ + ok: false, + stage: "authenticate", + reason: "prisma_auth_command_failed", + }); + if (result.ok || result.cancelled) throw new Error("Expected a classified failure."); + expect(result.error).toBeDefined(); + } finally { + if (originalPath === undefined) { + delete process.env.PATH; + } else { + process.env.PATH = originalPath; + } + } + }); +}); diff --git a/tests/e2e/create-prisma.e2e.test.ts b/tests/e2e/create-prisma.e2e.test.ts index 8c3e635..5e56291 100644 --- a/tests/e2e/create-prisma.e2e.test.ts +++ b/tests/e2e/create-prisma.e2e.test.ts @@ -205,7 +205,7 @@ describe("create-prisma e2e", () => { expect(JSON.parse(stdout)).toMatchObject({ schemaVersion: 1, ok: false, - error: { stage: "prisma_setup" }, + error: { stage: "initialize_prisma" }, }); expect(stderr).toBe(""); expect(await pathExists(path.join(rootDir, "failed-app", "package.json"))).toBe(true); diff --git a/tests/telemetry.test.ts b/tests/telemetry.test.ts index 7b9d977..c745986 100644 --- a/tests/telemetry.test.ts +++ b/tests/telemetry.test.ts @@ -8,8 +8,10 @@ const trackCliTelemetry = mock(async () => {}); mock.module("../src/telemetry/client", () => ({ trackCliTelemetry })); const { + CREATE_PRISMA_NEXT_CANCELLED_EVENT, CREATE_PRISMA_NEXT_COMPLETED_EVENT, CREATE_PRISMA_NEXT_FAILED_EVENT, + trackCreateCancelled, trackCreateCompleted, trackCreateFailed, } = await import("../src/telemetry/create"); @@ -41,6 +43,7 @@ describe("create telemetry", () => { CREATE_PRISMA_NEXT_COMPLETED_EVENT, expect.objectContaining({ command: "create", + "telemetry-schema-version": 2, template: "hono", "database-provider": "postgres", "should-deploy": true, @@ -49,20 +52,45 @@ describe("create telemetry", () => { ); }); - test("tracks setup failures", async () => { + test("tracks a normalized setup failure without its raw message", async () => { await trackCreateFailed({ input: createInput, context: createContext, durationMs: 456, - error: Object.assign(new Error("boom"), { code: "ERR_TEST" }), - stage: "prisma_setup", + error: Object.assign(new Error("DATABASE_URL=secret"), { code: "ERR_TEST" }), + stage: "plan_migration", + reason: "migration_plan_failed", }); - expect(trackCliTelemetry).toHaveBeenCalledWith( - CREATE_PRISMA_NEXT_FAILED_EVENT, + const [event, properties] = trackCliTelemetry.mock.calls[0] as [ + string, + Record, + ]; + expect(event).toBe(CREATE_PRISMA_NEXT_FAILED_EVENT); + expect(properties).toEqual( expect.objectContaining({ "duration-ms": 456, "error-code": "ERR_TEST", - "failure-stage": "prisma_setup", + "failure-stage": "plan_migration", + "failure-reason": "migration_plan_failed", + }), + ); + expect(properties).not.toHaveProperty("error-message"); + expect(JSON.stringify(properties)).not.toContain("secret"); + }); + + test("tracks prompt cancellation as a separate outcome", async () => { + await trackCreateCancelled({ + input: createInput, + context: createContext, + durationMs: 789, + stage: "select_workspace", + }); + expect(trackCliTelemetry).toHaveBeenCalledWith( + CREATE_PRISMA_NEXT_CANCELLED_EVENT, + expect.objectContaining({ + "duration-ms": 789, + "cancellation-stage": "select_workspace", + "should-deploy": true, }), ); }); From 7b4d9ba39490261542cf4cb4d22b333afb21a815 Mon Sep 17 00:00:00 2001 From: Aman Varshney Date: Thu, 27 Aug 2026 20:28:30 +0530 Subject: [PATCH 2/2] fix: preserve cancellation stages in results --- src/commands/create.ts | 2 +- src/result.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/create.ts b/src/commands/create.ts index 112f3b9..003128c 100644 --- a/src/commands/create.ts +++ b/src/commands/create.ts @@ -314,7 +314,7 @@ export async function runCreateCommand( stage: error.stage, }); return createCommandFailureResult( - failureStage, + error.stage, error.message, context ? getProjectResult(context) : undefined, ); diff --git a/src/result.ts b/src/result.ts index 9f4f6f1..c9ce32e 100644 --- a/src/result.ts +++ b/src/result.ts @@ -1,4 +1,4 @@ -import type { CreateFailureStage } from "./create-outcome"; +import type { CreateCancellationStage, CreateFailureStage } from "./create-outcome"; import type { ComposerDeployResult } from "./tasks/deploy-with-composer"; import type { AuthoringStyle, CreateTemplate, DatabaseProvider, PackageManager } from "./types"; @@ -18,7 +18,10 @@ export type CreateProjectResult = { packageManager: PackageManager; }; -export type CreateCommandFailureStage = CreateFailureStage | "parse_arguments"; +export type CreateCommandFailureStage = + | CreateFailureStage + | CreateCancellationStage + | "parse_arguments"; export type CreateCommandSuccessResult = { schemaVersion: typeof CREATE_PRISMA_RESULT_SCHEMA_VERSION;