@simonyea/holysheep-cli 2.0.6 → 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,59 +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.
125
131
 
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).
132
+ **What got patched (source-level, reviewable):**
133
+
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 — one-command** (recommended — 21 MB prebuilt, HolySheep-auth-ready):
149
+ **Prebuilt runtime (baked-in default):**
138
150
 
139
- ```bash
140
- export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.17-holysheep.tar.gz
141
- export HOLYSHEEP_AIONUI_RUNTIME_SHA256=d75adcea3c57c85f64b5db96d18c593f20ad150888f47283dc2da0a440fb652c
142
- hs web --aionui --setup-runtime
143
- ```
144
-
145
- The fetcher downloads to `~/.holysheep/aionui-runtime/`, verifies SHA256, extracts `dist-server/` + `out/` + `package.json`, boots AionUi, and opens your browser at the bootstrap redirect (auto-logged-in with your HS API key). Runs macOS / Linux. Windows support depends on `bun` availability.
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)
146
154
 
147
- **Alternative — build from AionUi source** (advanced):
155
+ **Rebuild from source** (audit the patches yourself):
148
156
 
149
157
  ```bash
150
- git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi
151
- cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server
152
- mkdir -p ~/.holysheep/aionui-runtime
153
- cp -R ~/AionUi/{dist-server,out,package.json} ~/.holysheep/aionui-runtime/
154
- 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
155
164
  ```
156
- Note: upstream AionUi's `/login` still expects `{ username, password }`; the legacy wrapper path handles that via an auto-provisioned bridge admin in `~/.holysheep/aionui-bridge.json` (0600).
157
165
 
158
- **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:**
159
172
 
160
- - `/login` is still reachable at `http://127.0.0.1:9876/login` for direct apiKey entry.
161
- - To rotate credentials: `rm -rf ~/.holysheep/aionui-runtime && hs web --aionui --setup-runtime` (or edit `~/.holysheep/config.json`).
162
- - Startup logs showing `[AionUi] Could not find builtin src/process/resources/...` are harmless — the prebuilt tarball ships only `dist-server/` + `out/` (not the source tree), so template assistants/skills load from built-in fallbacks instead. The UI works fully; only the "source-tree" assistant templates are unavailable.
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.
163
175
 
164
176
  ---
165
177
 
@@ -252,62 +264,71 @@ npx openclaw gateway --port <显示的端口>
252
264
  | Anthropic SDK / Claude Code | `https://api.holysheep.ai`(不带 /v1) |
253
265
  | OpenAI 兼容 / Codex / Aider | `https://api.holysheep.ai/v1`(带 /v1) |
254
266
 
255
- ### AionUi 模式(实验性,opt-in)
267
+ ### `hs web` —— 真 AionUi v1.9.18,HolySheep 驱动(2.1.0+)
256
268
 
257
- `hs web` 默认启动轻量版 WebUI,所有平台(Windows / macOS / Linux)开箱即用,无额外依赖。
258
-
259
- 要体验完整的 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 等)全部保留,只是登录换了种方式。
260
270
 
261
271
  ```bash
262
- # 普通安装(始终可用的轻量 WebUI)
272
+ # 安装 CLI(npm 包仍然 ~100 kB;21 MB runtime 首次运行再下载)
263
273
  npm install -g @simonyea/holysheep-cli
264
274
 
265
- # 可选:启用 AionUi 模式(需本地安装 AionUi runtime
266
- hs web --aionui
275
+ # 第一次跑:自动下载预编译 AionUi runtime ~/.holysheep/aionui-runtime/
276
+ hs web --setup-runtime
277
+
278
+ # 后续直接跑,复用缓存
279
+ hs web
267
280
  ```
268
281
 
269
- **原理**
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
+ **改了哪些源码(可审计):**
270
290
 
271
- 1. 代理监听公开端口 `9876`,将请求转发到 `127.0.0.1:9877` 上的 AionUi 服务(仅回环)。
272
- 2. 自动将保存在 `~/.holysheep/config.json` 的 HS API Key POST 到 AionUi 的 `/login`,拿到 JWT cookie。
273
- 3. 一次性 bootstrap token(≤30 s TTL,仅回环,拒绝 `X-Forwarded-For` / `X-Real-IP`)把 cookie 交给浏览器 用户直接落到 `/guid`,无需再输入任何凭据。
274
- 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" 链接保留上游账号密码流程(**任何功能都没删**)|
275
298
 
276
299
  **runtime 查找顺序**(命中即用):
277
300
 
278
301
  1. `~/.holysheep/aionui-runtime/`(已安装 / 已缓存)
279
- 2. `<cli>/src/webui/vendor/aionui/`(开发者本地仓库 —— npm 包不携带)
280
- 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`)下载
281
305
 
282
- **安装 runtime —— 一条命令**(推荐,21 MB 预编译包,内置 HolySheep Key 登录):
306
+ **预编译 runtime(内置默认值):**
283
307
 
284
- ```bash
285
- export HOLYSHEEP_AIONUI_RUNTIME_URL=https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.17-holysheep.tar.gz
286
- export HOLYSHEEP_AIONUI_RUNTIME_SHA256=d75adcea3c57c85f64b5db96d18c593f20ad150888f47283dc2da0a440fb652c
287
- hs web --aionui --setup-runtime
288
- ```
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/`)
289
311
 
290
- fetcher 会下载到 `~/.holysheep/aionui-runtime/`,校验 SHA256,解压 `dist-server/ + out/ + package.json`,启动 AionUi,然后直接把浏览器带到 bootstrap 重定向(用你的 HS API Key 自动登录)。macOS / Linux 都行,Windows 视 `bun` 支持情况而定。
291
-
292
- **备用方案 —— 从 AionUi 源码构建**(高级用户):
312
+ **从源码自己构建**(审计所有 patch):
293
313
 
294
314
  ```bash
295
- git clone https://github.com/iOfficeAI/AionUi.git ~/AionUi
296
- cd ~/AionUi && bun install && bun run build:renderer:web && bun run build:server
297
- mkdir -p ~/.holysheep/aionui-runtime
298
- cp -R ~/AionUi/{dist-server,out,package.json} ~/.holysheep/aionui-runtime/
299
- 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
300
321
  ```
301
- 注意:上游 AionUi 的 `/login` 仍然是账号密码模式;wrapper 会自动走 legacy 路径,在 `~/.holysheep/aionui-bridge.json` (0600) 建一个桥接管理员账号,不影响使用。
302
322
 
303
- **恢复 / 高级**
323
+ **备用模式:**
324
+
325
+ - `HOLYSHEEP_WEBUI_LEGACY=1 hs web` —— 老 HolySheep Workspace 页面(零依赖 node,仍然随 npm 包发)
326
+ - `hs web --aionui` —— 强制 AionUi 模式;runtime 缺失时**不会自动回退**,直接报错(方便 CI 场景)
304
327
 
305
- - `/login` 仍可直接访问:`http://127.0.0.1:9876/login`,里面可以手动粘 HS API Key 再登录。
306
- - 要重置:`rm -rf ~/.holysheep/aionui-runtime && hs web --aionui --setup-runtime`,或直接编辑 `~/.holysheep/config.json`。
307
- - runtime 缺失时:`hs web --aionui` 会打印清晰的安装指引并自动回退到 legacy WebUI,不会静默失败。
308
- - 启动日志里 `[AionUi] Could not find builtin src/process/resources/...` 是已知噪音 —— 预编译 tarball 只带 `dist-server/` 和 `out/`,不含源码目录,所以模板 assistants/skills 会走内置 fallback。界面完全可用,只是少几个"源码级"模板。
328
+ **后续规划:**
309
329
 
310
- 不设置该环境变量时 `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。
311
332
 
312
333
  ### 常见问题
313
334
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simonyea/holysheep-cli",
3
- "version": "2.0.6",
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,