| 1 | """Build the embedded WebUI before packaging Echo Director Agent.""" |
| 2 | |
| 3 | from __future__ import annotations |
| 4 | |
| 5 | import shutil |
| 6 | import subprocess |
| 7 | from pathlib import Path |
| 8 | |
| 9 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface |
| 10 | |
| 11 | |
| 12 | class CustomBuildHook(BuildHookInterface): |
| 13 | """Ensure wheels and source distributions never contain a stale or missing WebUI.""" |
| 14 | |
| 15 | PLUGIN_NAME = "custom" |
| 16 | |
| 17 | def initialize(self, version: str, build_data: dict) -> None: |
| 18 | root = Path(self.root) |
| 19 | webui = root / "webui" |
| 20 | dist_index = root / "nanobot" / "web" / "dist" / "index.html" |
| 21 | |
| 22 | # A repository checkout contains the frontend lockfile, so build the |
| 23 | # embedded UI from its exact dependency graph. A wheel built from our |
| 24 | # sdist receives the already-built assets instead of the frontend |
| 25 | # sources and must not attempt another npm install. |
| 26 | if (webui / "package-lock.json").is_file(): |
| 27 | npm = shutil.which("npm") |
| 28 | if npm is None: |
| 29 | raise RuntimeError("Node.js and npm are required to build Echo Director Agent") |
| 30 | subprocess.run([npm, "ci"], cwd=webui, check=True) |
| 31 | subprocess.run([npm, "run", "build"], cwd=webui, check=True) |
| 32 | |
| 33 | if not dist_index.is_file(): |
| 34 | raise RuntimeError("WebUI build did not produce nanobot/web/dist/index.html") |
| 35 |