From 9a276b6e8804f0d3436db3853c0703d6399f4aa0 Mon Sep 17 00:00:00 2001 From: ioannis Date: Tue, 21 Apr 2026 07:41:20 +0100 Subject: [PATCH 1/2] fix(web): cross-platform sync-assets + surface build errors on failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two independent Windows bugs in the web-dashboard build path. 1) `web/package.json`'s `sync-assets` script shelled out to `rm -rf` and `cp -r`, which don't exist on Windows cmd.exe. `hermes_cli/main.py` invokes `npm install` / `npm run build` through `subprocess.run(...)` which on Windows defaults to cmd.exe, so the `prebuild` hook crashed before Vite ran and the dashboard never got built. Replaced with a tiny `web/scripts/sync-assets.mjs` Node script using `fs.rmSync` + `fs.cpSync` (stdlib, Node >= 16.7). No new dependencies, works identically on POSIX and Windows. Verified end-to-end: `subprocess.run([npm, 'run', 'build'], ...)` from the bundled Python now completes with exit 0 on Windows 11 and produces `hermes_cli/web_dist/` as expected. 2) `_build_web_ui()` in `hermes_cli/main.py` ran both `npm install` and `npm run build` with `capture_output=True` and never relayed the captured buffers on failure. Users got: ✗ Web UI build failed ...with no diagnostic, making this class of bug extremely hard to report or self-diagnose. Added a `_relay()` inner helper that prints stdout + stderr (utf-8, errors='replace') when a subprocess returns non-zero. The success path is unchanged — still silent when things work. Scope ----- Intentionally NOT bundled with the `pid_is_alive` PR (#13198) because these are build-time shell-script portability issues, not runtime liveness-check bugs. Keeping them separate for cleaner review. --- hermes_cli/main.py | 16 +++++++++++++++- web/package.json | 2 +- web/scripts/sync-assets.mjs | 27 +++++++++++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 web/scripts/sync-assets.mjs diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f88c42ddaf884..55b9333c19a55 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -4372,12 +4372,25 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: print("Install Node.js, then run: cd web && npm install && npm run build") return not fatal print("→ Building web UI...") - r1 = subprocess.run([npm, "install", "--silent"], cwd=web_dir, capture_output=True) + + def _relay(result: "subprocess.CompletedProcess") -> None: + """Print captured npm output so users can see *why* a step failed.""" + for blob in (result.stdout, result.stderr): + if not blob: + continue + text = blob.decode("utf-8", errors="replace").rstrip() if isinstance(blob, bytes) else blob.rstrip() + if text: + print(text) + + r1 = subprocess.run( + [npm, "install", "--silent"], cwd=web_dir, capture_output=True + ) if r1.returncode != 0: print( f" {'✗' if fatal else '⚠'} Web UI npm install failed" + ("" if fatal else " (hermes web will not be available)") ) + _relay(r1) if fatal: print(" Run manually: cd web && npm install && npm run build") return False @@ -4387,6 +4400,7 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: f" {'✗' if fatal else '⚠'} Web UI build failed" + ("" if fatal else " (hermes web will not be available)") ) + _relay(r2) if fatal: print(" Run manually: cd web && npm install && npm run build") return False diff --git a/web/package.json b/web/package.json index 8882c5c1c8050..355fbd8ef528d 100644 --- a/web/package.json +++ b/web/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "sync-assets": "rm -rf public/fonts public/ds-assets && cp -r node_modules/@nous-research/ui/dist/fonts public/fonts && cp -r node_modules/@nous-research/ui/dist/assets public/ds-assets", + "sync-assets": "node scripts/sync-assets.mjs", "predev": "npm run sync-assets", "prebuild": "npm run sync-assets", "dev": "vite", diff --git a/web/scripts/sync-assets.mjs b/web/scripts/sync-assets.mjs new file mode 100644 index 0000000000000..19b0bafb6aabf --- /dev/null +++ b/web/scripts/sync-assets.mjs @@ -0,0 +1,27 @@ +#!/usr/bin/env node +// Cross-platform replacement for the previous shell pipeline: +// +// rm -rf public/fonts public/ds-assets +// && cp -r node_modules/@nous-research/ui/dist/fonts public/fonts +// && cp -r node_modules/@nous-research/ui/dist/assets public/ds-assets +// +// `rm -rf` / `cp -r` don't exist on Windows cmd.exe, so `npm run build` +// (invoked from Python via subprocess → cmd.exe) failed before Vite ran. +// Using Node's stdlib fs keeps this dependency-free and platform-neutral. + +import { cpSync, rmSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const uiDist = resolve(webRoot, "node_modules", "@nous-research", "ui", "dist"); + +const targets = [ + { from: resolve(uiDist, "fonts"), to: resolve(webRoot, "public", "fonts") }, + { from: resolve(uiDist, "assets"), to: resolve(webRoot, "public", "ds-assets") }, +]; + +for (const { from, to } of targets) { + rmSync(to, { recursive: true, force: true }); + cpSync(from, to, { recursive: true }); +} From 2b0d1ffd22064ca97c724041dee40e75753299bc Mon Sep 17 00:00:00 2001 From: ioannis Date: Tue, 21 Apr 2026 07:49:15 +0100 Subject: [PATCH 2/2] fix(web): handle non-UTF8 Windows console encodings in _build_web_ui MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review pointed out that even with the sync-assets fix applied, _build_web_ui still crashes on a stock Windows console before reaching npm: Python stdout defaults to cp1252 (or similar) and raises UnicodeEncodeError when print() hits the arrow/check glyphs used for status messages (→, ✗, ⚠, ✓). Reproduced locally in PowerShell: $ PYTHONIOENCODING=cp1252 python -c "from hermes_cli.main import _build_web_ui; _build_web_ui(Path('web'), fatal=True)" UnicodeEncodeError: 'charmap' codec can't encode character '\u2192' ... The previous PR body claimed "end-to-end verified on Windows 11", but that was under the venv's default (utf-8) stdout. A plain `py` or PowerShell invocation would still fail before sync-assets ever ran. Fix: inner _say() helper that falls back to text.encode(sys.stdout.encoding, errors="replace") when print() raises UnicodeEncodeError. Glyphs degrade to '?' on ASCII / cp1252 consoles; utf-8 consoles are unaffected. Verified the full build pipeline runs to completion with PYTHONIOENCODING=cp1252. Scoped tightly to _build_web_ui (the function this PR already touches); other call sites in the codebase with the same risk are out of scope. --- hermes_cli/main.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 55b9333c19a55..a99907f217fb3 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -4365,13 +4365,25 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: return True import shutil + # Console-encoding-safe print: Windows consoles default to cp1252 + # (or similar) and will raise UnicodeEncodeError on arrow / check + # glyphs unless PYTHONIOENCODING=utf-8 is set. Routing every print + # in this function through _say() with errors="replace" keeps the + # build path usable on a stock `py -m hermes_cli.main web` invocation. + def _say(text: str) -> None: + try: + print(text) + except UnicodeEncodeError: + encoding = getattr(sys.stdout, "encoding", None) or "ascii" + print(text.encode(encoding, errors="replace").decode(encoding, errors="replace")) + npm = shutil.which("npm") if not npm: if fatal: - print("Web UI frontend not built and npm is not available.") - print("Install Node.js, then run: cd web && npm install && npm run build") + _say("Web UI frontend not built and npm is not available.") + _say("Install Node.js, then run: cd web && npm install && npm run build") return not fatal - print("→ Building web UI...") + _say("→ Building web UI...") def _relay(result: "subprocess.CompletedProcess") -> None: """Print captured npm output so users can see *why* a step failed.""" @@ -4380,31 +4392,31 @@ def _relay(result: "subprocess.CompletedProcess") -> None: continue text = blob.decode("utf-8", errors="replace").rstrip() if isinstance(blob, bytes) else blob.rstrip() if text: - print(text) + _say(text) r1 = subprocess.run( [npm, "install", "--silent"], cwd=web_dir, capture_output=True ) if r1.returncode != 0: - print( + _say( f" {'✗' if fatal else '⚠'} Web UI npm install failed" + ("" if fatal else " (hermes web will not be available)") ) _relay(r1) if fatal: - print(" Run manually: cd web && npm install && npm run build") + _say(" Run manually: cd web && npm install && npm run build") return False r2 = subprocess.run([npm, "run", "build"], cwd=web_dir, capture_output=True) if r2.returncode != 0: - print( + _say( f" {'✗' if fatal else '⚠'} Web UI build failed" + ("" if fatal else " (hermes web will not be available)") ) _relay(r2) if fatal: - print(" Run manually: cd web && npm install && npm run build") + _say(" Run manually: cd web && npm install && npm run build") return False - print(" ✓ Web UI built") + _say(" ✓ Web UI built") return True