From a54913efd24250264ef4d206ee5502499684888f Mon Sep 17 00:00:00 2001 From: Jack Date: Sat, 24 Oct 2026 20:59:08 +0800 Subject: [PATCH] fix false missing Hermes CLI detection after deployment --- DEVELOPMENT.md | 17 +++++++ deploy.sh | 17 +++++++ docs/openapi.json | 15 +++++- packages/client/src/api/hermes/system.ts | 26 ++++++---- packages/client/src/i18n/locales/en.ts | 6 +++ packages/client/src/i18n/locales/zh.ts | 6 +++ .../src/views/hermes/HermesMissingView.vue | 22 +++++---- packages/server/src/controllers/health.ts | 34 +++++++++----- .../src/services/hermes/hermes-process.ts | 37 +++++++++++---- tests/client/cli-status.test.ts | 34 ++++++++++++++ tests/e2e/cli-detection.spec.ts | 33 +++++++++++++ tests/server/deploy-cli-readiness.test.ts | 29 ++++++++++++ tests/server/health-controller.test.ts | 47 +++++++++++++++++++ tests/server/hermes-process.test.ts | 35 +++++++++++++- 14 files changed, 317 insertions(+), 41 deletions(-) create mode 100644 tests/client/cli-status.test.ts create mode 100644 tests/e2e/cli-detection.spec.ts create mode 100644 tests/server/deploy-cli-readiness.test.ts diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 6dabc4d..15368f6 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -33,6 +33,23 @@ npm run build - `npm run test:e2e` runs Playwright browser tests against a mocked BFF API. - `npm run build` type-checks and builds both client and server. +## Hermes CLI Detection + +The installation probe runs `hermes --help` in the Web UI service environment. +Some Hermes versions synchronously check upstream updates during `--version`, so +version output is not a reliable installation/liveness probe. A timeout does not +mean Hermes is absent and must not send users to the installation guide. + +`GET /api/hermes/cli-status` reports `available`, `bin`, and `reason` (`ready`, +`missing`, `permission_denied`, `timeout`, or `failed`). `?refresh=1` bypasses the +completed-result cache while sharing an in-flight probe. Deploy validation must +check this endpoint as well as HTTP health; a running Node process alone does not +prove that its service user can execute Hermes. + +When investigating, reproduce with the service's `HERMES_BIN`, `PATH`, `HOME`, and +`HERMES_HOME`. Inspect the complete launcher/interpreter path before changing +permissions. Do not use recursive permission changes to treat a probe timeout. + ## One-Click Deploy `deploy.sh` deploys a checkout to a host that runs the built server under diff --git a/deploy.sh b/deploy.sh index 953b248..5db378e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -40,6 +40,7 @@ GIT_PROXY="${HERMES_UI_GIT_PROXY:-http://127.0.0.1:7890}" HEALTH_URL="http://127.0.0.1:$PORT/health" AUTH_URL="http://127.0.0.1:$PORT/api/auth/status" +CLI_URL="http://127.0.0.1:$PORT/api/hermes/cli-status?refresh=1" # --- Output helpers ------------------------------------------------------- @@ -390,6 +391,22 @@ if [ "$auth_code" != "200" ]; then die "Deploy finished but the second health check failed." fi +# Probe from the running service so its user, PATH and HERMES_BIN are authoritative. +step "Checking Hermes CLI in the service environment" +if ! cli_status="$(curl -fsS --connect-timeout 5 --max-time 25 "$CLI_URL")"; then + die "Deploy finished but the Hermes CLI probe could not be reached. Check the service logs." +fi +if ! node -e ' + const result = JSON.parse(process.argv[1]); + if (result.available !== true) { + console.error("Hermes CLI check failed: " + (result.reason || "unknown") + " (" + (result.bin || "hermes") + ")"); + process.exit(1); + } +' "$cli_status"; then + die "Deploy finished but Hermes CLI is not ready. Check HERMES_BIN and the service logs." +fi +ok "Hermes CLI is executable by the service" + ok "health $HEALTH_URL -> 200" ok "auth $AUTH_URL -> 200" ok "Deployed $(git rev-parse --short HEAD 2>/dev/null || echo 'unknown commit') on port $PORT" diff --git a/docs/openapi.json b/docs/openapi.json index 24fdae5..6d0e328 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -2698,7 +2698,20 @@ "404": { "description": "Not found" } - } + }, + "parameters": [ + { + "name": "refresh", + "in": "query", + "required": false, + "schema": { + "type": "boolean", + "enum": [ + "1" + ] + } + } + ] } }, "/api/hermes/config": { diff --git a/packages/client/src/api/hermes/system.ts b/packages/client/src/api/hermes/system.ts index 0ae5746..865df6c 100644 --- a/packages/client/src/api/hermes/system.ts +++ b/packages/client/src/api/hermes/system.ts @@ -144,30 +144,38 @@ export async function checkHealth(): Promise { export interface HermesCliStatus { available: boolean bin?: string + reason?: 'ready' | 'missing' | 'permission_denied' | 'timeout' | 'failed' } -let cliProbePromise: Promise | null = null +let cliProbePromise: Promise | null = null +let refreshCliProbe = false -async function probeHermesCli(): Promise { +async function probeHermesCli(refresh: boolean): Promise { try { - const res = await request('/api/hermes/cli-status') - return res.available !== false + return await request(`/api/hermes/cli-status${refresh ? '?refresh=1' : ''}`) } catch { - // Fail open: probe errors (older servers, mocked backends) must not block the app. - return true + // Probe transport errors must not block access to the rest of the application. + return { available: true } } } -/** Cached per page load; the router guard probes once before showing the app. */ -export function checkHermesCliAvailable(): Promise { +export function getHermesCliStatus(): Promise { if (!cliProbePromise) { - cliProbePromise = probeHermesCli() + cliProbePromise = probeHermesCli(refreshCliProbe) + refreshCliProbe = false } return cliProbePromise } +/** A slow startup is not evidence that Hermes needs installing. */ +export async function checkHermesCliAvailable(): Promise { + const status = await getHermesCliStatus() + return status.available !== false || status.reason === 'timeout' +} + export function resetHermesCliProbe(): void { cliProbePromise = null + refreshCliProbe = true } export async function triggerUpdate(): Promise<{ success: boolean; message: string }> { diff --git a/packages/client/src/i18n/locales/en.ts b/packages/client/src/i18n/locales/en.ts index be53e5d..a2b5d72 100644 --- a/packages/client/src/i18n/locales/en.ts +++ b/packages/client/src/i18n/locales/en.ts @@ -53,6 +53,12 @@ export default { // hermes CLI missing guide hermesMissing: { + repairHint: 'After fixing the executable or runtime, use Re-check below.', + permissionTitle: 'Hermes CLI cannot be executed', + permissionIntro: 'The service cannot execute Hermes or its Python interpreter. Check executable permissions and directory access for the service user.', + failedTitle: 'Hermes CLI failed to start', + failedIntro: 'Hermes was found but its startup check failed. Check the service logs and the Hermes Python environment.', + title: 'Hermes CLI not detected', intro: 'hermes-ui no longer bundles the hermes CLI. Install it on the machine running this service first:', installCmd: 'pip install hermes-agent', diff --git a/packages/client/src/i18n/locales/zh.ts b/packages/client/src/i18n/locales/zh.ts index 6c0d6aa..bddf722 100644 --- a/packages/client/src/i18n/locales/zh.ts +++ b/packages/client/src/i18n/locales/zh.ts @@ -53,6 +53,12 @@ export default { // hermes CLI 缺失引导 hermesMissing: { + repairHint: '修复可执行文件或运行环境后,点击下方按钮重新检测。', + permissionTitle: 'Hermes CLI 执行权限不足', + permissionIntro: '服务无法执行 Hermes 或其 Python 解释器,请检查服务用户的文件执行权限和目录访问权限。', + failedTitle: 'Hermes CLI 启动失败', + failedIntro: '已找到 Hermes,但启动检测失败。请检查服务日志和 Hermes 的 Python 运行环境。', + title: '未检测到 hermes CLI', intro: 'hermes-ui 不再内置 hermes CLI,请先在运行本服务的机器上安装:', installCmd: 'pip install hermes-agent', diff --git a/packages/client/src/views/hermes/HermesMissingView.vue b/packages/client/src/views/hermes/HermesMissingView.vue index ea45acc..cb87249 100644 --- a/packages/client/src/views/hermes/HermesMissingView.vue +++ b/packages/client/src/views/hermes/HermesMissingView.vue @@ -1,19 +1,24 @@