#!/usr/bin/env python3 """ StudyCraft deployment script — HuggingFace Spaces & local Docker. Usage: uv run python scripts/deploy.py --target huggingface --setup Create a new HuggingFace Space (Docker SDK) for studycraft. uv run python scripts/deploy.py --target huggingface --deploy Push code to HuggingFace Spaces (uses hf-deploy branch, swaps README). uv run python scripts/deploy.py --target huggingface --secret Set OPENROUTER_API_KEY secret on the HuggingFace Space. uv run python scripts/deploy.py --target local Build and run locally with docker-compose up -d --build. uv run python scripts/deploy.py --target local --stop Stop local docker-compose deployment. Options: --space-name NAME Space name (default: studycraft) --org ORG HuggingFace organization/username --branch BRANCH Deploy branch name (default: hf-deploy) --secret-value VAL Secret value (if not provided, will prompt) """ import argparse import shutil import subprocess import sys from pathlib import Path RED = "\033[91m" GREEN = "\033[92m" YELLOW = "\033[93m" RESET = "\033[0m" DIM = "\033[2m" PROJECT_ROOT = Path(__file__).parent.parent HF_README = PROJECT_ROOT / "README.hf-spaces.md" MAIN_README = PROJECT_ROOT / "README.md" def run(cmd: list[str], check: bool = True) -> subprocess.CompletedProcess: """Run a command, echo it, and return the result.""" print(f"{DIM}$ {' '.join(cmd)}{RESET}") result = subprocess.run(cmd, cwd=PROJECT_ROOT) if check and result.returncode != 0: print(f"{RED}Command failed: {' '.join(cmd)}{RESET}") sys.exit(1) return result def _ensure_hf_cli() -> None: """Check that hf CLI is available.""" try: run(["hf", "--help"], check=False) except FileNotFoundError: print(f"{RED}Error: hf CLI not found. Install with:{RESET}") print(f" {DIM}pip install huggingface_hub{RESET}") sys.exit(1) def cmd_hf_setup(args: argparse.Namespace) -> None: """Create the HuggingFace Space if it doesn't exist.""" _ensure_hf_cli() space_name = args.space_name org = args.org or "YOUR_HF_USERNAME" repo_id = f"{org}/{space_name}" if org else space_name print(f"{YELLOW}Creating HuggingFace Space: {repo_id}{RESET}") print(f" SDK: docker") print(f" Port: 8000") # Use hf CLI to create the space # The CLI will prompt for confirmation if the space already exists cmd = [ "hf", "repo", "create", space_name, "--type", "space", "--space-sdk", "docker", ] if org: cmd.extend(["--organization", org]) try: run(cmd) print(f"{GREEN}✓ Space created: https://huggingface.co/spaces/{repo_id}{RESET}") except SystemExit: print(f"{YELLOW}Space may already exist. Continuing...{RESET}") print(f"\nNext steps:") print( f" 1. Set secrets: {DIM}hf env --repo-id {repo_id} set OPENROUTER_API_KEY {RESET}" ) print( f" 2. Deploy: {DIM}uv run python scripts/deploy.py --target huggingface --deploy{RESET}" ) def cmd_hf_deploy(args: argparse.Namespace) -> None: """Push code to HuggingFace Spaces via a dedicated deploy branch.""" _ensure_hf_cli() space_name = args.space_name org = args.org or "YOUR_HF_USERNAME" branch = args.branch repo_id = f"{org}/{space_name}" if org else space_name print(f"{YELLOW}Deploying to HuggingFace Space: {repo_id}{RESET}") print(f" Branch: {branch}") # 1. Check for uncommitted changes result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True) if result.stdout.strip(): print(f"{RED}Error: You have uncommitted changes. Commit or stash them first.{RESET}") sys.exit(1) # 2. Ensure HF README exists if not HF_README.exists(): print(f"{RED}Error: Missing {HF_README}{RESET}") print(f" This file should contain the YAML frontmatter for HF Spaces.") sys.exit(1) # 3. Create/update deploy branch current_branch = subprocess.run( ["git", "branch", "--show-current"], capture_output=True, text=True ).stdout.strip() # Check if branch exists branches = subprocess.run(["git", "branch", "-a"], capture_output=True, text=True).stdout if branch not in branches: print(f"{YELLOW}Creating deploy branch '{branch}'...{RESET}") run(["git", "checkout", "-b", branch]) else: print(f"{YELLOW}Switching to deploy branch '{branch}'...{RESET}") run(["git", "checkout", branch]) # 4. Replace README.md with HF version print(f"{YELLOW}Swapping README.md for HF Spaces version...{RESET}") backup = MAIN_README.with_suffix(".md.backup") MAIN_README.rename(backup) shutil.copy(HF_README, MAIN_README) run(["git", "add", "README.md"]) run(["git", "commit", "-m", "chore: use HF Spaces README for deployment"], check=False) # 5. Push to HF # Add remote if not present remote_name = "hf" remotes = subprocess.run(["git", "remote"], capture_output=True, text=True).stdout.split() if remote_name not in remotes: remote_url = f"https://huggingface.co/spaces/{repo_id}" print(f"{YELLOW}Adding remote '{remote_name}': {remote_url}{RESET}") run(["git", "remote", "add", remote_name, remote_url]) print(f"{YELLOW}Pushing to HuggingFace Spaces (branch:main)...{RESET}") run(["git", "push", remote_name, f"{branch}:main", "--force"]) # 6. Restore main branch README print(f"{YELLOW}Restoring original README...{RESET}") run(["git", "checkout", current_branch]) backup.rename(MAIN_README) print(f"\n{GREEN}✓ Deployment complete!{RESET}") print(f" https://huggingface.co/spaces/{repo_id}") print(f"\nTo set secrets:") print(f" {DIM}hf env --repo-id {repo_id} set OPENROUTER_API_KEY {RESET}") def cmd_hf_secret(args: argparse.Namespace) -> None: """Set a secret on the HuggingFace Space.""" _ensure_hf_cli() space_name = args.space_name org = args.org or "YOUR_HF_USERNAME" repo_id = f"{org}/{space_name}" if org else space_name secret_name = args.secret_name or "OPENROUTER_API_KEY" secret_value = args.secret_value if not secret_value: import getpass secret_value = getpass.getpass(f"Enter value for {secret_name}: ") print(f"{YELLOW}Setting secret '{secret_name}' on {repo_id}...{RESET}") run(["hf", "env", "--repo-id", repo_id, "set", secret_name, secret_value]) print(f"{GREEN}✓ Secret set{RESET}") def cmd_local(args: argparse.Namespace) -> None: """Build and run locally with docker-compose.""" print(f"{YELLOW}Local Docker Compose deployment{RESET}") compose_file = PROJECT_ROOT / "docker-compose.yml" if not compose_file.exists(): print(f"{RED}Error: {compose_file} not found{RESET}") sys.exit(1) if args.stop: print(f"{YELLOW}Stopping StudyCraft...{RESET}") run(["docker-compose", "-f", str(compose_file), "down"]) print(f"{GREEN}✓ Stopped{RESET}") return # Build and start print(f"{YELLOW}Building and starting StudyCraft...{RESET}") run(["docker-compose", "-f", str(compose_file), "up", "-d", "--build"]) print(f"{GREEN}✓ StudyCraft is running{RESET}") print(f" Web UI: {DIM}http://localhost:8000{RESET}") print(f" API docs: {DIM}http://localhost:8000/docs{RESET}") print(f"\nTo view logs:") print(f" {DIM}docker-compose -f {compose_file} logs -f{RESET}") print(f"\nTo stop:") print(f" {DIM}uv run python scripts/deploy.py --target local --stop{RESET}") def main() -> None: parser = argparse.ArgumentParser( description="Deploy StudyCraft to HuggingFace Spaces or local Docker.", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # One-time HF Space setup uv run python scripts/deploy.py --target huggingface --setup --org my-org # Deploy code to HF Spaces uv run python scripts/deploy.py --target huggingface --deploy # Set API key secret on HF Space uv run python scripts/deploy.py --target huggingface --secret # Deploy locally with docker-compose uv run python scripts/deploy.py --target local Note: Uses the 'hf' CLI (huggingface_hub). Install with: pip install huggingface_hub """, ) parser.add_argument( "--target", "-t", choices=["huggingface", "local"], required=True, help="Deployment target: huggingface (HF Spaces) or local (docker-compose)", ) # HuggingFace actions parser.add_argument( "--setup", action="store_true", help="Create the HuggingFace Space (one-time setup)", ) parser.add_argument( "--deploy", action="store_true", help="Push code to HuggingFace Spaces (requires hf-deploy branch workflow)", ) parser.add_argument( "--secret", action="store_true", help="Set a secret on the HuggingFace Space", ) parser.add_argument( "--secret-name", default="OPENROUTER_API_KEY", help="Secret name (default: OPENROUTER_API_KEY)", ) parser.add_argument( "--secret-value", help="Secret value (will prompt if not provided)", ) # Local actions parser.add_argument( "--stop", action="store_true", help="Stop local docker-compose deployment", ) # Common options parser.add_argument( "--space-name", default="studycraft", help="HuggingFace Space name (default: studycraft)", ) parser.add_argument( "--org", default=None, help="HuggingFace organization/username (default: your logged-in user)", ) parser.add_argument( "--branch", default="hf-deploy", help="Deploy branch name (default: hf-deploy)", ) args = parser.parse_args() if args.target == "huggingface": if args.setup: cmd_hf_setup(args) elif args.deploy: cmd_hf_deploy(args) elif args.secret: cmd_hf_secret(args) else: parser.error("Specify --setup, --deploy, or --secret for HuggingFace target") elif args.target == "local": cmd_local(args) if __name__ == "__main__": main()