|
| 1 | +// v0.3.0 is the first CLI that can use an OAuth session; v0.1.x/0.2.x reject it |
| 2 | +// ("heygen-cli can't use OAuth yet"), and OAuth is what the free-usage path |
| 3 | +// needs — so anything below this can't authenticate for free usage at all. |
| 4 | +export const HEYGEN_MIN_VERSION = "0.3.0"; |
| 5 | +// Free-usage path is OAuth (`--oauth` → subscription/free credits); `--api-key` |
| 6 | +// bills API credits, so the onboarding steers to OAuth. |
| 7 | +export const HEYGEN_INSTALL_COMMAND = |
| 8 | + "curl -fsSL https://static.heygen.ai/cli/install.sh | bash && heygen auth login --oauth"; |
| 9 | +export const HEYGEN_AUTH_COMMAND = "heygen auth login --oauth"; |
| 10 | +export const HEYGEN_UPDATE_COMMAND = "heygen update"; |
| 11 | + |
| 12 | +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}`; |
| 13 | +export const HEYGEN_NOT_AUTHENTICATED_MESSAGE = `media-use: heygen CLI not authenticated (free usage) — run: ${HEYGEN_AUTH_COMMAND}`; |
| 14 | +export const HEYGEN_OUTDATED_MESSAGE = `media-use: heygen CLI is outdated — run: ${HEYGEN_UPDATE_COMMAND} (need >= v${HEYGEN_MIN_VERSION})`; |
| 15 | + |
| 16 | +const ACTIONABLE_MESSAGES = new Set([ |
| 17 | + HEYGEN_NOT_FOUND_MESSAGE, |
| 18 | + HEYGEN_NOT_AUTHENTICATED_MESSAGE, |
| 19 | + HEYGEN_OUTDATED_MESSAGE, |
| 20 | +]); |
| 21 | + |
| 22 | +export function classifyHeygenError(err) { |
| 23 | + const detail = heygenErrorDetail(err); |
| 24 | + const text = [err?.stderr, err?.stdout, err?.message, detail] |
| 25 | + .map((value) => textOf(value)) |
| 26 | + .filter(Boolean) |
| 27 | + .join("\n"); |
| 28 | + const lower = text.toLowerCase(); |
| 29 | + |
| 30 | + // Only ENOENT (spawn of a missing binary) or a shell's "command not found" |
| 31 | + // mean the CLI itself is absent. A bare "not found" would misfire on the CLI's |
| 32 | + // own resource errors (e.g. a stale voiceId → "voice not found"), whose message |
| 33 | + // embeds the `heygen ...` command line — sending users to reinstall a CLI they |
| 34 | + // just ran successfully. Keep this narrow. |
| 35 | + if (err?.code === "ENOENT" || lower.includes("command not found")) { |
| 36 | + return HEYGEN_NOT_FOUND_MESSAGE; |
| 37 | + } |
| 38 | + |
| 39 | + if ( |
| 40 | + lower.includes("unauthorized") || |
| 41 | + lower.includes("unauthenticated") || |
| 42 | + // \b401\b, not a bare "401" substring — otherwise request IDs (req-401abc), |
| 43 | + // URLs, and retry-after headers would misclassify as an auth failure. |
| 44 | + /\b401\b/.test(lower) || |
| 45 | + lower.includes("not logged in") || |
| 46 | + lower.includes("no api key") || |
| 47 | + lower.includes("missing api key") || |
| 48 | + lower.includes("invalid api key") || |
| 49 | + lower.includes("login required") || |
| 50 | + lower.includes("auth required") || |
| 51 | + lower.includes("authentication required") |
| 52 | + ) { |
| 53 | + return HEYGEN_NOT_AUTHENTICATED_MESSAGE; |
| 54 | + } |
| 55 | + |
| 56 | + const version = firstSemver(text); |
| 57 | + if (version && versionLessThan(version, HEYGEN_MIN_VERSION)) { |
| 58 | + return HEYGEN_OUTDATED_MESSAGE; |
| 59 | + } |
| 60 | + |
| 61 | + return detail; |
| 62 | +} |
| 63 | + |
| 64 | +export function reportHeygenFailure(err, context) { |
| 65 | + const message = classifyHeygenError(err); |
| 66 | + if (ACTIONABLE_MESSAGES.has(message)) { |
| 67 | + console.error(message); |
| 68 | + } else { |
| 69 | + console.error(`media-use: \`${context}\` failed: ${message}`); |
| 70 | + } |
| 71 | +} |
| 72 | + |
| 73 | +export function firstSemver(text) { |
| 74 | + const match = String(text || "").match(/\bv?(\d+)\.(\d+)\.(\d+)\b/); |
| 75 | + return match ? `${match[1]}.${match[2]}.${match[3]}` : null; |
| 76 | +} |
| 77 | + |
| 78 | +export function versionLessThan(version, minimum) { |
| 79 | + const left = versionParts(version); |
| 80 | + const right = versionParts(minimum); |
| 81 | + if (!left || !right) return false; |
| 82 | + for (let i = 0; i < 3; i++) { |
| 83 | + if (left[i] < right[i]) return true; |
| 84 | + if (left[i] > right[i]) return false; |
| 85 | + } |
| 86 | + return false; |
| 87 | +} |
| 88 | + |
| 89 | +function heygenErrorDetail(err) { |
| 90 | + return textOf(err?.stderr) || textOf(err?.stdout) || err?.message || String(err); |
| 91 | +} |
| 92 | + |
| 93 | +function textOf(value) { |
| 94 | + return value == null ? "" : String(value).trim(); |
| 95 | +} |
| 96 | + |
| 97 | +function versionParts(version) { |
| 98 | + const match = String(version || "").match(/^v?(\d+)\.(\d+)\.(\d+)$/); |
| 99 | + return match ? match.slice(1).map((part) => Number.parseInt(part, 10)) : null; |
| 100 | +} |
0 commit comments