@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,429 +0,0 @@
1
- /**
2
- * AionUi runtime resolver
3
- *
4
- * Resolution order:
5
- * 1. ~/.holysheep/aionui-runtime/ (installed / cached) — ONLY if its
6
- * version string matches DEFAULT_RUNTIME_VERSION (or
7
- * HOLYSHEEP_AIONUI_PIN_VERSION). A mismatch is treated as *stale*: the
8
- * cache is atomically replaced with the current version on next launch.
9
- * 2. <cli>/src/webui/vendor/aionui/ (dev checkout — not shipped to npm,
10
- * no version gate)
11
- * 3. <cli>/../aionui-fork/ (ONLY when `HOLYSHEEP_DEV=1` is set — when you
12
- * are actively iterating on the fork and want `bun run build` to take
13
- * precedence over any cached runtime). OFF BY DEFAULT so that even
14
- * when the CLI is run from the source repo, it behaves exactly like a
15
- * user install (user-cache → download). This removed a massive
16
- * silent-drift risk: previously a dev commit that broke the fork would
17
- * silently ship to developers' `hs web` sessions but not to end users,
18
- * so bugs were invisible locally.
19
- * 4. DEFAULT_RUNTIME_URL + DEFAULT_RUNTIME_SHA256 (baked in; auto-download
20
- * when --setup-runtime is passed OR when cache is stale). Users can
21
- * override via env.
22
- * 5. null → caller must show a clear "runtime not installed" error
23
- *
24
- * ── Why stale detection exists ──────────────────────────────────────────────
25
- * Before 2.1.10 the resolver returned the cache as soon as it saw
26
- * `dist-server/server.mjs` on disk, regardless of which version it was. That
27
- * meant users who installed hs web back when hs0/hs1 was the baked-in default
28
- * kept running that exact build forever — every subsequent release (hs2
29
- * rebrand, hs3 link fixes, hs5 OpenClaw model selector + HolySheep CLI card)
30
- * silently did nothing on their machines, because their ~/.holysheep/…
31
- * directory already "looked valid." This file now compares the cached
32
- * package.json version against DEFAULT_RUNTIME_VERSION and upgrades in place
33
- * via an atomic rename flow (stage → backup old → swap → drop backup).
34
- *
35
- * ── Atomic upgrade safety ───────────────────────────────────────────────────
36
- * 1. Download tarball to `${USER_CACHE_DIR}.staging-${ts}` and verify SHA256.
37
- * 2. Rename existing `USER_CACHE_DIR` → `${USER_CACHE_DIR}.bak-${ts}` (fast).
38
- * 3. Rename staging dir → `USER_CACHE_DIR` (fast).
39
- * 4. rm -rf the backup.
40
- * Any failure before step 3 leaves the old cache untouched. A failure between
41
- * step 2 and 3 is auto-restored from the backup. The user never ends up with
42
- * no runtime at all.
43
- */
44
-
45
- 'use strict'
46
-
47
- const fs = require('fs')
48
- const path = require('path')
49
- const os = require('os')
50
- const crypto = require('crypto')
51
- const https = require('https')
52
- const http = require('http')
53
-
54
- const USER_CACHE_DIR = path.join(os.homedir(), '.holysheep', 'aionui-runtime')
55
- const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
56
-
57
- // Baked-in defaults — updated with every release that bumps the runtime bundle.
58
- // Override at runtime via HOLYSHEEP_AIONUI_RUNTIME_URL / HOLYSHEEP_AIONUI_RUNTIME_SHA256
59
- // / HOLYSHEEP_AIONUI_PIN_VERSION. When DEFAULT_RUNTIME_VERSION changes in a
60
- // new CLI release, the next `hs web` invocation on user machines will detect
61
- // the version drift and upgrade the cache in place.
62
- const DEFAULT_RUNTIME_URL =
63
- 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs27.tar.gz'
64
- const DEFAULT_RUNTIME_SHA256 =
65
- 'de9cb08d071d1254f8e6ad69f249d73d8b1b8fd5c4a07ca6d6009a902e0cccb1'
66
- const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs27'
67
-
68
- function isValidRuntimeDir(dir) {
69
- if (!dir) return false
70
- try {
71
- return (
72
- fs.existsSync(path.join(dir, 'dist-server', 'server.mjs')) &&
73
- fs.existsSync(path.join(dir, 'out', 'renderer', 'index.html'))
74
- )
75
- } catch {
76
- return false
77
- }
78
- }
79
-
80
- /**
81
- * Read the version of a runtime directory.
82
- *
83
- * Order:
84
- * 1. `.holysheep-runtime-version` (flat text file we stamp after a
85
- * successful download/upgrade — guaranteed to match what the fetcher
86
- * pulled, regardless of whether the tarball itself shipped a
87
- * package.json). This is the file future `isVersionCurrent` comparisons
88
- * rely on; without it, every subsequent `hs web` would mis-detect the
89
- * fresh cache as stale and re-download forever.
90
- * 2. `package.json` `version` field (legacy — the hs0/hs1 tarballs did
91
- * include one with `1.9.18-holysheep`). Kept for backwards compat.
92
- * 3. 'unknown' — treated as NOT current, so the fetcher will upgrade.
93
- */
94
- function readVersion(dir) {
95
- try {
96
- const stamp = path.join(dir, '.holysheep-runtime-version')
97
- if (fs.existsSync(stamp)) {
98
- // .trim() is REQUIRED. writeVersionStamp() appends '\n', and a human
99
- // doing manual recovery might have used `echo foo > file` (writes
100
- // 'foo\n') or `echo -n foo > file` (writes 'foo' but on bash `echo`
101
- // builtins that don't recognize -n you get '-n foo\n'). Trimming
102
- // whitespace makes all these shapes compare equal — without it,
103
- // isVersionCurrent() returned false on a freshly-stamped cache and
104
- // looped into auto-upgrade. DO NOT remove this .trim().
105
- const v = fs.readFileSync(stamp, 'utf8').trim()
106
- if (v) return v
107
- }
108
- } catch {}
109
- try {
110
- const pkgPath = path.join(dir, 'package.json')
111
- if (fs.existsSync(pkgPath)) {
112
- const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
113
- return pkg.version || 'unknown'
114
- }
115
- } catch {}
116
- return 'unknown'
117
- }
118
-
119
- /**
120
- * Stamp the runtime directory with the version we just installed so that
121
- * readVersion() returns the right answer on subsequent launches. Fails
122
- * silently — a missing stamp only means the _next_ launch will re-download,
123
- * which is annoying but not dangerous.
124
- */
125
- function writeVersionStamp(dir, version) {
126
- try {
127
- fs.writeFileSync(path.join(dir, '.holysheep-runtime-version'), version + '\n', 'utf8')
128
- } catch {}
129
- }
130
-
131
- /**
132
- * Strict string equality against the expected version.
133
- *
134
- * We intentionally do not parse semver ranges — runtime releases are tagged
135
- * explicitly (hs5, hs6, …) and every CLI release pins exactly one expected
136
- * string. This keeps the upgrade decision a boolean, not a version-math
137
- * guessing game.
138
- *
139
- * Priority:
140
- * 1. HOLYSHEEP_AIONUI_PIN_VERSION (user opted-in lock)
141
- * 2. DEFAULT_RUNTIME_VERSION (baked into this CLI release)
142
- */
143
- function expectedVersion() {
144
- return process.env.HOLYSHEEP_AIONUI_PIN_VERSION || DEFAULT_RUNTIME_VERSION
145
- }
146
-
147
- function isVersionCurrent(dir) {
148
- return readVersion(dir) === expectedVersion()
149
- }
150
-
151
- /**
152
- * Resolve an AionUi runtime directory.
153
- *
154
- * @param {object} opts
155
- * @param {boolean} opts.allowDownload - if true, will attempt to download
156
- * the runtime when no local cache exists, OR when the local cache is
157
- * stale (version mismatch). Stale-upgrade is suppressed when the env var
158
- * `HOLYSHEEP_AIONUI_SKIP_UPGRADE=1` is set, in which case the stale cache
159
- * is returned as-is with `source: 'user-cache-stale'`.
160
- * @param {(msg:string)=>void} opts.logger
161
- *
162
- * @returns {{ dir: string, version: string,
163
- * source: 'user-cache'|'user-cache-stale'|'vendor'|'aionui-fork'|'download'|'download-upgrade'
164
- * } | null}
165
- */
166
- async function resolveRuntime({ allowDownload = false, logger = () => {} } = {}) {
167
- // 1. User cache (installed runtime)
168
- if (isValidRuntimeDir(USER_CACHE_DIR)) {
169
- const ver = readVersion(USER_CACHE_DIR)
170
- if (isVersionCurrent(USER_CACHE_DIR)) {
171
- return { dir: USER_CACHE_DIR, version: ver, source: 'user-cache' }
172
- }
173
-
174
- // Stale. Decide whether to upgrade in place.
175
- const skipUpgrade = process.env.HOLYSHEEP_AIONUI_SKIP_UPGRADE === '1'
176
- if (allowDownload && !skipUpgrade) {
177
- logger(`runtime cache is out of date (found ${ver}, expected ${expectedVersion()}) — upgrading...`)
178
- try {
179
- await performAtomicUpgrade(USER_CACHE_DIR, logger)
180
- return {
181
- dir: USER_CACHE_DIR,
182
- version: readVersion(USER_CACHE_DIR) || expectedVersion(),
183
- source: 'download-upgrade',
184
- }
185
- } catch (e) {
186
- logger(`runtime upgrade failed: ${e.message}`)
187
- // Fall through: return stale cache as a degraded-but-usable state
188
- // so the user still gets _some_ WebUI rather than a hard error.
189
- return { dir: USER_CACHE_DIR, version: ver, source: 'user-cache-stale' }
190
- }
191
- }
192
- // No upgrade allowed/desired → return stale
193
- return { dir: USER_CACHE_DIR, version: ver, source: 'user-cache-stale' }
194
- }
195
-
196
- // 2. Dev vendor checkout (bundled for darwin-arm64 dev rigs — not in npm).
197
- // No version gate: a dev checkout is assumed current by construction.
198
- if (isValidRuntimeDir(VENDOR_DIR)) {
199
- return { dir: VENDOR_DIR, version: readVersion(VENDOR_DIR), source: 'vendor' }
200
- }
201
-
202
- // 3. Dev repo aionui-fork/ — GATED BEHIND HOLYSHEEP_DEV=1. See resolver
203
- // header docstring (order step 3) for rationale. Users (even devs running
204
- // from the repo checkout) go through user-cache → download, so the
205
- // production behavior is always what they see locally.
206
- if (process.env.HOLYSHEEP_DEV === '1') {
207
- const forkDir = path.resolve(__dirname, '..', '..', 'aionui-fork')
208
- if (isValidRuntimeDir(forkDir)) {
209
- return { dir: forkDir, version: readVersion(forkDir), source: 'dev-checkout' }
210
- }
211
- }
212
-
213
- // 4. Fresh install — download from env override OR baked-in default
214
- if (allowDownload) {
215
- const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL || DEFAULT_RUNTIME_URL
216
- const expectedSha = process.env.HOLYSHEEP_AIONUI_RUNTIME_SHA256 || DEFAULT_RUNTIME_SHA256
217
- try {
218
- fs.mkdirSync(USER_CACHE_DIR, { recursive: true })
219
- await downloadAndExtract(url, USER_CACHE_DIR, expectedSha, logger)
220
- if (isValidRuntimeDir(USER_CACHE_DIR)) {
221
- // Stamp the version so subsequent launches don't re-detect as stale
222
- // (hs5+ tarballs ship no root package.json).
223
- writeVersionStamp(USER_CACHE_DIR, expectedVersion())
224
- return {
225
- dir: USER_CACHE_DIR,
226
- version: readVersion(USER_CACHE_DIR) || DEFAULT_RUNTIME_VERSION,
227
- source: 'download',
228
- }
229
- }
230
- logger('HolySheep runtime downloaded but directory structure invalid')
231
- } catch (e) {
232
- logger(`HolySheep runtime download failed: ${e.message}`)
233
- }
234
- }
235
-
236
- return null
237
- }
238
-
239
- /**
240
- * Atomically replace the runtime at `targetDir` with a freshly downloaded
241
- * copy. Safe against mid-operation failure: the old cache is preserved
242
- * either in place (if download/verify fails) or as a `.bak-*` directory
243
- * (if we crash between backup and swap).
244
- *
245
- * Implementation:
246
- * a) Download + extract to `${targetDir}.staging-${ts}`, verify SHA256.
247
- * b) `mv targetDir → ${targetDir}.bak-${ts}` (atomic, same filesystem)
248
- * c) `mv stagingDir → targetDir` (atomic, same filesystem)
249
- * d) `rm -rf ${targetDir}.bak-${ts}` on success.
250
- * e) On any failure in (b)→(c), try to restore from the `.bak-*` path.
251
- */
252
- async function performAtomicUpgrade(targetDir, logger) {
253
- const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL || DEFAULT_RUNTIME_URL
254
- const expectedSha = process.env.HOLYSHEEP_AIONUI_RUNTIME_SHA256 || DEFAULT_RUNTIME_SHA256
255
- const ts = Date.now()
256
- const stagingDir = `${targetDir}.staging-${ts}`
257
- const backupDir = `${targetDir}.bak-${ts}`
258
-
259
- // (a) Download + extract + verify into an isolated staging dir.
260
- fs.mkdirSync(stagingDir, { recursive: true })
261
- try {
262
- await downloadAndExtract(url, stagingDir, expectedSha, logger)
263
- } catch (e) {
264
- try { rmrf(stagingDir) } catch {}
265
- throw e
266
- }
267
- if (!isValidRuntimeDir(stagingDir)) {
268
- try { rmrf(stagingDir) } catch {}
269
- throw new Error('staged runtime failed structure validation')
270
- }
271
-
272
- // (b) Back up the old cache, if any.
273
- let backedUp = false
274
- if (fs.existsSync(targetDir)) {
275
- try {
276
- fs.renameSync(targetDir, backupDir)
277
- backedUp = true
278
- } catch (e) {
279
- try { rmrf(stagingDir) } catch {}
280
- throw new Error(`failed to back up old runtime: ${e.message}`)
281
- }
282
- }
283
-
284
- // (c) Move staging into place.
285
- try {
286
- fs.renameSync(stagingDir, targetDir)
287
- } catch (e) {
288
- // Try to restore the backup so the user isn't left runtime-less.
289
- if (backedUp) {
290
- try { fs.renameSync(backupDir, targetDir) } catch {}
291
- }
292
- try { rmrf(stagingDir) } catch {}
293
- throw new Error(`failed to swap in new runtime: ${e.message}`)
294
- }
295
-
296
- // (d) Stamp the version. hs5+ tarballs do NOT include a root package.json,
297
- // so without this stamp readVersion() would fall back to 'unknown' and the
298
- // very next `hs web` would re-enter the stale-upgrade branch and download
299
- // again. See readVersion() docstring above.
300
- writeVersionStamp(targetDir, expectedVersion())
301
-
302
- // (e) Success — drop the backup.
303
- if (backedUp) {
304
- try { rmrf(backupDir) } catch (e) {
305
- logger(`warning: could not remove old runtime backup at ${backupDir}: ${e.message}`)
306
- }
307
- }
308
- }
309
-
310
- function rmrf(p) {
311
- // fs.rmSync landed in Node 14.14, which is well below our floor.
312
- fs.rmSync(p, { recursive: true, force: true })
313
- }
314
-
315
- /**
316
- * Download a tar.gz from url, verify optional SHA256, extract into destDir.
317
- * Uses only Node built-ins (https + tar via exec) — zero new deps.
318
- */
319
- function downloadAndExtract(url, destDir, expectedSha, logger) {
320
- return new Promise((resolve, reject) => {
321
- const tmpFile = path.join(os.tmpdir(), `aionui-runtime-${Date.now()}.tar.gz`)
322
- logger(`Downloading HolySheep runtime from ${url}`)
323
-
324
- const client = url.startsWith('https:') ? https : http
325
- const file = fs.createWriteStream(tmpFile)
326
- const hasher = crypto.createHash('sha256')
327
- let totalBytes = 0
328
-
329
- const req = client.get(url, (res) => {
330
- // Follow redirects (GitHub Releases → S3 CDN)
331
- if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
332
- file.close()
333
- try { fs.unlinkSync(tmpFile) } catch {}
334
- return resolve(downloadAndExtract(res.headers.location, destDir, expectedSha, logger))
335
- }
336
- if (res.statusCode !== 200) {
337
- file.close()
338
- try { fs.unlinkSync(tmpFile) } catch {}
339
- return reject(new Error(`HTTP ${res.statusCode} from ${url}`))
340
- }
341
- res.on('data', (chunk) => {
342
- hasher.update(chunk)
343
- totalBytes += chunk.length
344
- })
345
- res.pipe(file)
346
- file.on('finish', () => {
347
- file.close(() => {
348
- const actualSha = hasher.digest('hex')
349
- if (expectedSha && expectedSha.toLowerCase() !== actualSha.toLowerCase()) {
350
- try { fs.unlinkSync(tmpFile) } catch {}
351
- return reject(new Error(`SHA256 mismatch: expected ${expectedSha}, got ${actualSha}`))
352
- }
353
- logger(`Downloaded ${(totalBytes / 1024 / 1024).toFixed(1)} MB, sha256=${actualSha.slice(0, 12)}…`)
354
- try {
355
- fs.mkdirSync(destDir, { recursive: true })
356
- const { execSync } = require('child_process')
357
- execSync(`tar -xzf "${tmpFile}" -C "${destDir}" --strip-components=0`, { stdio: 'ignore' })
358
- try { fs.unlinkSync(tmpFile) } catch {}
359
- // Post-extract sanity: hs4 once shipped a tarball that only
360
- // contained dist-server/ — the WebUI launched but every
361
- // renderer-side feature silently did nothing. Fail loudly here so
362
- // the user gets a clear error instead of a half-broken UI.
363
- const serverMjs = path.join(destDir, 'dist-server', 'server.mjs')
364
- const rendererIndex = path.join(destDir, 'out', 'renderer', 'index.html')
365
- const missing = []
366
- if (!fs.existsSync(serverMjs)) missing.push('dist-server/server.mjs')
367
- if (!fs.existsSync(rendererIndex)) missing.push('out/renderer/index.html')
368
- if (missing.length) {
369
- return reject(new Error(
370
- `Runtime tarball is missing required files: ${missing.join(', ')}.\n` +
371
- `This runtime cannot launch the WebUI. Check HOLYSHEEP_AIONUI_RUNTIME_URL ` +
372
- `or report the broken URL (${url}).`
373
- ))
374
- }
375
- resolve()
376
- } catch (e) {
377
- try { fs.unlinkSync(tmpFile) } catch {}
378
- reject(new Error(`extract failed: ${e.message}`))
379
- }
380
- })
381
- })
382
- })
383
-
384
- req.on('error', (e) => {
385
- file.close()
386
- try { fs.unlinkSync(tmpFile) } catch {}
387
- reject(e)
388
- })
389
- req.setTimeout(120_000, () => {
390
- req.destroy(new Error('download timeout (120s)'))
391
- })
392
- })
393
- }
394
-
395
- function describeInstallGuidance() {
396
- return [
397
- 'HolySheep runtime not installed. Fastest way to get started:',
398
- '',
399
- ' One command (downloads the prebuilt 21 MB runtime):',
400
- ' hs web --setup-runtime',
401
- '',
402
- ' Manual install via env overrides:',
403
- ` export HOLYSHEEP_AIONUI_RUNTIME_URL=${DEFAULT_RUNTIME_URL}`,
404
- ` export HOLYSHEEP_AIONUI_RUNTIME_SHA256=${DEFAULT_RUNTIME_SHA256}`,
405
- ' hs web --setup-runtime',
406
- '',
407
- ' Force a fresh runtime download even if cache exists:',
408
- ` rm -rf ${USER_CACHE_DIR} && hs web`,
409
- '',
410
- ' Or fall back to the legacy HolySheep Workspace:',
411
- ' HOLYSHEEP_WEBUI_LEGACY=1 hs web',
412
- ].join('\n')
413
- }
414
-
415
- module.exports = {
416
- USER_CACHE_DIR,
417
- VENDOR_DIR,
418
- DEFAULT_RUNTIME_URL,
419
- DEFAULT_RUNTIME_SHA256,
420
- DEFAULT_RUNTIME_VERSION,
421
- expectedVersion,
422
- isVersionCurrent,
423
- isValidRuntimeDir,
424
- readVersion,
425
- writeVersionStamp,
426
- resolveRuntime,
427
- performAtomicUpgrade,
428
- describeInstallGuidance,
429
- }
@@ -1,139 +0,0 @@
1
- 'use strict'
2
-
3
- const fs = require('fs')
4
- const path = require('path')
5
- const os = require('os')
6
- const http = require('http')
7
- const { execSync, spawn } = require('child_process')
8
-
9
- function resolveBunPath() {
10
- const bundledBun = path.join(__dirname, 'vendor', 'bun-darwin-arm64')
11
- if (process.platform === 'darwin' && process.arch === 'arm64' && fs.existsSync(bundledBun)) {
12
- return bundledBun
13
- }
14
- if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
15
- const isWindows = process.platform === 'win32'
16
- try {
17
- const cmd = isWindows ? 'where.exe bun' : 'which bun'
18
- const raw = execSync(cmd, {
19
- stdio: ['ignore', 'pipe', 'ignore'],
20
- encoding: 'utf8',
21
- timeout: 2000,
22
- })
23
- const candidates = String(raw).split(/\r?\n/).map((s) => s.trim()).filter(Boolean)
24
- for (const p of candidates) {
25
- if (fs.existsSync(p)) return p
26
- }
27
- } catch {}
28
- // ~/.bun/bin fallback — canonical location of bun's official installer.
29
- // Catches "installed bun but didn't reopen terminal" without needing PATH refresh.
30
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir()
31
- if (home) {
32
- const name = isWindows ? 'bun.exe' : 'bun'
33
- const homeBun = path.join(home, '.bun', 'bin', name)
34
- if (fs.existsSync(homeBun)) return homeBun
35
- }
36
- return null
37
- }
38
-
39
- function getRuntimeCandidates() {
40
- const home = os.homedir()
41
- return [
42
- path.join(__dirname, 'vendor', 'aionui'),
43
- process.env.HOLYSHEEP_AIONUI_DIR,
44
- path.join(home, 'AionUi'),
45
- path.join(home, 'Projects', 'AionUi'),
46
- path.join(__dirname, '..', '..', 'vendor', 'aionui'),
47
- ].filter(Boolean)
48
- }
49
-
50
- function isValidRuntimeDir(dir) {
51
- if (!dir) return false
52
- return fs.existsSync(path.join(dir, 'dist-server', 'server.mjs')) &&
53
- fs.existsSync(path.join(dir, 'out', 'renderer', 'index.html'))
54
- }
55
-
56
- function resolveAionUiRuntimeDir() {
57
- return getRuntimeCandidates().find(isValidRuntimeDir) || null
58
- }
59
-
60
- function waitForReady(port, timeoutMs = 15000) {
61
- const startedAt = Date.now()
62
- return new Promise((resolve, reject) => {
63
- const tick = () => {
64
- const req = http.get({
65
- hostname: '127.0.0.1',
66
- port,
67
- path: '/',
68
- family: 4,
69
- timeout: 1500,
70
- }, (res) => {
71
- res.resume()
72
- if (res.statusCode && res.statusCode < 500) {
73
- resolve(true)
74
- return
75
- }
76
- retry()
77
- })
78
-
79
- req.on('timeout', () => {
80
- req.destroy()
81
- retry()
82
- })
83
- req.on('error', retry)
84
- }
85
-
86
- const retry = () => {
87
- if (Date.now() - startedAt >= timeoutMs) {
88
- reject(new Error('HolySheep runtime did not become ready in time'))
89
- return
90
- }
91
- setTimeout(tick, 500)
92
- }
93
-
94
- tick()
95
- })
96
- }
97
-
98
- async function startAionUiRuntime(port) {
99
- const runtimeDir = resolveAionUiRuntimeDir()
100
- if (!runtimeDir) {
101
- throw new Error(
102
- 'HolySheep runtime not bundled in this build — set HOLYSHEEP_WEBUI_AIONUI=1 with a local vendor/ directory to enable'
103
- )
104
- }
105
-
106
- const bunPath = resolveBunPath()
107
- if (!bunPath) {
108
- throw new Error('bun is required to start the HolySheep runtime (install: https://bun.sh)')
109
- }
110
-
111
- const child = spawn(bunPath, ['dist-server/server.mjs'], {
112
- cwd: runtimeDir,
113
- env: {
114
- ...process.env,
115
- PORT: String(port),
116
- NODE_ENV: 'production',
117
- },
118
- stdio: 'inherit',
119
- })
120
-
121
- try {
122
- await waitForReady(port)
123
- } catch (error) {
124
- child.kill('SIGTERM')
125
- throw error
126
- }
127
-
128
- return {
129
- child,
130
- runtimeDir,
131
- bunPath,
132
- mode: 'aionui',
133
- }
134
- }
135
-
136
- module.exports = {
137
- resolveAionUiRuntimeDir,
138
- startAionUiRuntime,
139
- }