mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 23:00:03 +00:00
refactor(cli): centralize process lifecycle
This commit is contained in:
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { readStore, writeStore } from "../../auth/store.js";
|
||||
import { CliRuntimeError } from "../../utils/commandResult.js";
|
||||
|
||||
// Mock only AuthClient — keep the real store/resolver so the test
|
||||
// exercises the actual on-disk rollback / persistence behavior.
|
||||
@@ -58,10 +59,6 @@ describe("auth login --api-key rollback", () => {
|
||||
verifyState.reject = false;
|
||||
verifyState.user = { email: "alice@example.com" };
|
||||
for (const fn of Object.values(telemetry)) fn.mockClear();
|
||||
// process.exit throws so we can assert the post-rollback state.
|
||||
vi.spyOn(process, "exit").mockImplementation(((code?: string | number | null) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as never);
|
||||
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
});
|
||||
@@ -86,7 +83,7 @@ describe("auth login --api-key rollback", () => {
|
||||
|
||||
it("removes the rejected key on a failed FIRST login (no prior credential)", async () => {
|
||||
verifyState.reject = true;
|
||||
await expect(runLogin("hg_badkey123")).rejects.toThrow(/process\.exit:1/);
|
||||
await expect(runLogin("hg_badkey123")).rejects.toThrow(CliRuntimeError);
|
||||
|
||||
// The store must NOT retain the rejected key — otherwise the next
|
||||
// command would silently resolve a known-bad credential.
|
||||
@@ -97,7 +94,7 @@ describe("auth login --api-key rollback", () => {
|
||||
it("restores the previous credential on a failed re-login", async () => {
|
||||
await writeStore({ api_key: "hg_previous_good" });
|
||||
verifyState.reject = true;
|
||||
await expect(runLogin("hg_newbadkey99")).rejects.toThrow(/process\.exit:1/);
|
||||
await expect(runLogin("hg_newbadkey99")).rejects.toThrow(CliRuntimeError);
|
||||
|
||||
const { credentials } = await readStore();
|
||||
expect(credentials.api_key).toBe("hg_previous_good");
|
||||
@@ -144,7 +141,7 @@ describe("auth login --api-key rollback", () => {
|
||||
it("rollback on a rejected key restores the previous user block too", async () => {
|
||||
await writeStore({ api_key: "hg_prev", user: { email: "prev@example.com" } });
|
||||
verifyState.reject = true;
|
||||
await expect(runLogin("hg_badnewkey")).rejects.toThrow(/process\.exit:1/);
|
||||
await expect(runLogin("hg_badnewkey")).rejects.toThrow(CliRuntimeError);
|
||||
|
||||
const { credentials } = await readStore();
|
||||
expect(credentials.api_key).toBe("hg_prev");
|
||||
@@ -163,7 +160,7 @@ describe("auth login --api-key rollback", () => {
|
||||
{ mode: 0o600 },
|
||||
);
|
||||
verifyState.reject = true;
|
||||
await expect(runLogin("hg_badnewkey")).rejects.toThrow(/process\.exit:1/);
|
||||
await expect(runLogin("hg_badnewkey")).rejects.toThrow(CliRuntimeError);
|
||||
|
||||
const onDisk = JSON.parse(await fs.readFile(join(dir, "credentials"), "utf8"));
|
||||
expect(onDisk.api_key).toBeUndefined();
|
||||
@@ -193,7 +190,7 @@ describe("auth login --api-key rollback", () => {
|
||||
|
||||
it("records a rejected key as failed and never identifies", async () => {
|
||||
verifyState.reject = true;
|
||||
await expect(runLogin("hg_badkey123")).rejects.toThrow(/process\.exit:1/);
|
||||
await expect(runLogin("hg_badkey123")).rejects.toThrow(CliRuntimeError);
|
||||
expect(telemetry.identifyUser).not.toHaveBeenCalled();
|
||||
expect(telemetry.trackAuthLoginFailed).toHaveBeenCalledWith("api_key", "rejected");
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { failCommand } from "../../utils/commandResult.js";
|
||||
/**
|
||||
* `hyperframes auth login` — sign in to HeyGen.
|
||||
*
|
||||
@@ -91,7 +92,7 @@ async function runOAuthLogin(): Promise<void> {
|
||||
// (IdP misconfig, network) instead of lumping everything as flow_error.
|
||||
trackAuthLoginFailed("oauth", /timed out/i.test(message) ? "flow_timeout" : "flow_error");
|
||||
console.error(c.error(`Sign-in failed: ${message}`));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
|
||||
await reportIdentity();
|
||||
@@ -105,7 +106,7 @@ async function reportIdentity(): Promise<void> {
|
||||
if (!credential) {
|
||||
trackAuthLoginFailed("oauth", "no_credential");
|
||||
console.error(c.warn("Sign-in completed but no credential was persisted."));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
// Wire the refresh hook here too — a freshly-minted token shouldn't
|
||||
// need it, but a fast IdP-side rotation (or a misconfigured short
|
||||
@@ -211,12 +212,12 @@ async function runApiKeyLogin(inlineKey: string): Promise<void> {
|
||||
} catch (err) {
|
||||
trackAuthLoginFailed("api_key", "aborted");
|
||||
console.error(c.error((err as Error).message || "Sign-in aborted."));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
if (!key) {
|
||||
trackAuthLoginFailed("api_key", "invalid_input");
|
||||
console.error(c.error("No API key provided."));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
if (!isHeaderSafe(key)) {
|
||||
// CR/LF in the value would smuggle headers when the key is sent
|
||||
@@ -224,12 +225,12 @@ async function runApiKeyLogin(inlineKey: string): Promise<void> {
|
||||
// header-injection has to be caught here.
|
||||
trackAuthLoginFailed("api_key", "invalid_input");
|
||||
console.error(c.error("API key must not contain newline or control characters."));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
if (key.length < MIN_KEY_LENGTH) {
|
||||
trackAuthLoginFailed("api_key", "invalid_input");
|
||||
console.error(c.error(`API key looks too short (got ${key.length} chars).`));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
|
||||
const previous = await snapshotStore();
|
||||
@@ -240,7 +241,7 @@ async function runApiKeyLogin(inlineKey: string): Promise<void> {
|
||||
if (!user) {
|
||||
trackAuthLoginFailed("api_key", "rejected");
|
||||
await rollback(previous);
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
const id = identityKey(user);
|
||||
if (id) identifyUser(id);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { failCommand } from "../../utils/commandResult.js";
|
||||
/**
|
||||
* `hyperframes auth logout` — remove the credential file. With
|
||||
* `--keep-api-key`, only the OAuth block is cleared (no-op for
|
||||
@@ -38,7 +39,7 @@ export default defineCommand({
|
||||
|
||||
if (!(await ensureConfirmed(Boolean(args.yes), keepApiKey))) {
|
||||
console.log("Aborted.");
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
|
||||
// Best-effort revoke before we wipe local state. RFC 7009 says
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { failCommand } from "../../utils/commandResult.js";
|
||||
/**
|
||||
* `hyperframes auth refresh` — force-refresh the OAuth access_token
|
||||
* using the stored refresh_token.
|
||||
@@ -26,7 +27,7 @@ export default defineCommand({
|
||||
const { credentials, source } = await readStore();
|
||||
if (source === "absent" || !credentials.oauth?.refresh_token) {
|
||||
console.error(c.warn("No OAuth refresh token to use. Run `hyperframes auth login` first."));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -40,7 +41,7 @@ export default defineCommand({
|
||||
if (isAuthError(err) && err.code === "REFRESH_FAILED") {
|
||||
console.error(c.error(err.message));
|
||||
if (err.hint) console.error(c.dim(err.hint));
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
// fallow-ignore-file code-duplication
|
||||
import { promises as fs } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { writeStore } from "../../auth/store.js";
|
||||
import { consumeCommandResult } from "../../utils/commandResult.js";
|
||||
|
||||
// Mock only AuthClient so the live /v3/users/me probe is controllable;
|
||||
// keep the real store/resolver/user helpers so the test exercises the
|
||||
@@ -48,9 +50,7 @@ describe("auth status — persisted user block surface", () => {
|
||||
probeState.apiReject = false;
|
||||
probeState.user = { email: "live@example.com" };
|
||||
stdout = [];
|
||||
vi.spyOn(process, "exit").mockImplementation(((code?: string | number | null) => {
|
||||
throw new Error(`process.exit:${code ?? 0}`);
|
||||
}) as never);
|
||||
consumeCommandResult();
|
||||
vi.spyOn(console, "log").mockImplementation((...args: unknown[]) => {
|
||||
stdout.push(args.join(" "));
|
||||
});
|
||||
@@ -58,6 +58,7 @@ describe("auth status — persisted user block surface", () => {
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
consumeCommandResult();
|
||||
vi.restoreAllMocks();
|
||||
for (const k of ENV_KEYS) {
|
||||
const v = saved[k];
|
||||
@@ -69,16 +70,10 @@ describe("auth status — persisted user block surface", () => {
|
||||
|
||||
async function runStatus(asJson: boolean): Promise<number> {
|
||||
const cmd = (await import("./status.js")).default;
|
||||
try {
|
||||
await (cmd.run as (ctx: { args: Record<string, unknown> }) => Promise<void>)({
|
||||
args: { json: asJson },
|
||||
});
|
||||
return 0;
|
||||
} catch (err) {
|
||||
const m = /process\.exit:(\d+)/.exec((err as Error).message);
|
||||
if (m) return Number(m[1]);
|
||||
throw err;
|
||||
}
|
||||
await (cmd.run as (ctx: { args: Record<string, unknown> }) => Promise<void>)({
|
||||
args: { json: asJson },
|
||||
});
|
||||
return consumeCommandResult().exitCode;
|
||||
}
|
||||
|
||||
function lastJson(): Record<string, unknown> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { failCommand, setCommandExitCode } from "../../utils/commandResult.js";
|
||||
/**
|
||||
* `hyperframes auth status` — print the active credential's source,
|
||||
* type, and identity (verified against `GET /v3/users/me`).
|
||||
@@ -81,7 +82,7 @@ export default defineCommand({
|
||||
const status = await verify(credential);
|
||||
if (asJson) printJsonStatus(status);
|
||||
else printHumanStatus(status);
|
||||
process.exit(status.apiError ? 1 : 0);
|
||||
setCommandExitCode(status.apiError ? 1 : 0);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -123,7 +124,7 @@ function handleUnconfigured(asJson: boolean): never {
|
||||
? JSON.stringify(buildUnconfiguredJson(ctx, engines))
|
||||
: buildUnconfiguredLines(ctx, engines).join("\n");
|
||||
console.log(output);
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
@@ -135,7 +136,7 @@ function handleResolveError(err: unknown, asJson: boolean): never {
|
||||
console.error(c.error(err.message));
|
||||
if (err.hint) console.error(c.dim(err.hint));
|
||||
}
|
||||
process.exit(1);
|
||||
failCommand();
|
||||
}
|
||||
|
||||
async function verify(credential: ResolvedCredential): Promise<VerifiedStatus> {
|
||||
|
||||
Reference in New Issue
Block a user