Skip to content

core types + interfaces: error taxonomy, RequestSpec, Formatter, Resolver, Provider (PRINFRA-120)#1

Merged
somanshreddy merged 2 commits into
mainfrom
03-30-m0_core_types
Mar 31, 2026
Merged

core types + interfaces: error taxonomy, RequestSpec, Formatter, Resolver, Provider (PRINFRA-120)#1
somanshreddy merged 2 commits into
mainfrom
03-30-m0_core_types

Conversation

@somanshreddy
Copy link
Copy Markdown
Collaborator

@somanshreddy somanshreddy commented Mar 30, 2026

Description

Defines the core types and interfaces that the rest of the CLI is built on. Every command, the HTTP executor, the output system, and the auth/config layers all depend on these.

What's introduced:

  • CLIError — the single error type used everywhere. Carries an error code, message, optional hint, and an exit code (0 = success, 1 = general, 2 = usage, 3 = auth). All errors rendered to stderr go through this type, ensuring consistent JSON output.
  • RequestSpec — the contract between commands and the HTTP layer. A command describes what API call to make (endpoint, method, query params, body fields) by filling in a RequestSpec. The executor handles the actual HTTP call. This separation is what makes codegen possible — generated commands just produce data structs.
  • CredentialResolver — interface for finding the API key. Today it checks one env var; later implementations will search the OS keychain and a credentials file in priority order.
  • Provider — interface for non-secret settings like the API base URL. Separate from credentials because config and secrets have different storage and security needs.
  • Formatter — interface for rendering output. Data() writes successful responses to stdout, Error() writes failures to stderr. Different implementations (JSON today, TUI tables later) can slot in without changing any command code.

Testing

Error type tests cover exit code mapping for all HTTP statuses, the JSON envelope shape, field omission for empty values, and all constructor functions.

Linear: PRINFRA-120

Copy link
Copy Markdown
Collaborator Author

somanshreddy commented Mar 30, 2026

@somanshreddy somanshreddy marked this pull request as ready for review March 30, 2026 21:00
@somanshreddy somanshreddy changed the title [M0] core types + interfaces: the stable contracts core types + interfaces: error taxonomy, RequestSpec, Formatter, Resolver, Provider Mar 30, 2026
Comment thread internal/client/request_spec.go Outdated
// the framework executor converts them to HTTP requests.
//
// All fields are defined even if not all are used yet — the codegen pipeline
// and framework features (pagination, polling, upload) depend on this shape.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The full struct is defined upfront (including Pollable, PollConfig, FilePath, BodyEncoding) even though only a subset is used by video list. This avoids reshaping the core type when codegen, pagination, polling, and multipart upload are added.


// Formatter is the interface for CLI output rendering.
// JSONFormatter outputs structured JSON. TUIFormatter (future) adds tables and progress bars.
type Formatter interface {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only JSONFormatter exists today. A TUIFormatter implementing this same interface will add --human mode with lipgloss tables and bubbletea spinners — no changes to commands needed.

Comment thread internal/auth/resolver.go Outdated

// Resolver resolves API credentials.
// EnvResolver reads from the HEYGEN_API_KEY env var.
// Future: OS keyring and file-based credential storage with priority chain.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently only EnvResolver exists. The plan is to add KeyringResolver (OS keychain via go-keyring) and FileResolver (~/.heygen/credentials) with a priority chain: env > keyring > file.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is actualy how I solved it before as well

wondering though what do other CLIs do? do we need the keyring resolver immediately or get away with just looking at the file

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. Here's how others handle it:

  • Stripe CLI: env var (STRIPE_API_KEY) → config file (~/.config/stripe/config.toml) → interactive stripe login (device auth flow). No keyring.
  • gh CLI: env var (GH_TOKEN) → config file (~/.config/gh/hosts.yml). No keyring.
  • AWS CLI: env var → ~/.aws/credentials file → instance metadata. No keyring.

None of the major CLIs use OS keyring.

File-based storage is the industry standard. We can skip keyring entirely and go: env var → config file (~/.heygen/credentials).

For this PR, env-only is intentional. File-based storage comes in future PRs (auth/config milestone).

Comment thread internal/config/config.go
// EnvProvider reads from env vars. FileProvider (future) reads ~/.heygen/config.toml.
type Provider interface {
APIKey() string
BaseURL() string
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Provider intentionally does NOT include APIKey() — credentials are auth.Resolver's job. This prevents two sources of truth for API keys when keyring/file storage is added.

@somanshreddy somanshreddy force-pushed the 03-30-m0_core_types branch 2 times, most recently from 740533d to 0b271fa Compare March 30, 2026 22:10
…Resolver, Provider

Stable types and interfaces that codegen and framework features build against:

- CLIError with exit codes (0/1/2/3), ToErrorEnvelope for JSON output
- APIError matching HeyGen's standard error envelope
- FromAPIError mapping HTTP status → exit code
- RequestSpec: []QueryParam, []FieldSpec, BodyEncoding, FilePath, *PollConfig
- output.Formatter interface (Data + Error)
- auth.CredentialResolver interface (priority chain for API keys)
- config.Provider interface (non-secret settings, BaseURL only)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@somanshreddy somanshreddy self-assigned this Mar 30, 2026
Industry research shows none of the major CLIs (Stripe, gh, AWS, gcloud)
use OS keyring. File-based storage with 0600 permissions is the standard.
Removes keyring mentions from auth resolver comment and git conventions.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@somanshreddy somanshreddy changed the title core types + interfaces: error taxonomy, RequestSpec, Formatter, Resolver, Provider core types + interfaces: error taxonomy, RequestSpec, Formatter, Resolver, Provider (PRINFRA-120) Mar 31, 2026
@linear
Copy link
Copy Markdown

linear Bot commented Mar 31, 2026

PRINFRA-120 CLI M0: Repo scaffold + core types

Set up the Go repository and define the core type contracts that M1 and M2 depend on.

Scope

  • Initialize Go module (github.com/heygen-com/heygen-cli)
  • Cobra root command (heygen) with --version and --help
  • Directory structure: cmd/heygen/, cmd/, internal/, gen/, codegen/
  • Makefile with build, test, lint, fmt targets
  • .goreleaser.yaml skeleton (6 targets)
  • CI pipeline (GitHub Actions): lint, test, build matrix
  • Core type definitions (shared contracts):
    • RequestSpec struct (endpoint, method, params, body, pagination/polling fields, columns)
    • CommandSpec struct (group, name, flags, columns — generated commands produce these)
    • CLIError type + exit code mapping (0/1/2/3)
    • Formatter interface (FormatOne, FormatList, FormatError)

Why first

These types are the shared contracts consumed by M1 (codegen, framework) and M2 (auth, config). They must be stable before parallel work begins.

Acceptance Criteria

  • go build produces a working heygen binary
  • heygen --version and heygen --help work
  • Core types compile and have doc comments
  • CI passes on all targets

Copy link
Copy Markdown
Collaborator Author

somanshreddy commented Mar 31, 2026

Merge activity

  • Mar 31, 5:53 AM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Mar 31, 5:54 AM UTC: @somanshreddy merged this pull request with Graphite.

@somanshreddy somanshreddy merged commit 5a34029 into main Mar 31, 2026
9 checks passed
@somanshreddy somanshreddy deleted the 03-30-m0_core_types branch March 31, 2026 05:54
somanshreddy added a commit that referenced this pull request Mar 31, 2026
…-122) (#2)

## Description

Implements the interfaces from PR #1 — this is where the actual behavior lives.

**Components:**

- **EnvCredentialResolver** — reads `HEYGEN_API_KEY` from the environment. If missing, returns an auth error (exit 3) with a hint telling the user what to set.
- **EnvProvider** — reads `HEYGEN_API_BASE` from the environment, defaults to `https://api.heygen.com`. Used to override the API URL in tests and staging.
- **Client** — wraps Go's `net/http` with automatic `x-api-key` and `User-Agent` header injection on every request. The `WithHTTPClient` option is how tests swap in a mock server.
- **Executor** — takes a RequestSpec and turns it into a real HTTP call. Handles URL construction, path parameter substitution (e.g. `/v3/videos/{video_id}`), query param encoding, JSON body marshaling, and error parsing. Returns raw JSON on success or a CLIError on failure, with the API's request ID attached when available.
- **JSONFormatter** — the default output renderer. Pretty-prints successful JSON to stdout, renders errors as `{"error": {...}}` envelopes to stderr. Rejects non-JSON responses as errors (the CLI's contract is structured output).

**How they connect:** The root command's startup hook creates an EnvProvider, resolves credentials via EnvCredentialResolver, then builds a Client configured with the provider's base URL. Commands use the Client's Executor to run their RequestSpecs, and the Formatter to write the result.

## Testing

All tests use Go's `httptest.Server` — no real API calls. Coverage includes header injection, retry scenarios, query param encoding with repeated keys, path param substitution, POST body marshaling, error envelope parsing for 400/401/500, network errors, and invalid JSON rejection.

Linear: [PRINFRA-122](https://linear.app/heygen/issue/PRINFRA-122)
somanshreddy added a commit that referenced this pull request Mar 31, 2026
…RA-142) (#3)

## Description

Wires everything from PRs #1 and #2 into a working CLI. After this, `heygen video list` works end-to-end against the real HeyGen API.

**What happens when you run `heygen video list --limit 5`:**

1. `main.go` creates a JSON formatter before anything else — so even early failures (bad auth, bad flags) get structured JSON error output, never plain text.
2. Cobra parses the command and flags. Unknown commands and bad flags are caught and returned as usage errors (exit 2).
3. The root command's startup hook runs: creates a config provider, resolves the API key, and builds an HTTP client.
4. The `video list` command validates `--limit` is 1-100, builds a RequestSpec, and hands it to the executor.
5. The executor makes the HTTP call and returns raw JSON. The formatter pretty-prints it to stdout.
6. If anything fails at any step, the error flows back to `main.go` where it's classified (auth → exit 3, usage → exit 2, everything else → exit 1) and rendered as a JSON envelope to stderr.

**Test harness:** `runCommand()` in the test helper mirrors this exact flow — fresh Cobra tree, buffer-backed formatter, same error classification. Tests assert on exit codes and the JSON envelope shape, not just whether an error occurred.

## Testing

10 command tests using httptest mocks:
- Success with valid JSON response
- Missing auth (exit 3 with hint)
- API 401 and 400 responses (correct exit codes, request ID preserved)
- Flags passed correctly as query params
- Limit validation (exit 2 for 0, -5, 999)
- Unknown flag and unknown command (exit 2)
- Server error (exit 1)

Manual smoke test: `HEYGEN_API_KEY=<key> ./bin/heygen video list --limit 2` returns real video JSON.

Linear: [PRINFRA-142](https://linear.app/heygen/issue/PRINFRA-142)
@somanshreddy somanshreddy added this to the M0: Walking Skeleton milestone Apr 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants