Improve Arcade CLI (#244)
This commit is contained in:
parent
be2539602f
commit
df969d9d73
6 changed files with 93 additions and 138 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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"""
|
||||
<link rel="icon" href="https://cdn.arcade.dev/favicons/favicon.ico" sizes="any">
|
||||
<link rel="apple-touch-icon" href="https://cdn.arcade.dev/favicons/apple-touch-icon.png">
|
||||
|
|
|
|||
|
|
@ -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.")
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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=
|
||||
Loading…
Reference in a new issue