@simonyea/holysheep-cli 2.1.38 → 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 +19 -6
- 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 -63
- 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 -1566
- 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/commands/webui.js
DELETED
|
@@ -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
|