@viceme-ai/cli 0.15.2 → 0.16.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 +23 -0
- package/README.md +43 -20
- package/README.zh.md +36 -16
- package/checksums.txt +6 -6
- package/npm/bin/viceme.mjs +17 -8
- package/npm/lib/installer.mjs +96 -52
- package/npm/lib/launcher-environment.mjs +13 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.16.1] - 2026-08-19
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- 修复安装重试与启动信号传播 (`cae2f39`)
|
|
8
|
+
- unbrick npm activation recovery and anchor launcher cache to the bundled checksum manifest (`acf28b2`)
|
|
9
|
+
|
|
10
|
+
## [0.16.0] - 2026-08-19
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
- 自动更新 CLI 与官方 Skills (`d68d096`)
|
|
15
|
+
|
|
16
|
+
### Fixes
|
|
17
|
+
|
|
18
|
+
- 合并并发自动更新并重执行等待命令 (`f48d2f6`)
|
|
19
|
+
|
|
20
|
+
### Other Changes
|
|
21
|
+
|
|
22
|
+
- 稳定激活子进程并发测试 (`fa139f9`)
|
|
23
|
+
- 注册 POC CLI 手动发布工作流 (`3e265fa`)
|
|
24
|
+
- 默认使用 Agent 补全发布内容 (`ee5030e`)
|
|
25
|
+
|
|
3
26
|
## [0.15.2] - 2026-08-18
|
|
4
27
|
|
|
5
28
|
### Fixes
|
package/README.md
CHANGED
|
@@ -61,14 +61,14 @@
|
|
|
61
61
|
|
|
62
62
|
The Agent checks login, keeps the selected Profile fixed throughout the
|
|
63
63
|
workflow, validates the Skill, immediately uploads the private Draft, and opens
|
|
64
|
-
its real Owner Preview. It
|
|
65
|
-
|
|
64
|
+
its real Owner Preview. It uploads media candidates without asking for a price,
|
|
65
|
+
then the user's Agent prepares the bilingual copy and media suggestions. The Agent shows
|
|
66
66
|
the complete listing—including the images—and asks one combined question for
|
|
67
67
|
the price and any desired changes. It then shows the final review and asks once
|
|
68
68
|
whether to confirm and publish it publicly.
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
|
-
Local Skill → Login → Validate and private upload → Owner Preview →
|
|
71
|
+
Local Skill → Login → Validate and private upload → Owner Preview → Agent enrichment
|
|
72
72
|
→ Complete listing details + price question → Final review
|
|
73
73
|
→ Confirm and publish → Public URL
|
|
74
74
|
```
|
|
@@ -90,11 +90,14 @@ viceme auth login
|
|
|
90
90
|
# Upload the real private draft and open its Owner Preview before pricing.
|
|
91
91
|
viceme skill publish --path ./my-skill
|
|
92
92
|
|
|
93
|
-
# Continue the same unpriced draft
|
|
93
|
+
# Continue the same unpriced draft and upload media candidates.
|
|
94
94
|
viceme skill publish --resume <publication-id>
|
|
95
|
-
viceme publication wait <publication-id>
|
|
96
95
|
viceme publication review <publication-id>
|
|
97
96
|
|
|
97
|
+
# The Agent writes a revision-protected suggestion. Platform analysis is an explicit fallback only.
|
|
98
|
+
viceme publication suggest <publication-id> --input <suggestion.json>
|
|
99
|
+
# viceme publication analyze <publication-id> && viceme publication wait <publication-id>
|
|
100
|
+
|
|
98
101
|
# After reviewing the complete listing details, set CNY 1.00 on that draft.
|
|
99
102
|
viceme skill publish --resume <publication-id> --price-minor 100
|
|
100
103
|
```
|
|
@@ -123,12 +126,14 @@ VICEME_REGION=global sh -c "$(curl -fsSL https://s3.viceme.ai/start/install.sh)"
|
|
|
123
126
|
China:
|
|
124
127
|
|
|
125
128
|
```powershell
|
|
129
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
126
130
|
irm https://s3.viceme.cn/start/install.ps1 | iex
|
|
127
131
|
```
|
|
128
132
|
|
|
129
133
|
International:
|
|
130
134
|
|
|
131
135
|
```powershell
|
|
136
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
132
137
|
$env:VICEME_REGION="global"; irm https://s3.viceme.ai/start/install.ps1 | iex
|
|
133
138
|
```
|
|
134
139
|
|
|
@@ -175,8 +180,8 @@ are not accepted.
|
|
|
175
180
|
| Login | Browser authorization happens before protected API work. Every command remains pinned to the same Profile and API origin. |
|
|
176
181
|
| Inspect | Local validation rejects unsafe paths, special files, excessive content, sensitive files, and common secret patterns. |
|
|
177
182
|
| Private upload and preview | The initial publish request authorizes the private Draft upload. The real package is verified and its Owner Preview opens before pricing. |
|
|
178
|
-
|
|
|
179
|
-
| Listing details and price | After
|
|
183
|
+
| Agent enrichment | The user's Agent reads the Skill as untrusted source data, prepares bilingual copy, and selects verified cover and gallery uploads. Platform analysis is an explicit fallback only. |
|
|
184
|
+
| Listing details and price | After enrichment, the Agent displays the exact title, bilingual copy, cover, and gallery, then asks one combined question for the CNY price in fen and any desired changes. It never asks for price by itself. |
|
|
180
185
|
| Review | The Agent displays the exact copy, price, cover, and gallery images. A short summary has a maximum display width of 30; ASCII counts as 1 and Chinese/non-ASCII as 2. |
|
|
181
186
|
| Publish | One explicit final confirmation authorizes review confirmation followed by immediate, irreversible public publication. |
|
|
182
187
|
|
|
@@ -240,11 +245,13 @@ Never copy an access token into the conversation.
|
|
|
240
245
|
| `viceme skill listing get <listing-id>` | Read the authoritative private Listing state. |
|
|
241
246
|
| `viceme skill listing bind <listing-id> --path <path>` | Explicitly bind a source to a selected owned Listing. |
|
|
242
247
|
| `viceme skill publish --path <path>` | Upload the real private package and return its Owner Preview before pricing. |
|
|
243
|
-
| `viceme skill publish --resume <id>` | Continue the same unpriced Draft
|
|
244
|
-
| `viceme publication wait <id>` | Wait for background analysis without re-uploading. |
|
|
248
|
+
| `viceme skill publish --resume <id>` | Continue the same unpriced Draft and upload media candidates without starting a platform model. |
|
|
245
249
|
| `viceme publication review <id>` | Read the authoritative bilingual copy, price, selected media, and review state. |
|
|
250
|
+
| `viceme publication suggest <id> --input ...` | Apply Agent-generated bilingual copy and media with Draft revision protection. |
|
|
251
|
+
| `viceme publication analyze <id>` | Explicitly request platform-model analysis when the current Agent cannot perform enrichment. |
|
|
252
|
+
| `viceme publication wait <id>` | Wait for an explicitly requested platform analysis without re-uploading. |
|
|
246
253
|
| `viceme skill publish --resume <id> --price-minor <fen>` | Apply the reviewed CNY price to the same Draft without creating another Listing. |
|
|
247
|
-
| `viceme publication asset upload ...` | Replace or add
|
|
254
|
+
| `viceme publication asset upload ...` | Replace user-selected media, or add `--candidate-only` to stage Agent-provided media for `publication suggest`. |
|
|
248
255
|
| `viceme publication update ...` | Replace the complete listing draft from a strict JSON file. |
|
|
249
256
|
| `viceme publication confirm ...` | Confirm the exact current review digest. |
|
|
250
257
|
| `viceme publication publish ...` | Make a confirmed listing public. |
|
|
@@ -265,6 +272,11 @@ text.
|
|
|
265
272
|
"data": {},
|
|
266
273
|
"meta": {
|
|
267
274
|
"executingCliVersion": "<version>",
|
|
275
|
+
"autoUpdate": {
|
|
276
|
+
"from": "<previous-version>",
|
|
277
|
+
"to": "<executing-version>",
|
|
278
|
+
"status": "updated"
|
|
279
|
+
},
|
|
268
280
|
"requestId": "optional"
|
|
269
281
|
}
|
|
270
282
|
}
|
|
@@ -273,21 +285,31 @@ text.
|
|
|
273
285
|
`meta.executingCliVersion` is the version of the process that emitted the
|
|
274
286
|
response. For `viceme update`, the newly installed version is reported
|
|
275
287
|
separately as `data.cli_version` because the response is still emitted by the
|
|
276
|
-
process that started the update.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
288
|
+
process that started the update. `meta.autoUpdate` is present only when this
|
|
289
|
+
command was automatically continued by a newly activated generation.
|
|
290
|
+
|
|
291
|
+
Every released installation passes through a bounded freshness gate before an
|
|
292
|
+
ordinary command. A validated result is reused for five minutes so a single
|
|
293
|
+
workflow does not repeatedly contact the release channel. When a newer stable
|
|
294
|
+
release exists, the CLI and all detected official Skills are activated as one
|
|
295
|
+
recoverable generation and the original command is automatically re-executed by
|
|
296
|
+
the new CLI. Release discovery is fail-open while offline; activation failure
|
|
297
|
+
stops the original command so an older process cannot perform a mutation after
|
|
298
|
+
a failed generation change.
|
|
299
|
+
|
|
300
|
+
An npm installation continues the original command automatically on every
|
|
301
|
+
supported platform. A standalone Windows binary may return the retryable code
|
|
302
|
+
`AUTO_UPDATE_RESTART_REQUIRED` once while Windows releases the old executable;
|
|
303
|
+
rerunning the exact command completes under the new generation.
|
|
283
304
|
|
|
284
305
|
```bash
|
|
285
306
|
viceme update --check
|
|
286
307
|
viceme update
|
|
287
308
|
```
|
|
288
309
|
|
|
289
|
-
|
|
290
|
-
|
|
310
|
+
`viceme update` remains an explicit repair command. The normal startup gate
|
|
311
|
+
already verifies the exact release, refreshes the matching official Skills, and
|
|
312
|
+
recovers interrupted activation as one compatible local generation.
|
|
291
313
|
|
|
292
314
|
## Security
|
|
293
315
|
|
|
@@ -298,7 +320,8 @@ and recovers interrupted activation as one compatible local generation.
|
|
|
298
320
|
- Credentials remain in the CLI's secure local store and are scoped to a
|
|
299
321
|
Profile and API origin; they are never part of Agent conversation state.
|
|
300
322
|
- Presigned upload URLs are not written to the local pending-operation store.
|
|
301
|
-
-
|
|
323
|
+
- Agent enrichment treats all package content as untrusted data and cannot bypass server-side package, media, ownership, or revision validation.
|
|
324
|
+
- Explicit platform-model analysis receives only filtered text, metadata, and image thumbnails.
|
|
302
325
|
- Public publication requires the exact reviewed copy, price, cover, and
|
|
303
326
|
ordered gallery to be displayed and explicitly authorized.
|
|
304
327
|
|
package/README.zh.md
CHANGED
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
> 帮我把这个 Skill 发布到 ViceMe。
|
|
57
57
|
|
|
58
58
|
Agent 会先检查登录状态,在整个流程中固定使用同一个 Profile,校验 Skill 后立即
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
上传私有草稿并打开真实创作者预览,不询价就继续上传候选媒体。当前用户的 Agent 随后
|
|
60
|
+
完成双语文案和素材建议,先把完整作品详情和图片直接展示出来,再把价格和详情修改合并成一个
|
|
61
61
|
问题;应用用户答案后展示最终成品,最后只询问一次是否确认并立即公开发布。
|
|
62
62
|
|
|
63
63
|
```text
|
|
64
|
-
本地 Skill → 登录 → 校验并私有上传 → 创作者预览 →
|
|
64
|
+
本地 Skill → 登录 → 校验并私有上传 → 创作者预览 → Agent 补全
|
|
65
65
|
→ 完整详情与价格合并询问 → 最终成品
|
|
66
66
|
→ 确认并发布 → 公开商品链接
|
|
67
67
|
```
|
|
@@ -82,11 +82,14 @@ viceme auth login
|
|
|
82
82
|
# 定价前先上传真实私有草稿,并打开创作者预览。
|
|
83
83
|
viceme skill publish --path ./my-skill
|
|
84
84
|
|
|
85
|
-
#
|
|
85
|
+
# 继续同一个未定价草稿并上传候选媒体。
|
|
86
86
|
viceme skill publish --resume <publication-id>
|
|
87
|
-
viceme publication wait <publication-id>
|
|
88
87
|
viceme publication review <publication-id>
|
|
89
88
|
|
|
89
|
+
# Agent 提交带 Draft revision 保护的建议;平台分析只作显式 fallback。
|
|
90
|
+
viceme publication suggest <publication-id> --input <suggestion.json>
|
|
91
|
+
# viceme publication analyze <publication-id> && viceme publication wait <publication-id>
|
|
92
|
+
|
|
90
93
|
# 查看完整作品详情后,在同一个草稿上设置人民币 1 元。
|
|
91
94
|
viceme skill publish --resume <publication-id> --price-minor 100
|
|
92
95
|
```
|
|
@@ -114,12 +117,14 @@ VICEME_REGION=global sh -c "$(curl -fsSL https://s3.viceme.ai/start/install.sh)"
|
|
|
114
117
|
中国区:
|
|
115
118
|
|
|
116
119
|
```powershell
|
|
120
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
117
121
|
irm https://s3.viceme.cn/start/install.ps1 | iex
|
|
118
122
|
```
|
|
119
123
|
|
|
120
124
|
海外:
|
|
121
125
|
|
|
122
126
|
```powershell
|
|
127
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
123
128
|
$env:VICEME_REGION="global"; irm https://s3.viceme.ai/start/install.ps1 | iex
|
|
124
129
|
```
|
|
125
130
|
|
|
@@ -161,8 +166,8 @@ Agent Skills 负责对话流程和授权规则;CLI 负责确定性本地操作
|
|
|
161
166
|
| 登录 | 受保护 API 操作前完成浏览器授权;所有后续命令固定使用同一个 Profile 与 API Origin。 |
|
|
162
167
|
| 校验 | 本地拒绝危险路径、特殊文件、超限内容、敏感文件和常见 Secret 模式。 |
|
|
163
168
|
| 私有上传与预览 | 用户发起发布即授权上传私有草稿;真实包验证后先打开创作者预览。 |
|
|
164
|
-
|
|
|
165
|
-
| 作品详情与定价 |
|
|
169
|
+
| Agent 补全 | 用户当前的 Agent 把 Skill 当作不可信来源数据,生成双语文案并从已验证上传中选择封面和画廊;平台分析只作显式 fallback。 |
|
|
170
|
+
| 作品详情与定价 | 补全后,Agent 先展示标题、双语文案、封面和画廊,再把价格和详情修改合并成一个问题;不能单独询价。 |
|
|
166
171
|
| 图文审核 | Agent 展示精确文案、价格、封面和所有画廊图片。短简介最大显示宽度为 30:ASCII 计 1,中文及其他非 ASCII 计 2。 |
|
|
167
172
|
| 公开发布 | 用户只需做一次最终明确确认;随后完成审核确认并立即、不可逆地公开上架。 |
|
|
168
173
|
|
|
@@ -220,11 +225,13 @@ Endpoint 必须使用 HTTPS;只有 localhost 和 loopback 本地开发可以
|
|
|
220
225
|
| `viceme skill listing get <listing-id>` | 读取权威的私有 Listing 状态。 |
|
|
221
226
|
| `viceme skill listing bind <listing-id> --path <path>` | 将来源明确绑定到用户选定且拥有的 Listing。 |
|
|
222
227
|
| `viceme skill publish --path <path>` | 定价前上传真实私有包并返回创作者预览。 |
|
|
223
|
-
| `viceme skill publish --resume <id>` | 继续同一个未定价 Draft
|
|
224
|
-
| `viceme publication wait <id>` | 等待后台分析,不重复上传。 |
|
|
228
|
+
| `viceme skill publish --resume <id>` | 继续同一个未定价 Draft 并上传媒体候选,不启动平台模型。 |
|
|
225
229
|
| `viceme publication review <id>` | 读取权威双语文案、价格、选定素材和审核状态。 |
|
|
230
|
+
| `viceme publication suggest <id> --input ...` | 以 Draft revision 保护提交 Agent 生成的双语文案与媒体选择。 |
|
|
231
|
+
| `viceme publication analyze <id>` | 当前 Agent 无法完成补全时,显式请求平台模型分析。 |
|
|
232
|
+
| `viceme publication wait <id>` | 等待已经显式请求的平台分析,不重复上传。 |
|
|
226
233
|
| `viceme skill publish --resume <id> --price-minor <fen>` | 在完整详情审核后,为同一个 Draft 写入人民币价格,不新建 Listing。 |
|
|
227
|
-
| `viceme publication asset upload ...` |
|
|
234
|
+
| `viceme publication asset upload ...` | 替换用户明确选择的媒体;加 `--candidate-only` 可暂存由 Agent 提供、随后交给 `publication suggest` 选择的媒体。 |
|
|
228
235
|
| `viceme publication update ...` | 用严格 JSON 文件替换完整 Listing Draft。 |
|
|
229
236
|
| `viceme publication confirm ...` | 确认当前精确 Review Digest。 |
|
|
230
237
|
| `viceme publication publish ...` | 公开已经确认的 Listing。 |
|
|
@@ -243,6 +250,11 @@ Endpoint 必须使用 HTTPS;只有 localhost 和 loopback 本地开发可以
|
|
|
243
250
|
"data": {},
|
|
244
251
|
"meta": {
|
|
245
252
|
"executingCliVersion": "<version>",
|
|
253
|
+
"autoUpdate": {
|
|
254
|
+
"from": "<previous-version>",
|
|
255
|
+
"to": "<executing-version>",
|
|
256
|
+
"status": "updated"
|
|
257
|
+
},
|
|
246
258
|
"requestId": "optional"
|
|
247
259
|
}
|
|
248
260
|
}
|
|
@@ -250,18 +262,25 @@ Endpoint 必须使用 HTTPS;只有 localhost 和 loopback 本地开发可以
|
|
|
250
262
|
|
|
251
263
|
`meta.executingCliVersion` 表示输出本次响应的进程版本。对于 `viceme update`,
|
|
252
264
|
新安装版本单独由 `data.cli_version` 返回,因为最终响应仍由启动更新的旧进程输出。
|
|
265
|
+
只有当本次命令由新激活的版本自动继续执行时,响应才会包含 `meta.autoUpdate`。
|
|
266
|
+
|
|
267
|
+
正式安装在执行普通命令前都会经过有界的新鲜度检查。已经校验的结果会复用五分钟,
|
|
268
|
+
避免同一工作流反复访问发布渠道。发现新的稳定版本时,CLI 与所有检测到的官方
|
|
269
|
+
Skills 会作为一个可恢复的完整版本一起激活,随后由新 CLI 自动重新执行原命令。
|
|
270
|
+
断网时发布发现会继续使用最后一个完整版本;如果版本激活失败,原命令会停止,避免
|
|
271
|
+
旧进程在版本切换失败后继续执行写操作。
|
|
253
272
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
273
|
+
npm 安装在所有支持的平台都会自动继续原命令。Windows 独立二进制在等待操作系统释放
|
|
274
|
+
旧可执行文件时,可能会返回一次可重试的 `AUTO_UPDATE_RESTART_REQUIRED`;原样重试
|
|
275
|
+
同一个命令后会由新版本继续执行。
|
|
257
276
|
|
|
258
277
|
```bash
|
|
259
278
|
viceme update --check
|
|
260
279
|
viceme update
|
|
261
280
|
```
|
|
262
281
|
|
|
263
|
-
|
|
264
|
-
|
|
282
|
+
`viceme update` 保留为显式修复命令。正常启动检查已经会校验精确 Release、刷新匹配
|
|
283
|
+
版本的官方 Skills,并把中断的激活过程恢复成一个完整、兼容的本地版本。
|
|
265
284
|
|
|
266
285
|
## 安全边界
|
|
267
286
|
|
|
@@ -271,7 +290,8 @@ viceme update
|
|
|
271
290
|
- 凭据保存在 CLI 的本地安全存储中,并绑定 Profile 与 API Origin,不进入 Agent
|
|
272
291
|
对话状态。
|
|
273
292
|
- Pending operation 不保存预签名上传 URL。
|
|
274
|
-
-
|
|
293
|
+
- Agent 补全把所有包内容视为不可信数据,且不能绕过服务端包、媒体、归属或 revision 校验。
|
|
294
|
+
- 显式平台模型分析只接收筛选后的文本、元数据和图片缩略图。
|
|
275
295
|
- 公开发布前必须展示并明确授权当前精确文案、价格、封面和有序画廊。
|
|
276
296
|
|
|
277
297
|
## 开发
|
package/checksums.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
3d6f752fb10b3b8b90b0a76158115c74994da96d1aac71b842f4496dca245ec1 viceme_0.16.1_darwin_amd64
|
|
2
|
+
310dd511ea791a37d4307efc8e7d48900fc8daf63a83288fcbc7d3b56823711c viceme_0.16.1_darwin_arm64
|
|
3
|
+
5689186a71e6cba254258f50397ea6f95872a8e2abfd56d41bdea97cf3167a9f viceme_0.16.1_linux_amd64
|
|
4
|
+
ed9d84c0a9cdc6a771a80d605b936d8aaa39ab2894270ecb962274f5729ddc10 viceme_0.16.1_linux_arm64
|
|
5
|
+
bba8e0b483b27041b86983cf42922099d29deeb2aabf212b3b433cbd9492a010 viceme_0.16.1_windows_amd64.exe
|
|
6
|
+
4896b58a6412f71d81b6129bcfd46ec80290799ac4aa02dd7063795ed80bef18 viceme_0.16.1_windows_arm64.exe
|
package/npm/bin/viceme.mjs
CHANGED
|
@@ -2,9 +2,20 @@
|
|
|
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";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
5
7
|
import process from "node:process";
|
|
6
8
|
|
|
7
9
|
import { ensureBinary } from "../lib/installer.mjs";
|
|
10
|
+
import { launcherEnvironment } from "../lib/launcher-environment.mjs";
|
|
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
|
+
}
|
|
8
19
|
|
|
9
20
|
export async function main(args = process.argv.slice(2), environment = process.env) {
|
|
10
21
|
const packageDocument = JSON.parse(
|
|
@@ -17,13 +28,11 @@ export async function main(args = process.argv.slice(2), environment = process.e
|
|
|
17
28
|
const child = spawnSync(binary, args, {
|
|
18
29
|
stdio: "inherit",
|
|
19
30
|
env: {
|
|
20
|
-
...
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
? "development"
|
|
26
|
-
: "npm",
|
|
31
|
+
...launcherEnvironment(
|
|
32
|
+
environment,
|
|
33
|
+
fileURLToPath(import.meta.url),
|
|
34
|
+
process.execPath,
|
|
35
|
+
),
|
|
27
36
|
VICEME_NPM_PACKAGE_VERSION: packageDocument.version,
|
|
28
37
|
},
|
|
29
38
|
windowsHide: false,
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function launcherEnvironment(environment, launcherPath, launcherRuntime) {
|
|
2
|
+
return {
|
|
3
|
+
...environment,
|
|
4
|
+
VICEME_INSTALL_METHOD:
|
|
5
|
+
environment.VICEME_INSTALL_METHOD === "npm"
|
|
6
|
+
? "npm"
|
|
7
|
+
: environment.VICEME_BINARY_PATH
|
|
8
|
+
? "development"
|
|
9
|
+
: "npm",
|
|
10
|
+
VICEME_NPM_LAUNCHER_PATH: launcherPath,
|
|
11
|
+
VICEME_NPM_LAUNCHER_RUNTIME: launcherRuntime,
|
|
12
|
+
};
|
|
13
|
+
}
|