@simonyea/holysheep-cli 2.1.0 → 2.1.1
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 +54 -22
- 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.1",
|
|
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
|
@@ -31,21 +31,36 @@ function resolveBunPath() {
|
|
|
31
31
|
}
|
|
32
32
|
// 2. Env override
|
|
33
33
|
if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
|
|
34
|
-
// 3. System bun
|
|
34
|
+
// 3. System bun — platform-correct lookup command
|
|
35
|
+
const isWindows = process.platform === 'win32'
|
|
35
36
|
try {
|
|
36
|
-
|
|
37
|
+
// `where.exe bun` on Windows prints one line per match; pick the first.
|
|
38
|
+
// `which bun` on Unix prints exactly one line (or fails with exit !=0).
|
|
39
|
+
const cmd = isWindows ? 'where.exe bun' : 'which bun'
|
|
40
|
+
const raw = execSync(cmd, {
|
|
37
41
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
38
42
|
encoding: 'utf8',
|
|
39
|
-
timeout:
|
|
40
|
-
})
|
|
41
|
-
|
|
43
|
+
timeout: 3000,
|
|
44
|
+
})
|
|
45
|
+
// Windows emits CRLF and may list multiple paths — take the first existing one.
|
|
46
|
+
const candidates = raw.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
47
|
+
for (const p of candidates) {
|
|
48
|
+
if (fs.existsSync(p)) return p
|
|
49
|
+
}
|
|
42
50
|
} catch {}
|
|
43
|
-
// 4.
|
|
44
|
-
//
|
|
45
|
-
//
|
|
51
|
+
// 4. No bun found. AionUi's dist-server/server.mjs uses `bun:` URL scheme
|
|
52
|
+
// imports (verified 2026-04-21: node 25 throws ERR_UNSUPPORTED_ESM_URL_SCHEME
|
|
53
|
+
// immediately), so bun is a hard runtime dep. Caller shows install guidance.
|
|
46
54
|
return null
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
function describeBunInstall() {
|
|
58
|
+
if (process.platform === 'win32') {
|
|
59
|
+
return 'powershell -c "irm bun.sh/install.ps1 | iex"'
|
|
60
|
+
}
|
|
61
|
+
return 'curl -fsSL https://bun.sh/install | bash'
|
|
62
|
+
}
|
|
63
|
+
|
|
49
64
|
// ── Runtime resolution ───────────────────────────────────────────────────────
|
|
50
65
|
// Priority:
|
|
51
66
|
// 1. Local dev checkout at ../aionui-fork/dist-server (developer building locally)
|
|
@@ -71,14 +86,10 @@ function resolveAionUiRuntimeDir() {
|
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
async function downloadAionUiRuntime(logger) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
logger('HOLYSHEEP_AIONUI_RUNTIME_URL not set — cannot auto-download')
|
|
77
|
-
return null
|
|
78
|
-
}
|
|
89
|
+
// Fetcher has a baked-in default URL + SHA256 — no env var required.
|
|
90
|
+
// HOLYSHEEP_AIONUI_RUNTIME_URL still works as an override.
|
|
79
91
|
const { resolveRuntime } = require('../webui/aionui-runtime-fetcher')
|
|
80
|
-
|
|
81
|
-
return runtime
|
|
92
|
+
return resolveRuntime({ allowDownload: true, logger })
|
|
82
93
|
}
|
|
83
94
|
|
|
84
95
|
// ── HolySheep config → API key ───────────────────────────────────────────────
|
|
@@ -224,10 +235,14 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
|
|
|
224
235
|
async function startAionUiMode(opts) {
|
|
225
236
|
const port = Number(opts.port) || 9876
|
|
226
237
|
|
|
227
|
-
// 1. Resolve runtime
|
|
238
|
+
// 1. Resolve runtime. First launch after `npm i -g` won't have one locally
|
|
239
|
+
// — we auto-download from the baked-in URL (verified by SHA256) unless the
|
|
240
|
+
// user opts out via HOLYSHEEP_WEBUI_NO_AUTOFETCH=1.
|
|
228
241
|
let runtime = resolveAionUiRuntimeDir()
|
|
229
|
-
|
|
230
|
-
|
|
242
|
+
const autoFetchDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH === '1'
|
|
243
|
+
if (!runtime && !autoFetchDisabled) {
|
|
244
|
+
console.log(chalk.cyan('▶ AionUi runtime not installed — downloading automatically (~21 MB, one-time)'))
|
|
245
|
+
console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH=1; override URL with HOLYSHEEP_AIONUI_RUNTIME_URL)'))
|
|
231
246
|
const downloaded = await downloadAionUiRuntime((m) => console.log(chalk.gray(` ${m}`)))
|
|
232
247
|
if (downloaded) {
|
|
233
248
|
runtime = { dir: downloaded.dir, source: downloaded.source }
|
|
@@ -235,14 +250,17 @@ async function startAionUiMode(opts) {
|
|
|
235
250
|
}
|
|
236
251
|
if (!runtime) {
|
|
237
252
|
const home = process.env.HOME || process.env.USERPROFILE || '~'
|
|
238
|
-
console.log(chalk.red('✗ AionUi runtime not found'))
|
|
253
|
+
console.log(chalk.red('✗ AionUi runtime not found and auto-download did not succeed'))
|
|
239
254
|
console.log()
|
|
240
255
|
console.log(chalk.gray(' Expected at one of:'))
|
|
241
256
|
console.log(chalk.gray(` • ${path.resolve(__dirname, '..', '..', 'aionui-fork', 'dist-server')} (dev checkout)`))
|
|
242
257
|
console.log(chalk.gray(` • ${path.join(home, '.holysheep', 'aionui-runtime', 'dist-server')} (installed)`))
|
|
243
258
|
console.log()
|
|
244
|
-
console.log(chalk.yellow('
|
|
259
|
+
console.log(chalk.yellow(' Retry manually:'))
|
|
260
|
+
console.log(chalk.cyan(' hs web --setup-runtime'))
|
|
261
|
+
console.log(chalk.gray(' Or pin a specific runtime URL + SHA256:'))
|
|
245
262
|
console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'))
|
|
263
|
+
console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_SHA256=<see README>'))
|
|
246
264
|
console.log(chalk.cyan(' hs web --setup-runtime'))
|
|
247
265
|
console.log()
|
|
248
266
|
if (opts.aionui) {
|
|
@@ -258,12 +276,18 @@ async function startAionUiMode(opts) {
|
|
|
258
276
|
const bunPath = resolveBunPath()
|
|
259
277
|
if (!bunPath) {
|
|
260
278
|
console.log(chalk.red('✗ bun is required to run the AionUi server'))
|
|
261
|
-
console.log(chalk.gray('
|
|
279
|
+
console.log(chalk.gray(' (AionUi uses bun: URL-scheme imports that Node cannot load directly)'))
|
|
280
|
+
console.log()
|
|
281
|
+
console.log(chalk.yellow(' Install bun:'))
|
|
282
|
+
console.log(chalk.cyan(` ${describeBunInstall()}`))
|
|
283
|
+
console.log(chalk.gray(' After install, close and reopen your terminal, then retry `hs web`.'))
|
|
262
284
|
console.log()
|
|
263
285
|
if (opts.aionui) {
|
|
286
|
+
console.log(chalk.red(' --aionui flag requires bun. Aborting.'))
|
|
264
287
|
process.exit(1)
|
|
265
288
|
}
|
|
266
|
-
console.log(chalk.yellow(' Falling back to legacy workspace.'))
|
|
289
|
+
console.log(chalk.yellow(' Falling back to legacy HolySheep workspace.'))
|
|
290
|
+
console.log()
|
|
267
291
|
return startLegacyMode(opts)
|
|
268
292
|
}
|
|
269
293
|
|
|
@@ -366,8 +390,16 @@ async function webui(opts) {
|
|
|
366
390
|
try {
|
|
367
391
|
// 默认走 AionUi;HOLYSHEEP_WEBUI_LEGACY=1 才退回 legacy
|
|
368
392
|
if (isLegacy(opts) && !opts.aionui) {
|
|
393
|
+
console.log(chalk.gray(`[mode=legacy platform=${process.platform}]`))
|
|
394
|
+
console.log()
|
|
369
395
|
return await startLegacyMode(opts)
|
|
370
396
|
}
|
|
397
|
+
// Tri-state mode line: helps user-reports when things go sideways
|
|
398
|
+
const bunFound = resolveBunPath() ? 'found' : 'missing'
|
|
399
|
+
const rt = resolveAionUiRuntimeDir()
|
|
400
|
+
const rtLabel = rt ? rt.source : 'none'
|
|
401
|
+
console.log(chalk.gray(`[mode=aionui platform=${process.platform} bun=${bunFound} runtime=${rtLabel}]`))
|
|
402
|
+
console.log()
|
|
371
403
|
return await startAionUiMode(opts)
|
|
372
404
|
} catch (err) {
|
|
373
405
|
console.log(chalk.red(`✗ 启动失败: ${err.message}`))
|
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
|
|