@bitsocial/bitsocial-cli 0.19.65 → 0.19.66
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 +208 -16
- package/dist/challenge-packages/challenge-utils.d.ts +2 -1
- package/dist/challenge-packages/challenge-utils.js +6 -3
- package/dist/cli/commands/challenge/install.d.ts +1 -0
- package/dist/cli/commands/challenge/install.js +27 -31
- package/dist/cli/commands/challenge/list.d.ts +1 -0
- package/dist/cli/commands/challenge/list.js +10 -9
- package/dist/cli/commands/challenge/remove.d.ts +1 -0
- package/dist/cli/commands/challenge/remove.js +13 -2
- package/dist/cli/commands/daemon.js +112 -18
- package/dist/common-utils/daemon-state.d.ts +4 -2
- package/dist/common-utils/daemon-state.js +73 -9
- package/dist/ipfs/startIpfs.js +47 -47
- package/dist/webui/daemon-server.js +3 -3
- package/oclif.manifest.json +13 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -298,9 +298,15 @@ $ bitsocial community edit mysub.bso '--roles["author-address.bso"]' null
|
|
|
298
298
|
## Commands
|
|
299
299
|
|
|
300
300
|
<!-- commands -->
|
|
301
|
+
* [`bitsocial challenge add PACKAGE`](#bitsocial-challenge-add-package)
|
|
302
|
+
* [`bitsocial challenge i PACKAGE`](#bitsocial-challenge-i-package)
|
|
301
303
|
* [`bitsocial challenge install PACKAGE`](#bitsocial-challenge-install-package)
|
|
302
304
|
* [`bitsocial challenge list`](#bitsocial-challenge-list)
|
|
305
|
+
* [`bitsocial challenge ls`](#bitsocial-challenge-ls)
|
|
303
306
|
* [`bitsocial challenge remove NAME`](#bitsocial-challenge-remove-name)
|
|
307
|
+
* [`bitsocial challenge rm NAME`](#bitsocial-challenge-rm-name)
|
|
308
|
+
* [`bitsocial challenge un NAME`](#bitsocial-challenge-un-name)
|
|
309
|
+
* [`bitsocial challenge uninstall NAME`](#bitsocial-challenge-uninstall-name)
|
|
304
310
|
* [`bitsocial community create`](#bitsocial-community-create)
|
|
305
311
|
* [`bitsocial community delete ADDRESSES`](#bitsocial-community-delete-addresses)
|
|
306
312
|
* [`bitsocial community edit ADDRESS`](#bitsocial-community-edit-address)
|
|
@@ -316,6 +322,72 @@ $ bitsocial community edit mysub.bso '--roles["author-address.bso"]' null
|
|
|
316
322
|
* [`bitsocial update install [VERSION]`](#bitsocial-update-install-version)
|
|
317
323
|
* [`bitsocial update versions`](#bitsocial-update-versions)
|
|
318
324
|
|
|
325
|
+
## `bitsocial challenge add PACKAGE`
|
|
326
|
+
|
|
327
|
+
Install a challenge package (npm package name, git URL, tarball URL, or local path)
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
USAGE
|
|
331
|
+
$ bitsocial challenge add PACKAGE [--pkcOptions.dataPath <value>]
|
|
332
|
+
|
|
333
|
+
ARGUMENTS
|
|
334
|
+
PACKAGE Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path)
|
|
335
|
+
|
|
336
|
+
FLAGS
|
|
337
|
+
--pkcOptions.dataPath=<value> Data path to install the challenge into
|
|
338
|
+
|
|
339
|
+
DESCRIPTION
|
|
340
|
+
Install a challenge package (npm package name, git URL, tarball URL, or local path)
|
|
341
|
+
|
|
342
|
+
ALIASES
|
|
343
|
+
$ bitsocial challenge i
|
|
344
|
+
$ bitsocial challenge add
|
|
345
|
+
|
|
346
|
+
EXAMPLES
|
|
347
|
+
$ bitsocial challenge install @bitsocial/mintpass-challenge
|
|
348
|
+
|
|
349
|
+
$ bitsocial challenge install @bitsocial/mintpass-challenge@1.0.0
|
|
350
|
+
|
|
351
|
+
$ bitsocial challenge install github:user/repo
|
|
352
|
+
|
|
353
|
+
$ bitsocial challenge install https://example.com/my-challenge-1.0.0.tar.gz
|
|
354
|
+
|
|
355
|
+
$ bitsocial challenge install ./my-local-challenge
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## `bitsocial challenge i PACKAGE`
|
|
359
|
+
|
|
360
|
+
Install a challenge package (npm package name, git URL, tarball URL, or local path)
|
|
361
|
+
|
|
362
|
+
```
|
|
363
|
+
USAGE
|
|
364
|
+
$ bitsocial challenge i PACKAGE [--pkcOptions.dataPath <value>]
|
|
365
|
+
|
|
366
|
+
ARGUMENTS
|
|
367
|
+
PACKAGE Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path)
|
|
368
|
+
|
|
369
|
+
FLAGS
|
|
370
|
+
--pkcOptions.dataPath=<value> Data path to install the challenge into
|
|
371
|
+
|
|
372
|
+
DESCRIPTION
|
|
373
|
+
Install a challenge package (npm package name, git URL, tarball URL, or local path)
|
|
374
|
+
|
|
375
|
+
ALIASES
|
|
376
|
+
$ bitsocial challenge i
|
|
377
|
+
$ bitsocial challenge add
|
|
378
|
+
|
|
379
|
+
EXAMPLES
|
|
380
|
+
$ bitsocial challenge install @bitsocial/mintpass-challenge
|
|
381
|
+
|
|
382
|
+
$ bitsocial challenge install @bitsocial/mintpass-challenge@1.0.0
|
|
383
|
+
|
|
384
|
+
$ bitsocial challenge install github:user/repo
|
|
385
|
+
|
|
386
|
+
$ bitsocial challenge install https://example.com/my-challenge-1.0.0.tar.gz
|
|
387
|
+
|
|
388
|
+
$ bitsocial challenge install ./my-local-challenge
|
|
389
|
+
```
|
|
390
|
+
|
|
319
391
|
## `bitsocial challenge install PACKAGE`
|
|
320
392
|
|
|
321
393
|
Install a challenge package (npm package name, git URL, tarball URL, or local path)
|
|
@@ -333,6 +405,10 @@ FLAGS
|
|
|
333
405
|
DESCRIPTION
|
|
334
406
|
Install a challenge package (npm package name, git URL, tarball URL, or local path)
|
|
335
407
|
|
|
408
|
+
ALIASES
|
|
409
|
+
$ bitsocial challenge i
|
|
410
|
+
$ bitsocial challenge add
|
|
411
|
+
|
|
336
412
|
EXAMPLES
|
|
337
413
|
$ bitsocial challenge install @bitsocial/mintpass-challenge
|
|
338
414
|
|
|
@@ -345,7 +421,7 @@ EXAMPLES
|
|
|
345
421
|
$ bitsocial challenge install ./my-local-challenge
|
|
346
422
|
```
|
|
347
423
|
|
|
348
|
-
_See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
424
|
+
_See code: [src/cli/commands/challenge/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/challenge/install.ts)_
|
|
349
425
|
|
|
350
426
|
## `bitsocial challenge list`
|
|
351
427
|
|
|
@@ -362,13 +438,40 @@ FLAGS
|
|
|
362
438
|
DESCRIPTION
|
|
363
439
|
List installed challenge packages
|
|
364
440
|
|
|
441
|
+
ALIASES
|
|
442
|
+
$ bitsocial challenge ls
|
|
443
|
+
|
|
365
444
|
EXAMPLES
|
|
366
445
|
$ bitsocial challenge list
|
|
367
446
|
|
|
368
447
|
$ bitsocial challenge list -q
|
|
369
448
|
```
|
|
370
449
|
|
|
371
|
-
_See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
450
|
+
_See code: [src/cli/commands/challenge/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/challenge/list.ts)_
|
|
451
|
+
|
|
452
|
+
## `bitsocial challenge ls`
|
|
453
|
+
|
|
454
|
+
List installed challenge packages
|
|
455
|
+
|
|
456
|
+
```
|
|
457
|
+
USAGE
|
|
458
|
+
$ bitsocial challenge ls [-q] [--pkcOptions.dataPath <value>]
|
|
459
|
+
|
|
460
|
+
FLAGS
|
|
461
|
+
-q, --quiet Only display challenge names
|
|
462
|
+
--pkcOptions.dataPath=<value> Data path where challenges are installed
|
|
463
|
+
|
|
464
|
+
DESCRIPTION
|
|
465
|
+
List installed challenge packages
|
|
466
|
+
|
|
467
|
+
ALIASES
|
|
468
|
+
$ bitsocial challenge ls
|
|
469
|
+
|
|
470
|
+
EXAMPLES
|
|
471
|
+
$ bitsocial challenge list
|
|
472
|
+
|
|
473
|
+
$ bitsocial challenge list -q
|
|
474
|
+
```
|
|
372
475
|
|
|
373
476
|
## `bitsocial challenge remove NAME`
|
|
374
477
|
|
|
@@ -387,13 +490,102 @@ FLAGS
|
|
|
387
490
|
DESCRIPTION
|
|
388
491
|
Remove an installed challenge package
|
|
389
492
|
|
|
493
|
+
ALIASES
|
|
494
|
+
$ bitsocial challenge uninstall
|
|
495
|
+
$ bitsocial challenge rm
|
|
496
|
+
$ bitsocial challenge un
|
|
497
|
+
|
|
498
|
+
EXAMPLES
|
|
499
|
+
$ bitsocial challenge remove my-challenge
|
|
500
|
+
|
|
501
|
+
$ bitsocial challenge remove @scope/my-challenge
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
_See code: [src/cli/commands/challenge/remove.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/challenge/remove.ts)_
|
|
505
|
+
|
|
506
|
+
## `bitsocial challenge rm NAME`
|
|
507
|
+
|
|
508
|
+
Remove an installed challenge package
|
|
509
|
+
|
|
510
|
+
```
|
|
511
|
+
USAGE
|
|
512
|
+
$ bitsocial challenge rm NAME [--pkcOptions.dataPath <value>]
|
|
513
|
+
|
|
514
|
+
ARGUMENTS
|
|
515
|
+
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)
|
|
516
|
+
|
|
517
|
+
FLAGS
|
|
518
|
+
--pkcOptions.dataPath=<value> Data path where challenges are installed
|
|
519
|
+
|
|
520
|
+
DESCRIPTION
|
|
521
|
+
Remove an installed challenge package
|
|
522
|
+
|
|
523
|
+
ALIASES
|
|
524
|
+
$ bitsocial challenge uninstall
|
|
525
|
+
$ bitsocial challenge rm
|
|
526
|
+
$ bitsocial challenge un
|
|
527
|
+
|
|
528
|
+
EXAMPLES
|
|
529
|
+
$ bitsocial challenge remove my-challenge
|
|
530
|
+
|
|
531
|
+
$ bitsocial challenge remove @scope/my-challenge
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
## `bitsocial challenge un NAME`
|
|
535
|
+
|
|
536
|
+
Remove an installed challenge package
|
|
537
|
+
|
|
538
|
+
```
|
|
539
|
+
USAGE
|
|
540
|
+
$ bitsocial challenge un NAME [--pkcOptions.dataPath <value>]
|
|
541
|
+
|
|
542
|
+
ARGUMENTS
|
|
543
|
+
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)
|
|
544
|
+
|
|
545
|
+
FLAGS
|
|
546
|
+
--pkcOptions.dataPath=<value> Data path where challenges are installed
|
|
547
|
+
|
|
548
|
+
DESCRIPTION
|
|
549
|
+
Remove an installed challenge package
|
|
550
|
+
|
|
551
|
+
ALIASES
|
|
552
|
+
$ bitsocial challenge uninstall
|
|
553
|
+
$ bitsocial challenge rm
|
|
554
|
+
$ bitsocial challenge un
|
|
555
|
+
|
|
390
556
|
EXAMPLES
|
|
391
557
|
$ bitsocial challenge remove my-challenge
|
|
392
558
|
|
|
393
559
|
$ bitsocial challenge remove @scope/my-challenge
|
|
394
560
|
```
|
|
395
561
|
|
|
396
|
-
|
|
562
|
+
## `bitsocial challenge uninstall NAME`
|
|
563
|
+
|
|
564
|
+
Remove an installed challenge package
|
|
565
|
+
|
|
566
|
+
```
|
|
567
|
+
USAGE
|
|
568
|
+
$ bitsocial challenge uninstall NAME [--pkcOptions.dataPath <value>]
|
|
569
|
+
|
|
570
|
+
ARGUMENTS
|
|
571
|
+
NAME The challenge package name (e.g., my-challenge or @scope/my-challenge)
|
|
572
|
+
|
|
573
|
+
FLAGS
|
|
574
|
+
--pkcOptions.dataPath=<value> Data path where challenges are installed
|
|
575
|
+
|
|
576
|
+
DESCRIPTION
|
|
577
|
+
Remove an installed challenge package
|
|
578
|
+
|
|
579
|
+
ALIASES
|
|
580
|
+
$ bitsocial challenge uninstall
|
|
581
|
+
$ bitsocial challenge rm
|
|
582
|
+
$ bitsocial challenge un
|
|
583
|
+
|
|
584
|
+
EXAMPLES
|
|
585
|
+
$ bitsocial challenge remove my-challenge
|
|
586
|
+
|
|
587
|
+
$ bitsocial challenge remove @scope/my-challenge
|
|
588
|
+
```
|
|
397
589
|
|
|
398
590
|
## `bitsocial community create`
|
|
399
591
|
|
|
@@ -423,7 +615,7 @@ EXAMPLES
|
|
|
423
615
|
$ bitsocial community create --jsonFile ./create-options.json
|
|
424
616
|
```
|
|
425
617
|
|
|
426
|
-
_See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
618
|
+
_See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/create.ts)_
|
|
427
619
|
|
|
428
620
|
## `bitsocial community delete ADDRESSES`
|
|
429
621
|
|
|
@@ -448,7 +640,7 @@ EXAMPLES
|
|
|
448
640
|
$ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
449
641
|
```
|
|
450
642
|
|
|
451
|
-
_See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
643
|
+
_See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/delete.ts)_
|
|
452
644
|
|
|
453
645
|
## `bitsocial community edit ADDRESS`
|
|
454
646
|
|
|
@@ -518,7 +710,7 @@ EXAMPLES
|
|
|
518
710
|
$ bitsocial community edit bitsocial.bso --jsonFile ./edit-options.json
|
|
519
711
|
```
|
|
520
712
|
|
|
521
|
-
_See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
713
|
+
_See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/edit.ts)_
|
|
522
714
|
|
|
523
715
|
## `bitsocial community export [ADDRESS]`
|
|
524
716
|
|
|
@@ -559,7 +751,7 @@ EXAMPLES
|
|
|
559
751
|
$ bitsocial community export --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
560
752
|
```
|
|
561
753
|
|
|
562
|
-
_See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
754
|
+
_See code: [src/cli/commands/community/export.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/export.ts)_
|
|
563
755
|
|
|
564
756
|
## `bitsocial community get [ADDRESS]`
|
|
565
757
|
|
|
@@ -590,7 +782,7 @@ EXAMPLES
|
|
|
590
782
|
$ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
591
783
|
```
|
|
592
784
|
|
|
593
|
-
_See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
785
|
+
_See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/get.ts)_
|
|
594
786
|
|
|
595
787
|
## `bitsocial community list`
|
|
596
788
|
|
|
@@ -613,7 +805,7 @@ EXAMPLES
|
|
|
613
805
|
$ bitsocial community list
|
|
614
806
|
```
|
|
615
807
|
|
|
616
|
-
_See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
808
|
+
_See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/list.ts)_
|
|
617
809
|
|
|
618
810
|
## `bitsocial community start ADDRESSES`
|
|
619
811
|
|
|
@@ -647,7 +839,7 @@ EXAMPLES
|
|
|
647
839
|
$ bitsocial community start $(bitsocial community list -q) --concurrency 1
|
|
648
840
|
```
|
|
649
841
|
|
|
650
|
-
_See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
842
|
+
_See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/start.ts)_
|
|
651
843
|
|
|
652
844
|
## `bitsocial community stop ADDRESSES`
|
|
653
845
|
|
|
@@ -672,7 +864,7 @@ EXAMPLES
|
|
|
672
864
|
$ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
|
|
673
865
|
```
|
|
674
866
|
|
|
675
|
-
_See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
867
|
+
_See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/community/stop.ts)_
|
|
676
868
|
|
|
677
869
|
## `bitsocial daemon`
|
|
678
870
|
|
|
@@ -719,7 +911,7 @@ EXAMPLES
|
|
|
719
911
|
$ bitsocial daemon --no-allowPrivateKeyExport
|
|
720
912
|
```
|
|
721
913
|
|
|
722
|
-
_See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
914
|
+
_See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/daemon.ts)_
|
|
723
915
|
|
|
724
916
|
## `bitsocial help [COMMAND]`
|
|
725
917
|
|
|
@@ -785,7 +977,7 @@ EXAMPLES
|
|
|
785
977
|
$ bitsocial logs --stdout -f
|
|
786
978
|
```
|
|
787
979
|
|
|
788
|
-
_See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
980
|
+
_See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/logs.ts)_
|
|
789
981
|
|
|
790
982
|
## `bitsocial update check`
|
|
791
983
|
|
|
@@ -802,7 +994,7 @@ EXAMPLES
|
|
|
802
994
|
$ bitsocial update check
|
|
803
995
|
```
|
|
804
996
|
|
|
805
|
-
_See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
997
|
+
_See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/update/check.ts)_
|
|
806
998
|
|
|
807
999
|
## `bitsocial update install [VERSION]`
|
|
808
1000
|
|
|
@@ -834,7 +1026,7 @@ EXAMPLES
|
|
|
834
1026
|
$ bitsocial update install --no-restart-daemons
|
|
835
1027
|
```
|
|
836
1028
|
|
|
837
|
-
_See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
1029
|
+
_See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/update/install.ts)_
|
|
838
1030
|
|
|
839
1031
|
## `bitsocial update versions`
|
|
840
1032
|
|
|
@@ -856,7 +1048,7 @@ EXAMPLES
|
|
|
856
1048
|
$ bitsocial update versions --limit 5
|
|
857
1049
|
```
|
|
858
1050
|
|
|
859
|
-
_See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
1051
|
+
_See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.66/src/cli/commands/update/versions.ts)_
|
|
860
1052
|
<!-- commandsstop -->
|
|
861
1053
|
|
|
862
1054
|
## Contribution
|
|
@@ -21,4 +21,5 @@ export declare function ensureNpmAvailable(): Promise<void>;
|
|
|
21
21
|
export declare function runNpmPack(packageSpec: string, destDir: string): Promise<string>;
|
|
22
22
|
export declare function runNpmInstall(challengeDir: string): Promise<void>;
|
|
23
23
|
export declare function verifyNativeModuleAbi(challengeDir: string): Promise<void>;
|
|
24
|
-
export declare function
|
|
24
|
+
export declare function formatChallengeNameVersion(challenge: Pick<InstalledChallenge, "name" | "version">): string;
|
|
25
|
+
export declare function loadChallengesIntoPKC(dataPath?: string): Promise<InstalledChallenge[]>;
|
|
@@ -285,12 +285,15 @@ export async function verifyNativeModuleAbi(challengeDir) {
|
|
|
285
285
|
`Ensure the challenge package was built for this Node.js version.`);
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
|
+
export function formatChallengeNameVersion(challenge) {
|
|
289
|
+
return challenge.version && challenge.version !== "unknown" ? `${challenge.name}@${challenge.version}` : challenge.name;
|
|
290
|
+
}
|
|
288
291
|
export async function loadChallengesIntoPKC(dataPath) {
|
|
289
292
|
const challenges = await listInstalledChallenges(dataPath);
|
|
290
293
|
if (challenges.length === 0)
|
|
291
294
|
return [];
|
|
292
295
|
const PKC = await import("@pkcprotocol/pkc-js");
|
|
293
|
-
const
|
|
296
|
+
const loaded = [];
|
|
294
297
|
for (const challenge of challenges) {
|
|
295
298
|
try {
|
|
296
299
|
const pkg = await readChallengePackageJson(challenge.path);
|
|
@@ -300,11 +303,11 @@ export async function loadChallengesIntoPKC(dataPath) {
|
|
|
300
303
|
const imported = await import(pathToFileURL(entryPath).href);
|
|
301
304
|
const factory = imported.default || imported;
|
|
302
305
|
PKC.default.challenges[challenge.name] = factory;
|
|
303
|
-
|
|
306
|
+
loaded.push(challenge);
|
|
304
307
|
}
|
|
305
308
|
catch (err) {
|
|
306
309
|
console.error(`Failed to load challenge "${challenge.name}":`, err);
|
|
307
310
|
}
|
|
308
311
|
}
|
|
309
|
-
return
|
|
312
|
+
return loaded;
|
|
310
313
|
}
|
|
@@ -6,6 +6,7 @@ import defaults from "../../../common-utils/defaults.js";
|
|
|
6
6
|
import { ensureNpmAvailable, ensureChallengesDir, challengeNameToDir, readChallengePackageJson, runNpmPack, runNpmInstall, verifyNativeModuleAbi } from "../../../challenge-packages/challenge-utils.js";
|
|
7
7
|
export default class Install extends Command {
|
|
8
8
|
static description = "Install a challenge package (npm package name, git URL, tarball URL, or local path)";
|
|
9
|
+
static aliases = ["challenge:i", "challenge:add"];
|
|
9
10
|
static args = {
|
|
10
11
|
package: Args.string({
|
|
11
12
|
description: "Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path)",
|
|
@@ -26,9 +27,9 @@ export default class Install extends Command {
|
|
|
26
27
|
"bitsocial challenge install ./my-local-challenge"
|
|
27
28
|
];
|
|
28
29
|
async run() {
|
|
30
|
+
const startTime = Date.now();
|
|
29
31
|
const { args, flags } = await this.parse(Install);
|
|
30
32
|
const dataPath = flags["pkcOptions.dataPath"] || defaults.PKC_DATA_PATH;
|
|
31
|
-
this.log("Installing challenge package — this may take a few minutes...");
|
|
32
33
|
// 1. Check npm is available
|
|
33
34
|
await ensureNpmAvailable();
|
|
34
35
|
// 2. Use npm pack to download the package as a tarball
|
|
@@ -68,7 +69,16 @@ export default class Install extends Command {
|
|
|
68
69
|
}
|
|
69
70
|
// 5. Read package info
|
|
70
71
|
const pkg = await readChallengePackageJson(pkgDir);
|
|
71
|
-
// 6.
|
|
72
|
+
// 6. Run npm install in the temp dir, so a failed install never
|
|
73
|
+
// touches an existing working installation of the same package
|
|
74
|
+
process.stderr.write(`[challenge-install] starting npm install in ${pkgDir}\n`);
|
|
75
|
+
await runNpmInstall(pkgDir);
|
|
76
|
+
process.stderr.write("[challenge-install] npm install completed\n");
|
|
77
|
+
// 7. Verify native modules are ABI-compatible (still in the temp dir,
|
|
78
|
+
// so failure needs no rollback — the temp dir is cleaned up below)
|
|
79
|
+
await verifyNativeModuleAbi(pkgDir);
|
|
80
|
+
// 8. Swap the verified package into the challenges dir, replacing any
|
|
81
|
+
// existing installation (idempotent, like npm install)
|
|
72
82
|
const challengesDir = await ensureChallengesDir(dataPath);
|
|
73
83
|
const destDir = challengeNameToDir(challengesDir, pkg.name);
|
|
74
84
|
let alreadyExists = true;
|
|
@@ -78,44 +88,30 @@ export default class Install extends Command {
|
|
|
78
88
|
catch {
|
|
79
89
|
alreadyExists = false;
|
|
80
90
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// Move any existing install aside (same filesystem — tmpDir lives under
|
|
92
|
+
// dataPath) so it can be restored if the final rename fails
|
|
93
|
+
const backupDir = path.join(tmpDir, "previous-install");
|
|
94
|
+
if (alreadyExists)
|
|
95
|
+
await fs.rename(destDir, backupDir);
|
|
85
96
|
if (pkg.name.startsWith("@")) {
|
|
86
97
|
// Ensure scope dir exists for scoped packages
|
|
87
98
|
const scopeDir = path.dirname(destDir);
|
|
88
99
|
await fs.mkdir(scopeDir, { recursive: true });
|
|
89
100
|
}
|
|
90
|
-
await fs.rename(pkgDir, destDir);
|
|
91
|
-
// 8. Run npm install
|
|
92
|
-
process.stderr.write(`[challenge-install] starting npm install in ${destDir}\n`);
|
|
93
|
-
await runNpmInstall(destDir);
|
|
94
|
-
process.stderr.write("[challenge-install] npm install completed\n");
|
|
95
|
-
// 9. Verify native modules are ABI-compatible
|
|
96
101
|
try {
|
|
97
|
-
await
|
|
102
|
+
await fs.rename(pkgDir, destDir);
|
|
98
103
|
}
|
|
99
104
|
catch (err) {
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const entries = await fs.readdir(scopeDir);
|
|
106
|
-
if (entries.length === 0)
|
|
107
|
-
await fs.rmdir(scopeDir);
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// ignore
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
this.error(err instanceof Error ? err.message : String(err));
|
|
105
|
+
// Restore the previous installation before surfacing the error
|
|
106
|
+
if (alreadyExists)
|
|
107
|
+
await fs.rename(backupDir, destDir);
|
|
108
|
+
throw err;
|
|
114
109
|
}
|
|
115
|
-
//
|
|
110
|
+
// 9. Print success (npm-style)
|
|
116
111
|
const version = pkg.version ? `@${pkg.version}` : "";
|
|
117
|
-
|
|
118
|
-
|
|
112
|
+
const elapsedSeconds = Math.max(1, Math.round((Date.now() - startTime) / 1000));
|
|
113
|
+
this.log(`${alreadyExists ? "changed" : "added"} ${pkg.name}${version} in ${elapsedSeconds}s`);
|
|
114
|
+
// 10. Best-effort reload via daemon
|
|
119
115
|
try {
|
|
120
116
|
await fetch("http://localhost:9138/api/challenges/reload", { method: "POST" });
|
|
121
117
|
}
|
|
@@ -124,7 +120,7 @@ export default class Install extends Command {
|
|
|
124
120
|
}
|
|
125
121
|
}
|
|
126
122
|
finally {
|
|
127
|
-
//
|
|
123
|
+
// 11. Clean up temp dir (includes the previous-install backup, if any)
|
|
128
124
|
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
129
125
|
}
|
|
130
126
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Command } from "@oclif/core";
|
|
2
2
|
export default class List extends Command {
|
|
3
3
|
static description: string;
|
|
4
|
+
static aliases: string[];
|
|
4
5
|
static flags: {
|
|
5
6
|
quiet: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
6
7
|
"pkcOptions.dataPath": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Flags, Command } from "@oclif/core";
|
|
2
2
|
import { EOL } from "os";
|
|
3
|
-
import
|
|
3
|
+
import path from "path";
|
|
4
4
|
import defaults from "../../../common-utils/defaults.js";
|
|
5
|
-
import { listInstalledChallenges } from "../../../challenge-packages/challenge-utils.js";
|
|
5
|
+
import { getChallengesDir, listInstalledChallenges, formatChallengeNameVersion } from "../../../challenge-packages/challenge-utils.js";
|
|
6
6
|
export default class List extends Command {
|
|
7
7
|
static description = "List installed challenge packages";
|
|
8
|
+
static aliases = ["challenge:ls"];
|
|
8
9
|
static flags = {
|
|
9
10
|
quiet: Flags.boolean({ char: "q", summary: "Only display challenge names" }),
|
|
10
11
|
"pkcOptions.dataPath": Flags.directory({
|
|
@@ -16,7 +17,8 @@ export default class List extends Command {
|
|
|
16
17
|
async run() {
|
|
17
18
|
const { flags } = await this.parse(List);
|
|
18
19
|
const dataPath = flags["pkcOptions.dataPath"] || defaults.PKC_DATA_PATH;
|
|
19
|
-
|
|
20
|
+
// Sort alphabetically like npm ls (readdir order is filesystem-dependent)
|
|
21
|
+
const challenges = (await listInstalledChallenges(dataPath)).sort((a, b) => a.name.localeCompare(b.name));
|
|
20
22
|
if (challenges.length === 0) {
|
|
21
23
|
this.log("No challenge packages installed.");
|
|
22
24
|
return;
|
|
@@ -25,12 +27,11 @@ export default class List extends Command {
|
|
|
25
27
|
this.log(challenges.map((c) => c.name).join(EOL));
|
|
26
28
|
}
|
|
27
29
|
else {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}))
|
|
30
|
+
// npm-ls-style tree: challenges dir header, then name@version entries
|
|
31
|
+
this.log(path.resolve(getChallengesDir(dataPath)));
|
|
32
|
+
challenges.forEach((c, i) => {
|
|
33
|
+
const branch = i === challenges.length - 1 ? "└── " : "├── ";
|
|
34
|
+
this.log(`${branch}${formatChallengeNameVersion(c)}`);
|
|
34
35
|
});
|
|
35
36
|
}
|
|
36
37
|
}
|
|
@@ -2,9 +2,10 @@ import { Args, Flags, Command } from "@oclif/core";
|
|
|
2
2
|
import fs from "fs/promises";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import defaults from "../../../common-utils/defaults.js";
|
|
5
|
-
import { getChallengesDir, challengeNameToDir } from "../../../challenge-packages/challenge-utils.js";
|
|
5
|
+
import { getChallengesDir, challengeNameToDir, readChallengePackageJson } from "../../../challenge-packages/challenge-utils.js";
|
|
6
6
|
export default class Remove extends Command {
|
|
7
7
|
static description = "Remove an installed challenge package";
|
|
8
|
+
static aliases = ["challenge:uninstall", "challenge:rm", "challenge:un"];
|
|
8
9
|
static args = {
|
|
9
10
|
name: Args.string({
|
|
10
11
|
description: "The challenge package name (e.g., my-challenge or @scope/my-challenge)",
|
|
@@ -33,6 +34,16 @@ export default class Remove extends Command {
|
|
|
33
34
|
catch {
|
|
34
35
|
this.error(`Challenge "${args.name}" is not installed.`);
|
|
35
36
|
}
|
|
37
|
+
// Read the installed version for the success message (best-effort)
|
|
38
|
+
let version = "";
|
|
39
|
+
try {
|
|
40
|
+
const pkg = await readChallengePackageJson(challengeDir);
|
|
41
|
+
if (pkg.version)
|
|
42
|
+
version = `@${pkg.version}`;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// unreadable package.json — report the name only
|
|
46
|
+
}
|
|
36
47
|
// Remove the challenge directory
|
|
37
48
|
await fs.rm(challengeDir, { recursive: true, force: true });
|
|
38
49
|
// Clean up empty @scope/ dir for scoped packages
|
|
@@ -48,7 +59,7 @@ export default class Remove extends Command {
|
|
|
48
59
|
// ignore
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
|
-
this.log(`
|
|
62
|
+
this.log(`removed ${args.name}${version}`);
|
|
52
63
|
// Best-effort reload via daemon
|
|
53
64
|
try {
|
|
54
65
|
await fetch("http://localhost:9138/api/challenges/reload", { method: "POST" });
|
|
@@ -6,7 +6,7 @@ import tcpPortUsed from "tcp-port-used";
|
|
|
6
6
|
import { getLanIpV4Address, PKCLogger, setupDebugLogger, loadKuboConfigFile, parseMultiAddrKuboRpcToUrl, parseMultiAddrIpfsGatewayToUrl } from "../../util.js";
|
|
7
7
|
import { startDaemonServer } from "../../webui/daemon-server.js";
|
|
8
8
|
import { printBanner } from "../ascii-banner.js";
|
|
9
|
-
import { loadChallengesIntoPKC } from "../../challenge-packages/challenge-utils.js";
|
|
9
|
+
import { loadChallengesIntoPKC, formatChallengeNameVersion } from "../../challenge-packages/challenge-utils.js";
|
|
10
10
|
import { migrateDataDirectory } from "../../common-utils/data-migration.js";
|
|
11
11
|
import { createBsoResolvers, DEFAULT_PROVIDERS } from "../../common-utils/resolvers.js";
|
|
12
12
|
import { pruneStaleStates, writeDaemonState, deleteDaemonState } from "../../common-utils/daemon-state.js";
|
|
@@ -275,7 +275,10 @@ export default class Daemon extends Command {
|
|
|
275
275
|
let pendingKuboStart;
|
|
276
276
|
// Kubo Node may fail randomly, we need to set a listener so when it exits because of an error we restart it
|
|
277
277
|
let kuboProcess;
|
|
278
|
-
|
|
278
|
+
// Every kubo we've spawned that hasn't exited yet. Exit cleanup kills all of these,
|
|
279
|
+
// so a kubo that slipped out of kuboProcess tracking still dies with the daemon (issue #70)
|
|
280
|
+
const liveKuboPids = new Set();
|
|
281
|
+
const keepKuboUpOnce = async () => {
|
|
279
282
|
if (mainProcessExited)
|
|
280
283
|
return;
|
|
281
284
|
const kuboApiPort = Number(kuboRpcEndpoint.port);
|
|
@@ -283,6 +286,17 @@ export default class Daemon extends Command {
|
|
|
283
286
|
return; // already started, no need to intervene
|
|
284
287
|
const connectHostname = toConnectableHostname(kuboRpcEndpoint.hostname);
|
|
285
288
|
const isKuboApiPortTaken = await tcpPortUsed.check(kuboApiPort, connectHostname);
|
|
289
|
+
// Test hook: widens the window between the re-entrancy guard above and the pendingKuboStart
|
|
290
|
+
// assignment below, so tests can deterministically reproduce concurrent keepKuboUp entries
|
|
291
|
+
// (issue #70, see test/cli/daemon-kubo-restart-race.test.ts)
|
|
292
|
+
const portCheckDelayRaw = process.env["PKC_CLI_TEST_KEEPKUBOUP_PORTCHECK_DELAY_MS"];
|
|
293
|
+
const portCheckDelay = portCheckDelayRaw ? Number(portCheckDelayRaw) : 0;
|
|
294
|
+
if (Number.isFinite(portCheckDelay) && portCheckDelay > 0)
|
|
295
|
+
await new Promise((resolve) => setTimeout(resolve, portCheckDelay));
|
|
296
|
+
// Re-check after the awaits above: the daemon may have begun shutting down, or another
|
|
297
|
+
// kubo may have been adopted in the meantime — spawning now would race it (issue #70)
|
|
298
|
+
if (mainProcessExited || kuboProcess || pendingKuboStart)
|
|
299
|
+
return;
|
|
286
300
|
if (isKuboApiPortTaken) {
|
|
287
301
|
const connectableEndpoint = new URL(kuboRpcEndpoint.toString());
|
|
288
302
|
connectableEndpoint.hostname = connectHostname;
|
|
@@ -306,8 +320,15 @@ export default class Daemon extends Command {
|
|
|
306
320
|
}
|
|
307
321
|
throw new Error(`Cannot start IPFS daemon because the IPFS API port ${kuboRpcEndpoint.hostname}:${kuboApiPort} (configured as ${kuboRpcEndpoint.toString()}) is already in use.`);
|
|
308
322
|
}
|
|
323
|
+
let spawnedProcess;
|
|
309
324
|
const startPromise = startKuboNode(kuboRpcEndpoint, ipfsGatewayEndpoint, mergedPkcOptions.dataPath, (process) => {
|
|
325
|
+
spawnedProcess = process;
|
|
310
326
|
kuboProcess = process;
|
|
327
|
+
if (process.pid) {
|
|
328
|
+
const pid = process.pid;
|
|
329
|
+
liveKuboPids.add(pid);
|
|
330
|
+
process.once("exit", () => liveKuboPids.delete(pid));
|
|
331
|
+
}
|
|
311
332
|
});
|
|
312
333
|
pendingKuboStart = startPromise;
|
|
313
334
|
let startedProcess;
|
|
@@ -315,12 +336,15 @@ export default class Daemon extends Command {
|
|
|
315
336
|
startedProcess = await startPromise;
|
|
316
337
|
}
|
|
317
338
|
catch (error) {
|
|
318
|
-
|
|
319
|
-
if (
|
|
339
|
+
// Only clear state this attempt owns — it may track another attempt's healthy kubo (issue #70)
|
|
340
|
+
if (pendingKuboStart === startPromise)
|
|
341
|
+
pendingKuboStart = undefined;
|
|
342
|
+
if (!mainProcessExited && spawnedProcess && kuboProcess === spawnedProcess)
|
|
320
343
|
kuboProcess = undefined;
|
|
321
344
|
throw error;
|
|
322
345
|
}
|
|
323
|
-
pendingKuboStart
|
|
346
|
+
if (pendingKuboStart === startPromise)
|
|
347
|
+
pendingKuboStart = undefined;
|
|
324
348
|
if (mainProcessExited) {
|
|
325
349
|
if (startedProcess?.pid && !startedProcess.killed) {
|
|
326
350
|
// Race condition: Kubo finished starting after mainProcessExited.
|
|
@@ -341,7 +365,8 @@ export default class Daemon extends Command {
|
|
|
341
365
|
/* best effort */
|
|
342
366
|
}
|
|
343
367
|
}
|
|
344
|
-
kuboProcess
|
|
368
|
+
if (kuboProcess === startedProcess)
|
|
369
|
+
kuboProcess = undefined;
|
|
345
370
|
return;
|
|
346
371
|
}
|
|
347
372
|
kuboProcess = startedProcess;
|
|
@@ -353,7 +378,8 @@ export default class Daemon extends Command {
|
|
|
353
378
|
// Restart Kubo process because it failed
|
|
354
379
|
if (!mainProcessExited) {
|
|
355
380
|
log(`Kubo node with pid (${currentProcess?.pid}) exited. Will attempt to restart it`);
|
|
356
|
-
kuboProcess
|
|
381
|
+
if (kuboProcess === currentProcess)
|
|
382
|
+
kuboProcess = undefined;
|
|
357
383
|
try {
|
|
358
384
|
await keepKuboUp();
|
|
359
385
|
}
|
|
@@ -367,6 +393,19 @@ export default class Daemon extends Command {
|
|
|
367
393
|
};
|
|
368
394
|
currentProcess.once("exit", onKuboExit);
|
|
369
395
|
};
|
|
396
|
+
// Single-flight wrapper: keepKuboUp is invoked from independent places (the kubo exit
|
|
397
|
+
// handler and the watchdog interval). Concurrent callers must share one attempt —
|
|
398
|
+
// otherwise both can pass keepKuboUpOnce's re-entrancy guard during its awaits and
|
|
399
|
+
// spawn two kubo processes whose failure handling corrupts shared state (issue #70)
|
|
400
|
+
let keepKuboUpInFlight;
|
|
401
|
+
const keepKuboUp = () => {
|
|
402
|
+
if (!keepKuboUpInFlight) {
|
|
403
|
+
keepKuboUpInFlight = keepKuboUpOnce().finally(() => {
|
|
404
|
+
keepKuboUpInFlight = undefined;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
return keepKuboUpInFlight;
|
|
408
|
+
};
|
|
370
409
|
let startedOwnRpc = false;
|
|
371
410
|
let daemonServer;
|
|
372
411
|
const createOrConnectRpc = async () => {
|
|
@@ -384,7 +423,7 @@ export default class Daemon extends Command {
|
|
|
384
423
|
// Load installed challenge packages before starting the RPC server
|
|
385
424
|
const loadedChallenges = await loadChallengesIntoPKC(mergedPkcOptions.dataPath);
|
|
386
425
|
if (loadedChallenges.length > 0)
|
|
387
|
-
console.log(`Loaded challenge packages: ${loadedChallenges.join(", ")}`);
|
|
426
|
+
console.log(`Loaded challenge packages: ${loadedChallenges.map(formatChallengeNameVersion).join(", ")}`);
|
|
388
427
|
daemonServer = await startDaemonServer(pkcRpcUrl, ipfsGatewayEndpoint, mergedPkcOptions, {
|
|
389
428
|
allowPrivateKeyExport: flags.allowPrivateKeyExport
|
|
390
429
|
});
|
|
@@ -429,14 +468,19 @@ export default class Daemon extends Command {
|
|
|
429
468
|
}
|
|
430
469
|
};
|
|
431
470
|
const killKuboProcess = async () => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
471
|
+
// Wait (bounded) for any in-flight start attempt so we kill the kubo it may still
|
|
472
|
+
// spawn. Both promises settle on all failure paths (issue #70), but a spawned kubo
|
|
473
|
+
// that wedges before "Daemon is ready" without exiting keeps them pending — the
|
|
474
|
+
// bound ensures shutdown still reaches the SIGINT/SIGKILL flow below, which kills
|
|
475
|
+
// it via kuboProcess (set in onSpawn) or the liveKuboPids sweep (PR #71 review).
|
|
476
|
+
const inFlightStarts = [keepKuboUpInFlight, pendingKuboStart]
|
|
477
|
+
.filter((promise) => promise !== undefined)
|
|
478
|
+
.map((promise) => promise.catch(() => { }));
|
|
479
|
+
if (inFlightStarts.length > 0)
|
|
480
|
+
await Promise.race([
|
|
481
|
+
Promise.all(inFlightStarts),
|
|
482
|
+
new Promise((resolve) => setTimeout(resolve, 15_000).unref())
|
|
483
|
+
]);
|
|
440
484
|
if (kuboProcess?.pid && !kuboProcess.killed) {
|
|
441
485
|
const pid = kuboProcess.pid;
|
|
442
486
|
log("Attempting to kill kubo process with pid", pid);
|
|
@@ -466,6 +510,11 @@ export default class Daemon extends Command {
|
|
|
466
510
|
kuboProcess = undefined;
|
|
467
511
|
}
|
|
468
512
|
}
|
|
513
|
+
// Defense in depth: SIGKILL any spawned kubo that slipped out of kuboProcess
|
|
514
|
+
// tracking (e.g. via a state race) so nothing outlives the daemon (issue #70)
|
|
515
|
+
for (const pid of liveKuboPids)
|
|
516
|
+
killKuboProcessGroup(pid, "SIGKILL");
|
|
517
|
+
liveKuboPids.clear();
|
|
469
518
|
};
|
|
470
519
|
asyncExitHook(async () => {
|
|
471
520
|
if (keepKuboUpInterval)
|
|
@@ -492,13 +541,58 @@ export default class Daemon extends Command {
|
|
|
492
541
|
}, { wait: 120000 } // could take two minutes to shut down
|
|
493
542
|
);
|
|
494
543
|
// Emergency cleanup: if the process force-exits (e.g. double Ctrl+C),
|
|
495
|
-
// synchronously SIGKILL kubo's process group. This is a no-op if
|
|
496
|
-
// killKuboProcess() already ran (it
|
|
544
|
+
// synchronously SIGKILL every live kubo's process group. This is a no-op if
|
|
545
|
+
// killKuboProcess() already ran (it clears kuboProcess and liveKuboPids).
|
|
497
546
|
process.on("exit", () => {
|
|
498
547
|
if (kuboProcess?.pid) {
|
|
499
548
|
killKuboProcessGroup(kuboProcess.pid, "SIGKILL");
|
|
500
549
|
}
|
|
550
|
+
for (const pid of liveKuboPids)
|
|
551
|
+
killKuboProcessGroup(pid, "SIGKILL");
|
|
501
552
|
});
|
|
553
|
+
// Persistent signal guard (issue #70): exit-hook registers its SIGINT/SIGTERM handlers
|
|
554
|
+
// with process.once, so its listener vanishes from the listener list the moment a
|
|
555
|
+
// signal is dispatched. signal-exit (loaded by @pkcprotocol/proper-lock-file and other
|
|
556
|
+
// dependencies) re-raises the signal when every remaining listener is its own — which
|
|
557
|
+
// would kill the process while the async exit hook above is still shutting kubo down.
|
|
558
|
+
// A persistent non-signal-exit listener keeps that heuristic from ever firing.
|
|
559
|
+
// A repeated signal force-quits immediately (impatient Ctrl+C): process.exit triggers
|
|
560
|
+
// the emergency "exit" handler above, which SIGKILLs every live kubo.
|
|
561
|
+
let terminationSignalCount = 0;
|
|
562
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
563
|
+
process.on(signal, () => {
|
|
564
|
+
terminationSignalCount++;
|
|
565
|
+
if (terminationSignalCount >= 2) {
|
|
566
|
+
log(`Received ${signal} again during shutdown, force-quitting`);
|
|
567
|
+
process.exit(signal === "SIGINT" ? 130 : 143);
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
// Test hook (issue #70): simulates a dependency registering a signal-exit handler
|
|
572
|
+
// AFTER the asyncExitHook above — what @pkcprotocol/proper-lock-file (and the
|
|
573
|
+
// signal-exit copies under ink/restore-cursor) do at module load. signal-exit
|
|
574
|
+
// re-raises the signal when every remaining listener belongs to the signal-exit
|
|
575
|
+
// family (`if (listeners.length === count) { ... process.kill(process.pid, s) }`),
|
|
576
|
+
// which kills the daemon while the async exit hook is still cleaning up kubo —
|
|
577
|
+
// exit-hook registers with process.once, so its listener is already gone by then.
|
|
578
|
+
if (process.env["PKC_CLI_TEST_SIMULATE_LATE_SIGNAL_EXIT"]) {
|
|
579
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
580
|
+
// Real signal-exit copies only count each other as "family" (via a shared
|
|
581
|
+
// global marker); any other listener makes them defer. Identify family by
|
|
582
|
+
// source: signal-exit's dispatcher carries the "an exit is coming" comment.
|
|
583
|
+
const isSignalExitFamily = (listener) => String(listener).includes("an exit is coming");
|
|
584
|
+
const reRaiser = () => {
|
|
585
|
+
const onlyFamilyLeft = process
|
|
586
|
+
.listeners(signal)
|
|
587
|
+
.every((listener) => listener === reRaiser || isSignalExitFamily(listener));
|
|
588
|
+
if (onlyFamilyLeft) {
|
|
589
|
+
process.removeListener(signal, reRaiser);
|
|
590
|
+
process.kill(process.pid, signal);
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
process.on(signal, reRaiser);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
502
596
|
// RPC port was already verified free above (fail-fast); only the kuboRpcClientsOptions branch skips local kubo.
|
|
503
597
|
if (!pkcOptionsFromFlag?.kuboRpcClientsOptions)
|
|
504
598
|
await keepKuboUp();
|
|
@@ -3,6 +3,8 @@ export interface DaemonState {
|
|
|
3
3
|
startedAt: string;
|
|
4
4
|
argv: string[];
|
|
5
5
|
pkcRpcUrl: string;
|
|
6
|
+
/** OS-reported process start time, used to detect PID reuse. Absent in legacy state files. */
|
|
7
|
+
procStartTime?: string;
|
|
6
8
|
}
|
|
7
9
|
/** Write a daemon state file atomically (write to .tmp then rename). */
|
|
8
10
|
export declare function writeDaemonState(state: DaemonState): Promise<void>;
|
|
@@ -10,7 +12,7 @@ export declare function writeDaemonState(state: DaemonState): Promise<void>;
|
|
|
10
12
|
export declare function readAllDaemonStates(): Promise<DaemonState[]>;
|
|
11
13
|
/** Delete a specific daemon's state file. Ignores ENOENT. */
|
|
12
14
|
export declare function deleteDaemonState(pid: number): Promise<void>;
|
|
13
|
-
/** Delete state files for dead PIDs from disk. */
|
|
15
|
+
/** Delete state files for dead or reused PIDs from disk. */
|
|
14
16
|
export declare function pruneStaleStates(): Promise<void>;
|
|
15
|
-
/** Read all states, delete stale files (dead PIDs) from disk, return only alive ones. */
|
|
17
|
+
/** Read all states, delete stale files (dead or reused PIDs) from disk, return only alive ones. */
|
|
16
18
|
export declare function getAliveDaemonStates(): Promise<DaemonState[]>;
|
|
@@ -1,12 +1,60 @@
|
|
|
1
1
|
import defaults from "./defaults.js";
|
|
2
2
|
import path from "path";
|
|
3
3
|
import fs from "fs/promises";
|
|
4
|
+
import { execFile } from "child_process";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
4
7
|
const DAEMON_STATES_DIR = path.join(defaults.PKC_DATA_PATH, ".daemon_states");
|
|
5
8
|
function stateFilePath(pid) {
|
|
6
9
|
return path.join(DAEMON_STATES_DIR, `${pid}-daemon.state`);
|
|
7
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* OS-reported start time of a process, used as an identity token: if a state file's PID
|
|
13
|
+
* was reused by an unrelated process, its start time won't match the recorded one.
|
|
14
|
+
* Linux: starttime (field 22) of /proc/<pid>/stat, in clock ticks since boot.
|
|
15
|
+
* Other unix: `ps -o lstart=` output. Returns undefined when it can't be determined.
|
|
16
|
+
*/
|
|
17
|
+
async function getProcessStartTime(pid) {
|
|
18
|
+
try {
|
|
19
|
+
const stat = await fs.readFile(`/proc/${pid}/stat`, "utf-8");
|
|
20
|
+
// comm (field 2) may contain spaces/parens — real fields resume after the last ')'
|
|
21
|
+
const fields = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
|
22
|
+
return fields[19]; // field 22 (starttime), offset by the 3 fields before the split
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
try {
|
|
26
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "lstart="]);
|
|
27
|
+
return stdout.trim() || undefined;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Full command line of a process, or undefined when it can't be determined. */
|
|
35
|
+
async function getProcessCommandLine(pid) {
|
|
36
|
+
try {
|
|
37
|
+
// An empty /proc cmdline is meaningful (kernel thread — not a daemon), so keep it
|
|
38
|
+
const raw = await fs.readFile(`/proc/${pid}/cmdline`, "utf-8");
|
|
39
|
+
return raw.split("\0").join(" ").trim();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
try {
|
|
43
|
+
const { stdout } = await execFileAsync("ps", ["-p", String(pid), "-o", "args="]);
|
|
44
|
+
return stdout.trim() || undefined;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
8
51
|
/** Write a daemon state file atomically (write to .tmp then rename). */
|
|
9
52
|
export async function writeDaemonState(state) {
|
|
53
|
+
if (state.procStartTime === undefined) {
|
|
54
|
+
const procStartTime = await getProcessStartTime(state.pid);
|
|
55
|
+
if (procStartTime !== undefined)
|
|
56
|
+
state = { ...state, procStartTime };
|
|
57
|
+
}
|
|
10
58
|
await fs.mkdir(DAEMON_STATES_DIR, { recursive: true });
|
|
11
59
|
const dest = stateFilePath(state.pid);
|
|
12
60
|
const tmp = dest + ".tmp";
|
|
@@ -60,21 +108,37 @@ function isPidAlive(pid) {
|
|
|
60
108
|
return false; // ESRCH — no such process
|
|
61
109
|
}
|
|
62
110
|
}
|
|
63
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
111
|
+
/**
|
|
112
|
+
* Check whether the daemon that wrote `state` is still the process running under its PID.
|
|
113
|
+
* A bare liveness check is not enough: a stale state file's PID may have been reused by an
|
|
114
|
+
* unrelated process (e.g. a state file written inside a Docker container whose PID maps to
|
|
115
|
+
* a kernel thread on the host — issue #66).
|
|
116
|
+
*/
|
|
117
|
+
async function isDaemonStateAlive(state) {
|
|
118
|
+
if (!isPidAlive(state.pid))
|
|
119
|
+
return false;
|
|
120
|
+
if (state.procStartTime !== undefined) {
|
|
121
|
+
const current = await getProcessStartTime(state.pid);
|
|
122
|
+
if (current !== undefined)
|
|
123
|
+
return current === state.procStartTime; // mismatch — PID was reused
|
|
124
|
+
return true; // identity undeterminable — fall back to liveness only
|
|
70
125
|
}
|
|
126
|
+
// Legacy state file without procStartTime — heuristic: the command line must reference bitsocial
|
|
127
|
+
const cmdline = await getProcessCommandLine(state.pid);
|
|
128
|
+
if (cmdline === undefined)
|
|
129
|
+
return true; // identity undeterminable — fall back to liveness only
|
|
130
|
+
return cmdline.includes("bitsocial");
|
|
131
|
+
}
|
|
132
|
+
/** Delete state files for dead or reused PIDs from disk. */
|
|
133
|
+
export async function pruneStaleStates() {
|
|
134
|
+
await getAliveDaemonStates();
|
|
71
135
|
}
|
|
72
|
-
/** Read all states, delete stale files (dead PIDs) from disk, return only alive ones. */
|
|
136
|
+
/** Read all states, delete stale files (dead or reused PIDs) from disk, return only alive ones. */
|
|
73
137
|
export async function getAliveDaemonStates() {
|
|
74
138
|
const states = await readAllDaemonStates();
|
|
75
139
|
const alive = [];
|
|
76
140
|
for (const state of states) {
|
|
77
|
-
if (
|
|
141
|
+
if (await isDaemonStateAlive(state)) {
|
|
78
142
|
alive.push(state);
|
|
79
143
|
}
|
|
80
144
|
else {
|
package/dist/ipfs/startIpfs.js
CHANGED
|
@@ -185,53 +185,53 @@ async function ensureIpfsPortsAreAvailable(log, configPath, apiUrl, gatewayUrl)
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
export async function startKuboNode(apiUrl, gatewayUrl, dataPath, onSpawn) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
188
|
+
// Preparation phase runs as plain awaits so any failure rejects the returned promise.
|
|
189
|
+
// It must NOT live inside the new Promise() executor below: an async executor swallows
|
|
190
|
+
// throws as unhandledRejections and the promise never settles, which wedges the daemon's
|
|
191
|
+
// pendingKuboStart tracking and hangs its shutdown (issue #70).
|
|
192
|
+
const log = PKCLogger("bitsocial-cli:ipfs:startKuboNode");
|
|
193
|
+
const ipfsDataPath = process.env["IPFS_PATH"] || path.join(dataPath, ".bitsocial-cli.ipfs");
|
|
194
|
+
await fs.promises.mkdir(ipfsDataPath, { recursive: true });
|
|
195
|
+
const ipfsConfigPath = path.join(ipfsDataPath, "config");
|
|
196
|
+
const kuboExePath = await getKuboExePath();
|
|
197
|
+
const kuboVersion = await getKuboVersion();
|
|
198
|
+
log(`Using Kubo version: ${kuboVersion}`);
|
|
199
|
+
log(`IpfsDataPath (${ipfsDataPath}), kuboExePath (${kuboExePath})`, "kubo ipfs config file", path.join(ipfsDataPath, "config"));
|
|
200
|
+
log("If you would like to change kubo config, please edit the config file at", path.join(ipfsDataPath, "config"));
|
|
201
|
+
const env = { IPFS_PATH: ipfsDataPath, DEBUG_COLORS: "1" };
|
|
202
|
+
let configJustInitialized = false;
|
|
203
|
+
try {
|
|
204
|
+
await _spawnAsync(log, kuboExePath, ["init"], { env, hideWindows: true });
|
|
205
|
+
configJustInitialized = true;
|
|
206
|
+
}
|
|
207
|
+
catch (e) {
|
|
208
|
+
const error = e;
|
|
209
|
+
if (!error?.message?.includes("ipfs configuration file already exists!"))
|
|
210
|
+
throw new Error("Failed to call ipfs init" + error);
|
|
211
|
+
}
|
|
212
|
+
if (configJustInitialized) {
|
|
213
|
+
await _spawnAsync(log, kuboExePath, ["config", "profile", "apply", `server`], {
|
|
214
|
+
env,
|
|
215
|
+
hideWindows: true
|
|
216
|
+
});
|
|
217
|
+
log("Called 'ipfs config profile apply server' successfully");
|
|
218
|
+
await mergeCliDefaultsIntoIpfsConfig(log, ipfsConfigPath, apiUrl, gatewayUrl);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
log("IPFS config already exists; skipping config overrides to preserve user changes.");
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
await _spawnAsync(log, kuboExePath, ["repo", "migrate"], { env, hideWindows: true });
|
|
225
|
+
log("Ensured IPFS repository is migrated to the latest supported version.");
|
|
226
|
+
}
|
|
227
|
+
catch (migrationError) {
|
|
228
|
+
log.error("Failed to run IPFS repo migrations automatically", migrationError);
|
|
229
|
+
throw migrationError;
|
|
230
|
+
}
|
|
231
|
+
await ensureIpfsPortsAreAvailable(log, ipfsConfigPath, apiUrl, gatewayUrl);
|
|
232
|
+
// Spawn phase: the promise only wraps the event-driven wait for kubo's "Daemon is ready",
|
|
233
|
+
// so every settle path goes through resolve/reject.
|
|
234
|
+
return new Promise((resolve, reject) => {
|
|
235
235
|
const daemonArgs = ["--enable-namesys-pubsub", "--migrate"];
|
|
236
236
|
const kuboProcess = spawn(kuboExePath, ["daemon", ...daemonArgs], {
|
|
237
237
|
env,
|
|
@@ -6,7 +6,7 @@ import fs from "fs/promises";
|
|
|
6
6
|
import { PKCLogger } from "../util.js";
|
|
7
7
|
import { randomBytes } from "crypto";
|
|
8
8
|
import express from "express";
|
|
9
|
-
import { loadChallengesIntoPKC } from "../challenge-packages/challenge-utils.js";
|
|
9
|
+
import { loadChallengesIntoPKC, formatChallengeNameVersion } from "../challenge-packages/challenge-utils.js";
|
|
10
10
|
const rootHashRedirectScriptPattern = /<script\b[^>]*>(?:(?!<\/script>)[\s\S])*?window\.location\.replace\(["']\/#["']\s*\+\s*window\.location\.pathname\s*\+\s*window\.location\.search\);(?:(?!<\/script>)[\s\S])*?<\/script>/;
|
|
11
11
|
async function _generateModifiedIndexHtmlWithRpcSettings(webuiPath, webuiName, ipfsGatewayPort) {
|
|
12
12
|
const indexHtmlString = (await fs.readFile(path.join(webuiPath, "index_backup_no_rpc.html")))
|
|
@@ -124,7 +124,7 @@ export async function startDaemonServer(rpcUrl, ipfsGatewayUrl, pkcOptions, rpcS
|
|
|
124
124
|
// Challenge reload endpoints
|
|
125
125
|
const handleChallengeReload = async (_req, res) => {
|
|
126
126
|
try {
|
|
127
|
-
const
|
|
127
|
+
const loadedChallenges = await loadChallengesIntoPKC(pkcOptions.dataPath);
|
|
128
128
|
// Notify all connected RPC clients about the updated challenges
|
|
129
129
|
const onSettingsChange = rpcServer._onSettingsChange;
|
|
130
130
|
if (onSettingsChange) {
|
|
@@ -139,7 +139,7 @@ export async function startDaemonServer(rpcUrl, ipfsGatewayUrl, pkcOptions, rpcS
|
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
-
res.json({ ok: true, challenges:
|
|
142
|
+
res.json({ ok: true, challenges: loadedChallenges.map(formatChallengeNameVersion) });
|
|
143
143
|
}
|
|
144
144
|
catch (err) {
|
|
145
145
|
log.error("Failed to reload challenges", err);
|
package/oclif.manifest.json
CHANGED
|
@@ -161,7 +161,10 @@
|
|
|
161
161
|
]
|
|
162
162
|
},
|
|
163
163
|
"challenge:install": {
|
|
164
|
-
"aliases": [
|
|
164
|
+
"aliases": [
|
|
165
|
+
"challenge:i",
|
|
166
|
+
"challenge:add"
|
|
167
|
+
],
|
|
165
168
|
"args": {
|
|
166
169
|
"package": {
|
|
167
170
|
"description": "Package specifier — anything npm can install (name, name@version, git URL, tarball URL, local path)",
|
|
@@ -205,7 +208,9 @@
|
|
|
205
208
|
]
|
|
206
209
|
},
|
|
207
210
|
"challenge:list": {
|
|
208
|
-
"aliases": [
|
|
211
|
+
"aliases": [
|
|
212
|
+
"challenge:ls"
|
|
213
|
+
],
|
|
209
214
|
"args": {},
|
|
210
215
|
"description": "List installed challenge packages",
|
|
211
216
|
"examples": [
|
|
@@ -247,7 +252,11 @@
|
|
|
247
252
|
]
|
|
248
253
|
},
|
|
249
254
|
"challenge:remove": {
|
|
250
|
-
"aliases": [
|
|
255
|
+
"aliases": [
|
|
256
|
+
"challenge:uninstall",
|
|
257
|
+
"challenge:rm",
|
|
258
|
+
"challenge:un"
|
|
259
|
+
],
|
|
251
260
|
"args": {
|
|
252
261
|
"name": {
|
|
253
262
|
"description": "The challenge package name (e.g., my-challenge or @scope/my-challenge)",
|
|
@@ -862,5 +871,5 @@
|
|
|
862
871
|
]
|
|
863
872
|
}
|
|
864
873
|
},
|
|
865
|
-
"version": "0.19.
|
|
874
|
+
"version": "0.19.66"
|
|
866
875
|
}
|
package/package.json
CHANGED