from __future__ import annotations import argparse import json import shutil import subprocess import sys from dataclasses import dataclass from pathlib import Path from typing import Sequence ROOT_DIR = Path(__file__).resolve().parent.parent if str(ROOT_DIR) not in sys.path: sys.path.insert(0, str(ROOT_DIR)) from scripts.verify_site import verify_site DEFAULT_SITE_URL = "https://arabic-translator-mu.vercel.app" DIRECT_CLOUD_ENV_KEYS = ["ENABLE_DIRECT_CLOUD_TTS", "HF_API_TOKEN", "HF_TTS_MODEL", "DEFAULT_VOICE_ID"] VERCEL_COMMAND = shutil.which("vercel") or shutil.which("vercel.cmd") or "vercel" @dataclass class CommandResult: command: list[str] returncode: int stdout: str stderr: str def normalize_url(value: str) -> str: return value.rstrip("/") def looks_like_hf_space(url: str) -> bool: host = url.split("://", 1)[-1].split("/", 1)[0].lower() return host == "hf.space" or host.endswith(".hf.space") def validate_worker_url(worker_url: str) -> str: worker_url = normalize_url(worker_url.strip()) if not worker_url.startswith("https://"): raise ValueError("WORKER_BASE_URL must start with https://") if "localhost" in worker_url or "127.0.0.1" in worker_url: raise ValueError("WORKER_BASE_URL must be public, not localhost") if ".vercel.app" in worker_url: raise ValueError("WORKER_BASE_URL must point at the worker, not the Vercel site") if not looks_like_hf_space(worker_url): raise ValueError("WORKER_BASE_URL should be a Hugging Face Space URL ending in .hf.space") return worker_url def run_command(command: Sequence[str], input_text: str | None = None, check: bool = True) -> CommandResult: completed = subprocess.run( list(command), input=input_text, text=True, capture_output=True, ) result = CommandResult( command=list(command), returncode=completed.returncode, stdout=completed.stdout, stderr=completed.stderr, ) if check and completed.returncode != 0: message = completed.stderr.strip() or completed.stdout.strip() or f"command failed with {completed.returncode}" raise RuntimeError(f"{' '.join(command)} failed: {message}") return result def remove_vercel_env(name: str, environment: str) -> CommandResult: result = run_command([VERCEL_COMMAND, "env", "rm", name, environment, "--yes"], check=False) combined = f"{result.stdout}\n{result.stderr}" if result.returncode != 0 and "env_not_found" not in combined and "was not found" not in combined: message = result.stderr.strip() or result.stdout.strip() or f"command failed with {result.returncode}" raise RuntimeError(f"vercel env rm {name} failed: {message}") return result def add_vercel_env(name: str, value: str, environment: str) -> CommandResult: return run_command([VERCEL_COMMAND, "env", "add", name, environment], input_text=f"{value}\n") def configure_vercel_worker( worker_url: str, site_url: str = DEFAULT_SITE_URL, code: str = "1234", environment: str = "production", redeploy: bool = True, verify: bool = False, timeout: float = 60, ) -> dict[str, object]: worker_url = validate_worker_url(worker_url) site_url = normalize_url(site_url) commands: list[CommandResult] = [] commands.append(remove_vercel_env("WORKER_BASE_URL", environment)) commands.append(add_vercel_env("WORKER_BASE_URL", worker_url, environment)) for key in DIRECT_CLOUD_ENV_KEYS: commands.append(remove_vercel_env(key, environment)) if redeploy: commands.append(run_command([VERCEL_COMMAND, "deploy", "--prod", "--yes"])) site_checks = None if verify: site_checks = [check.__dict__ for check in verify_site(site_url, code, worker_url, timeout=timeout)] return { "workerUrl": worker_url, "siteUrl": site_url, "environment": environment, "redeployed": redeploy, "verified": verify, "commands": [ { "command": result.command, "returncode": result.returncode, "stdout": result.stdout, "stderr": result.stderr, } for result in commands ], "siteChecks": site_checks, "nextCommand": ( f"python scripts\\prove_live_deployment.py {worker_url} --origin {site_url} " f"--code {code} --smoke-ocr-engine arabic" ), } def main_with_args(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description="Set Vercel WORKER_BASE_URL, redeploy, and optionally verify the site.") parser.add_argument("worker_url", help="Public Hugging Face Space worker URL, e.g. https://user-space.hf.space") parser.add_argument("--site-url", default=DEFAULT_SITE_URL) parser.add_argument("--code", default="1234") parser.add_argument("--environment", default="production") parser.add_argument("--no-redeploy", action="store_true") parser.add_argument("--verify", action="store_true", help="Run scripts.verify_site after redeploy.") parser.add_argument("--timeout", type=float, default=60) parser.add_argument("--json", action="store_true") args = parser.parse_args(argv) result = configure_vercel_worker( args.worker_url, site_url=args.site_url, code=args.code, environment=args.environment, redeploy=not args.no_redeploy, verify=args.verify, timeout=args.timeout, ) if args.json: print(json.dumps(result, indent=2)) else: print(f"Configured WORKER_BASE_URL={result['workerUrl']}") if result["redeployed"]: print("Redeployed Vercel production.") print("Next:") print(result["nextCommand"]) site_checks = result.get("siteChecks") if site_checks is not None and not all(check["ok"] for check in site_checks): return 1 return 0 def main() -> int: return main_with_args() if __name__ == "__main__": raise SystemExit(main())