@bitsocial/bitsocial-cli 0.19.39
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/LICENSE +674 -0
- package/README.md +706 -0
- package/bin/dev +20 -0
- package/bin/dev.cmd +3 -0
- package/bin/postinstall.js +125 -0
- package/bin/run +13 -0
- package/bin/run.cmd +3 -0
- package/dist/challenge-packages/challenge-utils.d.ts +24 -0
- package/dist/challenge-packages/challenge-utils.js +304 -0
- package/dist/cli/base-command.d.ts +11 -0
- package/dist/cli/base-command.js +45 -0
- package/dist/cli/commands/challenge/install.d.ts +12 -0
- package/dist/cli/commands/challenge/install.js +131 -0
- package/dist/cli/commands/challenge/list.d.ts +10 -0
- package/dist/cli/commands/challenge/list.js +37 -0
- package/dist/cli/commands/challenge/remove.d.ts +12 -0
- package/dist/cli/commands/challenge/remove.js +60 -0
- package/dist/cli/commands/community/create.d.ts +12 -0
- package/dist/cli/commands/community/create.js +54 -0
- package/dist/cli/commands/community/delete.d.ts +10 -0
- package/dist/cli/commands/community/delete.js +44 -0
- package/dist/cli/commands/community/edit.d.ts +12 -0
- package/dist/cli/commands/community/edit.js +74 -0
- package/dist/cli/commands/community/get.d.ts +9 -0
- package/dist/cli/commands/community/get.js +32 -0
- package/dist/cli/commands/community/list.d.ts +9 -0
- package/dist/cli/commands/community/list.js +30 -0
- package/dist/cli/commands/community/start.d.ts +13 -0
- package/dist/cli/commands/community/start.js +46 -0
- package/dist/cli/commands/community/stop.d.ts +10 -0
- package/dist/cli/commands/community/stop.js +44 -0
- package/dist/cli/commands/daemon.d.ts +14 -0
- package/dist/cli/commands/daemon.js +484 -0
- package/dist/cli/commands/logs.d.ts +24 -0
- package/dist/cli/commands/logs.js +199 -0
- package/dist/cli/commands/subplebbit/create.d.ts +12 -0
- package/dist/cli/commands/subplebbit/create.js +54 -0
- package/dist/cli/commands/subplebbit/edit.d.ts +12 -0
- package/dist/cli/commands/subplebbit/edit.js +73 -0
- package/dist/cli/commands/subplebbit/get.d.ts +9 -0
- package/dist/cli/commands/subplebbit/get.js +32 -0
- package/dist/cli/commands/subplebbit/list.d.ts +9 -0
- package/dist/cli/commands/subplebbit/list.js +30 -0
- package/dist/cli/commands/subplebbit/start.d.ts +10 -0
- package/dist/cli/commands/subplebbit/start.js +41 -0
- package/dist/cli/commands/subplebbit/stop.d.ts +10 -0
- package/dist/cli/commands/subplebbit/stop.js +43 -0
- package/dist/cli/commands/update/check.d.ts +6 -0
- package/dist/cli/commands/update/check.js +28 -0
- package/dist/cli/commands/update/install.d.ts +12 -0
- package/dist/cli/commands/update/install.js +63 -0
- package/dist/cli/commands/update/versions.d.ts +9 -0
- package/dist/cli/commands/update/versions.js +29 -0
- package/dist/cli/hooks/init/version-hook.d.ts +3 -0
- package/dist/cli/hooks/init/version-hook.js +43 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.d.ts +3 -0
- package/dist/cli/hooks/prerun/parse-dynamic-flags-hook.js +94 -0
- package/dist/cli/types.d.ts +4 -0
- package/dist/cli/types.js +1 -0
- package/dist/common-utils/data-migration.d.ts +1 -0
- package/dist/common-utils/data-migration.js +27 -0
- package/dist/common-utils/defaults.d.ts +9 -0
- package/dist/common-utils/defaults.js +10 -0
- package/dist/common-utils/resolvers.d.ts +2 -0
- package/dist/common-utils/resolvers.js +6 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/ipfs/startIpfs.d.ts +3 -0
- package/dist/ipfs/startIpfs.js +304 -0
- package/dist/seeder.d.ts +1 -0
- package/dist/seeder.js +83 -0
- package/dist/update/npm-registry.d.ts +6 -0
- package/dist/update/npm-registry.js +66 -0
- package/dist/update/semver.d.ts +5 -0
- package/dist/update/semver.js +29 -0
- package/dist/util.d.ts +31 -0
- package/dist/util.js +157 -0
- package/dist/webui/daemon-server.d.ts +10 -0
- package/dist/webui/daemon-server.js +140 -0
- package/package.json +143 -0
package/bin/dev
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import oclif from "@oclif/core";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
import { register } from "ts-node";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const project = path.join(__dirname, "..", "tsconfig.json");
|
|
10
|
+
|
|
11
|
+
// In dev mode -> use ts-node and dev plugins
|
|
12
|
+
process.env.NODE_ENV = "development";
|
|
13
|
+
|
|
14
|
+
register({ project });
|
|
15
|
+
|
|
16
|
+
// In dev mode, always show stack traces
|
|
17
|
+
oclif.settings.debug = true;
|
|
18
|
+
|
|
19
|
+
// Start the CLI
|
|
20
|
+
oclif.run(undefined, import.meta.url).then(oclif.flush).catch(oclif.Errors.handle);
|
package/bin/dev.cmd
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { finished as streamFinished } from "node:stream/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import decompress from "decompress";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = path.dirname(__filename);
|
|
12
|
+
const packageRoot = path.join(__dirname, "..");
|
|
13
|
+
|
|
14
|
+
async function main() {
|
|
15
|
+
const distDir = path.join(packageRoot, "dist");
|
|
16
|
+
try {
|
|
17
|
+
await fs.access(distDir);
|
|
18
|
+
} catch {
|
|
19
|
+
// dist/ doesn't exist — dev install before build, skip silently
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const dstOfWebui = path.join(distDir, "webuis");
|
|
24
|
+
try {
|
|
25
|
+
await fs.mkdir(dstOfWebui);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
if (e.code === "EEXIST") {
|
|
28
|
+
console.log("Web UIs directory already exists, skipping download");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const pkg = JSON.parse(await fs.readFile(path.join(packageRoot, "package.json"), "utf-8"));
|
|
35
|
+
const webuis = pkg.webuis;
|
|
36
|
+
if (!webuis || webuis.length === 0) {
|
|
37
|
+
console.warn("Warning: No webuis configured in package.json");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const githubToken = process.env["GITHUB_TOKEN"];
|
|
42
|
+
if (githubToken) console.log("Using GITHUB_TOKEN for API requests");
|
|
43
|
+
|
|
44
|
+
for (const entry of webuis) {
|
|
45
|
+
const { url, sha256OfHtmlZip } = entry;
|
|
46
|
+
try {
|
|
47
|
+
// Parse "https://github.com/{owner}/{repo}/releases/tag/{tag}"
|
|
48
|
+
const match = url.match(/github\.com\/([^/]+\/[^/]+)\/releases\/tag\/(.+)$/);
|
|
49
|
+
if (!match) {
|
|
50
|
+
console.warn(`Warning: Could not parse GitHub release URL: ${url}. Skipping.`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const [, ownerRepo, tag] = match;
|
|
54
|
+
|
|
55
|
+
const headers = githubToken ? { authorization: `Bearer ${githubToken}` } : undefined;
|
|
56
|
+
const releaseReq = await fetch(`https://api.github.com/repos/${ownerRepo}/releases/tags/${tag}`, { headers });
|
|
57
|
+
if (!releaseReq.ok) {
|
|
58
|
+
console.warn(`Warning: Failed to fetch release for ${ownerRepo}@${tag}, status ${releaseReq.status}. Skipping.`);
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const release = await releaseReq.json();
|
|
63
|
+
const htmlZipAsset = release.assets.find((asset) => asset.name.includes("html"));
|
|
64
|
+
if (!htmlZipAsset) {
|
|
65
|
+
console.warn(`Warning: No HTML zip asset found in ${ownerRepo}@${tag}. Skipping.`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const zipfilePath = path.join(dstOfWebui, htmlZipAsset.name);
|
|
70
|
+
const downloadReq = await fetch(htmlZipAsset["browser_download_url"], { headers });
|
|
71
|
+
if (!downloadReq.ok || !downloadReq.body) {
|
|
72
|
+
console.warn(`Warning: Failed to download ${htmlZipAsset.name}, status ${downloadReq.status}. Skipping.`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const writer = createWriteStream(zipfilePath);
|
|
77
|
+
await streamFinished(Readable.fromWeb(downloadReq.body).pipe(writer));
|
|
78
|
+
writer.close();
|
|
79
|
+
console.log(`Downloaded ${htmlZipAsset.name}`);
|
|
80
|
+
|
|
81
|
+
// Verify SHA-256 checksum
|
|
82
|
+
const fileBuffer = await fs.readFile(zipfilePath);
|
|
83
|
+
const actualHash = createHash("sha256").update(fileBuffer).digest("hex");
|
|
84
|
+
if (actualHash !== sha256OfHtmlZip) {
|
|
85
|
+
await fs.rm(zipfilePath);
|
|
86
|
+
console.error(
|
|
87
|
+
`ERROR: SHA-256 mismatch for ${htmlZipAsset.name}!\n` +
|
|
88
|
+
` Expected: ${sha256OfHtmlZip}\n` +
|
|
89
|
+
` Actual: ${actualHash}\n` +
|
|
90
|
+
`This could indicate a supply chain attack. Aborting.`
|
|
91
|
+
);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log(`Verified SHA-256 checksum for ${htmlZipAsset.name}`);
|
|
95
|
+
|
|
96
|
+
await decompress(zipfilePath, dstOfWebui);
|
|
97
|
+
console.log(`Extracted ${htmlZipAsset.name}`);
|
|
98
|
+
await fs.rm(zipfilePath);
|
|
99
|
+
|
|
100
|
+
// Rename index.html to prevent access to unconfigured version
|
|
101
|
+
const extractedDirName = htmlZipAsset.name.replace(".zip", "");
|
|
102
|
+
const indexPath = path.join(dstOfWebui, extractedDirName, "index.html");
|
|
103
|
+
const backupPath = path.join(dstOfWebui, extractedDirName, "index_backup_no_rpc.html");
|
|
104
|
+
await fs.rename(indexPath, backupPath);
|
|
105
|
+
console.log(`Downloaded ${ownerRepo}@${tag} successfully`);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.warn(`Warning: Failed to process ${url}: ${e}. Skipping.`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Verify at least one web UI was downloaded
|
|
113
|
+
const downloadedEntries = await fs.readdir(dstOfWebui, { withFileTypes: true });
|
|
114
|
+
const webuiDirs = downloadedEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
|
|
115
|
+
if (webuiDirs.length === 0) {
|
|
116
|
+
console.error("ERROR: No web UIs were downloaded. At least one web UI is required.");
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
console.log(`Successfully downloaded ${webuiDirs.length} web UI(s):`, webuiDirs);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
main().catch((err) => {
|
|
123
|
+
console.warn(`Warning: postinstall webui download failed: ${err}`);
|
|
124
|
+
// Don't fail the install for non-checksum errors
|
|
125
|
+
});
|
package/bin/run
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Save and strip DEBUG before any imports so the debug module
|
|
4
|
+
// doesn't auto-enable stderr output. The daemon command will
|
|
5
|
+
// re-enable namespaces programmatically and redirect to the log file.
|
|
6
|
+
if (process.env.DEBUG) {
|
|
7
|
+
process.env._PLEBBIT_DEBUG = process.env.DEBUG;
|
|
8
|
+
delete process.env.DEBUG;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Dynamic import so the stripping above runs before oclif loads
|
|
12
|
+
const oclif = await import("@oclif/core");
|
|
13
|
+
oclif.default.run(undefined, import.meta.url).then(oclif.default.flush).catch(oclif.default.Errors.handle);
|
package/bin/run.cmd
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface InstalledChallenge {
|
|
2
|
+
name: string;
|
|
3
|
+
version: string;
|
|
4
|
+
description: string;
|
|
5
|
+
path: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function getChallengesDir(dataPath?: string): string;
|
|
8
|
+
export declare function ensureChallengesDir(dataPath?: string): Promise<string>;
|
|
9
|
+
export declare function challengeNameToDir(challengesDir: string, name: string): string;
|
|
10
|
+
export declare function readChallengePackageJson(challengeDir: string): Promise<{
|
|
11
|
+
name: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
main?: string;
|
|
15
|
+
exports?: any;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function listInstalledChallenges(dataPath?: string): Promise<InstalledChallenge[]>;
|
|
18
|
+
export declare function getNpmCliPath(): Promise<string>;
|
|
19
|
+
export declare function getNpmEnv(): NodeJS.ProcessEnv;
|
|
20
|
+
export declare function ensureNpmAvailable(): Promise<void>;
|
|
21
|
+
export declare function runNpmPack(packageSpec: string, destDir: string): Promise<string>;
|
|
22
|
+
export declare function runNpmInstall(challengeDir: string): Promise<void>;
|
|
23
|
+
export declare function verifyNativeModuleAbi(challengeDir: string): Promise<void>;
|
|
24
|
+
export declare function loadChallengesIntoPKC(dataPath?: string): Promise<string[]>;
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { execFileSync, spawn } from "child_process";
|
|
5
|
+
import defaults from "../common-utils/defaults.js";
|
|
6
|
+
export function getChallengesDir(dataPath) {
|
|
7
|
+
return path.join(dataPath || defaults.PKC_DATA_PATH, "challenges");
|
|
8
|
+
}
|
|
9
|
+
export async function ensureChallengesDir(dataPath) {
|
|
10
|
+
const dir = getChallengesDir(dataPath);
|
|
11
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
export function challengeNameToDir(challengesDir, name) {
|
|
15
|
+
// Handles both scoped (@org/pkg) and unscoped (pkg) names
|
|
16
|
+
return path.join(challengesDir, ...name.split("/"));
|
|
17
|
+
}
|
|
18
|
+
export async function readChallengePackageJson(challengeDir) {
|
|
19
|
+
const pkgPath = path.join(challengeDir, "package.json");
|
|
20
|
+
const content = await fs.readFile(pkgPath, "utf-8");
|
|
21
|
+
const pkg = JSON.parse(content);
|
|
22
|
+
if (!pkg.name || typeof pkg.name !== "string") {
|
|
23
|
+
throw new Error(`Invalid package.json in ${challengeDir}: missing "name" field`);
|
|
24
|
+
}
|
|
25
|
+
return pkg;
|
|
26
|
+
}
|
|
27
|
+
export async function listInstalledChallenges(dataPath) {
|
|
28
|
+
const challengesDir = getChallengesDir(dataPath);
|
|
29
|
+
const results = [];
|
|
30
|
+
let entries;
|
|
31
|
+
try {
|
|
32
|
+
entries = await fs.readdir(challengesDir, { withFileTypes: true });
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return results; // dir doesn't exist = no challenges
|
|
36
|
+
}
|
|
37
|
+
for (const entry of entries) {
|
|
38
|
+
if (!entry.isDirectory())
|
|
39
|
+
continue;
|
|
40
|
+
if (entry.name.startsWith("@")) {
|
|
41
|
+
// Scoped package: read @scope/*/package.json
|
|
42
|
+
const scopeDir = path.join(challengesDir, entry.name);
|
|
43
|
+
let scopeEntries;
|
|
44
|
+
try {
|
|
45
|
+
scopeEntries = await fs.readdir(scopeDir, { withFileTypes: true });
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
for (const scopeEntry of scopeEntries) {
|
|
51
|
+
if (!scopeEntry.isDirectory())
|
|
52
|
+
continue;
|
|
53
|
+
const pkgDir = path.join(scopeDir, scopeEntry.name);
|
|
54
|
+
try {
|
|
55
|
+
const pkg = await readChallengePackageJson(pkgDir);
|
|
56
|
+
results.push({
|
|
57
|
+
name: pkg.name,
|
|
58
|
+
version: pkg.version || "unknown",
|
|
59
|
+
description: pkg.description || "",
|
|
60
|
+
path: pkgDir
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// skip invalid entries
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// Unscoped package
|
|
70
|
+
const pkgDir = path.join(challengesDir, entry.name);
|
|
71
|
+
try {
|
|
72
|
+
const pkg = await readChallengePackageJson(pkgDir);
|
|
73
|
+
results.push({
|
|
74
|
+
name: pkg.name,
|
|
75
|
+
version: pkg.version || "unknown",
|
|
76
|
+
description: pkg.description || "",
|
|
77
|
+
path: pkgDir
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// skip invalid entries
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return results;
|
|
86
|
+
}
|
|
87
|
+
function getNpmCliPathRelative(nodeExecPath) {
|
|
88
|
+
// npm-cli.js lives at a standard location relative to a Node binary.
|
|
89
|
+
const nodeDir = path.dirname(nodeExecPath);
|
|
90
|
+
if (process.platform === "win32") {
|
|
91
|
+
// Windows: <node-dir>/node_modules/npm/bin/npm-cli.js
|
|
92
|
+
return path.join(nodeDir, "node_modules", "npm", "bin", "npm-cli.js");
|
|
93
|
+
}
|
|
94
|
+
// Unix (nvm, official installers, distro packages):
|
|
95
|
+
// <node-dir>/../lib/node_modules/npm/bin/npm-cli.js
|
|
96
|
+
return path.join(nodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
97
|
+
}
|
|
98
|
+
export async function getNpmCliPath() {
|
|
99
|
+
// 1. Try relative to our own Node binary (works for nvm / system Node)
|
|
100
|
+
const relativePath = getNpmCliPathRelative(process.execPath);
|
|
101
|
+
try {
|
|
102
|
+
await fs.access(relativePath);
|
|
103
|
+
return relativePath;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// Not found relative to process.execPath (e.g. oclif-bundled Node without npm)
|
|
107
|
+
}
|
|
108
|
+
// 2. Fall back to the system npm found on PATH
|
|
109
|
+
try {
|
|
110
|
+
const cmd = process.platform === "win32" ? "where.exe" : "which";
|
|
111
|
+
const npmBin = execFileSync(cmd, ["npm"], { encoding: "utf8" }).trim().split("\n")[0].trim();
|
|
112
|
+
// npmBin is a symlink or script; resolve to the real path, then derive npm-cli.js
|
|
113
|
+
// For most installs: npm -> <prefix>/lib/node_modules/npm/bin/npm-cli.js
|
|
114
|
+
const realNpmBin = await fs.realpath(npmBin);
|
|
115
|
+
// If realpath leads directly to npm-cli.js, use it
|
|
116
|
+
if (realNpmBin.endsWith("npm-cli.js")) {
|
|
117
|
+
return realNpmBin;
|
|
118
|
+
}
|
|
119
|
+
// Otherwise, the system npm binary lives beside a Node that has npm installed —
|
|
120
|
+
// derive npm-cli.js relative to that Node
|
|
121
|
+
const systemNodeDir = path.dirname(realNpmBin);
|
|
122
|
+
const systemNpmCli = process.platform === "win32"
|
|
123
|
+
? path.join(systemNodeDir, "node_modules", "npm", "bin", "npm-cli.js")
|
|
124
|
+
: path.join(systemNodeDir, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js");
|
|
125
|
+
await fs.access(systemNpmCli);
|
|
126
|
+
return systemNpmCli;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Could not locate npm on PATH either
|
|
130
|
+
}
|
|
131
|
+
// Return the original relative path so callers get the familiar error message
|
|
132
|
+
return relativePath;
|
|
133
|
+
}
|
|
134
|
+
export function getNpmEnv() {
|
|
135
|
+
// Prepend our Node's directory to PATH so that npm subprocesses
|
|
136
|
+
// (node-gyp, lifecycle scripts) also use the same Node binary
|
|
137
|
+
const nodeDir = path.dirname(process.execPath);
|
|
138
|
+
const pathSep = process.platform === "win32" ? ";" : ":";
|
|
139
|
+
return {
|
|
140
|
+
...process.env,
|
|
141
|
+
PATH: nodeDir + pathSep + (process.env["PATH"] || "")
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const npmErrorMessage = `npm is required to install challenge packages but was not found at the expected location ` +
|
|
145
|
+
`relative to Node ${process.version} (${process.execPath}).\n` +
|
|
146
|
+
`Install Node.js ${process.version} from https://nodejs.org/ (npm is included with Node.js) and retry.`;
|
|
147
|
+
export async function ensureNpmAvailable() {
|
|
148
|
+
const npmCliPath = await getNpmCliPath();
|
|
149
|
+
try {
|
|
150
|
+
await fs.access(npmCliPath);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
throw new Error(npmErrorMessage);
|
|
154
|
+
}
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
// Run npm through our own Node binary so process.execPath is correct
|
|
157
|
+
const proc = spawn(process.execPath, [npmCliPath, "--version"], { stdio: "ignore", env: getNpmEnv() });
|
|
158
|
+
proc.on("error", () => {
|
|
159
|
+
reject(new Error(npmErrorMessage));
|
|
160
|
+
});
|
|
161
|
+
proc.on("close", (code) => {
|
|
162
|
+
if (code === 0)
|
|
163
|
+
resolve();
|
|
164
|
+
else
|
|
165
|
+
reject(new Error(npmErrorMessage));
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
export async function runNpmPack(packageSpec, destDir) {
|
|
170
|
+
const npmCliPath = await getNpmCliPath();
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
const proc = spawn(process.execPath, [npmCliPath, "pack", packageSpec, "--pack-destination", destDir], {
|
|
173
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
174
|
+
env: getNpmEnv()
|
|
175
|
+
});
|
|
176
|
+
let stdout = "";
|
|
177
|
+
proc.stdout.on("data", (data) => {
|
|
178
|
+
stdout += data.toString();
|
|
179
|
+
});
|
|
180
|
+
proc.on("error", (err) => {
|
|
181
|
+
reject(new Error(`Failed to run npm pack: ${err.message}`));
|
|
182
|
+
});
|
|
183
|
+
proc.on("close", (code) => {
|
|
184
|
+
if (code === 0) {
|
|
185
|
+
// npm pack prints the tarball filename on the last non-empty line of stdout
|
|
186
|
+
const filename = stdout.trim().split("\n").pop()?.trim();
|
|
187
|
+
if (!filename) {
|
|
188
|
+
reject(new Error("npm pack succeeded but produced no output"));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
resolve(path.join(destDir, filename));
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
reject(new Error(`npm pack exited with code ${code}`));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
export async function runNpmInstall(challengeDir) {
|
|
200
|
+
const npmCliPath = await getNpmCliPath();
|
|
201
|
+
// Strip devDependencies from the manifest before running npm install.
|
|
202
|
+
// npm's Arborist resolves ALL declared deps (including dev) during tree
|
|
203
|
+
// building even with --omit=dev — unresolvable devDep versions cause
|
|
204
|
+
// ETARGET failures before the omit filter applies. Removing them from
|
|
205
|
+
// the manifest prevents Arborist from creating those edges at all.
|
|
206
|
+
// The original package.json is restored after install (byte-identical).
|
|
207
|
+
const pkgJsonPath = path.join(challengeDir, "package.json");
|
|
208
|
+
const originalContent = await fs.readFile(pkgJsonPath, "utf-8");
|
|
209
|
+
const pkg = JSON.parse(originalContent);
|
|
210
|
+
const hadDevDeps = pkg.devDependencies !== undefined;
|
|
211
|
+
if (hadDevDeps) {
|
|
212
|
+
const stripped = { ...pkg };
|
|
213
|
+
delete stripped.devDependencies;
|
|
214
|
+
await fs.writeFile(pkgJsonPath, JSON.stringify(stripped, null, 2) + "\n");
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await new Promise((resolve, reject) => {
|
|
218
|
+
// Run npm through our own Node binary to guarantee ABI-compatible
|
|
219
|
+
// native modules — npm's process.execPath and lifecycle scripts
|
|
220
|
+
// will all use the same Node that's running bitsocial-cli.
|
|
221
|
+
// Use piped stdio and forward explicitly so output is visible even
|
|
222
|
+
// when the parent process has piped stdio (e.g. spawned by tests).
|
|
223
|
+
const args = [npmCliPath, "install", "--omit=dev", "--no-audit", "--no-fund"];
|
|
224
|
+
if (process.platform === "win32") {
|
|
225
|
+
args.push("--legacy-peer-deps");
|
|
226
|
+
}
|
|
227
|
+
const proc = spawn(process.execPath, args, {
|
|
228
|
+
cwd: challengeDir,
|
|
229
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
230
|
+
env: getNpmEnv()
|
|
231
|
+
});
|
|
232
|
+
proc.stdout?.pipe(process.stdout);
|
|
233
|
+
proc.stderr?.pipe(process.stderr);
|
|
234
|
+
proc.on("error", (err) => {
|
|
235
|
+
reject(new Error(`Failed to run npm install: ${err.message}`));
|
|
236
|
+
});
|
|
237
|
+
proc.on("close", (code) => {
|
|
238
|
+
if (code === 0)
|
|
239
|
+
resolve();
|
|
240
|
+
else
|
|
241
|
+
reject(new Error(`npm install exited with code ${code}`));
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
finally {
|
|
246
|
+
if (hadDevDeps) {
|
|
247
|
+
await fs.writeFile(pkgJsonPath, originalContent);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export async function verifyNativeModuleAbi(challengeDir) {
|
|
252
|
+
// Scan for .node files (native addons) and try to dlopen each one.
|
|
253
|
+
// Node checks NODE_MODULE_VERSION before calling any init code,
|
|
254
|
+
// so this safely catches ABI mismatches without side effects.
|
|
255
|
+
const entries = await fs.readdir(challengeDir, { recursive: true });
|
|
256
|
+
const nodeFiles = entries.filter((entry) => typeof entry === "string" && entry.endsWith(".node"));
|
|
257
|
+
if (nodeFiles.length === 0)
|
|
258
|
+
return;
|
|
259
|
+
const mismatched = [];
|
|
260
|
+
for (const file of nodeFiles) {
|
|
261
|
+
const filePath = path.join(challengeDir, file);
|
|
262
|
+
const mod = { exports: {} };
|
|
263
|
+
try {
|
|
264
|
+
process.dlopen(mod, filePath);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
268
|
+
if (msg.includes("NODE_MODULE_VERSION") || msg.includes("was compiled against a different")) {
|
|
269
|
+
mismatched.push(file);
|
|
270
|
+
}
|
|
271
|
+
// Other errors (missing dependencies, etc.) are fine at this stage —
|
|
272
|
+
// the module might still work once the full package is loaded
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (mismatched.length > 0) {
|
|
276
|
+
throw new Error(`ABI mismatch: the following native modules were compiled for a different Node.js version:\n` +
|
|
277
|
+
mismatched.map((f) => ` - ${f}`).join("\n") +
|
|
278
|
+
`\nThe running Node.js is ${process.version} (modules ABI ${process.versions.modules}).\n` +
|
|
279
|
+
`Ensure the challenge package was built for this Node.js version.`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
export async function loadChallengesIntoPKC(dataPath) {
|
|
283
|
+
const challenges = await listInstalledChallenges(dataPath);
|
|
284
|
+
if (challenges.length === 0)
|
|
285
|
+
return [];
|
|
286
|
+
const PKC = await import("@pkcprotocol/pkc-js");
|
|
287
|
+
const loadedNames = [];
|
|
288
|
+
for (const challenge of challenges) {
|
|
289
|
+
try {
|
|
290
|
+
const pkg = await readChallengePackageJson(challenge.path);
|
|
291
|
+
// Resolve the entry point
|
|
292
|
+
const entryPoint = pkg.main || "index.js";
|
|
293
|
+
const entryPath = path.resolve(challenge.path, entryPoint);
|
|
294
|
+
const imported = await import(pathToFileURL(entryPath).href);
|
|
295
|
+
const factory = imported.default || imported;
|
|
296
|
+
PKC.default.challenges[challenge.name] = factory;
|
|
297
|
+
loadedNames.push(challenge.name);
|
|
298
|
+
}
|
|
299
|
+
catch (err) {
|
|
300
|
+
console.error(`Failed to load challenge "${challenge.name}":`, err);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return loadedNames;
|
|
304
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
import PKC from "@pkcprotocol/pkc-js";
|
|
3
|
+
type PKCInstance = Awaited<ReturnType<typeof PKC>>;
|
|
4
|
+
export declare abstract class BaseCommand extends Command {
|
|
5
|
+
static baseFlags: {
|
|
6
|
+
pkcRpcUrl: import("@oclif/core/interfaces").OptionFlag<import("url").URL, import("@oclif/core/interfaces").CustomOptions>;
|
|
7
|
+
};
|
|
8
|
+
init(): Promise<void>;
|
|
9
|
+
protected _connectToPkcRpc(pkcRpcUrl: string): Promise<PKCInstance>;
|
|
10
|
+
}
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Command, Flags } from "@oclif/core";
|
|
2
|
+
import defaults from "../common-utils/defaults.js";
|
|
3
|
+
import PKC from "@pkcprotocol/pkc-js";
|
|
4
|
+
import { getPKCLogger, setupDebugLogger } from "../util.js";
|
|
5
|
+
const getPKCConnectOverride = () => {
|
|
6
|
+
const globalWithOverride = globalThis;
|
|
7
|
+
return globalWithOverride.__PKC_RPC_CONNECT_OVERRIDE;
|
|
8
|
+
};
|
|
9
|
+
export class BaseCommand extends Command {
|
|
10
|
+
static baseFlags = {
|
|
11
|
+
pkcRpcUrl: Flags.url({
|
|
12
|
+
summary: "URL to PKC RPC",
|
|
13
|
+
required: true,
|
|
14
|
+
default: defaults.PKC_RPC_URL
|
|
15
|
+
})
|
|
16
|
+
};
|
|
17
|
+
async init() {
|
|
18
|
+
await super.init();
|
|
19
|
+
const Logger = await getPKCLogger();
|
|
20
|
+
setupDebugLogger(Logger, { enableDefaultNamespace: false });
|
|
21
|
+
}
|
|
22
|
+
async _connectToPkcRpc(pkcRpcUrl) {
|
|
23
|
+
const connectOverride = getPKCConnectOverride();
|
|
24
|
+
if (connectOverride) {
|
|
25
|
+
return connectOverride(pkcRpcUrl);
|
|
26
|
+
}
|
|
27
|
+
const pkc = await PKC({ pkcRpcClientsOptions: [pkcRpcUrl] });
|
|
28
|
+
const errors = [];
|
|
29
|
+
pkc.on("error", (err) => {
|
|
30
|
+
errors.push(err);
|
|
31
|
+
console.error("Error from pkc instance", err);
|
|
32
|
+
});
|
|
33
|
+
await new Promise((resolve, reject) => {
|
|
34
|
+
const timeout = setTimeout(() => {
|
|
35
|
+
const lastError = errors[errors.length - 1];
|
|
36
|
+
reject(lastError ?? new Error(`Timed out waiting for RPC server at ${pkcRpcUrl} to respond`));
|
|
37
|
+
}, 20000);
|
|
38
|
+
pkc.once("communitieschange", () => {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return pkc;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "@oclif/core";
|
|
2
|
+
export default class Install extends Command {
|
|
3
|
+
static description: string;
|
|
4
|
+
static args: {
|
|
5
|
+
package: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
|
|
6
|
+
};
|
|
7
|
+
static flags: {
|
|
8
|
+
"pkcOptions.dataPath": import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
9
|
+
};
|
|
10
|
+
static examples: string[];
|
|
11
|
+
run(): Promise<void>;
|
|
12
|
+
}
|