mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Plan | null> {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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" }),
|
||||
|
||||
Reference in New Issue
Block a user