@ljwei-stak/dsh-model-router 0.17.1 → 0.18.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.
@@ -1434,11 +1434,25 @@ export function routerRemoteServices(ctx, config, { terminals = null } = {}) {
1434
1434
  /** Register model-facing tools and the human /router command. */
1435
1435
  export function apply(ctx, config = {}) {
1436
1436
  // Install addresses (directory, npm source, per-tool method and script source)
1437
- // live in volatile config, so re-apply picks up whatever the user last saved.
1438
- // A malformed value is reported here rather than at the first click.
1437
+ // are volatile refs: Loader changes them without re-applying the plugin.
1438
+ // Read them after the owning fiber's live update, before a saved view is sent.
1439
1439
  let installPreferenceError = null
1440
- try { setInstallPreferences(parseInstallPreferences(config, { tools: OFFICIAL_TOOLS })) }
1441
- catch (error) { installPreferenceError = errorText(error) }
1440
+ const syncInstallPreferences = () => {
1441
+ try {
1442
+ const next = parseInstallPreferences(config, { tools: OFFICIAL_TOOLS, ...currentInstallOptions() })
1443
+ setInstallPreferences(next)
1444
+ installPreferenceError = null
1445
+ } catch (error) { installPreferenceError = errorText(error) }
1446
+ }
1447
+ const installFields = new Set(['toolInstallDir', 'toolNpmRegistry', 'toolInstallMethodsJson', 'toolScriptUrlsJson', 'toolAllowScriptInstall'])
1448
+ syncInstallPreferences()
1449
+ // Loader scopes this event to the fiber whose refs were committed.
1450
+ ctx.on('loader/volatile-update', paths => {
1451
+ if (Array.isArray(paths) && paths.some(path => installFields.has(path?.[0]))) syncInstallPreferences()
1452
+ })
1453
+ const assertInstallPreferences = () => {
1454
+ if (installPreferenceError) throw new Error(`官方工具安装设置无效:${installPreferenceError}`)
1455
+ }
1442
1456
  // The official Host injects typert; direct lightweight uses of apply may
1443
1457
  // supply only the model/command services and do not expose the Desktop RPC.
1444
1458
  if (ctx.typert) {
@@ -1449,7 +1463,11 @@ export function apply(ctx, config = {}) {
1449
1463
  process.once('exit', onExit)
1450
1464
  return () => { process.off('exit', onExit); terminals.disposeAll('dispose') }
1451
1465
  }, 'model-router-galgame: interactive terminals')
1452
- registerOfficialToolsRemote(ctx, routerRemoteServices(ctx, config, { terminals }))
1466
+ registerOfficialToolsRemote(ctx, {
1467
+ ...routerRemoteServices(ctx, config, { terminals }),
1468
+ installSettingsError: () => installPreferenceError,
1469
+ installRequestedDirectory: () => valueOf(config, 'toolInstallDir', ''),
1470
+ })
1453
1471
  }
1454
1472
  ctx.on('tools/pre-execute', async (exec, next) => {
1455
1473
  const decision = await next()
@@ -1498,7 +1516,7 @@ export function apply(ctx, config = {}) {
1498
1516
  if (reasons.length) return combinedAsk(reasons)
1499
1517
  return decision
1500
1518
  })
1501
- registerOfficialToolModels(ctx, config)
1519
+ registerOfficialToolModels(ctx, config, assertInstallPreferences)
1502
1520
  ctx.tools.register(defineTool({
1503
1521
  name: 'model_router_routes',
1504
1522
  description: 'List provider/model routes registered in the official DeepSeek Harness model directory. Credential and network availability are not verified. No API keys or endpoints are returned.',
@@ -1611,6 +1629,7 @@ export function apply(ctx, config = {}) {
1611
1629
  const toolId = matched[2]
1612
1630
  const method = matched[3]
1613
1631
  try {
1632
+ assertInstallPreferences()
1614
1633
  const job = action === 'uninstall' ? startUninstall(toolId, { method })
1615
1634
  : action === 'repair' ? startRepair(toolId, { method })
1616
1635
  : startInstall(toolId, { method })
@@ -1649,7 +1668,7 @@ async function waitForInstall(toolId, signal) {
1649
1668
  }
1650
1669
 
1651
1670
  /** Register the model-facing official-tool probes and installer. */
1652
- function registerOfficialToolModels(ctx, config) {
1671
+ function registerOfficialToolModels(ctx, config, assertInstallPreferences) {
1653
1672
  ctx.tools.register(defineTool({
1654
1673
  name: 'model_router_tools',
1655
1674
  description: 'Probe the fixed registry of official model tools (Kimi Code, Claude Code, Codex, MiniMax Code, MiMo Code, Grok Build, Gemini CLI, OpenCode, Step Code, ZCode) and report which are installed with their versions, plus each tool\'s available install methods and the effective install directory and npm source. Never reads credentials.',
@@ -1680,6 +1699,7 @@ function registerOfficialToolModels(ctx, config) {
1680
1699
  },
1681
1700
  output: JSON_OUTPUT,
1682
1701
  async execute(args, exec) {
1702
+ assertInstallPreferences()
1683
1703
  const job = startInstall(args.tool, { method: text(args.method) || undefined })
1684
1704
  const settled = await waitForInstall(job.tool, exec.signal)
1685
1705
  const tool = getOfficialTool(args.tool)
@@ -1704,6 +1724,7 @@ function registerOfficialToolModels(ctx, config) {
1704
1724
  },
1705
1725
  output: JSON_OUTPUT,
1706
1726
  async execute(args, exec) {
1727
+ assertInstallPreferences()
1707
1728
  const job = startRepair(args.tool, { method: text(args.method) || undefined })
1708
1729
  const settled = await waitForInstall(job.tool, exec.signal)
1709
1730
  const tool = getOfficialTool(args.tool)
@@ -1726,6 +1747,7 @@ function registerOfficialToolModels(ctx, config) {
1726
1747
  },
1727
1748
  output: JSON_OUTPUT,
1728
1749
  async execute(args, exec) {
1750
+ assertInstallPreferences()
1729
1751
  const job = startUninstall(args.tool, { method: text(args.method) || undefined })
1730
1752
  const settled = await waitForInstall(job.tool, exec.signal)
1731
1753
  const tool = getOfficialTool(args.tool)
@@ -1,8 +1,8 @@
1
1
  /** Host receiver for the official desktop's typed one-click installer RPC. */
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
4
- import { getOfficialTool, installMethodBlockReason, OFFICIAL_TOOLS } from './shared/official-tool-registry.mjs'
5
- import { resolveInstallPlan, resolveUninstallPlan } from './shared/tool-install-preferences.mjs'
4
+ import { getOfficialTool, installCommandLine, installMethodBlockReason, OFFICIAL_TOOLS } from './shared/official-tool-registry.mjs'
5
+ import { resolveInstallLocation, resolveInstallPlan, resolveUninstallPlan } from './shared/tool-install-preferences.mjs'
6
6
  import { officialToolExecutionCapabilities, officialToolReadiness } from './shared/official-tool-executor.mjs'
7
7
  import {
8
8
  cancelInstall,
@@ -41,8 +41,7 @@ const unavailable = () => { throw new Error('模型路由工作台服务尚未
41
41
  * command line that would actually run. The panel shows this before the user
42
42
  * clicks, so an install is never a surprise command.
43
43
  */
44
- function installSummary() {
45
- const preferences = installPreferences()
44
+ export function installSummary(preferences = installPreferences(), options = currentInstallOptions()) {
46
45
  return {
47
46
  installDir: preferences.installDir ?? '',
48
47
  registry: preferences.registry ?? '',
@@ -56,22 +55,59 @@ function installSummary() {
56
55
  command: null,
57
56
  uninstallCommand: null,
58
57
  methodId: null,
59
- // What the user pinned, which may differ from the effective method when
60
- // the pinned one does not run on this platform.
58
+ // A pinned unavailable method stays selected and reports its reason.
61
59
  savedMethodId: preferences.methods?.[tool.id] ?? null,
62
60
  notices: [],
63
61
  }
62
+ if (tool.manager === 'signed-windows-installer') {
63
+ entry.methodId = 'signed-windows-installer'
64
+ if (options.platform === 'win32') {
65
+ entry.command = installCommandLine(tool)
66
+ entry.updateCommand = entry.command
67
+ entry.notices = [preferences.installDir
68
+ ? `请在官方安装窗口手动选择 ${preferences.installDir},完成后重新体检。`
69
+ : '请在官方安装窗口选择目录并完成安装,然后重新体检。']
70
+ } else entry.installError = entry.error = 'ZCode 签名安装器仅支持 Windows。'
71
+ entry.uninstallError = 'ZCode 是桌面安装版,请在系统“应用”中卸载它。'
72
+ return entry
73
+ }
64
74
  for (const method of tool.installMethods ?? []) {
65
- entry.methods.push({ id: method.id, label: method.label, kind: method.kind, blocked: installMethodBlockReason(method) })
75
+ const item = { id: method.id, label: method.label, kind: method.kind,
76
+ supportsInstallDir: method.supportsInstallDir === true,
77
+ blocked: installMethodBlockReason(method, options.platform) }
78
+ try {
79
+ const plan = resolveInstallPlan(tool, method.id, preferences, options)
80
+ item.command = planCommandLine(plan)
81
+ item.installDir = resolveInstallLocation(tool, method, preferences, options)?.dir ?? ''
82
+ item.notices = plan.notices ?? []
83
+ } catch (error) {
84
+ item.blocked = errorText(error).slice(0, 200)
85
+ }
86
+ try {
87
+ const removal = resolveUninstallPlan(tool, method.id, preferences, options)
88
+ item.uninstallCommand = planCommandLine(removal, 'uninstall')
89
+ } catch (error) { item.uninstallError = errorText(error).slice(0, 200) }
90
+ entry.methods.push(item)
66
91
  }
67
92
  try {
68
- const plan = resolveInstallPlan(tool, undefined, preferences, currentInstallOptions())
93
+ const plan = resolveInstallPlan(tool, undefined, preferences, options)
69
94
  entry.methodId = plan.methodId
70
95
  entry.command = planCommandLine(plan)
96
+ entry.installDir = resolveInstallLocation(tool, plan.method, preferences, options)?.dir ?? ''
97
+ entry.updateMethodId = plan.methodId
98
+ entry.updateCommand = entry.command
99
+ entry.updateInstallDir = entry.installDir
71
100
  entry.notices = plan.notices ?? []
72
- entry.uninstallCommand = planCommandLine(resolveUninstallPlan(tool, plan.methodId, preferences, currentInstallOptions()), 'uninstall')
73
101
  } catch (error) {
74
- entry.error = errorText(error).slice(0, 200)
102
+ entry.installError = entry.error = errorText(error).slice(0, 200)
103
+ }
104
+ try {
105
+ const removal = resolveUninstallPlan(tool, undefined, preferences, options)
106
+ entry.methodId ??= removal.methodId
107
+ entry.installDir ??= resolveInstallLocation(tool, removal.method, preferences, options)?.dir ?? ''
108
+ entry.uninstallCommand = planCommandLine(removal, 'uninstall')
109
+ } catch (error) {
110
+ entry.uninstallError = errorText(error).slice(0, 200)
75
111
  }
76
112
  return entry
77
113
  }),
@@ -143,13 +179,25 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
143
179
  executionCapabilities: officialToolExecutionCapabilities(),
144
180
  executionReadiness,
145
181
  hostVersion: HOST_PLUGIN_VERSION,
146
- install: installSummary(),
182
+ install: this.installSettings().install,
147
183
  }
148
184
  }
149
185
 
186
+ /** Refresh saved methods and commands without spawning CLIs or checking downloads. */
187
+ installSettings() {
188
+ const error = this.services?.installSettingsError?.()
189
+ if (error) throw new Error(`官方工具安装设置无效:${error}`)
190
+ const install = installSummary()
191
+ // The saved input can contain Host-only home/environment expansions. Keep
192
+ // it separate from the resolved directory used by the actual command.
193
+ install.requestedInstallDir = this.services?.installRequestedDirectory?.() ?? install.installDir
194
+ return { install, hostVersion: HOST_PLUGIN_VERSION }
195
+ }
196
+
150
197
  /** Start one serialized fixed-registry install; return immediately for UI polling. */
151
198
  async installTool(request) {
152
199
  return settled(async () => {
200
+ this.installSettings()
153
201
  await ensureNpmPrefixOnPath()
154
202
  return { accepted: true, job: startInstall(request.tool, { method: request.method }) }
155
203
  })
@@ -161,6 +209,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
161
209
  */
162
210
  async uninstallTool(request) {
163
211
  return settled(async () => {
212
+ this.installSettings()
164
213
  await ensureNpmPrefixOnPath()
165
214
  return { accepted: true, job: startUninstall(request.tool, { method: request.method }) }
166
215
  })
@@ -169,6 +218,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
169
218
  /** Reinstall the tool even when it already reports the latest version. */
170
219
  async repairTool(request) {
171
220
  return settled(async () => {
221
+ this.installSettings()
172
222
  await ensureNpmPrefixOnPath()
173
223
  return { accepted: true, job: startRepair(request.tool, { method: request.method }) }
174
224
  })
@@ -18,13 +18,14 @@ import { access, readFile, realpath, stat } from 'node:fs/promises'
18
18
  import { constants } from 'node:fs'
19
19
  import { StringDecoder } from 'node:string_decoder'
20
20
  import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from 'node:path'
21
- import { getOfficialTool, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
22
- import { ensureNpmPrefixOnPath } from './official-tools-runtime.mjs'
21
+ import { getOfficialTool, installMethodFor, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
22
+ import { currentInstallOptions, ensureNpmPrefixOnPath, installPreferences, preferredToolDirectories, usesConfiguredInstallPlan } from './official-tools-runtime.mjs'
23
+ import { resolveInstallPlan } from './tool-install-preferences.mjs'
23
24
  import { buildMiniMaxInvocation, createMiniMaxStreamParser } from './vendor-minimax-adapter.mjs'
24
25
  import { discoverZCodeBundle } from './zcode-bundle.mjs'
25
26
  import { npmAttestor } from './npm-attestation.mjs'
26
27
  import { compareReleaseVersions, isReleaseVersion } from './latest-versions.mjs'
27
- import { resolveMiMoGrokLaunch, createMiMoGrokParser } from './vendor-mimo-grok-adapter.mjs'
28
+ import { resolveMiMoGrokLaunch, createMiMoGrokParser, installedModuleRoots } from './vendor-mimo-grok-adapter.mjs'
28
29
  import { emptyOutputError, outputIsEmpty, usageFromEvents, usageFromOutput } from './task-executors.mjs'
29
30
 
30
31
  const IS_WINDOWS = process.platform === 'win32'
@@ -169,6 +170,22 @@ function pathDirectories() {
169
170
  return [...new Set(value.split(delimiter).map(part => part.trim().replace(/^"|"$/g, '')).filter(Boolean))]
170
171
  }
171
172
 
173
+ /** The configured install location is authoritative even if another copy is newer. */
174
+ export function officialToolEntrySelection(toolId, { preferences = installPreferences(), plan: selectedPlan, preferredDirs } = {}) {
175
+ const tool = getOfficialTool(toolId)
176
+ if (!tool || tool.manager === 'signed-windows-installer') return { directories: pathDirectories(), plan: null, configured: false }
177
+ // A saved script choice stays usable after future script installs are
178
+ // disabled. Automatic selection still follows the same switch as installation.
179
+ const selectedScript = installMethodFor(tool, preferences.methods?.[toolId])?.kind === 'script'
180
+ const discoveryPreferences = selectedScript ? { ...preferences, allowScriptInstall: true } : preferences
181
+ const plan = selectedPlan ?? resolveInstallPlan(tool, undefined, discoveryPreferences, currentInstallOptions())
182
+ const preferred = preferredDirs ?? (selectedPlan ? (selectedPlan.binDir ? [selectedPlan.binDir] : [])
183
+ : preferredToolDirectories(toolId, discoveryPreferences))
184
+ return { directories: Array.isArray(preferredDirs) || preferred.length ? [...preferred] : pathDirectories(), plan,
185
+ configured: Boolean(selectedPlan) || usesConfiguredInstallPlan(toolId, preferences),
186
+ preferred: Array.isArray(preferredDirs) || preferred.length > 0, directoryOverride: Array.isArray(preferredDirs) }
187
+ }
188
+
172
189
  function versionAtLeast(value, minimum) {
173
190
  const found = /^(\d+)\.(\d+)\.(\d+)(?:$|-)/.exec(String(value ?? ''))
174
191
  if (!found) return false
@@ -216,27 +233,35 @@ async function readPackageEntry(packageDirectory, packageName, binName, expected
216
233
  }
217
234
 
218
235
  /** Resolve the package's real `bin` file, never its Windows .cmd shim. */
219
- async function findGlobalPackageEntries(packageName, binName, expectedBin, workspace) {
220
- await ensureNpmPrefixOnPath()
236
+ export async function findGlobalPackageEntries(packageName, binName, expectedBin, workspace, { directories, manager = null, moduleRoots, selection: selectedSelection } = {}) {
237
+ const tool = OFFICIAL_TOOLS.find(item => item.package === packageName)
238
+ const selection = selectedSelection ?? (directories || Array.isArray(moduleRoots) ? null : officialToolEntrySelection(tool?.id))
239
+ const exactRoots = Array.isArray(moduleRoots) ? moduleRoots : selection?.plan?.packageModuleRoots
240
+ if (!Array.isArray(exactRoots) && !directories && !selection?.preferred) await ensureNpmPrefixOnPath()
241
+ const targets = directories ?? (selection?.preferred ? selection.directories : pathDirectories())
242
+ const selectedManager = manager ?? selection?.plan?.manager ?? null
221
243
  const packageParts = packageName.split('/')
222
244
  const found = []
223
245
  const seen = new Set()
224
- for (const directory of pathDirectories()) {
225
- const moduleParents = [join(directory, 'node_modules')]
226
- if (basename(directory).toLowerCase() === '.bin') moduleParents.push(resolve(directory, '..'))
227
- if (!IS_WINDOWS) moduleParents.push(resolve(directory, '..', 'lib', 'node_modules'))
228
- const candidates = moduleParents.map(modules => join(modules, ...packageParts))
229
- // Native installs put the exe in `<package>/bin` and that directory on PATH
230
- // (for example D:\Program Files\Claude\node_modules\@anthropic-ai\claude-code\bin).
231
- candidates.push(directory, resolve(directory, '..'))
232
- for (const packageDirectory of candidates) {
233
- const entry = await readPackageEntry(packageDirectory, packageName, binName, expectedBin, workspace, seen)
234
- if (entry) found.push(entry)
235
- }
246
+ const roots = Array.isArray(exactRoots) ? exactRoots : await installedModuleRoots(targets, { manager: selectedManager })
247
+ const candidates = roots.map(modules => join(modules, ...packageParts))
248
+ // Native npm installs can put their package/bin directory directly on PATH.
249
+ if (!Array.isArray(exactRoots) && selectedManager !== 'pnpm') candidates.push(...targets.flatMap(directory => [directory, resolve(directory, '..')]))
250
+ for (const packageDirectory of candidates) {
251
+ const entry = await readPackageEntry(packageDirectory, packageName, binName, expectedBin, workspace, seen)
252
+ if (entry) found.push(entry)
236
253
  }
237
254
  return found
238
255
  }
239
256
 
257
+ /** Official Windows repairs use a distinct directory key for the same npm version. */
258
+ export function miniMaxReleaseVersion(releaseKey) {
259
+ const repair = /^(\d{1,6}\.\d{1,6}\.\d{1,6})-repair-[1-9]\d{0,9}-[0-9a-f]{32}$/i.exec(String(releaseKey ?? ''))
260
+ if (repair) return repair[1]
261
+ if (String(releaseKey ?? '').includes('-repair-')) return null
262
+ return isReleaseVersion(releaseKey) ? releaseKey : null
263
+ }
264
+
240
265
  /**
241
266
  * The official MiniMax Windows installer uses a versioned releases tree rather
242
267
  * than npm -g. Any release named by its `current` file is accepted, but every
@@ -252,10 +277,13 @@ export async function findManagedMiniMaxEntry(workspace,
252
277
  const root = await realpath(directory)
253
278
  if (root === workspace || inside(workspace, root)) continue
254
279
  if (!(await stat(join(root, 'mcode.cmd'))).isFile()) continue
255
- const version = (await readFile(join(root, 'current'), 'utf8')).trim()
256
- if (!isReleaseVersion(version)) continue
257
- const releaseRoot = await realpath(join(root, 'releases', version))
258
- if (!inside(root, releaseRoot)) continue
280
+ const releaseKey = (await readFile(join(root, 'current'), 'utf8')).trim()
281
+ const version = miniMaxReleaseVersion(releaseKey)
282
+ if (!version) continue
283
+ const releasesRoot = await realpath(join(root, 'releases'))
284
+ if (!inside(root, releasesRoot)) continue
285
+ const releaseRoot = await realpath(join(releasesRoot, releaseKey))
286
+ if (!inside(releasesRoot, releaseRoot)) continue
259
287
  const packageRoot = await realpath(join(releaseRoot, 'node_modules', '@minimax-ai', 'code'))
260
288
  if (!inside(releaseRoot, packageRoot)) continue
261
289
  const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
@@ -268,7 +296,7 @@ export async function findManagedMiniMaxEntry(workspace,
268
296
  const native = await realpath(join(packageRoot, 'node_modules', 'better-sqlite3',
269
297
  'build', 'Release', 'better_sqlite3.node'))
270
298
  if (!inside(packageRoot, native) || !(await stat(native)).isFile()) continue
271
- return { entry, packageRoot, version, source: 'official-windows-installer-npm-attested' }
299
+ return { entry, packageRoot, version, installDir: root, binDir: root, source: 'official-windows-installer-npm-attested' }
272
300
  } catch { /* inspect the next PATH directory */ }
273
301
  }
274
302
  return null
@@ -279,24 +307,25 @@ function newestFirst(entries) {
279
307
  return [...entries].sort((left, right) => (compareReleaseVersions(right.version, left.version) ?? 0))
280
308
  }
281
309
 
282
- async function findCodexExecutable(workspace) {
283
- await ensureNpmPrefixOnPath()
284
- const filename = IS_WINDOWS ? 'codex.exe' : 'codex'
285
- for (const directory of pathDirectories()) {
310
+ async function findSignedNativeExecutable(toolId, workspace, signer, selection = officialToolEntrySelection(toolId)) {
311
+ if (!selection.preferred) await ensureNpmPrefixOnPath()
312
+ const command = toolId === 'claude-code' ? 'claude' : toolId
313
+ const filename = IS_WINDOWS ? `${command}.exe` : command
314
+ for (const directory of selection.preferred ? selection.directories : pathDirectories()) {
286
315
  try {
287
316
  const pathRoot = await realpath(directory)
288
317
  if (pathRoot === workspace || inside(workspace, pathRoot)) continue
289
318
  const entry = await realpath(join(directory, filename))
290
319
  if (inside(workspace, entry) || !(await stat(entry)).isFile()) continue
291
320
  await access(entry, IS_WINDOWS ? constants.F_OK : constants.X_OK)
292
- if (IS_WINDOWS && !(await hasOfficialWindowsSignature(entry, CODEX_SIGNER))) continue
321
+ if (IS_WINDOWS && !(await hasOfficialWindowsSignature(entry, signer))) continue
293
322
  return { entry, source: IS_WINDOWS ? 'signed-native-executable' : 'path-native-executable' }
294
323
  } catch { /* try the next PATH entry */ }
295
324
  }
296
325
  return null
297
326
  }
298
327
 
299
- function nodeVersion(entry) {
328
+ function nodeVersion(entry, pattern = /^v(\d+\.\d+\.\d+)/) {
300
329
  return new Promise(resolveVersion => {
301
330
  let child
302
331
  try { child = spawn(entry, ['--version'], { shell: false, windowsHide: true, stdio: ['ignore', 'pipe', 'ignore'] }) }
@@ -312,7 +341,7 @@ function nodeVersion(entry) {
312
341
  const timer = setTimeout(() => { child.kill('SIGTERM'); finish(null) }, 4_000)
313
342
  child.stdout.on('data', bytes => { output = tail(output, String(bytes)) })
314
343
  child.on('error', () => finish(null))
315
- child.on('close', code => finish(code === 0 ? /^v(\d+\.\d+\.\d+)/.exec(output.trim())?.[1] ?? null : null))
344
+ child.on('close', code => finish(code === 0 ? pattern.exec(output.trim())?.[1] ?? null : null))
316
345
  })
317
346
  }
318
347
 
@@ -335,17 +364,53 @@ async function findNodeExecutable(workspace, minimum = [22, 19, 0], acceptedVers
335
364
  }
336
365
 
337
366
  /** Resolve the signed native binary carried by the official npm wrapper. */
338
- async function findSignedPackagedCodex(packageRoot, workspace) {
367
+ export async function findSignedPackagedCodex(packageRoot, workspace, {
368
+ installDir = '', manager = null, moduleRoots = [], verifySignature = hasOfficialWindowsSignature,
369
+ } = {}) {
339
370
  if (!IS_WINDOWS) return null
340
371
  const target = process.arch === 'arm64' ? ['codex-win32-arm64', 'aarch64-pc-windows-msvc']
341
372
  : process.arch === 'x64' ? ['codex-win32-x64', 'x86_64-pc-windows-msvc'] : null
342
373
  if (!target) return null
343
- const candidate = join(packageRoot, 'node_modules', '@openai', target[0], 'vendor', target[1], 'bin', 'codex.exe')
344
374
  try {
345
- const entry = await realpath(candidate)
346
- if (!inside(packageRoot, entry) || inside(workspace, entry) || !(await stat(entry)).isFile()) return null
347
- if (!(await hasOfficialWindowsSignature(entry, CODEX_SIGNER))) return null
348
- return { entry, source: 'signed-official-npm-native' }
375
+ const wrapperRoot = await realpath(packageRoot)
376
+ if (wrapperRoot === workspace || inside(workspace, wrapperRoot)) return null
377
+ const wrapper = JSON.parse(await readFile(join(wrapperRoot, 'package.json'), 'utf8'))
378
+ const nativeName = `@openai/${target[0]}`
379
+ const dependency = wrapper.optionalDependencies?.[nativeName]
380
+ const alias = /^npm:(@openai\/[A-Za-z0-9-]+)@(.+)$/.exec(String(dependency ?? ''))
381
+ const expectedName = alias ? alias[1] : nativeName
382
+ const expectedVersion = alias ? alias[2] : dependency
383
+ if (wrapper.name !== '@openai/codex' || !isReleaseVersion(wrapper.version)
384
+ || !['@openai/codex', nativeName].includes(expectedName) || !isReleaseVersion(expectedVersion)) return null
385
+ let virtualStore = null
386
+ for (let directory = wrapperRoot; directory !== resolve(directory, '..'); directory = resolve(directory, '..')) {
387
+ if (basename(directory) === '.pnpm') { virtualStore = directory; break }
388
+ }
389
+ // pnpm's optional platform package is a sibling symlink in its virtual
390
+ // store. A configured prefix confines both wrapper and native package to
391
+ // that prefix's global tree, never another store or arbitrary directory.
392
+ const boundary = manager === 'pnpm' && installDir
393
+ ? await realpath(join(installDir, '.pnpm', 'global'))
394
+ : virtualStore ?? await realpath(resolve(wrapperRoot, '..', '..'))
395
+ if (!inside(boundary, wrapperRoot)) return null
396
+ const nativePackages = [
397
+ join(wrapperRoot, 'node_modules', '@openai', target[0]),
398
+ resolve(wrapperRoot, '..', target[0]),
399
+ ...moduleRoots.map(modules => join(modules, '@openai', target[0])),
400
+ ]
401
+ for (const nativePackage of nativePackages) {
402
+ try {
403
+ const nativeRoot = await realpath(nativePackage)
404
+ if (!inside(boundary, nativeRoot) || inside(workspace, nativeRoot)) continue
405
+ const manifest = JSON.parse(await readFile(join(nativeRoot, 'package.json'), 'utf8'))
406
+ if (manifest.name !== expectedName || manifest.version !== expectedVersion) continue
407
+ const entry = await realpath(join(nativeRoot, 'vendor', target[1], 'bin', 'codex.exe'))
408
+ if (!inside(nativeRoot, entry) || inside(workspace, entry) || !(await stat(entry)).isFile()) continue
409
+ if (!(await verifySignature(entry, CODEX_SIGNER))) continue
410
+ return { entry, source: 'signed-official-npm-native' }
411
+ } catch { /* try the wrapper's other official optional-package location */ }
412
+ }
413
+ return null
349
414
  } catch { return null }
350
415
  }
351
416
 
@@ -404,13 +469,18 @@ function hasOfficialWindowsSignature(entry, expectedSigner) {
404
469
  })
405
470
  }
406
471
 
407
- async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readiness.') {
472
+ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readiness.', selectionOptions = {}) {
408
473
  if (toolId === 'claude-code') {
409
- const entries = await findGlobalPackageEntries('@anthropic-ai/claude-code', 'claude', 'bin/claude.exe', workspace)
474
+ const selection = officialToolEntrySelection(toolId, selectionOptions)
475
+ const native = selection.plan.kind === 'script'
476
+ ? await findSignedNativeExecutable(toolId, workspace, CLAUDE_SIGNER, selection) : null
477
+ if (native) native.version = await nodeVersion(native.entry, /(?:^|\s)(\d+\.\d+\.\d+(?:-[\w.-]+)?)/)
478
+ const entries = selection.plan.kind === 'script' ? (native ? [native] : [])
479
+ : await findGlobalPackageEntries('@anthropic-ai/claude-code', 'claude', 'bin/claude.exe', workspace, { selection })
410
480
  const compatible = entries.filter(entry => versionAtLeast(entry.version, CLAUDE_MIN_RESTRICTED_VERSION))
411
481
  let found = null
412
482
  for (const entry of compatible) {
413
- if (!IS_WINDOWS || await hasOfficialWindowsSignature(entry.entry, CLAUDE_SIGNER)) {
483
+ if (entry === native || !IS_WINDOWS || await hasOfficialWindowsSignature(entry.entry, CLAUDE_SIGNER)) {
414
484
  found = entry
415
485
  break
416
486
  }
@@ -440,13 +510,20 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
440
510
  }
441
511
  if (toolId === 'codex') {
442
512
  if (!IS_WINDOWS) return { unsupported: CAPABILITIES.codex.reason }
443
- const packages = await findGlobalPackageEntries('@openai/codex', 'codex', 'bin/codex.js', workspace)
513
+ const selection = officialToolEntrySelection(toolId, selectionOptions)
514
+ const packages = selection.plan.kind === 'script' ? []
515
+ : await findGlobalPackageEntries('@openai/codex', 'codex', 'bin/codex.js', workspace, { selection })
516
+ const moduleRoots = selection.plan.packageModuleRoots ?? (packages.length
517
+ ? await installedModuleRoots(selection.preferred ? selection.directories : pathDirectories(), { manager: selection.plan.manager }) : [])
444
518
  let found = null
445
519
  for (const entry of packages) {
446
- found = await findSignedPackagedCodex(entry.packageRoot, workspace)
520
+ found = await findSignedPackagedCodex(entry.packageRoot, workspace,
521
+ { installDir: selection.plan.installDir, manager: selection.plan.manager, moduleRoots })
447
522
  if (found) break
448
523
  }
449
- found ??= await findCodexExecutable(workspace)
524
+ if (!found && selection.plan.kind === 'script') {
525
+ found = await findSignedNativeExecutable(toolId, workspace, CODEX_SIGNER, selection)
526
+ }
450
527
  if (!found) return { unsupported: '未找到具有 OpenAI 有效签名的 codex.exe。' }
451
528
  return {
452
529
  file: found.entry,
@@ -456,8 +533,10 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
456
533
  }
457
534
  }
458
535
  if (toolId === 'kimi-code') {
536
+ const selection = officialToolEntrySelection(toolId, selectionOptions)
537
+ if (selection.plan.kind === 'script') return { unsupported: '所选 Kimi 原生脚本安装没有可核验的 npm 执行入口;不会改用其他安装目录中的 Kimi。' }
459
538
  const tool = getOfficialTool(toolId)
460
- const entries = await findGlobalPackageEntries(tool.package, 'kimi', 'dist/main.mjs', workspace)
539
+ const entries = await findGlobalPackageEntries(tool.package, 'kimi', 'dist/main.mjs', workspace, { selection })
461
540
  const found = newestFirst(entries)[0]
462
541
  if (!found) return { unsupported: `未找到官方 npm 包 ${tool.package} 的 kimi 入口(dist/main.mjs)。` }
463
542
  const nodeExecutable = await findNodeExecutable(workspace)
@@ -474,8 +553,12 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
474
553
  }
475
554
  if (toolId === 'minimax-code') {
476
555
  const tool = getOfficialTool(toolId)
477
- const entries = await findGlobalPackageEntries(tool.package, 'mcode', 'cli.js', workspace)
478
- const found = newestFirst(entries)[0] ?? await findManagedMiniMaxEntry(workspace)
556
+ const selection = officialToolEntrySelection(toolId, selectionOptions)
557
+ const managed = selection.plan.kind === 'script' || selection.directoryOverride
558
+ ? await findManagedMiniMaxEntry(workspace, selection.directories)
559
+ : !selection.configured ? await findManagedMiniMaxEntry(workspace) : null
560
+ const entries = selection.plan.kind === 'script' ? [] : await findGlobalPackageEntries(tool.package, 'mcode', 'cli.js', workspace, { selection })
561
+ const found = managed ?? newestFirst(entries)[0]
479
562
  if (!found) return { unsupported: `未找到官方 npm 包 ${tool.package} 的 mcode 入口(cli.js)。` }
480
563
  const nodeExecutable = await findNodeExecutable(workspace, [22, 19, 0], version => {
481
564
  const major = Number(String(version).split('.')[0])
@@ -492,7 +575,12 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
492
575
  }
493
576
  if (toolId === 'mimo-code' || toolId === 'grok-build') {
494
577
  try {
495
- return await resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId, task })
578
+ const selection = officialToolEntrySelection(toolId, selectionOptions)
579
+ if (selection.plan.kind === 'script') return { unsupported: `所选 ${getOfficialTool(toolId).label} 原生脚本安装没有可核验的 npm 元数据;不会改用其他安装目录中的工具。` }
580
+ return await resolveMiMoGrokLaunch({ toolId, workspace, mode, modelId, task,
581
+ directories: selection.preferred ? selection.directories : undefined,
582
+ moduleRoots: selection.plan.packageModuleRoots,
583
+ manager: selection.plan.manager })
496
584
  } catch (error) {
497
585
  return { unsupported: `官方原生入口校验失败:${String(error?.message ?? error).slice(0, 240)}` }
498
586
  }
@@ -586,14 +674,14 @@ export function harnessSandboxBlocker(toolId, platform = process.platform) {
586
674
  }
587
675
 
588
676
  /** Check the actual trusted launch entry without starting an account call. */
589
- export async function officialToolReadiness(toolId, workspace = process.cwd()) {
677
+ export async function officialToolReadiness(toolId, workspace = process.cwd(), selectionOptions = {}) {
590
678
  const tool = getOfficialTool(toolId)
591
679
  if (!tool) return { id: String(toolId), ready: false, reason: '工具不在固定官方注册表中。' }
592
680
  const capability = CAPABILITIES[tool.id]
593
681
  if (!capability?.supported) return { id: tool.id, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
594
682
  try {
595
683
  const directory = await checkedWorkspace(workspace)
596
- const spec = await launchSpec(tool.id, directory, capability.modes[0], null)
684
+ const spec = await launchSpec(tool.id, directory, capability.modes[0], null, undefined, selectionOptions)
597
685
  return spec.unsupported
598
686
  ? { id: tool.id, ready: false, reason: spec.unsupported }
599
687
  : { id: tool.id, ready: true, source: spec.source, modes: capability.modes }