From 0854640537ea1a33b785b142d41e71c6e726cf2a Mon Sep 17 00:00:00 2001 From: ioannis Date: Thu, 14 May 2026 15:46:54 -0700 Subject: [PATCH] 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 Three Windows-only bugs in the web-dashboard build path. Each is small, scoped, and verified end-to-end on Windows 11 — including under a stock cmd.exe / PowerShell console with its default cp1252 encoding. 1. `sync-assets` shells out to Unix-only commands web/package.json hard-codes `rm -rf … && cp -r …`. Neither exists on Windows cmd.exe. `hermes_cli/main.py::_build_web_ui` runs npm via subprocess (which on Windows defaults to cmd.exe), so the prebuild hook crashed before Vite ever ran and the dashboard never built. Fix: web/scripts/sync-assets.mjs — ~20 lines of Node using fs.rmSync + fs.cpSync (stdlib, Node >= 16.7). No new deps, identical behavior on POSIX and Windows. 2. Build failures were silent _build_web_ui ran both subprocess calls with capture_output=True and never relayed the captured buffers on failure. Users saw 'Web UI build failed' and nothing else — no stdout, no stderr, no hint that the real problem was 'rm is not recognized'. Fix: inner _relay() helper that decodes and prints stdout + stderr (utf-8, errors='replace') whenever a step returns non-zero. Replaces the existing stderr_tail-only relay on the build path; success path is unchanged. (stderr_tail is preserved for the stale-dist fallback branch added by #23817.) Salvaged from #13368 by @johnisag onto current main. Conflict resolution preserves main's improvements: - _run_npm_install_deterministic() (replaces bare subprocess.run for npm install) - npm-build retry-after-sleep for Windows boot-time races (#23817) - stale-dist fallback for non-interactive callers (#23817) Closes #25073, #13368. --- hermes_cli/main.py | 20 ++++++++++++++++++-- web/package.json | 2 +- web/scripts/sync-assets.mjs | 27 +++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 web/scripts/sync-assets.mjs diff --git a/hermes_cli/main.py b/hermes_cli/main.py index 214a1855b30..3c027e908c5 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -5688,12 +5688,29 @@ 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...") + + 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: + print(text) + r1 = _run_npm_install_deterministic(npm, web_dir, extra_args=("--silent",)) 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 @@ -5739,8 +5756,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)") ) - if stderr_tail: - print(f" Build error:\n {stderr_tail}") + _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 56262ff2a82..50456076b64 100644 --- a/web/package.json +++ b/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", diff --git a/web/scripts/sync-assets.mjs b/web/scripts/sync-assets.mjs new file mode 100644 index 00000000000..19b0bafb6aa --- /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 }); +}