@bitsocial/bitsocial-cli 0.19.44 → 0.19.46

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.44/src/cli/commands/challenge/install.ts)_
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)_
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.44/src/cli/commands/challenge/list.ts)_
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)_
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.44/src/cli/commands/challenge/remove.ts)_
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)_
396
396
 
397
397
  ## `bitsocial community create`
398
398
 
@@ -400,12 +400,13 @@ Create a community with specific properties. A newly created community will be s
400
400
 
401
401
  ```
402
402
  USAGE
403
- $ bitsocial community create --pkcRpcUrl <value> [--privateKeyPath <value>]
403
+ $ bitsocial community create --pkcRpcUrl <value> [--privateKeyPath <value>] [-f <value>]
404
404
 
405
405
  FLAGS
406
- --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
407
- --privateKeyPath=<value> Private key (PEM) of the community signer that will be used to determine address (if address
408
- is not a domain). If it's not provided then PKC will generate a private key
406
+ -f, --jsonFile=<value> Path to a JSON/JSONC file containing create options (supports comments)
407
+ --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
408
+ --privateKeyPath=<value> Private key (PEM) of the community signer that will be used to determine address (if
409
+ address is not a domain). If it's not provided then PKC will generate a private key
409
410
 
410
411
  DESCRIPTION
411
412
  Create a community with specific properties. A newly created community will be started after creation and be able to
@@ -415,9 +416,13 @@ EXAMPLES
415
416
  Create a community with title 'Hello Plebs' and description 'Welcome'
416
417
 
417
418
  $ bitsocial community create --title 'Hello Plebs' --description 'Welcome'
419
+
420
+ Create a community using options from a JSON/JSONC file
421
+
422
+ $ bitsocial community create --jsonFile ./create-options.json
418
423
  ```
419
424
 
420
- _See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/create.ts)_
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)_
421
426
 
422
427
  ## `bitsocial community delete ADDRESSES`
423
428
 
@@ -442,7 +447,7 @@ EXAMPLES
442
447
  $ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
443
448
  ```
444
449
 
445
- _See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/delete.ts)_
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)_
446
451
 
447
452
  ## `bitsocial community edit ADDRESS`
448
453
 
@@ -450,13 +455,14 @@ Edit a community's properties. For a list of properties, visit https://github.co
450
455
 
451
456
  ```
452
457
  USAGE
453
- $ bitsocial community edit ADDRESS --pkcRpcUrl <value>
458
+ $ bitsocial community edit ADDRESS --pkcRpcUrl <value> [-f <value>]
454
459
 
455
460
  ARGUMENTS
456
- ADDRESS Address of the community to edit
461
+ ADDRESS Address of the community to edit. It could be the name domain, or a public key
457
462
 
458
463
  FLAGS
459
- --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
464
+ -f, --jsonFile=<value> Path to a JSON/JSONC file containing edit options (supports comments)
465
+ --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
460
466
 
461
467
  DESCRIPTION
462
468
  Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js
@@ -468,33 +474,38 @@ EXAMPLES
468
474
 
469
475
  Add the author address 'esteban.bso' as an admin on the community
470
476
 
471
- $ bitsocial community edit mysub.bso '--roles["esteban.bso"].role' admin
477
+ $ bitsocial community edit mycommunity.bso '--roles["esteban.bso"].role' admin
472
478
 
473
479
  Add two challenges to the community. The first challenge will be a question and answer, and the second will be an
474
480
  image captcha
475
481
 
476
- $ bitsocial community edit mysub.bso --settings.challenges[0].name question \
482
+ $ bitsocial community edit mycommunity.bso --settings.challenges[0].name question \
477
483
  --settings.challenges[0].options.question "what is the password?" --settings.challenges[0].options.answer \
478
484
  thepassword --settings.challenges[1].name captcha-canvas-v3
479
485
 
480
486
  Change the title and description
481
487
 
482
- $ bitsocial community edit mysub.bso --title "This is the new title" --description "This is the new description"
488
+ $ bitsocial community edit mycommunity.bso --title "This is the new title" --description "This is the new \
489
+ description"
483
490
 
484
491
  Remove a role from a moderator/admin/owner
485
492
 
486
- $ bitsocial community edit plebbit.bso --roles['rinse12.bso'] null
493
+ $ bitsocial community edit bitsocial.bso --roles['rinse12.bso'] null
487
494
 
488
495
  Enable settings.fetchThumbnailUrls to fetch the thumbnail of url submitted by authors
489
496
 
490
- $ bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls
497
+ $ bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls
491
498
 
492
499
  disable settings.fetchThumbnailUrls
493
500
 
494
- $ bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls=false
501
+ $ bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false
502
+
503
+ Edit a community using options from a JSON/JSONC file
504
+
505
+ $ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
495
506
  ```
496
507
 
497
- _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/edit.ts)_
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)_
498
509
 
499
510
  ## `bitsocial community get [ADDRESS]`
500
511
 
@@ -525,7 +536,7 @@ EXAMPLES
525
536
  $ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
526
537
  ```
527
538
 
528
- _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/get.ts)_
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)_
529
540
 
530
541
  ## `bitsocial community list`
531
542
 
@@ -548,7 +559,7 @@ EXAMPLES
548
559
  $ bitsocial community list
549
560
  ```
550
561
 
551
- _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/list.ts)_
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)_
552
563
 
553
564
  ## `bitsocial community start ADDRESSES`
554
565
 
@@ -577,7 +588,7 @@ EXAMPLES
577
588
  $ bitsocial community start $(bitsocial community list -q)
578
589
  ```
579
590
 
580
- _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/start.ts)_
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)_
581
592
 
582
593
  ## `bitsocial community stop ADDRESSES`
583
594
 
@@ -602,7 +613,7 @@ EXAMPLES
602
613
  $ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
603
614
  ```
604
615
 
605
- _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/stop.ts)_
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)_
606
617
 
607
618
  ## `bitsocial daemon`
608
619
 
@@ -643,7 +654,7 @@ EXAMPLES
643
654
  $ bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY
644
655
  ```
645
656
 
646
- _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/daemon.ts)_
657
+ _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/daemon.ts)_
647
658
 
648
659
  ## `bitsocial help [COMMAND]`
649
660
 
@@ -671,7 +682,8 @@ View the latest BitSocial daemon log file. By default dumps the full log and exi
671
682
 
672
683
  ```
673
684
  USAGE
674
- $ bitsocial logs [-f] [-n <value>] [--since <value>] [--until <value>] [--logPath <value>]
685
+ $ bitsocial logs [-f] [-n <value>] [--since <value>] [--until <value>] [--logPath <value>] [--stdout |
686
+ --stderr]
675
687
 
676
688
  FLAGS
677
689
  -f, --follow Follow log output in real-time (like tail -f)
@@ -679,6 +691,8 @@ FLAGS
679
691
  --logPath=<value> Specify the directory containing log files
680
692
  --since=<value> Show logs since timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s,
681
693
  42m, 2h, 1d)
694
+ --stderr Show only stderr log entries (output of pkc-logger library)
695
+ --stdout Show only stdout log entries
682
696
  --until=<value> Show logs before timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s,
683
697
  42m, 2h, 1d)
684
698
 
@@ -698,9 +712,15 @@ EXAMPLES
698
712
  $ bitsocial logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z
699
713
 
700
714
  $ bitsocial logs --since 1h -f
715
+
716
+ $ bitsocial logs --stdout
717
+
718
+ $ bitsocial logs --stderr
719
+
720
+ $ bitsocial logs --stdout -f
701
721
  ```
702
722
 
703
- _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/logs.ts)_
723
+ _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.46/src/cli/commands/logs.ts)_
704
724
 
705
725
  ## `bitsocial update check`
706
726
 
@@ -717,7 +737,7 @@ EXAMPLES
717
737
  $ bitsocial update check
718
738
  ```
719
739
 
720
- _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/update/check.ts)_
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)_
721
741
 
722
742
  ## `bitsocial update install [VERSION]`
723
743
 
@@ -749,7 +769,7 @@ EXAMPLES
749
769
  $ bitsocial update install --no-restart-daemons
750
770
  ```
751
771
 
752
- _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/update/install.ts)_
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)_
753
773
 
754
774
  ## `bitsocial update versions`
755
775
 
@@ -771,7 +791,7 @@ EXAMPLES
771
791
  $ bitsocial update versions --limit 5
772
792
  ```
773
793
 
774
- _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/update/versions.ts)_
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)_
775
795
  <!-- commandsstop -->
776
796
 
777
797
  ## Contribution
@@ -7,6 +7,7 @@ export default class Create extends BaseCommand {
7
7
  }[];
8
8
  static flags: {
9
9
  privateKeyPath: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
+ jsonFile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
10
11
  };
11
12
  run(): Promise<void>;
12
13
  }
@@ -3,7 +3,7 @@ import { Flags } from "@oclif/core";
3
3
  import DataObjectParser from "dataobject-parser";
4
4
  import fs from "fs";
5
5
  import { BaseCommand } from "../../base-command.js";
6
- import { PKCLogger } from "../../../util.js";
6
+ import { PKCLogger, mergeDeep, parseJsoncFile } from "../../../util.js";
7
7
  import * as remeda from "remeda";
8
8
  export default class Create extends BaseCommand {
9
9
  static description = "Create a community with specific properties. A newly created community will be started after creation and be able to receive publications. For a list of properties, visit https://github.com/pkcprotocol/pkc-js";
@@ -11,12 +11,21 @@ export default class Create extends BaseCommand {
11
11
  {
12
12
  description: "Create a community with title 'Hello Plebs' and description 'Welcome'",
13
13
  command: "<%= config.bin %> <%= command.id %> --title 'Hello Plebs' --description 'Welcome'"
14
+ },
15
+ {
16
+ description: "Create a community using options from a JSON/JSONC file",
17
+ command: "<%= config.bin %> <%= command.id %> --jsonFile ./create-options.json"
14
18
  }
15
19
  ];
16
20
  static flags = {
17
21
  privateKeyPath: Flags.file({
18
22
  exists: true,
19
23
  description: "Private key (PEM) of the community signer that will be used to determine address (if address is not a domain). If it's not provided then PKC will generate a private key"
24
+ }),
25
+ jsonFile: Flags.file({
26
+ char: "f",
27
+ exists: true,
28
+ description: "Path to a JSON/JSONC file containing create options (supports comments)"
20
29
  })
21
30
  };
22
31
  async run() {
@@ -24,7 +33,34 @@ export default class Create extends BaseCommand {
24
33
  const log = PKCLogger("bitsocial-cli:commands:community:create");
25
34
  log(`flags: `, flags);
26
35
  const pkc = await this._connectToPkcRpc(flags.pkcRpcUrl.toString());
27
- const createOptions = DataObjectParser.transpose(remeda.omit(flags, ["pkcRpcUrl", "privateKeyPath"]))["_data"];
36
+ const cliCreateOptions = DataObjectParser.transpose(remeda.omit(flags, ["pkcRpcUrl", "privateKeyPath", "jsonFile"]))["_data"];
37
+ // Parse JSONC file if provided
38
+ let jsonFileOptions = {};
39
+ if (flags.jsonFile) {
40
+ try {
41
+ jsonFileOptions = await parseJsoncFile(flags.jsonFile);
42
+ log("JSONC file options parsed:", jsonFileOptions);
43
+ }
44
+ catch (e) {
45
+ if (e instanceof Error) {
46
+ await pkc.destroy();
47
+ this.error(e.message);
48
+ }
49
+ throw e;
50
+ }
51
+ }
52
+ // Merge: JSON file options first, then CLI flags override
53
+ let createOptions;
54
+ if (flags.jsonFile && Object.keys(cliCreateOptions).length > 0) {
55
+ createOptions = mergeDeep(jsonFileOptions, cliCreateOptions);
56
+ }
57
+ else if (flags.jsonFile) {
58
+ createOptions = jsonFileOptions;
59
+ }
60
+ else {
61
+ createOptions = cliCreateOptions;
62
+ }
63
+ log("Final create options:", createOptions);
28
64
  if (flags.privateKeyPath)
29
65
  try {
30
66
  //@ts-expect-error
@@ -4,6 +4,9 @@ export default class Edit extends BaseCommand {
4
4
  static args: {
5
5
  address: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
6
6
  };
7
+ static flags: {
8
+ jsonFile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
9
+ };
7
10
  static examples: {
8
11
  description: string;
9
12
  command: string;
@@ -1,8 +1,8 @@
1
1
  //@ts-expect-error
2
2
  import DataObjectParser from "dataobject-parser";
3
- import { Args } from "@oclif/core";
3
+ import { Args, Flags } from "@oclif/core";
4
4
  import { BaseCommand } from "../../base-command.js";
5
- import { PKCLogger, mergeDeep } from "../../../util.js";
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
8
  static description = "Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js";
@@ -10,37 +10,51 @@ export default class Edit extends BaseCommand {
10
10
  address: Args.string({
11
11
  name: "address",
12
12
  required: true,
13
- description: "Address of the community to edit"
13
+ description: "Address of the community to edit. It could be the name domain, or a public key"
14
+ })
15
+ };
16
+ static flags = {
17
+ jsonFile: Flags.file({
18
+ char: "f",
19
+ exists: true,
20
+ description: "Path to a JSON/JSONC file containing edit options (supports comments)"
14
21
  })
15
22
  };
16
23
  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
17
27
  {
18
28
  description: "Change the address of the community to a new domain address",
19
29
  command: "bitsocial community edit 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu --address newAddress.bso"
20
30
  },
21
31
  {
22
32
  description: "Add the author address 'esteban.bso' as an admin on the community",
23
- command: `bitsocial community edit mysub.bso '--roles["esteban.bso"].role' admin`
33
+ command: `bitsocial community edit mycommunity.bso '--roles["esteban.bso"].role' admin`
24
34
  },
25
35
  {
26
36
  description: "Add two challenges to the community. The first challenge will be a question and answer, and the second will be an image captcha",
27
- command: `bitsocial community edit mysub.bso --settings.challenges[0].name question --settings.challenges[0].options.question "what is the password?" --settings.challenges[0].options.answer thepassword --settings.challenges[1].name captcha-canvas-v3`
37
+ command: `bitsocial community edit mycommunity.bso --settings.challenges[0].name question --settings.challenges[0].options.question "what is the password?" --settings.challenges[0].options.answer thepassword --settings.challenges[1].name captcha-canvas-v3`
28
38
  },
29
39
  {
30
40
  description: "Change the title and description",
31
- command: `bitsocial community edit mysub.bso --title "This is the new title" --description "This is the new description" `
41
+ command: `bitsocial community edit mycommunity.bso --title "This is the new title" --description "This is the new description" `
32
42
  },
33
43
  {
34
44
  description: "Remove a role from a moderator/admin/owner",
35
- command: "bitsocial community edit plebbit.bso --roles['rinse12.bso'] null"
45
+ command: "bitsocial community edit bitsocial.bso --roles['rinse12.bso'] null"
36
46
  },
37
47
  {
38
48
  description: "Enable settings.fetchThumbnailUrls to fetch the thumbnail of url submitted by authors",
39
- command: "bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls"
49
+ command: "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls"
40
50
  },
41
51
  {
42
52
  description: "disable settings.fetchThumbnailUrls",
43
- command: "bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls=false"
53
+ command: "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false"
54
+ },
55
+ {
56
+ description: "Edit a community using options from a JSON/JSONC file",
57
+ command: "bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json"
44
58
  }
45
59
  ];
46
60
  async run() {
@@ -48,17 +62,45 @@ export default class Edit extends BaseCommand {
48
62
  const log = PKCLogger("bitsocial-cli:commands:community:edit");
49
63
  log(`flags: `, flags);
50
64
  const pkc = await this._connectToPkcRpc(flags.pkcRpcUrl.toString());
51
- const editOptions = DataObjectParser.transpose(remeda.omit(flags, ["pkcRpcUrl"]))["_data"];
52
- log("Edit options parsed:", editOptions);
65
+ const cliEditOptions = DataObjectParser.transpose(remeda.omit(flags, ["pkcRpcUrl", "jsonFile"]))["_data"];
66
+ log("CLI edit options parsed:", cliEditOptions);
67
+ // Parse JSONC file if provided
68
+ let jsonFileOptions = {};
69
+ if (flags.jsonFile) {
70
+ try {
71
+ jsonFileOptions = await parseJsoncFile(flags.jsonFile);
72
+ log("JSONC file options parsed:", jsonFileOptions);
73
+ }
74
+ catch (e) {
75
+ if (e instanceof Error) {
76
+ this.error(e.message);
77
+ }
78
+ throw e;
79
+ }
80
+ }
81
+ // Merge: JSON file options first, then CLI flags override
82
+ let editOptions;
83
+ if (flags.jsonFile && Object.keys(cliEditOptions).length > 0) {
84
+ editOptions = mergeDeep(jsonFileOptions, cliEditOptions);
85
+ }
86
+ else if (flags.jsonFile) {
87
+ editOptions = jsonFileOptions;
88
+ }
89
+ else {
90
+ editOptions = cliEditOptions;
91
+ }
92
+ log("Final edit options:", editOptions);
53
93
  const localCommunities = pkc.communities;
54
94
  if (!localCommunities.includes(args.address))
55
95
  this.error("Can't edit a remote community, make sure you're editing a local community");
56
96
  try {
57
97
  const community = await pkc.createCommunity({ address: args.address });
58
98
  const mergedState = remeda.pick(community, remeda.keys.strict(editOptions));
59
- const finalMergedState = mergeDeep(mergedState, editOptions);
99
+ // JSON file edits use RFC 7396 JSON Merge Patch semantics (arrays replace, objects merge).
100
+ // CLI flag edits use concat semantics (arrays extend with new values).
101
+ const finalMergedState = mergeDeep(mergedState, editOptions, flags.jsonFile ? "replace" : "concat");
60
102
  log("Internal community state after merge:", finalMergedState);
61
- await community.edit(finalMergedState);
103
+ await community.edit(replaceNullWithUndefined(finalMergedState));
62
104
  this.log(community.address);
63
105
  }
64
106
  catch (e) {
@@ -96,12 +96,12 @@ export default class Daemon extends Command {
96
96
  const stdoutWrite = process.stdout.write.bind(process.stdout);
97
97
  const stderrWrite = process.stderr.write.bind(process.stderr);
98
98
  const isLogFileOverLimit = () => logFile.bytesWritten > 20000000; // 20mb
99
- const writeTimestampedLine = (text) => {
99
+ const writeTimestampedLine = (text, stream) => {
100
100
  if (isLogFileOverLimit())
101
101
  return;
102
102
  if (!text || text.trim().length === 0)
103
103
  return;
104
- const timestamp = `[${new Date().toISOString()}] `;
104
+ const timestamp = `[${new Date().toISOString()}] [${stream}] `;
105
105
  const lines = text.split("\n");
106
106
  const timestamped = lines.map((line, i) => (i === 0 ? timestamp + line : line)).join("\n");
107
107
  logFile.write(timestamped);
@@ -115,13 +115,13 @@ export default class Daemon extends Command {
115
115
  debugModule.inspectOpts.colors = true;
116
116
  debugModule.inspectOpts.hideDate = true;
117
117
  debugModule.log = (...args) => {
118
- writeTimestampedLine(formatWithOptions({ depth: Logger.inspectOpts?.depth || 10, colors: true }, ...args).trimStart() + EOL);
118
+ writeTimestampedLine(formatWithOptions({ depth: Logger.inspectOpts?.depth || 10, colors: true }, ...args).trimStart() + EOL, "stderr");
119
119
  };
120
120
  const asString = (data) => (typeof data === "string" ? data : Buffer.from(data).toString());
121
121
  process.stdout.write = (...args) => {
122
122
  //@ts-expect-error
123
123
  const res = stdoutWrite(...args);
124
- writeTimestampedLine(asString(args[0]));
124
+ writeTimestampedLine(asString(args[0]), "stdout");
125
125
  return res;
126
126
  };
127
127
  process.stderr.write = (...args) => {
@@ -129,7 +129,7 @@ export default class Daemon extends Command {
129
129
  // Debug output goes to stderr; we want it in logs only.
130
130
  // Real errors are caught by uncaughtException/unhandledRejection handlers
131
131
  // which use console.error -> stderr.write -> this override -> log file.
132
- writeTimestampedLine(asString(args[0]).trimStart());
132
+ writeTimestampedLine(asString(args[0]).trimStart(), "stderr");
133
133
  return true;
134
134
  };
135
135
  const log = Logger("bitsocial-cli:daemon");
@@ -1,6 +1,7 @@
1
1
  import { Command } from "@oclif/core";
2
2
  interface LogEntry {
3
3
  timestamp: Date | null;
4
+ stream: "stdout" | "stderr" | null;
4
5
  lines: string[];
5
6
  }
6
7
  export default class Logs extends Command {
@@ -11,13 +12,17 @@ export default class Logs extends Command {
11
12
  since: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
13
  until: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
14
  logPath: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
15
+ stdout: import("@oclif/core/interfaces").BooleanFlag<boolean>;
16
+ stderr: import("@oclif/core/interfaces").BooleanFlag<boolean>;
14
17
  };
15
18
  static examples: string[];
16
19
  private _findLatestLogFile;
17
20
  _parseTimestamp(value: string): Date;
18
21
  _extractTimestamp(line: string): Date | null;
22
+ _extractStream(line: string): "stdout" | "stderr" | null;
19
23
  _parseLogEntries(content: string): LogEntry[];
20
24
  _filterEntries(entries: LogEntry[], since?: Date, until?: Date): LogEntry[];
25
+ _filterByStream(entries: LogEntry[], stream: "stdout" | "stderr"): LogEntry[];
21
26
  _tailEntries(entries: LogEntry[], tailValue: string): LogEntry[];
22
27
  run(): Promise<void>;
23
28
  }
@@ -27,6 +27,16 @@ export default class Logs extends Command {
27
27
  logPath: Flags.directory({
28
28
  description: "Specify the directory containing log files",
29
29
  required: false
30
+ }),
31
+ stdout: Flags.boolean({
32
+ description: "Show only stdout log entries",
33
+ default: false,
34
+ exclusive: ["stderr"]
35
+ }),
36
+ stderr: Flags.boolean({
37
+ description: "Show only stderr log entries (output of pkc-logger library)",
38
+ default: false,
39
+ exclusive: ["stdout"]
30
40
  })
31
41
  };
32
42
  static examples = [
@@ -35,7 +45,10 @@ export default class Logs extends Command {
35
45
  "bitsocial logs -n 50",
36
46
  "bitsocial logs --since 5m",
37
47
  "bitsocial logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z",
38
- "bitsocial logs --since 1h -f"
48
+ "bitsocial logs --since 1h -f",
49
+ "bitsocial logs --stdout",
50
+ "bitsocial logs --stderr",
51
+ "bitsocial logs --stdout -f"
39
52
  ];
40
53
  async _findLatestLogFile(logPath) {
41
54
  let entries;
@@ -76,6 +89,12 @@ export default class Logs extends Command {
76
89
  return null;
77
90
  return new Date(match[1]);
78
91
  }
92
+ _extractStream(line) {
93
+ const match = line.match(/^\[\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z\] \[(stdout|stderr)\] /);
94
+ if (!match)
95
+ return null;
96
+ return match[1];
97
+ }
79
98
  _parseLogEntries(content) {
80
99
  const lines = content.split("\n");
81
100
  const entries = [];
@@ -83,7 +102,8 @@ export default class Logs extends Command {
83
102
  const timestamp = this._extractTimestamp(line);
84
103
  if (timestamp !== null) {
85
104
  // New timestamped entry
86
- entries.push({ timestamp, lines: [line] });
105
+ const stream = this._extractStream(line);
106
+ entries.push({ timestamp, stream, lines: [line] });
87
107
  }
88
108
  else if (entries.length > 0) {
89
109
  // Continuation line — belongs to the previous entry
@@ -91,7 +111,7 @@ export default class Logs extends Command {
91
111
  }
92
112
  else {
93
113
  // Line before any timestamped entry (legacy/header)
94
- entries.push({ timestamp: null, lines: [line] });
114
+ entries.push({ timestamp: null, stream: null, lines: [line] });
95
115
  }
96
116
  }
97
117
  return entries;
@@ -109,6 +129,9 @@ export default class Logs extends Command {
109
129
  return true;
110
130
  });
111
131
  }
132
+ _filterByStream(entries, stream) {
133
+ return entries.filter((entry) => entry.stream === stream);
134
+ }
112
135
  _tailEntries(entries, tailValue) {
113
136
  if (tailValue === "all")
114
137
  return entries;
@@ -126,33 +149,37 @@ export default class Logs extends Command {
126
149
  const latestLogFile = await this._findLatestLogFile(logPath);
127
150
  const since = flags.since ? this._parseTimestamp(flags.since) : undefined;
128
151
  const until = flags.until ? this._parseTimestamp(flags.until) : undefined;
152
+ const streamFilter = flags.stdout ? "stdout" : flags.stderr ? "stderr" : undefined;
129
153
  if (!flags.follow) {
130
154
  const content = await fsPromise.readFile(latestLogFile, "utf-8");
131
155
  const entries = this._parseLogEntries(content);
132
156
  const filtered = this._filterEntries(entries, since, until);
133
- const tailed = this._tailEntries(filtered, flags.tail);
157
+ const streamFiltered = streamFilter ? this._filterByStream(filtered, streamFilter) : filtered;
158
+ const tailed = this._tailEntries(streamFiltered, flags.tail);
134
159
  const output = tailed.map((e) => e.lines.join("\n")).join("\n");
135
160
  if (output)
136
161
  process.stdout.write(output + "\n");
137
162
  return;
138
163
  }
139
164
  // Follow mode: dump existing content (filtered + tailed) then watch for new data
140
- const existingContent = await fsPromise.readFile(latestLogFile, "utf-8");
165
+ let currentLogFile = latestLogFile;
166
+ const existingContent = await fsPromise.readFile(currentLogFile, "utf-8");
141
167
  const entries = this._parseLogEntries(existingContent);
142
168
  const filtered = this._filterEntries(entries, since, until);
143
- const tailed = this._tailEntries(filtered, flags.tail);
169
+ const streamFiltered = streamFilter ? this._filterByStream(filtered, streamFilter) : filtered;
170
+ const tailed = this._tailEntries(streamFiltered, flags.tail);
144
171
  const initialOutput = tailed.map((e) => e.lines.join("\n")).join("\n");
145
172
  if (initialOutput)
146
173
  process.stdout.write(initialOutput + "\n");
147
- const stat = await fsPromise.stat(latestLogFile);
174
+ const stat = await fsPromise.stat(currentLogFile);
148
175
  let position = stat.size;
149
176
  let pendingBuffer = "";
150
177
  // Watch for new data using polling (works across filesystems including Docker volumes)
151
178
  const readNewData = async () => {
152
179
  try {
153
- const currentStat = await fsPromise.stat(latestLogFile);
180
+ const currentStat = await fsPromise.stat(currentLogFile);
154
181
  if (currentStat.size > position) {
155
- const fd = await fsPromise.open(latestLogFile, "r");
182
+ const fd = await fsPromise.open(currentLogFile, "r");
156
183
  const buf = new Uint8Array(currentStat.size - position);
157
184
  const { bytesRead } = await fd.read(buf, 0, buf.length, position);
158
185
  await fd.close();
@@ -166,14 +193,15 @@ export default class Logs extends Command {
166
193
  }
167
194
  pendingBuffer = chunk.slice(lastNewline + 1);
168
195
  const completeText = chunk.slice(0, lastNewline + 1);
169
- if (!since && !until) {
170
- // No time filtering — pass through directly
196
+ if (!since && !until && !streamFilter) {
197
+ // No filtering — pass through directly
171
198
  process.stdout.write(completeText);
172
199
  }
173
200
  else {
174
201
  const newEntries = this._parseLogEntries(completeText.replace(/\n$/, ""));
175
202
  const filteredNew = this._filterEntries(newEntries, since, until);
176
- const output = filteredNew.map((e) => e.lines.join("\n")).join("\n");
203
+ const streamFilteredNew = streamFilter ? this._filterByStream(filteredNew, streamFilter) : filteredNew;
204
+ const output = streamFilteredNew.map((e) => e.lines.join("\n")).join("\n");
177
205
  if (output)
178
206
  process.stdout.write(output + "\n");
179
207
  }
@@ -183,14 +211,67 @@ export default class Logs extends Command {
183
211
  // File may have been rotated or deleted
184
212
  }
185
213
  };
186
- fs.watchFile(latestLogFile, { interval: 300 }, readNewData);
214
+ // Periodically check if a newer log file has appeared (e.g. after daemon restart)
215
+ const checkForNewLogFile = async () => {
216
+ try {
217
+ const newestFile = await this._findLatestLogFile(logPath);
218
+ if (newestFile === currentLogFile)
219
+ return;
220
+ // Flush any remaining partial line from old file
221
+ if (pendingBuffer) {
222
+ if (!since && !until && !streamFilter) {
223
+ process.stdout.write(pendingBuffer + "\n");
224
+ }
225
+ else {
226
+ const pbEntries = this._parseLogEntries(pendingBuffer);
227
+ const pbFiltered = this._filterEntries(pbEntries, since, until);
228
+ const pbStreamFiltered = streamFilter ? this._filterByStream(pbFiltered, streamFilter) : pbFiltered;
229
+ const pbOutput = pbStreamFiltered.map((e) => e.lines.join("\n")).join("\n");
230
+ if (pbOutput)
231
+ process.stdout.write(pbOutput + "\n");
232
+ }
233
+ }
234
+ // Switch watchers
235
+ fs.unwatchFile(currentLogFile, readNewData);
236
+ currentLogFile = newestFile;
237
+ pendingBuffer = "";
238
+ process.stderr.write(`\n--- switched to new log file: ${path.basename(newestFile)} ---\n\n`);
239
+ // Read and output entire new file content (with filters, no tail limit)
240
+ const newContent = await fsPromise.readFile(currentLogFile, "utf-8");
241
+ if (newContent) {
242
+ if (!since && !until && !streamFilter) {
243
+ process.stdout.write(newContent);
244
+ }
245
+ else {
246
+ const newEntries = this._parseLogEntries(newContent.replace(/\n$/, ""));
247
+ const filteredNew = this._filterEntries(newEntries, since, until);
248
+ const streamFilteredNew = streamFilter
249
+ ? this._filterByStream(filteredNew, streamFilter)
250
+ : filteredNew;
251
+ const output = streamFilteredNew.map((e) => e.lines.join("\n")).join("\n");
252
+ if (output)
253
+ process.stdout.write(output + "\n");
254
+ }
255
+ }
256
+ const newStat = await fsPromise.stat(currentLogFile);
257
+ position = newStat.size;
258
+ fs.watchFile(currentLogFile, { interval: 300 }, readNewData);
259
+ }
260
+ catch {
261
+ // Directory listing failed or file disappeared — retry next cycle
262
+ }
263
+ };
264
+ fs.watchFile(currentLogFile, { interval: 300 }, readNewData);
265
+ const newFileCheckInterval = setInterval(checkForNewLogFile, 3000);
187
266
  // Keep the process alive and clean up on exit
188
267
  process.on("SIGINT", () => {
189
- fs.unwatchFile(latestLogFile, readNewData);
268
+ clearInterval(newFileCheckInterval);
269
+ fs.unwatchFile(currentLogFile, readNewData);
190
270
  process.exit(0);
191
271
  });
192
272
  process.on("SIGTERM", () => {
193
- fs.unwatchFile(latestLogFile, readNewData);
273
+ clearInterval(newFileCheckInterval);
274
+ fs.unwatchFile(currentLogFile, readNewData);
194
275
  process.exit(0);
195
276
  });
196
277
  // Keep process alive
@@ -11,4 +11,6 @@ export default class Install extends Command {
11
11
  static examples: string[];
12
12
  run(): Promise<void>;
13
13
  private _restartDaemons;
14
+ private _connectToRpc;
15
+ private _reportCommunityStatus;
14
16
  }
@@ -4,6 +4,11 @@ import tcpPortUsed from "tcp-port-used";
4
4
  import { fetchLatestVersion, installGlobal } from "../../../update/npm-registry.js";
5
5
  import { compareVersions } from "../../../update/semver.js";
6
6
  import { getAliveDaemonStates } from "../../../common-utils/daemon-state.js";
7
+ import PKC from "@pkcprotocol/pkc-js";
8
+ const getPKCConnectOverride = () => {
9
+ const globalWithOverride = globalThis;
10
+ return globalWithOverride.__PKC_RPC_CONNECT_OVERRIDE;
11
+ };
7
12
  export default class Install extends Command {
8
13
  static description = "Install a specific version of bitsocial from npm";
9
14
  static args = {
@@ -122,10 +127,68 @@ export default class Install extends Command {
122
127
  const started = await tcpPortUsed.waitUntilUsed(port, 500, 30000).then(() => true).catch(() => false);
123
128
  if (started) {
124
129
  this.log(` Daemon started (port ${port}).`);
130
+ await this._reportCommunityStatus(d.pkcRpcUrl);
125
131
  }
126
132
  else {
127
133
  this.warn(` Daemon may not have started — port ${port} not responding after 30s. Check logs with: bitsocial logs`);
128
134
  }
129
135
  }
130
136
  }
137
+ async _connectToRpc(pkcRpcUrl) {
138
+ const connectOverride = getPKCConnectOverride();
139
+ if (connectOverride) {
140
+ return connectOverride(pkcRpcUrl);
141
+ }
142
+ const pkc = await PKC({ pkcRpcClientsOptions: [pkcRpcUrl] });
143
+ const errors = [];
144
+ pkc.on("error", (err) => {
145
+ errors.push(err);
146
+ });
147
+ await new Promise((resolve) => {
148
+ const timeout = setTimeout(() => {
149
+ pkc.removeListener("communitieschange", handler);
150
+ resolve();
151
+ }, 20000);
152
+ const handler = () => {
153
+ if (pkc.communities.length > 0) {
154
+ pkc.removeListener("communitieschange", handler);
155
+ clearTimeout(timeout);
156
+ resolve();
157
+ }
158
+ };
159
+ pkc.on("communitieschange", handler);
160
+ });
161
+ return pkc;
162
+ }
163
+ async _reportCommunityStatus(pkcRpcUrl) {
164
+ let pkc;
165
+ try {
166
+ pkc = await this._connectToRpc(pkcRpcUrl);
167
+ const communities = pkc.communities;
168
+ if (communities.length === 0)
169
+ 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;
175
+ 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`);
184
+ }
185
+ }
186
+ catch {
187
+ this.warn("Could not check community status.");
188
+ }
189
+ finally {
190
+ if (pkc)
191
+ await pkc.destroy().catch(() => { });
192
+ }
193
+ }
131
194
  }
@@ -60,6 +60,10 @@ const traverseObjectToSetAsFlagInOclif = (opts, flagsGrouped, path = "") => {
60
60
  if (Object.keys(flagsGrouped[flagName]).length !== 0)
61
61
  continue;
62
62
  }
63
+ const fullFlagName = path + flagName;
64
+ // Skip flags that are already statically declared on the command
65
+ if (opts.Command.flags && fullFlagName in opts.Command.flags)
66
+ continue;
63
67
  const flagsIndices = Object.assign({}, ...Object.keys(flagsGrouped).map((flag) => ({ [flag]: 0 })));
64
68
  const multipleValues = Array.isArray(flagsGrouped[flagName]) && flagsGrouped[flagName].length > 1;
65
69
  const parsedValue = flagsGrouped[flagName];
@@ -76,9 +80,18 @@ const traverseObjectToSetAsFlagInOclif = (opts, flagsGrouped, path = "") => {
76
80
  });
77
81
  }
78
82
  };
83
+ // Tracks original static flags per Command class so dynamic flags can be reset across invocations (e.g. in tests)
84
+ const originalStaticFlags = new WeakMap();
79
85
  const hook = async function (opts) {
80
86
  // Need to parse flag here and add it to opts.Command._flags
81
87
  if (opts.Command.id === "community:edit" || opts.Command.id === "community:create" || opts.Command.id === "daemon") {
88
+ // Snapshot static flags on first run; restore on subsequent runs (test isolation)
89
+ if (!originalStaticFlags.has(opts.Command)) {
90
+ originalStaticFlags.set(opts.Command, opts.Command.flags ? { ...opts.Command.flags } : {});
91
+ }
92
+ else {
93
+ opts.Command.flags = { ...originalStaticFlags.get(opts.Command) };
94
+ }
82
95
  // Parse the dynamic flags and add them to opts.Command.flags so that it wouldn't throw
83
96
  if (opts.argv.length <= 1)
84
97
  return; // if no flags are provided, then we don't need to do anything
package/dist/util.d.ts CHANGED
@@ -25,8 +25,13 @@ export declare function getLanIpV4Address(): string | undefined;
25
25
  export declare function loadKuboConfigFile(pkcDataPath: string): Promise<any | undefined>;
26
26
  export declare function parseMultiAddrKuboRpcToUrl(kuboMultiAddrString: string): Promise<import("url").URL>;
27
27
  export declare function parseMultiAddrIpfsGatewayToUrl(ipfsGatewaymultiAddrString: string): Promise<import("url").URL>;
28
+ /** Recursively replaces all `null` values with `undefined`.
29
+ * Used before calling community.edit() since pkc-js expects `undefined` for removal,
30
+ * but JSON/CLI input produces `null`. */
31
+ export declare function replaceNullWithUndefined(obj: any): any;
28
32
  /**
29
33
  * Custom merge function that implements CLI-specific merge behavior.
30
34
  * This matches the expected behavior from the test suite.
31
35
  */
32
- export declare function mergeDeep(target: any, source: any): any;
36
+ export declare function mergeDeep(target: any, source: any, arrayStrategy?: "concat" | "replace"): any;
37
+ export declare function parseJsoncFile(filePath: string): Promise<Record<string, unknown>>;
package/dist/util.js CHANGED
@@ -2,6 +2,7 @@ import os from "os";
2
2
  import path from "path";
3
3
  import fs from "fs";
4
4
  import * as fsPromises from "fs/promises";
5
+ import stripJsonComments from "strip-json-comments";
5
6
  import PKCLogger from "@pkcprotocol/pkc-logger";
6
7
  export { PKCLogger };
7
8
  /**
@@ -75,11 +76,28 @@ export async function parseMultiAddrIpfsGatewayToUrl(ipfsGatewaymultiAddrString)
75
76
  throw new Error(`Unable to parse IPFS gateway multiaddr: ${ipfsGatewaymultiAddrString}`);
76
77
  return new URL(`http://${parsed.host}:${parsed.port}`);
77
78
  }
79
+ /** Recursively replaces all `null` values with `undefined`.
80
+ * Used before calling community.edit() since pkc-js expects `undefined` for removal,
81
+ * but JSON/CLI input produces `null`. */
82
+ export function replaceNullWithUndefined(obj) {
83
+ if (obj === null)
84
+ return undefined;
85
+ if (Array.isArray(obj))
86
+ return obj.map(replaceNullWithUndefined);
87
+ if (typeof obj === "object" && obj.constructor === Object) {
88
+ const result = {};
89
+ for (const [key, value] of Object.entries(obj)) {
90
+ result[key] = replaceNullWithUndefined(value);
91
+ }
92
+ return result;
93
+ }
94
+ return obj;
95
+ }
78
96
  /**
79
97
  * Custom merge function that implements CLI-specific merge behavior.
80
98
  * This matches the expected behavior from the test suite.
81
99
  */
82
- export function mergeDeep(target, source) {
100
+ export function mergeDeep(target, source, arrayStrategy = "concat") {
83
101
  function isObject(item) {
84
102
  return item && typeof item === "object" && !Array.isArray(item);
85
103
  }
@@ -88,6 +106,10 @@ export function mergeDeep(target, source) {
88
106
  }
89
107
  // Handle arrays with CLI-specific behavior
90
108
  if (Array.isArray(target) && Array.isArray(source)) {
109
+ // RFC 7396 JSON Merge Patch: arrays are replaced entirely
110
+ if (arrayStrategy === "replace") {
111
+ return source;
112
+ }
91
113
  // Check if source is sparse (has holes/empty items) - indicates indexed assignment like --rules[2]
92
114
  const sourceHasHoles = source.length !== Object.keys(source).length;
93
115
  if (sourceHasHoles) {
@@ -97,7 +119,7 @@ export function mergeDeep(target, source) {
97
119
  for (let i = 0; i < maxLength; i++) {
98
120
  if (i in source) {
99
121
  if (i in target && isPlainObject(target[i]) && isPlainObject(source[i])) {
100
- result[i] = mergeDeep(target[i], source[i]);
122
+ result[i] = mergeDeep(target[i], source[i], arrayStrategy);
101
123
  }
102
124
  else {
103
125
  result[i] = source[i];
@@ -138,10 +160,10 @@ export function mergeDeep(target, source) {
138
160
  for (const key in source) {
139
161
  if (source.hasOwnProperty(key)) {
140
162
  if (Array.isArray(target[key]) && Array.isArray(source[key])) {
141
- result[key] = mergeDeep(target[key], source[key]);
163
+ result[key] = mergeDeep(target[key], source[key], arrayStrategy);
142
164
  }
143
165
  else if (isPlainObject(target[key]) && isPlainObject(source[key])) {
144
- result[key] = mergeDeep(target[key], source[key]);
166
+ result[key] = mergeDeep(target[key], source[key], arrayStrategy);
145
167
  }
146
168
  else {
147
169
  result[key] = source[key];
@@ -153,3 +175,21 @@ export function mergeDeep(target, source) {
153
175
  // If not both objects/arrays, source takes precedence
154
176
  return source;
155
177
  }
178
+ export async function parseJsoncFile(filePath) {
179
+ const fileContent = await fsPromises.readFile(filePath, "utf-8");
180
+ const stripped = stripJsonComments(fileContent);
181
+ let parsed;
182
+ try {
183
+ parsed = JSON.parse(stripped);
184
+ }
185
+ catch (e) {
186
+ if (e instanceof SyntaxError) {
187
+ throw new Error(`Invalid JSONC in file ${filePath}: ${e.message}`);
188
+ }
189
+ throw e;
190
+ }
191
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
192
+ throw new Error("JSONC file must contain a JSON object (not an array, null, string, or number)");
193
+ }
194
+ return parsed;
195
+ }
@@ -72,7 +72,10 @@
72
72
  "bitsocial logs -n 50",
73
73
  "bitsocial logs --since 5m",
74
74
  "bitsocial logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z",
75
- "bitsocial logs --since 1h -f"
75
+ "bitsocial logs --since 1h -f",
76
+ "bitsocial logs --stdout",
77
+ "bitsocial logs --stderr",
78
+ "bitsocial logs --stdout -f"
76
79
  ],
77
80
  "flags": {
78
81
  "follow": {
@@ -114,6 +117,24 @@
114
117
  "hasDynamicHelp": false,
115
118
  "multiple": false,
116
119
  "type": "option"
120
+ },
121
+ "stdout": {
122
+ "description": "Show only stdout log entries",
123
+ "exclusive": [
124
+ "stderr"
125
+ ],
126
+ "name": "stdout",
127
+ "allowNo": false,
128
+ "type": "boolean"
129
+ },
130
+ "stderr": {
131
+ "description": "Show only stderr log entries (output of pkc-logger library)",
132
+ "exclusive": [
133
+ "stdout"
134
+ ],
135
+ "name": "stderr",
136
+ "allowNo": false,
137
+ "type": "boolean"
117
138
  }
118
139
  },
119
140
  "hasDynamicHelp": false,
@@ -267,6 +288,10 @@
267
288
  {
268
289
  "description": "Create a community with title 'Hello Plebs' and description 'Welcome'",
269
290
  "command": "<%= config.bin %> <%= command.id %> --title 'Hello Plebs' --description 'Welcome'"
291
+ },
292
+ {
293
+ "description": "Create a community using options from a JSON/JSONC file",
294
+ "command": "<%= config.bin %> <%= command.id %> --jsonFile ./create-options.json"
270
295
  }
271
296
  ],
272
297
  "flags": {
@@ -285,6 +310,14 @@
285
310
  "hasDynamicHelp": false,
286
311
  "multiple": false,
287
312
  "type": "option"
313
+ },
314
+ "jsonFile": {
315
+ "char": "f",
316
+ "description": "Path to a JSON/JSONC file containing create options (supports comments)",
317
+ "name": "jsonFile",
318
+ "hasDynamicHelp": false,
319
+ "multiple": false,
320
+ "type": "option"
288
321
  }
289
322
  },
290
323
  "hasDynamicHelp": false,
@@ -350,7 +383,7 @@
350
383
  "aliases": [],
351
384
  "args": {
352
385
  "address": {
353
- "description": "Address of the community to edit",
386
+ "description": "Address of the community to edit. It could be the name domain, or a public key",
354
387
  "name": "address",
355
388
  "required": true
356
389
  }
@@ -363,27 +396,31 @@
363
396
  },
364
397
  {
365
398
  "description": "Add the author address 'esteban.bso' as an admin on the community",
366
- "command": "bitsocial community edit mysub.bso '--roles[\"esteban.bso\"].role' admin"
399
+ "command": "bitsocial community edit mycommunity.bso '--roles[\"esteban.bso\"].role' admin"
367
400
  },
368
401
  {
369
402
  "description": "Add two challenges to the community. The first challenge will be a question and answer, and the second will be an image captcha",
370
- "command": "bitsocial community edit mysub.bso --settings.challenges[0].name question --settings.challenges[0].options.question \"what is the password?\" --settings.challenges[0].options.answer thepassword --settings.challenges[1].name captcha-canvas-v3"
403
+ "command": "bitsocial community edit mycommunity.bso --settings.challenges[0].name question --settings.challenges[0].options.question \"what is the password?\" --settings.challenges[0].options.answer thepassword --settings.challenges[1].name captcha-canvas-v3"
371
404
  },
372
405
  {
373
406
  "description": "Change the title and description",
374
- "command": "bitsocial community edit mysub.bso --title \"This is the new title\" --description \"This is the new description\" "
407
+ "command": "bitsocial community edit mycommunity.bso --title \"This is the new title\" --description \"This is the new description\" "
375
408
  },
376
409
  {
377
410
  "description": "Remove a role from a moderator/admin/owner",
378
- "command": "bitsocial community edit plebbit.bso --roles['rinse12.bso'] null"
411
+ "command": "bitsocial community edit bitsocial.bso --roles['rinse12.bso'] null"
379
412
  },
380
413
  {
381
414
  "description": "Enable settings.fetchThumbnailUrls to fetch the thumbnail of url submitted by authors",
382
- "command": "bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls"
415
+ "command": "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls"
383
416
  },
384
417
  {
385
418
  "description": "disable settings.fetchThumbnailUrls",
386
- "command": "bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls=false"
419
+ "command": "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false"
420
+ },
421
+ {
422
+ "description": "Edit a community using options from a JSON/JSONC file",
423
+ "command": "bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json"
387
424
  }
388
425
  ],
389
426
  "flags": {
@@ -395,6 +432,14 @@
395
432
  "hasDynamicHelp": false,
396
433
  "multiple": false,
397
434
  "type": "option"
435
+ },
436
+ "jsonFile": {
437
+ "char": "f",
438
+ "description": "Path to a JSON/JSONC file containing edit options (supports comments)",
439
+ "name": "jsonFile",
440
+ "hasDynamicHelp": false,
441
+ "multiple": false,
442
+ "type": "option"
398
443
  }
399
444
  },
400
445
  "hasDynamicHelp": false,
@@ -713,5 +758,5 @@
713
758
  ]
714
759
  }
715
760
  },
716
- "version": "0.19.44"
761
+ "version": "0.19.46"
717
762
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/bitsocial-cli",
3
- "version": "0.19.44",
3
+ "version": "0.19.46",
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",
@@ -125,6 +125,7 @@
125
125
  "exit-hook": "4.0.0",
126
126
  "express": "4.19.2",
127
127
  "kubo": "0.40.1",
128
+ "strip-json-comments": "5.0.3",
128
129
  "tcp-port-used": "1.0.2",
129
130
  "tslib": "2.6.2",
130
131
  "typescript": "5.9.3"