@weact-pipenet/weact-cli 1.0.0

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.
@@ -0,0 +1,347 @@
1
+ // Copyright (c) 2026 Lark Technologies Pte. Ltd.
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ const fs = require("fs");
5
+ const path = require("path");
6
+ const { execFileSync } = require("child_process");
7
+ const os = require("os");
8
+ const crypto = require("crypto");
9
+
10
+ const VERSION = require("../package.json").version.replace(/-.*$/, "");
11
+ const REPO = "jixinyuan1996/weact-cli";
12
+ const NAME = "weact-cli";
13
+ const DEFAULT_MIRROR_HOST = "https://registry.npmmirror.com";
14
+ // Allowlist gates the *initial* request URL only. curl --location follows
15
+ // redirects (capped by --max-redirs 3) without re-checking the target host.
16
+ // This is acceptable because checksum verification is the primary integrity
17
+ // control; the allowlist is defense-in-depth to reject obviously wrong URLs.
18
+ const ALLOWED_HOSTS = new Set([
19
+ "github.com",
20
+ "objects.githubusercontent.com",
21
+ "registry.npmmirror.com",
22
+ ]);
23
+
24
+ const PLATFORM_MAP = {
25
+ darwin: "darwin",
26
+ linux: "linux",
27
+ win32: "windows",
28
+ };
29
+
30
+ const ARCH_MAP = {
31
+ x64: "amd64",
32
+ arm64: "arm64",
33
+ riscv64: "riscv64",
34
+ };
35
+
36
+ const platform = PLATFORM_MAP[process.platform];
37
+ const arch = ARCH_MAP[process.arch];
38
+
39
+ const isWindows = process.platform === "win32";
40
+ const ext = isWindows ? ".zip" : ".tar.gz";
41
+ const archiveName = `${NAME}-${VERSION}-${platform}-${arch}${ext}`;
42
+ const GITHUB_URL = `https://github.com/${REPO}/releases/download/v${VERSION}/${archiveName}`;
43
+
44
+ const binDir = path.join(__dirname, "..", "bin");
45
+ const dest = path.join(binDir, NAME + (isWindows ? ".exe" : ""));
46
+
47
+ // Build the ordered list of binary mirror URLs to try. Resolution rules:
48
+ // 1. npm_config_registry — when the user has set a non-default
49
+ // registry (npmmirror clone, corp Verdaccio,
50
+ // Artifactory, …), include the derived path
51
+ // first. Many of these proxies don't actually
52
+ // host /-/binary/<pkg>/..., so we ALWAYS
53
+ // append the public npmmirror as a final
54
+ // fallback so the install does not regress
55
+ // from the previous behavior of "GitHub →
56
+ // npmmirror".
57
+ // 2. registry.npmmirror.com — public China mirror, always tried last.
58
+ // The default public npmjs registry is skipped in step 1 because it does not
59
+ // host binaries under /-/binary/...
60
+ //
61
+ // Non-https / malformed npm_config_registry is silently ignored so npm users
62
+ // with http-only internal registries don't have their installs broken.
63
+ function resolveMirrorUrls(env, archive, version) {
64
+ const binaryPath = `/-/binary/weact-cli/v${version}/${archive}`;
65
+ const defaultUrl = joinUrl(DEFAULT_MIRROR_HOST, binaryPath);
66
+
67
+ const urls = [];
68
+ const registry = (env.npm_config_registry || "").trim();
69
+ if (registry && !isDefaultNpmjsRegistry(registry) && isValidDownloadBase(registry)) {
70
+ const base = new URL(registry);
71
+ urls.push(joinUrl(base.origin + base.pathname, binaryPath));
72
+ }
73
+ if (!urls.includes(defaultUrl)) urls.push(defaultUrl);
74
+ return urls;
75
+ }
76
+
77
+ function joinUrl(base, suffix) {
78
+ return base.replace(/\/+$/, "") + suffix;
79
+ }
80
+
81
+ function isValidDownloadBase(raw) {
82
+ try {
83
+ const parsed = new URL(raw);
84
+ return parsed.protocol === "https:" && !!parsed.hostname;
85
+ } catch (_) {
86
+ return false;
87
+ }
88
+ }
89
+
90
+ function isDefaultNpmjsRegistry(url) {
91
+ try {
92
+ const { hostname } = new URL(url);
93
+ return hostname === "registry.npmjs.org";
94
+ } catch (_) {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ function assertAllowedHost(url) {
100
+ const { hostname } = new URL(url);
101
+ if (!ALLOWED_HOSTS.has(hostname)) {
102
+ throw new Error(`Download host not allowed: ${hostname}`);
103
+ }
104
+ }
105
+
106
+ // Resolve the mirror URL chain and admit each host. Called from install() so
107
+ // derived hosts only become trusted when actually needed.
108
+ function getMirrorUrls(env) {
109
+ const urls = resolveMirrorUrls(env, archiveName, VERSION);
110
+ for (const u of urls) ALLOWED_HOSTS.add(new URL(u).hostname);
111
+ return urls;
112
+ }
113
+
114
+ /**
115
+ * Decide from a `curl --version` output whether curl is >= 7.70.0 — the
116
+ * release (2020-04-29) that introduced --ssl-revoke-best-effort. Kept pure
117
+ * (no I/O) so the version-comparison logic can be unit tested without
118
+ * spawning a process. Reads the leading "curl X.Y.Z" token, ignoring the
119
+ * trailing "libcurl/X.Y.Z" that may report a different version.
120
+ *
121
+ * @param {string} versionOutput raw stdout of `curl --version`
122
+ * @returns {boolean} true when the parsed version is >= 7.70.0
123
+ */
124
+ function isCurlVersionSupported(versionOutput) {
125
+ const match = String(versionOutput).match(/^\s*curl\s+(\d+)\.(\d+)\.(\d+)/i);
126
+ if (!match) return false;
127
+ const major = parseInt(match[1], 10);
128
+ const minor = parseInt(match[2], 10);
129
+ return major > 7 || (major === 7 && minor >= 70);
130
+ }
131
+
132
+ // Memoized probe result. curl's version is invariant for the lifetime of the
133
+ // install, while download() runs once per mirror URL — so probe at most once.
134
+ let _curlSupportsSslRevokeBestEffort;
135
+
136
+ /**
137
+ * Detect whether the system curl supports --ssl-revoke-best-effort. Older
138
+ * versions (notably the curl 7.55.1 shipped with older Windows 10 builds)
139
+ * exit with "unknown option" if the flag is passed.
140
+ *
141
+ * @returns {boolean} true when curl >= 7.70.0 is available
142
+ */
143
+ function curlSupportsSslRevokeBestEffort() {
144
+ if (_curlSupportsSslRevokeBestEffort !== undefined) {
145
+ return _curlSupportsSslRevokeBestEffort;
146
+ }
147
+ try {
148
+ const output = execFileSync("curl", ["--version"], {
149
+ stdio: ["ignore", "pipe", "ignore"],
150
+ encoding: "utf8",
151
+ timeout: 5000,
152
+ });
153
+ _curlSupportsSslRevokeBestEffort = isCurlVersionSupported(output);
154
+ } catch (_) {
155
+ _curlSupportsSslRevokeBestEffort = false;
156
+ }
157
+ return _curlSupportsSslRevokeBestEffort;
158
+ }
159
+
160
+ function download(url, destPath) {
161
+ assertAllowedHost(url);
162
+ const args = [
163
+ "--fail", "--location", "--silent", "--show-error",
164
+ "--connect-timeout", "10", "--max-time", "120",
165
+ "--max-redirs", "3",
166
+ "--output", destPath,
167
+ ];
168
+ // --ssl-revoke-best-effort: on Windows (Schannel), avoid CRYPT_E_REVOCATION_OFFLINE
169
+ // errors when the certificate revocation list server is unreachable.
170
+ // Only use it when the system curl is new enough (>= 7.70.0).
171
+ if (isWindows && curlSupportsSslRevokeBestEffort()) {
172
+ args.unshift("--ssl-revoke-best-effort");
173
+ }
174
+ args.push(url);
175
+ execFileSync("curl", args, { stdio: ["ignore", "ignore", "pipe"] });
176
+ }
177
+
178
+ function extractZipWindows(archivePath, destDir) {
179
+ const psOpts = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"];
180
+ const psStdio = ["ignore", "inherit", "inherit"];
181
+ const psEnv = {
182
+ ...process.env,
183
+ LARK_CLI_ARCHIVE: archivePath,
184
+ LARK_CLI_DEST: destDir,
185
+ };
186
+
187
+ try {
188
+ const dotnet =
189
+ "$ErrorActionPreference='Stop';" +
190
+ "Add-Type -AssemblyName System.IO.Compression.FileSystem;" +
191
+ "[System.IO.Compression.ZipFile]::ExtractToDirectory($env:LARK_CLI_ARCHIVE,$env:LARK_CLI_DEST)";
192
+ execFileSync("powershell.exe", [...psOpts, dotnet], { stdio: psStdio, env: psEnv });
193
+ } catch (primaryErr) {
194
+ try {
195
+ const cmdlet =
196
+ "$ErrorActionPreference='Stop';" +
197
+ "Expand-Archive -LiteralPath $env:LARK_CLI_ARCHIVE -DestinationPath $env:LARK_CLI_DEST -Force";
198
+ execFileSync("powershell.exe", [...psOpts, cmdlet], { stdio: psStdio, env: psEnv });
199
+ } catch (secondErr) {
200
+ try {
201
+ execFileSync("tar", ["-xf", archivePath, "-C", destDir], { stdio: psStdio });
202
+ } catch (fallbackErr) {
203
+ throw new Error(
204
+ `Failed to extract ${archivePath}. ` +
205
+ `.NET ZipFile attempt: ${primaryErr.message}. ` +
206
+ `Expand-Archive fallback: ${secondErr.message}. ` +
207
+ `tar fallback: ${fallbackErr.message}`
208
+ );
209
+ }
210
+ }
211
+ }
212
+ }
213
+
214
+ function install() {
215
+ const mirrorUrls = getMirrorUrls(process.env);
216
+ const downloadUrls = [GITHUB_URL, ...mirrorUrls];
217
+
218
+ fs.mkdirSync(binDir, { recursive: true });
219
+
220
+ const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lark-cli-"));
221
+ const archivePath = path.join(tmpDir, archiveName);
222
+
223
+ try {
224
+ // Walk the chain in order; stop at the first success. Default chain:
225
+ // GitHub → derived(npm_config_registry)? → npmmirror. The npmmirror
226
+ // tail preserves the pre-PR safety net when a corporate proxy doesn't
227
+ // actually host /-/binary/<pkg>/...
228
+ let lastErr;
229
+ let downloaded = false;
230
+ for (const url of downloadUrls) {
231
+ try {
232
+ download(url, archivePath);
233
+ downloaded = true;
234
+ break;
235
+ } catch (e) {
236
+ lastErr = e;
237
+ }
238
+ }
239
+ if (!downloaded) throw lastErr;
240
+
241
+ const expectedHash = getExpectedChecksum(archiveName);
242
+ verifyChecksum(archivePath, expectedHash);
243
+
244
+ if (isWindows) {
245
+ extractZipWindows(archivePath, tmpDir);
246
+ } else {
247
+ execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], {
248
+ stdio: "ignore",
249
+ });
250
+ }
251
+
252
+ const binaryName = NAME + (isWindows ? ".exe" : "");
253
+ const extractedBinary = path.join(tmpDir, binaryName);
254
+
255
+ fs.copyFileSync(extractedBinary, dest);
256
+ fs.chmodSync(dest, 0o755);
257
+ console.log(`${NAME} v${VERSION} installed successfully`);
258
+ } finally {
259
+ fs.rmSync(tmpDir, { recursive: true, force: true });
260
+ }
261
+ }
262
+
263
+ function getExpectedChecksum(archiveName, checksumsDir) {
264
+ const dir = checksumsDir || path.join(__dirname, "..");
265
+ const checksumsPath = path.join(dir, "checksums.txt");
266
+
267
+ if (!fs.existsSync(checksumsPath)) {
268
+ console.error(
269
+ "[WARN] checksums.txt not found, skipping checksum verification"
270
+ );
271
+ return null;
272
+ }
273
+
274
+ const content = fs.readFileSync(checksumsPath, "utf8");
275
+ for (const line of content.split("\n")) {
276
+ const trimmed = line.trim();
277
+ if (!trimmed) continue;
278
+ const idx = trimmed.indexOf(" ");
279
+ if (idx === -1) continue;
280
+ const hash = trimmed.slice(0, idx);
281
+ const name = trimmed.slice(idx + 2);
282
+ if (name === archiveName) return hash;
283
+ }
284
+
285
+ throw new Error(`Checksum entry not found for ${archiveName}`);
286
+ }
287
+
288
+ function verifyChecksum(archivePath, expectedHash) {
289
+ if (expectedHash === null) return;
290
+
291
+ // Stream the file to avoid loading the entire archive into memory.
292
+ // Archives can be 10-100MB; streaming keeps RSS constant.
293
+ const hash = crypto.createHash("sha256");
294
+ const fd = fs.openSync(archivePath, "r");
295
+ try {
296
+ const buf = Buffer.alloc(64 * 1024);
297
+ let bytesRead;
298
+ while ((bytesRead = fs.readSync(fd, buf, 0, buf.length, null)) > 0) {
299
+ hash.update(buf.subarray(0, bytesRead));
300
+ }
301
+ } finally {
302
+ fs.closeSync(fd);
303
+ }
304
+ const actual = hash.digest("hex");
305
+
306
+ if (actual.toLowerCase() !== expectedHash.toLowerCase()) {
307
+ throw new Error(
308
+ `[SECURITY] Checksum mismatch for ${path.basename(archivePath)}: expected ${expectedHash} but got ${actual}`
309
+ );
310
+ }
311
+ }
312
+
313
+ if (require.main === module) {
314
+ if (!platform || !arch) {
315
+ console.error(
316
+ `Unsupported platform: ${process.platform}-${process.arch}`
317
+ );
318
+ process.exit(1);
319
+ }
320
+
321
+ // When triggered as a postinstall hook under npx, skip the binary download.
322
+ // The "install" wizard doesn't need it, and run.js calls install.js directly
323
+ // (with LARK_CLI_RUN=1) for other commands that do need the binary.
324
+ const isNpxPostinstall =
325
+ process.env.npm_command === "exec" && !process.env.LARK_CLI_RUN;
326
+
327
+ if (isNpxPostinstall) {
328
+ process.exit(0);
329
+ }
330
+
331
+ try {
332
+ install();
333
+ } catch (err) {
334
+ console.error(`Failed to install ${NAME}:`, err.message);
335
+ console.error(
336
+ `\nIf you are behind a firewall or in a restricted network, try one of:\n` +
337
+ ` # 1. Use a proxy:\n` +
338
+ ` export https_proxy=http://your-proxy:port\n` +
339
+ ` npm install -g @weact-pipenet/weact-cli\n\n` +
340
+ ` # 2. Point to a corporate npm mirror that proxies /-/binary/weact-cli/...:\n` +
341
+ ` npm install -g @weact-pipenet/weact-cli --registry=https://your-corp-mirror/`
342
+ );
343
+ process.exit(1);
344
+ }
345
+ }
346
+
347
+ module.exports = { getExpectedChecksum, verifyChecksum, assertAllowedHost, resolveMirrorUrls, curlSupportsSslRevokeBestEffort, isCurlVersionSupported };
package/scripts/run.js ADDED
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env node
2
+ // Copyright (c) 2026 Lark Technologies Pte. Ltd.
3
+ // SPDX-License-Identifier: MIT
4
+
5
+ const { execFileSync } = require("child_process");
6
+ const fs = require("fs");
7
+ const path = require("path");
8
+
9
+ const ext = process.platform === "win32" ? ".exe" : "";
10
+ const bin = path.join(__dirname, "..", "bin", "weact-cli" + ext);
11
+
12
+ // On Windows, a crashed self-update may have left the binary renamed to .old.
13
+ // Recover it before proceeding so the CLI remains functional.
14
+ const oldBin = bin + ".old";
15
+ function restoreOldBinary() {
16
+ try {
17
+ if (fs.existsSync(bin)) {
18
+ fs.rmSync(bin, { force: true });
19
+ }
20
+ fs.renameSync(oldBin, bin);
21
+ return true;
22
+ } catch (_) {
23
+ return false;
24
+ }
25
+ }
26
+
27
+ if (process.platform === "win32" && fs.existsSync(oldBin)) {
28
+ if (!fs.existsSync(bin)) {
29
+ restoreOldBinary();
30
+ } else {
31
+ try {
32
+ execFileSync(bin, ["--version"], { stdio: "ignore", timeout: 10000 });
33
+ try {
34
+ fs.rmSync(oldBin, { force: true });
35
+ } catch (_) {
36
+ // Best-effort cleanup; keep running the healthy binary.
37
+ }
38
+ } catch (_) {
39
+ restoreOldBinary();
40
+ }
41
+ }
42
+ }
43
+
44
+ // Intercept "install" subcommand — run the setup wizard directly,
45
+ // bypassing the native binary (which may not exist yet under npx).
46
+ const args = process.argv.slice(2);
47
+ if (args[0] === "install") {
48
+ require("./install-wizard.js");
49
+ } else {
50
+ // Auto-download binary if missing (e.g. npx skipped postinstall).
51
+ if (!fs.existsSync(bin)) {
52
+ try {
53
+ execFileSync(process.execPath, [path.join(__dirname, "install.js")], {
54
+ stdio: "inherit",
55
+ env: { ...process.env, LARK_CLI_RUN: "true" },
56
+ });
57
+ } catch (_) {
58
+ console.error(
59
+ `\nFailed to auto-install weact-cli binary.\n` +
60
+ `To fix, run the install script manually:\n` +
61
+ ` node "${path.join(__dirname, "install.js")}"\n`
62
+ );
63
+ process.exit(1);
64
+ }
65
+ }
66
+
67
+ try {
68
+ execFileSync(bin, args, { stdio: "inherit" });
69
+ } catch (e) {
70
+ process.exit(e.status || 1);
71
+ }
72
+ }