@bitpub/cli 2.1.2 → 2.1.4
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/commands/browser.js +310 -0
- package/src/commands/group.js +40 -3
- package/src/commands/sync.js +18 -13
- package/static/console.html +340 -5
- package/static/welcome.html +705 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitpub/cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
4
4
|
"description": "BitPub CLI — local-first shared memory for AI agents. Six daily verbs (save/load/list/find/sync/delete), zero-config private namespace, encrypted client-side.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bitpub": "./bin/bitpub.js"
|
package/src/commands/browser.js
CHANGED
|
@@ -21,6 +21,8 @@ const { exec, spawn } = require('child_process');
|
|
|
21
21
|
const { readConfig, BITPUB_DIR } = require('../config');
|
|
22
22
|
const { getSyncedNamespaces, initCache } = require('../db/cache');
|
|
23
23
|
const { isPrivateHcu, decrypt, isEncrypted } = require('../crypto');
|
|
24
|
+
const { runOneShot: runSyncOneShot } = require('./sync');
|
|
25
|
+
const { _internals: groupInternals } = require('./group');
|
|
24
26
|
|
|
25
27
|
const Database = require('better-sqlite3');
|
|
26
28
|
const DB_PATH = path.join(os.homedir(), '.bitpub', 'cache.db');
|
|
@@ -366,6 +368,271 @@ function handleBridgeRun(req, res, url) {
|
|
|
366
368
|
});
|
|
367
369
|
}
|
|
368
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Resolve the set of namespaces to sync for "sync everything visible
|
|
373
|
+
* to the current user." This is the same shape an attentive user would
|
|
374
|
+
* type by hand: their private root + every joined group.
|
|
375
|
+
*
|
|
376
|
+
* Returns an array of `{ pattern, label, config }` triples. The per-
|
|
377
|
+
* scope `config` is the user's primary config with `api_key` swapped
|
|
378
|
+
* out when a group entry pins its own key (multi-backend memberships).
|
|
379
|
+
*/
|
|
380
|
+
function resolveSyncTargets(baseConfig) {
|
|
381
|
+
const targets = [];
|
|
382
|
+
if (baseConfig && baseConfig.owner) {
|
|
383
|
+
targets.push({
|
|
384
|
+
pattern: `bitpub://private:${baseConfig.owner}/**`,
|
|
385
|
+
label: `private:${baseConfig.owner}`,
|
|
386
|
+
config: baseConfig,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const groups = Array.isArray(baseConfig && baseConfig.groups) ? baseConfig.groups : [];
|
|
390
|
+
for (const g of groups) {
|
|
391
|
+
if (!g || !g.slug) continue;
|
|
392
|
+
targets.push({
|
|
393
|
+
pattern: `bitpub://group:${g.slug}/**`,
|
|
394
|
+
label: `group:${g.slug}`,
|
|
395
|
+
// Per-group key/URL is only relevant on multi-backend setups; for
|
|
396
|
+
// the common single-backend case these fields equal the primary
|
|
397
|
+
// config and the fallback is a no-op.
|
|
398
|
+
config: {
|
|
399
|
+
...baseConfig,
|
|
400
|
+
api_key: g.key || baseConfig.api_key,
|
|
401
|
+
api_url: g.api_url || baseConfig.api_url,
|
|
402
|
+
},
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return targets;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Stream `bitpub sync` across every scope visible to the user as SSE.
|
|
410
|
+
*
|
|
411
|
+
* Wire format:
|
|
412
|
+
* event: started data: { targets: [{label, pattern}, ...] }
|
|
413
|
+
* event: scope data: { label, pattern, status: 'syncing' }
|
|
414
|
+
* event: scope data: { label, pattern, status: 'done', count, hitLimit }
|
|
415
|
+
* event: scope data: { label, pattern, status: 'error', message }
|
|
416
|
+
* event: done data: { total, scopes } ← terminal; connection closes
|
|
417
|
+
*
|
|
418
|
+
* This is browser-chrome plumbing, not an app function — it calls
|
|
419
|
+
* sync.js#runOneShot directly inside this process rather than spawning
|
|
420
|
+
* `bitpub sync` as a subprocess via the script.run primitive. Reasons:
|
|
421
|
+
* • No shell-out cost, no quoting hazard, no env propagation issues.
|
|
422
|
+
* • Per-scope progress is structured (event stream), not parsed stdout.
|
|
423
|
+
* • Manifests are for user-published apps; the Sync button is part of
|
|
424
|
+
* the browser shell and shouldn't go through that machinery.
|
|
425
|
+
*/
|
|
426
|
+
function handleBridgeSync(req, res) {
|
|
427
|
+
res.statusCode = 200;
|
|
428
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
429
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
430
|
+
res.setHeader('Connection', 'keep-alive');
|
|
431
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
432
|
+
|
|
433
|
+
function send(event, payload) {
|
|
434
|
+
if (res.writableEnded) return;
|
|
435
|
+
res.write(`event: ${event}\n`);
|
|
436
|
+
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
const cfg = readConfig();
|
|
440
|
+
if (!cfg || !cfg.owner) {
|
|
441
|
+
send('error', { message: 'No bitpub identity configured. Run `bitpub setup` first.' });
|
|
442
|
+
send('done', { total: 0, scopes: 0 });
|
|
443
|
+
res.end();
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const targets = resolveSyncTargets(cfg);
|
|
448
|
+
send('started', { targets: targets.map(t => ({ label: t.label, pattern: t.pattern })) });
|
|
449
|
+
|
|
450
|
+
let cancelled = false;
|
|
451
|
+
req.on('close', () => { cancelled = true; });
|
|
452
|
+
|
|
453
|
+
// Sync scopes sequentially so the progress events tell a single story
|
|
454
|
+
// and we don't slam the same backend with N concurrent pulls.
|
|
455
|
+
(async () => {
|
|
456
|
+
let total = 0;
|
|
457
|
+
let okScopes = 0;
|
|
458
|
+
for (const t of targets) {
|
|
459
|
+
if (cancelled) return;
|
|
460
|
+
send('scope', { label: t.label, pattern: t.pattern, status: 'syncing' });
|
|
461
|
+
try {
|
|
462
|
+
const r = await runSyncOneShot(
|
|
463
|
+
{ pattern: t.pattern, label: t.label, limit: 500, includeDeleted: false, quiet: true },
|
|
464
|
+
t.config
|
|
465
|
+
);
|
|
466
|
+
total += r.count;
|
|
467
|
+
okScopes += 1;
|
|
468
|
+
send('scope', {
|
|
469
|
+
label: t.label,
|
|
470
|
+
pattern: t.pattern,
|
|
471
|
+
status: 'done',
|
|
472
|
+
count: r.count,
|
|
473
|
+
hitLimit: !!r.hitLimit,
|
|
474
|
+
});
|
|
475
|
+
} catch (err) {
|
|
476
|
+
send('scope', {
|
|
477
|
+
label: t.label,
|
|
478
|
+
pattern: t.pattern,
|
|
479
|
+
status: 'error',
|
|
480
|
+
message: err && err.message ? err.message : String(err),
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
send('done', { total, scopes: okScopes });
|
|
485
|
+
res.end();
|
|
486
|
+
})().catch((err) => {
|
|
487
|
+
send('error', { message: err && err.message ? err.message : String(err) });
|
|
488
|
+
send('done', { total: 0, scopes: 0 });
|
|
489
|
+
res.end();
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/* ── Groups (browser chrome) ─────────────────────────────────────
|
|
494
|
+
*
|
|
495
|
+
* Three small JSON endpoints that back the "Make a group" / "Your
|
|
496
|
+
* groups" UI in the Welcome slice (and anywhere else in the browser
|
|
497
|
+
* chrome that wants to read/write group membership).
|
|
498
|
+
*
|
|
499
|
+
* Like /bridge/sync, this lives outside the manifest/apps system —
|
|
500
|
+
* group management is part of the browser shell itself, not something
|
|
501
|
+
* a user-published app should be able to do unprompted.
|
|
502
|
+
*
|
|
503
|
+
* Wire format:
|
|
504
|
+
* POST /bridge/group/create { display }
|
|
505
|
+
* → { ok, group: { slug, display, hosted_on, base_url, role, joined_at }, invite_url }
|
|
506
|
+
*
|
|
507
|
+
* GET /bridge/group/list
|
|
508
|
+
* → { ok, groups: [{ slug, display, address, role, joined_at, api_url }] }
|
|
509
|
+
*
|
|
510
|
+
* POST /bridge/group/invite { slug, rotate }
|
|
511
|
+
* rotate=false (default) → GET the current active invite from the
|
|
512
|
+
* server (server is source of truth);
|
|
513
|
+
* returns { invite_url, rotated: false }.
|
|
514
|
+
* invite_url is null if every invite has
|
|
515
|
+
* been revoked and not yet replaced.
|
|
516
|
+
* rotate=true → POST a new invite (invalidates the prior one);
|
|
517
|
+
* returns { invite_url, rotated: true }.
|
|
518
|
+
*
|
|
519
|
+
* No local invite-URL caching: every read is a live server call. Group
|
|
520
|
+
* operations require network connectivity by design (create/join/leave
|
|
521
|
+
* all hit the server), so making the read path also network-bound is
|
|
522
|
+
* consistent and avoids sync hazards (rotate from machine A, stale
|
|
523
|
+
* cache on machine B).
|
|
524
|
+
*/
|
|
525
|
+
function jsonBody(req) {
|
|
526
|
+
return new Promise((resolve, reject) => {
|
|
527
|
+
let buf = '';
|
|
528
|
+
req.setEncoding('utf-8');
|
|
529
|
+
req.on('data', (c) => {
|
|
530
|
+
buf += c;
|
|
531
|
+
// Reject oversized bodies early — group endpoints take small JSON only.
|
|
532
|
+
if (buf.length > 16 * 1024) { reject(new Error('body too large')); req.destroy(); }
|
|
533
|
+
});
|
|
534
|
+
req.on('end', () => {
|
|
535
|
+
if (!buf) return resolve({});
|
|
536
|
+
try { resolve(JSON.parse(buf)); }
|
|
537
|
+
catch (err) { reject(new Error(`invalid JSON: ${err.message}`)); }
|
|
538
|
+
});
|
|
539
|
+
req.on('error', reject);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function sendJson(res, status, payload) {
|
|
544
|
+
res.statusCode = status;
|
|
545
|
+
res.setHeader('Content-Type', 'application/json');
|
|
546
|
+
res.end(JSON.stringify(payload));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function handleBridgeGroupCreate(req, res) {
|
|
550
|
+
let body;
|
|
551
|
+
try { body = await jsonBody(req); }
|
|
552
|
+
catch (err) { return sendJson(res, 400, { ok: false, error: err.message }); }
|
|
553
|
+
|
|
554
|
+
const display = String((body && body.display) || '').trim();
|
|
555
|
+
if (!display) return sendJson(res, 400, { ok: false, error: 'display name required' });
|
|
556
|
+
if (display.length > 80) return sendJson(res, 400, { ok: false, error: 'display name too long (max 80 chars)' });
|
|
557
|
+
|
|
558
|
+
try {
|
|
559
|
+
// createGroup itself caches invite_url locally (see group.js) so
|
|
560
|
+
// we don't need to do it here too.
|
|
561
|
+
const out = await groupInternals.createGroup(display);
|
|
562
|
+
sendJson(res, 200, {
|
|
563
|
+
ok: true,
|
|
564
|
+
group: {
|
|
565
|
+
slug: out.slug,
|
|
566
|
+
display: out.display,
|
|
567
|
+
hosted_on: out.hosted_on || null,
|
|
568
|
+
base_url: out.base_url || null,
|
|
569
|
+
role: 'owner',
|
|
570
|
+
},
|
|
571
|
+
invite_url: out.invite_url || null,
|
|
572
|
+
});
|
|
573
|
+
} catch (err) {
|
|
574
|
+
const msg = (err && err.message) || String(err);
|
|
575
|
+
sendJson(res, 500, { ok: false, error: msg });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function handleBridgeGroupList(_req, res) {
|
|
580
|
+
const cfg = readConfig() || {};
|
|
581
|
+
const groups = Array.isArray(cfg.groups) ? cfg.groups : [];
|
|
582
|
+
const out = groups.map((g) => ({
|
|
583
|
+
slug: g.slug,
|
|
584
|
+
display: g.display || g.slug,
|
|
585
|
+
address: `bitpub://group:${g.slug}/`,
|
|
586
|
+
role: g.role || null,
|
|
587
|
+
joined_at: g.joined_at || null,
|
|
588
|
+
api_url: g.api_url || null,
|
|
589
|
+
}));
|
|
590
|
+
sendJson(res, 200, { ok: true, groups: out });
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function handleBridgeGroupInvite(req, res) {
|
|
594
|
+
let body;
|
|
595
|
+
try { body = await jsonBody(req); }
|
|
596
|
+
catch (err) { return sendJson(res, 400, { ok: false, error: err.message }); }
|
|
597
|
+
|
|
598
|
+
const slug = String((body && body.slug) || '').trim();
|
|
599
|
+
const rotate = !!(body && body.rotate);
|
|
600
|
+
if (!slug) return sendJson(res, 400, { ok: false, error: 'slug required' });
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
if (rotate) {
|
|
604
|
+
const out = await groupInternals.rotateInvite(slug);
|
|
605
|
+
return sendJson(res, 200, {
|
|
606
|
+
ok: true,
|
|
607
|
+
invite_url: out.invite_url,
|
|
608
|
+
rotated: true,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
// Read path — server is the source of truth (GET /v1/groups/:slug/invites).
|
|
612
|
+
// Returns { invite_url: null } if every invite has been revoked.
|
|
613
|
+
const out = await groupInternals.showInvite(slug);
|
|
614
|
+
sendJson(res, 200, {
|
|
615
|
+
ok: true,
|
|
616
|
+
invite_url: out.invite_url || null,
|
|
617
|
+
rotated: false,
|
|
618
|
+
});
|
|
619
|
+
} catch (err) {
|
|
620
|
+
const raw = (err && err.message) || String(err);
|
|
621
|
+
// httpJson wraps upstream errors as "[STATUS] message". Pluck the
|
|
622
|
+
// status so we can return a matching HTTP code, and strip the
|
|
623
|
+
// prefix from the message so the UI renders just the human part.
|
|
624
|
+
const m = /^\[(\d{3})\]\s*(.*)$/.exec(raw);
|
|
625
|
+
const status = m ? Number(m[1]) : 500;
|
|
626
|
+
let message = m ? m[2] : raw;
|
|
627
|
+
if (status === 404 && /not found/i.test(message)) {
|
|
628
|
+
// Most common reason today: the running backend hasn't shipped
|
|
629
|
+
// GET /v1/groups/:slug/invites yet. Make that diagnosable.
|
|
630
|
+
message = 'This backend does not support reading invites (upgrade required).';
|
|
631
|
+
}
|
|
632
|
+
sendJson(res, status, { ok: false, error: message });
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
369
636
|
function openInBrowser(url) {
|
|
370
637
|
const cmd = process.platform === 'darwin' ? 'open' :
|
|
371
638
|
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
@@ -422,15 +689,34 @@ function startBrowserServer(opts = {}) {
|
|
|
422
689
|
|
|
423
690
|
if (url.pathname === '/api/data') {
|
|
424
691
|
res.setHeader('Content-Type', 'application/json');
|
|
692
|
+
// Re-read the config on every /api/data hit. The captured
|
|
693
|
+
// `cfg` above is set once at server start, but groups (and
|
|
694
|
+
// other config) can change mid-session — e.g. after the
|
|
695
|
+
// Welcome UI calls /bridge/group/create. Without this we'd
|
|
696
|
+
// serve a stale known_groups list and the new group wouldn't
|
|
697
|
+
// show in the sidebar until the user restarted `bitpub
|
|
698
|
+
// browser`. We fall back to the boot cfg for the key/domain
|
|
699
|
+
// fields, which don't change at runtime.
|
|
700
|
+
const liveCfg = readConfig() || cfg || {};
|
|
425
701
|
const slices = getAllSlices().map(s => {
|
|
426
702
|
if (cfg && cfg.api_key) return decryptSlice(s, cfg.api_key);
|
|
427
703
|
return s;
|
|
428
704
|
});
|
|
705
|
+
// known_groups: every group in the user's local config, even
|
|
706
|
+
// groups that contain zero slices yet. Lets the sidebar tree
|
|
707
|
+
// render a freshly-created group address as an empty top-level
|
|
708
|
+
// node — without this, `bitpub group create` would succeed
|
|
709
|
+
// but nothing visible would change until the first slice
|
|
710
|
+
// landed under that namespace.
|
|
711
|
+
const knownGroups = Array.isArray(liveCfg.groups)
|
|
712
|
+
? liveCfg.groups.map((g) => ({ slug: g.slug, display: g.display || g.slug }))
|
|
713
|
+
: [];
|
|
429
714
|
res.end(JSON.stringify({
|
|
430
715
|
mode: 'local',
|
|
431
716
|
domain: (cfg && cfg.domain) || '',
|
|
432
717
|
slices,
|
|
433
718
|
synced_namespaces: getSyncedNamespaces(),
|
|
719
|
+
known_groups: knownGroups,
|
|
434
720
|
}));
|
|
435
721
|
return;
|
|
436
722
|
}
|
|
@@ -469,6 +755,30 @@ function startBrowserServer(opts = {}) {
|
|
|
469
755
|
return;
|
|
470
756
|
}
|
|
471
757
|
|
|
758
|
+
// Bridge: sync every namespace visible to this user (private +
|
|
759
|
+
// each joined group) and stream per-scope progress as SSE. This
|
|
760
|
+
// backs the sidebar "Sync" button — it's browser chrome, not an
|
|
761
|
+
// app function, and so doesn't go through the manifest system.
|
|
762
|
+
if (url.pathname === '/bridge/sync') {
|
|
763
|
+
handleBridgeSync(req, res);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Bridge: group management for the Welcome UI (create, list,
|
|
768
|
+
// invite). Same chrome-not-app reasoning as /bridge/sync.
|
|
769
|
+
if (url.pathname === '/bridge/group/create' && req.method === 'POST') {
|
|
770
|
+
handleBridgeGroupCreate(req, res);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (url.pathname === '/bridge/group/list' && req.method === 'GET') {
|
|
774
|
+
handleBridgeGroupList(req, res);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (url.pathname === '/bridge/group/invite' && req.method === 'POST') {
|
|
778
|
+
handleBridgeGroupInvite(req, res);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
472
782
|
// Bridge: deterministic namespace read. Apps call this to fetch
|
|
473
783
|
// any slices that match an HCU pattern (exact or prefix-with-/*).
|
|
474
784
|
// No LLM, no MCP, no auth in the loop — this is the read path
|
package/src/commands/group.js
CHANGED
|
@@ -99,6 +99,12 @@ async function createGroup(display) {
|
|
|
99
99
|
// the right backend with the same personal key (creator stays on
|
|
100
100
|
// their own key; the per-group-key mint happens at *accept* time, not
|
|
101
101
|
// create time, since the creator already has a key on this backend).
|
|
102
|
+
//
|
|
103
|
+
// We deliberately do NOT cache invite_url here. The server is the
|
|
104
|
+
// source of truth — `bitpub group invite <slug> --show` (and the
|
|
105
|
+
// browser-chrome `/bridge/group/invite` endpoint) re-fetch it via
|
|
106
|
+
// `listInvites` below whenever needed. Caching it would just be a
|
|
107
|
+
// sync hazard (rotate from another machine, lose the cache, etc.).
|
|
102
108
|
upsertGroupEntry({
|
|
103
109
|
slug: data.slug,
|
|
104
110
|
display: data.display,
|
|
@@ -112,6 +118,23 @@ async function createGroup(display) {
|
|
|
112
118
|
return { ...data, invite_url: data.invite_url, base_url: baseUrl };
|
|
113
119
|
}
|
|
114
120
|
|
|
121
|
+
// ── group invite show (read current invite from the server) ────────────────
|
|
122
|
+
|
|
123
|
+
async function showInvite(slug) {
|
|
124
|
+
const config = requireConfig();
|
|
125
|
+
const entry = groupEntryFor(config, slug);
|
|
126
|
+
if (!entry) {
|
|
127
|
+
throw new Error(`Not a member of group "${slug}". Run \`bitpub group list\` to see your groups.`);
|
|
128
|
+
}
|
|
129
|
+
const baseUrl = trimSlash(entry.api_url) || primaryApiUrl(config);
|
|
130
|
+
// GET /v1/groups/:slug/invites returns the current active invite for
|
|
131
|
+
// members. Server is the source of truth — we don't cache.
|
|
132
|
+
const data = await httpJson('GET', `${baseUrl}/v1/groups/${encodeURIComponent(slug)}/invites`, {
|
|
133
|
+
headers: { 'x-api-key': entry.key },
|
|
134
|
+
});
|
|
135
|
+
return data; // { slug, display, invite_url, created_at }
|
|
136
|
+
}
|
|
137
|
+
|
|
115
138
|
// ── group list ─────────────────────────────────────────────────────────────
|
|
116
139
|
|
|
117
140
|
async function listGroupsLocal() {
|
|
@@ -128,7 +151,10 @@ async function rotateInvite(slug, { show } = {}) {
|
|
|
128
151
|
throw new Error(`Not a member of group "${slug}". Run \`bitpub group list\` to see your groups.`);
|
|
129
152
|
}
|
|
130
153
|
if (show) {
|
|
131
|
-
|
|
154
|
+
// `--show` reads the current invite without rotating. Implemented
|
|
155
|
+
// by GET /v1/groups/:slug/invites (members-only) above; same
|
|
156
|
+
// endpoint the Welcome UI's lazy "Copy invite link" calls.
|
|
157
|
+
return showInvite(slug);
|
|
132
158
|
}
|
|
133
159
|
const baseUrl = trimSlash(entry.api_url) || primaryApiUrl(config);
|
|
134
160
|
const data = await httpJson('POST', `${baseUrl}/v1/groups/${encodeURIComponent(slug)}/invites`, {
|
|
@@ -259,11 +285,21 @@ module.exports = function registerGroup(program) {
|
|
|
259
285
|
|
|
260
286
|
group
|
|
261
287
|
.command('invite <slug>')
|
|
262
|
-
.description('
|
|
263
|
-
.option('--show', '
|
|
288
|
+
.description('Show or rotate the shareable invite link for a group')
|
|
289
|
+
.option('--show', 'Show the current link without rotating it (read-only)')
|
|
264
290
|
.action(async (slug, opts) => {
|
|
265
291
|
try {
|
|
266
292
|
const out = await rotateInvite(slug, opts);
|
|
293
|
+
if (opts.show) {
|
|
294
|
+
if (out.invite_url) {
|
|
295
|
+
console.log(`\n Share link for ${out.display || out.slug}:`);
|
|
296
|
+
console.log(` ${out.invite_url}`);
|
|
297
|
+
} else {
|
|
298
|
+
console.log(`\n No active invite link for ${out.display || out.slug}.`);
|
|
299
|
+
console.log(` Mint one: bitpub group invite ${out.slug}`);
|
|
300
|
+
}
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
267
303
|
console.log(`\n✓ Rotated invite for ${out.display || out.slug}`);
|
|
268
304
|
console.log(`\n New share link:`);
|
|
269
305
|
console.log(` ${out.invite_url}`);
|
|
@@ -329,5 +365,6 @@ module.exports._internals = {
|
|
|
329
365
|
createGroup,
|
|
330
366
|
joinFromInvite,
|
|
331
367
|
rotateInvite,
|
|
368
|
+
showInvite,
|
|
332
369
|
leaveGroup,
|
|
333
370
|
};
|
package/src/commands/sync.js
CHANGED
|
@@ -60,7 +60,7 @@ function resolveSyncPattern(input, config) {
|
|
|
60
60
|
return { pattern: active.namespace + resolved, label: input };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async function runOneShot({ pattern, label, limit, includeDeleted }, config) {
|
|
63
|
+
async function runOneShot({ pattern, label, limit, includeDeleted, quiet }, config) {
|
|
64
64
|
const api = createApiClient(config);
|
|
65
65
|
const limitNum = parseInt(limit, 10) || 500;
|
|
66
66
|
|
|
@@ -70,19 +70,24 @@ async function runOneShot({ pattern, label, limit, includeDeleted }, config) {
|
|
|
70
70
|
for (const slice of slices) upsertSlice(slice);
|
|
71
71
|
recordNamespaceSync(pattern, slices.length);
|
|
72
72
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
73
|
+
const hitLimit = slices.length >= Math.min(limitNum, 500);
|
|
74
|
+
|
|
75
|
+
if (!quiet) {
|
|
76
|
+
console.log(`Synced ${slices.length} slice(s) from ${label}`);
|
|
77
|
+
// Surface page-cap warning so the user knows to narrow their pattern.
|
|
78
|
+
if (hitLimit) {
|
|
79
|
+
console.log(
|
|
80
|
+
`\n Hit the page limit (${slices.length} rows). Server caps single sync at 500.` +
|
|
81
|
+
` If you expect more, sync sub-namespaces individually:`
|
|
82
|
+
);
|
|
83
|
+
const base = pattern.replace(/\/\*+$/, '');
|
|
84
|
+
console.log(` bitpub sync '${base}/Projects/**'`);
|
|
85
|
+
console.log(` bitpub sync '${base}/Memory/**'`);
|
|
86
|
+
console.log(` bitpub sync '${base}/Sessions/**'`);
|
|
87
|
+
}
|
|
85
88
|
}
|
|
89
|
+
|
|
90
|
+
return { count: slices.length, hitLimit, pattern, label };
|
|
86
91
|
}
|
|
87
92
|
|
|
88
93
|
function runWatch({ pattern, label }, config) {
|