diff --git a/.changeset/deploy-plan-unsupported-features.md b/.changeset/deploy-plan-unsupported-features.md new file mode 100644 index 000000000..7c949a3b5 --- /dev/null +++ b/.changeset/deploy-plan-unsupported-features.md @@ -0,0 +1,5 @@ +--- +"clerk": patch +--- + +Fix `clerk deploy` not listing which features your plan doesn't cover when the Platform API rejects the deploy for an insufficient subscription plan. diff --git a/packages/cli-core/src/commands/deploy/errors.test.ts b/packages/cli-core/src/commands/deploy/errors.test.ts new file mode 100644 index 000000000..8a2268fb7 --- /dev/null +++ b/packages/cli-core/src/commands/deploy/errors.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, test } from "bun:test"; +import { mapDeployError } from "./errors.ts"; +import { CliError, ERROR_CODE, PlapiError } from "../../lib/errors.ts"; + +const planError = (meta?: Record) => + new PlapiError( + 402, + JSON.stringify({ + errors: [ + { + code: "unsupported_subscription_plan_features", + message: "unsupported plan features", + ...(meta ? { meta } : {}), + }, + ], + }), + "https://x", + ); + +async function rejectionOf(promise: Promise): Promise { + try { + await promise; + } catch (error) { + return error; + } + throw new Error("expected promise to reject"); +} + +describe("mapDeployError", () => { + test("lists unsupported features from meta.unsupported_features on 402", async () => { + const error = await rejectionOf( + mapDeployError( + Promise.reject( + planError({ unsupported_features: ["app:allowlist", "app:remove_branding"] }), + ), + ), + ); + + expect(error).toBeInstanceOf(CliError); + const cliError = error as CliError; + expect(cliError.code).toBe(ERROR_CODE.PLAN_INSUFFICIENT); + expect(cliError.docsUrl).toBe("https://clerk.com/pricing"); + expect(cliError.message).toContain("• app:allowlist"); + expect(cliError.message).toContain("• app:remove_branding"); + expect(cliError.message).not.toContain("doesn't cover all the features"); + }); + + test("falls back to the generic plan message when unsupported_features is empty", async () => { + const error = await rejectionOf( + mapDeployError(Promise.reject(planError({ unsupported_features: [] }))), + ); + + expect(error).toBeInstanceOf(CliError); + const cliError = error as CliError; + expect(cliError.code).toBe(ERROR_CODE.PLAN_INSUFFICIENT); + expect(cliError.message).toContain("doesn't cover all the features"); + expect(cliError.message).not.toContain("•"); + }); +}); diff --git a/packages/cli-core/src/commands/deploy/errors.ts b/packages/cli-core/src/commands/deploy/errors.ts index ae21ef01f..0c96df9a4 100644 --- a/packages/cli-core/src/commands/deploy/errors.ts +++ b/packages/cli-core/src/commands/deploy/errors.ts @@ -120,7 +120,7 @@ function planInsufficientMessage(error: PlapiError): string { function readFeatures(meta: Record | null): string[] { if (!meta) return []; - const features = meta.features; + const features = meta.unsupported_features; if (!Array.isArray(features)) return []; return features.filter((f): f is string => typeof f === "string"); }