# NousResearch/hermes-agent · Web UI build broken on Windows due to Unix commands

## Open now

- 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.

## TL;DR

- 5 member(s): 0 open issue(s), 2 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #25983. Verify it addresses your case.
- **Recommended:** read PR #25983 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `partially_resolved` · deciding PR(s): `pr-25983`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_PARTIAL`): 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 member 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.

## Closed / duplicate issues

- #24827: \[Bug\]: Web UI build fails on Windows: 'rm' command not recognized
- #25073: \[Bug\] Web UI build fails on Windows: \`rm\` is not recognized in \`sync-assets\` script

## Prior & competing PRs

- **#25983: fix(web): cross-platform sync-assets + surface build errors + cp1252-safe prints**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`; implication `superseded`
  - files: `hermes_cli/main.py`, `hermes_cli/proxy/server.py`, `scripts/release.py`, `web/package.json`, `web/scripts/sync-assets.mjs`
  - fix hunks:
  - `hermes_cli/main.py`

    ```diff
    @@ -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(
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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",
    ```

    ```diff
    @@ -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`

    ```diff
    @@ -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`

    ```diff
    @@ -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 });
    +}
    ```
- **#25090: fix(web): use Node.js for sync-assets instead of Unix rm/cp**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#25983`, `#13368`
- **#25977: fix(web): use Node.js for sync-assets instead of Unix rm/cp**. closed without merge
  - maintainer direction: category `superseded_by_pr`; implication `superseded`; refs `#13368`

## Fix status

- Merged to main in PR #25983 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-05-14`.
- Subsystem: `hermes_cli`
