@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 +10 -0
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/checksums.txt +6 -0
- package/docs/releasing.md +10 -1
- package/npm/lib/installer.mjs +200 -31
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
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
|
|
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
|
|
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,
|
|
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
|
package/npm/lib/installer.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
59
|
-
throw new Error("
|
|
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
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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 (!
|
|
186
|
-
throw new Error(`
|
|
350
|
+
if (!checksum) {
|
|
351
|
+
throw new Error(`bundled checksum document does not contain ${asset}`);
|
|
187
352
|
}
|
|
188
|
-
return
|
|
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.
|
|
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",
|