-
Notifications
You must be signed in to change notification settings - Fork 3.2k
feat(media-use): fast heygen CLI onboarding — actionable diagnostics, --doctor, free-usage framing #2065
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat(media-use): fast heygen CLI onboarding — actionable diagnostics, --doctor, free-usage framing #2065
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
620f464
feat(media-use): fast heygen CLI onboarding — actionable diagnostics,…
miguel-heygen bc61dc6
fix(media-use): address #2065 review — classifier blocker, doctor con…
miguel-heygen 64c7609
fix(media-use): require OAuth-capable heygen CLI (v0.3.0), fix auth-s…
miguel-heygen b6836c7
fix(media-use): address #2065 review nits — one root cause on old CLI…
miguel-heygen ad9f8b2
fix(media-use): doctor prints one heygen row per fact
miguel-heygen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,100 @@ | ||
| // v0.3.0 is the first CLI that can use an OAuth session; v0.1.x/0.2.x reject it | ||
| // ("heygen-cli can't use OAuth yet"), and OAuth is what the free-usage path | ||
| // needs — so anything below this can't authenticate for free usage at all. | ||
| export const HEYGEN_MIN_VERSION = "0.3.0"; | ||
| // Free-usage path is OAuth (`--oauth` → subscription/free credits); `--api-key` | ||
| // bills API credits, so the onboarding steers to OAuth. | ||
| export const HEYGEN_INSTALL_COMMAND = | ||
| "curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --oauth"; | ||
| export const HEYGEN_AUTH_COMMAND = "heygen auth login --oauth"; | ||
| export const HEYGEN_UPDATE_COMMAND = "heygen update"; | ||
|
|
||
| export const HEYGEN_NOT_FOUND_MESSAGE = `media-use: heygen CLI not found — it's the free path for bgm/image/voice/avatar-video. Install: ${HEYGEN_INSTALL_COMMAND}`; | ||
| export const HEYGEN_NOT_AUTHENTICATED_MESSAGE = `media-use: heygen CLI not authenticated (free usage) — run: ${HEYGEN_AUTH_COMMAND}`; | ||
| export const HEYGEN_OUTDATED_MESSAGE = `media-use: heygen CLI is outdated — run: ${HEYGEN_UPDATE_COMMAND} (need >= v${HEYGEN_MIN_VERSION})`; | ||
|
|
||
| const ACTIONABLE_MESSAGES = new Set([ | ||
| HEYGEN_NOT_FOUND_MESSAGE, | ||
| HEYGEN_NOT_AUTHENTICATED_MESSAGE, | ||
| HEYGEN_OUTDATED_MESSAGE, | ||
| ]); | ||
|
|
||
| export function classifyHeygenError(err) { | ||
| const detail = heygenErrorDetail(err); | ||
| const text = [err?.stderr, err?.stdout, err?.message, detail] | ||
| .map((value) => textOf(value)) | ||
| .filter(Boolean) | ||
| .join("\n"); | ||
| const lower = text.toLowerCase(); | ||
|
|
||
| // Only ENOENT (spawn of a missing binary) or a shell's "command not found" | ||
| // mean the CLI itself is absent. A bare "not found" would misfire on the CLI's | ||
| // own resource errors (e.g. a stale voiceId → "voice not found"), whose message | ||
| // embeds the `heygen ...` command line — sending users to reinstall a CLI they | ||
| // just ran successfully. Keep this narrow. | ||
| if (err?.code === "ENOENT" || lower.includes("command not found")) { | ||
| return HEYGEN_NOT_FOUND_MESSAGE; | ||
| } | ||
|
|
||
| if ( | ||
| lower.includes("unauthorized") || | ||
| lower.includes("unauthenticated") || | ||
| // \b401\b, not a bare "401" substring — otherwise request IDs (req-401abc), | ||
| // URLs, and retry-after headers would misclassify as an auth failure. | ||
| /\b401\b/.test(lower) || | ||
| lower.includes("not logged in") || | ||
| lower.includes("no api key") || | ||
| lower.includes("missing api key") || | ||
| lower.includes("invalid api key") || | ||
| lower.includes("login required") || | ||
| lower.includes("auth required") || | ||
| lower.includes("authentication required") | ||
| ) { | ||
| return HEYGEN_NOT_AUTHENTICATED_MESSAGE; | ||
| } | ||
|
|
||
| const version = firstSemver(text); | ||
| if (version && versionLessThan(version, HEYGEN_MIN_VERSION)) { | ||
| return HEYGEN_OUTDATED_MESSAGE; | ||
| } | ||
|
|
||
| return detail; | ||
| } | ||
|
|
||
| export function reportHeygenFailure(err, context) { | ||
| const message = classifyHeygenError(err); | ||
| if (ACTIONABLE_MESSAGES.has(message)) { | ||
| console.error(message); | ||
| } else { | ||
| console.error(`media-use: \`${context}\` failed: ${message}`); | ||
| } | ||
| } | ||
|
|
||
| export function firstSemver(text) { | ||
| const match = String(text || "").match(/\bv?(\d+)\.(\d+)\.(\d+)\b/); | ||
| return match ? `${match[1]}.${match[2]}.${match[3]}` : null; | ||
| } | ||
|
|
||
| export function versionLessThan(version, minimum) { | ||
| const left = versionParts(version); | ||
| const right = versionParts(minimum); | ||
| if (!left || !right) return false; | ||
| for (let i = 0; i < 3; i++) { | ||
| if (left[i] < right[i]) return true; | ||
| if (left[i] > right[i]) return false; | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| function heygenErrorDetail(err) { | ||
| return textOf(err?.stderr) || textOf(err?.stdout) || err?.message || String(err); | ||
| } | ||
|
|
||
| function textOf(value) { | ||
| return value == null ? "" : String(value).trim(); | ||
| } | ||
|
|
||
| function versionParts(version) { | ||
| const match = String(version || "").match(/^v?(\d+)\.(\d+)\.(\d+)$/); | ||
| return match ? match.slice(1).map((part) => Number.parseInt(part, 10)) : null; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| import { strict as assert } from "node:assert"; | ||
| import { test } from "node:test"; | ||
| import { | ||
| classifyHeygenError, | ||
| HEYGEN_NOT_AUTHENTICATED_MESSAGE, | ||
| HEYGEN_NOT_FOUND_MESSAGE, | ||
| HEYGEN_OUTDATED_MESSAGE, | ||
| } from "./heygen-cli.mjs"; | ||
|
|
||
| test("classifies ENOENT-style missing heygen errors with install instructions", () => { | ||
| const message = classifyHeygenError({ code: "ENOENT", message: "spawn heygen ENOENT" }); | ||
|
|
||
| assert.equal(message, HEYGEN_NOT_FOUND_MESSAGE); | ||
| }); | ||
|
|
||
| test("classifies auth failures with login instructions", () => { | ||
| const message = classifyHeygenError({ stderr: Buffer.from("Error: not logged in") }); | ||
|
|
||
| assert.equal(message, HEYGEN_NOT_AUTHENTICATED_MESSAGE); | ||
| }); | ||
|
|
||
| test("classifies a real 401 as auth, but not a bare 401 substring in prose", () => { | ||
| assert.equal( | ||
| classifyHeygenError({ stderr: Buffer.from("HTTP 401 Unauthorized") }), | ||
| HEYGEN_NOT_AUTHENTICATED_MESSAGE, | ||
| ); | ||
| // A request id that merely contains "401" must NOT read as an auth failure. | ||
| const noise = classifyHeygenError({ stderr: Buffer.from("upload failed (request req-401abc)") }); | ||
| assert.notEqual(noise, HEYGEN_NOT_AUTHENTICATED_MESSAGE); | ||
| }); | ||
|
|
||
| test("classifies old heygen versions with update instructions", () => { | ||
| const message = classifyHeygenError({ | ||
| stderr: Buffer.from("heygen v0.1.5 does not support --headers"), | ||
| }); | ||
|
|
||
| assert.equal(message, HEYGEN_OUTDATED_MESSAGE); | ||
| }); | ||
|
|
||
| test("does not misclassify a resource 'not found' error as a missing CLI", () => { | ||
| // A stale voiceId makes `heygen voice speech create` fail with "voice not | ||
| // found"; the error message embeds the `heygen ...` command line. This must | ||
| // pass through as detail, not send the user to reinstall a working CLI. | ||
| const message = classifyHeygenError({ | ||
| stderr: Buffer.from("Error: voice not found (id: stale-123)"), | ||
| message: "Command failed: heygen voice speech create --voice stale-123", | ||
| }); | ||
|
|
||
| assert.notEqual(message, HEYGEN_NOT_FOUND_MESSAGE); | ||
| assert.equal(message, "Error: voice not found (id: stale-123)"); | ||
| }); | ||
|
|
||
| test("classifies a shell 'command not found' as a missing CLI", () => { | ||
| const message = classifyHeygenError({ stderr: Buffer.from("bash: heygen: command not found") }); | ||
|
|
||
| assert.equal(message, HEYGEN_NOT_FOUND_MESSAGE); | ||
| }); | ||
|
|
||
| test("passes through unrelated errors", () => { | ||
| const message = classifyHeygenError({ | ||
| stderr: Buffer.from("rate limit exceeded"), | ||
| message: "Command failed", | ||
| }); | ||
|
|
||
| assert.equal(message, "rate limit exceeded"); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 The bump from
0.1.6→0.3.0firesHEYGEN_OUTDATED_MESSAGEon--doctorfor anyone on 0.1.6-0.2.x, including users who never touch OAuth (API-key auth still works fine on those versions). The commit rationale is precise ("free usage needs OAuth needs v0.3.0"), butrunDoctordoesn't distinguish OAuth-first from API-key-first users — everyone gets nudged to update. Two ways to read it: (a) team policy is "we require 0.3.0 uniformly, including for API-key users" — reasonable, but worth stating inSKILL.mdsince it's a stronger claim than the commit body implies; (b) the check should branch on credential type (heygenCredential()?.type === "oauth") and only require 0.3.0 for the OAuth path. Non-blocking either way.— Review by Rames D Jusso