@irfanshadikrishad/anilist 1.2.5 → 1.2.7

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.
@@ -114,7 +114,78 @@ class AniList {
114
114
  static exportAnime() {
115
115
  return __awaiter(this, void 0, void 0, function* () {
116
116
  var _a, _b, _c;
117
- if (yield Auth.isLoggedIn()) {
117
+ if (!(yield Auth.isLoggedIn())) {
118
+ console.error(`\nMust login to use this feature.`);
119
+ return;
120
+ }
121
+ const { exportType } = yield inquirer.prompt([
122
+ {
123
+ type: "list",
124
+ name: "exportType",
125
+ message: "Choose export type:",
126
+ choices: [
127
+ { name: "CSV", value: 1 },
128
+ { name: "JSON", value: 2 },
129
+ { name: "XML (MyAnimeList/AniDB)", value: 3 },
130
+ ],
131
+ pageSize: 10,
132
+ },
133
+ ]);
134
+ const animeList = yield fetcher(currentUserAnimeList, {
135
+ id: yield Auth.MyUserId(),
136
+ });
137
+ if (animeList) {
138
+ const lists = (_c = (_b = (_a = animeList === null || animeList === void 0 ? void 0 : animeList.data) === null || _a === void 0 ? void 0 : _a.MediaListCollection) === null || _b === void 0 ? void 0 : _b.lists) !== null && _c !== void 0 ? _c : [];
139
+ const mediaWithProgress = lists.flatMap((list) => list.entries.map((entry) => {
140
+ var _a, _b, _c, _d, _e;
141
+ return ({
142
+ id: (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a.id,
143
+ title: exportType === 1
144
+ ? getTitle((_b = entry === null || entry === void 0 ? void 0 : entry.media) === null || _b === void 0 ? void 0 : _b.title)
145
+ : (_c = entry === null || entry === void 0 ? void 0 : entry.media) === null || _c === void 0 ? void 0 : _c.title,
146
+ episodes: (_d = entry === null || entry === void 0 ? void 0 : entry.media) === null || _d === void 0 ? void 0 : _d.episodes,
147
+ siteUrl: (_e = entry === null || entry === void 0 ? void 0 : entry.media) === null || _e === void 0 ? void 0 : _e.siteUrl,
148
+ progress: entry.progress,
149
+ status: entry === null || entry === void 0 ? void 0 : entry.status,
150
+ hiddenFromStatusLists: entry.hiddenFromStatusLists,
151
+ });
152
+ }));
153
+ switch (exportType) {
154
+ case 1:
155
+ yield saveJSONasCSV(mediaWithProgress, "anime");
156
+ break;
157
+ case 2:
158
+ yield saveJSONasJSON(mediaWithProgress, "anime");
159
+ break;
160
+ case 3:
161
+ yield MyAnimeList.exportAnime();
162
+ break;
163
+ default:
164
+ console.log(`\nInvalid export type. ${exportType}`);
165
+ break;
166
+ }
167
+ }
168
+ else {
169
+ console.error(`\nNo anime(s) found in your lists.`);
170
+ }
171
+ });
172
+ }
173
+ static exportManga() {
174
+ return __awaiter(this, void 0, void 0, function* () {
175
+ var _a, _b;
176
+ if (!(yield Auth.isLoggedIn())) {
177
+ console.error(`\nPlease login to use this feature.`);
178
+ return;
179
+ }
180
+ const mangaLists = yield fetcher(currentUserMangaList, {
181
+ id: yield Auth.MyUserId(),
182
+ });
183
+ if (!(mangaLists === null || mangaLists === void 0 ? void 0 : mangaLists.data)) {
184
+ console.error(`\nCould not get manga list.`);
185
+ return;
186
+ }
187
+ const lists = ((_b = (_a = mangaLists === null || mangaLists === void 0 ? void 0 : mangaLists.data) === null || _a === void 0 ? void 0 : _a.MediaListCollection) === null || _b === void 0 ? void 0 : _b.lists) || [];
188
+ if (lists.length > 0) {
118
189
  const { exportType } = yield inquirer.prompt([
119
190
  {
120
191
  type: "list",
@@ -123,116 +194,42 @@ class AniList {
123
194
  choices: [
124
195
  { name: "CSV", value: 1 },
125
196
  { name: "JSON", value: 2 },
126
- { name: "XML (MyAnimeList/AniDB)", value: 3 },
197
+ { name: "XML (MyAnimeList)", value: 3 },
127
198
  ],
128
199
  pageSize: 10,
129
200
  },
130
201
  ]);
131
- const animeList = yield fetcher(currentUserAnimeList, {
132
- id: yield Auth.MyUserId(),
133
- });
134
- if (animeList) {
135
- const lists = (_c = (_b = (_a = animeList === null || animeList === void 0 ? void 0 : animeList.data) === null || _a === void 0 ? void 0 : _a.MediaListCollection) === null || _b === void 0 ? void 0 : _b.lists) !== null && _c !== void 0 ? _c : [];
136
- const mediaWithProgress = lists.flatMap((list) => list.entries.map((entry) => {
137
- var _a, _b, _c, _d, _e;
138
- return ({
139
- id: (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a.id,
140
- title: exportType === 1
141
- ? getTitle((_b = entry === null || entry === void 0 ? void 0 : entry.media) === null || _b === void 0 ? void 0 : _b.title)
142
- : (_c = entry === null || entry === void 0 ? void 0 : entry.media) === null || _c === void 0 ? void 0 : _c.title,
143
- episodes: (_d = entry === null || entry === void 0 ? void 0 : entry.media) === null || _d === void 0 ? void 0 : _d.episodes,
144
- siteUrl: (_e = entry === null || entry === void 0 ? void 0 : entry.media) === null || _e === void 0 ? void 0 : _e.siteUrl,
145
- progress: entry.progress,
146
- status: entry === null || entry === void 0 ? void 0 : entry.status,
147
- hiddenFromStatusLists: entry.hiddenFromStatusLists,
148
- });
149
- }));
150
- switch (exportType) {
151
- case 1:
152
- yield saveJSONasCSV(mediaWithProgress, "anime");
153
- break;
154
- case 2:
155
- yield saveJSONasJSON(mediaWithProgress, "anime");
156
- break;
157
- case 3:
158
- yield MyAnimeList.exportAnime();
159
- break;
160
- default:
161
- console.log(`\nInvalid export type. ${exportType}`);
162
- break;
163
- }
164
- }
165
- else {
166
- console.error(`\nNo anime(s) found in your lists.`);
167
- }
168
- }
169
- else {
170
- console.error(`\nMust login to use this feature.`);
171
- }
172
- });
173
- }
174
- static exportManga() {
175
- return __awaiter(this, void 0, void 0, function* () {
176
- var _a, _b;
177
- if (yield Auth.isLoggedIn()) {
178
- const mangaLists = yield fetcher(currentUserMangaList, {
179
- id: yield Auth.MyUserId(),
180
- });
181
- if (mangaLists) {
182
- const lists = ((_b = (_a = mangaLists === null || mangaLists === void 0 ? void 0 : mangaLists.data) === null || _a === void 0 ? void 0 : _a.MediaListCollection) === null || _b === void 0 ? void 0 : _b.lists) || [];
183
- if (lists.length > 0) {
184
- const { exportType } = yield inquirer.prompt([
185
- {
186
- type: "list",
187
- name: "exportType",
188
- message: "Choose export type:",
189
- choices: [
190
- { name: "CSV", value: 1 },
191
- { name: "JSON", value: 2 },
192
- { name: "XML (MyAnimeList)", value: 3 },
193
- ],
194
- pageSize: 10,
195
- },
196
- ]);
197
- const mediaWithProgress = lists.flatMap((list) => list.entries.map((entry) => {
198
- var _a, _b, _c;
199
- return ({
200
- id: (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a.id,
201
- title: exportType === 1
202
- ? getTitle((_b = entry === null || entry === void 0 ? void 0 : entry.media) === null || _b === void 0 ? void 0 : _b.title)
203
- : (_c = entry === null || entry === void 0 ? void 0 : entry.media) === null || _c === void 0 ? void 0 : _c.title,
204
- private: entry.private,
205
- chapters: entry.media.chapters,
206
- progress: entry.progress,
207
- status: entry === null || entry === void 0 ? void 0 : entry.status,
208
- hiddenFromStatusLists: entry.hiddenFromStatusLists,
209
- });
210
- }));
211
- switch (exportType) {
212
- case 1:
213
- yield saveJSONasCSV(mediaWithProgress, "manga");
214
- break;
215
- case 2:
216
- yield saveJSONasJSON(mediaWithProgress, "manga");
217
- break;
218
- case 3:
219
- yield MyAnimeList.exportManga();
220
- break;
221
- default:
222
- console.log(`\nInvalid export type. ${exportType}`);
223
- break;
224
- }
225
- }
226
- else {
227
- console.log(`\nList seems to be empty.`);
228
- }
229
- }
230
- else {
231
- console.error(`\nCould not get manga list.`);
202
+ const mediaWithProgress = lists.flatMap((list) => list.entries.map((entry) => {
203
+ var _a, _b, _c;
204
+ return ({
205
+ id: (_a = entry === null || entry === void 0 ? void 0 : entry.media) === null || _a === void 0 ? void 0 : _a.id,
206
+ title: exportType === 1
207
+ ? getTitle((_b = entry === null || entry === void 0 ? void 0 : entry.media) === null || _b === void 0 ? void 0 : _b.title)
208
+ : (_c = entry === null || entry === void 0 ? void 0 : entry.media) === null || _c === void 0 ? void 0 : _c.title,
209
+ private: entry.private,
210
+ chapters: entry.media.chapters,
211
+ progress: entry.progress,
212
+ status: entry === null || entry === void 0 ? void 0 : entry.status,
213
+ hiddenFromStatusLists: entry.hiddenFromStatusLists,
214
+ });
215
+ }));
216
+ switch (exportType) {
217
+ case 1:
218
+ yield saveJSONasCSV(mediaWithProgress, "manga");
219
+ break;
220
+ case 2:
221
+ yield saveJSONasJSON(mediaWithProgress, "manga");
222
+ break;
223
+ case 3:
224
+ yield MyAnimeList.exportManga();
225
+ break;
226
+ default:
227
+ console.log(`\nInvalid export type. ${exportType}`);
228
+ break;
232
229
  }
233
230
  }
234
231
  else {
235
- console.error(`\nPlease login to use this feature.`);
232
+ console.log(`\nList seems to be empty.`);
236
233
  }
237
234
  });
238
235
  }
@@ -243,11 +240,10 @@ class AniList {
243
240
  if (!(yield Auth.isLoggedIn())) {
244
241
  return console.error(`\nPlease log in first to access your lists.`);
245
242
  }
246
- const userId = yield Auth.MyUserId();
247
- if (!userId) {
243
+ if (!(yield Auth.MyUserId())) {
248
244
  return console.log(`\nFailed getting current user Id.`);
249
245
  }
250
- const data = yield fetcher(currentUserAnimeList, { id: userId });
246
+ const data = yield fetcher(currentUserAnimeList, { id: yield Auth.MyUserId() });
251
247
  if (data === null || data === void 0 ? void 0 : data.errors) {
252
248
  return console.log(`\nSomething went wrong. ${(_b = (_a = data === null || data === void 0 ? void 0 : data.errors) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.message}`);
253
249
  }
@@ -19,6 +19,7 @@ declare const activityAllQuery = "query ($userId: Int, $page: Int, $perPage: Int
19
19
  declare const activityMediaList = "query ($userId: Int, $page: Int, $perPage: Int, $type: ActivityType) {\n Page(page: $page, perPage: $perPage) {\n pageInfo { total currentPage lastPage hasNextPage perPage }\n activities(userId: $userId, type: $type, sort: ID_DESC) {\n ... on ListActivity { id type status progress media { id title { romaji english native } format } createdAt }\n }\n }\n}";
20
20
  declare const malIdToAnilistAnimeId = "query ($malId: Int) {\n Media(idMal: $malId, type: ANIME) {\n id title { romaji english } } \n}\n";
21
21
  declare const malIdToAnilistMangaId = "query ($malId: Int) {\n Media(idMal: $malId, type: MANGA) {\n id title { romaji english } } \n}\n";
22
- declare const userFollowingQuery = "query ($userId: Int!) {\n Page {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n following(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage }\n }\n}\n";
23
- declare const userFollowersQuery = "query ($userId: Int!) {\n Page {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n followers(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage }\n }\n}\n";
24
- export { activityAllQuery, activityAnimeListQuery, activityMangaListQuery, activityMediaList, activityMessageQuery, activityTextQuery, animeDetailsQuery, animeSearchQuery, currentUserAnimeList, currentUserMangaList, currentUserQuery, deleteMangaEntryMutation, deleteMediaEntryMutation, malIdToAnilistAnimeId, malIdToAnilistMangaId, mangaSearchQuery, popularQuery, trendingQuery, upcomingAnimesQuery, userActivityQuery, userFollowersQuery, userFollowingQuery, userQuery, };
22
+ declare const userFollowingQuery = "query ($userId: Int!, $page: Int) {\n Page (page: $page) {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n following(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage isFollowing isFollower }\n }\n}\n";
23
+ declare const userFollowersQuery = "query ($userId: Int!, $page: Int) {\n Page (page: $page) {\n pageInfo { total perPage currentPage lastPage hasNextPage }\n followers(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage isFollowing isFollower }\n }\n}\n";
24
+ declare const toggleFollowMutation = "mutation ($userId: Int!) {\n ToggleFollow(userId: $userId) { id name isFollower isFollowing }\n}\n";
25
+ export { activityAllQuery, activityAnimeListQuery, activityMangaListQuery, activityMediaList, activityMessageQuery, activityTextQuery, animeDetailsQuery, animeSearchQuery, currentUserAnimeList, currentUserMangaList, currentUserQuery, deleteMangaEntryMutation, deleteMediaEntryMutation, malIdToAnilistAnimeId, malIdToAnilistMangaId, mangaSearchQuery, popularQuery, toggleFollowMutation, trendingQuery, upcomingAnimesQuery, userActivityQuery, userFollowersQuery, userFollowingQuery, userQuery, };
@@ -129,18 +129,22 @@ const malIdToAnilistMangaId = `query ($malId: Int) {
129
129
  id title { romaji english } }
130
130
  }
131
131
  `;
132
- const userFollowingQuery = `query ($userId: Int!) {
133
- Page {
132
+ const userFollowingQuery = `query ($userId: Int!, $page: Int) {
133
+ Page (page: $page) {
134
134
  pageInfo { total perPage currentPage lastPage hasNextPage }
135
- following(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage }
135
+ following(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage isFollowing isFollower }
136
136
  }
137
137
  }
138
138
  `;
139
- const userFollowersQuery = `query ($userId: Int!) {
140
- Page {
139
+ const userFollowersQuery = `query ($userId: Int!, $page: Int) {
140
+ Page (page: $page) {
141
141
  pageInfo { total perPage currentPage lastPage hasNextPage }
142
- followers(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage }
142
+ followers(userId: $userId, sort: [USERNAME]) { id name avatar { large medium } bannerImage isFollowing isFollower }
143
143
  }
144
144
  }
145
145
  `;
146
- export { activityAllQuery, activityAnimeListQuery, activityMangaListQuery, activityMediaList, activityMessageQuery, activityTextQuery, animeDetailsQuery, animeSearchQuery, currentUserAnimeList, currentUserMangaList, currentUserQuery, deleteMangaEntryMutation, deleteMediaEntryMutation, malIdToAnilistAnimeId, malIdToAnilistMangaId, mangaSearchQuery, popularQuery, trendingQuery, upcomingAnimesQuery, userActivityQuery, userFollowersQuery, userFollowingQuery, userQuery, };
146
+ const toggleFollowMutation = `mutation ($userId: Int!) {
147
+ ToggleFollow(userId: $userId) { id name isFollower isFollowing }
148
+ }
149
+ `;
150
+ export { activityAllQuery, activityAnimeListQuery, activityMangaListQuery, activityMediaList, activityMessageQuery, activityTextQuery, animeDetailsQuery, animeSearchQuery, currentUserAnimeList, currentUserMangaList, currentUserQuery, deleteMangaEntryMutation, deleteMediaEntryMutation, malIdToAnilistAnimeId, malIdToAnilistMangaId, mangaSearchQuery, popularQuery, toggleFollowMutation, trendingQuery, upcomingAnimesQuery, userActivityQuery, userFollowersQuery, userFollowingQuery, userQuery, };
@@ -121,6 +121,29 @@ interface MediaEntry {
121
121
  status: string;
122
122
  hiddenFromStatusLists: boolean;
123
123
  }
124
+ interface SaveTextActivityResponse {
125
+ data?: {
126
+ SaveTextActivity: {
127
+ id: number;
128
+ userId: number;
129
+ text: string;
130
+ createdAt: number;
131
+ };
132
+ };
133
+ errors?: {
134
+ message: string;
135
+ }[];
136
+ }
137
+ interface MediaListCollectionResponse {
138
+ data?: {
139
+ MediaListCollection: {
140
+ lists: MediaList[];
141
+ };
142
+ };
143
+ errors?: {
144
+ message: string;
145
+ }[];
146
+ }
124
147
  interface List {
125
148
  name: string;
126
149
  entries: MediaEntry[];
@@ -197,6 +220,17 @@ interface AnimeDetails {
197
220
  message: string;
198
221
  }[];
199
222
  }
223
+ interface SaveMediaListEntryResponse {
224
+ data?: {
225
+ SaveMediaListEntry: {
226
+ id: number;
227
+ status: string;
228
+ };
229
+ };
230
+ errors?: {
231
+ message: string;
232
+ }[];
233
+ }
200
234
  interface MediaListEntry {
201
235
  id?: number;
202
236
  media: {
@@ -215,14 +249,7 @@ interface MediaListEntry {
215
249
  type UserActivitiesResponse = {
216
250
  data?: {
217
251
  Page: {
218
- activities: {
219
- status: string;
220
- progress: number;
221
- createdAt: number;
222
- media: {
223
- title: MediaTitle;
224
- };
225
- }[];
252
+ activities: Activity[];
226
253
  };
227
254
  };
228
255
  errors?: {
@@ -264,6 +291,17 @@ type UserResponse = {
264
291
  message: string;
265
292
  }[];
266
293
  };
294
+ type User = {
295
+ id: number;
296
+ name: string;
297
+ avatar: {
298
+ large: string;
299
+ medium: string;
300
+ };
301
+ bannerImage: string;
302
+ isFollower: boolean;
303
+ isFollowing: boolean;
304
+ };
267
305
  type UserFollower = {
268
306
  data?: {
269
307
  Page: {
@@ -274,15 +312,7 @@ type UserFollower = {
274
312
  lastPage: number;
275
313
  hasNextPage: boolean;
276
314
  };
277
- followers: {
278
- id: number;
279
- name: string;
280
- avatar: {
281
- large: string;
282
- medium: string;
283
- };
284
- bannerImage: string;
285
- }[];
315
+ followers: User[];
286
316
  };
287
317
  };
288
318
  errors?: {
@@ -299,15 +329,7 @@ type UserFollowing = {
299
329
  lastPage: number;
300
330
  hasNextPage: boolean;
301
331
  };
302
- following: {
303
- id: number;
304
- name: string;
305
- avatar: {
306
- large: string;
307
- medium: string;
308
- };
309
- bannerImage: string;
310
- }[];
332
+ following: User[];
311
333
  };
312
334
  };
313
335
  errors?: {
@@ -331,4 +353,38 @@ type AnimeSearchResponse = {
331
353
  message: string;
332
354
  }[];
333
355
  };
334
- export { AniListMediaStatus, AnimeDetails, AnimeList, AnimeSearchResponse, DateMonthYear, DeleteMangaResponse, List, MALAnimeStatus, MALAnimeXML, MALMangaStatus, MalIdToAnilistIdResponse, MediaEntry, MediaList, MediaListEntry, MediaTitle, MediaWithProgress, Myself, UserActivitiesResponse, UserFollower, UserFollowing, UserResponse, saveAnimeWithProgressResponse, };
356
+ type ToggleFollowResponse = {
357
+ data?: {
358
+ ToggleFollow: {
359
+ id: number;
360
+ name: string;
361
+ isFollower: boolean;
362
+ isFollowing: boolean;
363
+ };
364
+ };
365
+ errors?: {
366
+ message: string;
367
+ }[];
368
+ };
369
+ type DeleteMediaListResponse = {
370
+ data?: {
371
+ DeleteMediaListEntry: {
372
+ deleted: boolean;
373
+ };
374
+ };
375
+ errors?: {
376
+ message: string;
377
+ }[];
378
+ };
379
+ type Activity = {
380
+ id: number;
381
+ type: string;
382
+ status: string;
383
+ progress: number | null;
384
+ media: {
385
+ id?: number;
386
+ title: MediaTitle;
387
+ };
388
+ createdAt: number;
389
+ };
390
+ export { Activity, AniListMediaStatus, AnimeDetails, AnimeList, AnimeSearchResponse, DateMonthYear, DeleteMangaResponse, DeleteMediaListResponse, List, MALAnimeStatus, MALAnimeXML, MALMangaStatus, MalIdToAnilistIdResponse, MediaEntry, MediaList, MediaListCollectionResponse, MediaListEntry, MediaTitle, MediaWithProgress, Myself, SaveMediaListEntryResponse, SaveTextActivityResponse, ToggleFollowResponse, User, UserActivitiesResponse, UserFollower, UserFollowing, UserResponse, saveAnimeWithProgressResponse, };
package/bin/index.js CHANGED
@@ -10,7 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  import { Command } from "commander";
12
12
  import process from "process";
13
- import { Auth } from "./helpers/auth.js";
13
+ import { Auth, Social } from "./helpers/auth.js";
14
14
  import { AniList } from "./helpers/lists.js";
15
15
  import { getCurrentPackageVersion } from "./helpers/workers.js";
16
16
  const cli = new Command();
@@ -203,4 +203,28 @@ cli
203
203
  }
204
204
  }
205
205
  }));
206
+ cli
207
+ .command("social")
208
+ .alias("sol")
209
+ .description("Automate your process")
210
+ .option("-f, --follow", "Follow the user whos following you.", false)
211
+ .option("-u, --unfollow", "Unfollow the user whos not following you.", false)
212
+ .action((_a) => __awaiter(void 0, [_a], void 0, function* ({ follow, unfollow }) {
213
+ if (!follow && !unfollow) {
214
+ console.error(`\nMust select an option, either --follow or --unfollow`);
215
+ }
216
+ else {
217
+ if (yield Auth.isLoggedIn()) {
218
+ if (follow) {
219
+ yield Social.follow();
220
+ }
221
+ else if (unfollow) {
222
+ yield Social.unfollow();
223
+ }
224
+ }
225
+ else {
226
+ console.error(`\nPlease login to use this feature.`);
227
+ }
228
+ }
229
+ }));
206
230
  cli.parse(process.argv);
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@irfanshadikrishad/anilist",
3
3
  "description": "Minimalist unofficial AniList CLI for Anime and Manga Enthusiasts",
4
4
  "author": "Irfan Shadik Rishad",
5
- "version": "1.2.5",
5
+ "version": "1.2.7",
6
6
  "main": "./bin/index.js",
7
7
  "type": "module",
8
8
  "types": "./bin/index.d.ts",
@@ -76,6 +76,7 @@
76
76
  "json2csv": "^6.0.0-alpha.2",
77
77
  "jsonrepair": "^3.11.2",
78
78
  "node-fetch": "^3.3.2",
79
- "open": "^10.1.0"
79
+ "open": "^10.1.0",
80
+ "tiny-spinner": "^2.0.4"
80
81
  }
81
82
  }