@viceme-ai/cli 0.2.0 → 0.3.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 CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-07-20
4
+
5
+ ### Features
6
+
7
+ - add profile management (`fec286e`)
8
+
9
+ ## [0.2.1] - 2026-07-20
10
+
11
+ ### Fixes
12
+
13
+ - add verified binary mirror fallback (`efb0d83`)
14
+
15
+ ### Other Changes
16
+
17
+ - add Feishu pull request notifications (`b6174b1`)
18
+
3
19
  ## [0.2.0] - 2026-07-19
4
20
 
5
21
  ### Features
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  The official command-line client and Agent Skill for publishing external Skills as stable, shareable Viceme Agents. It is built for AI coding tools such as Codex and Claude Code: the Agent Skill understands user intent, while the CLI provides the deterministic authentication, upload, publication, and status protocol.
10
10
 
11
- [Install](#installation--quick-start) · [AI Agent Skills](#agent-skills) · [Auth](#authentication) · [Regions](#regions) · [Commands](#command-overview) · [Output contract](#json-output-contract) · [Security](#security-and-risk-controls) · [Development](#development)
11
+ [Install](#installation--quick-start) · [AI Agent Skills](#agent-skills) · [Auth](#authentication) · [Regions & profiles](#regions--profiles) · [Commands](#command-overview) · [Output contract](#json-output-contract) · [Security](#security-and-risk-controls) · [Development](#development)
12
12
 
13
- > **Rollout status:** the Core publication transport and stable-link path are implemented behind a controlled allowlist. Public rollout remains blocked until the exact Candidate preview, test run, and result-confirmation gate is complete. The current `--yes` confirms the publication request; it is not proof that the user reviewed the final Candidate.
13
+ > **Rollout status:** the Core publication transport and stable-link path are implemented. Public rollout remains blocked until the exact Candidate preview, test run, and result-confirmation gate is complete. The current `--yes` confirms the publication request; it is not proof that the user reviewed the final Candidate.
14
14
 
15
15
  ## Why Viceme CLI?
16
16
 
@@ -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
@@ -103,6 +103,7 @@ viceme auth login --device-code <device-code>
103
103
  ```
104
104
 
105
105
  If authorization is still pending, reuse the same device code before it expires. Do not start a second device flow unless the original one has expired.
106
+ For a non-default profile, pass the same global `--profile <name>` when starting and continuing device login; the start result reports the effective `profile` and `region`.
106
107
 
107
108
  **Step 4 — Verify**
108
109
 
@@ -122,18 +123,29 @@ viceme skill inspect https://github.com/acme/poster-skill
122
123
 
123
124
  Inspection is read-only. Follow the bundled `viceme` Skill for source-specific handling, Target selection, confirmation, bounded job waiting, and result reporting. Public publication remains blocked until the exact Candidate confirmation gate described above is complete.
124
125
 
125
- ## Regions
126
+ ## Regions & Profiles
126
127
 
127
- Viceme exposes one product-level region choice during installation:
128
+ Each profile selects one Viceme region:
128
129
 
129
130
  | Region | Install command | API endpoint |
130
131
  |---|---|---|
131
132
  | China | `viceme install` | `https://api.viceme.cn` |
132
133
  | International | `viceme install --region global` | `https://api.viceme.ai` |
133
134
 
134
- The selected value is persisted as `region=cn|global`; later commands use it automatically. Credentials are isolated by region, so a China token is never reused against the international API, or vice versa.
135
+ The first install creates the `default` profile. Non-sensitive configuration is stored in `~/.viceme-cli/config.json`; access tokens remain exclusively in the operating-system keychain. Credentials are isolated by both profile and region.
135
136
 
136
- There is no public API URL, profile, or output-format configuration. For local development only, set `VICEME_API_BASE_URL` in the terminal environment.
137
+ ```bash
138
+ viceme profile list
139
+ viceme profile add --name work --region global --use
140
+ viceme profile use default
141
+ viceme --profile work auth status
142
+ viceme profile rename work company
143
+ viceme profile remove company
144
+ ```
145
+
146
+ `profile use` changes the persistent active profile; the global `--profile` flag overrides only one command. AI Agents must not switch or remove profiles unless the user explicitly requests it.
147
+
148
+ `VICEME_CLI_CONFIG_DIR` can override the config root. Local API development still uses the process-only `VICEME_API_BASE_URL`; it is never persisted in a profile.
137
149
 
138
150
  ## Agent Skills
139
151
 
@@ -160,10 +172,10 @@ viceme skills doctor
160
172
 
161
173
  | Command | Purpose |
162
174
  |---|---|
163
- | `viceme auth status` | Show whether the current region is authenticated |
175
+ | `viceme auth status` | Show whether the current profile is authenticated |
164
176
  | `viceme auth login --no-wait` | Start device authorization and return immediately |
165
177
  | `viceme auth login --device-code <code>` | Complete a previously started authorization |
166
- | `viceme auth logout` | Revoke and remove the current region credential |
178
+ | `viceme auth logout` | Revoke and remove the current profile credential |
167
179
 
168
180
  Tokens are stored only in the operating-system keychain. There is no plaintext token fallback, and successful login output never contains the access or refresh token.
169
181
 
@@ -204,8 +216,9 @@ viceme skill publish --file ./poster-skill-v2.zip \
204
216
 
205
217
  | Command group | Purpose |
206
218
  |---|---|
207
- | `viceme install` | Install the persistent launcher, Agent Skill, and region configuration |
219
+ | `viceme install` | Install the persistent launcher, Agent Skill, and default profile |
208
220
  | `viceme auth` | Start, complete, inspect, or revoke device authentication |
221
+ | `viceme profile` | Add, list, switch, rename, or remove local profiles |
209
222
  | `viceme skill inspect` | Freeze and inspect a source candidate without publishing |
210
223
  | `viceme skill publish` | Create or update a stable Skill Agent publication |
211
224
  | `viceme skill target` | Resolve existing logical Agent Targets and versions |
@@ -266,10 +279,10 @@ Determine command success from the process exit code or `ok == true`. A successf
266
279
  - **No source execution** — the CLI and compiler do not execute third-party scripts, binaries, shell fragments, marketplace commands, or copied instructions.
267
280
  - **Explicit public mutation** — publishing and cancellation require `--yes`; exit code `10` means the Agent must obtain confirmation, not silently retry.
268
281
  - **Safe preview** — use `--dry-run` on inspect or publish when the user needs to review the planned request without network or publication side effects.
269
- - **Credential isolation** — credentials stay in the OS keychain and are namespaced by region.
282
+ - **Credential isolation** — credentials stay in the OS keychain and are namespaced by profile and region.
270
283
  - **Immutable inputs** — inspection binds publication to an immutable source snapshot rather than re-reading a floating URL later.
271
284
  - **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.
285
+ - **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
286
 
274
287
  ## Diagnose and Update
275
288
 
package/README.zh.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  Viceme 官方命令行客户端与 Agent Skill,用于将外部 Skill 发布为稳定、可分享的 Viceme Agent。它面向 Codex、Claude Code 等 AI 编程工具:Agent Skill 负责理解用户意图,CLI 负责确定性的认证、上传、发布和状态协议。
10
10
 
11
- [安装](#安装与快速开始) · [AI Agent Skills](#agent-skills) · [认证](#认证) · [区域](#区域) · [命令](#命令概览) · [输出契约](#json-输出契约) · [安全](#安全与风险控制) · [开发](#开发)
11
+ [安装](#安装与快速开始) · [AI Agent Skills](#agent-skills) · [认证](#认证) · [区域与-profile](#区域与-profile) · [命令](#命令概览) · [输出契约](#json-输出契约) · [安全](#安全与风险控制) · [开发](#开发)
12
12
 
13
- > **开放状态:** Core 发布传输和稳定链接链路已在受控白名单后实现。正式对外开放仍取决于精确 Candidate 预览、试运行和结果确认门的完成。当前 `--yes` 只确认发起发布请求,并不代表用户已经审阅最终 Candidate。
13
+ > **开放状态:** Core 发布传输和稳定链接链路已经实现。正式对外开放仍取决于精确 Candidate 预览、试运行和结果确认门的完成。当前 `--yes` 只确认发起发布请求,并不代表用户已经审阅最终 Candidate。
14
14
 
15
15
  ## 为什么选择 Viceme CLI?
16
16
 
@@ -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
@@ -103,6 +103,7 @@ viceme auth login --device-code <device-code>
103
103
  ```
104
104
 
105
105
  如果授权仍处于等待状态,应在过期前继续使用同一个 device code。只有原流程已过期时才能重新发起设备登录。
106
+ 使用非默认 Profile 时,启动和继续设备登录必须传入同一个全局 `--profile <name>`;启动结果会返回实际的 `profile` 与 `region`。
106
107
 
107
108
  **第 4 步 — 验证**
108
109
 
@@ -122,18 +123,29 @@ viceme skill inspect https://github.com/acme/poster-skill
122
123
 
123
124
  inspect 是只读操作。后续应按照随包发布的 `viceme` Skill 处理不同来源、Target 选择、用户确认、有界任务等待和结果返回。在上方所述的精确 Candidate 确认门完成前,公开发布流程仍保持关闭。
124
125
 
125
- ## 区域
126
+ ## 区域与 Profile
126
127
 
127
- Viceme 在安装时只提供一个产品级区域选择:
128
+ 每个 Profile 独立选择一个 Viceme 区域:
128
129
 
129
130
  | 区域 | 安装命令 | API 地址 |
130
131
  |---|---|---|
131
132
  | 中国区 | `viceme install` | `https://api.viceme.cn` |
132
133
  | 国际区 | `viceme install --region global` | `https://api.viceme.ai` |
133
134
 
134
- 选择结果保存为 `region=cn|global`,后续命令会自动使用。不同区域的凭证彼此隔离,中国区令牌不会用于国际区 API,反之亦然。
135
+ 首次安装会创建 `default` Profile。配置保存在 `~/.viceme-cli/config.json`,访问令牌仍只保存在操作系统密钥链中。不同 Profile 和区域的凭证彼此隔离。
135
136
 
136
- CLI 不提供公开的 API 地址、profile 或输出格式配置。本地开发时可以在终端环境中设置 `VICEME_API_BASE_URL`。
137
+ ```bash
138
+ viceme profile list
139
+ viceme profile add --name work --region global --use
140
+ viceme profile use default
141
+ viceme --profile work auth status
142
+ viceme profile rename work company
143
+ viceme profile remove company
144
+ ```
145
+
146
+ `profile use` 修改持久化的当前 Profile;全局 `--profile` 只覆盖本次命令。不要让 AI Agent 在用户没有明确要求时切换或删除 Profile。
147
+
148
+ 可以用 `VICEME_CLI_CONFIG_DIR` 覆盖配置根目录。本地 API 联调仍使用进程环境变量 `VICEME_API_BASE_URL`,不会写入 Profile。
137
149
 
138
150
  ## Agent Skills
139
151
 
@@ -160,10 +172,10 @@ viceme skills doctor
160
172
 
161
173
  | 命令 | 用途 |
162
174
  |---|---|
163
- | `viceme auth status` | 查看当前区域是否已认证 |
175
+ | `viceme auth status` | 查看当前 Profile 是否已认证 |
164
176
  | `viceme auth login --no-wait` | 启动设备授权并立即返回 |
165
177
  | `viceme auth login --device-code <code>` | 完成之前启动的设备授权 |
166
- | `viceme auth logout` | 撤销并删除当前区域的凭证 |
178
+ | `viceme auth logout` | 撤销并删除当前 Profile 的凭证 |
167
179
 
168
180
  令牌只保存在操作系统密钥链中,不会回退到明文存储;登录成功的输出也不会包含访问令牌或刷新令牌。
169
181
 
@@ -204,8 +216,9 @@ viceme skill publish --file ./poster-skill-v2.zip \
204
216
 
205
217
  | 命令组 | 用途 |
206
218
  |---|---|
207
- | `viceme install` | 安装持久化启动器、Agent Skill 和区域配置 |
219
+ | `viceme install` | 安装持久化启动器、Agent Skill 和默认 Profile |
208
220
  | `viceme auth` | 启动、完成、检查或撤销设备认证 |
221
+ | `viceme profile` | 新增、列出、切换、重命名或删除本地 Profile |
209
222
  | `viceme skill inspect` | 固化并检查来源候选,不执行发布 |
210
223
  | `viceme skill publish` | 创建或更新具有稳定链接的 Skill Agent 发布 |
211
224
  | `viceme skill target` | 解析现有逻辑 Agent Target 及其版本 |
@@ -266,10 +279,10 @@ CLI 执行错误写入 **stderr**,退出码非零:
266
279
  - **不执行来源内容** — CLI 和编译器不会执行第三方脚本、二进制文件、shell 片段、市场命令或复制口令中的指令。
267
280
  - **公开变更需要明确确认** — 发布和取消操作需要 `--yes`;退出码 `10` 表示 Agent 必须向用户取得确认,不能静默重试。
268
281
  - **安全预览** — 用户需要检查计划请求时,可以对 inspect 或 publish 使用 `--dry-run`,不会产生网络请求或发布副作用。
269
- - **凭证隔离** — 凭证保存在操作系统密钥链中,并按区域隔离。
282
+ - **凭证隔离** — 凭证保存在操作系统密钥链中,并按 Profile 与区域隔离。
270
283
  - **不可变输入** — inspect 会把发布绑定到不可变来源快照,而不是在之后重新读取浮动 URL。
271
284
  - **有界等待** — `job wait` 有最大等待时间;超时后返回最新持久化状态,不会取消工作流。
272
- - **可信分发** — npm 启动器下载与其准确包版本匹配的二进制文件,并在启用前验证发布的 SHA-256 校验和。
285
+ - **可信分发** — npm 启动器从 GitHub 或 binary 镜像下载与其准确包版本匹配的二进制文件,并在启用前使用 npm 包内置的校验清单验证 SHA-256
273
286
 
274
287
  ## 诊断与更新
275
288
 
package/checksums.txt ADDED
@@ -0,0 +1,6 @@
1
+ 8be3c86b43e3121fde111bc195830bfb055d4a83f3aeb2516d6c232d0668831b viceme_0.3.0_darwin_amd64
2
+ bb4ee6635f2a42820296827a07ccc917b470e3941e2a002ebf78eec5ad02a4e9 viceme_0.3.0_darwin_arm64
3
+ ffcc623f6d14e9145b91a294c3903a121a54b23a0cd1b585f9a37cd16a50bbcf viceme_0.3.0_linux_amd64
4
+ e8635cee232fa604493548e81db75664a7a0315e829bef1d357341ee377e17d3 viceme_0.3.0_linux_arm64
5
+ 9e97ccd4834bd458d8334bb0ffe7cc613b1b83fa47d24ccca89a98d34316fc2a viceme_0.3.0_windows_amd64.exe
6
+ 4953ef1bd354ac5885103672df9bc125c7e4410a11da0352745b687e09049e97 viceme_0.3.0_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.3.0",
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",