From 55e08df8cb08e34ae696607cb56f9708052fa8bb Mon Sep 17 00:00:00 2001 From: James Date: Fri, 30 Oct 2026 08:38:32 +0000 Subject: [PATCH] fix(cli): plans link uses from=cli and follows HYPERFRAMES_ENV; no balance notice beside the plan - pageUrl sets from=cli, the parameter the pricing page reads to send a CLI buyer back to the terminal. - The plans and credits pages are on dev.hyperframes.dev with HYPERFRAMES_ENV=dev, hyperframes.studio otherwise. - auth status --json omits the 'HyperFrames balance is unavailable' notice when the plan was read. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/packages/cli.mdx | 4 ++- packages/cli/src/commands/auth.ts | 2 +- .../cli/src/commands/auth/status-user.test.ts | 26 +++++++++++++++++++ packages/cli/src/commands/auth/status.ts | 11 +++++--- packages/cli/src/heygen/plan.test.ts | 21 +++++++++++++-- packages/cli/src/heygen/plan.ts | 12 +++++---- packages/cli/src/heygen/v3.test.ts | 2 +- 7 files changed, 64 insertions(+), 14 deletions(-) diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx index 20c1d62ee6..cfab8acd2f 100644 --- a/docs/packages/cli.mdx +++ b/docs/packages/cli.mdx @@ -2262,7 +2262,9 @@ allowance or estimate what the next video will cost. ### `billing` Opens the HyperFrames plans or credits page in the browser, then waits until the -account shows the change, so an agent's run carries on by itself. +account shows the change, so an agent's run carries on by itself. The page is on +the web app for `HYPERFRAMES_ENV`: hyperframes.studio, or dev.hyperframes.dev +with `HYPERFRAMES_ENV=dev`. ```bash npx hyperframes billing subscribe diff --git a/packages/cli/src/commands/auth.ts b/packages/cli/src/commands/auth.ts index 0a6e7cb037..272553a278 100644 --- a/packages/cli/src/commands/auth.ts +++ b/packages/cli/src/commands/auth.ts @@ -42,7 +42,7 @@ ${c.bold("ENV VARS:")} ${c.accent("HYPERFRAMES_API_KEY")} Alias for HEYGEN_API_KEY. ${c.accent("HEYGEN_ACCESS_TOKEN")} OAuth token from a host app; used after the API keys, never refreshed or saved. ${c.accent("HEYGEN_API_URL")} Override the API base URL (default https://api.heygen.com). - ${c.accent("HYPERFRAMES_ENV")} prod (default) or dev; selects OAuth/API hosts together. + ${c.accent("HYPERFRAMES_ENV")} prod (default) or dev; selects OAuth, API and plans-page hosts together. ${c.accent("HYPERFRAMES_CONFIG_DIR")} Override the credentials directory (default ~/.hyperframes). ${c.accent("HYPERFRAMES_OAUTH_CLIENT_ID")} Override the OAuth client_id (for dev/test). ${c.accent("HYPERFRAMES_OAUTH_DEVICE_URL")} Override the RFC 8628 device endpoint (for dev/test). diff --git a/packages/cli/src/commands/auth/status-user.test.ts b/packages/cli/src/commands/auth/status-user.test.ts index 7179fc52c4..1fb6d3ddb0 100644 --- a/packages/cli/src/commands/auth/status-user.test.ts +++ b/packages/cli/src/commands/auth/status-user.test.ts @@ -51,6 +51,7 @@ describe("auth status — persisted user block surface", () => { afterEach(async () => { consumeCommandResult(); vi.restoreAllMocks(); + vi.unstubAllGlobals(); await envFixture.restore(); }); @@ -131,6 +132,31 @@ describe("auth status — persisted user block surface", () => { } }); + it("gives no 'balance unavailable' notice when the HyperFrames plan is shown", async () => { + vi.stubGlobal( + "fetch", + async () => + new Response( + JSON.stringify({ + data: { + subscription: { plan_name: "pro", status: "active" }, + usage: { remaining_units: 25000 }, + }, + }), + ), + ); + await configureBillingCredential("hyperframes"); + expect(await runStatus(true)).toBe(0); + const payload = lastJson(); + expect(payload["hyperframes"]).toEqual({ plan: "pro", credits: 25000 }); + expect(payload).not.toHaveProperty("billing_notice"); + stdout = []; + expect(await runStatus(false)).toBe(0); + const text = stdout.join("\n"); + expect(text).toContain("pro plan, 25,000 credits"); + expect(text).not.toContain("balance unavailable"); + }); + it("surfaces the persisted user block (with resolved display_name) for a file credential", async () => { await writeStore({ api_key: "hg_x", diff --git a/packages/cli/src/commands/auth/status.ts b/packages/cli/src/commands/auth/status.ts index fbffa60fc7..0abfdc5b92 100644 --- a/packages/cli/src/commands/auth/status.ts +++ b/packages/cli/src/commands/auth/status.ts @@ -215,10 +215,13 @@ function printJsonStatus(s: VerifiedStatus): void { }; if (s.credential.type === "oauth") { payload["hyperframes_balance"] = null; - payload["billing_notice"] = - product === "hyperframes" - ? "HyperFrames balance is unavailable here; user billing fields describe the HeyGen account." - : "OAuth billing product is not verified here; user billing fields describe the HeyGen account."; + // With the plan read, `hyperframes` above holds the plan and credits, so there is no notice to give. + if (product !== "hyperframes") + payload["billing_notice"] = + "OAuth billing product is not verified here; user billing fields describe the HeyGen account."; + else if (!s.plan) + payload["billing_notice"] = + "HyperFrames balance is unavailable here; user billing fields describe the HeyGen account."; payload["expires_at"] = s.credential.expires_at?.toISOString() ?? null; payload["refreshable"] = s.credential.refreshable; payload["scope"] = s.credential.scope ?? null; diff --git a/packages/cli/src/heygen/plan.test.ts b/packages/cli/src/heygen/plan.test.ts index 12f72644b9..46d6e1e29b 100644 --- a/packages/cli/src/heygen/plan.test.ts +++ b/packages/cli/src/heygen/plan.test.ts @@ -5,7 +5,7 @@ vi.mock("../cloud/auth.js", () => ({ resolveCloudBaseUrl: () => "https://api.dev.heygen.com", })); -const { readPlan } = await import("./plan.js"); +const { pageUrl, readPlan } = await import("./plan.js"); const asked: string[] = []; const answer = (data: unknown) => @@ -14,7 +14,10 @@ const answer = (data: unknown) => return new Response(JSON.stringify({ code: 100, data })); }); -afterEach(() => vi.unstubAllGlobals()); +afterEach(() => { + vi.unstubAllGlobals(); + vi.unstubAllEnvs(); +}); describe("readPlan", () => { it("reads the plan the way the subscription route sends it", async () => { @@ -33,3 +36,17 @@ describe("readPlan", () => { expect((await readPlan())?.plan).toBe("none"); }); }); + +describe("pageUrl", () => { + it("links the web app for HYPERFRAMES_ENV and tells the page the buyer came from the CLI", () => { + vi.stubEnv("HYPERFRAMES_PLANS_URL", ""); + vi.stubEnv("HYPERFRAMES_CREDITS_URL", ""); + vi.stubEnv("HYPERFRAMES_ENV", "prod"); + expect(pageUrl("plans")).toBe("https://hyperframes.studio/pricing?from=cli"); + vi.stubEnv("HYPERFRAMES_ENV", "dev"); + expect(pageUrl("plans")).toBe("https://dev.hyperframes.dev/pricing?from=cli"); + expect(pageUrl("credits", { needed: 180, tool: "reverse" })).toBe( + "https://dev.hyperframes.dev/account/plan?from=cli&needed_credits=180&tool=reverse", + ); + }); +}); diff --git a/packages/cli/src/heygen/plan.ts b/packages/cli/src/heygen/plan.ts index 0e647be776..c6a7c2c576 100644 --- a/packages/cli/src/heygen/plan.ts +++ b/packages/cli/src/heygen/plan.ts @@ -1,4 +1,5 @@ // The HyperFrames plan and credits the signed-in account holds, and the web pages that change them. +import { oauthEnvironment } from "../auth/config.js"; import { resolveCloudAuthHeaders, resolveCloudBaseUrl } from "../cloud/auth.js"; export interface Plan { @@ -46,17 +47,18 @@ export async function readPlan(): Promise { } } -const plansUrl = () => process.env["HYPERFRAMES_PLANS_URL"] || "https://hyperframes.studio/pricing"; -const creditsUrl = () => - process.env["HYPERFRAMES_CREDITS_URL"] || "https://hyperframes.studio/account/plan"; +const webBase = () => + oauthEnvironment() === "dev" ? "https://dev.hyperframes.dev" : "https://hyperframes.studio"; +const plansUrl = () => process.env["HYPERFRAMES_PLANS_URL"] || `${webBase()}/pricing`; +const creditsUrl = () => process.env["HYPERFRAMES_CREDITS_URL"] || `${webBase()}/account/plan`; -/** A plans or credits page link carrying the fields the web side reads (source, needed credits, the waiting step). */ +/** A plans or credits page link with what the web side reads: `from=cli`, the credits needed, the waiting step. */ export function pageUrl( kind: "plans" | "credits", fields: { needed?: number; tool?: string; base?: string } = {}, ): string { const url = new URL(fields.base ?? (kind === "plans" ? plansUrl() : creditsUrl())); - url.searchParams.set("source", "cli"); + url.searchParams.set("from", "cli"); if (fields.needed) url.searchParams.set("needed_credits", String(fields.needed)); if (fields.tool) url.searchParams.set("tool", fields.tool); return url.toString(); diff --git a/packages/cli/src/heygen/v3.test.ts b/packages/cli/src/heygen/v3.test.ts index 8adfbbd3e2..2fd688578a 100644 --- a/packages/cli/src/heygen/v3.test.ts +++ b/packages/cli/src/heygen/v3.test.ts @@ -47,7 +47,7 @@ describe("runJob", () => { reply(402, { error: { code: "hf_subscription_required", message: "no plan" } }), ); await expect(runJob("/v3/x", {}, "k", (id) => id)).rejects.toMatchObject({ - fix: "Subscribe: https://hyperframes.studio/pricing?source=cli", + fix: "Subscribe: https://hyperframes.studio/pricing?from=cli", }); vi.stubGlobal("fetch", async () => reply(402, { code: "hf_usage_exhausted", url: "https://pay.test/credits" }),