@ljwei-stak/dsh-model-router 0.18.0 → 0.18.2
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/.dsh-plugin/client.js +201 -35
- package/.dsh-plugin/index.mjs +29 -7
- package/.dsh-plugin/official-tools-remote-service.mjs +15 -1
- package/.dsh-plugin/shared/official-tool-executor.mjs +6 -6
- package/.dsh-plugin/shared/official-tool-registry.mjs +18 -1
- package/.dsh-plugin/shared/official-tools-remote.mjs +1 -0
- package/.dsh-plugin/shared/official-tools-runtime.mjs +137 -12
- package/.dsh-plugin/shared/task-executors.mjs +52 -3
- package/.dsh-plugin/shared/tool-install-preferences.mjs +20 -1
- package/CHANGELOG.md +19 -0
- package/README.md +9 -5
- package/README.zh.md +9 -5
- package/docs/INSTALLATION_GUIDE.zh.md +12 -12
- package/docs/WORKBENCH_USER_GUIDE.zh.md +10 -8
- package/package.json +2 -2
|
@@ -179,13 +179,25 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
179
179
|
executionCapabilities: officialToolExecutionCapabilities(),
|
|
180
180
|
executionReadiness,
|
|
181
181
|
hostVersion: HOST_PLUGIN_VERSION,
|
|
182
|
-
install:
|
|
182
|
+
install: this.installSettings().install,
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
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
|
+
|
|
186
197
|
/** Start one serialized fixed-registry install; return immediately for UI polling. */
|
|
187
198
|
async installTool(request) {
|
|
188
199
|
return settled(async () => {
|
|
200
|
+
this.installSettings()
|
|
189
201
|
await ensureNpmPrefixOnPath()
|
|
190
202
|
return { accepted: true, job: startInstall(request.tool, { method: request.method }) }
|
|
191
203
|
})
|
|
@@ -197,6 +209,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
197
209
|
*/
|
|
198
210
|
async uninstallTool(request) {
|
|
199
211
|
return settled(async () => {
|
|
212
|
+
this.installSettings()
|
|
200
213
|
await ensureNpmPrefixOnPath()
|
|
201
214
|
return { accepted: true, job: startUninstall(request.tool, { method: request.method }) }
|
|
202
215
|
})
|
|
@@ -205,6 +218,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
205
218
|
/** Reinstall the tool even when it already reports the latest version. */
|
|
206
219
|
async repairTool(request) {
|
|
207
220
|
return settled(async () => {
|
|
221
|
+
this.installSettings()
|
|
208
222
|
await ensureNpmPrefixOnPath()
|
|
209
223
|
return { accepted: true, job: startRepair(request.tool, { method: request.method }) }
|
|
210
224
|
})
|
|
@@ -345,7 +345,7 @@ function nodeVersion(entry, pattern = /^v(\d+\.\d+\.\d+)/) {
|
|
|
345
345
|
})
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
-
async function findNodeExecutable(workspace, minimum = [22, 19, 0], acceptedVersion = () => true) {
|
|
348
|
+
export async function findNodeExecutable(workspace, minimum = [22, 19, 0], acceptedVersion = () => true) {
|
|
349
349
|
await ensureNpmPrefixOnPath()
|
|
350
350
|
const candidates = [process.execPath, ...pathDirectories().map(directory => join(directory, IS_WINDOWS ? 'node.exe' : 'node'))]
|
|
351
351
|
for (const candidate of candidates) {
|
|
@@ -676,17 +676,17 @@ export function harnessSandboxBlocker(toolId, platform = process.platform) {
|
|
|
676
676
|
/** Check the actual trusted launch entry without starting an account call. */
|
|
677
677
|
export async function officialToolReadiness(toolId, workspace = process.cwd(), selectionOptions = {}) {
|
|
678
678
|
const tool = getOfficialTool(toolId)
|
|
679
|
-
if (!tool) return { id: String(toolId), ready: false, reason: '工具不在固定官方注册表中。' }
|
|
679
|
+
if (!tool) return { id: String(toolId), supported: false, ready: false, reason: '工具不在固定官方注册表中。' }
|
|
680
680
|
const capability = CAPABILITIES[tool.id]
|
|
681
|
-
if (!capability?.supported) return { id: tool.id, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
|
|
681
|
+
if (!capability?.supported) return { id: tool.id, supported: false, ready: false, reason: capability?.reason ?? '当前平台不支持托管执行。' }
|
|
682
682
|
try {
|
|
683
683
|
const directory = await checkedWorkspace(workspace)
|
|
684
684
|
const spec = await launchSpec(tool.id, directory, capability.modes[0], null, undefined, selectionOptions)
|
|
685
685
|
return spec.unsupported
|
|
686
|
-
? { id: tool.id, ready: false, reason: spec.unsupported }
|
|
687
|
-
: { id: tool.id, ready: true, source: spec.source, modes: capability.modes }
|
|
686
|
+
? { id: tool.id, supported: true, ready: false, reason: spec.unsupported }
|
|
687
|
+
: { id: tool.id, supported: true, ready: true, source: spec.source, modes: capability.modes }
|
|
688
688
|
} catch (error) {
|
|
689
|
-
return { id: tool.id, ready: false, reason: String(error?.message ?? error).slice(0, 300) }
|
|
689
|
+
return { id: tool.id, supported: true, ready: false, reason: String(error?.message ?? error).slice(0, 300) }
|
|
690
690
|
}
|
|
691
691
|
}
|
|
692
692
|
|
|
@@ -45,7 +45,7 @@ function scriptMethod(id, label, {
|
|
|
45
45
|
shell, scriptUrl, verify, installArg = [], env = [], platforms,
|
|
46
46
|
supportsInstallDir = false, installDirEnv = '', installDirSuffix = '',
|
|
47
47
|
defaultScriptInstallDir, defaultScriptInstallDirEnv, scriptBinaryNames,
|
|
48
|
-
registryEnv, installDirSubdir, uninstallUnsupportedReason, scriptFallbackUrls,
|
|
48
|
+
registryEnv, installDirSubdir, legacySharedInstallDir = false, sharedDefaultBin = false, uninstallUnsupportedReason, scriptFallbackUrls,
|
|
49
49
|
}) {
|
|
50
50
|
return Object.freeze({
|
|
51
51
|
id,
|
|
@@ -64,6 +64,8 @@ function scriptMethod(id, label, {
|
|
|
64
64
|
installDirEnv,
|
|
65
65
|
installDirSuffix,
|
|
66
66
|
...(installDirSubdir ? { installDirSubdir } : {}),
|
|
67
|
+
...(legacySharedInstallDir ? { legacySharedInstallDir: true } : {}),
|
|
68
|
+
...(sharedDefaultBin ? { sharedDefaultBin: true } : {}),
|
|
67
69
|
...(uninstallUnsupportedReason ? { uninstallUnsupportedReason } : {}),
|
|
68
70
|
...(defaultScriptInstallDir ? { defaultScriptInstallDir } : {}),
|
|
69
71
|
...(defaultScriptInstallDirEnv ? { defaultScriptInstallDirEnv } : {}),
|
|
@@ -96,6 +98,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
96
98
|
shell: 'bash', scriptUrl: 'https://code.kimi.com/kimi-code/install.sh',
|
|
97
99
|
platforms: ['linux', 'darwin'], supportsInstallDir: true,
|
|
98
100
|
installDirEnv: 'KIMI_INSTALL_DIR', installDirSuffix: 'bin',
|
|
101
|
+
installDirSubdir: 'kimi-code', legacySharedInstallDir: true,
|
|
99
102
|
env: ['KIMI_INSTALL_DIR'],
|
|
100
103
|
scriptBinaryNames: ['kimi'],
|
|
101
104
|
verify: Object.freeze({ mustInclude: ['kimi-code', 'code.kimi.com/kimi-code'] }),
|
|
@@ -104,6 +107,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
104
107
|
shell: 'powershell', scriptUrl: 'https://code.kimi.com/kimi-code/install.ps1',
|
|
105
108
|
platforms: ['win32'], supportsInstallDir: true,
|
|
106
109
|
installDirEnv: 'KIMI_INSTALL_DIR', installDirSuffix: 'bin',
|
|
110
|
+
installDirSubdir: 'kimi-code', legacySharedInstallDir: true,
|
|
107
111
|
env: ['KIMI_INSTALL_DIR'],
|
|
108
112
|
scriptBinaryNames: ['kimi.exe'],
|
|
109
113
|
verify: Object.freeze({ mustInclude: ['kimi-code', 'code.kimi.com/kimi-code'] }),
|
|
@@ -135,6 +139,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
135
139
|
scriptFallbackUrls: ['https://downloads.claude.ai/claude-code-releases/bootstrap.sh'],
|
|
136
140
|
platforms: ['linux', 'darwin'],
|
|
137
141
|
scriptBinaryNames: ['claude'],
|
|
142
|
+
sharedDefaultBin: true,
|
|
138
143
|
verify: Object.freeze({ mustInclude: ['downloads.claude.ai/claude-code-releases', 'manifest.json'] }),
|
|
139
144
|
}),
|
|
140
145
|
scriptMethod('script-powershell', 'irm | iex', {
|
|
@@ -142,6 +147,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
142
147
|
scriptFallbackUrls: ['https://downloads.claude.ai/claude-code-releases/bootstrap.ps1'],
|
|
143
148
|
platforms: ['win32'],
|
|
144
149
|
scriptBinaryNames: ['claude.exe'],
|
|
150
|
+
sharedDefaultBin: true,
|
|
145
151
|
verify: Object.freeze({ mustInclude: ['downloads.claude.ai/claude-code-releases', 'manifest.json'] }),
|
|
146
152
|
}),
|
|
147
153
|
]),
|
|
@@ -167,6 +173,8 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
167
173
|
shell: 'sh', scriptUrl: 'https://chatgpt.com/codex/install.sh',
|
|
168
174
|
platforms: ['linux', 'darwin'], supportsInstallDir: true,
|
|
169
175
|
installDirEnv: 'CODEX_INSTALL_DIR', env: ['CODEX_INSTALL_DIR'],
|
|
176
|
+
installDirSubdir: 'codex/bin', legacySharedInstallDir: true,
|
|
177
|
+
sharedDefaultBin: true,
|
|
170
178
|
scriptBinaryNames: ['codex'],
|
|
171
179
|
verify: Object.freeze({ mustInclude: ['releases.openai.com/codex', 'CODEX_INSTALL_DIR'] }),
|
|
172
180
|
}),
|
|
@@ -210,6 +218,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
210
218
|
shell: 'bash', scriptUrl: 'https://filecdn.minimax.chat/public/install.sh',
|
|
211
219
|
platforms: ['linux', 'darwin'], supportsInstallDir: true,
|
|
212
220
|
installDirEnv: 'MCODE_INSTALL_DIR', installDirSuffix: 'bin',
|
|
221
|
+
installDirSubdir: 'minimax-code', legacySharedInstallDir: true,
|
|
213
222
|
registryEnv: 'MCODE_NPM_REGISTRY', env: ['MCODE_INSTALL_DIR', 'MCODE_NPM_REGISTRY'],
|
|
214
223
|
verify: Object.freeze({ mustInclude: ['MiniMax Code CLI', '@minimax-ai/code'] }),
|
|
215
224
|
}),
|
|
@@ -217,6 +226,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
217
226
|
shell: 'powershell', scriptUrl: 'https://filecdn.minimax.chat/public/install.ps1',
|
|
218
227
|
platforms: ['win32'], supportsInstallDir: true,
|
|
219
228
|
installDirEnv: 'MCODE_INSTALL_DIR',
|
|
229
|
+
installDirSubdir: 'minimax-code', legacySharedInstallDir: true,
|
|
220
230
|
registryEnv: 'MCODE_NPM_REGISTRY', env: ['MCODE_INSTALL_DIR', 'MCODE_NPM_REGISTRY'],
|
|
221
231
|
defaultScriptInstallDir: '.minimax-code',
|
|
222
232
|
scriptBinaryNames: ['mcode.cmd', 'mcode.ps1', 'mcode-tools.cmd', 'mcode-tools.ps1'],
|
|
@@ -250,6 +260,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
250
260
|
shell: 'powershell', scriptUrl: 'https://mimo.xiaomi.com/install.ps1',
|
|
251
261
|
platforms: ['win32'], supportsInstallDir: true,
|
|
252
262
|
installDirEnv: 'MIMOCODE_INSTALL_DIR', env: ['MIMOCODE_INSTALL_DIR'],
|
|
263
|
+
installDirSubdir: 'mimo-code', legacySharedInstallDir: true,
|
|
253
264
|
scriptBinaryNames: ['mimo.exe'],
|
|
254
265
|
verify: Object.freeze({ mustInclude: ['mimo.xiaomi.com', '.mimocode'] }),
|
|
255
266
|
}),
|
|
@@ -276,6 +287,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
276
287
|
shell: 'bash', scriptUrl: 'https://x.ai/cli/install.sh',
|
|
277
288
|
platforms: ['linux', 'darwin'], supportsInstallDir: true,
|
|
278
289
|
installDirEnv: 'GROK_BIN_DIR', env: ['GROK_BIN_DIR'],
|
|
290
|
+
installDirSubdir: 'grok-build', legacySharedInstallDir: true,
|
|
279
291
|
scriptBinaryNames: ['grok', 'agent'],
|
|
280
292
|
verify: Object.freeze({ mustInclude: ['Grok CLI installer', 'x.ai/cli'] }),
|
|
281
293
|
}),
|
|
@@ -283,6 +295,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
283
295
|
shell: 'powershell', scriptUrl: 'https://x.ai/cli/install.ps1',
|
|
284
296
|
platforms: ['win32'], supportsInstallDir: true,
|
|
285
297
|
installDirEnv: 'GROK_BIN_DIR', env: ['GROK_BIN_DIR'],
|
|
298
|
+
installDirSubdir: 'grok-build', legacySharedInstallDir: true,
|
|
286
299
|
scriptBinaryNames: ['grok.exe', 'agent.exe', 'grove.exe', 'grove-fsmonitor.exe', 'grove-credential.exe'],
|
|
287
300
|
verify: Object.freeze({ mustInclude: ['Grok CLI installer', 'x.ai/cli'] }),
|
|
288
301
|
}),
|
|
@@ -365,6 +378,9 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
365
378
|
installArg: ['--install-dir'],
|
|
366
379
|
env: ['STEP_INSTALL_DIR', 'STEP_RELEASE_BASE_URL'],
|
|
367
380
|
supportsInstallDir: true,
|
|
381
|
+
// The installer replaces its runtime directories, including the entire
|
|
382
|
+
// node_modules tree on Windows. It must own a dedicated directory.
|
|
383
|
+
installDirSubdir: 'stepcode', legacySharedInstallDir: true,
|
|
368
384
|
verify: Object.freeze({
|
|
369
385
|
mustInclude: ['static-openapi.stepfun.com', 'stepcode installer'],
|
|
370
386
|
mustNotInclude: ['anomalyco/opencode'],
|
|
@@ -377,6 +393,7 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
377
393
|
installArg: ['-InstallDir'],
|
|
378
394
|
env: ['STEP_INSTALL_DIR', 'STEP_RELEASE_BASE_URL'],
|
|
379
395
|
supportsInstallDir: true,
|
|
396
|
+
installDirSubdir: 'stepcode', legacySharedInstallDir: true,
|
|
380
397
|
verify: Object.freeze({
|
|
381
398
|
mustInclude: ['static-openapi.stepfun.com', 'stepcode'],
|
|
382
399
|
mustNotInclude: ['anomalyco/opencode'],
|
|
@@ -193,6 +193,7 @@ const jsonParameter = (name, codec) => Object.freeze({ name, wire: name, source:
|
|
|
193
193
|
|
|
194
194
|
export const OFFICIAL_TOOLS_REMOTE_DESCRIPTORS = Object.freeze([
|
|
195
195
|
descriptor('list', [], listResultCodec),
|
|
196
|
+
descriptor('installSettings', [], anyObjectCodec('OfficialInstallSettings')),
|
|
196
197
|
descriptor('installTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
|
|
197
198
|
descriptor('uninstallTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
|
|
198
199
|
descriptor('repairTool', [jsonParameter('request', toolActionCodec)], installResultCodec),
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
expandInstallDir,
|
|
28
28
|
resolveInstallLocation,
|
|
29
|
+
resolveLegacyScriptLocation,
|
|
29
30
|
resolveInstallPlan,
|
|
30
31
|
resolveUninstallPlan,
|
|
31
32
|
} from './tool-install-preferences.mjs'
|
|
@@ -56,7 +57,7 @@ export const CLAUDE_UPDATE_ARGS = Object.freeze(['update'])
|
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* Host-side install preferences (install directory, npm source, per-tool method
|
|
59
|
-
* and script source). Refreshed
|
|
60
|
+
* and script source). Refreshed on apply and live config updates, so a client
|
|
60
61
|
* can only ever pick a method id — these addresses never arrive over the wire.
|
|
61
62
|
*/
|
|
62
63
|
let preferences = { installDir: '', registry: '', methods: {}, scriptUrls: {}, allowScriptInstall: true }
|
|
@@ -308,16 +309,22 @@ async function fetchOfficialMiniMaxInstaller(signal) {
|
|
|
308
309
|
}
|
|
309
310
|
|
|
310
311
|
export function minimaxOfficialInstallDir(selectedPreferences = {}, home = homedir(), existingRoot = '') {
|
|
311
|
-
return selectedPreferences.installDir
|
|
312
|
+
return selectedPreferences.installDir
|
|
313
|
+
? resolveInstallPlan(getOfficialTool('minimax-code'), 'script-powershell', selectedPreferences, { platform: 'win32', home }).installDir
|
|
314
|
+
: existingRoot || join(home, '.minimax-code')
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
/** Keep a discovered native installation in place when its legacy updater runs. */
|
|
315
318
|
export function miniMaxFallbackPlan(selectedPreferences, beforeProbe, prefix, options = currentInstallOptions()) {
|
|
316
319
|
const installDir = minimaxOfficialInstallDir(selectedPreferences, options.home,
|
|
317
320
|
beforeProbe?.installed ? beforeProbe.installDir : '')
|
|
318
|
-
const executionPreferences = { ...selectedPreferences,
|
|
321
|
+
const executionPreferences = { ...selectedPreferences,
|
|
319
322
|
methods: { ...selectedPreferences.methods, 'minimax-code': 'script-powershell' } }
|
|
320
|
-
|
|
323
|
+
const plan = resolveInstallPlan(getOfficialTool('minimax-code'), 'script-powershell', selectedPreferences, options)
|
|
324
|
+
// installDir is already the discovered exclusive native root or the resolved
|
|
325
|
+
// child of the configured base. Never reinterpret it as another shared base.
|
|
326
|
+
return { plan: { ...plan, installDir, binDir: installDir,
|
|
327
|
+
env: { ...plan.env, MCODE_INSTALL_DIR: installDir } }, executionPreferences }
|
|
321
328
|
}
|
|
322
329
|
|
|
323
330
|
async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
@@ -330,6 +337,7 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
330
337
|
pushLine(job, reason || '— npm 原生依赖未就绪;改用 MiniMax 官方 Windows 安装脚本(安装最新版)—')
|
|
331
338
|
const { plan, executionPreferences } = miniMaxFallbackPlan(job.preferences, job.beforeProbe, '')
|
|
332
339
|
const installDir = plan.installDir
|
|
340
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
333
341
|
await mkdir(installDir, { recursive: true })
|
|
334
342
|
prefixRoot = await realpath(installDir)
|
|
335
343
|
const script = await fetchOfficialMiniMaxInstaller(job.controller.signal)
|
|
@@ -354,6 +362,7 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
354
362
|
+ 'Import-Module -Name $env:MODEL_ROUTER_UTILITY_MODULE -ErrorAction Stop; '
|
|
355
363
|
+ '& $env:MODEL_ROUTER_MINIMAX_INSTALL_SCRIPT'
|
|
356
364
|
job.phase = 'installing'
|
|
365
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
357
366
|
const outcome = await runCapture(powershell,
|
|
358
367
|
['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', command], {
|
|
359
368
|
timeoutMs: INSTALL_TIMEOUT_MS, signal: job.controller.signal,
|
|
@@ -378,7 +387,7 @@ async function runOfficialMiniMaxInstaller(job, { reason } = {}) {
|
|
|
378
387
|
probeCache.delete(job.tool.id)
|
|
379
388
|
const probe = await probeToolWith(job.tool, defaultRunner, { preferences: executionPreferences, plan, cacheMs: 0 })
|
|
380
389
|
job.postInstallProbe = probe
|
|
381
|
-
const readiness = await trustedExecutionReadiness(job.tool.id, { preferences: executionPreferences })
|
|
390
|
+
const readiness = await trustedExecutionReadiness(job.tool.id, { preferences: executionPreferences, plan })
|
|
382
391
|
if (!probe.installed || !readiness.ready) {
|
|
383
392
|
throw new Error(`官方安装器完成,但执行入口未通过 npm registry 摘要核验:${readiness.reason ?? probe.detail}`)
|
|
384
393
|
}
|
|
@@ -516,6 +525,95 @@ export function vendorScriptInvocation(plan, scriptPath) {
|
|
|
516
525
|
return { file: plan.shell === 'sh' ? 'sh' : 'bash', args: [scriptPath, ...directoryArgs] }
|
|
517
526
|
}
|
|
518
527
|
|
|
528
|
+
/** Resolve an existing ancestor too, so a missing child under a junction is checked. */
|
|
529
|
+
async function canonicalInstallPath(path) {
|
|
530
|
+
let current = resolve(path)
|
|
531
|
+
const suffix = []
|
|
532
|
+
for (;;) {
|
|
533
|
+
try { return join(await realpath(current), ...suffix) }
|
|
534
|
+
catch (error) {
|
|
535
|
+
if (error.code !== 'ENOENT') throw error
|
|
536
|
+
const parent = dirname(current)
|
|
537
|
+
if (parent === current) throw error
|
|
538
|
+
suffix.unshift(current.slice(parent.length).replace(/^[\\/]+/, ''))
|
|
539
|
+
current = parent
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** A vendor owns its target; its cleanup must never touch a shared npm prefix. */
|
|
545
|
+
export async function assertVendorInstallDirectory(tool, plan, selectedPreferences = {}, options = currentInstallOptions()) {
|
|
546
|
+
const directory = plan.installDir || plan.binDir
|
|
547
|
+
if (!directory) throw new Error('官方脚本安装目录无法确定,已停止安装。')
|
|
548
|
+
const canonical = await canonicalInstallPath(directory)
|
|
549
|
+
if (canonical.split(/[\\/]+/).some(part => ['node_modules', '.pnpm'].includes(part.toLowerCase()))) {
|
|
550
|
+
throw new Error('官方脚本不能写入 npm/pnpm 的包目录或共享存储目录。')
|
|
551
|
+
}
|
|
552
|
+
if (canonical === dirname(canonical) || canonical === await canonicalInstallPath(homedir())) {
|
|
553
|
+
throw new Error('官方脚本不能在磁盘根目录或用户主目录中执行。')
|
|
554
|
+
}
|
|
555
|
+
let codexOwnedRelease = false
|
|
556
|
+
if (tool.id === 'codex' && plan.shell === 'powershell') {
|
|
557
|
+
// The official Windows script keeps releases under the user's existing
|
|
558
|
+
// CODEX_HOME. CODEX_INSTALL_DIR selects only the visible bin junction.
|
|
559
|
+
// Respect that documented layout without redirecting the user's config.
|
|
560
|
+
const codexHome = options.env?.CODEX_HOME || join(options.home || homedir(), '.codex')
|
|
561
|
+
const homeRoot = await canonicalInstallPath(codexHome)
|
|
562
|
+
const releases = join(homeRoot, 'packages', 'standalone', 'releases')
|
|
563
|
+
const member = relative(releases, canonical)
|
|
564
|
+
const expectedVisible = !selectedPreferences.installDir || resolve(directory) === resolve(join(selectedPreferences.installDir, plan.method.installDirSubdir))
|
|
565
|
+
codexOwnedRelease = Boolean(expectedVisible && member && member !== '..' && !member.startsWith(`..${sep}`) && !isAbsolute(member))
|
|
566
|
+
}
|
|
567
|
+
if (selectedPreferences.installDir && plan.method?.supportsInstallDir) {
|
|
568
|
+
const base = await canonicalInstallPath(selectedPreferences.installDir)
|
|
569
|
+
const child = relative(base, canonical)
|
|
570
|
+
if (!codexOwnedRelease && (!child || child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child))) {
|
|
571
|
+
throw new Error('官方脚本必须使用统一安装根目录下的工具专属子目录;已拒绝覆盖共享目录。')
|
|
572
|
+
}
|
|
573
|
+
const owner = plan.method.installDirSubdir?.split(/[\\/]+/)[0]
|
|
574
|
+
const ownedRoot = owner ? join(base, owner) : null
|
|
575
|
+
const ownedChild = ownedRoot ? relative(ownedRoot, canonical) : '..'
|
|
576
|
+
if (!codexOwnedRelease && (ownedChild === '..' || ownedChild.startsWith(`..${sep}`) || isAbsolute(ownedChild))) {
|
|
577
|
+
throw new Error('官方脚本的目录联接必须留在本工具专属目录中,不能指向其他工具。')
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
// These fixed/default bin installers replace only their own launcher, not
|
|
581
|
+
// another CLI's runtime directory. Sharing ~/.local/bin is intentional.
|
|
582
|
+
if (plan.method?.sharedDefaultBin && (!selectedPreferences.installDir || !plan.method.supportsInstallDir)) return
|
|
583
|
+
const targetStat = await stat(directory).catch(error => {
|
|
584
|
+
if (error.code === 'ENOENT') return null
|
|
585
|
+
throw error
|
|
586
|
+
})
|
|
587
|
+
if (targetStat && !targetStat.isDirectory()) throw new Error('官方脚本安装目标不是目录。')
|
|
588
|
+
const packageText = await readFile(join(directory, 'package.json'), 'utf8').catch(error => {
|
|
589
|
+
if (error.code === 'ENOENT') return null
|
|
590
|
+
throw error
|
|
591
|
+
})
|
|
592
|
+
if (packageText) {
|
|
593
|
+
let name
|
|
594
|
+
try { name = JSON.parse(packageText).name } catch { throw new Error('安装目录已有无法核验归属的 package.json,已停止运行官方脚本。') }
|
|
595
|
+
const ownPackage = tool.package || (tool.id === 'stepcode' ? '@step-harness/coding-agent' : '')
|
|
596
|
+
if (!name || name !== ownPackage) throw new Error('安装目录已有其他工具或项目的 package.json,已停止运行官方脚本。')
|
|
597
|
+
}
|
|
598
|
+
for (const other of OFFICIAL_TOOLS) {
|
|
599
|
+
if (other.id === tool.id) continue
|
|
600
|
+
const commandDirectories = [...new Set([directory, plan.binDir].filter(Boolean))]
|
|
601
|
+
const candidates = commandDirectories.flatMap(dir => (other.probeExecutables ?? []).flatMap(name =>
|
|
602
|
+
[name, `${name}.exe`, `${name}.cmd`, `${name}.ps1`].map(file => join(dir, file))))
|
|
603
|
+
if (other.package) {
|
|
604
|
+
candidates.push(join(directory, 'node_modules', ...other.package.split('/'), 'package.json'))
|
|
605
|
+
candidates.push(join(directory, 'lib', 'node_modules', ...other.package.split('/'), 'package.json'))
|
|
606
|
+
}
|
|
607
|
+
for (const path of candidates) {
|
|
608
|
+
const found = await stat(path).catch(error => {
|
|
609
|
+
if (error.code === 'ENOENT' || error.code === 'ENOTDIR') return null
|
|
610
|
+
throw error
|
|
611
|
+
})
|
|
612
|
+
if (found?.isFile()) throw new Error(`安装目录中已有 ${other.label} 的文件,已拒绝运行可能替换其资源的官方脚本。请使用独立目录。`)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
519
617
|
/**
|
|
520
618
|
* Install one tool from its vendor script: download, verify, stage, run the
|
|
521
619
|
* staged copy. The binary lands in a directory the plugin appends to its own
|
|
@@ -527,6 +625,7 @@ async function runVendorScriptInstallJob(job, plan) {
|
|
|
527
625
|
let staging = null
|
|
528
626
|
try {
|
|
529
627
|
job.phase = 'preflight'
|
|
628
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
530
629
|
const before = await probeToolWith(job.tool, defaultRunner, { plan, preferences: job.preferences, cacheMs: 0 })
|
|
531
630
|
job.beforeProbe = before
|
|
532
631
|
const latestBefore = before.installed ? await latestVersions.lookup(job.tool, { fresh: true }).catch(() => null) : null
|
|
@@ -548,6 +647,7 @@ async function runVendorScriptInstallJob(job, plan) {
|
|
|
548
647
|
await writeFile(scriptPath, isPowerShell ? `\uFEFF${source}` : source, 'utf8')
|
|
549
648
|
const invocation = vendorScriptInvocation(plan, scriptPath)
|
|
550
649
|
job.phase = 'installing'
|
|
650
|
+
await assertVendorInstallDirectory(job.tool, plan, job.preferences)
|
|
551
651
|
pushLine(job, `$ ${plan.label}${invocation.args.length > 1 ? ` ${invocation.args.slice(1).join(' ')}` : ''}`)
|
|
552
652
|
const outcome = await runCapture(invocation.file, invocation.args, {
|
|
553
653
|
timeoutMs: INSTALL_TIMEOUT_MS,
|
|
@@ -827,6 +927,22 @@ async function probeUncached(tool, runner, { selectedPreferences, plan, fileExis
|
|
|
827
927
|
bannerLine: banner.split('\n').find(Boolean)?.slice(0, 200) ?? '' }
|
|
828
928
|
}
|
|
829
929
|
}
|
|
930
|
+
const legacy = resolveLegacyScriptLocation(tool, selectedPlan.method, selectedPreferences, currentInstallOptions())
|
|
931
|
+
if (legacy) {
|
|
932
|
+
for (const executable of tool.probeExecutables) {
|
|
933
|
+
const names = IS_WINDOWS ? [`${executable}.exe`, `${executable}.cmd`, executable] : [executable]
|
|
934
|
+
for (const name of names) {
|
|
935
|
+
const target = join(legacy.dir, name)
|
|
936
|
+
if (!(await fileExists(target))) continue
|
|
937
|
+
// Discovery is read-only. The old root can never satisfy a new job's
|
|
938
|
+
// target probe or become a script update/uninstall destination.
|
|
939
|
+
return { id: tool.id, installed: false, version: null, status: 'legacy-install',
|
|
940
|
+
installDir: selectedPlan.installDir, binDir: selectedPlan.binDir,
|
|
941
|
+
legacyExecutablePath: target,
|
|
942
|
+
detail: `发现旧共享目录中的 ${tool.label}:${target}。请安装到独立目录 ${selectedPlan.installDir};旧文件会保留,不能用旧目录覆盖其他工具。` }
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
}
|
|
830
946
|
return { id: tool.id, installed: false, version: null, status: 'not-installed',
|
|
831
947
|
installDir: selectedPlan.installDir, binDir: selectedPlan.binDir,
|
|
832
948
|
detail: `未在所选安装目录 ${selectedPlan.binDir} 中找到官方命令。` }
|
|
@@ -1006,6 +1122,7 @@ function jobView(job) {
|
|
|
1006
1122
|
exitCode: job.exitCode,
|
|
1007
1123
|
timedOut: job.timedOut,
|
|
1008
1124
|
error: job.error,
|
|
1125
|
+
executionReadiness: job.executionReadiness ?? null,
|
|
1009
1126
|
outputTail: [...job.outputTail],
|
|
1010
1127
|
outputTruncated: job.outputTruncated,
|
|
1011
1128
|
postInstallProbe: job.postInstallProbe,
|
|
@@ -1272,8 +1389,10 @@ export function installEnvironment(plan, base = process.env) {
|
|
|
1272
1389
|
return env
|
|
1273
1390
|
}
|
|
1274
1391
|
|
|
1275
|
-
/**
|
|
1276
|
-
async function finishVerifiedInstall(job, probe
|
|
1392
|
+
/** Installation and version must pass; verify execution only where an adapter supports it. */
|
|
1393
|
+
export async function finishVerifiedInstall(job, probe, {
|
|
1394
|
+
lookupLatest = tool => latestVersions.lookup(tool), readinessFor = trustedExecutionReadiness,
|
|
1395
|
+
} = {}) {
|
|
1277
1396
|
job.postInstallProbe = probe
|
|
1278
1397
|
if (job.cancelRequested) { markCancelled(job); return true }
|
|
1279
1398
|
if (!probe.installed) {
|
|
@@ -1282,14 +1401,15 @@ async function finishVerifiedInstall(job, probe) {
|
|
|
1282
1401
|
job.error = `安装命令已完成,但重探测失败:${probe.detail}`
|
|
1283
1402
|
return true
|
|
1284
1403
|
}
|
|
1285
|
-
const latest = await
|
|
1404
|
+
const latest = await lookupLatest(job.tool).catch(() => null)
|
|
1286
1405
|
if (latest?.version && versionStillOlder(probe.version, latest.version)) {
|
|
1287
1406
|
failBecauseStillOlder(job, probe, latest.version)
|
|
1288
1407
|
return true
|
|
1289
1408
|
}
|
|
1290
|
-
const readiness = await
|
|
1409
|
+
const readiness = await readinessFor(job.tool.id, { preferences: job.preferences, plan: job.plan })
|
|
1410
|
+
job.executionReadiness = readiness
|
|
1291
1411
|
if (job.cancelRequested) { markCancelled(job); return true }
|
|
1292
|
-
if (!readiness.ready) {
|
|
1412
|
+
if (!readiness.ready && readiness.supported !== false) {
|
|
1293
1413
|
if (IS_WINDOWS && job.tool.id === 'minimax-code' && !job.configured) {
|
|
1294
1414
|
await runOfficialMiniMaxInstaller(job)
|
|
1295
1415
|
return true
|
|
@@ -1300,8 +1420,11 @@ async function finishVerifiedInstall(job, probe) {
|
|
|
1300
1420
|
return true
|
|
1301
1421
|
}
|
|
1302
1422
|
job.status = 'succeeded'
|
|
1423
|
+
job.error = null
|
|
1303
1424
|
job.finishedAt = new Date().toISOString()
|
|
1304
|
-
pushLine(job,
|
|
1425
|
+
pushLine(job, readiness.supported === false
|
|
1426
|
+
? `— 已安装 ${job.tool.label} ${probe.version ?? ''},版本探测已通过;受限执行未启用:${readiness.reason} —`
|
|
1427
|
+
: `— 已安装 ${job.tool.label} ${probe.version ?? ''},官方执行入口已核验(新版本未经插件测试)—`)
|
|
1305
1428
|
return true
|
|
1306
1429
|
}
|
|
1307
1430
|
|
|
@@ -1378,11 +1501,13 @@ async function runInstallJob(installPlan, job) {
|
|
|
1378
1501
|
// exactly the case the user pressed 修复 for.
|
|
1379
1502
|
if (job.repair) {
|
|
1380
1503
|
pushLine(job, `— ${job.tool.label} ${before.version} 已是最新版本,按修复流程重新安装 —`)
|
|
1381
|
-
} else if (readiness.ready || !CAPABILITY_REPAIRABLE.has(job.tool.id)) {
|
|
1504
|
+
} else if (readiness.ready || readiness.supported === false || !CAPABILITY_REPAIRABLE.has(job.tool.id)) {
|
|
1382
1505
|
job.status = 'succeeded'
|
|
1383
1506
|
job.finishedAt = new Date().toISOString()
|
|
1384
1507
|
job.postInstallProbe = before
|
|
1508
|
+
job.executionReadiness = readiness
|
|
1385
1509
|
pushLine(job, `— ${job.tool.label} ${before.version} 已是最新版本,无需重复安装 —`)
|
|
1510
|
+
if (readiness.supported === false) pushLine(job, `— 受限执行未启用:${readiness.reason} —`)
|
|
1386
1511
|
return
|
|
1387
1512
|
} else {
|
|
1388
1513
|
pushLine(job, `— 已是最新版本 ${before.version},但执行入口未就绪:${readiness.reason};重新安装修复 —`)
|
|
@@ -14,8 +14,11 @@
|
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { StringDecoder } from 'node:string_decoder'
|
|
16
16
|
import { realpath, stat } from 'node:fs/promises'
|
|
17
|
-
import { isAbsolute, win32 } from 'node:path'
|
|
17
|
+
import { dirname, isAbsolute, join, relative, sep, win32 } from 'node:path'
|
|
18
18
|
import { toolForProvider } from './official-tool-registry.mjs'
|
|
19
|
+
import { findGlobalPackageEntries, findNodeExecutable, officialToolEntrySelection } from './official-tool-executor.mjs'
|
|
20
|
+
import { installedModuleRoots } from './vendor-mimo-grok-adapter.mjs'
|
|
21
|
+
import { CODE_FILE, npmAttestor } from './npm-attestation.mjs'
|
|
19
22
|
import { normalizeExecutionPreference } from './model-profiles.mjs'
|
|
20
23
|
import { BILLING_MODE_LABEL, billingPlan, detectQuotaExhaustion, vendorKey } from './subscription-billing.mjs'
|
|
21
24
|
|
|
@@ -666,6 +669,45 @@ function officialSuccess({ route, adapter, preference, answer, exitCode, version
|
|
|
666
669
|
}
|
|
667
670
|
}
|
|
668
671
|
|
|
672
|
+
/** Gemini's Windows npm shim cannot be spawned without a shell; use its selected package bin. */
|
|
673
|
+
export async function resolvePortableToolLaunch(adapter, workspace, {
|
|
674
|
+
selection = adapter?.id === 'gemini' ? officialToolEntrySelection('gemini') : null,
|
|
675
|
+
findEntries = findGlobalPackageEntries, findNode = findNodeExecutable, attestor = npmAttestor,
|
|
676
|
+
} = {}) {
|
|
677
|
+
if (adapter?.id !== 'gemini') return { file: adapter.executable, args: [] }
|
|
678
|
+
const entries = await findEntries('@google/gemini-cli', 'gemini', 'dist/index.js', workspace, { selection })
|
|
679
|
+
const found = entries[0]
|
|
680
|
+
if (!found) return { reason: '未在所选安装目录中找到 @google/gemini-cli 的 gemini 入口;已回退模型目录 API。' }
|
|
681
|
+
const inside = (root, entry) => {
|
|
682
|
+
const part = relative(root, entry)
|
|
683
|
+
return part !== '' && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
|
|
684
|
+
}
|
|
685
|
+
const roots = Array.isArray(selection?.plan?.packageModuleRoots) ? selection.plan.packageModuleRoots
|
|
686
|
+
: await installedModuleRoots(selection?.directories ?? [], { manager: selection?.plan?.manager })
|
|
687
|
+
const prefix = selection?.plan?.installDir ? await realpath(selection.plan.installDir).catch(() => null) : null
|
|
688
|
+
if (selection?.plan?.installDir && !prefix) return { reason: 'Gemini 所选安装根无法确认,已拒绝启动并回退模型目录 API。' }
|
|
689
|
+
let contained = false
|
|
690
|
+
for (const declared of roots) {
|
|
691
|
+
const boundary = await realpath(declared).catch(() => null)
|
|
692
|
+
const parent = await realpath(dirname(declared)).catch(() => null)
|
|
693
|
+
if (!boundary || !parent || !inside(parent, boundary) || (prefix && !inside(prefix, boundary))) continue
|
|
694
|
+
if (inside(boundary, found.packageRoot) && inside(found.packageRoot, found.entry)
|
|
695
|
+
&& !inside(workspace, found.packageRoot) && found.packageRoot !== workspace) { contained = true; break }
|
|
696
|
+
}
|
|
697
|
+
if (!contained) return { reason: 'Gemini 包或入口离开了所选安装目录的包存储树,已拒绝启动并回退模型目录 API。' }
|
|
698
|
+
// The manifest controls bin/import resolution, so attest it as well as all
|
|
699
|
+
// package code. Checking name/version or a signed Node alone is insufficient.
|
|
700
|
+
const include = new RegExp(`${CODE_FILE.source}|(?:^|/)package\\.json$`, 'i')
|
|
701
|
+
const checked = await attestor.matchesPackage(found.packageRoot, '@google/gemini-cli', found.version, include)
|
|
702
|
+
if (!checked.ok) return { reason: `Gemini 安装文件未通过官方 npm 摘要核验:${checked.reason};已回退模型目录 API。` }
|
|
703
|
+
const member = relative(found.packageRoot, found.entry).split(sep).join('/')
|
|
704
|
+
const entryChecked = await attestor.matches(found.entry, '@google/gemini-cli', found.version, member)
|
|
705
|
+
if (!entryChecked.ok) return { reason: `Gemini 入口未通过官方 npm 摘要核验:${entryChecked.reason};已回退模型目录 API。` }
|
|
706
|
+
const node = await findNode(workspace, [20, 0, 0])
|
|
707
|
+
if (!node) return { reason: '未找到工作区外的 Node.js 20+ 可执行文件;已回退模型目录 API。' }
|
|
708
|
+
return { file: node, args: [found.entry], version: found.version, source: 'official-npm-registry-attested' }
|
|
709
|
+
}
|
|
710
|
+
|
|
669
711
|
/**
|
|
670
712
|
* Run one assigned model task. `auto` and `official` try the vendor CLI first.
|
|
671
713
|
* `api` skips the CLI. A missing tool or a failed process uses `apiFallback`.
|
|
@@ -673,6 +715,7 @@ function officialSuccess({ route, adapter, preference, answer, exitCode, version
|
|
|
673
715
|
export async function executeAssignedTask({
|
|
674
716
|
route, task, workspace, timeoutMs, signal, credentials = null, spawnImpl = spawn,
|
|
675
717
|
apiFallback, runVerified = null, onChunk = null, skipOfficial = null, stopGraceMs, credentialMode = 'default',
|
|
718
|
+
resolvePortableLaunch = resolvePortableToolLaunch,
|
|
676
719
|
} = {}) {
|
|
677
720
|
const prompt = checkedTask(task)
|
|
678
721
|
const preference = executionPreference(route)
|
|
@@ -734,7 +777,13 @@ export async function executeAssignedTask({
|
|
|
734
777
|
})
|
|
735
778
|
}
|
|
736
779
|
const { env, secret, credentialSource } = childEnvironment(adapter, credentials, credentialMode)
|
|
737
|
-
|
|
780
|
+
let launch
|
|
781
|
+
try { launch = await resolvePortableLaunch(adapter, cwd) } catch (error) {
|
|
782
|
+
launch = { reason: `${adapter.label} 的安装入口无法读取:${String(error?.message ?? error).slice(0, 200)};已回退模型目录 API。` }
|
|
783
|
+
}
|
|
784
|
+
if (!launch?.file) return useApi(apiFallback, { route, task: prompt, signal, adapter, preference,
|
|
785
|
+
reason: launch?.reason ?? `${adapter.label} 未安装或无法启动,已回退模型目录 API。` })
|
|
786
|
+
const probe = await captureProcess(spawnImpl, launch.file, [...launch.args, '--version'], {
|
|
738
787
|
cwd, env, stdin: '', timeoutMs: Math.min(PROBE_TIMEOUT_MS, timeout), signal, secret,
|
|
739
788
|
})
|
|
740
789
|
if (probe.spawnError || probe.cancelled || probe.timedOut || probe.exitCode !== 0) {
|
|
@@ -748,7 +797,7 @@ export async function executeAssignedTask({
|
|
|
748
797
|
return useApi(apiFallback, { route, task: prompt, signal, adapter, preference, reason })
|
|
749
798
|
}
|
|
750
799
|
const args = adapter.buildArgs(modelId)
|
|
751
|
-
const run = await captureProcess(spawnImpl,
|
|
800
|
+
const run = await captureProcess(spawnImpl, launch.file, [...launch.args, ...args], {
|
|
752
801
|
cwd, env, stdin: prompt, timeoutMs: timeout, signal, onChunk, secret,
|
|
753
802
|
...(Number.isSafeInteger(stopGraceMs) && stopGraceMs >= 10 && stopGraceMs <= STOP_GRACE_MS ? { stopGraceMs } : {}),
|
|
754
803
|
})
|
|
@@ -249,12 +249,17 @@ export function resolveInstallPlan(tool, methodId, preferences = {}, { platform
|
|
|
249
249
|
const binDir = target
|
|
250
250
|
? (method.installDirSuffix ? joinPath(target, method.installDirSuffix) : target)
|
|
251
251
|
: vendorScriptDir(tool, { home, env, method })
|
|
252
|
-
const notices = target
|
|
252
|
+
const notices = target && installDir && method.installDirSubdir
|
|
253
|
+
? [`官方脚本在工具专属子目录 ${target} 维护资源,保护统一安装根中的其他工具。`]
|
|
254
|
+
: target ? [] : installDir
|
|
253
255
|
? [`${method.label} 无法使用自定义安装目录(官方脚本写死了安装路径),将安装到 ${vendorScriptDir(tool, { home, env, method })}。`]
|
|
254
256
|
: []
|
|
255
257
|
if (method.registryEnv === 'MCODE_NPM_REGISTRY' && preferences.registry && !minimaxScriptRegistry(preferences.registry)) {
|
|
256
258
|
notices.push('MiniMax 官方脚本只接受 https://registry.npmjs.org/ 或 https://registry.npmmirror.com/,本次不改写它的 npm 源。')
|
|
257
259
|
}
|
|
260
|
+
if (tool.id === 'codex' && method.id === 'script-powershell') {
|
|
261
|
+
notices.push('Codex Windows 脚本将命令目录联接到用户 Codex 资源目录;统一根控制命令位置,运行资源仍由厂商保存在用户目录。需要所有文件位于统一根时请选 npm/pnpm。')
|
|
262
|
+
}
|
|
258
263
|
return {
|
|
259
264
|
toolId: tool.id,
|
|
260
265
|
method,
|
|
@@ -332,6 +337,20 @@ export function resolveInstallLocation(tool, method, preferences = {}, { home =
|
|
|
332
337
|
return { kind: 'package-manager', dir: packageManagerBinDir(preferences?.installDir || '', { platform }), installDir: preferences?.installDir || '', binaries: [] }
|
|
333
338
|
}
|
|
334
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Read-only compatibility location for scripts installed directly into a
|
|
342
|
+
* configured shared root before dedicated script directories were introduced.
|
|
343
|
+
* Never use this result as an install/update target: those installers can
|
|
344
|
+
* replace a whole runtime tree belonging to another tool.
|
|
345
|
+
*/
|
|
346
|
+
export function resolveLegacyScriptLocation(tool, method, preferences = {}, options = {}) {
|
|
347
|
+
if (!preferences.installDir || !method?.legacySharedInstallDir || !methodHonoursInstallDir(method)) return null
|
|
348
|
+
return {
|
|
349
|
+
...resolveInstallLocation(tool, { ...method, installDirSubdir: undefined }, preferences, options),
|
|
350
|
+
legacySharedInstallDir: true,
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
335
354
|
function scriptEnv(method, { installDir, scriptUrl, registry }) {
|
|
336
355
|
const env = {}
|
|
337
356
|
if (installDir && method.installDirEnv) env[method.installDirEnv] = installDir
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.18.2 — 2026-10-12
|
|
4
|
+
|
|
5
|
+
Stable patch under the npm latest tag. Upgrade and fully restart Harness before using Step install, repair or update.
|
|
6
|
+
|
|
7
|
+
- Treat the configured install directory as a shared base. Configurable vendor scripts use dedicated tool subdirectories; npm/pnpm retain their existing shared prefix. Step's official installer replaces its entire runtime directories, so passing the shared root could delete every other npm package there.
|
|
8
|
+
- Check script targets before download or execution: refuse shared roots, other tool/project manifests and commands, package stores, and junctions into another tool's tree. Codex's junction within its own release tree and intentional shared default launcher directories remain supported. Legacy script roots are discovered read-only and cannot satisfy or redirect new install jobs.
|
|
9
|
+
- Separate successful version/install verification from unsupported restricted execution capabilities. Gemini, OpenCode and Step can be installed successfully while retaining explicit execution limitations; supported tools still require their original trusted-entry checks.
|
|
10
|
+
- Resolve Windows Gemini through its selected npm/pnpm package bin and a verified Node executable. Check package-tree containment and official registry file digests before launch, and preserve API fallback when the selected copy is missing or fails verification.
|
|
11
|
+
- Add regressions for destructive vendor runtime replacement, shared-prefix sentinels, junctions, legacy discovery, install capability status and Windows Gemini entry selection. Release notes record validation and the original-version recovery performed on the reported machine.
|
|
12
|
+
|
|
13
|
+
## 0.18.1 — 2026-10-12
|
|
14
|
+
|
|
15
|
+
Stable patch under the npm latest tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading.
|
|
16
|
+
|
|
17
|
+
- Fix official-tool method selection stuck on saving: configuration persistence no longer waits for CLI inspection, and selectors reflect the acknowledged settings snapshot.
|
|
18
|
+
- Refresh install commands through a lightweight Host endpoint; delayed tool inspections cannot overwrite a newer install preview. Install actions wait for confirmed settings while method selection remains available.
|
|
19
|
+
- Synchronize volatile install settings into Host preferences without requiring plugin reload. Invalid configuration is reported and cannot start an operation with older settings.
|
|
20
|
+
- Add regression coverage using the real settings controller and Loader, plus a mock UI scenario with a hanging tool inspection. Release validation is recorded in the release notes.
|
|
21
|
+
|
|
3
22
|
## 0.18.0 — 2026-10-12
|
|
4
23
|
|
|
5
24
|
Stable minor under the npm latest tag. Fully quit Harness, including the tray or WebUI host process, and restart after upgrading. Existing releases remain unchanged.
|