diff --git a/skyvern/cli/commands.py b/skyvern/cli/commands.py index 897fff5e..2387f2d1 100644 --- a/skyvern/cli/commands.py +++ b/skyvern/cli/commands.py @@ -1,6 +1,8 @@ import typer from dotenv import load_dotenv +from skyvern.utils.env_paths import resolve_backend_env_path + from .docs import docs_app from .init_command import init, init_browser from .quickstart import quickstart_app @@ -54,5 +56,5 @@ def init_browser_command() -> None: if __name__ == "__main__": # pragma: no cover - manual CLI invocation - load_dotenv() + load_dotenv(resolve_backend_env_path()) cli_app() diff --git a/skyvern/cli/init_command.py b/skyvern/cli/init_command.py index b5fb9c27..d7bb6684 100644 --- a/skyvern/cli/init_command.py +++ b/skyvern/cli/init_command.py @@ -1,5 +1,4 @@ import asyncio -import os import subprocess import uuid @@ -10,6 +9,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn from rich.prompt import Confirm, Prompt from skyvern.utils import migrate_db +from skyvern.utils.env_paths import resolve_backend_env_path from .browser import setup_browser_config from .console import console @@ -50,8 +50,9 @@ def init( else: console.print("[red]Failed to generate local organization API key. Please check server logs.[/red]") - if os.path.exists(".env"): - console.print("šŸ’” [.env] file already exists.", style="yellow") + backend_env_path = resolve_backend_env_path() + if backend_env_path.exists(): + console.print(f"šŸ’” [{backend_env_path}] file already exists.", style="yellow") redo_llm_setup = Confirm.ask( "Do you want to go through [bold yellow]LLM provider setup again[/bold yellow]?", default=False, @@ -108,7 +109,7 @@ def init( analytics_id = analytics_id_input if analytics_id_input else str(uuid.uuid4()) update_or_add_env_var("ANALYTICS_ID", analytics_id) update_or_add_env_var("SKYVERN_API_KEY", api_key) - console.print("āœ… [green].env file has been initialized.[/green]") + console.print(f"āœ… [green]{resolve_backend_env_path()} file has been initialized.[/green]") if Confirm.ask("\nWould you like to [bold yellow]configure the MCP server[/bold yellow]?", default=True): setup_mcp() diff --git a/skyvern/cli/llm_setup.py b/skyvern/cli/llm_setup.py index da30d612..62a5cd35 100644 --- a/skyvern/cli/llm_setup.py +++ b/skyvern/cli/llm_setup.py @@ -1,15 +1,15 @@ -from pathlib import Path - from dotenv import load_dotenv, set_key from rich.panel import Panel from rich.prompt import Confirm, Prompt +from skyvern.utils.env_paths import resolve_backend_env_path + from .console import console def update_or_add_env_var(key: str, value: str) -> None: """Update or add environment variable in .env file.""" - env_path = Path(".env") + env_path = resolve_backend_env_path() if not env_path.exists(): env_path.touch() defaults = { diff --git a/skyvern/cli/mcp.py b/skyvern/cli/mcp.py index 1aa25741..52ed010c 100644 --- a/skyvern/cli/mcp.py +++ b/skyvern/cli/mcp.py @@ -11,6 +11,7 @@ from skyvern.forge import app from skyvern.forge.sdk.db.enums import OrganizationAuthTokenType from skyvern.library import Skyvern from skyvern.utils import detect_os, get_windows_appdata_roaming +from skyvern.utils.env_paths import resolve_backend_env_path from .console import console @@ -132,12 +133,13 @@ def setup_claude_desktop_config(host_system: str, path_to_env: str) -> bool: with open(path_claude_config, "w") as f: json.dump({"mcpServers": {}}, f, indent=2) - load_dotenv(".env") + backend_env_path = resolve_backend_env_path() + load_dotenv(backend_env_path) skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "") skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "") if not skyvern_base_url or not skyvern_api_key: console.print( - f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Claude MCP. Please open {path_claude_config} and set these variables manually.[/red]" + f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in {backend_env_path} to set up Claude MCP. Please open {path_claude_config} and set these variables manually.[/red]" ) return False @@ -184,12 +186,13 @@ def setup_cursor_config(host_system: str, path_to_env: str) -> bool: with open(path_cursor_config, "w") as f: json.dump({"mcpServers": {}}, f, indent=2) - load_dotenv(".env") + backend_env_path = resolve_backend_env_path() + load_dotenv(backend_env_path) skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "") skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "") if not skyvern_base_url or not skyvern_api_key: console.print( - f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Cursor MCP. Please open [link]{path_cursor_config}[/link] and set these variables manually.[/red]" + f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in {backend_env_path} to set up Cursor MCP. Please open [link]{path_cursor_config}[/link] and set these variables manually.[/red]" ) return False @@ -228,12 +231,13 @@ def setup_windsurf_config(host_system: str, path_to_env: str) -> bool: return False path_windsurf_config = get_windsurf_config_path(host_system) - load_dotenv(".env") + backend_env_path = resolve_backend_env_path() + load_dotenv(backend_env_path) skyvern_base_url = os.environ.get("SKYVERN_BASE_URL", "") skyvern_api_key = os.environ.get("SKYVERN_API_KEY", "") if not skyvern_base_url or not skyvern_api_key: console.print( - f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in .env file to set up Windsurf MCP. Please open {path_windsurf_config} and set these variables manually.[/red]" + f"[red]Error: SKYVERN_BASE_URL and SKYVERN_API_KEY must be set in {backend_env_path} to set up Windsurf MCP. Please open {path_windsurf_config} and set these variables manually.[/red]" ) try: diff --git a/skyvern/cli/run_commands.py b/skyvern/cli/run_commands.py index c8e2bd22..b6d74f29 100644 --- a/skyvern/cli/run_commands.py +++ b/skyvern/cli/run_commands.py @@ -2,7 +2,6 @@ import asyncio import os import shutil import subprocess -from pathlib import Path from typing import Any, List import psutil @@ -17,6 +16,7 @@ from skyvern.cli.utils import start_services from skyvern.config import settings from skyvern.library.skyvern import Skyvern from skyvern.utils import detect_os +from skyvern.utils.env_paths import resolve_backend_env_path, resolve_frontend_env_path from .console import console @@ -83,8 +83,7 @@ def kill_pids(pids: List[int]) -> None: @run_app.command(name="server") def run_server() -> None: """Run the Skyvern API server.""" - load_dotenv() - load_dotenv(".env") + load_dotenv(resolve_backend_env_path()) from skyvern.config import settings # noqa: PLC0415 port = settings.PORT @@ -117,68 +116,27 @@ def run_ui() -> None: except Exception as e: # pragma: no cover - CLI safeguards console.print(f"[red]Error checking for process: {e}[/red]") - # Try multiple methods to find the frontend directory - frontend_dir = None - - # Method 1: Relative to current working directory - cwd_frontend = Path.cwd() / "skyvern-frontend" - if cwd_frontend.exists(): - frontend_dir = cwd_frontend - - # Method 2: Relative to the module file (original method) - if frontend_dir is None: - module_based_frontend = Path(__file__).parent.parent.parent / "skyvern-frontend" - if module_based_frontend.exists(): - frontend_dir = module_based_frontend - - # Method 3: Search up the directory tree from current working directory - if frontend_dir is None: - current = Path.cwd() - while current != current.parent: # Stop at filesystem root - candidate = current / "skyvern-frontend" - if candidate.exists() and candidate.is_dir(): - frontend_dir = candidate - break - current = current.parent - - if frontend_dir is None: - console.print( - f"[bold red]ERROR: Skyvern Frontend directory not found. Searched in:[/bold red]\n" - f" • {cwd_frontend}\n" - f" • {Path(__file__).parent.parent.parent / 'skyvern-frontend'}\n" - f" • Parent directories of {Path.cwd()}\n" - f"[bold red]Are you in the right repo?[/bold red]" - ) + frontend_env_path = resolve_frontend_env_path() + if frontend_env_path is None: + console.print("[bold red]ERROR: Skyvern Frontend directory not found.[/bold red]") return - frontend_env_path = frontend_dir / ".env" + frontend_dir = frontend_env_path.parent if not frontend_env_path.exists(): console.print("[bold blue]Setting up frontend .env file...[/bold blue]") shutil.copy(frontend_dir / ".env.example", frontend_env_path) console.print("āœ… [green]Successfully set up frontend .env file[/green]") - # Look for .env file in multiple locations - main_env_path = None - env_search_paths = [ - Path.cwd() / ".env", - frontend_dir.parent / ".env", - Path(__file__).parent.parent.parent / ".env", - ] - - for env_path in env_search_paths: - if env_path.exists(): - main_env_path = env_path - break - - if main_env_path and main_env_path.exists(): - load_dotenv(main_env_path) + backend_env_path = resolve_backend_env_path() + if backend_env_path.exists(): + load_dotenv(backend_env_path) skyvern_api_key = os.getenv("SKYVERN_API_KEY") if skyvern_api_key: - set_key(str(frontend_env_path), "VITE_SKYVERN_API_KEY", skyvern_api_key) + set_key(frontend_env_path, "VITE_SKYVERN_API_KEY", skyvern_api_key) else: console.print("[red]ERROR: SKYVERN_API_KEY not found in .env file[/red]") else: - console.print("[red]ERROR: .env file not found[/red]") + console.print(f"[red]ERROR: Backend .env file not found at {backend_env_path}[/red]") os.chdir(frontend_dir) diff --git a/skyvern/cli/tasks.py b/skyvern/cli/tasks.py index 823eae86..14050db1 100644 --- a/skyvern/cli/tasks.py +++ b/skyvern/cli/tasks.py @@ -11,6 +11,7 @@ from rich.panel import Panel from skyvern.client import Skyvern from skyvern.config import settings +from skyvern.utils.env_paths import resolve_backend_env_path from .console import console @@ -33,8 +34,7 @@ def tasks_callback( def _get_client(api_key: str | None = None) -> Skyvern: """Instantiate a Skyvern SDK client using environment variables.""" - load_dotenv() - load_dotenv(".env") + load_dotenv(resolve_backend_env_path()) key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key) diff --git a/skyvern/cli/utils.py b/skyvern/cli/utils.py index 1ebc0e89..f17a6c8d 100644 --- a/skyvern/cli/utils.py +++ b/skyvern/cli/utils.py @@ -5,6 +5,7 @@ import sys import typer from skyvern.cli.console import console +from skyvern.utils.env_paths import resolve_backend_env_path async def start_services(server_only: bool = False) -> None: @@ -28,7 +29,7 @@ async def start_services(server_only: bool = False) -> None: console.print("\nšŸŽ‰ [bold green]Skyvern is now running![/bold green]") console.print("🌐 [bold]Access the UI at:[/bold] [cyan]http://localhost:8080[/cyan]") - console.print("šŸ”‘ [bold]Your API key is in your .env file as SKYVERN_API_KEY[/bold]") + console.print(f"šŸ”‘ [bold]Your API key is in {resolve_backend_env_path()} as SKYVERN_API_KEY[/bold]") # Wait for processes to complete (they won't unless killed) if not server_only: diff --git a/skyvern/cli/workflow.py b/skyvern/cli/workflow.py index 7e307a1a..abb83446 100644 --- a/skyvern/cli/workflow.py +++ b/skyvern/cli/workflow.py @@ -11,6 +11,7 @@ from rich.panel import Panel from skyvern.client import Skyvern from skyvern.config import settings +from skyvern.utils.env_paths import resolve_backend_env_path from .console import console from .tasks import _list_workflow_tasks @@ -34,8 +35,7 @@ def workflow_callback( def _get_client(api_key: str | None = None) -> Skyvern: """Instantiate a Skyvern SDK client using environment variables.""" - load_dotenv() - load_dotenv(".env") + load_dotenv(resolve_backend_env_path()) key = api_key or os.getenv("SKYVERN_API_KEY") or settings.SKYVERN_API_KEY return Skyvern(base_url=settings.SKYVERN_BASE_URL, api_key=key) diff --git a/skyvern/config.py b/skyvern/config.py index b90450ff..1c8a7a31 100644 --- a/skyvern/config.py +++ b/skyvern/config.py @@ -2,10 +2,22 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from skyvern import constants from skyvern.constants import SKYVERN_DIR +from skyvern.utils.env_paths import resolve_backend_env_path + +# NOTE: _DEFAULT_ENV_FILES resolves .env paths at import time and assumes +# the process has changed dir to the desired project root by this time. +# Even if we were to resolve paths at instantiation time, the global `settings` +# singleton instantiation at the bottom of this file also runs at import time +# and relies on the same assumption. +_DEFAULT_ENV_FILES = ( + resolve_backend_env_path(".env"), + resolve_backend_env_path(".env.staging"), + resolve_backend_env_path(".env.prod"), +) class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file=(".env", ".env.staging", ".env.prod"), extra="ignore") + model_config = SettingsConfigDict(env_file=_DEFAULT_ENV_FILES, extra="ignore") # settings for experimentation ENABLE_EXP_ALL_TEXTUAL_ELEMENTS_INTERACTABLE: bool = False diff --git a/skyvern/library/skyvern.py b/skyvern/library/skyvern.py index a7851ce2..d60910a4 100644 --- a/skyvern/library/skyvern.py +++ b/skyvern/library/skyvern.py @@ -1,5 +1,4 @@ import asyncio -import os import typing from typing import Any @@ -28,6 +27,7 @@ from skyvern.schemas.run_blocks import CredentialType from skyvern.schemas.runs import CUA_ENGINES, ProxyLocation, RunEngine, RunStatus, RunType from skyvern.services import run_service, task_v1_service, task_v2_service from skyvern.utils import migrate_db +from skyvern.utils.env_paths import resolve_backend_env_path class Skyvern(AsyncSkyvern): @@ -54,10 +54,11 @@ class Skyvern(AsyncSkyvern): httpx_client=httpx_client, ) if base_url is None and api_key is None: - if not os.path.exists(".env"): + env_path = resolve_backend_env_path() + if not env_path.exists(): raise Exception("No .env file found. Please run 'skyvern init' first to set up your environment.") - load_dotenv(".env") + load_dotenv(env_path) migrate_db() self._api_key = api_key diff --git a/skyvern/utils/env_paths.py b/skyvern/utils/env_paths.py new file mode 100644 index 00000000..4e86cd41 --- /dev/null +++ b/skyvern/utils/env_paths.py @@ -0,0 +1,46 @@ +from pathlib import Path +from typing import Optional + +BACKEND_ENV_DEFAULT = ".env" +FRONTEND_DIRNAME = "skyvern-frontend" +FRONTEND_ENV_FILENAME = ".env" + + +def resolve_backend_env_path( + basename: str = BACKEND_ENV_DEFAULT, +) -> Path: + """Return the backend env file path inside the current working directory.""" + + env_path = Path.cwd() / basename + + return env_path + + +def resolve_frontend_env_path() -> Optional[Path]: + """Return the path to the frontend .env file (may not exist).""" + + frontend_root: Optional[Path] = None + + if frontend_root is None: + cwd_frontend = Path.cwd() / FRONTEND_DIRNAME + if cwd_frontend.exists() and cwd_frontend.is_dir(): + frontend_root = cwd_frontend + + if frontend_root is None: + module_based_frontend = Path(__file__).resolve().parent.parent.parent / FRONTEND_DIRNAME + if module_based_frontend.exists() and module_based_frontend.is_dir(): + frontend_root = module_based_frontend + + if frontend_root is None: + for parent in Path.cwd().parents: + candidate = parent / FRONTEND_DIRNAME + if candidate.exists() and candidate.is_dir(): + frontend_root = candidate + break + + if frontend_root is None: + return None + + env_path = frontend_root / FRONTEND_ENV_FILENAME + + return env_path