fix(cli): add agents project switch command to project CLI
CI / helm (pull_request) Successful in 34s
CI / push-validation (pull_request) Successful in 27s
CI / build (pull_request) Successful in 49s
CI / lint (pull_request) Failing after 57s
CI / quality (pull_request) Successful in 1m13s
CI / typecheck (pull_request) Successful in 1m21s
CI / security (pull_request) Successful in 1m38s
CI / coverage (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 3m22s
CI / integration_tests (pull_request) Successful in 7m10s
CI / unit_tests (pull_request) Failing after 8m18s
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 4s
CI / helm (pull_request) Successful in 34s
CI / push-validation (pull_request) Successful in 27s
CI / build (pull_request) Successful in 49s
CI / lint (pull_request) Failing after 57s
CI / quality (pull_request) Successful in 1m13s
CI / typecheck (pull_request) Successful in 1m21s
CI / security (pull_request) Successful in 1m38s
CI / coverage (pull_request) Has been skipped
CI / e2e_tests (pull_request) Successful in 3m22s
CI / integration_tests (pull_request) Successful in 7m10s
CI / unit_tests (pull_request) Failing after 8m18s
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 4s
Implements the switch subcommand to update the active project context. - Extracted switch command to project_switch.py (resolves 500-line guideline violation) - Added _get_project_service() helper that does not require a current project - Updated BDD test patches to cover project_switch module helpers - Switch command routes through ProjectService.set_current_project() (4-layer architecture) - Switch command properly updates .cleveragents/project.name file Closes #8623 ISSUES CLOSED: #8623
This commit is contained in:
@@ -102,6 +102,7 @@ _ORIG_FNS: dict[str, Any] = {}
|
||||
def _patch_project_mod(context: Any) -> None:
|
||||
"""Monkey-patch the four DI look-up helpers in project module."""
|
||||
import cleveragents.cli.commands.project as project_mod
|
||||
import cleveragents.cli.commands.project_switch as project_switch_mod
|
||||
|
||||
_ORIG_FNS["repo"] = project_mod._get_namespaced_project_repo
|
||||
_ORIG_FNS["link"] = project_mod._get_resource_link_repo
|
||||
@@ -116,16 +117,33 @@ def _patch_project_mod(context: Any) -> None:
|
||||
_ORIG_FNS["store_extras"] = project_mod._store_project_extras
|
||||
project_mod._store_project_extras = lambda *a, **kw: None
|
||||
|
||||
# Patch project_switch module helpers so the switch command works in tests
|
||||
# without a real DI container or .cleveragents directory.
|
||||
_ORIG_FNS["switch_repo"] = project_switch_mod._get_namespaced_project_repo
|
||||
_ORIG_FNS["switch_svc"] = project_switch_mod._get_project_service
|
||||
project_switch_mod._get_namespaced_project_repo = lambda: context._cmd_project_repo
|
||||
|
||||
# Provide a mock service whose set_current_project is a no-op in tests.
|
||||
from unittest.mock import MagicMock
|
||||
mock_svc = MagicMock()
|
||||
mock_svc.set_current_project = lambda name: None
|
||||
project_switch_mod._get_project_service = lambda: mock_svc
|
||||
|
||||
|
||||
def _unpatch_project_mod() -> None:
|
||||
"""Restore original helpers."""
|
||||
import cleveragents.cli.commands.project as project_mod
|
||||
import cleveragents.cli.commands.project_switch as project_switch_mod
|
||||
|
||||
project_mod._get_namespaced_project_repo = _ORIG_FNS["repo"]
|
||||
project_mod._get_resource_link_repo = _ORIG_FNS["link"]
|
||||
project_mod._get_resource_registry_service = _ORIG_FNS["svc"]
|
||||
if _ORIG_FNS.get("store_extras"):
|
||||
project_mod._store_project_extras = _ORIG_FNS["store_extras"]
|
||||
if _ORIG_FNS.get("switch_repo"):
|
||||
project_switch_mod._get_namespaced_project_repo = _ORIG_FNS["switch_repo"]
|
||||
if _ORIG_FNS.get("switch_svc"):
|
||||
project_switch_mod._get_project_service = _ORIG_FNS["switch_svc"]
|
||||
|
||||
|
||||
def _capture(context: Any, func: Any, *args: Any, **kwargs: Any) -> None:
|
||||
@@ -659,14 +677,14 @@ def step_invoke_delete_short_force(context: Any, name: str) -> None:
|
||||
|
||||
@when('I invoke project-switch for "{name}" default format')
|
||||
def step_invoke_switch(context: Any, name: str) -> None:
|
||||
from cleveragents.cli.commands.project import switch
|
||||
from cleveragents.cli.commands.project_switch import switch
|
||||
|
||||
_capture(context, switch, project=name)
|
||||
|
||||
|
||||
@when('I invoke project-switch for "{name}" format "{fmt}"')
|
||||
def step_invoke_switch_fmt(context: Any, name: str, fmt: str) -> None:
|
||||
from cleveragents.cli.commands.project import switch
|
||||
from cleveragents.cli.commands.project_switch import switch
|
||||
|
||||
_capture(context, switch, project=name, output_format=fmt)
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from rich.table import Table
|
||||
|
||||
from cleveragents.application.services.context_service import DEFAULT_IGNORE_PATTERNS
|
||||
from cleveragents.cli.commands.project_context import app as context_app
|
||||
from cleveragents.cli.commands.project_switch import switch as _switch_fn
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.cli.renderers import _get_console, _get_err_console
|
||||
from cleveragents.core.exceptions import (
|
||||
@@ -923,52 +924,9 @@ def show(
|
||||
console.print(format_output(data, output_format))
|
||||
|
||||
|
||||
@app.command(name="switch")
|
||||
def switch(
|
||||
project: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Project namespaced name to switch to"),
|
||||
],
|
||||
output_format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Switch the active project context to the specified project.
|
||||
|
||||
Updates the current project context so that subsequent commands
|
||||
operate in the context of the newly selected project.
|
||||
"""
|
||||
repo = _get_namespaced_project_repo()
|
||||
|
||||
# Validate project exists
|
||||
try:
|
||||
proj = repo.get(project)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Project not found:[/red] {project}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
# Update the active project context via the service
|
||||
try:
|
||||
service, _ = _get_project_service_and_current_project()
|
||||
service.set_current_project(proj.namespaced_name)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Error switching project context:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
# Format output
|
||||
data = _project_spec_dict(proj)
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]Switched to project:[/bold] {proj.namespaced_name}",
|
||||
title="Project Switch",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
console.print(format_output(data, output_format))
|
||||
# Switch command is implemented in project_switch.py to keep this file
|
||||
# below the 500-line guideline (CONTRIBUTING.md).
|
||||
app.command(name="switch")(_switch_fn)
|
||||
|
||||
|
||||
@app.command(name="delete")
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Switch subcommand for the project CLI.
|
||||
|
||||
Extracted from ``project.py`` to keep each module below the 500-line
|
||||
guideline (CONTRIBUTING.md).
|
||||
|
||||
Implements ``agents project switch <name>``, which updates the active
|
||||
project context so that subsequent commands operate in the context of
|
||||
the newly selected project.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated, Any
|
||||
|
||||
import typer
|
||||
from rich.panel import Panel
|
||||
|
||||
from cleveragents.cli.formatting import OutputFormat, format_output
|
||||
from cleveragents.cli.renderers import _get_console, _get_err_console
|
||||
|
||||
console = _get_console()
|
||||
err_console = _get_err_console()
|
||||
|
||||
# Reusable --format option description (mirrors project.py)
|
||||
_FORMAT_HELP = "Output format: json, yaml, plain, table, or rich (default: rich)"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers (patched in tests)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_namespaced_project_repo() -> Any:
|
||||
"""Return a NamespacedProjectRepository from the DI container."""
|
||||
from cleveragents.application.container import get_container
|
||||
|
||||
container = get_container()
|
||||
return container.namespaced_project_repo()
|
||||
|
||||
|
||||
def _get_project_service() -> Any:
|
||||
"""Return a ProjectService from the DI container.
|
||||
|
||||
Unlike ``_get_project_service_and_current_project`` in project.py,
|
||||
this helper does **not** require a current project to exist. The
|
||||
switch command must work even when no project is currently active.
|
||||
"""
|
||||
from cleveragents.application.container import get_container
|
||||
from cleveragents.application.services.project_service import ProjectService
|
||||
|
||||
container = get_container()
|
||||
project_service: ProjectService = container.project_service()
|
||||
return project_service
|
||||
|
||||
|
||||
def _project_spec_dict(project: Any) -> dict[str, object]:
|
||||
"""Return project data as a dict using spec field names."""
|
||||
linked: list[dict[str, object]] = []
|
||||
for lr in project.linked_resources:
|
||||
linked.append(
|
||||
{
|
||||
"resource_id": lr.resource_id,
|
||||
"read_only": lr.project_read_only,
|
||||
"alias": lr.alias,
|
||||
"linked_at": lr.linked_at.isoformat()
|
||||
if hasattr(lr.linked_at, "isoformat")
|
||||
else str(lr.linked_at),
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
"namespaced_name": project.namespaced_name,
|
||||
"namespace": project.namespace,
|
||||
"name": project.name,
|
||||
"description": project.description,
|
||||
"linked_resources": linked,
|
||||
"created_at": project.created_at.isoformat()
|
||||
if hasattr(project.created_at, "isoformat")
|
||||
else str(project.created_at),
|
||||
"updated_at": project.updated_at.isoformat()
|
||||
if hasattr(project.updated_at, "isoformat")
|
||||
else str(project.updated_at),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Switch command
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def switch(
|
||||
project: Annotated[
|
||||
str,
|
||||
typer.Argument(help="Project namespaced name to switch to"),
|
||||
],
|
||||
output_format: Annotated[
|
||||
str,
|
||||
typer.Option("--format", "-f", help=_FORMAT_HELP),
|
||||
] = "rich",
|
||||
) -> None:
|
||||
"""Switch the active project context to the specified project.
|
||||
|
||||
Updates the current project context so that subsequent commands
|
||||
operate in the context of the newly selected project.
|
||||
"""
|
||||
repo = _get_namespaced_project_repo()
|
||||
|
||||
# Validate project exists
|
||||
try:
|
||||
proj = repo.get(project)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Project not found:[/red] {project}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
# Update the active project context via the service layer
|
||||
# (CLI -> Service -> Repository -- maintains 4-layer architecture boundary)
|
||||
try:
|
||||
service = _get_project_service()
|
||||
service.set_current_project(proj.namespaced_name)
|
||||
except Exception as exc:
|
||||
err_console.print(f"[red]Error switching project context:[/red] {exc}")
|
||||
raise typer.Exit(1) from exc
|
||||
|
||||
# Format output
|
||||
data = _project_spec_dict(proj)
|
||||
|
||||
if output_format.lower() == OutputFormat.RICH:
|
||||
console.print(
|
||||
Panel(
|
||||
f"[bold]Switched to project:[/bold] {proj.namespaced_name}",
|
||||
title="Project Switch",
|
||||
expand=False,
|
||||
)
|
||||
)
|
||||
else:
|
||||
console.print(format_output(data, output_format))
|
||||
Reference in New Issue
Block a user