@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 CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.16.1] - 2026-08-19
4
+
5
+ ### Fixes
6
+
7
+ - 修复安装重试与启动信号传播 (`cae2f39`)
8
+ - unbrick npm activation recovery and anchor launcher cache to the bundled checksum manifest (`acf28b2`)
9
+
3
10
  ## [0.16.0] - 2026-08-19
4
11
 
5
12
  ### Features
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
- 5d1a01daa98e11d4e8bbe4e06f369235f0b328b5622b413adf17d078dc19f149 viceme_0.16.0_darwin_amd64
2
- c5668fe1b1306a2553573cf5c9f96e80a81d2771444b5b5bde44c2ece73141fc viceme_0.16.0_darwin_arm64
3
- 6c2d3b2c5d6ad9fe8b1479872f08d0e8b3fe52c7ed7d6ad591542d6588da8fcf viceme_0.16.0_linux_amd64
4
- 0e472d484c38ac0b60acaa044d99b4913936abe96f0a4aef7e1c9e2db207fe3f viceme_0.16.0_linux_arm64
5
- 8ea8942c0ad36505fa46abec12177cb2f75e7eaf4bec6213aefeb6962debf4f0 viceme_0.16.0_windows_amd64.exe
6
- 824895a0ea8b941b31944efb74ce339ba9be0782bc4b42043428146cc52df51c viceme_0.16.0_windows_arm64.exe
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
@@ -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 128;
44
+ return signalExitCode(child.signal);
36
45
  }
37
46
  return child.status ?? 1;
38
47
  }
@@ -1,6 +1,16 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { access, chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
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
- const cached = await findValidGeneration(generationsDirectory, asset);
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 cachedBinaryIsValid(stagedBinary, stagedChecksum))) {
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 cachedBinaryIsValid(installedBinary, `${installedBinary}.sha256`))) {
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, { allowInsecureURL });
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(url, { allowInsecureURL = false } = {}) {
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
- "120",
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
- return await new Promise((resolve, reject) => {
257
- const child = spawn("curl", arguments_, {
258
- stdio: ["ignore", "pipe", "pipe"],
259
- windowsHide: true,
260
- });
261
- const stdout = [];
262
- let stderr = "";
263
- child.stdout.on("data", (chunk) => stdout.push(chunk));
264
- child.stderr.on("data", (chunk) => {
265
- stderr += chunk;
266
- });
267
- child.on("error", (error) => {
268
- if (error.code === "ENOENT") {
269
- reject(new Error("curl is required to download the ViceMe CLI binary"));
270
- return;
271
- }
272
- reject(error);
273
- });
274
- child.on("close", (code, signal) => {
275
- if (code === 0) {
276
- resolve(Buffer.concat(stdout));
277
- return;
278
- }
279
- const reason = stderr.trim() || `curl exited ${code ?? `on signal ${signal}`}`;
280
- reject(new Error(reason));
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 cachedBinaryIsValid(binaryPath, `${binaryPath}.sha256`)) {
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 cachedBinaryIsValid(binaryPath, checksumPath) {
373
+ async function binaryMatchesChecksum(binaryPath, expectedChecksum) {
320
374
  try {
321
- const [binary, checksumDocument] = await Promise.all([
375
+ const [binary] = await Promise.all([
322
376
  readFile(binaryPath),
323
- readFile(checksumPath, "utf8"),
324
377
  access(binaryPath, constants.X_OK),
325
378
  ]);
326
- const expected = parseChecksum(checksumDocument);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viceme-ai/cli",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Install the ViceMe creator CLI and official Agent Skills",
5
5
  "type": "module",
6
6
  "bin": {