refactor: update setup process with enhanced user prompts and virtual environment management

- Removed the clear_progress function as it was no longer needed.
- Added user prompts to skip Supabase database setup during installation.
- Implemented checks for existing virtual environments and created one if absent.
- Updated instructions for starting backend services to use 'python' instead of 'uv'.
- Enhanced test cases to cover new setup functionalities and user interactions.
This commit is contained in:
sharath 2025-06-13 22:20:33 +00:00
parent 56c6ed9fd8
commit fbeb95888d
No known key found for this signature in database
3 changed files with 240 additions and 48 deletions

View File

@ -178,12 +178,6 @@ def load_progress():
return {"step": 0, "data": {}}
def clear_progress():
"""Removes the progress file."""
if os.path.exists(PROGRESS_FILE):
os.remove(PROGRESS_FILE)
# --- Validators ---
def validate_url(url, allow_empty=False):
"""Validates a URL format."""
@ -313,7 +307,6 @@ class SetupWizard:
self.run_step(11, self.start_suna)
self.final_instructions()
clear_progress()
except KeyboardInterrupt:
print("\n\nSetup interrupted. Your progress has been saved.")
@ -798,6 +791,26 @@ class SetupWizard:
"""Links the project to Supabase and pushes database migrations."""
print_step(9, self.total_steps, "Setting up Supabase Database")
print_info(
"This step will link your project to Supabase and push database migrations."
)
print_info(
"You can skip this if you've already set up your database or prefer to do it manually."
)
skip_db_setup = (
input("Do you want to skip the database setup? (y/N): ").lower().strip()
)
if skip_db_setup == "y":
print_info("Skipping Supabase database setup.")
print_warning(
"Remember to manually set up your Supabase database with the required migrations."
)
print_info(
"You can find the migration files in the backend/supabase/migrations directory."
)
return
try:
subprocess.run(
["supabase", "--version"],
@ -809,6 +822,13 @@ class SetupWizard:
print_error(
"Supabase CLI not found. Install it from: https://supabase.com/docs/guides/cli"
)
print_info("You can skip this step and set up the database manually later.")
skip_due_to_cli = (
input("Skip database setup due to missing CLI? (y/N): ").lower().strip()
)
if skip_due_to_cli == "y":
print_info("Skipping Supabase database setup.")
return
sys.exit(1)
supabase_url = self.env_vars["supabase"]["SUPABASE_URL"]
@ -868,14 +888,33 @@ class SetupWizard:
print_success("Frontend dependencies installed.")
print_info("Installing backend dependencies with uv...")
# Assuming pyproject.toml is used by uv
# Check if a virtual environment already exists
venv_exists = os.path.exists(os.path.join("backend", ".venv"))
if not venv_exists:
print_info("Creating virtual environment...")
subprocess.run(
["uv", "venv"], cwd="backend", check=True, shell=IS_WINDOWS
)
print_success("Virtual environment created.")
# Install dependencies in the virtual environment
subprocess.run(
["uv", "pip", "install", "-r", "requirements.txt"],
cwd="backend",
check=True,
shell=IS_WINDOWS,
)
# Install the package itself in editable mode
subprocess.run(
["uv", "pip", "install", "-e", "."],
cwd="backend",
check=True,
shell=IS_WINDOWS,
)
print_success("Backend dependencies installed.")
print_success("Backend dependencies and package installed.")
except subprocess.SubprocessError as e:
print_error(f"Failed to install dependencies: {e}")
@ -952,13 +991,13 @@ class SetupWizard:
print(f"{Colors.CYAN} cd frontend && npm run dev{Colors.ENDC}")
print(f"\n{Colors.BOLD}3. Start Backend (in a new terminal):{Colors.ENDC}")
print(f"{Colors.CYAN} cd backend && uv run api.py{Colors.ENDC}")
print(f"{Colors.CYAN} cd backend && python run api.py{Colors.ENDC}")
print(
f"\n{Colors.BOLD}4. Start Background Worker (in a new terminal):{Colors.ENDC}"
)
print(
f"{Colors.CYAN} cd backend && uv run -m dramatiq run_agent_background{Colors.ENDC}"
f"{Colors.CYAN} cd backend && python run -m dramatiq run_agent_background{Colors.ENDC}"
)
print(

183
start.py
View File

@ -3,57 +3,180 @@
import subprocess
import sys
import platform
import os
import json
IS_WINDOWS = platform.system() == "Windows"
PROGRESS_FILE = ".setup_progress"
# --- ANSI Colors ---
class Colors:
HEADER = "\033[95m"
BLUE = "\033[94m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
def load_progress():
"""Loads the last saved step and data from setup."""
if os.path.exists(PROGRESS_FILE):
with open(PROGRESS_FILE, "r") as f:
try:
return json.load(f)
except (json.JSONDecodeError, KeyError):
return {"step": 0, "data": {}}
return {"step": 0, "data": {}}
def get_setup_method():
"""Gets the setup method chosen during setup."""
progress = load_progress()
return progress.get("data", {}).get("setup_method")
IS_WINDOWS = platform.system() == 'Windows'
def check_docker_compose_up():
result = subprocess.run(
["docker", "compose", "ps", "-q"],
capture_output=True,
text=True,
shell=IS_WINDOWS
shell=IS_WINDOWS,
)
return len(result.stdout.strip()) > 0
def print_manual_instructions():
"""Prints instructions for manually starting Suna services."""
print(f"\n{Colors.BLUE}{Colors.BOLD}🚀 Manual Startup Instructions{Colors.ENDC}\n")
print("To start Suna, you need to run these commands in separate terminals:\n")
print(f"{Colors.BOLD}1. Start Infrastructure (in project root):{Colors.ENDC}")
print(f"{Colors.CYAN} docker compose up redis rabbitmq -d{Colors.ENDC}\n")
print(f"{Colors.BOLD}2. Start Frontend (in a new terminal):{Colors.ENDC}")
print(f"{Colors.CYAN} cd frontend && npm run dev{Colors.ENDC}\n")
print(f"{Colors.BOLD}3. Start Backend (in a new terminal):{Colors.ENDC}")
print(f"{Colors.CYAN} cd backend && python run api.py{Colors.ENDC}\n")
print(f"{Colors.BOLD}4. Start Background Worker (in a new terminal):{Colors.ENDC}")
print(
f"{Colors.CYAN} cd backend && python run -m dramatiq run_agent_background{Colors.ENDC}\n"
)
print("Once all services are running, access Suna at: http://localhost:3000\n")
print(
f"{Colors.YELLOW}💡 Tip:{Colors.ENDC} You can use '{Colors.CYAN}./start.py{Colors.ENDC}' to start/stop the infrastructure services."
)
def main():
force = False
setup_method = get_setup_method()
if "--help" in sys.argv:
print("Usage: ./script.py [OPTION]")
print("Manage docker-compose services interactively")
print("Usage: ./start.py [OPTION]")
print("Manage Suna services based on your setup method")
print("\nOptions:")
print(" -f\tForce start containers without confirmation")
print(" --help\tShow this help message")
return
if "-f" in sys.argv:
force = True
print("Force awakened. Skipping confirmation.")
is_up = check_docker_compose_up()
# If setup hasn't been run or method is not determined, default to docker
if not setup_method:
print(
f"{Colors.YELLOW}⚠️ Setup method not detected. Run './setup.py' first or using Docker Compose as default.{Colors.ENDC}"
)
setup_method = "docker"
if is_up:
action = "stop"
msg = "🛑 Stop containers? [y/N] " # No default
else:
action = "start"
msg = "⚡ Start containers? [Y/n] " # Yes default
if setup_method == "manual":
# For manual setup, we only manage infrastructure services (redis, rabbitmq)
# and show instructions for the rest
print(f"{Colors.BLUE}{Colors.BOLD}Manual Setup Detected{Colors.ENDC}")
print("Managing infrastructure services (Redis, RabbitMQ)...\n")
if not force:
response = input(msg).strip().lower()
if action == "stop":
# Only proceed if user explicitly types 'y'
if response != "y":
print("Aborting.")
return
force = "-f" in sys.argv
if force:
print("Force awakened. Skipping confirmation.")
is_infra_up = subprocess.run(
["docker", "compose", "ps", "-q", "redis", "rabbitmq"],
capture_output=True,
text=True,
shell=IS_WINDOWS,
)
is_up = len(is_infra_up.stdout.strip()) > 0
if is_up:
action = "stop"
msg = "🛑 Stop infrastructure services? [y/N] "
else:
# Proceed unless user types 'n'
if response == "n":
print("Aborting.")
return
action = "start"
msg = "⚡ Start infrastructure services? [Y/n] "
if not force:
response = input(msg).strip().lower()
if action == "stop":
if response != "y":
print("Aborting.")
return
else:
if response == "n":
print("Aborting.")
return
if action == "stop":
subprocess.run(["docker", "compose", "down"], shell=IS_WINDOWS)
print(f"\n{Colors.GREEN}✅ Infrastructure services stopped.{Colors.ENDC}")
else:
subprocess.run(
["docker", "compose", "up", "redis", "rabbitmq", "-d"], shell=IS_WINDOWS
)
print(f"\n{Colors.GREEN}✅ Infrastructure services started.{Colors.ENDC}")
print_manual_instructions()
else: # docker setup
print(f"{Colors.BLUE}{Colors.BOLD}Docker Setup Detected{Colors.ENDC}")
print("Managing all Suna services with Docker Compose...\n")
force = "-f" in sys.argv
if force:
print("Force awakened. Skipping confirmation.")
is_up = check_docker_compose_up()
if is_up:
action = "stop"
msg = "🛑 Stop all Suna services? [y/N] "
else:
action = "start"
msg = "⚡ Start all Suna services? [Y/n] "
if not force:
response = input(msg).strip().lower()
if action == "stop":
if response != "y":
print("Aborting.")
return
else:
if response == "n":
print("Aborting.")
return
if action == "stop":
subprocess.run(["docker", "compose", "down"], shell=IS_WINDOWS)
print(f"\n{Colors.GREEN}✅ All Suna services stopped.{Colors.ENDC}")
else:
subprocess.run(["docker", "compose", "up", "-d"], shell=IS_WINDOWS)
print(f"\n{Colors.GREEN}✅ All Suna services started.{Colors.ENDC}")
print(f"{Colors.CYAN}🌐 Access Suna at: http://localhost:3000{Colors.ENDC}")
if action == "stop":
subprocess.run(["docker", "compose", "down"], shell=IS_WINDOWS)
else:
subprocess.run(["docker", "compose", "up", "-d"], shell=IS_WINDOWS)
if __name__ == "__main__":
main()

View File

@ -49,7 +49,7 @@ class TestSetupWizard(unittest.TestCase):
def test_01_choose_docker_setup(self):
"""Test choosing Docker setup method."""
with patch("builtins.input", return_value="1"), patch(
"install.load_progress", return_value={"step": 0, "data": {}}
"setup.load_progress", return_value={"step": 0, "data": {}}
):
wizard = setup.SetupWizard()
wizard.choose_setup_method()
@ -58,7 +58,7 @@ class TestSetupWizard(unittest.TestCase):
def test_02_choose_manual_setup(self):
"""Test choosing manual setup method."""
with patch("builtins.input", return_value="2"), patch(
"install.load_progress", return_value={"step": 0, "data": {}}
"setup.load_progress", return_value={"step": 0, "data": {}}
):
wizard = setup.SetupWizard()
wizard.choose_setup_method()
@ -67,7 +67,7 @@ class TestSetupWizard(unittest.TestCase):
def test_03_check_requirements_docker(self):
"""Test requirement checking for Docker setup."""
with patch(
"install.load_progress",
"setup.load_progress",
return_value={"step": 0, "data": {"setup_method": "docker"}},
), patch("subprocess.run") as mock_subprocess:
@ -111,7 +111,7 @@ class TestSetupWizard(unittest.TestCase):
def test_04_check_requirements_manual(self):
"""Test requirement checking for manual setup."""
with patch(
"install.load_progress",
"setup.load_progress",
return_value={"step": 0, "data": {"setup_method": "manual"}},
), patch("subprocess.run") as mock_subprocess:
@ -183,7 +183,7 @@ class TestSetupWizard(unittest.TestCase):
]
with patch("builtins.input", side_effect=user_inputs), patch(
"install.load_progress", return_value={"step": 0, "data": {}}
"setup.load_progress", return_value={"step": 0, "data": {}}
):
wizard = setup.SetupWizard()
# Ensure the supabase key exists in env_vars
@ -204,7 +204,7 @@ class TestSetupWizard(unittest.TestCase):
def test_06_configure_env_files(self):
"""Test environment file configuration."""
with patch("install.load_progress", return_value={"step": 0, "data": {}}):
with patch("setup.load_progress", return_value={"step": 0, "data": {}}):
wizard = setup.SetupWizard()
wizard.env_vars = {
"setup_method": "docker",
@ -239,7 +239,7 @@ class TestSetupWizard(unittest.TestCase):
"""Test that the wizard can resume from a saved step."""
saved_progress = {"step": 1, "data": {"setup_method": "docker"}}
with patch("install.load_progress", return_value=saved_progress):
with patch("setup.load_progress", return_value=saved_progress):
wizard = setup.SetupWizard()
# Verify it loaded correctly
@ -258,6 +258,36 @@ class TestSetupWizard(unittest.TestCase):
self.assertFalse(setup.validate_api_key(None))
self.assertTrue(setup.validate_api_key("", allow_empty=True))
def test_09_setup_supabase_database(self):
"""Test the setup_supabase_database method."""
# Create a test wizard instance
wizard = setup.SetupWizard()
wizard.env_vars['supabase'] = {'SUPABASE_URL': 'https://test.supabase.co', 'SUPABASE_ANON_KEY': 'test', 'SUPABASE_SERVICE_ROLE_KEY': 'test'}
# Mock the input function to return 'y' for skip
import builtins
original_input = builtins.input
def mock_input(prompt):
if 'skip' in prompt.lower():
print(f'Mock input: "{prompt}" -> "y"')
return 'y'
return original_input(prompt)
builtins.input = mock_input
# Test the method
try:
print("Testing setup_supabase_database with skip...")
wizard.setup_supabase_database()
print('Method completed successfully')
except SystemExit as e:
print(f'SystemExit called with code: {e.code}')
except Exception as e:
print(f'Exception: {e}')
import traceback
traceback.print_exc()
if __name__ == "__main__":
unittest.main(argv=["first-arg-is-ignored"], exit=False)