From b4dce33e38da38aa94fd10d5dac053ee72c0d9f9 Mon Sep 17 00:00:00 2001 From: Musa Date: Tue, 13 Jan 2026 11:00:41 -0800 Subject: [PATCH] Revert "add cli ux" This reverts commit 1681e29fa8a32ae027503b2c9b108a35fb721cf8. --- cli/planoai/main.py | 450 ++------------------------------- cli/pyproject.toml | 2 - cli/test/test_validate.py | 218 ---------------- cli/test/test_version_check.py | 163 ------------ cli/uv.lock | 53 ---- 5 files changed, 21 insertions(+), 865 deletions(-) delete mode 100644 cli/test/test_validate.py delete mode 100644 cli/test/test_version_check.py diff --git a/cli/planoai/main.py b/cli/planoai/main.py index 284e57db..a2737883 100644 --- a/cli/planoai/main.py +++ b/cli/planoai/main.py @@ -1,4 +1,4 @@ -import rich_click as click +import click import os import sys import subprocess @@ -6,74 +6,6 @@ import multiprocessing import importlib.metadata import json from planoai import targets - -# Brand color - Plano purple -PLANO_COLOR = "#969FF4" - -# Configure rich-click styling -click.rich_click.USE_RICH_MARKUP = True -click.rich_click.USE_MARKDOWN = False -click.rich_click.SHOW_ARGUMENTS = True -click.rich_click.GROUP_ARGUMENTS_OPTIONS = True -click.rich_click.STYLE_ERRORS_SUGGESTION = "dim italic" -click.rich_click.ERRORS_SUGGESTION = ( - "Try running the '--help' flag for more information." -) -click.rich_click.ERRORS_EPILOGUE = "" - -# Custom colors matching Plano brand -click.rich_click.STYLE_OPTION = f"dim {PLANO_COLOR}" -click.rich_click.STYLE_ARGUMENT = f"dim {PLANO_COLOR}" -click.rich_click.STYLE_COMMAND = f"bold {PLANO_COLOR}" -click.rich_click.STYLE_SWITCH = "bold green" -click.rich_click.STYLE_METAVAR = "bold yellow" -click.rich_click.STYLE_USAGE = "bold" -click.rich_click.STYLE_USAGE_COMMAND = f"bold dim {PLANO_COLOR}" -click.rich_click.STYLE_HELPTEXT_FIRST_LINE = f"white italic" -click.rich_click.STYLE_HELPTEXT = "" -click.rich_click.STYLE_HEADER_TEXT = "bold" -click.rich_click.STYLE_FOOTER_TEXT = "dim" -click.rich_click.STYLE_OPTIONS_PANEL_BORDER = "dim" -click.rich_click.ALIGN_OPTIONS_PANEL = "left" -click.rich_click.MAX_WIDTH = 100 - -# Option groups for better organization -click.rich_click.OPTION_GROUPS = { - "planoai up": [ - { - "name": "Configuration", - "options": ["--path", "file"], - }, - { - "name": "Runtime Options", - "options": ["--foreground"], - }, - ], - "planoai logs": [ - { - "name": "Log Options", - "options": ["--debug", "--follow"], - }, - ], -} - -# Command groups for main help -click.rich_click.COMMAND_GROUPS = { - "planoai": [ - { - "name": "Gateway Commands", - "commands": ["up", "down", "build", "logs"], - }, - { - "name": "Agent Commands", - "commands": ["cli-agent"], - }, - { - "name": "Utilities", - "commands": ["validate", "generate-prompt-targets"], - }, - ], -} from planoai.docker_cli import ( docker_validate_plano_schema, stream_gateway_logs, @@ -102,22 +34,19 @@ from planoai.consts import ( log = getLogger(__name__) # ref https://patorjk.com/software/taag/#p=display&f=Doom&t=Plano&x=none&v=4&h=4&w=80&we=false -LOGO = f"""[bold {PLANO_COLOR}] - ______ _ - | ___ \\ | - | |_/ / | __ _ _ __ ___ - | __/| |/ _` | '_ \\ / _ \\ - | | | | (_| | | | | (_) | - \\_| |_|\\__,_|_| |_|\\___/ -[/bold {PLANO_COLOR}]""" +logo = r""" +______ _ +| ___ \ | +| |_/ / | __ _ _ __ ___ +| __/| |/ _` | '_ \ / _ \ +| | | | (_| | | | | (_) | +\_| |_|\__,_|_| |_|\___/ + +""" # Command to build plano Docker images ARCHGW_DOCKERFILE = "./Dockerfile" -# PyPI package name for version checking -PYPI_PACKAGE_NAME = "planoai" -PYPI_URL = f"https://pypi.org/pypi/{PYPI_PACKAGE_NAME}/json" - def get_version(): try: @@ -134,110 +63,19 @@ def get_version(): return "version not found" -def get_latest_version(timeout: float = 2.0) -> str | None: - """Fetch the latest version from PyPI. - - Args: - timeout: Request timeout in seconds - - Returns: - Latest version string or None if fetch failed - """ - import requests - - try: - response = requests.get(PYPI_URL, timeout=timeout) - if response.status_code == 200: - data = response.json() - return data.get("info", {}).get("version") - except (requests.RequestException, ValueError): - # Network error or invalid JSON - fail silently - pass - return None - - -def parse_version(version_str: str) -> tuple: - """Parse version string into comparable tuple. - - Handles versions like "0.4.1", "1.0.0", "0.4.1a1" - """ - import re - - # Remove any pre-release suffixes for comparison - clean_version = re.split(r"[a-zA-Z]", version_str)[0] - parts = clean_version.split(".") - return tuple(int(p) for p in parts if p.isdigit()) - - -def check_version_status(current: str, latest: str | None) -> dict: - """Compare current version with latest and return status. - - Returns: - dict with keys: is_outdated, current, latest, message - """ - if latest is None: - return { - "is_outdated": False, - "current": current, - "latest": None, - "message": None, - } - - try: - current_tuple = parse_version(current) - latest_tuple = parse_version(latest) - is_outdated = current_tuple < latest_tuple - - return { - "is_outdated": is_outdated, - "current": current, - "latest": latest, - "message": f"Update available: {latest}" if is_outdated else None, - } - except (ValueError, TypeError): - # Version parsing failed - return { - "is_outdated": False, - "current": current, - "latest": latest, - "message": None, - } - - @click.group(invoke_without_command=True) -@click.option("--version", is_flag=True, help="Show the Plano CLI version and exit.") +@click.option("--version", is_flag=True, help="Show the plano cli version and exit.") @click.pass_context def main(ctx, version): - from rich.console import Console - - console = Console() - if version: - current_version = get_version() - console.print( - f"[bold {PLANO_COLOR}]plano[/bold {PLANO_COLOR}] version [cyan]{current_version}[/cyan]" - ) - - # Check for updates (skip if PLANO_SKIP_VERSION_CHECK is set) - if not os.environ.get("PLANO_SKIP_VERSION_CHECK"): - latest_version = get_latest_version() - status = check_version_status(current_version, latest_version) - - if status["is_outdated"]: - console.print( - f"\n[yellow]⚠ Update available:[/yellow] [bold]{status['latest']}[/bold]" - ) - console.print( - f"[dim]Run: uv pip install --upgrade {PYPI_PACKAGE_NAME}[/dim]" - ) - elif latest_version: - console.print(f"[dim]✓ You're up to date[/dim]") - + click.echo(f"plano cli version: {get_version()}") ctx.exit() + log.info(f"Starting plano cli version: {get_version()}") + if ctx.invoked_subcommand is None: - console.print(LOGO) - console.print("[dim]The Delivery Infrastructure for Agentic Apps[/dim]\n") + click.echo("""Arch (The Intelligent Prompt Gateway) CLI""") + click.echo(logo) click.echo(ctx.get_help()) @@ -283,16 +121,16 @@ def build(): @click.command() @click.argument("file", required=False) # Optional file argument @click.option( - "--path", default=".", help="Path to the directory containing config.yaml" + "--path", default=".", help="Path to the directory containing arch_config.yaml" ) @click.option( "--foreground", default=False, - help="Run Plano in the foreground. Default is False", + help="Run Arch in the foreground. Default is False", is_flag=True, ) def up(file, path, foreground): - """Starts Plano.""" + """Starts Arch.""" # Use the utility function to find config file arch_config_file = find_config_file(path, file) @@ -360,7 +198,7 @@ def up(file, path, foreground): @click.command() def down(): - """Stops Plano.""" + """Stops Arch.""" stop_docker_container() @@ -432,7 +270,7 @@ def logs(debug, follow): help="Additional settings as JSON string for the CLI agent.", ) def cli_agent(type, file, path, settings): - """Start a CLI agent connected to Plano. Currently only 'claude' is supported. + """Start a CLI agent connected to Arch. CLI_AGENT: The type of CLI agent to start (currently only 'claude' is supported) """ @@ -460,258 +298,12 @@ def cli_agent(type, file, path, settings): sys.exit(1) -def validate_config_file(config_path: str) -> dict: - """Validate a Plano config file and return validation results. - - Args: - config_path: Path to the config file - - Returns: - dict with keys: valid, errors, warnings, config, summary - """ - import yaml - from jsonschema import validate as json_validate, ValidationError - - result = { - "valid": True, - "errors": [], - "warnings": [], - "config": None, - "summary": { - "model_providers": [], - "listeners": [], - "env_vars_required": [], - }, - } - - # Check file exists - if not os.path.exists(config_path): - result["valid"] = False - result["errors"].append(f"Config file not found: {config_path}") - return result - - # Try to load YAML - try: - with open(config_path, "r") as f: - config = yaml.safe_load(f) - result["config"] = config - except yaml.YAMLError as e: - result["valid"] = False - result["errors"].append(f"Invalid YAML syntax: {e}") - return result - - # Find schema file - schema_path = find_repo_root() - if schema_path: - schema_file = os.path.join(schema_path, "config", "arch_config_schema.yaml") - else: - # Fallback - try relative paths - schema_file = None - for possible_path in [ - "../config/arch_config_schema.yaml", - "config/arch_config_schema.yaml", - ]: - if os.path.exists(possible_path): - schema_file = possible_path - break - - # Schema validation - if schema_file and os.path.exists(schema_file): - try: - with open(schema_file, "r") as f: - schema = yaml.safe_load(f) - json_validate(config, schema) - except ValidationError as e: - result["valid"] = False - result["errors"].append(f"Schema validation failed: {e.message}") - except Exception as e: - result["warnings"].append(f"Could not validate schema: {e}") - else: - result["warnings"].append("Schema file not found, skipping schema validation") - - # Extract model providers - model_providers = config.get("model_providers", config.get("llm_providers", [])) - for provider in model_providers: - model = provider.get("model", "unknown") - is_default = provider.get("default", False) - result["summary"]["model_providers"].append( - { - "model": model, - "default": is_default, - "name": provider.get("name", model), - } - ) - - # Check for env vars - access_key = provider.get("access_key", "") - if access_key.startswith("$"): - env_var = access_key[1:] - result["summary"]["env_vars_required"].append(env_var) - - # Extract listeners - listeners = config.get("listeners", {}) - if isinstance(listeners, dict): - # Legacy format - if "egress_traffic" in listeners: - result["summary"]["listeners"].append( - { - "name": "egress_traffic (LLM Gateway)", - "port": listeners["egress_traffic"].get("port", 12000), - "type": "model", - } - ) - if "ingress_traffic" in listeners: - result["summary"]["listeners"].append( - { - "name": "ingress_traffic (Prompt Gateway)", - "port": listeners["ingress_traffic"].get("port", 10000), - "type": "prompt", - } - ) - elif isinstance(listeners, list): - for listener in listeners: - result["summary"]["listeners"].append( - { - "name": listener.get("name", "unnamed"), - "port": listener.get("port", "unknown"), - "type": listener.get("type", "unknown"), - } - ) - - # Remove duplicates from env vars first - result["summary"]["env_vars_required"] = list( - set(result["summary"]["env_vars_required"]) - ) - - # Check environment variables (after deduplication) - for env_var in result["summary"]["env_vars_required"]: - if not os.environ.get(env_var): - result["warnings"].append(f"Environment variable ${env_var} is not set") - - return result - - -@click.command() -@click.argument("config_file", required=False, type=click.Path()) -@click.option( - "--path", "-p", default=".", help="Path to directory containing config.yaml" -) -@click.option("--quiet", "-q", is_flag=True, help="Only show errors, no summary") -def validate(config_file, path, quiet): - """Validate a Plano configuration file. - - If no CONFIG_FILE is provided, looks for config.yaml in the current directory - or the directory specified by --path. - """ - from rich.console import Console - from rich.table import Table - from rich.panel import Panel - - console = Console() - - # Determine config file path - if config_file: - config_path = os.path.abspath(config_file) - else: - # Look for config.yaml in path - config_path = os.path.join(os.path.abspath(path), "config.yaml") - if not os.path.exists(config_path): - # Try arch_config.yaml as fallback - config_path = os.path.join(os.path.abspath(path), "arch_config.yaml") - - # Show what we're validating - console.print(f"\n[bold]Validating:[/bold] [dim]{config_path}[/dim]\n") - - # Run validation - result = validate_config_file(config_path) - - # Display results - if result["valid"]: - console.print( - f"[bold green]✓[/bold green] [green]Configuration is valid[/green]\n" - ) - else: - console.print(f"[bold red]✗[/bold red] [red]Configuration is invalid[/red]\n") - - # Show errors - if result["errors"]: - for error in result["errors"]: - console.print(f" [red]✗ {error}[/red]") - console.print() - - # Show warnings - if result["warnings"]: - for warning in result["warnings"]: - console.print(f" [yellow]⚠ {warning}[/yellow]") - console.print() - - # Show summary (unless quiet mode) - if not quiet and result["config"]: - summary = result["summary"] - - # Model Providers table - if summary["model_providers"]: - table = Table( - title=f"[bold {PLANO_COLOR}]Model Providers[/bold {PLANO_COLOR}]", - border_style="dim", - show_header=True, - header_style=f"bold {PLANO_COLOR}", - ) - table.add_column("Model", style="cyan") - table.add_column("Default", style="green", justify="center") - - for provider in summary["model_providers"]: - default_marker = "●" if provider["default"] else "" - table.add_row(provider["model"], default_marker) - - console.print(table) - console.print() - - # Listeners table - if summary["listeners"]: - table = Table( - title=f"[bold {PLANO_COLOR}]Listeners[/bold {PLANO_COLOR}]", - border_style="dim", - show_header=True, - header_style=f"bold {PLANO_COLOR}", - ) - table.add_column("Name", style="cyan") - table.add_column("Type", style="magenta") - table.add_column("Port", style="yellow", justify="right") - - for listener in summary["listeners"]: - table.add_row(listener["name"], listener["type"], str(listener["port"])) - - console.print(table) - console.print() - - # Environment variables - if summary["env_vars_required"]: - env_status = [] - for env_var in sorted(summary["env_vars_required"]): - is_set = os.environ.get(env_var) is not None - status = f"[green]✓[/green]" if is_set else f"[yellow]○[/yellow]" - env_status.append(f" {status} [dim]${env_var}[/dim]") - - console.print( - f"[bold {PLANO_COLOR}]Environment Variables[/bold {PLANO_COLOR}]" - ) - for line in env_status: - console.print(line) - console.print() - - # Exit with appropriate code - if not result["valid"]: - sys.exit(1) - - main.add_command(up) main.add_command(down) main.add_command(build) main.add_command(logs) main.add_command(cli_agent) main.add_command(generate_prompt_targets) -main.add_command(validate) if __name__ == "__main__": main() diff --git a/cli/pyproject.toml b/cli/pyproject.toml index be5ba0ca..f9108178 100644 --- a/cli/pyproject.toml +++ b/cli/pyproject.toml @@ -11,8 +11,6 @@ dependencies = [ "jsonschema>=4.23.0,<5.0.0", "pyyaml>=6.0.2,<7.0.0", "requests>=2.31.0,<3.0.0", - "rich>=14.2.0", - "rich-click>=1.9.5", ] [project.optional-dependencies] diff --git a/cli/test/test_validate.py b/cli/test/test_validate.py deleted file mode 100644 index 9e64fcda..00000000 --- a/cli/test/test_validate.py +++ /dev/null @@ -1,218 +0,0 @@ -import pytest -import os -import tempfile -from unittest import mock -from click.testing import CliRunner -from planoai.main import validate, validate_config_file - - -class TestValidateConfigFile: - """Tests for the validate_config_file function.""" - - def test_valid_config(self, tmp_path): - """Test validation of a valid config file.""" - config_content = """ -version: v0.3.0 - -listeners: - - type: model - name: llm_gateway - port: 12000 - -model_providers: - - model: openai/gpt-4o-mini - access_key: $OPENAI_API_KEY - default: true -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - result = validate_config_file(str(config_file)) - - assert result["valid"] is True - assert len(result["errors"]) == 0 - assert result["config"] is not None - assert len(result["summary"]["model_providers"]) == 1 - assert result["summary"]["model_providers"][0]["model"] == "openai/gpt-4o-mini" - assert result["summary"]["model_providers"][0]["default"] is True - assert "OPENAI_API_KEY" in result["summary"]["env_vars_required"] - - def test_invalid_yaml_syntax(self, tmp_path): - """Test validation fails for invalid YAML syntax.""" - config_content = """ -version: v0.3.0 -listeners: - - type: model - name: [invalid yaml -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - result = validate_config_file(str(config_file)) - - assert result["valid"] is False - assert any("Invalid YAML" in error for error in result["errors"]) - - def test_file_not_found(self): - """Test validation fails for non-existent file.""" - result = validate_config_file("/nonexistent/path/config.yaml") - - assert result["valid"] is False - assert any("not found" in error for error in result["errors"]) - - def test_multiple_model_providers(self, tmp_path): - """Test config with multiple model providers.""" - config_content = """ -version: v0.3.0 - -listeners: - - type: model - name: llm_gateway - port: 12000 - -model_providers: - - model: openai/gpt-4o-mini - access_key: $OPENAI_API_KEY - default: true - - - model: anthropic/claude-3-5-sonnet - access_key: $ANTHROPIC_API_KEY - - - model: mistral/mistral-large - access_key: $MISTRAL_API_KEY -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - result = validate_config_file(str(config_file)) - - assert result["valid"] is True - assert len(result["summary"]["model_providers"]) == 3 - assert set(result["summary"]["env_vars_required"]) == { - "OPENAI_API_KEY", - "ANTHROPIC_API_KEY", - "MISTRAL_API_KEY", - } - - def test_legacy_listener_format(self, tmp_path): - """Test config with legacy listener format.""" - config_content = """ -version: v0.1.0 - -listeners: - egress_traffic: - address: 0.0.0.0 - port: 12000 - ingress_traffic: - address: 0.0.0.0 - port: 10000 - -llm_providers: - - model: openai/gpt-4o - access_key: $OPENAI_API_KEY -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - result = validate_config_file(str(config_file)) - - assert result["valid"] is True - assert len(result["summary"]["listeners"]) == 2 - - # Check listener ports are correctly extracted - ports = [l["port"] for l in result["summary"]["listeners"]] - assert 12000 in ports - assert 10000 in ports - - -class TestValidateCommand: - """Tests for the CLI validate command.""" - - def test_validate_command_with_valid_file(self, tmp_path): - """Test validate command with a valid config file.""" - config_content = """ -version: v0.3.0 - -listeners: - - type: model - name: llm_gateway - port: 12000 - -model_providers: - - model: openai/gpt-4o-mini - access_key: $OPENAI_API_KEY - default: true -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - runner = CliRunner() - result = runner.invoke(validate, [str(config_file)]) - - assert result.exit_code == 0 - assert "valid" in result.output.lower() or "✓" in result.output - - def test_validate_command_with_invalid_file(self, tmp_path): - """Test validate command fails with invalid config.""" - config_content = """ -version: v0.3.0 -invalid_yaml: [broken -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - runner = CliRunner() - result = runner.invoke(validate, [str(config_file)]) - - assert result.exit_code == 1 - assert "invalid" in result.output.lower() or "✗" in result.output - - def test_validate_command_auto_detect_config(self, tmp_path, monkeypatch): - """Test validate command auto-detects config.yaml in current directory.""" - config_content = """ -version: v0.3.0 - -listeners: - - type: model - name: test - port: 12000 - -model_providers: - - model: openai/gpt-4o - access_key: $OPENAI_API_KEY -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - # Change to the temp directory - monkeypatch.chdir(tmp_path) - - runner = CliRunner() - result = runner.invoke(validate, []) - - assert result.exit_code == 0 - assert "valid" in result.output.lower() or "✓" in result.output - - def test_validate_command_quiet_mode(self, tmp_path): - """Test validate command with --quiet flag.""" - config_content = """ -version: v0.3.0 - -listeners: - - type: model - name: llm_gateway - port: 12000 - -model_providers: - - model: openai/gpt-4o-mini - access_key: $OPENAI_API_KEY -""" - config_file = tmp_path / "config.yaml" - config_file.write_text(config_content) - - runner = CliRunner() - result = runner.invoke(validate, [str(config_file), "--quiet"]) - - assert result.exit_code == 0 - # Quiet mode should have minimal output (no tables) - assert "Model Providers" not in result.output diff --git a/cli/test/test_version_check.py b/cli/test/test_version_check.py deleted file mode 100644 index c51eeae5..00000000 --- a/cli/test/test_version_check.py +++ /dev/null @@ -1,163 +0,0 @@ -import pytest -from unittest import mock -from planoai.main import ( - get_version, - get_latest_version, - parse_version, - check_version_status, - PYPI_URL, -) - - -class TestParseVersion: - """Tests for version string parsing.""" - - def test_parse_simple_version(self): - assert parse_version("1.0.0") == (1, 0, 0) - assert parse_version("0.4.1") == (0, 4, 1) - assert parse_version("10.20.30") == (10, 20, 30) - - def test_parse_two_part_version(self): - assert parse_version("1.0") == (1, 0) - assert parse_version("2.5") == (2, 5) - - def test_parse_version_with_prerelease(self): - # Pre-release suffixes should be stripped - assert parse_version("0.4.1a1") == (0, 4, 1) - assert parse_version("1.0.0beta2") == (1, 0, 0) - assert parse_version("2.0.0rc1") == (2, 0, 0) - - -class TestCheckVersionStatus: - """Tests for version comparison logic.""" - - def test_current_equals_latest(self): - status = check_version_status("0.4.1", "0.4.1") - assert status["is_outdated"] is False - assert status["current"] == "0.4.1" - assert status["latest"] == "0.4.1" - assert status["message"] is None - - def test_current_is_outdated(self): - status = check_version_status("0.4.1", "0.5.0") - assert status["is_outdated"] is True - assert status["current"] == "0.4.1" - assert status["latest"] == "0.5.0" - assert "Update available" in status["message"] - assert "0.5.0" in status["message"] - - def test_current_is_newer(self): - # Dev version might be newer than PyPI - status = check_version_status("0.5.0", "0.4.1") - assert status["is_outdated"] is False - assert status["message"] is None - - def test_major_version_outdated(self): - status = check_version_status("0.4.1", "1.0.0") - assert status["is_outdated"] is True - - def test_minor_version_outdated(self): - status = check_version_status("0.4.1", "0.5.0") - assert status["is_outdated"] is True - - def test_patch_version_outdated(self): - status = check_version_status("0.4.1", "0.4.2") - assert status["is_outdated"] is True - - def test_latest_is_none(self): - # When PyPI check fails - status = check_version_status("0.4.1", None) - assert status["is_outdated"] is False - assert status["latest"] is None - assert status["message"] is None - - -class TestGetLatestVersion: - """Tests for PyPI version fetching.""" - - def test_successful_fetch(self): - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"info": {"version": "0.5.0"}} - - with mock.patch("requests.get", return_value=mock_response): - version = get_latest_version() - assert version == "0.5.0" - - def test_network_error(self): - import requests - - with mock.patch( - "requests.get", side_effect=requests.RequestException("Network error") - ): - version = get_latest_version() - assert version is None - - def test_timeout(self): - import requests - - with mock.patch("requests.get", side_effect=requests.Timeout("Timeout")): - version = get_latest_version() - assert version is None - - def test_invalid_json(self): - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_response.json.side_effect = ValueError("Invalid JSON") - - with mock.patch("requests.get", return_value=mock_response): - version = get_latest_version() - assert version is None - - def test_404_response(self): - mock_response = mock.Mock() - mock_response.status_code = 404 - - with mock.patch("requests.get", return_value=mock_response): - version = get_latest_version() - assert version is None - - -class TestVersionCheckIntegration: - """Integration tests simulating version check scenarios.""" - - def test_outdated_version_message(self, capsys): - """Simulate an outdated version scenario.""" - from rich.console import Console - - console = Console(force_terminal=True) - current_version = "0.4.1" - - # Mock PyPI returning a newer version - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"info": {"version": "0.5.0"}} - - with mock.patch("requests.get", return_value=mock_response): - latest = get_latest_version() - status = check_version_status(current_version, latest) - - assert status["is_outdated"] is True - assert status["latest"] == "0.5.0" - - def test_up_to_date_version(self): - """Simulate an up-to-date version scenario.""" - current_version = "0.4.1" - - mock_response = mock.Mock() - mock_response.status_code = 200 - mock_response.json.return_value = {"info": {"version": "0.4.1"}} - - with mock.patch("requests.get", return_value=mock_response): - latest = get_latest_version() - status = check_version_status(current_version, latest) - - assert status["is_outdated"] is False - - def test_skip_version_check_env_var(self, monkeypatch): - """Test that PLANO_SKIP_VERSION_CHECK skips the check.""" - monkeypatch.setenv("PLANO_SKIP_VERSION_CHECK", "1") - - import os - - assert os.environ.get("PLANO_SKIP_VERSION_CHECK") == "1" diff --git a/cli/uv.lock b/cli/uv.lock index 764badcc..fffd3eb2 100644 --- a/cli/uv.lock +++ b/cli/uv.lock @@ -174,18 +174,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] -[[package]] -name = "markdown-it-py" -version = "4.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mdurl" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - [[package]] name = "markupsafe" version = "3.0.2" @@ -244,15 +232,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, ] -[[package]] -name = "mdurl" -version = "0.1.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, -] - [[package]] name = "packaging" version = "25.0" @@ -272,8 +251,6 @@ dependencies = [ { name = "jsonschema" }, { name = "pyyaml" }, { name = "requests" }, - { name = "rich" }, - { name = "rich-click" }, ] [package.optional-dependencies] @@ -294,8 +271,6 @@ requires-dist = [ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.1,<9.0.0" }, { name = "pyyaml", specifier = ">=6.0.2,<7.0.0" }, { name = "requests", specifier = ">=2.31.0,<3.0.0" }, - { name = "rich", specifier = ">=14.2.0" }, - { name = "rich-click", specifier = ">=1.9.5" }, ] provides-extras = ["dev"] @@ -411,34 +386,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] -[[package]] -name = "rich" -version = "14.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "markdown-it-py" }, - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/d2/8920e102050a0de7bfabeb4c4614a49248cf8d5d7a8d01885fbb24dc767a/rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4", size = 219990, upload-time = "2025-10-09T14:16:53.064Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/7a/b0178788f8dc6cafce37a212c99565fa1fe7872c70c6c9c1e1a372d9d88f/rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd", size = 243393, upload-time = "2025-10-09T14:16:51.245Z" }, -] - -[[package]] -name = "rich-click" -version = "1.9.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "rich" }, - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6b/d1/b60ca6a8745e76800b50c7ee246fd73f08a3be5d8e0b551fc93c19fa1203/rich_click-1.9.5.tar.gz", hash = "sha256:48120531493f1533828da80e13e839d471979ec8d7d0ca7b35f86a1379cc74b6", size = 73927, upload-time = "2025-12-21T14:49:44.167Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/0a/d865895e1e5d88a60baee0fc3703eb111c502ee10c8c107516bc7623abf8/rich_click-1.9.5-py3-none-any.whl", hash = "sha256:9b195721a773b1acf0e16ff9ec68cef1e7d237e53471e6e3f7ade462f86c403a", size = 70580, upload-time = "2025-12-21T14:49:42.905Z" }, -] - [[package]] name = "rpds-py" version = "0.27.1"