Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions hermes_cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4365,32 +4365,58 @@ def _build_web_ui(web_dir: Path, *, fatal: bool = False) -> bool:
return True
import shutil

# 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...")
r1 = subprocess.run([npm, "install", "--silent"], cwd=web_dir, capture_output=True)
_say("→ Building web UI...")

def _relay(result: "subprocess.CompletedProcess") -> None:
"""Print captured npm output so users can see *why* a step failed."""
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 = subprocess.run(
[npm, "install", "--silent"], cwd=web_dir, capture_output=True
)
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
r2 = subprocess.run([npm, "run", "build"], cwd=web_dir, capture_output=True)
if r2.returncode != 0:
print(
_say(
f" {'✗' if fatal else '⚠'} Web UI build failed"
+ ("" if fatal else " (hermes web will not be available)")
)
_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


Expand Down
2 changes: 1 addition & 1 deletion web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"sync-assets": "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",
"sync-assets": "node scripts/sync-assets.mjs",
"predev": "npm run sync-assets",
"prebuild": "npm run sync-assets",
"dev": "vite",
Expand Down
27 changes: 27 additions & 0 deletions web/scripts/sync-assets.mjs
Original file line number Diff line number Diff line change
@@ -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 });
}