@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.
- package/CHANGELOG.md +1339 -0
- package/LICENSE +21 -0
- package/README.md +293 -0
- package/README.zh.md +294 -0
- package/checksums.txt +7 -0
- package/package.json +39 -0
- package/scripts/install-wizard.js +380 -0
- package/scripts/install.js +347 -0
- package/scripts/run.js +72 -0
|
@@ -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
|
+
}
|