arcade-mcp/libs/arcade-cli/arcade_cli/org.py
jottakka fe8ddfd500
[TOO-326] Windows papercuts (#768)
<!-- CURSOR_SUMMARY -->
> [!NOTE]
> **Medium Risk**
> Touches authentication/login flow, credentials-file permissions, and
subprocess lifecycle behavior across platforms; while mostly defensive,
regressions could impact login or process management on Windows/macOS
runners.
> 
> **Overview**
> Improves Windows/cross-platform reliability across the CLI and MCP
server: OAuth login now binds the callback server to `127.0.0.1`, avoids
slow loopback reverse-DNS, adds a configurable callback timeout
(`--timeout` + env default), and opens URLs via a Windows-friendly
`_open_browser` to avoid flashing console windows.
> 
> Centralizes CLI output via a shared `console` that forces UTF-8 on
Windows, standardizes UTF-8 file reads/writes throughout, tightens
credentials-file permissions on Windows using `icacls`, and adds shared
Windows subprocess helpers for **no-window** process creation and
graceful termination (used by `deploy`, MCP reload, and usage-tracking
worker).
> 
> Updates client configuration UX/robustness (Windows AppData resolution
via `platformdirs`, Cursor config path fallbacks + compatibility writes,
overwrite warnings, absolute `uv` path for GUI clients, safer path
display) and improves `deploy` child-process handling to avoid
pipe-buffer deadlocks while giving better debug-aware error messages.
> 
> Expands CI to run tests on Linux/Windows/macOS, adds a no-auth CLI
integration workflow, disables usage tracking in toolkits CI, and adds
extensive regression tests for Windows signals, subprocess cleanup,
UTF-8, and config-path edge cases; bumps `arcade-core` to `4.4.2` and
`arcade-mcp-server` to `1.17.2` (with updated dependency pin).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0fabd8ca1cd647039ba6ddbdf3f7809c330bab9e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
2026-02-25 13:18:16 -03:00

160 lines
5 KiB
Python

import typer
from arcade_core.constants import PROD_COORDINATOR_HOST
from arcade_cli.authn import (
fetch_organizations,
fetch_projects,
select_default_project,
)
from arcade_cli.console import console
from arcade_cli.usage.command_tracker import TrackedTyper, TrackedTyperGroup
from arcade_cli.utils import (
compute_base_url,
handle_cli_error,
)
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 organization 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 organizations you belong to")
def org_list(
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
) -> None:
"""List all organizations the current user belongs to."""
from arcade_core.config_model import Config
from rich.table import Table
try:
coordinator_url = state["coordinator_url"]
orgs = fetch_organizations(coordinator_url)
if not orgs:
console.print("No organizations found.", style="yellow")
return
# Get current active org
config = Config.load_from_file()
active_org_id = config.get_active_org_id()
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 org in orgs:
is_active = "" if org.org_id == active_org_id else ""
is_default = "" if org.is_default else ""
table.add_row(org.name, org.org_id, is_default, is_active)
console.print(table)
console.print("\nUse 'arcade org set <org_id>' to switch organizations.\n")
except ValueError as e:
handle_cli_error(str(e))
except Exception as e:
handle_cli_error("Failed to list organizations", e, debug)
@app.command("set", help="Set the active organization")
def org_set(
org_id: str = typer.Argument(..., help="Organization ID to set as active"),
debug: bool = typer.Option(False, "--debug", "-d", help="Show debug information"),
) -> None:
"""Set the active organization and reset project to its default."""
from arcade_core.config_model import Config, ContextConfig
try:
coordinator_url = state["coordinator_url"]
# Verify org exists and user has access
orgs = fetch_organizations(coordinator_url)
target_org = next((o for o in orgs if o.org_id == org_id), None)
if not target_org:
console.print(
f"Organization '{org_id}' not found or you don't have access.", style="bold red"
)
console.print("Run 'arcade org list' to see available organizations.", style="dim")
return
# Fetch projects and select default
projects = fetch_projects(coordinator_url, org_id)
if not projects:
handle_cli_error(
f"No projects found in organization '{target_org.name}'. "
"Contact support@arcade.dev for assistance."
)
return
selected_project = select_default_project(projects)
if not selected_project:
handle_cli_error("Could not select a default project.")
return
# Update config
config = Config.load_from_file()
config.context = ContextConfig(
org_id=target_org.org_id,
org_name=target_org.name,
project_id=selected_project.project_id,
project_name=selected_project.name,
)
config.save_to_file()
console.print(f"✓ Switched to organization: {target_org.name}", style="bold green")
console.print(f" Active project: {selected_project.name}", style="dim")
except ValueError as e:
handle_cli_error(str(e))
except Exception as e:
handle_cli_error("Failed to set organization", e, debug)