diff --git a/README.md b/README.md index 598622d..28a8ee4 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,6 @@ If you plan to contribute, read [CONTRIBUTING.md](CONTRIBUTING.md). - [UV](https://astral.sh/uv) package manager: `curl -LsSf https://astral.sh/uv/install.sh | sh` - MeshCore radio connected via USB serial, TCP, or BLE -If you are on a low-resource system and do not want to build the frontend locally, download the release zip named `remoteterm-prebuilt-frontend-vX.X.X-.zip`. That bundle includes `frontend/prebuilt`, so you can run the app without doing a frontend build from source. -
Finding your serial port @@ -111,6 +109,8 @@ uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 The release bundle includes `frontend/prebuilt`, so it does not require a local frontend build. +Alternatively, if you have already cloned the repo, you can fetch just the prebuilt frontend into your working tree without downloading the full release zip via `python3 scripts/fetch_prebuilt_frontend.py`. + ## Path 2: Docker > **Warning:** Docker has had reports intermittent issues with serial event subscriptions. The native method above is more reliable. diff --git a/scripts/fetch_prebuilt_frontend.py b/scripts/fetch_prebuilt_frontend.py new file mode 100755 index 0000000..e775949 --- /dev/null +++ b/scripts/fetch_prebuilt_frontend.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +fetch_prebuilt_frontend.py + +Downloads the latest prebuilt frontend artifact from the GitHub releases page +and installs it into frontend/prebuilt/ so the backend can serve it directly. + +No GitHub CLI or authentication required — uses only the public releases API +and browser_download_url. Requires only the Python standard library. +""" + +import json +import shutil +import sys +import urllib.request +import zipfile +from pathlib import Path + +REPO = "jkingsman/Remote-Terminal-for-MeshCore" +API_URL = f"https://api.github.com/repos/{REPO}/releases/latest" +PREBUILT_PREFIX = "Remote-Terminal-for-MeshCore/frontend/prebuilt/" + +SCRIPT_DIR = Path(__file__).resolve().parent +PREBUILT_DIR = SCRIPT_DIR.parent / "frontend" / "prebuilt" + + +def fetch_json(url: str) -> dict: + req = urllib.request.Request(url, headers={"Accept": "application/vnd.github+json"}) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read()) + + +def find_prebuilt_asset(release: dict) -> tuple[str, str, str]: + """Return (tag_name, asset_name, download_url) for the prebuilt zip.""" + tag = release.get("tag_name", "") + for asset in release.get("assets", []): + name = asset.get("name", "") + if name.startswith("remoteterm-prebuilt-frontend-") and name.endswith(".zip"): + return tag, name, asset["browser_download_url"] + raise SystemExit( + f"No prebuilt frontend artifact found in the latest release.\n" + f"Check https://github.com/{REPO}/releases for available assets." + ) + + +def download(url: str, dest: Path) -> None: + with urllib.request.urlopen(url) as resp, open(dest, "wb") as f: + shutil.copyfileobj(resp, f) + + +def extract_prebuilt(zip_path: Path, dest: Path) -> int: + with zipfile.ZipFile(zip_path) as zf: + members = [m for m in zf.namelist() if m.startswith(PREBUILT_PREFIX)] + if not members: + raise SystemExit(f"'{PREBUILT_PREFIX}' not found inside zip.") + + if dest.exists(): + shutil.rmtree(dest) + dest.mkdir(parents=True) + + for member in members: + rel = member[len(PREBUILT_PREFIX):] + if not rel: + continue + target = dest / rel + if member.endswith("/"): + target.mkdir(parents=True, exist_ok=True) + else: + target.parent.mkdir(parents=True, exist_ok=True) + with zf.open(member) as src, open(target, "wb") as dst: + shutil.copyfileobj(src, dst) + + return len(members) + + +def main() -> None: + print("Fetching latest release info...") + release = fetch_json(API_URL) + tag, asset_name, download_url = find_prebuilt_asset(release) + print(f" Release : {tag}") + print(f" Asset : {asset_name}") + print() + + zip_path = PREBUILT_DIR.parent / asset_name + try: + print(f"Downloading {asset_name}...") + download(download_url, zip_path) + + print("Extracting prebuilt frontend...") + count = extract_prebuilt(zip_path, PREBUILT_DIR) + print(f"Extracted {count} entries.") + finally: + zip_path.unlink(missing_ok=True) + + print() + print(f"Done! Prebuilt frontend ({tag}) installed to frontend/prebuilt/") + print("Start the server with:") + print(" uv run uvicorn app.main:app --host 0.0.0.0 --port 8000") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\nAborted.", file=sys.stderr) + sys.exit(1)