Skip to content

Latest commit

 

History

History
90 lines (66 loc) · 7.16 KB

File metadata and controls

90 lines (66 loc) · 7.16 KB

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 (videosvideo). Groups whose tag and path noun are different words pin theirs in groupPrimaryRoots (video-translatevideo-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. Use clierrors.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(). Never fmt.Fprintln(os.Stderr, ...).

Formatter contract

Data(v json.RawMessage, dataField string, columns []command.Column) error
  • Generated commands pass client.APIDataField and defaultColumnsForSpec(spec).
  • Hand-written commands (auth, config) pass "", nil.
  • JSONFormatter ignores dataField and columns — always renders the full response.
  • HumanFormatter uses dataField to unwrap the envelope, then renders a table (array) or key-value (object).

Pagination

  • List commands paginate manually via API query flags like --limit and --token.
  • Do not add a generic --all mode. If an agent needs multiple pages, it should read next_token from 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.CredentialResolver owns API keys. Config does not own credentials.
  • config.Provider owns non-secret settings (BaseURL, output format, etc.).
  • command.Spec is the contract between codegen and the runtime builder. Commands never call the HTTP client directly.
  • cmd/heygen/columns.go owns curated --human table columns. Not in gen/, not in the OpenAPI spec.

Testing

  • All automated tests use httptest.Server. No real API calls in tests or CI.
  • Command tests: use runCommand() in cmd/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-test in Claude Code before cutting a stable release. It exercises the built binary against the live API. Requires HEYGEN_API_KEY and 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.