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

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #25983 fix(web): cross-platform sync-assets + surface build errors + cp1252-safe prints

    merged to main

    files: hermes_cli/main.py, hermes_cli/proxy/server.py, scripts/release.py, web/package.json, web/scripts/sync-assets.mjs

    Show 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

  • #25090 fix(web): use Node.js for sync-assets instead of Unix rm/cp · superseded by another PR → #25983, #13368
  • #25977 fix(web): use Node.js for sync-assets instead of Unix rm/cp · superseded by another PR → #13368
Closed issues · 2
  • Closed#24827[Bug]: Web UI build fails on Windows: 'rm' command not recognized
  • Closed#25073[Bug] Web UI build fails on Windows: `rm` is not recognized in `sync-assets` script
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.00 means 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 Lab

At a glance

Subsystem
hermes cli
Merged PRs
#25983
Closed issues
2
Attempts
2 PRs closed without merging
Fix link checked
14 May 2026

For your agent

The same facts, typed.