@bitsocial/bitsocial-cli 0.19.42 → 0.19.44
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 +25 -23
- package/bin/update-webuis.js +129 -0
- package/dist/challenge-packages/challenge-utils.js +4 -2
- package/dist/cli/commands/daemon.js +23 -5
- package/dist/cli/commands/update/install.d.ts +2 -0
- package/dist/cli/commands/update/install.js +75 -7
- package/dist/common-utils/daemon-state.d.ts +16 -0
- package/dist/common-utils/daemon-state.js +85 -0
- package/dist/common-utils/resolvers.d.ts +1 -0
- package/dist/common-utils/resolvers.js +8 -1
- package/oclif.manifest.json +17 -7
- package/package.json +8 -7
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Bitsocial is p2p and decentralized social media protocol built completely with I
|
|
|
32
32
|
|
|
33
33
|
## Install
|
|
34
34
|
|
|
35
|
-
Requires
|
|
35
|
+
Requires Node.js 22 or later. We recommend using [nvm](https://github.com/nvm-sh/nvm) to install and manage Node.js versions.
|
|
36
36
|
|
|
37
37
|
```sh-session
|
|
38
38
|
npm install -g @bitsocial/bitsocial-cli
|
|
@@ -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.44/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.44/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.44/src/cli/commands/challenge/remove.ts)_
|
|
396
396
|
|
|
397
397
|
## `bitsocial community create`
|
|
398
398
|
|
|
@@ -417,7 +417,7 @@ EXAMPLES
|
|
|
417
417
|
$ bitsocial community create --title 'Hello Plebs' --description 'Welcome'
|
|
418
418
|
```
|
|
419
419
|
|
|
420
|
-
_See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
420
|
+
_See code: [src/cli/commands/community/create.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/create.ts)_
|
|
421
421
|
|
|
422
422
|
## `bitsocial community delete ADDRESSES`
|
|
423
423
|
|
|
@@ -442,7 +442,7 @@ EXAMPLES
|
|
|
442
442
|
$ bitsocial community delete 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
443
443
|
```
|
|
444
444
|
|
|
445
|
-
_See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
445
|
+
_See code: [src/cli/commands/community/delete.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/delete.ts)_
|
|
446
446
|
|
|
447
447
|
## `bitsocial community edit ADDRESS`
|
|
448
448
|
|
|
@@ -494,7 +494,7 @@ EXAMPLES
|
|
|
494
494
|
$ bitsocial community edit plebbit.bso --settings.fetchThumbnailUrls=false
|
|
495
495
|
```
|
|
496
496
|
|
|
497
|
-
_See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
497
|
+
_See code: [src/cli/commands/community/edit.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/edit.ts)_
|
|
498
498
|
|
|
499
499
|
## `bitsocial community get [ADDRESS]`
|
|
500
500
|
|
|
@@ -525,7 +525,7 @@ EXAMPLES
|
|
|
525
525
|
$ bitsocial community get --publicKey 12D3KooWG3XbzoVyAE6Y9vHZKF64Yuuu4TjdgQKedk14iYmTEPWu
|
|
526
526
|
```
|
|
527
527
|
|
|
528
|
-
_See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
528
|
+
_See code: [src/cli/commands/community/get.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/get.ts)_
|
|
529
529
|
|
|
530
530
|
## `bitsocial community list`
|
|
531
531
|
|
|
@@ -548,7 +548,7 @@ EXAMPLES
|
|
|
548
548
|
$ bitsocial community list
|
|
549
549
|
```
|
|
550
550
|
|
|
551
|
-
_See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
551
|
+
_See code: [src/cli/commands/community/list.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/list.ts)_
|
|
552
552
|
|
|
553
553
|
## `bitsocial community start ADDRESSES`
|
|
554
554
|
|
|
@@ -577,7 +577,7 @@ EXAMPLES
|
|
|
577
577
|
$ bitsocial community start $(bitsocial community list -q)
|
|
578
578
|
```
|
|
579
579
|
|
|
580
|
-
_See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
580
|
+
_See code: [src/cli/commands/community/start.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/start.ts)_
|
|
581
581
|
|
|
582
582
|
## `bitsocial community stop ADDRESSES`
|
|
583
583
|
|
|
@@ -602,7 +602,7 @@ EXAMPLES
|
|
|
602
602
|
$ bitsocial community stop Qmb99crTbSUfKXamXwZBe829Vf6w5w5TktPkb6WstC9RFW
|
|
603
603
|
```
|
|
604
604
|
|
|
605
|
-
_See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
605
|
+
_See code: [src/cli/commands/community/stop.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/community/stop.ts)_
|
|
606
606
|
|
|
607
607
|
## `bitsocial daemon`
|
|
608
608
|
|
|
@@ -613,9 +613,10 @@ USAGE
|
|
|
613
613
|
$ bitsocial daemon --pkcRpcUrl <value> --logPath <value> [--chainProviderUrls <value>...]
|
|
614
614
|
|
|
615
615
|
FLAGS
|
|
616
|
-
--chainProviderUrls=<value>... [default:
|
|
617
|
-
|
|
618
|
-
https://
|
|
616
|
+
--chainProviderUrls=<value>... [default:
|
|
617
|
+
https://eth.drpc.org,https://ethereum.publicnode.com,https://ethereum-rpc.publicnode.c
|
|
618
|
+
om,https://rpc.mevblocker.io,https://1rpc.io/eth,https://eth-pokt.nodies.app] RPC
|
|
619
|
+
URL(s) for .bso name resolution. Can be specified multiple times.
|
|
619
620
|
--logPath=<value> (required) [default: /home/runner/.local/state/bitsocial] Specify a directory which
|
|
620
621
|
will be used to store logs
|
|
621
622
|
--pkcRpcUrl=<value> (required) [default: ws://localhost:9138/] Specify PKC RPC URL to listen on
|
|
@@ -640,11 +641,9 @@ EXAMPLES
|
|
|
640
641
|
$ bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com
|
|
641
642
|
|
|
642
643
|
$ bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY
|
|
643
|
-
|
|
644
|
-
$ bitsocial daemon --chainProviderUrls viem --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY
|
|
645
644
|
```
|
|
646
645
|
|
|
647
|
-
_See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
646
|
+
_See code: [src/cli/commands/daemon.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/daemon.ts)_
|
|
648
647
|
|
|
649
648
|
## `bitsocial help [COMMAND]`
|
|
650
649
|
|
|
@@ -701,7 +700,7 @@ EXAMPLES
|
|
|
701
700
|
$ bitsocial logs --since 1h -f
|
|
702
701
|
```
|
|
703
702
|
|
|
704
|
-
_See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
703
|
+
_See code: [src/cli/commands/logs.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/logs.ts)_
|
|
705
704
|
|
|
706
705
|
## `bitsocial update check`
|
|
707
706
|
|
|
@@ -718,7 +717,7 @@ EXAMPLES
|
|
|
718
717
|
$ bitsocial update check
|
|
719
718
|
```
|
|
720
719
|
|
|
721
|
-
_See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
720
|
+
_See code: [src/cli/commands/update/check.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/update/check.ts)_
|
|
722
721
|
|
|
723
722
|
## `bitsocial update install [VERSION]`
|
|
724
723
|
|
|
@@ -726,13 +725,14 @@ Install a specific version of bitsocial from npm
|
|
|
726
725
|
|
|
727
726
|
```
|
|
728
727
|
USAGE
|
|
729
|
-
$ bitsocial update install [VERSION] [--force]
|
|
728
|
+
$ bitsocial update install [VERSION] [--force] [--restart-daemons]
|
|
730
729
|
|
|
731
730
|
ARGUMENTS
|
|
732
731
|
[VERSION] [default: latest] Version to install (e.g. "0.19.40" or "latest")
|
|
733
732
|
|
|
734
733
|
FLAGS
|
|
735
|
-
--force
|
|
734
|
+
--force Reinstall even if already on the requested version
|
|
735
|
+
--[no-]restart-daemons Stop all running daemons, update, and restart them with the same settings
|
|
736
736
|
|
|
737
737
|
DESCRIPTION
|
|
738
738
|
Install a specific version of bitsocial from npm
|
|
@@ -745,9 +745,11 @@ EXAMPLES
|
|
|
745
745
|
$ bitsocial update install 0.19.40
|
|
746
746
|
|
|
747
747
|
$ bitsocial update install --force
|
|
748
|
+
|
|
749
|
+
$ bitsocial update install --no-restart-daemons
|
|
748
750
|
```
|
|
749
751
|
|
|
750
|
-
_See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
752
|
+
_See code: [src/cli/commands/update/install.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/update/install.ts)_
|
|
751
753
|
|
|
752
754
|
## `bitsocial update versions`
|
|
753
755
|
|
|
@@ -769,7 +771,7 @@ EXAMPLES
|
|
|
769
771
|
$ bitsocial update versions --limit 5
|
|
770
772
|
```
|
|
771
773
|
|
|
772
|
-
_See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.
|
|
774
|
+
_See code: [src/cli/commands/update/versions.ts](https://github.com/bitsocialnet/bitsocial-cli/blob/v0.19.44/src/cli/commands/update/versions.ts)_
|
|
773
775
|
<!-- commandsstop -->
|
|
774
776
|
|
|
775
777
|
## Contribution
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { Readable } from "node:stream";
|
|
7
|
+
import { finished as streamFinished } from "node:stream/promises";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const packageRoot = path.join(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
const dryRun = process.argv.includes("--dry-run");
|
|
15
|
+
|
|
16
|
+
async function main() {
|
|
17
|
+
const pkgPath = path.join(packageRoot, "package.json");
|
|
18
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf-8"));
|
|
19
|
+
const webuis = pkg.webuis;
|
|
20
|
+
if (!webuis || webuis.length === 0) {
|
|
21
|
+
console.log("No webuis configured in package.json");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const githubToken = process.env["GITHUB_TOKEN"];
|
|
26
|
+
if (githubToken) console.log("Using GITHUB_TOKEN for API requests");
|
|
27
|
+
const headers = githubToken ? { authorization: `Bearer ${githubToken}` } : undefined;
|
|
28
|
+
|
|
29
|
+
let updatedCount = 0;
|
|
30
|
+
|
|
31
|
+
for (const entry of webuis) {
|
|
32
|
+
const { url, sha256OfHtmlZip } = entry;
|
|
33
|
+
|
|
34
|
+
// Parse "https://github.com/{owner}/{repo}/releases/tag/{tag}"
|
|
35
|
+
const match = url.match(/github\.com\/([^/]+\/[^/]+)\/releases\/tag\/(.+)$/);
|
|
36
|
+
if (!match) {
|
|
37
|
+
console.warn(`Warning: Could not parse GitHub release URL: ${url}. Skipping.`);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const [, ownerRepo, currentTag] = match;
|
|
41
|
+
const repoName = ownerRepo.split("/")[1];
|
|
42
|
+
|
|
43
|
+
// Fetch latest release
|
|
44
|
+
const latestRes = await fetch(`https://api.github.com/repos/${ownerRepo}/releases/latest`, { headers });
|
|
45
|
+
if (!latestRes.ok) {
|
|
46
|
+
if (latestRes.status === 403) {
|
|
47
|
+
console.warn(`Warning: GitHub API rate limited for ${ownerRepo}. Set GITHUB_TOKEN to avoid this. Skipping.`);
|
|
48
|
+
} else {
|
|
49
|
+
console.warn(`Warning: Failed to fetch latest release for ${ownerRepo}, status ${latestRes.status}. Skipping.`);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const latest = await latestRes.json();
|
|
55
|
+
const latestTag = latest.tag_name;
|
|
56
|
+
|
|
57
|
+
if (latestTag === currentTag) {
|
|
58
|
+
console.log(`${repoName}: ${currentTag} (already latest)`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Find html zip asset
|
|
63
|
+
const htmlZipAsset = latest.assets.find((asset) => asset.name.includes("html"));
|
|
64
|
+
if (!htmlZipAsset) {
|
|
65
|
+
console.warn(`Warning: No HTML zip asset found in ${ownerRepo}@${latestTag}. Skipping.`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (dryRun) {
|
|
70
|
+
console.log(`${repoName}: ${currentTag} -> ${latestTag} (would update)`);
|
|
71
|
+
updatedCount++;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Download zip to temp dir
|
|
76
|
+
const tmpPath = path.join(os.tmpdir(), htmlZipAsset.name);
|
|
77
|
+
const downloadRes = await fetch(htmlZipAsset["browser_download_url"], { headers });
|
|
78
|
+
if (!downloadRes.ok || !downloadRes.body) {
|
|
79
|
+
console.warn(`Warning: Failed to download ${htmlZipAsset.name}, status ${downloadRes.status}. Skipping.`);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const writer = createWriteStream(tmpPath);
|
|
84
|
+
await streamFinished(Readable.fromWeb(downloadRes.body).pipe(writer));
|
|
85
|
+
writer.close();
|
|
86
|
+
|
|
87
|
+
// Compute SHA256
|
|
88
|
+
const fileBuffer = await fs.readFile(tmpPath);
|
|
89
|
+
const newHash = createHash("sha256").update(fileBuffer).digest("hex");
|
|
90
|
+
|
|
91
|
+
// Clean up temp file
|
|
92
|
+
await fs.rm(tmpPath);
|
|
93
|
+
|
|
94
|
+
// Update entry
|
|
95
|
+
entry.url = `https://github.com/${ownerRepo}/releases/tag/${latestTag}`;
|
|
96
|
+
entry.sha256OfHtmlZip = newHash;
|
|
97
|
+
updatedCount++;
|
|
98
|
+
console.log(`${repoName}: ${currentTag} -> ${latestTag} (updated)`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (updatedCount === 0) {
|
|
102
|
+
console.log("\nAll web UIs are already at their latest versions.");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (dryRun) {
|
|
107
|
+
console.log(`\n${updatedCount} web UI(s) would be updated (dry run, no changes made).`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Write updated package.json
|
|
112
|
+
pkg.webuis = webuis;
|
|
113
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 4) + "\n");
|
|
114
|
+
console.log(`\nUpdated ${updatedCount} web UI(s) in package.json.`);
|
|
115
|
+
|
|
116
|
+
// Remove dist/webuis/ so postinstall re-downloads
|
|
117
|
+
const distWebuis = path.join(packageRoot, "dist", "webuis");
|
|
118
|
+
try {
|
|
119
|
+
await fs.rm(distWebuis, { recursive: true });
|
|
120
|
+
console.log("Removed dist/webuis/ — run 'npm run ci:download-web-uis' to re-download.");
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (e.code !== "ENOENT") throw e;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
main().catch((err) => {
|
|
127
|
+
console.error(err);
|
|
128
|
+
process.exit(1);
|
|
129
|
+
});
|
|
@@ -208,9 +208,11 @@ export async function runNpmInstall(challengeDir) {
|
|
|
208
208
|
const originalContent = await fs.readFile(pkgJsonPath, "utf-8");
|
|
209
209
|
const pkg = JSON.parse(originalContent);
|
|
210
210
|
const hadDevDeps = pkg.devDependencies !== undefined;
|
|
211
|
-
|
|
211
|
+
const hadScripts = pkg.scripts !== undefined;
|
|
212
|
+
if (hadDevDeps || hadScripts) {
|
|
212
213
|
const stripped = { ...pkg };
|
|
213
214
|
delete stripped.devDependencies;
|
|
215
|
+
delete stripped.scripts;
|
|
214
216
|
await fs.writeFile(pkgJsonPath, JSON.stringify(stripped, null, 2) + "\n");
|
|
215
217
|
}
|
|
216
218
|
try {
|
|
@@ -243,7 +245,7 @@ export async function runNpmInstall(challengeDir) {
|
|
|
243
245
|
});
|
|
244
246
|
}
|
|
245
247
|
finally {
|
|
246
|
-
if (hadDevDeps) {
|
|
248
|
+
if (hadDevDeps || hadScripts) {
|
|
247
249
|
await fs.writeFile(pkgJsonPath, originalContent);
|
|
248
250
|
}
|
|
249
251
|
}
|
|
@@ -8,7 +8,8 @@ import { startDaemonServer } from "../../webui/daemon-server.js";
|
|
|
8
8
|
import { printBanner } from "../ascii-banner.js";
|
|
9
9
|
import { loadChallengesIntoPKC } from "../../challenge-packages/challenge-utils.js";
|
|
10
10
|
import { migrateDataDirectory } from "../../common-utils/data-migration.js";
|
|
11
|
-
import { createBsoResolvers } from "../../common-utils/resolvers.js";
|
|
11
|
+
import { createBsoResolvers, DEFAULT_PROVIDERS } from "../../common-utils/resolvers.js";
|
|
12
|
+
import { pruneStaleStates, writeDaemonState, deleteDaemonState } from "../../common-utils/daemon-state.js";
|
|
12
13
|
import fs from "fs";
|
|
13
14
|
import fsPromise from "fs/promises";
|
|
14
15
|
/** Replace wildcard bind addresses with loopback for connectivity checks (macOS rejects connect to 0.0.0.0 with EINVAL) */
|
|
@@ -48,9 +49,9 @@ export default class Daemon extends Command {
|
|
|
48
49
|
default: defaults.PKC_LOG_PATH
|
|
49
50
|
}),
|
|
50
51
|
chainProviderUrls: Flags.string({
|
|
51
|
-
description:
|
|
52
|
+
description: "RPC URL(s) for .bso name resolution. Can be specified multiple times.",
|
|
52
53
|
multiple: true,
|
|
53
|
-
default:
|
|
54
|
+
default: DEFAULT_PROVIDERS
|
|
54
55
|
})
|
|
55
56
|
};
|
|
56
57
|
static examples = [
|
|
@@ -59,7 +60,6 @@ export default class Daemon extends Command {
|
|
|
59
60
|
"bitsocial daemon --pkcOptions.dataPath /tmp/bitsocial-datapath/",
|
|
60
61
|
"bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com",
|
|
61
62
|
"bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY",
|
|
62
|
-
"bitsocial daemon --chainProviderUrls viem --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY"
|
|
63
63
|
];
|
|
64
64
|
_setupLogger(Logger) {
|
|
65
65
|
setupDebugLogger(Logger, { enableDefaultNamespace: true });
|
|
@@ -84,7 +84,11 @@ export default class Daemon extends Command {
|
|
|
84
84
|
deletedLogFile = logFileToDelete;
|
|
85
85
|
await fsPromise.rm(path.join(logPath, logFileToDelete));
|
|
86
86
|
}
|
|
87
|
-
return {
|
|
87
|
+
return {
|
|
88
|
+
logFilePath: path.join(logPath, `bitsocial_cli_daemon_${new Date().toISOString().replace(/:/g, "-")}.log`),
|
|
89
|
+
deletedLogFile,
|
|
90
|
+
logfilesCapacity
|
|
91
|
+
};
|
|
88
92
|
}
|
|
89
93
|
async _pipeDebugLogsToLogFile(logPath, Logger) {
|
|
90
94
|
const { logFilePath, deletedLogFile, logfilesCapacity } = await this._getNewLogfileByEvacuatingOldLogsIfNeeded(logPath);
|
|
@@ -202,8 +206,20 @@ export default class Daemon extends Command {
|
|
|
202
206
|
defaultPkcOptions.kuboRpcClientsOptions = [kuboRpcEndpoint.toString()];
|
|
203
207
|
const mergedPkcOptions = { ...defaultPkcOptions, ...pkcOptionsFromFlag };
|
|
204
208
|
log("Merged pkc options that will be used for this node", mergedPkcOptions);
|
|
209
|
+
const { nameResolvers: _nr, ...printablePkcOptions } = mergedPkcOptions;
|
|
210
|
+
console.log("PKC options:", JSON.stringify(printablePkcOptions, null, 2));
|
|
205
211
|
// Migrate data directory before creating PKC instance
|
|
206
212
|
migrateDataDirectory(mergedPkcOptions.dataPath);
|
|
213
|
+
// Prune stale daemon state files (dead PIDs from crashed daemons)
|
|
214
|
+
await pruneStaleStates();
|
|
215
|
+
// Persist this daemon's PID and startup args so `bitsocial update install --restart-daemons` can stop and restart it
|
|
216
|
+
const daemonArgv = process.argv.slice(process.argv.indexOf("daemon") + 1);
|
|
217
|
+
await writeDaemonState({
|
|
218
|
+
pid: process.pid,
|
|
219
|
+
startedAt: new Date().toISOString(),
|
|
220
|
+
argv: daemonArgv,
|
|
221
|
+
pkcRpcUrl: pkcRpcUrl.toString()
|
|
222
|
+
});
|
|
207
223
|
// Create BSO name resolvers for .bso/.eth domain resolution
|
|
208
224
|
const bsoResolvers = createBsoResolvers(flags.chainProviderUrls, mergedPkcOptions.dataPath);
|
|
209
225
|
mergedPkcOptions.nameResolvers = [...(mergedPkcOptions.nameResolvers || []), ...bsoResolvers];
|
|
@@ -423,6 +439,8 @@ export default class Daemon extends Command {
|
|
|
423
439
|
console.log("\nShutting down Bitsocial daemon, it may take a few seconds to shut down all communities and the IPFS node...");
|
|
424
440
|
log("Received signal to exit, shutting down both kubo and pkc rpc. Please wait, it may take a few seconds");
|
|
425
441
|
mainProcessExited = true;
|
|
442
|
+
// Remove daemon state file so update install knows we're gone
|
|
443
|
+
await deleteDaemonState(process.pid).catch(() => { });
|
|
426
444
|
// Start killing Kubo immediately, in parallel with daemon server destroy.
|
|
427
445
|
// This way Kubo receives SIGINT right away, even if daemonServer.destroy() hangs.
|
|
428
446
|
const kuboKillPromise = killKuboProcess();
|
|
@@ -6,7 +6,9 @@ export default class Install extends Command {
|
|
|
6
6
|
};
|
|
7
7
|
static flags: {
|
|
8
8
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
|
+
"restart-daemons": import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
9
10
|
};
|
|
10
11
|
static examples: string[];
|
|
11
12
|
run(): Promise<void>;
|
|
13
|
+
private _restartDaemons;
|
|
12
14
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { Args, Flags, Command } from "@oclif/core";
|
|
2
|
+
import { spawn } from "child_process";
|
|
2
3
|
import tcpPortUsed from "tcp-port-used";
|
|
3
|
-
import defaults from "../../../common-utils/defaults.js";
|
|
4
4
|
import { fetchLatestVersion, installGlobal } from "../../../update/npm-registry.js";
|
|
5
5
|
import { compareVersions } from "../../../update/semver.js";
|
|
6
|
+
import { getAliveDaemonStates } from "../../../common-utils/daemon-state.js";
|
|
6
7
|
export default class Install extends Command {
|
|
7
8
|
static description = "Install a specific version of bitsocial from npm";
|
|
8
9
|
static args = {
|
|
@@ -16,21 +17,54 @@ export default class Install extends Command {
|
|
|
16
17
|
force: Flags.boolean({
|
|
17
18
|
description: "Reinstall even if already on the requested version",
|
|
18
19
|
default: false
|
|
20
|
+
}),
|
|
21
|
+
"restart-daemons": Flags.boolean({
|
|
22
|
+
description: "Stop all running daemons, update, and restart them with the same settings",
|
|
23
|
+
default: true,
|
|
24
|
+
allowNo: true
|
|
19
25
|
})
|
|
20
26
|
};
|
|
21
27
|
static examples = [
|
|
22
28
|
"bitsocial update install",
|
|
23
29
|
"bitsocial update install latest",
|
|
24
30
|
"bitsocial update install 0.19.40",
|
|
25
|
-
"bitsocial update install --force"
|
|
31
|
+
"bitsocial update install --force",
|
|
32
|
+
"bitsocial update install --no-restart-daemons"
|
|
26
33
|
];
|
|
27
34
|
async run() {
|
|
28
35
|
const { args, flags } = await this.parse(Install);
|
|
29
|
-
// Check
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
36
|
+
// Check for running daemons via state files
|
|
37
|
+
const aliveDaemons = await getAliveDaemonStates();
|
|
38
|
+
if (aliveDaemons.length > 0) {
|
|
39
|
+
if (!flags["restart-daemons"]) {
|
|
40
|
+
this.error(`${aliveDaemons.length} daemon(s) running. Stop them first, then retry.`, { exit: 1 });
|
|
41
|
+
}
|
|
42
|
+
// Stop all running daemons
|
|
43
|
+
for (const d of aliveDaemons) {
|
|
44
|
+
this.log(`Stopping daemon (PID ${d.pid})...`);
|
|
45
|
+
try {
|
|
46
|
+
process.kill(d.pid, "SIGINT");
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
if (e.code === "ESRCH") {
|
|
50
|
+
this.log(` PID ${d.pid} already exited.`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
throw e;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Wait for all daemon ports to be free
|
|
57
|
+
for (const d of aliveDaemons) {
|
|
58
|
+
const url = new URL(d.pkcRpcUrl);
|
|
59
|
+
const port = Number(url.port);
|
|
60
|
+
const host = url.hostname;
|
|
61
|
+
this.log(`Waiting for port ${port} to be free...`);
|
|
62
|
+
const freed = await tcpPortUsed.waitUntilFree(port, 500, 30000).then(() => true).catch(() => false);
|
|
63
|
+
if (!freed) {
|
|
64
|
+
this.error(`Daemon (PID ${d.pid}) did not shut down within 30 seconds on port ${port}.`, { exit: 1 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.log("All daemons stopped.");
|
|
34
68
|
}
|
|
35
69
|
// Resolve the target version
|
|
36
70
|
let targetVersion;
|
|
@@ -49,6 +83,10 @@ export default class Install extends Command {
|
|
|
49
83
|
// Skip if already on this version (unless --force)
|
|
50
84
|
if (compareVersions(current, targetVersion) === 0 && !flags.force) {
|
|
51
85
|
this.log(`Already on v${current}. Use --force to reinstall.`);
|
|
86
|
+
if (aliveDaemons.length > 0 && flags["restart-daemons"]) {
|
|
87
|
+
// We stopped daemons but don't need to update — restart them
|
|
88
|
+
await this._restartDaemons(aliveDaemons);
|
|
89
|
+
}
|
|
52
90
|
return;
|
|
53
91
|
}
|
|
54
92
|
this.log(`Installing bitsocial-cli@${targetVersion}...`);
|
|
@@ -59,5 +97,35 @@ export default class Install extends Command {
|
|
|
59
97
|
this.error(`Update failed: ${err.message}`, { exit: 1 });
|
|
60
98
|
}
|
|
61
99
|
this.log(`Installed bitsocial v${targetVersion} (was v${current}).`);
|
|
100
|
+
// Restart daemons with the new binary
|
|
101
|
+
if (aliveDaemons.length > 0 && flags["restart-daemons"]) {
|
|
102
|
+
await this._restartDaemons(aliveDaemons);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async _restartDaemons(daemons) {
|
|
106
|
+
this.log(`Restarting ${daemons.length} daemon(s)...`);
|
|
107
|
+
for (const d of daemons) {
|
|
108
|
+
const argStr = d.argv.length > 0 ? d.argv.join(" ") : "(defaults)";
|
|
109
|
+
this.log(` Starting daemon with args: ${argStr}`);
|
|
110
|
+
const child = spawn("bitsocial", ["daemon", ...d.argv], {
|
|
111
|
+
detached: true,
|
|
112
|
+
stdio: "ignore"
|
|
113
|
+
});
|
|
114
|
+
child.unref();
|
|
115
|
+
if (!child.pid) {
|
|
116
|
+
this.warn(`Failed to spawn daemon for args: ${argStr}`);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
// Wait briefly for the daemon's RPC port to come up
|
|
120
|
+
const url = new URL(d.pkcRpcUrl);
|
|
121
|
+
const port = Number(url.port);
|
|
122
|
+
const started = await tcpPortUsed.waitUntilUsed(port, 500, 30000).then(() => true).catch(() => false);
|
|
123
|
+
if (started) {
|
|
124
|
+
this.log(` Daemon started (port ${port}).`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
this.warn(` Daemon may not have started — port ${port} not responding after 30s. Check logs with: bitsocial logs`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
62
130
|
}
|
|
63
131
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface DaemonState {
|
|
2
|
+
pid: number;
|
|
3
|
+
startedAt: string;
|
|
4
|
+
argv: string[];
|
|
5
|
+
pkcRpcUrl: string;
|
|
6
|
+
}
|
|
7
|
+
/** Write a daemon state file atomically (write to .tmp then rename). */
|
|
8
|
+
export declare function writeDaemonState(state: DaemonState): Promise<void>;
|
|
9
|
+
/** Read all state files from the daemon states directory. */
|
|
10
|
+
export declare function readAllDaemonStates(): Promise<DaemonState[]>;
|
|
11
|
+
/** Delete a specific daemon's state file. Ignores ENOENT. */
|
|
12
|
+
export declare function deleteDaemonState(pid: number): Promise<void>;
|
|
13
|
+
/** Delete state files for dead PIDs from disk. */
|
|
14
|
+
export declare function pruneStaleStates(): Promise<void>;
|
|
15
|
+
/** Read all states, delete stale files (dead PIDs) from disk, return only alive ones. */
|
|
16
|
+
export declare function getAliveDaemonStates(): Promise<DaemonState[]>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import defaults from "./defaults.js";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
const DAEMON_STATES_DIR = path.join(defaults.PKC_DATA_PATH, ".daemon_states");
|
|
5
|
+
function stateFilePath(pid) {
|
|
6
|
+
return path.join(DAEMON_STATES_DIR, `${pid}-daemon.state`);
|
|
7
|
+
}
|
|
8
|
+
/** Write a daemon state file atomically (write to .tmp then rename). */
|
|
9
|
+
export async function writeDaemonState(state) {
|
|
10
|
+
await fs.mkdir(DAEMON_STATES_DIR, { recursive: true });
|
|
11
|
+
const dest = stateFilePath(state.pid);
|
|
12
|
+
const tmp = dest + ".tmp";
|
|
13
|
+
await fs.writeFile(tmp, JSON.stringify(state, null, 2));
|
|
14
|
+
await fs.rename(tmp, dest);
|
|
15
|
+
}
|
|
16
|
+
/** Read all state files from the daemon states directory. */
|
|
17
|
+
export async function readAllDaemonStates() {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await fs.readdir(DAEMON_STATES_DIR);
|
|
21
|
+
}
|
|
22
|
+
catch (e) {
|
|
23
|
+
if (e.code === "ENOENT")
|
|
24
|
+
return [];
|
|
25
|
+
throw e;
|
|
26
|
+
}
|
|
27
|
+
const states = [];
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
if (!entry.endsWith("-daemon.state"))
|
|
30
|
+
continue;
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(path.join(DAEMON_STATES_DIR, entry), "utf-8");
|
|
33
|
+
states.push(JSON.parse(content));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Corrupted or partially written — skip
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return states;
|
|
40
|
+
}
|
|
41
|
+
/** Delete a specific daemon's state file. Ignores ENOENT. */
|
|
42
|
+
export async function deleteDaemonState(pid) {
|
|
43
|
+
try {
|
|
44
|
+
await fs.unlink(stateFilePath(pid));
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
if (e.code !== "ENOENT")
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Check whether a PID is alive. */
|
|
52
|
+
function isPidAlive(pid) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
if (e.code === "EPERM")
|
|
59
|
+
return true; // alive but owned by another user
|
|
60
|
+
return false; // ESRCH — no such process
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Delete state files for dead PIDs from disk. */
|
|
64
|
+
export async function pruneStaleStates() {
|
|
65
|
+
const states = await readAllDaemonStates();
|
|
66
|
+
for (const state of states) {
|
|
67
|
+
if (!isPidAlive(state.pid)) {
|
|
68
|
+
await deleteDaemonState(state.pid);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Read all states, delete stale files (dead PIDs) from disk, return only alive ones. */
|
|
73
|
+
export async function getAliveDaemonStates() {
|
|
74
|
+
const states = await readAllDaemonStates();
|
|
75
|
+
const alive = [];
|
|
76
|
+
for (const state of states) {
|
|
77
|
+
if (isPidAlive(state.pid)) {
|
|
78
|
+
alive.push(state);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
await deleteDaemonState(state.pid);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return alive;
|
|
85
|
+
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { BsoResolver } from "@bitsocial/bso-resolver";
|
|
2
|
-
const DEFAULT_PROVIDERS = [
|
|
2
|
+
export const DEFAULT_PROVIDERS = [
|
|
3
|
+
"https://eth.drpc.org",
|
|
4
|
+
"https://ethereum.publicnode.com",
|
|
5
|
+
"https://ethereum-rpc.publicnode.com",
|
|
6
|
+
"https://rpc.mevblocker.io",
|
|
7
|
+
"https://1rpc.io/eth",
|
|
8
|
+
"https://eth-pokt.nodies.app"
|
|
9
|
+
];
|
|
3
10
|
export function createBsoResolvers(providers, dataPath) {
|
|
4
11
|
const resolverProviders = providers && providers.length > 0 ? providers : DEFAULT_PROVIDERS;
|
|
5
12
|
return resolverProviders.map((provider) => new BsoResolver({ key: `bso-${provider}`, provider, dataPath }));
|
package/oclif.manifest.json
CHANGED
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
"bitsocial daemon --pkcRpcUrl ws://localhost:53812",
|
|
10
10
|
"bitsocial daemon --pkcOptions.dataPath /tmp/bitsocial-datapath/",
|
|
11
11
|
"bitsocial daemon --pkcOptions.kuboRpcClientsOptions[0] https://remoteipfsnode.com",
|
|
12
|
-
"bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY"
|
|
13
|
-
"bitsocial daemon --chainProviderUrls viem --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY"
|
|
12
|
+
"bitsocial daemon --chainProviderUrls https://mainnet.infura.io/v3/YOUR_KEY"
|
|
14
13
|
],
|
|
15
14
|
"flags": {
|
|
16
15
|
"pkcRpcUrl": {
|
|
@@ -32,11 +31,15 @@
|
|
|
32
31
|
"type": "option"
|
|
33
32
|
},
|
|
34
33
|
"chainProviderUrls": {
|
|
35
|
-
"description": "
|
|
34
|
+
"description": "RPC URL(s) for .bso name resolution. Can be specified multiple times.",
|
|
36
35
|
"name": "chainProviderUrls",
|
|
37
36
|
"default": [
|
|
38
|
-
"
|
|
39
|
-
"https://
|
|
37
|
+
"https://eth.drpc.org",
|
|
38
|
+
"https://ethereum.publicnode.com",
|
|
39
|
+
"https://ethereum-rpc.publicnode.com",
|
|
40
|
+
"https://rpc.mevblocker.io",
|
|
41
|
+
"https://1rpc.io/eth",
|
|
42
|
+
"https://eth-pokt.nodies.app"
|
|
40
43
|
],
|
|
41
44
|
"hasDynamicHelp": false,
|
|
42
45
|
"multiple": true,
|
|
@@ -640,7 +643,8 @@
|
|
|
640
643
|
"bitsocial update install",
|
|
641
644
|
"bitsocial update install latest",
|
|
642
645
|
"bitsocial update install 0.19.40",
|
|
643
|
-
"bitsocial update install --force"
|
|
646
|
+
"bitsocial update install --force",
|
|
647
|
+
"bitsocial update install --no-restart-daemons"
|
|
644
648
|
],
|
|
645
649
|
"flags": {
|
|
646
650
|
"force": {
|
|
@@ -648,6 +652,12 @@
|
|
|
648
652
|
"name": "force",
|
|
649
653
|
"allowNo": false,
|
|
650
654
|
"type": "boolean"
|
|
655
|
+
},
|
|
656
|
+
"restart-daemons": {
|
|
657
|
+
"description": "Stop all running daemons, update, and restart them with the same settings",
|
|
658
|
+
"name": "restart-daemons",
|
|
659
|
+
"allowNo": true,
|
|
660
|
+
"type": "boolean"
|
|
651
661
|
}
|
|
652
662
|
},
|
|
653
663
|
"hasDynamicHelp": false,
|
|
@@ -703,5 +713,5 @@
|
|
|
703
713
|
]
|
|
704
714
|
}
|
|
705
715
|
},
|
|
706
|
-
"version": "0.19.
|
|
716
|
+
"version": "0.19.44"
|
|
707
717
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitsocial/bitsocial-cli",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.44",
|
|
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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"postinstall": "node bin/postinstall.js",
|
|
26
26
|
"generate:readme": "oclif readme && sed -i 's@/src/commands/@/src/cli/commands/@g' README.md && sed -i 's@src/commands@src/cli/commands@g' README.md",
|
|
27
27
|
"ci:download-web-uis": "node bin/postinstall.js",
|
|
28
|
-
"ci:daemon:test-before-release": "node --loader ts-node/esm ci-bin/run-daemon-before-release.ts"
|
|
28
|
+
"ci:daemon:test-before-release": "node --loader ts-node/esm ci-bin/run-daemon-before-release.ts",
|
|
29
|
+
"update-webuis": "node bin/update-webuis.js"
|
|
29
30
|
},
|
|
30
31
|
"files": [
|
|
31
32
|
"/bin",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"@types/node": "22.19.13",
|
|
94
95
|
"@types/prettier": "2.7.1",
|
|
95
96
|
"@types/tcp-port-used": "1.0.4",
|
|
96
|
-
"@types/ws": "
|
|
97
|
+
"@types/ws": "8.18.1",
|
|
97
98
|
"commitizen": "4.2.5",
|
|
98
99
|
"cz-conventional-changelog": "3.3.0",
|
|
99
100
|
"eslint": "8.27.0",
|
|
@@ -111,13 +112,13 @@
|
|
|
111
112
|
"wait-on": "6.0.1"
|
|
112
113
|
},
|
|
113
114
|
"dependencies": {
|
|
114
|
-
"@bitsocial/bso-resolver": "0.0.
|
|
115
|
+
"@bitsocial/bso-resolver": "0.0.6",
|
|
115
116
|
"@multiformats/multiaddr": "13.0.1",
|
|
116
117
|
"@oclif/core": "4.8.0",
|
|
117
118
|
"@oclif/plugin-help": "6.2.36",
|
|
118
119
|
"@oclif/plugin-not-found": "3.2.73",
|
|
119
120
|
"@oclif/table": "0.5.1",
|
|
120
|
-
"@pkcprotocol/pkc-js": "0.0.
|
|
121
|
+
"@pkcprotocol/pkc-js": "0.0.17",
|
|
121
122
|
"dataobject-parser": "1.2.22",
|
|
122
123
|
"decompress": "4.2.1",
|
|
123
124
|
"env-paths": "2.2.1",
|
|
@@ -139,8 +140,8 @@
|
|
|
139
140
|
"sha256OfHtmlZip": "14653b19e88868fda6fef26601f477947e44251cc52bd95d7d6fec158ae67b6f"
|
|
140
141
|
},
|
|
141
142
|
{
|
|
142
|
-
"url": "https://github.com/bitsocialnet/5chan/releases/tag/v0.
|
|
143
|
-
"sha256OfHtmlZip": "
|
|
143
|
+
"url": "https://github.com/bitsocialnet/5chan/releases/tag/v0.7.4",
|
|
144
|
+
"sha256OfHtmlZip": "c768bd40a6f9e6c3889d2e57df843e667594974c6f3048357e2b6148aeec3385"
|
|
144
145
|
}
|
|
145
146
|
]
|
|
146
147
|
}
|