@viceme-ai/cli 0.16.0 → 0.16.1
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 +7 -0
- package/README.md +2 -0
- package/README.zh.md +2 -0
- package/checksums.txt +6 -6
- package/npm/bin/viceme.mjs +10 -1
- package/npm/lib/installer.mjs +96 -52
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -126,12 +126,14 @@ VICEME_REGION=global sh -c "$(curl -fsSL https://s3.viceme.ai/start/install.sh)"
|
|
|
126
126
|
China:
|
|
127
127
|
|
|
128
128
|
```powershell
|
|
129
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
129
130
|
irm https://s3.viceme.cn/start/install.ps1 | iex
|
|
130
131
|
```
|
|
131
132
|
|
|
132
133
|
International:
|
|
133
134
|
|
|
134
135
|
```powershell
|
|
136
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
135
137
|
$env:VICEME_REGION="global"; irm https://s3.viceme.ai/start/install.ps1 | iex
|
|
136
138
|
```
|
|
137
139
|
|
package/README.zh.md
CHANGED
|
@@ -117,12 +117,14 @@ VICEME_REGION=global sh -c "$(curl -fsSL https://s3.viceme.ai/start/install.sh)"
|
|
|
117
117
|
中国区:
|
|
118
118
|
|
|
119
119
|
```powershell
|
|
120
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
120
121
|
irm https://s3.viceme.cn/start/install.ps1 | iex
|
|
121
122
|
```
|
|
122
123
|
|
|
123
124
|
海外:
|
|
124
125
|
|
|
125
126
|
```powershell
|
|
127
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
126
128
|
$env:VICEME_REGION="global"; irm https://s3.viceme.ai/start/install.ps1 | iex
|
|
127
129
|
```
|
|
128
130
|
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
3d6f752fb10b3b8b90b0a76158115c74994da96d1aac71b842f4496dca245ec1 viceme_0.16.1_darwin_amd64
|
|
2
|
+
310dd511ea791a37d4307efc8e7d48900fc8daf63a83288fcbc7d3b56823711c viceme_0.16.1_darwin_arm64
|
|
3
|
+
5689186a71e6cba254258f50397ea6f95872a8e2abfd56d41bdea97cf3167a9f viceme_0.16.1_linux_amd64
|
|
4
|
+
ed9d84c0a9cdc6a771a80d605b936d8aaa39ab2894270ecb962274f5729ddc10 viceme_0.16.1_linux_arm64
|
|
5
|
+
bba8e0b483b27041b86983cf42922099d29deeb2aabf212b3b433cbd9492a010 viceme_0.16.1_windows_amd64.exe
|
|
6
|
+
4896b58a6412f71d81b6129bcfd46ec80290799ac4aa02dd7063795ed80bef18 viceme_0.16.1_windows_arm64.exe
|
package/npm/bin/viceme.mjs
CHANGED
|
@@ -2,12 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { readFile } from "node:fs/promises";
|
|
5
|
+
import { constants as osConstants } from "node:os";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import process from "node:process";
|
|
7
8
|
|
|
8
9
|
import { ensureBinary } from "../lib/installer.mjs";
|
|
9
10
|
import { launcherEnvironment } from "../lib/launcher-environment.mjs";
|
|
10
11
|
|
|
12
|
+
function signalExitCode(signal) {
|
|
13
|
+
const signalNumber = osConstants.signals[signal];
|
|
14
|
+
if (!Number.isInteger(signalNumber)) {
|
|
15
|
+
throw new Error(`unsupported child termination signal ${signal}`);
|
|
16
|
+
}
|
|
17
|
+
return 128 + signalNumber;
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
export async function main(args = process.argv.slice(2), environment = process.env) {
|
|
12
21
|
const packageDocument = JSON.parse(
|
|
13
22
|
await readFile(new URL("../../package.json", import.meta.url), "utf8"),
|
|
@@ -32,7 +41,7 @@ export async function main(args = process.argv.slice(2), environment = process.e
|
|
|
32
41
|
throw child.error;
|
|
33
42
|
}
|
|
34
43
|
if (child.signal) {
|
|
35
|
-
return
|
|
44
|
+
return signalExitCode(child.signal);
|
|
36
45
|
}
|
|
37
46
|
return child.status ?? 1;
|
|
38
47
|
}
|
package/npm/lib/installer.mjs
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
access,
|
|
5
|
+
chmod,
|
|
6
|
+
mkdir,
|
|
7
|
+
mkdtemp,
|
|
8
|
+
readFile,
|
|
9
|
+
readdir,
|
|
10
|
+
rename,
|
|
11
|
+
rm,
|
|
12
|
+
writeFile,
|
|
13
|
+
} from "node:fs/promises";
|
|
4
14
|
import { constants } from "node:fs";
|
|
5
15
|
import os from "node:os";
|
|
6
16
|
import path from "node:path";
|
|
@@ -14,6 +24,7 @@ const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
|
14
24
|
const GENERATIONS_DIRECTORY = "generations";
|
|
15
25
|
const GENERATION_PREFIX = "generation-";
|
|
16
26
|
const STAGING_PREFIX = ".staging-";
|
|
27
|
+
const BINARY_DOWNLOAD_TIMEOUT_MILLISECONDS = 10 * 60 * 1_000;
|
|
17
28
|
|
|
18
29
|
export function releaseTarget(platform = process.platform, architecture = process.arch) {
|
|
19
30
|
const operatingSystems = {
|
|
@@ -80,6 +91,7 @@ export async function ensureBinary({
|
|
|
80
91
|
cacheDirectory,
|
|
81
92
|
checksumsDocument,
|
|
82
93
|
allowInsecureURL = false,
|
|
94
|
+
downloadTimeoutMilliseconds = BINARY_DOWNLOAD_TIMEOUT_MILLISECONDS,
|
|
83
95
|
}) {
|
|
84
96
|
if (environment.VICEME_BINARY_PATH) {
|
|
85
97
|
const overridden = path.resolve(environment.VICEME_BINARY_PATH);
|
|
@@ -93,14 +105,17 @@ export async function ensureBinary({
|
|
|
93
105
|
const root = cacheDirectory ?? defaultCacheDirectory(environment, platform);
|
|
94
106
|
const destinationDirectory = path.join(root, "cli", packageVersion);
|
|
95
107
|
const generationsDirectory = path.join(destinationDirectory, GENERATIONS_DIRECTORY);
|
|
96
|
-
|
|
108
|
+
// The bundled checksum manifest is the trust root for every code path, cached
|
|
109
|
+
// generations included. A generation is only reusable when its bytes match
|
|
110
|
+
// the bundled digest, never merely a sidecar stored next to it.
|
|
111
|
+
const checksumSource =
|
|
112
|
+
checksumsDocument ?? (await readFile(CHECKSUMS_URL, "utf8"));
|
|
113
|
+
const expectedChecksum = checksumForAsset(checksumSource, asset);
|
|
114
|
+
const cached = await findValidGeneration(generationsDirectory, asset, expectedChecksum);
|
|
97
115
|
if (cached) {
|
|
98
116
|
return cached;
|
|
99
117
|
}
|
|
100
118
|
await mkdir(generationsDirectory, { recursive: true, mode: 0o700 });
|
|
101
|
-
const checksumSource =
|
|
102
|
-
checksumsDocument ?? (await readFile(CHECKSUMS_URL, "utf8"));
|
|
103
|
-
const expectedChecksum = checksumForAsset(checksumSource, asset);
|
|
104
119
|
const urls = binaryDownloadURLs({
|
|
105
120
|
packageVersion,
|
|
106
121
|
asset,
|
|
@@ -114,12 +129,13 @@ export async function ensureBinary({
|
|
|
114
129
|
urls,
|
|
115
130
|
downloadImplementation,
|
|
116
131
|
allowInsecureURL,
|
|
132
|
+
downloadTimeoutMilliseconds,
|
|
117
133
|
});
|
|
118
134
|
|
|
119
135
|
// If another cold start finished while this process downloaded, reuse its
|
|
120
136
|
// complete immutable generation. Duplicate downloads are harmless; shared
|
|
121
137
|
// partially-written binary/checksum pairs do not exist.
|
|
122
|
-
const publishedByContender = await findValidGeneration(generationsDirectory, asset);
|
|
138
|
+
const publishedByContender = await findValidGeneration(generationsDirectory, asset, expectedChecksum);
|
|
123
139
|
if (publishedByContender) {
|
|
124
140
|
return publishedByContender;
|
|
125
141
|
}
|
|
@@ -140,7 +156,7 @@ export async function ensureBinary({
|
|
|
140
156
|
await writeFile(stagedBinary, binary, { mode: 0o700 });
|
|
141
157
|
await chmod(stagedBinary, 0o700);
|
|
142
158
|
await writeFile(stagedChecksum, `${expectedChecksum} ${asset}\n`, { mode: 0o600 });
|
|
143
|
-
if (!(await
|
|
159
|
+
if (!(await binaryMatchesChecksum(stagedBinary, expectedChecksum))) {
|
|
144
160
|
throw new Error(`staged binary verification failed for ${asset}`);
|
|
145
161
|
}
|
|
146
162
|
|
|
@@ -152,7 +168,7 @@ export async function ensureBinary({
|
|
|
152
168
|
await rm(stagingDirectory, { recursive: true, force: true });
|
|
153
169
|
}
|
|
154
170
|
const installedBinary = path.join(generationDirectory, asset);
|
|
155
|
-
if (!(await
|
|
171
|
+
if (!(await binaryMatchesChecksum(installedBinary, expectedChecksum))) {
|
|
156
172
|
throw new Error(`published binary verification failed for ${asset}`);
|
|
157
173
|
}
|
|
158
174
|
return installedBinary;
|
|
@@ -216,11 +232,23 @@ async function downloadVerifiedBinary({
|
|
|
216
232
|
urls,
|
|
217
233
|
downloadImplementation,
|
|
218
234
|
allowInsecureURL,
|
|
235
|
+
downloadTimeoutMilliseconds,
|
|
219
236
|
}) {
|
|
237
|
+
if (!Number.isFinite(downloadTimeoutMilliseconds) || downloadTimeoutMilliseconds <= 0) {
|
|
238
|
+
throw new Error("binary download timeout must be greater than zero");
|
|
239
|
+
}
|
|
240
|
+
const deadline = Date.now() + downloadTimeoutMilliseconds;
|
|
220
241
|
const failures = [];
|
|
221
242
|
for (const url of urls) {
|
|
243
|
+
const remainingMilliseconds = deadline - Date.now();
|
|
244
|
+
if (remainingMilliseconds <= 0) {
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
222
247
|
try {
|
|
223
|
-
const downloaded = await downloadImplementation(url, {
|
|
248
|
+
const downloaded = await downloadImplementation(url, {
|
|
249
|
+
allowInsecureURL,
|
|
250
|
+
timeoutMilliseconds: remainingMilliseconds,
|
|
251
|
+
});
|
|
224
252
|
const binary = Buffer.isBuffer(downloaded) ? downloaded : Buffer.from(downloaded);
|
|
225
253
|
if (digest(binary) !== expectedChecksum) {
|
|
226
254
|
throw new Error(`checksum mismatch for ${asset}`);
|
|
@@ -235,8 +263,18 @@ async function downloadVerifiedBinary({
|
|
|
235
263
|
);
|
|
236
264
|
}
|
|
237
265
|
|
|
238
|
-
async function downloadWithCurl(
|
|
266
|
+
export async function downloadWithCurl(
|
|
267
|
+
url,
|
|
268
|
+
{
|
|
269
|
+
allowInsecureURL = false,
|
|
270
|
+
timeoutMilliseconds = BINARY_DOWNLOAD_TIMEOUT_MILLISECONDS,
|
|
271
|
+
maxTimeSeconds = 600,
|
|
272
|
+
retryDelaySeconds = 2,
|
|
273
|
+
} = {},
|
|
274
|
+
) {
|
|
239
275
|
const parsed = parseDownloadBaseURL(url, allowInsecureURL);
|
|
276
|
+
const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), "viceme-curl-"));
|
|
277
|
+
const outputPath = path.join(temporaryDirectory, "download");
|
|
240
278
|
const arguments_ = [
|
|
241
279
|
"--fail",
|
|
242
280
|
"--location",
|
|
@@ -244,45 +282,61 @@ async function downloadWithCurl(url, { allowInsecureURL = false } = {}) {
|
|
|
244
282
|
"--show-error",
|
|
245
283
|
"--connect-timeout",
|
|
246
284
|
"10",
|
|
285
|
+
// The release binary is tens of megabytes, so allow one healthy transfer
|
|
286
|
+
// to use the full download budget. curl owns transient retry
|
|
287
|
+
// classification, while the Node child timeout bounds all retries and
|
|
288
|
+
// mirrors together. Only curl 7.12+ options are used.
|
|
247
289
|
"--max-time",
|
|
248
|
-
|
|
290
|
+
String(maxTimeSeconds),
|
|
291
|
+
"--retry",
|
|
292
|
+
"3",
|
|
293
|
+
"--retry-delay",
|
|
294
|
+
String(retryDelaySeconds),
|
|
249
295
|
"--max-redirs",
|
|
250
296
|
"5",
|
|
297
|
+
// curl can reset a regular output file before retrying. A stdout pipe
|
|
298
|
+
// retains partial bytes and would concatenate them with the next attempt.
|
|
299
|
+
"--output",
|
|
300
|
+
outputPath,
|
|
251
301
|
];
|
|
252
302
|
if (!allowInsecureURL) {
|
|
253
303
|
arguments_.push("--proto", "=https", "--proto-redir", "=https");
|
|
254
304
|
}
|
|
255
305
|
arguments_.push(parsed.href);
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
306
|
+
try {
|
|
307
|
+
await new Promise((resolve, reject) => {
|
|
308
|
+
const child = spawn("curl", arguments_, {
|
|
309
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
310
|
+
timeout: Math.max(1, Math.ceil(timeoutMilliseconds)),
|
|
311
|
+
windowsHide: true,
|
|
312
|
+
});
|
|
313
|
+
let stderr = "";
|
|
314
|
+
child.stderr.on("data", (chunk) => {
|
|
315
|
+
stderr += chunk;
|
|
316
|
+
});
|
|
317
|
+
child.on("error", (error) => {
|
|
318
|
+
if (error.code === "ENOENT") {
|
|
319
|
+
reject(new Error("curl is required to download the ViceMe CLI binary"));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
reject(error);
|
|
323
|
+
});
|
|
324
|
+
child.on("close", (code, signal) => {
|
|
325
|
+
if (code === 0) {
|
|
326
|
+
resolve();
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const reason = stderr.trim() || `curl exited ${code ?? `on signal ${signal}`}`;
|
|
330
|
+
reject(new Error(reason));
|
|
331
|
+
});
|
|
281
332
|
});
|
|
282
|
-
|
|
333
|
+
return await readFile(outputPath);
|
|
334
|
+
} finally {
|
|
335
|
+
await rm(temporaryDirectory, { recursive: true, force: true });
|
|
336
|
+
}
|
|
283
337
|
}
|
|
284
338
|
|
|
285
|
-
async function findValidGeneration(generationsDirectory, asset) {
|
|
339
|
+
async function findValidGeneration(generationsDirectory, asset, expectedChecksum) {
|
|
286
340
|
let entries;
|
|
287
341
|
try {
|
|
288
342
|
entries = await readdir(generationsDirectory, { withFileTypes: true });
|
|
@@ -296,7 +350,7 @@ async function findValidGeneration(generationsDirectory, asset) {
|
|
|
296
350
|
.filter((candidate) => candidate.isDirectory() && candidate.name.startsWith(GENERATION_PREFIX))
|
|
297
351
|
.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
298
352
|
const binaryPath = path.join(generationsDirectory, entry.name, asset);
|
|
299
|
-
if (await
|
|
353
|
+
if (await binaryMatchesChecksum(binaryPath, expectedChecksum)) {
|
|
300
354
|
return binaryPath;
|
|
301
355
|
}
|
|
302
356
|
}
|
|
@@ -316,15 +370,13 @@ function defaultCacheDirectory(environment, platform) {
|
|
|
316
370
|
return path.join(os.homedir(), ".cache", "viceme");
|
|
317
371
|
}
|
|
318
372
|
|
|
319
|
-
async function
|
|
373
|
+
async function binaryMatchesChecksum(binaryPath, expectedChecksum) {
|
|
320
374
|
try {
|
|
321
|
-
const [binary
|
|
375
|
+
const [binary] = await Promise.all([
|
|
322
376
|
readFile(binaryPath),
|
|
323
|
-
readFile(checksumPath, "utf8"),
|
|
324
377
|
access(binaryPath, constants.X_OK),
|
|
325
378
|
]);
|
|
326
|
-
|
|
327
|
-
return digest(binary) === expected;
|
|
379
|
+
return digest(binary) === expectedChecksum;
|
|
328
380
|
} catch {
|
|
329
381
|
return false;
|
|
330
382
|
}
|
|
@@ -353,14 +405,6 @@ function checksumForAsset(document, asset) {
|
|
|
353
405
|
return checksum;
|
|
354
406
|
}
|
|
355
407
|
|
|
356
|
-
function parseChecksum(document) {
|
|
357
|
-
const match = document.trim().match(/^([a-fA-F0-9]{64})(?:\s|$)/);
|
|
358
|
-
if (!match) {
|
|
359
|
-
throw new Error("release checksum document is invalid");
|
|
360
|
-
}
|
|
361
|
-
return match[1].toLowerCase();
|
|
362
|
-
}
|
|
363
|
-
|
|
364
408
|
function digest(buffer) {
|
|
365
409
|
return createHash("sha256").update(buffer).digest("hex");
|
|
366
410
|
}
|