@shiori-sh/cli 0.3.0 → 0.5.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.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/dist/index.js +207 -80
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -46,8 +46,11 @@ shiori update <id> --read # Mark as read
46
46
  shiori update <id> --unread # Mark as unread
47
47
  shiori update <id> --title "..." # Update title
48
48
  shiori update <id> --restore # Restore from trash
49
+ shiori update --ids <id1,id2,...> --read # Bulk mark as read
50
+ shiori update --ids <id1,id2,...> --unread # Bulk mark as unread
49
51
 
50
52
  shiori delete <id> # Move to trash
53
+ shiori delete --ids <id1,id2,...> # Bulk move to trash
51
54
  shiori trash # List trashed links
52
55
  shiori trash --empty # Permanently delete all trash
53
56
 
@@ -58,7 +61,7 @@ shiori subscriptions remove <id> # Remove a subscription
58
61
  shiori subscriptions sync <id> # Sync a subscription now
59
62
  shiori subscriptions sync <id> --limit 5 # Sync with item limit
60
63
 
61
- shiori me # Show account info
64
+ shiori whoami # Show account info
62
65
  shiori auth --status # Check auth status
63
66
  shiori auth --logout # Remove stored credentials
64
67
  ```
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/commands/auth.ts
4
+ import { execFile } from "child_process";
5
+ import { hostname } from "os";
4
6
  import { createInterface } from "readline";
5
7
 
6
8
  // src/config.ts
@@ -87,16 +89,120 @@ function promptSecret(question) {
87
89
  stdin.on("data", onData);
88
90
  });
89
91
  }
92
+ function openUrl(url) {
93
+ const platform = process.platform;
94
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
95
+ execFile(cmd, [url]);
96
+ }
97
+ function sleep(ms) {
98
+ return new Promise((resolve) => setTimeout(resolve, ms));
99
+ }
100
+ async function browserAuth() {
101
+ const baseUrl = getBaseUrl();
102
+ process.stdout.write("Starting browser authentication... ");
103
+ const deviceName = hostname();
104
+ const startRes = await fetch(`${baseUrl}/api/auth/cli/start`, {
105
+ method: "POST",
106
+ headers: { "Content-Type": "application/json" },
107
+ body: JSON.stringify({ device_name: deviceName })
108
+ });
109
+ if (!startRes.ok) {
110
+ console.error("failed.");
111
+ console.error("Could not start browser auth. Try `shiori auth --api-key` instead.");
112
+ process.exit(1);
113
+ }
114
+ const { code, auth_url } = await startRes.json();
115
+ console.log("done.\n");
116
+ console.log(` Your code: ${code}
117
+ `);
118
+ console.log(` ${auth_url}
119
+ `);
120
+ await prompt("Press Enter to open in browser (or open the URL above manually)...");
121
+ openUrl(auth_url);
122
+ console.log("\nWaiting for authorization...");
123
+ const POLL_INTERVAL = 2e3;
124
+ const MAX_POLLS = 150;
125
+ let polls = 0;
126
+ while (polls < MAX_POLLS) {
127
+ await sleep(POLL_INTERVAL);
128
+ polls++;
129
+ try {
130
+ const pollRes = await fetch(`${baseUrl}/api/auth/cli/poll?code=${code}`);
131
+ if (!pollRes.ok) {
132
+ console.error("\nAuthorization failed. The code may have expired.");
133
+ process.exit(1);
134
+ }
135
+ const data = await pollRes.json();
136
+ if (data.status === "approved" && data.api_key) {
137
+ const meRes = await fetch(`${baseUrl}/api/user/me`, {
138
+ headers: { Authorization: `Bearer ${data.api_key}` }
139
+ });
140
+ if (!meRes.ok) {
141
+ console.error("\nReceived API key but verification failed.");
142
+ process.exit(1);
143
+ }
144
+ const meData = await meRes.json();
145
+ writeConfig({ ...readConfig(), api_key: data.api_key });
146
+ console.log(`
147
+ Authenticated as ${meData.user?.full_name || meData.user?.id}`);
148
+ console.log("API key saved to ~/.shiori/config.json");
149
+ return;
150
+ }
151
+ if (data.status === "expired") {
152
+ console.error("\nAuthorization code expired. Please try again.");
153
+ process.exit(1);
154
+ }
155
+ if (data.status === "consumed") {
156
+ console.error("\nAuthorization code already used. Please try again.");
157
+ process.exit(1);
158
+ }
159
+ } catch {
160
+ }
161
+ }
162
+ console.error("\nTimed out waiting for authorization. Please try again.");
163
+ process.exit(1);
164
+ }
165
+ async function apiKeyAuth() {
166
+ console.log("\nTo authenticate, you need an API key from Shiori.\n");
167
+ console.log(" 1. Open https://www.shiori.sh/home -> Settings");
168
+ console.log(" 2. Create and copy your API key");
169
+ console.log(" 3. Paste it below\n");
170
+ const key = await promptSecret("API key: ");
171
+ const trimmed = key.trim();
172
+ if (!trimmed.startsWith("shk_")) {
173
+ console.error("\nInvalid API key format. Keys start with 'shk_'.");
174
+ process.exit(1);
175
+ }
176
+ process.stdout.write("\nVerifying... ");
177
+ const baseUrl = getBaseUrl();
178
+ const res = await fetch(`${baseUrl}/api/user/me`, {
179
+ headers: { Authorization: `Bearer ${trimmed}` }
180
+ });
181
+ if (!res.ok) {
182
+ console.error("failed.\nAuthentication failed. Check your API key and try again.");
183
+ process.exit(1);
184
+ }
185
+ const data = await res.json();
186
+ writeConfig({ ...readConfig(), api_key: trimmed });
187
+ console.log(`done.
188
+
189
+ Authenticated as ${data.user?.full_name || data.user?.id}`);
190
+ console.log("API key saved to ~/.shiori/config.json");
191
+ }
90
192
  async function run(args) {
91
193
  if (args.includes("--help") || args.includes("-h")) {
92
- console.log(`shiori auth - Authenticate with your Shiori API key
194
+ console.log(`shiori auth - Authenticate with Shiori
93
195
 
94
196
  Usage: shiori auth [options]
95
197
 
96
198
  Options:
199
+ --api-key Authenticate by pasting an API key manually
97
200
  --status Show current authentication status
98
201
  --logout Remove stored credentials
99
- --help, -h Show this help`);
202
+ --help, -h Show this help
203
+
204
+ By default, opens your browser for a quick sign-in.
205
+ Use --api-key if you prefer to paste a key manually or are on a headless machine.`);
100
206
  return;
101
207
  }
102
208
  if (args.includes("--status")) {
@@ -122,31 +228,11 @@ Options:
122
228
  const answer = await prompt("You are already authenticated. Replace existing key? (y/N) ");
123
229
  if (answer.trim().toLowerCase() !== "y") return;
124
230
  }
125
- console.log("\nTo authenticate, you need an API key from Shiori.\n");
126
- console.log(" 1. Open https://www.shiori.sh/home -> Settings");
127
- console.log(" 2. Create and copy your API key");
128
- console.log(" 3. Paste it below\n");
129
- const key = await promptSecret("API key: ");
130
- const trimmed = key.trim();
131
- if (!trimmed.startsWith("shk_")) {
132
- console.error("\nInvalid API key format. Keys start with 'shk_'.");
133
- process.exit(1);
134
- }
135
- process.stdout.write("\nVerifying... ");
136
- const baseUrl = getBaseUrl();
137
- const res = await fetch(`${baseUrl}/api/user/me`, {
138
- headers: { Authorization: `Bearer ${trimmed}` }
139
- });
140
- if (!res.ok) {
141
- console.error("failed.\nAuthentication failed. Check your API key and try again.");
142
- process.exit(1);
231
+ if (args.includes("--api-key")) {
232
+ await apiKeyAuth();
233
+ } else {
234
+ await browserAuth();
143
235
  }
144
- const data = await res.json();
145
- writeConfig({ ...readConfig(), api_key: trimmed });
146
- console.log(`done.
147
-
148
- Authenticated as ${data.user?.full_name || data.user?.id}`);
149
- console.log("API key saved to ~/.shiori/config.json");
150
236
  }
151
237
 
152
238
  // src/api.ts
@@ -204,7 +290,8 @@ function getPositional(args) {
204
290
  "--title",
205
291
  "--summary",
206
292
  "--since",
207
- "--format"
293
+ "--format",
294
+ "--ids"
208
295
  ].includes(args[i])) {
209
296
  i++;
210
297
  }
@@ -275,18 +362,35 @@ Options:
275
362
  // src/commands/delete.ts
276
363
  async function run3(args) {
277
364
  if (hasFlag(args, "--help", "-h")) {
278
- console.log(`shiori delete - Delete a link (move to trash)
365
+ console.log(`shiori delete - Delete links (move to trash)
279
366
 
280
367
  Usage: shiori delete <id> [options]
368
+ shiori delete --ids <id1,id2,...> [options]
281
369
 
282
370
  Options:
283
- --json Output raw JSON
284
- --help, -h Show this help`);
371
+ --ids <ids> Comma-separated list of link IDs for bulk delete
372
+ --json Output raw JSON
373
+ --help, -h Show this help`);
374
+ return;
375
+ }
376
+ const idsFlag = getFlag(args, "--ids");
377
+ if (idsFlag) {
378
+ const ids = idsFlag.split(",").map((id2) => id2.trim()).filter(Boolean);
379
+ if (ids.length === 0) {
380
+ console.error("No valid IDs provided.");
381
+ process.exit(1);
382
+ }
383
+ const { data: data2 } = await api("PATCH", "/api/links", { ids, deleted: true });
384
+ if (hasFlag(args, "--json")) {
385
+ console.log(JSON.stringify(data2, null, 2));
386
+ return;
387
+ }
388
+ console.log(`Deleted ${data2.updated} link(s).`);
285
389
  return;
286
390
  }
287
391
  const id = getPositional(args);
288
392
  if (!id) {
289
- console.error("Usage: shiori delete <id>");
393
+ console.error("Usage: shiori delete <id> or shiori delete --ids <id1,id2,...>");
290
394
  process.exit(1);
291
395
  }
292
396
  const { data } = await api("DELETE", `/api/links/${id}`);
@@ -389,36 +493,8 @@ Links: ${data.links.length} of ${data.total} total
389
493
  console.log();
390
494
  }
391
495
 
392
- // src/commands/me.ts
393
- async function run6(args) {
394
- if (hasFlag(args, "--help", "-h")) {
395
- console.log(`shiori me - Show current user info
396
-
397
- Usage: shiori me [options]
398
-
399
- Options:
400
- --json Output raw JSON
401
- --help, -h Show this help`);
402
- return;
403
- }
404
- const { data } = await api("GET", "/api/user/me");
405
- if (hasFlag(args, "--json")) {
406
- console.log(JSON.stringify(data, null, 2));
407
- return;
408
- }
409
- const user = data.user;
410
- console.log(`
411
- Name: ${user.full_name || "--"}`);
412
- const plan = user.subscription?.plan === "subscription" ? "Pro" : user.subscription?.plan || "Free";
413
- console.log(` Plan: ${plan}`);
414
- console.log(
415
- ` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`
416
- );
417
- console.log();
418
- }
419
-
420
496
  // src/commands/save.ts
421
- async function run7(args) {
497
+ async function run6(args) {
422
498
  if (hasFlag(args, "--help", "-h")) {
423
499
  console.log(`shiori save - Save a new link
424
500
 
@@ -453,7 +529,7 @@ Options:
453
529
  }
454
530
 
455
531
  // src/commands/search.ts
456
- async function run8(args) {
532
+ async function run7(args) {
457
533
  if (hasFlag(args, "--help", "-h")) {
458
534
  console.log(`shiori search - Search saved links
459
535
 
@@ -520,7 +596,7 @@ Examples:
520
596
  shiori subscriptions add https://example.com --sync
521
597
  shiori subscriptions remove <id>
522
598
  shiori subscriptions sync <id> --limit 5`;
523
- async function run9(args) {
599
+ async function run8(args) {
524
600
  if (hasFlag(args, "--help", "-h") && !args[0]) {
525
601
  console.log(HELP);
526
602
  return;
@@ -659,7 +735,7 @@ Options:
659
735
  }
660
736
 
661
737
  // src/commands/trash.ts
662
- async function run10(args) {
738
+ async function run9(args) {
663
739
  if (hasFlag(args, "--help", "-h")) {
664
740
  console.log(`shiori trash - List or empty the trash
665
741
 
@@ -710,22 +786,44 @@ Trashed links: ${data.links.length} of ${data.total} total
710
786
  }
711
787
 
712
788
  // src/commands/update.ts
713
- async function run11(args) {
789
+ async function run10(args) {
714
790
  if (hasFlag(args, "--help", "-h")) {
715
- console.log(`shiori update - Update a link
791
+ console.log(`shiori update - Update links
716
792
 
717
793
  Usage: shiori update <id> [options]
794
+ shiori update --ids <id1,id2,...> --read|--unread [options]
718
795
 
719
796
  Options:
797
+ --ids <ids> Comma-separated list of link IDs for bulk update
720
798
  --read Mark as read
721
799
  --unread Mark as unread
722
- --title <title> Update title
723
- --summary <text> Update summary
724
- --restore Restore from trash
800
+ --title <title> Update title (single link only)
801
+ --summary <text> Update summary (single link only)
802
+ --restore Restore from trash (single link only)
725
803
  --json Output raw JSON
726
804
  --help, -h Show this help`);
727
805
  return;
728
806
  }
807
+ const idsFlag = getFlag(args, "--ids");
808
+ if (idsFlag) {
809
+ const ids = idsFlag.split(",").map((id2) => id2.trim()).filter(Boolean);
810
+ if (ids.length === 0) {
811
+ console.error("No valid IDs provided.");
812
+ process.exit(1);
813
+ }
814
+ if (!hasFlag(args, "--read") && !hasFlag(args, "--unread")) {
815
+ console.error("Bulk update requires --read or --unread.");
816
+ process.exit(1);
817
+ }
818
+ const read = hasFlag(args, "--read");
819
+ const { data: data2 } = await api("PATCH", "/api/links", { ids, read });
820
+ if (hasFlag(args, "--json")) {
821
+ console.log(JSON.stringify(data2, null, 2));
822
+ return;
823
+ }
824
+ console.log(`Updated ${data2.updated} link(s).`);
825
+ return;
826
+ }
729
827
  const id = getPositional(args);
730
828
  if (!id) {
731
829
  console.error("Usage: shiori update <id> [--read | --unread | --title <title> | --restore]");
@@ -759,6 +857,34 @@ Options:
759
857
  console.log(data.message || "Updated.");
760
858
  }
761
859
 
860
+ // src/commands/whoami.ts
861
+ async function run11(args) {
862
+ if (hasFlag(args, "--help", "-h")) {
863
+ console.log(`shiori whoami - Show current user info
864
+
865
+ Usage: shiori whoami [options]
866
+
867
+ Options:
868
+ --json Output raw JSON
869
+ --help, -h Show this help`);
870
+ return;
871
+ }
872
+ const { data } = await api("GET", "/api/user/me");
873
+ if (hasFlag(args, "--json")) {
874
+ console.log(JSON.stringify(data, null, 2));
875
+ return;
876
+ }
877
+ const user = data.user;
878
+ console.log(`
879
+ Name: ${user.full_name || "--"}`);
880
+ const plan = user.subscription?.plan === "subscription" ? "Pro" : user.subscription?.plan || "Free";
881
+ console.log(` Plan: ${plan}`);
882
+ console.log(
883
+ ` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`
884
+ );
885
+ console.log();
886
+ }
887
+
762
888
  // src/error-report.ts
763
889
  function reportError(command, error) {
764
890
  const raw = error instanceof Error ? error.message : String(error);
@@ -768,7 +894,7 @@ function reportError(command, error) {
768
894
  method: "POST",
769
895
  headers: { "Content-Type": "application/json" },
770
896
  body: JSON.stringify({
771
- version: "0.3.0",
897
+ version: "0.5.0",
772
898
  command,
773
899
  error: message,
774
900
  platform: process.platform
@@ -808,17 +934,17 @@ async function checkForUpdate(currentVersion, isJson) {
808
934
 
809
935
  // src/index.ts
810
936
  var COMMANDS = {
811
- auth: { run, desc: "Authenticate with your Shiori API key" },
937
+ auth: { run, desc: "Authenticate with Shiori (browser or API key)" },
812
938
  list: { run: run5, desc: "List saved links" },
813
- search: { run: run8, desc: "Search saved links" },
939
+ search: { run: run7, desc: "Search saved links" },
814
940
  get: { run: run4, desc: "Get a link by ID (includes content)" },
815
941
  content: { run: run2, desc: "Print link content as markdown" },
816
- save: { run: run7, desc: "Save a new link" },
817
- update: { run: run11, desc: "Update a link" },
942
+ save: { run: run6, desc: "Save a new link" },
943
+ update: { run: run10, desc: "Update a link" },
818
944
  delete: { run: run3, desc: "Delete a link (move to trash)" },
819
- trash: { run: run10, desc: "List or empty the trash" },
820
- subscriptions: { run: run9, desc: "Manage RSS subscriptions" },
821
- me: { run: run6, desc: "Show current user info" }
945
+ trash: { run: run9, desc: "List or empty the trash" },
946
+ subscriptions: { run: run8, desc: "Manage RSS subscriptions" },
947
+ whoami: { run: run11, desc: "Show current user info" }
822
948
  };
823
949
  function printHelp() {
824
950
  console.log(`shiori - Manage your Shiori link library from the terminal
@@ -833,11 +959,12 @@ Options:
833
959
  --version, -v Show version
834
960
 
835
961
  Get started:
836
- shiori auth Authenticate with your API key
962
+ shiori auth Sign in via your browser
837
963
  shiori list List your recent links
838
964
  shiori save <url> Save a new link
839
965
  shiori search <query> Search your links
840
- shiori content <id> Print markdown content (pipe to other tools)`);
966
+ shiori content <id> Print markdown content (pipe to other tools)
967
+ shiori whoami Show your account info`);
841
968
  }
842
969
  async function main() {
843
970
  const args = process.argv.slice(2);
@@ -847,7 +974,7 @@ async function main() {
847
974
  return;
848
975
  }
849
976
  if (command === "--version" || command === "-v") {
850
- console.log("0.3.0");
977
+ console.log("0.5.0");
851
978
  return;
852
979
  }
853
980
  const cmd = COMMANDS[command];
@@ -860,7 +987,7 @@ async function main() {
860
987
  const cmdArgs = args.slice(1);
861
988
  const isJson = cmdArgs.includes("--json");
862
989
  await cmd.run(cmdArgs);
863
- checkForUpdate("0.3.0", isJson).catch(() => {
990
+ checkForUpdate("0.5.0", isJson).catch(() => {
864
991
  });
865
992
  }
866
993
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shiori-sh/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for managing your Shiori link library",
5
5
  "author": "Brian Lovin",
6
6
  "license": "MIT",