@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 +49 -29
- package/dist/cli/commands/community/create.d.ts +1 -0
- package/dist/cli/commands/community/create.js +38 -2
- package/dist/cli/commands/community/edit.d.ts +3 -0
- package/dist/cli/commands/community/edit.js +55 -13
- package/dist/cli/commands/daemon.js +5 -5
- package/dist/cli/commands/logs.d.ts +5 -0
- package/dist/cli/commands/logs.js +96 -15
- package/dist/cli/commands/update/install.d.ts +2 -0
- package/dist/cli/commands/update/install.js +63 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.js +13 -0
- package/dist/util.d.ts +6 -1
- package/dist/util.js +44 -4
- package/oclif.manifest.json +54 -9
- package/package.json +2 -1
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.
|
|
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.
|
|
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.
|
|
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
|
-
--
|
|
407
|
-
|
|
408
|
-
|
|
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.
|
|
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.
|
|
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
|
-
--
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
497
|
+
$ bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls
|
|
491
498
|
|
|
492
499
|
disable settings.fetchThumbnailUrls
|
|
493
500
|
|
|
494
|
-
$ bitsocial community edit
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
49
|
+
command: "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls"
|
|
40
50
|
},
|
|
41
51
|
{
|
|
42
52
|
description: "disable settings.fetchThumbnailUrls",
|
|
43
|
-
command: "bitsocial community edit
|
|
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
|
|
52
|
-
log("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
180
|
+
const currentStat = await fsPromise.stat(currentLogFile);
|
|
154
181
|
if (currentStat.size > position) {
|
|
155
|
-
const fd = await fsPromise.open(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
268
|
+
clearInterval(newFileCheckInterval);
|
|
269
|
+
fs.unwatchFile(currentLogFile, readNewData);
|
|
190
270
|
process.exit(0);
|
|
191
271
|
});
|
|
192
272
|
process.on("SIGTERM", () => {
|
|
193
|
-
|
|
273
|
+
clearInterval(newFileCheckInterval);
|
|
274
|
+
fs.unwatchFile(currentLogFile, readNewData);
|
|
194
275
|
process.exit(0);
|
|
195
276
|
});
|
|
196
277
|
// Keep process alive
|
|
@@ -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
|
+
}
|
package/oclif.manifest.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
415
|
+
"command": "bitsocial community edit bitsocial.bso --settings.fetchThumbnailUrls"
|
|
383
416
|
},
|
|
384
417
|
{
|
|
385
418
|
"description": "disable settings.fetchThumbnailUrls",
|
|
386
|
-
"command": "bitsocial community edit
|
|
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.
|
|
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.
|
|
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"
|