@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.
Files changed (45) hide show
  1. package/dist/configure-worker.js +4491 -0
  2. package/dist/index.js +9591 -0
  3. package/dist/process-proxy-inject.js +117 -0
  4. package/package.json +20 -7
  5. package/.gitea/workflows/sanity.yml +0 -125
  6. package/scripts/check-tarball-size.js +0 -44
  7. package/src/commands/balance.js +0 -57
  8. package/src/commands/claude-proxy.js +0 -248
  9. package/src/commands/claude.js +0 -135
  10. package/src/commands/doctor.js +0 -282
  11. package/src/commands/login.js +0 -211
  12. package/src/commands/openclaw.js +0 -258
  13. package/src/commands/reset.js +0 -53
  14. package/src/commands/setup.js +0 -493
  15. package/src/commands/upgrade.js +0 -168
  16. package/src/commands/webui.js +0 -622
  17. package/src/index.js +0 -226
  18. package/src/tools/aider.js +0 -78
  19. package/src/tools/antigravity.js +0 -42
  20. package/src/tools/claude-code.js +0 -228
  21. package/src/tools/claude-process-proxy.js +0 -1030
  22. package/src/tools/codex.js +0 -254
  23. package/src/tools/continue.js +0 -146
  24. package/src/tools/cursor.js +0 -71
  25. package/src/tools/droid.js +0 -281
  26. package/src/tools/env-config.js +0 -185
  27. package/src/tools/gemini-cli.js +0 -82
  28. package/src/tools/hermes.js +0 -354
  29. package/src/tools/index.js +0 -13
  30. package/src/tools/openclaw-bridge.js +0 -987
  31. package/src/tools/openclaw.js +0 -925
  32. package/src/tools/opencode.js +0 -227
  33. package/src/tools/process-proxy-inject.js +0 -142
  34. package/src/utils/config.js +0 -54
  35. package/src/utils/shell.js +0 -342
  36. package/src/utils/which.js +0 -176
  37. package/src/webui/aionui-runtime-fetcher.js +0 -429
  38. package/src/webui/aionui-runtime.js +0 -139
  39. package/src/webui/aionui-wrapper.js +0 -734
  40. package/src/webui/configure-worker.js +0 -67
  41. package/src/webui/server.js +0 -1572
  42. package/src/webui/workspace-runtime.js +0 -288
  43. package/src/webui/workspace-store.js +0 -325
  44. /package/{src/webui → dist}/index.html +0 -0
  45. /package/{src/tools → dist}/pty-hermes-wrapper.py +0 -0
@@ -1,622 +0,0 @@
1
- /**
2
- * hs web — 启动 WebUI 本地管理面板
3
- *
4
- * 2.1.0 开始:`hs web` 默认直接启动 **真 AionUi v1.9.18 源码 fork 的 server**
5
- * (`dist-server/server.mjs`),登录改成 HolySheep API Key(源码级改造,不是
6
- * proxy wrapper 也不是外壳)。用户看到的就是 AionUi 自己的 UI。
7
- *
8
- * Modes:
9
- * <default> → AionUi fork + HolySheep API Key login
10
- * HOLYSHEEP_WEBUI_LEGACY=1 → 旧 HolySheep Workspace 页面 (fallback)
11
- * --aionui → 强制走 AionUi 路径(即使 runtime 缺失也不退回)
12
- */
13
- 'use strict'
14
-
15
- const chalk = require('chalk')
16
- const { execSync, spawn } = require('child_process')
17
- const fs = require('fs')
18
- const path = require('path')
19
- const http = require('http')
20
-
21
- function isLegacy(opts) {
22
- return process.env.HOLYSHEEP_WEBUI_LEGACY === '1'
23
- }
24
-
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
-
38
- function resolveBunPath() {
39
- // 1. Dev: use bundled bun if present (darwin-arm64 only in 2.0.x)
40
- const bundledBun = path.join(__dirname, '..', 'webui', 'vendor', 'bun-darwin-arm64')
41
- if (process.platform === 'darwin' && process.arch === 'arm64' && fs.existsSync(bundledBun)) {
42
- return bundledBun
43
- }
44
- // 2. Env override
45
- if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
46
- // 3. System bun — platform-correct lookup command
47
- const isWindows = process.platform === 'win32'
48
- try {
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, {
53
- stdio: ['ignore', 'pipe', 'ignore'],
54
- encoding: 'utf8',
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
- }
62
- } catch {}
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.
71
- return null
72
- }
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
-
115
- // ── Runtime resolution ───────────────────────────────────────────────────────
116
- // This file used to have its own "does `server.mjs` exist?" resolver and
117
- // short-circuit before the fetcher was ever consulted. That's why earlier
118
- // releases' version-drift detection (added in `aionui-runtime-fetcher.js`)
119
- // never kicked in — we never called it. Now the single source of truth is
120
- // `resolveRuntime()` from the fetcher module, with stale detection + atomic
121
- // upgrade baked in. We just call it twice:
122
- // * Once synchronously-shaped (allowDownload:false) for the "[mode=…]"
123
- // status line so we don't incur a network hit just to print a label.
124
- // * Once with allowDownload:true inside startAionUiMode, where we're
125
- // willing to pay the ~20MB download to get a fresh runtime.
126
- async function resolveAionUiRuntime({ allowDownload, logger = () => {} } = {}) {
127
- const { resolveRuntime } = require('../webui/aionui-runtime-fetcher')
128
- return resolveRuntime({ allowDownload: !!allowDownload, logger })
129
- }
130
-
131
- // Cheap probe for the status line. No download, never blocks.
132
- function probeAionUiRuntimeLabel() {
133
- try {
134
- const {
135
- USER_CACHE_DIR,
136
- VENDOR_DIR,
137
- isValidRuntimeDir,
138
- isVersionCurrent,
139
- } = require('../webui/aionui-runtime-fetcher')
140
-
141
- // aionui-fork/ label only appears when dev opted-in via HOLYSHEEP_DEV=1 —
142
- // must match the resolver gating in aionui-runtime-fetcher.js, otherwise
143
- // the status line would lie ("runtime=aionui-fork" when the actual resolver
144
- // is about to return user-cache).
145
- if (process.env.HOLYSHEEP_DEV === '1') {
146
- const forkDir = path.resolve(__dirname, '..', '..', 'aionui-fork')
147
- if (isValidRuntimeDir(forkDir)) return 'dev-checkout'
148
- }
149
- if (isValidRuntimeDir(VENDOR_DIR)) return 'vendor'
150
- if (isValidRuntimeDir(USER_CACHE_DIR)) {
151
- return isVersionCurrent(USER_CACHE_DIR) ? 'installed' : 'installed-stale'
152
- }
153
- } catch {}
154
- return 'none'
155
- }
156
-
157
- async function downloadAionUiRuntime(logger) {
158
- // Fetcher has a baked-in default URL + SHA256 — no env var required.
159
- // HOLYSHEEP_AIONUI_RUNTIME_URL still works as an override.
160
- return resolveAionUiRuntime({ allowDownload: true, logger })
161
- }
162
-
163
- // ── HolySheep config → API key ───────────────────────────────────────────────
164
- function readHolySheepApiKey() {
165
- try {
166
- const { getApiKey } = require('../utils/config')
167
- const key = getApiKey()
168
- if (key && /^cr_/.test(key)) return key
169
- } catch {}
170
- return null
171
- }
172
-
173
- // ── HTTP utility: POST JSON with CSRF auto-bootstrap ─────────────────────────
174
- function httpRequest(options, body) {
175
- return new Promise((resolve, reject) => {
176
- const req = http.request(options, (res) => {
177
- const chunks = []
178
- res.on('data', (c) => chunks.push(c))
179
- res.on('end', () => {
180
- const buf = Buffer.concat(chunks)
181
- resolve({ status: res.statusCode || 0, headers: res.headers, body: buf.toString('utf8') })
182
- })
183
- })
184
- req.on('error', reject)
185
- if (body) req.write(body)
186
- req.end()
187
- })
188
- }
189
-
190
- async function fetchCsrfToken(port) {
191
- // AionUi sets csrf cookie on first GET /. We fetch / and read Set-Cookie.
192
- const r = await httpRequest({ host: '127.0.0.1', port, path: '/', method: 'GET' })
193
- const setCookie = r.headers['set-cookie'] || []
194
- for (const sc of setCookie) {
195
- const m = sc.match(/csrfToken=([^;]+);/)
196
- if (m) return { csrfToken: m[1], allCookies: setCookie.map((s) => s.split(';')[0]).join('; ') }
197
- }
198
- return null
199
- }
200
-
201
- async function loginWithApiKey(port, apiKey) {
202
- const csrf = await fetchCsrfToken(port)
203
- if (!csrf) throw new Error('Failed to acquire CSRF token from HolySheep runtime')
204
- const body = JSON.stringify({ apiKey, csrfToken: decodeURIComponent(csrf.csrfToken) })
205
- const r = await httpRequest(
206
- {
207
- host: '127.0.0.1',
208
- port,
209
- path: '/login',
210
- method: 'POST',
211
- headers: {
212
- 'Content-Type': 'application/json',
213
- 'Content-Length': Buffer.byteLength(body),
214
- Cookie: csrf.allCookies,
215
- 'x-csrf-token': decodeURIComponent(csrf.csrfToken),
216
- },
217
- },
218
- body
219
- )
220
- if (r.status !== 200) {
221
- throw new Error(`/login returned ${r.status}: ${r.body.slice(0, 200)}`)
222
- }
223
- const setCookie = r.headers['set-cookie'] || []
224
- // Return a Cookie header string the browser can be seeded with.
225
- const cookieLine = setCookie.map((s) => s.split(';')[0]).join('; ')
226
- return { cookieLine, body: JSON.parse(r.body) }
227
- }
228
-
229
- // ── Start patched AionUi server ──────────────────────────────────────────────
230
- // Timeout is generous for Windows first launch: Defender scans 41MB server.mjs,
231
- // bun JIT-compiles, AionUi initializes sqlite under ~/.aionui-home, and npm
232
- // may still pull the renderer. 60s is the realistic upper bound on a cold box.
233
- const AIONUI_STARTUP_TIMEOUT_MS = Number(process.env.HS_WEB_STARTUP_TIMEOUT_MS) || 60_000
234
- const AIONUI_LOG_TAIL_BYTES = 4096
235
-
236
- function spawnAionUiServer({ bunPath, runtimeDir, port }) {
237
- return new Promise((resolve, reject) => {
238
- const entry = path.join(runtimeDir, 'dist-server', 'server.mjs')
239
- if (!fs.existsSync(entry)) {
240
- return reject(new Error(`HolySheep runtime entry not found: ${entry}`))
241
- }
242
-
243
- // AionUi reads PORT / HOST / JWT_SECRET from env. We set a stable JWT secret
244
- // derived from machine so cookies survive restarts of hs web within a session.
245
- const child = spawn(bunPath, [entry], {
246
- cwd: runtimeDir,
247
- env: {
248
- ...process.env,
249
- PORT: String(port),
250
- HOST: '127.0.0.1',
251
- NODE_ENV: 'production',
252
- // Let AionUi pick its own JWT secret; it'll persist under ~/.aionui-home
253
- },
254
- stdio: ['ignore', 'pipe', 'pipe'],
255
- })
256
-
257
- // ── Log capture ────────────────────────────────────────────────────────
258
- // Always subscribe to stdout+stderr. In debug mode we ALSO print live; in
259
- // normal mode we keep the last ~4KB in a ring buffer so that if startup
260
- // fails (timeout OR exit-before-ready) we can surface the real error to
261
- // the user instead of the previous silent "failed to start within 20s".
262
- const debug = process.env.HS_WEB_DEBUG === '1'
263
- let logTail = ''
264
- const appendLog = (stream, chunk) => {
265
- const s = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
266
- logTail += s
267
- if (logTail.length > AIONUI_LOG_TAIL_BYTES) {
268
- logTail = logTail.slice(logTail.length - AIONUI_LOG_TAIL_BYTES)
269
- }
270
- if (debug) {
271
- const target = stream === 'err' ? process.stderr : process.stdout
272
- target.write(chalk.gray(`[holysheep] ${s}`))
273
- }
274
- }
275
- child.stdout.on('data', (d) => appendLog('out', d))
276
- child.stderr.on('data', (d) => appendLog('err', d))
277
-
278
- // ── Progress + timeout ─────────────────────────────────────────────────
279
- let settled = false
280
- const startedAt = Date.now()
281
- const onReady = () => {
282
- if (!settled) {
283
- settled = true
284
- clearInterval(progressTick)
285
- resolve(child)
286
- }
287
- }
288
- const fail = (reason) => {
289
- if (settled) return
290
- settled = true
291
- clearInterval(progressTick)
292
- const tail = logTail.trim()
293
- let msg = reason
294
- if (tail) {
295
- msg += `\n\n --- last HolySheep runtime output (stderr+stdout tail) ---\n${tail
296
- .split(/\r?\n/)
297
- .map((line) => ` ${line}`)
298
- .join('\n')}\n ------------------------------------------------`
299
- } else {
300
- msg += '\n (no output captured from HolySheep runtime — check ' +
301
- (process.platform === 'win32' ? 'Windows Defender / antivirus quarantining bun.exe' : 'bun or runtime corruption') +
302
- ')'
303
- }
304
- reject(new Error(msg))
305
- }
306
-
307
- // Progress log every 10s, so user knows we're not hung. On Windows first
308
- // launch (Defender scanning bun + bun JIT + sqlite init), 30-50s is normal.
309
- const progressTick = setInterval(() => {
310
- if (settled) return
311
- const waited = Math.round((Date.now() - startedAt) / 1000)
312
- const hint = process.platform === 'win32'
313
- ? ' (Windows first-launch can take up to 60s while Defender scans bun.exe + server.mjs)'
314
- : ''
315
- console.log(chalk.gray(` still starting HolySheep runtime — waited ${waited}s…${hint}`))
316
- }, 10_000)
317
-
318
- const timer = setTimeout(() => {
319
- fail(`HolySheep runtime failed to start within ${Math.round(AIONUI_STARTUP_TIMEOUT_MS / 1000)}s`)
320
- }, AIONUI_STARTUP_TIMEOUT_MS)
321
-
322
- // ── Readiness poll ─────────────────────────────────────────────────────
323
- const pollReady = () => {
324
- if (settled) return
325
- const req = http.request(
326
- { host: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 500 },
327
- (res) => {
328
- if (res.statusCode && res.statusCode < 500) {
329
- clearTimeout(timer)
330
- onReady()
331
- } else {
332
- setTimeout(pollReady, 300)
333
- }
334
- }
335
- )
336
- req.on('error', () => setTimeout(pollReady, 300))
337
- req.on('timeout', () => {
338
- req.destroy()
339
- setTimeout(pollReady, 300)
340
- })
341
- req.end()
342
- }
343
- // Start polling after a short grace period so bun has time to boot.
344
- setTimeout(pollReady, 600)
345
-
346
- // ── Early exit ─────────────────────────────────────────────────────────
347
- child.on('exit', (code, signal) => {
348
- const reason = signal
349
- ? `HolySheep runtime exited with signal ${signal} before becoming ready`
350
- : `HolySheep runtime exited with code ${code} before becoming ready`
351
- clearTimeout(timer)
352
- fail(reason)
353
- })
354
- child.on('error', (err) => {
355
- clearTimeout(timer)
356
- fail(`HolySheep runtime spawn error: ${err.message || err}`)
357
- })
358
- })
359
- }
360
-
361
- // ── The real AionUi-mode flow ────────────────────────────────────────────────
362
- async function startAionUiMode(opts) {
363
- const port = Number(opts.port) || 9876
364
-
365
- // 1. Resolve runtime. Fetcher returns one of:
366
- // source='user-cache' → version matches baked-in DEFAULT_RUNTIME_VERSION
367
- // source='user-cache-stale' → version mismatch AND upgrade was skipped/failed
368
- // (we can still boot with the old runtime,
369
- // but the user sees an explicit warning)
370
- // source='download-upgrade' → cache was stale, we auto-upgraded in place
371
- // source='download' → first-ever install, freshly downloaded
372
- // source='aionui-fork'/'vendor' → dev checkout
373
- //
374
- // We always call with allowDownload:true here (unless the user disabled
375
- // auto-fetch) so stale caches get upgraded the same way first-run downloads
376
- // happen. Before 2.1.10 the local resolver short-circuited on "file exists"
377
- // and stale caches stayed stale forever.
378
- const autoFetchDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH === '1'
379
- if (!autoFetchDisabled) {
380
- // Pre-announce only when we know we're going to hit the network. For
381
- // first-ever install we always do. For stale cache we _may_ do, depending
382
- // on HOLYSHEEP_AIONUI_SKIP_UPGRADE.
383
- const probe = probeAionUiRuntimeLabel()
384
- if (probe === 'none') {
385
- console.log(chalk.cyan('▶ HolySheep runtime not installed — downloading automatically (~21 MB, one-time)'))
386
- console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH=1; override URL with HOLYSHEEP_AIONUI_RUNTIME_URL)'))
387
- } else if (probe === 'installed-stale' && process.env.HOLYSHEEP_AIONUI_SKIP_UPGRADE !== '1') {
388
- console.log(chalk.cyan('▶ HolySheep runtime is out of date — upgrading in place (atomic)'))
389
- console.log(chalk.gray(' (set HOLYSHEEP_AIONUI_SKIP_UPGRADE=1 to pin current cache; HOLYSHEEP_AIONUI_PIN_VERSION=<v> to lock a version)'))
390
- }
391
- }
392
- let runtime = await resolveAionUiRuntime({
393
- allowDownload: !autoFetchDisabled,
394
- logger: (m) => console.log(chalk.gray(` ${m}`)),
395
- })
396
- if (runtime && runtime.source === 'download-upgrade') {
397
- console.log(chalk.green(` ✓ HolySheep runtime upgraded to ${runtime.version}`))
398
- } else if (runtime && runtime.source === 'user-cache-stale') {
399
- console.log(chalk.yellow(
400
- ` ⚠ Running stale HolySheep runtime (${runtime.version}, expected newer). ` +
401
- `Run: rm -rf ~/.holysheep/aionui-runtime && hs web to force rebuild.`
402
- ))
403
- } else if (runtime && runtime.source === 'download') {
404
- console.log(chalk.gray(' HolySheep runtime installed. Next: ensure bun is available to launch it.'))
405
- }
406
- if (!runtime) {
407
- const home = process.env.HOME || process.env.USERPROFILE || '~'
408
- console.log(chalk.red('✗ HolySheep runtime not found and auto-download did not succeed'))
409
- console.log()
410
- console.log(chalk.gray(' Expected at one of:'))
411
- console.log(chalk.gray(` • ${path.resolve(__dirname, '..', '..', 'aionui-fork', 'dist-server')} (dev checkout)`))
412
- console.log(chalk.gray(` • ${path.join(home, '.holysheep', 'aionui-runtime', 'dist-server')} (installed)`))
413
- console.log()
414
- console.log(chalk.yellow(' Retry manually:'))
415
- console.log(chalk.cyan(' hs web --setup-runtime'))
416
- console.log(chalk.gray(' Or pin a specific runtime URL + SHA256:'))
417
- console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'))
418
- console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_SHA256=<see README>'))
419
- console.log(chalk.cyan(' hs web --setup-runtime'))
420
- console.log()
421
- if (opts.aionui) {
422
- console.log(chalk.red(' --aionui flag requires runtime. Aborting.'))
423
- process.exit(1)
424
- }
425
- console.log(chalk.yellow(' Falling back to legacy HolySheep workspace. Set HOLYSHEEP_WEBUI_LEGACY=1 to silence this message.'))
426
- console.log()
427
- return startLegacyMode(opts)
428
- }
429
-
430
- // 2. Resolve bun — and auto-install if missing (opt-out via env, TTY-gated)
431
- let bunPath = resolveBunPath()
432
- const autoBunDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN === '1'
433
- if (!bunPath && !autoBunDisabled) {
434
- // TTY guard: auto-install pipes a remote script into a shell. In CI,
435
- // Docker builds, or other non-interactive environments the user can't
436
- // cancel and didn't knowingly opt in. Skip auto-install there and fall
437
- // through to the existing manual-install guidance + legacy fallback.
438
- if (!process.stdout.isTTY) {
439
- console.log(chalk.yellow(' bun missing and not running in a TTY — skipping auto-install (safety).'))
440
- console.log(chalk.gray(' In CI / Docker / non-interactive shells, install bun explicitly first:'))
441
- console.log(chalk.cyan(` ${describeBunInstall()}`))
442
- console.log(chalk.gray(' Or opt out of this safety with HOLYSHEEP_WEBUI_FORCE_AUTOFETCH_BUN=1.'))
443
- if (process.env.HOLYSHEEP_WEBUI_FORCE_AUTOFETCH_BUN === '1') {
444
- console.log(chalk.cyan('▶ HOLYSHEEP_WEBUI_FORCE_AUTOFETCH_BUN=1 set — attempting auto-install anyway'))
445
- bunPath = autoInstallBun((m) => console.log(chalk.gray(` ${m}`)))
446
- }
447
- } else {
448
- console.log(chalk.cyan('▶ bun runtime not installed — installing automatically (one-time)'))
449
- console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN=1; takes ~30s; source: bun.sh official installer)'))
450
- bunPath = autoInstallBun((m) => console.log(chalk.gray(` ${m}`)))
451
- }
452
- }
453
- if (!bunPath) {
454
- console.log(chalk.red('✗ bun is required to run the HolySheep runtime'))
455
- console.log(chalk.gray(' (HolySheep runtime uses bun: URL-scheme imports that Node cannot load directly)'))
456
- console.log(chalk.gray(' HolySheep runtime itself is ready — bun is the last missing piece.'))
457
- console.log()
458
- console.log(chalk.yellow(' Install bun manually:'))
459
- console.log(chalk.cyan(` ${describeBunInstall()}`))
460
- console.log(chalk.gray(' After install, close and reopen your terminal, then retry `hs web`.'))
461
- console.log()
462
- if (opts.aionui) {
463
- console.log(chalk.red(' --aionui flag requires bun. Aborting.'))
464
- process.exit(1)
465
- }
466
- console.log(chalk.yellow(' HolySheep runtime ready but bun auto-install failed — falling back to legacy HolySheep workspace.'))
467
- console.log()
468
- return startLegacyMode(opts)
469
- }
470
-
471
- console.log(chalk.cyan(`▶ Starting HolySheep runtime (source: ${runtime.source})`))
472
-
473
- // 3. Spawn AionUi on an internal loopback port and wrap it with
474
- // `aionui-wrapper.js` which listens on the user-visible port. This is the
475
- // critical architectural piece that makes `/api/holysheep/balance`,
476
- // `/api/holysheep/tools`, `/api/holysheep/setup`, etc. actually reachable
477
- // from the browser: the wrapper in-process dispatches those routes to
478
- // `src/webui/server.js`'s exported handlers, and proxies everything else
479
- // (including /login, /api/auth/*, WebSocket /ws) to AionUi.
480
- //
481
- // Before 2.1.14 hs web spawned AionUi directly on :9876 and the wrapper
482
- // was dead code — so the Dashboard couldn't fetch balance/tools at all.
483
- const { startWrapper } = require('../webui/aionui-wrapper')
484
-
485
- // Pick an internal port. We rely on the wrapper's own picker to avoid
486
- // duplicating logic, but we need to know the public port upfront for the
487
- // user-visible URL message.
488
- let wrapper
489
- let aionuiProc
490
- try {
491
- wrapper = await startWrapper({
492
- port, // user-visible port (default 9876)
493
- runtimeDir: runtime.dir,
494
- runtimeVersion: runtime.version,
495
- runtimeSource: runtime.source,
496
- bunPath,
497
- })
498
- aionuiProc = wrapper.aionui
499
- } catch (e) {
500
- const [firstLine, ...rest] = String(e.message).split(/\r?\n/)
501
- console.log(chalk.red(`✗ HolySheep runtime failed to start: ${firstLine}`))
502
- for (const line of rest) {
503
- if (line.trim()) console.log(chalk.gray(line))
504
- }
505
- console.log()
506
- console.log(chalk.gray(' Tip: run again with HS_WEB_DEBUG=1 to stream HolySheep runtime logs live.'))
507
- console.log(chalk.gray(' If this is a first launch on Windows, try once more — Defender'))
508
- console.log(chalk.gray(' will cache bun.exe + server.mjs after the first scan.'))
509
- if (opts.aionui) process.exit(1)
510
- console.log(chalk.yellow(' Falling back to legacy workspace.'))
511
- return startLegacyMode(opts)
512
- }
513
-
514
- const baseUrl = `http://127.0.0.1:${port}`
515
- const internalPort = wrapper.internalPort
516
-
517
- // 4. Auto-login via HolySheep API key if available — talks to the INTERNAL
518
- // AionUi port (bypasses the wrapper's rate limits / middleware), because
519
- // this is just a cookie-warmer for the browser launch URL.
520
- const apiKey = readHolySheepApiKey()
521
- let launchUrl = baseUrl
522
- if (apiKey) {
523
- try {
524
- const { cookieLine } = await loginWithApiKey(internalPort, apiKey)
525
- // We can't programmatically seed the browser with cookies easily. Best UX:
526
- // - AionUi /login already set the cookie in our HTTP client, not the browser
527
- // - We ask user to paste key once in UI, OR we display a one-shot magic link
528
- // For now, we open /login with the key prefilled via URL hash so the React
529
- // login page can auto-submit. Implemented via localStorage fallback below.
530
- if (cookieLine) {
531
- // The cookie was set on our programmatic client, not the browser. Since
532
- // we can't cross-write cookies, we just let AionUi's /login page auto-fill
533
- // the apiKey via URL hash. React page reads window.location.hash on mount.
534
- launchUrl = `${baseUrl}/login#apiKey=${encodeURIComponent(apiKey)}`
535
- console.log(chalk.green('✓ HolySheep API key detected — auto-filled on login page'))
536
- }
537
- } catch (e) {
538
- console.log(chalk.yellow(` (auto-login pre-check failed: ${e.message} — will open /login manually)`))
539
- launchUrl = `${baseUrl}/login`
540
- }
541
- } else {
542
- launchUrl = `${baseUrl}/login`
543
- console.log(chalk.yellow(' No HolySheep API key saved yet. Run `hs login` first to enable auto-fill.'))
544
- }
545
-
546
- console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(launchUrl)}`))
547
- console.log(chalk.gray(' Mode: HolySheep WebUI (HolySheep 登录)'))
548
- console.log(chalk.gray(' Press Ctrl+C to stop'))
549
- console.log()
550
-
551
- if (opts.open !== false) {
552
- try {
553
- const platform = process.platform
554
- if (platform === 'darwin') execSync(`open "${launchUrl}"`)
555
- else if (platform === 'win32') execSync(`start "" "${launchUrl}"`)
556
- else execSync(`xdg-open "${launchUrl}"`)
557
- } catch {}
558
- }
559
-
560
- const stop = () => {
561
- try { aionuiProc.kill('SIGTERM') } catch {}
562
- setTimeout(() => {
563
- try { aionuiProc.kill('SIGKILL') } catch {}
564
- process.exit(0)
565
- }, 2000)
566
- }
567
- process.on('SIGINT', stop)
568
- process.on('SIGTERM', stop)
569
-
570
- await new Promise(() => {})
571
- }
572
-
573
- // ── Legacy fallback (old HolySheep Workspace page) ───────────────────────────
574
- async function startLegacyMode(opts) {
575
- const port = Number(opts.port) || 9876
576
- const { startServer } = require('../webui/server')
577
- await startServer(port)
578
- const url = `http://127.0.0.1:${port}`
579
- console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(url)}`))
580
- console.log(chalk.gray(' (Legacy HolySheep Workspace — 原生 node 轻量模式)'))
581
- console.log(chalk.gray(' 按 Ctrl+C 停止'))
582
- console.log()
583
- if (opts.open !== false) {
584
- try {
585
- const platform = process.platform
586
- if (platform === 'darwin') execSync(`open "${url}"`)
587
- else if (platform === 'win32') execSync(`start "" "${url}"`)
588
- else execSync(`xdg-open "${url}"`)
589
- } catch {}
590
- }
591
- await new Promise(() => {})
592
- }
593
-
594
- async function webui(opts) {
595
- console.log()
596
- console.log(chalk.bold('🌐 HolySheep WebUI'))
597
- console.log(chalk.gray('━'.repeat(50)))
598
- console.log()
599
-
600
- try {
601
- // 默认走 AionUi;HOLYSHEEP_WEBUI_LEGACY=1 才退回 legacy
602
- if (isLegacy(opts) && !opts.aionui) {
603
- console.log(chalk.gray(`[mode=legacy platform=${process.platform}]`))
604
- console.log()
605
- return await startLegacyMode(opts)
606
- }
607
- // Tri-state mode line: helps user-reports when things go sideways.
608
- // Runtime label now distinguishes `installed` (version matches baked-in
609
- // DEFAULT_RUNTIME_VERSION) from `installed-stale` (will auto-upgrade on
610
- // next call to resolveAionUiRuntime). `none` means first-ever launch.
611
- const bunFound = resolveBunPath() ? 'found' : 'missing'
612
- const rtLabel = probeAionUiRuntimeLabel()
613
- console.log(chalk.gray(`[mode=holysheep platform=${process.platform} bun=${bunFound} runtime=${rtLabel}]`))
614
- console.log()
615
- return await startAionUiMode(opts)
616
- } catch (err) {
617
- console.log(chalk.red(`✗ 启动失败: ${err.message}`))
618
- process.exit(1)
619
- }
620
- }
621
-
622
- module.exports = webui