#!/usr/bin/env python3 """Odysseus — first-time setup script. Creates data directories, initializes the database, and sets up an initial admin user. Safe to re-run (skips what already exists). """ import os import platform import shutil import subprocess import sys BASE_DIR = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, BASE_DIR) from src.constants import ( DATA_DIR, AUTH_FILE, UPLOAD_DIR, PERSONAL_DIR, PERSONAL_UPLOADS_DIR, TTS_CACHE_DIR, GENERATED_IMAGES_DIR, DEEP_RESEARCH_DIR, CHROMA_DIR, RAG_DIR, MEMORY_VECTORS_DIR, ) DIRS = [ DATA_DIR, UPLOAD_DIR, PERSONAL_DIR, PERSONAL_UPLOADS_DIR, TTS_CACHE_DIR, GENERATED_IMAGES_DIR, DEEP_RESEARCH_DIR, CHROMA_DIR, RAG_DIR, MEMORY_VECTORS_DIR, os.path.join(BASE_DIR, "logs"), ] def create_dirs(): for d in DIRS: os.makedirs(d, exist_ok=True) print(f" [ok] {os.path.relpath(d, BASE_DIR)}/") def init_database(): """Create all SQLAlchemy tables.""" sys.path.insert(0, BASE_DIR) os.environ.setdefault("DATABASE_URL", f"sqlite:///{os.path.join(DATA_DIR, 'app.db')}") from core.database import Base, engine Base.metadata.create_all(bind=engine) print(" [ok] Database initialized") def _prompt_admin_credentials(): """Interactively ask for admin username and password when running in a terminal.""" import getpass print() print(" Set up your admin account:") print(" (Press Enter to accept defaults)") print() username = input(" Username [admin]: ").strip().lower() if not username: username = "admin" while True: password = getpass.getpass(" Password: ") if not password: print(" Password cannot be empty.") continue confirm = getpass.getpass(" Confirm password: ") if password != confirm: print(" Passwords don't match. Try again.") continue break return username, password def create_default_admin(): """Create an initial admin user if none exists.""" auth_path = AUTH_FILE if os.path.exists(auth_path): print(" [skip] auth.json already exists") return "exists" try: import bcrypt import json # Priority: env vars > interactive prompt > random password username = os.getenv("ODYSSEUS_ADMIN_USER", "").strip().lower() password = os.getenv("ODYSSEUS_ADMIN_PASSWORD", "").strip() if username and password: # Both provided via env — use them directly pass elif sys.stdin.isatty() and not os.getenv("ODYSSEUS_SKIP_ADMIN_PROMPT"): # Interactive terminal — ask the user username, password = _prompt_admin_credentials() else: # Non-interactive (Docker, CI) — fall back to generated password username = username or "admin" password = password or __import__("secrets").token_urlsafe(18) username = username or "admin" hashed = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() auth_data = { "users": { username: { "password_hash": hashed, "is_admin": True, } } } with open(auth_path, "w", encoding="utf-8") as f: json.dump(auth_data, f, indent=2) if sys.stdin.isatty() and not os.getenv("ODYSSEUS_ADMIN_PASSWORD"): print(f" [ok] Admin account created ({username})") else: print(f" [ok] Initial admin user created ({username})") if not os.getenv("ODYSSEUS_ADMIN_PASSWORD"): print(f" Temporary password: {password}") print(f" ** Change it after first login. Set ODYSSEUS_ADMIN_PASSWORD to choose your own. **") return "created" except ImportError as e: if "incompatible architecture" in str(e).lower(): # bcrypt is present but built for the wrong CPU architecture — the # same Apple Silicon mismatch check_arch() guards against, caught here # for the rarer case of an x86 wheel inside an arm64 venv. print(" [error] bcrypt loaded with the wrong CPU architecture.") print(" Rebuild the venv with an arm64 Python:") print(" rm -rf venv && /opt/homebrew/bin/python3.11 -m venv venv") print(" ./venv/bin/pip install -r requirements.txt") return "skipped" print(" [warn] bcrypt not installed — skipping admin user creation") print(" Run: pip install bcrypt") return "skipped" def create_env(): """Copy .env.example to .env if it doesn't exist.""" env_path = os.path.join(BASE_DIR, ".env") example_path = os.path.join(BASE_DIR, ".env.example") if os.path.exists(env_path): print(" [skip] .env already exists") return if os.path.exists(example_path): import shutil shutil.copy2(example_path, env_path) print(" [ok] .env created from .env.example") print(" ** Edit .env with your LLM host and API keys **") else: print(" [warn] .env.example not found — create .env manually") def check_deps(): """Check for common missing dependencies.""" missing = [] for mod in ["fastapi", "uvicorn", "sqlalchemy", "bcrypt", "httpx", "dotenv"]: try: __import__(mod) except ImportError: missing.append(mod) if missing: print(f"\n [warn] Missing packages: {', '.join(missing)}") print(f" Run: pip install -r requirements.txt") else: print(" [ok] All core dependencies installed") if os.name != "nt" and shutil.which("tmux") is None: print("\n [warn] tmux not found") print(" Cookbook uses tmux for background downloads and model serves.") print(" Install it with your OS package manager, for example:") if sys.platform == "darwin": print(" brew install tmux") else: print(" sudo apt install tmux") print(" sudo pacman -S tmux") print(" sudo dnf install tmux") elif os.name != "nt": print(" [ok] tmux installed") def check_arch(): """Stop early, with guidance, if we're on Apple Silicon but running an Intel (x86_64) Python through Rosetta. A venv built with such an interpreter installs and loads compiled packages (bcrypt, pydantic-core, onnxruntime, …) for the wrong CPU architecture, then dies deep inside an import with a cryptic "(mach-o file, but is an incompatible architecture)" error. Catching it here turns that into one clear, actionable message. """ if sys.platform != "darwin" or platform.machine() == "arm64": return # Not macOS, or already an arm64-native interpreter — nothing to do. # platform.machine() == "x86_64": either a genuine Intel Mac (fine) or an x86 # interpreter running under Rosetta on Apple Silicon (the case we must catch). try: translated = subprocess.run( ["sysctl", "-n", "sysctl.proc_translated"], capture_output=True, text=True, timeout=5, ).stdout.strip() except Exception: translated = "" if translated != "1": return # Genuine Intel Mac — carry on. print("\n [error] This is an Apple Silicon Mac, but setup is running under an") print(" Intel (x86_64) Python through Rosetta. Compiled packages would") print(' load as the wrong architecture and crash with "incompatible') print(' architecture" later on.') print("\n Rebuild the environment with Homebrew's arm64 Python:") print(" brew install python@3.11 # if you don't have it yet") print(" rm -rf venv") print(" /opt/homebrew/bin/python3.11 -m venv venv") print(" ./venv/bin/pip install -r requirements.txt") print(" ./venv/bin/python setup.py") print("\n Tip: ./start-macos.sh does all of this with the right Python.\n") sys.exit(1) def main(): print("\n=== Odysseus Setup ===\n") # Fail fast with a clear message if the CPU architecture is wrong (Apple # Silicon under an x86/Rosetta Python) before importing anything native. check_arch() print("1. Creating directories...") create_dirs() print("\n2. Environment file...") create_env() print("\n3. Checking dependencies...") check_deps() print("\n4. Initializing database...") try: init_database() except Exception as e: print(f" [warn] Database init failed: {e}") print(" This is OK if dependencies aren't installed yet.") print("\n5. Creating initial admin...") admin_status = "failed" try: admin_status = create_default_admin() except Exception as e: print(f" [warn] Admin creation failed: {e}") admin_status = "failed" print("\n=== Setup complete ===") # start-macos.sh launches the server itself (on its own port) right after # this, so suppress the manual hint there to avoid a contradictory URL. if not os.getenv("ODYSSEUS_SKIP_RUN_HINT"): print(f"\nStart the server with:") print(f" python -m uvicorn app:app --host 127.0.0.1 --port 7000") print(f"\nThen open http://localhost:7000") # Cleaned, action-focused final instruction strings if admin_status == "created": print("Login with your admin credentials.\n") elif admin_status == "exists": print("Login with your existing admin credentials.\n") elif admin_status == "skipped": print("Admin creation did not happen: dependencies are missing.\nRun 'pip install bcrypt' and rerun setup.\n") elif admin_status == "failed": print("Admin creation did not happen: a system or file error occurred.\nCheck write permissions for the 'data' directory and rerun setup.\n") else: # handling "failed" or any unhandled edge case print("Admin creation did not happen: a system or file error occurred.\nCheck write permissions for the 'data' directory and rerun setup.\n") if __name__ == "__main__": main()