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/dist/cli.js ADDED
@@ -0,0 +1,3132 @@
1
+ #!/usr/bin/env node
2
+ // node_modules/citty/dist/_chunks/libs/scule.mjs
3
+ var NUMBER_CHAR_RE = /\d/;
4
+ var STR_SPLITTERS = [
5
+ "-",
6
+ "_",
7
+ "/",
8
+ "."
9
+ ];
10
+ function isUppercase(char = "") {
11
+ if (NUMBER_CHAR_RE.test(char))
12
+ return;
13
+ return char !== char.toLowerCase();
14
+ }
15
+ function splitByCase(str, separators) {
16
+ const splitters = separators ?? STR_SPLITTERS;
17
+ const parts = [];
18
+ if (!str || typeof str !== "string")
19
+ return parts;
20
+ let buff = "";
21
+ let previousUpper;
22
+ let previousSplitter;
23
+ for (const char of str) {
24
+ const isSplitter = splitters.includes(char);
25
+ if (isSplitter === true) {
26
+ parts.push(buff);
27
+ buff = "";
28
+ previousUpper = undefined;
29
+ continue;
30
+ }
31
+ const isUpper = isUppercase(char);
32
+ if (previousSplitter === false) {
33
+ if (previousUpper === false && isUpper === true) {
34
+ parts.push(buff);
35
+ buff = char;
36
+ previousUpper = isUpper;
37
+ continue;
38
+ }
39
+ if (previousUpper === true && isUpper === false && buff.length > 1) {
40
+ const lastChar = buff.at(-1);
41
+ parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
42
+ buff = lastChar + char;
43
+ previousUpper = isUpper;
44
+ continue;
45
+ }
46
+ }
47
+ buff += char;
48
+ previousUpper = isUpper;
49
+ previousSplitter = isSplitter;
50
+ }
51
+ parts.push(buff);
52
+ return parts;
53
+ }
54
+ function upperFirst(str) {
55
+ return str ? str[0].toUpperCase() + str.slice(1) : "";
56
+ }
57
+ function lowerFirst(str) {
58
+ return str ? str[0].toLowerCase() + str.slice(1) : "";
59
+ }
60
+ function pascalCase(str, opts) {
61
+ return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => upperFirst(opts?.normalize ? p.toLowerCase() : p)).join("") : "";
62
+ }
63
+ function camelCase(str, opts) {
64
+ return lowerFirst(pascalCase(str || "", opts));
65
+ }
66
+ function kebabCase(str, joiner) {
67
+ return str ? (Array.isArray(str) ? str : splitByCase(str)).map((p) => p.toLowerCase()).join(joiner ?? "-") : "";
68
+ }
69
+ function snakeCase(str) {
70
+ return kebabCase(str || "", "_");
71
+ }
72
+
73
+ // node_modules/citty/dist/index.mjs
74
+ import { parseArgs as parseArgs$1 } from "node:util";
75
+ function toArray(val) {
76
+ if (Array.isArray(val))
77
+ return val;
78
+ return val === undefined ? [] : [val];
79
+ }
80
+ function formatLineColumns(lines, linePrefix = "") {
81
+ const maxLength = [];
82
+ for (const line of lines)
83
+ for (const [i, element] of line.entries())
84
+ maxLength[i] = Math.max(maxLength[i] || 0, element.length);
85
+ return lines.map((l) => l.map((c, i) => linePrefix + c[i === 0 ? "padStart" : "padEnd"](maxLength[i])).join(" ")).join(`
86
+ `);
87
+ }
88
+ function resolveValue(input) {
89
+ return typeof input === "function" ? input() : input;
90
+ }
91
+ var CLIError = class extends Error {
92
+ code;
93
+ constructor(message, code) {
94
+ super(message);
95
+ this.name = "CLIError";
96
+ this.code = code;
97
+ }
98
+ };
99
+ function parseRawArgs(args = [], opts = {}) {
100
+ const booleans = new Set(opts.boolean || []);
101
+ const strings = new Set(opts.string || []);
102
+ const aliasMap = opts.alias || {};
103
+ const defaults = opts.default || {};
104
+ const aliasToMain = /* @__PURE__ */ new Map;
105
+ const mainToAliases = /* @__PURE__ */ new Map;
106
+ for (const [key, value] of Object.entries(aliasMap)) {
107
+ const targets = value;
108
+ for (const target of targets) {
109
+ aliasToMain.set(key, target);
110
+ if (!mainToAliases.has(target))
111
+ mainToAliases.set(target, []);
112
+ mainToAliases.get(target).push(key);
113
+ aliasToMain.set(target, key);
114
+ if (!mainToAliases.has(key))
115
+ mainToAliases.set(key, []);
116
+ mainToAliases.get(key).push(target);
117
+ }
118
+ }
119
+ const options = {};
120
+ function getType(name) {
121
+ if (booleans.has(name))
122
+ return "boolean";
123
+ const aliases = mainToAliases.get(name) || [];
124
+ for (const alias of aliases)
125
+ if (booleans.has(alias))
126
+ return "boolean";
127
+ return "string";
128
+ }
129
+ function isStringType(name) {
130
+ if (strings.has(name))
131
+ return true;
132
+ const aliases = mainToAliases.get(name) || [];
133
+ for (const alias of aliases)
134
+ if (strings.has(alias))
135
+ return true;
136
+ return false;
137
+ }
138
+ const allOptions = new Set([
139
+ ...booleans,
140
+ ...strings,
141
+ ...Object.keys(aliasMap),
142
+ ...Object.values(aliasMap).flat(),
143
+ ...Object.keys(defaults)
144
+ ]);
145
+ for (const name of allOptions)
146
+ if (!options[name])
147
+ options[name] = {
148
+ type: getType(name),
149
+ default: defaults[name]
150
+ };
151
+ for (const [alias, main] of aliasToMain.entries())
152
+ if (alias.length === 1 && options[main] && !options[main].short)
153
+ options[main].short = alias;
154
+ const processedArgs = [];
155
+ const negatedFlags = {};
156
+ for (let i = 0;i < args.length; i++) {
157
+ const arg = args[i];
158
+ if (arg === "--") {
159
+ processedArgs.push(...args.slice(i));
160
+ break;
161
+ }
162
+ if (arg.startsWith("--no-")) {
163
+ const flagName = arg.slice(5);
164
+ negatedFlags[flagName] = true;
165
+ continue;
166
+ }
167
+ processedArgs.push(arg);
168
+ }
169
+ let parsed;
170
+ try {
171
+ parsed = parseArgs$1({
172
+ args: processedArgs,
173
+ options: Object.keys(options).length > 0 ? options : undefined,
174
+ allowPositionals: true,
175
+ strict: false
176
+ });
177
+ } catch {
178
+ parsed = {
179
+ values: {},
180
+ positionals: processedArgs
181
+ };
182
+ }
183
+ const out = { _: [] };
184
+ out._ = parsed.positionals;
185
+ for (const [key, value] of Object.entries(parsed.values)) {
186
+ let coerced = value;
187
+ if (getType(key) === "boolean" && typeof value === "string")
188
+ coerced = value !== "false";
189
+ else if (isStringType(key) && typeof value === "boolean")
190
+ coerced = "";
191
+ out[key] = coerced;
192
+ }
193
+ for (const [name] of Object.entries(negatedFlags)) {
194
+ out[name] = false;
195
+ const mainName = aliasToMain.get(name);
196
+ if (mainName)
197
+ out[mainName] = false;
198
+ const aliases = mainToAliases.get(name);
199
+ if (aliases)
200
+ for (const alias of aliases)
201
+ out[alias] = false;
202
+ }
203
+ for (const [alias, main] of aliasToMain.entries()) {
204
+ if (out[alias] !== undefined && out[main] === undefined)
205
+ out[main] = out[alias];
206
+ if (out[main] !== undefined && out[alias] === undefined)
207
+ out[alias] = out[main];
208
+ if (out[alias] !== out[main] && defaults[main] === out[main])
209
+ out[main] = out[alias];
210
+ }
211
+ return out;
212
+ }
213
+ var noColor = /* @__PURE__ */ (() => {
214
+ const env = globalThis.process?.env ?? {};
215
+ return env.NO_COLOR === "1" || env.TERM === "dumb" || env.TEST || env.CI;
216
+ })();
217
+ var _c = (c, r = 39) => (t) => noColor ? t : `\x1B[${c}m${t}\x1B[${r}m`;
218
+ var bold = /* @__PURE__ */ _c(1, 22);
219
+ var cyan = /* @__PURE__ */ _c(36);
220
+ var gray = /* @__PURE__ */ _c(90);
221
+ var underline = /* @__PURE__ */ _c(4, 24);
222
+ function parseArgs(rawArgs, argsDef) {
223
+ const parseOptions = {
224
+ boolean: [],
225
+ string: [],
226
+ alias: {},
227
+ default: {}
228
+ };
229
+ const args = resolveArgs(argsDef);
230
+ for (const arg of args) {
231
+ if (arg.type === "positional")
232
+ continue;
233
+ if (arg.type === "string" || arg.type === "enum")
234
+ parseOptions.string.push(arg.name);
235
+ else if (arg.type === "boolean")
236
+ parseOptions.boolean.push(arg.name);
237
+ if (arg.default !== undefined)
238
+ parseOptions.default[arg.name] = arg.default;
239
+ if (arg.alias)
240
+ parseOptions.alias[arg.name] = arg.alias;
241
+ const camelName = camelCase(arg.name);
242
+ const kebabName = kebabCase(arg.name);
243
+ if (camelName !== arg.name || kebabName !== arg.name) {
244
+ const existingAliases = toArray(parseOptions.alias[arg.name] || []);
245
+ if (camelName !== arg.name && !existingAliases.includes(camelName))
246
+ existingAliases.push(camelName);
247
+ if (kebabName !== arg.name && !existingAliases.includes(kebabName))
248
+ existingAliases.push(kebabName);
249
+ if (existingAliases.length > 0)
250
+ parseOptions.alias[arg.name] = existingAliases;
251
+ }
252
+ }
253
+ const parsed = parseRawArgs(rawArgs, parseOptions);
254
+ const [...positionalArguments] = parsed._;
255
+ const parsedArgsProxy = new Proxy(parsed, { get(target, prop) {
256
+ return target[prop] ?? target[camelCase(prop)] ?? target[kebabCase(prop)];
257
+ } });
258
+ for (const [, arg] of args.entries())
259
+ if (arg.type === "positional") {
260
+ const nextPositionalArgument = positionalArguments.shift();
261
+ if (nextPositionalArgument !== undefined)
262
+ parsedArgsProxy[arg.name] = nextPositionalArgument;
263
+ else if (arg.default === undefined && arg.required !== false)
264
+ throw new CLIError(`Missing required positional argument: ${arg.name.toUpperCase()}`, "EARG");
265
+ else
266
+ parsedArgsProxy[arg.name] = arg.default;
267
+ } else if (arg.type === "enum") {
268
+ const argument = parsedArgsProxy[arg.name];
269
+ const options = arg.options || [];
270
+ if (argument !== undefined && options.length > 0 && !options.includes(argument))
271
+ throw new CLIError(`Invalid value for argument: ${cyan(`--${arg.name}`)} (${cyan(argument)}). Expected one of: ${options.map((o) => cyan(o)).join(", ")}.`, "EARG");
272
+ } else if (arg.required && parsedArgsProxy[arg.name] === undefined)
273
+ throw new CLIError(`Missing required argument: --${arg.name}`, "EARG");
274
+ return parsedArgsProxy;
275
+ }
276
+ function resolveArgs(argsDef) {
277
+ const args = [];
278
+ for (const [name, argDef] of Object.entries(argsDef || {}))
279
+ args.push({
280
+ ...argDef,
281
+ name,
282
+ alias: toArray(argDef.alias)
283
+ });
284
+ return args;
285
+ }
286
+ async function resolvePlugins(plugins) {
287
+ return Promise.all(plugins.map((p) => resolveValue(p)));
288
+ }
289
+ function defineCommand(def) {
290
+ return def;
291
+ }
292
+ async function runCommand(cmd, opts) {
293
+ const cmdArgs = await resolveValue(cmd.args || {});
294
+ const parsedArgs = parseArgs(opts.rawArgs, cmdArgs);
295
+ const context = {
296
+ rawArgs: opts.rawArgs,
297
+ args: parsedArgs,
298
+ data: opts.data,
299
+ cmd
300
+ };
301
+ const plugins = await resolvePlugins(cmd.plugins ?? []);
302
+ let result;
303
+ let runError;
304
+ try {
305
+ for (const plugin of plugins)
306
+ await plugin.setup?.(context);
307
+ if (typeof cmd.setup === "function")
308
+ await cmd.setup(context);
309
+ const subCommands = await resolveValue(cmd.subCommands);
310
+ if (subCommands && Object.keys(subCommands).length > 0) {
311
+ const subCommandArgIndex = findSubCommandIndex(opts.rawArgs, cmdArgs);
312
+ const explicitName = opts.rawArgs[subCommandArgIndex];
313
+ if (explicitName) {
314
+ const subCommand = await _findSubCommand(subCommands, explicitName);
315
+ if (!subCommand)
316
+ throw new CLIError(`Unknown command ${cyan(explicitName)}`, "E_UNKNOWN_COMMAND");
317
+ await runCommand(subCommand, { rawArgs: opts.rawArgs.slice(subCommandArgIndex + 1) });
318
+ } else {
319
+ const defaultSubCommand = await resolveValue(cmd.default);
320
+ if (defaultSubCommand) {
321
+ if (cmd.run)
322
+ throw new CLIError(`Cannot specify both 'run' and 'default' on the same command.`, "E_DEFAULT_CONFLICT");
323
+ const subCommand = await _findSubCommand(subCommands, defaultSubCommand);
324
+ if (!subCommand)
325
+ throw new CLIError(`Default sub command ${cyan(defaultSubCommand)} not found in subCommands.`, "E_UNKNOWN_COMMAND");
326
+ await runCommand(subCommand, { rawArgs: opts.rawArgs });
327
+ } else if (!cmd.run)
328
+ throw new CLIError(`No command specified.`, "E_NO_COMMAND");
329
+ }
330
+ }
331
+ if (typeof cmd.run === "function")
332
+ result = await cmd.run(context);
333
+ } catch (error) {
334
+ runError = error;
335
+ }
336
+ const cleanupErrors = [];
337
+ if (typeof cmd.cleanup === "function")
338
+ try {
339
+ await cmd.cleanup(context);
340
+ } catch (error) {
341
+ cleanupErrors.push(error);
342
+ }
343
+ for (const plugin of [...plugins].reverse())
344
+ try {
345
+ await plugin.cleanup?.(context);
346
+ } catch (error) {
347
+ cleanupErrors.push(error);
348
+ }
349
+ if (runError)
350
+ throw runError;
351
+ if (cleanupErrors.length === 1)
352
+ throw cleanupErrors[0];
353
+ if (cleanupErrors.length > 1)
354
+ throw new Error("Multiple cleanup errors", { cause: cleanupErrors });
355
+ return { result };
356
+ }
357
+ async function resolveSubCommand(cmd, rawArgs, parent) {
358
+ const subCommands = await resolveValue(cmd.subCommands);
359
+ if (subCommands && Object.keys(subCommands).length > 0) {
360
+ const subCommandArgIndex = findSubCommandIndex(rawArgs, await resolveValue(cmd.args || {}));
361
+ const subCommandName = rawArgs[subCommandArgIndex];
362
+ const subCommand = await _findSubCommand(subCommands, subCommandName);
363
+ if (subCommand)
364
+ return resolveSubCommand(subCommand, rawArgs.slice(subCommandArgIndex + 1), cmd);
365
+ }
366
+ return [cmd, parent];
367
+ }
368
+ async function _findSubCommand(subCommands, name) {
369
+ if (name in subCommands)
370
+ return resolveValue(subCommands[name]);
371
+ for (const sub of Object.values(subCommands)) {
372
+ const resolved = await resolveValue(sub);
373
+ const meta = await resolveValue(resolved?.meta);
374
+ if (meta?.alias) {
375
+ if (toArray(meta.alias).includes(name))
376
+ return resolved;
377
+ }
378
+ }
379
+ }
380
+ function findSubCommandIndex(rawArgs, argsDef) {
381
+ for (let i = 0;i < rawArgs.length; i++) {
382
+ const arg = rawArgs[i];
383
+ if (arg === "--")
384
+ return -1;
385
+ if (arg.startsWith("-")) {
386
+ if (!arg.includes("=") && _isValueFlag(arg, argsDef))
387
+ i++;
388
+ continue;
389
+ }
390
+ return i;
391
+ }
392
+ return -1;
393
+ }
394
+ function _isValueFlag(flag, argsDef) {
395
+ const name = flag.replace(/^-{1,2}/, "");
396
+ const normalized = camelCase(name);
397
+ for (const [key, def] of Object.entries(argsDef)) {
398
+ if (def.type !== "string" && def.type !== "enum")
399
+ continue;
400
+ if (normalized === camelCase(key))
401
+ return true;
402
+ if ((Array.isArray(def.alias) ? def.alias : def.alias ? [def.alias] : []).includes(name))
403
+ return true;
404
+ }
405
+ return false;
406
+ }
407
+ async function showUsage(cmd, parent) {
408
+ try {
409
+ console.log(await renderUsage(cmd, parent) + `
410
+ `);
411
+ } catch (error) {
412
+ console.error(error);
413
+ }
414
+ }
415
+ var negativePrefixRe = /^no[-A-Z]/;
416
+ async function renderUsage(cmd, parent) {
417
+ const cmdMeta = await resolveValue(cmd.meta || {});
418
+ const cmdArgs = resolveArgs(await resolveValue(cmd.args || {}));
419
+ const parentMeta = await resolveValue(parent?.meta || {});
420
+ const commandName = `${parentMeta.name ? `${parentMeta.name} ` : ""}` + (cmdMeta.name || process.argv[1]);
421
+ const argLines = [];
422
+ const posLines = [];
423
+ const commandsLines = [];
424
+ const usageLine = [];
425
+ for (const arg of cmdArgs)
426
+ if (arg.type === "positional") {
427
+ const name = arg.name.toUpperCase();
428
+ const isRequired = arg.required !== false && arg.default === undefined;
429
+ posLines.push([cyan(name + renderValueHint(arg)), renderDescription(arg, isRequired)]);
430
+ usageLine.push(isRequired ? `<${name}>` : `[${name}]`);
431
+ } else {
432
+ const isRequired = arg.required === true && arg.default === undefined;
433
+ const argStr = [...(arg.alias || []).map((a) => `-${a}`), `--${arg.name}`].join(", ") + renderValueHint(arg);
434
+ argLines.push([cyan(argStr), renderDescription(arg, isRequired)]);
435
+ if (arg.type === "boolean" && (arg.default === true || arg.negativeDescription) && !negativePrefixRe.test(arg.name)) {
436
+ const negativeArgStr = [...(arg.alias || []).map((a) => `--no-${a}`), `--no-${arg.name}`].join(", ");
437
+ argLines.push([cyan(negativeArgStr), [arg.negativeDescription, isRequired ? gray("(Required)") : ""].filter(Boolean).join(" ")]);
438
+ }
439
+ if (isRequired)
440
+ usageLine.push(`--${arg.name}` + renderValueHint(arg));
441
+ }
442
+ if (cmd.subCommands) {
443
+ const commandNames = [];
444
+ const subCommands = await resolveValue(cmd.subCommands);
445
+ for (const [name, sub] of Object.entries(subCommands)) {
446
+ const meta = await resolveValue((await resolveValue(sub))?.meta);
447
+ if (meta?.hidden)
448
+ continue;
449
+ const aliases = toArray(meta?.alias);
450
+ const label = [name, ...aliases].join(", ");
451
+ commandsLines.push([cyan(label), meta?.description || ""]);
452
+ commandNames.push(name, ...aliases);
453
+ }
454
+ usageLine.push(commandNames.join("|"));
455
+ }
456
+ const usageLines = [];
457
+ const version = cmdMeta.version || parentMeta.version;
458
+ usageLines.push(gray(`${cmdMeta.description} (${commandName + (version ? ` v${version}` : "")})`), "");
459
+ const hasOptions = argLines.length > 0 || posLines.length > 0;
460
+ usageLines.push(`${underline(bold("USAGE"))} ${cyan(`${commandName}${hasOptions ? " [OPTIONS]" : ""} ${usageLine.join(" ")}`)}`, "");
461
+ if (posLines.length > 0) {
462
+ usageLines.push(underline(bold("ARGUMENTS")), "");
463
+ usageLines.push(formatLineColumns(posLines, " "));
464
+ usageLines.push("");
465
+ }
466
+ if (argLines.length > 0) {
467
+ usageLines.push(underline(bold("OPTIONS")), "");
468
+ usageLines.push(formatLineColumns(argLines, " "));
469
+ usageLines.push("");
470
+ }
471
+ if (commandsLines.length > 0) {
472
+ usageLines.push(underline(bold("COMMANDS")), "");
473
+ usageLines.push(formatLineColumns(commandsLines, " "));
474
+ usageLines.push("", `Use ${cyan(`${commandName} <command> --help`)} for more information about a command.`);
475
+ }
476
+ return usageLines.filter((l) => typeof l === "string").join(`
477
+ `);
478
+ }
479
+ function renderValueHint(arg) {
480
+ const valueHint = arg.valueHint ? `=<${arg.valueHint}>` : "";
481
+ const fallbackValueHint = valueHint || `=<${snakeCase(arg.name)}>`;
482
+ if (!arg.type || arg.type === "positional" || arg.type === "boolean")
483
+ return valueHint;
484
+ if (arg.type === "enum" && arg.options?.length)
485
+ return `=<${arg.options.join("|")}>`;
486
+ return fallbackValueHint;
487
+ }
488
+ function renderDescription(arg, required) {
489
+ const requiredHint = required ? gray("(Required)") : "";
490
+ const defaultHint = arg.default === undefined ? "" : gray(`(Default: ${arg.default})`);
491
+ return [
492
+ arg.description,
493
+ requiredHint,
494
+ defaultHint
495
+ ].filter(Boolean).join(" ");
496
+ }
497
+ async function runMain(cmd, opts = {}) {
498
+ const rawArgs = opts.rawArgs || process.argv.slice(2);
499
+ const showUsage$1 = opts.showUsage || showUsage;
500
+ try {
501
+ const builtinFlags = await _resolveBuiltinFlags(cmd);
502
+ if (builtinFlags.help.length > 0 && rawArgs.some((arg) => builtinFlags.help.includes(arg))) {
503
+ await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
504
+ process.exit(0);
505
+ } else if (rawArgs.length === 1 && builtinFlags.version.includes(rawArgs[0])) {
506
+ const meta = typeof cmd.meta === "function" ? await cmd.meta() : await cmd.meta;
507
+ if (!meta?.version)
508
+ throw new CLIError("No version specified", "E_NO_VERSION");
509
+ console.log(meta.version);
510
+ } else
511
+ await runCommand(cmd, { rawArgs });
512
+ } catch (error) {
513
+ if (error instanceof CLIError) {
514
+ await showUsage$1(...await resolveSubCommand(cmd, rawArgs));
515
+ console.error(error.message);
516
+ } else
517
+ console.error(error, `
518
+ `);
519
+ process.exit(1);
520
+ }
521
+ }
522
+ async function _resolveBuiltinFlags(cmd) {
523
+ const argsDef = await resolveValue(cmd.args || {});
524
+ const userNames = /* @__PURE__ */ new Set;
525
+ const userAliases = /* @__PURE__ */ new Set;
526
+ for (const [name, def] of Object.entries(argsDef)) {
527
+ userNames.add(name);
528
+ for (const alias of toArray(def.alias))
529
+ userAliases.add(alias);
530
+ }
531
+ return {
532
+ help: _getBuiltinFlags("help", "h", userNames, userAliases),
533
+ version: _getBuiltinFlags("version", "v", userNames, userAliases)
534
+ };
535
+ }
536
+ function _getBuiltinFlags(long, short, userNames, userAliases) {
537
+ if (userNames.has(long) || userAliases.has(long))
538
+ return [];
539
+ if (userNames.has(short) || userAliases.has(short))
540
+ return [`--${long}`];
541
+ return [`--${long}`, `-${short}`];
542
+ }
543
+ // package.json
544
+ var package_default = {
545
+ name: "tmdb-kit",
546
+ type: "module",
547
+ version: "1.1.0",
548
+ description: "A tiny, typed, zero-runtime-dependency TMDB SDK and CLI for Node.js, Bun, Deno, and edge runtimes.",
549
+ author: "isaced",
550
+ license: "MIT",
551
+ homepage: "https://github.com/isaced/tmdb-kit#readme",
552
+ keywords: [
553
+ "tmdb",
554
+ "themoviedb",
555
+ "sdk",
556
+ "typescript",
557
+ "node",
558
+ "bun",
559
+ "cli",
560
+ "deno",
561
+ "edge",
562
+ "esm",
563
+ "cjs"
564
+ ],
565
+ repository: {
566
+ type: "git",
567
+ url: "git+https://github.com/isaced/tmdb-kit.git"
568
+ },
569
+ bugs: {
570
+ url: "https://github.com/isaced/tmdb-kit/issues"
571
+ },
572
+ sideEffects: false,
573
+ engines: {
574
+ node: ">=18"
575
+ },
576
+ packageManager: "bun@1.4.4",
577
+ exports: {
578
+ ".": {
579
+ import: {
580
+ types: "./dist/index.d.ts",
581
+ default: "./dist/index.js"
582
+ },
583
+ require: {
584
+ types: "./dist/index.d.cts",
585
+ default: "./dist/index.cjs"
586
+ },
587
+ default: "./dist/index.js"
588
+ },
589
+ "./package.json": "./package.json"
590
+ },
591
+ main: "./dist/index.cjs",
592
+ module: "./dist/index.js",
593
+ types: "./dist/index.d.ts",
594
+ bin: {
595
+ tmdb: "./dist/cli.js",
596
+ "tmdb-kit": "./dist/cli.js"
597
+ },
598
+ files: [
599
+ "dist",
600
+ "README.md"
601
+ ],
602
+ publishConfig: {
603
+ access: "public",
604
+ provenance: true
605
+ },
606
+ scripts: {
607
+ build: "bun run scripts/build.ts",
608
+ "build:cli": "bun run scripts/build.ts --cli",
609
+ cli: "bun run build:cli && bun ./dist/cli.js",
610
+ dev: "bun build ./src/index.ts --outdir ./dist --entry-naming '[name].js' --format esm --target browser --sourcemap=linked --watch",
611
+ test: "bun test --isolate tests/unit tests/resources tests/cli",
612
+ "test:resources": "bun test --isolate tests/resources",
613
+ "test:unit": "bun test --isolate tests/unit",
614
+ "test:watch": "bun test --isolate --watch tests/unit tests/resources tests/cli",
615
+ "test:coverage": "bun test --isolate --coverage tests/unit tests/resources tests/cli",
616
+ "test:integration": "bun test --isolate tests/integration",
617
+ typecheck: "bun check",
618
+ format: "bun fmt",
619
+ "format:check": "bun fmt --check",
620
+ ci: "bun run format:check && bun run typecheck && bun run test:coverage && bun run build",
621
+ release: "bumpp",
622
+ prepublishOnly: "bun run ci"
623
+ },
624
+ devDependencies: {
625
+ "@types/bun": "^1.4.4",
626
+ "@types/node": "^25.6.2",
627
+ bumpp: "^11.1.0",
628
+ citty: "^0.2.2",
629
+ typescript: "^6.0.3"
630
+ }
631
+ };
632
+
633
+ // src/cli/commands/auth.ts
634
+ import { existsSync as existsSync2 } from "node:fs";
635
+
636
+ // src/errors.ts
637
+ class TMDBError extends Error {
638
+ constructor(message, options) {
639
+ super(message, options);
640
+ this.name = "TMDBError";
641
+ }
642
+ }
643
+
644
+ class TMDBRequestError extends TMDBError {
645
+ constructor(message, options) {
646
+ super(message, options);
647
+ this.name = "TMDBRequestError";
648
+ }
649
+ }
650
+
651
+ class TMDBResponseError extends TMDBError {
652
+ body;
653
+ headers;
654
+ requestId;
655
+ status;
656
+ statusCode;
657
+ statusMessage;
658
+ statusText;
659
+ url;
660
+ constructor(message, options) {
661
+ super(message);
662
+ this.name = "TMDBResponseError";
663
+ this.body = options.body;
664
+ this.headers = options.headers;
665
+ this.requestId = options.headers.get("x-request-id");
666
+ this.status = options.status;
667
+ this.statusText = options.statusText;
668
+ this.url = options.url;
669
+ const body = isErrorResponseBody(options.body) ? options.body : undefined;
670
+ this.statusCode = body?.status_code;
671
+ this.statusMessage = body?.status_message;
672
+ }
673
+ get isRateLimit() {
674
+ return this.status === 429;
675
+ }
676
+ get isNotFound() {
677
+ return this.status === 404;
678
+ }
679
+ get isUnauthorized() {
680
+ return this.status === 401;
681
+ }
682
+ get isForbidden() {
683
+ return this.status === 403;
684
+ }
685
+ get isServerError() {
686
+ return this.status >= 500 && this.status < 600;
687
+ }
688
+ }
689
+
690
+ class TMDBRateLimitError extends TMDBResponseError {
691
+ retryAfter;
692
+ constructor(message, options) {
693
+ super(message, options);
694
+ this.name = "TMDBRateLimitError";
695
+ this.retryAfter = options.retryAfter;
696
+ }
697
+ }
698
+ function createResponseError(options) {
699
+ const body = isErrorResponseBody(options.body) ? options.body : undefined;
700
+ const message = body?.status_message ?? `${options.status} ${options.statusText}`.trim();
701
+ if (options.status === 429) {
702
+ return new TMDBRateLimitError(message, {
703
+ ...options,
704
+ retryAfter: parseRetryAfter(options.headers.get("retry-after"))
705
+ });
706
+ }
707
+ return new TMDBResponseError(message, options);
708
+ }
709
+ function parseRetryAfter(value) {
710
+ if (value === null || value === undefined) {
711
+ return 1;
712
+ }
713
+ const trimmed = value.trim();
714
+ if (trimmed === "") {
715
+ return 1;
716
+ }
717
+ const seconds = Number(trimmed);
718
+ if (Number.isFinite(seconds) && seconds >= 0) {
719
+ return seconds;
720
+ }
721
+ const date = Date.parse(trimmed);
722
+ if (Number.isFinite(date)) {
723
+ const deltaMs = date - Date.now();
724
+ return Math.max(1, Math.ceil(deltaMs / 1000));
725
+ }
726
+ return 1;
727
+ }
728
+ function isErrorResponseBody(value) {
729
+ return typeof value === "object" && value !== null;
730
+ }
731
+
732
+ // src/query.ts
733
+ var DEFAULT_API_BASE_URL = "https://api.themoviedb.org/3";
734
+ var DEFAULT_IMAGE_BASE_URL = "https://image.tmdb.org/t/p";
735
+ function appendQuery(url, query) {
736
+ for (const [key, value] of Object.entries(query)) {
737
+ if (value === undefined || value === null) {
738
+ continue;
739
+ }
740
+ if (Array.isArray(value)) {
741
+ url.searchParams.set(key, value.map(String).join(","));
742
+ continue;
743
+ }
744
+ url.searchParams.set(key, String(value));
745
+ }
746
+ }
747
+ function withLanguage(defaults, query) {
748
+ const language = typeof query.language === "string" ? query.language : defaults.language;
749
+ return {
750
+ ...query,
751
+ language
752
+ };
753
+ }
754
+ function withListDefaults(defaults, query) {
755
+ const region = typeof query.region === "string" ? query.region : defaults.region;
756
+ return {
757
+ ...withLanguage(defaults, query),
758
+ region
759
+ };
760
+ }
761
+ function toCommaList(value) {
762
+ if (value === undefined) {
763
+ return;
764
+ }
765
+ return typeof value === "string" ? value : value.join(",");
766
+ }
767
+ function toId(value, name) {
768
+ if (!Number.isInteger(value) || value <= 0) {
769
+ throw new TMDBRequestError(`${name} must be a positive integer`);
770
+ }
771
+ return String(value);
772
+ }
773
+ function toSearchQuery(value) {
774
+ const query = value.trim();
775
+ if (query.length === 0) {
776
+ throw new TMDBRequestError("search query must not be empty");
777
+ }
778
+ return query;
779
+ }
780
+ function toTimeWindow(value) {
781
+ if (value !== "day" && value !== "week") {
782
+ throw new TMDBRequestError('timeWindow must be either "day" or "week"');
783
+ }
784
+ return value;
785
+ }
786
+ function buildImageUrl(path, size = "original", options = {}) {
787
+ if (path === undefined || path === null || path.trim() === "") {
788
+ return null;
789
+ }
790
+ const baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_IMAGE_BASE_URL);
791
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
792
+ const url = `${baseUrl}/${size}${normalizedPath}`;
793
+ return options.transform ? options.transform(url) : url;
794
+ }
795
+ function normalizeBaseUrl(baseUrl) {
796
+ return baseUrl.replace(/\/+$/, "");
797
+ }
798
+
799
+ // src/http.ts
800
+ class TMDBHttpClient {
801
+ defaults;
802
+ imageTransform;
803
+ #apiKey;
804
+ #accessToken;
805
+ #baseUrl;
806
+ #fetch;
807
+ #headers;
808
+ constructor(options) {
809
+ const accessToken = normalizeSecret("accessToken" in options ? options.accessToken : undefined);
810
+ const apiKey = normalizeSecret("apiKey" in options ? options.apiKey : undefined);
811
+ if (Boolean(accessToken) === Boolean(apiKey)) {
812
+ throw new TMDBRequestError("Provide exactly one of accessToken or apiKey");
813
+ }
814
+ this.#accessToken = accessToken;
815
+ this.#apiKey = apiKey;
816
+ this.#baseUrl = normalizeBaseUrl(options.baseUrl ?? DEFAULT_API_BASE_URL);
817
+ this.#fetch = options.fetch;
818
+ this.#headers = options.headers;
819
+ this.defaults = {
820
+ imageBaseUrl: normalizeBaseUrl(options.imageBaseUrl ?? DEFAULT_IMAGE_BASE_URL),
821
+ language: options.defaultLanguage,
822
+ region: options.defaultRegion
823
+ };
824
+ this.imageTransform = options.images?.transform;
825
+ }
826
+ async get(path, options = {}) {
827
+ const url = this.#createUrl(path, options.query ?? {});
828
+ const headers = this.#createHeaders(options.headers);
829
+ const fetchImpl = this.#resolveFetch();
830
+ let response;
831
+ try {
832
+ response = await fetchImpl(url, {
833
+ headers,
834
+ method: "GET",
835
+ signal: options.signal
836
+ });
837
+ } catch (error) {
838
+ throw new TMDBRequestError(`TMDB request failed: ${String(error)}`, { cause: error });
839
+ }
840
+ const body = await readJson(response);
841
+ if (!response.ok) {
842
+ throw createResponseError({
843
+ body,
844
+ headers: response.headers,
845
+ status: response.status,
846
+ statusText: response.statusText,
847
+ url
848
+ });
849
+ }
850
+ return body;
851
+ }
852
+ #resolveFetch() {
853
+ if (this.#fetch !== undefined) {
854
+ return this.#fetch;
855
+ }
856
+ const globalFetch = globalThis.fetch;
857
+ if (typeof globalFetch !== "function") {
858
+ throw new TMDBRequestError("No fetch implementation found. Pass options.fetch in this runtime.");
859
+ }
860
+ return globalFetch.bind(globalThis);
861
+ }
862
+ #createHeaders(headersInit) {
863
+ const headers = new Headers(this.#headers);
864
+ const requestHeaders = new Headers(headersInit);
865
+ headers.set("Accept", "application/json");
866
+ for (const [key, value] of requestHeaders) {
867
+ headers.set(key, value);
868
+ }
869
+ if (this.#accessToken !== undefined) {
870
+ headers.set("Authorization", `Bearer ${this.#accessToken}`);
871
+ }
872
+ return headers;
873
+ }
874
+ #createUrl(path, query) {
875
+ const relativePath = path.replace(/^\/+/, "");
876
+ const url = new URL(relativePath, `${this.#baseUrl}/`);
877
+ appendQuery(url, {
878
+ ...query,
879
+ api_key: this.#apiKey
880
+ });
881
+ return url.toString();
882
+ }
883
+ }
884
+ async function readJson(response) {
885
+ const text = await response.text();
886
+ if (text.length === 0) {
887
+ return;
888
+ }
889
+ try {
890
+ return JSON.parse(text);
891
+ } catch {
892
+ return text;
893
+ }
894
+ }
895
+ function normalizeSecret(value) {
896
+ const secret = value?.trim();
897
+ return secret === "" ? undefined : secret;
898
+ }
899
+
900
+ // src/resources/configuration.ts
901
+ class ConfigurationResource {
902
+ #transport;
903
+ constructor(transport) {
904
+ this.#transport = transport;
905
+ }
906
+ details() {
907
+ return this.#transport.get("/configuration");
908
+ }
909
+ countries() {
910
+ return this.#transport.get("/configuration/countries");
911
+ }
912
+ jobs() {
913
+ return this.#transport.get("/configuration/jobs");
914
+ }
915
+ languages() {
916
+ return this.#transport.get("/configuration/languages");
917
+ }
918
+ primaryTranslations() {
919
+ return this.#transport.get("/configuration/primary_translations");
920
+ }
921
+ timezones() {
922
+ return this.#transport.get("/configuration/timezones");
923
+ }
924
+ }
925
+
926
+ // src/resources/genres.ts
927
+ class GenresResource {
928
+ #transport;
929
+ constructor(transport) {
930
+ this.#transport = transport;
931
+ }
932
+ movies(options = {}) {
933
+ return this.#transport.get("/genre/movie/list", {
934
+ query: withLanguage(this.#transport.defaults, {
935
+ language: options.language
936
+ })
937
+ });
938
+ }
939
+ tv(options = {}) {
940
+ return this.#transport.get("/genre/tv/list", {
941
+ query: withLanguage(this.#transport.defaults, {
942
+ language: options.language
943
+ })
944
+ });
945
+ }
946
+ }
947
+
948
+ // src/resources/images.ts
949
+ class ImagesHelper {
950
+ #transport;
951
+ #transform;
952
+ constructor(transport, transform) {
953
+ this.#transport = transport;
954
+ this.#transform = transform;
955
+ }
956
+ url(path, size = "original", options = {}) {
957
+ return buildImageUrl(path, size, {
958
+ baseUrl: this.#transport.defaults.imageBaseUrl,
959
+ transform: options.transform ?? this.#transform
960
+ });
961
+ }
962
+ }
963
+
964
+ // src/resources/movies.ts
965
+ class MoviesResource {
966
+ #transport;
967
+ constructor(transport) {
968
+ this.#transport = transport;
969
+ }
970
+ popular(options = {}) {
971
+ return this.#transport.get("/movie/popular", {
972
+ query: movieListQuery(this.#transport, options)
973
+ });
974
+ }
975
+ topRated(options = {}) {
976
+ return this.#transport.get("/movie/top_rated", {
977
+ query: movieListQuery(this.#transport, options)
978
+ });
979
+ }
980
+ nowPlaying(options = {}) {
981
+ return this.#transport.get("/movie/now_playing", {
982
+ query: movieListQuery(this.#transport, options)
983
+ });
984
+ }
985
+ upcoming(options = {}) {
986
+ return this.#transport.get("/movie/upcoming", {
987
+ query: movieListQuery(this.#transport, options)
988
+ });
989
+ }
990
+ details(movieId, options = {}) {
991
+ return this.#transport.get(`/movie/${toId(movieId, "movieId")}`, {
992
+ query: withLanguage(this.#transport.defaults, {
993
+ append_to_response: toCommaList(options.appendToResponse),
994
+ language: options.language
995
+ })
996
+ });
997
+ }
998
+ credits(movieId, options = {}) {
999
+ return this.#transport.get(`/movie/${toId(movieId, "movieId")}/credits`, {
1000
+ query: withLanguage(this.#transport.defaults, {
1001
+ language: options.language
1002
+ })
1003
+ });
1004
+ }
1005
+ images(movieId, options = {}) {
1006
+ return this.#transport.get(`/movie/${toId(movieId, "movieId")}/images`, {
1007
+ query: withLanguage(this.#transport.defaults, {
1008
+ include_image_language: toCommaList(options.includeImageLanguage),
1009
+ language: options.language
1010
+ })
1011
+ });
1012
+ }
1013
+ recommendations(movieId, options = {}) {
1014
+ return this.#transport.get(`/movie/${toId(movieId, "movieId")}/recommendations`, {
1015
+ query: movieListQuery(this.#transport, options)
1016
+ });
1017
+ }
1018
+ similar(movieId, options = {}) {
1019
+ return this.#transport.get(`/movie/${toId(movieId, "movieId")}/similar`, {
1020
+ query: movieListQuery(this.#transport, options)
1021
+ });
1022
+ }
1023
+ videos(movieId, options = {}) {
1024
+ return this.#transport.get(`/movie/${toId(movieId, "movieId")}/videos`, {
1025
+ query: withLanguage(this.#transport.defaults, {
1026
+ language: options.language
1027
+ })
1028
+ });
1029
+ }
1030
+ }
1031
+ function movieListQuery(transport, options) {
1032
+ return withListDefaults(transport.defaults, {
1033
+ language: options.language,
1034
+ page: options.page,
1035
+ region: options.region
1036
+ });
1037
+ }
1038
+
1039
+ // src/resources/people.ts
1040
+ class PeopleResource {
1041
+ #transport;
1042
+ constructor(transport) {
1043
+ this.#transport = transport;
1044
+ }
1045
+ popular(options = {}) {
1046
+ return this.#transport.get("/person/popular", {
1047
+ query: peopleListQuery(this.#transport, options)
1048
+ });
1049
+ }
1050
+ details(personId, options = {}) {
1051
+ return this.#transport.get(`/person/${toId(personId, "personId")}`, {
1052
+ query: withLanguage(this.#transport.defaults, {
1053
+ language: options.language
1054
+ })
1055
+ });
1056
+ }
1057
+ combinedCredits(personId, options = {}) {
1058
+ return this.#transport.get(`/person/${toId(personId, "personId")}/combined_credits`, {
1059
+ query: withLanguage(this.#transport.defaults, {
1060
+ language: options.language
1061
+ })
1062
+ });
1063
+ }
1064
+ }
1065
+ function peopleListQuery(transport, options) {
1066
+ return withLanguage(transport.defaults, {
1067
+ language: options.language,
1068
+ page: options.page
1069
+ });
1070
+ }
1071
+
1072
+ // src/resources/search.ts
1073
+ class SearchResource {
1074
+ #transport;
1075
+ constructor(transport) {
1076
+ this.#transport = transport;
1077
+ }
1078
+ movies(query, options = {}) {
1079
+ return this.#transport.get("/search/movie", {
1080
+ query: movieSearchQuery(this.#transport, query, options)
1081
+ });
1082
+ }
1083
+ tv(query, options = {}) {
1084
+ return this.#transport.get("/search/tv", {
1085
+ query: tvSearchQuery(this.#transport, query, options)
1086
+ });
1087
+ }
1088
+ people(query, options = {}) {
1089
+ return this.#transport.get("/search/person", {
1090
+ query: commonSearchQuery(this.#transport, query, options)
1091
+ });
1092
+ }
1093
+ async multi(query, options = {}) {
1094
+ const response = await this.#transport.get("/search/multi", {
1095
+ query: commonSearchQuery(this.#transport, query, options)
1096
+ });
1097
+ const excluded = options.exclude;
1098
+ if (excluded === undefined || excluded.length === 0) {
1099
+ return response;
1100
+ }
1101
+ const excludedSet = new Set(excluded);
1102
+ return {
1103
+ ...response,
1104
+ results: response.results.filter((result) => !excludedSet.has(result.media_type))
1105
+ };
1106
+ }
1107
+ }
1108
+ function movieSearchQuery(transport, query, options) {
1109
+ return withListDefaults(transport.defaults, {
1110
+ include_adult: options.includeAdult,
1111
+ language: options.language,
1112
+ page: options.page,
1113
+ primary_release_year: options.primaryReleaseYear,
1114
+ query: toSearchQuery(query),
1115
+ region: options.region,
1116
+ year: options.year
1117
+ });
1118
+ }
1119
+ function tvSearchQuery(transport, query, options) {
1120
+ return withLanguage(transport.defaults, {
1121
+ first_air_date_year: options.firstAirDateYear,
1122
+ include_adult: options.includeAdult,
1123
+ language: options.language,
1124
+ page: options.page,
1125
+ query: toSearchQuery(query),
1126
+ year: options.year
1127
+ });
1128
+ }
1129
+ function commonSearchQuery(transport, query, options) {
1130
+ return withLanguage(transport.defaults, {
1131
+ include_adult: options.includeAdult,
1132
+ language: options.language,
1133
+ page: options.page,
1134
+ query: toSearchQuery(query)
1135
+ });
1136
+ }
1137
+
1138
+ // src/resources/trending.ts
1139
+ class TrendingResource {
1140
+ #transport;
1141
+ constructor(transport) {
1142
+ this.#transport = transport;
1143
+ }
1144
+ all(timeWindow = "day", options = {}) {
1145
+ return this.#transport.get(`/trending/all/${toTimeWindow(timeWindow)}`, {
1146
+ query: withLanguage(this.#transport.defaults, {
1147
+ language: options.language,
1148
+ page: options.page
1149
+ })
1150
+ });
1151
+ }
1152
+ movies(timeWindow = "day", options = {}) {
1153
+ return this.#transport.get(`/trending/movie/${toTimeWindow(timeWindow)}`, {
1154
+ query: withLanguage(this.#transport.defaults, {
1155
+ language: options.language,
1156
+ page: options.page
1157
+ })
1158
+ });
1159
+ }
1160
+ tv(timeWindow = "day", options = {}) {
1161
+ return this.#transport.get(`/trending/tv/${toTimeWindow(timeWindow)}`, {
1162
+ query: withLanguage(this.#transport.defaults, {
1163
+ language: options.language,
1164
+ page: options.page
1165
+ })
1166
+ });
1167
+ }
1168
+ people(timeWindow = "day", options = {}) {
1169
+ return this.#transport.get(`/trending/person/${toTimeWindow(timeWindow)}`, {
1170
+ query: withLanguage(this.#transport.defaults, {
1171
+ language: options.language,
1172
+ page: options.page
1173
+ })
1174
+ });
1175
+ }
1176
+ }
1177
+
1178
+ // src/resources/tv.ts
1179
+ class TVResource {
1180
+ #transport;
1181
+ constructor(transport) {
1182
+ this.#transport = transport;
1183
+ }
1184
+ popular(options = {}) {
1185
+ return this.#transport.get("/tv/popular", {
1186
+ query: tvListQuery(this.#transport, options)
1187
+ });
1188
+ }
1189
+ topRated(options = {}) {
1190
+ return this.#transport.get("/tv/top_rated", {
1191
+ query: tvListQuery(this.#transport, options)
1192
+ });
1193
+ }
1194
+ airingToday(options = {}) {
1195
+ return this.#transport.get("/tv/airing_today", {
1196
+ query: tvListQuery(this.#transport, options)
1197
+ });
1198
+ }
1199
+ onTheAir(options = {}) {
1200
+ return this.#transport.get("/tv/on_the_air", {
1201
+ query: tvListQuery(this.#transport, options)
1202
+ });
1203
+ }
1204
+ details(seriesId, options = {}) {
1205
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}`, {
1206
+ query: withLanguage(this.#transport.defaults, {
1207
+ append_to_response: toCommaList(options.appendToResponse),
1208
+ language: options.language
1209
+ })
1210
+ });
1211
+ }
1212
+ credits(seriesId, options = {}) {
1213
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/credits`, {
1214
+ query: withLanguage(this.#transport.defaults, {
1215
+ language: options.language
1216
+ })
1217
+ });
1218
+ }
1219
+ images(seriesId, options = {}) {
1220
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/images`, {
1221
+ query: withLanguage(this.#transport.defaults, {
1222
+ include_image_language: toCommaList(options.includeImageLanguage),
1223
+ language: options.language
1224
+ })
1225
+ });
1226
+ }
1227
+ recommendations(seriesId, options = {}) {
1228
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/recommendations`, {
1229
+ query: tvListQuery(this.#transport, options)
1230
+ });
1231
+ }
1232
+ similar(seriesId, options = {}) {
1233
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/similar`, {
1234
+ query: tvListQuery(this.#transport, options)
1235
+ });
1236
+ }
1237
+ videos(seriesId, options = {}) {
1238
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/videos`, {
1239
+ query: withLanguage(this.#transport.defaults, {
1240
+ language: options.language
1241
+ })
1242
+ });
1243
+ }
1244
+ seasonDetails(seriesId, seasonNumber, options = {}) {
1245
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toId(seasonNumber, "seasonNumber")}`, {
1246
+ query: withLanguage(this.#transport.defaults, {
1247
+ append_to_response: toCommaList(options.appendToResponse),
1248
+ language: options.language
1249
+ })
1250
+ });
1251
+ }
1252
+ }
1253
+ function tvListQuery(transport, options) {
1254
+ return withLanguage(transport.defaults, {
1255
+ language: options.language,
1256
+ page: options.page
1257
+ });
1258
+ }
1259
+
1260
+ // src/client.ts
1261
+ class TMDBClient {
1262
+ configuration;
1263
+ genres;
1264
+ images;
1265
+ movies;
1266
+ people;
1267
+ search;
1268
+ trending;
1269
+ tv;
1270
+ #transport;
1271
+ constructor(options) {
1272
+ const { transport, images, ...httpOptions } = options;
1273
+ const fallbackTransform = images?.transform;
1274
+ const builtInClient = transport === undefined ? new TMDBHttpClient({ ...httpOptions, images }) : undefined;
1275
+ this.#transport = builtInClient ?? transport;
1276
+ this.configuration = new ConfigurationResource(this.#transport);
1277
+ this.genres = new GenresResource(this.#transport);
1278
+ this.images = new ImagesHelper(this.#transport, builtInClient ? builtInClient.imageTransform : fallbackTransform);
1279
+ this.movies = new MoviesResource(this.#transport);
1280
+ this.people = new PeopleResource(this.#transport);
1281
+ this.search = new SearchResource(this.#transport);
1282
+ this.trending = new TrendingResource(this.#transport);
1283
+ this.tv = new TVResource(this.#transport);
1284
+ }
1285
+ request(path, options) {
1286
+ return this.#transport.get(path, options);
1287
+ }
1288
+ }
1289
+ function createTMDB(options) {
1290
+ return new TMDBClient(options);
1291
+ }
1292
+ // src/cli/config.ts
1293
+ import {
1294
+ chmodSync,
1295
+ existsSync,
1296
+ mkdirSync,
1297
+ readFileSync,
1298
+ renameSync,
1299
+ unlinkSync,
1300
+ writeFileSync
1301
+ } from "node:fs";
1302
+ import { homedir } from "node:os";
1303
+ import { dirname, join } from "node:path";
1304
+
1305
+ // src/cli/errors.ts
1306
+ class CliError extends Error {
1307
+ exitCode;
1308
+ constructor(message, exitCode = 1) {
1309
+ super(message);
1310
+ this.name = "CliError";
1311
+ this.exitCode = exitCode;
1312
+ }
1313
+ }
1314
+ function exitCodeFor(error) {
1315
+ if (error instanceof CliError) {
1316
+ return error.exitCode;
1317
+ }
1318
+ if (error instanceof TMDBRateLimitError) {
1319
+ return 5;
1320
+ }
1321
+ if (error instanceof TMDBResponseError) {
1322
+ if (error.status === 401 || error.status === 403)
1323
+ return 3;
1324
+ if (error.status === 404)
1325
+ return 4;
1326
+ if (error.status === 429)
1327
+ return 5;
1328
+ }
1329
+ return 1;
1330
+ }
1331
+ function errorMessage(error) {
1332
+ if (error instanceof CliError) {
1333
+ return error.message;
1334
+ }
1335
+ if (error instanceof TMDBRateLimitError) {
1336
+ return `TMDB rate limit exceeded. Retry after ${error.retryAfter} seconds.`;
1337
+ }
1338
+ if (error instanceof TMDBResponseError) {
1339
+ if (error.status === 401 || error.status === 403) {
1340
+ return [
1341
+ `TMDB rejected the credentials (HTTP ${error.status}): ${error.message}`,
1342
+ "Run `tmdb auth login` to replace the token, or check `tmdb config show`."
1343
+ ].join(`
1344
+ `);
1345
+ }
1346
+ return `TMDB request failed (${error.status}): ${error.message}`;
1347
+ }
1348
+ if (error instanceof TMDBRequestError) {
1349
+ return error.message;
1350
+ }
1351
+ if (error instanceof Error) {
1352
+ return error.message;
1353
+ }
1354
+ return String(error);
1355
+ }
1356
+ async function runSafely(task) {
1357
+ try {
1358
+ await task();
1359
+ } catch (error) {
1360
+ process.stderr.write(`${errorMessage(error)}
1361
+ `);
1362
+ process.exitCode = exitCodeFor(error);
1363
+ }
1364
+ }
1365
+
1366
+ // src/cli/config.ts
1367
+ var CONFIG_DIR = "tmdb-kit";
1368
+ function getConfigPath() {
1369
+ if (process.platform === "win32") {
1370
+ return join(process.env.APPDATA ?? homedir(), CONFIG_DIR, "config.json");
1371
+ }
1372
+ const configHome = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
1373
+ return join(configHome, CONFIG_DIR, "config.json");
1374
+ }
1375
+ function readConfig() {
1376
+ const path = getConfigPath();
1377
+ let raw;
1378
+ try {
1379
+ raw = readFileSync(path, "utf8");
1380
+ } catch (error) {
1381
+ if (isNotFound(error)) {
1382
+ return {};
1383
+ }
1384
+ throw new CliError(`Failed to read config file at ${path}: ${messageOf(error)}`, 3);
1385
+ }
1386
+ try {
1387
+ const parsed = JSON.parse(raw);
1388
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1389
+ throw new Error("expected a JSON object");
1390
+ }
1391
+ const record = parsed;
1392
+ return {
1393
+ accessToken: readStringField(record, "accessToken"),
1394
+ apiKey: readStringField(record, "apiKey"),
1395
+ language: readStringField(record, "language"),
1396
+ region: readStringField(record, "region")
1397
+ };
1398
+ } catch (error) {
1399
+ throw new CliError(`Invalid config file at ${path}: ${messageOf(error)}`, 3);
1400
+ }
1401
+ }
1402
+ function setConfigValue(field, value) {
1403
+ writeConfig({ ...readConfig(), [field]: value });
1404
+ }
1405
+ function unsetConfigValue(field) {
1406
+ const config = readConfig();
1407
+ delete config[field];
1408
+ writeConfig(config);
1409
+ }
1410
+ function writeConfig(config) {
1411
+ const path = getConfigPath();
1412
+ const directory = dirname(path);
1413
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
1414
+ try {
1415
+ mkdirSync(directory, { recursive: true, mode: 448 });
1416
+ if (process.platform !== "win32") {
1417
+ chmodSync(directory, 448);
1418
+ }
1419
+ writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}
1420
+ `, {
1421
+ mode: 384
1422
+ });
1423
+ if (process.platform !== "win32") {
1424
+ chmodSync(tempPath, 384);
1425
+ }
1426
+ renameSync(tempPath, path);
1427
+ if (process.platform !== "win32") {
1428
+ chmodSync(path, 384);
1429
+ }
1430
+ } catch (error) {
1431
+ if (existsSync(tempPath)) {
1432
+ unlinkSync(tempPath);
1433
+ }
1434
+ throw new CliError(`Failed to write config file at ${path}: ${messageOf(error)}`, 3);
1435
+ }
1436
+ }
1437
+ function readStringField(record, key) {
1438
+ const value = record[key];
1439
+ if (value === undefined) {
1440
+ return;
1441
+ }
1442
+ if (typeof value !== "string") {
1443
+ throw new Error(`${key} must be a string`);
1444
+ }
1445
+ return value;
1446
+ }
1447
+ function isNotFound(error) {
1448
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
1449
+ }
1450
+ function messageOf(error) {
1451
+ return error instanceof Error ? error.message : String(error);
1452
+ }
1453
+
1454
+ // src/cli/credential-format.ts
1455
+ var ACCESS_TOKEN_PATTERN = /^eyJ[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
1456
+ var API_KEY_PATTERN = /^[a-fA-F0-9]{32}$/;
1457
+ function detectCredential(input) {
1458
+ const value = input.trim().replace(/^Bearer\s+/i, "");
1459
+ if (API_KEY_PATTERN.test(value)) {
1460
+ return { kind: "apiKey", value };
1461
+ }
1462
+ if (ACCESS_TOKEN_PATTERN.test(value)) {
1463
+ return { kind: "accessToken", value };
1464
+ }
1465
+ return;
1466
+ }
1467
+ function unrecognizedCredentialMessage() {
1468
+ return [
1469
+ "Unrecognized TMDB credential format.",
1470
+ "",
1471
+ "Expected one of:",
1472
+ " - API Read Access Token (JWT, three dot-separated segments)",
1473
+ " - API Key (32 hexadecimal characters)",
1474
+ "",
1475
+ "Create credentials:",
1476
+ " https://www.themoviedb.org/settings/api"
1477
+ ].join(`
1478
+ `);
1479
+ }
1480
+
1481
+ // src/cli/redact.ts
1482
+ var PLACEHOLDER_CREDENTIALS = new Set(["YOUR_TMDB_ACCESS_TOKEN", "YOUR_TMDB_API_KEY"]);
1483
+ function isPlaceholderCredential(value) {
1484
+ return value !== undefined && PLACEHOLDER_CREDENTIALS.has(value.trim());
1485
+ }
1486
+ function maskSecret(value) {
1487
+ if (!value) {
1488
+ return null;
1489
+ }
1490
+ return value.length <= 4 ? "••••" : `••••${value.slice(-4)}`;
1491
+ }
1492
+ function describeSecret(value) {
1493
+ if (!value) {
1494
+ return null;
1495
+ }
1496
+ return isPlaceholderCredential(value) ? "(placeholder)" : maskSecret(value);
1497
+ }
1498
+
1499
+ // src/cli/credentials.ts
1500
+ function resolveCredentials() {
1501
+ const config = readConfig();
1502
+ const envAccessToken = usableCredential(process.env.TMDB_ACCESS_TOKEN);
1503
+ const envApiKey = usableCredential(process.env.TMDB_API_KEY);
1504
+ const configAccessToken = usableCredential(config.accessToken);
1505
+ const configApiKey = usableCredential(config.apiKey);
1506
+ if (envAccessToken) {
1507
+ return {
1508
+ accessToken: envAccessToken,
1509
+ config,
1510
+ configField: null,
1511
+ envVar: "TMDB_ACCESS_TOKEN",
1512
+ source: "environment"
1513
+ };
1514
+ }
1515
+ if (envApiKey) {
1516
+ return {
1517
+ apiKey: envApiKey,
1518
+ config,
1519
+ configField: null,
1520
+ envVar: "TMDB_API_KEY",
1521
+ source: "environment"
1522
+ };
1523
+ }
1524
+ if (configAccessToken) {
1525
+ return {
1526
+ accessToken: configAccessToken,
1527
+ config,
1528
+ configField: "accessToken",
1529
+ envVar: null,
1530
+ source: "config"
1531
+ };
1532
+ }
1533
+ if (configApiKey) {
1534
+ return {
1535
+ apiKey: configApiKey,
1536
+ config,
1537
+ configField: "apiKey",
1538
+ envVar: null,
1539
+ source: "config"
1540
+ };
1541
+ }
1542
+ return {
1543
+ config,
1544
+ configField: null,
1545
+ envVar: null,
1546
+ source: "none"
1547
+ };
1548
+ }
1549
+ function usableCredential(value) {
1550
+ const trimmed = value?.trim();
1551
+ if (!trimmed || isPlaceholderCredential(trimmed)) {
1552
+ return;
1553
+ }
1554
+ return trimmed;
1555
+ }
1556
+ function missingCredentialsMessage() {
1557
+ return [
1558
+ "No TMDB credentials found.",
1559
+ "",
1560
+ "Recommended:",
1561
+ " tmdb auth login",
1562
+ "",
1563
+ "Or use an environment variable:",
1564
+ " export TMDB_ACCESS_TOKEN=...",
1565
+ " export TMDB_API_KEY=... (legacy v3 API key)",
1566
+ "",
1567
+ "Or edit the config file:",
1568
+ ` ${getConfigPath()}`,
1569
+ "",
1570
+ " {",
1571
+ ' "accessToken": "YOUR_TMDB_ACCESS_TOKEN",',
1572
+ ' "language": "en-US",',
1573
+ ' "region": "US"',
1574
+ " }",
1575
+ "",
1576
+ "Create a token:",
1577
+ " https://www.themoviedb.org/settings/api",
1578
+ "",
1579
+ "Use the API Read Access Token, not the legacy API Key."
1580
+ ].join(`
1581
+ `);
1582
+ }
1583
+
1584
+ // src/cli/format.ts
1585
+ function printJson(value) {
1586
+ process.stdout.write(`${JSON.stringify(value, null, 2)}
1587
+ `);
1588
+ }
1589
+ function printSearchResults(response, type) {
1590
+ const lines = [
1591
+ `Page ${response.page}/${response.total_pages} · ${response.total_results} results`
1592
+ ];
1593
+ if (response.results.length === 0) {
1594
+ lines.push("No results.");
1595
+ } else {
1596
+ response.results.forEach((result, index) => {
1597
+ lines.push(formatSearchResult(result, index, type));
1598
+ });
1599
+ }
1600
+ writeLines(lines);
1601
+ }
1602
+ function printMovie(movie, tmdb) {
1603
+ const year = movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : "";
1604
+ const lines = [`${movie.title}${year}`];
1605
+ if (movie.original_title !== movie.title) {
1606
+ lines.push(`Original title: ${movie.original_title}`);
1607
+ }
1608
+ if (movie.tagline) {
1609
+ lines.push(movie.tagline);
1610
+ }
1611
+ const facts = [
1612
+ movie.status,
1613
+ movie.runtime ? `${movie.runtime} min` : undefined,
1614
+ `★ ${movie.vote_average.toFixed(1)}/10 (${movie.vote_count} votes)`
1615
+ ].filter(Boolean);
1616
+ lines.push(facts.join(" · "));
1617
+ if (movie.genres.length > 0) {
1618
+ lines.push(`Genres: ${movie.genres.map((genre) => genre.name).join(", ")}`);
1619
+ }
1620
+ lines.push(`TMDB: https://www.themoviedb.org/movie/${movie.id}`);
1621
+ const poster = tmdb.images.url(movie.poster_path, "w500");
1622
+ if (poster) {
1623
+ lines.push(`Poster: ${poster}`);
1624
+ }
1625
+ const backdrop = tmdb.images.url(movie.backdrop_path, "w780");
1626
+ if (backdrop) {
1627
+ lines.push(`Backdrop: ${backdrop}`);
1628
+ }
1629
+ if (movie.homepage) {
1630
+ lines.push(`Homepage: ${movie.homepage}`);
1631
+ }
1632
+ if (movie.overview) {
1633
+ lines.push("", movie.overview);
1634
+ }
1635
+ writeLines(lines);
1636
+ }
1637
+ function printTV(tv, tmdb) {
1638
+ const year = tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : "";
1639
+ const lines = [`${tv.name}${year}`];
1640
+ if (tv.original_name !== tv.name) {
1641
+ lines.push(`Original name: ${tv.original_name}`);
1642
+ }
1643
+ if (tv.tagline) {
1644
+ lines.push(tv.tagline);
1645
+ }
1646
+ const facts = [
1647
+ tv.status,
1648
+ `${tv.number_of_seasons} seasons`,
1649
+ `${tv.number_of_episodes} episodes`,
1650
+ `★ ${tv.vote_average.toFixed(1)}/10 (${tv.vote_count} votes)`
1651
+ ].filter(Boolean);
1652
+ lines.push(facts.join(" · "));
1653
+ if (tv.genres.length > 0) {
1654
+ lines.push(`Genres: ${tv.genres.map((genre) => genre.name).join(", ")}`);
1655
+ }
1656
+ if (tv.networks.length > 0) {
1657
+ lines.push(`Networks: ${tv.networks.map((network) => network.name).join(", ")}`);
1658
+ }
1659
+ if (tv.created_by.length > 0) {
1660
+ lines.push(`Created by: ${tv.created_by.map((person) => person.name).join(", ")}`);
1661
+ }
1662
+ lines.push(`TMDB: https://www.themoviedb.org/tv/${tv.id}`);
1663
+ const poster = tmdb.images.url(tv.poster_path, "w500");
1664
+ if (poster) {
1665
+ lines.push(`Poster: ${poster}`);
1666
+ }
1667
+ const backdrop = tmdb.images.url(tv.backdrop_path, "w780");
1668
+ if (backdrop) {
1669
+ lines.push(`Backdrop: ${backdrop}`);
1670
+ }
1671
+ if (tv.homepage) {
1672
+ lines.push(`Homepage: ${tv.homepage}`);
1673
+ }
1674
+ if (tv.overview) {
1675
+ lines.push("", tv.overview);
1676
+ }
1677
+ if (tv.seasons.length > 0) {
1678
+ lines.push("", "Seasons:");
1679
+ for (const season of tv.seasons) {
1680
+ const airDate = season.air_date ? ` (${season.air_date.slice(0, 4)})` : "";
1681
+ lines.push(` ${season.season_number}. ${season.name}${airDate} · ${season.episode_count} episodes`);
1682
+ }
1683
+ }
1684
+ writeLines(lines);
1685
+ }
1686
+ function printCredits(credits, limit = 10) {
1687
+ const lines = [];
1688
+ if (credits.cast.length > 0) {
1689
+ lines.push("Cast:");
1690
+ for (const member of credits.cast.slice(0, limit)) {
1691
+ const character = member.character ? ` as ${member.character}` : "";
1692
+ lines.push(` ${member.name}${character} #${member.id}`);
1693
+ }
1694
+ }
1695
+ if (credits.crew.length > 0) {
1696
+ if (lines.length > 0) {
1697
+ lines.push("");
1698
+ }
1699
+ lines.push("Crew:");
1700
+ for (const member of credits.crew.slice(0, limit)) {
1701
+ lines.push(` ${member.name} — ${member.job} (${member.department}) #${member.id}`);
1702
+ }
1703
+ }
1704
+ if (lines.length === 0) {
1705
+ lines.push("No credits.");
1706
+ }
1707
+ writeLines(lines);
1708
+ }
1709
+ function printImages(images, tmdb) {
1710
+ const lines = [];
1711
+ appendImageSection(lines, "Posters", images.posters, "w500", tmdb);
1712
+ appendImageSection(lines, "Backdrops", images.backdrops, "w780", tmdb);
1713
+ appendImageSection(lines, "Logos", images.logos ?? [], "w300", tmdb);
1714
+ if (lines.length === 0) {
1715
+ lines.push("No images.");
1716
+ }
1717
+ writeLines(lines);
1718
+ }
1719
+ function printVideos(videos) {
1720
+ if (videos.results.length === 0) {
1721
+ writeLines(["No videos."]);
1722
+ return;
1723
+ }
1724
+ const lines = videos.results.map((video) => {
1725
+ const url = video.site.toLowerCase() === "youtube" ? `https://www.youtube.com/watch?v=${video.key}` : video.key;
1726
+ return `${video.type}: ${video.name} ${url}`;
1727
+ });
1728
+ writeLines(lines);
1729
+ }
1730
+ function printSeason(season, tmdb) {
1731
+ const year = season.air_date ? ` (${season.air_date.slice(0, 4)})` : "";
1732
+ const lines = [
1733
+ `${season.name}${year}`,
1734
+ `Episodes: ${season.episodes.length} · ★ ${season.vote_average.toFixed(1)}/10`
1735
+ ];
1736
+ if (season.overview) {
1737
+ lines.push("", season.overview);
1738
+ }
1739
+ const poster = tmdb.images.url(season.poster_path, "w500");
1740
+ if (poster) {
1741
+ lines.push(`Poster: ${poster}`);
1742
+ }
1743
+ if (season.episodes.length > 0) {
1744
+ lines.push("", "Episodes:");
1745
+ for (const episode of season.episodes) {
1746
+ const airDate = episode.air_date ? ` ${episode.air_date}` : "";
1747
+ const runtime = episode.runtime ? ` · ${episode.runtime} min` : "";
1748
+ lines.push(` E${String(episode.episode_number).padStart(2, "0")} ${episode.name}${airDate}${runtime} ★ ${episode.vote_average.toFixed(1)} #${episode.id}`);
1749
+ }
1750
+ }
1751
+ writeLines(lines);
1752
+ }
1753
+ function printPerson(person, tmdb) {
1754
+ const lines = [person.name];
1755
+ const facts = [
1756
+ person.known_for_department,
1757
+ person.birthday ? `Born: ${person.birthday}` : undefined,
1758
+ person.deathday ? `Died: ${person.deathday}` : undefined
1759
+ ].filter(Boolean);
1760
+ if (facts.length > 0) {
1761
+ lines.push(facts.join(" · "));
1762
+ }
1763
+ if (person.place_of_birth) {
1764
+ lines.push(`From: ${person.place_of_birth}`);
1765
+ }
1766
+ lines.push(`TMDB: https://www.themoviedb.org/person/${person.id}`);
1767
+ const profile = tmdb.images.url(person.profile_path, "h632");
1768
+ if (profile) {
1769
+ lines.push(`Profile: ${profile}`);
1770
+ }
1771
+ if (person.homepage) {
1772
+ lines.push(`Homepage: ${person.homepage}`);
1773
+ }
1774
+ if (person.imdb_id) {
1775
+ lines.push(`IMDb: https://www.imdb.com/name/${person.imdb_id}`);
1776
+ }
1777
+ if (person.biography) {
1778
+ lines.push("", person.biography);
1779
+ }
1780
+ writeLines(lines);
1781
+ }
1782
+ function printPersonCredits(credits, limit = 10) {
1783
+ const lines = [];
1784
+ const cast = [...credits.cast].sort(byCreditDate);
1785
+ const crew = [...credits.crew].sort(byCreditDate);
1786
+ if (cast.length > 0) {
1787
+ lines.push("Cast:");
1788
+ for (const item of cast.slice(0, limit)) {
1789
+ lines.push(formatCombinedCast(item));
1790
+ }
1791
+ }
1792
+ if (crew.length > 0) {
1793
+ if (lines.length > 0) {
1794
+ lines.push("");
1795
+ }
1796
+ lines.push("Crew:");
1797
+ for (const item of crew.slice(0, limit)) {
1798
+ lines.push(formatCombinedCrew(item));
1799
+ }
1800
+ }
1801
+ if (lines.length === 0) {
1802
+ lines.push("No credits.");
1803
+ }
1804
+ writeLines(lines);
1805
+ }
1806
+ function printGenres(response) {
1807
+ if (response.genres.length === 0) {
1808
+ writeLines(["No genres."]);
1809
+ return;
1810
+ }
1811
+ writeLines(response.genres.map((genre) => `${genre.id} ${genre.name}`));
1812
+ }
1813
+ function appendImageSection(lines, title, images, size, tmdb) {
1814
+ if (images.length === 0) {
1815
+ return;
1816
+ }
1817
+ if (lines.length > 0) {
1818
+ lines.push("");
1819
+ }
1820
+ lines.push(`${title}:`);
1821
+ for (const image of images) {
1822
+ const url = tmdb.images.url(image.file_path, size);
1823
+ if (url) {
1824
+ lines.push(` ${url} (${image.width}x${image.height})`);
1825
+ }
1826
+ }
1827
+ }
1828
+ function formatCombinedCast(item) {
1829
+ const character = item.character ? ` as ${item.character}` : "";
1830
+ const episodes = item.media_type === "tv" && item.episode_count ? ` · ${item.episode_count} eps` : "";
1831
+ return ` ${creditYear(item)} ${creditTitle(item)}${character}${episodes} (${item.media_type}, ${item.id})`;
1832
+ }
1833
+ function formatCombinedCrew(item) {
1834
+ return ` ${creditYear(item)} ${creditTitle(item)} — ${item.job} (${item.department}) (${item.media_type}, ${item.id})`;
1835
+ }
1836
+ function byCreditDate(a, b) {
1837
+ return creditDate(b).localeCompare(creditDate(a));
1838
+ }
1839
+ function creditDate(item) {
1840
+ return "release_date" in item ? item.release_date : item.first_air_date;
1841
+ }
1842
+ function creditTitle(item) {
1843
+ return "title" in item ? item.title : item.name;
1844
+ }
1845
+ function creditYear(item) {
1846
+ const date = creditDate(item);
1847
+ return date ? date.slice(0, 4) : "----";
1848
+ }
1849
+ function writeLines(lines) {
1850
+ process.stdout.write(`${lines.join(`
1851
+ `)}
1852
+ `);
1853
+ }
1854
+ function formatSearchResult(result, index, fallbackType) {
1855
+ const title = "title" in result ? result.title : result.name;
1856
+ const date = "release_date" in result ? result.release_date : ("first_air_date" in result) ? result.first_air_date : "";
1857
+ const year = date ? ` (${date.slice(0, 4)})` : "";
1858
+ const mediaType = "media_type" in result && result.media_type ? result.media_type : fallbackType;
1859
+ const score = "vote_average" in result && result.vote_average > 0 ? ` ★ ${result.vote_average.toFixed(1)}` : "";
1860
+ return `${String(index + 1).padStart(2)}. ${title}${year}${score} ${mediaType} ${result.id}`;
1861
+ }
1862
+
1863
+ // src/cli/options.ts
1864
+ var jsonArg = {
1865
+ json: {
1866
+ type: "boolean",
1867
+ description: "Print the raw TMDB JSON response"
1868
+ }
1869
+ };
1870
+ var commonArgs = {
1871
+ ...jsonArg,
1872
+ lang: {
1873
+ type: "string",
1874
+ description: "TMDB language code (default: en-US)"
1875
+ }
1876
+ };
1877
+ var SEARCH_TYPES = ["all", "movie", "tv", "person"];
1878
+ function parseSearchType(value) {
1879
+ if (!SEARCH_TYPES.includes(value)) {
1880
+ throw new CliError(`Invalid --type "${value}". Expected one of: ${SEARCH_TYPES.join(", ")}.`, 2);
1881
+ }
1882
+ return value;
1883
+ }
1884
+ function parsePage(value) {
1885
+ if (value === undefined || value === "") {
1886
+ return 1;
1887
+ }
1888
+ const page = Number(value);
1889
+ if (!Number.isInteger(page) || page < 1) {
1890
+ throw new CliError("Invalid --page value: expected a positive integer.", 2);
1891
+ }
1892
+ return page;
1893
+ }
1894
+ function parseId(value, label) {
1895
+ const id = Number(value);
1896
+ if (!Number.isInteger(id) || id <= 0) {
1897
+ throw new CliError(`Invalid ${label}: expected a positive integer.`, 2);
1898
+ }
1899
+ return id;
1900
+ }
1901
+ function parseSeasonNumber(value) {
1902
+ const season = Number(value);
1903
+ if (!Number.isInteger(season) || season < 0) {
1904
+ throw new CliError("Invalid season number: expected a non-negative integer.", 2);
1905
+ }
1906
+ return season;
1907
+ }
1908
+ function parseLimit(value, fallback = 10) {
1909
+ if (value === undefined || value === "") {
1910
+ return fallback;
1911
+ }
1912
+ const limit = Number(value);
1913
+ if (!Number.isInteger(limit) || limit < 1) {
1914
+ throw new CliError("Invalid --limit value: expected a positive integer.", 2);
1915
+ }
1916
+ return limit;
1917
+ }
1918
+ function parseCommaList(value) {
1919
+ if (value === undefined) {
1920
+ return;
1921
+ }
1922
+ const values = value.split(",").map((item) => item.trim()).filter(Boolean);
1923
+ return values.length > 0 ? values : undefined;
1924
+ }
1925
+ var TRENDING_WINDOWS = ["day", "week"];
1926
+ function parseWindow(value) {
1927
+ if (!TRENDING_WINDOWS.includes(value)) {
1928
+ throw new CliError(`Invalid --window "${value}". Expected one of: ${TRENDING_WINDOWS.join(", ")}.`, 2);
1929
+ }
1930
+ return value;
1931
+ }
1932
+ var GENRE_TYPES = ["movie", "tv"];
1933
+ function parseGenreType(value) {
1934
+ if (!GENRE_TYPES.includes(value)) {
1935
+ throw new CliError(`Invalid --type "${value}". Expected one of: ${GENRE_TYPES.join(", ")}.`, 2);
1936
+ }
1937
+ return value;
1938
+ }
1939
+ var MOVIE_LIST_CATEGORIES = ["popular", "top-rated", "now-playing", "upcoming"];
1940
+ function parseMovieListCategory(value) {
1941
+ if (!MOVIE_LIST_CATEGORIES.includes(value)) {
1942
+ throw new CliError(`Invalid movie list category "${value}". Expected one of: ${MOVIE_LIST_CATEGORIES.join(", ")}.`, 2);
1943
+ }
1944
+ return value;
1945
+ }
1946
+ var TV_LIST_CATEGORIES = ["popular", "top-rated", "airing-today", "on-the-air"];
1947
+ function parseTVListCategory(value) {
1948
+ if (!TV_LIST_CATEGORIES.includes(value)) {
1949
+ throw new CliError(`Invalid TV list category "${value}". Expected one of: ${TV_LIST_CATEGORIES.join(", ")}.`, 2);
1950
+ }
1951
+ return value;
1952
+ }
1953
+
1954
+ // src/cli/prompt.ts
1955
+ import { createInterface } from "node:readline/promises";
1956
+ async function readSecret(prompt) {
1957
+ if (!process.stdin.isTTY) {
1958
+ return readStdinSecret();
1959
+ }
1960
+ const rl = createInterface({
1961
+ input: process.stdin,
1962
+ output: process.stdout,
1963
+ terminal: true
1964
+ });
1965
+ const target = rl;
1966
+ const writeToOutput = target._writeToOutput;
1967
+ target._writeToOutput = () => {};
1968
+ try {
1969
+ const answer = await rl.question(prompt);
1970
+ process.stdout.write(`
1971
+ `);
1972
+ return answer.trim();
1973
+ } finally {
1974
+ target._writeToOutput = writeToOutput;
1975
+ rl.close();
1976
+ }
1977
+ }
1978
+ async function confirm(question) {
1979
+ const rl = createInterface({
1980
+ input: process.stdin,
1981
+ output: process.stdout
1982
+ });
1983
+ try {
1984
+ const answer = (await rl.question(question)).trim().toLowerCase();
1985
+ return answer === "y" || answer === "yes";
1986
+ } finally {
1987
+ rl.close();
1988
+ }
1989
+ }
1990
+ async function readStdinSecret() {
1991
+ const chunks = [];
1992
+ for await (const chunk of process.stdin) {
1993
+ chunks.push(Buffer.from(chunk));
1994
+ }
1995
+ return Buffer.concat(chunks).toString("utf8").trim();
1996
+ }
1997
+
1998
+ // src/cli/commands/auth.ts
1999
+ var authCommand = defineCommand({
2000
+ meta: {
2001
+ name: "auth",
2002
+ description: "Manage TMDB API credentials"
2003
+ },
2004
+ subCommands: {
2005
+ login: defineCommand({
2006
+ meta: {
2007
+ name: "login",
2008
+ description: "Validate and save TMDB API credentials"
2009
+ },
2010
+ args: {
2011
+ "token-stdin": {
2012
+ type: "boolean",
2013
+ description: "Read the credential from stdin instead of prompting"
2014
+ },
2015
+ force: {
2016
+ type: "boolean",
2017
+ description: "Replace existing credentials without confirmation"
2018
+ }
2019
+ },
2020
+ async run({ args }) {
2021
+ await runSafely(async () => {
2022
+ const existing = resolveCredentials();
2023
+ if (!args.force && existing.source !== "none") {
2024
+ if (!process.stdin.isTTY) {
2025
+ throw new CliError(existingCredentialsMessage(existing), 2);
2026
+ }
2027
+ process.stdout.write(`${describeExistingCredentials(existing)}
2028
+ `);
2029
+ const question = existing.source === "environment" ? "Save a config token anyway? [y/N] " : "Replace existing credentials? [y/N] ";
2030
+ if (!await confirm(question)) {
2031
+ process.stdout.write(`Kept existing credentials.
2032
+ `);
2033
+ return;
2034
+ }
2035
+ }
2036
+ const input = args["token-stdin"] ? await readStdinSecret() : await promptForCredential();
2037
+ if (!input) {
2038
+ throw new CliError("Credential must not be empty.", 2);
2039
+ }
2040
+ const credential = detectCredential(input);
2041
+ if (!credential) {
2042
+ throw new CliError(unrecognizedCredentialMessage(), 2);
2043
+ }
2044
+ const config = readConfig();
2045
+ const common = {
2046
+ baseUrl: process.env.TMDB_BASE_URL,
2047
+ defaultLanguage: config.language ?? "en-US"
2048
+ };
2049
+ const client = credential.kind === "accessToken" ? createTMDB({ accessToken: credential.value, ...common }) : createTMDB({ apiKey: credential.value, ...common });
2050
+ try {
2051
+ await client.movies.details(550);
2052
+ } catch (error) {
2053
+ if (error instanceof TMDBResponseError && (error.status === 401 || error.status === 403)) {
2054
+ throw new CliError([
2055
+ "Credential validation failed: TMDB rejected the credential.",
2056
+ "Make sure you copied the API Read Access Token or API Key correctly.",
2057
+ "Existing config was not changed."
2058
+ ].join(`
2059
+ `), 3);
2060
+ }
2061
+ throw new CliError(`Credential validation failed; existing config was not changed.
2062
+ ${errorMessage(error)}`, exitCodeFor(error));
2063
+ }
2064
+ delete config.accessToken;
2065
+ delete config.apiKey;
2066
+ config[credential.kind] = credential.value;
2067
+ writeConfig(config);
2068
+ process.stdout.write(`Credential validated.
2069
+ Saved ${credential.kind} to ${getConfigPath()}
2070
+ `);
2071
+ if (credential.kind === "apiKey") {
2072
+ process.stdout.write(`Note: API Key is the legacy v3 method. The API Read Access Token is recommended.
2073
+ `);
2074
+ }
2075
+ const effective = resolveCredentials();
2076
+ if (effective.source === "environment") {
2077
+ process.stdout.write(`Note: ${effective.envVar} is set and takes precedence over the config file.
2078
+ `);
2079
+ }
2080
+ });
2081
+ }
2082
+ }),
2083
+ status: defineCommand({
2084
+ meta: {
2085
+ name: "status",
2086
+ description: "Show credential source, token mask, and config path"
2087
+ },
2088
+ args: { ...jsonArg },
2089
+ async run({ args }) {
2090
+ await runSafely(async () => {
2091
+ const credentials = resolveCredentials();
2092
+ const path = getConfigPath();
2093
+ const configExists = existsSync2(path);
2094
+ const view = {
2095
+ source: credentials.source,
2096
+ environmentVariable: credentials.envVar,
2097
+ configField: credentials.configField,
2098
+ token: maskSecret(credentials.accessToken ?? credentials.apiKey),
2099
+ configPath: path,
2100
+ configExists,
2101
+ language: credentials.config.language ?? null,
2102
+ region: credentials.config.region ?? null
2103
+ };
2104
+ if (args.json) {
2105
+ printJson(view);
2106
+ return;
2107
+ }
2108
+ const lines = [
2109
+ `Credential source: ${describeSource(credentials)}`,
2110
+ `Token: ${view.token ?? "(not set)"}`,
2111
+ `Config: ${path}${configExists ? "" : " (missing)"}`,
2112
+ `Language: ${view.language ?? "(not set)"}`,
2113
+ `Region: ${view.region ?? "(not set)"}`
2114
+ ];
2115
+ const hasConfigCredentials = Boolean(credentials.config.accessToken && !isPlaceholderCredential(credentials.config.accessToken) || credentials.config.apiKey && !isPlaceholderCredential(credentials.config.apiKey));
2116
+ if (credentials.source === "environment" && hasConfigCredentials) {
2117
+ lines.push("Note: config credentials are shadowed by environment variables.");
2118
+ }
2119
+ if (credentials.source === "none") {
2120
+ lines.push("", "Run `tmdb auth login` to set up credentials.");
2121
+ }
2122
+ process.stdout.write(`${lines.join(`
2123
+ `)}
2124
+ `);
2125
+ });
2126
+ }
2127
+ }),
2128
+ logout: defineCommand({
2129
+ meta: {
2130
+ name: "logout",
2131
+ description: "Remove saved API credentials from the config file"
2132
+ },
2133
+ async run() {
2134
+ await runSafely(async () => {
2135
+ const config = readConfig();
2136
+ const hadCredentials = Boolean(config.accessToken || config.apiKey);
2137
+ if (hadCredentials) {
2138
+ delete config.accessToken;
2139
+ delete config.apiKey;
2140
+ writeConfig(config);
2141
+ process.stdout.write(`Removed API credentials from ${getConfigPath()}
2142
+ `);
2143
+ } else {
2144
+ process.stdout.write(`No saved API credentials found.
2145
+ `);
2146
+ }
2147
+ if (resolveCredentials().source === "environment") {
2148
+ process.stdout.write(`Environment credentials are still set and will continue to be used.
2149
+ `);
2150
+ }
2151
+ });
2152
+ }
2153
+ })
2154
+ }
2155
+ });
2156
+ async function promptForCredential() {
2157
+ if (!process.stdin.isTTY) {
2158
+ throw new CliError([
2159
+ "No interactive terminal available.",
2160
+ "Pipe the credential with --token-stdin:",
2161
+ ` printf '%s' "$TMDB_ACCESS_TOKEN" | tmdb auth login --token-stdin`,
2162
+ "Or set TMDB_ACCESS_TOKEN / TMDB_API_KEY directly."
2163
+ ].join(`
2164
+ `), 2);
2165
+ }
2166
+ process.stdout.write([
2167
+ "Create credentials at https://www.themoviedb.org/settings/api",
2168
+ "API Read Access Token (recommended) or API Key are accepted.",
2169
+ ""
2170
+ ].join(`
2171
+ `));
2172
+ return readSecret("TMDB credential: ");
2173
+ }
2174
+ function describeSource(credentials) {
2175
+ if (credentials.source === "environment") {
2176
+ return `environment (${credentials.envVar})`;
2177
+ }
2178
+ if (credentials.source === "config") {
2179
+ return `config (${credentials.configField})`;
2180
+ }
2181
+ return "none";
2182
+ }
2183
+ function describeExistingCredentials(credentials) {
2184
+ const secret = maskSecret(credentials.accessToken ?? credentials.apiKey) ?? "(unknown)";
2185
+ if (credentials.source === "environment") {
2186
+ return [
2187
+ `Credentials are currently provided by ${credentials.envVar} (${secret}).`,
2188
+ "A config token will not take effect while that environment variable is set."
2189
+ ].join(`
2190
+ `);
2191
+ }
2192
+ return `Existing credentials: config (${credentials.configField}, ${secret}).`;
2193
+ }
2194
+ function existingCredentialsMessage(credentials) {
2195
+ const secret = maskSecret(credentials.accessToken ?? credentials.apiKey) ?? "(unknown)";
2196
+ const label = credentials.source === "environment" ? `environment (${credentials.envVar})` : `config (${credentials.configField})`;
2197
+ return [
2198
+ `Existing credentials found: ${label} (${secret}).`,
2199
+ credentials.source === "environment" ? "Re-run with --force to save a config token anyway." : "Re-run with --force to replace them."
2200
+ ].join(`
2201
+ `);
2202
+ }
2203
+
2204
+ // src/cli/client.ts
2205
+ function createClient(options = {}) {
2206
+ const credentials = resolveCredentials();
2207
+ const common = {
2208
+ baseUrl: process.env.TMDB_BASE_URL,
2209
+ defaultLanguage: options.language ?? credentials.config.language ?? "en-US",
2210
+ defaultRegion: credentials.config.region
2211
+ };
2212
+ if (credentials.accessToken) {
2213
+ return createTMDB({ accessToken: credentials.accessToken, ...common });
2214
+ }
2215
+ if (credentials.apiKey) {
2216
+ return createTMDB({ apiKey: credentials.apiKey, ...common });
2217
+ }
2218
+ throw new CliError(missingCredentialsMessage(), 3);
2219
+ }
2220
+
2221
+ // src/cli/commands/config.ts
2222
+ var CONFIG_KEY_ALIASES = {
2223
+ token: "accessToken",
2224
+ accessToken: "accessToken",
2225
+ "api-key": "apiKey",
2226
+ apiKey: "apiKey",
2227
+ language: "language",
2228
+ region: "region"
2229
+ };
2230
+ var configCommand = defineCommand({
2231
+ meta: {
2232
+ name: "config",
2233
+ description: "Manage TMDB CLI configuration"
2234
+ },
2235
+ subCommands: {
2236
+ show: defineCommand({
2237
+ meta: {
2238
+ name: "show",
2239
+ description: "Show the resolved config with secrets masked"
2240
+ },
2241
+ args: { ...jsonArg },
2242
+ async run({ args }) {
2243
+ await runSafely(async () => {
2244
+ const config = readConfig();
2245
+ const path = getConfigPath();
2246
+ const view = {
2247
+ path,
2248
+ accessToken: describeSecret(config.accessToken),
2249
+ apiKey: describeSecret(config.apiKey),
2250
+ language: config.language ?? null,
2251
+ region: config.region ?? null
2252
+ };
2253
+ if (args.json) {
2254
+ printJson(view);
2255
+ return;
2256
+ }
2257
+ process.stdout.write([
2258
+ `Path: ${path}`,
2259
+ `accessToken: ${view.accessToken ?? "(not set)"}`,
2260
+ `apiKey: ${view.apiKey ?? "(not set)"}`,
2261
+ `language: ${view.language ?? "(not set)"}`,
2262
+ `region: ${view.region ?? "(not set)"}`
2263
+ ].join(`
2264
+ `) + `
2265
+ `);
2266
+ });
2267
+ }
2268
+ }),
2269
+ path: defineCommand({
2270
+ meta: {
2271
+ name: "path",
2272
+ description: "Print the config file path"
2273
+ },
2274
+ run() {
2275
+ process.stdout.write(`${getConfigPath()}
2276
+ `);
2277
+ }
2278
+ }),
2279
+ test: defineCommand({
2280
+ meta: {
2281
+ name: "test",
2282
+ description: "Verify credentials with an authenticated TMDB request"
2283
+ },
2284
+ args: { ...jsonArg },
2285
+ async run({ args }) {
2286
+ await runSafely(async () => {
2287
+ const client = createClient();
2288
+ await client.movies.details(550);
2289
+ if (args.json) {
2290
+ printJson({ ok: true });
2291
+ return;
2292
+ }
2293
+ process.stdout.write(`TMDB connection OK
2294
+ Credentials are valid.
2295
+ `);
2296
+ });
2297
+ }
2298
+ }),
2299
+ set: defineCommand({
2300
+ meta: {
2301
+ name: "set",
2302
+ description: "Set a config value (token and api-key are read securely)"
2303
+ },
2304
+ args: {
2305
+ key: {
2306
+ type: "positional",
2307
+ description: "token, api-key, language, or region",
2308
+ required: true
2309
+ },
2310
+ value: {
2311
+ type: "positional",
2312
+ description: "Value for language or region",
2313
+ required: false
2314
+ }
2315
+ },
2316
+ async run({ args }) {
2317
+ await runSafely(async () => {
2318
+ const field = parseConfigKey(args.key);
2319
+ const path = getConfigPath();
2320
+ if (field === "accessToken" || field === "apiKey") {
2321
+ if (args.value !== undefined) {
2322
+ throw new CliError("Do not pass secrets as command arguments. Run the command without a value to enter it securely.", 2);
2323
+ }
2324
+ const label = field === "accessToken" ? "TMDB access token" : "TMDB API key";
2325
+ const secret = await readSecret(`${label}: `);
2326
+ if (!secret) {
2327
+ throw new CliError("Secret must not be empty.", 2);
2328
+ }
2329
+ setConfigValue(field, secret);
2330
+ process.stdout.write(`Saved ${field} to ${path}
2331
+ `);
2332
+ return;
2333
+ }
2334
+ const value = args.value?.trim();
2335
+ if (!value) {
2336
+ throw new CliError(`A value is required for ${args.key}.`, 2);
2337
+ }
2338
+ setConfigValue(field, field === "region" ? value.toUpperCase() : value);
2339
+ process.stdout.write(`Saved ${field} to ${path}
2340
+ `);
2341
+ });
2342
+ }
2343
+ }),
2344
+ unset: defineCommand({
2345
+ meta: {
2346
+ name: "unset",
2347
+ description: "Remove a config value"
2348
+ },
2349
+ args: {
2350
+ key: {
2351
+ type: "positional",
2352
+ description: "token, api-key, language, or region",
2353
+ required: true
2354
+ }
2355
+ },
2356
+ async run({ args }) {
2357
+ await runSafely(async () => {
2358
+ const field = parseConfigKey(args.key);
2359
+ unsetConfigValue(field);
2360
+ process.stdout.write(`Removed ${field} from ${getConfigPath()}
2361
+ `);
2362
+ });
2363
+ }
2364
+ })
2365
+ }
2366
+ });
2367
+ function parseConfigKey(value) {
2368
+ if (!(value in CONFIG_KEY_ALIASES)) {
2369
+ throw new CliError(`Unknown config key "${value}". Expected one of: ${Object.keys(CONFIG_KEY_ALIASES).join(", ")}.`, 2);
2370
+ }
2371
+ return CONFIG_KEY_ALIASES[value];
2372
+ }
2373
+
2374
+ // src/cli/commands/genres.ts
2375
+ var genresCommand = defineCommand({
2376
+ meta: {
2377
+ name: "genres",
2378
+ description: "List movie or TV genres"
2379
+ },
2380
+ args: {
2381
+ ...commonArgs,
2382
+ type: {
2383
+ type: "positional",
2384
+ description: "Genre list type: movie or tv",
2385
+ default: "movie"
2386
+ }
2387
+ },
2388
+ async run({ args }) {
2389
+ await runSafely(async () => {
2390
+ const type = parseGenreType(args.type);
2391
+ const client = createClient({ language: args.lang });
2392
+ const response = type === "tv" ? await client.genres.tv() : await client.genres.movies();
2393
+ if (args.json) {
2394
+ printJson(response);
2395
+ } else {
2396
+ printGenres(response);
2397
+ }
2398
+ });
2399
+ }
2400
+ });
2401
+
2402
+ // src/cli/commands/movie.ts
2403
+ var movieCommand = defineCommand({
2404
+ meta: {
2405
+ name: "movie",
2406
+ description: "Movie commands"
2407
+ },
2408
+ subCommands: {
2409
+ list: defineCommand({
2410
+ meta: {
2411
+ name: "list",
2412
+ description: "List popular, top-rated, now-playing, or upcoming movies"
2413
+ },
2414
+ args: {
2415
+ ...commonArgs,
2416
+ category: {
2417
+ type: "positional",
2418
+ description: "popular, top-rated, now-playing, or upcoming",
2419
+ default: "popular"
2420
+ },
2421
+ page: {
2422
+ type: "string",
2423
+ description: "Page number"
2424
+ },
2425
+ region: {
2426
+ type: "string",
2427
+ description: "ISO 3166-1 region code"
2428
+ }
2429
+ },
2430
+ async run({ args }) {
2431
+ await runSafely(async () => {
2432
+ const category = parseMovieListCategory(args.category);
2433
+ const options = { page: parsePage(args.page), region: args.region };
2434
+ const client = createClient({ language: args.lang });
2435
+ const response = category === "top-rated" ? await client.movies.topRated(options) : category === "now-playing" ? await client.movies.nowPlaying(options) : category === "upcoming" ? await client.movies.upcoming(options) : await client.movies.popular(options);
2436
+ if (args.json) {
2437
+ printJson(response);
2438
+ } else {
2439
+ printSearchResults(response, "movie");
2440
+ }
2441
+ });
2442
+ }
2443
+ }),
2444
+ get: defineCommand({
2445
+ meta: {
2446
+ name: "get",
2447
+ description: "Get movie details by TMDB ID"
2448
+ },
2449
+ args: {
2450
+ ...commonArgs,
2451
+ id: {
2452
+ type: "positional",
2453
+ description: "TMDB movie ID",
2454
+ required: true
2455
+ },
2456
+ append: {
2457
+ type: "string",
2458
+ description: "Comma-separated append_to_response values"
2459
+ }
2460
+ },
2461
+ async run({ args }) {
2462
+ await runSafely(async () => {
2463
+ const id = parseId(args.id, "movie ID");
2464
+ const client = createClient({ language: args.lang });
2465
+ const appendToResponse = parseCommaList(args.append);
2466
+ const movie = await client.movies.details(id, { appendToResponse });
2467
+ if (args.json) {
2468
+ printJson(movie);
2469
+ } else {
2470
+ printMovie(movie, client);
2471
+ }
2472
+ });
2473
+ }
2474
+ }),
2475
+ credits: defineCommand({
2476
+ meta: {
2477
+ name: "credits",
2478
+ description: "Show movie cast and crew"
2479
+ },
2480
+ args: {
2481
+ ...commonArgs,
2482
+ id: {
2483
+ type: "positional",
2484
+ description: "TMDB movie ID",
2485
+ required: true
2486
+ },
2487
+ limit: {
2488
+ type: "string",
2489
+ description: "Maximum cast/crew rows (default: 10)"
2490
+ }
2491
+ },
2492
+ async run({ args }) {
2493
+ await runSafely(async () => {
2494
+ const id = parseId(args.id, "movie ID");
2495
+ const limit = parseLimit(args.limit);
2496
+ const client = createClient({ language: args.lang });
2497
+ const credits = await client.movies.credits(id);
2498
+ if (args.json) {
2499
+ printJson(credits);
2500
+ } else {
2501
+ printCredits(credits, limit);
2502
+ }
2503
+ });
2504
+ }
2505
+ }),
2506
+ images: defineCommand({
2507
+ meta: {
2508
+ name: "images",
2509
+ description: "Show movie image URLs"
2510
+ },
2511
+ args: {
2512
+ ...commonArgs,
2513
+ id: {
2514
+ type: "positional",
2515
+ description: "TMDB movie ID",
2516
+ required: true
2517
+ },
2518
+ "image-language": {
2519
+ type: "string",
2520
+ description: "Comma-separated image language codes"
2521
+ }
2522
+ },
2523
+ async run({ args }) {
2524
+ await runSafely(async () => {
2525
+ const id = parseId(args.id, "movie ID");
2526
+ const includeImageLanguage = parseCommaList(args["image-language"]);
2527
+ const client = createClient({ language: args.lang });
2528
+ const images = await client.movies.images(id, { includeImageLanguage });
2529
+ if (args.json) {
2530
+ printJson(images);
2531
+ } else {
2532
+ printImages(images, client);
2533
+ }
2534
+ });
2535
+ }
2536
+ }),
2537
+ videos: defineCommand({
2538
+ meta: {
2539
+ name: "videos",
2540
+ description: "Show movie videos and trailers"
2541
+ },
2542
+ args: {
2543
+ ...commonArgs,
2544
+ id: {
2545
+ type: "positional",
2546
+ description: "TMDB movie ID",
2547
+ required: true
2548
+ }
2549
+ },
2550
+ async run({ args }) {
2551
+ await runSafely(async () => {
2552
+ const id = parseId(args.id, "movie ID");
2553
+ const client = createClient({ language: args.lang });
2554
+ const videos = await client.movies.videos(id);
2555
+ if (args.json) {
2556
+ printJson(videos);
2557
+ } else {
2558
+ printVideos(videos);
2559
+ }
2560
+ });
2561
+ }
2562
+ }),
2563
+ similar: defineCommand({
2564
+ meta: {
2565
+ name: "similar",
2566
+ description: "List movies similar to a movie"
2567
+ },
2568
+ args: {
2569
+ ...commonArgs,
2570
+ id: {
2571
+ type: "positional",
2572
+ description: "TMDB movie ID",
2573
+ required: true
2574
+ },
2575
+ page: {
2576
+ type: "string",
2577
+ description: "Page number"
2578
+ },
2579
+ region: {
2580
+ type: "string",
2581
+ description: "ISO 3166-1 region code"
2582
+ }
2583
+ },
2584
+ async run({ args }) {
2585
+ await runSafely(async () => {
2586
+ const id = parseId(args.id, "movie ID");
2587
+ const options = { page: parsePage(args.page), region: args.region };
2588
+ const client = createClient({ language: args.lang });
2589
+ const response = await client.movies.similar(id, options);
2590
+ if (args.json) {
2591
+ printJson(response);
2592
+ } else {
2593
+ printSearchResults(response, "movie");
2594
+ }
2595
+ });
2596
+ }
2597
+ }),
2598
+ recommendations: defineCommand({
2599
+ meta: {
2600
+ name: "recommendations",
2601
+ description: "List recommended movies for a movie"
2602
+ },
2603
+ args: {
2604
+ ...commonArgs,
2605
+ id: {
2606
+ type: "positional",
2607
+ description: "TMDB movie ID",
2608
+ required: true
2609
+ },
2610
+ page: {
2611
+ type: "string",
2612
+ description: "Page number"
2613
+ },
2614
+ region: {
2615
+ type: "string",
2616
+ description: "ISO 3166-1 region code"
2617
+ }
2618
+ },
2619
+ async run({ args }) {
2620
+ await runSafely(async () => {
2621
+ const id = parseId(args.id, "movie ID");
2622
+ const options = { page: parsePage(args.page), region: args.region };
2623
+ const client = createClient({ language: args.lang });
2624
+ const response = await client.movies.recommendations(id, options);
2625
+ if (args.json) {
2626
+ printJson(response);
2627
+ } else {
2628
+ printSearchResults(response, "movie");
2629
+ }
2630
+ });
2631
+ }
2632
+ })
2633
+ }
2634
+ });
2635
+
2636
+ // src/cli/commands/person.ts
2637
+ var personCommand = defineCommand({
2638
+ meta: {
2639
+ name: "person",
2640
+ description: "Person commands"
2641
+ },
2642
+ subCommands: {
2643
+ get: defineCommand({
2644
+ meta: {
2645
+ name: "get",
2646
+ description: "Get person details by TMDB ID"
2647
+ },
2648
+ args: {
2649
+ ...commonArgs,
2650
+ id: {
2651
+ type: "positional",
2652
+ description: "TMDB person ID",
2653
+ required: true
2654
+ }
2655
+ },
2656
+ async run({ args }) {
2657
+ await runSafely(async () => {
2658
+ const id = parseId(args.id, "person ID");
2659
+ const client = createClient({ language: args.lang });
2660
+ const person = await client.people.details(id);
2661
+ if (args.json) {
2662
+ printJson(person);
2663
+ } else {
2664
+ printPerson(person, client);
2665
+ }
2666
+ });
2667
+ }
2668
+ }),
2669
+ credits: defineCommand({
2670
+ meta: {
2671
+ name: "credits",
2672
+ description: "Show a person combined cast and crew credits"
2673
+ },
2674
+ args: {
2675
+ ...commonArgs,
2676
+ id: {
2677
+ type: "positional",
2678
+ description: "TMDB person ID",
2679
+ required: true
2680
+ },
2681
+ limit: {
2682
+ type: "string",
2683
+ description: "Maximum rows per section (default: 10)"
2684
+ }
2685
+ },
2686
+ async run({ args }) {
2687
+ await runSafely(async () => {
2688
+ const id = parseId(args.id, "person ID");
2689
+ const limit = parseLimit(args.limit);
2690
+ const client = createClient({ language: args.lang });
2691
+ const credits = await client.people.combinedCredits(id);
2692
+ if (args.json) {
2693
+ printJson(credits);
2694
+ } else {
2695
+ printPersonCredits(credits, limit);
2696
+ }
2697
+ });
2698
+ }
2699
+ })
2700
+ }
2701
+ });
2702
+
2703
+ // src/cli/commands/search.ts
2704
+ var searchCommand = defineCommand({
2705
+ meta: {
2706
+ name: "search",
2707
+ description: "Search movies, TV shows, and people"
2708
+ },
2709
+ args: {
2710
+ ...commonArgs,
2711
+ query: {
2712
+ type: "positional",
2713
+ description: "Search query",
2714
+ required: true
2715
+ },
2716
+ type: {
2717
+ type: "enum",
2718
+ description: "Media type to search",
2719
+ options: [...SEARCH_TYPES],
2720
+ default: "all"
2721
+ },
2722
+ page: {
2723
+ type: "string",
2724
+ description: "Page number"
2725
+ },
2726
+ year: {
2727
+ type: "string",
2728
+ description: "Release year for movies, first-air year for TV"
2729
+ }
2730
+ },
2731
+ async run({ args }) {
2732
+ await runSafely(async () => {
2733
+ const query = args.query.trim();
2734
+ if (!query) {
2735
+ throw new CliError("Search query must not be empty.", 2);
2736
+ }
2737
+ const type = parseSearchType(args.type);
2738
+ const page = parsePage(args.page);
2739
+ const year = args.year?.trim() || undefined;
2740
+ if (year && type !== "movie" && type !== "tv") {
2741
+ throw new CliError("--year is only supported with --type movie or tv.", 2);
2742
+ }
2743
+ const client = createClient({ language: args.lang });
2744
+ const options = { page };
2745
+ const response = type === "movie" ? await client.search.movies(query, { ...options, year }) : type === "tv" ? await client.search.tv(query, { ...options, firstAirDateYear: year }) : type === "person" ? await client.search.people(query, options) : await client.search.multi(query, options);
2746
+ if (args.json) {
2747
+ printJson(response);
2748
+ } else {
2749
+ printSearchResults(response, type);
2750
+ }
2751
+ });
2752
+ }
2753
+ });
2754
+
2755
+ // src/cli/commands/trending.ts
2756
+ var trendingCommand = defineCommand({
2757
+ meta: {
2758
+ name: "trending",
2759
+ description: "Show trending movies, TV shows, people, or all media"
2760
+ },
2761
+ args: {
2762
+ ...commonArgs,
2763
+ type: {
2764
+ type: "positional",
2765
+ description: "Media type: all, movie, tv, or person",
2766
+ default: "all"
2767
+ },
2768
+ window: {
2769
+ type: "enum",
2770
+ description: "Trending window",
2771
+ options: [...TRENDING_WINDOWS],
2772
+ default: "week"
2773
+ },
2774
+ page: {
2775
+ type: "string",
2776
+ description: "Page number"
2777
+ }
2778
+ },
2779
+ async run({ args }) {
2780
+ await runSafely(async () => {
2781
+ const type = parseSearchType(args.type);
2782
+ const window = parseWindow(args.window);
2783
+ const page = parsePage(args.page);
2784
+ const client = createClient({ language: args.lang });
2785
+ const response = type === "movie" ? await client.trending.movies(window, { page }) : type === "tv" ? await client.trending.tv(window, { page }) : type === "person" ? await client.trending.people(window, { page }) : await client.trending.all(window, { page });
2786
+ if (args.json) {
2787
+ printJson(response);
2788
+ } else {
2789
+ printSearchResults(response, type);
2790
+ }
2791
+ });
2792
+ }
2793
+ });
2794
+
2795
+ // src/cli/commands/tv.ts
2796
+ var tvCommand = defineCommand({
2797
+ meta: {
2798
+ name: "tv",
2799
+ description: "TV show commands"
2800
+ },
2801
+ subCommands: {
2802
+ list: defineCommand({
2803
+ meta: {
2804
+ name: "list",
2805
+ description: "List popular, top-rated, airing-today, or on-the-air shows"
2806
+ },
2807
+ args: {
2808
+ ...commonArgs,
2809
+ category: {
2810
+ type: "positional",
2811
+ description: "popular, top-rated, airing-today, or on-the-air",
2812
+ default: "popular"
2813
+ },
2814
+ page: {
2815
+ type: "string",
2816
+ description: "Page number"
2817
+ }
2818
+ },
2819
+ async run({ args }) {
2820
+ await runSafely(async () => {
2821
+ const category = parseTVListCategory(args.category);
2822
+ const options = { page: parsePage(args.page) };
2823
+ const client = createClient({ language: args.lang });
2824
+ const response = category === "top-rated" ? await client.tv.topRated(options) : category === "airing-today" ? await client.tv.airingToday(options) : category === "on-the-air" ? await client.tv.onTheAir(options) : await client.tv.popular(options);
2825
+ if (args.json) {
2826
+ printJson(response);
2827
+ } else {
2828
+ printSearchResults(response, "tv");
2829
+ }
2830
+ });
2831
+ }
2832
+ }),
2833
+ get: defineCommand({
2834
+ meta: {
2835
+ name: "get",
2836
+ description: "Get TV show details by TMDB ID"
2837
+ },
2838
+ args: {
2839
+ ...commonArgs,
2840
+ id: {
2841
+ type: "positional",
2842
+ description: "TMDB TV show ID",
2843
+ required: true
2844
+ },
2845
+ append: {
2846
+ type: "string",
2847
+ description: "Comma-separated append_to_response values"
2848
+ }
2849
+ },
2850
+ async run({ args }) {
2851
+ await runSafely(async () => {
2852
+ const id = parseId(args.id, "TV show ID");
2853
+ const client = createClient({ language: args.lang });
2854
+ const appendToResponse = parseCommaList(args.append);
2855
+ const tv = await client.tv.details(id, { appendToResponse });
2856
+ if (args.json) {
2857
+ printJson(tv);
2858
+ } else {
2859
+ printTV(tv, client);
2860
+ }
2861
+ });
2862
+ }
2863
+ }),
2864
+ credits: defineCommand({
2865
+ meta: {
2866
+ name: "credits",
2867
+ description: "Show TV show cast and crew"
2868
+ },
2869
+ args: {
2870
+ ...commonArgs,
2871
+ id: {
2872
+ type: "positional",
2873
+ description: "TMDB TV show ID",
2874
+ required: true
2875
+ },
2876
+ limit: {
2877
+ type: "string",
2878
+ description: "Maximum cast/crew rows (default: 10)"
2879
+ }
2880
+ },
2881
+ async run({ args }) {
2882
+ await runSafely(async () => {
2883
+ const id = parseId(args.id, "TV show ID");
2884
+ const limit = parseLimit(args.limit);
2885
+ const client = createClient({ language: args.lang });
2886
+ const credits = await client.tv.credits(id);
2887
+ if (args.json) {
2888
+ printJson(credits);
2889
+ } else {
2890
+ printCredits(credits, limit);
2891
+ }
2892
+ });
2893
+ }
2894
+ }),
2895
+ images: defineCommand({
2896
+ meta: {
2897
+ name: "images",
2898
+ description: "Show TV show image URLs"
2899
+ },
2900
+ args: {
2901
+ ...commonArgs,
2902
+ id: {
2903
+ type: "positional",
2904
+ description: "TMDB TV show ID",
2905
+ required: true
2906
+ },
2907
+ "image-language": {
2908
+ type: "string",
2909
+ description: "Comma-separated image language codes"
2910
+ }
2911
+ },
2912
+ async run({ args }) {
2913
+ await runSafely(async () => {
2914
+ const id = parseId(args.id, "TV show ID");
2915
+ const includeImageLanguage = parseCommaList(args["image-language"]);
2916
+ const client = createClient({ language: args.lang });
2917
+ const images = await client.tv.images(id, { includeImageLanguage });
2918
+ if (args.json) {
2919
+ printJson(images);
2920
+ } else {
2921
+ printImages(images, client);
2922
+ }
2923
+ });
2924
+ }
2925
+ }),
2926
+ videos: defineCommand({
2927
+ meta: {
2928
+ name: "videos",
2929
+ description: "Show TV show videos and trailers"
2930
+ },
2931
+ args: {
2932
+ ...commonArgs,
2933
+ id: {
2934
+ type: "positional",
2935
+ description: "TMDB TV show ID",
2936
+ required: true
2937
+ }
2938
+ },
2939
+ async run({ args }) {
2940
+ await runSafely(async () => {
2941
+ const id = parseId(args.id, "TV show ID");
2942
+ const client = createClient({ language: args.lang });
2943
+ const videos = await client.tv.videos(id);
2944
+ if (args.json) {
2945
+ printJson(videos);
2946
+ } else {
2947
+ printVideos(videos);
2948
+ }
2949
+ });
2950
+ }
2951
+ }),
2952
+ similar: defineCommand({
2953
+ meta: {
2954
+ name: "similar",
2955
+ description: "List TV shows similar to a show"
2956
+ },
2957
+ args: {
2958
+ ...commonArgs,
2959
+ id: {
2960
+ type: "positional",
2961
+ description: "TMDB TV show ID",
2962
+ required: true
2963
+ },
2964
+ page: {
2965
+ type: "string",
2966
+ description: "Page number"
2967
+ }
2968
+ },
2969
+ async run({ args }) {
2970
+ await runSafely(async () => {
2971
+ const id = parseId(args.id, "TV show ID");
2972
+ const options = { page: parsePage(args.page) };
2973
+ const client = createClient({ language: args.lang });
2974
+ const response = await client.tv.similar(id, options);
2975
+ if (args.json) {
2976
+ printJson(response);
2977
+ } else {
2978
+ printSearchResults(response, "tv");
2979
+ }
2980
+ });
2981
+ }
2982
+ }),
2983
+ recommendations: defineCommand({
2984
+ meta: {
2985
+ name: "recommendations",
2986
+ description: "List recommended TV shows for a show"
2987
+ },
2988
+ args: {
2989
+ ...commonArgs,
2990
+ id: {
2991
+ type: "positional",
2992
+ description: "TMDB TV show ID",
2993
+ required: true
2994
+ },
2995
+ page: {
2996
+ type: "string",
2997
+ description: "Page number"
2998
+ }
2999
+ },
3000
+ async run({ args }) {
3001
+ await runSafely(async () => {
3002
+ const id = parseId(args.id, "TV show ID");
3003
+ const options = { page: parsePage(args.page) };
3004
+ const client = createClient({ language: args.lang });
3005
+ const response = await client.tv.recommendations(id, options);
3006
+ if (args.json) {
3007
+ printJson(response);
3008
+ } else {
3009
+ printSearchResults(response, "tv");
3010
+ }
3011
+ });
3012
+ }
3013
+ }),
3014
+ season: defineCommand({
3015
+ meta: {
3016
+ name: "season",
3017
+ description: "Show a TV season and its episodes"
3018
+ },
3019
+ args: {
3020
+ ...commonArgs,
3021
+ id: {
3022
+ type: "positional",
3023
+ description: "TMDB TV show ID",
3024
+ required: true
3025
+ },
3026
+ season: {
3027
+ type: "positional",
3028
+ description: "Season number (0 for specials)",
3029
+ required: true
3030
+ }
3031
+ },
3032
+ async run({ args }) {
3033
+ await runSafely(async () => {
3034
+ const id = parseId(args.id, "TV show ID");
3035
+ const seasonNumber = parseSeasonNumber(args.season);
3036
+ const client = createClient({ language: args.lang });
3037
+ const season = await client.tv.seasonDetails(id, seasonNumber);
3038
+ if (args.json) {
3039
+ printJson(season);
3040
+ } else {
3041
+ printSeason(season, client);
3042
+ }
3043
+ });
3044
+ }
3045
+ })
3046
+ }
3047
+ });
3048
+
3049
+ // src/cli/command.ts
3050
+ var CLI_VERSION = package_default.version;
3051
+ var mainCommand = defineCommand({
3052
+ meta: {
3053
+ name: "tmdb",
3054
+ version: CLI_VERSION,
3055
+ description: "A read-only TMDB command-line client"
3056
+ },
3057
+ subCommands: {
3058
+ search: searchCommand,
3059
+ movie: movieCommand,
3060
+ tv: tvCommand,
3061
+ person: personCommand,
3062
+ trending: trendingCommand,
3063
+ genres: genresCommand,
3064
+ auth: authCommand,
3065
+ config: configCommand
3066
+ }
3067
+ });
3068
+
3069
+ // src/cli/help.ts
3070
+ var AUTHENTICATION_HELP = [
3071
+ "AUTHENTICATION",
3072
+ "",
3073
+ " tmdb auth login Set up a TMDB API Read Access Token",
3074
+ " tmdb auth status Show credential source and config path",
3075
+ " tmdb auth logout Remove saved API credentials",
3076
+ " tmdb config show Show config path and masked values",
3077
+ " tmdb config path Print the config file path",
3078
+ "",
3079
+ " TMDB_ACCESS_TOKEN Environment variable (takes precedence)",
3080
+ " TMDB_API_KEY Legacy v3 API key (takes precedence)"
3081
+ ].join(`
3082
+ `);
3083
+
3084
+ // src/cli/main.ts
3085
+ await runMain(mainCommand, { showUsage: showFullUsage });
3086
+ async function showFullUsage(cmd, parent) {
3087
+ const path = await findPath(mainCommand, cmd);
3088
+ if (!parent || !path || path.length === 0) {
3089
+ const usage = await renderUsage(cmd);
3090
+ const authentication = cmd === mainCommand ? `
3091
+
3092
+ ${AUTHENTICATION_HELP}` : "";
3093
+ process.stdout.write(`${usage}${authentication}
3094
+ `);
3095
+ return;
3096
+ }
3097
+ const parentName = ["tmdb", ...path.slice(0, -1)].join(" ");
3098
+ const usageParent = defineCommand({
3099
+ meta: { name: parentName, version: CLI_VERSION }
3100
+ });
3101
+ process.stdout.write(`${await renderUsage(cmd, usageParent)}
3102
+ `);
3103
+ }
3104
+ async function findPath(root, target, path = []) {
3105
+ if (root === target) {
3106
+ return path;
3107
+ }
3108
+ const subCommands = await resolveValue2(root.subCommands);
3109
+ if (!subCommands) {
3110
+ return;
3111
+ }
3112
+ for (const [name, subCommand] of Object.entries(subCommands)) {
3113
+ const resolved = await resolveValue2(subCommand);
3114
+ if (!resolved) {
3115
+ continue;
3116
+ }
3117
+ const found = await findPath(resolved, target, [...path, name]);
3118
+ if (found) {
3119
+ return found;
3120
+ }
3121
+ }
3122
+ return;
3123
+ }
3124
+ async function resolveValue2(value) {
3125
+ if (value === undefined) {
3126
+ return;
3127
+ }
3128
+ if (typeof value === "function") {
3129
+ return await value();
3130
+ }
3131
+ return await value;
3132
+ }