@simonyea/holysheep-cli 2.1.40 → 2.1.41
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/dist/configure-worker.js +4491 -0
- package/dist/index.js +9591 -0
- package/dist/process-proxy-inject.js +117 -0
- package/package.json +20 -7
- package/.gitea/workflows/sanity.yml +0 -125
- package/scripts/check-tarball-size.js +0 -44
- package/src/commands/balance.js +0 -57
- package/src/commands/claude-proxy.js +0 -248
- package/src/commands/claude.js +0 -135
- package/src/commands/doctor.js +0 -282
- package/src/commands/login.js +0 -211
- package/src/commands/openclaw.js +0 -258
- package/src/commands/reset.js +0 -53
- package/src/commands/setup.js +0 -493
- package/src/commands/upgrade.js +0 -168
- package/src/commands/webui.js +0 -622
- package/src/index.js +0 -226
- package/src/tools/aider.js +0 -78
- package/src/tools/antigravity.js +0 -42
- package/src/tools/claude-code.js +0 -228
- package/src/tools/claude-process-proxy.js +0 -1030
- package/src/tools/codex.js +0 -254
- package/src/tools/continue.js +0 -146
- package/src/tools/cursor.js +0 -71
- package/src/tools/droid.js +0 -281
- package/src/tools/env-config.js +0 -185
- package/src/tools/gemini-cli.js +0 -82
- package/src/tools/hermes.js +0 -354
- package/src/tools/index.js +0 -13
- package/src/tools/openclaw-bridge.js +0 -987
- package/src/tools/openclaw.js +0 -925
- package/src/tools/opencode.js +0 -227
- package/src/tools/process-proxy-inject.js +0 -142
- package/src/utils/config.js +0 -54
- package/src/utils/shell.js +0 -342
- package/src/utils/which.js +0 -176
- package/src/webui/aionui-runtime-fetcher.js +0 -429
- package/src/webui/aionui-runtime.js +0 -139
- package/src/webui/aionui-wrapper.js +0 -734
- package/src/webui/configure-worker.js +0 -67
- package/src/webui/server.js +0 -1572
- package/src/webui/workspace-runtime.js +0 -288
- package/src/webui/workspace-store.js +0 -325
- /package/{src/webui → dist}/index.html +0 -0
- /package/{src/tools → dist}/pty-hermes-wrapper.py +0 -0
package/src/utils/shell.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shell RC 文件管理 — 写入/清理环境变量
|
|
3
|
-
*/
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const os = require('os')
|
|
7
|
-
const { execSync } = require('child_process')
|
|
8
|
-
const pkg = require('../../package.json')
|
|
9
|
-
|
|
10
|
-
const MARKER_START = '# >>> holysheep-cli managed >>>'
|
|
11
|
-
const MARKER_END = '# <<< holysheep-cli managed <<<'
|
|
12
|
-
|
|
13
|
-
function getShellRcFiles() {
|
|
14
|
-
const home = os.homedir()
|
|
15
|
-
|
|
16
|
-
// Windows:不写 shell rc,改用 setx 写系统环境变量
|
|
17
|
-
if (process.platform === 'win32') return []
|
|
18
|
-
|
|
19
|
-
const shell = process.env.SHELL || ''
|
|
20
|
-
const candidates = []
|
|
21
|
-
|
|
22
|
-
if (shell.includes('zsh')) candidates.push(path.join(home, '.zshrc'))
|
|
23
|
-
if (shell.includes('bash')) candidates.push(path.join(home, '.bashrc'), path.join(home, '.bash_profile'))
|
|
24
|
-
if (shell.includes('fish')) candidates.push(path.join(home, '.config', 'fish', 'config.fish'))
|
|
25
|
-
|
|
26
|
-
// 默认兜底
|
|
27
|
-
if (candidates.length === 0) {
|
|
28
|
-
const zshrc = path.join(home, '.zshrc')
|
|
29
|
-
const bashrc = path.join(home, '.bashrc')
|
|
30
|
-
if (fs.existsSync(zshrc)) candidates.push(zshrc)
|
|
31
|
-
if (fs.existsSync(bashrc)) candidates.push(bashrc)
|
|
32
|
-
if (candidates.length === 0) candidates.push(zshrc)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return candidates
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function removeHsBlock(content) {
|
|
39
|
-
const re = new RegExp(
|
|
40
|
-
`\\n?${escapeRegex(MARKER_START)}[\\s\\S]*?${escapeRegex(MARKER_END)}\\n?`,
|
|
41
|
-
'g'
|
|
42
|
-
)
|
|
43
|
-
return content.replace(re, '')
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* 移除 rc 文件里用户手动写的同名 export/set -gx 行
|
|
48
|
-
* 防止旧值在 holysheep-cli managed 块之后覆盖新值
|
|
49
|
-
*/
|
|
50
|
-
function removeStaleExports(content, keys, isFish = false) {
|
|
51
|
-
let result = content
|
|
52
|
-
for (const key of keys) {
|
|
53
|
-
if (isFish) {
|
|
54
|
-
// fish: set -gx KEY "..." 或 set -gx KEY ...
|
|
55
|
-
result = result.replace(new RegExp(`\\n?set\\s+-gx\\s+${escapeRegex(key)}\\s+[^\\n]*\\n?`, 'g'), '\n')
|
|
56
|
-
} else {
|
|
57
|
-
// bash/zsh: export KEY="..." 或 export KEY=...
|
|
58
|
-
result = result.replace(new RegExp(`\\n?export\\s+${escapeRegex(key)}=[^\\n]*\\n?`, 'g'), '\n')
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
// 清理多余空行
|
|
62
|
-
return result.replace(/\n{3,}/g, '\n\n')
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function buildEnvBlock(envVars, isFish = false) {
|
|
66
|
-
const lines = [MARKER_START]
|
|
67
|
-
for (const [k, v] of Object.entries(envVars)) {
|
|
68
|
-
// fish shell 用 set -gx,其他 shell 用 export
|
|
69
|
-
lines.push(isFish ? `set -gx ${k} "${v}"` : `export ${k}="${v}"`)
|
|
70
|
-
}
|
|
71
|
-
lines.push(MARKER_END)
|
|
72
|
-
return '\n' + lines.join('\n') + '\n'
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function ensureWindowsUserPathHasNpmBin() {
|
|
76
|
-
if (process.platform !== 'win32') return []
|
|
77
|
-
|
|
78
|
-
const appData = process.env.APPDATA
|
|
79
|
-
if (!appData) return []
|
|
80
|
-
|
|
81
|
-
const npmBin = path.join(appData, 'npm')
|
|
82
|
-
let currentPath = ''
|
|
83
|
-
try {
|
|
84
|
-
currentPath = execSync(
|
|
85
|
-
'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Environment]::GetEnvironmentVariable(\'Path\', \'User\')"',
|
|
86
|
-
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
87
|
-
).trim()
|
|
88
|
-
} catch {
|
|
89
|
-
currentPath = process.env.PATH || ''
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const parts = currentPath
|
|
93
|
-
.split(';')
|
|
94
|
-
.map((item) => item.trim())
|
|
95
|
-
.filter(Boolean)
|
|
96
|
-
|
|
97
|
-
const hasNpmBin = parts.some((item) => item.toLowerCase() === npmBin.toLowerCase())
|
|
98
|
-
if (hasNpmBin) return []
|
|
99
|
-
|
|
100
|
-
const nextPath = [...parts, npmBin].join(';')
|
|
101
|
-
try {
|
|
102
|
-
const escapedPath = nextPath.replace(/'/g, "''")
|
|
103
|
-
execSync(
|
|
104
|
-
`powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Environment]::SetEnvironmentVariable('Path', '${escapedPath}', 'User')"`,
|
|
105
|
-
{ stdio: 'ignore' }
|
|
106
|
-
)
|
|
107
|
-
return ['[用户 PATH] %APPDATA%\\npm']
|
|
108
|
-
} catch {
|
|
109
|
-
try {
|
|
110
|
-
const chalk = require('chalk')
|
|
111
|
-
console.warn(chalk.yellow(
|
|
112
|
-
` ⚠️ 无法自动更新 PATH,请手动将以下路径加入系统环境变量 PATH:\n ${npmBin}`
|
|
113
|
-
))
|
|
114
|
-
} catch {}
|
|
115
|
-
return []
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* [HolySheep fork v2.1.36 / hs25] Same mechanism as
|
|
121
|
-
* `ensureWindowsUserPathHasNpmBin`, but targets `%USERPROFILE%\.local\bin`.
|
|
122
|
-
*
|
|
123
|
-
* Background: Claude Code's official Windows installer (irm
|
|
124
|
-
* https://claude.ai/install.ps1 | iex) drops `claude.exe` under
|
|
125
|
-
* `C:\Users\<user>\.local\bin` and prints a warning that this directory
|
|
126
|
-
* is NOT on PATH. Our CLI Manager page then calls `commandExists('claude')`
|
|
127
|
-
* which runs `where claude` — finds nothing — and reports 未安装, even
|
|
128
|
-
* though the install succeeded.
|
|
129
|
-
*
|
|
130
|
-
* This helper:
|
|
131
|
-
* 1. Persists `.local\bin` into the USER-level Path (so new terminals
|
|
132
|
-
* see it too).
|
|
133
|
-
* 2. Appends it to `process.env.PATH` of the current process, so that
|
|
134
|
-
* the very next `commandExists('claude')` call in the same Node
|
|
135
|
-
* instance resolves correctly — no restart required.
|
|
136
|
-
*
|
|
137
|
-
* Returns a list of human-readable lines describing what was touched.
|
|
138
|
-
* No-op on non-Windows (returns []).
|
|
139
|
-
*/
|
|
140
|
-
function ensureWindowsUserPathHasLocalBin() {
|
|
141
|
-
if (process.platform !== 'win32') return []
|
|
142
|
-
|
|
143
|
-
const userProfile = process.env.USERPROFILE || os.homedir()
|
|
144
|
-
if (!userProfile) return []
|
|
145
|
-
|
|
146
|
-
const localBin = path.join(userProfile, '.local', 'bin')
|
|
147
|
-
|
|
148
|
-
let currentPath = ''
|
|
149
|
-
try {
|
|
150
|
-
currentPath = execSync(
|
|
151
|
-
'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Environment]::GetEnvironmentVariable(\'Path\', \'User\')"',
|
|
152
|
-
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
|
|
153
|
-
).trim()
|
|
154
|
-
} catch {
|
|
155
|
-
currentPath = process.env.PATH || ''
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const parts = currentPath
|
|
159
|
-
.split(';')
|
|
160
|
-
.map((item) => item.trim())
|
|
161
|
-
.filter(Boolean)
|
|
162
|
-
|
|
163
|
-
const hasLocalBin = parts.some((item) => item.toLowerCase() === localBin.toLowerCase())
|
|
164
|
-
|
|
165
|
-
// Always also hot-patch the in-process PATH so the immediate follow-up
|
|
166
|
-
// `commandExists('claude')` check sees the new directory. This covers
|
|
167
|
-
// the (common) case where the persisted user PATH already has it but
|
|
168
|
-
// our own process was spawned before the PowerShell installer ran.
|
|
169
|
-
const procParts = (process.env.PATH || '').split(';').map((p) => p.trim()).filter(Boolean)
|
|
170
|
-
const inProcessHasIt = procParts.some((item) => item.toLowerCase() === localBin.toLowerCase())
|
|
171
|
-
if (!inProcessHasIt) {
|
|
172
|
-
process.env.PATH = [...procParts, localBin].join(';')
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (hasLocalBin) return inProcessHasIt ? [] : ['[当前进程 PATH] %USERPROFILE%\\.local\\bin']
|
|
176
|
-
|
|
177
|
-
const nextPath = [...parts, localBin].join(';')
|
|
178
|
-
try {
|
|
179
|
-
const escapedPath = nextPath.replace(/'/g, "''")
|
|
180
|
-
execSync(
|
|
181
|
-
`powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Environment]::SetEnvironmentVariable('Path', '${escapedPath}', 'User')"`,
|
|
182
|
-
{ stdio: 'ignore' }
|
|
183
|
-
)
|
|
184
|
-
return ['[用户 PATH] %USERPROFILE%\\.local\\bin']
|
|
185
|
-
} catch {
|
|
186
|
-
try {
|
|
187
|
-
const chalk = require('chalk')
|
|
188
|
-
console.warn(chalk.yellow(
|
|
189
|
-
` ⚠️ 无法自动更新 PATH,请手动将以下路径加入系统环境变量 PATH:\n ${localBin}`
|
|
190
|
-
))
|
|
191
|
-
} catch {}
|
|
192
|
-
return []
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function installWindowsCliShims() {
|
|
197
|
-
if (process.platform !== 'win32') return []
|
|
198
|
-
|
|
199
|
-
const appData = process.env.APPDATA
|
|
200
|
-
if (!appData) return []
|
|
201
|
-
|
|
202
|
-
const npmBin = path.join(appData, 'npm')
|
|
203
|
-
fs.mkdirSync(npmBin, { recursive: true })
|
|
204
|
-
|
|
205
|
-
// 如果 hs.cmd 已存在(npm install -g 生成的),不覆盖
|
|
206
|
-
if (fs.existsSync(path.join(npmBin, 'hs.cmd'))) return []
|
|
207
|
-
|
|
208
|
-
const cliSpec = `@simonyea/holysheep-cli@${pkg.version}`
|
|
209
|
-
// 用 %APPDATA% 展开路径(比 %~dp0 在 cmd.exe 里更可靠)
|
|
210
|
-
const cmdContent = [
|
|
211
|
-
'@echo off',
|
|
212
|
-
'setlocal',
|
|
213
|
-
'if exist "%APPDATA%\\npm\\node_modules\\@simonyea\\holysheep-cli\\bin\\hs.js" (',
|
|
214
|
-
' node "%APPDATA%\\npm\\node_modules\\@simonyea\\holysheep-cli\\bin\\hs.js" %*',
|
|
215
|
-
') else if exist "%~dp0npx.cmd" (',
|
|
216
|
-
` call "%~dp0npx.cmd" ${cliSpec} %*`,
|
|
217
|
-
') else (',
|
|
218
|
-
` call npx ${cliSpec} %*`,
|
|
219
|
-
')',
|
|
220
|
-
''
|
|
221
|
-
].join('\r\n')
|
|
222
|
-
|
|
223
|
-
const ps1Content = [
|
|
224
|
-
'$localPkg = "$env:APPDATA\\npm\\node_modules\\@simonyea\\holysheep-cli\\bin\\hs.js"',
|
|
225
|
-
'if (Test-Path $localPkg) {',
|
|
226
|
-
' & node $localPkg @args',
|
|
227
|
-
'} elseif (Test-Path (Join-Path $PSScriptRoot "npx.cmd")) {',
|
|
228
|
-
` & (Join-Path $PSScriptRoot "npx.cmd") "${cliSpec}" @args`,
|
|
229
|
-
'} else {',
|
|
230
|
-
` & npx "${cliSpec}" @args`,
|
|
231
|
-
'}',
|
|
232
|
-
''
|
|
233
|
-
].join('\r\n')
|
|
234
|
-
|
|
235
|
-
const written = []
|
|
236
|
-
for (const name of ['hs', 'holysheep']) {
|
|
237
|
-
fs.writeFileSync(path.join(npmBin, `${name}.cmd`), cmdContent, 'utf8')
|
|
238
|
-
fs.writeFileSync(path.join(npmBin, `${name}.ps1`), ps1Content, 'utf8')
|
|
239
|
-
written.push(`[启动器] %APPDATA%\\npm\\${name}.cmd`)
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
return written
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function writeEnvToShell(envVars) {
|
|
246
|
-
// Windows: 用 setx 写入用户级环境变量(需重启终端生效)
|
|
247
|
-
if (process.platform === 'win32') {
|
|
248
|
-
const written = []
|
|
249
|
-
for (const [k, v] of Object.entries(envVars)) {
|
|
250
|
-
try {
|
|
251
|
-
execSync(`setx ${k} "${v}"`, { stdio: 'ignore' })
|
|
252
|
-
written.push(`[系统环境变量] ${k}`)
|
|
253
|
-
} catch {}
|
|
254
|
-
}
|
|
255
|
-
written.push(...installWindowsCliShims())
|
|
256
|
-
written.push(...ensureWindowsUserPathHasNpmBin())
|
|
257
|
-
if (written.length > 0) {
|
|
258
|
-
const chalk = require('chalk')
|
|
259
|
-
console.log(chalk.yellow('\n ⚠️ Windows 环境变量已写入,需要重启终端后生效'))
|
|
260
|
-
}
|
|
261
|
-
return written
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const files = getShellRcFiles()
|
|
265
|
-
const written = []
|
|
266
|
-
|
|
267
|
-
for (const file of files) {
|
|
268
|
-
let content = ''
|
|
269
|
-
try { content = fs.readFileSync(file, 'utf8') } catch {}
|
|
270
|
-
const isFish = file.endsWith('config.fish')
|
|
271
|
-
// 1. 清理旧的 holysheep managed 块
|
|
272
|
-
content = removeHsBlock(content)
|
|
273
|
-
// 2. 清理用户手动写的同名 export(防止旧值覆盖新值)
|
|
274
|
-
content = removeStaleExports(content, Object.keys(envVars), isFish)
|
|
275
|
-
// 3. 追加新的 managed 块
|
|
276
|
-
content += buildEnvBlock(envVars, isFish)
|
|
277
|
-
fs.writeFileSync(file, content, 'utf8')
|
|
278
|
-
written.push(file)
|
|
279
|
-
}
|
|
280
|
-
return written
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function removeWindowsUserEnvVars(keys = []) {
|
|
284
|
-
if (process.platform !== 'win32') return []
|
|
285
|
-
|
|
286
|
-
const removed = []
|
|
287
|
-
for (const key of keys) {
|
|
288
|
-
try {
|
|
289
|
-
execSync(
|
|
290
|
-
`powershell.exe -NoProfile -Command "[Environment]::SetEnvironmentVariable('${key}', $null, 'User')"`,
|
|
291
|
-
{ stdio: 'ignore' }
|
|
292
|
-
)
|
|
293
|
-
delete process.env[key]
|
|
294
|
-
removed.push(`[系统环境变量] ${key}`)
|
|
295
|
-
} catch {}
|
|
296
|
-
}
|
|
297
|
-
return removed
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function removeEnvFromShell(extraKeys = []) {
|
|
301
|
-
// 默认清理的 key 列表(holysheep 相关的所有环境变量)
|
|
302
|
-
const HS_KEYS = [
|
|
303
|
-
'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_API_KEY', 'ANTHROPIC_BASE_URL',
|
|
304
|
-
'OPENAI_API_KEY', 'OPENAI_BASE_URL',
|
|
305
|
-
'HOLYSHEEP_API_KEY',
|
|
306
|
-
...extraKeys,
|
|
307
|
-
]
|
|
308
|
-
|
|
309
|
-
// Windows:通过 PowerShell 删除用户级注册表环境变量
|
|
310
|
-
if (process.platform === 'win32') {
|
|
311
|
-
return removeWindowsUserEnvVars(HS_KEYS)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const files = getShellRcFiles()
|
|
315
|
-
const cleaned = []
|
|
316
|
-
for (const file of files) {
|
|
317
|
-
if (!fs.existsSync(file)) continue
|
|
318
|
-
const isFish = file.endsWith('config.fish')
|
|
319
|
-
let content = fs.readFileSync(file, 'utf8')
|
|
320
|
-
let updated = removeHsBlock(content)
|
|
321
|
-
updated = removeStaleExports(updated, HS_KEYS, isFish)
|
|
322
|
-
if (updated !== content) {
|
|
323
|
-
fs.writeFileSync(file, updated, 'utf8')
|
|
324
|
-
cleaned.push(file)
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return cleaned
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
function escapeRegex(s) {
|
|
331
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
module.exports = {
|
|
335
|
-
getShellRcFiles,
|
|
336
|
-
writeEnvToShell,
|
|
337
|
-
removeEnvFromShell,
|
|
338
|
-
ensureWindowsUserPathHasNpmBin,
|
|
339
|
-
ensureWindowsUserPathHasLocalBin,
|
|
340
|
-
installWindowsCliShims,
|
|
341
|
-
removeWindowsUserEnvVars
|
|
342
|
-
}
|
package/src/utils/which.js
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 跨平台检测命令是否存在
|
|
3
|
-
* Windows 用 where,Unix 用 which,兜底用 --version
|
|
4
|
-
*
|
|
5
|
-
* [HolySheep fork v2.1.40 / hs27] Windows-specific fallbacks.
|
|
6
|
-
*
|
|
7
|
-
* Background: some installers (notably Claude Code's Windows installer
|
|
8
|
-
* irm https://claude.ai/install.ps1 | iex) drop their binary into
|
|
9
|
-
* `%USERPROFILE%\.local\bin\` and print a warning that this directory
|
|
10
|
-
* is NOT on PATH. The *current* Node process started by hs web has a
|
|
11
|
-
* frozen process.env.PATH snapshot, so even after
|
|
12
|
-
* ensureWindowsUserPathHasLocalBin() persists the User PATH in the
|
|
13
|
-
* registry, `where claude` still reports "not found" until the next
|
|
14
|
-
* terminal is opened. The UI then shows 未安装 despite a successful
|
|
15
|
-
* install.
|
|
16
|
-
*
|
|
17
|
-
* Fix strategy (Windows only):
|
|
18
|
-
* 1. Enumerate known install locations as a fallback AFTER `where` fails:
|
|
19
|
-
* %USERPROFILE%\.local\bin\<cmd>.exe (Claude official installer)
|
|
20
|
-
* %APPDATA%\npm\<cmd>.cmd (npm global bin)
|
|
21
|
-
* %LOCALAPPDATA%\Programs\<cmd>\<cmd>.exe
|
|
22
|
-
* %LOCALAPPDATA%\<cmd>\<cmd>.exe
|
|
23
|
-
* 2. Also re-read the USER-level PATH from the registry via PowerShell
|
|
24
|
-
* so we catch updates that landed after this process started.
|
|
25
|
-
*/
|
|
26
|
-
const path = require('path')
|
|
27
|
-
const fs = require('fs')
|
|
28
|
-
const { exec, execSync } = require('child_process')
|
|
29
|
-
|
|
30
|
-
function canRun(command, options = {}) {
|
|
31
|
-
try {
|
|
32
|
-
execSync(command, { stdio: 'ignore', ...options })
|
|
33
|
-
return true
|
|
34
|
-
} catch {
|
|
35
|
-
return false
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function canRunAsync(command, options = {}) {
|
|
40
|
-
return new Promise((resolve) => {
|
|
41
|
-
exec(command, { timeout: 3000, windowsHide: true, ...options }, (error) => {
|
|
42
|
-
resolve(!error)
|
|
43
|
-
})
|
|
44
|
-
})
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// [HolySheep fork v2.1.40 / hs27]
|
|
48
|
-
// Known absolute install locations per-CLI on Windows. Checked AFTER
|
|
49
|
-
// PATH lookup fails but BEFORE we report "not found". Keeping this list
|
|
50
|
-
// small on purpose — only locations that official installers actually use.
|
|
51
|
-
function getWindowsKnownPaths(cmd) {
|
|
52
|
-
if (process.platform !== 'win32') return []
|
|
53
|
-
const home = process.env.USERPROFILE || process.env.HOME || ''
|
|
54
|
-
const appData = process.env.APPDATA || ''
|
|
55
|
-
const localAppData = process.env.LOCALAPPDATA || ''
|
|
56
|
-
const paths = []
|
|
57
|
-
// Claude official installer → %USERPROFILE%\.local\bin\claude.exe
|
|
58
|
-
if (home) paths.push(path.join(home, '.local', 'bin', `${cmd}.exe`))
|
|
59
|
-
// npm global → %APPDATA%\npm\<cmd>.{cmd,exe,ps1}
|
|
60
|
-
if (appData) {
|
|
61
|
-
paths.push(path.join(appData, 'npm', `${cmd}.cmd`))
|
|
62
|
-
paths.push(path.join(appData, 'npm', `${cmd}.exe`))
|
|
63
|
-
}
|
|
64
|
-
// Programs directories → %LOCALAPPDATA%\Programs\<cmd>\<cmd>.exe
|
|
65
|
-
if (localAppData) {
|
|
66
|
-
paths.push(path.join(localAppData, 'Programs', cmd, `${cmd}.exe`))
|
|
67
|
-
paths.push(path.join(localAppData, cmd, `${cmd}.exe`))
|
|
68
|
-
paths.push(path.join(localAppData, 'factory', `${cmd}.exe`)) // droid
|
|
69
|
-
}
|
|
70
|
-
return paths
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function checkKnownPathsSync(cmd) {
|
|
74
|
-
for (const p of getWindowsKnownPaths(cmd)) {
|
|
75
|
-
try {
|
|
76
|
-
if (fs.existsSync(p)) return p
|
|
77
|
-
} catch {}
|
|
78
|
-
}
|
|
79
|
-
return null
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Refresh this process's PATH from the USER-level registry PATH. Cheap — only
|
|
83
|
-
// runs once per commandExists miss. Noop on non-Windows. Guarded by a module
|
|
84
|
-
// flag so we don't run it more than once per short window.
|
|
85
|
-
let _userPathRefreshedAt = 0
|
|
86
|
-
function refreshWindowsUserPath() {
|
|
87
|
-
if (process.platform !== 'win32') return
|
|
88
|
-
const now = Date.now()
|
|
89
|
-
if (now - _userPathRefreshedAt < 5000) return
|
|
90
|
-
_userPathRefreshedAt = now
|
|
91
|
-
try {
|
|
92
|
-
const out = execSync(
|
|
93
|
-
'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Environment]::GetEnvironmentVariable(\'Path\', \'User\')"',
|
|
94
|
-
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000 }
|
|
95
|
-
).trim()
|
|
96
|
-
if (!out) return
|
|
97
|
-
const userParts = out.split(';').map((s) => s.trim()).filter(Boolean)
|
|
98
|
-
const curParts = (process.env.PATH || '').split(';').map((s) => s.trim()).filter(Boolean)
|
|
99
|
-
const curSet = new Set(curParts.map((s) => s.toLowerCase()))
|
|
100
|
-
const added = []
|
|
101
|
-
for (const p of userParts) {
|
|
102
|
-
if (!curSet.has(p.toLowerCase())) {
|
|
103
|
-
curParts.push(p)
|
|
104
|
-
added.push(p)
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (added.length) {
|
|
108
|
-
process.env.PATH = curParts.join(';')
|
|
109
|
-
}
|
|
110
|
-
} catch {
|
|
111
|
-
// noop
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function commandExists(cmd) {
|
|
116
|
-
if (process.platform === 'win32') {
|
|
117
|
-
const variants = [cmd, `${cmd}.cmd`, `${cmd}.exe`, `${cmd}.bat`]
|
|
118
|
-
for (const variant of variants) {
|
|
119
|
-
if (canRun(`where ${variant}`)) return true
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Windows 上很多 npm 全局命令实际是 .cmd 包装器,需要交给 cmd.exe 执行。
|
|
123
|
-
for (const variant of variants) {
|
|
124
|
-
if (canRun(`cmd /d /s /c "${variant} --version"`, { timeout: 3000 })) return true
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// [HolySheep fork v2.1.40 / hs27] Last-resort: re-read the USER-level
|
|
128
|
-
// registry PATH (catches recently installed binaries) and check a
|
|
129
|
-
// handful of known install locations before giving up.
|
|
130
|
-
refreshWindowsUserPath()
|
|
131
|
-
for (const variant of variants) {
|
|
132
|
-
if (canRun(`where ${variant}`)) return true
|
|
133
|
-
}
|
|
134
|
-
if (checkKnownPathsSync(cmd)) return true
|
|
135
|
-
|
|
136
|
-
return false
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
if (canRun(`which ${cmd}`)) return true
|
|
140
|
-
|
|
141
|
-
// 兜底:直接跑 --version
|
|
142
|
-
return canRun(`${cmd} --version`, { timeout: 3000 })
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
async function commandExistsAsync(cmd) {
|
|
146
|
-
if (process.platform === 'win32') {
|
|
147
|
-
const variants = [cmd, `${cmd}.cmd`, `${cmd}.exe`, `${cmd}.bat`]
|
|
148
|
-
for (const variant of variants) {
|
|
149
|
-
if (await canRunAsync(`where ${variant}`)) return true
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
for (const variant of variants) {
|
|
153
|
-
if (await canRunAsync(`cmd /d /s /c "${variant} --version"`)) return true
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// [HolySheep fork v2.1.40 / hs27] Last-resort (see commandExists).
|
|
157
|
-
refreshWindowsUserPath()
|
|
158
|
-
for (const variant of variants) {
|
|
159
|
-
if (await canRunAsync(`where ${variant}`)) return true
|
|
160
|
-
}
|
|
161
|
-
if (checkKnownPathsSync(cmd)) return true
|
|
162
|
-
|
|
163
|
-
return false
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (await canRunAsync(`which ${cmd}`)) return true
|
|
167
|
-
return canRunAsync(`${cmd} --version`)
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
module.exports = {
|
|
171
|
-
commandExists,
|
|
172
|
-
commandExistsAsync,
|
|
173
|
-
// test-only exports
|
|
174
|
-
_getWindowsKnownPaths: getWindowsKnownPaths,
|
|
175
|
-
_refreshWindowsUserPath: refreshWindowsUserPath,
|
|
176
|
-
}
|