{"repo":"NousResearch/hermes-agent","public_id":"24827","display_label":"Web UI build broken on Windows due to Unix commands","summary":"The sync-assets build script uses Unix shell commands (rm, cp) that fail on Windows. Fixes replace these with cross-platform Node.js equivalents.","subsystem":"hermes_cli","counts":{"members":5,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":2,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":25983},"resolution":{"state":"partially_resolved","by":"pr-25983"},"freshness":{"state":"verified","last_verified_at":"2026-05-14T22:58:00+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_PARTIAL","link_density":0.33599999999999997},"fix_edges":[{"issue_number":25073,"pr_number":25983,"tier":"verified","basis":"closing_ref","confidence":0.95,"recoverable_from_timeline":true}],"members":[{"kind":"issue","number":24827,"title":"[Bug]: Web UI build fails on Windows: 'rm' command not recognized","url":null,"state":"closed"},{"kind":"issue","number":25073,"title":"[Bug] Web UI build fails on Windows: `rm` is not recognized in `sync-assets` script","url":null,"state":"closed"},{"kind":"pr","number":25090,"title":"fix(web): use Node.js for sync-assets instead of Unix rm/cp","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#25983","number":25983},{"kind":"pr","value":"#13368","number":13368}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":25977,"title":"fix(web): use Node.js for sync-assets instead of Unix rm/cp","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#13368","number":13368}],"category":"superseded_by_pr","implication":"superseded"},"merged":false},{"kind":"pr","number":25983,"title":"fix(web): cross-platform sync-assets + surface build errors + cp1252-safe prints","url":null,"state":"merged","close_reason":{"category":"merged","implication":"superseded"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["hermes_cli/main.py","hermes_cli/proxy/server.py","scripts/release.py","web/package.json","web/scripts/sync-assets.mjs"],"fix_hunks":[{"file":"hermes_cli/main.py","hunks":["@@ -5681,21 +5681,50 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool:\n     if not _web_ui_build_needed(web_dir):\n         return True\n \n+    # Console-encoding-safe print: Windows consoles default to cp1252\n+    # (or similar) and will raise UnicodeEncodeError on arrow / check\n+    # glyphs unless PYTHONIOENCODING=utf-8 is set. Routing every print\n+    # in this function through _say() with errors=\"replace\" keeps the\n+    # build path usable on a stock `py -m hermes_cli.main web` invocation.\n+    def _say(text: str) -> None:\n+        try:\n+            print(text)\n+        except UnicodeEncodeError:\n+            encoding = getattr(sys.stdout, \"encoding\", None) or \"ascii\"\n+            print(text.encode(encoding, errors=\"replace\").decode(encoding, errors=\"replace\"))\n+\n     npm = shutil.which(\"npm\")\n     if not npm:\n         if fatal:\n-            print(\"Web UI frontend not built and npm is not available.\")\n-            print(\"Install Node.js, then run:  cd web && npm install && npm run build\")\n+            _say(\"Web UI frontend not built and npm is not available.\")\n+            _say(\"Install Node.js, then run:  cd web && npm install && npm run build\")\n         return not fatal\n-    print(\"→ Building web UI...\")\n+    _say(\"→ Building web UI...\")\n+\n+    def _relay(result: \"subprocess.CompletedProcess\") -> None:\n+        \"\"\"Print captured npm output so users can see *why* a step failed.\n+\n+        Windows users hitting `rm -rf` / `cp -r` errors (or any other\n+        sync-assets / Vite failure) would otherwise see only ``Web UI\n+        build failed`` with no hint of the underlying cause, because\n+        the npm calls run with ``capture_output=True``.\n+        \"\"\"\n+        for blob in (result.stdout, result.stderr):\n+            if not blob:\n+                continue\n+            text = blob.decode(\"utf-8\", errors=\"replace\").rstrip() if isinstance(blob, bytes) else blob.rstrip()\n+            if text:\n+                _say(text)\n+\n     r1 = _run_npm_install_deterministic(npm, web_dir, extra_args=(\"--silent\",))\n     if r1.returncode != 0:\n-        print(\n+        _say(\n             f\"  {'✗' if fatal else '⚠'} Web UI npm install failed\"\n             + (\"\" if fatal else \" (hermes web will not be available)\")\n         )\n+        _relay(r1)\n         if fatal:\n-            print(\"  Run manually:  cd web && npm install && npm run build\")\n+            _say(\"  Run manually:  cd web && npm install && npm run build\")\n         return False\n     # First attempt\n     r2 = subprocess.run(","@@ -5730,21 +5759,20 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool:\n         # A stale UI is far better than no UI for non-interactive callers\n         # (Windows Scheduled Tasks, CI) — issue #23817.\n         if dist_index.exists():\n-            print(\"  ⚠ Web UI build failed — serving stale dist as fallback\")\n+            _say(\"  ⚠ Web UI build failed — serving stale dist as fallback\")\n             if stderr_tail:\n-                print(f\"  Build error:\\n  {stderr_tail}\")\n+                _say(f\"  Build error:\\n  {stderr_tail}\")\n             return True\n \n-        print(\n+        _say(\n             f\"  {'✗' if fatal else '⚠'} Web UI build failed\"\n             + (\"\" if fatal else \" (hermes web will not be available)\")\n         )\n-        if stderr_tail:\n-            print(f\"  Build error:\\n  {stderr_tail}\")\n+        _relay(r2)\n         if fatal:\n-            print(\"  Run manually:  cd web && npm install && npm run build\")\n+            _say(\"  Run manually:  cd web && npm install && npm run build\")\n         return False\n-    print(\"  ✓ Web UI built\")\n+    _say(\"  ✓ Web UI built\")\n     return True\n \n "]},{"file":"hermes_cli/proxy/server.py","hunks":["@@ -243,7 +243,7 @@ async def run_server(\n         loop = asyncio.get_running_loop()\n         for sig in (signal.SIGINT, signal.SIGTERM):\n             try:\n-                loop.add_signal_handler(sig, stop_event.set)\n+                loop.add_signal_handler(sig, stop_event.set)  # windows-footgun: ok\n             except NotImplementedError:\n                 # Windows / restricted environments — Ctrl+C will still\n                 # raise KeyboardInterrupt and unwind us."]},{"file":"scripts/release.py","hunks":["@@ -767,6 +767,7 @@\n     \"[redacted:email]\": \"ycbai\",\n     \"[redacted:email]\": \"longsizhuo\",\n     \"[redacted:email]\": \"ms-alan\",\n+    \"[redacted:email]\": \"johnisag\",\n     \"[redacted:email]\": \"WuTianyi123\",\n     \"[redacted:email]\": \"li0near\",\n     \"[redacted:email]\": \"li0near\",","@@ -865,6 +866,7 @@\n     \"[redacted:email]\": \"dpaluy\",\n     \"[redacted:email]\": \"el-analista\",\n     \"[redacted:email]\": \"ms-alan\",\n+    \"[redacted:email]\": \"johnisag\",\n     \"[redacted:email]\": \"hex-clawd\",\n     \"[redacted:email]\": \"LeonSGP43\",\n     \"[redacted:email]\": \"barteqpl\","]},{"file":"web/package.json","hunks":["@@ -4,7 +4,7 @@\n   \"version\": \"0.0.0\",\n   \"type\": \"module\",\n   \"scripts\": {\n-    \"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});\\\"\",\n+    \"sync-assets\": \"node scripts/sync-assets.mjs\",\n     \"predev\": \"npm run sync-assets\",\n     \"prebuild\": \"npm run sync-assets\",\n     \"dev\": \"vite\","]},{"file":"web/scripts/sync-assets.mjs","hunks":["@@ -0,0 +1,27 @@\n+#!/usr/bin/env node\n+// Cross-platform replacement for the previous shell pipeline:\n+//\n+//   rm -rf public/fonts public/ds-assets\n+//   && cp -r node_modules/@nous-research/ui/dist/fonts public/fonts\n+//   && cp -r node_modules/@nous-research/ui/dist/assets public/ds-assets\n+//\n+// `rm -rf` / `cp -r` don't exist on Windows cmd.exe, so `npm run build`\n+// (invoked from Python via subprocess → cmd.exe) failed before Vite ran.\n+// Using Node's stdlib fs keeps this dependency-free and platform-neutral.\n+\n+import { cpSync, rmSync } from \"node:fs\";\n+import { dirname, resolve } from \"node:path\";\n+import { fileURLToPath } from \"node:url\";\n+\n+const webRoot = resolve(dirname(fileURLToPath(import.meta.url)), \"..\");\n+const uiDist = resolve(webRoot, \"node_modules\", \"@nous-research\", \"ui\", \"dist\");\n+\n+const targets = [\n+  { from: resolve(uiDist, \"fonts\"), to: resolve(webRoot, \"public\", \"fonts\") },\n+  { from: resolve(uiDist, \"assets\"), to: resolve(webRoot, \"public\", \"ds-assets\") },\n+];\n+\n+for (const { from, to } of targets) {\n+  rmSync(to, { recursive: true, force: true });\n+  cpSync(from, to, { recursive: true });\n+}\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}