@viceme-ai/cli 0.16.0 → 0.17.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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.17.0] - 2026-08-25
4
+
5
+ ### Features
6
+
7
+ - 支持公开互动能力自助接入 (#144) (`21ff78b`)
8
+ - 支持网站功能独立定价 (`14cf4ae`)
9
+ - 上传网站发布封面 (`af88653`)
10
+ - 完善网站发布展示信息 (`16f8926`)
11
+ - 统一 Skill 与网站发布流程 (`9ec8ca3`)
12
+ - 支持网站发布补齐创作者身份 (`602bd70`)
13
+ - 支持外部网站发布与作品售卖 (`d2f694b`)
14
+ - 限制创作者接入 Work (`3958862`)
15
+ - 自动选择唯一创作者作品 (`b6caeac`)
16
+ - 一步完成创作者接入 (`832aa4d`)
17
+ - 移除接入 Origin 配置 (`0f12281`)
18
+ - 提供创作者网页接入命令 (`e3a0e75`)
19
+
20
+ ### Fixes
21
+
22
+ - 更新创作者访问发布摘要 (`9d42715`)
23
+ - 明确发布先于网站能力接入 (`fdf8b61`)
24
+ - 接入初始化时引导完成网站发布 (`4820b26`)
25
+ - 按发布源类型路由发布流程 (`c5fd6ae`)
26
+ - 允许无域名发布网站作品 (`88a15dc`)
27
+
28
+ ### Other Changes
29
+
30
+ - 同步创作者授权弹层说明 (`3959f1c`)
31
+ - 同步创作者授权弹层交互 (`53a2a40`)
32
+ - 更新网站授权接入规则 (`d4a038c`)
33
+ - 支持普通用户创建 Work (`fc4d210`)
34
+ - 同步创作者访问交互 (`4be32a6`)
35
+ - 固定创作者接入交互样式 (`cde115d`)
36
+ - 更新权限交互集成流程 (`3df2a0a`)
37
+
38
+ ## [0.16.1] - 2026-08-19
39
+
40
+ ### Fixes
41
+
42
+ - 修复安装重试与启动信号传播 (`cae2f39`)
43
+ - unbrick npm activation recovery and anchor launcher cache to the bundled checksum manifest (`acf28b2`)
44
+
3
45
  ## [0.16.0] - 2026-08-19
4
46
 
5
47
  ### Features
package/README.md CHANGED
@@ -39,7 +39,7 @@
39
39
  | Capability | What ViceMe provides |
40
40
  | --- | --- |
41
41
  | Publish a Skill | Validate a local Skill directory or ZIP, set a CNY price, upload it, review platform suggestions, and publish a paid listing. |
42
- | Build a component | Ask an Agent to integrate the bundled production danmaku blueprint into a React and Tailwind CSS v4 project. |
42
+ | Add website engagement | Create a creator-owned public danmaku Work, bind a tip-enabled Creator App, and install the CLI-generated hosted script. |
43
43
  | Set up an Agent | Install, authenticate, update, diagnose, and repair the CLI and official Skills as one compatible release. |
44
44
  | Recover safely | Continue the same publication after a network or process interruption without uploading a duplicate product. |
45
45
 
@@ -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
 
@@ -159,7 +161,9 @@ Select a target explicitly with `viceme install --agent codex`, `claude`,
159
161
  | --- | --- |
160
162
  | `viceme-shared` | install ViceMe, sign in through the browser, manage Profiles, update, diagnose, or repair the local setup. |
161
163
  | `viceme-publish` | validate, upload, review, resume, or publish a local Skill directory or ZIP as a paid ViceMe listing. |
162
- | `viceme-danmaku` | build or adapt the bundled production React and Tailwind CSS v4 danmaku component in an existing project. |
164
+ | `viceme-danmaku` | create a public danmaku Work and install the CLI-generated hosted SDK snippet. |
165
+ | `viceme-tip` | sign in, bind a creator work and verified domain, and add the hosted ViceMe tip checkout to one HTML page. |
166
+ | `viceme-engagement` | create hosted danmaku, bind tips, and install one authoritative combined engagement script. |
163
167
 
164
168
  The Agent Skills own the conversational workflow and approval rules. The CLI
165
169
  owns deterministic local work and API calls. This separation lets an Agent
@@ -194,9 +198,9 @@ unknown. Query or resume the existing ID first.
194
198
 
195
199
  ## Authentication and Profiles
196
200
 
197
- Each Profile binds one explicit API endpoint and one device-authorized account.
198
- The separately stored distribution region selects only the CLI and bundled
199
- Skill download source; it never selects a publication market.
201
+ Each Profile binds one API base URL, Web base URL, market region, and
202
+ device-authorized account as one authority. The separately stored distribution
203
+ region selects only the CLI and bundled Skill download source.
200
204
 
201
205
  ```bash
202
206
  viceme auth login
@@ -214,13 +218,16 @@ dedicated Profile before signing in:
214
218
  viceme profile add \
215
219
  --name private-cn \
216
220
  --api-base-url https://api.example.com \
221
+ --web-base-url https://www.example.com \
222
+ --market-region cn \
217
223
  --use
218
224
  viceme auth login
219
225
  ```
220
226
 
221
- `VICEME_API_BASE_URL` is a one-process CI/debug override, not Profile state.
227
+ `VICEME_API_BASE_URL` is an API-only one-process CI/debug override, not Profile
228
+ state. Persistent login and website embed generation are disabled while it is set.
222
229
  Remote custom endpoints require HTTPS; only localhost and loopback development
223
- may use HTTP. Credentials are isolated by Profile and API origin, and an Agent
230
+ may use HTTP. Credentials are isolated by Profile authority, and an Agent
224
231
  must never switch to another Profile merely because it is already signed in.
225
232
 
226
233
  `viceme auth login` intentionally keeps running until browser authorization
@@ -237,7 +244,10 @@ Never copy an access token into the conversation.
237
244
  | `viceme version` | Show the CLI and bundled Skill versions. |
238
245
  | `viceme doctor` | Check the CLI, active Profile, credentials, API readiness, and installed official Skills. |
239
246
  | `viceme auth status` | Show whether the active Profile is signed in. |
240
- | `viceme profile list` | Show Profiles and their effective API endpoints. |
247
+ | `viceme access init --name <name> --danmaku` | Create and activate a creator-owned public hosted danmaku Work. |
248
+ | `viceme access inspect` / `viceme access apply` | Inspect or explicitly reconcile the Profile-bound danmaku config. |
249
+ | `viceme creator-app show <app-id> --work-key <work-key> --locale <locale>` | Validate both owned resources and emit the authoritative combined engagement snippet for the selected `zh-CN` or `en-US` page locale. |
250
+ | `viceme profile list` | Show Profiles and their API, Web, and market authorities. |
241
251
  | `viceme skill inspect --path <path>` | Validate a local Skill without side effects. |
242
252
  | `viceme skill listing prepare --path <path>` | Create or recover the stable private owner preview and persist the local binding. |
243
253
  | `viceme skill listing get <listing-id>` | Read the authoritative private Listing state. |
package/README.zh.md CHANGED
@@ -35,7 +35,7 @@
35
35
  | 能力 | ViceMe 提供的内容 |
36
36
  | --- | --- |
37
37
  | 发布 Skill | 校验本地 Skill 目录或 ZIP、设置人民币价格、上传、审核平台建议,并发布付费商品。 |
38
- | 构建组件 | Agent 把随包提供的生产级弹幕蓝图接入 React Tailwind CSS v4 项目。 |
38
+ | 接入网站互动 | 创建创作者自有的公开弹幕 Work,绑定支持赞赏的 Creator App,并安装 CLI 生成的托管脚本。 |
39
39
  | 配置 Agent | 把 CLI 与官方 Skills 作为同一个兼容版本安装、登录、更新、诊断和修复。 |
40
40
  | 安全恢复 | 网络或进程中断后继续原发布任务,不重复上传和创建商品。 |
41
41
 
@@ -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
 
@@ -149,7 +151,9 @@ npx --yes @viceme-ai/cli@latest install
149
151
  | --- | --- |
150
152
  | `viceme-shared` | 安装 ViceMe、通过浏览器登录、管理 Profile、更新、诊断或修复本地环境。 |
151
153
  | `viceme-publish` | 把本地 Skill 目录或 ZIP 校验、上传、审核、恢复或发布为 ViceMe 付费商品。 |
152
- | `viceme-danmaku` | 在已有项目中构建或适配随包提供的生产级 React Tailwind CSS v4 弹幕组件。 |
154
+ | `viceme-danmaku` | 创建公开弹幕 Work,并安装 CLI 生成的托管 SDK 片段。 |
155
+ | `viceme-tip` | 完成登录、绑定创作者作品与验证域名,并把 ViceMe 托管赞赏接入一个 HTML 页面。 |
156
+ | `viceme-engagement` | 创建托管弹幕、绑定赞赏,并安装唯一权威的组合交互脚本。 |
153
157
 
154
158
  Agent Skills 负责对话流程和授权规则;CLI 负责确定性本地操作与 API 调用。因此 Agent
155
159
  可以解释每一步决策,而相同的命令契约仍可在终端或自动化中复现。
@@ -179,8 +183,8 @@ viceme skill publish --resume <publication-id>
179
183
 
180
184
  ## 登录与 Profile
181
185
 
182
- 每个 Profile 绑定一个明确的 API Endpoint 和一个通过浏览器授权的账户。发布目标完全
183
- 由当前 Profile 的 Endpoint 决定;单独的分发区域只选择 CLI 与官方 Skills 的下载来源。
186
+ 每个 Profile API Base URL、Web Base URL、市场区域和浏览器授权账户绑定为一个完整
187
+ authority;单独的分发区域只选择 CLI 与官方 Skills 的下载来源。
184
188
 
185
189
  ```bash
186
190
  viceme auth login
@@ -197,13 +201,16 @@ viceme profile use default
197
201
  viceme profile add \
198
202
  --name private-cn \
199
203
  --api-base-url https://api.example.com \
204
+ --web-base-url https://www.example.com \
205
+ --market-region cn \
200
206
  --use
201
207
  viceme auth login
202
208
  ```
203
209
 
204
- `VICEME_API_BASE_URL` 只是单进程 CI / 调试覆盖,不是 Profile 状态。远程自定义
210
+ `VICEME_API_BASE_URL` 只是 API-only 的单进程 CI / 调试覆盖,不是 Profile 状态;
211
+ 设置时禁止持久化登录和生成网站嵌入片段。远程自定义
205
212
  Endpoint 必须使用 HTTPS;只有 localhost 和 loopback 本地开发可以使用 HTTP。
206
- 凭据按 Profile API Origin 隔离;Agent 不能因为另一个 Profile 已登录就擅自切换。
213
+ 凭据按完整 Profile authority 隔离;Agent 不能因为另一个 Profile 已登录就擅自切换。
207
214
 
208
215
  `viceme auth login` 会一直等待,直到浏览器授权完成或达到有界超时。Agent 必须保持
209
216
  命令运行,向用户展示一次性完整链接并等待最终结果。页面会在必要时先完成登录,随后
@@ -217,7 +224,10 @@ Endpoint 必须使用 HTTPS;只有 localhost 和 loopback 本地开发可以
217
224
  | `viceme version` | 显示 CLI 与随包 Skills 版本。 |
218
225
  | `viceme doctor` | 检查 CLI、当前 Profile、凭据、API readiness 和已安装官方 Skills。 |
219
226
  | `viceme auth status` | 显示当前 Profile 是否已登录。 |
220
- | `viceme profile list` | 显示 Profile 及其实际 API Endpoint。 |
227
+ | `viceme access init --name <名称> --danmaku` | 创建并激活创作者自有的公开托管弹幕 Work。 |
228
+ | `viceme access inspect` / `viceme access apply` | 检查或显式协调绑定 Profile 的弹幕配置。 |
229
+ | `viceme creator-app show <app-id> --work-key <work-key> --locale <locale>` | 校验两个自有资源,并按页面的 `zh-CN` 或 `en-US` 语言生成权威组合交互片段。 |
230
+ | `viceme profile list` | 显示 Profile 的 API、Web 与市场 authority。 |
221
231
  | `viceme skill inspect --path <path>` | 无副作用校验本地 Skill。 |
222
232
  | `viceme skill listing prepare --path <path>` | 创建或恢复稳定的创作者私有预览,并保存本地绑定。 |
223
233
  | `viceme skill listing get <listing-id>` | 读取权威的私有 Listing 状态。 |
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
+ 67148b7be4c9d1fb69eb753d84b0f5e9c70f6b47585c87d89439cf4d41ced00f viceme_0.17.0_darwin_amd64
2
+ 2b5bb35058438a6a41032d49d329fe79968d53c14aa55863273b2ba2c635b16a viceme_0.17.0_darwin_arm64
3
+ 3c5a9fe7fcd152fc9b4824ea1cf239afdbf2a04089a4a5a1c49e2bdecc971bbc viceme_0.17.0_linux_amd64
4
+ 65800f513300e84dfd2127d982ec1eb6d55624116a2853b9bdbede0291ac2ca9 viceme_0.17.0_linux_arm64
5
+ 8885fb5dd4216d97b7a9d89aca5344bb4ba7e9fdb7ef3adda2afcd931d0b31e4 viceme_0.17.0_windows_amd64.exe
6
+ a78eaed10b42581d65d9874b27ce93264139b99dadf8070ae3abdca5f070e752 viceme_0.17.0_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.17.0",
4
4
  "description": "Install the ViceMe creator CLI and official Agent Skills",
5
5
  "type": "module",
6
6
  "bin": {