@profullstack/nichedb 0.10.0 → 0.12.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/package.json +1 -1
- package/src/index.js +197 -2
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
* `bin/` shim executes it.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { spawnSync } from 'node:child_process';
|
|
12
13
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
|
+
import { homedir, tmpdir } from 'node:os';
|
|
14
15
|
import { join } from 'node:path';
|
|
15
16
|
import { createInterface } from 'node:readline';
|
|
16
17
|
|
|
17
|
-
export const VERSION = '0.
|
|
18
|
+
export const VERSION = '0.12.0';
|
|
18
19
|
const DEFAULT_API = process.env.NICHEDB_API ?? 'https://nichedb.dev';
|
|
19
20
|
const CONFIG_DIR = join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'nichedb');
|
|
20
21
|
const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
@@ -36,6 +37,12 @@ export const COMMANDS = [
|
|
|
36
37
|
summary: 'Sources with status, last run and item counts.',
|
|
37
38
|
options: ['--collection <slug>', '--json'],
|
|
38
39
|
},
|
|
40
|
+
{
|
|
41
|
+
name: 'submit',
|
|
42
|
+
usage: 'submit <url> [--collection <slug>] [--note …] [--email …]',
|
|
43
|
+
summary: 'Suggest a feed for the index. No key needed; an admin reviews it.',
|
|
44
|
+
options: ['--collection <slug>', '--note <text>', '--email <address>', '--json'],
|
|
45
|
+
},
|
|
39
46
|
{
|
|
40
47
|
name: 'source',
|
|
41
48
|
usage: 'source <slug>',
|
|
@@ -171,6 +178,38 @@ export const COMMANDS = [
|
|
|
171
178
|
summary: 'Print the RSS URL (or the feed itself with --fetch).',
|
|
172
179
|
options: ['--fetch'],
|
|
173
180
|
},
|
|
181
|
+
{
|
|
182
|
+
name: 'profiles',
|
|
183
|
+
usage: 'profiles [<query>] [--since <iso>] [--mine]',
|
|
184
|
+
summary: 'People with an OpenProfile.md: search them, or list the ones your key owns.',
|
|
185
|
+
options: ['--since <iso>', '--mine', '--limit', '--json', '--urls'],
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: 'profile',
|
|
189
|
+
usage: 'profile <id|slug-id|handle>',
|
|
190
|
+
summary: 'One person’s OpenProfile.md, as served. --json for the parsed view.',
|
|
191
|
+
options: ['--json'],
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
name: 'profile claim',
|
|
195
|
+
usage: 'profile claim <ref> [--email <address>]',
|
|
196
|
+
summary:
|
|
197
|
+
'Claim a profile as yours (key required): proven by your email or a link back from your site.',
|
|
198
|
+
options: ['--email (admins: claim it for someone)'],
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
name: 'profile edit',
|
|
202
|
+
usage: 'profile edit <ref> [--file openprofile.md] [--handle …] [--public|--private]',
|
|
203
|
+
summary:
|
|
204
|
+
'Edit a profile you own (key required). Opens $EDITOR on the file when no --file is given.',
|
|
205
|
+
options: ['--file <path>', '--handle <handle>', '--public', '--private', '--json'],
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
name: 'profile handle',
|
|
209
|
+
usage: 'profile handle <ref> <handle>',
|
|
210
|
+
summary: 'Take a handle: your URL becomes /c/profiles/<handle> (key required).',
|
|
211
|
+
options: [],
|
|
212
|
+
},
|
|
174
213
|
{
|
|
175
214
|
name: 'login',
|
|
176
215
|
usage: 'login [--api <url>] [--key <ndb_…>]',
|
|
@@ -259,13 +298,51 @@ export function makeClient({ api, key, fetchImpl = fetch }) {
|
|
|
259
298
|
if (!res.ok) throw new Error(data?.error ?? `${res.status} from ${path}`);
|
|
260
299
|
return data;
|
|
261
300
|
}
|
|
301
|
+
/** A body that is not JSON: a whole file, sent as its own media type. */
|
|
302
|
+
async function send(method, path, text, contentType) {
|
|
303
|
+
const res = await fetchImpl(`${base}${path}`, {
|
|
304
|
+
method,
|
|
305
|
+
headers: {
|
|
306
|
+
accept: 'application/json',
|
|
307
|
+
'user-agent': `nichedb-cli/${VERSION}`,
|
|
308
|
+
'content-type': contentType,
|
|
309
|
+
...(key ? { authorization: `Bearer ${key}` } : {}),
|
|
310
|
+
},
|
|
311
|
+
body: text,
|
|
312
|
+
});
|
|
313
|
+
const raw = await res.text();
|
|
314
|
+
let data;
|
|
315
|
+
try {
|
|
316
|
+
data = JSON.parse(raw);
|
|
317
|
+
} catch {
|
|
318
|
+
data = { raw };
|
|
319
|
+
}
|
|
320
|
+
if (!res.ok) throw new Error(data?.error ?? `${res.status} from ${path}`);
|
|
321
|
+
return data;
|
|
322
|
+
}
|
|
323
|
+
/** A document, not JSON: an openprofile.md as served. */
|
|
324
|
+
async function text(path) {
|
|
325
|
+
const res = await fetchImpl(`${base}${path}`, {
|
|
326
|
+
headers: {
|
|
327
|
+
accept: 'text/markdown, text/plain',
|
|
328
|
+
'user-agent': `nichedb-cli/${VERSION}`,
|
|
329
|
+
...(key ? { authorization: `Bearer ${key}` } : {}),
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
const body = await res.text();
|
|
333
|
+
if (!res.ok) throw new Error(`${res.status} from ${path}`);
|
|
334
|
+
return body;
|
|
335
|
+
}
|
|
262
336
|
return {
|
|
263
337
|
base,
|
|
264
338
|
key,
|
|
265
339
|
get: (p) => call('GET', p),
|
|
266
340
|
post: (p, b) => call('POST', p, b ?? {}),
|
|
267
341
|
patch: (p, b) => call('PATCH', p, b),
|
|
342
|
+
put: (p, b) => call('PUT', p, b),
|
|
268
343
|
del: (p) => call('DELETE', p),
|
|
344
|
+
send,
|
|
345
|
+
text,
|
|
269
346
|
};
|
|
270
347
|
}
|
|
271
348
|
|
|
@@ -398,6 +475,29 @@ export async function run(
|
|
|
398
475
|
}
|
|
399
476
|
return 0;
|
|
400
477
|
}
|
|
478
|
+
case 'submit': {
|
|
479
|
+
const [url] = rest;
|
|
480
|
+
if (!url) {
|
|
481
|
+
process.stderr.write(
|
|
482
|
+
'usage: nichedb submit <url> [--collection <slug>] [--note …] [--email …]\n',
|
|
483
|
+
);
|
|
484
|
+
return 2;
|
|
485
|
+
}
|
|
486
|
+
const { submission, duplicate } = await client.post('/api/v1/submissions', {
|
|
487
|
+
url,
|
|
488
|
+
collection: flags.collection,
|
|
489
|
+
note: flags.note,
|
|
490
|
+
email: flags.email,
|
|
491
|
+
});
|
|
492
|
+
out(
|
|
493
|
+
json
|
|
494
|
+
? JSON.stringify({ submission, duplicate }, null, 2)
|
|
495
|
+
: duplicate
|
|
496
|
+
? `Already suggested and waiting for review: ${submission.feed_url}`
|
|
497
|
+
: `Suggested ${submission.feed_url}. An admin will review it.`,
|
|
498
|
+
);
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
401
501
|
case 'sources': {
|
|
402
502
|
const qs = flags.collection ? `?collection=${encodeURIComponent(flags.collection)}` : '';
|
|
403
503
|
const { sources } = await client.get(`/api/v1/sources${qs}`);
|
|
@@ -628,6 +728,101 @@ export async function run(
|
|
|
628
728
|
out(await res.text());
|
|
629
729
|
return 0;
|
|
630
730
|
}
|
|
731
|
+
case 'profiles': {
|
|
732
|
+
const term = rest.join(' ');
|
|
733
|
+
const qs = new URLSearchParams();
|
|
734
|
+
if (term) qs.set('q', term);
|
|
735
|
+
if (flags.since) qs.set('since', flags.since);
|
|
736
|
+
if (flags.limit) qs.set('limit', flags.limit);
|
|
737
|
+
if (flags.mine) qs.set('mine', '1');
|
|
738
|
+
const { profiles } = await client.get(`/api/v1/profiles?${qs}`);
|
|
739
|
+
if (json) {
|
|
740
|
+
out(JSON.stringify(profiles, null, 2));
|
|
741
|
+
return 0;
|
|
742
|
+
}
|
|
743
|
+
for (const p of profiles) {
|
|
744
|
+
if (flags.urls) {
|
|
745
|
+
out(p.page);
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
748
|
+
out(`${pad(p.ref, 34)} ${pad(p.kind ?? '', 12)} ${p.name}${p.claimed ? ' (claimed)' : ''}`);
|
|
749
|
+
if (p.headline) out(` ${p.headline}`);
|
|
750
|
+
}
|
|
751
|
+
if (profiles.length === 0) out('(nobody yet)');
|
|
752
|
+
return 0;
|
|
753
|
+
}
|
|
754
|
+
case 'profile': {
|
|
755
|
+
const [sub, arg, extra] = rest;
|
|
756
|
+
if (sub === 'claim') {
|
|
757
|
+
if (!arg) throw new Error('profile claim <ref> [--email …]');
|
|
758
|
+
const r = await client.post(`/api/v1/profiles/${encodeURIComponent(arg)}/claim`, {
|
|
759
|
+
email: flags.email,
|
|
760
|
+
});
|
|
761
|
+
out(
|
|
762
|
+
json
|
|
763
|
+
? JSON.stringify(r, null, 2)
|
|
764
|
+
: r.already
|
|
765
|
+
? `Already yours: ${r.profile.page}`
|
|
766
|
+
: `Claimed by ${r.method}: ${r.profile.page}`,
|
|
767
|
+
);
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
if (sub === 'handle') {
|
|
771
|
+
if (!arg || !extra) throw new Error('profile handle <ref> <handle>');
|
|
772
|
+
const { profile } = await client.put(`/api/v1/profiles/${encodeURIComponent(arg)}`, {
|
|
773
|
+
handle: extra,
|
|
774
|
+
});
|
|
775
|
+
out(json ? JSON.stringify(profile, null, 2) : `Now at ${profile.page}`);
|
|
776
|
+
return 0;
|
|
777
|
+
}
|
|
778
|
+
if (sub === 'edit') {
|
|
779
|
+
if (!arg)
|
|
780
|
+
throw new Error('profile edit <ref> [--file …] [--handle …] [--public|--private]');
|
|
781
|
+
const ref = encodeURIComponent(arg);
|
|
782
|
+
let markdown = null;
|
|
783
|
+
if (flags.file) markdown = await readFile(String(flags.file), 'utf8');
|
|
784
|
+
else if (!flags.handle && !flags.public && !flags.private) {
|
|
785
|
+
// No file and nothing else to set: the editor, on the file as served.
|
|
786
|
+
const current = await client.text(`/api/v1/profiles/${ref}/openprofile.md`);
|
|
787
|
+
const path = join(tmpdir(), `nichedb-profile-${Date.now()}.md`);
|
|
788
|
+
await writeFile(path, current, { mode: 0o600 });
|
|
789
|
+
const editor = process.env.VISUAL ?? process.env.EDITOR ?? 'vi';
|
|
790
|
+
const r = spawnSync(editor, [path], {
|
|
791
|
+
stdio: 'inherit',
|
|
792
|
+
shell: process.platform === 'win32',
|
|
793
|
+
});
|
|
794
|
+
if (r.status !== 0) throw new Error(`${editor} exited ${r.status}; nothing saved.`);
|
|
795
|
+
markdown = await readFile(path, 'utf8');
|
|
796
|
+
if (markdown === current) {
|
|
797
|
+
out('No change.');
|
|
798
|
+
return 0;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
let answer = null;
|
|
802
|
+
if (markdown !== null)
|
|
803
|
+
answer = await client.send(
|
|
804
|
+
'PUT',
|
|
805
|
+
`/api/v1/profiles/${ref}`,
|
|
806
|
+
markdown,
|
|
807
|
+
'text/markdown; charset=utf-8',
|
|
808
|
+
);
|
|
809
|
+
const patch = {};
|
|
810
|
+
if (flags.handle) patch.handle = String(flags.handle);
|
|
811
|
+
if (flags.public) patch.public = true;
|
|
812
|
+
if (flags.private) patch.public = false;
|
|
813
|
+
if (Object.keys(patch).length) answer = await client.put(`/api/v1/profiles/${ref}`, patch);
|
|
814
|
+
out(json ? JSON.stringify(answer.profile, null, 2) : `Saved: ${answer.profile.page}`);
|
|
815
|
+
return 0;
|
|
816
|
+
}
|
|
817
|
+
if (!sub) throw new Error('profile <ref>, or profile claim|edit|handle');
|
|
818
|
+
if (json) {
|
|
819
|
+
const r = await client.get(`/api/v1/profiles/${encodeURIComponent(sub)}`);
|
|
820
|
+
out(JSON.stringify(r.profile, null, 2));
|
|
821
|
+
return 0;
|
|
822
|
+
}
|
|
823
|
+
out(await client.text(`/api/v1/profiles/${encodeURIComponent(sub)}/openprofile.md`));
|
|
824
|
+
return 0;
|
|
825
|
+
}
|
|
631
826
|
case 'mcp':
|
|
632
827
|
return serveMcp({ client, stdin, stdout });
|
|
633
828
|
default:
|