Web UI build broken on Windows due to Unix commands
NousResearch/hermes-agent · cluster 24827 · hermes cli
Data as of
Nothing open on GitHub. Fix merged in #25983. Check it covers your case.
The short answer
0open issues
0open PRs
5items grouped
- Known?
- Yes. 5 related items grouped here, 2 issues already closed.
- Fix event
- A fix was merged to main in PR #25983. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #25983. That is separate from the fix event. - Still open
- Nothing open on GitHub.
The sync-assets build script uses Unix shell commands (rm, cp) that fail on Windows. Fixes replace these with cross-platform Node.js equivalents.
Open issues0
Open PRs0
Fixes on record
-
#25983 fix(web): cross-platform sync-assets + surface build errors + cp1252-safe prints
files:
hermes_cli/main.py,hermes_cli/proxy/server.py,scripts/release.py,web/package.json,web/scripts/sync-assets.mjsShow the diff
-
hermes_cli/main.py@@ -5681,21 +5681,50 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: if not _web_ui_build_needed(web_dir): return True + # 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. + + Windows users hitting `rm -rf` / `cp -r` errors (or any other + sync-assets / Vite failure) would otherwise see only ``Web UI + build failed`` with no hint of the underlying cause, because + the npm calls run with ``capture_output=True``. + """ + 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: + _say(text) + r1 = _run_npm_install_deterministic(npm, web_dir, extra_args=("--silent",)) 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 # First attempt r2 = subprocess.run( @@ -5730,21 +5759,20 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool: # A stale UI is far better than no UI for non-interactive callers # (Windows Scheduled Tasks, CI) — issue #23817. if dist_index.exists(): - print(" ⚠ Web UI build failed — serving stale dist as fallback") + _say(" ⚠ Web UI build failed — serving stale dist as fallback") if stderr_tail: - print(f" Build error:\n {stderr_tail}") + _say(f" Build error:\n {stderr_tail}") return True - print( + _say( f" {'✗' if fatal else '⚠'} Web UI build failed" + ("" if fatal else " (hermes web will not be available)") ) - if stderr_tail: - print(f" Build error:\n {stderr_tail}") + _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 -
hermes_cli/proxy/server.py@@ -243,7 +243,7 @@ async def run_server( loop = asyncio.get_running_loop() for sig in (signal.SIGINT, signal.SIGTERM): try: - loop.add_signal_handler(sig, stop_event.set) + loop.add_signal_handler(sig, stop_event.set) # windows-footgun: ok except NotImplementedError: # Windows / restricted environments — Ctrl+C will still # raise KeyboardInterrupt and unwind us. -
scripts/release.py@@ -767,6 +767,7 @@ "[redacted:email]": "ycbai", "[redacted:email]": "longsizhuo", "[redacted:email]": "ms-alan", + "[redacted:email]": "johnisag", "[redacted:email]": "WuTianyi123", "[redacted:email]": "li0near", "[redacted:email]": "li0near", @@ -865,6 +866,7 @@ "[redacted:email]": "dpaluy", "[redacted:email]": "el-analista", "[redacted:email]": "ms-alan", + "[redacted:email]": "johnisag", "[redacted:email]": "hex-clawd", "[redacted:email]": "LeonSGP43", "[redacted:email]": "barteqpl", -
web/package.json@@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "sync-assets": "node -e \"const fs=require('fs');fs.rmSync('public/fonts',{recursive:true,force:true});fs.rmSync('public/ds-assets',{recursive:true,force:true});fs.cpSync('node_modules/@nous-research/ui/dist/fonts','public/fonts',{recursive:true});fs.cpSync('node_modules/@nous-research/ui/dist/assets','public/ds-assets',{recursive:true});\"", + "sync-assets": "node scripts/sync-assets.mjs", "predev": "npm run sync-assets", "prebuild": "npm run sync-assets", "dev": "vite", -
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 }); +}
-
2 PRs closed without merging
From the close comments: superseded by another PR 2
Closed issues · 2
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.34: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #25983
- Closed issues
- 2
- Attempts
- 2 PRs closed without merging
- Fix link checked
- 14 May 2026