tmdb-kit 1.1.0 → 1.2.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 CHANGED
@@ -354,17 +354,6 @@ async function runCommand(cmd, opts) {
354
354
  throw new Error("Multiple cleanup errors", { cause: cleanupErrors });
355
355
  return { result };
356
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
357
  async function _findSubCommand(subCommands, name) {
369
358
  if (name in subCommands)
370
359
  return resolveValue(subCommands[name]);
@@ -404,14 +393,6 @@ function _isValueFlag(flag, argsDef) {
404
393
  }
405
394
  return false;
406
395
  }
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
396
  var negativePrefixRe = /^no[-A-Z]/;
416
397
  async function renderUsage(cmd, parent) {
417
398
  const cmdMeta = await resolveValue(cmd.meta || {});
@@ -494,57 +475,11 @@ function renderDescription(arg, required) {
494
475
  defaultHint
495
476
  ].filter(Boolean).join(" ");
496
477
  }
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
478
  // package.json
544
479
  var package_default = {
545
480
  name: "tmdb-kit",
546
481
  type: "module",
547
- version: "1.1.0",
482
+ version: "1.2.0",
548
483
  description: "A tiny, typed, zero-runtime-dependency TMDB SDK and CLI for Node.js, Bun, Deno, and edge runtimes.",
549
484
  author: "isaced",
550
485
  license: "MIT",
@@ -648,6 +583,15 @@ class TMDBRequestError extends TMDBError {
648
583
  }
649
584
  }
650
585
 
586
+ class TMDBTimeoutError extends TMDBError {
587
+ timeoutMs;
588
+ constructor(timeoutMs, message = `TMDB request timed out after ${timeoutMs}ms`) {
589
+ super(message);
590
+ this.name = "TMDBTimeoutError";
591
+ this.timeoutMs = timeoutMs;
592
+ }
593
+ }
594
+
651
595
  class TMDBResponseError extends TMDBError {
652
596
  body;
653
597
  headers;
@@ -770,6 +714,19 @@ function toId(value, name) {
770
714
  }
771
715
  return String(value);
772
716
  }
717
+ function toSeasonNumber(value) {
718
+ if (!Number.isInteger(value) || value < 0) {
719
+ throw new TMDBRequestError("seasonNumber must be a non-negative integer");
720
+ }
721
+ return String(value);
722
+ }
723
+ function toGroupId(value) {
724
+ const groupId = value.trim();
725
+ if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {
726
+ throw new TMDBRequestError("groupId must be a non-empty identifier");
727
+ }
728
+ return groupId;
729
+ }
773
730
  function toSearchQuery(value) {
774
731
  const query = value.trim();
775
732
  if (query.length === 0) {
@@ -903,23 +860,25 @@ class ConfigurationResource {
903
860
  constructor(transport) {
904
861
  this.#transport = transport;
905
862
  }
906
- details() {
907
- return this.#transport.get("/configuration");
863
+ details(options = {}) {
864
+ return this.#transport.get("/configuration", { signal: options.signal });
908
865
  }
909
- countries() {
910
- return this.#transport.get("/configuration/countries");
866
+ countries(options = {}) {
867
+ return this.#transport.get("/configuration/countries", { signal: options.signal });
911
868
  }
912
- jobs() {
913
- return this.#transport.get("/configuration/jobs");
869
+ jobs(options = {}) {
870
+ return this.#transport.get("/configuration/jobs", { signal: options.signal });
914
871
  }
915
- languages() {
916
- return this.#transport.get("/configuration/languages");
872
+ languages(options = {}) {
873
+ return this.#transport.get("/configuration/languages", { signal: options.signal });
917
874
  }
918
- primaryTranslations() {
919
- return this.#transport.get("/configuration/primary_translations");
875
+ primaryTranslations(options = {}) {
876
+ return this.#transport.get("/configuration/primary_translations", {
877
+ signal: options.signal
878
+ });
920
879
  }
921
- timezones() {
922
- return this.#transport.get("/configuration/timezones");
880
+ timezones(options = {}) {
881
+ return this.#transport.get("/configuration/timezones", { signal: options.signal });
923
882
  }
924
883
  }
925
884
 
@@ -933,14 +892,16 @@ class GenresResource {
933
892
  return this.#transport.get("/genre/movie/list", {
934
893
  query: withLanguage(this.#transport.defaults, {
935
894
  language: options.language
936
- })
895
+ }),
896
+ signal: options.signal
937
897
  });
938
898
  }
939
899
  tv(options = {}) {
940
900
  return this.#transport.get("/genre/tv/list", {
941
901
  query: withLanguage(this.#transport.defaults, {
942
902
  language: options.language
943
- })
903
+ }),
904
+ signal: options.signal
944
905
  });
945
906
  }
946
907
  }
@@ -969,22 +930,26 @@ class MoviesResource {
969
930
  }
970
931
  popular(options = {}) {
971
932
  return this.#transport.get("/movie/popular", {
972
- query: movieListQuery(this.#transport, options)
933
+ query: movieListQuery(this.#transport, options),
934
+ signal: options.signal
973
935
  });
974
936
  }
975
937
  topRated(options = {}) {
976
938
  return this.#transport.get("/movie/top_rated", {
977
- query: movieListQuery(this.#transport, options)
939
+ query: movieListQuery(this.#transport, options),
940
+ signal: options.signal
978
941
  });
979
942
  }
980
943
  nowPlaying(options = {}) {
981
944
  return this.#transport.get("/movie/now_playing", {
982
- query: movieListQuery(this.#transport, options)
945
+ query: movieListQuery(this.#transport, options),
946
+ signal: options.signal
983
947
  });
984
948
  }
985
949
  upcoming(options = {}) {
986
950
  return this.#transport.get("/movie/upcoming", {
987
- query: movieListQuery(this.#transport, options)
951
+ query: movieListQuery(this.#transport, options),
952
+ signal: options.signal
988
953
  });
989
954
  }
990
955
  details(movieId, options = {}) {
@@ -992,14 +957,16 @@ class MoviesResource {
992
957
  query: withLanguage(this.#transport.defaults, {
993
958
  append_to_response: toCommaList(options.appendToResponse),
994
959
  language: options.language
995
- })
960
+ }),
961
+ signal: options.signal
996
962
  });
997
963
  }
998
964
  credits(movieId, options = {}) {
999
965
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/credits`, {
1000
966
  query: withLanguage(this.#transport.defaults, {
1001
967
  language: options.language
1002
- })
968
+ }),
969
+ signal: options.signal
1003
970
  });
1004
971
  }
1005
972
  images(movieId, options = {}) {
@@ -1007,24 +974,28 @@ class MoviesResource {
1007
974
  query: withLanguage(this.#transport.defaults, {
1008
975
  include_image_language: toCommaList(options.includeImageLanguage),
1009
976
  language: options.language
1010
- })
977
+ }),
978
+ signal: options.signal
1011
979
  });
1012
980
  }
1013
981
  recommendations(movieId, options = {}) {
1014
982
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/recommendations`, {
1015
- query: movieListQuery(this.#transport, options)
983
+ query: movieListQuery(this.#transport, options),
984
+ signal: options.signal
1016
985
  });
1017
986
  }
1018
987
  similar(movieId, options = {}) {
1019
988
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/similar`, {
1020
- query: movieListQuery(this.#transport, options)
989
+ query: movieListQuery(this.#transport, options),
990
+ signal: options.signal
1021
991
  });
1022
992
  }
1023
993
  videos(movieId, options = {}) {
1024
994
  return this.#transport.get(`/movie/${toId(movieId, "movieId")}/videos`, {
1025
995
  query: withLanguage(this.#transport.defaults, {
1026
996
  language: options.language
1027
- })
997
+ }),
998
+ signal: options.signal
1028
999
  });
1029
1000
  }
1030
1001
  }
@@ -1044,21 +1015,24 @@ class PeopleResource {
1044
1015
  }
1045
1016
  popular(options = {}) {
1046
1017
  return this.#transport.get("/person/popular", {
1047
- query: peopleListQuery(this.#transport, options)
1018
+ query: peopleListQuery(this.#transport, options),
1019
+ signal: options.signal
1048
1020
  });
1049
1021
  }
1050
1022
  details(personId, options = {}) {
1051
1023
  return this.#transport.get(`/person/${toId(personId, "personId")}`, {
1052
1024
  query: withLanguage(this.#transport.defaults, {
1053
1025
  language: options.language
1054
- })
1026
+ }),
1027
+ signal: options.signal
1055
1028
  });
1056
1029
  }
1057
1030
  combinedCredits(personId, options = {}) {
1058
1031
  return this.#transport.get(`/person/${toId(personId, "personId")}/combined_credits`, {
1059
1032
  query: withLanguage(this.#transport.defaults, {
1060
1033
  language: options.language
1061
- })
1034
+ }),
1035
+ signal: options.signal
1062
1036
  });
1063
1037
  }
1064
1038
  }
@@ -1077,22 +1051,26 @@ class SearchResource {
1077
1051
  }
1078
1052
  movies(query, options = {}) {
1079
1053
  return this.#transport.get("/search/movie", {
1080
- query: movieSearchQuery(this.#transport, query, options)
1054
+ query: movieSearchQuery(this.#transport, query, options),
1055
+ signal: options.signal
1081
1056
  });
1082
1057
  }
1083
1058
  tv(query, options = {}) {
1084
1059
  return this.#transport.get("/search/tv", {
1085
- query: tvSearchQuery(this.#transport, query, options)
1060
+ query: tvSearchQuery(this.#transport, query, options),
1061
+ signal: options.signal
1086
1062
  });
1087
1063
  }
1088
1064
  people(query, options = {}) {
1089
1065
  return this.#transport.get("/search/person", {
1090
- query: commonSearchQuery(this.#transport, query, options)
1066
+ query: commonSearchQuery(this.#transport, query, options),
1067
+ signal: options.signal
1091
1068
  });
1092
1069
  }
1093
1070
  async multi(query, options = {}) {
1094
1071
  const response = await this.#transport.get("/search/multi", {
1095
- query: commonSearchQuery(this.#transport, query, options)
1072
+ query: commonSearchQuery(this.#transport, query, options),
1073
+ signal: options.signal
1096
1074
  });
1097
1075
  const excluded = options.exclude;
1098
1076
  if (excluded === undefined || excluded.length === 0) {
@@ -1146,7 +1124,8 @@ class TrendingResource {
1146
1124
  query: withLanguage(this.#transport.defaults, {
1147
1125
  language: options.language,
1148
1126
  page: options.page
1149
- })
1127
+ }),
1128
+ signal: options.signal
1150
1129
  });
1151
1130
  }
1152
1131
  movies(timeWindow = "day", options = {}) {
@@ -1154,7 +1133,8 @@ class TrendingResource {
1154
1133
  query: withLanguage(this.#transport.defaults, {
1155
1134
  language: options.language,
1156
1135
  page: options.page
1157
- })
1136
+ }),
1137
+ signal: options.signal
1158
1138
  });
1159
1139
  }
1160
1140
  tv(timeWindow = "day", options = {}) {
@@ -1162,7 +1142,8 @@ class TrendingResource {
1162
1142
  query: withLanguage(this.#transport.defaults, {
1163
1143
  language: options.language,
1164
1144
  page: options.page
1165
- })
1145
+ }),
1146
+ signal: options.signal
1166
1147
  });
1167
1148
  }
1168
1149
  people(timeWindow = "day", options = {}) {
@@ -1170,7 +1151,8 @@ class TrendingResource {
1170
1151
  query: withLanguage(this.#transport.defaults, {
1171
1152
  language: options.language,
1172
1153
  page: options.page
1173
- })
1154
+ }),
1155
+ signal: options.signal
1174
1156
  });
1175
1157
  }
1176
1158
  }
@@ -1183,22 +1165,26 @@ class TVResource {
1183
1165
  }
1184
1166
  popular(options = {}) {
1185
1167
  return this.#transport.get("/tv/popular", {
1186
- query: tvListQuery(this.#transport, options)
1168
+ query: tvListQuery(this.#transport, options),
1169
+ signal: options.signal
1187
1170
  });
1188
1171
  }
1189
1172
  topRated(options = {}) {
1190
1173
  return this.#transport.get("/tv/top_rated", {
1191
- query: tvListQuery(this.#transport, options)
1174
+ query: tvListQuery(this.#transport, options),
1175
+ signal: options.signal
1192
1176
  });
1193
1177
  }
1194
1178
  airingToday(options = {}) {
1195
1179
  return this.#transport.get("/tv/airing_today", {
1196
- query: tvListQuery(this.#transport, options)
1180
+ query: tvListQuery(this.#transport, options),
1181
+ signal: options.signal
1197
1182
  });
1198
1183
  }
1199
1184
  onTheAir(options = {}) {
1200
1185
  return this.#transport.get("/tv/on_the_air", {
1201
- query: tvListQuery(this.#transport, options)
1186
+ query: tvListQuery(this.#transport, options),
1187
+ signal: options.signal
1202
1188
  });
1203
1189
  }
1204
1190
  details(seriesId, options = {}) {
@@ -1206,14 +1192,16 @@ class TVResource {
1206
1192
  query: withLanguage(this.#transport.defaults, {
1207
1193
  append_to_response: toCommaList(options.appendToResponse),
1208
1194
  language: options.language
1209
- })
1195
+ }),
1196
+ signal: options.signal
1210
1197
  });
1211
1198
  }
1212
1199
  credits(seriesId, options = {}) {
1213
1200
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/credits`, {
1214
1201
  query: withLanguage(this.#transport.defaults, {
1215
1202
  language: options.language
1216
- })
1203
+ }),
1204
+ signal: options.signal
1217
1205
  });
1218
1206
  }
1219
1207
  images(seriesId, options = {}) {
@@ -1221,32 +1209,103 @@ class TVResource {
1221
1209
  query: withLanguage(this.#transport.defaults, {
1222
1210
  include_image_language: toCommaList(options.includeImageLanguage),
1223
1211
  language: options.language
1224
- })
1212
+ }),
1213
+ signal: options.signal
1225
1214
  });
1226
1215
  }
1227
1216
  recommendations(seriesId, options = {}) {
1228
1217
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/recommendations`, {
1229
- query: tvListQuery(this.#transport, options)
1218
+ query: tvListQuery(this.#transport, options),
1219
+ signal: options.signal
1230
1220
  });
1231
1221
  }
1232
1222
  similar(seriesId, options = {}) {
1233
1223
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/similar`, {
1234
- query: tvListQuery(this.#transport, options)
1224
+ query: tvListQuery(this.#transport, options),
1225
+ signal: options.signal
1235
1226
  });
1236
1227
  }
1237
1228
  videos(seriesId, options = {}) {
1238
1229
  return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/videos`, {
1239
1230
  query: withLanguage(this.#transport.defaults, {
1240
1231
  language: options.language
1241
- })
1232
+ }),
1233
+ signal: options.signal
1242
1234
  });
1243
1235
  }
1244
1236
  seasonDetails(seriesId, seasonNumber, options = {}) {
1245
- return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toId(seasonNumber, "seasonNumber")}`, {
1237
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}`, {
1238
+ query: withLanguage(this.#transport.defaults, {
1239
+ append_to_response: toCommaList(options.appendToResponse),
1240
+ language: options.language
1241
+ }),
1242
+ signal: options.signal
1243
+ });
1244
+ }
1245
+ seasonCredits(seriesId, seasonNumber, options = {}) {
1246
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/credits`, {
1247
+ query: withLanguage(this.#transport.defaults, {
1248
+ language: options.language
1249
+ }),
1250
+ signal: options.signal
1251
+ });
1252
+ }
1253
+ seasonAggregateCredits(seriesId, seasonNumber, options = {}) {
1254
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/aggregate_credits`, {
1255
+ query: withLanguage(this.#transport.defaults, {
1256
+ language: options.language
1257
+ }),
1258
+ signal: options.signal
1259
+ });
1260
+ }
1261
+ seasonImages(seriesId, seasonNumber, options = {}) {
1262
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/images`, {
1263
+ query: withLanguage(this.#transport.defaults, {
1264
+ include_image_language: toCommaList(options.includeImageLanguage),
1265
+ language: options.language
1266
+ }),
1267
+ signal: options.signal
1268
+ });
1269
+ }
1270
+ seasonVideos(seriesId, seasonNumber, options = {}) {
1271
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/videos`, {
1272
+ query: withLanguage(this.#transport.defaults, {
1273
+ language: options.language
1274
+ }),
1275
+ signal: options.signal
1276
+ });
1277
+ }
1278
+ episodeDetails(seriesId, seasonNumber, episodeNumber, options = {}) {
1279
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/season/${toSeasonNumber(seasonNumber)}/episode/${toId(episodeNumber, "episodeNumber")}`, {
1246
1280
  query: withLanguage(this.#transport.defaults, {
1247
1281
  append_to_response: toCommaList(options.appendToResponse),
1248
1282
  language: options.language
1249
- })
1283
+ }),
1284
+ signal: options.signal
1285
+ });
1286
+ }
1287
+ aggregateCredits(seriesId, options = {}) {
1288
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/aggregate_credits`, {
1289
+ query: withLanguage(this.#transport.defaults, {
1290
+ language: options.language
1291
+ }),
1292
+ signal: options.signal
1293
+ });
1294
+ }
1295
+ episodeGroups(seriesId, options = {}) {
1296
+ return this.#transport.get(`/tv/${toId(seriesId, "seriesId")}/episode_groups`, {
1297
+ query: withLanguage(this.#transport.defaults, {
1298
+ language: options.language
1299
+ }),
1300
+ signal: options.signal
1301
+ });
1302
+ }
1303
+ episodeGroup(groupId, options = {}) {
1304
+ return this.#transport.get(`/tv/episode_group/${toGroupId(groupId)}`, {
1305
+ query: withLanguage(this.#transport.defaults, {
1306
+ language: options.language
1307
+ }),
1308
+ signal: options.signal
1250
1309
  });
1251
1310
  }
1252
1311
  }
@@ -1269,13 +1328,17 @@ class TMDBClient {
1269
1328
  tv;
1270
1329
  #transport;
1271
1330
  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;
1331
+ let imageTransform = options.images?.transform;
1332
+ if (options.transport === undefined) {
1333
+ const builtInClient = new TMDBHttpClient(options);
1334
+ this.#transport = builtInClient;
1335
+ imageTransform = builtInClient.imageTransform;
1336
+ } else {
1337
+ this.#transport = options.transport;
1338
+ }
1276
1339
  this.configuration = new ConfigurationResource(this.#transport);
1277
1340
  this.genres = new GenresResource(this.#transport);
1278
- this.images = new ImagesHelper(this.#transport, builtInClient ? builtInClient.imageTransform : fallbackTransform);
1341
+ this.images = new ImagesHelper(this.#transport, imageTransform);
1279
1342
  this.movies = new MoviesResource(this.#transport);
1280
1343
  this.people = new PeopleResource(this.#transport);
1281
1344
  this.search = new SearchResource(this.#transport);
@@ -1353,14 +1416,50 @@ function errorMessage(error) {
1353
1416
  }
1354
1417
  return String(error);
1355
1418
  }
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);
1419
+ var EXIT_CODE_KINDS = {
1420
+ 2: "usage",
1421
+ 3: "auth",
1422
+ 4: "not_found",
1423
+ 5: "rate_limit"
1424
+ };
1425
+ function errorKind(error) {
1426
+ if (error instanceof CliError) {
1427
+ return EXIT_CODE_KINDS[error.exitCode] ?? "internal";
1428
+ }
1429
+ if (error instanceof TMDBRateLimitError) {
1430
+ return "rate_limit";
1431
+ }
1432
+ if (error instanceof TMDBResponseError) {
1433
+ if (error.status === 401 || error.status === 403)
1434
+ return "auth";
1435
+ if (error.status === 404)
1436
+ return "not_found";
1437
+ if (error.status === 429)
1438
+ return "rate_limit";
1439
+ return "api";
1440
+ }
1441
+ if (error instanceof TMDBRequestError || error instanceof TMDBTimeoutError) {
1442
+ return "network";
1443
+ }
1444
+ return "internal";
1445
+ }
1446
+ function toErrorRecord(error) {
1447
+ const record = {
1448
+ type: "error",
1449
+ kind: errorKind(error),
1450
+ message: errorMessage(error),
1451
+ exitCode: exitCodeFor(error)
1452
+ };
1453
+ if (error instanceof TMDBResponseError) {
1454
+ record.status = error.status;
1455
+ if (error.requestId !== null) {
1456
+ record.requestId = error.requestId;
1457
+ }
1458
+ }
1459
+ if (error instanceof TMDBRateLimitError) {
1460
+ record.retryAfter = error.retryAfter;
1363
1461
  }
1462
+ return record;
1364
1463
  }
1365
1464
 
1366
1465
  // src/cli/config.ts
@@ -1581,9 +1680,135 @@ function missingCredentialsMessage() {
1581
1680
  `);
1582
1681
  }
1583
1682
 
1683
+ // src/cli/options.ts
1684
+ var jsonArg = {
1685
+ json: {
1686
+ type: "boolean",
1687
+ description: "Alias for --format json"
1688
+ }
1689
+ };
1690
+ var formatArg = {
1691
+ format: {
1692
+ type: "string",
1693
+ description: "Output format: text, json, or ndjson (default: text)"
1694
+ },
1695
+ compact: {
1696
+ type: "boolean",
1697
+ description: "Print JSON on a single line"
1698
+ },
1699
+ fields: {
1700
+ type: "string",
1701
+ description: "Comma-separated top-level fields to keep in JSON output"
1702
+ }
1703
+ };
1704
+ var outputArgs = {
1705
+ ...jsonArg,
1706
+ ...formatArg
1707
+ };
1708
+ var paginationArgs = {
1709
+ all: {
1710
+ type: "boolean",
1711
+ description: "Fetch every page instead of one (requires --format ndjson)"
1712
+ },
1713
+ "max-items": {
1714
+ type: "string",
1715
+ description: "Stop after this many items (requires --format ndjson)"
1716
+ }
1717
+ };
1718
+ var commonArgs = {
1719
+ ...outputArgs,
1720
+ lang: {
1721
+ type: "string",
1722
+ description: "TMDB language code (default: en-US)"
1723
+ }
1724
+ };
1725
+ var SEARCH_TYPES = ["all", "movie", "tv", "person"];
1726
+ function parseSearchType(value) {
1727
+ if (!SEARCH_TYPES.includes(value)) {
1728
+ throw new CliError(`Invalid --type "${value}". Expected one of: ${SEARCH_TYPES.join(", ")}.`, 2);
1729
+ }
1730
+ return value;
1731
+ }
1732
+ function parsePage(value) {
1733
+ if (value === undefined || value === "") {
1734
+ return 1;
1735
+ }
1736
+ const page = Number(value);
1737
+ if (!Number.isInteger(page) || page < 1) {
1738
+ throw new CliError("Invalid --page value: expected a positive integer.", 2);
1739
+ }
1740
+ return page;
1741
+ }
1742
+ function parseId(value, label) {
1743
+ const id = Number(value);
1744
+ if (!Number.isInteger(id) || id <= 0) {
1745
+ throw new CliError(`Invalid ${label}: expected a positive integer.`, 2);
1746
+ }
1747
+ return id;
1748
+ }
1749
+ function parseSeasonNumber(value) {
1750
+ const season = Number(value);
1751
+ if (!Number.isInteger(season) || season < 0) {
1752
+ throw new CliError("Invalid season number: expected a non-negative integer.", 2);
1753
+ }
1754
+ return season;
1755
+ }
1756
+ function parseGroupId(value) {
1757
+ const groupId = value.trim();
1758
+ if (!/^[A-Za-z0-9_-]+$/.test(groupId)) {
1759
+ throw new CliError("Invalid group ID: expected a non-empty identifier.", 2);
1760
+ }
1761
+ return groupId;
1762
+ }
1763
+ function parseLimit(value, fallback = 10) {
1764
+ if (value === undefined || value === "") {
1765
+ return fallback;
1766
+ }
1767
+ const limit = Number(value);
1768
+ if (!Number.isInteger(limit) || limit < 1) {
1769
+ throw new CliError("Invalid --limit value: expected a positive integer.", 2);
1770
+ }
1771
+ return limit;
1772
+ }
1773
+ function parseCommaList(value) {
1774
+ if (value === undefined) {
1775
+ return;
1776
+ }
1777
+ const values = value.split(",").map((item) => item.trim()).filter(Boolean);
1778
+ return values.length > 0 ? values : undefined;
1779
+ }
1780
+ var TRENDING_WINDOWS = ["day", "week"];
1781
+ function parseWindow(value) {
1782
+ if (!TRENDING_WINDOWS.includes(value)) {
1783
+ throw new CliError(`Invalid --window "${value}". Expected one of: ${TRENDING_WINDOWS.join(", ")}.`, 2);
1784
+ }
1785
+ return value;
1786
+ }
1787
+ var GENRE_TYPES = ["movie", "tv"];
1788
+ function parseGenreType(value) {
1789
+ if (!GENRE_TYPES.includes(value)) {
1790
+ throw new CliError(`Invalid --type "${value}". Expected one of: ${GENRE_TYPES.join(", ")}.`, 2);
1791
+ }
1792
+ return value;
1793
+ }
1794
+ var MOVIE_LIST_CATEGORIES = ["popular", "top-rated", "now-playing", "upcoming"];
1795
+ function parseMovieListCategory(value) {
1796
+ if (!MOVIE_LIST_CATEGORIES.includes(value)) {
1797
+ throw new CliError(`Invalid movie list category "${value}". Expected one of: ${MOVIE_LIST_CATEGORIES.join(", ")}.`, 2);
1798
+ }
1799
+ return value;
1800
+ }
1801
+ var TV_LIST_CATEGORIES = ["popular", "top-rated", "airing-today", "on-the-air"];
1802
+ function parseTVListCategory(value) {
1803
+ if (!TV_LIST_CATEGORIES.includes(value)) {
1804
+ throw new CliError(`Invalid TV list category "${value}". Expected one of: ${TV_LIST_CATEGORIES.join(", ")}.`, 2);
1805
+ }
1806
+ return value;
1807
+ }
1808
+
1584
1809
  // src/cli/format.ts
1585
- function printJson(value) {
1586
- process.stdout.write(`${JSON.stringify(value, null, 2)}
1810
+ function printJson(value, compact = false) {
1811
+ process.stdout.write(`${compact ? JSON.stringify(value) : JSON.stringify(value, null, 2)}
1587
1812
  `);
1588
1813
  }
1589
1814
  function printSearchResults(response, type) {
@@ -1706,6 +1931,30 @@ function printCredits(credits, limit = 10) {
1706
1931
  }
1707
1932
  writeLines(lines);
1708
1933
  }
1934
+ function printAggregateCredits(credits, limit = 10) {
1935
+ const lines = [];
1936
+ if (credits.cast.length > 0) {
1937
+ lines.push("Cast:");
1938
+ for (const member of credits.cast.slice(0, limit)) {
1939
+ const roles = member.roles.map((role) => `${role.character} (${role.episode_count} eps)`).join(", ");
1940
+ lines.push(` ${member.name} as ${roles} #${member.id}`);
1941
+ }
1942
+ }
1943
+ if (credits.crew.length > 0) {
1944
+ if (lines.length > 0) {
1945
+ lines.push("");
1946
+ }
1947
+ lines.push("Crew:");
1948
+ for (const member of credits.crew.slice(0, limit)) {
1949
+ const jobs = member.jobs.map((job) => `${job.job} (${job.episode_count} eps)`).join(", ");
1950
+ lines.push(` ${member.name} — ${jobs} (${member.department}) #${member.id}`);
1951
+ }
1952
+ }
1953
+ if (lines.length === 0) {
1954
+ lines.push("No credits.");
1955
+ }
1956
+ writeLines(lines);
1957
+ }
1709
1958
  function printImages(images, tmdb) {
1710
1959
  const lines = [];
1711
1960
  appendImageSection(lines, "Posters", images.posters, "w500", tmdb);
@@ -1716,6 +1965,14 @@ function printImages(images, tmdb) {
1716
1965
  }
1717
1966
  writeLines(lines);
1718
1967
  }
1968
+ function printSeasonImages(images, tmdb) {
1969
+ const lines = [];
1970
+ appendImageSection(lines, "Posters", images.posters, "w500", tmdb);
1971
+ if (lines.length === 0) {
1972
+ lines.push("No images.");
1973
+ }
1974
+ writeLines(lines);
1975
+ }
1719
1976
  function printVideos(videos) {
1720
1977
  if (videos.results.length === 0) {
1721
1978
  writeLines(["No videos."]);
@@ -1750,6 +2007,49 @@ function printSeason(season, tmdb) {
1750
2007
  }
1751
2008
  writeLines(lines);
1752
2009
  }
2010
+ function printEpisode(episode, tmdb, seriesId) {
2011
+ const lines = [`${episodeCode(episode)} · ${episode.name}`];
2012
+ const facts = [
2013
+ episode.air_date,
2014
+ episode.runtime ? `${episode.runtime} min` : undefined,
2015
+ `★ ${episode.vote_average.toFixed(1)}/10 (${episode.vote_count} votes)`
2016
+ ].filter(Boolean);
2017
+ lines.push(facts.join(" · "));
2018
+ lines.push(`TMDB: https://www.themoviedb.org/tv/${seriesId}/season/${episode.season_number}/episode/${episode.episode_number}`);
2019
+ const still = tmdb.images.url(episode.still_path, "w300");
2020
+ if (still) {
2021
+ lines.push(`Still: ${still}`);
2022
+ }
2023
+ if (episode.overview) {
2024
+ lines.push("", episode.overview);
2025
+ }
2026
+ writeLines(lines);
2027
+ }
2028
+ function printEpisodeGroups(response) {
2029
+ if (response.results.length === 0) {
2030
+ writeLines(["No episode groups."]);
2031
+ return;
2032
+ }
2033
+ writeLines(response.results.map((group) => `${group.id} ${group.name} (${group.group_count} groups, ${group.episode_count} episodes)`));
2034
+ }
2035
+ function printEpisodeGroup(group) {
2036
+ const lines = [group.name];
2037
+ if (group.description) {
2038
+ lines.push(group.description);
2039
+ }
2040
+ if (group.groups.length === 0) {
2041
+ lines.push("", "No episodes.");
2042
+ writeLines(lines);
2043
+ return;
2044
+ }
2045
+ for (const section of group.groups) {
2046
+ lines.push("", section.name);
2047
+ for (const episode of section.episodes) {
2048
+ lines.push(` ${episode.order}. ${episodeCode(episode)} · ${episode.name} #${episode.id}`);
2049
+ }
2050
+ }
2051
+ writeLines(lines);
2052
+ }
1753
2053
  function printPerson(person, tmdb) {
1754
2054
  const lines = [person.name];
1755
2055
  const facts = [
@@ -1851,6 +2151,9 @@ function writeLines(lines) {
1851
2151
  `)}
1852
2152
  `);
1853
2153
  }
2154
+ function episodeCode(episode) {
2155
+ return `S${String(episode.season_number).padStart(2, "0")}E${String(episode.episode_number).padStart(2, "0")}`;
2156
+ }
1854
2157
  function formatSearchResult(result, index, fallbackType) {
1855
2158
  const title = "title" in result ? result.title : result.name;
1856
2159
  const date = "release_date" in result ? result.release_date : ("first_air_date" in result) ? result.first_air_date : "";
@@ -1860,95 +2163,211 @@ function formatSearchResult(result, index, fallbackType) {
1860
2163
  return `${String(index + 1).padStart(2)}. ${title}${year}${score} ${mediaType} ${result.id}`;
1861
2164
  }
1862
2165
 
1863
- // src/cli/options.ts
1864
- var jsonArg = {
1865
- json: {
1866
- type: "boolean",
1867
- description: "Print the raw TMDB JSON response"
2166
+ // src/cli/output.ts
2167
+ var OUTPUT_FORMATS = ["text", "json", "ndjson"];
2168
+ var MAX_PAGE = 500;
2169
+ var FORMAT_ALIASES = { jsonl: "ndjson" };
2170
+ function resolveOutput(args, options = {}) {
2171
+ const explicitFormat = args.format !== undefined && args.format.trim() !== "";
2172
+ const format = parseFormat(args.format);
2173
+ const fields = parseFields(args.fields);
2174
+ const all = args.all === true;
2175
+ const maxItems = parseMaxItems(args["max-items"]);
2176
+ if (args.json && explicitFormat && format !== "json") {
2177
+ throw new CliError(`--json cannot be combined with --format ${format}.`, 2);
2178
+ }
2179
+ const resolvedFormat = args.json ? "json" : format;
2180
+ if (fields && resolvedFormat === "text") {
2181
+ throw new CliError("--fields requires --format json or ndjson.", 2);
2182
+ }
2183
+ if (all || maxItems !== undefined) {
2184
+ if (resolvedFormat !== "ndjson") {
2185
+ throw new CliError(`${all ? "--all" : "--max-items"} requires --format ndjson.`, 2);
2186
+ }
2187
+ if (options.paginated !== true) {
2188
+ throw new CliError(`${all ? "--all" : "--max-items"} is only supported by list commands.`, 2);
2189
+ }
1868
2190
  }
1869
- };
1870
- var commonArgs = {
1871
- ...jsonArg,
1872
- lang: {
1873
- type: "string",
1874
- description: "TMDB language code (default: en-US)"
2191
+ return {
2192
+ ...all ? { all: true } : {},
2193
+ compact: args.compact === true,
2194
+ ...fields ? { fields } : {},
2195
+ format: resolvedFormat,
2196
+ ...maxItems !== undefined ? { maxItems } : {}
2197
+ };
2198
+ }
2199
+ function printResult(value, output, renderText) {
2200
+ if (output.format === "text") {
2201
+ renderText();
2202
+ return;
1875
2203
  }
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);
2204
+ if (output.format === "json") {
2205
+ printJson(output.fields ? projectPayload(value, output.fields) : value, output.compact);
2206
+ return;
1881
2207
  }
1882
- return value;
1883
- }
1884
- function parsePage(value) {
1885
- if (value === undefined || value === "") {
1886
- return 1;
2208
+ printNdjson(value, output);
2209
+ }
2210
+ async function printPaged({
2211
+ fetchPage,
2212
+ output,
2213
+ renderText,
2214
+ startPage
2215
+ }) {
2216
+ let response = await fetchPage(startPage);
2217
+ if (output.format === "text") {
2218
+ renderText(response);
2219
+ return;
1887
2220
  }
1888
- const page = Number(value);
1889
- if (!Number.isInteger(page) || page < 1) {
1890
- throw new CliError("Invalid --page value: expected a positive integer.", 2);
2221
+ if (output.format === "json") {
2222
+ printJson(output.fields ? projectPayload(response, output.fields) : response, output.compact);
2223
+ return;
1891
2224
  }
1892
- return page;
2225
+ const maxItems = output.maxItems ?? Number.POSITIVE_INFINITY;
2226
+ let page = startPage;
2227
+ let pages = 0;
2228
+ let count = 0;
2229
+ writeNdjson({ ...envelopeOf(response), type: "meta" });
2230
+ while (true) {
2231
+ pages += 1;
2232
+ for (const item of response.results) {
2233
+ if (count >= maxItems) {
2234
+ break;
2235
+ }
2236
+ writeNdjson({
2237
+ type: "item",
2238
+ index: count,
2239
+ value: output.fields ? projectRecord(item, output.fields, count === 0) : item
2240
+ });
2241
+ count += 1;
2242
+ }
2243
+ if (!output.all || count >= maxItems || response.results.length === 0) {
2244
+ break;
2245
+ }
2246
+ if (page >= Math.min(response.total_pages, MAX_PAGE)) {
2247
+ break;
2248
+ }
2249
+ page += 1;
2250
+ response = await fetchPage(page);
2251
+ }
2252
+ writeNdjson({ type: "summary", count, pages });
1893
2253
  }
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);
2254
+ async function runSafely(args, task, options = {}) {
2255
+ let output;
2256
+ try {
2257
+ output = resolveOutput(args, options);
2258
+ await task(output);
2259
+ } catch (error) {
2260
+ writeError(error, output ?? fallbackOutput(args));
2261
+ process.exitCode = exitCodeFor(error);
1898
2262
  }
1899
- return id;
1900
2263
  }
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);
2264
+ function writeError(error, output) {
2265
+ if (output.format === "text") {
2266
+ process.stderr.write(`${errorMessage(error)}
2267
+ `);
2268
+ return;
1905
2269
  }
1906
- return season;
2270
+ process.stderr.write(`${JSON.stringify(toErrorRecord(error))}
2271
+ `);
1907
2272
  }
1908
- function parseLimit(value, fallback = 10) {
2273
+ function fallbackOutput(args) {
2274
+ const format = args.format?.trim().toLowerCase();
2275
+ const wantsJson = args.json === true || format === "json" || format === "jsonl" || format === "ndjson";
2276
+ return {
2277
+ compact: args.compact === true,
2278
+ format: wantsJson ? "json" : "text"
2279
+ };
2280
+ }
2281
+ function parseFormat(value) {
1909
2282
  if (value === undefined || value === "") {
1910
- return fallback;
2283
+ return "text";
1911
2284
  }
1912
- const limit = Number(value);
1913
- if (!Number.isInteger(limit) || limit < 1) {
1914
- throw new CliError("Invalid --limit value: expected a positive integer.", 2);
2285
+ const requested = value.trim().toLowerCase();
2286
+ const format = FORMAT_ALIASES[requested] ?? requested;
2287
+ if (!OUTPUT_FORMATS.includes(format)) {
2288
+ throw new CliError(`Invalid --format "${value}". Expected one of: ${OUTPUT_FORMATS.join(", ")}.`, 2);
1915
2289
  }
1916
- return limit;
2290
+ return format;
1917
2291
  }
1918
- function parseCommaList(value) {
2292
+ function parseFields(value) {
1919
2293
  if (value === undefined) {
1920
2294
  return;
1921
2295
  }
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);
2296
+ const fields = [
2297
+ ...new Set(value.split(",").map((field) => field.trim()).filter(Boolean))
2298
+ ];
2299
+ if (fields.length === 0) {
2300
+ throw new CliError("--fields requires at least one field name.", 2);
1929
2301
  }
1930
- return value;
2302
+ return fields;
1931
2303
  }
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);
2304
+ function parseMaxItems(value) {
2305
+ if (value === undefined) {
2306
+ return;
1936
2307
  }
1937
- return value;
2308
+ const maxItems = Number(value);
2309
+ if (!Number.isInteger(maxItems) || maxItems < 1) {
2310
+ throw new CliError("Invalid --max-items value: expected a positive integer.", 2);
2311
+ }
2312
+ return maxItems;
2313
+ }
2314
+ function printNdjson(value, output) {
2315
+ if (isRecord(value) && Array.isArray(value.results)) {
2316
+ const { results, ...envelope } = value;
2317
+ writeNdjson({ ...envelope, type: "meta" });
2318
+ results.forEach((item, index) => {
2319
+ writeNdjson({
2320
+ type: "item",
2321
+ index,
2322
+ value: output.fields ? projectRecord(item, output.fields, index === 0) : item
2323
+ });
2324
+ });
2325
+ writeNdjson({ type: "summary", count: results.length, pages: 1 });
2326
+ return;
2327
+ }
2328
+ writeNdjson({
2329
+ type: "result",
2330
+ value: output.fields ? projectRecord(value, output.fields, true) : value
2331
+ });
1938
2332
  }
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);
2333
+ function writeNdjson(record) {
2334
+ process.stdout.write(`${JSON.stringify(record)}
2335
+ `);
2336
+ }
2337
+ function envelopeOf(response) {
2338
+ const envelope = { ...response };
2339
+ delete envelope.results;
2340
+ return envelope;
2341
+ }
2342
+ function projectPayload(value, fields) {
2343
+ if (!isRecord(value)) {
2344
+ throw new CliError("--fields requires an object response.", 2);
1943
2345
  }
1944
- return value;
2346
+ if (Array.isArray(value.results)) {
2347
+ return {
2348
+ ...value,
2349
+ results: value.results.map((item, index) => projectRecord(item, fields, index === 0))
2350
+ };
2351
+ }
2352
+ return projectRecord(value, fields, true);
1945
2353
  }
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);
2354
+ function projectRecord(record, fields, validate) {
2355
+ if (!isRecord(record)) {
2356
+ throw new CliError("--fields requires object records.", 2);
1950
2357
  }
1951
- return value;
2358
+ const projected = {};
2359
+ for (const field of fields) {
2360
+ if (Object.hasOwn(record, field)) {
2361
+ projected[field] = record[field];
2362
+ }
2363
+ }
2364
+ if (validate && Object.keys(projected).length === 0) {
2365
+ throw new CliError(`Unknown --fields value(s): ${fields.join(", ")}. Available: ${Object.keys(record).sort().join(", ")}.`, 2);
2366
+ }
2367
+ return projected;
2368
+ }
2369
+ function isRecord(value) {
2370
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1952
2371
  }
1953
2372
 
1954
2373
  // src/cli/prompt.ts
@@ -2018,7 +2437,7 @@ var authCommand = defineCommand({
2018
2437
  }
2019
2438
  },
2020
2439
  async run({ args }) {
2021
- await runSafely(async () => {
2440
+ await runSafely({}, async () => {
2022
2441
  const existing = resolveCredentials();
2023
2442
  if (!args.force && existing.source !== "none") {
2024
2443
  if (!process.stdin.isTTY) {
@@ -2085,9 +2504,9 @@ Saved ${credential.kind} to ${getConfigPath()}
2085
2504
  name: "status",
2086
2505
  description: "Show credential source, token mask, and config path"
2087
2506
  },
2088
- args: { ...jsonArg },
2507
+ args: { ...outputArgs },
2089
2508
  async run({ args }) {
2090
- await runSafely(async () => {
2509
+ await runSafely(args, async (output) => {
2091
2510
  const credentials = resolveCredentials();
2092
2511
  const path = getConfigPath();
2093
2512
  const configExists = existsSync2(path);
@@ -2101,10 +2520,6 @@ Saved ${credential.kind} to ${getConfigPath()}
2101
2520
  language: credentials.config.language ?? null,
2102
2521
  region: credentials.config.region ?? null
2103
2522
  };
2104
- if (args.json) {
2105
- printJson(view);
2106
- return;
2107
- }
2108
2523
  const lines = [
2109
2524
  `Credential source: ${describeSource(credentials)}`,
2110
2525
  `Token: ${view.token ?? "(not set)"}`,
@@ -2119,9 +2534,9 @@ Saved ${credential.kind} to ${getConfigPath()}
2119
2534
  if (credentials.source === "none") {
2120
2535
  lines.push("", "Run `tmdb auth login` to set up credentials.");
2121
2536
  }
2122
- process.stdout.write(`${lines.join(`
2537
+ printResult(view, output, () => process.stdout.write(`${lines.join(`
2123
2538
  `)}
2124
- `);
2539
+ `));
2125
2540
  });
2126
2541
  }
2127
2542
  }),
@@ -2131,7 +2546,7 @@ Saved ${credential.kind} to ${getConfigPath()}
2131
2546
  description: "Remove saved API credentials from the config file"
2132
2547
  },
2133
2548
  async run() {
2134
- await runSafely(async () => {
2549
+ await runSafely({}, async () => {
2135
2550
  const config = readConfig();
2136
2551
  const hadCredentials = Boolean(config.accessToken || config.apiKey);
2137
2552
  if (hadCredentials) {
@@ -2238,9 +2653,9 @@ var configCommand = defineCommand({
2238
2653
  name: "show",
2239
2654
  description: "Show the resolved config with secrets masked"
2240
2655
  },
2241
- args: { ...jsonArg },
2656
+ args: { ...outputArgs },
2242
2657
  async run({ args }) {
2243
- await runSafely(async () => {
2658
+ await runSafely(args, async (output) => {
2244
2659
  const config = readConfig();
2245
2660
  const path = getConfigPath();
2246
2661
  const view = {
@@ -2250,19 +2665,17 @@ var configCommand = defineCommand({
2250
2665
  language: config.language ?? null,
2251
2666
  region: config.region ?? null
2252
2667
  };
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(`
2668
+ printResult(view, output, () => {
2669
+ process.stdout.write([
2670
+ `Path: ${path}`,
2671
+ `accessToken: ${view.accessToken ?? "(not set)"}`,
2672
+ `apiKey: ${view.apiKey ?? "(not set)"}`,
2673
+ `language: ${view.language ?? "(not set)"}`,
2674
+ `region: ${view.region ?? "(not set)"}`
2675
+ ].join(`
2264
2676
  `) + `
2265
2677
  `);
2678
+ });
2266
2679
  });
2267
2680
  }
2268
2681
  }),
@@ -2281,18 +2694,16 @@ var configCommand = defineCommand({
2281
2694
  name: "test",
2282
2695
  description: "Verify credentials with an authenticated TMDB request"
2283
2696
  },
2284
- args: { ...jsonArg },
2697
+ args: { ...outputArgs },
2285
2698
  async run({ args }) {
2286
- await runSafely(async () => {
2699
+ await runSafely(args, async (output) => {
2287
2700
  const client = createClient();
2288
2701
  await client.movies.details(550);
2289
- if (args.json) {
2290
- printJson({ ok: true });
2291
- return;
2292
- }
2293
- process.stdout.write(`TMDB connection OK
2702
+ printResult({ ok: true }, output, () => {
2703
+ process.stdout.write(`TMDB connection OK
2294
2704
  Credentials are valid.
2295
2705
  `);
2706
+ });
2296
2707
  });
2297
2708
  }
2298
2709
  }),
@@ -2314,7 +2725,7 @@ Credentials are valid.
2314
2725
  }
2315
2726
  },
2316
2727
  async run({ args }) {
2317
- await runSafely(async () => {
2728
+ await runSafely({}, async () => {
2318
2729
  const field = parseConfigKey(args.key);
2319
2730
  const path = getConfigPath();
2320
2731
  if (field === "accessToken" || field === "apiKey") {
@@ -2354,7 +2765,7 @@ Credentials are valid.
2354
2765
  }
2355
2766
  },
2356
2767
  async run({ args }) {
2357
- await runSafely(async () => {
2768
+ await runSafely({}, async () => {
2358
2769
  const field = parseConfigKey(args.key);
2359
2770
  unsetConfigValue(field);
2360
2771
  process.stdout.write(`Removed ${field} from ${getConfigPath()}
@@ -2386,15 +2797,11 @@ var genresCommand = defineCommand({
2386
2797
  }
2387
2798
  },
2388
2799
  async run({ args }) {
2389
- await runSafely(async () => {
2800
+ await runSafely(args, async (output) => {
2390
2801
  const type = parseGenreType(args.type);
2391
2802
  const client = createClient({ language: args.lang });
2392
2803
  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
- }
2804
+ printResult(response, output, () => printGenres(response));
2398
2805
  });
2399
2806
  }
2400
2807
  });
@@ -2413,6 +2820,7 @@ var movieCommand = defineCommand({
2413
2820
  },
2414
2821
  args: {
2415
2822
  ...commonArgs,
2823
+ ...paginationArgs,
2416
2824
  category: {
2417
2825
  type: "positional",
2418
2826
  description: "popular, top-rated, now-playing, or upcoming",
@@ -2428,17 +2836,20 @@ var movieCommand = defineCommand({
2428
2836
  }
2429
2837
  },
2430
2838
  async run({ args }) {
2431
- await runSafely(async () => {
2839
+ await runSafely(args, async (output) => {
2432
2840
  const category = parseMovieListCategory(args.category);
2433
- const options = { page: parsePage(args.page), region: args.region };
2841
+ const params = { page: parsePage(args.page), region: args.region };
2434
2842
  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
- });
2843
+ await printPaged({
2844
+ fetchPage: (page) => {
2845
+ const options = { ...params, page };
2846
+ return category === "top-rated" ? client.movies.topRated(options) : category === "now-playing" ? client.movies.nowPlaying(options) : category === "upcoming" ? client.movies.upcoming(options) : client.movies.popular(options);
2847
+ },
2848
+ output,
2849
+ renderText: (response) => printSearchResults(response, "movie"),
2850
+ startPage: params.page
2851
+ });
2852
+ }, { paginated: true });
2442
2853
  }
2443
2854
  }),
2444
2855
  get: defineCommand({
@@ -2459,16 +2870,12 @@ var movieCommand = defineCommand({
2459
2870
  }
2460
2871
  },
2461
2872
  async run({ args }) {
2462
- await runSafely(async () => {
2873
+ await runSafely(args, async (output) => {
2463
2874
  const id = parseId(args.id, "movie ID");
2464
2875
  const client = createClient({ language: args.lang });
2465
2876
  const appendToResponse = parseCommaList(args.append);
2466
2877
  const movie = await client.movies.details(id, { appendToResponse });
2467
- if (args.json) {
2468
- printJson(movie);
2469
- } else {
2470
- printMovie(movie, client);
2471
- }
2878
+ printResult(movie, output, () => printMovie(movie, client));
2472
2879
  });
2473
2880
  }
2474
2881
  }),
@@ -2490,16 +2897,12 @@ var movieCommand = defineCommand({
2490
2897
  }
2491
2898
  },
2492
2899
  async run({ args }) {
2493
- await runSafely(async () => {
2900
+ await runSafely(args, async (output) => {
2494
2901
  const id = parseId(args.id, "movie ID");
2495
2902
  const limit = parseLimit(args.limit);
2496
2903
  const client = createClient({ language: args.lang });
2497
2904
  const credits = await client.movies.credits(id);
2498
- if (args.json) {
2499
- printJson(credits);
2500
- } else {
2501
- printCredits(credits, limit);
2502
- }
2905
+ printResult(credits, output, () => printCredits(credits, limit));
2503
2906
  });
2504
2907
  }
2505
2908
  }),
@@ -2521,16 +2924,12 @@ var movieCommand = defineCommand({
2521
2924
  }
2522
2925
  },
2523
2926
  async run({ args }) {
2524
- await runSafely(async () => {
2927
+ await runSafely(args, async (output) => {
2525
2928
  const id = parseId(args.id, "movie ID");
2526
2929
  const includeImageLanguage = parseCommaList(args["image-language"]);
2527
2930
  const client = createClient({ language: args.lang });
2528
2931
  const images = await client.movies.images(id, { includeImageLanguage });
2529
- if (args.json) {
2530
- printJson(images);
2531
- } else {
2532
- printImages(images, client);
2533
- }
2932
+ printResult(images, output, () => printImages(images, client));
2534
2933
  });
2535
2934
  }
2536
2935
  }),
@@ -2548,15 +2947,11 @@ var movieCommand = defineCommand({
2548
2947
  }
2549
2948
  },
2550
2949
  async run({ args }) {
2551
- await runSafely(async () => {
2950
+ await runSafely(args, async (output) => {
2552
2951
  const id = parseId(args.id, "movie ID");
2553
2952
  const client = createClient({ language: args.lang });
2554
2953
  const videos = await client.movies.videos(id);
2555
- if (args.json) {
2556
- printJson(videos);
2557
- } else {
2558
- printVideos(videos);
2559
- }
2954
+ printResult(videos, output, () => printVideos(videos));
2560
2955
  });
2561
2956
  }
2562
2957
  }),
@@ -2579,20 +2974,21 @@ var movieCommand = defineCommand({
2579
2974
  region: {
2580
2975
  type: "string",
2581
2976
  description: "ISO 3166-1 region code"
2582
- }
2977
+ },
2978
+ ...paginationArgs
2583
2979
  },
2584
2980
  async run({ args }) {
2585
- await runSafely(async () => {
2981
+ await runSafely(args, async (output) => {
2586
2982
  const id = parseId(args.id, "movie ID");
2587
- const options = { page: parsePage(args.page), region: args.region };
2983
+ const params = { page: parsePage(args.page), region: args.region };
2588
2984
  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
- });
2985
+ await printPaged({
2986
+ fetchPage: (page) => client.movies.similar(id, { ...params, page }),
2987
+ output,
2988
+ renderText: (response) => printSearchResults(response, "movie"),
2989
+ startPage: params.page
2990
+ });
2991
+ }, { paginated: true });
2596
2992
  }
2597
2993
  }),
2598
2994
  recommendations: defineCommand({
@@ -2614,20 +3010,21 @@ var movieCommand = defineCommand({
2614
3010
  region: {
2615
3011
  type: "string",
2616
3012
  description: "ISO 3166-1 region code"
2617
- }
3013
+ },
3014
+ ...paginationArgs
2618
3015
  },
2619
3016
  async run({ args }) {
2620
- await runSafely(async () => {
3017
+ await runSafely(args, async (output) => {
2621
3018
  const id = parseId(args.id, "movie ID");
2622
- const options = { page: parsePage(args.page), region: args.region };
3019
+ const params = { page: parsePage(args.page), region: args.region };
2623
3020
  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
- });
3021
+ await printPaged({
3022
+ fetchPage: (page) => client.movies.recommendations(id, { ...params, page }),
3023
+ output,
3024
+ renderText: (response) => printSearchResults(response, "movie"),
3025
+ startPage: params.page
3026
+ });
3027
+ }, { paginated: true });
2631
3028
  }
2632
3029
  })
2633
3030
  }
@@ -2654,15 +3051,11 @@ var personCommand = defineCommand({
2654
3051
  }
2655
3052
  },
2656
3053
  async run({ args }) {
2657
- await runSafely(async () => {
3054
+ await runSafely(args, async (output) => {
2658
3055
  const id = parseId(args.id, "person ID");
2659
3056
  const client = createClient({ language: args.lang });
2660
3057
  const person = await client.people.details(id);
2661
- if (args.json) {
2662
- printJson(person);
2663
- } else {
2664
- printPerson(person, client);
2665
- }
3058
+ printResult(person, output, () => printPerson(person, client));
2666
3059
  });
2667
3060
  }
2668
3061
  }),
@@ -2684,16 +3077,12 @@ var personCommand = defineCommand({
2684
3077
  }
2685
3078
  },
2686
3079
  async run({ args }) {
2687
- await runSafely(async () => {
3080
+ await runSafely(args, async (output) => {
2688
3081
  const id = parseId(args.id, "person ID");
2689
3082
  const limit = parseLimit(args.limit);
2690
3083
  const client = createClient({ language: args.lang });
2691
3084
  const credits = await client.people.combinedCredits(id);
2692
- if (args.json) {
2693
- printJson(credits);
2694
- } else {
2695
- printPersonCredits(credits, limit);
2696
- }
3085
+ printResult(credits, output, () => printPersonCredits(credits, limit));
2697
3086
  });
2698
3087
  }
2699
3088
  })
@@ -2708,6 +3097,7 @@ var searchCommand = defineCommand({
2708
3097
  },
2709
3098
  args: {
2710
3099
  ...commonArgs,
3100
+ ...paginationArgs,
2711
3101
  query: {
2712
3102
  type: "positional",
2713
3103
  description: "Search query",
@@ -2729,7 +3119,7 @@ var searchCommand = defineCommand({
2729
3119
  }
2730
3120
  },
2731
3121
  async run({ args }) {
2732
- await runSafely(async () => {
3122
+ await runSafely(args, async (output) => {
2733
3123
  const query = args.query.trim();
2734
3124
  if (!query) {
2735
3125
  throw new CliError("Search query must not be empty.", 2);
@@ -2741,14 +3131,16 @@ var searchCommand = defineCommand({
2741
3131
  throw new CliError("--year is only supported with --type movie or tv.", 2);
2742
3132
  }
2743
3133
  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
- });
3134
+ await printPaged({
3135
+ fetchPage: (nextPage) => {
3136
+ const options = { page: nextPage };
3137
+ return type === "movie" ? client.search.movies(query, { ...options, year }) : type === "tv" ? client.search.tv(query, { ...options, firstAirDateYear: year }) : type === "person" ? client.search.people(query, options) : client.search.multi(query, options);
3138
+ },
3139
+ output,
3140
+ renderText: (response) => printSearchResults(response, type),
3141
+ startPage: page
3142
+ });
3143
+ }, { paginated: true });
2752
3144
  }
2753
3145
  });
2754
3146
 
@@ -2760,6 +3152,7 @@ var trendingCommand = defineCommand({
2760
3152
  },
2761
3153
  args: {
2762
3154
  ...commonArgs,
3155
+ ...paginationArgs,
2763
3156
  type: {
2764
3157
  type: "positional",
2765
3158
  description: "Media type: all, movie, tv, or person",
@@ -2777,18 +3170,21 @@ var trendingCommand = defineCommand({
2777
3170
  }
2778
3171
  },
2779
3172
  async run({ args }) {
2780
- await runSafely(async () => {
3173
+ await runSafely(args, async (output) => {
2781
3174
  const type = parseSearchType(args.type);
2782
3175
  const window = parseWindow(args.window);
2783
3176
  const page = parsePage(args.page);
2784
3177
  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
- });
3178
+ await printPaged({
3179
+ fetchPage: (nextPage) => {
3180
+ const options = { page: nextPage };
3181
+ return type === "movie" ? client.trending.movies(window, options) : type === "tv" ? client.trending.tv(window, options) : type === "person" ? client.trending.people(window, options) : client.trending.all(window, options);
3182
+ },
3183
+ output,
3184
+ renderText: (response) => printSearchResults(response, type),
3185
+ startPage: page
3186
+ });
3187
+ }, { paginated: true });
2792
3188
  }
2793
3189
  });
2794
3190
 
@@ -2806,6 +3202,7 @@ var tvCommand = defineCommand({
2806
3202
  },
2807
3203
  args: {
2808
3204
  ...commonArgs,
3205
+ ...paginationArgs,
2809
3206
  category: {
2810
3207
  type: "positional",
2811
3208
  description: "popular, top-rated, airing-today, or on-the-air",
@@ -2817,17 +3214,20 @@ var tvCommand = defineCommand({
2817
3214
  }
2818
3215
  },
2819
3216
  async run({ args }) {
2820
- await runSafely(async () => {
3217
+ await runSafely(args, async (output) => {
2821
3218
  const category = parseTVListCategory(args.category);
2822
- const options = { page: parsePage(args.page) };
3219
+ const params = { page: parsePage(args.page) };
2823
3220
  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
- });
3221
+ await printPaged({
3222
+ fetchPage: (page) => {
3223
+ const options = { ...params, page };
3224
+ return category === "top-rated" ? client.tv.topRated(options) : category === "airing-today" ? client.tv.airingToday(options) : category === "on-the-air" ? client.tv.onTheAir(options) : client.tv.popular(options);
3225
+ },
3226
+ output,
3227
+ renderText: (response) => printSearchResults(response, "tv"),
3228
+ startPage: params.page
3229
+ });
3230
+ }, { paginated: true });
2831
3231
  }
2832
3232
  }),
2833
3233
  get: defineCommand({
@@ -2848,16 +3248,12 @@ var tvCommand = defineCommand({
2848
3248
  }
2849
3249
  },
2850
3250
  async run({ args }) {
2851
- await runSafely(async () => {
3251
+ await runSafely(args, async (output) => {
2852
3252
  const id = parseId(args.id, "TV show ID");
2853
3253
  const client = createClient({ language: args.lang });
2854
3254
  const appendToResponse = parseCommaList(args.append);
2855
3255
  const tv = await client.tv.details(id, { appendToResponse });
2856
- if (args.json) {
2857
- printJson(tv);
2858
- } else {
2859
- printTV(tv, client);
2860
- }
3256
+ printResult(tv, output, () => printTV(tv, client));
2861
3257
  });
2862
3258
  }
2863
3259
  }),
@@ -2879,16 +3275,12 @@ var tvCommand = defineCommand({
2879
3275
  }
2880
3276
  },
2881
3277
  async run({ args }) {
2882
- await runSafely(async () => {
3278
+ await runSafely(args, async (output) => {
2883
3279
  const id = parseId(args.id, "TV show ID");
2884
3280
  const limit = parseLimit(args.limit);
2885
3281
  const client = createClient({ language: args.lang });
2886
3282
  const credits = await client.tv.credits(id);
2887
- if (args.json) {
2888
- printJson(credits);
2889
- } else {
2890
- printCredits(credits, limit);
2891
- }
3283
+ printResult(credits, output, () => printCredits(credits, limit));
2892
3284
  });
2893
3285
  }
2894
3286
  }),
@@ -2910,16 +3302,12 @@ var tvCommand = defineCommand({
2910
3302
  }
2911
3303
  },
2912
3304
  async run({ args }) {
2913
- await runSafely(async () => {
3305
+ await runSafely(args, async (output) => {
2914
3306
  const id = parseId(args.id, "TV show ID");
2915
3307
  const includeImageLanguage = parseCommaList(args["image-language"]);
2916
3308
  const client = createClient({ language: args.lang });
2917
3309
  const images = await client.tv.images(id, { includeImageLanguage });
2918
- if (args.json) {
2919
- printJson(images);
2920
- } else {
2921
- printImages(images, client);
2922
- }
3310
+ printResult(images, output, () => printImages(images, client));
2923
3311
  });
2924
3312
  }
2925
3313
  }),
@@ -2937,15 +3325,11 @@ var tvCommand = defineCommand({
2937
3325
  }
2938
3326
  },
2939
3327
  async run({ args }) {
2940
- await runSafely(async () => {
3328
+ await runSafely(args, async (output) => {
2941
3329
  const id = parseId(args.id, "TV show ID");
2942
3330
  const client = createClient({ language: args.lang });
2943
3331
  const videos = await client.tv.videos(id);
2944
- if (args.json) {
2945
- printJson(videos);
2946
- } else {
2947
- printVideos(videos);
2948
- }
3332
+ printResult(videos, output, () => printVideos(videos));
2949
3333
  });
2950
3334
  }
2951
3335
  }),
@@ -2956,6 +3340,7 @@ var tvCommand = defineCommand({
2956
3340
  },
2957
3341
  args: {
2958
3342
  ...commonArgs,
3343
+ ...paginationArgs,
2959
3344
  id: {
2960
3345
  type: "positional",
2961
3346
  description: "TMDB TV show ID",
@@ -2967,17 +3352,17 @@ var tvCommand = defineCommand({
2967
3352
  }
2968
3353
  },
2969
3354
  async run({ args }) {
2970
- await runSafely(async () => {
3355
+ await runSafely(args, async (output) => {
2971
3356
  const id = parseId(args.id, "TV show ID");
2972
- const options = { page: parsePage(args.page) };
3357
+ const params = { page: parsePage(args.page) };
2973
3358
  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
- });
3359
+ await printPaged({
3360
+ fetchPage: (page) => client.tv.similar(id, { ...params, page }),
3361
+ output,
3362
+ renderText: (response) => printSearchResults(response, "tv"),
3363
+ startPage: params.page
3364
+ });
3365
+ }, { paginated: true });
2981
3366
  }
2982
3367
  }),
2983
3368
  recommendations: defineCommand({
@@ -2987,6 +3372,7 @@ var tvCommand = defineCommand({
2987
3372
  },
2988
3373
  args: {
2989
3374
  ...commonArgs,
3375
+ ...paginationArgs,
2990
3376
  id: {
2991
3377
  type: "positional",
2992
3378
  description: "TMDB TV show ID",
@@ -2998,17 +3384,17 @@ var tvCommand = defineCommand({
2998
3384
  }
2999
3385
  },
3000
3386
  async run({ args }) {
3001
- await runSafely(async () => {
3387
+ await runSafely(args, async (output) => {
3002
3388
  const id = parseId(args.id, "TV show ID");
3003
- const options = { page: parsePage(args.page) };
3389
+ const params = { page: parsePage(args.page) };
3004
3390
  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
- });
3391
+ await printPaged({
3392
+ fetchPage: (page) => client.tv.recommendations(id, { ...params, page }),
3393
+ output,
3394
+ renderText: (response) => printSearchResults(response, "tv"),
3395
+ startPage: params.page
3396
+ });
3397
+ }, { paginated: true });
3012
3398
  }
3013
3399
  }),
3014
3400
  season: defineCommand({
@@ -3030,16 +3416,251 @@ var tvCommand = defineCommand({
3030
3416
  }
3031
3417
  },
3032
3418
  async run({ args }) {
3033
- await runSafely(async () => {
3419
+ await runSafely(args, async (output) => {
3034
3420
  const id = parseId(args.id, "TV show ID");
3035
3421
  const seasonNumber = parseSeasonNumber(args.season);
3036
3422
  const client = createClient({ language: args.lang });
3037
3423
  const season = await client.tv.seasonDetails(id, seasonNumber);
3038
- if (args.json) {
3039
- printJson(season);
3040
- } else {
3041
- printSeason(season, client);
3042
- }
3424
+ printResult(season, output, () => printSeason(season, client));
3425
+ });
3426
+ }
3427
+ }),
3428
+ episode: defineCommand({
3429
+ meta: {
3430
+ name: "episode",
3431
+ description: "Show a TV episode"
3432
+ },
3433
+ args: {
3434
+ ...commonArgs,
3435
+ id: {
3436
+ type: "positional",
3437
+ description: "TMDB TV show ID",
3438
+ required: true
3439
+ },
3440
+ season: {
3441
+ type: "positional",
3442
+ description: "Season number (0 for specials)",
3443
+ required: true
3444
+ },
3445
+ episode: {
3446
+ type: "positional",
3447
+ description: "Episode number",
3448
+ required: true
3449
+ },
3450
+ append: {
3451
+ type: "string",
3452
+ description: "Comma-separated append_to_response values"
3453
+ }
3454
+ },
3455
+ async run({ args }) {
3456
+ await runSafely(args, async (output) => {
3457
+ const id = parseId(args.id, "TV show ID");
3458
+ const seasonNumber = parseSeasonNumber(args.season);
3459
+ const episodeNumber = parseId(args.episode, "episode number");
3460
+ const appendToResponse = parseCommaList(args.append);
3461
+ const client = createClient({ language: args.lang });
3462
+ const episode = await client.tv.episodeDetails(id, seasonNumber, episodeNumber, {
3463
+ appendToResponse
3464
+ });
3465
+ printResult(episode, output, () => printEpisode(episode, client, id));
3466
+ });
3467
+ }
3468
+ }),
3469
+ "aggregate-credits": defineCommand({
3470
+ meta: {
3471
+ name: "aggregate-credits",
3472
+ description: "Show aggregate cast and crew credits for a TV show"
3473
+ },
3474
+ args: {
3475
+ ...commonArgs,
3476
+ id: {
3477
+ type: "positional",
3478
+ description: "TMDB TV show ID",
3479
+ required: true
3480
+ },
3481
+ limit: {
3482
+ type: "string",
3483
+ description: "Maximum cast/crew rows (default: 10)"
3484
+ }
3485
+ },
3486
+ async run({ args }) {
3487
+ await runSafely(args, async (output) => {
3488
+ const id = parseId(args.id, "TV show ID");
3489
+ const limit = parseLimit(args.limit);
3490
+ const client = createClient({ language: args.lang });
3491
+ const credits = await client.tv.aggregateCredits(id);
3492
+ printResult(credits, output, () => printAggregateCredits(credits, limit));
3493
+ });
3494
+ }
3495
+ }),
3496
+ "season-credits": defineCommand({
3497
+ meta: {
3498
+ name: "season-credits",
3499
+ description: "Show cast and crew credits for a TV season"
3500
+ },
3501
+ args: {
3502
+ ...commonArgs,
3503
+ id: {
3504
+ type: "positional",
3505
+ description: "TMDB TV show ID",
3506
+ required: true
3507
+ },
3508
+ season: {
3509
+ type: "positional",
3510
+ description: "Season number (0 for specials)",
3511
+ required: true
3512
+ },
3513
+ limit: {
3514
+ type: "string",
3515
+ description: "Maximum cast/crew rows (default: 10)"
3516
+ }
3517
+ },
3518
+ async run({ args }) {
3519
+ await runSafely(args, async (output) => {
3520
+ const id = parseId(args.id, "TV show ID");
3521
+ const seasonNumber = parseSeasonNumber(args.season);
3522
+ const limit = parseLimit(args.limit);
3523
+ const client = createClient({ language: args.lang });
3524
+ const credits = await client.tv.seasonCredits(id, seasonNumber);
3525
+ printResult(credits, output, () => printCredits(credits, limit));
3526
+ });
3527
+ }
3528
+ }),
3529
+ "season-aggregate-credits": defineCommand({
3530
+ meta: {
3531
+ name: "season-aggregate-credits",
3532
+ description: "Show aggregate cast and crew credits for a TV season"
3533
+ },
3534
+ args: {
3535
+ ...commonArgs,
3536
+ id: {
3537
+ type: "positional",
3538
+ description: "TMDB TV show ID",
3539
+ required: true
3540
+ },
3541
+ season: {
3542
+ type: "positional",
3543
+ description: "Season number (0 for specials)",
3544
+ required: true
3545
+ },
3546
+ limit: {
3547
+ type: "string",
3548
+ description: "Maximum cast/crew rows (default: 10)"
3549
+ }
3550
+ },
3551
+ async run({ args }) {
3552
+ await runSafely(args, async (output) => {
3553
+ const id = parseId(args.id, "TV show ID");
3554
+ const seasonNumber = parseSeasonNumber(args.season);
3555
+ const limit = parseLimit(args.limit);
3556
+ const client = createClient({ language: args.lang });
3557
+ const credits = await client.tv.seasonAggregateCredits(id, seasonNumber);
3558
+ printResult(credits, output, () => printAggregateCredits(credits, limit));
3559
+ });
3560
+ }
3561
+ }),
3562
+ "season-images": defineCommand({
3563
+ meta: {
3564
+ name: "season-images",
3565
+ description: "Show TV season image URLs"
3566
+ },
3567
+ args: {
3568
+ ...commonArgs,
3569
+ id: {
3570
+ type: "positional",
3571
+ description: "TMDB TV show ID",
3572
+ required: true
3573
+ },
3574
+ season: {
3575
+ type: "positional",
3576
+ description: "Season number (0 for specials)",
3577
+ required: true
3578
+ },
3579
+ "image-language": {
3580
+ type: "string",
3581
+ description: "Comma-separated image language codes"
3582
+ }
3583
+ },
3584
+ async run({ args }) {
3585
+ await runSafely(args, async (output) => {
3586
+ const id = parseId(args.id, "TV show ID");
3587
+ const seasonNumber = parseSeasonNumber(args.season);
3588
+ const includeImageLanguage = parseCommaList(args["image-language"]);
3589
+ const client = createClient({ language: args.lang });
3590
+ const images = await client.tv.seasonImages(id, seasonNumber, { includeImageLanguage });
3591
+ printResult(images, output, () => printSeasonImages(images, client));
3592
+ });
3593
+ }
3594
+ }),
3595
+ "season-videos": defineCommand({
3596
+ meta: {
3597
+ name: "season-videos",
3598
+ description: "Show TV season videos and trailers"
3599
+ },
3600
+ args: {
3601
+ ...commonArgs,
3602
+ id: {
3603
+ type: "positional",
3604
+ description: "TMDB TV show ID",
3605
+ required: true
3606
+ },
3607
+ season: {
3608
+ type: "positional",
3609
+ description: "Season number (0 for specials)",
3610
+ required: true
3611
+ }
3612
+ },
3613
+ async run({ args }) {
3614
+ await runSafely(args, async (output) => {
3615
+ const id = parseId(args.id, "TV show ID");
3616
+ const seasonNumber = parseSeasonNumber(args.season);
3617
+ const client = createClient({ language: args.lang });
3618
+ const videos = await client.tv.seasonVideos(id, seasonNumber);
3619
+ printResult(videos, output, () => printVideos(videos));
3620
+ });
3621
+ }
3622
+ }),
3623
+ "episode-groups": defineCommand({
3624
+ meta: {
3625
+ name: "episode-groups",
3626
+ description: "List alternate episode groupings for a TV show"
3627
+ },
3628
+ args: {
3629
+ ...commonArgs,
3630
+ id: {
3631
+ type: "positional",
3632
+ description: "TMDB TV show ID",
3633
+ required: true
3634
+ }
3635
+ },
3636
+ async run({ args }) {
3637
+ await runSafely(args, async (output) => {
3638
+ const id = parseId(args.id, "TV show ID");
3639
+ const client = createClient({ language: args.lang });
3640
+ const groups = await client.tv.episodeGroups(id);
3641
+ printResult(groups, output, () => printEpisodeGroups(groups));
3642
+ });
3643
+ }
3644
+ }),
3645
+ "episode-group": defineCommand({
3646
+ meta: {
3647
+ name: "episode-group",
3648
+ description: "Show an alternate episode group"
3649
+ },
3650
+ args: {
3651
+ ...commonArgs,
3652
+ "group-id": {
3653
+ type: "positional",
3654
+ description: "TMDB episode group ID",
3655
+ required: true
3656
+ }
3657
+ },
3658
+ async run({ args }) {
3659
+ await runSafely(args, async (output) => {
3660
+ const groupId = parseGroupId(args["group-id"]);
3661
+ const client = createClient({ language: args.lang });
3662
+ const group = await client.tv.episodeGroup(groupId);
3663
+ printResult(group, output, () => printEpisodeGroup(group));
3043
3664
  });
3044
3665
  }
3045
3666
  })
@@ -3082,12 +3703,82 @@ var AUTHENTICATION_HELP = [
3082
3703
  `);
3083
3704
 
3084
3705
  // 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 ? `
3706
+ var rawArgs = process.argv.slice(2);
3707
+ if (rawArgs.length === 0) {
3708
+ await showFullUsage(mainCommand);
3709
+ process.exitCode = 1;
3710
+ } else if (rawArgs.some((arg) => arg === "--help" || arg === "-h")) {
3711
+ await showFullUsage(await resolveCommand(rawArgs));
3712
+ } else if (isVersionRequest(rawArgs)) {
3713
+ process.stdout.write(`${CLI_VERSION}
3714
+ `);
3715
+ } else {
3716
+ try {
3717
+ await runCommand(mainCommand, { rawArgs });
3718
+ } catch (error) {
3719
+ reportUsageError(error, rawArgs);
3720
+ }
3721
+ }
3722
+ function reportUsageError(error, args) {
3723
+ const output = outputFromArgs(args);
3724
+ const message = stripAnsi(error instanceof Error ? error.message : String(error));
3725
+ const usageError = new CliError(message, 2);
3726
+ if (output.format === "json") {
3727
+ process.stderr.write(`${JSON.stringify(toErrorRecord(usageError))}
3728
+ `);
3729
+ } else {
3730
+ process.stderr.write(`${message}
3731
+ `);
3732
+ }
3733
+ process.exitCode = 2;
3734
+ }
3735
+ function outputFromArgs(args) {
3736
+ const outputArgs = {};
3737
+ for (let index = 0;index < args.length; index += 1) {
3738
+ const arg = args[index];
3739
+ if (arg === "--json") {
3740
+ outputArgs.json = true;
3741
+ } else if (arg === "--compact") {
3742
+ outputArgs.compact = true;
3743
+ } else if (arg === "--format") {
3744
+ outputArgs.format = args[index + 1];
3745
+ index += 1;
3746
+ } else if (arg.startsWith("--format=")) {
3747
+ outputArgs.format = arg.slice("--format=".length);
3748
+ }
3749
+ }
3750
+ try {
3751
+ return resolveOutput(outputArgs);
3752
+ } catch {
3753
+ return { compact: false, format: "text" };
3754
+ }
3755
+ }
3756
+ function isVersionRequest(args) {
3757
+ return args.length === 1 && (args[0] === "--version" || args[0] === "-v");
3758
+ }
3759
+ function stripAnsi(value) {
3760
+ return value.replace(/\u001B\[[0-9;]*m/g, "");
3761
+ }
3762
+ async function resolveCommand(args) {
3763
+ let command = mainCommand;
3764
+ for (const arg of args) {
3765
+ if (arg.startsWith("-")) {
3766
+ continue;
3767
+ }
3768
+ const subCommands = await resolveValue2(command.subCommands);
3769
+ const next = subCommands?.[arg];
3770
+ if (next === undefined) {
3771
+ break;
3772
+ }
3773
+ command = await resolveValue2(next);
3774
+ }
3775
+ return command;
3776
+ }
3777
+ async function showFullUsage(command) {
3778
+ const path = await findPath(mainCommand, command);
3779
+ if (!path || path.length === 0) {
3780
+ const usage = await renderUsage(command);
3781
+ const authentication = command === mainCommand ? `
3091
3782
 
3092
3783
  ${AUTHENTICATION_HELP}` : "";
3093
3784
  process.stdout.write(`${usage}${authentication}
@@ -3095,10 +3786,10 @@ ${AUTHENTICATION_HELP}` : "";
3095
3786
  return;
3096
3787
  }
3097
3788
  const parentName = ["tmdb", ...path.slice(0, -1)].join(" ");
3098
- const usageParent = defineCommand({
3789
+ const parent = defineCommand({
3099
3790
  meta: { name: parentName, version: CLI_VERSION }
3100
3791
  });
3101
- process.stdout.write(`${await renderUsage(cmd, usageParent)}
3792
+ process.stdout.write(`${await renderUsage(command, parent)}
3102
3793
  `);
3103
3794
  }
3104
3795
  async function findPath(root, target, path = []) {