@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.
- package/README.md +7 -0
- package/dist/index.js +291 -38
- 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
|
|
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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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("--") && [
|
|
200
|
-
|
|
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/
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
903
|
+
update: { run: run11, desc: "Update a link" },
|
|
652
904
|
delete: { run: run3, desc: "Delete a link (move to trash)" },
|
|
653
|
-
trash: { run:
|
|
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
|
|
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.
|
|
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.
|
|
949
|
+
checkForUpdate("0.4.0", isJson).catch(() => {
|
|
697
950
|
});
|
|
698
951
|
}
|
|
699
952
|
main().catch((err) => {
|