@simonyea/holysheep-cli 2.1.0 → 2.1.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/package.json +1 -1
- package/src/commands/webui.js +115 -24
- package/src/webui/aionui-runtime.js +17 -5
- package/src/webui/server.js +20 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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
|
"scripts": {
|
|
6
6
|
"test": "node tests/droid.test.js && node tests/workspace-store.test.js",
|
package/src/commands/webui.js
CHANGED
|
@@ -23,6 +23,18 @@ function isLegacy(opts) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
// ── Bun resolution ───────────────────────────────────────────────────────────
|
|
26
|
+
function homeBunCandidate() {
|
|
27
|
+
// bun's official installer always drops the binary at:
|
|
28
|
+
// Windows: %USERPROFILE%\.bun\bin\bun.exe
|
|
29
|
+
// Unix: $HOME/.bun/bin/bun
|
|
30
|
+
// This fallback covers the very common "user just installed bun but hasn't
|
|
31
|
+
// reopened the terminal, so PATH doesn't see it yet" case.
|
|
32
|
+
const home = process.env.HOME || process.env.USERPROFILE || require('os').homedir()
|
|
33
|
+
if (!home) return null
|
|
34
|
+
const name = process.platform === 'win32' ? 'bun.exe' : 'bun'
|
|
35
|
+
return path.join(home, '.bun', 'bin', name)
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
function resolveBunPath() {
|
|
27
39
|
// 1. Dev: use bundled bun if present (darwin-arm64 only in 2.0.x)
|
|
28
40
|
const bundledBun = path.join(__dirname, '..', 'webui', 'vendor', 'bun-darwin-arm64')
|
|
@@ -31,21 +43,75 @@ function resolveBunPath() {
|
|
|
31
43
|
}
|
|
32
44
|
// 2. Env override
|
|
33
45
|
if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
|
|
34
|
-
// 3. System bun
|
|
46
|
+
// 3. System bun — platform-correct lookup command
|
|
47
|
+
const isWindows = process.platform === 'win32'
|
|
35
48
|
try {
|
|
36
|
-
|
|
49
|
+
// `where.exe bun` on Windows prints one line per match; pick the first.
|
|
50
|
+
// `which bun` on Unix prints exactly one line (or fails with exit !=0).
|
|
51
|
+
const cmd = isWindows ? 'where.exe bun' : 'which bun'
|
|
52
|
+
const raw = execSync(cmd, {
|
|
37
53
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
54
|
encoding: 'utf8',
|
|
39
|
-
timeout:
|
|
40
|
-
})
|
|
41
|
-
|
|
55
|
+
timeout: 3000,
|
|
56
|
+
})
|
|
57
|
+
// Windows emits CRLF and may list multiple paths — take the first existing one.
|
|
58
|
+
const candidates = raw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
59
|
+
for (const p of candidates) {
|
|
60
|
+
if (fs.existsSync(p)) return p
|
|
61
|
+
}
|
|
42
62
|
} catch {}
|
|
43
|
-
// 4.
|
|
44
|
-
//
|
|
45
|
-
//
|
|
63
|
+
// 4. ~/.bun/bin fallback — bun installer's canonical location. Catches the
|
|
64
|
+
// "installed bun, didn't reopen terminal" scenario that previously forced
|
|
65
|
+
// users to fallback to legacy workspace on first `hs web`.
|
|
66
|
+
const homeBun = homeBunCandidate()
|
|
67
|
+
if (homeBun && fs.existsSync(homeBun)) return homeBun
|
|
68
|
+
// 5. No bun found. AionUi's dist-server/server.mjs uses `bun:` URL scheme
|
|
69
|
+
// imports (verified 2026-04-21: node 25 throws ERR_UNSUPPORTED_ESM_URL_SCHEME
|
|
70
|
+
// immediately), so bun is a hard runtime dep. Caller can auto-install.
|
|
46
71
|
return null
|
|
47
72
|
}
|
|
48
73
|
|
|
74
|
+
function describeBunInstall() {
|
|
75
|
+
if (process.platform === 'win32') {
|
|
76
|
+
return 'powershell -c "irm bun.sh/install.ps1 | iex"'
|
|
77
|
+
}
|
|
78
|
+
return 'curl -fsSL https://bun.sh/install | bash'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Auto-install bun using the official bun.sh installer. Returns the newly
|
|
82
|
+
// resolved bun path on success, or null on any failure (caller falls back).
|
|
83
|
+
// Controlled by HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN=1 (opt-out).
|
|
84
|
+
function autoInstallBun(logger) {
|
|
85
|
+
const isWindows = process.platform === 'win32'
|
|
86
|
+
// Both installers are official bun.sh first-party scripts. We log the URL
|
|
87
|
+
// explicitly so the user knows what's running.
|
|
88
|
+
const cmd = isWindows
|
|
89
|
+
? 'powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "irm https://bun.sh/install.ps1 | iex"'
|
|
90
|
+
: 'bash -c "curl -fsSL https://bun.sh/install | bash"'
|
|
91
|
+
logger(`[auto-install-bun] platform=${process.platform} command=${cmd}`)
|
|
92
|
+
logger(' Fetching official bun installer from https://bun.sh — this takes ~30s')
|
|
93
|
+
try {
|
|
94
|
+
execSync(cmd, {
|
|
95
|
+
stdio: 'inherit',
|
|
96
|
+
timeout: 180_000,
|
|
97
|
+
// Keep env minimal — don't leak weird node_options into the installer
|
|
98
|
+
env: { ...process.env, NODE_OPTIONS: '' },
|
|
99
|
+
})
|
|
100
|
+
} catch (e) {
|
|
101
|
+
logger(`[auto-install-bun] failed: ${e.message || e}`)
|
|
102
|
+
return null
|
|
103
|
+
}
|
|
104
|
+
// Re-resolve — the installer drops bun at ~/.bun/bin/bun[.exe], which our
|
|
105
|
+
// resolveBunPath() fallback (step 4) picks up without needing a shell reload.
|
|
106
|
+
const p = resolveBunPath()
|
|
107
|
+
if (!p) {
|
|
108
|
+
logger('[auto-install-bun] installer finished but bun still not found at ~/.bun/bin — aborting')
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
logger(`[auto-install-bun] success → ${p}`)
|
|
112
|
+
return p
|
|
113
|
+
}
|
|
114
|
+
|
|
49
115
|
// ── Runtime resolution ───────────────────────────────────────────────────────
|
|
50
116
|
// Priority:
|
|
51
117
|
// 1. Local dev checkout at ../aionui-fork/dist-server (developer building locally)
|
|
@@ -71,14 +137,10 @@ function resolveAionUiRuntimeDir() {
|
|
|
71
137
|
}
|
|
72
138
|
|
|
73
139
|
async function downloadAionUiRuntime(logger) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
logger('HOLYSHEEP_AIONUI_RUNTIME_URL not set — cannot auto-download')
|
|
77
|
-
return null
|
|
78
|
-
}
|
|
140
|
+
// Fetcher has a baked-in default URL + SHA256 — no env var required.
|
|
141
|
+
// HOLYSHEEP_AIONUI_RUNTIME_URL still works as an override.
|
|
79
142
|
const { resolveRuntime } = require('../webui/aionui-runtime-fetcher')
|
|
80
|
-
|
|
81
|
-
return runtime
|
|
143
|
+
return resolveRuntime({ allowDownload: true, logger })
|
|
82
144
|
}
|
|
83
145
|
|
|
84
146
|
// ── HolySheep config → API key ───────────────────────────────────────────────
|
|
@@ -224,25 +286,33 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
|
|
|
224
286
|
async function startAionUiMode(opts) {
|
|
225
287
|
const port = Number(opts.port) || 9876
|
|
226
288
|
|
|
227
|
-
// 1. Resolve runtime
|
|
289
|
+
// 1. Resolve runtime. First launch after `npm i -g` won't have one locally
|
|
290
|
+
// — we auto-download from the baked-in URL (verified by SHA256) unless the
|
|
291
|
+
// user opts out via HOLYSHEEP_WEBUI_NO_AUTOFETCH=1.
|
|
228
292
|
let runtime = resolveAionUiRuntimeDir()
|
|
229
|
-
|
|
230
|
-
|
|
293
|
+
const autoFetchDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH === '1'
|
|
294
|
+
if (!runtime && !autoFetchDisabled) {
|
|
295
|
+
console.log(chalk.cyan('▶ AionUi runtime not installed — downloading automatically (~21 MB, one-time)'))
|
|
296
|
+
console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH=1; override URL with HOLYSHEEP_AIONUI_RUNTIME_URL)'))
|
|
231
297
|
const downloaded = await downloadAionUiRuntime((m) => console.log(chalk.gray(` ${m}`)))
|
|
232
298
|
if (downloaded) {
|
|
233
299
|
runtime = { dir: downloaded.dir, source: downloaded.source }
|
|
300
|
+
console.log(chalk.gray(' AionUi runtime installed. Next: ensure bun is available to launch it.'))
|
|
234
301
|
}
|
|
235
302
|
}
|
|
236
303
|
if (!runtime) {
|
|
237
304
|
const home = process.env.HOME || process.env.USERPROFILE || '~'
|
|
238
|
-
console.log(chalk.red('✗ AionUi runtime not found'))
|
|
305
|
+
console.log(chalk.red('✗ AionUi runtime not found and auto-download did not succeed'))
|
|
239
306
|
console.log()
|
|
240
307
|
console.log(chalk.gray(' Expected at one of:'))
|
|
241
308
|
console.log(chalk.gray(` • ${path.resolve(__dirname, '..', '..', 'aionui-fork', 'dist-server')} (dev checkout)`))
|
|
242
309
|
console.log(chalk.gray(` • ${path.join(home, '.holysheep', 'aionui-runtime', 'dist-server')} (installed)`))
|
|
243
310
|
console.log()
|
|
244
|
-
console.log(chalk.yellow('
|
|
311
|
+
console.log(chalk.yellow(' Retry manually:'))
|
|
312
|
+
console.log(chalk.cyan(' hs web --setup-runtime'))
|
|
313
|
+
console.log(chalk.gray(' Or pin a specific runtime URL + SHA256:'))
|
|
245
314
|
console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'))
|
|
315
|
+
console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_SHA256=<see README>'))
|
|
246
316
|
console.log(chalk.cyan(' hs web --setup-runtime'))
|
|
247
317
|
console.log()
|
|
248
318
|
if (opts.aionui) {
|
|
@@ -254,16 +324,29 @@ async function startAionUiMode(opts) {
|
|
|
254
324
|
return startLegacyMode(opts)
|
|
255
325
|
}
|
|
256
326
|
|
|
257
|
-
// 2. Resolve bun
|
|
258
|
-
|
|
327
|
+
// 2. Resolve bun — and auto-install if missing (opt-out via env)
|
|
328
|
+
let bunPath = resolveBunPath()
|
|
329
|
+
const autoBunDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN === '1'
|
|
330
|
+
if (!bunPath && !autoBunDisabled) {
|
|
331
|
+
console.log(chalk.cyan('▶ bun runtime not installed — installing automatically (one-time)'))
|
|
332
|
+
console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN=1; takes ~30s; source: bun.sh official installer)'))
|
|
333
|
+
bunPath = autoInstallBun((m) => console.log(chalk.gray(` ${m}`)))
|
|
334
|
+
}
|
|
259
335
|
if (!bunPath) {
|
|
260
336
|
console.log(chalk.red('✗ bun is required to run the AionUi server'))
|
|
261
|
-
console.log(chalk.gray('
|
|
337
|
+
console.log(chalk.gray(' (AionUi uses bun: URL-scheme imports that Node cannot load directly)'))
|
|
338
|
+
console.log(chalk.gray(' AionUi runtime itself is ready — bun is the last missing piece.'))
|
|
339
|
+
console.log()
|
|
340
|
+
console.log(chalk.yellow(' Install bun manually:'))
|
|
341
|
+
console.log(chalk.cyan(` ${describeBunInstall()}`))
|
|
342
|
+
console.log(chalk.gray(' After install, close and reopen your terminal, then retry `hs web`.'))
|
|
262
343
|
console.log()
|
|
263
344
|
if (opts.aionui) {
|
|
345
|
+
console.log(chalk.red(' --aionui flag requires bun. Aborting.'))
|
|
264
346
|
process.exit(1)
|
|
265
347
|
}
|
|
266
|
-
console.log(chalk.yellow('
|
|
348
|
+
console.log(chalk.yellow(' AionUi runtime ready but bun auto-install failed — falling back to legacy HolySheep workspace.'))
|
|
349
|
+
console.log()
|
|
267
350
|
return startLegacyMode(opts)
|
|
268
351
|
}
|
|
269
352
|
|
|
@@ -366,8 +449,16 @@ async function webui(opts) {
|
|
|
366
449
|
try {
|
|
367
450
|
// 默认走 AionUi;HOLYSHEEP_WEBUI_LEGACY=1 才退回 legacy
|
|
368
451
|
if (isLegacy(opts) && !opts.aionui) {
|
|
452
|
+
console.log(chalk.gray(`[mode=legacy platform=${process.platform}]`))
|
|
453
|
+
console.log()
|
|
369
454
|
return await startLegacyMode(opts)
|
|
370
455
|
}
|
|
456
|
+
// Tri-state mode line: helps user-reports when things go sideways
|
|
457
|
+
const bunFound = resolveBunPath() ? 'found' : 'missing'
|
|
458
|
+
const rt = resolveAionUiRuntimeDir()
|
|
459
|
+
const rtLabel = rt ? rt.source : 'none'
|
|
460
|
+
console.log(chalk.gray(`[mode=aionui platform=${process.platform} bun=${bunFound} runtime=${rtLabel}]`))
|
|
461
|
+
console.log()
|
|
371
462
|
return await startAionUiMode(opts)
|
|
372
463
|
} catch (err) {
|
|
373
464
|
console.log(chalk.red(`✗ 启动失败: ${err.message}`))
|
|
@@ -12,16 +12,28 @@ function resolveBunPath() {
|
|
|
12
12
|
return bundledBun
|
|
13
13
|
}
|
|
14
14
|
if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
|
|
15
|
+
const isWindows = process.platform === 'win32'
|
|
15
16
|
try {
|
|
16
|
-
const
|
|
17
|
+
const cmd = isWindows ? 'where.exe bun' : 'which bun'
|
|
18
|
+
const raw = execSync(cmd, {
|
|
17
19
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
18
20
|
encoding: 'utf8',
|
|
19
21
|
timeout: 2000,
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
})
|
|
23
|
+
const candidates = String(raw).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
24
|
+
for (const p of candidates) {
|
|
25
|
+
if (fs.existsSync(p)) return p
|
|
26
|
+
}
|
|
27
|
+
} catch {}
|
|
28
|
+
// ~/.bun/bin fallback — canonical location of bun's official installer.
|
|
29
|
+
// Catches "installed bun but didn't reopen terminal" without needing PATH refresh.
|
|
30
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir()
|
|
31
|
+
if (home) {
|
|
32
|
+
const name = isWindows ? 'bun.exe' : 'bun'
|
|
33
|
+
const homeBun = path.join(home, '.bun', 'bin', name)
|
|
34
|
+
if (fs.existsSync(homeBun)) return homeBun
|
|
24
35
|
}
|
|
36
|
+
return null
|
|
25
37
|
}
|
|
26
38
|
|
|
27
39
|
function getRuntimeCandidates() {
|
package/src/webui/server.js
CHANGED
|
@@ -239,7 +239,7 @@ async function handleLogin(req, res) {
|
|
|
239
239
|
}
|
|
240
240
|
try {
|
|
241
241
|
const valid = await validateApiKey(apiKey)
|
|
242
|
-
if (!valid) return json(res, { success: false, message: 'API Key 无效' }, 401)
|
|
242
|
+
if (!valid) return json(res, { success: false, message: 'API Key 无效 (server returned non-2xx)' }, 401)
|
|
243
243
|
saveConfig({ apiKey, savedAt: new Date().toISOString() })
|
|
244
244
|
workspaceStore.saveHolySheepApiConfig({
|
|
245
245
|
apiKey,
|
|
@@ -248,7 +248,25 @@ async function handleLogin(req, res) {
|
|
|
248
248
|
})
|
|
249
249
|
json(res, { success: true, apiKey: maskKey(apiKey) })
|
|
250
250
|
} catch (e) {
|
|
251
|
-
|
|
251
|
+
// Translate cryptic node-fetch / DNS / proxy errors into something actionable.
|
|
252
|
+
// Previously users just saw "链接失败" in the browser alert — now we surface
|
|
253
|
+
// the underlying cause (DNS, timeout, proxy interference, TLS, etc).
|
|
254
|
+
const code = e && (e.code || e.errno || e.type)
|
|
255
|
+
const name = e && e.name
|
|
256
|
+
let hint
|
|
257
|
+
if (code === 'EAI_AGAIN' || code === 'ENOTFOUND') {
|
|
258
|
+
hint = 'DNS 解析失败 — 检查网络 / 代理 / 防火墙是否拦截了 api.holysheep.ai'
|
|
259
|
+
} else if (code === 'ETIMEDOUT' || name === 'AbortError') {
|
|
260
|
+
hint = '连接超时 — 可能是网络慢或代理阻塞;试试开关代理再重试'
|
|
261
|
+
} else if (code === 'ECONNREFUSED' || code === 'ECONNRESET') {
|
|
262
|
+
hint = '连接被拒绝 — 检查本机防火墙或公司代理'
|
|
263
|
+
} else if (code === 'CERT_HAS_EXPIRED' || code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
264
|
+
hint = 'TLS 证书校验失败 — 可能是系统时间错误或中间人代理'
|
|
265
|
+
} else {
|
|
266
|
+
hint = e && e.message ? e.message : String(e)
|
|
267
|
+
}
|
|
268
|
+
const detail = `${hint}${code ? ` [${code}]` : ''}`
|
|
269
|
+
json(res, { success: false, message: `验证失败: ${detail}` }, 500)
|
|
252
270
|
}
|
|
253
271
|
}
|
|
254
272
|
|