@simonyea/holysheep-cli 2.1.30 → 2.1.32

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.30",
3
+ "version": "2.1.32",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China \u2014 \u00a51=$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 && node tests/runtime-stale-upgrade.test.js && node tests/hermes.test.js && node tests/preflight.test.js",
@@ -144,7 +144,7 @@ function probeAionUiRuntimeLabel() {
144
144
  // is about to return user-cache).
145
145
  if (process.env.HOLYSHEEP_DEV === '1') {
146
146
  const forkDir = path.resolve(__dirname, '..', '..', 'aionui-fork')
147
- if (isValidRuntimeDir(forkDir)) return 'aionui-fork'
147
+ if (isValidRuntimeDir(forkDir)) return 'dev-checkout'
148
148
  }
149
149
  if (isValidRuntimeDir(VENDOR_DIR)) return 'vendor'
150
150
  if (isValidRuntimeDir(USER_CACHE_DIR)) {
@@ -200,7 +200,7 @@ async function fetchCsrfToken(port) {
200
200
 
201
201
  async function loginWithApiKey(port, apiKey) {
202
202
  const csrf = await fetchCsrfToken(port)
203
- if (!csrf) throw new Error('Failed to acquire CSRF token from AionUi server')
203
+ if (!csrf) throw new Error('Failed to acquire CSRF token from HolySheep runtime')
204
204
  const body = JSON.stringify({ apiKey, csrfToken: decodeURIComponent(csrf.csrfToken) })
205
205
  const r = await httpRequest(
206
206
  {
@@ -237,7 +237,7 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
237
237
  return new Promise((resolve, reject) => {
238
238
  const entry = path.join(runtimeDir, 'dist-server', 'server.mjs')
239
239
  if (!fs.existsSync(entry)) {
240
- return reject(new Error(`AionUi entry not found: ${entry}`))
240
+ return reject(new Error(`HolySheep runtime entry not found: ${entry}`))
241
241
  }
242
242
 
243
243
  // AionUi reads PORT / HOST / JWT_SECRET from env. We set a stable JWT secret
@@ -269,7 +269,7 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
269
269
  }
270
270
  if (debug) {
271
271
  const target = stream === 'err' ? process.stderr : process.stdout
272
- target.write(chalk.gray(`[aionui] ${s}`))
272
+ target.write(chalk.gray(`[holysheep] ${s}`))
273
273
  }
274
274
  }
275
275
  child.stdout.on('data', (d) => appendLog('out', d))
@@ -292,12 +292,12 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
292
292
  const tail = logTail.trim()
293
293
  let msg = reason
294
294
  if (tail) {
295
- msg += `\n\n --- last AionUi output (stderr+stdout tail) ---\n${tail
295
+ msg += `\n\n --- last HolySheep runtime output (stderr+stdout tail) ---\n${tail
296
296
  .split(/\r?\n/)
297
297
  .map((line) => ` ${line}`)
298
298
  .join('\n')}\n ------------------------------------------------`
299
299
  } else {
300
- msg += '\n (no output captured from AionUi — check ' +
300
+ msg += '\n (no output captured from HolySheep runtime — check ' +
301
301
  (process.platform === 'win32' ? 'Windows Defender / antivirus quarantining bun.exe' : 'bun or runtime corruption') +
302
302
  ')'
303
303
  }
@@ -312,11 +312,11 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
312
312
  const hint = process.platform === 'win32'
313
313
  ? ' (Windows first-launch can take up to 60s while Defender scans bun.exe + server.mjs)'
314
314
  : ''
315
- console.log(chalk.gray(` still starting AionUi — waited ${waited}s…${hint}`))
315
+ console.log(chalk.gray(` still starting HolySheep runtime — waited ${waited}s…${hint}`))
316
316
  }, 10_000)
317
317
 
318
318
  const timer = setTimeout(() => {
319
- fail(`AionUi server failed to start within ${Math.round(AIONUI_STARTUP_TIMEOUT_MS / 1000)}s`)
319
+ fail(`HolySheep runtime failed to start within ${Math.round(AIONUI_STARTUP_TIMEOUT_MS / 1000)}s`)
320
320
  }, AIONUI_STARTUP_TIMEOUT_MS)
321
321
 
322
322
  // ── Readiness poll ─────────────────────────────────────────────────────
@@ -346,14 +346,14 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
346
346
  // ── Early exit ─────────────────────────────────────────────────────────
347
347
  child.on('exit', (code, signal) => {
348
348
  const reason = signal
349
- ? `AionUi server exited with signal ${signal} before becoming ready`
350
- : `AionUi server exited with code ${code} before becoming ready`
349
+ ? `HolySheep runtime exited with signal ${signal} before becoming ready`
350
+ : `HolySheep runtime exited with code ${code} before becoming ready`
351
351
  clearTimeout(timer)
352
352
  fail(reason)
353
353
  })
354
354
  child.on('error', (err) => {
355
355
  clearTimeout(timer)
356
- fail(`AionUi server spawn error: ${err.message || err}`)
356
+ fail(`HolySheep runtime spawn error: ${err.message || err}`)
357
357
  })
358
358
  })
359
359
  }
@@ -382,10 +382,10 @@ async function startAionUiMode(opts) {
382
382
  // on HOLYSHEEP_AIONUI_SKIP_UPGRADE.
383
383
  const probe = probeAionUiRuntimeLabel()
384
384
  if (probe === 'none') {
385
- console.log(chalk.cyan('▶ AionUi runtime not installed — downloading automatically (~21 MB, one-time)'))
385
+ console.log(chalk.cyan('▶ HolySheep runtime not installed — downloading automatically (~21 MB, one-time)'))
386
386
  console.log(chalk.gray(' (disable with HOLYSHEEP_WEBUI_NO_AUTOFETCH=1; override URL with HOLYSHEEP_AIONUI_RUNTIME_URL)'))
387
387
  } else if (probe === 'installed-stale' && process.env.HOLYSHEEP_AIONUI_SKIP_UPGRADE !== '1') {
388
- console.log(chalk.cyan('▶ AionUi runtime is out of date — upgrading in place (atomic)'))
388
+ console.log(chalk.cyan('▶ HolySheep runtime is out of date — upgrading in place (atomic)'))
389
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
390
  }
391
391
  }
@@ -394,18 +394,18 @@ async function startAionUiMode(opts) {
394
394
  logger: (m) => console.log(chalk.gray(` ${m}`)),
395
395
  })
396
396
  if (runtime && runtime.source === 'download-upgrade') {
397
- console.log(chalk.green(` ✓ AionUi runtime upgraded to ${runtime.version}`))
397
+ console.log(chalk.green(` ✓ HolySheep runtime upgraded to ${runtime.version}`))
398
398
  } else if (runtime && runtime.source === 'user-cache-stale') {
399
399
  console.log(chalk.yellow(
400
- ` ⚠ Running stale AionUi runtime (${runtime.version}, expected newer). ` +
400
+ ` ⚠ Running stale HolySheep runtime (${runtime.version}, expected newer). ` +
401
401
  `Run: rm -rf ~/.holysheep/aionui-runtime && hs web to force rebuild.`
402
402
  ))
403
403
  } else if (runtime && runtime.source === 'download') {
404
- console.log(chalk.gray(' AionUi runtime installed. Next: ensure bun is available to launch it.'))
404
+ console.log(chalk.gray(' HolySheep runtime installed. Next: ensure bun is available to launch it.'))
405
405
  }
406
406
  if (!runtime) {
407
407
  const home = process.env.HOME || process.env.USERPROFILE || '~'
408
- console.log(chalk.red('✗ AionUi runtime not found and auto-download did not succeed'))
408
+ console.log(chalk.red('✗ HolySheep runtime not found and auto-download did not succeed'))
409
409
  console.log()
410
410
  console.log(chalk.gray(' Expected at one of:'))
411
411
  console.log(chalk.gray(` • ${path.resolve(__dirname, '..', '..', 'aionui-fork', 'dist-server')} (dev checkout)`))
@@ -451,9 +451,9 @@ async function startAionUiMode(opts) {
451
451
  }
452
452
  }
453
453
  if (!bunPath) {
454
- console.log(chalk.red('✗ bun is required to run the AionUi server'))
455
- console.log(chalk.gray(' (AionUi uses bun: URL-scheme imports that Node cannot load directly)'))
456
- console.log(chalk.gray(' AionUi runtime itself is ready — bun is the last missing piece.'))
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
457
  console.log()
458
458
  console.log(chalk.yellow(' Install bun manually:'))
459
459
  console.log(chalk.cyan(` ${describeBunInstall()}`))
@@ -463,12 +463,12 @@ async function startAionUiMode(opts) {
463
463
  console.log(chalk.red(' --aionui flag requires bun. Aborting.'))
464
464
  process.exit(1)
465
465
  }
466
- console.log(chalk.yellow(' AionUi runtime ready but bun auto-install failed — falling back to legacy HolySheep workspace.'))
466
+ console.log(chalk.yellow(' HolySheep runtime ready but bun auto-install failed — falling back to legacy HolySheep workspace.'))
467
467
  console.log()
468
468
  return startLegacyMode(opts)
469
469
  }
470
470
 
471
- console.log(chalk.cyan(`▶ Starting AionUi v1.9.18 (HolySheep fork, source: ${runtime.source})`))
471
+ console.log(chalk.cyan(`▶ Starting HolySheep runtime (source: ${runtime.source})`))
472
472
 
473
473
  // 3. Spawn AionUi on an internal loopback port and wrap it with
474
474
  // `aionui-wrapper.js` which listens on the user-visible port. This is the
@@ -498,12 +498,12 @@ async function startAionUiMode(opts) {
498
498
  aionuiProc = wrapper.aionui
499
499
  } catch (e) {
500
500
  const [firstLine, ...rest] = String(e.message).split(/\r?\n/)
501
- console.log(chalk.red(`✗ AionUi server failed to start: ${firstLine}`))
501
+ console.log(chalk.red(`✗ HolySheep runtime failed to start: ${firstLine}`))
502
502
  for (const line of rest) {
503
503
  if (line.trim()) console.log(chalk.gray(line))
504
504
  }
505
505
  console.log()
506
- console.log(chalk.gray(' Tip: run again with HS_WEB_DEBUG=1 to stream AionUi logs live.'))
506
+ console.log(chalk.gray(' Tip: run again with HS_WEB_DEBUG=1 to stream HolySheep runtime logs live.'))
507
507
  console.log(chalk.gray(' If this is a first launch on Windows, try once more — Defender'))
508
508
  console.log(chalk.gray(' will cache bun.exe + server.mjs after the first scan.'))
509
509
  if (opts.aionui) process.exit(1)
@@ -544,7 +544,7 @@ async function startAionUiMode(opts) {
544
544
  }
545
545
 
546
546
  console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(launchUrl)}`))
547
- console.log(chalk.gray(' Mode: AionUi v1.9.18 (源码 fork, HolySheep 登录)'))
547
+ console.log(chalk.gray(' Mode: HolySheep WebUI (HolySheep 登录)'))
548
548
  console.log(chalk.gray(' Press Ctrl+C to stop'))
549
549
  console.log()
550
550
 
@@ -610,7 +610,7 @@ async function webui(opts) {
610
610
  // next call to resolveAionUiRuntime). `none` means first-ever launch.
611
611
  const bunFound = resolveBunPath() ? 'found' : 'missing'
612
612
  const rtLabel = probeAionUiRuntimeLabel()
613
- console.log(chalk.gray(`[mode=aionui platform=${process.platform} bun=${bunFound} runtime=${rtLabel}]`))
613
+ console.log(chalk.gray(`[mode=holysheep platform=${process.platform} bun=${bunFound} runtime=${rtLabel}]`))
614
614
  console.log()
615
615
  return await startAionUiMode(opts)
616
616
  } catch (err) {
package/src/index.js CHANGED
@@ -174,11 +174,11 @@ program
174
174
  program
175
175
  .command('web')
176
176
  .alias('webui')
177
- .description('启动 WebUI 本地管理面板 (--aionui 启用 AionUi 界面 + HolySheep API Key 登录)')
177
+ .description('启动 HolySheep WebUI 本地管理面板 (默认 HolySheep API Key 登录)')
178
178
  .option('-p, --port <port>', '指定端口', '9876')
179
179
  .option('--no-open', '不自动打开浏览器')
180
- .option('--aionui', '使用 AionUi 运行时 (需本地已安装 runtime;npm 包不打包,保持轻量)')
181
- .option('--setup-runtime', '允许首次运行时从 HOLYSHEEP_AIONUI_RUNTIME_URL 下载 AionUi runtime')
180
+ .option('--aionui', '强制使用 HolySheep runtime (兼容 flag 名保留;需本地已安装 runtime)')
181
+ .option('--setup-runtime', '允许首次运行时自动下载 HolySheep runtime')
182
182
  .action(async (opts) => {
183
183
  printBanner()
184
184
  await require('./commands/webui')(opts)
@@ -616,6 +616,64 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAn
616
616
  )
617
617
  }
618
618
 
619
+ // [HolySheep fork v2.1.30 / hs22b] Buffer the incoming request body ONCE
620
+ // so retries + direct-https fallback can re-send it. Before this, each
621
+ // retry called clientReq.pipe(forwardReq) on a stream that had already
622
+ // been consumed by the previous attempt, making the second/third/...
623
+ // retry (and the fallback) hit CRS with an EMPTY body — upstream
624
+ // responded 400 "model is required" and the user saw no reply. Only
625
+ // the very first (often doomed-to-ECONNREFUSED) attempt had the real
626
+ // body; every retry+fallback arrived empty.
627
+ //
628
+ // Buffer into memory before any forward. Typical claude-agent-acp
629
+ // POST bodies are 5-40 KB so this is fine. Override from wire type
630
+ // to Buffer so downstream functions can pipe(from body stream)
631
+ // multiple times by wrapping a fresh Readable each time.
632
+ let bufferedBody = null
633
+ if (clientReq.method && !['GET', 'HEAD', 'OPTIONS'].includes(clientReq.method.toUpperCase())) {
634
+ try {
635
+ const chunks = []
636
+ for await (const chunk of clientReq) chunks.push(chunk)
637
+ bufferedBody = Buffer.concat(chunks)
638
+ } catch (e) {
639
+ // client disconnected mid-read; bail out early.
640
+ if (!clientRes.headersSent) {
641
+ clientRes.writeHead(400, { 'content-type': 'text/plain' })
642
+ clientRes.end('client disconnected while uploading body: ' + (e && e.message))
643
+ }
644
+ return
645
+ }
646
+ }
647
+ // Shadow `clientReq` with a wrapper that yields a fresh Readable body
648
+ // stream each time `.pipe()` is called. This keeps the rest of the
649
+ // handler unchanged (node-proxy + direct-fallback both still use
650
+ // `clientReq.pipe(upstream)`).
651
+ const { Readable } = require('stream')
652
+ const makeBodyStream = () => {
653
+ if (bufferedBody === null) return null
654
+ const r = new Readable({ read() {} })
655
+ r.push(bufferedBody)
656
+ r.push(null)
657
+ return r
658
+ }
659
+ const originalClientReq = clientReq
660
+ clientReq = new Proxy(originalClientReq, {
661
+ get(target, prop, receiver) {
662
+ if (prop === 'pipe') {
663
+ return (dest, opts) => {
664
+ const s = makeBodyStream()
665
+ if (!s) {
666
+ // GET/HEAD — just end the dest
667
+ if (dest && typeof dest.end === 'function') dest.end()
668
+ return dest
669
+ }
670
+ return s.pipe(dest, opts)
671
+ }
672
+ }
673
+ return Reflect.get(target, prop, receiver)
674
+ },
675
+ })
676
+
619
677
  const doForward = async (lease, attempt) => {
620
678
  const config = readConfig(configPath)
621
679
  const nodeProxyUrl = deriveNodeProxyUrl(lease)
@@ -60,10 +60,10 @@ const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
60
60
  // new CLI release, the next `hs web` invocation on user machines will detect
61
61
  // the version drift and upgrade the cache in place.
62
62
  const DEFAULT_RUNTIME_URL =
63
- 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs22.tar.gz'
63
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs23.tar.gz'
64
64
  const DEFAULT_RUNTIME_SHA256 =
65
- '03e386ea83660b4074e29026dfc53be6ed01fcd51fa35a9379ed64a396e99f33'
66
- const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs22'
65
+ '89901b6d60b412d1f69b48bf700965c070871f54bf6ee28323244db82c35b3f0'
66
+ const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs23'
67
67
 
68
68
  function isValidRuntimeDir(dir) {
69
69
  if (!dir) return false
@@ -206,7 +206,7 @@ async function resolveRuntime({ allowDownload = false, logger = () => {} } = {})
206
206
  if (process.env.HOLYSHEEP_DEV === '1') {
207
207
  const forkDir = path.resolve(__dirname, '..', '..', 'aionui-fork')
208
208
  if (isValidRuntimeDir(forkDir)) {
209
- return { dir: forkDir, version: readVersion(forkDir), source: 'aionui-fork' }
209
+ return { dir: forkDir, version: readVersion(forkDir), source: 'dev-checkout' }
210
210
  }
211
211
  }
212
212
 
@@ -227,9 +227,9 @@ async function resolveRuntime({ allowDownload = false, logger = () => {} } = {})
227
227
  source: 'download',
228
228
  }
229
229
  }
230
- logger('AionUi runtime downloaded but directory structure invalid')
230
+ logger('HolySheep runtime downloaded but directory structure invalid')
231
231
  } catch (e) {
232
- logger(`AionUi runtime download failed: ${e.message}`)
232
+ logger(`HolySheep runtime download failed: ${e.message}`)
233
233
  }
234
234
  }
235
235
 
@@ -319,7 +319,7 @@ function rmrf(p) {
319
319
  function downloadAndExtract(url, destDir, expectedSha, logger) {
320
320
  return new Promise((resolve, reject) => {
321
321
  const tmpFile = path.join(os.tmpdir(), `aionui-runtime-${Date.now()}.tar.gz`)
322
- logger(`Downloading AionUi runtime from ${url}`)
322
+ logger(`Downloading HolySheep runtime from ${url}`)
323
323
 
324
324
  const client = url.startsWith('https:') ? https : http
325
325
  const file = fs.createWriteStream(tmpFile)
@@ -394,7 +394,7 @@ function downloadAndExtract(url, destDir, expectedSha, logger) {
394
394
 
395
395
  function describeInstallGuidance() {
396
396
  return [
397
- 'AionUi runtime not installed. Fastest way to get started:',
397
+ 'HolySheep runtime not installed. Fastest way to get started:',
398
398
  '',
399
399
  ' One command (downloads the prebuilt 21 MB runtime):',
400
400
  ' hs web --setup-runtime',
@@ -85,7 +85,7 @@ function waitForReady(port, timeoutMs = 15000) {
85
85
 
86
86
  const retry = () => {
87
87
  if (Date.now() - startedAt >= timeoutMs) {
88
- reject(new Error('AionUi runtime did not become ready in time'))
88
+ reject(new Error('HolySheep runtime did not become ready in time'))
89
89
  return
90
90
  }
91
91
  setTimeout(tick, 500)
@@ -99,13 +99,13 @@ async function startAionUiRuntime(port) {
99
99
  const runtimeDir = resolveAionUiRuntimeDir()
100
100
  if (!runtimeDir) {
101
101
  throw new Error(
102
- 'AionUi runtime not bundled in this build — set HOLYSHEEP_WEBUI_AIONUI=1 with a local vendor/ directory to enable'
102
+ 'HolySheep runtime not bundled in this build — set HOLYSHEEP_WEBUI_AIONUI=1 with a local vendor/ directory to enable'
103
103
  )
104
104
  }
105
105
 
106
106
  const bunPath = resolveBunPath()
107
107
  if (!bunPath) {
108
- throw new Error('bun is required to start the AionUi runtime (install: https://bun.sh)')
108
+ throw new Error('bun is required to start the HolySheep runtime (install: https://bun.sh)')
109
109
  }
110
110
 
111
111
  const child = spawn(bunPath, ['dist-server/server.mjs'], {
@@ -73,7 +73,7 @@ const AIONUI_COOKIE_TTL_MS = 10 * 60 * 1000
73
73
 
74
74
  function log(msg) {
75
75
  // eslint-disable-next-line no-console
76
- console.log(`[aionui-wrapper] ${msg}`)
76
+ console.log(`[holysheep-web] ${msg}`)
77
77
  }
78
78
 
79
79
  function randomToken(bytes = 24) {
@@ -235,17 +235,17 @@ function aionuiInternalLoginWithApiKey({ internalPort, apiKey }) {
235
235
  res.on('data', (c) => { buf += c.toString() })
236
236
  res.on('end', () => {
237
237
  if (res.statusCode !== 200) {
238
- return reject(new Error(`AionUi /login returned ${res.statusCode}: ${buf.slice(0, 200)}`))
238
+ return reject(new Error(`HolySheep runtime /login returned ${res.statusCode}: ${buf.slice(0, 200)}`))
239
239
  }
240
240
  const setCookie = res.headers['set-cookie']
241
241
  if (!setCookie || setCookie.length === 0) {
242
- return reject(new Error('AionUi /login succeeded but no Set-Cookie header returned'))
242
+ return reject(new Error('HolySheep runtime /login succeeded but no Set-Cookie header returned'))
243
243
  }
244
244
  resolve(setCookie)
245
245
  })
246
246
  })
247
247
  req.on('error', reject)
248
- req.setTimeout(15_000, () => { req.destroy(new Error('AionUi /login timed out')) })
248
+ req.setTimeout(15_000, () => { req.destroy(new Error('HolySheep runtime /login timed out')) })
249
249
  req.write(body)
250
250
  req.end()
251
251
  })
@@ -308,7 +308,7 @@ async function handleBootstrap(req, res, ctx) {
308
308
  })
309
309
  res.end()
310
310
  } catch (e) {
311
- sendJson(res, 502, { success: false, message: `AionUi bridge login failed: ${e.message}` })
311
+ sendJson(res, 502, { success: false, message: `bridge login failed: ${e.message}` })
312
312
  }
313
313
  }
314
314
 
@@ -317,7 +317,7 @@ async function handleHolySheepStatus(req, res) {
317
317
  sendJson(res, 200, {
318
318
  loggedIn: !!apiKey,
319
319
  apiKeyMasked: apiKey ? `${apiKey.slice(0, 6)}...${apiKey.slice(-4)}` : null,
320
- mode: 'aionui-wrapper',
320
+ mode: 'holysheep-webui',
321
321
  version: require('../../package.json').version,
322
322
  })
323
323
  }
@@ -380,8 +380,29 @@ function proxyHttp(req, res, internalPort) {
380
380
  req.pipe(upstream)
381
381
  }
382
382
 
383
- // Client disconnect → kill upstream
384
- req.on('close', () => { if (!upstream.destroyed) upstream.destroy() })
383
+ // Client disconnect → kill upstream.
384
+ //
385
+ // [HolySheep fork v2.1.32 / hs23] Root-cause fix for "Network error" on
386
+ // /login: Node's IncomingMessage (`req`) emits 'close' as soon as its
387
+ // Readable side is fully consumed — NOT when the TCP socket disconnects.
388
+ // For POST requests, req.pipe(upstream) drains the body and the very next
389
+ // tick `req` fires 'close', which here destroyed the upstream *before*
390
+ // Express on 9877 had a chance to respond → ECONNRESET → wrapper 502.
391
+ //
392
+ // The correct signal for "client disconnected" is `res.on('close')` +
393
+ // `res.writableFinished === false`. That only fires if the downstream
394
+ // client aborts before we've finished writing the response.
395
+ res.on('close', () => {
396
+ if (res.writableFinished) return
397
+ if (!upstream.destroyed) upstream.destroy()
398
+ })
399
+ upstream.on('close', () => {
400
+ // If upstream closes before we finished writing to `res` (e.g. upstream
401
+ // crash), make sure we don't leave the client hanging.
402
+ if (!res.writableFinished && !res.headersSent) {
403
+ try { res.writeHead(502, { 'Content-Type': 'text/plain' }); res.end('upstream closed') } catch {}
404
+ }
405
+ })
385
406
  }
386
407
 
387
408
  // ── WebSocket proxy (upgrade event) ──────────────────────────────────────────
@@ -586,7 +607,7 @@ async function startWrapper({ port, runtimeDir, runtimeVersion, runtimeSource, b
586
607
  // Detect if the vendored AionUi build natively speaks HolySheep auth.
587
608
  // Vendored v1.9.17 does; upstream AionUi releases do not (use username/password).
588
609
  const hsNative = detectHolySheepAionUi(runtimeDir)
589
- log(`AionUi /login mode: ${hsNative ? 'holysheep-native (apiKey)' : 'legacy (username/password bridge)'}`)
610
+ log(`/login mode: ${hsNative ? 'holysheep-native (apiKey)' : 'legacy (username/password bridge)'}`)
590
611
 
591
612
  // If the build is legacy username/password, eager pre-flight the bridge cred
592
613
  // perms so a misconfigured file fails at boot rather than during a request.
@@ -595,7 +616,7 @@ async function startWrapper({ port, runtimeDir, runtimeVersion, runtimeSource, b
595
616
  }
596
617
 
597
618
  const internalPort = await pickInternalPort()
598
- log(`internal AionUi port: ${internalPort}`)
619
+ log(`internal runtime port: ${internalPort}`)
599
620
 
600
621
  // Spawn AionUi, bound 127.0.0.1 only (ALLOW_REMOTE never set).
601
622
  // stdio is piped (not inherited) so that:
@@ -655,7 +676,7 @@ async function startWrapper({ port, runtimeDir, runtimeVersion, runtimeSource, b
655
676
  aionui.stdout.on('data', (d) => appendLog('out', d))
656
677
  aionui.stderr.on('data', (d) => appendLog('err', d))
657
678
  aionui.on('exit', (code) => {
658
- log(`AionUi upstream exited (code=${code})`)
679
+ log(`runtime upstream exited (code=${code})`)
659
680
  process.exit(code || 1)
660
681
  })
661
682
 
@@ -671,7 +692,7 @@ async function startWrapper({ port, runtimeDir, runtimeVersion, runtimeSource, b
671
692
  : e.message
672
693
  throw new Error(msg)
673
694
  }
674
- log(`AionUi upstream ready (version=${runtimeVersion}, source=${runtimeSource})`)
695
+ log(`runtime ready (version=${runtimeVersion}, source=${runtimeSource})`)
675
696
 
676
697
  const ctx = { internalPort, runtimeDir, runtimeVersion, runtimeSource, bunPath }
677
698
  const server = http.createServer(buildRouter(ctx))