@profullstack/nichedb 0.11.0 → 0.13.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +168 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@profullstack/nichedb",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "CLI and MCP bridge for NicheDB: browse collections, manage sources and feeds, search items, from any deployment",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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.11.0';
18
+ export const VERSION = '0.13.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: