@viceme-ai/cli 0.2.0 → 0.2.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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.1] - 2026-07-20
4
+
5
+ ### Fixes
6
+
7
+ - add verified binary mirror fallback (`efb0d83`)
8
+
9
+ ### Other Changes
10
+
11
+ - add Feishu pull request notifications (`b6174b1`)
12
+
3
13
  ## [0.2.0] - 2026-07-19
4
14
 
5
15
  ### Features
package/README.md CHANGED
@@ -50,7 +50,7 @@ npm install --global @viceme-ai/cli
50
50
  viceme install
51
51
  ```
52
52
 
53
- Both methods persist the npm launcher, download the matching checksum-verified Go binary, and install the bundled Viceme Agent Skill. They default to the China service. For the international service:
53
+ Both methods persist the npm launcher, download the matching checksum-verified Go binary, and install the bundled Viceme Agent Skill. Binary downloads try GitHub Release first, then a configured non-default npm registry's `/-/binary/viceme-cli/` mirror, and finally the public npmmirror binary mirror. The launcher uses system `curl`, so standard proxy environment variables are honored. They default to the China service. For the international service:
54
54
 
55
55
  ```bash
56
56
  npx --yes @viceme-ai/cli@latest install --region global
@@ -269,7 +269,7 @@ Determine command success from the process exit code or `ok == true`. A successf
269
269
  - **Credential isolation** — credentials stay in the OS keychain and are namespaced by region.
270
270
  - **Immutable inputs** — inspection binds publication to an immutable source snapshot rather than re-reading a floating URL later.
271
271
  - **Bounded waiting** — `job wait` has a maximum duration and returns the latest durable state without cancelling the workflow.
272
- - **Verified distribution** — the npm launcher downloads the binary for its exact package version and verifies the published SHA-256 checksum before activation.
272
+ - **Verified distribution** — the npm launcher downloads the binary for its exact package version from GitHub or a binary mirror and verifies it against the checksum manifest bundled in the npm package before activation.
273
273
 
274
274
  ## Diagnose and Update
275
275
 
package/README.zh.md CHANGED
@@ -50,7 +50,7 @@ npm install --global @viceme-ai/cli
50
50
  viceme install
51
51
  ```
52
52
 
53
- 两种方式都会持久化 npm 启动器、下载匹配且通过校验和验证的 Go 二进制文件,并安装随包发布的 Viceme Agent Skill。默认使用中国区服务。使用国际区服务:
53
+ 两种方式都会持久化 npm 启动器、下载匹配且通过校验和验证的 Go 二进制文件,并安装随包发布的 Viceme Agent Skill。二进制下载依次尝试 GitHub Release、用户配置的非默认 npm registry 的 `/-/binary/viceme-cli/` 镜像,最后回退到公共 npmmirror binary 镜像。启动器使用系统 `curl`,因此会遵循标准代理环境变量。默认使用中国区服务。使用国际区服务:
54
54
 
55
55
  ```bash
56
56
  npx --yes @viceme-ai/cli@latest install --region global
@@ -269,7 +269,7 @@ CLI 执行错误写入 **stderr**,退出码非零:
269
269
  - **凭证隔离** — 凭证保存在操作系统密钥链中,并按区域隔离。
270
270
  - **不可变输入** — inspect 会把发布绑定到不可变来源快照,而不是在之后重新读取浮动 URL。
271
271
  - **有界等待** — `job wait` 有最大等待时间;超时后返回最新持久化状态,不会取消工作流。
272
- - **可信分发** — npm 启动器下载与其准确包版本匹配的二进制文件,并在启用前验证发布的 SHA-256 校验和。
272
+ - **可信分发** — npm 启动器从 GitHub 或 binary 镜像下载与其准确包版本匹配的二进制文件,并在启用前使用 npm 包内置的校验清单验证 SHA-256
273
273
 
274
274
  ## 诊断与更新
275
275
 
package/checksums.txt ADDED
@@ -0,0 +1,6 @@
1
+ d22d97438aa887b238ef6c506ccd19ffe049877754660d00e114069079ca2151 viceme_0.2.1_darwin_amd64
2
+ 2157e6ccdbe76c6404e238fb2d04ea17c79289a1b712bfff42ddc8b1acdb7db6 viceme_0.2.1_darwin_arm64
3
+ 96dc6b38b609639e3111bdbfcc9bf0bcedf3fa6a0c66e3b2b2d769261a6acc84 viceme_0.2.1_linux_amd64
4
+ 7a9b0576ee36e9eab893f84669ca8cb6bd58d4bfc50a58054356c3863f710570 viceme_0.2.1_linux_arm64
5
+ b825703ce462985bf8810b8c386e39b0e50ca6b600f301cec7a6822309e101b2 viceme_0.2.1_windows_amd64.exe
6
+ cea8060b6f1f7f5a1c6bf44b787cd1ca5312c9b1f4e585b0f8bef325d6f748e0 viceme_0.2.1_windows_arm64.exe
package/docs/releasing.md CHANGED
@@ -28,7 +28,8 @@ files, create tags, write changelog entries, or run npm commands locally.
28
28
  8. A maintainer reviews and merges that same Release PR.
29
29
  9. `Release CLI and npm launcher` tags the exact reviewed `dev` head, reruns
30
30
  the quality gates, builds six platform binaries and six checksums, creates
31
- the GitHub Release, publishes the npm launcher, and then sends an
31
+ the GitHub Release, bundles those exact checksums into the npm launcher,
32
+ publishes it, and then sends an
32
33
  AI-generated release summary to the release notification group in Feishu.
33
34
 
34
35
  ## One-time repository setup
@@ -73,6 +74,14 @@ its first release, add a repository secret named `NPM_TOKEN` containing a
73
74
  granular automation token limited to `@viceme-ai/cli` publication. Remove that
74
75
  secret after the package exists and trusted publishing is confirmed.
75
76
 
77
+ The npm tarball contains `checksums.txt`, generated from the six immutable
78
+ GitHub Release checksum assets immediately before publication. The launcher
79
+ uses that bundled manifest as its trust root whether the matching binary is
80
+ transported by GitHub Release, a configured npm registry binary mirror, or the
81
+ public npmmirror binary mirror. Registering `viceme-cli` with cnpmcore enables
82
+ the public `/-/binary/viceme-cli/` mirror; it does not create another npm
83
+ package.
84
+
76
85
  `GITHUB_TOKEN` is provided by Actions and is used only to maintain the Release
77
86
  PR. `RELEASE_APP_ID` and `RELEASE_APP_PRIVATE_KEY` authenticate the narrowly
78
87
  scoped Release App. `NPM_TOKEN` is optional and should only be retained when the
@@ -1,10 +1,15 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { access, chmod, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
3
4
  import { constants } from "node:fs";
4
5
  import os from "node:os";
5
6
  import path from "node:path";
6
7
 
7
- const RELEASE_BASE_URL = "https://github.com/ViceMe-AI/cli/releases/download";
8
+ const GITHUB_RELEASE_BASE_URL = "https://github.com/ViceMe-AI/cli/releases/download";
9
+ const NPM_REGISTRY_URL = "https://registry.npmjs.org";
10
+ const NPMMIRROR_BINARY_BASE_URL =
11
+ "https://registry.npmmirror.com/-/binary/viceme-cli";
12
+ const CHECKSUMS_URL = new URL("../../checksums.txt", import.meta.url);
8
13
  const VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
9
14
  const GENERATIONS_DIRECTORY = "generations";
10
15
  const GENERATION_PREFIX = "generation-";
@@ -40,14 +45,40 @@ export function releaseAssetName(version, platform, architecture) {
40
45
  return `viceme_${version}_${target.operatingSystem}_${target.architecture}${target.extension}`;
41
46
  }
42
47
 
48
+ export function binaryDownloadURLs({
49
+ packageVersion,
50
+ asset,
51
+ environment = process.env,
52
+ sourceBaseURLs,
53
+ allowInsecureURL = false,
54
+ }) {
55
+ const bases = sourceBaseURLs ?? defaultBinarySourceBaseURLs(environment);
56
+ const urls = [];
57
+ const seen = new Set();
58
+ for (const base of bases) {
59
+ const parsed = parseDownloadBaseURL(base, allowInsecureURL);
60
+ const normalizedBase = parsed.href.replace(/\/$/, "");
61
+ const url = `${normalizedBase}/v${packageVersion}/${asset}`;
62
+ if (!seen.has(url)) {
63
+ seen.add(url);
64
+ urls.push(url);
65
+ }
66
+ }
67
+ if (urls.length === 0) {
68
+ throw new Error("no valid binary download source is configured");
69
+ }
70
+ return urls;
71
+ }
72
+
43
73
  export async function ensureBinary({
44
74
  packageVersion,
45
75
  environment = process.env,
46
76
  platform = process.platform,
47
77
  architecture = process.arch,
48
- fetchImplementation = globalThis.fetch,
49
- releaseBaseURL = RELEASE_BASE_URL,
78
+ downloadImplementation = downloadWithCurl,
79
+ sourceBaseURLs,
50
80
  cacheDirectory,
81
+ checksumsDocument,
51
82
  allowInsecureURL = false,
52
83
  }) {
53
84
  if (environment.VICEME_BINARY_PATH) {
@@ -55,8 +86,8 @@ export async function ensureBinary({
55
86
  await access(overridden, constants.X_OK);
56
87
  return overridden;
57
88
  }
58
- if (typeof fetchImplementation !== "function") {
59
- throw new Error("Node.js fetch support is required");
89
+ if (typeof downloadImplementation !== "function") {
90
+ throw new Error("a binary download implementation is required");
60
91
  }
61
92
  const asset = releaseAssetName(packageVersion, platform, architecture);
62
93
  const root = cacheDirectory ?? defaultCacheDirectory(environment, platform);
@@ -67,18 +98,23 @@ export async function ensureBinary({
67
98
  return cached;
68
99
  }
69
100
  await mkdir(generationsDirectory, { recursive: true, mode: 0o700 });
70
- const versionURL = `${releaseBaseURL}/v${packageVersion}`;
71
- const checksumURL = `${versionURL}/${asset}.sha256`;
72
- const binaryURL = `${versionURL}/${asset}`;
73
- const [checksumDocument, binary] = await Promise.all([
74
- download(checksumURL, fetchImplementation, allowInsecureURL),
75
- download(binaryURL, fetchImplementation, allowInsecureURL),
76
- ]);
77
- const expectedChecksum = parseChecksum(checksumDocument.toString("utf8"));
78
- const actualChecksum = digest(binary);
79
- if (actualChecksum !== expectedChecksum) {
80
- throw new Error(`checksum mismatch for ${asset}`);
81
- }
101
+ const checksumSource =
102
+ checksumsDocument ?? (await readFile(CHECKSUMS_URL, "utf8"));
103
+ const expectedChecksum = checksumForAsset(checksumSource, asset);
104
+ const urls = binaryDownloadURLs({
105
+ packageVersion,
106
+ asset,
107
+ environment,
108
+ sourceBaseURLs,
109
+ allowInsecureURL,
110
+ });
111
+ const binary = await downloadVerifiedBinary({
112
+ asset,
113
+ expectedChecksum,
114
+ urls,
115
+ downloadImplementation,
116
+ allowInsecureURL,
117
+ });
82
118
 
83
119
  // If another cold start finished while this process downloaded, reuse its
84
120
  // complete immutable generation. Duplicate downloads are harmless; shared
@@ -122,6 +158,130 @@ export async function ensureBinary({
122
158
  return installedBinary;
123
159
  }
124
160
 
161
+ function defaultBinarySourceBaseURLs(environment) {
162
+ const sources = [GITHUB_RELEASE_BASE_URL];
163
+ const configuredRegistry =
164
+ environment.npm_config_registry ?? environment.NPM_CONFIG_REGISTRY;
165
+ const registryBinaryBase = customRegistryBinaryBaseURL(configuredRegistry);
166
+ if (registryBinaryBase) {
167
+ sources.push(registryBinaryBase);
168
+ }
169
+ sources.push(NPMMIRROR_BINARY_BASE_URL);
170
+ return sources;
171
+ }
172
+
173
+ function customRegistryBinaryBaseURL(value) {
174
+ if (!value) {
175
+ return undefined;
176
+ }
177
+ let registry;
178
+ try {
179
+ registry = new URL(value);
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ if (
184
+ registry.protocol !== "https:" ||
185
+ registry.username ||
186
+ registry.password ||
187
+ registry.origin === new URL(NPM_REGISTRY_URL).origin
188
+ ) {
189
+ return undefined;
190
+ }
191
+ return `${registry.href.replace(/\/$/, "")}/-/binary/viceme-cli`;
192
+ }
193
+
194
+ function parseDownloadBaseURL(value, allowInsecureURL) {
195
+ let parsed;
196
+ try {
197
+ parsed = new URL(value);
198
+ } catch {
199
+ throw new Error(`invalid binary download source ${value}`);
200
+ }
201
+ if (parsed.username || parsed.password) {
202
+ throw new Error("binary download sources must not contain credentials");
203
+ }
204
+ if (!allowInsecureURL && parsed.protocol !== "https:") {
205
+ throw new Error(`refusing non-HTTPS binary download source ${value}`);
206
+ }
207
+ if (allowInsecureURL && !["http:", "https:"].includes(parsed.protocol)) {
208
+ throw new Error(`unsupported binary download protocol ${parsed.protocol}`);
209
+ }
210
+ return parsed;
211
+ }
212
+
213
+ async function downloadVerifiedBinary({
214
+ asset,
215
+ expectedChecksum,
216
+ urls,
217
+ downloadImplementation,
218
+ allowInsecureURL,
219
+ }) {
220
+ const failures = [];
221
+ for (const url of urls) {
222
+ try {
223
+ const downloaded = await downloadImplementation(url, { allowInsecureURL });
224
+ const binary = Buffer.isBuffer(downloaded) ? downloaded : Buffer.from(downloaded);
225
+ if (digest(binary) !== expectedChecksum) {
226
+ throw new Error(`checksum mismatch for ${asset}`);
227
+ }
228
+ return binary;
229
+ } catch (error) {
230
+ failures.push(`- ${url}: ${errorMessage(error)}`);
231
+ }
232
+ }
233
+ throw new Error(
234
+ `could not download a verified ${asset}; attempted:\n${failures.join("\n")}`,
235
+ );
236
+ }
237
+
238
+ async function downloadWithCurl(url, { allowInsecureURL = false } = {}) {
239
+ const parsed = parseDownloadBaseURL(url, allowInsecureURL);
240
+ const arguments_ = [
241
+ "--fail",
242
+ "--location",
243
+ "--silent",
244
+ "--show-error",
245
+ "--connect-timeout",
246
+ "10",
247
+ "--max-time",
248
+ "120",
249
+ "--max-redirs",
250
+ "5",
251
+ ];
252
+ if (!allowInsecureURL) {
253
+ arguments_.push("--proto", "=https", "--proto-redir", "=https");
254
+ }
255
+ 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));
281
+ });
282
+ });
283
+ }
284
+
125
285
  async function findValidGeneration(generationsDirectory, asset) {
126
286
  let entries;
127
287
  try {
@@ -170,22 +330,27 @@ async function cachedBinaryIsValid(binaryPath, checksumPath) {
170
330
  }
171
331
  }
172
332
 
173
- async function download(url, fetchImplementation, allowInsecureURL) {
174
- const parsed = new URL(url);
175
- if (!allowInsecureURL && parsed.protocol !== "https:") {
176
- throw new Error(`refusing non-HTTPS release URL ${url}`);
177
- }
178
- const response = await fetchImplementation(url, {
179
- redirect: "follow",
180
- headers: { "user-agent": "@viceme-ai/cli npm launcher" },
181
- });
182
- if (!response.ok) {
183
- throw new Error(`download failed (${response.status}) for ${url}`);
333
+ function checksumForAsset(document, asset) {
334
+ let checksum;
335
+ for (const line of document.split(/\r?\n/)) {
336
+ if (line.trim() === "") {
337
+ continue;
338
+ }
339
+ const match = line.match(/^([a-fA-F0-9]{64})\s+([^\s]+)$/);
340
+ if (!match) {
341
+ throw new Error("bundled checksum document is invalid");
342
+ }
343
+ if (match[2] === asset) {
344
+ if (checksum) {
345
+ throw new Error(`bundled checksum document contains duplicate ${asset}`);
346
+ }
347
+ checksum = match[1].toLowerCase();
348
+ }
184
349
  }
185
- if (!allowInsecureURL && response.url && new URL(response.url).protocol !== "https:") {
186
- throw new Error(`release download redirected to a non-HTTPS URL`);
350
+ if (!checksum) {
351
+ throw new Error(`bundled checksum document does not contain ${asset}`);
187
352
  }
188
- return Buffer.from(await response.arrayBuffer());
353
+ return checksum;
189
354
  }
190
355
 
191
356
  function parseChecksum(document) {
@@ -199,3 +364,7 @@ function parseChecksum(document) {
199
364
  function digest(buffer) {
200
365
  return createHash("sha256").update(buffer).digest("hex");
201
366
  }
367
+
368
+ function errorMessage(error) {
369
+ return error instanceof Error ? error.message : String(error);
370
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viceme-ai/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Install and run the Viceme CLI and Viceme Agent Skill",
5
5
  "type": "module",
6
6
  "bin": {
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "npm/bin/",
11
11
  "npm/lib/",
12
+ "checksums.txt",
12
13
  "README.md",
13
14
  "README.zh.md",
14
15
  "CHANGELOG.md",