diff --git a/arcade/arcade/cli/authn.py b/arcade/arcade/cli/authn.py
index 90ea5fa5..d2615b1b 100644
--- a/arcade/arcade/cli/authn.py
+++ b/arcade/arcade/cli/authn.py
@@ -7,8 +7,12 @@ from urllib.parse import parse_qs
import yaml
from rich.console import Console
-from arcade.cli.constants import LOGIN_FAILED_HTML, LOGIN_SUCCESS_HTML
-from arcade.cli.utils import create_new_env_file
+from arcade.cli.constants import (
+ ARCADE_CONFIG_PATH,
+ CREDENTIALS_FILE_PATH,
+ LOGIN_FAILED_HTML,
+ LOGIN_SUCCESS_HTML,
+)
console = Console()
@@ -56,16 +60,13 @@ class LoginCallbackHandler(BaseHTTPRequestHandler):
)
return False
- # ensure the ~/.arcade directory exists
- # TODO: this should use WORK_DIR from env if set
- arcade_dir = os.path.join(os.path.expanduser("~"), ".arcade")
- if not os.path.exists(arcade_dir):
- os.makedirs(arcade_dir, exist_ok=True)
+ # ensure the ARCADE_CONFIG_PATH directory exists
+ if not os.path.exists(ARCADE_CONFIG_PATH):
+ os.makedirs(ARCADE_CONFIG_PATH, exist_ok=True)
# TODO don't overwrite existing config
- config_file_path = os.path.join(arcade_dir, "credentials.yaml")
new_config = {"cloud": {"api": {"key": api_key}, "user": {"email": email}}}
- with open(config_file_path, "w") as f:
+ with open(CREDENTIALS_FILE_PATH, "w") as f:
yaml.dump(new_config, f)
# Send a success response to the browser
@@ -73,7 +74,7 @@ class LoginCallbackHandler(BaseHTTPRequestHandler):
f"""✅ Hi there, {email}!
Your Arcade API key is: {api_key}
-Stored in: {config_file_path}""",
+Stored in: {CREDENTIALS_FILE_PATH}""",
style="bold green",
)
return True
@@ -112,35 +113,34 @@ class LocalAuthCallbackServer:
self.httpd.shutdown()
-def check_existing_login() -> bool:
+def check_existing_login(suppress_message: bool = False) -> bool:
"""
Check if the user is already logged in by verifying the config file.
+ Args:
+ suppress_message (bool): If True, suppress the logged in message.
+
Returns:
bool: True if the user is already logged in, False otherwise.
"""
- # Create a new env file if one doesn't already exist
- create_new_env_file()
-
- config_file_path = os.path.expanduser("~/.arcade/credentials.yaml")
-
- if not os.path.exists(config_file_path):
+ if not os.path.exists(CREDENTIALS_FILE_PATH):
return False
- if os.path.exists(config_file_path):
+ if os.path.exists(CREDENTIALS_FILE_PATH):
try:
- with open(config_file_path) as f:
+ with open(CREDENTIALS_FILE_PATH) as f:
config: dict[str, Any] = yaml.safe_load(f)
cloud_config = config.get("cloud", {})
api_key = cloud_config.get("api", {}).get("key")
email = cloud_config.get("user", {}).get("email")
if api_key and email:
- console.print(f"You're already logged in as {email}. ", style="bold green")
+ if not suppress_message:
+ console.print(f"You're already logged in as {email}. ", style="bold green")
return True
except yaml.YAMLError:
console.print(
- f"Error: Invalid configuration file at {config_file_path}", style="bold red"
+ f"Error: Invalid configuration file at {CREDENTIALS_FILE_PATH}", style="bold red"
)
except Exception as e:
console.print(f"Error: Unable to read configuration file: {e!s}", style="bold red")
diff --git a/arcade/arcade/cli/constants.py b/arcade/arcade/cli/constants.py
index 811cf8d8..56783609 100644
--- a/arcade/arcade/cli/constants.py
+++ b/arcade/arcade/cli/constants.py
@@ -1,7 +1,15 @@
-DEFAULT_CLOUD_HOST = "cloud.arcade.dev"
-DEFAULT_ENGINE_HOST = "api.arcade.dev"
+import os
+
+PROD_CLOUD_HOST = "cloud.arcade.dev"
+PROD_ENGINE_HOST = "api.arcade.dev"
LOCALHOST = "localhost"
+# The path to the directory containing the Arcade configuration files. Typically ~/.arcade
+ARCADE_CONFIG_PATH = os.path.join(os.path.expanduser(os.getenv("ARCADE_WORK_DIR", "~")), ".arcade")
+
+# The path to the file containing the user's Arcade-related credentials (e.g., ARCADE_API_KEY).
+CREDENTIALS_FILE_PATH = os.path.join(ARCADE_CONFIG_PATH, "credentials.yaml")
+
_style_block = b"""
diff --git a/arcade/arcade/cli/launcher.py b/arcade/arcade/cli/launcher.py
index 74d5d827..bae9f952 100644
--- a/arcade/arcade/cli/launcher.py
+++ b/arcade/arcade/cli/launcher.py
@@ -26,7 +26,8 @@ known_engine_config_locations = [
if os.environ.get("HOMEBREW_REPOSITORY") is not None:
homebrew_home = os.path.join(os.environ["HOMEBREW_REPOSITORY"], "etc", "arcade-engine")
- known_engine_config_locations.append(homebrew_home)
+ if homebrew_home not in known_engine_config_locations:
+ known_engine_config_locations.append(homebrew_home)
def start_servers(
@@ -54,7 +55,7 @@ def start_servers(
engine_config = _get_config_file(engine_config, default_filename="engine.yaml")
# Ensure engine_env is provided or found and either way, validated
- env_file = _get_config_file(engine_env, default_filename="arcade.env", optional=True)
+ env_file = _get_config_file(engine_env, default_filename="engine.env", optional=True)
# Prepare command-line arguments for the worker server and engine
worker_cmd = _build_worker_command(worker_host, worker_port, debug)
@@ -113,7 +114,13 @@ def _get_config_file(
file_path: str | None, default_filename: str = "engine.yaml", optional: bool = False
) -> str | None:
"""
- Determines and validates the config file path.
+ Resolves and validates the config file path from a set of candidate locations.
+
+ If a file_path is provided, it is checked directly.
+ Otherwise, the following candidate locations are checked in order:
+ 1. Current working directory.
+ 2. User's home directory under .arcade.
+ 3. Known engine config locations.
Args:
file_path: Optional path provided by the user.
@@ -127,52 +134,47 @@ def _get_config_file(
RuntimeError: If the config file is not found and is not optional.
"""
if file_path:
- config_path = Path(os.path.expanduser(file_path)).resolve()
- if not config_path.is_file():
- console.print(f"❌ Config file not found at {config_path}", style="bold red")
- raise RuntimeError(f"Config file not found at {config_path}")
- return str(config_path)
+ candidate = Path(os.path.expanduser(file_path)).resolve()
+ if not candidate.is_file():
+ console.print(f"❌ Config file not found at {candidate}", style="bold red")
+ raise RuntimeError(f"Config file not found at {candidate}")
+ return str(candidate)
- # Look for the file in the current working directory
- config_path = Path(os.getcwd()) / default_filename
- if config_path.is_file():
- console.print(f"Using config file at {config_path}", style="bold green")
- return str(config_path)
+ # List of all config file path locations to check.
+ candidates = [
+ Path(os.getcwd()) / default_filename,
+ Path.home() / ".arcade" / default_filename,
+ ]
+ candidates.extend(Path(path) / default_filename for path in known_engine_config_locations)
- # Look for the file in the user's home directory under .arcade/
- home_config_path = Path.home() / ".arcade" / default_filename
- if home_config_path.is_file():
- console.print(f"Using config file at {home_config_path}", style="bold green")
- return str(home_config_path)
-
- # Look for known installation directories
- for path in known_engine_config_locations:
- etc_path = Path(path) / default_filename
- if etc_path.is_file():
- console.print(f"Using config file at {etc_path}", style="bold green")
- return str(etc_path)
+ # Find the first candidate that exists.
+ for candidate in candidates:
+ if candidate.is_file():
+ console.print(f"Using config file at {candidate}", style="bold green")
+ return str(candidate)
+ # No config file was found. Handle according to the optional flag.
if optional:
console.print(
- f"⚠️ Optional config file '{default_filename}' not found in either of the default locations: "
- f" 1) Current working directory: {Path.cwd() / default_filename}, or "
- f" 2) User's home directory: {Path.home() / '.arcade' / default_filename}.",
+ f"⚠️ Optional config file '{default_filename}' not found in any of the following locations:",
style="bold yellow",
)
+ for i, candidate in enumerate(candidates, start=1):
+ console.print(f" {i}) {candidate}", style="bold yellow")
return None
console.print(
- f"❌ Error: Required config file '{default_filename}' not found in any of the default locations:\n"
- f" 1) Current working directory: {Path.cwd() / default_filename}, or\n"
- f" 2) User's home directory: {Path.home() / '.arcade' / default_filename}\n",
+ f"❌ Error: Required config file '{default_filename}' not found in any of the following locations:",
style="bold red",
)
+ for i, candidate in enumerate(candidates, start=1):
+ console.print(f" {i}) {candidate}", style="bold red")
+
console.print(
- "TIP: Please install the Arcade Engine by following the instructions at:\n"
+ "\nTIP: Please install the Arcade Engine by following the instructions at:\n"
" https://docs.arcade.dev/home/install/local#install-the-engine\n",
style="bold green",
)
-
raise RuntimeError(f"Config file '{default_filename}' not found.")
diff --git a/arcade/arcade/cli/main.py b/arcade/arcade/cli/main.py
index 3a0ce8be..680eafe9 100644
--- a/arcade/arcade/cli/main.py
+++ b/arcade/arcade/cli/main.py
@@ -15,7 +15,12 @@ from rich.text import Text
from tqdm import tqdm
from arcade.cli.authn import LocalAuthCallbackServer, check_existing_login
-from arcade.cli.constants import DEFAULT_CLOUD_HOST, DEFAULT_ENGINE_HOST, LOCALHOST
+from arcade.cli.constants import (
+ CREDENTIALS_FILE_PATH,
+ LOCALHOST,
+ PROD_CLOUD_HOST,
+ PROD_ENGINE_HOST,
+)
from arcade.cli.display import (
display_arcade_chat_header,
display_eval_results,
@@ -53,7 +58,7 @@ console = Console()
@cli.command(help="Log in to Arcade Cloud", rich_help_panel="User")
def login(
host: str = typer.Option(
- DEFAULT_CLOUD_HOST,
+ PROD_CLOUD_HOST,
"-h",
"--host",
help="The Arcade Cloud host to log in to.",
@@ -70,7 +75,9 @@ def login(
"""
if check_existing_login():
- console.print("Delete ~/.arcade/credentials.yaml to log in as a different user.\n")
+ console.print("\nTo log out and delete your locally-stored credentials, use ", end="")
+ console.print("arcade logout", style="bold green", end="")
+ console.print(".\n")
return
# Start the HTTP server in a new thread
@@ -104,10 +111,9 @@ def logout() -> None:
"""
Logs the user out of Arcade Cloud.
"""
- # If ~/.arcade/credentials.yaml exists, delete it
- config_file_path = os.path.expanduser("~/.arcade/credentials.yaml")
- if os.path.exists(config_file_path):
- os.remove(config_file_path)
+ # If the credentials file exists, delete it
+ if os.path.exists(CREDENTIALS_FILE_PATH):
+ os.remove(CREDENTIALS_FILE_PATH)
console.print("You're now logged out.", style="bold")
else:
console.print("You're not logged in.", style="bold red")
@@ -141,7 +147,7 @@ def show(
None, "-t", "--tool", help="The specific tool to show details for"
),
host: str = typer.Option(
- DEFAULT_ENGINE_HOST,
+ PROD_ENGINE_HOST,
"-h",
"--host",
help="The Arcade Engine address to show the tools/toolkits of.",
@@ -185,7 +191,7 @@ def chat(
prompt: str = typer.Option(None, "--prompt", help="The system prompt to use for the chat."),
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
host: str = typer.Option(
- DEFAULT_ENGINE_HOST,
+ PROD_ENGINE_HOST,
"-h",
"--host",
help="The Arcade Engine address to send chat requests to.",
@@ -345,7 +351,7 @@ def evals(
"""
config = validate_and_get_config()
- host = DEFAULT_ENGINE_HOST if cloud else host
+ host = PROD_ENGINE_HOST if cloud else host
base_url = compute_engine_base_url(force_tls, force_no_tls, host, port)
models_list = models.split(",") # Use 'models_list' to avoid shadowing
@@ -479,8 +485,9 @@ def workerup(
@cli.callback()
-def version(
- _: bool = typer.Option(
+def main_callback(
+ ctx: typer.Context,
+ _: Optional[bool] = typer.Option(
None,
"-v",
"--version",
@@ -489,4 +496,11 @@ def version(
help="Print version and exit.",
),
) -> None:
- pass
+ excluded_commands = {login.__name__, logout.__name__}
+ if ctx.invoked_subcommand in excluded_commands:
+ return
+
+ if not check_existing_login(suppress_message=True):
+ console.print("Not logged in to Arcade CLI. Use ", style="bold red", end="")
+ console.print("arcade login", style="bold green")
+ raise typer.Exit()
diff --git a/arcade/arcade/cli/utils.py b/arcade/arcade/cli/utils.py
index 595600fb..388dad76 100644
--- a/arcade/arcade/cli/utils.py
+++ b/arcade/arcade/cli/utils.py
@@ -1,6 +1,5 @@
import importlib.util
import ipaddress
-import os
import webbrowser
from dataclasses import dataclass
from enum import Enum
@@ -560,24 +559,6 @@ def load_eval_suites(eval_files: list[Path]) -> list[Callable]:
return eval_suites
-def create_new_env_file() -> None:
- """
- Create a new env file if one doesn't already exist.
- """
- env_file = os.path.expanduser("~/.arcade/arcade.env")
- if not os.path.exists(env_file):
- template_path = os.path.join(
- os.path.dirname(__file__), "..", "templates", "arcade.template.env"
- )
- os.makedirs(os.path.dirname(env_file), exist_ok=True)
-
- with open(template_path) as template_file, open(env_file, "w") as new_env_file:
- template_contents = template_file.read()
- new_env_file.write(template_contents)
-
- console.print(f"Created new environment file at {env_file}", style="bold green")
-
-
def get_user_input() -> str:
"""
Get input from the user, handling multi-line input.
diff --git a/arcade/arcade/templates/arcade.template.env b/arcade/arcade/templates/arcade.template.env
deleted file mode 100644
index 88b01f3e..00000000
--- a/arcade/arcade/templates/arcade.template.env
+++ /dev/null
@@ -1,50 +0,0 @@
-### Engine Configuration ###
-
-TELEMETRY_ENVIRONMENT=local
-TELEMETRY_LOGGING_LEVEL=debug
-TELEMETRY_LOGGING_ENCODING=console
-API_DEVELOPMENT=true
-ARCADE_API_HOST=localhost
-ARCADE_API_PORT=9099
-
-
-### LLM API KEY ###
-
-# OPENAI_API_KEY=
-# ANTHROPIC_API_KEY=
-# GROQ_API_KEY=
-
-### Worker Configuration ###
-
-ARCADE_WORKER_URI=http://localhost:8002
-# ARCADE_WORKER_SECRET=
-
-### Token Storage ###
-# REDIS_HOST=
-# REDIS_PORT=
-
-### Integrations ###
-
-# GITHUB_CLIENT_ID=
-# GITHUB_CLIENT_SECRET=
-
-# GOOGLE_CLIENT_ID=
-# GOOGLE_CLIENT_SECRET=
-
-# LINKEDIN_CLIENT_ID=
-# LINKEDIN_CLIENT_SECRET=
-
-# MICROSOFT_CLIENT_ID=
-# MICROSOFT_CLIENT_SECRET=
-
-# SLACK_CLIENT_ID=
-# SLACK_CLIENT_SECRET=
-
-# SPOTIFY_CLIENT_ID=
-# SPOTIFY_CLIENT_SECRET=
-
-# X_CLIENT_ID=
-# X_CLIENT_SECRET=
-
-# ZOOM_CLIENT_ID=
-# ZOOM_CLIENT_SECRET=