@kyoji2/raindrop-cli 0.1.1

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.
@@ -0,0 +1,157 @@
1
+ import type { RaindropUpdate } from "../api";
2
+ import { type GlobalOptions, output, outputError } from "../utils/output";
3
+ import { withSpinner } from "../utils/spinner";
4
+ import { getAuthenticatedAPI } from "./auth";
5
+
6
+ export interface SearchOptions extends GlobalOptions {
7
+ query: string;
8
+ collection?: string;
9
+ limit: string;
10
+ pretty?: boolean;
11
+ }
12
+
13
+ export async function cmdSearch(options: SearchOptions): Promise<void> {
14
+ const api = getAuthenticatedAPI(options);
15
+ const collectionId = options.collection ? parseInt(options.collection, 10) : 0;
16
+ const limit = parseInt(options.limit, 10) || 50;
17
+
18
+ const results = await withSpinner("Searching...", () => api.search(options.query, collectionId, limit));
19
+
20
+ if (options.pretty) {
21
+ console.log(`\n${"ID".padEnd(12)}${"Title".padEnd(50)}${"Tags".padEnd(30)}Link`);
22
+ console.log("-".repeat(120));
23
+ for (const r of results) {
24
+ const title = r.title.length > 47 ? `${r.title.slice(0, 47)}...` : r.title;
25
+ const tags = r.tags.join(", ");
26
+ const tagsDisplay = tags.length > 27 ? `${tags.slice(0, 27)}...` : tags;
27
+ const link = r.link.length > 40 ? `${r.link.slice(0, 40)}...` : r.link;
28
+ console.log(`${String(r._id).padEnd(12)}${title.padEnd(50)}${tagsDisplay.padEnd(30)}${link}`);
29
+ }
30
+ console.log(`\nTotal results: ${results.length}`);
31
+ } else {
32
+ output(
33
+ {
34
+ items: results.map((r) => ({
35
+ id: r._id,
36
+ title: r.title,
37
+ link: r.link,
38
+ tags: r.tags.join(","),
39
+ type: r.type || "link",
40
+ created: r.created,
41
+ })),
42
+ },
43
+ options.format,
44
+ );
45
+ }
46
+ }
47
+
48
+ export interface GetOptions extends GlobalOptions {
49
+ id: string;
50
+ }
51
+
52
+ export async function cmdGet(options: GetOptions): Promise<void> {
53
+ const id = parseInt(options.id, 10);
54
+
55
+ if (Number.isNaN(id)) {
56
+ outputError("Invalid raindrop ID", 400);
57
+ }
58
+
59
+ const api = getAuthenticatedAPI(options);
60
+ const result = await withSpinner("Fetching bookmark...", () => api.getRaindrop(id));
61
+ output(result, options.format);
62
+ }
63
+
64
+ export interface SuggestOptions extends GlobalOptions {
65
+ id: string;
66
+ }
67
+
68
+ export async function cmdSuggest(options: SuggestOptions): Promise<void> {
69
+ const id = parseInt(options.id, 10);
70
+
71
+ if (Number.isNaN(id)) {
72
+ outputError("Invalid raindrop ID", 400);
73
+ }
74
+
75
+ const api = getAuthenticatedAPI(options);
76
+ const suggestions = await withSpinner("Getting suggestions...", () => api.getSuggestions(id));
77
+ output(suggestions, options.format);
78
+ }
79
+
80
+ export interface WaybackOptions extends GlobalOptions {
81
+ url: string;
82
+ }
83
+
84
+ export async function cmdWayback(options: WaybackOptions): Promise<void> {
85
+ if (!options.url) {
86
+ outputError("URL is required", 400);
87
+ }
88
+
89
+ const api = getAuthenticatedAPI(options);
90
+ const snapshot = await withSpinner("Checking Wayback Machine...", () => api.checkWayback(options.url));
91
+ output({ url: options.url, snapshot }, options.format);
92
+ }
93
+
94
+ export interface AddOptions extends GlobalOptions {
95
+ url: string;
96
+ title?: string;
97
+ tags?: string;
98
+ collection?: string;
99
+ }
100
+
101
+ export async function cmdAdd(options: AddOptions): Promise<void> {
102
+ if (!options.url) {
103
+ outputError("URL is required", 400);
104
+ }
105
+
106
+ const api = getAuthenticatedAPI(options);
107
+ const tags = options.tags ? options.tags.split(",").map((t) => t.trim()) : undefined;
108
+ const collectionId = options.collection ? parseInt(options.collection, 10) : undefined;
109
+
110
+ const result = await withSpinner("Adding bookmark...", () =>
111
+ api.addRaindrop({
112
+ link: options.url,
113
+ title: options.title,
114
+ tags,
115
+ collection: collectionId ? { $id: collectionId } : undefined,
116
+ }),
117
+ );
118
+ output(result, options.format);
119
+ }
120
+
121
+ export interface PatchOptions extends GlobalOptions {
122
+ id: string;
123
+ json: string;
124
+ }
125
+
126
+ export async function cmdPatch(options: PatchOptions): Promise<void> {
127
+ const id = parseInt(options.id, 10);
128
+
129
+ if (Number.isNaN(id)) {
130
+ outputError("Invalid raindrop ID", 400);
131
+ }
132
+
133
+ if (!options.json) {
134
+ outputError("JSON patch data is required", 400);
135
+ }
136
+
137
+ const api = getAuthenticatedAPI(options);
138
+ const patchData: RaindropUpdate = JSON.parse(options.json);
139
+ const result = await withSpinner("Updating bookmark...", () => api.updateRaindrop(id, patchData));
140
+ output(result, options.format);
141
+ }
142
+
143
+ export interface DeleteOptions extends GlobalOptions {
144
+ id: string;
145
+ }
146
+
147
+ export async function cmdDelete(options: DeleteOptions): Promise<void> {
148
+ const id = parseInt(options.id, 10);
149
+
150
+ if (Number.isNaN(id)) {
151
+ outputError("Invalid raindrop ID", 400);
152
+ }
153
+
154
+ const api = getAuthenticatedAPI(options);
155
+ const success = await withSpinner("Deleting bookmark...", () => api.deleteRaindrop(id));
156
+ output({ success }, options.format);
157
+ }
@@ -0,0 +1,40 @@
1
+ import { type GlobalOptions, output, outputError } from "../utils/output";
2
+ import { withSpinner } from "../utils/spinner";
3
+ import { getAuthenticatedAPI } from "./auth";
4
+
5
+ export interface TagDeleteOptions extends GlobalOptions {
6
+ tags: string[];
7
+ collection?: string;
8
+ }
9
+
10
+ export async function cmdTagDelete(options: TagDeleteOptions): Promise<void> {
11
+ if (options.tags.length === 0) {
12
+ outputError("At least one tag is required", 400);
13
+ }
14
+
15
+ const collectionId = options.collection ? parseInt(options.collection, 10) : 0;
16
+ const api = getAuthenticatedAPI(options);
17
+ const success = await withSpinner(`Deleting ${options.tags.length} tag(s)...`, () =>
18
+ api.deleteTags(options.tags, collectionId),
19
+ );
20
+ output({ success }, options.format);
21
+ }
22
+
23
+ export interface TagRenameOptions extends GlobalOptions {
24
+ oldName: string;
25
+ newName: string;
26
+ collection?: string;
27
+ }
28
+
29
+ export async function cmdTagRename(options: TagRenameOptions): Promise<void> {
30
+ if (!options.oldName || !options.newName) {
31
+ outputError("Both old and new tag names are required", 400);
32
+ }
33
+
34
+ const collectionId = options.collection ? parseInt(options.collection, 10) : 0;
35
+ const api = getAuthenticatedAPI(options);
36
+ const success = await withSpinner(`Renaming tag '${options.oldName}' to '${options.newName}'...`, () =>
37
+ api.renameTag(options.oldName, options.newName, collectionId),
38
+ );
39
+ output({ success }, options.format);
40
+ }
package/src/index.ts ADDED
@@ -0,0 +1,454 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { Command } from "commander";
4
+ import { RaindropError } from "./api";
5
+ import {
6
+ cmdAdd,
7
+ cmdBatchDelete,
8
+ cmdBatchUpdate,
9
+ cmdCollectionClean,
10
+ cmdCollectionCover,
11
+ cmdCollectionCreate,
12
+ cmdCollectionDelete,
13
+ cmdCollectionDeleteMultiple,
14
+ cmdCollectionEmptyTrash,
15
+ cmdCollectionExpandAll,
16
+ cmdCollectionGet,
17
+ cmdCollectionMerge,
18
+ cmdCollectionReorder,
19
+ cmdCollectionSetIcon,
20
+ cmdCollectionUpdate,
21
+ cmdContext,
22
+ cmdDelete,
23
+ cmdGet,
24
+ cmdLogin,
25
+ cmdLogout,
26
+ cmdPatch,
27
+ cmdSchema,
28
+ cmdSearch,
29
+ cmdStructure,
30
+ cmdSuggest,
31
+ cmdTagDelete,
32
+ cmdTagRename,
33
+ cmdWayback,
34
+ cmdWhoami,
35
+ } from "./commands";
36
+ import { CLIError, type GlobalOptions, type OutputFormat } from "./utils";
37
+
38
+ const VERSION = "0.1.0";
39
+
40
+ function getGlobalOptions(cmd: Command): GlobalOptions {
41
+ const opts = cmd.optsWithGlobals();
42
+ return {
43
+ dryRun: opts.dryRun ?? false,
44
+ format: (opts.format as OutputFormat) ?? "toon",
45
+ };
46
+ }
47
+
48
+ function formatAndExit(error: string, statusCode: number, hint?: string, format: OutputFormat = "toon"): never {
49
+ const errorData = { error, status: statusCode, hint };
50
+
51
+ if (format === "toon") {
52
+ console.error(`error: ${error}`);
53
+ console.error(`status: ${statusCode}`);
54
+ if (hint) console.error(`hint: ${hint}`);
55
+ } else {
56
+ console.error(JSON.stringify(errorData, null, 2));
57
+ }
58
+
59
+ process.exit(1);
60
+ }
61
+
62
+ function withErrorHandler<T extends unknown[]>(fn: (...args: T) => Promise<void>): (...args: T) => Promise<void> {
63
+ return async (...args: T) => {
64
+ try {
65
+ await fn(...args);
66
+ } catch (error) {
67
+ handleError(error);
68
+ }
69
+ };
70
+ }
71
+
72
+ function handleError(error: unknown): never {
73
+ if (error instanceof CLIError) {
74
+ formatAndExit(error.message, error.statusCode, error.hint);
75
+ } else if (error instanceof RaindropError) {
76
+ let hint = error.hint;
77
+ if (!hint) {
78
+ if (error.statusCode === 404) {
79
+ hint = "The requested resource was not found. Verify the ID is correct.";
80
+ } else if (error.statusCode === 401) {
81
+ hint = "Authentication failed. Try running 'raindrop login' again.";
82
+ }
83
+ }
84
+ formatAndExit(error.message, error.statusCode, hint);
85
+ } else if (error instanceof SyntaxError) {
86
+ formatAndExit(
87
+ "Invalid JSON input provided to command.",
88
+ 400,
89
+ "Ensure your JSON data is valid and properly escaped for the shell.",
90
+ );
91
+ } else {
92
+ formatAndExit(`Unexpected error: ${error}`, 500, "Check the CLI logs or report this issue.");
93
+ }
94
+ }
95
+
96
+ const program = new Command();
97
+
98
+ program
99
+ .name("raindrop")
100
+ .description("AI-native CLI for Raindrop.io")
101
+ .version(VERSION, "-v, --version")
102
+ .option("--dry-run", "Log actions instead of making real API requests", false)
103
+ .option("-f, --format <format>", "Output format: toon (default) or json", "toon");
104
+
105
+ program
106
+ .command("login")
107
+ .description("Login with your Raindrop.io API token")
108
+ .argument("[token]", "API token (will prompt if not provided)")
109
+ .action(
110
+ withErrorHandler(async (token: string | undefined, _options, cmd: Command) => {
111
+ const globalOpts = getGlobalOptions(cmd);
112
+ await cmdLogin({ ...globalOpts, token });
113
+ }),
114
+ );
115
+
116
+ program
117
+ .command("logout")
118
+ .description("Remove stored credentials")
119
+ .action(
120
+ withErrorHandler(async (_options, cmd: Command) => {
121
+ const globalOpts = getGlobalOptions(cmd);
122
+ await cmdLogout(globalOpts);
123
+ }),
124
+ );
125
+
126
+ program
127
+ .command("whoami")
128
+ .description("Show current user details")
129
+ .action(
130
+ withErrorHandler(async (_options, cmd: Command) => {
131
+ const globalOpts = getGlobalOptions(cmd);
132
+ await cmdWhoami(globalOpts);
133
+ }),
134
+ );
135
+
136
+ program
137
+ .command("context")
138
+ .description("Show high-level account context (User, Stats, Recent Activity)")
139
+ .action(
140
+ withErrorHandler(async (_options, cmd: Command) => {
141
+ const globalOpts = getGlobalOptions(cmd);
142
+ await cmdContext(globalOpts);
143
+ }),
144
+ );
145
+
146
+ program
147
+ .command("structure")
148
+ .description("Show collections and tags")
149
+ .action(
150
+ withErrorHandler(async (_options, cmd: Command) => {
151
+ const globalOpts = getGlobalOptions(cmd);
152
+ await cmdStructure(globalOpts);
153
+ }),
154
+ );
155
+
156
+ program
157
+ .command("schema")
158
+ .description("Dump JSON schemas and usage examples (for AI context)")
159
+ .action(() => {
160
+ cmdSchema();
161
+ });
162
+
163
+ program
164
+ .command("search")
165
+ .description("Search for bookmarks")
166
+ .argument("[query]", "Search query", "")
167
+ .option("-c, --collection <id>", "Filter by collection ID")
168
+ .option("-l, --limit <count>", "Maximum results to return", "50")
169
+ .option("-p, --pretty", "Display as formatted table")
170
+ .action(
171
+ withErrorHandler(async (query: string, options, cmd: Command) => {
172
+ const globalOpts = getGlobalOptions(cmd);
173
+ await cmdSearch({
174
+ ...globalOpts,
175
+ query,
176
+ collection: options.collection,
177
+ limit: options.limit,
178
+ pretty: options.pretty,
179
+ });
180
+ }),
181
+ );
182
+
183
+ program
184
+ .command("get")
185
+ .description("Get full details for a specific bookmark")
186
+ .argument("<id>", "Bookmark ID")
187
+ .action(
188
+ withErrorHandler(async (id: string, _options, cmd: Command) => {
189
+ const globalOpts = getGlobalOptions(cmd);
190
+ await cmdGet({ ...globalOpts, id });
191
+ }),
192
+ );
193
+
194
+ program
195
+ .command("suggest")
196
+ .description("Get tag/collection suggestions for a bookmark")
197
+ .argument("<id>", "Bookmark ID")
198
+ .action(
199
+ withErrorHandler(async (id: string, _options, cmd: Command) => {
200
+ const globalOpts = getGlobalOptions(cmd);
201
+ await cmdSuggest({ ...globalOpts, id });
202
+ }),
203
+ );
204
+
205
+ program
206
+ .command("wayback")
207
+ .description("Check if a URL is available in the Wayback Machine")
208
+ .argument("<url>", "URL to check")
209
+ .action(
210
+ withErrorHandler(async (url: string, _options, cmd: Command) => {
211
+ const globalOpts = getGlobalOptions(cmd);
212
+ await cmdWayback({ ...globalOpts, url });
213
+ }),
214
+ );
215
+
216
+ program
217
+ .command("add")
218
+ .description("Add a new bookmark")
219
+ .argument("<url>", "URL to bookmark")
220
+ .option("-t, --title <title>", "Bookmark title")
221
+ .option("--tags <tags>", "Comma-separated tags")
222
+ .option("-c, --collection <id>", "Collection ID")
223
+ .action(
224
+ withErrorHandler(async (url: string, options, cmd: Command) => {
225
+ const globalOpts = getGlobalOptions(cmd);
226
+ await cmdAdd({ ...globalOpts, url, title: options.title, tags: options.tags, collection: options.collection });
227
+ }),
228
+ );
229
+
230
+ program
231
+ .command("patch")
232
+ .description("Update a bookmark with a JSON patch")
233
+ .argument("<id>", "Bookmark ID")
234
+ .argument("<json>", "JSON patch data")
235
+ .action(
236
+ withErrorHandler(async (id: string, json: string, _options, cmd: Command) => {
237
+ const globalOpts = getGlobalOptions(cmd);
238
+ await cmdPatch({ ...globalOpts, id, json });
239
+ }),
240
+ );
241
+
242
+ program
243
+ .command("delete")
244
+ .description("Delete a bookmark")
245
+ .argument("<id>", "Bookmark ID")
246
+ .action(
247
+ withErrorHandler(async (id: string, _options, cmd: Command) => {
248
+ const globalOpts = getGlobalOptions(cmd);
249
+ await cmdDelete({ ...globalOpts, id });
250
+ }),
251
+ );
252
+
253
+ const collectionCmd = program.command("collection").description("Manage collections");
254
+
255
+ collectionCmd
256
+ .command("create")
257
+ .description("Create a new collection")
258
+ .argument("<title>", "Collection title")
259
+ .option("--parent <id>", "Parent collection ID")
260
+ .option("--public", "Make collection public")
261
+ .option("--private", "Make collection private")
262
+ .option("--view <view>", "View type: list, simple, grid, masonry")
263
+ .action(
264
+ withErrorHandler(async (title: string, options, cmd: Command) => {
265
+ const globalOpts = getGlobalOptions(cmd);
266
+ await cmdCollectionCreate({
267
+ ...globalOpts,
268
+ title,
269
+ parent: options.parent,
270
+ public: options.public,
271
+ private: options.private,
272
+ view: options.view,
273
+ });
274
+ }),
275
+ );
276
+
277
+ collectionCmd
278
+ .command("get")
279
+ .description("Get details of a specific collection")
280
+ .argument("<id>", "Collection ID")
281
+ .action(
282
+ withErrorHandler(async (id: string, _options, cmd: Command) => {
283
+ const globalOpts = getGlobalOptions(cmd);
284
+ await cmdCollectionGet({ ...globalOpts, id });
285
+ }),
286
+ );
287
+
288
+ collectionCmd
289
+ .command("update")
290
+ .description("Update a collection with a JSON patch")
291
+ .argument("<id>", "Collection ID")
292
+ .argument("<json>", "JSON patch data")
293
+ .action(
294
+ withErrorHandler(async (id: string, json: string, _options, cmd: Command) => {
295
+ const globalOpts = getGlobalOptions(cmd);
296
+ await cmdCollectionUpdate({ ...globalOpts, id, json });
297
+ }),
298
+ );
299
+
300
+ collectionCmd
301
+ .command("delete")
302
+ .description("Delete a collection")
303
+ .argument("<id>", "Collection ID")
304
+ .action(
305
+ withErrorHandler(async (id: string, _options, cmd: Command) => {
306
+ const globalOpts = getGlobalOptions(cmd);
307
+ await cmdCollectionDelete({ ...globalOpts, id });
308
+ }),
309
+ );
310
+
311
+ collectionCmd
312
+ .command("delete-multiple")
313
+ .description("Delete multiple collections")
314
+ .argument("<ids>", "Comma-separated collection IDs")
315
+ .action(
316
+ withErrorHandler(async (ids: string, _options, cmd: Command) => {
317
+ const globalOpts = getGlobalOptions(cmd);
318
+ await cmdCollectionDeleteMultiple({ ...globalOpts, ids });
319
+ }),
320
+ );
321
+
322
+ collectionCmd
323
+ .command("reorder")
324
+ .description("Reorder collections")
325
+ .argument("<sort>", "Sort order: title, -title, -count")
326
+ .action(
327
+ withErrorHandler(async (sort: string, _options, cmd: Command) => {
328
+ const globalOpts = getGlobalOptions(cmd);
329
+ await cmdCollectionReorder({ ...globalOpts, sort });
330
+ }),
331
+ );
332
+
333
+ collectionCmd
334
+ .command("expand-all")
335
+ .description("Expand or collapse all collections")
336
+ .argument("<expanded>", "true or false")
337
+ .action(
338
+ withErrorHandler(async (expanded: string, _options, cmd: Command) => {
339
+ const globalOpts = getGlobalOptions(cmd);
340
+ await cmdCollectionExpandAll({ ...globalOpts, expanded });
341
+ }),
342
+ );
343
+
344
+ collectionCmd
345
+ .command("merge")
346
+ .description("Merge collections into target")
347
+ .argument("<ids>", "Comma-separated source collection IDs")
348
+ .argument("<target>", "Target collection ID")
349
+ .action(
350
+ withErrorHandler(async (ids: string, target: string, _options, cmd: Command) => {
351
+ const globalOpts = getGlobalOptions(cmd);
352
+ await cmdCollectionMerge({ ...globalOpts, ids, target });
353
+ }),
354
+ );
355
+
356
+ collectionCmd
357
+ .command("clean")
358
+ .description("Remove all empty collections")
359
+ .action(
360
+ withErrorHandler(async (_options, cmd: Command) => {
361
+ const globalOpts = getGlobalOptions(cmd);
362
+ await cmdCollectionClean(globalOpts);
363
+ }),
364
+ );
365
+
366
+ collectionCmd
367
+ .command("empty-trash")
368
+ .description("Empty the trash collection")
369
+ .action(
370
+ withErrorHandler(async (_options, cmd: Command) => {
371
+ const globalOpts = getGlobalOptions(cmd);
372
+ await cmdCollectionEmptyTrash(globalOpts);
373
+ }),
374
+ );
375
+
376
+ collectionCmd
377
+ .command("cover")
378
+ .description("Upload a cover image (file path or URL)")
379
+ .argument("<id>", "Collection ID")
380
+ .argument("<source>", "Image file path or URL")
381
+ .action(
382
+ withErrorHandler(async (id: string, source: string, _options, cmd: Command) => {
383
+ const globalOpts = getGlobalOptions(cmd);
384
+ await cmdCollectionCover({ ...globalOpts, id, source });
385
+ }),
386
+ );
387
+
388
+ collectionCmd
389
+ .command("set-icon")
390
+ .description("Search and set a collection icon")
391
+ .argument("<id>", "Collection ID")
392
+ .argument("<query>", "Icon search query")
393
+ .action(
394
+ withErrorHandler(async (id: string, query: string, _options, cmd: Command) => {
395
+ const globalOpts = getGlobalOptions(cmd);
396
+ await cmdCollectionSetIcon({ ...globalOpts, id, query });
397
+ }),
398
+ );
399
+
400
+ const tagCmd = program.command("tag").description("Manage tags");
401
+
402
+ tagCmd
403
+ .command("delete")
404
+ .description("Delete tags")
405
+ .argument("<tags...>", "Tags to delete")
406
+ .option("-c, --collection <id>", "Scope to collection ID")
407
+ .action(
408
+ withErrorHandler(async (tags: string[], options, cmd: Command) => {
409
+ const globalOpts = getGlobalOptions(cmd);
410
+ await cmdTagDelete({ ...globalOpts, tags, collection: options.collection });
411
+ }),
412
+ );
413
+
414
+ tagCmd
415
+ .command("rename")
416
+ .description("Rename a tag")
417
+ .argument("<old>", "Old tag name")
418
+ .argument("<new>", "New tag name")
419
+ .option("-c, --collection <id>", "Scope to collection ID")
420
+ .action(
421
+ withErrorHandler(async (oldName: string, newName: string, options, cmd: Command) => {
422
+ const globalOpts = getGlobalOptions(cmd);
423
+ await cmdTagRename({ ...globalOpts, oldName, newName, collection: options.collection });
424
+ }),
425
+ );
426
+
427
+ const batchCmd = program.command("batch").description("Batch operations");
428
+
429
+ batchCmd
430
+ .command("update")
431
+ .description("Update multiple bookmarks")
432
+ .argument("<json>", "JSON patch data")
433
+ .requiredOption("--ids <ids>", "Comma-separated bookmark IDs")
434
+ .option("-c, --collection <id>", "Collection ID for scope")
435
+ .action(
436
+ withErrorHandler(async (json: string, options, cmd: Command) => {
437
+ const globalOpts = getGlobalOptions(cmd);
438
+ await cmdBatchUpdate({ ...globalOpts, ids: options.ids, json, collection: options.collection });
439
+ }),
440
+ );
441
+
442
+ batchCmd
443
+ .command("delete")
444
+ .description("Delete multiple bookmarks")
445
+ .requiredOption("--ids <ids>", "Comma-separated bookmark IDs")
446
+ .option("-c, --collection <id>", "Collection ID for scope")
447
+ .action(
448
+ withErrorHandler(async (options, cmd: Command) => {
449
+ const globalOpts = getGlobalOptions(cmd);
450
+ await cmdBatchDelete({ ...globalOpts, ids: options.ids, collection: options.collection });
451
+ }),
452
+ );
453
+
454
+ program.parse();