Fixes [PLT-720: Refactor CLI to support multiple orgs + projects](https://linear.app/arcadedev/issue/PLT-720/refactor-cli-to-support-multiple-orgs-projects) This PR removes the legacy login flow (login to get an API key) from Arcade CLI. Believe it or not, this flow predates the ability to get an API key from the Dashboard, or even the Dashboard itself! Notable changes: **Legacy handling** - When a user with an existing `credentials.yaml` updates the CLI, they will get instructions on fixing their old credentials: <img width="978" height="146" alt="Screenshot 2025-12-08 at 10 10 37" src="https://github.com/user-attachments/assets/5aeaef2c-bef7-4642-a2f7-f917b257c94b" /> Any commands that require login (non-public commands) will be blocked with the above message until `arcade logout / arcade login` is performed again. **New login flow** ```sh arcade login Opening a browser to log you in... ✅ Logged in as nate@arcade.dev. Active project: Nate Barbettini's organization / Default project Run 'arcade org list' or 'arcade project list' to see available options. ``` **List and set the active organization** ```sh arcade org list ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓ ┃ Name ┃ ID ┃ Default ┃ Active ┃ ┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩ │ Nate Barbettini's organization │ 1c64968e-fdc5-4c55-8612-2ce46cd7881b │ ✓ │ ✓ │ │ Sergio 743 │ 1f1f6184-58dc-4bac-bdde-b9184e43fdf3 │ │ │ └────────────────────────────────┴──────────────────────────────────────┴─────────┴────────┘ Use 'arcade org set <org_id>' to switch organizations. ``` ```sh arcade org set 1c64968e-fdc5-4c55-8612-2ce46cd7881b ✓ Switched to organization: Nate Barbettini's organization Active project: Default project ``` **List and set the active project** ```sh arcade project list Active organization: Nate Barbettini's organization Use 'arcade org list' and 'arcade org set <org_id>' to switch organizations. ┏━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━┓ ┃ Name ┃ ID ┃ Default ┃ Active ┃ ┡━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━┩ │ Default project │ 35166bf3-6e68-481e-bf16-f747fadc6c22 │ ✓ │ ✓ │ │ Second project │ 62963205-31ea-4fda-9fc4-af10db89c06f │ │ │ └─────────────────┴──────────────────────────────────────┴─────────┴────────┘ Use 'arcade project set <project_id>' to switch projects. ``` ```sh arcade project set 35166bf3-6e68-481e-bf16-f747fadc6c22 ✓ Switched to project: Default project ``` <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Migrates CLI to OAuth2 (PKCE) with saved org/project context, adds org/project commands, rewrites Engine calls to org-scoped endpoints, and bumps core packages. > > - **Auth & Config** > - Implement OAuth2 Authorization Code + PKCE (`arcade_cli/authn.py`) with local callback server and Jinja templates. > - Persist tokens and active `context` (org/project) in `credentials.yaml` via updated config models (`arcade_core/config_model.py`). > - Add token refresh and CLI config fetch utilities (`arcade_core/auth_tokens.py`). > - Detect legacy API-key credentials and block protected commands until re-login; add `whoami` command. > - **Org/Project Management** > - New subcommands: `arcade org list|set`, `arcade project list|set` (fetch via Coordinator). > - **Engine API usage (org-scoped)** > - Introduce org/project URL rewriting transports (`arcade_core/network/org_transport.py`) and helpers (`get_org_scoped_url`, `get_arcade_client`, `get_auth_headers`). > - Update `deploy`, `server`, and `secret` commands to use Bearer tokens and org-scoped paths; adjust log streaming/status, secrets CRUD, and deployment workflows. > - **CLI UX** > - Replace legacy login URLs/constants; add success/failure HTML templates for browser callback. > - Tweak `dashboard` to health-check without credentials. > - Usage tracking now includes `org_id`/`project_id` properties. > - **Tests** > - Update tests for dashboard, secrets, utils, and usage identity (OAuth `/whoami`). > - **Dependencies & Versions** > - Bump packages: `arcade-core@4.0.0`, `arcade-mcp-server@1.12.0`, `arcade-serve@3.2.0`, `arcade-tdk@3.3.0`; add `authlib`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 49702c2f74b9db15bb286d3ec71179b4e74a9134. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
156 lines
4.9 KiB
Python
156 lines
4.9 KiB
Python
import typer
|
|
from arcade_core.constants import PROD_COORDINATOR_HOST
|
|
from rich.console import Console
|
|
|
|
from arcade_cli.authn import fetch_projects
|
|
from arcade_cli.usage.command_tracker import TrackedTyper, TrackedTyperGroup
|
|
from arcade_cli.utils import (
|
|
compute_base_url,
|
|
handle_cli_error,
|
|
)
|
|
|
|
console = Console()
|
|
|
|
|
|
app = TrackedTyper(
|
|
cls=TrackedTyperGroup,
|
|
add_completion=False,
|
|
no_args_is_help=True,
|
|
pretty_exceptions_enable=True,
|
|
pretty_exceptions_show_locals=False,
|
|
pretty_exceptions_short=True,
|
|
)
|
|
|
|
state = {
|
|
"coordinator_url": compute_base_url(
|
|
force_tls=False,
|
|
force_no_tls=False,
|
|
host=PROD_COORDINATOR_HOST,
|
|
port=None,
|
|
default_port=None,
|
|
)
|
|
}
|
|
|
|
|
|
@app.callback()
|
|
def main(
|
|
host: str = typer.Option(
|
|
PROD_COORDINATOR_HOST,
|
|
"--host",
|
|
"-h",
|
|
help="The Arcade Coordinator host.",
|
|
),
|
|
port: int = typer.Option(
|
|
None,
|
|
"--port",
|
|
"-p",
|
|
help="The port of the Arcade Coordinator host.",
|
|
),
|
|
force_tls: bool = typer.Option(
|
|
False,
|
|
"--tls",
|
|
help="Whether to force TLS for the connection to Arcade Coordinator.",
|
|
),
|
|
force_no_tls: bool = typer.Option(
|
|
False,
|
|
"--no-tls",
|
|
help="Whether to disable TLS for the connection to Arcade Coordinator.",
|
|
),
|
|
) -> None:
|
|
"""Configure Coordinator connection options for project commands."""
|
|
coordinator_url = compute_base_url(force_tls, force_no_tls, host, port, default_port=None)
|
|
state["coordinator_url"] = coordinator_url
|
|
|
|
|
|
@app.command("list", help="List projects in the active organization")
|
|
def project_list(
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""List all projects in the current active organization."""
|
|
from arcade_core.config_model import Config
|
|
from rich.table import Table
|
|
|
|
try:
|
|
config = Config.load_from_file()
|
|
|
|
if not config.context:
|
|
console.print("No active organization set. Run 'arcade login' first.", style="bold red")
|
|
return
|
|
|
|
coordinator_url = state["coordinator_url"]
|
|
projects = fetch_projects(coordinator_url, config.context.org_id)
|
|
|
|
if not projects:
|
|
console.print(
|
|
f"No projects found in organization '{config.context.org_name}'.",
|
|
style="yellow",
|
|
)
|
|
return
|
|
|
|
active_project_id = config.get_active_project_id()
|
|
|
|
console.print(
|
|
f"\nActive organization: {config.context.org_name}\n"
|
|
"Use 'arcade org list' and 'arcade org set <org_id>' to switch organizations.\n",
|
|
)
|
|
|
|
table = Table()
|
|
table.add_column("Name", style="cyan")
|
|
table.add_column("ID", style="dim")
|
|
table.add_column("Default", style="green")
|
|
table.add_column("Active", style="bold yellow")
|
|
|
|
for project in projects:
|
|
is_active = "✓" if project.project_id == active_project_id else ""
|
|
is_default = "✓" if project.is_default else ""
|
|
table.add_row(project.name, project.project_id, is_default, is_active)
|
|
|
|
console.print(table)
|
|
console.print("\nUse 'arcade project set <project_id>' to switch projects.\n")
|
|
|
|
except ValueError as e:
|
|
handle_cli_error(str(e))
|
|
except Exception as e:
|
|
handle_cli_error("Failed to list projects", e, debug)
|
|
|
|
|
|
@app.command("set", help="Set the active project")
|
|
def project_set(
|
|
project_id: str = typer.Argument(..., help="Project ID to set as active"),
|
|
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
|
|
) -> None:
|
|
"""Set the active project within the current organization."""
|
|
from arcade_core.config_model import Config
|
|
|
|
try:
|
|
config = Config.load_from_file()
|
|
|
|
if not config.context:
|
|
console.print("No active organization set. Run 'arcade login' first.", style="bold red")
|
|
return
|
|
|
|
coordinator_url = state["coordinator_url"]
|
|
|
|
# Verify project exists in current org
|
|
projects = fetch_projects(coordinator_url, config.context.org_id)
|
|
target_project = next((p for p in projects if p.project_id == project_id), None)
|
|
|
|
if not target_project:
|
|
console.print(
|
|
f"Project '{project_id}' not found in organization '{config.context.org_name}'.",
|
|
style="bold red",
|
|
)
|
|
console.print("Run 'arcade project list' to see available projects.", style="dim")
|
|
return
|
|
|
|
# Update config
|
|
config.context.project_id = target_project.project_id
|
|
config.context.project_name = target_project.name
|
|
config.save_to_file()
|
|
|
|
console.print(f"✓ Switched to project: {target_project.name}", style="bold green")
|
|
|
|
except ValueError as e:
|
|
handle_cli_error(str(e))
|
|
except Exception as e:
|
|
handle_cli_error("Failed to set project", e, debug)
|