tmdb-kit 1.0.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,29 +6,67 @@
6
6
  [![node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
7
7
  [![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)](https://github.com/isaced/tmdb-kit/actions/workflows/ci.yml)
8
8
 
9
- A minimal, fully typed TMDB SDK with zero runtime dependencies. Built for Node.js, Bun, Deno, and edge runtimes.
10
-
11
- `tmdb-kit` covers the most common TMDB workflows — movies, TV shows, search, trending, people, genres, and more — with a small, hand-crafted API designed for day-to-day use.
9
+ A minimal, fully typed SDK and read-only command-line interface (CLI) for The Movie Database (TMDB). Zero runtime dependencies. Built for Node.js, Bun, and edge runtimes.
10
+
11
+ `tmdb-kit` covers common TMDB workflows: movies, TV shows, search, trending, people, genres, and images. Use it as a library, or run the bundled `tmdb` command in your terminal.
12
+
13
+ ## Contents
14
+
15
+ - [Features](#features)
16
+ - [Install](#install)
17
+ - [SDK](#sdk)
18
+ - [Create a client](#create-a-client)
19
+ - [Authentication](#authentication)
20
+ - [Configuration](#configuration)
21
+ - [Custom transport](#custom-transport)
22
+ - [Supported resources](#supported-resources)
23
+ - [Image URLs](#image-urls)
24
+ - [Search](#search)
25
+ - [Image sizes](#image-sizes)
26
+ - [Errors](#errors)
27
+ - [CLI](#cli)
28
+ - [Credentials and config](#credentials-and-config)
29
+ - [Commands](#commands)
30
+ - [Common options](#common-options)
31
+ - [Output and exit codes](#output-and-exit-codes)
32
+ - [Examples](#examples)
33
+ - [Development](#development)
34
+ - [References](#references)
12
35
 
13
36
  ## Features
14
37
 
15
- - **Zero dependencies** — nothing to audit, nothing to conflict.
16
- - **Universal runtime** — uses only `fetch`, `Headers`, `URL`, and `AbortSignal`. Works on Node.js 18+, Bun, Deno, Cloudflare Workers, Vercel Edge, and any fetch-compatible environment.
17
- - **Strict TypeScript** — full type safety with autocomplete for request options, response shapes, image sizes, and error handling.
18
- - **ESM + CommonJS** — dual-format output via tsdown, ready for any module system.
19
- - **Typed resources** — first-class methods for movies, TV, search, trending, people, genres, configuration, and images.
20
- - **Injectable transport** — swap the HTTP layer for tests, caching, or instrumentation without mocking `fetch`.
21
- - **Image helpers** — build CDN / proxy / signed URLs with per-request transform overrides.
22
- - **Search type guards** — narrow `search.multi()` results to movies, TV, or people with runtime-checked helpers.
23
- - **100% test coverage** — comprehensive unit tests with mocked fetch; no real TMDB requests.
38
+ - **Zero dependencies**: nothing to audit, nothing to conflict.
39
+ - **Universal runtime**: uses only `fetch`, `Headers`, `URL`, and `AbortSignal`. Works on Node.js 18+, Bun, Cloudflare Workers, Vercel Edge, and any fetch-compatible environment.
40
+ - **SDK and CLI**: typed resource methods for your app, plus `tmdb` and `tmdb-kit` binaries for terminal use.
41
+ - **Strict TypeScript**: full type safety with autocomplete for request options, response shapes, image sizes, and error handling.
42
+ - **ESM output**: modern module output with bundled type declarations.
43
+ - **Typed resources**: first-class methods for movies, TV, search, trending, people, genres, configuration, and images.
44
+ - **Injectable transport**: swap the HTTP layer for tests, caching, or instrumentation without mocking `fetch`.
45
+ - **Image helpers**: build content delivery network (CDN), proxy, or signed URLs with per-request transform overrides.
46
+ - **Search type guards**: narrow `search.multi()` results to movies, TV, or people with runtime-checked helpers.
47
+ - **Tested**: unit and resource tests mock `fetch`; an optional live integration suite validates real API behavior.
24
48
 
25
49
  ## Install
26
50
 
51
+ Install the SDK with npm:
52
+
27
53
  ```bash
28
54
  npm install tmdb-kit
29
55
  ```
30
56
 
31
- ## Quick Start
57
+ Install the CLI globally:
58
+
59
+ ```bash
60
+ npm install --global tmdb-kit
61
+ ```
62
+
63
+ The CLI is available as `tmdb`.
64
+
65
+ ## SDK
66
+
67
+ Create a client, authenticate, and call typed resource methods. The client uses the Fetch API and has no runtime dependencies.
68
+
69
+ ### Create a client
32
70
 
33
71
  ```ts
34
72
  import { createTMDB } from 'tmdb-kit'
@@ -46,79 +84,60 @@ const movie = await tmdb.movies.details(popular.results[0]!.id, {
46
84
  const posterUrl = tmdb.images.url(movie.poster_path, 'w500')
47
85
  ```
48
86
 
49
- CommonJS:
87
+ ### Authentication
50
88
 
51
- ```js
52
- const { createTMDB } = require('tmdb-kit')
89
+ The SDK targets the TMDB v3 API at `api.themoviedb.org/3`. Two authentication methods are supported.
53
90
 
54
- const tmdb = createTMDB({ accessToken: process.env.TMDB_ACCESS_TOKEN })
55
- ```
56
-
57
- Deno:
91
+ An API Read Access Token is recommended and works with both v3 and v4:
58
92
 
59
93
  ```ts
60
- import { createTMDB } from 'npm:tmdb-kit'
61
-
62
- const tmdb = createTMDB({ accessToken: Deno.env.get('TMDB_ACCESS_TOKEN')! })
94
+ createTMDB({ accessToken: 'your_access_token_here' })
63
95
  ```
64
96
 
65
- ## Authentication
66
-
67
- The SDK targets the **TMDB v3 API** (`api.themoviedb.org/3`). Two authentication methods are supported:
68
-
69
- **API Read Access Token** (recommended) — works with both v3 and v4:
97
+ A legacy v3 API key is sent as an `api_key` query parameter:
70
98
 
71
99
  ```ts
72
- createTMDB({ accessToken: 'tmdb-read-access-token' })
100
+ createTMDB({ apiKey: 'your_api_key_here' })
73
101
  ```
74
102
 
75
- **Legacy v3 API key** — sent as an `api_key` query parameter:
103
+ Pass exactly one of `accessToken` or `apiKey`. The client throws when both or neither are set.
76
104
 
77
- ```ts
78
- createTMDB({ apiKey: 'tmdb-v3-api-key' })
79
- ```
105
+ ### Configuration
80
106
 
81
- Exactly one of `accessToken` or `apiKey` is required.
107
+ `createTMDB` accepts these options:
82
108
 
83
- ## Configuration
109
+ | Option | Default | Description |
110
+ | ------------------ | ------------------------------------ | ----------------------------------------------------------------- |
111
+ | `accessToken` | required unless `apiKey` is set | API Read Access Token, sent as a Bearer header. |
112
+ | `apiKey` | required unless `accessToken` is set | Legacy v3 API key, sent as `api_key`. |
113
+ | `baseUrl` | `https://api.themoviedb.org/3` | API base URL. Useful for proxies and tests. |
114
+ | `fetch` | `globalThis.fetch` at request time | Custom fetch implementation. |
115
+ | `defaultLanguage` | TMDB default | Language for endpoints that accept `language`. |
116
+ | `defaultRegion` | TMDB default | Region for movie lists and search endpoints that accept `region`. |
117
+ | `imageBaseUrl` | `https://image.tmdb.org/t/p` | Image host used by `tmdb.images`. |
118
+ | `images.transform` | none | Default URL transform for `tmdb.images.url()`. |
119
+ | `headers` | none | Extra headers merged into every request before authentication. |
120
+ | `transport` | built-in HTTP client | Custom transport, see [Custom transport](#custom-transport). |
84
121
 
85
- The factory accepts a few options beyond authentication:
122
+ A typical client:
86
123
 
87
124
  ```ts
88
- import { createTMDB } from 'tmdb-kit'
89
-
90
125
  const tmdb = createTMDB({
91
- accessToken: 'tmdb-read-access-token',
92
-
93
- // Defaults to https://api.themoviedb.org/3. Useful for tests or proxies.
94
- baseUrl: 'https://api.themoviedb.org/3',
95
-
96
- // Inject a fetch implementation. The SDK reads `globalThis.fetch` lazily
97
- // on every request, so test-time mocks (vi.spyOn, etc.) are picked up
98
- // automatically after the client has been constructed.
99
- fetch: customFetch,
100
-
101
- // Defaults applied to every request that supports them.
126
+ accessToken: 'your_access_token_here',
102
127
  defaultLanguage: 'en-US',
103
128
  defaultRegion: 'US',
104
-
105
- // Custom image host. Defaults to https://image.tmdb.org/t/p.
106
- imageBaseUrl: 'https://image.tmdb.org/t/p',
107
-
108
- // Image URL transform applied by the ImagesHelper. See "Image URLs".
129
+ headers: { 'X-Client': 'my-app' },
109
130
  images: {
110
131
  transform: (url) => url.replace('image.tmdb.org/t/p', 'img-proxy.example/tmdb'),
111
132
  },
112
-
113
- // Extra headers merged into every request before authentication.
114
- headers: { 'X-Client': 'my-app' },
115
133
  })
116
134
  ```
117
135
 
118
- ### Injecting a custom transport
136
+ The SDK reads `globalThis.fetch` lazily on every request. Spies on `globalThis.fetch` are picked up after the client has been constructed.
137
+
138
+ ### Custom transport
119
139
 
120
- For tests, caching, or instrumentation, you can swap the entire transport
121
- layer instead of mocking `fetch`:
140
+ For tests, caching, or instrumentation, swap the entire transport layer instead of mocking `fetch`:
122
141
 
123
142
  ```ts
124
143
  import { createTMDB, type TMDBTransport } from 'tmdb-kit'
@@ -128,64 +147,25 @@ const stub: TMDBTransport = {
128
147
  get: async (path, _options) => ({ results: [] }),
129
148
  }
130
149
 
131
- const tmdb = createTMDB({ accessToken: 'x', transport: stub })
150
+ const tmdb = createTMDB({ accessToken: 'your_access_token_here', transport: stub })
132
151
  ```
133
152
 
134
- `transport.get()` is called for every resource and for the `request()`
135
- escape hatch, so a single stub covers the whole surface area.
136
- `TMDBTransport` is exported as part of the public API.
153
+ `transport.get()` runs for every resource and for `request()`, so a single stub covers the whole surface. `TMDBTransport` is part of the public API.
137
154
 
138
- ## Supported Resources
155
+ ### Supported resources
139
156
 
140
- ```ts
141
- tmdb.movies.popular()
142
- tmdb.movies.topRated()
143
- tmdb.movies.nowPlaying()
144
- tmdb.movies.upcoming()
145
- tmdb.movies.details(550, { appendToResponse: ['credits', 'videos'] })
146
- tmdb.movies.credits(550)
147
- tmdb.movies.images(550)
148
- tmdb.movies.recommendations(550)
149
- tmdb.movies.similar(550)
150
- tmdb.movies.videos(550)
151
-
152
- tmdb.tv.popular()
153
- tmdb.tv.topRated()
154
- tmdb.tv.airingToday()
155
- tmdb.tv.onTheAir()
156
- tmdb.tv.details(1399)
157
- tmdb.tv.credits(1399)
158
- tmdb.tv.images(1399)
159
- tmdb.tv.recommendations(1399)
160
- tmdb.tv.similar(1399)
161
- tmdb.tv.videos(1399)
162
- tmdb.tv.seasonDetails(1399, 1)
163
-
164
- tmdb.search.movies('Dune')
165
- tmdb.search.tv('Dark')
166
- tmdb.search.people('Sofia Coppola')
167
- tmdb.search.multi('Tom Hardy')
168
-
169
- tmdb.trending.all('week')
170
- tmdb.trending.movies('day')
171
- tmdb.trending.tv('day')
172
- tmdb.trending.people('week')
173
-
174
- tmdb.people.popular()
175
- tmdb.people.details(31)
176
-
177
- tmdb.genres.movies()
178
- tmdb.genres.tv()
179
-
180
- tmdb.configuration.details()
181
- tmdb.configuration.countries()
182
- tmdb.configuration.jobs()
183
- tmdb.configuration.languages()
184
- tmdb.configuration.primaryTranslations()
185
- tmdb.configuration.timezones()
186
- ```
157
+ | Resource | Methods |
158
+ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
159
+ | `tmdb.movies` | `popular()`, `topRated()`, `nowPlaying()`, `upcoming()`, `details()`, `credits()`, `images()`, `recommendations()`, `similar()`, `videos()` |
160
+ | `tmdb.tv` | `popular()`, `topRated()`, `airingToday()`, `onTheAir()`, `details()`, `credits()`, `images()`, `recommendations()`, `similar()`, `videos()`, `seasonDetails()` |
161
+ | `tmdb.search` | `movies()`, `tv()`, `people()`, `multi()` |
162
+ | `tmdb.trending` | `all()`, `movies()`, `tv()`, `people()` |
163
+ | `tmdb.people` | `popular()`, `details()`, `combinedCredits()` |
164
+ | `tmdb.genres` | `movies()`, `tv()` |
165
+ | `tmdb.configuration` | `details()`, `countries()`, `jobs()`, `languages()`, `primaryTranslations()`, `timezones()` |
166
+ | `tmdb.images` | `url()` |
187
167
 
188
- For an endpoint that is not wrapped yet:
168
+ For an endpoint that is not wrapped yet, use the `request()` escape hatch:
189
169
 
190
170
  ```ts
191
171
  const response = await tmdb.request<{ results: unknown[] }>('/discover/movie', {
@@ -196,34 +176,29 @@ const response = await tmdb.request<{ results: unknown[] }>('/discover/movie', {
196
176
  })
197
177
  ```
198
178
 
199
- ## Image URLs
179
+ ### Image URLs
200
180
 
201
- `tmdb.images.url(path, size)` builds a fully-qualified image URL. The
202
- helper returns `null` for nullish paths so UI code can branch naturally:
181
+ `tmdb.images.url(path, size)` builds a fully qualified image URL. It returns `null` for nullish paths so UI code can branch naturally:
203
182
 
204
183
  ```ts
205
184
  const posterUrl = tmdb.images.url(movie.poster_path, 'w500')
206
185
  ```
207
186
 
208
- ### Routing images through a CDN or proxy
209
-
210
- Pass a `transform` callback to rewrite the resolved URL. The most common
211
- use case is pointing TMDB image requests at a self-hosted proxy:
187
+ Pass a `transform` callback to route images through a CDN or proxy:
212
188
 
213
189
  ```ts
214
190
  const tmdb = createTMDB({
215
- accessToken: 'x',
191
+ accessToken: 'your_access_token_here',
216
192
  images: {
217
193
  transform: (url) => url.replace('https://image.tmdb.org/t/p', 'https://img-proxy.example/tmdb'),
218
194
  },
219
195
  })
220
196
 
221
197
  tmdb.images.url('/abc.jpg', 'w500')
222
- // → 'https://img-proxy.example/tmdb/w500/abc.jpg'
198
+ // 'https://img-proxy.example/tmdb/w500/abc.jpg'
223
199
  ```
224
200
 
225
- You can also pass a one-off transform per call — useful for adding
226
- signed query parameters for selected sizes:
201
+ Pass a one-off transform per call to add signed query parameters:
227
202
 
228
203
  ```ts
229
204
  tmdb.images.url('/abc.jpg', 'original', {
@@ -231,25 +206,14 @@ tmdb.images.url('/abc.jpg', 'original', {
231
206
  })
232
207
  ```
233
208
 
234
- The transform is never invoked for nullish paths, so `null` results stay
235
- `null`.
236
-
237
- `buildImageUrl(path, size, options)` is exported directly for callers
238
- that want to build URLs without a `TMDBClient` instance.
209
+ The transform is never invoked for nullish paths, so `null` results stay `null`. `buildImageUrl(path, size, options)` is exported for callers that build URLs without a `TMDBClient` instance.
239
210
 
240
- ## Search
211
+ ### Search
241
212
 
242
- `search.multi()` returns a union of movie / TV / person results. Filter
243
- out media types you do not want and narrow the result type with the
244
- provided type guards:
213
+ `search.multi()` returns a union of movie, TV, and person results. Filter media types and narrow the result type with the provided type guards:
245
214
 
246
215
  ```ts
247
- import {
248
- createTMDB,
249
- isMovieSearchResult,
250
- isTVSearchResult,
251
- isPersonSearchResult,
252
- } from 'tmdb-kit'
216
+ import { createTMDB, isMovieSearchResult, isTVSearchResult, isPersonSearchResult } from 'tmdb-kit'
253
217
 
254
218
  const { results } = await tmdb.search.multi('Inception', {
255
219
  exclude: ['person'],
@@ -260,104 +224,235 @@ const tvShows = results.filter(isTVSearchResult) // TVMultiResult[]
260
224
  const people = results.filter(isPersonSearchResult) // PersonMultiResult[]
261
225
  ```
262
226
 
263
- - `exclude` performs client-side filtering — the TMDB multi-search
264
- endpoint has no server-side media_type filter. `total_results` reflects
265
- the unfiltered payload; the filtered count is just `results.length`.
266
- - Short-name aliases `isMovie` / `isTV` / `isPerson` are also exported
267
- for callers that prefer them.
268
- - The single-resource search methods (`search.movies`, `search.tv`,
269
- `search.people`) already return the narrowed result type, so the
270
- guards are only useful for `search.multi()` payloads.
227
+ `exclude` performs client-side filtering because the TMDB multi-search endpoint has no server-side media type filter. `total_results` reflects the unfiltered payload, and the filtered count is `results.length`.
228
+
229
+ The short-name aliases `isMovie`, `isTV`, and `isPerson` are also exported. The single-resource search methods already return narrowed types, so the guards matter only for `search.multi()`.
271
230
 
272
- ## Image Sizes
231
+ ### Image sizes
273
232
 
274
- The `ImageSize` type is intentionally permissive — TMDB has added new
275
- sizes in the past (e.g. `h632` for profile) without notice, and the SDK
276
- keeps the `(string & {})` escape hatch so any future size keeps
277
- working at runtime. If you need the canonical set of documented sizes,
278
- the SDK ships them as readonly constants:
233
+ The `ImageSize` type is intentionally permissive. TMDB has added sizes without notice, such as `h632` for profiles, so the SDK keeps the `(string & {})` escape hatch for forward compatibility. The SDK also ships the documented sizes as readonly constants:
279
234
 
280
235
  ```ts
281
- import {
282
- KNOWN_BACKDROP_SIZES,
283
- KNOWN_LOGO_SIZES,
284
- KNOWN_POSTER_SIZES,
285
- KNOWN_PROFILE_SIZES,
286
- KNOWN_STILL_SIZES,
287
- KNOWN_IMAGE_SIZES,
288
- isKnownBackdropSize,
289
- isKnownImageSize,
290
- // …plus one helper per type
291
- } from 'tmdb-kit'
292
-
293
- // Iterate to build a size picker
236
+ import { KNOWN_POSTER_SIZES, isKnownPosterSize } from 'tmdb-kit'
237
+
294
238
  for (const size of KNOWN_POSTER_SIZES) {
295
239
  console.log(size) // 'w92' | 'w154' | 'w185' | 'w342' | 'w500' | 'w780' | 'original'
296
240
  }
297
241
 
298
- // Validate user-supplied sizes at runtime
299
242
  if (!isKnownPosterSize(userInput)) {
300
243
  throw new Error(`Unknown TMDB poster size: ${userInput}`)
301
244
  }
302
245
  ```
303
246
 
304
- `KnownBackdropSize` / `KnownPosterSize` / `KnownLogoSize` /
305
- `KnownProfileSize` / `KnownStillSize` / `KnownImageSize` are the
306
- matching TS literal-union types for callers that want strict typing
307
- without giving up forward-compatibility.
247
+ The SDK also exports `KNOWN_BACKDROP_SIZES`, `KNOWN_LOGO_SIZES`, `KNOWN_PROFILE_SIZES`, `KNOWN_STILL_SIZES`, `KNOWN_IMAGE_SIZES`, and a matching type guard for each list. The literal-union types are `KnownBackdropSize`, `KnownPosterSize`, `KnownLogoSize`, `KnownProfileSize`, `KnownStillSize`, and `KnownImageSize`.
308
248
 
309
- ## Errors
249
+ ### Errors
310
250
 
311
251
  ```ts
312
- import {
313
- TMDBRateLimitError,
314
- TMDBRequestError,
315
- TMDBResponseError,
316
- } from 'tmdb-kit'
252
+ import { TMDBRateLimitError, TMDBRequestError, TMDBResponseError } from 'tmdb-kit'
317
253
 
318
254
  try {
319
255
  await tmdb.movies.popular()
320
256
  } catch (error) {
321
257
  if (error instanceof TMDBRateLimitError) {
322
- // 429 — TMDB asked the caller to back off. The SDK does NOT retry
323
- // automatically; back off for `retryAfter` seconds (or use your own
324
- // policy) and try again.
258
+ // 429: TMDB asked the caller to back off. The SDK does not retry.
325
259
  console.log('rate limited; retry in', error.retryAfter, 's')
326
260
  } else if (error instanceof TMDBResponseError) {
327
261
  console.log(error.status, error.statusCode, error.statusMessage)
328
262
  } else if (error instanceof TMDBRequestError) {
329
- // Thrown when a request fails before receiving a response (e.g. network
330
- // error, no fetch implementation found, or invalid client configuration).
263
+ // Network failure, missing fetch, or invalid client configuration.
331
264
  console.log('request error:', error.message)
332
265
  }
333
266
  }
334
267
  ```
335
268
 
336
- `TMDBResponseError` exposes convenience getters for the most common
337
- status codes:
269
+ `TMDBResponseError` exposes convenience getters for common status codes:
270
+
271
+ | Getter | Status |
272
+ | ---------------- | ------ |
273
+ | `isUnauthorized` | 401 |
274
+ | `isForbidden` | 403 |
275
+ | `isNotFound` | 404 |
276
+ | `isRateLimit` | 429 |
277
+ | `isServerError` | 5xx |
278
+
279
+ All errors extend `TMDBError`, which you can use as a single catch-all.
280
+
281
+ ## CLI
282
+
283
+ The `tmdb` command is a read-only client for the same TMDB API. It reads credentials from the environment or a config file, prints human-readable summaries by default, and accepts `--json` for raw TMDB payloads.
284
+
285
+ The CLI runs on Node.js 18+ and Bun.
338
286
 
339
- | Getter | Status |
340
- | --- | --- |
341
- | `isUnauthorized` | 401 |
342
- | `isForbidden` | 403 |
343
- | `isNotFound` | 404 |
344
- | `isRateLimit` | 429 |
345
- | `isServerError` | 5xx |
287
+ ### Credentials and config
346
288
 
347
- All errors are also instances of the base class `TMDBError`; use that
348
- when you want a single catch-all.
289
+ The CLI checks credentials in this order:
290
+
291
+ 1. `TMDB_ACCESS_TOKEN`
292
+ 2. `TMDB_API_KEY` (legacy v3 API key)
293
+ 3. `accessToken` in the config file
294
+ 4. `apiKey` in the config file
295
+
296
+ Environment variables take precedence over the config file. `TMDB_BASE_URL` overrides the API base URL for every command.
297
+
298
+ The config file lives at:
299
+
300
+ - Windows: `%APPDATA%\tmdb-kit\config.json`
301
+ - macOS and Linux: `$XDG_CONFIG_HOME/tmdb-kit/config.json`, or `~/.config/tmdb-kit/config.json` when `XDG_CONFIG_HOME` is unset
302
+
303
+ On Unix, the directory is created with mode `0700` and the file with mode `0600`.
304
+
305
+ Set up credentials with the auth commands:
306
+
307
+ ```bash
308
+ tmdb auth login # prompt, validate, and save a credential
309
+ tmdb auth login --force # replace existing credentials
310
+ printf '%s' "$TMDB_ACCESS_TOKEN" | tmdb auth login --token-stdin --force
311
+ tmdb auth status # source, masked token, and config path
312
+ tmdb auth logout # remove saved credentials
313
+ tmdb config test # verify credentials with a TMDB request
314
+ ```
315
+
316
+ `auth login` validates the credential with a TMDB request before saving it. `config test` performs the same check without changing the config. Use `--force` to replace existing credentials, or to save a config token while an environment variable is set and stdin is not a TTY.
317
+
318
+ Manage defaults with `config`:
319
+
320
+ ```bash
321
+ tmdb config set language zh-CN
322
+ tmdb config set region CN
323
+ tmdb config set token # prompts; secrets are not accepted as arguments
324
+ tmdb config unset language
325
+ tmdb config show # masked values and config path
326
+ tmdb config path
327
+ ```
328
+
329
+ Config keys are `token` or `accessToken`, `api-key` or `apiKey`, `language`, and `region`. Secrets entered with `config set token` or `config set api-key` are read from a prompt, never from the command line. `auth status` and `config show` mask stored secrets.
330
+
331
+ Use `--lang` to override the config language for a single command. `auth logout` removes saved credentials but leaves environment variables in place.
332
+
333
+ ### Commands
334
+
335
+ Every API command accepts `--lang` and `--json`. Run `tmdb <command> --help` for the full option list.
336
+
337
+ | Command | Description | Command options |
338
+ | --------------------------------- | --------------------------------------------------------------------------------- | ---------------------------- |
339
+ | `tmdb search <query>` | Search all media, movies, TV, or people. | `--type`, `--year`, `--page` |
340
+ | `tmdb movie list [category]` | List movies. Default `popular`; also `top-rated`, `now-playing`, `upcoming`. | `--page`, `--region` |
341
+ | `tmdb movie get <id>` | Movie details. | `--append` |
342
+ | `tmdb movie credits <id>` | Movie cast and crew. | `--limit` |
343
+ | `tmdb movie images <id>` | Movie image URLs. | `--image-language` |
344
+ | `tmdb movie videos <id>` | Movie videos and trailers. | none |
345
+ | `tmdb movie similar <id>` | Similar movies. | `--page`, `--region` |
346
+ | `tmdb movie recommendations <id>` | Recommended movies. | `--page`, `--region` |
347
+ | `tmdb tv list [category]` | List TV shows. Default `popular`; also `top-rated`, `airing-today`, `on-the-air`. | `--page` |
348
+ | `tmdb tv get <id>` | TV show details. | `--append` |
349
+ | `tmdb tv credits <id>` | TV cast and crew. | `--limit` |
350
+ | `tmdb tv images <id>` | TV image URLs. | `--image-language` |
351
+ | `tmdb tv videos <id>` | TV videos and trailers. | none |
352
+ | `tmdb tv similar <id>` | Similar TV shows. | `--page` |
353
+ | `tmdb tv recommendations <id>` | Recommended TV shows. | `--page` |
354
+ | `tmdb tv season <id> <season>` | TV season and episodes. Use `0` for specials. | none |
355
+ | `tmdb person get <id>` | Person details. | none |
356
+ | `tmdb person credits <id>` | Combined cast and crew credits. | `--limit` |
357
+ | `tmdb trending [type]` | Trending media. Default `all`; also `movie`, `tv`, `person`. | `--window`, `--page` |
358
+ | `tmdb genres [type]` | Genres. Default `movie`; also `tv`. | none |
359
+
360
+ Credential and config commands:
361
+
362
+ | Command | Description | Command options |
363
+ | ------------------------------- | ------------------------------------------------------ | -------------------------- |
364
+ | `tmdb auth login` | Validate and save credentials. | `--token-stdin`, `--force` |
365
+ | `tmdb auth status` | Show credential source, masked token, and config path. | `--json` |
366
+ | `tmdb auth logout` | Remove saved credentials. | none |
367
+ | `tmdb config show` | Show config values with secrets masked. | `--json` |
368
+ | `tmdb config path` | Print the config file path. | none |
369
+ | `tmdb config test` | Verify credentials with an authenticated request. | `--json` |
370
+ | `tmdb config set <key> [value]` | Set `token`, `api-key`, `language`, or `region`. | none |
371
+ | `tmdb config unset <key>` | Remove a config value. | none |
372
+
373
+ ### Common options
374
+
375
+ | Option | Description |
376
+ | ------------------------- | ---------------------------------------------------------------------------------- |
377
+ | `--json` | Print the raw TMDB JSON response instead of the formatted summary. |
378
+ | `--lang <code>` | Language for this command. Defaults to config `language`, then `en-US`. |
379
+ | `--page <number>` | Page number. Defaults to `1`. |
380
+ | `--region <code>` | Region for movie lists, similar, and recommendations. Defaults to config `region`. |
381
+ | `--limit <number>` | Maximum rows for credits. Defaults to `10`. |
382
+ | `--append <list>` | Comma-separated `append_to_response` values, such as `credits,videos`. |
383
+ | `--image-language <list>` | Comma-separated image language codes. |
384
+ | `--year <year>` | Movie release year or TV first-air year. Only with `--type movie` or `--type tv`. |
385
+ | `--type <value>` | Media selector for `search`: `all`, `movie`, `tv`, or `person`. |
386
+ | `--window <day\|week>` | Trending window. Defaults to `week`. |
387
+
388
+ `--version` prints the CLI version.
389
+
390
+ ### Output and exit codes
391
+
392
+ The default output is a compact summary for reading in a terminal. Use `--json` for scripts:
393
+
394
+ ```bash
395
+ tmdb movie get 550 --json | jq '.title, .release_date'
396
+ ```
397
+
398
+ Exit codes:
399
+
400
+ | Code | Meaning |
401
+ | ---- | -------------------------------------------------------------- |
402
+ | `0` | Success. |
403
+ | `1` | Unexpected error, network failure, or non-auth TMDB response. |
404
+ | `2` | Invalid command usage or input. |
405
+ | `3` | Missing or rejected credentials, or a config read/write error. |
406
+ | `4` | TMDB returned `404 Not Found`. |
407
+ | `5` | TMDB rate limit (`429 Too Many Requests`). |
408
+
409
+ ### Examples
410
+
411
+ ```bash
412
+ # Search movies released in 2010
413
+ tmdb search "Inception" --type movie --year 2010
414
+
415
+ # Movie details with credits and videos
416
+ tmdb movie get 550 --append credits,videos
417
+
418
+ # Trending movies today, raw JSON
419
+ tmdb trending movie --window day --json | jq '.results[].title'
420
+
421
+ # Season 1 episodes for a TV show
422
+ tmdb tv season 1399 1 --json | jq '.episodes[].name'
423
+
424
+ # Save defaults for later commands
425
+ tmdb config set language zh-CN
426
+ tmdb config set region CN
427
+ ```
349
428
 
350
429
  ## Development
351
430
 
431
+ Requires [Bun](https://bun.sh) 1.4.4+. The published package still runs on Node.js 18+.
432
+
433
+ ```bash
434
+ bun install
435
+ bun run format
436
+ bun run typecheck
437
+ bun run test:coverage
438
+ bun run build
439
+ ```
440
+
441
+ Run the integration suite with a real token:
442
+
443
+ ```bash
444
+ TMDB_ACCESS_TOKEN=your_access_token_here bun run test:integration
445
+ ```
446
+
447
+ Build and run the CLI from source:
448
+
352
449
  ```bash
353
- npm install
354
- npm run typecheck
355
- npm run test:coverage
356
- npm run build
450
+ bun run build:cli
451
+ bun dist/cli.js --help
357
452
  ```
358
453
 
359
454
  ## References
360
455
 
361
456
  - [TMDB authentication](https://developer.themoviedb.org/docs/authentication-application)
362
457
  - [TMDB API reference](https://developer.themoviedb.org/reference/intro/getting-started)
363
- - [tsdown output formats](https://tsdown.dev/options/output-format)
458
+ - [Bun bundler docs](https://bun.com/docs/bundler)