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
|
import yaml
|
||||||
from rich.console import Console
|
from rich.console import Console
|
||||||
|
|
||||||
from arcade.cli.constants import LOGIN_FAILED_HTML, LOGIN_SUCCESS_HTML
|
from arcade.cli.constants import (
|
||||||
from arcade.cli.utils import create_new_env_file
|
ARCADE_CONFIG_PATH,
|
||||||
|
CREDENTIALS_FILE_PATH,
|
||||||
|
LOGIN_FAILED_HTML,
|
||||||
|
LOGIN_SUCCESS_HTML,
|
||||||
|
)
|
||||||
|
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
|
|
@ -56,16 +60,13 @@ class LoginCallbackHandler(BaseHTTPRequestHandler):
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# ensure the ~/.arcade directory exists
|
# ensure the ARCADE_CONFIG_PATH directory exists
|
||||||
# TODO: this should use WORK_DIR from env if set
|
if not os.path.exists(ARCADE_CONFIG_PATH):
|
||||||
arcade_dir = os.path.join(os.path.expanduser("~"), ".arcade")
|
os.makedirs(ARCADE_CONFIG_PATH, exist_ok=True)
|
||||||
if not os.path.exists(arcade_dir):
|
|
||||||
os.makedirs(arcade_dir, exist_ok=True)
|
|
||||||
|
|
||||||
# TODO don't overwrite existing config
|
# 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}}}
|
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)
|
yaml.dump(new_config, f)
|
||||||
|
|
||||||
# Send a success response to the browser
|
# Send a success response to the browser
|
||||||
|
|
@ -73,7 +74,7 @@ class LoginCallbackHandler(BaseHTTPRequestHandler):
|
||||||
f"""✅ Hi there, {email}!
|
f"""✅ Hi there, {email}!
|
||||||
|
|
||||||
Your Arcade API key is: {api_key}
|
Your Arcade API key is: {api_key}
|
||||||
Stored in: {config_file_path}""",
|
Stored in: {CREDENTIALS_FILE_PATH}""",
|
||||||
style="bold green",
|
style="bold green",
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
|
|
@ -112,35 +113,34 @@ class LocalAuthCallbackServer:
|
||||||
self.httpd.shutdown()
|
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.
|
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:
|
Returns:
|
||||||
bool: True if the user is already logged in, False otherwise.
|
bool: True if the user is already logged in, False otherwise.
|
||||||
"""
|
"""
|
||||||
# Create a new env file if one doesn't already exist
|
if not os.path.exists(CREDENTIALS_FILE_PATH):
|
||||||
create_new_env_file()
|
|
||||||
|
|
||||||
config_file_path = os.path.expanduser("~/.arcade/credentials.yaml")
|
|
||||||
|
|
||||||
if not os.path.exists(config_file_path):
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if os.path.exists(config_file_path):
|
if os.path.exists(CREDENTIALS_FILE_PATH):
|
||||||
try:
|
try:
|
||||||
with open(config_file_path) as f:
|
with open(CREDENTIALS_FILE_PATH) as f:
|
||||||
config: dict[str, Any] = yaml.safe_load(f)
|
config: dict[str, Any] = yaml.safe_load(f)
|
||||||
cloud_config = config.get("cloud", {})
|
cloud_config = config.get("cloud", {})
|
||||||
api_key = cloud_config.get("api", {}).get("key")
|
api_key = cloud_config.get("api", {}).get("key")
|
||||||
email = cloud_config.get("user", {}).get("email")
|
email = cloud_config.get("user", {}).get("email")
|
||||||
|
|
||||||
if api_key and 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
|
return True
|
||||||
except yaml.YAMLError:
|
except yaml.YAMLError:
|
||||||
console.print(
|
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:
|
except Exception as e:
|
||||||
console.print(f"Error: Unable to read configuration file: {e!s}", style="bold red")
|
console.print(f"Error: Unable to read configuration file: {e!s}", style="bold red")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,15 @@
|
||||||
DEFAULT_CLOUD_HOST = "cloud.arcade.dev"
|
import os
|
||||||
DEFAULT_ENGINE_HOST = "api.arcade.dev"
|
|
||||||
|
PROD_CLOUD_HOST = "cloud.arcade.dev"
|
||||||
|
PROD_ENGINE_HOST = "api.arcade.dev"
|
||||||
LOCALHOST = "localhost"
|
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"""
|
_style_block = b"""
|
||||||
<link rel="icon" href="https://cdn.arcade.dev/favicons/favicon.ico" sizes="any">
|
<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">
|
<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:
|
if os.environ.get("HOMEBREW_REPOSITORY") is not None:
|
||||||
homebrew_home = os.path.join(os.environ["HOMEBREW_REPOSITORY"], "etc", "arcade-engine")
|
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(
|
def start_servers(
|
||||||
|
|
@ -54,7 +55,7 @@ def start_servers(
|
||||||
engine_config = _get_config_file(engine_config, default_filename="engine.yaml")
|
engine_config = _get_config_file(engine_config, default_filename="engine.yaml")
|
||||||
|
|
||||||
# Ensure engine_env is provided or found and either way, validated
|
# 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
|
# Prepare command-line arguments for the worker server and engine
|
||||||
worker_cmd = _build_worker_command(worker_host, worker_port, debug)
|
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
|
file_path: str | None, default_filename: str = "engine.yaml", optional: bool = False
|
||||||
) -> str | None:
|
) -> 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:
|
Args:
|
||||||
file_path: Optional path provided by the user.
|
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.
|
RuntimeError: If the config file is not found and is not optional.
|
||||||
"""
|
"""
|
||||||
if file_path:
|
if file_path:
|
||||||
config_path = Path(os.path.expanduser(file_path)).resolve()
|
candidate = Path(os.path.expanduser(file_path)).resolve()
|
||||||
if not config_path.is_file():
|
if not candidate.is_file():
|
||||||
console.print(f"❌ Config file not found at {config_path}", style="bold red")
|
console.print(f"❌ Config file not found at {candidate}", style="bold red")
|
||||||
raise RuntimeError(f"Config file not found at {config_path}")
|
raise RuntimeError(f"Config file not found at {candidate}")
|
||||||
return str(config_path)
|
return str(candidate)
|
||||||
|
|
||||||
# Look for the file in the current working directory
|
# List of all config file path locations to check.
|
||||||
config_path = Path(os.getcwd()) / default_filename
|
candidates = [
|
||||||
if config_path.is_file():
|
Path(os.getcwd()) / default_filename,
|
||||||
console.print(f"Using config file at {config_path}", style="bold green")
|
Path.home() / ".arcade" / default_filename,
|
||||||
return str(config_path)
|
]
|
||||||
|
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/
|
# Find the first candidate that exists.
|
||||||
home_config_path = Path.home() / ".arcade" / default_filename
|
for candidate in candidates:
|
||||||
if home_config_path.is_file():
|
if candidate.is_file():
|
||||||
console.print(f"Using config file at {home_config_path}", style="bold green")
|
console.print(f"Using config file at {candidate}", style="bold green")
|
||||||
return str(home_config_path)
|
return str(candidate)
|
||||||
|
|
||||||
# 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)
|
|
||||||
|
|
||||||
|
# No config file was found. Handle according to the optional flag.
|
||||||
if optional:
|
if optional:
|
||||||
console.print(
|
console.print(
|
||||||
f"⚠️ Optional config file '{default_filename}' not found in either of the default locations: "
|
f"⚠️ Optional config file '{default_filename}' not found in any of the following locations:",
|
||||||
f" 1) Current working directory: {Path.cwd() / default_filename}, or "
|
|
||||||
f" 2) User's home directory: {Path.home() / '.arcade' / default_filename}.",
|
|
||||||
style="bold yellow",
|
style="bold yellow",
|
||||||
)
|
)
|
||||||
|
for i, candidate in enumerate(candidates, start=1):
|
||||||
|
console.print(f" {i}) {candidate}", style="bold yellow")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
console.print(
|
console.print(
|
||||||
f"❌ Error: Required config file '{default_filename}' not found in any of the default locations:\n"
|
f"❌ Error: Required config file '{default_filename}' not found in any of the following locations:",
|
||||||
f" 1) Current working directory: {Path.cwd() / default_filename}, or\n"
|
|
||||||
f" 2) User's home directory: {Path.home() / '.arcade' / default_filename}\n",
|
|
||||||
style="bold red",
|
style="bold red",
|
||||||
)
|
)
|
||||||
|
for i, candidate in enumerate(candidates, start=1):
|
||||||
|
console.print(f" {i}) {candidate}", style="bold red")
|
||||||
|
|
||||||
console.print(
|
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",
|
" https://docs.arcade.dev/home/install/local#install-the-engine\n",
|
||||||
style="bold green",
|
style="bold green",
|
||||||
)
|
)
|
||||||
|
|
||||||
raise RuntimeError(f"Config file '{default_filename}' not found.")
|
raise RuntimeError(f"Config file '{default_filename}' not found.")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,12 @@ from rich.text import Text
|
||||||
from tqdm import tqdm
|
from tqdm import tqdm
|
||||||
|
|
||||||
from arcade.cli.authn import LocalAuthCallbackServer, check_existing_login
|
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 (
|
from arcade.cli.display import (
|
||||||
display_arcade_chat_header,
|
display_arcade_chat_header,
|
||||||
display_eval_results,
|
display_eval_results,
|
||||||
|
|
@ -53,7 +58,7 @@ console = Console()
|
||||||
@cli.command(help="Log in to Arcade Cloud", rich_help_panel="User")
|
@cli.command(help="Log in to Arcade Cloud", rich_help_panel="User")
|
||||||
def login(
|
def login(
|
||||||
host: str = typer.Option(
|
host: str = typer.Option(
|
||||||
DEFAULT_CLOUD_HOST,
|
PROD_CLOUD_HOST,
|
||||||
"-h",
|
"-h",
|
||||||
"--host",
|
"--host",
|
||||||
help="The Arcade Cloud host to log in to.",
|
help="The Arcade Cloud host to log in to.",
|
||||||
|
|
@ -70,7 +75,9 @@ def login(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if check_existing_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
|
return
|
||||||
|
|
||||||
# Start the HTTP server in a new thread
|
# Start the HTTP server in a new thread
|
||||||
|
|
@ -104,10 +111,9 @@ def logout() -> None:
|
||||||
"""
|
"""
|
||||||
Logs the user out of Arcade Cloud.
|
Logs the user out of Arcade Cloud.
|
||||||
"""
|
"""
|
||||||
# If ~/.arcade/credentials.yaml exists, delete it
|
# If the credentials file exists, delete it
|
||||||
config_file_path = os.path.expanduser("~/.arcade/credentials.yaml")
|
if os.path.exists(CREDENTIALS_FILE_PATH):
|
||||||
if os.path.exists(config_file_path):
|
os.remove(CREDENTIALS_FILE_PATH)
|
||||||
os.remove(config_file_path)
|
|
||||||
console.print("You're now logged out.", style="bold")
|
console.print("You're now logged out.", style="bold")
|
||||||
else:
|
else:
|
||||||
console.print("You're not logged in.", style="bold red")
|
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"
|
None, "-t", "--tool", help="The specific tool to show details for"
|
||||||
),
|
),
|
||||||
host: str = typer.Option(
|
host: str = typer.Option(
|
||||||
DEFAULT_ENGINE_HOST,
|
PROD_ENGINE_HOST,
|
||||||
"-h",
|
"-h",
|
||||||
"--host",
|
"--host",
|
||||||
help="The Arcade Engine address to show the tools/toolkits of.",
|
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."),
|
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"),
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
||||||
host: str = typer.Option(
|
host: str = typer.Option(
|
||||||
DEFAULT_ENGINE_HOST,
|
PROD_ENGINE_HOST,
|
||||||
"-h",
|
"-h",
|
||||||
"--host",
|
"--host",
|
||||||
help="The Arcade Engine address to send chat requests to.",
|
help="The Arcade Engine address to send chat requests to.",
|
||||||
|
|
@ -345,7 +351,7 @@ def evals(
|
||||||
"""
|
"""
|
||||||
config = validate_and_get_config()
|
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)
|
base_url = compute_engine_base_url(force_tls, force_no_tls, host, port)
|
||||||
|
|
||||||
models_list = models.split(",") # Use 'models_list' to avoid shadowing
|
models_list = models.split(",") # Use 'models_list' to avoid shadowing
|
||||||
|
|
@ -479,8 +485,9 @@ def workerup(
|
||||||
|
|
||||||
|
|
||||||
@cli.callback()
|
@cli.callback()
|
||||||
def version(
|
def main_callback(
|
||||||
_: bool = typer.Option(
|
ctx: typer.Context,
|
||||||
|
_: Optional[bool] = typer.Option(
|
||||||
None,
|
None,
|
||||||
"-v",
|
"-v",
|
||||||
"--version",
|
"--version",
|
||||||
|
|
@ -489,4 +496,11 @@ def version(
|
||||||
help="Print version and exit.",
|
help="Print version and exit.",
|
||||||
),
|
),
|
||||||
) -> None:
|
) -> 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 importlib.util
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import os
|
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
@ -560,24 +559,6 @@ def load_eval_suites(eval_files: list[Path]) -> list[Callable]:
|
||||||
return eval_suites
|
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:
|
def get_user_input() -> str:
|
||||||
"""
|
"""
|
||||||
Get input from the user, handling multi-line input.
|
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