artyfax 0.2.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/LICENSE +21 -0
- package/README.md +139 -0
- package/dist/cli.js +1432 -0
- package/package.json +49 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
5
|
+
import { resolve as resolve2 } from "path";
|
|
6
|
+
|
|
7
|
+
// src/config.ts
|
|
8
|
+
function getConfig(flags) {
|
|
9
|
+
const apiKey = flags["api-key"] || process.env.ARTYFAX_API_KEY;
|
|
10
|
+
if (!apiKey) {
|
|
11
|
+
console.error("No API key. Set ARTYFAX_API_KEY or pass --api-key.");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
apiKey,
|
|
16
|
+
endpoint: flags.endpoint || process.env.ARTYFAX_ENDPOINT || "https://artyfax.io",
|
|
17
|
+
passphrase: process.env.ARTYFAX_SECURE_PASSPHRASE || null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function apiFetch(config, path, init) {
|
|
21
|
+
const res = await fetch(`${config.endpoint}/api${path}`, {
|
|
22
|
+
...init,
|
|
23
|
+
headers: {
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
"X-API-Key": config.apiKey,
|
|
26
|
+
...init?.headers
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const body = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
|
|
31
|
+
throw new Error(body.error ?? `HTTP ${res.status}`);
|
|
32
|
+
}
|
|
33
|
+
return res.json();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// src/passphrase.ts
|
|
37
|
+
import { createInterface } from "readline";
|
|
38
|
+
|
|
39
|
+
// ../shared/src/secure-crypto.ts
|
|
40
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
41
|
+
var VERIFICATION_STRING = "artyfax-secure-docs-v1";
|
|
42
|
+
async function deriveSDK(passphrase, salt) {
|
|
43
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
44
|
+
"raw",
|
|
45
|
+
new TextEncoder().encode(passphrase),
|
|
46
|
+
"PBKDF2",
|
|
47
|
+
false,
|
|
48
|
+
["deriveKey"]
|
|
49
|
+
);
|
|
50
|
+
return crypto.subtle.deriveKey(
|
|
51
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-512" },
|
|
52
|
+
keyMaterial,
|
|
53
|
+
{ name: "AES-GCM", length: 256 },
|
|
54
|
+
true,
|
|
55
|
+
["encrypt", "decrypt"]
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
async function encryptSecure(plaintext, sdk) {
|
|
59
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
60
|
+
const encoded = new TextEncoder().encode(plaintext);
|
|
61
|
+
const ciphertext = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, sdk, encoded);
|
|
62
|
+
const combined = new Uint8Array(iv.length + ciphertext.byteLength);
|
|
63
|
+
combined.set(iv);
|
|
64
|
+
combined.set(new Uint8Array(ciphertext), iv.length);
|
|
65
|
+
return bufferToBase64(combined.buffer);
|
|
66
|
+
}
|
|
67
|
+
async function decryptSecure(blob, sdk) {
|
|
68
|
+
const data = new Uint8Array(base64ToBuffer(blob));
|
|
69
|
+
const iv = data.slice(0, 12);
|
|
70
|
+
const ciphertext = data.slice(12);
|
|
71
|
+
const decrypted = await crypto.subtle.decrypt({ name: "AES-GCM", iv }, sdk, ciphertext);
|
|
72
|
+
return new TextDecoder().decode(decrypted);
|
|
73
|
+
}
|
|
74
|
+
async function verifyPassphrase(sdk, verifyBlob) {
|
|
75
|
+
try {
|
|
76
|
+
const result = await decryptSecure(verifyBlob, sdk);
|
|
77
|
+
return result === VERIFICATION_STRING;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function base64ToBuffer(base64) {
|
|
83
|
+
const binary = atob(base64);
|
|
84
|
+
const bytes = new Uint8Array(binary.length);
|
|
85
|
+
for (let i = 0; i < binary.length; i++) {
|
|
86
|
+
bytes[i] = binary.charCodeAt(i);
|
|
87
|
+
}
|
|
88
|
+
return bytes.buffer;
|
|
89
|
+
}
|
|
90
|
+
function bufferToBase64(buffer) {
|
|
91
|
+
const bytes = new Uint8Array(buffer);
|
|
92
|
+
let binary = "";
|
|
93
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
94
|
+
binary += String.fromCharCode(bytes[i]);
|
|
95
|
+
}
|
|
96
|
+
return btoa(binary);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/passphrase.ts
|
|
100
|
+
async function promptPassphrase(prompt = "Passphrase: ") {
|
|
101
|
+
return new Promise((resolve3, reject) => {
|
|
102
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
103
|
+
process.stderr.write(prompt);
|
|
104
|
+
const cleanup = () => {
|
|
105
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
106
|
+
process.stdin.removeListener("data", onData);
|
|
107
|
+
rl.close();
|
|
108
|
+
};
|
|
109
|
+
process.on("exit", cleanup);
|
|
110
|
+
if (process.stdin.isTTY) {
|
|
111
|
+
process.stdin.setRawMode(true);
|
|
112
|
+
}
|
|
113
|
+
let input = "";
|
|
114
|
+
const onData = (ch) => {
|
|
115
|
+
const c2 = ch.toString();
|
|
116
|
+
if (c2 === "\n" || c2 === "\r") {
|
|
117
|
+
process.stderr.write("\n");
|
|
118
|
+
cleanup();
|
|
119
|
+
resolve3(input);
|
|
120
|
+
} else if (c2 === "" || c2.length === 0) {
|
|
121
|
+
process.stderr.write("\n");
|
|
122
|
+
cleanup();
|
|
123
|
+
process.exit(1);
|
|
124
|
+
} else if (c2 === "\x7F" || c2 === "\b") {
|
|
125
|
+
input = input.slice(0, -1);
|
|
126
|
+
} else {
|
|
127
|
+
input += c2;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
process.stdin.on("data", onData);
|
|
131
|
+
process.stdin.resume();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
async function getSDK(config) {
|
|
135
|
+
const passphrase = config.passphrase ?? await promptPassphrase();
|
|
136
|
+
if (config.passphrase) {
|
|
137
|
+
Reflect.deleteProperty(process.env, "ARTYFAX_SECURE_PASSPHRASE");
|
|
138
|
+
}
|
|
139
|
+
const state = await apiFetch(config, "/account/secure-state");
|
|
140
|
+
if (!state.salt || !state.verify) {
|
|
141
|
+
throw new Error("E2EE not set up. Configure a passphrase in the Artyfax web app first.");
|
|
142
|
+
}
|
|
143
|
+
const saltBytes = new Uint8Array(base64ToBuffer(state.salt));
|
|
144
|
+
const sdk = await deriveSDK(passphrase, saltBytes);
|
|
145
|
+
const valid = await verifyPassphrase(sdk, state.verify);
|
|
146
|
+
if (!valid) {
|
|
147
|
+
throw new Error("Incorrect passphrase");
|
|
148
|
+
}
|
|
149
|
+
return sdk;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// src/ui.ts
|
|
153
|
+
import { createInterface as createInterface2 } from "readline";
|
|
154
|
+
import pc from "picocolors";
|
|
155
|
+
import ora from "ora";
|
|
156
|
+
var isTTY = process.stdout.isTTY === true;
|
|
157
|
+
var c = {
|
|
158
|
+
amber: (s) => pc.yellow(s),
|
|
159
|
+
teal: (s) => pc.green(s),
|
|
160
|
+
rose: (s) => pc.red(s),
|
|
161
|
+
muted: (s) => pc.dim(s),
|
|
162
|
+
bright: (s) => pc.bold(s),
|
|
163
|
+
blue: (s) => pc.blue(s)
|
|
164
|
+
};
|
|
165
|
+
function spinner(text) {
|
|
166
|
+
return ora({ text, isSilent: !isTTY });
|
|
167
|
+
}
|
|
168
|
+
function table(rows, { header } = {}) {
|
|
169
|
+
const all = header ? [header, ...rows] : rows;
|
|
170
|
+
const widths = [];
|
|
171
|
+
for (const row of all) {
|
|
172
|
+
for (let i = 0; i < row.length; i++) {
|
|
173
|
+
const len = stripAnsi(row[i]).length;
|
|
174
|
+
widths[i] = Math.max(widths[i] || 0, len);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const termWidth = process.stdout.columns || 120;
|
|
178
|
+
const lines = [];
|
|
179
|
+
for (let r = 0; r < all.length; r++) {
|
|
180
|
+
const row = all[r];
|
|
181
|
+
const parts = [];
|
|
182
|
+
for (let i = 0; i < row.length; i++) {
|
|
183
|
+
const raw = stripAnsi(row[i]);
|
|
184
|
+
const pad = i < row.length - 1 ? " ".repeat(Math.max(0, widths[i] - raw.length)) : "";
|
|
185
|
+
parts.push(row[i] + pad);
|
|
186
|
+
}
|
|
187
|
+
let line = parts.join(" ");
|
|
188
|
+
if (stripAnsi(line).length > termWidth) {
|
|
189
|
+
const plain = stripAnsi(line);
|
|
190
|
+
line = plain.slice(0, termWidth - 1) + "\u2026";
|
|
191
|
+
}
|
|
192
|
+
lines.push(line);
|
|
193
|
+
if (r === 0 && header) {
|
|
194
|
+
lines.push(c.muted("\u2500".repeat(Math.min(stripAnsi(line).length, termWidth))));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return lines.join("\n");
|
|
198
|
+
}
|
|
199
|
+
function stripAnsi(s) {
|
|
200
|
+
return s.replace(/\x1b\[[0-9;]*m/g, "");
|
|
201
|
+
}
|
|
202
|
+
function confirm(message) {
|
|
203
|
+
return new Promise((resolve3) => {
|
|
204
|
+
const rl = createInterface2({ input: process.stdin, output: process.stderr });
|
|
205
|
+
rl.question(`${message} ${c.muted("[y/N]")} `, (answer) => {
|
|
206
|
+
rl.close();
|
|
207
|
+
resolve3(answer.trim().toLowerCase() === "y");
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
function error(message, suggestion) {
|
|
212
|
+
console.error(c.rose(`Error: ${message}`));
|
|
213
|
+
if (suggestion) console.error(c.muted(suggestion));
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/commands/save.ts
|
|
218
|
+
async function save(config, opts) {
|
|
219
|
+
const spin = spinner(opts.url ? "Extracting article\u2026" : "Saving document\u2026");
|
|
220
|
+
spin.start();
|
|
221
|
+
if (opts.url && opts.secure) {
|
|
222
|
+
spin.stop();
|
|
223
|
+
throw new Error("Cannot combine --url with --secure. URL extraction happens server-side, so the CLI cannot encrypt content it never sees.");
|
|
224
|
+
}
|
|
225
|
+
let content = opts.content;
|
|
226
|
+
if (opts.secure && content) {
|
|
227
|
+
spin.text = "Encrypting\u2026";
|
|
228
|
+
const sdk = await getSDK(config);
|
|
229
|
+
content = await encryptSecure(content, sdk);
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const body = {
|
|
233
|
+
title: opts.title || void 0,
|
|
234
|
+
content: content || void 0,
|
|
235
|
+
format: opts.format,
|
|
236
|
+
category: opts.category,
|
|
237
|
+
doc_type: opts.docType || "original",
|
|
238
|
+
secure: opts.secure || void 0
|
|
239
|
+
};
|
|
240
|
+
if (opts.url) body.url = opts.url;
|
|
241
|
+
if (opts.tags) body.tags = opts.tags.split(",").map((t) => t.trim());
|
|
242
|
+
if (opts.theme) body.theme = opts.theme;
|
|
243
|
+
if (opts.parentId) body.parent_id = opts.parentId;
|
|
244
|
+
const result = await apiFetch(
|
|
245
|
+
config,
|
|
246
|
+
"/ingest",
|
|
247
|
+
{ method: "POST", body: JSON.stringify(body) }
|
|
248
|
+
);
|
|
249
|
+
spin.stop();
|
|
250
|
+
if (opts.json) {
|
|
251
|
+
console.log(JSON.stringify(result.document));
|
|
252
|
+
} else {
|
|
253
|
+
console.log(`${c.teal("Saved:")} ${result.document.slug}${opts.secure ? c.muted(" (encrypted)") : ""}`);
|
|
254
|
+
}
|
|
255
|
+
} catch (e) {
|
|
256
|
+
spin.fail("Save failed");
|
|
257
|
+
throw e;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/resolve.ts
|
|
262
|
+
async function resolveSlug(config, slugOrId) {
|
|
263
|
+
if (slugOrId.includes("/")) {
|
|
264
|
+
try {
|
|
265
|
+
const doc = await apiFetch(config, `/documents/by-slug/${slugOrId}`);
|
|
266
|
+
return doc;
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (slugOrId.match(/^[0-9a-f-]{36}$/i)) {
|
|
271
|
+
try {
|
|
272
|
+
const doc = await apiFetch(config, `/documents/${slugOrId}`);
|
|
273
|
+
return doc;
|
|
274
|
+
} catch {
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
const docs = await apiFetch(
|
|
278
|
+
config,
|
|
279
|
+
`/documents?limit=500`
|
|
280
|
+
);
|
|
281
|
+
const exact = docs.documents.find(
|
|
282
|
+
(d) => d.slug === slugOrId || d.slug.endsWith(`/${slugOrId}`)
|
|
283
|
+
);
|
|
284
|
+
if (exact) return exact;
|
|
285
|
+
const partial = docs.documents.filter(
|
|
286
|
+
(d) => d.slug.includes(slugOrId) || d.title.toLowerCase().includes(slugOrId.toLowerCase())
|
|
287
|
+
);
|
|
288
|
+
if (partial.length === 1) return partial[0];
|
|
289
|
+
if (partial.length > 1) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`Ambiguous match for "${slugOrId}". Did you mean:
|
|
292
|
+
` + partial.slice(0, 5).map((d) => ` ${d.slug}`).join("\n")
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
throw new Error(`Document not found: ${slugOrId}. Try \`arty search ${slugOrId}\` or \`arty list\``);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// src/commands/read.ts
|
|
299
|
+
async function read(config, slugOrId, forceSecure) {
|
|
300
|
+
const spin = spinner("Resolving document\u2026");
|
|
301
|
+
spin.start();
|
|
302
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
303
|
+
const isSecure = doc.secure === 1 || forceSecure;
|
|
304
|
+
spin.text = "Fetching content\u2026";
|
|
305
|
+
const data = await apiFetch(
|
|
306
|
+
config,
|
|
307
|
+
`/documents/${doc.id}/content`
|
|
308
|
+
);
|
|
309
|
+
spin.stop();
|
|
310
|
+
if (isSecure) {
|
|
311
|
+
const sdk = await getSDK(config);
|
|
312
|
+
const plaintext = await decryptSecure(data.content, sdk);
|
|
313
|
+
process.stdout.write(plaintext);
|
|
314
|
+
} else {
|
|
315
|
+
process.stdout.write(data.md_source ?? data.content);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/commands/secure.ts
|
|
320
|
+
async function secure(config, slugOrId) {
|
|
321
|
+
const spin = spinner("Resolving document\u2026");
|
|
322
|
+
spin.start();
|
|
323
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
324
|
+
if (doc.secure === 1) {
|
|
325
|
+
spin.stop();
|
|
326
|
+
console.log(c.muted(`Already encrypted: ${doc.slug}`));
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const sdk = await getSDK(config);
|
|
330
|
+
spin.text = "Fetching content\u2026";
|
|
331
|
+
const data = await apiFetch(
|
|
332
|
+
config,
|
|
333
|
+
`/documents/${doc.id}/content`
|
|
334
|
+
);
|
|
335
|
+
const source = data.md_source ?? data.content;
|
|
336
|
+
spin.text = "Encrypting\u2026";
|
|
337
|
+
const encrypted = await encryptSecure(source, sdk);
|
|
338
|
+
spin.text = "Uploading\u2026";
|
|
339
|
+
await apiFetch(config, `/documents/${doc.id}/content`, {
|
|
340
|
+
method: "PATCH",
|
|
341
|
+
body: JSON.stringify({ content: encrypted })
|
|
342
|
+
});
|
|
343
|
+
await apiFetch(config, `/documents/${doc.id}`, {
|
|
344
|
+
method: "PATCH",
|
|
345
|
+
body: JSON.stringify({ secure: 1 })
|
|
346
|
+
});
|
|
347
|
+
spin.succeed(`${c.teal("Encrypted:")} ${doc.slug}`);
|
|
348
|
+
}
|
|
349
|
+
async function unsecure(config, slugOrId) {
|
|
350
|
+
const spin = spinner("Resolving document\u2026");
|
|
351
|
+
spin.start();
|
|
352
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
353
|
+
if (doc.secure !== 1) {
|
|
354
|
+
spin.stop();
|
|
355
|
+
console.log(c.muted(`Not encrypted: ${doc.slug}`));
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const sdk = await getSDK(config);
|
|
359
|
+
spin.text = "Fetching content\u2026";
|
|
360
|
+
const data = await apiFetch(
|
|
361
|
+
config,
|
|
362
|
+
`/documents/${doc.id}/content`
|
|
363
|
+
);
|
|
364
|
+
spin.text = "Decrypting\u2026";
|
|
365
|
+
const plaintext = await decryptSecure(data.content, sdk);
|
|
366
|
+
spin.text = "Uploading\u2026";
|
|
367
|
+
await apiFetch(config, `/documents/${doc.id}/content`, {
|
|
368
|
+
method: "PATCH",
|
|
369
|
+
body: JSON.stringify({ content: plaintext })
|
|
370
|
+
});
|
|
371
|
+
await apiFetch(config, `/documents/${doc.id}`, {
|
|
372
|
+
method: "PATCH",
|
|
373
|
+
body: JSON.stringify({ secure: 0 })
|
|
374
|
+
});
|
|
375
|
+
spin.succeed(`${c.teal("Decrypted:")} ${doc.slug}`);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/commands/list.ts
|
|
379
|
+
async function list(config, opts) {
|
|
380
|
+
const spin = spinner("Loading documents\u2026");
|
|
381
|
+
spin.start();
|
|
382
|
+
let path = `/documents?limit=${opts.limit}`;
|
|
383
|
+
if (opts.category) path += `&category=${encodeURIComponent(opts.category)}`;
|
|
384
|
+
if (opts.offset) path += `&offset=${opts.offset}`;
|
|
385
|
+
if (opts.archived) path += `&archived=1`;
|
|
386
|
+
if (opts.parentId) path += `&parent_id=${encodeURIComponent(opts.parentId)}`;
|
|
387
|
+
const data = await apiFetch(config, path);
|
|
388
|
+
spin.stop();
|
|
389
|
+
if (opts.json) {
|
|
390
|
+
console.log(JSON.stringify(data.documents));
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (data.documents.length === 0) {
|
|
394
|
+
console.log(c.muted("No documents found."));
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
const rows = data.documents.map((doc) => {
|
|
398
|
+
const icons = [
|
|
399
|
+
doc.secure === 1 ? "\u{1F512}" : "",
|
|
400
|
+
(doc.shared_count ?? 0) > 0 ? "\u{1F517}" : ""
|
|
401
|
+
].filter(Boolean).join("") || " ";
|
|
402
|
+
return [
|
|
403
|
+
c.muted(doc.updated_at.slice(0, 10)),
|
|
404
|
+
icons,
|
|
405
|
+
doc.title,
|
|
406
|
+
c.muted(doc.slug)
|
|
407
|
+
];
|
|
408
|
+
});
|
|
409
|
+
console.log(table(rows, { header: [c.muted("Updated"), "", "Title", c.muted("Slug")] }));
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// src/commands/search.ts
|
|
413
|
+
async function search(config, query, json) {
|
|
414
|
+
const spin = spinner(`Searching for "${query}"\u2026`);
|
|
415
|
+
spin.start();
|
|
416
|
+
const data = await apiFetch(
|
|
417
|
+
config,
|
|
418
|
+
`/search?q=${encodeURIComponent(query)}`
|
|
419
|
+
);
|
|
420
|
+
spin.stop();
|
|
421
|
+
if (json) {
|
|
422
|
+
console.log(JSON.stringify(data.results));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (data.results.length === 0) {
|
|
426
|
+
console.log(c.muted(`No results for "${query}".`));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
const rows = data.results.map((r) => [
|
|
430
|
+
r.title,
|
|
431
|
+
c.muted(r.slug),
|
|
432
|
+
c.muted(r.updated_at.slice(0, 10))
|
|
433
|
+
]);
|
|
434
|
+
console.log(table(rows, { header: ["Title", c.muted("Slug"), c.muted("Updated")] }));
|
|
435
|
+
console.log(c.muted(`
|
|
436
|
+
${data.results.length} result${data.results.length === 1 ? "" : "s"}`));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/commands/get.ts
|
|
440
|
+
async function get(config, slugOrId) {
|
|
441
|
+
const spin = spinner("Resolving document\u2026");
|
|
442
|
+
spin.start();
|
|
443
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
444
|
+
spin.stop();
|
|
445
|
+
console.log(JSON.stringify(doc, null, 2));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// src/commands/delete.ts
|
|
449
|
+
async function del(config, slugOrId, yes, json) {
|
|
450
|
+
const spin = spinner("Resolving document\u2026");
|
|
451
|
+
spin.start();
|
|
452
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
453
|
+
spin.stop();
|
|
454
|
+
if (!yes) {
|
|
455
|
+
const confirmed = await confirm(`Delete ${c.bright(doc.slug)}?`);
|
|
456
|
+
if (!confirmed) {
|
|
457
|
+
console.log(c.muted("Cancelled."));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
const delSpin = spinner("Deleting\u2026");
|
|
462
|
+
delSpin.start();
|
|
463
|
+
await apiFetch(config, `/documents/${doc.id}`, { method: "DELETE" });
|
|
464
|
+
delSpin.stop();
|
|
465
|
+
if (json) {
|
|
466
|
+
console.log(JSON.stringify({ deleted: true, slug: doc.slug, id: doc.id }));
|
|
467
|
+
} else {
|
|
468
|
+
console.log(`${c.rose("Deleted:")} ${doc.slug}`);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/commands/open.ts
|
|
473
|
+
import { execFile } from "child_process";
|
|
474
|
+
async function open(config, slugOrId) {
|
|
475
|
+
const spin = spinner("Resolving document\u2026");
|
|
476
|
+
spin.start();
|
|
477
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
478
|
+
spin.stop();
|
|
479
|
+
const url = `${config.endpoint}/${doc.slug}`;
|
|
480
|
+
const platform = process.platform;
|
|
481
|
+
const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
|
|
482
|
+
const args = platform === "win32" ? ["/c", "start", url] : [url];
|
|
483
|
+
execFile(cmd, args, (err) => {
|
|
484
|
+
if (err) {
|
|
485
|
+
console.log(c.muted(`Could not open browser. URL: ${url}`));
|
|
486
|
+
} else {
|
|
487
|
+
console.log(`${c.teal("Opened:")} ${url}`);
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/commands/update.ts
|
|
493
|
+
import { readFileSync } from "fs";
|
|
494
|
+
import { resolve } from "path";
|
|
495
|
+
async function update(config, slugOrId, file, json) {
|
|
496
|
+
const spin = spinner("Resolving document\u2026");
|
|
497
|
+
spin.start();
|
|
498
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
499
|
+
spin.text = "Reading content\u2026";
|
|
500
|
+
let content;
|
|
501
|
+
if (file === "-" || !file) {
|
|
502
|
+
if (process.stdin.isTTY) {
|
|
503
|
+
spin.stop();
|
|
504
|
+
throw new Error("No file provided and stdin is a terminal. Pipe content or specify a file path.");
|
|
505
|
+
}
|
|
506
|
+
const chunks = [];
|
|
507
|
+
for await (const chunk of process.stdin) {
|
|
508
|
+
chunks.push(chunk);
|
|
509
|
+
}
|
|
510
|
+
content = Buffer.concat(chunks).toString("utf-8");
|
|
511
|
+
} else {
|
|
512
|
+
try {
|
|
513
|
+
content = readFileSync(resolve(file), "utf-8");
|
|
514
|
+
} catch {
|
|
515
|
+
spin.stop();
|
|
516
|
+
throw new Error(`File not found: ${file}`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
if (doc.secure === 1) {
|
|
520
|
+
spin.text = "Encrypting (secure document)\u2026";
|
|
521
|
+
const sdk = await getSDK(config);
|
|
522
|
+
content = await encryptSecure(content, sdk);
|
|
523
|
+
}
|
|
524
|
+
spin.text = "Uploading\u2026";
|
|
525
|
+
await apiFetch(config, `/documents/${doc.id}/content`, {
|
|
526
|
+
method: "PATCH",
|
|
527
|
+
body: JSON.stringify({ content })
|
|
528
|
+
});
|
|
529
|
+
spin.stop();
|
|
530
|
+
if (json) {
|
|
531
|
+
console.log(JSON.stringify({ updated: true, slug: doc.slug, id: doc.id, secure: doc.secure === 1 }));
|
|
532
|
+
} else {
|
|
533
|
+
const suffix = doc.secure === 1 ? c.muted(" (re-encrypted)") : "";
|
|
534
|
+
console.log(`${c.teal("Updated:")} ${doc.slug}${suffix}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// src/commands/metadata.ts
|
|
539
|
+
async function metadata(config, slugOrId, opts) {
|
|
540
|
+
const spin = spinner("Resolving document\u2026");
|
|
541
|
+
spin.start();
|
|
542
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
543
|
+
const patch = {};
|
|
544
|
+
if (opts.category !== void 0) patch.category = opts.category;
|
|
545
|
+
if (opts.title !== void 0) patch.title = opts.title;
|
|
546
|
+
if (opts.theme !== void 0) patch.theme = opts.theme;
|
|
547
|
+
if (opts.visibility !== void 0) patch.visibility = opts.visibility;
|
|
548
|
+
if (opts.archived !== void 0) patch.archived = opts.archived ? 1 : 0;
|
|
549
|
+
if (opts.tags !== void 0) patch.tags = opts.tags.split(",").map((t) => t.trim());
|
|
550
|
+
if (Object.keys(patch).length === 0) {
|
|
551
|
+
spin.stop();
|
|
552
|
+
throw new Error("No metadata flags provided. Use --category, --tags, --title, --theme, --visibility, or --archived");
|
|
553
|
+
}
|
|
554
|
+
spin.text = "Updating metadata\u2026";
|
|
555
|
+
await apiFetch(config, `/documents/${doc.id}/metadata`, {
|
|
556
|
+
method: "PATCH",
|
|
557
|
+
body: JSON.stringify(patch)
|
|
558
|
+
});
|
|
559
|
+
spin.stop();
|
|
560
|
+
if (opts.json) {
|
|
561
|
+
console.log(JSON.stringify({ updated: true, slug: doc.slug, id: doc.id, fields: Object.keys(patch) }));
|
|
562
|
+
} else {
|
|
563
|
+
const fields = Object.keys(patch).join(", ");
|
|
564
|
+
console.log(`${c.teal("Updated:")} ${doc.slug} ${c.muted(`(${fields})`)}`);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/commands/share.ts
|
|
569
|
+
async function shareCreate(config, slugOrId, json) {
|
|
570
|
+
const spin = spinner("Resolving document\u2026");
|
|
571
|
+
spin.start();
|
|
572
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
573
|
+
spin.text = "Creating share link\u2026";
|
|
574
|
+
const result = await apiFetch(
|
|
575
|
+
config,
|
|
576
|
+
"/shares",
|
|
577
|
+
{
|
|
578
|
+
method: "POST",
|
|
579
|
+
body: JSON.stringify({ document_id: doc.id })
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
spin.stop();
|
|
583
|
+
const url = `${config.endpoint}/s/${result.hash}`;
|
|
584
|
+
if (json) {
|
|
585
|
+
console.log(JSON.stringify({ hash: result.hash, url, slug: doc.slug }));
|
|
586
|
+
} else {
|
|
587
|
+
console.log(`${c.teal("Share created:")} ${url}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
async function shareList(config, slugOrId, json) {
|
|
591
|
+
const spin = spinner("Loading shares\u2026");
|
|
592
|
+
spin.start();
|
|
593
|
+
let path = "/shares";
|
|
594
|
+
if (slugOrId) {
|
|
595
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
596
|
+
path = `/shares/${doc.id}`;
|
|
597
|
+
}
|
|
598
|
+
const data = await apiFetch(config, path);
|
|
599
|
+
spin.stop();
|
|
600
|
+
if (json) {
|
|
601
|
+
console.log(JSON.stringify(data.shares));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (data.shares.length === 0) {
|
|
605
|
+
console.log(c.muted("No shares found."));
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const rows = data.shares.map((s) => [
|
|
609
|
+
s.hash,
|
|
610
|
+
s.title || s.slug || s.document_id,
|
|
611
|
+
String(s.view_count),
|
|
612
|
+
c.muted(s.created_at.slice(0, 10))
|
|
613
|
+
]);
|
|
614
|
+
console.log(table(rows, { header: ["Hash", "Document", "Views", c.muted("Created")] }));
|
|
615
|
+
}
|
|
616
|
+
async function shareRevoke(config, hash, yes, json) {
|
|
617
|
+
if (!yes) {
|
|
618
|
+
const confirmed = await confirm(`Revoke share ${c.bright(hash)}?`);
|
|
619
|
+
if (!confirmed) {
|
|
620
|
+
console.log(c.muted("Cancelled."));
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const spin = spinner("Revoking\u2026");
|
|
625
|
+
spin.start();
|
|
626
|
+
await apiFetch(config, `/shares/${hash}`, { method: "DELETE" });
|
|
627
|
+
spin.stop();
|
|
628
|
+
if (json) {
|
|
629
|
+
console.log(JSON.stringify({ revoked: true, hash }));
|
|
630
|
+
} else {
|
|
631
|
+
console.log(`${c.rose("Revoked:")} ${hash}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/commands/category.ts
|
|
636
|
+
async function catList(config, json) {
|
|
637
|
+
const spin = spinner("Loading categories\u2026");
|
|
638
|
+
spin.start();
|
|
639
|
+
const data = await apiFetch(config, "/categories");
|
|
640
|
+
spin.stop();
|
|
641
|
+
if (json) {
|
|
642
|
+
console.log(JSON.stringify(data.categories));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (data.categories.length === 0) {
|
|
646
|
+
console.log(c.muted("No categories found."));
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
const rows = data.categories.map((cat) => [
|
|
650
|
+
cat.icon,
|
|
651
|
+
cat.label,
|
|
652
|
+
c.muted(cat.slug),
|
|
653
|
+
String(cat.count)
|
|
654
|
+
]);
|
|
655
|
+
console.log(table(rows, { header: ["", "Label", c.muted("Slug"), "Docs"] }));
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// src/commands/tag.ts
|
|
659
|
+
async function tagList(config, json) {
|
|
660
|
+
const spin = spinner("Loading tags\u2026");
|
|
661
|
+
spin.start();
|
|
662
|
+
const data = await apiFetch(config, "/tags");
|
|
663
|
+
spin.stop();
|
|
664
|
+
if (json) {
|
|
665
|
+
console.log(JSON.stringify(data.tags));
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
if (data.tags.length === 0) {
|
|
669
|
+
console.log(c.muted("No tags found."));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const rows = data.tags.map((t) => [t.tag, String(t.count)]);
|
|
673
|
+
console.log(table(rows, { header: ["Tag", "Count"] }));
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/commands/version.ts
|
|
677
|
+
async function versionList(config, slugOrId, json) {
|
|
678
|
+
const spin = spinner("Resolving document\u2026");
|
|
679
|
+
spin.start();
|
|
680
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
681
|
+
spin.text = "Loading versions\u2026";
|
|
682
|
+
const data = await apiFetch(config, `/documents/${doc.id}/versions`);
|
|
683
|
+
spin.stop();
|
|
684
|
+
if (json) {
|
|
685
|
+
console.log(JSON.stringify(data.versions));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (data.versions.length === 0) {
|
|
689
|
+
console.log(c.muted("No versions found."));
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${data.versions.length} version${data.versions.length === 1 ? "" : "s"}
|
|
693
|
+
`);
|
|
694
|
+
const rows = data.versions.map((v, i) => [
|
|
695
|
+
i === 0 ? c.teal("current") : c.muted(`v${data.versions.length - i}`),
|
|
696
|
+
v.timestamp.replace("T", " ").replace(/\.\d+Z$/, ""),
|
|
697
|
+
v.word_count != null ? `${v.word_count} words` : c.muted("\u2014")
|
|
698
|
+
]);
|
|
699
|
+
console.log(table(rows, { header: ["", "Timestamp", "Words"] }));
|
|
700
|
+
}
|
|
701
|
+
async function versionRestore(config, slugOrId, yes, json) {
|
|
702
|
+
const spin = spinner("Resolving document\u2026");
|
|
703
|
+
spin.start();
|
|
704
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
705
|
+
spin.text = "Loading versions\u2026";
|
|
706
|
+
const data = await apiFetch(config, `/documents/${doc.id}/versions`);
|
|
707
|
+
spin.stop();
|
|
708
|
+
if (data.versions.length < 2) {
|
|
709
|
+
console.log(c.muted("No previous versions to restore."));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
const previous = data.versions.slice(1);
|
|
713
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${previous.length} previous version${previous.length === 1 ? "" : "s"}:
|
|
714
|
+
`);
|
|
715
|
+
for (let i = 0; i < previous.length; i++) {
|
|
716
|
+
const v = previous[i];
|
|
717
|
+
const label = `v${data.versions.length - 1 - i}`;
|
|
718
|
+
const ts = v.timestamp.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
719
|
+
const words = v.word_count != null ? ` (${v.word_count} words)` : "";
|
|
720
|
+
console.log(` ${c.amber(label)} ${ts}${c.muted(words)}`);
|
|
721
|
+
}
|
|
722
|
+
const target = previous[0];
|
|
723
|
+
console.log();
|
|
724
|
+
if (!yes) {
|
|
725
|
+
const ts = target.timestamp.replace("T", " ").replace(/\.\d+Z$/, "");
|
|
726
|
+
const confirmed = await confirm(`Restore to ${ts}?`);
|
|
727
|
+
if (!confirmed) {
|
|
728
|
+
console.log(c.muted("Cancelled."));
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const restoreSpin = spinner("Restoring\u2026");
|
|
733
|
+
restoreSpin.start();
|
|
734
|
+
await apiFetch(config, `/documents/${doc.id}/versions/restore`, {
|
|
735
|
+
method: "POST",
|
|
736
|
+
body: JSON.stringify({ timestamp: target.timestamp })
|
|
737
|
+
});
|
|
738
|
+
restoreSpin.stop();
|
|
739
|
+
if (json) {
|
|
740
|
+
console.log(JSON.stringify({ restored: true, slug: doc.slug, timestamp: target.timestamp }));
|
|
741
|
+
} else {
|
|
742
|
+
console.log(`${c.teal("Restored:")} ${doc.slug} to ${target.timestamp.replace("T", " ").replace(/\.\d+Z$/, "")}`);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/commands/note.ts
|
|
747
|
+
async function noteList(config, slugOrId, json) {
|
|
748
|
+
const spin = spinner("Resolving document\u2026");
|
|
749
|
+
spin.start();
|
|
750
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
751
|
+
spin.text = "Loading annotations\u2026";
|
|
752
|
+
const data = await apiFetch(
|
|
753
|
+
config,
|
|
754
|
+
`/annotations/doc/${doc.id}`
|
|
755
|
+
);
|
|
756
|
+
spin.stop();
|
|
757
|
+
if (json) {
|
|
758
|
+
console.log(JSON.stringify(data.annotations));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (data.annotations.length === 0) {
|
|
762
|
+
console.log(c.muted("No annotations found."));
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
765
|
+
console.log(`${c.bright(doc.slug)} \u2014 ${data.annotations.length} annotation${data.annotations.length === 1 ? "" : "s"}
|
|
766
|
+
`);
|
|
767
|
+
for (const a of data.annotations) {
|
|
768
|
+
const type = a.type === "highlight" ? c.amber("highlight") : a.type === "comment" ? c.blue("comment") : c.muted(a.type);
|
|
769
|
+
const date = c.muted(a.created_at.slice(0, 10));
|
|
770
|
+
console.log(` ${type} ${date}`);
|
|
771
|
+
if (a.selector_text) console.log(` ${c.muted(">")} ${a.selector_text.slice(0, 80)}${a.selector_text.length > 80 ? "\u2026" : ""}`);
|
|
772
|
+
if (a.body) console.log(` ${a.body.slice(0, 120)}${a.body.length > 120 ? "\u2026" : ""}`);
|
|
773
|
+
console.log();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
async function noteAdd(config, slugOrId, text, json) {
|
|
777
|
+
const spin = spinner("Resolving document\u2026");
|
|
778
|
+
spin.start();
|
|
779
|
+
const doc = await resolveSlug(config, slugOrId);
|
|
780
|
+
let body;
|
|
781
|
+
if (text) {
|
|
782
|
+
body = text;
|
|
783
|
+
} else {
|
|
784
|
+
if (process.stdin.isTTY) {
|
|
785
|
+
spin.stop();
|
|
786
|
+
throw new Error("No text provided. Use --text or pipe text via stdin.");
|
|
787
|
+
}
|
|
788
|
+
spin.stop();
|
|
789
|
+
const chunks = [];
|
|
790
|
+
for await (const chunk of process.stdin) {
|
|
791
|
+
chunks.push(chunk);
|
|
792
|
+
}
|
|
793
|
+
body = Buffer.concat(chunks).toString("utf-8").trim();
|
|
794
|
+
spin.start();
|
|
795
|
+
}
|
|
796
|
+
if (!body) {
|
|
797
|
+
spin.stop();
|
|
798
|
+
throw new Error("No text provided. Use --text or pipe to stdin");
|
|
799
|
+
}
|
|
800
|
+
spin.text = "Adding annotation\u2026";
|
|
801
|
+
const result = await apiFetch(
|
|
802
|
+
config,
|
|
803
|
+
`/annotations/doc/${doc.id}`,
|
|
804
|
+
{
|
|
805
|
+
method: "POST",
|
|
806
|
+
body: JSON.stringify({ type: "comment", body })
|
|
807
|
+
}
|
|
808
|
+
);
|
|
809
|
+
spin.stop();
|
|
810
|
+
if (json) {
|
|
811
|
+
console.log(JSON.stringify({ added: true, id: result.annotation.id, slug: doc.slug }));
|
|
812
|
+
} else {
|
|
813
|
+
console.log(`${c.teal("Added note to")} ${doc.slug}`);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async function noteSearch(config, query, json) {
|
|
817
|
+
const spin = spinner(`Searching annotations for "${query}"\u2026`);
|
|
818
|
+
spin.start();
|
|
819
|
+
const data = await apiFetch(
|
|
820
|
+
config,
|
|
821
|
+
`/annotations/search?q=${encodeURIComponent(query)}`
|
|
822
|
+
);
|
|
823
|
+
spin.stop();
|
|
824
|
+
if (json) {
|
|
825
|
+
console.log(JSON.stringify(data.annotations));
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (data.annotations.length === 0) {
|
|
829
|
+
console.log(c.muted(`No annotation results for "${query}".`));
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
const rows = data.annotations.map((a) => [
|
|
833
|
+
a.type === "highlight" ? c.amber("HL") : c.blue("CM"),
|
|
834
|
+
a.body.slice(0, 60) + (a.body.length > 60 ? "\u2026" : ""),
|
|
835
|
+
c.muted(a.created_at.slice(0, 10))
|
|
836
|
+
]);
|
|
837
|
+
console.log(table(rows, { header: ["", "Text", c.muted("Date")] }));
|
|
838
|
+
console.log(c.muted(`
|
|
839
|
+
${data.annotations.length} result${data.annotations.length === 1 ? "" : "s"}`));
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/commands/doctor.ts
|
|
843
|
+
import { existsSync } from "fs";
|
|
844
|
+
import { join } from "path";
|
|
845
|
+
async function doctor(config, json) {
|
|
846
|
+
const checks = [];
|
|
847
|
+
checks.push({ name: "CLI version", status: "ok", detail: `v${VERSION}` });
|
|
848
|
+
const keySource = process.env.ARTYFAX_API_KEY ? "ARTYFAX_API_KEY env var" : "--api-key flag";
|
|
849
|
+
checks.push({ name: "API key", status: "ok", detail: `configured via ${keySource}` });
|
|
850
|
+
checks.push({ name: "Endpoint", status: "ok", detail: config.endpoint });
|
|
851
|
+
try {
|
|
852
|
+
await apiFetch(config, "/categories");
|
|
853
|
+
checks.push({ name: "API reachable", status: "ok", detail: "connected" });
|
|
854
|
+
} catch (e) {
|
|
855
|
+
checks.push({ name: "API reachable", status: "fail", detail: e.message });
|
|
856
|
+
}
|
|
857
|
+
try {
|
|
858
|
+
const state = await apiFetch(config, "/account/secure-state");
|
|
859
|
+
if (state.salt && state.verify) {
|
|
860
|
+
checks.push({ name: "E2EE", status: "ok", detail: "passphrase configured" });
|
|
861
|
+
} else {
|
|
862
|
+
checks.push({ name: "E2EE", status: "warn", detail: "not configured (set up in web app)" });
|
|
863
|
+
}
|
|
864
|
+
} catch {
|
|
865
|
+
checks.push({ name: "E2EE", status: "warn", detail: "could not check" });
|
|
866
|
+
}
|
|
867
|
+
if (process.env.ARTYFAX_SECURE_PASSPHRASE) {
|
|
868
|
+
checks.push({ name: "Passphrase", status: "ok", detail: "ARTYFAX_SECURE_PASSPHRASE set" });
|
|
869
|
+
} else {
|
|
870
|
+
checks.push({ name: "Passphrase", status: "warn", detail: "not set (will prompt interactively)" });
|
|
871
|
+
}
|
|
872
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
873
|
+
const userSkillPath = join(homeDir, ".claude", "skills");
|
|
874
|
+
const hasUserSkill = existsSync(join(userSkillPath, "artyfax"));
|
|
875
|
+
const hasProjectSkill = existsSync(join(process.cwd(), ".claude", "skills", "artyfax"));
|
|
876
|
+
if (hasUserSkill || hasProjectSkill) {
|
|
877
|
+
const where = hasProjectSkill ? "project" : "user";
|
|
878
|
+
checks.push({ name: "Claude skill", status: "ok", detail: `installed (${where}-level)` });
|
|
879
|
+
} else {
|
|
880
|
+
checks.push({ name: "Claude skill", status: "warn", detail: "not installed (run `arty skill install`)" });
|
|
881
|
+
}
|
|
882
|
+
if (json) {
|
|
883
|
+
console.log(JSON.stringify(checks));
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
console.log(`${c.amber("artyfax doctor")} ${c.muted(`v${VERSION}`)}
|
|
887
|
+
`);
|
|
888
|
+
for (const check of checks) {
|
|
889
|
+
const icon = check.status === "ok" ? c.teal("\u2713") : check.status === "warn" ? c.amber("\u26A0") : c.rose("\u2717");
|
|
890
|
+
console.log(` ${icon} ${check.name.padEnd(16)} ${check.status === "fail" ? c.rose(check.detail) : check.detail}`);
|
|
891
|
+
}
|
|
892
|
+
const fails = checks.filter((ch) => ch.status === "fail");
|
|
893
|
+
const warns = checks.filter((ch) => ch.status === "warn");
|
|
894
|
+
console.log();
|
|
895
|
+
if (fails.length > 0) {
|
|
896
|
+
console.log(c.rose(`${fails.length} issue${fails.length === 1 ? "" : "s"} found.`));
|
|
897
|
+
} else if (warns.length > 0) {
|
|
898
|
+
console.log(c.amber(`All good, ${warns.length} optional improvement${warns.length === 1 ? "" : "s"}.`));
|
|
899
|
+
} else {
|
|
900
|
+
console.log(c.teal("Everything looks good."));
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// src/commands/skill.ts
|
|
905
|
+
async function skillInstall(_project) {
|
|
906
|
+
console.log(c.amber("Skill installation is not available yet."));
|
|
907
|
+
console.log(c.muted("The Artyfax skill content is being designed in design-28."));
|
|
908
|
+
console.log(c.muted("Once ready, `arty skill install` will write skill files to ~/.claude/skills/artyfax"));
|
|
909
|
+
}
|
|
910
|
+
async function skillStatus() {
|
|
911
|
+
console.log(c.amber("Skill status is not available yet."));
|
|
912
|
+
console.log(c.muted("Run `arty doctor` to check basic setup."));
|
|
913
|
+
}
|
|
914
|
+
async function skillUpdate() {
|
|
915
|
+
console.log(c.amber("Skill update is not available yet."));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// src/cli.ts
|
|
919
|
+
var VERSION = true ? "0.2.0" : "0.0.0-dev";
|
|
920
|
+
function brandedHelp() {
|
|
921
|
+
return `
|
|
922
|
+
${c.amber("artyfax")} ${c.muted(`v${VERSION}`)} \u2014 your personal document library
|
|
923
|
+
|
|
924
|
+
${c.bright("Usage:")} arty <command> [options]
|
|
925
|
+
|
|
926
|
+
${c.bright("Documents")}
|
|
927
|
+
save <file> Save a document (markdown or HTML)
|
|
928
|
+
read <slug> Read document content (handles E2EE)
|
|
929
|
+
list List documents
|
|
930
|
+
get <slug> Document metadata as JSON
|
|
931
|
+
search <query> Full-text search
|
|
932
|
+
update <slug> <file> Push updated content (auto-encrypts secure docs)
|
|
933
|
+
metadata <slug> Update document metadata
|
|
934
|
+
delete <slug> Delete a document
|
|
935
|
+
open <slug> Open document in browser
|
|
936
|
+
secure <slug> Encrypt a document
|
|
937
|
+
unsecure <slug> Remove encryption
|
|
938
|
+
|
|
939
|
+
${c.bright("Sub-resources")}
|
|
940
|
+
share create <slug> Create a share link
|
|
941
|
+
share list [slug] List shares
|
|
942
|
+
share revoke <hash> Revoke a share link
|
|
943
|
+
cat list List categories
|
|
944
|
+
tag list List tags
|
|
945
|
+
version list <slug> Version history
|
|
946
|
+
version restore <slug> Restore a previous version
|
|
947
|
+
|
|
948
|
+
${c.bright("Annotations")}
|
|
949
|
+
note list <slug> List annotations on a document
|
|
950
|
+
note add <slug> Add an annotation
|
|
951
|
+
note search <query> Search annotations
|
|
952
|
+
|
|
953
|
+
${c.bright("Tools")}
|
|
954
|
+
doctor Verify CLI setup
|
|
955
|
+
skill install Install Artyfax skills for Claude Code
|
|
956
|
+
skill status Check skill installation
|
|
957
|
+
skill update Update installed skills
|
|
958
|
+
|
|
959
|
+
${c.bright("Global options")}
|
|
960
|
+
--json Machine-readable JSON output
|
|
961
|
+
--yes, -y Skip confirmations
|
|
962
|
+
--api-key <key> API key (prefer ARTYFAX_API_KEY env var)
|
|
963
|
+
--endpoint <url> API endpoint (default: https://artyfax.io)
|
|
964
|
+
--help, -h Show help
|
|
965
|
+
--version, -v Show version
|
|
966
|
+
|
|
967
|
+
${c.bright("Environment")}
|
|
968
|
+
ARTYFAX_API_KEY API key for authentication
|
|
969
|
+
ARTYFAX_ENDPOINT API endpoint URL
|
|
970
|
+
ARTYFAX_SECURE_PASSPHRASE Passphrase for E2EE (avoids prompt)
|
|
971
|
+
`.trim();
|
|
972
|
+
}
|
|
973
|
+
var COMMAND_HELP = {
|
|
974
|
+
save: `${c.amber("arty save")} \u2014 save a document
|
|
975
|
+
|
|
976
|
+
${c.bright("Usage:")} arty save <file> [options]
|
|
977
|
+
arty save --url <url> [options]
|
|
978
|
+
|
|
979
|
+
${c.bright("Options:")}
|
|
980
|
+
--url <url> Save from URL (server-side extraction)
|
|
981
|
+
--secure Encrypt the document
|
|
982
|
+
--category <name> Category (default: inbox)
|
|
983
|
+
--format <md|html> Content format (default: md)
|
|
984
|
+
--tags <t1,t2> Comma-separated tags
|
|
985
|
+
--doc-type <type> Document type (default: original, or article for URLs)
|
|
986
|
+
--theme <name> Theme override
|
|
987
|
+
--parent <id> Parent document ID
|
|
988
|
+
--title <name> Title (default: derived from filename)
|
|
989
|
+
--json JSON output
|
|
990
|
+
|
|
991
|
+
${c.bright("Examples:")}
|
|
992
|
+
arty save notes.md --category inbox
|
|
993
|
+
arty save --url https://example.com/article --category articles
|
|
994
|
+
arty save report.md --secure --category reports`,
|
|
995
|
+
read: `${c.amber("arty read")} \u2014 read document content
|
|
996
|
+
|
|
997
|
+
${c.bright("Usage:")} arty read <slug>
|
|
998
|
+
|
|
999
|
+
Outputs the document's markdown source to stdout. Handles E2EE
|
|
1000
|
+
decryption automatically (prompts for passphrase if needed).
|
|
1001
|
+
|
|
1002
|
+
${c.bright("Examples:")}
|
|
1003
|
+
arty read inbox/my-doc
|
|
1004
|
+
arty read my-doc # partial slug match
|
|
1005
|
+
arty read inbox/my-doc > out.md # pipe to file`,
|
|
1006
|
+
list: `${c.amber("arty list")} \u2014 list documents
|
|
1007
|
+
|
|
1008
|
+
${c.bright("Usage:")} arty list [options]
|
|
1009
|
+
|
|
1010
|
+
${c.bright("Options:")}
|
|
1011
|
+
--category <name> Filter by category
|
|
1012
|
+
--limit <n> Max results (default: 20)
|
|
1013
|
+
--offset <n> Skip first N results
|
|
1014
|
+
--archived Include archived documents
|
|
1015
|
+
--parent-id <id> Filter by parent document
|
|
1016
|
+
--json JSON output
|
|
1017
|
+
|
|
1018
|
+
${c.bright("Examples:")}
|
|
1019
|
+
arty list --category inbox --limit 10
|
|
1020
|
+
arty list --archived --json`,
|
|
1021
|
+
get: `${c.amber("arty get")} \u2014 document metadata
|
|
1022
|
+
|
|
1023
|
+
${c.bright("Usage:")} arty get <slug>
|
|
1024
|
+
|
|
1025
|
+
Outputs full document metadata as JSON. For scripting and inspection.
|
|
1026
|
+
|
|
1027
|
+
${c.bright("Examples:")}
|
|
1028
|
+
arty get inbox/my-doc
|
|
1029
|
+
arty get my-doc | jq .category`,
|
|
1030
|
+
search: `${c.amber("arty search")} \u2014 full-text search
|
|
1031
|
+
|
|
1032
|
+
${c.bright("Usage:")} arty search <query> [--json]
|
|
1033
|
+
|
|
1034
|
+
${c.bright("Examples:")}
|
|
1035
|
+
arty search "deployment guide"
|
|
1036
|
+
arty search cloudflare --json`,
|
|
1037
|
+
update: `${c.amber("arty update")} \u2014 push updated content
|
|
1038
|
+
|
|
1039
|
+
${c.bright("Usage:")} arty update <slug> [file]
|
|
1040
|
+
cat file.md | arty update <slug>
|
|
1041
|
+
|
|
1042
|
+
Reads from file or stdin. Auto-encrypts if the target is a secure document.
|
|
1043
|
+
|
|
1044
|
+
${c.bright("Examples:")}
|
|
1045
|
+
arty update inbox/my-doc updated.md
|
|
1046
|
+
echo "# New content" | arty update inbox/my-doc`,
|
|
1047
|
+
metadata: `${c.amber("arty metadata")} \u2014 update document metadata
|
|
1048
|
+
|
|
1049
|
+
${c.bright("Usage:")} arty metadata <slug> [options]
|
|
1050
|
+
|
|
1051
|
+
${c.bright("Options:")}
|
|
1052
|
+
--category <name> Move to category
|
|
1053
|
+
--tags <t1,t2> Set tags (comma-separated)
|
|
1054
|
+
--title <name> Update title
|
|
1055
|
+
--theme <name> Set theme
|
|
1056
|
+
--visibility <v> Set visibility (private/public)
|
|
1057
|
+
--archived Archive the document
|
|
1058
|
+
--json JSON output
|
|
1059
|
+
|
|
1060
|
+
${c.bright("Examples:")}
|
|
1061
|
+
arty metadata inbox/my-doc --category reports --tags "q1,finance"
|
|
1062
|
+
arty metadata my-doc --archived`,
|
|
1063
|
+
delete: `${c.amber("arty delete")} \u2014 delete a document
|
|
1064
|
+
|
|
1065
|
+
${c.bright("Usage:")} arty delete <slug> [--yes] [--json]
|
|
1066
|
+
|
|
1067
|
+
Prompts for confirmation unless --yes is passed.
|
|
1068
|
+
|
|
1069
|
+
${c.bright("Examples:")}
|
|
1070
|
+
arty delete inbox/old-doc
|
|
1071
|
+
arty delete inbox/old-doc --yes # skip confirmation`,
|
|
1072
|
+
open: `${c.amber("arty open")} \u2014 open in browser
|
|
1073
|
+
|
|
1074
|
+
${c.bright("Usage:")} arty open <slug>
|
|
1075
|
+
|
|
1076
|
+
Opens the document URL in your default browser.`,
|
|
1077
|
+
share: `${c.amber("arty share")} \u2014 manage share links
|
|
1078
|
+
|
|
1079
|
+
${c.bright("Usage:")} arty share create <slug> Create a share link
|
|
1080
|
+
arty share list [slug] List all shares (or per-document)
|
|
1081
|
+
arty share revoke <hash> Revoke a share link
|
|
1082
|
+
|
|
1083
|
+
${c.bright("Examples:")}
|
|
1084
|
+
arty share create inbox/my-doc
|
|
1085
|
+
arty share list --json
|
|
1086
|
+
arty share revoke SnP5UJUhP6 --yes`,
|
|
1087
|
+
version: `${c.amber("arty version")} \u2014 version history
|
|
1088
|
+
|
|
1089
|
+
${c.bright("Usage:")} arty version list <slug> Show version history
|
|
1090
|
+
arty version restore <slug> Restore a previous version
|
|
1091
|
+
|
|
1092
|
+
${c.bright("Examples:")}
|
|
1093
|
+
arty version list inbox/my-doc
|
|
1094
|
+
arty version restore inbox/my-doc --yes`,
|
|
1095
|
+
note: `${c.amber("arty note")} \u2014 annotations
|
|
1096
|
+
|
|
1097
|
+
${c.bright("Usage:")} arty note list <slug> List annotations
|
|
1098
|
+
arty note add <slug> Add a note (--text or stdin)
|
|
1099
|
+
arty note search <query> Search annotations
|
|
1100
|
+
|
|
1101
|
+
${c.bright("Examples:")}
|
|
1102
|
+
arty note list inbox/my-doc
|
|
1103
|
+
arty note add inbox/my-doc --text "Needs review"
|
|
1104
|
+
echo "Reviewed and approved" | arty note add inbox/my-doc
|
|
1105
|
+
arty note search "review"`,
|
|
1106
|
+
doctor: `${c.amber("arty doctor")} \u2014 verify CLI setup
|
|
1107
|
+
|
|
1108
|
+
Checks API key, endpoint, connectivity, E2EE status, and skill installation.
|
|
1109
|
+
|
|
1110
|
+
${c.bright("Usage:")} arty doctor [--json]`,
|
|
1111
|
+
skill: `${c.amber("arty skill")} \u2014 Claude Code skill management
|
|
1112
|
+
|
|
1113
|
+
${c.bright("Usage:")} arty skill install [--project] Install Artyfax skill
|
|
1114
|
+
arty skill status Check installation
|
|
1115
|
+
arty skill update Update to latest version`
|
|
1116
|
+
};
|
|
1117
|
+
function parseArgs(args) {
|
|
1118
|
+
const flags = {};
|
|
1119
|
+
const positional = [];
|
|
1120
|
+
let command = "";
|
|
1121
|
+
let subcommand = "";
|
|
1122
|
+
for (let i = 0; i < args.length; i++) {
|
|
1123
|
+
const arg = args[i];
|
|
1124
|
+
if (arg.startsWith("--")) {
|
|
1125
|
+
const key = arg.slice(2);
|
|
1126
|
+
const next = args[i + 1];
|
|
1127
|
+
if (next && !next.startsWith("-")) {
|
|
1128
|
+
flags[key] = next;
|
|
1129
|
+
i++;
|
|
1130
|
+
} else {
|
|
1131
|
+
flags[key] = true;
|
|
1132
|
+
}
|
|
1133
|
+
} else if (arg.startsWith("-")) {
|
|
1134
|
+
flags[arg.slice(1)] = true;
|
|
1135
|
+
} else if (!command) {
|
|
1136
|
+
command = arg;
|
|
1137
|
+
} else if (!subcommand && isSubResource(command)) {
|
|
1138
|
+
subcommand = arg;
|
|
1139
|
+
} else {
|
|
1140
|
+
positional.push(arg);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
return { command, subcommand, positional, flags };
|
|
1144
|
+
}
|
|
1145
|
+
var SUB_RESOURCES = /* @__PURE__ */ new Set(["share", "cat", "tag", "version", "note", "skill"]);
|
|
1146
|
+
function isSubResource(cmd) {
|
|
1147
|
+
return SUB_RESOURCES.has(cmd);
|
|
1148
|
+
}
|
|
1149
|
+
async function main() {
|
|
1150
|
+
const parsed = parseArgs(process.argv.slice(2));
|
|
1151
|
+
const { command, subcommand, positional, flags } = parsed;
|
|
1152
|
+
const json = !!flags.json;
|
|
1153
|
+
const yes = !!(flags.yes || flags.y);
|
|
1154
|
+
if (flags.version || flags.v) {
|
|
1155
|
+
console.log(json ? JSON.stringify({ version: VERSION }) : `artyfax v${VERSION}`);
|
|
1156
|
+
process.exit(0);
|
|
1157
|
+
}
|
|
1158
|
+
if (flags.help || flags.h) {
|
|
1159
|
+
if (command && COMMAND_HELP[command]) {
|
|
1160
|
+
console.log(COMMAND_HELP[command]);
|
|
1161
|
+
} else {
|
|
1162
|
+
console.log(brandedHelp());
|
|
1163
|
+
}
|
|
1164
|
+
process.exit(0);
|
|
1165
|
+
}
|
|
1166
|
+
if (!command) {
|
|
1167
|
+
console.log(brandedHelp());
|
|
1168
|
+
process.exit(0);
|
|
1169
|
+
}
|
|
1170
|
+
const KNOWN_COMMANDS = /* @__PURE__ */ new Set([
|
|
1171
|
+
"save",
|
|
1172
|
+
"read",
|
|
1173
|
+
"list",
|
|
1174
|
+
"get",
|
|
1175
|
+
"search",
|
|
1176
|
+
"delete",
|
|
1177
|
+
"open",
|
|
1178
|
+
"secure",
|
|
1179
|
+
"unsecure",
|
|
1180
|
+
"update",
|
|
1181
|
+
"metadata",
|
|
1182
|
+
"share",
|
|
1183
|
+
"cat",
|
|
1184
|
+
"tag",
|
|
1185
|
+
"version",
|
|
1186
|
+
"note",
|
|
1187
|
+
"skill",
|
|
1188
|
+
"doctor"
|
|
1189
|
+
]);
|
|
1190
|
+
if (!KNOWN_COMMANDS.has(command)) {
|
|
1191
|
+
error(`Unknown command: ${command}`, `Run \`arty --help\` for available commands`);
|
|
1192
|
+
}
|
|
1193
|
+
if (command === "skill") {
|
|
1194
|
+
switch (subcommand) {
|
|
1195
|
+
case "install":
|
|
1196
|
+
await skillInstall(!!flags.project);
|
|
1197
|
+
break;
|
|
1198
|
+
case "status":
|
|
1199
|
+
await skillStatus();
|
|
1200
|
+
break;
|
|
1201
|
+
case "update":
|
|
1202
|
+
await skillUpdate();
|
|
1203
|
+
break;
|
|
1204
|
+
default:
|
|
1205
|
+
error(`Unknown skill subcommand: ${subcommand || "(none)"}`, "Usage: arty skill <install|status|update>");
|
|
1206
|
+
}
|
|
1207
|
+
process.exit(0);
|
|
1208
|
+
}
|
|
1209
|
+
const config = getConfig(flags);
|
|
1210
|
+
if (command === "doctor") {
|
|
1211
|
+
await doctor(config, json);
|
|
1212
|
+
process.exit(0);
|
|
1213
|
+
}
|
|
1214
|
+
switch (command) {
|
|
1215
|
+
case "save": {
|
|
1216
|
+
const fileOrUrl = positional[0];
|
|
1217
|
+
const url = flags.url;
|
|
1218
|
+
if (!fileOrUrl && !url) {
|
|
1219
|
+
error("Missing file argument", "Usage: arty save <file> [--secure] [--category <name>]");
|
|
1220
|
+
}
|
|
1221
|
+
if (url) {
|
|
1222
|
+
await save(config, {
|
|
1223
|
+
title: flags.title || "",
|
|
1224
|
+
content: "",
|
|
1225
|
+
category: flags.category || "inbox",
|
|
1226
|
+
format: "md",
|
|
1227
|
+
secure: !!flags.secure,
|
|
1228
|
+
url,
|
|
1229
|
+
tags: flags.tags,
|
|
1230
|
+
docType: flags["doc-type"] || "article",
|
|
1231
|
+
theme: flags.theme,
|
|
1232
|
+
parentId: flags.parent,
|
|
1233
|
+
json
|
|
1234
|
+
});
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
const format = flags.format || "md";
|
|
1238
|
+
if (format !== "md" && format !== "html") {
|
|
1239
|
+
error('--format must be "md" or "html"');
|
|
1240
|
+
}
|
|
1241
|
+
let content;
|
|
1242
|
+
try {
|
|
1243
|
+
content = readFileSync2(resolve2(fileOrUrl), "utf-8");
|
|
1244
|
+
} catch {
|
|
1245
|
+
error(`File not found: ${fileOrUrl}`);
|
|
1246
|
+
}
|
|
1247
|
+
const title = flags.title || fileOrUrl.replace(/\.[^.]+$/, "").replace(/[-_]/g, " ");
|
|
1248
|
+
await save(config, {
|
|
1249
|
+
title,
|
|
1250
|
+
content,
|
|
1251
|
+
category: flags.category || "inbox",
|
|
1252
|
+
format,
|
|
1253
|
+
secure: !!flags.secure,
|
|
1254
|
+
tags: flags.tags,
|
|
1255
|
+
docType: flags["doc-type"] || "original",
|
|
1256
|
+
theme: flags.theme,
|
|
1257
|
+
parentId: flags.parent,
|
|
1258
|
+
json
|
|
1259
|
+
});
|
|
1260
|
+
break;
|
|
1261
|
+
}
|
|
1262
|
+
case "read": {
|
|
1263
|
+
const slug = positional[0];
|
|
1264
|
+
if (!slug) error("Missing slug argument", "Usage: arty read <slug>");
|
|
1265
|
+
await read(config, slug, !!flags.secure);
|
|
1266
|
+
break;
|
|
1267
|
+
}
|
|
1268
|
+
case "list": {
|
|
1269
|
+
let limit = 20;
|
|
1270
|
+
if (flags.limit) {
|
|
1271
|
+
limit = parseInt(flags.limit, 10);
|
|
1272
|
+
if (isNaN(limit) || limit < 1) error("--limit must be a positive integer");
|
|
1273
|
+
}
|
|
1274
|
+
let offset = 0;
|
|
1275
|
+
if (flags.offset) {
|
|
1276
|
+
offset = parseInt(flags.offset, 10);
|
|
1277
|
+
if (isNaN(offset) || offset < 0) error("--offset must be a non-negative integer");
|
|
1278
|
+
}
|
|
1279
|
+
await list(config, {
|
|
1280
|
+
category: flags.category,
|
|
1281
|
+
limit,
|
|
1282
|
+
offset,
|
|
1283
|
+
archived: !!flags.archived,
|
|
1284
|
+
parentId: flags["parent-id"],
|
|
1285
|
+
json
|
|
1286
|
+
});
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
case "get": {
|
|
1290
|
+
const slug = positional[0];
|
|
1291
|
+
if (!slug) error("Missing slug argument", "Usage: arty get <slug>");
|
|
1292
|
+
await get(config, slug);
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
case "search": {
|
|
1296
|
+
const query = positional.join(" ");
|
|
1297
|
+
if (!query) error("Missing search query", "Usage: arty search <query>");
|
|
1298
|
+
await search(config, query, json);
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
case "delete": {
|
|
1302
|
+
const slug = positional[0];
|
|
1303
|
+
if (!slug) error("Missing slug argument", "Usage: arty delete <slug>");
|
|
1304
|
+
await del(config, slug, yes, json);
|
|
1305
|
+
break;
|
|
1306
|
+
}
|
|
1307
|
+
case "open": {
|
|
1308
|
+
const slug = positional[0];
|
|
1309
|
+
if (!slug) error("Missing slug argument", "Usage: arty open <slug>");
|
|
1310
|
+
await open(config, slug);
|
|
1311
|
+
break;
|
|
1312
|
+
}
|
|
1313
|
+
case "secure": {
|
|
1314
|
+
const slug = positional[0];
|
|
1315
|
+
if (!slug) error("Missing slug argument", "Usage: arty secure <slug>");
|
|
1316
|
+
await secure(config, slug);
|
|
1317
|
+
break;
|
|
1318
|
+
}
|
|
1319
|
+
case "unsecure": {
|
|
1320
|
+
const slug = positional[0];
|
|
1321
|
+
if (!slug) error("Missing slug argument", "Usage: arty unsecure <slug>");
|
|
1322
|
+
await unsecure(config, slug);
|
|
1323
|
+
break;
|
|
1324
|
+
}
|
|
1325
|
+
case "update": {
|
|
1326
|
+
const slug = positional[0];
|
|
1327
|
+
if (!slug) error("Missing slug argument", "Usage: arty update <slug> [file] (reads from stdin if no file)");
|
|
1328
|
+
await update(config, slug, positional[1], json);
|
|
1329
|
+
break;
|
|
1330
|
+
}
|
|
1331
|
+
case "metadata": {
|
|
1332
|
+
const slug = positional[0];
|
|
1333
|
+
if (!slug) error("Missing slug argument", "Usage: arty metadata <slug> --category <name> --tags <t1,t2>");
|
|
1334
|
+
await metadata(config, slug, {
|
|
1335
|
+
category: flags.category,
|
|
1336
|
+
tags: flags.tags,
|
|
1337
|
+
title: flags.title,
|
|
1338
|
+
theme: flags.theme,
|
|
1339
|
+
visibility: flags.visibility,
|
|
1340
|
+
archived: flags.archived === true ? true : flags.archived === "false" ? false : void 0,
|
|
1341
|
+
json
|
|
1342
|
+
});
|
|
1343
|
+
break;
|
|
1344
|
+
}
|
|
1345
|
+
case "share": {
|
|
1346
|
+
switch (subcommand) {
|
|
1347
|
+
case "create": {
|
|
1348
|
+
const slug = positional[0];
|
|
1349
|
+
if (!slug) error("Missing slug argument", "Usage: arty share create <slug>");
|
|
1350
|
+
await shareCreate(config, slug, json);
|
|
1351
|
+
break;
|
|
1352
|
+
}
|
|
1353
|
+
case "list":
|
|
1354
|
+
await shareList(config, positional[0], json);
|
|
1355
|
+
break;
|
|
1356
|
+
case "revoke": {
|
|
1357
|
+
const hash = positional[0];
|
|
1358
|
+
if (!hash) error("Missing share hash", "Usage: arty share revoke <hash>");
|
|
1359
|
+
await shareRevoke(config, hash, yes, json);
|
|
1360
|
+
break;
|
|
1361
|
+
}
|
|
1362
|
+
default:
|
|
1363
|
+
error(`Unknown share subcommand: ${subcommand || "(none)"}`, "Usage: arty share <create|list|revoke>");
|
|
1364
|
+
}
|
|
1365
|
+
break;
|
|
1366
|
+
}
|
|
1367
|
+
case "cat": {
|
|
1368
|
+
if (subcommand !== "list") error(`Unknown cat subcommand: ${subcommand || "(none)"}`, "Usage: arty cat list");
|
|
1369
|
+
await catList(config, json);
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case "tag": {
|
|
1373
|
+
if (subcommand !== "list") error(`Unknown tag subcommand: ${subcommand || "(none)"}`, "Usage: arty tag list");
|
|
1374
|
+
await tagList(config, json);
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
case "version": {
|
|
1378
|
+
switch (subcommand) {
|
|
1379
|
+
case "list": {
|
|
1380
|
+
const slug = positional[0];
|
|
1381
|
+
if (!slug) error("Missing slug argument", "Usage: arty version list <slug>");
|
|
1382
|
+
await versionList(config, slug, json);
|
|
1383
|
+
break;
|
|
1384
|
+
}
|
|
1385
|
+
case "restore": {
|
|
1386
|
+
const slug = positional[0];
|
|
1387
|
+
if (!slug) error("Missing slug argument", "Usage: arty version restore <slug>");
|
|
1388
|
+
await versionRestore(config, slug, yes, json);
|
|
1389
|
+
break;
|
|
1390
|
+
}
|
|
1391
|
+
default:
|
|
1392
|
+
error(`Unknown version subcommand: ${subcommand || "(none)"}`, "Usage: arty version <list|restore>");
|
|
1393
|
+
}
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
1396
|
+
case "note": {
|
|
1397
|
+
switch (subcommand) {
|
|
1398
|
+
case "list": {
|
|
1399
|
+
const slug = positional[0];
|
|
1400
|
+
if (!slug) error("Missing slug argument", "Usage: arty note list <slug>");
|
|
1401
|
+
await noteList(config, slug, json);
|
|
1402
|
+
break;
|
|
1403
|
+
}
|
|
1404
|
+
case "add": {
|
|
1405
|
+
const slug = positional[0];
|
|
1406
|
+
if (!slug) error("Missing slug argument", 'Usage: arty note add <slug> --text "..."');
|
|
1407
|
+
await noteAdd(config, slug, flags.text, json);
|
|
1408
|
+
break;
|
|
1409
|
+
}
|
|
1410
|
+
case "search": {
|
|
1411
|
+
const query = positional.join(" ");
|
|
1412
|
+
if (!query) error("Missing search query", "Usage: arty note search <query>");
|
|
1413
|
+
await noteSearch(config, query, json);
|
|
1414
|
+
break;
|
|
1415
|
+
}
|
|
1416
|
+
default:
|
|
1417
|
+
error(`Unknown note subcommand: ${subcommand || "(none)"}`, "Usage: arty note <list|add|search>");
|
|
1418
|
+
}
|
|
1419
|
+
break;
|
|
1420
|
+
}
|
|
1421
|
+
default:
|
|
1422
|
+
error(`Unknown command: ${command}`, `Run \`arty --help\` for available commands`);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
main().catch((e) => {
|
|
1426
|
+
console.error(c.rose(e.message || String(e)));
|
|
1427
|
+
process.exit(1);
|
|
1428
|
+
});
|
|
1429
|
+
export {
|
|
1430
|
+
VERSION,
|
|
1431
|
+
parseArgs
|
|
1432
|
+
};
|