@simonyea/holysheep-cli 2.1.2 → 2.1.4
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 +107 -22
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.4",
|
|
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
|
@@ -210,6 +210,12 @@ async function loginWithApiKey(port, apiKey) {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
// ── Start patched AionUi server ──────────────────────────────────────────────
|
|
213
|
+
// Timeout is generous for Windows first launch: Defender scans 41MB server.mjs,
|
|
214
|
+
// bun JIT-compiles, AionUi initializes sqlite under ~/.aionui-home, and npm
|
|
215
|
+
// may still pull the renderer. 60s is the realistic upper bound on a cold box.
|
|
216
|
+
const AIONUI_STARTUP_TIMEOUT_MS = Number(process.env.HS_WEB_STARTUP_TIMEOUT_MS) || 60_000
|
|
217
|
+
const AIONUI_LOG_TAIL_BYTES = 4096
|
|
218
|
+
|
|
213
219
|
function spawnAionUiServer({ bunPath, runtimeDir, port }) {
|
|
214
220
|
return new Promise((resolve, reject) => {
|
|
215
221
|
const entry = path.join(runtimeDir, 'dist-server', 'server.mjs')
|
|
@@ -231,21 +237,74 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
|
|
|
231
237
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
232
238
|
})
|
|
233
239
|
|
|
240
|
+
// ── Log capture ────────────────────────────────────────────────────────
|
|
241
|
+
// Always subscribe to stdout+stderr. In debug mode we ALSO print live; in
|
|
242
|
+
// normal mode we keep the last ~4KB in a ring buffer so that if startup
|
|
243
|
+
// fails (timeout OR exit-before-ready) we can surface the real error to
|
|
244
|
+
// the user instead of the previous silent "failed to start within 20s".
|
|
245
|
+
const debug = process.env.HS_WEB_DEBUG === '1'
|
|
246
|
+
let logTail = ''
|
|
247
|
+
const appendLog = (stream, chunk) => {
|
|
248
|
+
const s = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
|
|
249
|
+
logTail += s
|
|
250
|
+
if (logTail.length > AIONUI_LOG_TAIL_BYTES) {
|
|
251
|
+
logTail = logTail.slice(logTail.length - AIONUI_LOG_TAIL_BYTES)
|
|
252
|
+
}
|
|
253
|
+
if (debug) {
|
|
254
|
+
const target = stream === 'err' ? process.stderr : process.stdout
|
|
255
|
+
target.write(chalk.gray(`[aionui] ${s}`))
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
child.stdout.on('data', (d) => appendLog('out', d))
|
|
259
|
+
child.stderr.on('data', (d) => appendLog('err', d))
|
|
260
|
+
|
|
261
|
+
// ── Progress + timeout ─────────────────────────────────────────────────
|
|
234
262
|
let settled = false
|
|
263
|
+
const startedAt = Date.now()
|
|
235
264
|
const onReady = () => {
|
|
236
265
|
if (!settled) {
|
|
237
266
|
settled = true
|
|
267
|
+
clearInterval(progressTick)
|
|
238
268
|
resolve(child)
|
|
239
269
|
}
|
|
240
270
|
}
|
|
241
|
-
const
|
|
242
|
-
if (
|
|
243
|
-
|
|
244
|
-
|
|
271
|
+
const fail = (reason) => {
|
|
272
|
+
if (settled) return
|
|
273
|
+
settled = true
|
|
274
|
+
clearInterval(progressTick)
|
|
275
|
+
const tail = logTail.trim()
|
|
276
|
+
let msg = reason
|
|
277
|
+
if (tail) {
|
|
278
|
+
msg += `\n\n --- last AionUi output (stderr+stdout tail) ---\n${tail
|
|
279
|
+
.split(/\r?\n/)
|
|
280
|
+
.map((line) => ` ${line}`)
|
|
281
|
+
.join('\n')}\n ------------------------------------------------`
|
|
282
|
+
} else {
|
|
283
|
+
msg += '\n (no output captured from AionUi — check ' +
|
|
284
|
+
(process.platform === 'win32' ? 'Windows Defender / antivirus quarantining bun.exe' : 'bun or runtime corruption') +
|
|
285
|
+
')'
|
|
245
286
|
}
|
|
246
|
-
|
|
247
|
-
|
|
287
|
+
reject(new Error(msg))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Progress log every 10s, so user knows we're not hung. On Windows first
|
|
291
|
+
// launch (Defender scanning bun + bun JIT + sqlite init), 30-50s is normal.
|
|
292
|
+
const progressTick = setInterval(() => {
|
|
293
|
+
if (settled) return
|
|
294
|
+
const waited = Math.round((Date.now() - startedAt) / 1000)
|
|
295
|
+
const hint = process.platform === 'win32'
|
|
296
|
+
? ' (Windows first-launch can take up to 60s while Defender scans bun.exe + server.mjs)'
|
|
297
|
+
: ''
|
|
298
|
+
console.log(chalk.gray(` still starting AionUi — waited ${waited}s…${hint}`))
|
|
299
|
+
}, 10_000)
|
|
300
|
+
|
|
301
|
+
const timer = setTimeout(() => {
|
|
302
|
+
fail(`AionUi server failed to start within ${Math.round(AIONUI_STARTUP_TIMEOUT_MS / 1000)}s`)
|
|
303
|
+
}, AIONUI_STARTUP_TIMEOUT_MS)
|
|
304
|
+
|
|
305
|
+
// ── Readiness poll ─────────────────────────────────────────────────────
|
|
248
306
|
const pollReady = () => {
|
|
307
|
+
if (settled) return
|
|
249
308
|
const req = http.request(
|
|
250
309
|
{ host: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 500 },
|
|
251
310
|
(res) => {
|
|
@@ -267,17 +326,17 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
|
|
|
267
326
|
// Start polling after a short grace period so bun has time to boot.
|
|
268
327
|
setTimeout(pollReady, 600)
|
|
269
328
|
|
|
270
|
-
//
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
329
|
+
// ── Early exit ─────────────────────────────────────────────────────────
|
|
330
|
+
child.on('exit', (code, signal) => {
|
|
331
|
+
const reason = signal
|
|
332
|
+
? `AionUi server exited with signal ${signal} before becoming ready`
|
|
333
|
+
: `AionUi server exited with code ${code} before becoming ready`
|
|
334
|
+
clearTimeout(timer)
|
|
335
|
+
fail(reason)
|
|
336
|
+
})
|
|
337
|
+
child.on('error', (err) => {
|
|
338
|
+
clearTimeout(timer)
|
|
339
|
+
fail(`AionUi server spawn error: ${err.message || err}`)
|
|
281
340
|
})
|
|
282
341
|
})
|
|
283
342
|
}
|
|
@@ -324,13 +383,28 @@ async function startAionUiMode(opts) {
|
|
|
324
383
|
return startLegacyMode(opts)
|
|
325
384
|
}
|
|
326
385
|
|
|
327
|
-
// 2. Resolve bun — and auto-install if missing (opt-out via env)
|
|
386
|
+
// 2. Resolve bun — and auto-install if missing (opt-out via env, TTY-gated)
|
|
328
387
|
let bunPath = resolveBunPath()
|
|
329
388
|
const autoBunDisabled = process.env.HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN === '1'
|
|
330
389
|
if (!bunPath && !autoBunDisabled) {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
390
|
+
// TTY guard: auto-install pipes a remote script into a shell. In CI,
|
|
391
|
+
// Docker builds, or other non-interactive environments the user can't
|
|
392
|
+
// cancel and didn't knowingly opt in. Skip auto-install there and fall
|
|
393
|
+
// through to the existing manual-install guidance + legacy fallback.
|
|
394
|
+
if (!process.stdout.isTTY) {
|
|
395
|
+
console.log(chalk.yellow(' bun missing and not running in a TTY — skipping auto-install (safety).'))
|
|
396
|
+
console.log(chalk.gray(' In CI / Docker / non-interactive shells, install bun explicitly first:'))
|
|
397
|
+
console.log(chalk.cyan(` ${describeBunInstall()}`))
|
|
398
|
+
console.log(chalk.gray(' Or opt out of this safety with HOLYSHEEP_WEBUI_FORCE_AUTOFETCH_BUN=1.'))
|
|
399
|
+
if (process.env.HOLYSHEEP_WEBUI_FORCE_AUTOFETCH_BUN === '1') {
|
|
400
|
+
console.log(chalk.cyan('▶ HOLYSHEEP_WEBUI_FORCE_AUTOFETCH_BUN=1 set — attempting auto-install anyway'))
|
|
401
|
+
bunPath = autoInstallBun((m) => console.log(chalk.gray(` ${m}`)))
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
console.log(chalk.cyan('▶ bun runtime not installed — installing automatically (one-time)'))
|
|
405
|
+
console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH_BUN=1; takes ~30s; source: bun.sh official installer)'))
|
|
406
|
+
bunPath = autoInstallBun((m) => console.log(chalk.gray(` ${m}`)))
|
|
407
|
+
}
|
|
334
408
|
}
|
|
335
409
|
if (!bunPath) {
|
|
336
410
|
console.log(chalk.red('✗ bun is required to run the AionUi server'))
|
|
@@ -357,7 +431,18 @@ async function startAionUiMode(opts) {
|
|
|
357
431
|
try {
|
|
358
432
|
aionuiProc = await spawnAionUiServer({ bunPath, runtimeDir: runtime.dir, port })
|
|
359
433
|
} catch (e) {
|
|
360
|
-
|
|
434
|
+
// `e.message` may now be multi-line (includes bun/AionUi stderr tail).
|
|
435
|
+
// Print the first line in red (the reason), then any extra lines verbatim
|
|
436
|
+
// so the user can see the real bun/AionUi error and paste it back to us.
|
|
437
|
+
const [firstLine, ...rest] = String(e.message).split(/\r?\n/)
|
|
438
|
+
console.log(chalk.red(`✗ AionUi server failed to start: ${firstLine}`))
|
|
439
|
+
for (const line of rest) {
|
|
440
|
+
if (line.trim()) console.log(chalk.gray(line))
|
|
441
|
+
}
|
|
442
|
+
console.log()
|
|
443
|
+
console.log(chalk.gray(' Tip: run again with HS_WEB_DEBUG=1 to stream AionUi logs live.'))
|
|
444
|
+
console.log(chalk.gray(' If this is a first launch on Windows, try once more — Defender'))
|
|
445
|
+
console.log(chalk.gray(' will cache bun.exe + server.mjs after the first scan.'))
|
|
361
446
|
if (opts.aionui) process.exit(1)
|
|
362
447
|
console.log(chalk.yellow(' Falling back to legacy workspace.'))
|
|
363
448
|
return startLegacyMode(opts)
|