Imported from heygen-com/heygen-cli (
AGENTS.md). Install upstream withnpx skills add heygen-com/heygen-cli. Copyright stays with the author.
HeyGen CLI
Go CLI wrapping HeyGen's v3 API. Built on Cobra, command surface auto-generated from OpenAPI spec.
Build & Test
make build # → bin/heygen
make test # all tests are mocked, no API key needed
make lint # golangci-lint
make clean
make generate SPEC=path/to/spec.json # regenerate gen/ from OpenAPI spec
Smoke test against real API: HEYGEN_API_KEY=<key> ./bin/heygen video list --limit 2
For project layout and development guide, see CONTRIBUTING.md. For branch names, commit messages, and PR titles, see .github/GIT_CONVENTIONS.md.
Key Rules
gen/ is generated — never hand-edit
Files in gen/ are deleted and regenerated by make generate. Never put hand-written logic there. Hand-written enhancements (column definitions, flag constraints, custom commands) go in cmd/heygen/.
command.Spec is immutable
Spec is a generated, read-only definition. Never mutate it at runtime. Column definitions, poll configs, and other runtime enhancements are external lookup data in cmd/heygen/, passed to the formatter or builder as separate arguments.
Cobra command tree is single-use
newRootCmd() builds a fresh Cobra command tree for each CLI invocation. Command instances are never reused across executions — each main() call and each test creates a new tree. This is load-bearing: the builder's PreRunE mutates Cobra flag annotations to bypass required-flag validation for --request-schema / --response-schema.
Codegen requires examples
make generate warns if any command lacks examples but still generates the code. Use make generate STRICT=1 locally to fail on missing examples. A test in cmd/heygen/generated_examples_test.go enforces that every generated command has at least one example, so make test (and CI) will fail if examples are missing. Examples live in codegen/examples/{group}.yaml.
Codegen command naming
A command name is the path segments after the group root plus a terminal verb derived from the HTTP method. A group has at most one primary root — the one that restates the group name — and only that root is dropped. Every other root stays as a sub-group, minus the group prefix, which is why one OpenAPI tag can span several resources without any override: Brand covers both /v3/brand-kits and /v3/brand-glossaries, giving brand kits list and brand glossaries get. An umbrella group like that has no primary root, which is correct — don't add one to groupPrimaryRoots to force it.
The primary root is normally recognized by normalizing it (videos → video). Groups whose tag and path noun are different words pin theirs in groupPrimaryRoots (video-translate → video-translations); add an entry only when a group's own root is wrongly kept as a sub-group. The choice never depends on which other roots exist, so adding a resource to a group cannot rename the commands already in it. Codegen rejects a spec where two roots in one group would collapse onto the same sub-group.
Codegen hard-fails, rather than emitting a plausible-but-wrong name, on four spec shapes: a path with no resource segment after the version prefix, a leading segment that isn't v<N> (so a /beta/… or unversioned path is a build error, not a command with a junk beta sub-group), a {param} where the resource root belongs, and two roots in one group collapsing onto the same sub-group. A spec author adding a path in a new URL shape will see the error at make generate.
Reach for nameOverrides in codegen/grouper.go only when the derived verb is semantically wrong (POST /v3/templates/{template_id} is generate, not create), never to tell two resources apart. An override replaces the terminal verb; derived sub-groups are preserved. When two endpoints still produce the same name, codegen fails with a clear error.
New list commands
When adding a list endpoint, verify: verb is list not get (use nameOverrides if the heuristic gets it wrong), Paginated is set if the API supports limit/token, add curated --human columns in cmd/heygen/columns.go, and add the new command to .claude/skills/e2e-cli-test/SKILL.md (Phase 2 for list, Phase 3 for get/detail).
Errors
- All command errors must be
*CLIError. Useclierrors.New()(exit 1),clierrors.NewAuth()(exit 3),clierrors.NewUsage()(exit 2). - API responses:
clierrors.FromAPIError(statusCode, apiErr, requestID). - Flag/arg validation: always
NewUsage()so exit code is 2, not 1. - All error output goes through
formatter.Error(). Neverfmt.Fprintln(os.Stderr, ...).
Formatter contract
Data(v json.RawMessage, dataField string, columns []command.Column) error
- Generated commands pass
client.APIDataFieldanddefaultColumnsForSpec(spec). - Hand-written commands (auth, config) pass
"", nil. JSONFormatterignoresdataFieldandcolumns— always renders the full response.HumanFormatterusesdataFieldto unwrap the envelope, then renders a table (array) or key-value (object).
Pagination
- List commands paginate manually via API query flags like
--limitand--token. - Do not add a generic
--allmode. If an agent needs multiple pages, it should readnext_tokenfrom the JSON response and request the next page explicitly.
Auth and skipAuth
initContext() resolves credentials via ChainCredentialResolver (env → file). Commands that don't need auth (e.g., auth login, config set) annotate with skipAuth: true in Cobra Annotations.
Separation of concerns
auth.CredentialResolverowns API keys. Config does not own credentials.config.Providerowns non-secret settings (BaseURL, output format, etc.).command.Specis the contract between codegen and the runtime builder. Commands never call the HTTP client directly.cmd/heygen/columns.goowns curated--humantable columns. Not ingen/, not in the OpenAPI spec.
Testing
- All automated tests use
httptest.Server. No real API calls in tests or CI. - Command tests: use
runCommand()incmd/heygen/testutil_test.go. It creates a fresh Cobra tree, captures stdout/stderr/exit code, and renders errors through the formatter. - Use
t.Setenv()for env vars (auto-restored). - Assert on exit codes (0/1/2/3/4) and stderr envelope shape, not just error presence.
- Pre-release E2E: Run
/e2e-cli-testin Claude Code before cutting a stable release. It exercises the built binary against the live API. RequiresHEYGEN_API_KEYand spends a small number of credits. See SKILL.md for details.
Documentation
When a change affects user-facing behavior, update the relevant doc:
- New command group or removed group → README.md command table
- New hand-written command or enhancement pattern → CONTRIBUTING.md
- Changed release or CI workflow (triggers, versioning, channels) → RELEASE.md
- Changed key commands or async patterns → SKILL.md
CI workflow changes that affect how releases are built, triggered, or distributed count as release workflow changes — update RELEASE.md.
Do not update docs for internal refactors, test changes, or codegen-only changes. Lint/CI changes that don't affect the release pipeline are exempt.