@simonyea/holysheep-cli 1.6.6 → 1.6.8
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/README.md +2 -0
- package/package.json +1 -1
- package/src/index.js +0 -5
- package/src/tools/codex.js +62 -18
- package/src/tools/openclaw.js +110 -28
- package/src/utils/which.js +24 -8
package/README.md
CHANGED
|
@@ -218,6 +218,8 @@ A: OpenClaw 需要 Node.js 20+,运行 `node --version` 确认版本后重试
|
|
|
218
218
|
|
|
219
219
|
## Changelog
|
|
220
220
|
|
|
221
|
+
- **v1.6.8** — 修复 Codex 重复写入 `config.toml` 导致的 duplicate key,并修复 OpenClaw 在 Windows 下的安装检测;针对 OpenClaw 2026.3.13 的模型路由回归,临时跳过 MiniMax 避免 `model not allowed`
|
|
222
|
+
- **v1.6.7** — OpenClaw 配置新增 `MiniMax-M2.7-highspeed`,并补齐节点迁移脚本中的 SSH 代理账号创建逻辑
|
|
221
223
|
- **v1.6.6** — 修复 Droid CLI 的 GPT-5.4 配置残留问题,同时同步 `~/.factory/settings.json` 和 `~/.factory/config.json`,统一使用 `openai + https://api.holysheep.ai/v1`
|
|
222
224
|
- **v1.6.5** — 修复 HolySheep 对 Droid Responses API 的兼容
|
|
223
225
|
- **v1.6.4** — 修复 OpenClaw 的 npx 运行时检测,避免配置后页面仍卡在 Unauthorized / 未连接状态
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.8",
|
|
4
4
|
"description": "Claude Code/Cursor/Cline API relay for China — ¥1=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"openai-china",
|
package/src/index.js
CHANGED
package/src/tools/codex.js
CHANGED
|
@@ -25,6 +25,49 @@ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml')
|
|
|
25
25
|
// 保留 JSON 兼容性(老版本 TypeScript Codex 用)
|
|
26
26
|
const CONFIG_FILE_JSON = path.join(CONFIG_DIR, 'config.json')
|
|
27
27
|
|
|
28
|
+
function normalizeToml(content) {
|
|
29
|
+
return String(content || '').replace(/\r\n/g, '\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function cleanupToml(content) {
|
|
33
|
+
return normalizeToml(content)
|
|
34
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
35
|
+
.trim()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stripManagedTomlConfig(content) {
|
|
39
|
+
const lines = normalizeToml(content).split('\n')
|
|
40
|
+
const output = []
|
|
41
|
+
let currentSection = null
|
|
42
|
+
let skipHolySheepBlock = false
|
|
43
|
+
|
|
44
|
+
for (const line of lines) {
|
|
45
|
+
const trimmed = line.trim()
|
|
46
|
+
|
|
47
|
+
if (/^\[[^\]]+\]$/.test(trimmed)) {
|
|
48
|
+
if (trimmed === '[model_providers.holysheep]') {
|
|
49
|
+
currentSection = trimmed
|
|
50
|
+
skipHolySheepBlock = true
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
currentSection = trimmed
|
|
55
|
+
skipHolySheepBlock = false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (skipHolySheepBlock) continue
|
|
59
|
+
|
|
60
|
+
if (!currentSection) {
|
|
61
|
+
if (/^model\s*=\s*"[^"]*"\s*$/.test(trimmed)) continue
|
|
62
|
+
if (/^model_provider\s*=\s*"holysheep"\s*$/.test(trimmed)) continue
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
output.push(line)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return cleanupToml(output.join('\n'))
|
|
69
|
+
}
|
|
70
|
+
|
|
28
71
|
/**
|
|
29
72
|
* 读取 TOML config(简单解析,不依赖 toml 库)
|
|
30
73
|
*/
|
|
@@ -55,14 +98,7 @@ function writeTomlConfig(apiKey, baseUrlOpenAI, model) {
|
|
|
55
98
|
fs.mkdirSync(CONFIG_DIR, { recursive: true })
|
|
56
99
|
}
|
|
57
100
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// 移除旧的 holysheep 相关配置
|
|
61
|
-
content = content
|
|
62
|
-
.replace(/\nmodel\s*=\s*"[^"]*"\n/g, '\n')
|
|
63
|
-
.replace(/\nmodel_provider\s*=\s*"holysheep"\n/g, '\n')
|
|
64
|
-
.replace(/\[model_providers\.holysheep\][^\[]*(\[|$)/gs, (m, end) => end === '[' ? '[' : '')
|
|
65
|
-
.trim()
|
|
101
|
+
const content = stripManagedTomlConfig(readTomlConfig())
|
|
66
102
|
|
|
67
103
|
// 在开头插入 holysheep 配置
|
|
68
104
|
const newConfig = [
|
|
@@ -76,9 +112,9 @@ function writeTomlConfig(apiKey, baseUrlOpenAI, model) {
|
|
|
76
112
|
`base_url = "${baseUrlOpenAI}"`,
|
|
77
113
|
`env_key = "OPENAI_API_KEY"`,
|
|
78
114
|
'',
|
|
79
|
-
].join('\n')
|
|
115
|
+
].join('\n')
|
|
80
116
|
|
|
81
|
-
fs.writeFileSync(CONFIG_FILE, newConfig, 'utf8')
|
|
117
|
+
fs.writeFileSync(CONFIG_FILE, cleanupToml(newConfig) + '\n', 'utf8')
|
|
82
118
|
}
|
|
83
119
|
|
|
84
120
|
/**
|
|
@@ -91,8 +127,15 @@ function writeJsonConfigIfNeeded(apiKey, baseUrlOpenAI, model) {
|
|
|
91
127
|
jsonConfig = JSON.parse(fs.readFileSync(CONFIG_FILE_JSON, 'utf8'))
|
|
92
128
|
}
|
|
93
129
|
jsonConfig.model = model || 'gpt-5.4'
|
|
130
|
+
jsonConfig.model_provider = 'holysheep'
|
|
94
131
|
jsonConfig.provider = 'holysheep'
|
|
132
|
+
if (!jsonConfig.model_providers) jsonConfig.model_providers = {}
|
|
95
133
|
if (!jsonConfig.providers) jsonConfig.providers = {}
|
|
134
|
+
jsonConfig.model_providers.holysheep = {
|
|
135
|
+
name: 'HolySheep',
|
|
136
|
+
base_url: baseUrlOpenAI,
|
|
137
|
+
env_key: 'OPENAI_API_KEY',
|
|
138
|
+
}
|
|
96
139
|
jsonConfig.providers.holysheep = {
|
|
97
140
|
name: 'HolySheep',
|
|
98
141
|
baseURL: baseUrlOpenAI,
|
|
@@ -132,22 +175,23 @@ module.exports = {
|
|
|
132
175
|
reset() {
|
|
133
176
|
// 清理 TOML
|
|
134
177
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
135
|
-
|
|
136
|
-
content
|
|
137
|
-
.replace(/^model\s*=\s*"[^"]*"\n/m, '')
|
|
138
|
-
.replace(/^model_provider\s*=\s*"holysheep"\n/m, '')
|
|
139
|
-
.replace(/\[model_providers\.holysheep\][^\[]*(\[|$)/gs, (m, end) => end === '[' ? '[' : '')
|
|
140
|
-
.trim() + '\n'
|
|
141
|
-
fs.writeFileSync(CONFIG_FILE, content, 'utf8')
|
|
178
|
+
const content = stripManagedTomlConfig(readTomlConfig())
|
|
179
|
+
fs.writeFileSync(CONFIG_FILE, (content ? content + '\n' : ''), 'utf8')
|
|
142
180
|
}
|
|
143
181
|
// 清理 JSON
|
|
144
182
|
if (fs.existsSync(CONFIG_FILE_JSON)) {
|
|
145
183
|
try {
|
|
146
184
|
const c = JSON.parse(fs.readFileSync(CONFIG_FILE_JSON, 'utf8'))
|
|
185
|
+
if (c.model_provider === 'holysheep') {
|
|
186
|
+
delete c.model_provider
|
|
187
|
+
}
|
|
147
188
|
if (c.provider === 'holysheep') {
|
|
148
189
|
delete c.provider
|
|
149
|
-
delete c.providers?.holysheep
|
|
150
190
|
}
|
|
191
|
+
delete c.model_providers?.holysheep
|
|
192
|
+
delete c.providers?.holysheep
|
|
193
|
+
if (c.model_providers && Object.keys(c.model_providers).length === 0) delete c.model_providers
|
|
194
|
+
if (c.providers && Object.keys(c.providers).length === 0) delete c.providers
|
|
151
195
|
fs.writeFileSync(CONFIG_FILE_JSON, JSON.stringify(c, null, 2), 'utf8')
|
|
152
196
|
} catch {}
|
|
153
197
|
}
|
package/src/tools/openclaw.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenClaw 适配器 (v2 — 基于实测的正确配置格式)
|
|
3
3
|
*
|
|
4
|
-
* 正确方案:写入 HolySheep 的 OpenAI + Anthropic
|
|
5
|
-
* 默认模型固定为 GPT-5.4,同时保留 Claude 模型供 /model 切换。
|
|
4
|
+
* 正确方案:写入 HolySheep 的 OpenAI + Anthropic + MiniMax provider,
|
|
5
|
+
* 默认模型固定为 GPT-5.4,同时保留 Claude / MiniMax 模型供 /model 切换。
|
|
6
6
|
*/
|
|
7
7
|
const fs = require('fs')
|
|
8
8
|
const path = require('path')
|
|
@@ -17,9 +17,21 @@ const DEFAULT_GATEWAY_PORT = 18789
|
|
|
17
17
|
const MAX_PORT_SCAN = 20
|
|
18
18
|
const OPENCLAW_DEFAULT_MODEL = 'gpt-5.4'
|
|
19
19
|
const OPENCLAW_DEFAULT_CLAUDE_MODEL = 'claude-sonnet-4-6'
|
|
20
|
+
const OPENCLAW_DEFAULT_MINIMAX_MODEL = 'MiniMax-M2.7-highspeed'
|
|
21
|
+
const OPENCLAW_ROUTING_REGRESSION_VERSION = /^2026\.3\.13(?:\D|$)/
|
|
22
|
+
|
|
23
|
+
function getOpenClawBinaryCandidates() {
|
|
24
|
+
return isWin ? ['openclaw.cmd', 'openclaw'] : ['openclaw']
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getBinaryRunner() {
|
|
28
|
+
return isWin
|
|
29
|
+
? { cmd: 'openclaw.cmd', argsPrefix: [], shell: true, label: 'openclaw', via: 'binary' }
|
|
30
|
+
: { cmd: 'openclaw', argsPrefix: [], shell: false, label: 'openclaw', via: 'binary' }
|
|
31
|
+
}
|
|
20
32
|
|
|
21
33
|
function hasOpenClawBinary() {
|
|
22
|
-
return commandExists(
|
|
34
|
+
return getOpenClawBinaryCandidates().some((cmd) => commandExists(cmd))
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
function hasNpx() {
|
|
@@ -27,18 +39,41 @@ function hasNpx() {
|
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
function getRunner(preferNpx = false) {
|
|
42
|
+
const binaryRunner = hasOpenClawBinary() ? getBinaryRunner() : null
|
|
43
|
+
|
|
30
44
|
if (!preferNpx && hasOpenClawBinary()) {
|
|
31
|
-
return
|
|
45
|
+
return binaryRunner
|
|
32
46
|
}
|
|
33
47
|
if (hasNpx()) {
|
|
34
48
|
return { cmd: 'npx', argsPrefix: ['openclaw'], shell: isWin, label: 'npx openclaw', via: 'npx' }
|
|
35
49
|
}
|
|
36
|
-
if (
|
|
37
|
-
return
|
|
50
|
+
if (binaryRunner) {
|
|
51
|
+
return binaryRunner
|
|
38
52
|
}
|
|
39
53
|
return null
|
|
40
54
|
}
|
|
41
55
|
|
|
56
|
+
function runWithRunner(runner, args, opts = {}) {
|
|
57
|
+
return spawnSync(runner.cmd, [...runner.argsPrefix, ...args], {
|
|
58
|
+
shell: runner.shell,
|
|
59
|
+
timeout: opts.timeout || 30000,
|
|
60
|
+
stdio: opts.stdio || 'pipe',
|
|
61
|
+
encoding: 'utf8',
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeVersionOutput(text) {
|
|
66
|
+
return firstLine(text).replace(/^openclaw\s+/i, '').trim()
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function probeRunner(runner, timeout) {
|
|
70
|
+
const result = runWithRunner(runner, ['--version'], { timeout })
|
|
71
|
+
if (result.error || result.status !== 0) return null
|
|
72
|
+
|
|
73
|
+
const version = normalizeVersionOutput(result.stdout || result.stderr || '')
|
|
74
|
+
return version || null
|
|
75
|
+
}
|
|
76
|
+
|
|
42
77
|
/** 运行 openclaw CLI(优先全局命令,可切换到 npx 回退) */
|
|
43
78
|
function runOpenClaw(args, opts = {}) {
|
|
44
79
|
const runner = getRunner(Boolean(opts.preferNpx))
|
|
@@ -46,12 +81,7 @@ function runOpenClaw(args, opts = {}) {
|
|
|
46
81
|
return { status: 1, stdout: '', stderr: 'OpenClaw CLI not found' }
|
|
47
82
|
}
|
|
48
83
|
|
|
49
|
-
return
|
|
50
|
-
shell: runner.shell,
|
|
51
|
-
timeout: opts.timeout || 30000,
|
|
52
|
-
stdio: opts.stdio || 'pipe',
|
|
53
|
-
encoding: 'utf8',
|
|
54
|
-
})
|
|
84
|
+
return runWithRunner(runner, args, opts)
|
|
55
85
|
}
|
|
56
86
|
|
|
57
87
|
function spawnOpenClaw(args, opts = {}) {
|
|
@@ -74,37 +104,68 @@ function firstLine(text) {
|
|
|
74
104
|
}
|
|
75
105
|
|
|
76
106
|
function getOpenClawVersion(preferNpx = false) {
|
|
77
|
-
const
|
|
78
|
-
if (
|
|
79
|
-
return
|
|
107
|
+
const runner = getRunner(preferNpx)
|
|
108
|
+
if (!runner) return null
|
|
109
|
+
return probeRunner(runner, preferNpx ? 60000 : 15000)
|
|
80
110
|
}
|
|
81
111
|
|
|
82
112
|
function detectRuntime() {
|
|
83
113
|
const preferNpx = getPreferredRuntime()
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
114
|
+
const runnerOrder = preferNpx ? [getRunner(true), getRunner(false)] : [getRunner(false), getRunner(true)]
|
|
115
|
+
const seen = new Set()
|
|
116
|
+
|
|
117
|
+
for (const runner of runnerOrder) {
|
|
118
|
+
if (!runner) continue
|
|
119
|
+
const key = `${runner.via}:${runner.cmd}:${runner.argsPrefix.join(' ')}`
|
|
120
|
+
if (seen.has(key)) continue
|
|
121
|
+
seen.add(key)
|
|
122
|
+
|
|
123
|
+
const version = probeRunner(runner, runner.via === 'npx' ? 60000 : 15000)
|
|
124
|
+
if (version) {
|
|
125
|
+
return {
|
|
126
|
+
available: true,
|
|
127
|
+
via: runner.via,
|
|
128
|
+
command: runner.label,
|
|
129
|
+
version,
|
|
130
|
+
}
|
|
92
131
|
}
|
|
93
132
|
}
|
|
94
133
|
|
|
95
|
-
const fallbackRunner = getRunner(
|
|
134
|
+
const fallbackRunner = getRunner(preferNpx)
|
|
96
135
|
if (fallbackRunner) {
|
|
97
136
|
return {
|
|
98
|
-
available:
|
|
137
|
+
available: false,
|
|
99
138
|
via: fallbackRunner.via,
|
|
100
139
|
command: fallbackRunner.label,
|
|
101
|
-
version:
|
|
140
|
+
version: null,
|
|
102
141
|
}
|
|
103
142
|
}
|
|
104
143
|
|
|
105
144
|
return { available: false, via: null, command: null, version: null }
|
|
106
145
|
}
|
|
107
146
|
|
|
147
|
+
function isRoutingRegressionVersion(version) {
|
|
148
|
+
return OPENCLAW_ROUTING_REGRESSION_VERSION.test(String(version || '').trim())
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sanitizeSelectedModelsForRuntime(selectedModels, runtimeVersion) {
|
|
152
|
+
if (!isRoutingRegressionVersion(runtimeVersion)) {
|
|
153
|
+
return { models: selectedModels, warning: '' }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const originalModels = Array.isArray(selectedModels) ? selectedModels : []
|
|
157
|
+
const filteredModels = originalModels.filter((model) => !model.startsWith('MiniMax-'))
|
|
158
|
+
|
|
159
|
+
if (filteredModels.length === originalModels.length) {
|
|
160
|
+
return { models: selectedModels, warning: '' }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
models: filteredModels,
|
|
165
|
+
warning: '当前 OpenClaw 2026.3.13 存在 provider 路由回归,已暂时跳过 MiniMax 模型,避免 /model 和网页切换时报 model not allowed。升级 OpenClaw 后可重新运行 hs setup 恢复。',
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
108
169
|
function readConfig() {
|
|
109
170
|
try {
|
|
110
171
|
if (fs.existsSync(CONFIG_FILE)) {
|
|
@@ -213,7 +274,7 @@ function buildModelEntry(id) {
|
|
|
213
274
|
function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModels) {
|
|
214
275
|
const requestedModels = Array.isArray(selectedModels) && selectedModels.length > 0
|
|
215
276
|
? selectedModels
|
|
216
|
-
: [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL]
|
|
277
|
+
: [OPENCLAW_DEFAULT_MODEL, OPENCLAW_DEFAULT_CLAUDE_MODEL, OPENCLAW_DEFAULT_MINIMAX_MODEL]
|
|
217
278
|
|
|
218
279
|
const openaiModels = requestedModels.filter((model) => model.startsWith('gpt-'))
|
|
219
280
|
if (!openaiModels.includes(OPENCLAW_DEFAULT_MODEL)) {
|
|
@@ -225,8 +286,14 @@ function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModel
|
|
|
225
286
|
claudeModels.push(OPENCLAW_DEFAULT_CLAUDE_MODEL)
|
|
226
287
|
}
|
|
227
288
|
|
|
289
|
+
const minimaxModels = requestedModels.filter((model) => model.startsWith('MiniMax-'))
|
|
290
|
+
if (requestedModels.includes(OPENCLAW_DEFAULT_MINIMAX_MODEL) && !minimaxModels.includes(OPENCLAW_DEFAULT_MINIMAX_MODEL)) {
|
|
291
|
+
minimaxModels.unshift(OPENCLAW_DEFAULT_MINIMAX_MODEL)
|
|
292
|
+
}
|
|
293
|
+
|
|
228
294
|
const openaiProviderName = buildProviderName(baseUrlOpenAI, 'custom-openai')
|
|
229
295
|
const anthropicProviderName = buildProviderName(baseUrlAnthropic, 'custom-anthropic')
|
|
296
|
+
const minimaxProviderName = buildProviderName(`${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`, 'custom-anthropic')
|
|
230
297
|
|
|
231
298
|
const providers = {
|
|
232
299
|
[openaiProviderName]: {
|
|
@@ -243,9 +310,19 @@ function buildManagedPlan(apiKey, baseUrlAnthropic, baseUrlOpenAI, selectedModel
|
|
|
243
310
|
},
|
|
244
311
|
}
|
|
245
312
|
|
|
313
|
+
if (minimaxModels.length > 0) {
|
|
314
|
+
providers[minimaxProviderName] = {
|
|
315
|
+
baseUrl: `${baseUrlAnthropic.replace(/\/+$/, '')}/minimax`,
|
|
316
|
+
apiKey,
|
|
317
|
+
api: 'anthropic-messages',
|
|
318
|
+
models: minimaxModels.map(buildModelEntry),
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
246
322
|
const managedModelRefs = [
|
|
247
323
|
...openaiModels.map((id) => `${openaiProviderName}/${id}`),
|
|
248
324
|
...claudeModels.map((id) => `${anthropicProviderName}/${id}`),
|
|
325
|
+
...minimaxModels.map((id) => `${minimaxProviderName}/${id}`),
|
|
249
326
|
]
|
|
250
327
|
|
|
251
328
|
return {
|
|
@@ -408,6 +485,11 @@ module.exports = {
|
|
|
408
485
|
}
|
|
409
486
|
this._lastRuntimeCommand = runtime.command
|
|
410
487
|
|
|
488
|
+
const sanitizedSelection = sanitizeSelectedModelsForRuntime(selectedModels, runtime.version)
|
|
489
|
+
if (sanitizedSelection.warning) {
|
|
490
|
+
console.log(chalk.yellow(` ⚠️ ${sanitizedSelection.warning}`))
|
|
491
|
+
}
|
|
492
|
+
|
|
411
493
|
runOpenClaw(['gateway', 'stop'], { preferNpx: runtime.via === 'npx' })
|
|
412
494
|
|
|
413
495
|
const gatewayPort = findAvailableGatewayPort(DEFAULT_GATEWAY_PORT)
|
|
@@ -453,7 +535,7 @@ module.exports = {
|
|
|
453
535
|
apiKey,
|
|
454
536
|
baseUrlAnthropic,
|
|
455
537
|
baseUrlOpenAI,
|
|
456
|
-
|
|
538
|
+
sanitizedSelection.models,
|
|
457
539
|
gatewayPort,
|
|
458
540
|
)
|
|
459
541
|
|
package/src/utils/which.js
CHANGED
|
@@ -4,18 +4,34 @@
|
|
|
4
4
|
*/
|
|
5
5
|
const { execSync } = require('child_process')
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
const finder = process.platform === 'win32' ? `where ${cmd}` : `which ${cmd}`
|
|
7
|
+
function canRun(command, options = {}) {
|
|
9
8
|
try {
|
|
10
|
-
execSync(
|
|
9
|
+
execSync(command, { stdio: 'ignore', ...options })
|
|
11
10
|
return true
|
|
12
11
|
} catch {
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
execSync(`${cmd} --version`, { stdio: 'ignore', timeout: 3000 })
|
|
16
|
-
return true
|
|
17
|
-
} catch { return false }
|
|
12
|
+
return false
|
|
18
13
|
}
|
|
19
14
|
}
|
|
20
15
|
|
|
16
|
+
function commandExists(cmd) {
|
|
17
|
+
if (process.platform === 'win32') {
|
|
18
|
+
const variants = [cmd, `${cmd}.cmd`, `${cmd}.exe`, `${cmd}.bat`]
|
|
19
|
+
for (const variant of variants) {
|
|
20
|
+
if (canRun(`where ${variant}`)) return true
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Windows 上很多 npm 全局命令实际是 .cmd 包装器,需要交给 cmd.exe 执行。
|
|
24
|
+
for (const variant of variants) {
|
|
25
|
+
if (canRun(`cmd /d /s /c "${variant} --version"`, { timeout: 3000 })) return true
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (canRun(`which ${cmd}`)) return true
|
|
32
|
+
|
|
33
|
+
// 兜底:直接跑 --version
|
|
34
|
+
return canRun(`${cmd} --version`, { timeout: 3000 })
|
|
35
|
+
}
|
|
36
|
+
|
|
21
37
|
module.exports = { commandExists }
|