@ljwei-stak/dsh-model-router 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.dsh-plugin/client.js +210 -43
- package/.dsh-plugin/index.mjs +14 -7
- package/.dsh-plugin/official-tools-remote-service.mjs +7 -3
- package/.dsh-plugin/shared/cli-terminal-protocol.mjs +15 -5
- package/.dsh-plugin/shared/cli-terminal.mjs +126 -18
- package/.dsh-plugin/shared/official-tool-executor.mjs +7 -7
- package/.dsh-plugin/shared/official-tool-registry.mjs +20 -3
- package/.dsh-plugin/shared/official-tools-runtime.mjs +169 -24
- package/.dsh-plugin/shared/task-executors.mjs +52 -3
- package/.dsh-plugin/shared/tool-install-preferences.mjs +42 -1
- package/.dsh-plugin/shared/zcode-bundle.mjs +29 -8
- package/.dsh-plugin/shared/zcode-installer.mjs +2 -2
- package/CHANGELOG.md +21 -0
- package/README.md +10 -6
- package/README.zh.md +10 -6
- package/docs/INSTALLATION_GUIDE.zh.md +12 -12
- package/docs/WORKBENCH_USER_GUIDE.zh.md +13 -7
- package/package.json +2 -2
package/.dsh-plugin/index.mjs
CHANGED
|
@@ -95,6 +95,7 @@ export const Config = z.object({
|
|
|
95
95
|
toolInstallMethodsJson: z.string().max(8_000).default('{}').volatile(),
|
|
96
96
|
toolScriptUrlsJson: z.string().max(8_000).default('{}').volatile(),
|
|
97
97
|
toolAllowScriptInstall: z.boolean().default(true).volatile(),
|
|
98
|
+
zcodeInstallDir: z.string().max(4_096).default('').volatile(),
|
|
98
99
|
})
|
|
99
100
|
|
|
100
101
|
const JSON_OUTPUT = {
|
|
@@ -1387,6 +1388,8 @@ export async function startWorkbenchRun(ctx, config, request = {}, options = {})
|
|
|
1387
1388
|
export function createRouterTerminals(options = {}) {
|
|
1388
1389
|
return createTerminalManager({
|
|
1389
1390
|
prepare: () => ensureNpmPrefixOnPath(),
|
|
1391
|
+
preferences: () => installPreferences(),
|
|
1392
|
+
launchOptions: () => currentInstallOptions(),
|
|
1390
1393
|
onSessionEnd: record => { void routerStateStore().appendTerminalSession(record).catch(() => {}) },
|
|
1391
1394
|
...options,
|
|
1392
1395
|
})
|
|
@@ -1397,11 +1400,14 @@ async function terminalHistory(limit = 20) {
|
|
|
1397
1400
|
return (saved.terminalSessions ?? []).slice(-limit).reverse()
|
|
1398
1401
|
}
|
|
1399
1402
|
|
|
1400
|
-
function terminalServices(terminals) {
|
|
1403
|
+
function terminalServices(terminals, assertPreferences = () => {}) {
|
|
1401
1404
|
if (!terminals) return {}
|
|
1402
1405
|
return {
|
|
1403
|
-
terminalInfo: async () =>
|
|
1404
|
-
|
|
1406
|
+
terminalInfo: async () => {
|
|
1407
|
+
const tools = await probeAllTools({ fresh: true })
|
|
1408
|
+
return { ...terminals.info(), tools, toolsCheckedAt: Date.now(), history: await terminalHistory() }
|
|
1409
|
+
},
|
|
1410
|
+
terminalStart: async request => { assertPreferences(); return terminals.start(parseTerminalStart(request)) },
|
|
1405
1411
|
terminalRead: async request => terminals.read(parseTerminalRead(request)),
|
|
1406
1412
|
terminalWrite: async request => terminals.write(parseTerminalWrite(request)),
|
|
1407
1413
|
terminalResize: async request => terminals.resize(parseTerminalResize(request)),
|
|
@@ -1409,9 +1415,9 @@ function terminalServices(terminals) {
|
|
|
1409
1415
|
}
|
|
1410
1416
|
}
|
|
1411
1417
|
|
|
1412
|
-
export function routerRemoteServices(ctx, config, { terminals = null } = {}) {
|
|
1418
|
+
export function routerRemoteServices(ctx, config, { terminals = null, assertPreferences } = {}) {
|
|
1413
1419
|
return {
|
|
1414
|
-
...terminalServices(terminals),
|
|
1420
|
+
...terminalServices(terminals, assertPreferences),
|
|
1415
1421
|
health: async fresh => {
|
|
1416
1422
|
const report = await toolHealthReport({ fresh: fresh === true })
|
|
1417
1423
|
const saved = await savedState()
|
|
@@ -1444,7 +1450,7 @@ export function apply(ctx, config = {}) {
|
|
|
1444
1450
|
installPreferenceError = null
|
|
1445
1451
|
} catch (error) { installPreferenceError = errorText(error) }
|
|
1446
1452
|
}
|
|
1447
|
-
const installFields = new Set(['toolInstallDir', 'toolNpmRegistry', 'toolInstallMethodsJson', 'toolScriptUrlsJson', 'toolAllowScriptInstall'])
|
|
1453
|
+
const installFields = new Set(['toolInstallDir', 'toolNpmRegistry', 'toolInstallMethodsJson', 'toolScriptUrlsJson', 'toolAllowScriptInstall', 'zcodeInstallDir'])
|
|
1448
1454
|
syncInstallPreferences()
|
|
1449
1455
|
// Loader scopes this event to the fiber whose refs were committed.
|
|
1450
1456
|
ctx.on('loader/volatile-update', paths => {
|
|
@@ -1464,9 +1470,10 @@ export function apply(ctx, config = {}) {
|
|
|
1464
1470
|
return () => { process.off('exit', onExit); terminals.disposeAll('dispose') }
|
|
1465
1471
|
}, 'model-router-galgame: interactive terminals')
|
|
1466
1472
|
registerOfficialToolsRemote(ctx, {
|
|
1467
|
-
...routerRemoteServices(ctx, config, { terminals }),
|
|
1473
|
+
...routerRemoteServices(ctx, config, { terminals, assertPreferences: assertInstallPreferences }),
|
|
1468
1474
|
installSettingsError: () => installPreferenceError,
|
|
1469
1475
|
installRequestedDirectory: () => valueOf(config, 'toolInstallDir', ''),
|
|
1476
|
+
zcodeRequestedDirectory: () => valueOf(config, 'zcodeInstallDir', ''),
|
|
1470
1477
|
})
|
|
1471
1478
|
}
|
|
1472
1479
|
ctx.on('tools/pre-execute', async (exec, next) => {
|
|
@@ -44,6 +44,7 @@ const unavailable = () => { throw new Error('模型路由工作台服务尚未
|
|
|
44
44
|
export function installSummary(preferences = installPreferences(), options = currentInstallOptions()) {
|
|
45
45
|
return {
|
|
46
46
|
installDir: preferences.installDir ?? '',
|
|
47
|
+
zcodeInstallDir: preferences.zcodeInstallDir ?? '',
|
|
47
48
|
registry: preferences.registry ?? '',
|
|
48
49
|
allowScriptInstall: preferences.allowScriptInstall !== false,
|
|
49
50
|
scriptUrls: { ...(preferences.scriptUrls ?? {}) },
|
|
@@ -61,12 +62,14 @@ export function installSummary(preferences = installPreferences(), options = cur
|
|
|
61
62
|
}
|
|
62
63
|
if (tool.manager === 'signed-windows-installer') {
|
|
63
64
|
entry.methodId = 'signed-windows-installer'
|
|
65
|
+
entry.installDir = preferences.zcodeInstallDir ?? ''
|
|
64
66
|
if (options.platform === 'win32') {
|
|
65
67
|
entry.command = installCommandLine(tool)
|
|
66
68
|
entry.updateCommand = entry.command
|
|
67
|
-
entry.notices = [preferences.
|
|
68
|
-
?
|
|
69
|
-
: '
|
|
69
|
+
entry.notices = [preferences.zcodeInstallDir
|
|
70
|
+
? `TUI 使用已保存的 ZCode 桌面目录 ${preferences.zcodeInstallDir}。更新时请在官方安装窗口选择同一目录,完成后重新体检。`
|
|
71
|
+
: '请在下方单独配置 ZCode 桌面目录,或留空自动检测。内置 CLI 随桌面版更新,TUI 的登录、模型和插件在终端中自行配置。']
|
|
72
|
+
entry.notices.push('ZCode 桌面目录独立于统一 npm 安装根;不会把桌面资源放进其他工具的共享目录。')
|
|
70
73
|
} else entry.installError = entry.error = 'ZCode 签名安装器仅支持 Windows。'
|
|
71
74
|
entry.uninstallError = 'ZCode 是桌面安装版,请在系统“应用”中卸载它。'
|
|
72
75
|
return entry
|
|
@@ -191,6 +194,7 @@ export class OfficialToolsRemoteService extends TypertRemoteService {
|
|
|
191
194
|
// The saved input can contain Host-only home/environment expansions. Keep
|
|
192
195
|
// it separate from the resolved directory used by the actual command.
|
|
193
196
|
install.requestedInstallDir = this.services?.installRequestedDirectory?.() ?? install.installDir
|
|
197
|
+
install.requestedZCodeInstallDir = this.services?.zcodeRequestedDirectory?.() ?? install.zcodeInstallDir
|
|
194
198
|
return { install, hostVersion: HOST_PLUGIN_VERSION }
|
|
195
199
|
}
|
|
196
200
|
|
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* into that interactive process, exactly as in their own terminal, so a
|
|
10
10
|
* session starts only after an explicit confirmation.
|
|
11
11
|
*/
|
|
12
|
-
import { getOfficialTool } from './official-tool-registry.mjs'
|
|
12
|
+
import { getOfficialTool, OFFICIAL_TOOLS } from './official-tool-registry.mjs'
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
export const TERMINAL_TOOL_IDS = Object.freeze(
|
|
14
|
+
/** Follow the tool registry, including the TUI bundled with ZCode desktop. */
|
|
15
|
+
export const TERMINAL_TOOL_IDS = Object.freeze(OFFICIAL_TOOLS
|
|
16
|
+
.filter(tool => !tool.unsupported && (tool.probeExecutables?.length || tool.manager === 'signed-windows-installer'))
|
|
17
|
+
.map(tool => tool.id))
|
|
16
18
|
export const TERMINAL_SHELL_ID = 'shell'
|
|
17
19
|
export const TERMINAL_TARGETS = Object.freeze([TERMINAL_SHELL_ID, ...TERMINAL_TOOL_IDS])
|
|
18
20
|
export const TERMINAL_MODES = Object.freeze(['interactive', 'login'])
|
|
@@ -46,8 +48,16 @@ export const TERMINAL_LOGIN_ARGS = Object.freeze({
|
|
|
46
48
|
'mimo-code': Object.freeze(['auth', 'login']),
|
|
47
49
|
'grok-build': Object.freeze(['login']),
|
|
48
50
|
gemini: Object.freeze([]),
|
|
51
|
+
opencode: Object.freeze(['auth', 'login']),
|
|
52
|
+
stepcode: Object.freeze(['login']),
|
|
53
|
+
zcode: Object.freeze([]),
|
|
49
54
|
})
|
|
50
55
|
|
|
56
|
+
/** Tools without a separate login command authenticate from their TUI. */
|
|
57
|
+
export function terminalCanLogin(target) {
|
|
58
|
+
return Boolean(TERMINAL_LOGIN_ARGS[target]?.length)
|
|
59
|
+
}
|
|
60
|
+
|
|
51
61
|
const SESSION_ID = /^term-[a-z0-9]{6,40}$/
|
|
52
62
|
|
|
53
63
|
function plainObject(value, subject) {
|
|
@@ -82,7 +92,7 @@ export function terminalTargetLabel(target) {
|
|
|
82
92
|
export function terminalCommandPreview(target, mode = 'interactive', platform = 'win32') {
|
|
83
93
|
if (target === TERMINAL_SHELL_ID) return platform === 'win32' ? 'powershell.exe -NoLogo' : '$SHELL -l'
|
|
84
94
|
const tool = getOfficialTool(target)
|
|
85
|
-
const executable = tool?.probeExecutables?.[0] ?? String(target)
|
|
95
|
+
const executable = target === 'zcode' ? 'ZCode 桌面版内置 TUI' : tool?.probeExecutables?.[0] ?? String(target)
|
|
86
96
|
const args = mode === 'login' ? TERMINAL_LOGIN_ARGS[target] ?? [] : []
|
|
87
97
|
return [executable, ...args].join(' ')
|
|
88
98
|
}
|
|
@@ -92,7 +102,7 @@ export function parseTerminalStart(value) {
|
|
|
92
102
|
if (!TERMINAL_TARGETS.includes(request.target)) throw new TypeError('target must be the shell or a fixed official CLI')
|
|
93
103
|
const mode = request.mode === undefined ? 'interactive' : request.mode
|
|
94
104
|
if (!TERMINAL_MODES.includes(mode)) throw new TypeError('mode must be interactive or login')
|
|
95
|
-
if (mode === 'login' && request.target
|
|
105
|
+
if (mode === 'login' && !terminalCanLogin(request.target)) throw new TypeError('this target has no login mode')
|
|
96
106
|
if (!isAbsoluteDirectoryText(request.cwd)) throw new TypeError('cwd must be an absolute directory path')
|
|
97
107
|
if (request.confirmed !== true) throw new TypeError('starting a terminal requires the user\'s confirmation')
|
|
98
108
|
return {
|
|
@@ -13,11 +13,14 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { spawn } from 'node:child_process'
|
|
15
15
|
import { randomBytes } from 'node:crypto'
|
|
16
|
-
import { stat } from 'node:fs/promises'
|
|
16
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises'
|
|
17
17
|
import { createRequire } from 'node:module'
|
|
18
18
|
import { StringDecoder } from 'node:string_decoder'
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
19
|
+
import { homedir } from 'node:os'
|
|
20
|
+
import { extname, isAbsolute, join, relative, sep } from 'node:path'
|
|
21
|
+
import { getOfficialTool, installMethodFor } from './official-tool-registry.mjs'
|
|
22
|
+
import { resolveInstallPlan } from './tool-install-preferences.mjs'
|
|
23
|
+
import { discoverZCodeBundle } from './zcode-bundle.mjs'
|
|
21
24
|
import {
|
|
22
25
|
TERMINAL_LIMITS,
|
|
23
26
|
TERMINAL_LOGIN_ARGS,
|
|
@@ -84,6 +87,75 @@ export function pickWindowsExecutable(candidates) {
|
|
|
84
87
|
|
|
85
88
|
const quoteWindows = path => `"${String(path).replace(/"/g, '')}"`
|
|
86
89
|
|
|
90
|
+
function within(root, child) {
|
|
91
|
+
const part = relative(root, child)
|
|
92
|
+
return Boolean(part) && part !== '..' && !part.startsWith(`..${sep}`) && !isAbsolute(part)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Bind a configured npm/pnpm tool to its declared package entry, not a shared shim. */
|
|
96
|
+
export async function findTerminalPackageEntry(tool, plan, { platform = process.platform } = {}) {
|
|
97
|
+
let roots = plan.packageModuleRoots
|
|
98
|
+
let boundary
|
|
99
|
+
if (!Array.isArray(roots)) {
|
|
100
|
+
if (!plan.installDir) return null
|
|
101
|
+
if (plan.manager === 'pnpm') {
|
|
102
|
+
const global = join(plan.installDir, '.pnpm', 'global')
|
|
103
|
+
boundary = await realpath(global).catch(() => null)
|
|
104
|
+
const versions = await readdir(global, { withFileTypes: true }).catch(() => [])
|
|
105
|
+
roots = versions.filter(item => item.isDirectory() && /^\d+$/.test(item.name))
|
|
106
|
+
.sort((left, right) => Number(left.name) - Number(right.name))
|
|
107
|
+
.map(item => join(global, item.name, 'node_modules'))
|
|
108
|
+
} else {
|
|
109
|
+
const modules = platform === 'win32' ? join(plan.installDir, 'node_modules') : join(plan.installDir, 'lib', 'node_modules')
|
|
110
|
+
boundary = await realpath(modules).catch(() => null)
|
|
111
|
+
roots = [modules]
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
boundary = await realpath(plan.globalDir || plan.installDir || roots[0]).catch(() => null)
|
|
115
|
+
}
|
|
116
|
+
if (!boundary) return null
|
|
117
|
+
const entries = []
|
|
118
|
+
for (const modules of roots) {
|
|
119
|
+
try {
|
|
120
|
+
const packageRoot = await realpath(join(modules, ...tool.package.split('/')))
|
|
121
|
+
if (!within(boundary, packageRoot)) continue
|
|
122
|
+
const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'))
|
|
123
|
+
if (manifest.name !== tool.package) continue
|
|
124
|
+
for (const executable of tool.probeExecutables) {
|
|
125
|
+
const raw = typeof manifest.bin === 'string' ? manifest.bin : manifest.bin?.[executable]
|
|
126
|
+
const bin = typeof raw === 'string' ? raw.replace(/\\/g, '/').replace(/^\.\//, '') : ''
|
|
127
|
+
if (!bin || bin.split('/').some(part => !part || part === '..') || isAbsolute(bin)) continue
|
|
128
|
+
const entry = await realpath(join(packageRoot, ...bin.split('/')))
|
|
129
|
+
if (!within(packageRoot, entry) || !(await stat(entry)).isFile()) continue
|
|
130
|
+
entries.push({ entry, packageRoot, version: String(manifest.version ?? '') })
|
|
131
|
+
}
|
|
132
|
+
} catch { /* not the selected tool's usable package */ }
|
|
133
|
+
}
|
|
134
|
+
// pnpm normally has one active global tree. Prefer the highest numbered
|
|
135
|
+
// layout when multiple layouts survive an upgrade, following its directories.
|
|
136
|
+
return entries.at(-1) ?? null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function terminalNode(workspace, minimum, acceptedVersion) {
|
|
140
|
+
const { findNodeExecutable } = await import('./official-tool-executor.mjs')
|
|
141
|
+
return findNodeExecutable(workspace, minimum, acceptedVersion)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function executableLaunch(file, args, display, { platform, env }) {
|
|
145
|
+
if (platform !== 'win32') return { file, args, verbatim: false, display }
|
|
146
|
+
const extension = extname(file).toLowerCase()
|
|
147
|
+
if (extension === '.cmd' || extension === '.bat') {
|
|
148
|
+
const comspec = env.ComSpec || env.COMSPEC || 'cmd.exe'
|
|
149
|
+
const tail = `/d /s /c "${[quoteWindows(file), ...args].join(' ')}"`
|
|
150
|
+
return { file: comspec, args: tail, verbatim: true, display }
|
|
151
|
+
}
|
|
152
|
+
if (extension === '.ps1') {
|
|
153
|
+
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows'
|
|
154
|
+
return { file: `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, args: ['-NoLogo', '-NoProfile', '-File', file, ...args], verbatim: false, display }
|
|
155
|
+
}
|
|
156
|
+
return { file, args, verbatim: false, display }
|
|
157
|
+
}
|
|
158
|
+
|
|
87
159
|
/**
|
|
88
160
|
* Build the fixed launch for one target. `locate(executable)` returns the
|
|
89
161
|
* candidate paths `where`/`which` printed. Returns
|
|
@@ -92,6 +164,9 @@ const quoteWindows = path => `"${String(path).replace(/"/g, '')}"`
|
|
|
92
164
|
*/
|
|
93
165
|
export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
94
166
|
platform = process.platform, env = process.env, locate = defaultLocate,
|
|
167
|
+
preferences = {}, home = homedir(), cwd = process.cwd(),
|
|
168
|
+
packageEntry = findTerminalPackageEntry, fileExists = async path => (await stat(path).catch(() => null))?.isFile() === true,
|
|
169
|
+
discoverBundle = discoverZCodeBundle, findNode = terminalNode,
|
|
95
170
|
} = {}) {
|
|
96
171
|
if (target === TERMINAL_SHELL_ID) {
|
|
97
172
|
if (platform === 'win32') {
|
|
@@ -104,9 +179,47 @@ export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
|
104
179
|
}
|
|
105
180
|
if (!TERMINAL_TOOL_IDS.includes(target)) throw new Error('只能启动系统终端或固定注册表中的官方 CLI。')
|
|
106
181
|
const tool = getOfficialTool(target)
|
|
182
|
+
if (target === 'zcode') {
|
|
183
|
+
if (platform !== 'win32') throw new Error('当前仅支持 Windows ZCode 桌面版内置 TUI。')
|
|
184
|
+
const bundle = await discoverBundle({ installDir: preferences.zcodeInstallDir, env })
|
|
185
|
+
if (!bundle) throw new Error('未找到通过智谱签名与组件核验的 ZCode 桌面版;请先配置 ZCode 独立安装目录并重新体检。')
|
|
186
|
+
const node = await findNode(cwd, [24, 14, 0], version => Number(String(version).split('.')[0]) === 24)
|
|
187
|
+
// The verified desktop's electron-node entry is usable independently of
|
|
188
|
+
// its GUI. It supplies its own runtime when external Node 24 is absent.
|
|
189
|
+
return { file: node || bundle.executable, args: [bundle.cliEntry], verbatim: false,
|
|
190
|
+
display: `ZCode TUI · ${bundle.cliEntry}`, ...(!node ? { env: { ELECTRON_RUN_AS_NODE: '1' } } : {}) }
|
|
191
|
+
}
|
|
107
192
|
const executable = tool.probeExecutables[0]
|
|
108
193
|
const args = mode === 'login' ? [...(TERMINAL_LOGIN_ARGS[target] ?? [])] : []
|
|
109
194
|
const display = [executable, ...args].join(' ')
|
|
195
|
+
const selectedScript = installMethodFor(tool, preferences.methods?.[target])?.kind === 'script'
|
|
196
|
+
const selectedPreferences = selectedScript ? { ...preferences, allowScriptInstall: true } : preferences
|
|
197
|
+
const plan = resolveInstallPlan(tool, undefined, selectedPreferences, { platform, env, home })
|
|
198
|
+
const configured = Boolean(preferences.installDir || preferences.methods?.[target])
|
|
199
|
+
if (plan.kind === 'package-manager' && configured) {
|
|
200
|
+
const boundPlan = plan.installDir ? plan : await (await import('./official-tools-runtime.mjs')).resolvePackageJobPlan(tool, plan, selectedPreferences, { options: { platform, env, home } })
|
|
201
|
+
const found = await packageEntry(tool, boundPlan, { platform })
|
|
202
|
+
if (!found) throw new Error(`未在所选 ${boundPlan.manager} 安装目录中找到 ${tool.label} 的真实命令入口;请重新体检或修复。`)
|
|
203
|
+
const extension = extname(found.entry).toLowerCase()
|
|
204
|
+
if (['.js', '.mjs', '.cjs'].includes(extension) || (platform === 'win32' && !extension)) {
|
|
205
|
+
const node = await findNode(cwd)
|
|
206
|
+
if (!node) throw new Error(`未找到可核验的 Node.js 运行时,无法启动 ${tool.label}。`)
|
|
207
|
+
return { file: node, args: [found.entry, ...args], verbatim: false, display }
|
|
208
|
+
}
|
|
209
|
+
return executableLaunch(found.entry, args, display, { platform, env })
|
|
210
|
+
}
|
|
211
|
+
if (plan.kind === 'script' && plan.binDir) {
|
|
212
|
+
for (const name of tool.probeExecutables) {
|
|
213
|
+
const filenames = platform === 'win32' ? WINDOWS_PREFERENCE.map(extension => `${name}${extension}`) : [name]
|
|
214
|
+
for (const filename of filenames) {
|
|
215
|
+
const file = join(plan.binDir, filename)
|
|
216
|
+
if (await fileExists(file)) return executableLaunch(file, args, display, { platform, env })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
// A saved method or directory is authoritative: an older PATH copy cannot
|
|
220
|
+
// satisfy a missing install in the user's selected tool directory.
|
|
221
|
+
if (configured || tool.manager === 'script-installer') throw new Error(`未在所选安装目录 ${plan.binDir} 中找到 ${tool.label};请重新体检或安装。`)
|
|
222
|
+
}
|
|
110
223
|
const candidates = await locate(executable, { platform, env })
|
|
111
224
|
if (platform !== 'win32') {
|
|
112
225
|
const file = candidates.map(item => String(item).trim()).find(item => item.startsWith('/'))
|
|
@@ -115,18 +228,7 @@ export async function resolveTerminalLaunch(target, mode = 'interactive', {
|
|
|
115
228
|
}
|
|
116
229
|
const file = pickWindowsExecutable(candidates)
|
|
117
230
|
if (!file) throw new Error(`未在 PATH 中找到 ${executable};请先在“官方工具 · 体检”中安装 ${tool.label}。`)
|
|
118
|
-
|
|
119
|
-
if (extension === '.cmd' || extension === '.bat') {
|
|
120
|
-
// npm shims only run under cmd.exe. Every token is fixed by the registry.
|
|
121
|
-
const comspec = env.ComSpec || env.COMSPEC || 'cmd.exe'
|
|
122
|
-
const tail = `/d /s /c "${[quoteWindows(file), ...args].join(' ')}"`
|
|
123
|
-
return { file: comspec, args: tail, verbatim: true, display }
|
|
124
|
-
}
|
|
125
|
-
if (extension === '.ps1') {
|
|
126
|
-
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows'
|
|
127
|
-
return { file: `${root}\\System32\\WindowsPowerShell\\v1.0\\powershell.exe`, args: ['-NoLogo', '-NoProfile', '-File', file, ...args], verbatim: false, display }
|
|
128
|
-
}
|
|
129
|
-
return { file, args, verbatim: false, display }
|
|
231
|
+
return executableLaunch(file, args, display, { platform, env })
|
|
130
232
|
}
|
|
131
233
|
|
|
132
234
|
/** Locate an executable with where.exe / which (fixed registry names only). */
|
|
@@ -161,7 +263,10 @@ function spawnPtyProcess(pty, launch, { cwd, cols, rows, env }) {
|
|
|
161
263
|
|
|
162
264
|
/** Pipe fallback: no TTY; the Host echoes input and maps Enter/Ctrl+C. */
|
|
163
265
|
export function spawnPipeProcess(launch, { cwd, env, platform = process.platform, spawnImpl = spawn }) {
|
|
164
|
-
|
|
266
|
+
// node-pty accepts a Windows command-line string; node:child_process needs
|
|
267
|
+
// an array even with windowsVerbatimArguments. Keep the prepared tail intact.
|
|
268
|
+
const args = typeof launch.args === 'string' ? [launch.args] : launch.args
|
|
269
|
+
const child = spawnImpl(launch.file, args, {
|
|
165
270
|
cwd, env, stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true, shell: false,
|
|
166
271
|
windowsVerbatimArguments: launch.verbatim === true,
|
|
167
272
|
})
|
|
@@ -220,6 +325,8 @@ export function createTerminalManager({
|
|
|
220
325
|
statPath = stat,
|
|
221
326
|
prepare = async () => {},
|
|
222
327
|
environment = () => terminalEnvironment(process.env),
|
|
328
|
+
preferences = () => ({}),
|
|
329
|
+
launchOptions = () => ({}),
|
|
223
330
|
onSessionEnd = () => {},
|
|
224
331
|
now = Date.now,
|
|
225
332
|
platform = process.platform,
|
|
@@ -329,10 +436,11 @@ export function createTerminalManager({
|
|
|
329
436
|
try { info = await statPath(request.cwd) } catch { info = null }
|
|
330
437
|
if (!info?.isDirectory?.()) throw new Error(`工作目录不存在或不是目录:${request.cwd}`)
|
|
331
438
|
await prepare()
|
|
332
|
-
const launch = await resolveLaunch(request.target, request.mode, { platform })
|
|
333
439
|
const env = environment()
|
|
440
|
+
const launch = await resolveLaunch(request.target, request.mode, { ...launchOptions(), platform, env,
|
|
441
|
+
preferences: preferences(), cwd: request.cwd })
|
|
334
442
|
const backend = backendInfo()
|
|
335
|
-
const options = { cwd: request.cwd, cols: request.cols, rows: request.rows, env, platform }
|
|
443
|
+
const options = { cwd: request.cwd, cols: request.cols, rows: request.rows, env: { ...env, ...launch.env }, platform }
|
|
336
444
|
let proc
|
|
337
445
|
try {
|
|
338
446
|
proc = backend.backend === 'pty'
|
|
@@ -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) {
|
|
@@ -586,7 +586,7 @@ async function launchSpec(toolId, workspace, mode, modelId, task = 'Check readin
|
|
|
586
586
|
}
|
|
587
587
|
}
|
|
588
588
|
if (toolId === 'zcode') {
|
|
589
|
-
const found = await discoverZCodeBundle()
|
|
589
|
+
const found = await discoverZCodeBundle({ installDir: (selectionOptions?.preferences ?? installPreferences()).zcodeInstallDir })
|
|
590
590
|
if (!found) return { unsupported: '未找到具有智谱有效签名、且 CLI 脚本与首次核验记录一致的 ZCode 桌面版。' }
|
|
591
591
|
const nodeExecutable = await findNodeExecutable(workspace, [24, 14, 0], version =>
|
|
592
592
|
Number(String(version).split('.')[0]) === 24)
|
|
@@ -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'],
|
|
@@ -388,12 +405,12 @@ export const OFFICIAL_TOOLS = Object.freeze([
|
|
|
388
405
|
id: 'zcode',
|
|
389
406
|
label: 'ZCode',
|
|
390
407
|
vendor: 'Z.ai',
|
|
391
|
-
purpose: '智谱官方 ZCode
|
|
408
|
+
purpose: '智谱官方 ZCode 桌面版,内含可独立运行的 GLM CLI/TUI。可单独配置桌面目录,内置 CLI 随桌面版更新。',
|
|
392
409
|
manager: 'signed-windows-installer',
|
|
393
410
|
installArgs: [],
|
|
394
411
|
uninstallArgs: [],
|
|
395
412
|
probeExecutables: [],
|
|
396
|
-
probeNote: '检测经过有效签名的 ZCode.exe 和同目录 GLM 资源;桌面安装器需人工选择安装位置。',
|
|
413
|
+
probeNote: '检测经过有效签名的 ZCode.exe 和同目录 GLM CLI/TUI 资源;桌面安装器需人工选择安装位置。',
|
|
397
414
|
providerHints: ['zai', 'z.ai', 'zcode', 'glm', 'zhipu', 'bigmodel'],
|
|
398
415
|
}),
|
|
399
416
|
])
|