@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
|
@@ -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-hs26.tar.gz'
|
|
64
|
-
const DEFAULT_RUNTIME_SHA256 =
|
|
65
|
-
'e0502f21376d21b3797c51a3d932970c5a86e29076cadf3eeb2939941abb47ed'
|
|
66
|
-
const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs26'
|
|
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
|
-
}
|