@kyoji2/raindrop-cli 0.1.2 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # raindrop-cli
2
2
 
3
- AI-native CLI for Raindrop.io. Built with TypeScript and Bun.
3
+ AI-native CLI for Raindrop.io. Built with TypeScript, using Bun for tooling and Node standard APIs in the codebase.
4
4
 
5
5
  Designed for AI agents and automation scripts. **TOON** format for maximum token efficiency, with optional JSON output for standard integrations.
6
6
 
@@ -12,6 +12,11 @@ Designed for AI agents and automation scripts. **TOON** format for maximum token
12
12
  - **Batch Operations:** Bulk update or delete bookmarks efficiently
13
13
  - **Dry Run Mode:** Safe account management with `--dry-run` flag
14
14
 
15
+ ## Runtime Notes
16
+
17
+ - Prefer Node standard APIs (`node:fs`, `node:timers/promises`, `node:child_process`) over `Bun.*` in source code.
18
+ - Tests still run with `bun test`.
19
+
15
20
  ## Installation
16
21
 
17
22
  ```bash
package/dist/index.js CHANGED
@@ -2121,6 +2121,9 @@ var require_commander = __commonJS((exports) => {
2121
2121
  exports.InvalidOptionArgumentError = InvalidArgumentError;
2122
2122
  });
2123
2123
 
2124
+ // src/index.ts
2125
+ import { readFile as readFile3 } from "fs/promises";
2126
+
2124
2127
  // node_modules/commander/esm.mjs
2125
2128
  var import__ = __toESM(require_commander(), 1);
2126
2129
  var {
@@ -2137,6 +2140,12 @@ var {
2137
2140
  Help
2138
2141
  } = import__.default;
2139
2142
 
2143
+ // src/api/client.ts
2144
+ import { Blob } from "buffer";
2145
+ import { readFile } from "fs/promises";
2146
+ import { basename } from "path";
2147
+ import { setTimeout as delay } from "timers/promises";
2148
+
2140
2149
  // node_modules/zod/v4/classic/external.js
2141
2150
  var exports_external = {};
2142
2151
  __export(exports_external, {
@@ -15898,7 +15907,7 @@ class RaindropAPI {
15898
15907
  throw new RaindropError("Rate limit exceeded. Maximum retries reached.", 429, "Wait a few minutes before trying again.");
15899
15908
  }
15900
15909
  this.logger.warn(`[${method} ${path}] Rate limited. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`);
15901
- await Bun.sleep(backoff);
15910
+ await delay(backoff);
15902
15911
  continue;
15903
15912
  }
15904
15913
  if (response.status >= 500) {
@@ -15908,7 +15917,7 @@ class RaindropAPI {
15908
15917
  }
15909
15918
  const backoff = calculateBackoff(attempt);
15910
15919
  this.logger.warn(`[${method} ${path}] Server error ${response.status}. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`);
15911
- await Bun.sleep(backoff);
15920
+ await delay(backoff);
15912
15921
  continue;
15913
15922
  }
15914
15923
  if (!response.ok) {
@@ -15941,7 +15950,7 @@ class RaindropAPI {
15941
15950
  }
15942
15951
  const backoff = calculateBackoff(attempt);
15943
15952
  this.logger.warn(`[${method} ${path}] Network error. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`);
15944
- await Bun.sleep(backoff);
15953
+ await delay(backoff);
15945
15954
  }
15946
15955
  }
15947
15956
  throw new RaindropError("Maximum retries exceeded", 504);
@@ -16034,9 +16043,9 @@ class RaindropAPI {
16034
16043
  this.logger.log(`File: ${filePath}`);
16035
16044
  return { _id: id, title: "Dry Run Icon", count: 0 };
16036
16045
  }
16037
- const file2 = Bun.file(filePath);
16046
+ const fileBuffer = await readFile(filePath);
16038
16047
  const formData = new FormData;
16039
- formData.append("cover", file2);
16048
+ formData.append("cover", new Blob([fileBuffer]), basename(filePath));
16040
16049
  const response = await fetch(`${RaindropAPI.BASE_URL}/collection/${id}/cover`, {
16041
16050
  method: "PUT",
16042
16051
  headers: { Authorization: `Bearer ${this.token}` },
@@ -16167,6 +16176,7 @@ class RaindropAPI {
16167
16176
  }
16168
16177
  // src/utils/config.ts
16169
16178
  import { existsSync, readFileSync } from "fs";
16179
+ import { mkdir, readFile as readFile2, rm, writeFile } from "fs/promises";
16170
16180
  import { homedir } from "os";
16171
16181
  import { join } from "path";
16172
16182
 
@@ -16203,15 +16213,12 @@ async function saveConfig(config2) {
16203
16213
  if (!config2.token || typeof config2.token !== "string") {
16204
16214
  throw new ConfigError("Invalid token: token must be a non-empty string");
16205
16215
  }
16206
- await Bun.$`mkdir -p ${CONFIG_DIR}`;
16207
- await Bun.write(CONFIG_FILE, JSON.stringify(config2, null, 2));
16216
+ await mkdir(CONFIG_DIR, { recursive: true });
16217
+ await writeFile(CONFIG_FILE, JSON.stringify(config2, null, 2));
16208
16218
  }
16209
16219
  async function deleteConfig() {
16210
16220
  try {
16211
- const file2 = Bun.file(CONFIG_FILE);
16212
- if (await file2.exists()) {
16213
- await Bun.$`rm -f ${CONFIG_FILE}`;
16214
- }
16221
+ await rm(CONFIG_FILE, { force: true });
16215
16222
  } catch {}
16216
16223
  }
16217
16224
  function getToken() {
@@ -19900,6 +19907,9 @@ async function cmdBatchDelete(options) {
19900
19907
  const success3 = await withSpinner(`Deleting ${ids.length} bookmark(s)...`, () => api2.batchDeleteRaindrops(collectionId, ids));
19901
19908
  output({ success: success3 }, options.format);
19902
19909
  }
19910
+ // src/commands/collections.ts
19911
+ import { rm as rm2, writeFile as writeFile2 } from "fs/promises";
19912
+
19903
19913
  // src/utils/tempfile.ts
19904
19914
  import { tmpdir } from "os";
19905
19915
  import { join as join2 } from "path";
@@ -20027,13 +20037,13 @@ async function cmdCollectionCover(options) {
20027
20037
  outputError(`Failed to download image: ${response.status}`, response.status);
20028
20038
  }
20029
20039
  filePath = getTempFilePath("raindrop_cover", ".png");
20030
- await Bun.write(filePath, await response.arrayBuffer());
20040
+ await writeFile2(filePath, Buffer.from(await response.arrayBuffer()));
20031
20041
  stopSpinner(spinner, true, "Downloaded");
20032
20042
  isTemp = true;
20033
20043
  }
20034
20044
  const result = await withSpinner("Uploading cover...", () => api2.uploadCollectionCover(id, filePath));
20035
20045
  if (isTemp) {
20036
- await Bun.$`rm -f ${filePath}`;
20046
+ await rm2(filePath, { force: true });
20037
20047
  }
20038
20048
  output(result, options.format);
20039
20049
  }
@@ -20061,10 +20071,10 @@ async function cmdCollectionSetIcon(options) {
20061
20071
  outputError(`Failed to download icon: ${response.status}`, response.status);
20062
20072
  }
20063
20073
  const filePath = getTempFilePath("raindrop_icon", ".png");
20064
- await Bun.write(filePath, await response.arrayBuffer());
20074
+ await writeFile2(filePath, Buffer.from(await response.arrayBuffer()));
20065
20075
  stopSpinner(spinner, true, "Downloaded");
20066
20076
  const result = await withSpinner("Uploading icon...", () => api2.uploadCollectionCover(id, filePath));
20067
- await Bun.$`rm -f ${filePath}`;
20077
+ await rm2(filePath, { force: true });
20068
20078
  output(result, options.format);
20069
20079
  }
20070
20080
  // src/commands/overview.ts
@@ -20254,7 +20264,17 @@ async function cmdTagRename(options) {
20254
20264
  output({ success: success3 }, options.format);
20255
20265
  }
20256
20266
  // src/index.ts
20257
- var VERSION = "0.1.0";
20267
+ async function getPackageVersion() {
20268
+ try {
20269
+ const text = await readFile3(new URL("../package.json", import.meta.url), "utf-8");
20270
+ const pkg = JSON.parse(text);
20271
+ if (pkg && typeof pkg === "object" && typeof pkg.version === "string" && pkg.version.trim().length > 0) {
20272
+ return pkg.version;
20273
+ }
20274
+ } catch {}
20275
+ return "0.0.0";
20276
+ }
20277
+ var VERSION = await getPackageVersion();
20258
20278
  function getGlobalOptions(cmd) {
20259
20279
  const opts = cmd.optsWithGlobals();
20260
20280
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kyoji2/raindrop-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "AI-native CLI for Raindrop.io",
5
5
  "author": "kyoji2",
6
6
  "repository": {
package/src/api/client.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { Blob } from "node:buffer";
2
+ import { readFile } from "node:fs/promises";
3
+ import { basename } from "node:path";
4
+ import { setTimeout as delay } from "node:timers/promises";
1
5
  import type { z } from "zod";
2
6
  import {
3
7
  CollectionResponseSchema,
@@ -181,7 +185,7 @@ export class RaindropAPI {
181
185
  this.logger.warn(
182
186
  `[${method} ${path}] Rate limited. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`,
183
187
  );
184
- await Bun.sleep(backoff);
188
+ await delay(backoff);
185
189
  continue;
186
190
  }
187
191
 
@@ -199,7 +203,7 @@ export class RaindropAPI {
199
203
  this.logger.warn(
200
204
  `[${method} ${path}] Server error ${response.status}. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`,
201
205
  );
202
- await Bun.sleep(backoff);
206
+ await delay(backoff);
203
207
  continue;
204
208
  }
205
209
 
@@ -245,7 +249,7 @@ export class RaindropAPI {
245
249
  this.logger.warn(
246
250
  `[${method} ${path}] Network error. Retrying in ${Math.round(backoff / 1000)}s (attempt ${attempt}/${RaindropAPI.MAX_RETRIES})`,
247
251
  );
248
- await Bun.sleep(backoff);
252
+ await delay(backoff);
249
253
  }
250
254
  }
251
255
 
@@ -355,9 +359,9 @@ export class RaindropAPI {
355
359
  return { _id: id, title: "Dry Run Icon", count: 0 };
356
360
  }
357
361
 
358
- const file = Bun.file(filePath);
362
+ const fileBuffer = await readFile(filePath);
359
363
  const formData = new FormData();
360
- formData.append("cover", file);
364
+ formData.append("cover", new Blob([fileBuffer]), basename(filePath));
361
365
 
362
366
  const response = await fetch(`${RaindropAPI.BASE_URL}/collection/${id}/cover`, {
363
367
  method: "PUT",
@@ -1,3 +1,4 @@
1
+ import { rm, writeFile } from "node:fs/promises";
1
2
  import type { CollectionCreate, CollectionUpdate } from "../api";
2
3
  import { type GlobalOptions, output, outputError } from "../utils/output";
3
4
  import { startSpinner, stopSpinner, withSpinner } from "../utils/spinner";
@@ -196,7 +197,7 @@ export async function cmdCollectionCover(options: CollectionCoverOptions): Promi
196
197
  outputError(`Failed to download image: ${response.status}`, response.status);
197
198
  }
198
199
  filePath = getTempFilePath("raindrop_cover", ".png");
199
- await Bun.write(filePath, await response.arrayBuffer());
200
+ await writeFile(filePath, Buffer.from(await response.arrayBuffer()));
200
201
  stopSpinner(spinner, true, "Downloaded");
201
202
  isTemp = true;
202
203
  }
@@ -204,7 +205,7 @@ export async function cmdCollectionCover(options: CollectionCoverOptions): Promi
204
205
  const result = await withSpinner("Uploading cover...", () => api.uploadCollectionCover(id, filePath));
205
206
 
206
207
  if (isTemp) {
207
- await Bun.$`rm -f ${filePath}`;
208
+ await rm(filePath, { force: true });
208
209
  }
209
210
 
210
211
  output(result, options.format);
@@ -246,12 +247,12 @@ export async function cmdCollectionSetIcon(options: CollectionSetIconOptions): P
246
247
  }
247
248
 
248
249
  const filePath = getTempFilePath("raindrop_icon", ".png");
249
- await Bun.write(filePath, await response.arrayBuffer());
250
+ await writeFile(filePath, Buffer.from(await response.arrayBuffer()));
250
251
  stopSpinner(spinner, true, "Downloaded");
251
252
 
252
253
  const result = await withSpinner("Uploading icon...", () => api.uploadCollectionCover(id, filePath));
253
254
 
254
- await Bun.$`rm -f ${filePath}`;
255
+ await rm(filePath, { force: true });
255
256
 
256
257
  output(result, options.format);
257
258
  }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
+ import { readFile } from "node:fs/promises";
3
4
  import { Command } from "commander";
4
5
  import { RaindropError } from "./api";
5
6
  import {
@@ -35,7 +36,20 @@ import {
35
36
  } from "./commands";
36
37
  import { CLIError, type GlobalOptions, type OutputFormat } from "./utils";
37
38
 
38
- const VERSION = "0.1.0";
39
+ async function getPackageVersion(): Promise<string> {
40
+ try {
41
+ const text = await readFile(new URL("../package.json", import.meta.url), "utf-8");
42
+ const pkg = JSON.parse(text) as { version?: unknown };
43
+
44
+ if (pkg && typeof pkg === "object" && typeof pkg.version === "string" && pkg.version.trim().length > 0) {
45
+ return pkg.version;
46
+ }
47
+ } catch {}
48
+
49
+ return "0.0.0";
50
+ }
51
+
52
+ const VERSION = await getPackageVersion();
39
53
 
40
54
  function getGlobalOptions(cmd: Command): GlobalOptions {
41
55
  const opts = cmd.optsWithGlobals();
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
3
  import { homedir } from "node:os";
3
4
  import { join } from "node:path";
4
5
 
@@ -22,13 +23,23 @@ function isValidConfig(data: unknown): data is Config {
22
23
  return typeof obj.token === "string" && obj.token.length > 0;
23
24
  }
24
25
 
26
+ function isNotFoundError(error: unknown): error is { code?: string } {
27
+ if (typeof error !== "object" || error === null || !("code" in error)) {
28
+ return false;
29
+ }
30
+ return (error as { code?: string }).code === "ENOENT";
31
+ }
32
+
25
33
  export async function loadConfig(): Promise<Config | null> {
26
34
  try {
27
- const file = Bun.file(CONFIG_FILE);
28
- const exists = await file.exists();
29
- if (!exists) return null;
35
+ let text = "";
36
+ try {
37
+ text = await readFile(CONFIG_FILE, "utf-8");
38
+ } catch (error) {
39
+ if (isNotFoundError(error)) return null;
40
+ throw error;
41
+ }
30
42
 
31
- const text = await file.text();
32
43
  if (!text.trim()) return null;
33
44
 
34
45
  let data: unknown;
@@ -74,16 +85,13 @@ export async function saveConfig(config: Config): Promise<void> {
74
85
  throw new ConfigError("Invalid token: token must be a non-empty string");
75
86
  }
76
87
 
77
- await Bun.$`mkdir -p ${CONFIG_DIR}`;
78
- await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
88
+ await mkdir(CONFIG_DIR, { recursive: true });
89
+ await writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
79
90
  }
80
91
 
81
92
  export async function deleteConfig(): Promise<void> {
82
93
  try {
83
- const file = Bun.file(CONFIG_FILE);
84
- if (await file.exists()) {
85
- await Bun.$`rm -f ${CONFIG_FILE}`;
86
- }
94
+ await rm(CONFIG_FILE, { force: true });
87
95
  } catch {}
88
96
  }
89
97