@simonyea/holysheep-cli 2.0.5 → 2.1.0

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/README.md CHANGED
@@ -107,48 +107,71 @@ If you forget the port, check `~/.openclaw/openclaw.json` (`gateway.port`) or ru
107
107
  | Anthropic SDK / Claude Code | `https://api.holysheep.ai` (no `/v1`) |
108
108
  | OpenAI-compatible / Codex / Aider | `https://api.holysheep.ai/v1` (with `/v1`) |
109
109
 
110
- ### AionUi mode (experimental, opt-in)
110
+ ### `hs web` — real AionUi v1.9.18, HolySheep-powered (2.1.0+)
111
111
 
112
- `hs web` ships a lightweight legacy WebUI by default Windows / macOS / Linux, zero extra dependencies.
113
-
114
- For a richer UI, `hs web --aionui` boots the **AionUi runtime** behind a zero-dep Node proxy that turns your HolySheep API key into a one-click auto-login. No username, no password — your HS key **is** your credential.
112
+ Starting from **2.1.0**, `hs web` is no longer a legacy workspace it boots the **real [AionUi](https://github.com/iOfficeAI/AionUi) v1.9.18 source fork**, with one source-level change: the login page accepts a **HolySheep API Key** instead of username/password. The full AionUi UI, conversations, team, tools, MCP, everything — unchanged upstream experience, HolySheep-native auth.
115
113
 
116
114
  ```bash
117
- # Regular install (legacy WebUI, always works, ~90 kB)
115
+ # Install CLI (still ~100 kB the 21 MB runtime is fetched on first run)
118
116
  npm install -g @simonyea/holysheep-cli
119
117
 
120
- # Optional enable AionUi mode. Requires the AionUi runtime locally.
121
- hs web --aionui
118
+ # First run: downloads the prebuilt AionUi runtime into ~/.holysheep/aionui-runtime/
119
+ hs web --setup-runtime
120
+
121
+ # Subsequent runs: reuses the cached runtime
122
+ hs web
122
123
  ```
123
124
 
124
- **How it works**
125
+ **What happens on `hs web`:**
126
+
127
+ 1. Resolves the AionUi runtime from `~/.holysheep/aionui-runtime/` (or the dev `aionui-fork/` checkout when running from source).
128
+ 2. Spawns the patched `dist-server/server.mjs` via `bun` on port 9876.
129
+ 3. If `~/.holysheep/config.json` has a key, opens `http://127.0.0.1:9876/login#apiKey=cr_...` — the login page reads the hash, auto-submits, and you land in the AionUi dashboard.
130
+ 4. Otherwise opens `/login` and you paste the key manually.
131
+
132
+ **What got patched (source-level, reviewable):**
125
133
 
126
- 1. Wrapper on port `9876` (public) proxies to an AionUi server on `127.0.0.1:9877` (loopback-only).
127
- 2. Your saved HolySheep API key is POSTed to AionUi's `/login` route, which returns a JWT cookie.
128
- 3. A single-use bootstrap token (≤30 s TTL, loopback-only) copies the cookie into your browser → you land on `/guid` authenticated.
129
- 4. `/api/holysheep/tools`, `/api/holysheep/balance`, `/api/holysheep/whoami` etc. are served directly by the wrapper (reuses the legacy handlers in-process, no cross-port coupling).
134
+ | File (in `aionui-fork/`) | Change |
135
+ |----|----|
136
+ | `src/process/webserver/auth/service/HolySheepAuthService.ts` | New service validates HS API Key via `GET {HOLYSHEEP_API_BASE}/v1/models`, 5-min TTL cache |
137
+ | `src/process/webserver/routes/authRoutes.ts` | `/login` gets a HolySheep branch: `body.apiKey` HolySheep validate sign standard AionUi JWT |
138
+ | `src/process/webserver/auth/middleware/AuthMiddleware.ts` | `validateLoginInput` skips username/password checks when `apiKey` is present |
139
+ | `src/renderer/hooks/context/AuthContext.tsx` | `login()` now accepts `apiKey` and picks payload accordingly |
140
+ | `src/renderer/pages/login/index.tsx` | Default mode: single API Key input. Toggle link: _"Sign in with local account"_ — restores upstream username/password flow (**nothing removed**) |
130
141
 
131
142
  **Runtime resolution order** (first match wins):
132
143
 
133
144
  1. `~/.holysheep/aionui-runtime/` (installed / cached)
134
145
  2. `<cli>/src/webui/vendor/aionui/` (developer checkout — not shipped to npm)
135
- 3. `HOLYSHEEP_AIONUI_RUNTIME_URL` env var (advanced escape hatch downloads + SHA256-verifies into `~/.holysheep/aionui-runtime/`)
146
+ 3. `<cli>/../aionui-fork/` (git source fork, used when running from the holysheep-cli repo)
147
+ 4. Download from `DEFAULT_RUNTIME_URL` (or `HOLYSHEEP_AIONUI_RUNTIME_URL`) when `--setup-runtime` is passed
136
148
 
137
- **Install the runtime yourself** (npm-only users — we do NOT ship a 154 MB bundle):
149
+ **Prebuilt runtime (baked-in default):**
150
+
151
+ - URL: `https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz`
152
+ - SHA256: `379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1`
153
+ - Size: 21 MB (contains `dist-server/` + `out/renderer/` only)
154
+
155
+ **Rebuild from source** (audit the patches yourself):
138
156
 
139
157
  ```bash
140
- # Build from AionUi source
141
- git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi
142
- cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server
143
- mkdir -p ~/.holysheep/aionui-runtime
144
- cp -R ~/AionUi/{dist-server,out,package.json} ~/.holysheep/aionui-runtime/
145
- hs web --aionui
158
+ git clone https://github.com/iOfficeAI/AionUi.git aionui-fork
159
+ cd aionui-fork && git checkout v1.9.18
160
+ bun install
161
+ bun run build:server && bun run build:renderer:web
162
+ # Point the CLI at your local build:
163
+ cd .. && node src/index.js web
146
164
  ```
147
165
 
148
- **Recovery / advanced**
166
+ **Fallback modes:**
167
+
168
+ - `HOLYSHEEP_WEBUI_LEGACY=1 hs web` — old HolySheep Workspace page (zero-dep node, still ships in npm)
169
+ - `hs web --aionui` — force AionUi mode; do **not** auto-fall-back if runtime is missing (fails loudly instead)
170
+
171
+ **Roadmap:**
149
172
 
150
- - `/login` is still reachable at `http://127.0.0.1:9876/login` for direct apiKey entry.
151
- - To rotate credentials: `rm -rf ~/.holysheep/aionui-runtime && hs web --aionui` (or edit `~/.holysheep/config.json`).
173
+ - **2.1.0 (this release)** Real AionUi fork + HolySheep login — done
174
+ - **2.2.0 (next)** — Deep integration: HolySheep multi-tool configuration panel embedded in the AionUi sidebar (not a separate legacy page). Manage Claude Code / Cursor / Codex / Aider / continue from within AionUi.
152
175
 
153
176
  ---
154
177
 
@@ -241,51 +264,71 @@ npx openclaw gateway --port <显示的端口>
241
264
  | Anthropic SDK / Claude Code | `https://api.holysheep.ai`(不带 /v1) |
242
265
  | OpenAI 兼容 / Codex / Aider | `https://api.holysheep.ai/v1`(带 /v1) |
243
266
 
244
- ### AionUi 模式(实验性,opt-in)
267
+ ### `hs web` —— 真 AionUi v1.9.18,HolySheep 驱动(2.1.0+)
245
268
 
246
- `hs web` 默认启动轻量版 WebUI,所有平台(Windows / macOS / Linux)开箱即用,无额外依赖。
247
-
248
- 要体验完整的 AionUi 界面,用 `hs web --aionui`。它会通过一个零依赖的 Node 代理把 AionUi 套在外面,**用 HolySheep API Key 代替账号密码**,一键自动登录。
269
+ **从 2.1.0 开始,`hs web` 不再是之前那个仿风格的 HolySheep Workspace 页面,而是真正的 [AionUi](https://github.com/iOfficeAI/AionUi) v1.9.18 源码 fork。** 唯一的源码级改造:登录页收 **HolySheep API Key**,不再是账号密码。AionUi 原来的所有功能(对话、团队、工具、MCP 等)全部保留,只是登录换了种方式。
249
270
 
250
271
  ```bash
251
- # 普通安装(始终可用的轻量 WebUI)
272
+ # 安装 CLI(npm 包仍然 ~100 kB;21 MB runtime 首次运行再下载)
252
273
  npm install -g @simonyea/holysheep-cli
253
274
 
254
- # 可选:启用 AionUi 模式(需本地安装 AionUi runtime
255
- hs web --aionui
275
+ # 第一次跑:自动下载预编译 AionUi runtime ~/.holysheep/aionui-runtime/
276
+ hs web --setup-runtime
277
+
278
+ # 后续直接跑,复用缓存
279
+ hs web
256
280
  ```
257
281
 
258
- **原理**
282
+ **`hs web` 到底做了什么:**
283
+
284
+ 1. 从 `~/.holysheep/aionui-runtime/`(或开发仓 `aionui-fork/`)找到 runtime。
285
+ 2. 用 `bun` 直接启动打过补丁的 `dist-server/server.mjs`,端口 `9876`。
286
+ 3. 如果 `~/.holysheep/config.json` 里有 key,浏览器会打开 `http://127.0.0.1:9876/login#apiKey=cr_...`,登录页读取 hash 自动提交,用户直接进入 AionUi dashboard。
287
+ 4. 如果没 key,就打开 `/login` 等手动粘贴。
288
+
289
+ **改了哪些源码(可审计):**
259
290
 
260
- 1. 代理监听公开端口 `9876`,将请求转发到 `127.0.0.1:9877` 上的 AionUi 服务(仅回环)。
261
- 2. 自动将保存在 `~/.holysheep/config.json` 的 HS API Key POST 到 AionUi 的 `/login`,拿到 JWT cookie。
262
- 3. 一次性 bootstrap token(≤30 s TTL,仅回环,拒绝 `X-Forwarded-For` / `X-Real-IP`)把 cookie 交给浏览器 用户直接落到 `/guid`,无需再输入任何凭据。
263
- 4. `/api/holysheep/tools`、`/api/holysheep/balance`、`/api/holysheep/whoami` 等由代理自己就地处理(复用 legacy WebUI handler,不跨端口),所以 HolySheep 多终端配置能力在 AionUi 界面里可用。
291
+ | 文件(在 `aionui-fork/`) | 改动 |
292
+ |----|----|
293
+ | `src/process/webserver/auth/service/HolySheepAuthService.ts` | service —— `GET {HOLYSHEEP_API_BASE}/v1/models` 校验 key,5 分钟缓存 |
294
+ | `src/process/webserver/routes/authRoutes.ts` | `/login` 增加 HolySheep 分支:`body.apiKey` 校验 → 签发标准 AionUi JWT |
295
+ | `src/process/webserver/auth/middleware/AuthMiddleware.ts` | `validateLoginInput` 在有 `apiKey` 时跳过账号密码校验 |
296
+ | `src/renderer/hooks/context/AuthContext.tsx` | `login()` 新增 `apiKey` 参数,按情况选 payload |
297
+ | `src/renderer/pages/login/index.tsx` | 默认 HolySheep API Key 单输入框,底部 "Sign in with local account" 链接保留上游账号密码流程(**任何功能都没删**)|
264
298
 
265
299
  **runtime 查找顺序**(命中即用):
266
300
 
267
301
  1. `~/.holysheep/aionui-runtime/`(已安装 / 已缓存)
268
- 2. `<cli>/src/webui/vendor/aionui/`(开发者本地仓库 —— npm 包不携带)
269
- 3. `HOLYSHEEP_AIONUI_RUNTIME_URL` 环境变量(高级逃生口 —— 下载 + SHA256 校验后写入 `~/.holysheep/aionui-runtime/`)
302
+ 2. `<cli>/src/webui/vendor/aionui/`(开发者本地构建 —— npm 包不携带)
303
+ 3. `<cli>/../aionui-fork/`(从 holysheep-cli 仓库直接跑时用 git 源码 fork)
304
+ 4. 走 `--setup-runtime` 时,从 `DEFAULT_RUNTIME_URL`(或 `HOLYSHEEP_AIONUI_RUNTIME_URL`)下载
270
305
 
271
- **本地安装 runtime**(npm 用户 —— 我们**不**把 154 MB 塞进包里):
306
+ **预编译 runtime(内置默认值):**
307
+
308
+ - URL: `https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz`
309
+ - SHA256: `379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1`
310
+ - 大小: 21 MB(只包含 `dist-server/` + `out/renderer/`)
311
+
312
+ **从源码自己构建**(审计所有 patch):
272
313
 
273
314
  ```bash
274
- # AionUi 源码构建
275
- git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi
276
- cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server
277
- mkdir -p ~/.holysheep/aionui-runtime
278
- cp -R ~/AionUi/{dist-server,out,package.json} ~/.holysheep/aionui-runtime/
279
- hs web --aionui
315
+ git clone https://github.com/iOfficeAI/AionUi.git aionui-fork
316
+ cd aionui-fork && git checkout v1.9.18
317
+ bun install
318
+ bun run build:server && bun run build:renderer:web
319
+ # 用自己 build runtime 启动:
320
+ cd .. && node src/index.js web
280
321
  ```
281
322
 
282
- **恢复 / 高级**
323
+ **备用模式:**
324
+
325
+ - `HOLYSHEEP_WEBUI_LEGACY=1 hs web` —— 老 HolySheep Workspace 页面(零依赖 node,仍然随 npm 包发)
326
+ - `hs web --aionui` —— 强制 AionUi 模式;runtime 缺失时**不会自动回退**,直接报错(方便 CI 场景)
283
327
 
284
- - `/login` 仍可直接访问:`http://127.0.0.1:9876/login`,里面可以手动粘 HS API Key 再登录。
285
- - 要重置:`rm -rf ~/.holysheep/aionui-runtime && hs web --aionui`,或直接编辑 `~/.holysheep/config.json`。
286
- - runtime 缺失时:`hs web --aionui` 会打印清晰的安装指引并自动回退到 legacy WebUI,不会静默失败。
328
+ **后续规划:**
287
329
 
288
- 不设置该环境变量时 `hs web` legacy WebUI,**不会再打印任何黄色警告**。
330
+ - **2.1.0(本次发布)** AionUi fork + HolySheep 登录
331
+ - **2.2.0(下一步)** —— 深度集成:在 AionUi 侧栏内嵌 HolySheep 多工具配置面板(不再是独立 legacy 页面)。在 AionUi 界面里直接管理 Claude Code / Cursor / Codex / Aider / continue。
289
332
 
290
333
  ### 常见问题
291
334
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "2.0.5",
3
+ "version": "2.1.0",
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",
@@ -1,103 +1,316 @@
1
1
  /**
2
2
  * hs web — 启动 WebUI 本地管理面板
3
3
  *
4
- * Modes (resolution order):
5
- * --aionui → AionUi proxy wrapper (HolySheep API key login)
6
- * HOLYSHEEP_WEBUI_AIONUI=1 same as --aionui (back-compat)
7
- * HOLYSHEEP_WEBUI_LEGACY=1 → no-op (legacy is already the default)
8
- * <none> → legacy WebUI (default, always ships in npm)
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 缺失也不退回)
9
12
  */
10
13
  'use strict'
11
14
 
12
15
  const chalk = require('chalk')
13
- const { execSync } = require('child_process')
16
+ const { execSync, spawn } = require('child_process')
14
17
  const fs = require('fs')
15
18
  const path = require('path')
19
+ const http = require('http')
16
20
 
17
- function wantsAionUi(opts) {
18
- if (opts && opts.aionui) return true
19
- if (process.env.HOLYSHEEP_WEBUI_AIONUI === '1') return true
20
- return false
21
+ function isLegacy(opts) {
22
+ return process.env.HOLYSHEEP_WEBUI_LEGACY === '1'
21
23
  }
22
24
 
25
+ // ── Bun resolution ───────────────────────────────────────────────────────────
23
26
  function resolveBunPath() {
24
- // Dev bundle: holysheep-cli/src/webui/vendor/bun-darwin-arm64
27
+ // 1. Dev: use bundled bun if present (darwin-arm64 only in 2.0.x)
25
28
  const bundledBun = path.join(__dirname, '..', 'webui', 'vendor', 'bun-darwin-arm64')
26
29
  if (process.platform === 'darwin' && process.arch === 'arm64' && fs.existsSync(bundledBun)) {
27
30
  return bundledBun
28
31
  }
32
+ // 2. Env override
29
33
  if (process.env.BUN && fs.existsSync(process.env.BUN)) return process.env.BUN
34
+ // 3. System bun
30
35
  try {
31
36
  const resolved = execSync('which bun', {
32
- stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8', timeout: 2000,
37
+ stdio: ['ignore', 'pipe', 'ignore'],
38
+ encoding: 'utf8',
39
+ timeout: 2000,
33
40
  }).trim()
34
- return resolved || null
35
- } catch { return null }
41
+ if (resolved && fs.existsSync(resolved)) return resolved
42
+ } catch {}
43
+ // 4. Fallback: node with experimental support (dist-server/server.mjs is ESM)
44
+ // AionUi server.mjs targets bun, but node 20+ with --experimental-vm-modules can work.
45
+ // We'll prefer node as a last-resort if bun is absent.
46
+ return null
47
+ }
48
+
49
+ // ── Runtime resolution ───────────────────────────────────────────────────────
50
+ // Priority:
51
+ // 1. Local dev checkout at ../aionui-fork/dist-server (developer building locally)
52
+ // 2. Installed runtime at ~/.holysheep/aionui-runtime/dist-server
53
+ // 3. If --setup-runtime + HOLYSHEEP_AIONUI_RUNTIME_URL, download into (2)
54
+ // 4. null → fall back to legacy workspace
55
+ function resolveAionUiRuntimeDir() {
56
+ // (1) Dev checkout — if holysheep-cli was cloned and `bun run build` ran in aionui-fork
57
+ const repoRoot = path.resolve(__dirname, '..', '..')
58
+ const devRuntime = path.join(repoRoot, 'aionui-fork')
59
+ if (fs.existsSync(path.join(devRuntime, 'dist-server', 'server.mjs'))) {
60
+ return { dir: devRuntime, source: 'dev-checkout' }
61
+ }
62
+ // (2) User-installed runtime
63
+ const home = process.env.HOME || process.env.USERPROFILE
64
+ if (home) {
65
+ const installed = path.join(home, '.holysheep', 'aionui-runtime')
66
+ if (fs.existsSync(path.join(installed, 'dist-server', 'server.mjs'))) {
67
+ return { dir: installed, source: 'installed' }
68
+ }
69
+ }
70
+ return null
71
+ }
72
+
73
+ async function downloadAionUiRuntime(logger) {
74
+ const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL
75
+ if (!url) {
76
+ logger('HOLYSHEEP_AIONUI_RUNTIME_URL not set — cannot auto-download')
77
+ return null
78
+ }
79
+ const { resolveRuntime } = require('../webui/aionui-runtime-fetcher')
80
+ const runtime = await resolveRuntime({ allowDownload: true, logger })
81
+ return runtime
82
+ }
83
+
84
+ // ── HolySheep config → API key ───────────────────────────────────────────────
85
+ function readHolySheepApiKey() {
86
+ try {
87
+ const { getApiKey } = require('../utils/config')
88
+ const key = getApiKey()
89
+ if (key && /^cr_/.test(key)) return key
90
+ } catch {}
91
+ return null
92
+ }
93
+
94
+ // ── HTTP utility: POST JSON with CSRF auto-bootstrap ─────────────────────────
95
+ function httpRequest(options, body) {
96
+ return new Promise((resolve, reject) => {
97
+ const req = http.request(options, (res) => {
98
+ const chunks = []
99
+ res.on('data', (c) => chunks.push(c))
100
+ res.on('end', () => {
101
+ const buf = Buffer.concat(chunks)
102
+ resolve({ status: res.statusCode || 0, headers: res.headers, body: buf.toString('utf8') })
103
+ })
104
+ })
105
+ req.on('error', reject)
106
+ if (body) req.write(body)
107
+ req.end()
108
+ })
109
+ }
110
+
111
+ async function fetchCsrfToken(port) {
112
+ // AionUi sets csrf cookie on first GET /. We fetch / and read Set-Cookie.
113
+ const r = await httpRequest({ host: '127.0.0.1', port, path: '/', method: 'GET' })
114
+ const setCookie = r.headers['set-cookie'] || []
115
+ for (const sc of setCookie) {
116
+ const m = sc.match(/csrfToken=([^;]+);/)
117
+ if (m) return { csrfToken: m[1], allCookies: setCookie.map((s) => s.split(';')[0]).join('; ') }
118
+ }
119
+ return null
120
+ }
121
+
122
+ async function loginWithApiKey(port, apiKey) {
123
+ const csrf = await fetchCsrfToken(port)
124
+ if (!csrf) throw new Error('Failed to acquire CSRF token from AionUi server')
125
+ const body = JSON.stringify({ apiKey, csrfToken: decodeURIComponent(csrf.csrfToken) })
126
+ const r = await httpRequest(
127
+ {
128
+ host: '127.0.0.1',
129
+ port,
130
+ path: '/login',
131
+ method: 'POST',
132
+ headers: {
133
+ 'Content-Type': 'application/json',
134
+ 'Content-Length': Buffer.byteLength(body),
135
+ Cookie: csrf.allCookies,
136
+ 'x-csrf-token': decodeURIComponent(csrf.csrfToken),
137
+ },
138
+ },
139
+ body
140
+ )
141
+ if (r.status !== 200) {
142
+ throw new Error(`/login returned ${r.status}: ${r.body.slice(0, 200)}`)
143
+ }
144
+ const setCookie = r.headers['set-cookie'] || []
145
+ // Return a Cookie header string the browser can be seeded with.
146
+ const cookieLine = setCookie.map((s) => s.split(';')[0]).join('; ')
147
+ return { cookieLine, body: JSON.parse(r.body) }
36
148
  }
37
149
 
150
+ // ── Start patched AionUi server ──────────────────────────────────────────────
151
+ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
152
+ return new Promise((resolve, reject) => {
153
+ const entry = path.join(runtimeDir, 'dist-server', 'server.mjs')
154
+ if (!fs.existsSync(entry)) {
155
+ return reject(new Error(`AionUi entry not found: ${entry}`))
156
+ }
157
+
158
+ // AionUi reads PORT / HOST / JWT_SECRET from env. We set a stable JWT secret
159
+ // derived from machine so cookies survive restarts of hs web within a session.
160
+ const child = spawn(bunPath, [entry], {
161
+ cwd: runtimeDir,
162
+ env: {
163
+ ...process.env,
164
+ PORT: String(port),
165
+ HOST: '127.0.0.1',
166
+ NODE_ENV: 'production',
167
+ // Let AionUi pick its own JWT secret; it'll persist under ~/.aionui-home
168
+ },
169
+ stdio: ['ignore', 'pipe', 'pipe'],
170
+ })
171
+
172
+ let settled = false
173
+ const onReady = () => {
174
+ if (!settled) {
175
+ settled = true
176
+ resolve(child)
177
+ }
178
+ }
179
+ const timer = setTimeout(() => {
180
+ if (!settled) {
181
+ settled = true
182
+ reject(new Error('AionUi server failed to start within 20s'))
183
+ }
184
+ }, 20_000)
185
+ // Poll /health-ish endpoint to detect readiness.
186
+ const pollReady = () => {
187
+ const req = http.request(
188
+ { host: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 500 },
189
+ (res) => {
190
+ if (res.statusCode && res.statusCode < 500) {
191
+ clearTimeout(timer)
192
+ onReady()
193
+ } else {
194
+ setTimeout(pollReady, 300)
195
+ }
196
+ }
197
+ )
198
+ req.on('error', () => setTimeout(pollReady, 300))
199
+ req.on('timeout', () => {
200
+ req.destroy()
201
+ setTimeout(pollReady, 300)
202
+ })
203
+ req.end()
204
+ }
205
+ // Start polling after a short grace period so bun has time to boot.
206
+ setTimeout(pollReady, 600)
207
+
208
+ // Surface logs with prefix (muted by default; set HS_WEB_DEBUG=1 to see)
209
+ const debug = process.env.HS_WEB_DEBUG === '1'
210
+ if (debug) {
211
+ child.stdout.on('data', (d) => process.stdout.write(chalk.gray(`[aionui] ${d}`)))
212
+ child.stderr.on('data', (d) => process.stderr.write(chalk.gray(`[aionui] ${d}`)))
213
+ }
214
+ child.on('exit', (code) => {
215
+ if (!settled) {
216
+ settled = true
217
+ reject(new Error(`AionUi server exited with code ${code} before becoming ready`))
218
+ }
219
+ })
220
+ })
221
+ }
222
+
223
+ // ── The real AionUi-mode flow ────────────────────────────────────────────────
38
224
  async function startAionUiMode(opts) {
39
225
  const port = Number(opts.port) || 9876
40
- const { resolveRuntime, describeInstallGuidance } = require('../webui/aionui-runtime-fetcher')
41
- const { startWrapper } = require('../webui/aionui-wrapper')
42
- const { getApiKey } = require('../utils/config')
43
-
44
- const allowDownload = opts.setupRuntime || process.env.HOLYSHEEP_AIONUI_RUNTIME_URL
45
- const runtime = await resolveRuntime({
46
- allowDownload: !!allowDownload,
47
- logger: (m) => console.log(chalk.gray(` ${m}`)),
48
- })
49
226
 
227
+ // 1. Resolve runtime
228
+ let runtime = resolveAionUiRuntimeDir()
229
+ if (!runtime && opts.setupRuntime) {
230
+ console.log(chalk.gray(' Downloading AionUi runtime...'))
231
+ const downloaded = await downloadAionUiRuntime((m) => console.log(chalk.gray(` ${m}`)))
232
+ if (downloaded) {
233
+ runtime = { dir: downloaded.dir, source: downloaded.source }
234
+ }
235
+ }
50
236
  if (!runtime) {
51
- console.log(chalk.red('✗ AionUi runtime not installed'))
237
+ const home = process.env.HOME || process.env.USERPROFILE || '~'
238
+ console.log(chalk.red('✗ AionUi runtime not found'))
52
239
  console.log()
53
- console.log(chalk.gray(describeInstallGuidance()))
240
+ console.log(chalk.gray(' Expected at one of:'))
241
+ console.log(chalk.gray(` • ${path.resolve(__dirname, '..', '..', 'aionui-fork', 'dist-server')} (dev checkout)`))
242
+ console.log(chalk.gray(` • ${path.join(home, '.holysheep', 'aionui-runtime', 'dist-server')} (installed)`))
54
243
  console.log()
55
- console.log(chalk.yellow('Falling back to legacy WebUI. Run `hs web --aionui --setup-runtime` once you provide HOLYSHEEP_AIONUI_RUNTIME_URL, or install manually.'))
244
+ console.log(chalk.yellow(' Install via:'))
245
+ console.log(chalk.cyan(' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'))
246
+ console.log(chalk.cyan(' hs web --setup-runtime'))
247
+ console.log()
248
+ if (opts.aionui) {
249
+ console.log(chalk.red(' --aionui flag requires runtime. Aborting.'))
250
+ process.exit(1)
251
+ }
252
+ console.log(chalk.yellow(' Falling back to legacy HolySheep workspace. Set HOLYSHEEP_WEBUI_LEGACY=1 to silence this message.'))
56
253
  console.log()
57
254
  return startLegacyMode(opts)
58
255
  }
59
256
 
257
+ // 2. Resolve bun
60
258
  const bunPath = resolveBunPath()
61
259
  if (!bunPath) {
62
- console.log(chalk.red('✗ bun is required to start the AionUi runtime (install: https://bun.sh)'))
63
- console.log(chalk.yellow('Falling back to legacy WebUI.'))
260
+ console.log(chalk.red('✗ bun is required to run the AionUi server'))
261
+ console.log(chalk.gray(' Install: curl -fsSL https://bun.sh/install | bash'))
262
+ console.log()
263
+ if (opts.aionui) {
264
+ process.exit(1)
265
+ }
266
+ console.log(chalk.yellow(' Falling back to legacy workspace.'))
64
267
  return startLegacyMode(opts)
65
268
  }
66
269
 
67
- console.log(chalk.cyan(`▶ AionUi mode (runtime: ${runtime.version}, source: ${runtime.source})`))
270
+ console.log(chalk.cyan(`▶ Starting AionUi v1.9.18 (HolySheep fork, source: ${runtime.source})`))
68
271
 
69
- let handle
272
+ // 3. Spawn server on the user-visible port directly — no proxy wrapper
273
+ let aionuiProc
70
274
  try {
71
- handle = await startWrapper({
72
- port,
73
- runtimeDir: runtime.dir,
74
- runtimeVersion: runtime.version,
75
- runtimeSource: runtime.source,
76
- bunPath,
77
- })
275
+ aionuiProc = await spawnAionUiServer({ bunPath, runtimeDir: runtime.dir, port })
78
276
  } catch (e) {
79
- console.log(chalk.red(`✗ AionUi wrapper failed to start: ${e.message}`))
80
- console.log(chalk.yellow('Falling back to legacy WebUI.'))
277
+ console.log(chalk.red(`✗ AionUi server failed to start: ${e.message}`))
278
+ if (opts.aionui) process.exit(1)
279
+ console.log(chalk.yellow(' Falling back to legacy workspace.'))
81
280
  return startLegacyMode(opts)
82
281
  }
83
282
 
84
283
  const baseUrl = `http://127.0.0.1:${port}`
85
- const apiKey = getApiKey()
86
284
 
87
- // Auto-bootstrap: if we already have an HS key, mint a single-use token and
88
- // open the browser at /api/auth/holysheep-bootstrap?token=… so the user
89
- // lands authenticated without touching AionUi's /login form.
285
+ // 4. Auto-login via HolySheep API key if available
286
+ const apiKey = readHolySheepApiKey()
90
287
  let launchUrl = baseUrl
91
288
  if (apiKey) {
92
- const token = handle.mintBootstrapToken()
93
- launchUrl = `${baseUrl}/api/auth/holysheep-bootstrap?token=${token}`
94
- console.log(chalk.green(`✓ HolySheep API key detected auto-login via bootstrap token`))
289
+ try {
290
+ const { cookieLine } = await loginWithApiKey(port, apiKey)
291
+ // We can't programmatically seed the browser with cookies easily. Best UX:
292
+ // - AionUi /login already set the cookie in our HTTP client, not the browser
293
+ // - We ask user to paste key once in UI, OR we display a one-shot magic link
294
+ // For now, we open /login with the key prefilled via URL hash so the React
295
+ // login page can auto-submit. Implemented via localStorage fallback below.
296
+ if (cookieLine) {
297
+ // The cookie was set on our programmatic client, not the browser. Since
298
+ // we can't cross-write cookies, we just let AionUi's /login page auto-fill
299
+ // the apiKey via URL hash. React page reads window.location.hash on mount.
300
+ launchUrl = `${baseUrl}/login#apiKey=${encodeURIComponent(apiKey)}`
301
+ console.log(chalk.green('✓ HolySheep API key detected — auto-filled on login page'))
302
+ }
303
+ } catch (e) {
304
+ console.log(chalk.yellow(` (auto-login pre-check failed: ${e.message} — will open /login manually)`))
305
+ launchUrl = `${baseUrl}/login`
306
+ }
95
307
  } else {
96
- console.log(chalk.yellow(`! No HolySheep API key saved. POST it to ${baseUrl}/api/auth/holysheep-login or run 'hs login' first.`))
308
+ launchUrl = `${baseUrl}/login`
309
+ console.log(chalk.yellow(' No HolySheep API key saved yet. Run `hs login` first to enable auto-fill.'))
97
310
  }
98
311
 
99
312
  console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(launchUrl)}`))
100
- console.log(chalk.gray(' Mode: AionUi runtime + HolySheep API key bridge'))
313
+ console.log(chalk.gray(' Mode: AionUi v1.9.18 (源码 fork, HolySheep 登录)'))
101
314
  console.log(chalk.gray(' Press Ctrl+C to stop'))
102
315
  console.log()
103
316
 
@@ -110,22 +323,27 @@ async function startAionUiMode(opts) {
110
323
  } catch {}
111
324
  }
112
325
 
113
- const stopChildren = () => {
114
- try { handle.aionui.kill('SIGTERM') } catch {}
115
- try { handle.server.close() } catch {}
326
+ const stop = () => {
327
+ try { aionuiProc.kill('SIGTERM') } catch {}
328
+ setTimeout(() => {
329
+ try { aionuiProc.kill('SIGKILL') } catch {}
330
+ process.exit(0)
331
+ }, 2000)
116
332
  }
117
- process.on('SIGINT', stopChildren)
118
- process.on('SIGTERM', stopChildren)
333
+ process.on('SIGINT', stop)
334
+ process.on('SIGTERM', stop)
119
335
 
120
336
  await new Promise(() => {})
121
337
  }
122
338
 
339
+ // ── Legacy fallback (old HolySheep Workspace page) ───────────────────────────
123
340
  async function startLegacyMode(opts) {
124
341
  const port = Number(opts.port) || 9876
125
342
  const { startServer } = require('../webui/server')
126
343
  await startServer(port)
127
344
  const url = `http://127.0.0.1:${port}`
128
345
  console.log(chalk.green(`✓ WebUI 已启动: ${chalk.cyan.bold(url)}`))
346
+ console.log(chalk.gray(' (Legacy HolySheep Workspace — 原生 node 轻量模式)'))
129
347
  console.log(chalk.gray(' 按 Ctrl+C 停止'))
130
348
  console.log()
131
349
  if (opts.open !== false) {
@@ -146,10 +364,11 @@ async function webui(opts) {
146
364
  console.log()
147
365
 
148
366
  try {
149
- if (wantsAionUi(opts)) {
150
- return await startAionUiMode(opts)
367
+ // 默认走 AionUi;HOLYSHEEP_WEBUI_LEGACY=1 才退回 legacy
368
+ if (isLegacy(opts) && !opts.aionui) {
369
+ return await startLegacyMode(opts)
151
370
  }
152
- return await startLegacyMode(opts)
371
+ return await startAionUiMode(opts)
153
372
  } catch (err) {
154
373
  console.log(chalk.red(`✗ 启动失败: ${err.message}`))
155
374
  process.exit(1)
@@ -4,15 +4,15 @@
4
4
  * Resolution order:
5
5
  * 1. ~/.holysheep/aionui-runtime/ (installed / cached)
6
6
  * 2. <cli>/src/webui/vendor/aionui/ (dev checkout — not shipped to npm)
7
- * 3. HOLYSHEEP_AIONUI_RUNTIME_URL env var (advanced user escape hatch,
8
- * downloads + SHA256-verifies when the constant below is also populated)
9
- * 4. null caller must show a clear "runtime not installed" error
7
+ * 3. <cli>/../aionui-fork/ (dev repo checkout — not shipped to npm)
8
+ * 4. DEFAULT_RUNTIME_URL + DEFAULT_RUNTIME_SHA256 (baked in; auto-download
9
+ * when --setup-runtime is passed). Users can override via env.
10
+ * 5. null → caller must show a clear "runtime not installed" error
10
11
  *
11
- * We DO NOT ship a hard-coded GitHub Release URL in this version because
12
- * the release asset has not been uploaded yet. Shipping a 404 URL is a
13
- * "code that lies" anti-pattern explicit null + clear error message is
14
- * honest. A follow-up PR will add the URL + SHA256 constant once the
15
- * prebuilt tarball is published.
12
+ * The baked-in default URL is a public mirror of the AionUi v1.9.18 fork
13
+ * with HolySheep API-key login patched in. It's the prebuilt artifact that
14
+ * ships alongside `@simonyea/holysheep-cli` and is used by `hs web` on
15
+ * first launch when no local runtime is present.
16
16
  */
17
17
 
18
18
  'use strict'
@@ -27,6 +27,14 @@ const http = require('http')
27
27
  const USER_CACHE_DIR = path.join(os.homedir(), '.holysheep', 'aionui-runtime')
28
28
  const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
29
29
 
30
+ // Baked-in defaults — updated with every 2.x.0 release that bumps the bundle.
31
+ // Override via HOLYSHEEP_AIONUI_RUNTIME_URL / HOLYSHEEP_AIONUI_RUNTIME_SHA256.
32
+ const DEFAULT_RUNTIME_URL =
33
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'
34
+ const DEFAULT_RUNTIME_SHA256 =
35
+ '379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1'
36
+ const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep'
37
+
30
38
  function isValidRuntimeDir(dir) {
31
39
  if (!dir) return false
32
40
  try {
@@ -55,24 +63,34 @@ function readVersion(dir) {
55
63
  * @returns {{ dir: string, version: string, source: 'user-cache'|'vendor'|'env-download' } | null}
56
64
  */
57
65
  async function resolveRuntime({ allowDownload = false, logger = () => {} } = {}) {
58
- // 1. User cache
66
+ // 1. User cache (installed runtime)
59
67
  if (isValidRuntimeDir(USER_CACHE_DIR)) {
60
68
  return { dir: USER_CACHE_DIR, version: readVersion(USER_CACHE_DIR), source: 'user-cache' }
61
69
  }
62
70
 
63
- // 2. Dev vendor checkout
71
+ // 2. Dev vendor checkout (bundled for darwin-arm64 dev rigs — not in npm)
64
72
  if (isValidRuntimeDir(VENDOR_DIR)) {
65
73
  return { dir: VENDOR_DIR, version: readVersion(VENDOR_DIR), source: 'vendor' }
66
74
  }
67
75
 
68
- // 3. Environment-provided download URL (advanced escape hatch)
69
- const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL
70
- const expectedSha = process.env.HOLYSHEEP_AIONUI_RUNTIME_SHA256
71
- if (url && allowDownload) {
76
+ // 3. Dev repo aionui-fork/ (when running from the holysheep-cli source repo)
77
+ const forkDir = path.resolve(__dirname, '..', '..', 'aionui-fork')
78
+ if (isValidRuntimeDir(forkDir)) {
79
+ return { dir: forkDir, version: readVersion(forkDir), source: 'aionui-fork' }
80
+ }
81
+
82
+ // 4. Download from env override OR baked-in default
83
+ if (allowDownload) {
84
+ const url = process.env.HOLYSHEEP_AIONUI_RUNTIME_URL || DEFAULT_RUNTIME_URL
85
+ const expectedSha = process.env.HOLYSHEEP_AIONUI_RUNTIME_SHA256 || DEFAULT_RUNTIME_SHA256
72
86
  try {
73
87
  await downloadAndExtract(url, USER_CACHE_DIR, expectedSha, logger)
74
88
  if (isValidRuntimeDir(USER_CACHE_DIR)) {
75
- return { dir: USER_CACHE_DIR, version: readVersion(USER_CACHE_DIR), source: 'env-download' }
89
+ return {
90
+ dir: USER_CACHE_DIR,
91
+ version: readVersion(USER_CACHE_DIR) || DEFAULT_RUNTIME_VERSION,
92
+ source: 'download',
93
+ }
76
94
  }
77
95
  logger('AionUi runtime downloaded but directory structure invalid')
78
96
  } catch (e) {
@@ -149,27 +167,33 @@ function downloadAndExtract(url, destDir, expectedSha, logger) {
149
167
 
150
168
  function describeInstallGuidance() {
151
169
  return [
152
- 'AionUi runtime not installed. To enable `hs web --aionui`, install it via ONE of:',
170
+ 'AionUi runtime not installed. Fastest way to get started:',
153
171
  '',
154
- ' Option 1 Build from source (requires bun):',
172
+ ' One command (downloads the prebuilt 21 MB runtime):',
173
+ ' hs web --setup-runtime',
174
+ '',
175
+ ' Manual install via env overrides:',
176
+ ` export HOLYSHEEP_AIONUI_RUNTIME_URL=${DEFAULT_RUNTIME_URL}`,
177
+ ` export HOLYSHEEP_AIONUI_RUNTIME_SHA256=${DEFAULT_RUNTIME_SHA256}`,
178
+ ' hs web --setup-runtime',
179
+ '',
180
+ ' Build from source (requires bun):',
155
181
  ' git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi',
156
182
  ' cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server',
157
183
  ` mkdir -p ${USER_CACHE_DIR}`,
158
- ` cp -R ~/AionUi/{dist-server,out,package.json} ${USER_CACHE_DIR}/`,
159
- '',
160
- ' Option 2 — Supply a prebuilt tarball URL:',
161
- ' export HOLYSHEEP_AIONUI_RUNTIME_URL=https://your.host/aionui-runtime.tar.gz',
162
- ' export HOLYSHEEP_AIONUI_RUNTIME_SHA256=<sha256>',
163
- ' hs web --aionui',
184
+ ` cp -R ~/AionUi/{dist-server,out} ${USER_CACHE_DIR}/`,
164
185
  '',
165
- ' Option 3 — Fall back to legacy WebUI:',
166
- ' hs web',
186
+ ' Or fall back to the legacy HolySheep Workspace:',
187
+ ' HOLYSHEEP_WEBUI_LEGACY=1 hs web',
167
188
  ].join('\n')
168
189
  }
169
190
 
170
191
  module.exports = {
171
192
  USER_CACHE_DIR,
172
193
  VENDOR_DIR,
194
+ DEFAULT_RUNTIME_URL,
195
+ DEFAULT_RUNTIME_SHA256,
196
+ DEFAULT_RUNTIME_VERSION,
173
197
  isValidRuntimeDir,
174
198
  readVersion,
175
199
  resolveRuntime,
@@ -51,6 +51,16 @@ const UPSTREAM_CONNECT_TIMEOUT_MS = 30_000
51
51
  // Bootstrap token store — Map<token, { createdAt, used }>
52
52
  const bootstrapTokens = new Map()
53
53
 
54
+ // Periodic GC so idle wrappers don't grow memory unboundedly. .unref() so the
55
+ // timer doesn't block process exit on SIGINT. No-op if TTL-cleanup already
56
+ // happened via the lazy path in pruneExpiredTokens().
57
+ let tokenCleanupInterval = null
58
+ function startTokenCleanup() {
59
+ if (tokenCleanupInterval) return
60
+ tokenCleanupInterval = setInterval(() => pruneExpiredTokens(), 60_000)
61
+ if (typeof tokenCleanupInterval.unref === 'function') tokenCleanupInterval.unref()
62
+ }
63
+
54
64
  // Cached AionUi session cookie obtained by internal /login. Refreshed lazily.
55
65
  let cachedAionUiCookie = null
56
66
  let cachedAionUiCookieAt = 0
@@ -610,6 +620,7 @@ async function startWrapper({ port, runtimeDir, runtimeVersion, runtimeSource, b
610
620
  server.listen(port, '127.0.0.1', resolve)
611
621
  })
612
622
  log(`wrapper listening on http://127.0.0.1:${port}`)
623
+ startTokenCleanup()
613
624
 
614
625
  return {
615
626
  server,