@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "2.1.0",
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",
@@ -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
- const resolved = execSync('which bun', {
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: 2000,
40
- }).trim()
41
- if (resolved && fs.existsSync(resolved)) return resolved
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. Fallback: node with experimental support (dist-server/server.mjs is ESM)
44
- // AionUi server.mjs targets bun, but node 20+ with --experimental-vm-modules can work.
45
- // We'll prefer node as a last-resort if bun is absent.
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
- const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL
75
- if (!url) {
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
- const runtime = await resolveRuntime({ allowDownload: true, logger })
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
- if (!runtime && opts.setupRuntime) {
230
- console.log(chalk.gray(' Downloading AionUi runtime...'))
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(' Install via:'))
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
- const bunPath = resolveBunPath()
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(' Install: curl -fsSL https://bun.sh/install | bash'))
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(' Falling back to legacy workspace.'))
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 resolved = execSync('which bun', {
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
- }).trim()
21
- return resolved || null
22
- } catch {
23
- return null
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() {
@@ -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
- json(res, { success: false, message: `验证失败: ${e.message}` }, 500)
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