@ljwei-stak/dsh-model-router 0.16.4 → 0.17.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.
@@ -1,16 +1,77 @@
1
1
  /**
2
2
  * Fixed registry of official model CLI tools, shared by the Host runtime
3
- * (probe + install) and the Desktop panel (display). Pure data: no Node or
4
- * browser APIs, so both sides and the test suite import it unchanged.
3
+ * (probe + install + uninstall + repair) and the Desktop panel (display).
4
+ * Pure data: no Node or browser APIs, so both sides and the test suite import
5
+ * it unchanged.
5
6
  *
6
7
  * The registry is fail-closed by design: installer sources are fixed (the
7
- * official npm registry, the vendor's signed installer) and clients can only
8
- * name a registry id — never an arbitrary package or command. Versions are not
9
- * pinned: installs take each vendor's latest release (`@latest`), and the
10
- * executor trusts entries by publisher signature or registry-attested digest
11
- * rather than by version.
8
+ * official npm registry, the vendor's published installer script) and clients
9
+ * can only name a registry id and one of that tool's declared install methods
10
+ * — never an arbitrary package, URL or command. Versions are not pinned:
11
+ * installs take each vendor's latest release (`@latest`, the vendor script's
12
+ * own `latest`), and the executor trusts entries by publisher signature or
13
+ * registry-attested digest rather than by version.
14
+ *
15
+ * Every tool declares `installMethods`: the complete, closed list of ways this
16
+ * plugin may obtain the tool. `manager` / `installArgs` / `uninstallArgs` on the
17
+ * tool itself stay the default (first) method, so existing callers and tests
18
+ * keep working. Script methods are never piped straight into an interpreter:
19
+ * the runtime downloads the vendor script, checks it still is that script, and
20
+ * runs the staged copy (same pattern as the MiniMax Windows installer).
12
21
  */
13
22
 
23
+ /** npm registry every fixed npm method installs from unless the user overrides it. */
24
+ export const DEFAULT_NPM_REGISTRY = 'https://registry.npmjs.org/'
25
+
26
+ /** Shared shape of a "package manager" method: argv only, no script staging. */
27
+ function packageManagerMethod(id, label, manager, spec, { registryFlag = '--registry=' } = {}) {
28
+ return Object.freeze({
29
+ id,
30
+ label,
31
+ kind: 'package-manager',
32
+ manager,
33
+ spec,
34
+ registryFlag,
35
+ installArgs: Object.freeze(['install', '-g', spec]),
36
+ uninstallArgs: Object.freeze(['uninstall', '-g', spec.replace(/@latest$/, '')]),
37
+ /** npm/pnpm both accept --prefix to place a global install off the default. */
38
+ supportsInstallDir: true,
39
+ supportsRegistry: true,
40
+ })
41
+ }
42
+
43
+ /**
44
+ * Vendor installer script run from a verified local copy.
45
+ * A directory is passed either as `installArg` (`--install-dir`) or as
46
+ * `installDirEnv` (`MCODE_INSTALL_DIR`, `CODEX_INSTALL_DIR`, …). Scripts that
47
+ * hardcode their own path set neither and `supportsInstallDir` stays false.
48
+ */
49
+ function scriptMethod(id, label, {
50
+ shell, scriptUrl, verify, installArg = [], env = [], platforms,
51
+ installDirEnv = '', defaultInstallDir = '',
52
+ }) {
53
+ return Object.freeze({
54
+ id,
55
+ label,
56
+ kind: 'script',
57
+ shell,
58
+ scriptUrl,
59
+ verify,
60
+ /** Passed after the staged script path; empty means the script takes no argument. */
61
+ installArgs: Object.freeze(installArg),
62
+ /** Environment the vendor script itself documents for this install. */
63
+ env: Object.freeze(env),
64
+ /** When set, the configured directory is this environment variable, not an argv flag. */
65
+ installDirEnv,
66
+ /** Home-relative directory the vendor uses when the user sets none. Slashes only. */
67
+ defaultInstallDir,
68
+ platforms: Object.freeze(platforms),
69
+ /** Whether the vendor script honours a caller-chosen directory at all. */
70
+ supportsInstallDir: Boolean(installDirEnv) || installArg.length > 0,
71
+ supportsRegistry: false,
72
+ })
73
+ }
74
+
14
75
  export const OFFICIAL_TOOLS = Object.freeze([
15
76
  Object.freeze({
16
77
  id: 'kimi-code',
@@ -20,9 +81,41 @@ export const OFFICIAL_TOOLS = Object.freeze([
20
81
  package: '@moonshot-ai/kimi-code',
21
82
  manager: 'npm',
22
83
  installArgs: ['install', '-g', '@moonshot-ai/kimi-code@latest', '--registry=https://registry.npmjs.org/'],
84
+ uninstallArgs: ['uninstall', '-g', '@moonshot-ai/kimi-code'],
23
85
  probeExecutables: ['kimi'],
24
86
  probeNote: 'kimi 与旧 Python 版 kimi-cli 同名;请核对可执行文件来源和版本。',
25
87
  providerHints: ['moonshot', 'kimi'],
88
+ scriptBinaryNames: Object.freeze(['kimi', 'kimi.exe']),
89
+ scriptBinaryRelatives: Object.freeze(['bin/kimi', 'bin/kimi.exe']),
90
+ defaultScriptInstallDir: '.kimi-code',
91
+ installMethods: Object.freeze([
92
+ packageManagerMethod('npm', 'npm', 'npm', '@moonshot-ai/kimi-code@latest'),
93
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@moonshot-ai/kimi-code@latest'),
94
+ scriptMethod('script-bash', 'curl | bash', {
95
+ shell: 'bash',
96
+ scriptUrl: 'https://code.kimi.com/kimi-code/install.sh',
97
+ platforms: ['linux', 'darwin'],
98
+ installDirEnv: 'KIMI_INSTALL_DIR',
99
+ defaultInstallDir: '.kimi-code',
100
+ env: ['KIMI_INSTALL_DIR'],
101
+ verify: Object.freeze({
102
+ mustInclude: ['code.kimi.com/kimi-code', 'KIMI_INSTALL_DIR'],
103
+ mustNotInclude: ['downloads.claude.ai/claude-code-releases', '@minimax-ai/code'],
104
+ }),
105
+ }),
106
+ scriptMethod('script-powershell', 'irm | iex', {
107
+ shell: 'powershell',
108
+ scriptUrl: 'https://code.kimi.com/kimi-code/install.ps1',
109
+ platforms: ['win32'],
110
+ installDirEnv: 'KIMI_INSTALL_DIR',
111
+ defaultInstallDir: '.kimi-code',
112
+ env: ['KIMI_INSTALL_DIR'],
113
+ verify: Object.freeze({
114
+ mustInclude: ['code.kimi.com/kimi-code', 'KIMI_INSTALL_DIR'],
115
+ mustNotInclude: ['downloads.claude.ai/claude-code-releases', '@minimax-ai/code'],
116
+ }),
117
+ }),
118
+ ]),
26
119
  }),
27
120
  Object.freeze({
28
121
  id: 'claude-code',
@@ -32,8 +125,36 @@ export const OFFICIAL_TOOLS = Object.freeze([
32
125
  package: '@anthropic-ai/claude-code',
33
126
  manager: 'npm',
34
127
  installArgs: ['install', '-g', '@anthropic-ai/claude-code@latest', '--registry=https://registry.npmjs.org/'],
128
+ uninstallArgs: ['uninstall', '-g', '@anthropic-ai/claude-code'],
35
129
  probeExecutables: ['claude'],
36
130
  providerHints: ['anthropic', 'claude'],
131
+ // The native installer always lands in ~/.local/bin; it has no directory flag.
132
+ scriptBinaryNames: Object.freeze(['claude', 'claude.exe']),
133
+ defaultScriptInstallDir: '.local/bin',
134
+ installMethods: Object.freeze([
135
+ packageManagerMethod('npm', 'npm', 'npm', '@anthropic-ai/claude-code@latest'),
136
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@anthropic-ai/claude-code@latest'),
137
+ scriptMethod('script-bash', 'curl | bash', {
138
+ shell: 'bash',
139
+ scriptUrl: 'https://claude.ai/install.sh',
140
+ platforms: ['linux', 'darwin'],
141
+ defaultInstallDir: '.local/bin',
142
+ verify: Object.freeze({
143
+ mustInclude: ['downloads.claude.ai/claude-code-releases', 'claude.ai/install.sh'],
144
+ mustNotInclude: ['@minimax-ai/code', 'releases.openai.com/codex'],
145
+ }),
146
+ }),
147
+ scriptMethod('script-powershell', 'irm | iex', {
148
+ shell: 'powershell',
149
+ scriptUrl: 'https://claude.ai/install.ps1',
150
+ platforms: ['win32'],
151
+ defaultInstallDir: '.local/bin',
152
+ verify: Object.freeze({
153
+ mustInclude: ['downloads.claude.ai/claude-code-releases'],
154
+ mustNotInclude: ['@minimax-ai/code', 'releases.openai.com/codex'],
155
+ }),
156
+ }),
157
+ ]),
37
158
  }),
38
159
  Object.freeze({
39
160
  id: 'codex',
@@ -41,11 +162,42 @@ export const OFFICIAL_TOOLS = Object.freeze([
41
162
  vendor: 'OpenAI',
42
163
  purpose: 'OpenAI 官方编程 CLI,提供 codex 命令。',
43
164
  package: '@openai/codex',
44
- installArgs: ['install', '-g', '@openai/codex@latest', '--registry=https://registry.npmjs.org/'],
45
165
  manager: 'npm',
166
+ installArgs: ['install', '-g', '@openai/codex@latest', '--registry=https://registry.npmjs.org/'],
167
+ uninstallArgs: ['uninstall', '-g', '@openai/codex'],
46
168
  probeExecutables: ['codex'],
47
169
  probeNote: 'Codex 版本横幅由适配层宽匹配;安装时取 npm 最新版。',
48
170
  providerHints: ['openai', 'gpt', 'codex'],
171
+ scriptBinaryNames: Object.freeze(['codex', 'codex.exe']),
172
+ installMethods: Object.freeze([
173
+ packageManagerMethod('npm', 'npm', 'npm', '@openai/codex@latest'),
174
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@openai/codex@latest'),
175
+ scriptMethod('script-bash', 'curl | bash', {
176
+ shell: 'bash',
177
+ scriptUrl: 'https://chatgpt.com/codex/install.sh',
178
+ platforms: ['linux', 'darwin'],
179
+ installDirEnv: 'CODEX_INSTALL_DIR',
180
+ defaultInstallDir: '.local/bin',
181
+ env: ['CODEX_INSTALL_DIR'],
182
+ verify: Object.freeze({
183
+ mustInclude: ['releases.openai.com/codex', 'CODEX_INSTALL_DIR'],
184
+ mustNotInclude: ['downloads.claude.ai/claude-code-releases', '@minimax-ai/code'],
185
+ }),
186
+ }),
187
+ scriptMethod('script-powershell', 'irm | iex', {
188
+ shell: 'powershell',
189
+ scriptUrl: 'https://chatgpt.com/codex/install.ps1',
190
+ platforms: ['win32'],
191
+ installDirEnv: 'CODEX_INSTALL_DIR',
192
+ // %LOCALAPPDATA%\Programs\OpenAI\Codex\bin under a normal user profile.
193
+ defaultInstallDir: 'AppData/Local/Programs/OpenAI/Codex/bin',
194
+ env: ['CODEX_INSTALL_DIR'],
195
+ verify: Object.freeze({
196
+ mustInclude: ['releases.openai.com/codex', 'CODEX_INSTALL_DIR'],
197
+ mustNotInclude: ['downloads.claude.ai/claude-code-releases', '@minimax-ai/code'],
198
+ }),
199
+ }),
200
+ ]),
49
201
  }),
50
202
  Object.freeze({
51
203
  id: 'minimax-code',
@@ -55,8 +207,46 @@ export const OFFICIAL_TOOLS = Object.freeze([
55
207
  package: '@minimax-ai/code',
56
208
  manager: 'npm',
57
209
  installArgs: ['install', '-g', '@minimax-ai/code@latest', '--registry=https://registry.npmjs.org/', '--ignore-scripts=false', '--include=optional', '--allow-scripts=@minimax-ai/code,better-sqlite3'],
210
+ uninstallArgs: ['uninstall', '-g', '@minimax-ai/code'],
58
211
  probeExecutables: ['mcode'],
59
212
  providerHints: ['minimax'],
213
+ scriptBinaryNames: Object.freeze([
214
+ 'mcode', 'mcode.exe', 'mcode.cmd', 'mcode.ps1',
215
+ 'mcode-tools', 'mcode-tools.cmd', 'mcode-tools.ps1',
216
+ ]),
217
+ scriptBinaryRelatives: Object.freeze([
218
+ 'mcode.cmd', 'mcode.ps1', 'mcode-tools.cmd', 'mcode-tools.ps1',
219
+ 'bin/mcode', 'bin/mcode-tools',
220
+ ]),
221
+ defaultScriptInstallDir: '.minimax-code',
222
+ installMethods: Object.freeze([
223
+ packageManagerMethod('npm', 'npm', 'npm', '@minimax-ai/code@latest'),
224
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@minimax-ai/code@latest'),
225
+ scriptMethod('script-bash', 'curl | bash', {
226
+ shell: 'bash',
227
+ scriptUrl: 'https://filecdn.minimax.chat/public/install.sh',
228
+ platforms: ['linux', 'darwin'],
229
+ installDirEnv: 'MCODE_INSTALL_DIR',
230
+ defaultInstallDir: '.minimax-code',
231
+ env: ['MCODE_INSTALL_DIR', 'MCODE_NPM_REGISTRY'],
232
+ verify: Object.freeze({
233
+ mustInclude: ['@minimax-ai/code', 'MCODE_INSTALL_DIR', 'MiniMax Code'],
234
+ mustNotInclude: ['downloads.claude.ai/claude-code-releases', 'releases.openai.com/codex'],
235
+ }),
236
+ }),
237
+ scriptMethod('script-powershell', 'irm | iex', {
238
+ shell: 'powershell',
239
+ scriptUrl: 'https://filecdn.minimax.chat/public/install.ps1',
240
+ platforms: ['win32'],
241
+ installDirEnv: 'MCODE_INSTALL_DIR',
242
+ defaultInstallDir: '.minimax-code',
243
+ env: ['MCODE_INSTALL_DIR', 'MCODE_NPM_REGISTRY'],
244
+ verify: Object.freeze({
245
+ mustInclude: ['@minimax-ai/code', 'MCODE_INSTALL_DIR', 'MiniMax Code'],
246
+ mustNotInclude: ['downloads.claude.ai/claude-code-releases', 'releases.openai.com/codex'],
247
+ }),
248
+ }),
249
+ ]),
60
250
  }),
61
251
  Object.freeze({
62
252
  id: 'mimo-code',
@@ -66,8 +256,38 @@ export const OFFICIAL_TOOLS = Object.freeze([
66
256
  package: '@mimo-ai/cli',
67
257
  manager: 'npm',
68
258
  installArgs: ['install', '-g', '@mimo-ai/cli@latest', '--registry=https://registry.npmjs.org/'],
259
+ uninstallArgs: ['uninstall', '-g', '@mimo-ai/cli'],
69
260
  probeExecutables: ['mimo'],
70
261
  providerHints: ['mimo', 'xiaomi'],
262
+ scriptBinaryNames: Object.freeze(['mimo', 'mimo.exe']),
263
+ defaultScriptInstallDir: '.mimocode/bin',
264
+ installMethods: Object.freeze([
265
+ packageManagerMethod('npm', 'npm', 'npm', '@mimo-ai/cli@latest'),
266
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@mimo-ai/cli@latest'),
267
+ // The Unix script hardcodes ~/.mimocode/bin and ignores MIMOCODE_INSTALL_DIR.
268
+ scriptMethod('script-bash', 'curl | bash', {
269
+ shell: 'bash',
270
+ scriptUrl: 'https://mimo.xiaomi.com/install',
271
+ platforms: ['linux', 'darwin'],
272
+ defaultInstallDir: '.mimocode/bin',
273
+ verify: Object.freeze({
274
+ mustInclude: ['MiMoCode', 'mimo.xiaomi.com'],
275
+ mustNotInclude: ['@minimax-ai/code', 'downloads.claude.ai/claude-code-releases'],
276
+ }),
277
+ }),
278
+ scriptMethod('script-powershell', 'irm | iex', {
279
+ shell: 'powershell',
280
+ scriptUrl: 'https://mimo.xiaomi.com/install.ps1',
281
+ platforms: ['win32'],
282
+ installDirEnv: 'MIMOCODE_INSTALL_DIR',
283
+ defaultInstallDir: '.mimocode/bin',
284
+ env: ['MIMOCODE_INSTALL_DIR'],
285
+ verify: Object.freeze({
286
+ mustInclude: ['MiMoCode', 'mimo.xiaomi.com', 'MIMOCODE_INSTALL_DIR'],
287
+ mustNotInclude: ['@minimax-ai/code', 'downloads.claude.ai/claude-code-releases'],
288
+ }),
289
+ }),
290
+ ]),
71
291
  }),
72
292
  Object.freeze({
73
293
  id: 'grok-build',
@@ -77,8 +297,39 @@ export const OFFICIAL_TOOLS = Object.freeze([
77
297
  package: '@xai-official/grok',
78
298
  manager: 'npm',
79
299
  installArgs: ['install', '-g', '@xai-official/grok@latest', '--registry=https://registry.npmjs.org/'],
300
+ uninstallArgs: ['uninstall', '-g', '@xai-official/grok'],
80
301
  probeExecutables: ['grok'],
81
302
  providerHints: ['xai', 'grok'],
303
+ scriptBinaryNames: Object.freeze(['grok', 'grok.exe', 'agent', 'agent.exe']),
304
+ defaultScriptInstallDir: '.grok/bin',
305
+ installMethods: Object.freeze([
306
+ packageManagerMethod('npm', 'npm', 'npm', '@xai-official/grok@latest'),
307
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@xai-official/grok@latest'),
308
+ scriptMethod('script-bash', 'curl | bash', {
309
+ shell: 'bash',
310
+ scriptUrl: 'https://x.ai/cli/install.sh',
311
+ platforms: ['linux', 'darwin'],
312
+ installDirEnv: 'GROK_BIN_DIR',
313
+ defaultInstallDir: '.grok/bin',
314
+ env: ['GROK_BIN_DIR'],
315
+ verify: Object.freeze({
316
+ mustInclude: ['x.ai/cli', 'GROK_BIN_DIR'],
317
+ mustNotInclude: ['@minimax-ai/code', 'downloads.claude.ai/claude-code-releases'],
318
+ }),
319
+ }),
320
+ scriptMethod('script-powershell', 'irm | iex', {
321
+ shell: 'powershell',
322
+ scriptUrl: 'https://x.ai/cli/install.ps1',
323
+ platforms: ['win32'],
324
+ installDirEnv: 'GROK_BIN_DIR',
325
+ defaultInstallDir: '.grok/bin',
326
+ env: ['GROK_BIN_DIR'],
327
+ verify: Object.freeze({
328
+ mustInclude: ['x.ai/cli', 'GROK_BIN_DIR'],
329
+ mustNotInclude: ['@minimax-ai/code', 'downloads.claude.ai/claude-code-releases'],
330
+ }),
331
+ }),
332
+ ]),
82
333
  }),
83
334
  Object.freeze({
84
335
  id: 'gemini',
@@ -88,11 +339,91 @@ export const OFFICIAL_TOOLS = Object.freeze([
88
339
  package: '@google/gemini-cli',
89
340
  manager: 'npm',
90
341
  installArgs: ['install', '-g', '@google/gemini-cli@latest', '--registry=https://registry.npmjs.org/'],
342
+ uninstallArgs: ['uninstall', '-g', '@google/gemini-cli'],
91
343
  probeExecutables: ['gemini'],
92
344
  providerHints: ['gemini', 'google'],
93
345
  // Headless runs go through the task adapter. The signed sandbox runner does
94
346
  // not launch this CLI; a missing or failed process falls back to the API.
95
347
  headlessAdapter: true,
348
+ installMethods: Object.freeze([
349
+ packageManagerMethod('npm', 'npm', 'npm', '@google/gemini-cli@latest'),
350
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', '@google/gemini-cli@latest'),
351
+ ]),
352
+ }),
353
+ Object.freeze({
354
+ id: 'opencode',
355
+ label: 'OpenCode',
356
+ vendor: 'anomalyco / SST',
357
+ purpose: '开源终端编程代理,提供 opencode 命令;可用 npm/pnpm 或官方安装脚本获取。',
358
+ package: 'opencode-ai',
359
+ manager: 'npm',
360
+ installArgs: ['install', '-g', 'opencode-ai@latest', '--registry=https://registry.npmjs.org/'],
361
+ uninstallArgs: ['uninstall', '-g', 'opencode-ai'],
362
+ probeExecutables: ['opencode'],
363
+ probeNote: 'opencode 命令由 npm 全局包或官方脚本安装的二进制提供;脚本安装固定落在 ~/.opencode/bin。',
364
+ providerHints: ['opencode', 'anomalyco'],
365
+ /** File an uninstall may delete, when the copy came from a script install. */
366
+ scriptBinaryNames: Object.freeze(['opencode', 'opencode.exe']),
367
+ /** Where the vendor script puts the binary when the user sets no directory. */
368
+ defaultScriptInstallDir: '.opencode/bin',
369
+ installMethods: Object.freeze([
370
+ packageManagerMethod('npm', 'npm', 'npm', 'opencode-ai@latest'),
371
+ packageManagerMethod('pnpm', 'pnpm', 'pnpm', 'opencode-ai@latest'),
372
+ // The published script hardcodes $HOME/.opencode/bin, so it cannot honour
373
+ // a custom install directory; the npm/pnpm methods above can.
374
+ scriptMethod('script-bash', 'curl | bash', {
375
+ shell: 'bash',
376
+ scriptUrl: 'https://opencode.ai/install',
377
+ platforms: ['linux', 'darwin'],
378
+ defaultInstallDir: '.opencode/bin',
379
+ verify: Object.freeze({
380
+ mustInclude: ['anomalyco/opencode', 'opencode.ai'],
381
+ mustNotInclude: ['static-openapi.stepfun.com'],
382
+ }),
383
+ }),
384
+ ]),
385
+ }),
386
+ Object.freeze({
387
+ id: 'stepcode',
388
+ label: 'Step Code',
389
+ vendor: 'StepFun 阶跃星辰',
390
+ purpose: '阶跃星辰开源终端编程代理,提供 step 命令;官方只发布安装脚本,没有 npm 包。',
391
+ manager: 'script-installer',
392
+ installArgs: [],
393
+ uninstallArgs: [],
394
+ probeExecutables: ['step'],
395
+ probeNote: 'step 命令来自阶跃官方安装脚本,校验和写入 ~/.stepcode;配置与凭据同在该目录,卸载只删除可执行文件。',
396
+ providerHints: ['stepfun', 'stepcode'],
397
+ scriptBinaryNames: Object.freeze(['step', 'step.exe']),
398
+ defaultScriptInstallDir: '.stepcode/bin',
399
+ /** The vendor installer writes this marked block into the shell profile. */
400
+ pathProfileMarkers: Object.freeze(['# stepcode']),
401
+ installMethods: Object.freeze([
402
+ scriptMethod('script-bash', 'curl | bash', {
403
+ shell: 'bash',
404
+ scriptUrl: 'https://static-openapi.stepfun.com/stepcode/install.sh',
405
+ platforms: ['linux', 'darwin'],
406
+ installArg: ['--install-dir'],
407
+ env: ['STEP_INSTALL_DIR', 'STEP_RELEASE_BASE_URL'],
408
+ defaultInstallDir: '.stepcode/bin',
409
+ verify: Object.freeze({
410
+ mustInclude: ['static-openapi.stepfun.com', 'stepcode installer'],
411
+ mustNotInclude: ['anomalyco/opencode'],
412
+ }),
413
+ }),
414
+ scriptMethod('script-powershell', 'irm | iex', {
415
+ shell: 'powershell',
416
+ scriptUrl: 'https://static-openapi.stepfun.com/stepcode/install.ps1',
417
+ platforms: ['win32'],
418
+ installArg: ['-InstallDir'],
419
+ env: ['STEP_INSTALL_DIR', 'STEP_RELEASE_BASE_URL'],
420
+ defaultInstallDir: '.stepcode/bin',
421
+ verify: Object.freeze({
422
+ mustInclude: ['static-openapi.stepfun.com', 'stepcode'],
423
+ mustNotInclude: ['anomalyco/opencode'],
424
+ }),
425
+ }),
426
+ ]),
96
427
  }),
97
428
  Object.freeze({
98
429
  id: 'zcode',
@@ -101,6 +432,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
101
432
  purpose: '智谱官方 ZCode 桌面版,内含 GLM 编程代理。Windows 安装器可选择 D 盘目录。',
102
433
  manager: 'signed-windows-installer',
103
434
  installArgs: [],
435
+ uninstallArgs: [],
104
436
  probeExecutables: [],
105
437
  probeNote: '检测经过有效签名的 ZCode.exe 和同目录 GLM 资源;桌面安装器需人工选择安装位置。',
106
438
  providerHints: ['zai', 'z.ai', 'zcode', 'glm', 'zhipu', 'bigmodel'],
@@ -123,11 +455,60 @@ export function toolForProvider(provider) {
123
455
  return null
124
456
  }
125
457
 
126
- /** Human-readable install command line for one registry entry. */
458
+ /** Every install method a tool declares, or an empty list for the desktop installer. */
459
+ export function installMethodsFor(tool) {
460
+ return Array.isArray(tool?.installMethods) ? tool.installMethods : []
461
+ }
462
+
463
+ /** One closed method id of one tool; null when the tool does not offer it. */
464
+ export function installMethodFor(tool, methodId) {
465
+ const wanted = String(methodId ?? '').trim()
466
+ if (!wanted) return null
467
+ return installMethodsFor(tool).find(method => method.id === wanted) ?? null
468
+ }
469
+
470
+ /** The tool's default method, kept identical to `manager` / `installArgs`. */
471
+ export function defaultInstallMethod(tool) {
472
+ return installMethodsFor(tool)[0] ?? null
473
+ }
474
+
475
+ /** Why a method cannot run here, or null when the current platform can run it. */
476
+ export function installMethodBlockReason(method, platform = 'linux') {
477
+ if (!method?.platforms) return null
478
+ return method.platforms.includes(platform) ? null : `此安装方式仅支持 ${method.platforms.join(' / ')}。`
479
+ }
480
+
481
+ /**
482
+ * A method able to place the tool in the user's chosen directory. npm and pnpm
483
+ * take --prefix; the opencode script hardcodes its own path and cannot.
484
+ */
485
+ export function methodHonoursInstallDir(method) {
486
+ return method?.supportsInstallDir === true
487
+ }
488
+
489
+ /** Human-readable install command line for one registry entry (default method). */
127
490
  export function installCommandLine(tool) {
128
491
  if (!tool || tool.unsupported) return null
492
+ // The signed desktop installer is not a command; it is its own thing.
129
493
  if (tool.manager === 'signed-windows-installer') return '打开官方签名安装器(选择安装目录)'
130
- return tool.manager === 'npm'
131
- ? `npm ${tool.installArgs.join(' ')}`
132
- : `${tool.manager} ${tool.installArgs.join(' ')}`
494
+ return installCommandLineFor(tool, defaultInstallMethod(tool))
495
+ }
496
+
497
+ /** Human-readable command line for one specific method of one tool. */
498
+ export function installCommandLineFor(tool, method) {
499
+ if (!tool || tool.unsupported) return null
500
+ if (!method) return null
501
+ if (method.kind === 'script') {
502
+ const piped = method.shell === 'powershell' ? `irm ${method.scriptUrl} | iex` : `curl -fsSL ${method.scriptUrl} | bash`
503
+ return method.installArgs.length ? `${piped} ${method.installArgs[0]} <安装目录>` : piped
504
+ }
505
+ return `${method.manager} ${method.installArgs.join(' ')}`
506
+ }
507
+
508
+ /** Human-readable uninstall command line, or null when removal is not a command. */
509
+ export function uninstallCommandLineFor(tool, method) {
510
+ if (!tool || tool.unsupported || !method) return null
511
+ if (tool.manager === 'signed-windows-installer') return null
512
+ if (method.kind === 'script') return '删除官方安装的可执行文件(保留配置与登录信息)'
513
+ return `${method.manager} ${method.uninstallArgs.join(' ')}`
133
514
  }
@@ -5,7 +5,7 @@
5
5
  * the Host Typert registry owns the matching strict descriptors. No endpoint
6
6
  * accepts a command, package name, URL, argument vector, or executable path.
7
7
  */
8
- import { getOfficialTool } from './official-tool-registry.mjs'
8
+ import { getOfficialTool, installMethodsFor } from './official-tool-registry.mjs'
9
9
  import {
10
10
  parseTerminalRead,
11
11
  parseTerminalResize,
@@ -35,6 +35,40 @@ const toolIdCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#OfficialToolId
35
35
  return value
36
36
  })
37
37
 
38
+ const MAX_METHOD_ID_CHARS = 40
39
+
40
+ const text = value => (typeof value === 'string' ? value.trim() : '')
41
+
42
+ /**
43
+ * One install/uninstall request: a registry id plus, optionally, which of that
44
+ * tool's declared install methods to use. The install directory, the npm source
45
+ * and the installer-script source are Host settings and never travel on this
46
+ * call, so a compromised client cannot redirect an install at a package or URL
47
+ * of its choosing.
48
+ */
49
+ const toolActionCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#OfficialToolAction`, value => {
50
+ const request = plainObject(value, 'tool action request')
51
+ // Closed shape: an unknown key is refused rather than ignored, so a client
52
+ // cannot smuggle a command, URL or argument list past the codec.
53
+ for (const key of Object.keys(request)) {
54
+ if (key !== 'tool' && key !== 'method') throw new TypeError(`unknown field ${key} in tool action request`)
55
+ }
56
+ const tool = getOfficialTool(text(request.tool))
57
+ if (!tool) throw new TypeError('tool must name a fixed official tool')
58
+ const method = request.method === undefined || request.method === null || request.method === ''
59
+ ? null
60
+ : text(request.method)
61
+ if (method !== null) {
62
+ if (method.length > MAX_METHOD_ID_CHARS || !/^[A-Za-z0-9-]+$/.test(method)) {
63
+ throw new TypeError('method must be one of the tool\'s declared install methods')
64
+ }
65
+ if (!installMethodsFor(tool).some(item => item.id === method)) {
66
+ throw new TypeError(`${tool.label} does not offer the install method ${method}`)
67
+ }
68
+ }
69
+ return method === null ? { tool: tool.id } : { tool: tool.id, method }
70
+ })
71
+
38
72
  const listResultCodec = strictCodec(`${OFFICIAL_TOOLS_REMOTE_PACKAGE}#OfficialToolList`, value => {
39
73
  const result = plainObject(value, 'tool list result')
40
74
  if (!Array.isArray(result.tools)) throw new TypeError('tool list result needs a tools array')
@@ -159,7 +193,9 @@ const jsonParameter = (name, codec) => Object.freeze({ name, wire: name, source:
159
193
 
160
194
  export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
161
195
  descriptor('list', [], listResultCodec),
162
- descriptor('installTool', [toolIdParameter], installResultCodec),
196
+ descriptor('installTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
197
+ descriptor('uninstallTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
198
+ descriptor('repairTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
163
199
  descriptor('cancel', [toolIdParameter], installResultCodec),
164
200
  descriptor('status', [toolIdParameter], statusResultCodec),
165
201
  // Workbench: onboarding health check, run ledger, ratings, step retry, security boundaries.