@bitsocial/bitsocial-cli 0.19.45 → 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.45/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.45/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.45/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.45/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.45/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
 
@@ -456,7 +461,7 @@ ARGUMENTS
456
461
  ADDRESS Address of the community to edit. It could be the name domain, or a public key
457
462
 
458
463
  FLAGS
459
- -f, --jsonFile=<value> Path to a JSON file containing edit options
464
+ -f, --jsonFile=<value> Path to a JSON/JSONC file containing edit options (supports comments)
460
465
  --pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] URL to PKC RPC
461
466
 
462
467
  DESCRIPTION
@@ -495,12 +500,12 @@ EXAMPLES
495
500
 
496
501
  $ bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false
497
502
 
498
- Edit a community using options from a JSON file
503
+ Edit a community using options from a JSON/JSONC file
499
504
 
500
505
  $ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
501
506
  ```
502
507
 
503
- _See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
504
509
 
505
510
  ## `bitsocial community get [ADDRESS]`
506
511
 
@@ -531,7 +536,7 @@ EXAMPLES
531
536
  $ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
532
537
  ```
533
538
 
534
- _See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
535
540
 
536
541
  ## `bitsocial community list`
537
542
 
@@ -554,7 +559,7 @@ EXAMPLES
554
559
  $ bitsocial community list
555
560
  ```
556
561
 
557
- _See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
558
563
 
559
564
  ## `bitsocial community start ADDRESSES`
560
565
 
@@ -583,7 +588,7 @@ EXAMPLES
583
588
  $ bitsocial community start $(bitsocial community list -q)
584
589
  ```
585
590
 
586
- _See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
587
592
 
588
593
  ## `bitsocial community stop ADDRESSES`
589
594
 
@@ -608,7 +613,7 @@ EXAMPLES
608
613
  $ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
609
614
  ```
610
615
 
611
- _See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
612
617
 
613
618
  ## `bitsocial daemon`
614
619
 
@@ -649,7 +654,7 @@ EXAMPLES
649
654
  $ bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY
650
655
  ```
651
656
 
652
- _See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
653
658
 
654
659
  ## `bitsocial help [COMMAND]`
655
660
 
@@ -677,7 +682,8 @@ View the latest BitSocial daemon log file. By default dumps the full log and exi
677
682
 
678
683
  ```
679
684
  USAGE
680
- $ 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]
681
687
 
682
688
  FLAGS
683
689
  -f, --follow Follow log output in real-time (like tail -f)
@@ -685,6 +691,8 @@ FLAGS
685
691
  --logPath=<value> Specify the directory containing log files
686
692
  --since=<value> Show logs since timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s,
687
693
  42m, 2h, 1d)
694
+ --stderr Show only stderr log entries (output of pkc-logger library)
695
+ --stdout Show only stdout log entries
688
696
  --until=<value> Show logs before timestamp (ISO 8601, e.g. 2026-01-02T13:23:37Z) or relative time (e.g. 30s,
689
697
  42m, 2h, 1d)
690
698
 
@@ -704,9 +712,15 @@ EXAMPLES
704
712
  $ bitsocial logs --since 2026-01-02T13:23:37Z --until 2026-01-02T14:00:00Z
705
713
 
706
714
  $ bitsocial logs --since 1h -f
715
+
716
+ $ bitsocial logs --stdout
717
+
718
+ $ bitsocial logs --stderr
719
+
720
+ $ bitsocial logs --stdout -f
707
721
  ```
708
722
 
709
- _See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
710
724
 
711
725
  ## `bitsocial update check`
712
726
 
@@ -723,7 +737,7 @@ EXAMPLES
723
737
  $ bitsocial update check
724
738
  ```
725
739
 
726
- _See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
727
741
 
728
742
  ## `bitsocial update install [VERSION]`
729
743
 
@@ -755,7 +769,7 @@ EXAMPLES
755
769
  $ bitsocial update install --no-restart-daemons
756
770
  ```
757
771
 
758
- _See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
759
773
 
760
774
  ## `bitsocial update versions`
761
775
 
@@ -777,7 +791,7 @@ EXAMPLES
777
791
  $ bitsocial update versions --limit 5
778
792
  ```
779
793
 
780
- _See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.45/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)_
781
795
  <!-- commandsstop -->
782
796
 
783
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
@@ -1,9 +1,8 @@
1
1
  //@ts-expect-error
2
2
  import DataObjectParser from "dataobject-parser";
3
3
  import { Args, Flags } from "@oclif/core";
4
- import fs from "fs";
5
4
  import { BaseCommand } from "../../base-command.js";
6
- import { PKCLogger, mergeDeep } from "../../../util.js";
5
+ import { PKCLogger, mergeDeep, parseJsoncFile, replaceNullWithUndefined } from "../../../util.js";
7
6
  import * as remeda from "remeda";
8
7
  export default class Edit extends BaseCommand {
9
8
  static description = "Edit a community's properties. For a list of properties, visit https://github.com/pkcprotocol/pkc-js";
@@ -18,7 +17,7 @@ export default class Edit extends BaseCommand {
18
17
  jsonFile: Flags.file({
19
18
  char: "f",
20
19
  exists: true,
21
- description: "Path to a JSON file containing edit options"
20
+ description: "Path to a JSON/JSONC file containing edit options (supports comments)"
22
21
  })
23
22
  };
24
23
  static examples = [
@@ -54,7 +53,7 @@ export default class Edit extends BaseCommand {
54
53
  command: "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false"
55
54
  },
56
55
  {
57
- description: "Edit a community using options from a JSON file",
56
+ description: "Edit a community using options from a JSON/JSONC file",
58
57
  command: "bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json"
59
58
  }
60
59
  ];
@@ -65,21 +64,16 @@ export default class Edit extends BaseCommand {
65
64
  const pkc = await this._connectToPkcRpc(flags.pkcRpcUrl.toString());
66
65
  const cliEditOptions = DataObjectParser.transpose(remeda.omit(flags, ["pkcRpcUrl", "jsonFile"]))["_data"];
67
66
  log("CLI edit options parsed:", cliEditOptions);
68
- // Parse JSON file if provided
67
+ // Parse JSONC file if provided
69
68
  let jsonFileOptions = {};
70
69
  if (flags.jsonFile) {
71
70
  try {
72
- const fileContent = await fs.promises.readFile(flags.jsonFile, "utf-8");
73
- const parsed = JSON.parse(fileContent);
74
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
75
- this.error("JSON file must contain a JSON object (not an array, null, string, or number)");
76
- }
77
- jsonFileOptions = parsed;
78
- log("JSON file options parsed:", jsonFileOptions);
71
+ jsonFileOptions = await parseJsoncFile(flags.jsonFile);
72
+ log("JSONC file options parsed:", jsonFileOptions);
79
73
  }
80
74
  catch (e) {
81
- if (e instanceof SyntaxError) {
82
- this.error(`Invalid JSON in file ${flags.jsonFile}: ${e.message}`);
75
+ if (e instanceof Error) {
76
+ this.error(e.message);
83
77
  }
84
78
  throw e;
85
79
  }
@@ -102,9 +96,11 @@ export default class Edit extends BaseCommand {
102
96
  try {
103
97
  const community = await pkc.createCommunity({ address: args.address });
104
98
  const mergedState = remeda.pick(community, remeda.keys.strict(editOptions));
105
- 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");
106
102
  log("Internal community state after merge:", finalMergedState);
107
- await community.edit(finalMergedState);
103
+ await community.edit(replaceNullWithUndefined(finalMergedState));
108
104
  this.log(community.address);
109
105
  }
110
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
@@ -144,15 +144,19 @@ export default class Install extends Command {
144
144
  pkc.on("error", (err) => {
145
145
  errors.push(err);
146
146
  });
147
- await new Promise((resolve, reject) => {
147
+ await new Promise((resolve) => {
148
148
  const timeout = setTimeout(() => {
149
- const lastError = errors[errors.length - 1];
150
- reject(lastError ?? new Error(`Timed out waiting for RPC server at ${pkcRpcUrl} to respond`));
151
- }, 20000);
152
- pkc.once("communitieschange", () => {
153
- clearTimeout(timeout);
149
+ pkc.removeListener("communitieschange", handler);
154
150
  resolve();
155
- });
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);
156
160
  });
157
161
  return pkc;
158
162
  }
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,
@@ -386,7 +419,7 @@
386
419
  "command": "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls=false"
387
420
  },
388
421
  {
389
- "description": "Edit a community using options from a JSON file",
422
+ "description": "Edit a community using options from a JSON/JSONC file",
390
423
  "command": "bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json"
391
424
  }
392
425
  ],
@@ -402,7 +435,7 @@
402
435
  },
403
436
  "jsonFile": {
404
437
  "char": "f",
405
- "description": "Path to a JSON file containing edit options",
438
+ "description": "Path to a JSON/JSONC file containing edit options (supports comments)",
406
439
  "name": "jsonFile",
407
440
  "hasDynamicHelp": false,
408
441
  "multiple": false,
@@ -725,5 +758,5 @@
725
758
  ]
726
759
  }
727
760
  },
728
- "version": "0.19.45"
761
+ "version": "0.19.46"
729
762
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitsocial/bitsocial-cli",
3
- "version": "0.19.45",
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"