@shiori-sh/cli 0.2.3 → 0.4.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 +7 -0
  2. package/dist/index.js +291 -38
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -51,6 +51,13 @@ shiori delete <id> # Move to trash
51
51
  shiori trash # List trashed links
52
52
  shiori trash --empty # Permanently delete all trash
53
53
 
54
+ shiori subscriptions list # List RSS subscriptions
55
+ shiori subscriptions add <url> # Subscribe to an RSS feed
56
+ shiori subscriptions add <url> --sync # Subscribe and sync recent items
57
+ shiori subscriptions remove <id> # Remove a subscription
58
+ shiori subscriptions sync <id> # Sync a subscription now
59
+ shiori subscriptions sync <id> --limit 5 # Sync with item limit
60
+
54
61
  shiori me # Show account info
55
62
  shiori auth --status # Check auth status
56
63
  shiori auth --logout # Remove stored credentials
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 { exec } 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
+ exec(`${cmd} ${JSON.stringify(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
@@ -196,9 +282,16 @@ function hasFlag(args, ...names) {
196
282
  function getPositional(args) {
197
283
  for (let i = 0; i < args.length; i++) {
198
284
  if (args[i].startsWith("--")) {
199
- if (i + 1 < args.length && !args[i + 1].startsWith("--") && ["--limit", "--offset", "--sort", "--read", "--title", "--summary", "--since"].includes(
200
- args[i]
201
- )) {
285
+ if (i + 1 < args.length && !args[i + 1].startsWith("--") && [
286
+ "--limit",
287
+ "--offset",
288
+ "--sort",
289
+ "--read",
290
+ "--title",
291
+ "--summary",
292
+ "--since",
293
+ "--format"
294
+ ].includes(args[i])) {
202
295
  i++;
203
296
  }
204
297
  continue;
@@ -492,8 +585,167 @@ Results: ${data.links.length} of ${data.total} matches
492
585
  console.log();
493
586
  }
494
587
 
495
- // src/commands/trash.ts
588
+ // src/commands/subscriptions.ts
589
+ var HELP = `shiori subscriptions - Manage RSS subscriptions
590
+
591
+ Usage: shiori subscriptions <subcommand> [options]
592
+
593
+ Subcommands:
594
+ list List your subscriptions
595
+ add <url> Subscribe to an RSS feed
596
+ remove <id> Remove a subscription
597
+ sync <id> Sync a subscription now
598
+
599
+ Options:
600
+ --json Output raw JSON
601
+ --help, -h Show this help
602
+
603
+ Examples:
604
+ shiori subscriptions list
605
+ shiori subscriptions add https://example.com/feed.xml
606
+ shiori subscriptions add https://example.com --sync
607
+ shiori subscriptions remove <id>
608
+ shiori subscriptions sync <id> --limit 5`;
496
609
  async function run9(args) {
610
+ if (hasFlag(args, "--help", "-h") && !args[0]) {
611
+ console.log(HELP);
612
+ return;
613
+ }
614
+ const subcommand = args[0];
615
+ const subArgs = args.slice(1);
616
+ switch (subcommand) {
617
+ case "list":
618
+ case "ls":
619
+ return listSubscriptions(subArgs);
620
+ case "add":
621
+ return addSubscription(subArgs);
622
+ case "remove":
623
+ case "rm":
624
+ return removeSubscription(subArgs);
625
+ case "sync":
626
+ return syncSubscription(subArgs);
627
+ default:
628
+ if (subcommand) {
629
+ console.error(`Unknown subcommand: ${subcommand}
630
+ `);
631
+ }
632
+ console.log(HELP);
633
+ if (subcommand) process.exit(1);
634
+ }
635
+ }
636
+ async function listSubscriptions(args) {
637
+ if (hasFlag(args, "--help", "-h")) {
638
+ console.log(`shiori subscriptions list - List your RSS subscriptions
639
+
640
+ Usage: shiori subscriptions list [options]
641
+
642
+ Options:
643
+ --json Output raw JSON
644
+ --help, -h Show this help`);
645
+ return;
646
+ }
647
+ const { data } = await api("GET", "/api/subscriptions/");
648
+ if (hasFlag(args, "--json")) {
649
+ console.log(JSON.stringify(data, null, 2));
650
+ return;
651
+ }
652
+ const subs = data.subscriptions;
653
+ if (subs.length === 0) {
654
+ console.log("\n No subscriptions. Add one with: shiori subscriptions add <url>\n");
655
+ return;
656
+ }
657
+ console.log(`
658
+ Subscriptions: ${subs.length}
659
+ `);
660
+ for (const sub of subs) {
661
+ const title = truncate(sub.title || sub.feed_url, 50);
662
+ const synced = sub.last_synced_at ? `synced ${formatDate(sub.last_synced_at)}` : "never synced";
663
+ console.log(` ${sub.id} ${title} ${synced}`);
664
+ }
665
+ console.log();
666
+ }
667
+ async function addSubscription(args) {
668
+ if (hasFlag(args, "--help", "-h")) {
669
+ console.log(`shiori subscriptions add - Subscribe to an RSS feed
670
+
671
+ Usage: shiori subscriptions add <url> [options]
672
+
673
+ Options:
674
+ --sync Sync the 3 most recent items immediately
675
+ --json Output raw JSON
676
+ --help, -h Show this help`);
677
+ return;
678
+ }
679
+ const url = getPositional(args);
680
+ if (!url) {
681
+ console.error("Usage: shiori subscriptions add <url>");
682
+ process.exit(1);
683
+ }
684
+ const body = { feedUrl: url };
685
+ if (hasFlag(args, "--sync")) {
686
+ body.initialSync = true;
687
+ }
688
+ const { data } = await api("POST", "/api/subscriptions/", body);
689
+ if (hasFlag(args, "--json")) {
690
+ console.log(JSON.stringify(data, null, 2));
691
+ return;
692
+ }
693
+ const sub = data.subscription;
694
+ console.log(`Subscribed: ${sub.title || sub.feed_url} (${sub.id})`);
695
+ }
696
+ async function removeSubscription(args) {
697
+ if (hasFlag(args, "--help", "-h")) {
698
+ console.log(`shiori subscriptions remove - Remove a subscription
699
+
700
+ Usage: shiori subscriptions remove <id> [options]
701
+
702
+ Options:
703
+ --json Output raw JSON
704
+ --help, -h Show this help`);
705
+ return;
706
+ }
707
+ const id = getPositional(args);
708
+ if (!id) {
709
+ console.error("Usage: shiori subscriptions remove <id>");
710
+ process.exit(1);
711
+ }
712
+ const { data } = await api("DELETE", `/api/subscriptions/${id}`);
713
+ if (hasFlag(args, "--json")) {
714
+ console.log(JSON.stringify(data, null, 2));
715
+ return;
716
+ }
717
+ console.log("Subscription removed.");
718
+ }
719
+ async function syncSubscription(args) {
720
+ if (hasFlag(args, "--help", "-h")) {
721
+ console.log(`shiori subscriptions sync - Sync a subscription now
722
+
723
+ Usage: shiori subscriptions sync <id> [options]
724
+
725
+ Options:
726
+ --limit <n> Max items to sync (1-100)
727
+ --json Output raw JSON
728
+ --help, -h Show this help`);
729
+ return;
730
+ }
731
+ const id = getPositional(args);
732
+ if (!id) {
733
+ console.error("Usage: shiori subscriptions sync <id>");
734
+ process.exit(1);
735
+ }
736
+ const body = {};
737
+ const limit = getFlag(args, "--limit");
738
+ if (limit) body.limit = Number(limit);
739
+ const { data } = await api("POST", `/api/subscriptions/sync/${id}`, body);
740
+ if (hasFlag(args, "--json")) {
741
+ console.log(JSON.stringify(data, null, 2));
742
+ return;
743
+ }
744
+ console.log(`Synced: ${data.newItems} new, ${data.skipped} skipped, ${data.errors} errors`);
745
+ }
746
+
747
+ // src/commands/trash.ts
748
+ async function run10(args) {
497
749
  if (hasFlag(args, "--help", "-h")) {
498
750
  console.log(`shiori trash - List or empty the trash
499
751
 
@@ -544,7 +796,7 @@ Trashed links: ${data.links.length} of ${data.total} total
544
796
  }
545
797
 
546
798
  // src/commands/update.ts
547
- async function run10(args) {
799
+ async function run11(args) {
548
800
  if (hasFlag(args, "--help", "-h")) {
549
801
  console.log(`shiori update - Update a link
550
802
 
@@ -602,7 +854,7 @@ function reportError(command, error) {
602
854
  method: "POST",
603
855
  headers: { "Content-Type": "application/json" },
604
856
  body: JSON.stringify({
605
- version: "0.2.3",
857
+ version: "0.4.0",
606
858
  command,
607
859
  error: message,
608
860
  platform: process.platform
@@ -642,15 +894,16 @@ async function checkForUpdate(currentVersion, isJson) {
642
894
 
643
895
  // src/index.ts
644
896
  var COMMANDS = {
645
- auth: { run, desc: "Authenticate with your Shiori API key" },
897
+ auth: { run, desc: "Authenticate with Shiori (browser or API key)" },
646
898
  list: { run: run5, desc: "List saved links" },
647
899
  search: { run: run8, desc: "Search saved links" },
648
900
  get: { run: run4, desc: "Get a link by ID (includes content)" },
649
901
  content: { run: run2, desc: "Print link content as markdown" },
650
902
  save: { run: run7, desc: "Save a new link" },
651
- update: { run: run10, desc: "Update a link" },
903
+ update: { run: run11, desc: "Update a link" },
652
904
  delete: { run: run3, desc: "Delete a link (move to trash)" },
653
- trash: { run: run9, desc: "List or empty the trash" },
905
+ trash: { run: run10, desc: "List or empty the trash" },
906
+ subscriptions: { run: run9, desc: "Manage RSS subscriptions" },
654
907
  me: { run: run6, desc: "Show current user info" }
655
908
  };
656
909
  function printHelp() {
@@ -666,7 +919,7 @@ Options:
666
919
  --version, -v Show version
667
920
 
668
921
  Get started:
669
- shiori auth Authenticate with your API key
922
+ shiori auth Sign in via your browser
670
923
  shiori list List your recent links
671
924
  shiori save <url> Save a new link
672
925
  shiori search <query> Search your links
@@ -680,7 +933,7 @@ async function main() {
680
933
  return;
681
934
  }
682
935
  if (command === "--version" || command === "-v") {
683
- console.log("0.2.3");
936
+ console.log("0.4.0");
684
937
  return;
685
938
  }
686
939
  const cmd = COMMANDS[command];
@@ -693,7 +946,7 @@ async function main() {
693
946
  const cmdArgs = args.slice(1);
694
947
  const isJson = cmdArgs.includes("--json");
695
948
  await cmd.run(cmdArgs);
696
- checkForUpdate("0.2.3", isJson).catch(() => {
949
+ checkForUpdate("0.4.0", isJson).catch(() => {
697
950
  });
698
951
  }
699
952
  main().catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shiori-sh/cli",
3
- "version": "0.2.3",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for managing your Shiori link library",
5
5
  "author": "Brian Lovin",
6
6
  "license": "MIT",