@profullstack/nichedb 0.11.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 +168 -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');
|
|
@@ -177,6 +178,38 @@ export const COMMANDS = [
|
|
|
177
178
|
summary: 'Print the RSS URL (or the feed itself with --fetch).',
|
|
178
179
|
options: ['--fetch'],
|
|
179
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
|
+
},
|
|
180
213
|
{
|
|
181
214
|
name: 'login',
|
|
182
215
|
usage: 'login [--api <url>] [--key <ndb_…>]',
|
|
@@ -265,13 +298,51 @@ export function makeClient({ api, key, fetchImpl = fetch }) {
|
|
|
265
298
|
if (!res.ok) throw new Error(data?.error ?? `${res.status} from ${path}`);
|
|
266
299
|
return data;
|
|
267
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
|
+
}
|
|
268
336
|
return {
|
|
269
337
|
base,
|
|
270
338
|
key,
|
|
271
339
|
get: (p) => call('GET', p),
|
|
272
340
|
post: (p, b) => call('POST', p, b ?? {}),
|
|
273
341
|
patch: (p, b) => call('PATCH', p, b),
|
|
342
|
+
put: (p, b) => call('PUT', p, b),
|
|
274
343
|
del: (p) => call('DELETE', p),
|
|
344
|
+
send,
|
|
345
|
+
text,
|
|
275
346
|
};
|
|
276
347
|
}
|
|
277
348
|
|
|
@@ -657,6 +728,101 @@ export async function run(
|
|
|
657
728
|
out(await res.text());
|
|
658
729
|
return 0;
|
|
659
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
|
+
}
|
|
660
826
|
case 'mcp':
|
|
661
827
|
return serveMcp({ client, stdin, stdout });
|
|
662
828
|
default:
|