fix(web): cross-platform sync-assets + surface build errors on failure

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.
This commit is contained in:
ioannis 2026-05-14 15:46:54 -07:00 committed by Teknium
parent 19071529f6
commit 0854640537
3 changed files with 46 additions and 3 deletions

View file

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