@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.
- package/README.md +225 -0
- package/dist/index.js +20438 -0
- package/package.json +63 -0
- package/src/api/client.ts +509 -0
- package/src/api/index.ts +2 -0
- package/src/api/schemas.ts +125 -0
- package/src/api/types.ts +74 -0
- package/src/commands/auth.ts +58 -0
- package/src/commands/batch.ts +50 -0
- package/src/commands/collections.ts +257 -0
- package/src/commands/index.ts +27 -0
- package/src/commands/overview.ts +95 -0
- package/src/commands/raindrops.ts +157 -0
- package/src/commands/tags.ts +40 -0
- package/src/index.ts +454 -0
- package/src/utils/config.ts +114 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/output.ts +37 -0
- package/src/utils/spinner.ts +35 -0
- package/src/utils/tempfile.ts +6 -0
|
@@ -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();
|