tsarr 2.0.0 → 2.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/index.js CHANGED
@@ -4540,15 +4540,82 @@ ${indent}`);
4540
4540
  consola = createConsola2();
4541
4541
  });
4542
4542
 
4543
+ // src/cli/prompt.ts
4544
+ async function promptIfMissing(value, message) {
4545
+ if (value)
4546
+ return value;
4547
+ if (!process.stdin.isTTY) {
4548
+ throw new Error(`Missing required argument. Use --help for usage info.`);
4549
+ }
4550
+ const result = await consola.prompt(message, { type: "text" });
4551
+ if (typeof result !== "string" || !result.trim()) {
4552
+ throw new Error("No input provided.");
4553
+ }
4554
+ return result.trim();
4555
+ }
4556
+ async function promptConfirm(message, skipPrompt = false) {
4557
+ if (skipPrompt)
4558
+ return true;
4559
+ if (!process.stdin.isTTY) {
4560
+ throw new Error("Destructive action requires confirmation. Use --yes to skip in non-interactive mode.");
4561
+ }
4562
+ const result = await consola.prompt(message, { type: "confirm" });
4563
+ return result === true;
4564
+ }
4565
+ async function promptSelect(message, options) {
4566
+ if (!process.stdin.isTTY) {
4567
+ throw new Error("Interactive selection requires a TTY.");
4568
+ }
4569
+ const result = await consola.prompt(message, {
4570
+ type: "select",
4571
+ options: options.map((o3) => ({ label: o3.label, value: o3.value }))
4572
+ });
4573
+ return result;
4574
+ }
4575
+ async function promptMultiSelect(message, options) {
4576
+ if (!process.stdin.isTTY) {
4577
+ throw new Error("Interactive selection requires a TTY.");
4578
+ }
4579
+ const result = await consola.prompt(message, {
4580
+ type: "multiselect",
4581
+ options: options.map((o3) => ({ label: o3.label, value: o3.value }))
4582
+ });
4583
+ return result;
4584
+ }
4585
+ var init_prompt2 = __esm(() => {
4586
+ init_dist2();
4587
+ });
4588
+
4543
4589
  // src/cli/config.ts
4544
4590
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
4545
4591
  import { homedir } from "os";
4546
- import { join, resolve } from "path";
4592
+ import { dirname, isAbsolute, join, resolve } from "path";
4593
+ function normalizeServiceConfig(service) {
4594
+ if (!service)
4595
+ return;
4596
+ const normalized = {
4597
+ baseUrl: service.baseUrl ?? "",
4598
+ apiKey: service.apiKey ?? "",
4599
+ ...service.apiKeyFile ? { apiKeyFile: service.apiKeyFile } : {}
4600
+ };
4601
+ const timeout = typeof service.timeout === "string" ? Number(service.timeout) : service.timeout;
4602
+ if (typeof timeout === "number" && Number.isFinite(timeout)) {
4603
+ normalized.timeout = timeout;
4604
+ }
4605
+ return normalized;
4606
+ }
4607
+ function normalizeConfig(config) {
4608
+ const services = Object.fromEntries(Object.entries(config.services ?? {}).map(([name, service]) => [name, normalizeServiceConfig(service)]).filter(([, service]) => service != null));
4609
+ return {
4610
+ ...config,
4611
+ ...Object.keys(services).length > 0 ? { services } : {}
4612
+ };
4613
+ }
4547
4614
  function readJsonFile(path) {
4548
4615
  if (!existsSync(path))
4549
4616
  return {};
4550
4617
  try {
4551
- return JSON.parse(readFileSync(path, "utf-8"));
4618
+ return normalizeConfig(JSON.parse(readFileSync(path, "utf-8")));
4552
4619
  } catch {
4553
4620
  return {};
4554
4621
  }
@@ -4583,18 +4650,25 @@ function findLocalConfigPath() {
4583
4650
  }
4584
4651
  }
4585
4652
  function loadConfig() {
4586
- const base = { services: {} };
4587
4653
  const global = readJsonFile(GLOBAL_CONFIG_PATH);
4588
4654
  const localPath = findLocalConfigPath();
4589
4655
  const local = localPath ? readJsonFile(localPath) : {};
4590
4656
  const env2 = getEnvConfig();
4657
+ const allServiceNames = new Set([
4658
+ ...Object.keys(global.services ?? {}),
4659
+ ...Object.keys(local.services ?? {}),
4660
+ ...Object.keys(env2.services ?? {})
4661
+ ]);
4662
+ const services = {};
4663
+ for (const name of allServiceNames) {
4664
+ services[name] = {
4665
+ ...global.services?.[name],
4666
+ ...local.services?.[name],
4667
+ ...env2.services?.[name]
4668
+ };
4669
+ }
4591
4670
  const merged = {
4592
- services: {
4593
- ...base.services,
4594
- ...global.services,
4595
- ...local.services,
4596
- ...env2.services
4597
- },
4671
+ services,
4598
4672
  defaults: {
4599
4673
  ...global.defaults,
4600
4674
  ...local.defaults
@@ -4602,14 +4676,51 @@ function loadConfig() {
4602
4676
  };
4603
4677
  return merged;
4604
4678
  }
4679
+ function resolveConfigRelativePath(filePath, configPath) {
4680
+ if (isAbsolute(filePath) || !configPath) {
4681
+ return filePath;
4682
+ }
4683
+ return resolve(dirname(configPath), filePath);
4684
+ }
4685
+ function getResolvedApiKeyFilePath(serviceName, service, localPath, local, global) {
4686
+ if (!service.apiKeyFile)
4687
+ return;
4688
+ if (local.services?.[serviceName]?.apiKeyFile) {
4689
+ return resolveConfigRelativePath(service.apiKeyFile, localPath);
4690
+ }
4691
+ if (global.services?.[serviceName]?.apiKeyFile) {
4692
+ return resolveConfigRelativePath(service.apiKeyFile, GLOBAL_CONFIG_PATH);
4693
+ }
4694
+ return service.apiKeyFile;
4695
+ }
4696
+ function readApiKeyFile(filePath) {
4697
+ if (!existsSync(filePath)) {
4698
+ throw new Error(`API key file not found: ${filePath}`);
4699
+ }
4700
+ try {
4701
+ return readFileSync(filePath, "utf-8").trimEnd();
4702
+ } catch (err) {
4703
+ throw new Error(`Failed to read API key file: ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
4704
+ }
4705
+ }
4605
4706
  function getServiceConfig(serviceName) {
4707
+ const global = readJsonFile(GLOBAL_CONFIG_PATH);
4708
+ const localPath = findLocalConfigPath();
4709
+ const local = localPath ? readJsonFile(localPath) : {};
4606
4710
  const config = loadConfig();
4607
4711
  const service = config.services[serviceName];
4608
- if (!service?.baseUrl || !service?.apiKey)
4712
+ if (!service?.baseUrl)
4713
+ return null;
4714
+ let apiKey = service.apiKey;
4715
+ const apiKeyFilePath = getResolvedApiKeyFilePath(serviceName, service, localPath, local, global);
4716
+ if (!apiKey && apiKeyFilePath) {
4717
+ apiKey = readApiKeyFile(apiKeyFilePath);
4718
+ }
4719
+ if (!apiKey)
4609
4720
  return null;
4610
4721
  return {
4611
4722
  baseUrl: service.baseUrl,
4612
- apiKey: service.apiKey,
4723
+ apiKey,
4613
4724
  ...service.timeout ? { timeout: service.timeout } : {}
4614
4725
  };
4615
4726
  }
@@ -4646,13 +4757,23 @@ function setConfigValue(key, value, global = true) {
4646
4757
  }
4647
4758
  current = current[parts[i2]];
4648
4759
  }
4649
- current[parts[parts.length - 1]] = value;
4760
+ current[parts[parts.length - 1]] = parseConfigValue(key, value);
4650
4761
  if (global) {
4651
4762
  saveGlobalConfig(config);
4652
4763
  } else {
4653
4764
  saveLocalConfig(config);
4654
4765
  }
4655
4766
  }
4767
+ function parseConfigValue(key, value) {
4768
+ if (key.endsWith(".timeout")) {
4769
+ const timeout = Number(value);
4770
+ if (!Number.isFinite(timeout) || timeout < 0) {
4771
+ throw new Error(`Invalid timeout value "${value}". Expected a non-negative number.`);
4772
+ }
4773
+ return timeout;
4774
+ }
4775
+ return value;
4776
+ }
4656
4777
  function loadScopedConfig(scope) {
4657
4778
  if (scope === "global") {
4658
4779
  return readJsonFile(GLOBAL_CONFIG_PATH);
@@ -4737,52 +4858,6 @@ function formatCell(value) {
4737
4858
  return String(value);
4738
4859
  }
4739
4860
 
4740
- // src/cli/prompt.ts
4741
- async function promptIfMissing(value, message) {
4742
- if (value)
4743
- return value;
4744
- if (!process.stdin.isTTY) {
4745
- throw new Error(`Missing required argument. Use --help for usage info.`);
4746
- }
4747
- const result = await consola.prompt(message, { type: "text" });
4748
- if (typeof result !== "string" || !result.trim()) {
4749
- throw new Error("No input provided.");
4750
- }
4751
- return result.trim();
4752
- }
4753
- async function promptConfirm(message, skipPrompt = false) {
4754
- if (skipPrompt)
4755
- return true;
4756
- if (!process.stdin.isTTY) {
4757
- throw new Error("Destructive action requires confirmation. Use --yes to skip in non-interactive mode.");
4758
- }
4759
- const result = await consola.prompt(message, { type: "confirm" });
4760
- return result === true;
4761
- }
4762
- async function promptSelect(message, options) {
4763
- if (!process.stdin.isTTY) {
4764
- throw new Error("Interactive selection requires a TTY.");
4765
- }
4766
- const result = await consola.prompt(message, {
4767
- type: "select",
4768
- options: options.map((o3) => ({ label: o3.label, value: o3.value }))
4769
- });
4770
- return result;
4771
- }
4772
- async function promptMultiSelect(message, options) {
4773
- if (!process.stdin.isTTY) {
4774
- throw new Error("Interactive selection requires a TTY.");
4775
- }
4776
- const result = await consola.prompt(message, {
4777
- type: "multiselect",
4778
- options: options.map((o3) => ({ label: o3.label, value: o3.value }))
4779
- });
4780
- return result;
4781
- }
4782
- var init_prompt2 = __esm(() => {
4783
- init_dist2();
4784
- });
4785
-
4786
4861
  // src/cli/commands/service.ts
4787
4862
  function buildServiceCommand(serviceName, description, clientFactory, resources) {
4788
4863
  const subCommands = {};
@@ -4851,6 +4926,9 @@ Run \`tsarr config init\` or set TSARR_${serviceName.toUpperCase()}_API_KEY`);
4851
4926
  process.exit(1);
4852
4927
  }
4853
4928
  let result = raw?.data !== undefined ? raw.data : raw;
4929
+ if (result != null && typeof result === "object" && "data" in result && Object.keys(result).length === 1) {
4930
+ result = result.data;
4931
+ }
4854
4932
  if (result?.records !== undefined && Array.isArray(result.records)) {
4855
4933
  result = result.records;
4856
4934
  }
@@ -4916,6 +4994,7 @@ __export(exports_radarr3, {
4916
4994
  var resources, radarr;
4917
4995
  var init_radarr3 = __esm(() => {
4918
4996
  init_radarr2();
4997
+ init_prompt2();
4919
4998
  init_service();
4920
4999
  resources = [
4921
5000
  {
@@ -4942,6 +5021,79 @@ var init_radarr3 = __esm(() => {
4942
5021
  idField: "tmdbId",
4943
5022
  run: (c3, a2) => c3.searchMovies(a2.term)
4944
5023
  },
5024
+ {
5025
+ name: "add",
5026
+ description: "Search and add a movie",
5027
+ args: [{ name: "term", description: "Search term", required: true }],
5028
+ run: async (c3, a2) => {
5029
+ const searchResult = await c3.searchMovies(a2.term);
5030
+ const results = searchResult?.data ?? searchResult;
5031
+ if (!Array.isArray(results) || results.length === 0) {
5032
+ throw new Error("No movies found.");
5033
+ }
5034
+ const movieId = await promptSelect("Select a movie:", results.map((m2) => ({ label: `${m2.title} (${m2.year})`, value: String(m2.tmdbId) })));
5035
+ const movie = results.find((m2) => String(m2.tmdbId) === movieId);
5036
+ if (!movie) {
5037
+ throw new Error("Selected movie was not found in the search results.");
5038
+ }
5039
+ const profilesResult = await c3.getQualityProfiles();
5040
+ const profiles = profilesResult?.data ?? profilesResult;
5041
+ if (!Array.isArray(profiles) || profiles.length === 0) {
5042
+ throw new Error("No quality profiles found. Configure one in Radarr first.");
5043
+ }
5044
+ const profileId = await promptSelect("Select quality profile:", profiles.map((p) => ({ label: p.name, value: String(p.id) })));
5045
+ const foldersResult = await c3.getRootFolders();
5046
+ const folders = foldersResult?.data ?? foldersResult;
5047
+ if (!Array.isArray(folders) || folders.length === 0) {
5048
+ throw new Error("No root folders found. Configure one in Radarr first.");
5049
+ }
5050
+ const rootFolderPath = await promptSelect("Select root folder:", folders.map((f3) => ({ label: f3.path, value: f3.path })));
5051
+ const confirmed = await promptConfirm(`Add "${movie.title} (${movie.year})"?`, !!a2.yes);
5052
+ if (!confirmed)
5053
+ throw new Error("Cancelled.");
5054
+ return c3.addMovie({
5055
+ ...movie,
5056
+ qualityProfileId: Number(profileId),
5057
+ rootFolderPath,
5058
+ monitored: true,
5059
+ addOptions: { searchForMovie: true }
5060
+ });
5061
+ }
5062
+ },
5063
+ {
5064
+ name: "edit",
5065
+ description: "Edit a movie",
5066
+ args: [
5067
+ { name: "id", description: "Movie ID", required: true, type: "number" },
5068
+ { name: "monitored", description: "Set monitored (true/false)" },
5069
+ { name: "quality-profile-id", description: "Quality profile ID", type: "number" },
5070
+ { name: "tags", description: "Comma-separated tag IDs" }
5071
+ ],
5072
+ run: async (c3, a2) => {
5073
+ const result = await c3.getMovie(a2.id);
5074
+ const movie = result?.data ?? result;
5075
+ const updates = { ...movie };
5076
+ if (a2.monitored !== undefined)
5077
+ updates.monitored = a2.monitored === "true";
5078
+ if (a2["quality-profile-id"] !== undefined)
5079
+ updates.qualityProfileId = Number(a2["quality-profile-id"]);
5080
+ if (a2.tags !== undefined)
5081
+ updates.tags = a2.tags.split(",").map((t2) => Number(t2.trim()));
5082
+ return c3.updateMovie(a2.id, updates);
5083
+ }
5084
+ },
5085
+ {
5086
+ name: "refresh",
5087
+ description: "Refresh movie metadata",
5088
+ args: [{ name: "id", description: "Movie ID", required: true, type: "number" }],
5089
+ run: (c3, a2) => c3.runCommand({ name: "RefreshMovie", movieIds: [a2.id] })
5090
+ },
5091
+ {
5092
+ name: "manual-search",
5093
+ description: "Trigger a manual search for releases",
5094
+ args: [{ name: "id", description: "Movie ID", required: true, type: "number" }],
5095
+ run: (c3, a2) => c3.runCommand({ name: "MoviesSearch", movieIds: [a2.id] })
5096
+ },
4945
5097
  {
4946
5098
  name: "delete",
4947
5099
  description: "Delete a movie",
@@ -7442,6 +7594,7 @@ __export(exports_sonarr3, {
7442
7594
  var resources2, sonarr;
7443
7595
  var init_sonarr3 = __esm(() => {
7444
7596
  init_sonarr2();
7597
+ init_prompt2();
7445
7598
  init_service();
7446
7599
  resources2 = [
7447
7600
  {
@@ -7467,6 +7620,79 @@ var init_sonarr3 = __esm(() => {
7467
7620
  columns: ["tvdbId", "title", "year", "overview"],
7468
7621
  run: (c3, a2) => c3.searchSeries(a2.term)
7469
7622
  },
7623
+ {
7624
+ name: "add",
7625
+ description: "Search and add a series",
7626
+ args: [{ name: "term", description: "Search term", required: true }],
7627
+ run: async (c3, a2) => {
7628
+ const searchResult = await c3.searchSeries(a2.term);
7629
+ const results = searchResult?.data ?? searchResult;
7630
+ if (!Array.isArray(results) || results.length === 0) {
7631
+ throw new Error("No series found.");
7632
+ }
7633
+ const seriesId = await promptSelect("Select a series:", results.map((s2) => ({ label: `${s2.title} (${s2.year})`, value: String(s2.tvdbId) })));
7634
+ const series = results.find((s2) => String(s2.tvdbId) === seriesId);
7635
+ if (!series) {
7636
+ throw new Error("Selected series was not found in the search results.");
7637
+ }
7638
+ const profilesResult = await c3.getQualityProfiles();
7639
+ const profiles = profilesResult?.data ?? profilesResult;
7640
+ if (!Array.isArray(profiles) || profiles.length === 0) {
7641
+ throw new Error("No quality profiles found. Configure one in Sonarr first.");
7642
+ }
7643
+ const profileId = await promptSelect("Select quality profile:", profiles.map((p) => ({ label: p.name, value: String(p.id) })));
7644
+ const foldersResult = await c3.getRootFolders();
7645
+ const folders = foldersResult?.data ?? foldersResult;
7646
+ if (!Array.isArray(folders) || folders.length === 0) {
7647
+ throw new Error("No root folders found. Configure one in Sonarr first.");
7648
+ }
7649
+ const rootFolderPath = await promptSelect("Select root folder:", folders.map((f3) => ({ label: f3.path, value: f3.path })));
7650
+ const confirmed = await promptConfirm(`Add "${series.title} (${series.year})"?`, !!a2.yes);
7651
+ if (!confirmed)
7652
+ throw new Error("Cancelled.");
7653
+ return c3.addSeries({
7654
+ ...series,
7655
+ qualityProfileId: Number(profileId),
7656
+ rootFolderPath,
7657
+ monitored: true,
7658
+ addOptions: { searchForMissingEpisodes: true }
7659
+ });
7660
+ }
7661
+ },
7662
+ {
7663
+ name: "edit",
7664
+ description: "Edit a series",
7665
+ args: [
7666
+ { name: "id", description: "Series ID", required: true, type: "number" },
7667
+ { name: "monitored", description: "Set monitored (true/false)" },
7668
+ { name: "quality-profile-id", description: "Quality profile ID", type: "number" },
7669
+ { name: "tags", description: "Comma-separated tag IDs" }
7670
+ ],
7671
+ run: async (c3, a2) => {
7672
+ const result = await c3.getSeriesById(a2.id);
7673
+ const series = result?.data ?? result;
7674
+ const updates = { ...series };
7675
+ if (a2.monitored !== undefined)
7676
+ updates.monitored = a2.monitored === "true";
7677
+ if (a2["quality-profile-id"] !== undefined)
7678
+ updates.qualityProfileId = Number(a2["quality-profile-id"]);
7679
+ if (a2.tags !== undefined)
7680
+ updates.tags = a2.tags.split(",").map((t2) => Number(t2.trim()));
7681
+ return c3.updateSeries(String(a2.id), updates);
7682
+ }
7683
+ },
7684
+ {
7685
+ name: "refresh",
7686
+ description: "Refresh series metadata",
7687
+ args: [{ name: "id", description: "Series ID", required: true, type: "number" }],
7688
+ run: (c3, a2) => c3.runCommand({ name: "RefreshSeries", seriesId: a2.id })
7689
+ },
7690
+ {
7691
+ name: "manual-search",
7692
+ description: "Trigger a manual search for releases",
7693
+ args: [{ name: "id", description: "Series ID", required: true, type: "number" }],
7694
+ run: (c3, a2) => c3.runCommand({ name: "SeriesSearch", seriesId: a2.id })
7695
+ },
7470
7696
  {
7471
7697
  name: "delete",
7472
7698
  description: "Delete a series",
@@ -9882,6 +10108,7 @@ __export(exports_lidarr3, {
9882
10108
  var resources3, lidarr;
9883
10109
  var init_lidarr3 = __esm(() => {
9884
10110
  init_lidarr2();
10111
+ init_prompt2();
9885
10112
  init_service();
9886
10113
  resources3 = [
9887
10114
  {
@@ -9907,6 +10134,82 @@ var init_lidarr3 = __esm(() => {
9907
10134
  columns: ["foreignArtistId", "artistName", "overview"],
9908
10135
  run: (c3, a2) => c3.searchArtists(a2.term)
9909
10136
  },
10137
+ {
10138
+ name: "add",
10139
+ description: "Search and add an artist",
10140
+ args: [{ name: "term", description: "Search term", required: true }],
10141
+ run: async (c3, a2) => {
10142
+ const searchResult = await c3.searchArtists(a2.term);
10143
+ const results = searchResult?.data ?? searchResult;
10144
+ if (!Array.isArray(results) || results.length === 0) {
10145
+ throw new Error("No artists found.");
10146
+ }
10147
+ const artistId = await promptSelect("Select an artist:", results.map((ar) => ({
10148
+ label: ar.artistName,
10149
+ value: String(ar.foreignArtistId)
10150
+ })));
10151
+ const artist = results.find((ar) => String(ar.foreignArtistId) === artistId);
10152
+ if (!artist) {
10153
+ throw new Error("Selected artist was not found in the search results.");
10154
+ }
10155
+ const profilesResult = await c3.getQualityProfiles();
10156
+ const profiles = profilesResult?.data ?? profilesResult;
10157
+ if (!Array.isArray(profiles) || profiles.length === 0) {
10158
+ throw new Error("No quality profiles found. Configure one in Lidarr first.");
10159
+ }
10160
+ const profileId = await promptSelect("Select quality profile:", profiles.map((p) => ({ label: p.name, value: String(p.id) })));
10161
+ const foldersResult = await c3.getRootFolders();
10162
+ const folders = foldersResult?.data ?? foldersResult;
10163
+ if (!Array.isArray(folders) || folders.length === 0) {
10164
+ throw new Error("No root folders found. Configure one in Lidarr first.");
10165
+ }
10166
+ const rootFolderPath = await promptSelect("Select root folder:", folders.map((f3) => ({ label: f3.path, value: f3.path })));
10167
+ const confirmed = await promptConfirm(`Add "${artist.artistName}"?`, !!a2.yes);
10168
+ if (!confirmed)
10169
+ throw new Error("Cancelled.");
10170
+ return c3.addArtist({
10171
+ ...artist,
10172
+ qualityProfileId: Number(profileId),
10173
+ rootFolderPath,
10174
+ monitored: true,
10175
+ addOptions: { searchForMissingAlbums: true }
10176
+ });
10177
+ }
10178
+ },
10179
+ {
10180
+ name: "edit",
10181
+ description: "Edit an artist",
10182
+ args: [
10183
+ { name: "id", description: "Artist ID", required: true, type: "number" },
10184
+ { name: "monitored", description: "Set monitored (true/false)" },
10185
+ { name: "quality-profile-id", description: "Quality profile ID", type: "number" },
10186
+ { name: "tags", description: "Comma-separated tag IDs" }
10187
+ ],
10188
+ run: async (c3, a2) => {
10189
+ const result = await c3.getArtist(a2.id);
10190
+ const artist = result?.data ?? result;
10191
+ const updates = { ...artist };
10192
+ if (a2.monitored !== undefined)
10193
+ updates.monitored = a2.monitored === "true";
10194
+ if (a2["quality-profile-id"] !== undefined)
10195
+ updates.qualityProfileId = Number(a2["quality-profile-id"]);
10196
+ if (a2.tags !== undefined)
10197
+ updates.tags = a2.tags.split(",").map((t2) => Number(t2.trim()));
10198
+ return c3.updateArtist(a2.id, updates);
10199
+ }
10200
+ },
10201
+ {
10202
+ name: "refresh",
10203
+ description: "Refresh artist metadata",
10204
+ args: [{ name: "id", description: "Artist ID", required: true, type: "number" }],
10205
+ run: (c3, a2) => c3.runCommand({ name: "RefreshArtist", artistId: a2.id })
10206
+ },
10207
+ {
10208
+ name: "manual-search",
10209
+ description: "Trigger a manual search for releases",
10210
+ args: [{ name: "id", description: "Artist ID", required: true, type: "number" }],
10211
+ run: (c3, a2) => c3.runCommand({ name: "ArtistSearch", artistId: a2.id })
10212
+ },
9910
10213
  {
9911
10214
  name: "delete",
9912
10215
  description: "Delete an artist",
@@ -12275,6 +12578,7 @@ __export(exports_readarr3, {
12275
12578
  var resources4, readarr;
12276
12579
  var init_readarr3 = __esm(() => {
12277
12580
  init_readarr2();
12581
+ init_prompt2();
12278
12582
  init_service();
12279
12583
  resources4 = [
12280
12584
  {
@@ -12300,6 +12604,79 @@ var init_readarr3 = __esm(() => {
12300
12604
  columns: ["foreignAuthorId", "authorName", "overview"],
12301
12605
  run: (c3, a2) => c3.searchAuthors(a2.term)
12302
12606
  },
12607
+ {
12608
+ name: "add",
12609
+ description: "Search and add an author",
12610
+ args: [{ name: "term", description: "Search term", required: true }],
12611
+ run: async (c3, a2) => {
12612
+ const searchResult = await c3.searchAuthors(a2.term);
12613
+ const results = searchResult?.data ?? searchResult;
12614
+ if (!Array.isArray(results) || results.length === 0) {
12615
+ throw new Error("No authors found.");
12616
+ }
12617
+ const authorId = await promptSelect("Select an author:", results.map((au) => ({
12618
+ label: au.authorName,
12619
+ value: String(au.foreignAuthorId)
12620
+ })));
12621
+ const author = results.find((au) => String(au.foreignAuthorId) === authorId);
12622
+ const profilesResult = await c3.getQualityProfiles();
12623
+ const profiles = profilesResult?.data ?? profilesResult;
12624
+ if (!Array.isArray(profiles) || profiles.length === 0) {
12625
+ throw new Error("No quality profiles found. Configure one in Readarr first.");
12626
+ }
12627
+ const profileId = await promptSelect("Select quality profile:", profiles.map((p) => ({ label: p.name, value: String(p.id) })));
12628
+ const foldersResult = await c3.getRootFolders();
12629
+ const folders = foldersResult?.data ?? foldersResult;
12630
+ if (!Array.isArray(folders) || folders.length === 0) {
12631
+ throw new Error("No root folders found. Configure one in Readarr first.");
12632
+ }
12633
+ const rootFolderPath = await promptSelect("Select root folder:", folders.map((f3) => ({ label: f3.path, value: f3.path })));
12634
+ const confirmed = await promptConfirm(`Add "${author.authorName}"?`, !!a2.yes);
12635
+ if (!confirmed)
12636
+ throw new Error("Cancelled.");
12637
+ return c3.addAuthor({
12638
+ ...author,
12639
+ qualityProfileId: Number(profileId),
12640
+ rootFolderPath,
12641
+ monitored: true,
12642
+ addOptions: { searchForMissingBooks: true }
12643
+ });
12644
+ }
12645
+ },
12646
+ {
12647
+ name: "edit",
12648
+ description: "Edit an author",
12649
+ args: [
12650
+ { name: "id", description: "Author ID", required: true, type: "number" },
12651
+ { name: "monitored", description: "Set monitored (true/false)" },
12652
+ { name: "quality-profile-id", description: "Quality profile ID", type: "number" },
12653
+ { name: "tags", description: "Comma-separated tag IDs" }
12654
+ ],
12655
+ run: async (c3, a2) => {
12656
+ const result = await c3.getAuthor(a2.id);
12657
+ const author = result?.data ?? result;
12658
+ const updates = { ...author };
12659
+ if (a2.monitored !== undefined)
12660
+ updates.monitored = a2.monitored === "true";
12661
+ if (a2["quality-profile-id"] !== undefined)
12662
+ updates.qualityProfileId = Number(a2["quality-profile-id"]);
12663
+ if (a2.tags !== undefined)
12664
+ updates.tags = a2.tags.split(",").map((t2) => Number(t2.trim()));
12665
+ return c3.updateAuthor(a2.id, updates);
12666
+ }
12667
+ },
12668
+ {
12669
+ name: "refresh",
12670
+ description: "Refresh author metadata",
12671
+ args: [{ name: "id", description: "Author ID", required: true, type: "number" }],
12672
+ run: (c3, a2) => c3.runCommand({ name: "RefreshAuthor", authorId: a2.id })
12673
+ },
12674
+ {
12675
+ name: "manual-search",
12676
+ description: "Trigger a manual search for releases",
12677
+ args: [{ name: "id", description: "Author ID", required: true, type: "number" }],
12678
+ run: (c3, a2) => c3.runCommand({ name: "AuthorSearch", authorId: a2.id })
12679
+ },
12303
12680
  {
12304
12681
  name: "delete",
12305
12682
  description: "Delete an author",
@@ -14912,46 +15289,46 @@ var init_client7 = __esm(() => {
14912
15289
  var client6;
14913
15290
  var init_client_gen12 = __esm(() => {
14914
15291
  init_client7();
14915
- client6 = createClient6(createConfig6({ baseUrl: "/api" }));
15292
+ client6 = createClient6(createConfig6());
14916
15293
  });
14917
15294
 
14918
15295
  // src/generated/bazarr/sdk.gen.ts
14919
15296
  var getBadges = (options) => (options?.client ?? client6).get({
14920
15297
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14921
- url: "/badges",
15298
+ url: "/api/badges",
14922
15299
  ...options
14923
15300
  }), getEpisodes = (options) => (options?.client ?? client6).get({
14924
15301
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14925
- url: "/episodes",
15302
+ url: "/api/episodes",
14926
15303
  ...options
14927
15304
  }), deleteEpisodesBlacklist = (options) => (options?.client ?? client6).delete({
14928
15305
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14929
- url: "/episodes/blacklist",
15306
+ url: "/api/episodes/blacklist",
14930
15307
  ...options
14931
15308
  }), getEpisodesBlacklist = (options) => (options?.client ?? client6).get({
14932
15309
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14933
- url: "/episodes/blacklist",
15310
+ url: "/api/episodes/blacklist",
14934
15311
  ...options
14935
15312
  }), postEpisodesBlacklist = (options) => (options.client ?? client6).post({
14936
15313
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14937
- url: "/episodes/blacklist",
15314
+ url: "/api/episodes/blacklist",
14938
15315
  ...options
14939
15316
  }), getEpisodesHistory = (options) => (options?.client ?? client6).get({
14940
15317
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14941
- url: "/episodes/history",
15318
+ url: "/api/episodes/history",
14942
15319
  ...options
14943
15320
  }), deleteEpisodesSubtitles = (options) => (options.client ?? client6).delete({
14944
15321
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14945
- url: "/episodes/subtitles",
15322
+ url: "/api/episodes/subtitles",
14946
15323
  ...options
14947
15324
  }), patchEpisodesSubtitles = (options) => (options.client ?? client6).patch({
14948
15325
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14949
- url: "/episodes/subtitles",
15326
+ url: "/api/episodes/subtitles",
14950
15327
  ...options
14951
15328
  }), postEpisodesSubtitles = (options) => (options.client ?? client6).post({
14952
15329
  ...formDataBodySerializer,
14953
15330
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14954
- url: "/episodes/subtitles",
15331
+ url: "/api/episodes/subtitles",
14955
15332
  ...options,
14956
15333
  headers: {
14957
15334
  "Content-Type": null,
@@ -14959,64 +15336,64 @@ var getBadges = (options) => (options?.client ?? client6).get({
14959
15336
  }
14960
15337
  }), getEpisodesWanted = (options) => (options?.client ?? client6).get({
14961
15338
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14962
- url: "/episodes/wanted",
15339
+ url: "/api/episodes/wanted",
14963
15340
  ...options
14964
15341
  }), getBrowseBazarrFs = (options) => (options?.client ?? client6).get({
14965
15342
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14966
- url: "/files",
15343
+ url: "/api/files",
14967
15344
  ...options
14968
15345
  }), getBrowseRadarrFs = (options) => (options?.client ?? client6).get({
14969
15346
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14970
- url: "/files/radarr",
15347
+ url: "/api/files/radarr",
14971
15348
  ...options
14972
15349
  }), getBrowseSonarrFs = (options) => (options?.client ?? client6).get({
14973
15350
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14974
- url: "/files/sonarr",
15351
+ url: "/api/files/sonarr",
14975
15352
  ...options
14976
15353
  }), getHistoryStats = (options) => (options?.client ?? client6).get({
14977
15354
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14978
- url: "/history/stats",
15355
+ url: "/api/history/stats",
14979
15356
  ...options
14980
15357
  }), getMovies = (options) => (options?.client ?? client6).get({
14981
15358
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14982
- url: "/movies",
15359
+ url: "/api/movies",
14983
15360
  ...options
14984
15361
  }), patchMovies = (options) => (options?.client ?? client6).patch({
14985
15362
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14986
- url: "/movies",
15363
+ url: "/api/movies",
14987
15364
  ...options
14988
15365
  }), postMovies = (options) => (options?.client ?? client6).post({
14989
15366
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14990
- url: "/movies",
15367
+ url: "/api/movies",
14991
15368
  ...options
14992
15369
  }), deleteMoviesBlacklist = (options) => (options?.client ?? client6).delete({
14993
15370
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14994
- url: "/movies/blacklist",
15371
+ url: "/api/movies/blacklist",
14995
15372
  ...options
14996
15373
  }), getMoviesBlacklist = (options) => (options?.client ?? client6).get({
14997
15374
  security: [{ name: "X-API-KEY", type: "apiKey" }],
14998
- url: "/movies/blacklist",
15375
+ url: "/api/movies/blacklist",
14999
15376
  ...options
15000
15377
  }), postMoviesBlacklist = (options) => (options.client ?? client6).post({
15001
15378
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15002
- url: "/movies/blacklist",
15379
+ url: "/api/movies/blacklist",
15003
15380
  ...options
15004
15381
  }), getMoviesHistory = (options) => (options?.client ?? client6).get({
15005
15382
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15006
- url: "/movies/history",
15383
+ url: "/api/movies/history",
15007
15384
  ...options
15008
15385
  }), deleteMoviesSubtitles = (options) => (options.client ?? client6).delete({
15009
15386
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15010
- url: "/movies/subtitles",
15387
+ url: "/api/movies/subtitles",
15011
15388
  ...options
15012
15389
  }), patchMoviesSubtitles = (options) => (options.client ?? client6).patch({
15013
15390
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15014
- url: "/movies/subtitles",
15391
+ url: "/api/movies/subtitles",
15015
15392
  ...options
15016
15393
  }), postMoviesSubtitles = (options) => (options.client ?? client6).post({
15017
15394
  ...formDataBodySerializer,
15018
15395
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15019
- url: "/movies/subtitles",
15396
+ url: "/api/movies/subtitles",
15020
15397
  ...options,
15021
15398
  headers: {
15022
15399
  "Content-Type": null,
@@ -15024,151 +15401,151 @@ var getBadges = (options) => (options?.client ?? client6).get({
15024
15401
  }
15025
15402
  }), getMoviesWanted = (options) => (options?.client ?? client6).get({
15026
15403
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15027
- url: "/movies/wanted",
15404
+ url: "/api/movies/wanted",
15028
15405
  ...options
15029
15406
  }), getProviders = (options) => (options?.client ?? client6).get({
15030
15407
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15031
- url: "/providers",
15408
+ url: "/api/providers",
15032
15409
  ...options
15033
15410
  }), postProviders = (options) => (options.client ?? client6).post({
15034
15411
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15035
- url: "/providers",
15412
+ url: "/api/providers",
15036
15413
  ...options
15037
15414
  }), getProviderEpisodes = (options) => (options.client ?? client6).get({
15038
15415
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15039
- url: "/providers/episodes",
15416
+ url: "/api/providers/episodes",
15040
15417
  ...options
15041
15418
  }), postProviderEpisodes = (options) => (options.client ?? client6).post({
15042
15419
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15043
- url: "/providers/episodes",
15420
+ url: "/api/providers/episodes",
15044
15421
  ...options
15045
15422
  }), getProviderMovies = (options) => (options.client ?? client6).get({
15046
15423
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15047
- url: "/providers/movies",
15424
+ url: "/api/providers/movies",
15048
15425
  ...options
15049
15426
  }), postProviderMovies = (options) => (options.client ?? client6).post({
15050
15427
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15051
- url: "/providers/movies",
15428
+ url: "/api/providers/movies",
15052
15429
  ...options
15053
15430
  }), getSeries = (options) => (options?.client ?? client6).get({
15054
15431
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15055
- url: "/series",
15432
+ url: "/api/series",
15056
15433
  ...options
15057
15434
  }), patchSeries = (options) => (options?.client ?? client6).patch({
15058
15435
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15059
- url: "/series",
15436
+ url: "/api/series",
15060
15437
  ...options
15061
15438
  }), postSeries = (options) => (options?.client ?? client6).post({
15062
15439
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15063
- url: "/series",
15440
+ url: "/api/series",
15064
15441
  ...options
15065
15442
  }), getSubtitles = (options) => (options.client ?? client6).get({
15066
15443
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15067
- url: "/subtitles",
15444
+ url: "/api/subtitles",
15068
15445
  ...options
15069
15446
  }), patchSubtitles = (options) => (options.client ?? client6).patch({
15070
15447
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15071
- url: "/subtitles",
15448
+ url: "/api/subtitles",
15072
15449
  ...options
15073
15450
  }), getSubtitleNameInfo = (options) => (options.client ?? client6).get({
15074
15451
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15075
- url: "/subtitles/info",
15452
+ url: "/api/subtitles/info",
15076
15453
  ...options
15077
15454
  }), getSystemAnnouncements = (options) => (options?.client ?? client6).get({
15078
15455
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15079
- url: "/system/announcements",
15456
+ url: "/api/system/announcements",
15080
15457
  ...options
15081
15458
  }), postSystemAnnouncements = (options) => (options.client ?? client6).post({
15082
15459
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15083
- url: "/system/announcements",
15460
+ url: "/api/system/announcements",
15084
15461
  ...options
15085
15462
  }), deleteSystemBackups = (options) => (options.client ?? client6).delete({
15086
15463
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15087
- url: "/system/backups",
15464
+ url: "/api/system/backups",
15088
15465
  ...options
15089
15466
  }), getSystemBackups = (options) => (options?.client ?? client6).get({
15090
15467
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15091
- url: "/system/backups",
15468
+ url: "/api/system/backups",
15092
15469
  ...options
15093
15470
  }), patchSystemBackups = (options) => (options.client ?? client6).patch({
15094
15471
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15095
- url: "/system/backups",
15472
+ url: "/api/system/backups",
15096
15473
  ...options
15097
15474
  }), postSystemBackups = (options) => (options?.client ?? client6).post({
15098
15475
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15099
- url: "/system/backups",
15476
+ url: "/api/system/backups",
15100
15477
  ...options
15101
15478
  }), getSystemHealth = (options) => (options?.client ?? client6).get({
15102
15479
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15103
- url: "/system/health",
15480
+ url: "/api/system/health",
15104
15481
  ...options
15105
15482
  }), deleteSystemJobs = (options) => (options.client ?? client6).delete({
15106
15483
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15107
- url: "/system/jobs",
15484
+ url: "/api/system/jobs",
15108
15485
  ...options
15109
15486
  }), getSystemJobs = (options) => (options?.client ?? client6).get({
15110
15487
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15111
- url: "/system/jobs",
15488
+ url: "/api/system/jobs",
15112
15489
  ...options
15113
15490
  }), patchSystemJobs = (options) => (options.client ?? client6).patch({
15114
15491
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15115
- url: "/system/jobs",
15492
+ url: "/api/system/jobs",
15116
15493
  ...options
15117
15494
  }), postSystemJobs = (options) => (options.client ?? client6).post({
15118
15495
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15119
- url: "/system/jobs",
15496
+ url: "/api/system/jobs",
15120
15497
  ...options
15121
15498
  }), getLanguages = (options) => (options?.client ?? client6).get({
15122
15499
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15123
- url: "/system/languages",
15500
+ url: "/api/system/languages",
15124
15501
  ...options
15125
15502
  }), getLanguagesProfiles = (options) => (options?.client ?? client6).get({
15126
15503
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15127
- url: "/system/languages/profiles",
15504
+ url: "/api/system/languages/profiles",
15128
15505
  ...options
15129
15506
  }), deleteSystemLogs = (options) => (options?.client ?? client6).delete({
15130
15507
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15131
- url: "/system/logs",
15508
+ url: "/api/system/logs",
15132
15509
  ...options
15133
15510
  }), getSystemLogs = (options) => (options?.client ?? client6).get({
15134
15511
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15135
- url: "/system/logs",
15512
+ url: "/api/system/logs",
15136
15513
  ...options
15137
15514
  }), getSystemPing = (options) => (options?.client ?? client6).get({
15138
15515
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15139
- url: "/system/ping",
15516
+ url: "/api/system/ping",
15140
15517
  ...options
15141
15518
  }), getSystemReleases = (options) => (options?.client ?? client6).get({
15142
15519
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15143
- url: "/system/releases",
15520
+ url: "/api/system/releases",
15144
15521
  ...options
15145
15522
  }), getSearches = (options) => (options.client ?? client6).get({
15146
15523
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15147
- url: "/system/searches",
15524
+ url: "/api/system/searches",
15148
15525
  ...options
15149
15526
  }), getSystemStatus = (options) => (options?.client ?? client6).get({
15150
15527
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15151
- url: "/system/status",
15528
+ url: "/api/system/status",
15152
15529
  ...options
15153
15530
  }), getSystemTasks = (options) => (options?.client ?? client6).get({
15154
15531
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15155
- url: "/system/tasks",
15532
+ url: "/api/system/tasks",
15156
15533
  ...options
15157
15534
  }), postSystemTasks = (options) => (options.client ?? client6).post({
15158
15535
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15159
- url: "/system/tasks",
15536
+ url: "/api/system/tasks",
15160
15537
  ...options
15161
15538
  }), postSystemWebhookTest = (options) => (options?.client ?? client6).post({
15162
15539
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15163
- url: "/system/webhooks/test",
15540
+ url: "/api/system/webhooks/test",
15164
15541
  ...options
15165
15542
  }), postWebHooksPlex = (options) => (options.client ?? client6).post({
15166
15543
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15167
- url: "/webhooks/plex",
15544
+ url: "/api/webhooks/plex",
15168
15545
  ...options
15169
15546
  }), postWebHooksRadarr = (options) => (options.client ?? client6).post({
15170
15547
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15171
- url: "/webhooks/radarr",
15548
+ url: "/api/webhooks/radarr",
15172
15549
  ...options,
15173
15550
  headers: {
15174
15551
  "Content-Type": "application/json",
@@ -15176,7 +15553,7 @@ var getBadges = (options) => (options?.client ?? client6).get({
15176
15553
  }
15177
15554
  }), postWebHooksSonarr = (options) => (options.client ?? client6).post({
15178
15555
  security: [{ name: "X-API-KEY", type: "apiKey" }],
15179
- url: "/webhooks/sonarr",
15556
+ url: "/api/webhooks/sonarr",
15180
15557
  ...options,
15181
15558
  headers: {
15182
15559
  "Content-Type": "application/json",
@@ -15198,13 +15575,17 @@ var exports_bazarr = {};
15198
15575
  __export(exports_bazarr, {
15199
15576
  BazarrClient: () => BazarrClient
15200
15577
  });
15578
+ function getBazarrApiBaseUrl(baseUrl) {
15579
+ const normalizedBaseUrl = baseUrl.replace(/\/$/, "");
15580
+ return normalizedBaseUrl.endsWith("/api") ? normalizedBaseUrl : `${normalizedBaseUrl}/api`;
15581
+ }
15201
15582
 
15202
15583
  class BazarrClient {
15203
15584
  clientConfig;
15204
15585
  constructor(config) {
15205
15586
  this.clientConfig = createServarrClient(config);
15206
15587
  client6.setConfig({
15207
- baseUrl: this.clientConfig.getBaseUrl(),
15588
+ baseUrl: getBazarrApiBaseUrl(this.clientConfig.getBaseUrl()),
15208
15589
  headers: this.clientConfig.getHeaders()
15209
15590
  });
15210
15591
  }
@@ -15543,6 +15924,10 @@ class BazarrClient {
15543
15924
  updateConfig(newConfig) {
15544
15925
  const updatedConfig = { ...this.clientConfig.config, ...newConfig };
15545
15926
  this.clientConfig = createServarrClient(updatedConfig);
15927
+ client6.setConfig({
15928
+ baseUrl: getBazarrApiBaseUrl(this.clientConfig.getBaseUrl()),
15929
+ headers: this.clientConfig.getHeaders()
15930
+ });
15546
15931
  return this.clientConfig.config;
15547
15932
  }
15548
15933
  }
@@ -15657,6 +16042,16 @@ var exports_doctor = {};
15657
16042
  __export(exports_doctor, {
15658
16043
  doctor: () => doctor
15659
16044
  });
16045
+ function extractVersion(service, status) {
16046
+ const data = status?.data ?? status;
16047
+ if (typeof data === "string") {
16048
+ return null;
16049
+ }
16050
+ if (service === "bazarr") {
16051
+ return data?.data?.bazarr_version ?? data?.bazarr_version ?? null;
16052
+ }
16053
+ return data?.version ?? status?.version ?? null;
16054
+ }
15660
16055
  var clientFactories, doctor;
15661
16056
  var init_doctor = __esm(() => {
15662
16057
  init_dist();
@@ -15681,37 +16076,74 @@ var init_doctor = __esm(() => {
15681
16076
  name: "doctor",
15682
16077
  description: "Test all configured service connections"
15683
16078
  },
15684
- async run() {
15685
- consola.info(`Checking connections...
15686
- `);
15687
- const _config = loadConfig();
16079
+ args: {
16080
+ json: { type: "boolean", description: "Output as JSON" },
16081
+ table: { type: "boolean", description: "Output as table" },
16082
+ quiet: { type: "boolean", alias: "q", description: "Output service names only" }
16083
+ },
16084
+ async run({ args }) {
16085
+ const format = detectFormat(args);
15688
16086
  let hasAny = false;
16087
+ const results = [];
16088
+ if (format === "table") {
16089
+ consola.info(`Checking connections...
16090
+ `);
16091
+ }
15689
16092
  for (const service of SERVICES) {
15690
16093
  const svcConfig = getServiceConfig(service);
15691
16094
  if (!svcConfig) {
15692
- console.log(` ${service.padEnd(10)} (not configured)`);
16095
+ results.push({
16096
+ service,
16097
+ configured: false,
16098
+ status: "not configured"
16099
+ });
15693
16100
  continue;
15694
16101
  }
15695
16102
  hasAny = true;
15696
16103
  try {
15697
16104
  const factory = clientFactories[service];
15698
16105
  if (!factory) {
15699
- console.log(` ${service.padEnd(10)} ${svcConfig.baseUrl.padEnd(30)} \u2014 SKIP`);
16106
+ results.push({
16107
+ service,
16108
+ configured: true,
16109
+ status: "fail",
16110
+ baseUrl: svcConfig.baseUrl,
16111
+ error: "No client factory available"
16112
+ });
15700
16113
  continue;
15701
16114
  }
15702
16115
  const client7 = factory(svcConfig);
15703
16116
  const status = await client7.getSystemStatus();
15704
- const version = status?.data?.version ?? status?.version ?? "?";
15705
- console.log(` ${service.padEnd(10)} ${svcConfig.baseUrl.padEnd(30)} v${version} OK`);
16117
+ const version = extractVersion(service, status) ?? "?";
16118
+ if (version === "?") {
16119
+ throw new Error("Unexpected response payload");
16120
+ }
16121
+ results.push({
16122
+ service,
16123
+ configured: true,
16124
+ status: "ok",
16125
+ version: String(version),
16126
+ baseUrl: svcConfig.baseUrl
16127
+ });
15706
16128
  } catch (error) {
15707
16129
  const msg = error instanceof Error ? error.message : "Unknown error";
15708
- console.log(` ${service.padEnd(10)} ${svcConfig.baseUrl.padEnd(30)} \u2014 FAIL ${msg}`);
16130
+ results.push({
16131
+ service,
16132
+ configured: true,
16133
+ status: "fail",
16134
+ baseUrl: svcConfig.baseUrl,
16135
+ error: msg
16136
+ });
15709
16137
  }
15710
16138
  }
15711
- if (!hasAny) {
16139
+ if (!hasAny && format === "table") {
15712
16140
  consola.warn("\nNo services configured. Run `tsarr config init` to set up.");
15713
16141
  }
15714
- console.log();
16142
+ formatOutput(results, {
16143
+ format,
16144
+ columns: ["service", "status", "version", "baseUrl", "error"],
16145
+ idField: "service"
16146
+ });
15715
16147
  }
15716
16148
  });
15717
16149
  });
@@ -16002,7 +16434,7 @@ var init_completions = __esm(() => {
16002
16434
  init_dist2();
16003
16435
  SERVICE_COMMANDS = {
16004
16436
  radarr: {
16005
- movie: ["list", "get", "search", "delete"],
16437
+ movie: ["list", "get", "search", "add", "edit", "refresh", "manual-search", "delete"],
16006
16438
  profile: ["list", "get"],
16007
16439
  tag: ["list"],
16008
16440
  queue: ["list", "status"],
@@ -16012,7 +16444,7 @@ var init_completions = __esm(() => {
16012
16444
  customformat: ["list"]
16013
16445
  },
16014
16446
  sonarr: {
16015
- series: ["list", "get", "search", "delete"],
16447
+ series: ["list", "get", "search", "add", "edit", "refresh", "manual-search", "delete"],
16016
16448
  episode: ["list", "get"],
16017
16449
  profile: ["list"],
16018
16450
  tag: ["list"],
@@ -16020,7 +16452,7 @@ var init_completions = __esm(() => {
16020
16452
  system: ["status", "health"]
16021
16453
  },
16022
16454
  lidarr: {
16023
- artist: ["list", "get", "search", "delete"],
16455
+ artist: ["list", "get", "search", "add", "edit", "refresh", "manual-search", "delete"],
16024
16456
  album: ["list", "get", "search"],
16025
16457
  profile: ["list"],
16026
16458
  tag: ["list"],
@@ -16028,7 +16460,7 @@ var init_completions = __esm(() => {
16028
16460
  system: ["status", "health"]
16029
16461
  },
16030
16462
  readarr: {
16031
- author: ["list", "get", "search", "delete"],
16463
+ author: ["list", "get", "search", "add", "edit", "refresh", "manual-search", "delete"],
16032
16464
  book: ["list", "get", "search"],
16033
16465
  profile: ["list"],
16034
16466
  tag: ["list"],
@@ -16084,10 +16516,12 @@ var init_completions = __esm(() => {
16084
16516
 
16085
16517
  // src/cli/index.ts
16086
16518
  init_dist();
16519
+ import { readFileSync as readFileSync2 } from "fs";
16520
+ var { version } = JSON.parse(readFileSync2(new URL("../../package.json", import.meta.url), "utf-8"));
16087
16521
  var main = defineCommand({
16088
16522
  meta: {
16089
16523
  name: "tsarr",
16090
- version: "1.8.0",
16524
+ version,
16091
16525
  description: "CLI for Servarr APIs (Radarr, Sonarr, Lidarr, Readarr, Prowlarr, Bazarr)"
16092
16526
  },
16093
16527
  subCommands: {