@shiori-sh/cli 0.1.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 +63 -0
- package/dist/index.js +594 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @shiori-sh/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for [Shiori](https://www.shiori.sh) — save, organize, and manage your links from the terminal.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @shiori-sh/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or use without installing:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx @shiori-sh/cli list
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Authenticate
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
shiori auth
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
This saves your API key to `~/.shiori/config.json`. Generate an API key at [shiori.sh/home](https://www.shiori.sh/home) → Settings.
|
|
24
|
+
|
|
25
|
+
You can also set `SHIORI_API_KEY` as an environment variable.
|
|
26
|
+
|
|
27
|
+
## Commands
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
shiori list # List recent links
|
|
31
|
+
shiori list --read unread # List unread links
|
|
32
|
+
shiori list --limit 5 --sort oldest # Paginate and sort
|
|
33
|
+
|
|
34
|
+
shiori get <id> # Get a link with full content
|
|
35
|
+
shiori save <url> # Save a new link
|
|
36
|
+
shiori save <url> --title "..." # Save with custom title
|
|
37
|
+
|
|
38
|
+
shiori update <id> --read # Mark as read
|
|
39
|
+
shiori update <id> --unread # Mark as unread
|
|
40
|
+
shiori update <id> --title "..." # Update title
|
|
41
|
+
shiori update <id> --restore # Restore from trash
|
|
42
|
+
|
|
43
|
+
shiori delete <id> # Move to trash
|
|
44
|
+
shiori trash # List trashed links
|
|
45
|
+
shiori trash --empty # Permanently delete all trash
|
|
46
|
+
|
|
47
|
+
shiori me # Show account info
|
|
48
|
+
shiori auth --status # Check auth status
|
|
49
|
+
shiori auth --logout # Remove stored credentials
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Add `--json` to any command for machine-readable output.
|
|
53
|
+
|
|
54
|
+
## Environment Variables
|
|
55
|
+
|
|
56
|
+
| Variable | Description |
|
|
57
|
+
| ----------------- | -------------------------------------------------- |
|
|
58
|
+
| `SHIORI_API_KEY` | API key (overrides config file) |
|
|
59
|
+
| `SHIORI_BASE_URL` | Custom base URL (default: `https://www.shiori.sh`) |
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/commands/auth.ts
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
|
|
6
|
+
// src/config.ts
|
|
7
|
+
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
import { join } from "path";
|
|
10
|
+
var CONFIG_DIR = join(homedir(), ".shiori");
|
|
11
|
+
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
12
|
+
function readConfig() {
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
15
|
+
} catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function writeConfig(config) {
|
|
20
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
21
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2) + "\n", {
|
|
22
|
+
mode: 384
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function getApiKey() {
|
|
26
|
+
const envKey = process.env.SHIORI_API_KEY;
|
|
27
|
+
if (envKey) return envKey;
|
|
28
|
+
const config = readConfig();
|
|
29
|
+
if (config.api_key) return config.api_key;
|
|
30
|
+
console.error(
|
|
31
|
+
"Not authenticated. Run `shiori auth` to set up your API key."
|
|
32
|
+
);
|
|
33
|
+
console.error("Or set the SHIORI_API_KEY environment variable.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
function getBaseUrl() {
|
|
37
|
+
return process.env.SHIORI_BASE_URL || readConfig().base_url || "https://www.shiori.sh";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/commands/auth.ts
|
|
41
|
+
function prompt(question) {
|
|
42
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
43
|
+
return new Promise((resolve) => {
|
|
44
|
+
rl.question(question, (answer) => {
|
|
45
|
+
rl.close();
|
|
46
|
+
resolve(answer);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function promptSecret(question) {
|
|
51
|
+
return new Promise((resolve) => {
|
|
52
|
+
process.stdout.write(question);
|
|
53
|
+
const { stdin } = process;
|
|
54
|
+
stdin.setRawMode(true);
|
|
55
|
+
stdin.resume();
|
|
56
|
+
stdin.setEncoding("utf8");
|
|
57
|
+
let input = "";
|
|
58
|
+
let displayed = 0;
|
|
59
|
+
const MAX_STARS = 10;
|
|
60
|
+
const onData = (char) => {
|
|
61
|
+
if (char === "\r" || char === "\n") {
|
|
62
|
+
stdin.setRawMode(false);
|
|
63
|
+
stdin.pause();
|
|
64
|
+
stdin.removeListener("data", onData);
|
|
65
|
+
process.stdout.write("\n");
|
|
66
|
+
resolve(input);
|
|
67
|
+
} else if (char === "") {
|
|
68
|
+
stdin.setRawMode(false);
|
|
69
|
+
stdin.pause();
|
|
70
|
+
process.stdout.write("\n");
|
|
71
|
+
process.exit(1);
|
|
72
|
+
} else if (char === "\x7F" || char === "\b") {
|
|
73
|
+
if (input.length > 0) {
|
|
74
|
+
input = input.slice(0, -1);
|
|
75
|
+
if (displayed > 0) {
|
|
76
|
+
displayed--;
|
|
77
|
+
process.stdout.write("\b \b");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
input += char;
|
|
82
|
+
const toShow = Math.min(char.length, MAX_STARS - displayed);
|
|
83
|
+
if (toShow > 0) {
|
|
84
|
+
process.stdout.write("*".repeat(toShow));
|
|
85
|
+
displayed += toShow;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
stdin.on("data", onData);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
async function run(args) {
|
|
93
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
94
|
+
console.log(`shiori auth - Authenticate with your Shiori API key
|
|
95
|
+
|
|
96
|
+
Usage: shiori auth [options]
|
|
97
|
+
|
|
98
|
+
Options:
|
|
99
|
+
--status Show current authentication status
|
|
100
|
+
--logout Remove stored credentials
|
|
101
|
+
--help, -h Show this help`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (args.includes("--status")) {
|
|
105
|
+
const config2 = readConfig();
|
|
106
|
+
if (config2.api_key) {
|
|
107
|
+
const prefix = config2.api_key.slice(0, 8) + "...";
|
|
108
|
+
console.log(`Authenticated with key: ${prefix}`);
|
|
109
|
+
if (config2.base_url) console.log(`Base URL: ${config2.base_url}`);
|
|
110
|
+
} else if (process.env.SHIORI_API_KEY) {
|
|
111
|
+
console.log("Authenticated via SHIORI_API_KEY environment variable.");
|
|
112
|
+
} else {
|
|
113
|
+
console.log("Not authenticated. Run `shiori auth` to set up.");
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (args.includes("--logout")) {
|
|
118
|
+
writeConfig({});
|
|
119
|
+
console.log("Credentials removed from ~/.shiori/config.json");
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const config = readConfig();
|
|
123
|
+
if (config.api_key) {
|
|
124
|
+
const answer = await prompt("You are already authenticated. Replace existing key? (y/N) ");
|
|
125
|
+
if (answer.trim().toLowerCase() !== "y") return;
|
|
126
|
+
}
|
|
127
|
+
console.log("\nTo authenticate, you need an API key from Shiori.\n");
|
|
128
|
+
console.log(" 1. Open https://www.shiori.sh/home -> Settings");
|
|
129
|
+
console.log(" 2. Create and copy your API key");
|
|
130
|
+
console.log(" 3. Paste it below\n");
|
|
131
|
+
const key = await promptSecret("API key: ");
|
|
132
|
+
const trimmed = key.trim();
|
|
133
|
+
if (!trimmed.startsWith("shk_")) {
|
|
134
|
+
console.error("\nInvalid API key format. Keys start with 'shk_'.");
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
process.stdout.write("\nVerifying... ");
|
|
138
|
+
const baseUrl = getBaseUrl();
|
|
139
|
+
const res = await fetch(`${baseUrl}/api/user/me`, {
|
|
140
|
+
headers: { Authorization: `Bearer ${trimmed}` }
|
|
141
|
+
});
|
|
142
|
+
if (!res.ok) {
|
|
143
|
+
console.error("failed.\nAuthentication failed. Check your API key and try again.");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
const data = await res.json();
|
|
147
|
+
writeConfig({ ...readConfig(), api_key: trimmed });
|
|
148
|
+
console.log(`done.
|
|
149
|
+
|
|
150
|
+
Authenticated as ${data.user?.full_name || data.user?.id}`);
|
|
151
|
+
console.log("API key saved to ~/.shiori/config.json");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/api.ts
|
|
155
|
+
async function api(method, path, body) {
|
|
156
|
+
const baseUrl = getBaseUrl();
|
|
157
|
+
const apiKey = getApiKey();
|
|
158
|
+
const res = await fetch(`${baseUrl}${path}`, {
|
|
159
|
+
method,
|
|
160
|
+
headers: {
|
|
161
|
+
Authorization: `Bearer ${apiKey}`,
|
|
162
|
+
"Content-Type": "application/json"
|
|
163
|
+
},
|
|
164
|
+
body: body ? JSON.stringify(body) : void 0
|
|
165
|
+
});
|
|
166
|
+
const data = await res.json();
|
|
167
|
+
if (!res.ok || data.success === false) {
|
|
168
|
+
const message = data.error || `Request failed (HTTP ${res.status})`;
|
|
169
|
+
console.error(`Error: ${message}`);
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
return { status: res.status, data };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/format.ts
|
|
176
|
+
function formatDate(iso) {
|
|
177
|
+
if (!iso) return "--";
|
|
178
|
+
return new Date(iso).toLocaleDateString("en-US", {
|
|
179
|
+
month: "short",
|
|
180
|
+
day: "numeric",
|
|
181
|
+
year: "numeric",
|
|
182
|
+
hour: "numeric",
|
|
183
|
+
minute: "2-digit"
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
function truncate(str, max) {
|
|
187
|
+
if (str.length <= max) return str;
|
|
188
|
+
return str.slice(0, max - 1) + "\u2026";
|
|
189
|
+
}
|
|
190
|
+
function getFlag(args, name, fallback) {
|
|
191
|
+
const idx = args.indexOf(name);
|
|
192
|
+
if (idx === -1 || idx + 1 >= args.length) return fallback;
|
|
193
|
+
return args[idx + 1];
|
|
194
|
+
}
|
|
195
|
+
function hasFlag(args, ...names) {
|
|
196
|
+
return names.some((n) => args.includes(n));
|
|
197
|
+
}
|
|
198
|
+
function getPositional(args) {
|
|
199
|
+
for (let i = 0; i < args.length; i++) {
|
|
200
|
+
if (args[i].startsWith("--")) {
|
|
201
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("--") && ["--limit", "--offset", "--sort", "--read", "--title", "--summary"].includes(args[i])) {
|
|
202
|
+
i++;
|
|
203
|
+
}
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
return args[i];
|
|
207
|
+
}
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/commands/delete.ts
|
|
212
|
+
async function run2(args) {
|
|
213
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
214
|
+
console.log(`shiori delete - Delete a link (move to trash)
|
|
215
|
+
|
|
216
|
+
Usage: shiori delete <id> [options]
|
|
217
|
+
|
|
218
|
+
Options:
|
|
219
|
+
--json Output raw JSON
|
|
220
|
+
--help, -h Show this help`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const id = getPositional(args);
|
|
224
|
+
if (!id) {
|
|
225
|
+
console.error("Usage: shiori delete <id>");
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
const { data } = await api("DELETE", `/api/links/${id}`);
|
|
229
|
+
if (hasFlag(args, "--json")) {
|
|
230
|
+
console.log(JSON.stringify(data, null, 2));
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
console.log(data.message || "Deleted.");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/commands/get.ts
|
|
237
|
+
async function run3(args) {
|
|
238
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
239
|
+
console.log(`shiori get - Get a link by ID (includes content)
|
|
240
|
+
|
|
241
|
+
Usage: shiori get <id> [options]
|
|
242
|
+
|
|
243
|
+
Options:
|
|
244
|
+
--json Output raw JSON
|
|
245
|
+
--help, -h Show this help`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const id = getPositional(args);
|
|
249
|
+
if (!id) {
|
|
250
|
+
console.error("Usage: shiori get <link-id>");
|
|
251
|
+
process.exit(1);
|
|
252
|
+
}
|
|
253
|
+
const { data } = await api("GET", `/api/links/${id}`);
|
|
254
|
+
if (hasFlag(args, "--json")) {
|
|
255
|
+
console.log(JSON.stringify(data, null, 2));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
const link = data.link;
|
|
259
|
+
console.log();
|
|
260
|
+
console.log(` ID: ${link.id}`);
|
|
261
|
+
console.log(` Title: ${link.title || "(untitled)"}`);
|
|
262
|
+
console.log(` URL: ${link.url}`);
|
|
263
|
+
console.log(` Domain: ${link.domain || "--"}`);
|
|
264
|
+
console.log(` Status: ${link.status}`);
|
|
265
|
+
console.log(` Source: ${link.source || "--"}`);
|
|
266
|
+
console.log(` Author: ${link.author || "--"}`);
|
|
267
|
+
console.log(` Published: ${formatDate(link.publication_date)}`);
|
|
268
|
+
console.log(` Saved: ${formatDate(link.created_at)}`);
|
|
269
|
+
console.log(
|
|
270
|
+
` Read: ${link.read_at ? formatDate(link.read_at) : "unread"}`
|
|
271
|
+
);
|
|
272
|
+
console.log(` Summary: ${link.summary || "--"}`);
|
|
273
|
+
if (link.content) {
|
|
274
|
+
console.log(`
|
|
275
|
+
--- Content (${link.content.length} chars) ---`);
|
|
276
|
+
console.log(truncate(link.content, 3e3));
|
|
277
|
+
} else {
|
|
278
|
+
console.log("\n (no content)");
|
|
279
|
+
}
|
|
280
|
+
console.log();
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// src/commands/list.ts
|
|
284
|
+
async function run4(args) {
|
|
285
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
286
|
+
console.log(`shiori list - List saved links
|
|
287
|
+
|
|
288
|
+
Usage: shiori list [options]
|
|
289
|
+
|
|
290
|
+
Options:
|
|
291
|
+
--limit <n> Number of links (1-100, default: 25)
|
|
292
|
+
--offset <n> Pagination offset (default: 0)
|
|
293
|
+
--sort <newest|oldest> Sort order (default: newest)
|
|
294
|
+
--read <all|read|unread> Filter by read status (default: all)
|
|
295
|
+
--json Output raw JSON
|
|
296
|
+
--help, -h Show this help`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const limit = getFlag(args, "--limit", "25");
|
|
300
|
+
const offset = getFlag(args, "--offset", "0");
|
|
301
|
+
const sort = getFlag(args, "--sort", "newest");
|
|
302
|
+
const read = getFlag(args, "--read", "all");
|
|
303
|
+
const params = new URLSearchParams({
|
|
304
|
+
limit,
|
|
305
|
+
offset,
|
|
306
|
+
sort,
|
|
307
|
+
read
|
|
308
|
+
});
|
|
309
|
+
const { data } = await api("GET", `/api/links?${params}`);
|
|
310
|
+
if (hasFlag(args, "--json")) {
|
|
311
|
+
console.log(JSON.stringify(data, null, 2));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
console.log(`
|
|
315
|
+
Links: ${data.links.length} of ${data.total} total
|
|
316
|
+
`);
|
|
317
|
+
for (const [i, link] of data.links.entries()) {
|
|
318
|
+
const readStatus = link.read_at ? "read" : "unread";
|
|
319
|
+
const title = truncate(link.title || "(untitled)", 60);
|
|
320
|
+
console.log(
|
|
321
|
+
` ${String(Number(offset) + i + 1).padStart(3)}. ${title} ${link.domain || ""} ${formatDate(link.created_at)} ${readStatus}`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
if (data.links.length === 0) {
|
|
325
|
+
console.log(" No links found.");
|
|
326
|
+
}
|
|
327
|
+
console.log();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// src/commands/me.ts
|
|
331
|
+
async function run5(args) {
|
|
332
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
333
|
+
console.log(`shiori me - Show current user info
|
|
334
|
+
|
|
335
|
+
Usage: shiori me [options]
|
|
336
|
+
|
|
337
|
+
Options:
|
|
338
|
+
--json Output raw JSON
|
|
339
|
+
--help, -h Show this help`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const { data } = await api("GET", "/api/user/me");
|
|
343
|
+
if (hasFlag(args, "--json")) {
|
|
344
|
+
console.log(JSON.stringify(data, null, 2));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const user = data.user;
|
|
348
|
+
console.log(`
|
|
349
|
+
Name: ${user.full_name || "--"}`);
|
|
350
|
+
const plan = user.subscription?.plan === "subscription" ? "Pro" : user.subscription?.plan || "Free";
|
|
351
|
+
console.log(` Plan: ${plan}`);
|
|
352
|
+
console.log(` Member since: ${new Date(user.created_at).toLocaleDateString("en-US", { month: "long", year: "numeric" })}`);
|
|
353
|
+
console.log();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// src/commands/save.ts
|
|
357
|
+
async function run6(args) {
|
|
358
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
359
|
+
console.log(`shiori save - Save a new link
|
|
360
|
+
|
|
361
|
+
Usage: shiori save <url> [options]
|
|
362
|
+
|
|
363
|
+
Options:
|
|
364
|
+
--title <title> Set a custom title
|
|
365
|
+
--read Mark as read immediately
|
|
366
|
+
--json Output raw JSON
|
|
367
|
+
--help, -h Show this help`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const url = getPositional(args);
|
|
371
|
+
if (!url) {
|
|
372
|
+
console.error("Usage: shiori save <url> [--title <title>] [--read]");
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
const body = { url };
|
|
376
|
+
const title = getFlag(args, "--title");
|
|
377
|
+
if (title) body.title = title;
|
|
378
|
+
if (hasFlag(args, "--read")) body.read = true;
|
|
379
|
+
const { data } = await api("POST", "/api/links", body);
|
|
380
|
+
if (hasFlag(args, "--json")) {
|
|
381
|
+
console.log(JSON.stringify(data, null, 2));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (data.duplicate) {
|
|
385
|
+
console.log(`Link already saved (bumped to top): ${data.linkId}`);
|
|
386
|
+
} else {
|
|
387
|
+
console.log(`Saved: ${data.linkId}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// src/commands/trash.ts
|
|
392
|
+
async function run7(args) {
|
|
393
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
394
|
+
console.log(`shiori trash - List or empty the trash
|
|
395
|
+
|
|
396
|
+
Usage: shiori trash [options]
|
|
397
|
+
|
|
398
|
+
Options:
|
|
399
|
+
--limit <n> Number of links (1-100, default: 25)
|
|
400
|
+
--offset <n> Pagination offset (default: 0)
|
|
401
|
+
--empty Permanently delete all trashed links
|
|
402
|
+
--json Output raw JSON
|
|
403
|
+
--help, -h Show this help`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (hasFlag(args, "--empty")) {
|
|
407
|
+
const { data: data2 } = await api("DELETE", "/api/links");
|
|
408
|
+
if (hasFlag(args, "--json")) {
|
|
409
|
+
console.log(JSON.stringify(data2, null, 2));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
console.log(`Permanently deleted ${data2.deleted || 0} link(s).`);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
const limit = getFlag(args, "--limit", "25");
|
|
416
|
+
const offset = getFlag(args, "--offset", "0");
|
|
417
|
+
const params = new URLSearchParams({
|
|
418
|
+
trash: "true",
|
|
419
|
+
limit,
|
|
420
|
+
offset
|
|
421
|
+
});
|
|
422
|
+
const { data } = await api("GET", `/api/links?${params}`);
|
|
423
|
+
if (hasFlag(args, "--json")) {
|
|
424
|
+
console.log(JSON.stringify(data, null, 2));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
console.log(`
|
|
428
|
+
Trashed links: ${data.links.length} of ${data.total} total
|
|
429
|
+
`);
|
|
430
|
+
for (const [i, link] of data.links.entries()) {
|
|
431
|
+
const title = truncate(link.title || "(untitled)", 60);
|
|
432
|
+
console.log(
|
|
433
|
+
` ${String(i + 1).padStart(3)}. ${title} ${link.domain || ""} deleted ${formatDate(link.deleted_at)}`
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
if (data.links.length === 0) {
|
|
437
|
+
console.log(" Trash is empty.");
|
|
438
|
+
}
|
|
439
|
+
console.log();
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/commands/update.ts
|
|
443
|
+
async function run8(args) {
|
|
444
|
+
if (hasFlag(args, "--help", "-h")) {
|
|
445
|
+
console.log(`shiori update - Update a link
|
|
446
|
+
|
|
447
|
+
Usage: shiori update <id> [options]
|
|
448
|
+
|
|
449
|
+
Options:
|
|
450
|
+
--read Mark as read
|
|
451
|
+
--unread Mark as unread
|
|
452
|
+
--title <title> Update title
|
|
453
|
+
--summary <text> Update summary
|
|
454
|
+
--restore Restore from trash
|
|
455
|
+
--json Output raw JSON
|
|
456
|
+
--help, -h Show this help`);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const id = getPositional(args);
|
|
460
|
+
if (!id) {
|
|
461
|
+
console.error("Usage: shiori update <id> [--read | --unread | --title <title> | --restore]");
|
|
462
|
+
process.exit(1);
|
|
463
|
+
}
|
|
464
|
+
let body;
|
|
465
|
+
if (hasFlag(args, "--restore")) {
|
|
466
|
+
body = { restore: true };
|
|
467
|
+
} else if (hasFlag(args, "--read")) {
|
|
468
|
+
body = { read: true };
|
|
469
|
+
} else if (hasFlag(args, "--unread")) {
|
|
470
|
+
body = { read: false };
|
|
471
|
+
} else {
|
|
472
|
+
const title = getFlag(args, "--title");
|
|
473
|
+
const summary = getFlag(args, "--summary");
|
|
474
|
+
if (!title && !summary) {
|
|
475
|
+
console.error("Provide at least one update flag: --read, --unread, --title, --summary, --restore");
|
|
476
|
+
process.exit(1);
|
|
477
|
+
}
|
|
478
|
+
body = {};
|
|
479
|
+
if (title) body.title = title;
|
|
480
|
+
if (summary !== void 0) body.summary = summary;
|
|
481
|
+
}
|
|
482
|
+
const { data } = await api("PATCH", `/api/links/${id}`, body);
|
|
483
|
+
if (hasFlag(args, "--json")) {
|
|
484
|
+
console.log(JSON.stringify(data, null, 2));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
console.log(data.message || "Updated.");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// src/error-report.ts
|
|
491
|
+
function reportError(command, error) {
|
|
492
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
493
|
+
const baseUrl = getBaseUrl();
|
|
494
|
+
fetch(`${baseUrl}/api/cli/report`, {
|
|
495
|
+
method: "POST",
|
|
496
|
+
headers: { "Content-Type": "application/json" },
|
|
497
|
+
body: JSON.stringify({
|
|
498
|
+
version: "0.1.0",
|
|
499
|
+
command,
|
|
500
|
+
error: message,
|
|
501
|
+
platform: process.platform
|
|
502
|
+
}),
|
|
503
|
+
signal: AbortSignal.timeout(3e3)
|
|
504
|
+
}).catch(() => {
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/update-check.ts
|
|
509
|
+
var REGISTRY_URL = "https://registry.npmjs.org/@shiori-sh/cli/latest";
|
|
510
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
511
|
+
async function checkForUpdate(currentVersion, isJson) {
|
|
512
|
+
if (process.env.CI || process.env.NO_UPDATE_NOTIFIER || isJson) return;
|
|
513
|
+
const config = readConfig();
|
|
514
|
+
const now = Date.now();
|
|
515
|
+
if (config.last_update_check && now - config.last_update_check < CHECK_INTERVAL_MS)
|
|
516
|
+
return;
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch(REGISTRY_URL, {
|
|
519
|
+
signal: AbortSignal.timeout(3e3)
|
|
520
|
+
});
|
|
521
|
+
if (!res.ok) return;
|
|
522
|
+
const data = await res.json();
|
|
523
|
+
writeConfig({ ...config, last_update_check: now });
|
|
524
|
+
if (data.version && data.version !== currentVersion) {
|
|
525
|
+
process.stderr.write(
|
|
526
|
+
`
|
|
527
|
+
Update available: ${currentVersion} \u2192 ${data.version}
|
|
528
|
+
Run: npm install -g @shiori-sh/cli
|
|
529
|
+
|
|
530
|
+
`
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
} catch {
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// src/index.ts
|
|
538
|
+
var COMMANDS = {
|
|
539
|
+
auth: { run, desc: "Authenticate with your Shiori API key" },
|
|
540
|
+
list: { run: run4, desc: "List saved links" },
|
|
541
|
+
get: { run: run3, desc: "Get a link by ID (includes content)" },
|
|
542
|
+
save: { run: run6, desc: "Save a new link" },
|
|
543
|
+
update: { run: run8, desc: "Update a link" },
|
|
544
|
+
delete: { run: run2, desc: "Delete a link (move to trash)" },
|
|
545
|
+
trash: { run: run7, desc: "List or empty the trash" },
|
|
546
|
+
me: { run: run5, desc: "Show current user info" }
|
|
547
|
+
};
|
|
548
|
+
function printHelp() {
|
|
549
|
+
console.log(`shiori - Manage your Shiori link library from the terminal
|
|
550
|
+
|
|
551
|
+
Usage: shiori <command> [options]
|
|
552
|
+
|
|
553
|
+
Commands:
|
|
554
|
+
${Object.entries(COMMANDS).map(([name, { desc }]) => ` ${name.padEnd(10)} ${desc}`).join("\n")}
|
|
555
|
+
|
|
556
|
+
Options:
|
|
557
|
+
--help, -h Show help
|
|
558
|
+
--version, -v Show version
|
|
559
|
+
|
|
560
|
+
Get started:
|
|
561
|
+
shiori auth Authenticate with your API key
|
|
562
|
+
shiori list List your recent links
|
|
563
|
+
shiori save <url> Save a new link`);
|
|
564
|
+
}
|
|
565
|
+
async function main() {
|
|
566
|
+
const args = process.argv.slice(2);
|
|
567
|
+
const command = args[0];
|
|
568
|
+
if (!command || command === "--help" || command === "-h") {
|
|
569
|
+
printHelp();
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
if (command === "--version" || command === "-v") {
|
|
573
|
+
console.log("0.1.0");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const cmd = COMMANDS[command];
|
|
577
|
+
if (!cmd) {
|
|
578
|
+
console.error(`Unknown command: ${command}
|
|
579
|
+
`);
|
|
580
|
+
printHelp();
|
|
581
|
+
process.exit(1);
|
|
582
|
+
}
|
|
583
|
+
const cmdArgs = args.slice(1);
|
|
584
|
+
const isJson = cmdArgs.includes("--json");
|
|
585
|
+
await cmd.run(cmdArgs);
|
|
586
|
+
checkForUpdate("0.1.0", isJson).catch(() => {
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
main().catch((err) => {
|
|
590
|
+
const command = process.argv[2] ?? "unknown";
|
|
591
|
+
reportError(command, err);
|
|
592
|
+
console.error(err.message || err);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shiori-sh/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for managing your Shiori link library",
|
|
5
|
+
"author": "Brian Lovin",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://www.shiori.sh",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/brianlovin/shiori",
|
|
11
|
+
"directory": "cli"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"shiori": "dist/index.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup",
|
|
22
|
+
"dev": "tsup --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"prepublishOnly": "bun run build"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"tsup": "^8.0.0",
|
|
28
|
+
"typescript": "^5.8.3"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=18"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"shiori",
|
|
35
|
+
"bookmarks",
|
|
36
|
+
"links",
|
|
37
|
+
"read-it-later",
|
|
38
|
+
"cli"
|
|
39
|
+
]
|
|
40
|
+
}
|