@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 +42 -0
- package/README.md +18 -8
- package/README.zh.md +17 -7
- package/checksums.txt +6 -6
- package/npm/bin/viceme.mjs +10 -1
- package/npm/lib/installer.mjs +96 -52
- package/package.json +1 -1
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
|
-
|
|
|
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` |
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
|
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` |
|
|
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
|
|
183
|
-
|
|
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`
|
|
210
|
+
`VICEME_API_BASE_URL` 只是 API-only 的单进程 CI / 调试覆盖,不是 Profile 状态;
|
|
211
|
+
设置时禁止持久化登录和生成网站嵌入片段。远程自定义
|
|
205
212
|
Endpoint 必须使用 HTTPS;只有 localhost 和 loopback 本地开发可以使用 HTTP。
|
|
206
|
-
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
package/npm/bin/viceme.mjs
CHANGED
|
@@ -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
|
|
44
|
+
return signalExitCode(child.signal);
|
|
36
45
|
}
|
|
37
46
|
return child.status ?? 1;
|
|
38
47
|
}
|
package/npm/lib/installer.mjs
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
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
|
|
373
|
+
async function binaryMatchesChecksum(binaryPath, expectedChecksum) {
|
|
320
374
|
try {
|
|
321
|
-
const [binary
|
|
375
|
+
const [binary] = await Promise.all([
|
|
322
376
|
readFile(binaryPath),
|
|
323
|
-
readFile(checksumPath, "utf8"),
|
|
324
377
|
access(binaryPath, constants.X_OK),
|
|
325
378
|
]);
|
|
326
|
-
|
|
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
|
}
|