@bitsocial/bitsocial-cli 0.19.46 → 0.19.47

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/README.md CHANGED
@@ -344,7 +344,7 @@ EXAMPLES
344
344
  $ bitsocial challenge install ./my-local-challenge
345
345
  ```
346
346
 
347
- _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/challenge/install.ts)_
347
+ _See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/challenge/install.ts)_
348
348
 
349
349
  ## `bitsocial challenge list`
350
350
 
@@ -367,7 +367,7 @@ EXAMPLES
367
367
  $ bitsocial challenge list -q
368
368
  ```
369
369
 
370
- _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/challenge/list.ts)_
370
+ _See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/challenge/list.ts)_
371
371
 
372
372
  ## `bitsocial challenge remove NAME`
373
373
 
@@ -392,7 +392,7 @@ EXAMPLES
392
392
  $ bitsocial challenge remove @scope/my-challenge
393
393
  ```
394
394
 
395
- _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/challenge/remove.ts)_
395
+ _See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/challenge/remove.ts)_
396
396
 
397
397
  ## `bitsocial community create`
398
398
 
@@ -422,7 +422,7 @@ EXAMPLES
422
422
  $ bitsocial community create --jsonFile ./create-options.json
423
423
  ```
424
424
 
425
- _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/create.ts)_
425
+ _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/create.ts)_
426
426
 
427
427
  ## `bitsocial community delete ADDRESSES`
428
428
 
@@ -447,7 +447,7 @@ EXAMPLES
447
447
  $ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
448
448
  ```
449
449
 
450
- _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/delete.ts)_
450
+ _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/delete.ts)_
451
451
 
452
452
  ## `bitsocial community edit ADDRESS`
453
453
 
@@ -467,10 +467,22 @@ FLAGS
467
467
  DESCRIPTION
468
468
  Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js
469
469
 
470
+ Merge behavior with CLI flags:
471
+ - Objects are merged with the community's current state (new keys are added, existing keys are overwritten).
472
+ - Arrays are extended: new values are prepended to the existing array.
473
+ - Setting a value to null removes it (e.g. --roles['mod.bso'] null).
474
+
475
+ Merge behavior with --jsonFile:
476
+ - Objects are merged the same way as CLI flags.
477
+ - Arrays are replaced entirely (RFC 7396 JSON Merge Patch semantics).
478
+ - When both --jsonFile and CLI flags are provided, CLI flags take priority.
479
+
480
+ For modifying complex settings like challenges, consider using a web UI instead: https://bitsocial.net/apps
481
+
470
482
  EXAMPLES
471
- Change the address of the community to a new domain address
483
+ Change the name of the community
472
484
 
473
- $ bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --address newAddress.bso
485
+ $ bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --name newName.bso
474
486
 
475
487
  Add the author address 'esteban.bso' as an admin on the community
476
488
 
@@ -505,7 +517,7 @@ EXAMPLES
505
517
  $ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
506
518
  ```
507
519
 
508
- _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/edit.ts)_
520
+ _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/edit.ts)_
509
521
 
510
522
  ## `bitsocial community get [ADDRESS]`
511
523
 
@@ -536,7 +548,7 @@ EXAMPLES
536
548
  $ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
537
549
  ```
538
550
 
539
- _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/get.ts)_
551
+ _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/get.ts)_
540
552
 
541
553
  ## `bitsocial community list`
542
554
 
@@ -559,7 +571,7 @@ EXAMPLES
559
571
  $ bitsocial community list
560
572
  ```
561
573
 
562
- _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/list.ts)_
574
+ _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/list.ts)_
563
575
 
564
576
  ## `bitsocial community start ADDRESSES`
565
577
 
@@ -567,13 +579,14 @@ Start a community
567
579
 
568
580
  ```
569
581
  USAGE
570
- $ bitsocial community start ADDRESSES... --pkcRpcUrl <value>
582
+ $ bitsocial community start ADDRESSES... --pkcRpcUrl <value> [--concurrency <value>]
571
583
 
572
584
  ARGUMENTS
573
585
  ADDRESSES... Addresses of communities to start. Separated by space
574
586
 
575
587
  FLAGS
576
- --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
588
+ --concurrency=<value> [default: 5] Number of communities to start in parallel
589
+ --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
577
590
 
578
591
  DESCRIPTION
579
592
  Start a community
@@ -586,9 +599,13 @@ EXAMPLES
586
599
  Start all communities in your data path
587
600
 
588
601
  $ bitsocial community start $(bitsocial community list -q)
602
+
603
+ Start communities sequentially (no concurrency)
604
+
605
+ $ bitsocial community start $(bitsocial community list -q) --concurrency 1
589
606
  ```
590
607
 
591
- _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/start.ts)_
608
+ _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/start.ts)_
592
609
 
593
610
  ## `bitsocial community stop ADDRESSES`
594
611
 
@@ -613,7 +630,7 @@ EXAMPLES
613
630
  $ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
614
631
  ```
615
632
 
616
- _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/community/stop.ts)_
633
+ _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/community/stop.ts)_
617
634
 
618
635
  ## `bitsocial daemon`
619
636
 
@@ -654,7 +671,7 @@ EXAMPLES
654
671
  $ bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY
655
672
  ```
656
673
 
657
- _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/daemon.ts)_
674
+ _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/daemon.ts)_
658
675
 
659
676
  ## `bitsocial help [COMMAND]`
660
677
 
@@ -720,7 +737,7 @@ EXAMPLES
720
737
  $ bitsocial logs --stdout -f
721
738
  ```
722
739
 
723
- _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/logs.ts)_
740
+ _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/logs.ts)_
724
741
 
725
742
  ## `bitsocial update check`
726
743
 
@@ -737,7 +754,7 @@ EXAMPLES
737
754
  $ bitsocial update check
738
755
  ```
739
756
 
740
- _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/update/check.ts)_
757
+ _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/update/check.ts)_
741
758
 
742
759
  ## `bitsocial update install [VERSION]`
743
760
 
@@ -769,7 +786,7 @@ EXAMPLES
769
786
  $ bitsocial update install --no-restart-daemons
770
787
  ```
771
788
 
772
- _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/update/install.ts)_
789
+ _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/update/install.ts)_
773
790
 
774
791
  ## `bitsocial update versions`
775
792
 
@@ -791,7 +808,7 @@ EXAMPLES
791
808
  $ bitsocial update versions --limit 5
792
809
  ```
793
810
 
794
- _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/update/versions.ts)_
811
+ _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.47/src/cli/commands/update/versions.ts)_
795
812
  <!-- commandsstop -->
796
813
 
797
814
  ## Contribution
@@ -5,7 +5,19 @@ import { BaseCommand } from "../../base-command.js";
5
5
  import { PKCLogger, mergeDeep, parseJsoncFile, replaceNullWithUndefined } from "../../../util.js";
6
6
  import * as remeda from "remeda";
7
7
  export default class Edit extends BaseCommand {
8
- static description = "Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js";
8
+ static description = `Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js
9
+
10
+ Merge behavior with CLI flags:
11
+ - Objects are merged with the community's current state (new keys are added, existing keys are overwritten).
12
+ - Arrays are extended: new values are prepended to the existing array.
13
+ - Setting a value to null removes it (e.g. --roles['mod.bso'] null).
14
+
15
+ Merge behavior with --jsonFile:
16
+ - Objects are merged the same way as CLI flags.
17
+ - Arrays are replaced entirely (RFC 7396 JSON Merge Patch semantics).
18
+ - When both --jsonFile and CLI flags are provided, CLI flags take priority.
19
+
20
+ For modifying complex settings like challenges, consider using a web UI instead: https://bitsocial.net/apps`;
9
21
  static args = {
10
22
  address: Args.string({
11
23
  name: "address",
@@ -21,12 +33,9 @@ export default class Edit extends BaseCommand {
21
33
  })
22
34
  };
23
35
  static examples = [
24
- // TODO update this to change the name instead
25
- // Also are we testing modifying name properly?
26
- // in theory user should not modify address, they should modify name
27
36
  {
28
- description: "Change the address of the community to a new domain address",
29
- command: "bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --address newAddress.bso"
37
+ description: "Change the name of the community",
38
+ command: "bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --name newName.bso"
30
39
  },
31
40
  {
32
41
  description: "Add the author address 'esteban.bso' as an admin on the community",
@@ -5,6 +5,9 @@ export default class Start extends BaseCommand {
5
5
  static args: {
6
6
  addresses: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
7
7
  };
8
+ static flags: {
9
+ concurrency: import("@oclif/core/interfaces").OptionFlag<number, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
8
11
  static examples: (string | {
9
12
  description: string;
10
13
  command: string;
@@ -1,6 +1,7 @@
1
1
  import { PKCLogger } from "../../../util.js";
2
2
  import { BaseCommand } from "../../base-command.js";
3
- import { Args } from "@oclif/core";
3
+ import { Args, Flags } from "@oclif/core";
4
+ import pLimit from "p-limit";
4
5
  export default class Start extends BaseCommand {
5
6
  static description = "Start a community";
6
7
  static strict = false; // To allow for variable length arguments
@@ -11,12 +12,23 @@ export default class Start extends BaseCommand {
11
12
  description: "Addresses of communities to start. Separated by space"
12
13
  })
13
14
  };
15
+ static flags = {
16
+ concurrency: Flags.integer({
17
+ description: "Number of communities to start in parallel",
18
+ default: 5,
19
+ min: 0
20
+ })
21
+ };
14
22
  static examples = [
15
23
  "bitsocial community start plebbit.bso",
16
24
  "bitsocial community start 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu",
17
25
  {
18
26
  description: "Start all communities in your data path",
19
27
  command: "bitsocial community start $(bitsocial community list -q)"
28
+ },
29
+ {
30
+ description: "Start communities sequentially (no concurrency)",
31
+ command: "bitsocial community start $(bitsocial community list -q) --concurrency 1"
20
32
  }
21
33
  ];
22
34
  async run() {
@@ -25,8 +37,11 @@ export default class Start extends BaseCommand {
25
37
  const log = PKCLogger("bitsocial-cli:commands:community:start");
26
38
  log(`addresses: `, addresses);
27
39
  log(`flags: `, flags);
40
+ const concurrency = Math.max(flags.concurrency, 1);
41
+ const limit = pLimit(concurrency);
28
42
  const pkc = await this._connectToPkcRpc(flags.pkcRpcUrl.toString());
29
- for (const address of addresses) {
43
+ const errors = [];
44
+ const tasks = addresses.map((address) => limit(async () => {
30
45
  try {
31
46
  const community = await pkc.createCommunity({ address });
32
47
  await community.start();
@@ -36,11 +51,16 @@ export default class Start extends BaseCommand {
36
51
  const error = e instanceof Error ? e : new Error(typeof e === "string" ? e : JSON.stringify(e));
37
52
  //@ts-expect-error
38
53
  error.details = { ...error.details, address };
54
+ errors.push({ address, error });
55
+ }
56
+ }));
57
+ await Promise.all(tasks);
58
+ await pkc.destroy();
59
+ if (errors.length > 0) {
60
+ for (const { error } of errors) {
39
61
  console.error(error);
40
- await pkc.destroy();
41
- this.exit(1);
42
62
  }
63
+ this.exit(1);
43
64
  }
44
- await pkc.destroy();
45
65
  }
46
66
  }
@@ -2,6 +2,7 @@ import { Args, Flags, Command } from "@oclif/core";
2
2
  import { spawn } from "child_process";
3
3
  import tcpPortUsed from "tcp-port-used";
4
4
  import { fetchLatestVersion, installGlobal } from "../../../update/npm-registry.js";
5
+ import { fastInstallGlobal } from "../../../update/fast-update.js";
5
6
  import { compareVersions } from "../../../update/semver.js";
6
7
  import { getAliveDaemonStates } from "../../../common-utils/daemon-state.js";
7
8
  import PKC from "@pkcprotocol/pkc-js";
@@ -95,11 +96,25 @@ export default class Install extends Command {
95
96
  return;
96
97
  }
97
98
  this.log(`Installing bitsocial-cli@${targetVersion}...`);
98
- try {
99
- await installGlobal(targetVersion);
99
+ let installed = false;
100
+ if (!flags.force) {
101
+ try {
102
+ installed = await fastInstallGlobal(targetVersion, this.config.root, (msg) => this.log(msg));
103
+ }
104
+ catch {
105
+ installed = false;
106
+ }
100
107
  }
101
- catch (err) {
102
- this.error(`Update failed: ${err.message}`, { exit: 1 });
108
+ if (!installed) {
109
+ if (!flags.force) {
110
+ this.log("Falling back to full install...");
111
+ }
112
+ try {
113
+ await installGlobal(targetVersion);
114
+ }
115
+ catch (err) {
116
+ this.error(`Update failed: ${err.message}`, { exit: 1 });
117
+ }
103
118
  }
104
119
  this.log(`Installed bitsocial v${targetVersion} (was v${current}).`);
105
120
  // Restart daemons with the new binary
@@ -161,26 +176,43 @@ export default class Install extends Command {
161
176
  return pkc;
162
177
  }
163
178
  async _reportCommunityStatus(pkcRpcUrl) {
179
+ const POLL_INTERVAL_MS = 2000;
180
+ const TIMEOUT_MS = 120_000;
164
181
  let pkc;
165
182
  try {
166
183
  pkc = await this._connectToRpc(pkcRpcUrl);
167
- const communities = pkc.communities;
168
- if (communities.length === 0)
184
+ const communityAddresses = pkc.communities;
185
+ if (communityAddresses.length === 0)
169
186
  return;
170
- const statuses = await Promise.all(communities.map(async (address) => {
171
- const community = await pkc.createCommunity({ address });
172
- return community.started;
173
- }));
174
- const startedCount = statuses.filter(Boolean).length;
187
+ // Create community objects once they are RPC proxies whose
188
+ // .started property reflects live daemon state
189
+ const communities = await Promise.all(communityAddresses.map((address) => pkc.createCommunity({ address })));
175
190
  const total = communities.length;
176
- if (startedCount === total) {
177
- this.log(` ${startedCount} ${startedCount === 1 ? "community" : "communities"} started.`);
178
- }
179
- else if (startedCount > 0) {
180
- this.log(` ${startedCount} of ${total} communities started (remaining still loading).`);
181
- }
182
- else {
183
- this.log(` ${total} ${total === 1 ? "community" : "communities"} in data path (still loading). Check with: bitsocial community list`);
191
+ const deadline = Date.now() + TIMEOUT_MS;
192
+ let lastReportedCount = -1;
193
+ while (true) {
194
+ const startedCount = communities.filter((c) => c.started === true).length;
195
+ if (startedCount === total) {
196
+ this.log(` All ${total} ${total === 1 ? "community" : "communities"} started.`);
197
+ return;
198
+ }
199
+ // Only print a progress update when the count changes
200
+ if (startedCount !== lastReportedCount) {
201
+ if (startedCount > 0) {
202
+ this.log(` ${startedCount} of ${total} communities started...`);
203
+ }
204
+ lastReportedCount = startedCount;
205
+ }
206
+ if (Date.now() >= deadline) {
207
+ if (startedCount > 0) {
208
+ this.log(` ${startedCount} of ${total} communities started (remaining still loading).`);
209
+ }
210
+ else {
211
+ this.log(` ${total} ${total === 1 ? "community" : "communities"} in data path (still loading). Check with: bitsocial community list`);
212
+ }
213
+ return;
214
+ }
215
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
184
216
  }
185
217
  }
186
218
  catch {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Attempt a fast update by downloading only the tarball and reusing node_modules/
3
+ * and dist/webuis/ from the existing install. Falls back (returns false) if
4
+ * dependencies changed or any step fails.
5
+ */
6
+ export declare function fastInstallGlobal(version: string, installRoot: string, log: (msg: string) => void): Promise<boolean>;
@@ -0,0 +1,182 @@
1
+ import fs from "node:fs/promises";
2
+ import { createHash } from "node:crypto";
3
+ import { createWriteStream } from "node:fs";
4
+ import path from "node:path";
5
+ import { Readable } from "node:stream";
6
+ import { finished as streamFinished } from "node:stream/promises";
7
+ import decompress from "decompress";
8
+ import { runNpmPack, ensureNpmAvailable } from "../challenge-packages/challenge-utils.js";
9
+ import { PACKAGE_NAME } from "./npm-registry.js";
10
+ /**
11
+ * Attempt a fast update by downloading only the tarball and reusing node_modules/
12
+ * and dist/webuis/ from the existing install. Falls back (returns false) if
13
+ * dependencies changed or any step fails.
14
+ */
15
+ export async function fastInstallGlobal(version, installRoot, log) {
16
+ const stagingDir = installRoot + ".__fast_update_staging";
17
+ const backupDir = installRoot + ".__fast_update_backup";
18
+ // Phase: cleanup leftovers from any interrupted previous fast-update
19
+ await fs.rm(stagingDir, { recursive: true, force: true });
20
+ await fs.rm(backupDir, { recursive: true, force: true });
21
+ let nodeModulesMoved = false;
22
+ try {
23
+ // Phase: download tarball
24
+ await ensureNpmAvailable();
25
+ await fs.mkdir(stagingDir, { recursive: true });
26
+ log("Trying fast update...");
27
+ const tgzPath = await runNpmPack(`${PACKAGE_NAME}@${version}`, stagingDir);
28
+ // Phase: extract (strip: 1 removes the "package/" prefix in npm tarballs)
29
+ await decompress(tgzPath, stagingDir, { strip: 1 });
30
+ await fs.rm(tgzPath);
31
+ // Phase: compare dependencies
32
+ const oldPkg = JSON.parse(await fs.readFile(path.join(installRoot, "package.json"), "utf-8"));
33
+ const newPkg = JSON.parse(await fs.readFile(path.join(stagingDir, "package.json"), "utf-8"));
34
+ if (JSON.stringify(oldPkg.dependencies) !== JSON.stringify(newPkg.dependencies)) {
35
+ log("Dependencies changed, falling back to full install.");
36
+ await fs.rm(stagingDir, { recursive: true, force: true });
37
+ return false;
38
+ }
39
+ // Phase: reuse node_modules from old install (O(1) rename, same filesystem)
40
+ try {
41
+ await fs.rename(path.join(installRoot, "node_modules"), path.join(stagingDir, "node_modules"));
42
+ nodeModulesMoved = true;
43
+ }
44
+ catch (err) {
45
+ if (err.code === "EXDEV") {
46
+ log("Cross-device rename, falling back to full install.");
47
+ await fs.rm(stagingDir, { recursive: true, force: true });
48
+ return false;
49
+ }
50
+ throw err;
51
+ }
52
+ // Phase: reuse unchanged webuis, download only changed ones
53
+ const oldWebuis = oldPkg.webuis ?? [];
54
+ const newWebuis = newPkg.webuis ?? [];
55
+ const oldWebuisByUrl = new Map(oldWebuis.map((w) => [w.url, w.sha256OfHtmlZip]));
56
+ // Move the entire dist/webuis/ directory from old install to staging
57
+ const oldWebuisDir = path.join(installRoot, "dist", "webuis");
58
+ const newWebuisDir = path.join(stagingDir, "dist", "webuis");
59
+ let webuisDirMoved = false;
60
+ try {
61
+ await fs.access(oldWebuisDir);
62
+ await fs.rename(oldWebuisDir, newWebuisDir);
63
+ webuisDirMoved = true;
64
+ }
65
+ catch {
66
+ // webuis dir missing in old install — will create fresh
67
+ }
68
+ // Identify which webui entries changed
69
+ const changedWebuis = newWebuis.filter((w) => oldWebuisByUrl.get(w.url) !== w.sha256OfHtmlZip);
70
+ if (changedWebuis.length > 0 && webuisDirMoved) {
71
+ // Delete subdirectories for changed webuis so they get re-downloaded.
72
+ // We don't know exact dir names, but we can match by repo name from the URL.
73
+ const existingDirs = await fs.readdir(newWebuisDir, { withFileTypes: true })
74
+ .catch(() => []);
75
+ for (const changed of changedWebuis) {
76
+ const repoName = extractRepoName(changed.url);
77
+ if (!repoName)
78
+ continue;
79
+ for (const entry of existingDirs) {
80
+ if (entry.isDirectory() && entry.name.toLowerCase().includes(repoName.toLowerCase())) {
81
+ await fs.rm(path.join(newWebuisDir, entry.name), { recursive: true, force: true });
82
+ }
83
+ }
84
+ }
85
+ }
86
+ // Phase: atomic swap
87
+ await fs.rename(installRoot, backupDir);
88
+ await fs.rename(stagingDir, installRoot);
89
+ nodeModulesMoved = false; // now part of installRoot, no rollback needed
90
+ // Phase: download changed/missing webuis
91
+ if (changedWebuis.length > 0 || !webuisDirMoved) {
92
+ const webuisDir = path.join(installRoot, "dist", "webuis");
93
+ await fs.mkdir(webuisDir, { recursive: true });
94
+ const toDownload = !webuisDirMoved ? newWebuis : changedWebuis;
95
+ if (toDownload.length > 0) {
96
+ log(`Downloading ${toDownload.length} changed web UI(s)...`);
97
+ for (const entry of toDownload) {
98
+ try {
99
+ await downloadWebui(entry, webuisDir, log);
100
+ }
101
+ catch (err) {
102
+ log(`Warning: failed to download ${entry.url}: ${err.message}`);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ // Phase: cleanup backup
108
+ await fs.rm(backupDir, { recursive: true, force: true });
109
+ return true;
110
+ }
111
+ catch (err) {
112
+ // Rollback: restore node_modules if we moved it out
113
+ if (nodeModulesMoved) {
114
+ try {
115
+ await fs.rename(path.join(stagingDir, "node_modules"), path.join(installRoot, "node_modules"));
116
+ }
117
+ catch {
118
+ // best-effort
119
+ }
120
+ }
121
+ // If installRoot was renamed to backup but staging didn't land, restore it
122
+ try {
123
+ await fs.access(installRoot);
124
+ }
125
+ catch {
126
+ try {
127
+ await fs.rename(backupDir, installRoot);
128
+ }
129
+ catch {
130
+ // best-effort
131
+ }
132
+ }
133
+ await fs.rm(stagingDir, { recursive: true, force: true });
134
+ log(`Fast update failed: ${err.message}`);
135
+ return false;
136
+ }
137
+ }
138
+ /** Extract repo name from a GitHub release URL (e.g. "seedit" from ".../plebbit/seedit/releases/tag/v0.5.10") */
139
+ function extractRepoName(url) {
140
+ const match = url.match(/github\.com\/[^/]+\/([^/]+)\/releases\/tag\//);
141
+ return match?.[1];
142
+ }
143
+ /** Download a single webui entry — mirrors the logic in bin/postinstall.js */
144
+ async function downloadWebui(entry, webuisDir, log) {
145
+ const match = entry.url.match(/github\.com\/([^/]+\/[^/]+)\/releases\/tag\/(.+)$/);
146
+ if (!match)
147
+ throw new Error(`Could not parse GitHub release URL: ${entry.url}`);
148
+ const [, ownerRepo, tag] = match;
149
+ const githubToken = process.env["GITHUB_TOKEN"];
150
+ const headers = {};
151
+ if (githubToken)
152
+ headers["authorization"] = `Bearer ${githubToken}`;
153
+ const releaseReq = await fetch(`https://api.github.com/repos/${ownerRepo}/releases/tags/${tag}`, { headers });
154
+ if (!releaseReq.ok)
155
+ throw new Error(`Failed to fetch release ${ownerRepo}@${tag}, status ${releaseReq.status}`);
156
+ const release = await releaseReq.json();
157
+ const htmlZipAsset = release.assets.find((asset) => asset.name.includes("html"));
158
+ if (!htmlZipAsset)
159
+ throw new Error(`No HTML zip asset in ${ownerRepo}@${tag}`);
160
+ const zipfilePath = path.join(webuisDir, htmlZipAsset.name);
161
+ const downloadReq = await fetch(htmlZipAsset["browser_download_url"], { headers });
162
+ if (!downloadReq.ok || !downloadReq.body)
163
+ throw new Error(`Failed to download ${htmlZipAsset.name}, status ${downloadReq.status}`);
164
+ const writer = createWriteStream(zipfilePath);
165
+ await streamFinished(Readable.fromWeb(downloadReq.body).pipe(writer));
166
+ writer.close();
167
+ // Verify SHA-256 checksum
168
+ const fileBuffer = await fs.readFile(zipfilePath);
169
+ const actualHash = createHash("sha256").update(fileBuffer).digest("hex");
170
+ if (actualHash !== entry.sha256OfHtmlZip) {
171
+ await fs.rm(zipfilePath);
172
+ throw new Error(`SHA-256 mismatch for ${htmlZipAsset.name}! Expected: ${entry.sha256OfHtmlZip}, Actual: ${actualHash}`);
173
+ }
174
+ await decompress(zipfilePath, webuisDir);
175
+ await fs.rm(zipfilePath);
176
+ // Rename index.html to prevent access to unconfigured version
177
+ const extractedDirName = htmlZipAsset.name.replace(".zip", "");
178
+ const indexPath = path.join(webuisDir, extractedDirName, "index.html");
179
+ const backupPath = path.join(webuisDir, extractedDirName, "index_backup_no_rpc.html");
180
+ await fs.rename(indexPath, backupPath);
181
+ log(`Downloaded ${ownerRepo}@${tag}`);
182
+ }
@@ -1,3 +1,4 @@
1
+ export declare const PACKAGE_NAME = "@bitsocial/bitsocial-cli";
1
2
  /** Query npm registry for the latest published version. */
2
3
  export declare function fetchLatestVersion(): Promise<string>;
3
4
  /** Query npm registry for all published versions (oldest-first). */
@@ -1,6 +1,6 @@
1
1
  import { spawn } from "child_process";
2
2
  import { getNpmCliPath, getNpmEnv, ensureNpmAvailable } from "../challenge-packages/challenge-utils.js";
3
- const PACKAGE_NAME = "@bitsocial/bitsocial-cli";
3
+ export const PACKAGE_NAME = "@bitsocial/bitsocial-cli";
4
4
  function runNpmView(args) {
5
5
  return new Promise(async (resolve, reject) => {
6
6
  const npmCliPath = await getNpmCliPath();
@@ -388,11 +388,11 @@
388
388
  "required": true
389
389
  }
390
390
  },
391
- "description": "Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js",
391
+ "description": "Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js\n\nMerge behavior with CLI flags:\n - Objects are merged with the community's current state (new keys are added, existing keys are overwritten).\n - Arrays are extended: new values are prepended to the existing array.\n - Setting a value to null removes it (e.g. --roles['mod.bso'] null).\n\nMerge behavior with --jsonFile:\n - Objects are merged the same way as CLI flags.\n - Arrays are replaced entirely (RFC 7396 JSON Merge Patch semantics).\n - When both --jsonFile and CLI flags are provided, CLI flags take priority.\n\nFor modifying complex settings like challenges, consider using a web UI instead: https://bitsocial.net/apps",
392
392
  "examples": [
393
393
  {
394
- "description": "Change the address of the community to a new domain address",
395
- "command": "bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --address newAddress.bso"
394
+ "description": "Change the name of the community",
395
+ "command": "bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --name newName.bso"
396
396
  },
397
397
  {
398
398
  "description": "Add the author address 'esteban.bso' as an admin on the community",
@@ -576,6 +576,10 @@
576
576
  {
577
577
  "description": "Start all communities in your data path",
578
578
  "command": "bitsocial community start $(bitsocial community list -q)"
579
+ },
580
+ {
581
+ "description": "Start communities sequentially (no concurrency)",
582
+ "command": "bitsocial community start $(bitsocial community list -q) --concurrency 1"
579
583
  }
580
584
  ],
581
585
  "flags": {
@@ -587,6 +591,14 @@
587
591
  "hasDynamicHelp": false,
588
592
  "multiple": false,
589
593
  "type": "option"
594
+ },
595
+ "concurrency": {
596
+ "description": "Number of communities to start in parallel",
597
+ "name": "concurrency",
598
+ "default": 5,
599
+ "hasDynamicHelp": false,
600
+ "multiple": false,
601
+ "type": "option"
590
602
  }
591
603
  },
592
604
  "hasDynamicHelp": false,
@@ -758,5 +770,5 @@
758
770
  ]
759
771
  }
760
772
  },
761
- "version": "0.19.46"
773
+ "version": "0.19.47"
762
774
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/bitsocial-cli",
3
- "version": "0.19.46",
3
+ "version": "0.19.47",
4
4
  "description": "Command line interface to Bitsocial API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "homepage": "https://github.com/bitsocialnet/bitsocial-cli",
@@ -118,13 +118,14 @@
118
118
  "@oclif/plugin-help": "6.2.36",
119
119
  "@oclif/plugin-not-found": "3.2.73",
120
120
  "@oclif/table": "0.5.1",
121
- "@pkcprotocol/pkc-js": "0.0.17",
121
+ "@pkcprotocol/pkc-js": "0.0.19",
122
122
  "dataobject-parser": "1.2.22",
123
123
  "decompress": "4.2.1",
124
124
  "env-paths": "2.2.1",
125
125
  "exit-hook": "4.0.0",
126
126
  "express": "4.19.2",
127
127
  "kubo": "0.40.1",
128
+ "p-limit": "7.3.0",
128
129
  "strip-json-comments": "5.0.3",
129
130
  "tcp-port-used": "1.0.2",
130
131
  "tslib": "2.6.2",