# oytc command operations and smoke test report Built and validated on **Friday, July 24, 2026**. ## What I validated I mapped the command tree and execution paths from source (`cmd/oytc/main.go`, `internal/cli/*.go`, and `internal/youtube` / `internal/update`), then ran a real command-line smoke sweep against all command groups and aliases. I also ran project checks: - `make fmt-check` - `make check` Both pass. ## Command tree (full command surface) ```text oytc ├ login [--oauth] ├ status [--check] ├ logout │ ├ analytics │ ├ report --metrics │ ├ overview [--by day|month] │ ├ video │ ├ traffic-sources │ └ demographics │ ├ search [QUERY] ├ channel │ ├ get ... │ ├ activities │ ├ sections [CHANNEL] | --id │ └ uploads ├ video │ ├ get ... │ ├ stats ... │ ├ popular │ └ trainability ├ playlist │ ├ get ... │ ├ list --channel │ └ items ├ comment │ ├ get ... │ ├ replies │ └ threads (--video|--channel|--id) ├ subscription │ └ list (--channel | --id) ├ live-chat │ ├ list (--video | --chat-id) │ └ stream (--video | --chat-id) ├ category list (--region | --id) ├ language list ├ region list ├ skills install ├ version ├ update [--check] [--version vX.Y.Z] ├ upgrade (alias update) └ oytc_update / oytc_upgrade (argv[0] shims -> update) ``` --- ## How the CLI is wired - One root Cobra app in `internal/cli/app.go`, with shared global flags: - `--format` (`table|json|jsonl|tsv`) - `--columns` - `--no-header` - `--no-color` - `--quiet / -q` - `--timeout` (default `20s`) - `main.go` handles signal-aware context and maps errors to exit codes. - Command execution generally follows: 1. Validate flags (before network) 2. Build endpoint + query params 3. Call either Data API, Analytics API, GitHub API, or local FS 4. Render via `internal/output` --- ## Request + response contracts by command family ### Common request behavior (Data API) - Base URL default: `https://www.googleapis.com/youtube/v3` - Auth: `X-Goog-Api-Key` header when key is present; OAuth Bearer token if key not present and OAuth scope `youtube.readonly` is already granted. - `client.Get` enforces: `--timeout` (default 20s), retries up to 3 times on: - transport errors (`net.Error`, EOF) - `429`, `500`, `502`, `503`, `504` with backoff + `Retry-After` - JSON decoding uses `json.Number` to preserve numeric strings. ### Auth and management commands | Command | Endpoint / behavior | Request fields | Response / side effects | Exit behavior | | --- | --- | --- | --- | --- | | `login` | `GET i18nLanguages` (validation call), writes `auth.json` | none | saves key; prints fingerprint | Usage error if blank key | | `login --oauth` | OAuth PKCE loopback (`oauth` package) | none | saves OAuth tokens and client creds; preserves API key if exists | requires client id + secret | | `status` | optional `GET i18nLanguages` (key) + Analytics check (OAuth) | none | table/json object showing configured creds, optional `valid` flags | returns first validation failure in check mode | | `logout` | token revoke (best-effort), then removes config | none | file removed, warnings printed for read failures | success if key file missing (idempotent remove style) | | `version` | local only | none | version/commit/date/go/os/arch | | `skills install` | local FS writes `~/.agents/skills/oytc` | none | copies embedded `SKILL.md`, `references/commands.md`, `references/recipes.md`; idempotent confirmation | `y/yes` required to proceed | | `update` / `upgrade` | GitHub release metadata + archive + `checksums.txt` | none | downloads + SHA-256 verify + extract + atomic replace binary | no update when `--check`; explicit version override allows downgrade | ### Analytics commands - API base default: `https://youtubeanalytics.googleapis.com` - All analytics routes require OAuth (`oauth` grant must exist), and compose Analytics report query with: - `ids=channel==MINE` - `metrics`, `dimensions`, `filters`, `sort`, `startDate`, `endDate` - Default window: last 28 days ending yesterday. | Command | Endpoint | Required fields | Response | | --- | --- | --- | --- | | `analytics report` | `reports` | `--metrics` required; optional `--dimensions` | normalized report rows mapped to objects | | `analytics overview` | `reports` | fixed `views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage,subscribersGained` | date/month grouped by `--by` | | `analytics video` | `reports` | fixed metrics + implicit `video==VIDEO_ID` | one row per query with video metric columns | | `analytics traffic-sources` | `reports` | fixed metrics + `insightTrafficSourceType` dimension | grouped traffic source rows | | `analytics demographics` | `reports` | `viewerPercentage` + dimensions `ageGroup,gender` | grouped demographics rows | ### Search / channel / video / playlists / comments / subscriptions / metadata | Command | API resource + params | Notable local validations | Response columns | | --- | --- | --- | --- | | `search` | `search` | enumerated enum/timestamp/CSV validation; video-only filters require `--type video`; channel-type requires `--type channel`; `--location` + `--location-radius` together | default `id.kind` filtered by requested type; envelope `{items,nextPageToken,requests}` | | `channel get` | `channels` (batched 50 IDs) | channel reference resolves URL/handle/ID; rejects owner parts `auditDetails`, `contentOwnerDetails` | id/title/subscriber/videoCount/viewCount | | `channel activities` | `activities` via list pagination | optional `published-after/before` RFC3339 | id/type/title/publishedAt | | `channel sections` | `channelSections` | either `CHANNEL` or `--id`, not both | snippet/type/position/title | | `channel uploads` | `channels`→`playlistItems` (requires uploads playlist in contentDetails) | resolves channel first, then follows uploads playlist | playlist item position/video/title/publishedAt | | `video get` | `videos` (batched 50) | rejects owner parts `fileDetails, processingDetails, suggestions` | depends on fields | | `video stats` | `videos` (batched 50), default `statistics` | same forbidden parts | id/stat counters | | `video popular` | `videos` with `chart=mostPopular` | supports `--region`, `--category` | id/title/channel/title/viewCount | | `video trainability` | `videoTrainability` with `id=` | none | object `{videoId, permitted, ...}` (no key, no quota, no `i18n` auth) | | `playlist get` | `playlists` (batched 50) | none | id/title/channel/itemCount/privacy | | `playlist list` | `playlists` with `--channel` required | none | id/title/itemCount/privacy | | `playlist items` | `playlistItems` with `--video` optional filter | page size + paging semantics | position/videoId/title/videoOwnerChannelTitle | | `comment get` | `comments` (batched 100) | text format enum | id/author/text/likes/publishedAt | | `comment replies` | `comments` | text format enum | same default comment columns | | `comment threads` | `commentThreads` | exactly one of `--video`, `--channel`, `--id`; `--order`/`--search` incompatible with `--id` | id/top-level text/author/totalReplyCount | | `subscription list` | `subscriptions` | exactly one of `--channel/--id`; `subscriberSnippet` forbidden; `--for-channel` and non-default `--order` incompatible with `--id` | id/title/totalItemCount | | `category list` | `videoCategories` with `--region | --id` | exactly one selector required | id/title/assignable | | `language list` | `i18nLanguages` | none | id/name | | `region list` | `i18nRegions` | none | id/name/glName | ### Live chat - `live-chat list` and `live-chat stream` both call `liveChat/messages`. - `--video` requires resolving `videos?part=liveStreamingDetails` to `liveStreamingDetails.activeLiveChatId` first. - List command intentionally fetches only one page. - Stream command: - carries `pageToken` forward, - waits at least `pollingIntervalMillis` between requests, - deduplicates message IDs, - supports `--limit` and external resume token, - ends cleanly when chat is offline (`--limit` reached or offline token field). ### Pagination and output envelope - Shared list command flags: - `--page-size` (validated 1..N by command) - `--page-token` - `--all` - `--limit` (0 = unlimited) - Finite output shape for list formats: - JSON: `{ items: [...], nextPageToken?: string, requests: n }` - JSONL: one object per line (no envelope) - table/tsv: columns via `--columns`, with optional `--no-header` --- ## Operational behavior and timeout/retry rationale - **Per-request timeout**: defaults to 20s (`--timeout`) and is applied to YouTube client calls. - **YouTube retry policy**: up to 3 transient retries with exponential backoff + jitter, and `Retry-After` support. - **Transport fallback**: retriable network failures (`net.Error`, EOF) and specific HTTP transient statuses. - **OAuth**: - Access token fetched from cache; refresh on expiration or near-expiry. - OAuth login callback server timeout defaults to 3 minutes (`DefaultLoginTimeout`). - Token refresh writes updated OAuth state back to config (`SaveRefreshedOAuth`) only if still current. - **Updater**: - release checks: 5 minute HTTP timeout, - release manifest limit 4 MiB, - checksum manifest limit 1 MiB, - archive limit 256 MiB, - refuses to install without matching checksums file, - explicit `--version` allows pin/downgrade, - default mode does not downgrade to older latest release. --- ## Smoke-test matrix (real command execution) ### Run with no credentials (fresh/no environment key) - `--help` and non-state-changing usage checks all pass. - Data commands correctly return: - `oytc: no API key configured; ...` (exit **3**) where applicable. - Analytics family correctly returns: - `no OAuth credentials... analytics requires OAuth` (exit **3**). - Validation branches are triggered before network as expected: - `search --event-type live` without `--type video` → usage error. - `channel sections` with no source and no `--id` → usage error. - `comment threads` with no selector → usage error. - `subscription list` with no selector → usage error. - `category list` without selector → usage error. - `live-chat list/stream` without `--video/--chat-id` → usage error. ### Run with deliberately invalid API key (`OYTC_API_KEY=bad-key`) - Most Data API routes return: - `YouTube API error (400, badRequest, API_KEY_INVALID)` (exit **3**) for: - `search`, `channel get`, `channel activities`, `video get`, `video stats`, `video popular`, `playlist get`, `playlist list`, `playlist items`, `comment get`, `comment replies`, `comment threads`, `subscription list`, `live-chat list`, `category list`, `language list`, `region list`. - `video trainability` still succeeds without API credentials and returns `{videoId, permitted, ...}`. ### Command update/dispatch behavior observed - `update --check`: - with current local version being newer than published latest, correctly refuses implicit downgrade and exits with **6**. - `upgrade` alias performs the same logic as `update`. - `oytc_update --help` dispatches to update command path (argv[0]-based aliasing), confirming shim support. --- ## Exit code map observed in runtime | Code | Meaning | | --- | --- | | 0 | success | | 2 | usage/validation error (missing/invalid args/flags) | | 3 | auth/key problem | | 4 | not found / unavailable | | 5 | quota/rate-limit | | 6 | network/transient/config/other operational failure | | 130 | interrupted (SIGINT/SIGTERM during clean shutdown) | --- ## Fit in one line `oytc` is effectively a read-only orchestration CLI: a validated, rate-aware command planner with a strict auth model (API key + optional OAuth), uniform pagination/output rendering, and a checksum-verified updater that can fail safely and deterministically when release integrity is uncertain.