diff --git a/features/repo_indexing_cli.feature b/features/repo_indexing_cli.feature new file mode 100644 index 000000000..7e0720aed --- /dev/null +++ b/features/repo_indexing_cli.feature @@ -0,0 +1,103 @@ +Feature: Repository indexing CLI commands + As a developer + I want to index repository resources via the CLI + So that I can manage repository indexing through agents repo commands + + Background: + Given a fresh in-memory repo indexing environment + + # ---- agents repo index ---- + + Scenario: Index a valid repository resource + Given a registered git-checkout resource "local/test-repo" with a temp directory + When I run repo index on resource "local/test-repo" + Then the repo command should succeed + And the repo output should contain "complete" + And the repo output should contain "local/test-repo" + + Scenario: Index with --full forces full re-index + Given a registered git-checkout resource "local/full-repo" with a temp directory + When I run repo full-index on resource "local/full-repo" + Then the repo command should succeed + And the repo output should contain "Full index complete" + + Scenario: Incremental re-index after initial index + Given a registered git-checkout resource "local/incr-repo" with a temp directory + And I have already indexed "local/incr-repo" + When I run repo index on resource "local/incr-repo" + Then the repo command should succeed + And the repo output should contain "Incremental refresh complete" + + Scenario: Index with JSON output format + Given a registered git-checkout resource "local/json-repo" with a temp directory + When I run repo index-json on resource "local/json-repo" + Then the repo command should succeed + And the repo output should be valid JSON + And the repo JSON output should contain key "status" + And the repo JSON output should contain key "file_count" + And the repo JSON output should contain key "primary_language" + + Scenario: Index with non-existent resource shows error + When I run repo index on resource "local/nonexistent-resource" + Then the repo command should fail + And the repo output should contain "Resource not found" + + # ---- agents repo status ---- + + Scenario: Status for an indexed resource + Given a registered git-checkout resource "local/status-repo" with a temp directory + And I have already indexed "local/status-repo" + When I run repo status on resource "local/status-repo" + Then the repo command should succeed + And the repo output should contain "Status" + And the repo output should contain "Files" + And the repo output should contain "Tokens" + + Scenario: Status with no index shows appropriate message + Given a registered git-checkout resource "local/noindex-repo" with a temp directory + When I run repo status on resource "local/noindex-repo" + Then the repo command should succeed + And the repo output should contain "No index found" + + Scenario: Status with JSON output format + Given a registered git-checkout resource "local/json-status-repo" with a temp directory + And I have already indexed "local/json-status-repo" + When I run repo status-json on resource "local/json-status-repo" + Then the repo command should succeed + And the repo output should be valid JSON + And the repo JSON output should contain key "status" + And the repo JSON output should contain key "file_count" + And the repo JSON output should contain key "token_estimate" + + Scenario: Status for non-existent resource shows error + When I run repo status on resource "local/missing-resource" + Then the repo command should fail + And the repo output should contain "Resource not found" + + Scenario: Status with no index in JSON format + Given a registered git-checkout resource "local/noindex-json" with a temp directory + When I run repo status-json on resource "local/noindex-json" + Then the repo command should succeed + And the repo output should be valid JSON + And the repo JSON output should contain key "status" + And the repo JSON output field "status" should equal "not_indexed" + + # ---- edge cases for coverage ---- + + Scenario: Index resource with no filesystem path shows error + Given a registered resource "local/no-path" with no location + When I run repo index on resource "local/no-path" + Then the repo command should fail + And the repo output should contain "no filesystem path" + + Scenario: Index resource with path only in properties + Given a registered resource "local/prop-path" with path in properties only + When I run repo index on resource "local/prop-path" + Then the repo command should succeed + And the repo output should contain "complete" + + Scenario: Index with missing root path shows error + Given a registered resource "local/bad-path" with nonexistent path + When I run repo index on resource "local/bad-path" + Then the repo command should fail + And the repo output should contain "Error" diff --git a/features/steps/repo_indexing_cli_steps.py b/features/steps/repo_indexing_cli_steps.py new file mode 100644 index 000000000..0076c25f0 --- /dev/null +++ b/features/steps/repo_indexing_cli_steps.py @@ -0,0 +1,274 @@ +"""Step definitions for Repository Indexing CLI feature tests.""" + +from __future__ import annotations + +import contextlib +import json +import sys +import tempfile +from io import StringIO +from pathlib import Path +from typing import Any +from unittest.mock import patch + +from behave import given, then, when # type: ignore[import-untyped] +from behave.runner import Context # type: ignore[import-untyped] +from rich.console import Console +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from cleveragents.application.services.repo_indexing_service import ( + RepoIndexingService, +) +from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, +) +from cleveragents.infrastructure.database.models import Base + + +def _setup_services( + context: Context, +) -> tuple[ResourceRegistryService, RepoIndexingService]: + """Return the cached services created by the Background step.""" + return context._repo_cli_resource_svc, context._repo_cli_indexing_svc + + +def _capture_repo_command( + context: Context, + func: Any, + *args: Any, + **kwargs: Any, +) -> None: + """Run a CLI function capturing output and success status. + + Captures both Rich console output (via *buf*) **and** direct + ``sys.stdout`` writes (used by ``format_output`` for JSON/YAML + to avoid Rich line-wrapping). + """ + buf = StringIO() + stdout_buf = StringIO() + console = Console( + file=buf, width=200, no_color=True, highlight=False, force_terminal=False + ) + + import cleveragents.cli.commands.repo as repo_mod + + orig_console = repo_mod.console + repo_mod.console = console + + failed = False + try: + with contextlib.redirect_stdout(stdout_buf): + func(*args, **kwargs) + except SystemExit: + failed = True + except Exception: + failed = True + finally: + repo_mod.console = orig_console + + # Combine Rich console output with any direct stdout writes + context.repo_cli_output = buf.getvalue() + stdout_buf.getvalue() + context.repo_cli_failed = failed + + +def _run_with_mock( + context: Context, + func: Any, + *args: Any, + **kwargs: Any, +) -> None: + """Run a CLI function with mocked container providing test services.""" + resource_svc, indexing_svc = _setup_services(context) + + with patch("cleveragents.cli.commands.repo.get_container") as mock_container: + container = mock_container.return_value + container.resource_registry_service.return_value = resource_svc + container.repo_indexing_service.return_value = indexing_svc + _capture_repo_command(context, func, *args, **kwargs) + + +@given("a fresh in-memory repo indexing environment") +def step_fresh_repo_env(context: Context) -> None: + """Set up fresh in-memory services for repo indexing tests.""" + # Create fresh in-memory database for each scenario. We cannot + # ``delattr`` on behave Context (it only works at the current level), + # so we unconditionally set new services each time. + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + context._repo_cli_engine = engine + context._repo_cli_factory = factory + context._repo_cli_resource_svc = ResourceRegistryService(session_factory=factory) + context._repo_cli_resource_svc.bootstrap_builtin_types() + context._repo_cli_indexing_svc = RepoIndexingService(session_factory=factory) + context.repo_cli_output = "" + context.repo_cli_failed = False + context._repo_cli_temp_dirs = {} # dict[str, str] + + +@given('a registered git-checkout resource "{name}" with a temp directory') +def step_register_resource_with_tempdir(context: Context, name: str) -> None: + """Register a git-checkout resource backed by a real temporary directory.""" + resource_svc, _indexing_svc = _setup_services(context) + + tmp_dir = tempfile.mkdtemp() + context._repo_cli_temp_dirs[name] = tmp_dir + + sample = Path(tmp_dir) / "README.md" + sample.write_text("# Test Repository\n\nSample content for indexing.\n") + + resource_svc.register_resource( + type_name="git-checkout", + name=name, + location=tmp_dir, + properties={"path": tmp_dir}, + ) + + +@given('I have already indexed "{name}"') +def step_already_indexed(context: Context, name: str) -> None: + """Perform an initial index on the resource.""" + resource_svc, indexing_svc = _setup_services(context) + resource = resource_svc.show_resource(name) + root_path = resource.location or "" + if not root_path and resource.properties: + path_val = resource.properties.get("path") + root_path = str(path_val) if path_val is not None else "" + indexing_svc.index_resource(resource.resource_id, root_path) + + +@when('I run repo index on resource "{name}"') +def step_run_repo_index(context: Context, name: str) -> None: + """Run the repo index command (incremental).""" + from cleveragents.cli.commands.repo import index_command + + _run_with_mock(context, index_command, name, full=False, fmt="text") + + +@when('I run repo full-index on resource "{name}"') +def step_run_repo_full_index(context: Context, name: str) -> None: + """Run the repo index command with --full flag.""" + from cleveragents.cli.commands.repo import index_command + + _run_with_mock(context, index_command, name, full=True, fmt="text") + + +@when('I run repo index-json on resource "{name}"') +def step_run_repo_index_json(context: Context, name: str) -> None: + """Run the repo index command with JSON format.""" + from cleveragents.cli.commands.repo import index_command + + _run_with_mock(context, index_command, name, full=False, fmt="json") + + +@when('I run repo status on resource "{name}"') +def step_run_repo_status(context: Context, name: str) -> None: + """Run the repo status command.""" + from cleveragents.cli.commands.repo import status_command + + _run_with_mock(context, status_command, name, fmt="text") + + +@when('I run repo status-json on resource "{name}"') +def step_run_repo_status_json(context: Context, name: str) -> None: + """Run the repo status command with JSON format.""" + from cleveragents.cli.commands.repo import status_command + + _run_with_mock(context, status_command, name, fmt="json") + + +@then("the repo command should succeed") +def step_repo_should_succeed(context: Context) -> None: + """Assert the repo command did not fail.""" + assert not context.repo_cli_failed, ( + f"Expected success but command failed. Output:\n{context.repo_cli_output}" + ) + + +@then("the repo command should fail") +def step_repo_should_fail(context: Context) -> None: + """Assert the repo command failed.""" + assert context.repo_cli_failed, ( + f"Expected failure but command succeeded. Output:\n{context.repo_cli_output}" + ) + + +@then('the repo output should contain "{text}"') +def step_repo_output_contains(context: Context, text: str) -> None: + """Assert the repo output contains the given text.""" + assert text in context.repo_cli_output, ( + f"Expected output to contain '{text}'. Got:\n{context.repo_cli_output}" + ) + + +@then("the repo output should be valid JSON") +def step_repo_output_valid_json(context: Context) -> None: + """Assert the repo output is valid JSON.""" + output = context.repo_cli_output.strip() + try: + context.repo_cli_json = json.loads(output) + except json.JSONDecodeError as exc: + raise AssertionError( + f"Output is not valid JSON: {exc}\nOutput:\n{output}" + ) from exc + + +@then('the repo JSON output should contain key "{key}"') +def step_repo_json_has_key(context: Context, key: str) -> None: + """Assert the parsed JSON output contains a key.""" + assert hasattr(context, "repo_cli_json"), "JSON not parsed; call 'valid JSON' first" + assert key in context.repo_cli_json, ( + f"Key '{key}' not found in JSON: {context.repo_cli_json}" + ) + + +@then('the repo JSON output field "{key}" should equal "{value}"') +def step_repo_json_field_equals(context: Context, key: str, value: str) -> None: + """Assert a JSON field equals the expected value.""" + assert hasattr(context, "repo_cli_json"), "JSON not parsed; call 'valid JSON' first" + actual = str(context.repo_cli_json.get(key, "")) + assert actual == value, f"Expected {key}='{value}', got '{actual}'" + + +@given('a registered resource "{name}" with no location') +def step_register_resource_no_location(context: Context, name: str) -> None: + """Register a resource with no location or path property.""" + resource_svc, _indexing_svc = _setup_services(context) + resource_svc.register_resource( + type_name="git-checkout", + name=name, + location=None, + properties={}, + ) + + +@given('a registered resource "{name}" with path in properties only') +def step_register_resource_properties_path(context: Context, name: str) -> None: + """Register a resource with path only in properties (no location).""" + resource_svc, _indexing_svc = _setup_services(context) + + tmp_dir = tempfile.mkdtemp() + context._repo_cli_temp_dirs[name] = tmp_dir + + sample = Path(tmp_dir) / "README.md" + sample.write_text("# Properties path test\n") + + resource_svc.register_resource( + type_name="git-checkout", + name=name, + location=None, + properties={"path": tmp_dir}, + ) + + +@given('a registered resource "{name}" with nonexistent path') +def step_register_resource_bad_path(context: Context, name: str) -> None: + """Register a resource pointing to a nonexistent directory.""" + resource_svc, _indexing_svc = _setup_services(context) + resource_svc.register_resource( + type_name="git-checkout", + name=name, + location="/tmp/nonexistent-path-for-indexing-test-12345", + properties={"path": "/tmp/nonexistent-path-for-indexing-test-12345"}, + ) diff --git a/robot/cli_core.robot b/robot/cli_core.robot index 00a6b7c73..54db72b23 100644 --- a/robot/cli_core.robot +++ b/robot/cli_core.robot @@ -16,20 +16,20 @@ Run CLI With Clean Home [Documentation] Run a CLI command with a temp HOME to avoid stale config [Arguments] @{cmd} ${tmpdir}= Evaluate __import__('tempfile').mkdtemp(prefix='cli_core_') - ${result}= Run Process @{cmd} timeout=120s on_timeout=kill env:HOME=${tmpdir} + ${result}= Run Process @{cmd} timeout=60s env:HOME=${tmpdir} RETURN ${result} *** Test Cases *** Version Command Default Rich Format [Documentation] Version command with default (rich) format shows version string - ${result}= Run Process ${PYTHON} -m cleveragents version timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents version timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} 1.0.0 Should Contain ${result.stdout} CleverAgents Version Command JSON Format [Documentation] Version command with --format json returns valid JSON - ${result}= Run Process ${PYTHON} -m cleveragents version --format json timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents version --format json timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} "version": "1.0.0" Should Contain ${result.stdout} "schema": "v3" @@ -41,21 +41,21 @@ Version Command JSON Format Version Command Plain Format [Documentation] Version command with --format plain returns key-value pairs - ${result}= Run Process ${PYTHON} -m cleveragents version --format plain timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents version --format plain timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} version: 1.0.0 Should Contain ${result.stdout} schema: v3 Version Command YAML Format [Documentation] Version command with --format yaml returns YAML - ${result}= Run Process ${PYTHON} -m cleveragents version --format yaml timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents version --format yaml timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} version: 1.0.0 Should Contain ${result.stdout} schema: v3 Info Command Default Rich Format [Documentation] Info command with default (rich) format shows environment details - ${result}= Run Process ${PYTHON} -m cleveragents info timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents info timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} Environment Should Contain ${result.stdout} Runtime @@ -78,14 +78,14 @@ Info Command Plain Format Diagnostics Command Default Rich Format [Documentation] Diagnostics command with default (rich) format runs checks - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents diagnostics timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} Checks Should Contain ${result.stdout} Summary Diagnostics Command JSON Format [Documentation] Diagnostics command with --format json returns structured data - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} "checks" Should Contain ${result.stdout} "summary" @@ -96,14 +96,14 @@ Diagnostics Command JSON Format Diagnostics Command Plain Format [Documentation] Diagnostics command with --format plain returns key-value pairs - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format plain timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format plain timeout=60s Should Be Equal As Integers ${result.rc} 0 Should Contain ${result.stdout} checks: Should Contain ${result.stdout} summary: Diagnostics Command Check Flag Returns Valid Exit Code [Documentation] Diagnostics --check exits 0 when no errors are found - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --check --format json timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --check --format json timeout=60s Should Be Equal As Integers ${result.rc} 0 Diagnostics --check failed with rc=${result.rc}: ${result.stdout} Should Contain ${result.stdout} "checks" Should Contain ${result.stdout} "has_errors" @@ -111,7 +111,7 @@ Diagnostics Command Check Flag Returns Valid Exit Code Diagnostics Command Performance [Documentation] Diagnostics command completes within acceptable time ${start}= Get Time epoch - ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=120s on_timeout=kill + ${result}= Run Process ${PYTHON} -m cleveragents diagnostics --format json timeout=60s ${end}= Get Time epoch ${duration}= Evaluate ${end} - ${start} Should Be True ${duration} < 30 Diagnostics took too long: ${duration}s diff --git a/robot/core_cli_commands.robot b/robot/core_cli_commands.robot index ec6fb286b..549811473 100644 --- a/robot/core_cli_commands.robot +++ b/robot/core_cli_commands.robot @@ -17,7 +17,7 @@ ${PROJECT_NAME} test-project *** Test Cases *** Test CLI Help Shows All Commands [Documentation] Verify that CLI help displays all expected command groups - ${result} = Run Process ${PYTHON} -m cleveragents --help timeout=120s on_timeout=kill + ${result} = Run Process ${PYTHON} -m cleveragents --help timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} AI-powered development assistant Should Contain ${result.stdout} project @@ -32,7 +32,7 @@ Test Project Initialization [Documentation] Test initializing a new CleverAgents project Create Directory ${TEST_DIR}/project1 ${result} = Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} - ... cwd=${TEST_DIR}/project1 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project1 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Project '${PROJECT_NAME}' initialized successfully Directory Should Exist ${TEST_DIR}/project1/.cleveragents @@ -45,10 +45,10 @@ Test Project Initialization Test Project Cannot Initialize Twice [Documentation] Verify project cannot be initialized twice without force Create Directory ${TEST_DIR}/project2 - ${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project2 timeout=120s on_timeout=kill + ${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project2 timeout=120s Should Be Equal As Numbers ${first_init.rc} 0 ${result} = Run Process ${PYTHON} -m cleveragents init second-project - ... cwd=${TEST_DIR}/project2 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project2 timeout=120s Should Not Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stderr} Project already initialized Should Contain ${result.stderr} --force @@ -56,21 +56,21 @@ Test Project Cannot Initialize Twice Test Force Reinitialize Project [Documentation] Test force reinitialization of a project Create Directory ${TEST_DIR}/project3 - ${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project3 timeout=120s on_timeout=kill + ${first_init}= Run Process ${PYTHON} -m cleveragents init first-project cwd=${TEST_DIR}/project3 timeout=120s Should Be Equal As Numbers ${first_init.rc} 0 ${result} = Run Process ${PYTHON} -m cleveragents init second-project --force - ... cwd=${TEST_DIR}/project3 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project3 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Project 'second-project' initialized successfully Test Context Add Files [Documentation] Test adding files to context Create Directory ${TEST_DIR}/project4 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project4 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project4 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 Create File ${TEST_DIR}/project4/test.py print('hello world') ${result} = Run Process ${PYTHON} -m cleveragents context add test.py - ... cwd=${TEST_DIR}/project4 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project4 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Added 1 file(s) to context Should Contain ${result.stdout} test.py @@ -80,16 +80,16 @@ Test Context List Files ${unique_dir} = Set Variable ${TEST_DIR}/project5_${TEST NAME.replace(' ', '_')} Create Directory ${unique_dir} ${init_result} = Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} - ... cwd=${unique_dir} timeout=120s on_timeout=kill + ... cwd=${unique_dir} timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 Create File ${unique_dir}/file1.py # File 1 Create File ${unique_dir}/file2.py # File 2 ${add_result} = Run Process ${PYTHON} -m cleveragents context add file1.py file2.py - ... cwd=${unique_dir} timeout=120s on_timeout=kill + ... cwd=${unique_dir} timeout=120s Should Be Equal As Numbers ${add_result.rc} 0 Directory Should Exist ${unique_dir} ${result} = Run Process ${PYTHON} -m cleveragents context list - ... cwd=${unique_dir} timeout=120s on_timeout=kill + ... cwd=${unique_dir} timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} file1.py Should Contain ${result.stdout} file2.py @@ -97,23 +97,23 @@ Test Context List Files Test Context Clear [Documentation] Test clearing context files Create Directory ${TEST_DIR}/project6 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project6 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project6 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 Create File ${TEST_DIR}/project6/test.py # Test file - ${add_result}= Run Process ${PYTHON} -m cleveragents context add test.py cwd=${TEST_DIR}/project6 timeout=120s on_timeout=kill + ${add_result}= Run Process ${PYTHON} -m cleveragents context add test.py cwd=${TEST_DIR}/project6 timeout=120s Should Be Equal As Numbers ${add_result.rc} 0 ${result} = Run Process ${PYTHON} -m cleveragents context clear --yes - ... cwd=${TEST_DIR}/project6 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project6 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Cleared all files from context Test Plan Creation With Tell [Documentation] Test creating a plan using tell command Create Directory ${TEST_DIR}/project7 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project7 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project7 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 ${result} = Run Process ${PYTHON} -m cleveragents tell Add error handling to main function - ... cwd=${TEST_DIR}/project7 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project7 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Plan created Should Contain ${result.stdout} error handling @@ -121,12 +121,12 @@ Test Plan Creation With Tell Test Plan Build [Documentation] Test building a plan Create Directory ${TEST_DIR}/project8 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project8 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project8 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 - ${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example code cwd=${TEST_DIR}/project8 timeout=120s on_timeout=kill + ${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example code cwd=${TEST_DIR}/project8 timeout=120s Should Be Equal As Numbers ${tell_result.rc} 0 ${result} = Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project8 - ... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill + ... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Plan built successfully Should Contain ${result.stdout} Generated @@ -135,14 +135,14 @@ Test Plan Build Test Plan Apply [Documentation] Test applying plan changes Create Directory ${TEST_DIR}/project9 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project9 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project9 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 - ${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example file cwd=${TEST_DIR}/project9 timeout=120s on_timeout=kill + ${tell_result}= Run Process ${PYTHON} -m cleveragents tell Create example file cwd=${TEST_DIR}/project9 timeout=120s Should Be Equal As Numbers ${tell_result.rc} 0 ${build_result}= Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project9 - ... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill + ... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s Should Be Equal As Numbers ${build_result.rc} 0 - ${result} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project9 timeout=120s on_timeout=kill + ${result} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project9 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Successfully applied File Should Exist ${TEST_DIR}/project9/example.py @@ -150,25 +150,25 @@ Test Plan Apply Test Plan List [Documentation] Test listing plans Create Directory ${TEST_DIR}/project10 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project10 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 - ${tell_result}= Run Process ${PYTHON} -m cleveragents tell First plan cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill + ${tell_result}= Run Process ${PYTHON} -m cleveragents tell First plan cwd=${TEST_DIR}/project10 timeout=120s Should Be Equal As Numbers ${tell_result.rc} 0 - ${plan_result}= Run Process ${PYTHON} -m cleveragents plan new second-plan cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill + ${plan_result}= Run Process ${PYTHON} -m cleveragents plan new second-plan cwd=${TEST_DIR}/project10 timeout=120s Should Be Equal As Numbers ${plan_result.rc} 0 - ${result} = Run Process ${PYTHON} -m cleveragents plan list cwd=${TEST_DIR}/project10 timeout=120s on_timeout=kill + ${result} = Run Process ${PYTHON} -m cleveragents plan list cwd=${TEST_DIR}/project10 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Plans (3 total) Test Shortcut Commands Work [Documentation] Test that shortcut commands work properly Create Directory ${TEST_DIR}/project11 - ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project11 timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init ${PROJECT_NAME} cwd=${TEST_DIR}/project11 timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 Create File ${TEST_DIR}/project11/test.txt Test content # Test context-load shortcut ${result} = Run Process ${PYTHON} -m cleveragents context-load test.txt - ... cwd=${TEST_DIR}/project11 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project11 timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Added 1 file(s) to context @@ -177,30 +177,30 @@ Test End To End Workflow Create Directory ${TEST_DIR}/project12 # Initialize project ${init} = Run Process ${PYTHON} -m cleveragents init workflow-project - ... cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project12 timeout=120s Should Be Equal As Numbers ${init.rc} 0 # Add context Create File ${TEST_DIR}/project12/input.py def main(): pass ${add} = Run Process ${PYTHON} -m cleveragents context add input.py - ... cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project12 timeout=120s Should Be Equal As Numbers ${add.rc} 0 # Create plan ${tell} = Run Process ${PYTHON} -m cleveragents tell Add logging to main function - ... cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/project12 timeout=120s Should Be Equal As Numbers ${tell.rc} 0 # Build plan ${build} = Run Process ${PYTHON} -m cleveragents build cwd=${TEST_DIR}/project12 - ... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s on_timeout=kill + ... env:CLEVERAGENTS_TESTING_USE_MOCK_AI=true timeout=120s Should Be Equal As Numbers ${build.rc} 0 # Apply changes - ${apply} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project12 timeout=120s on_timeout=kill + ${apply} = Run Process ${PYTHON} -m cleveragents apply --yes cwd=${TEST_DIR}/project12 timeout=120s Should Be Equal As Numbers ${apply.rc} 0 # Verify file created File Should Exist ${TEST_DIR}/project12/example.py Test Command Error Handling [Documentation] Test error handling for invalid commands - ${result} = Run Process ${PYTHON} -m cleveragents invalid-command timeout=120s on_timeout=kill + ${result} = Run Process ${PYTHON} -m cleveragents invalid-command timeout=120s Should Not Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stderr} Invalid command @@ -208,17 +208,17 @@ Test Project Status Without Project [Documentation] Test project status command without initialized project Create Directory ${TEST_DIR}/no_project ${result} = Run Process ${PYTHON} -m cleveragents project status - ... cwd=${TEST_DIR}/no_project timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/no_project timeout=120s Should Not Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stderr} No project found Test Project Status With Project [Documentation] Test project status command with initialized project Create Directory ${TEST_DIR}/with_project - ${init_result}= Run Process ${PYTHON} -m cleveragents init status-test cwd=${TEST_DIR}/with_project timeout=120s on_timeout=kill + ${init_result}= Run Process ${PYTHON} -m cleveragents init status-test cwd=${TEST_DIR}/with_project timeout=120s Should Be Equal As Numbers ${init_result.rc} 0 ${result} = Run Process ${PYTHON} -m cleveragents project status - ... cwd=${TEST_DIR}/with_project timeout=120s on_timeout=kill + ... cwd=${TEST_DIR}/with_project timeout=120s Should Be Equal As Numbers ${result.rc} 0 Should Contain ${result.stdout} Project: status-test Should Contain ${result.stdout} Plans: 1 diff --git a/robot/database_integration.robot b/robot/database_integration.robot index f4d1c8f89..e63fc30bd 100644 --- a/robot/database_integration.robot +++ b/robot/database_integration.robot @@ -759,7 +759,7 @@ Run Python Script # Create File writes to it (avoids leaking an open descriptor). ${temp_file}= Evaluate (lambda t: (__import__('os').close(t[0]), t[1])[-1])(__import__('tempfile').mkstemp(suffix='.py', dir='/tmp')) Create File ${temp_file} ${full_code} - ${result}= Run Process ${PYTHON} ${temp_file} timeout=120s on_timeout=kill stderr=STDOUT env:PYTHONWARNINGS=ignore env:PYTHONDONTWRITEBYTECODE=1 + ${result}= Run Process ${PYTHON} ${temp_file} timeout=60s stderr=STDOUT env:PYTHONWARNINGS=ignore env:PYTHONDONTWRITEBYTECODE=1 Remove File ${temp_file} # Check if process failed and log stderr if present IF ${result.rc} != 0 diff --git a/robot/helper_repo_indexing_cli.py b/robot/helper_repo_indexing_cli.py new file mode 100644 index 000000000..58c21d8fc --- /dev/null +++ b/robot/helper_repo_indexing_cli.py @@ -0,0 +1,184 @@ +"""Robot Framework helper for repo indexing CLI integration tests. + +Provides end-to-end test scenarios that exercise the RepoIndexingService +and CLI wiring together, using an in-memory SQLite database. +""" + +from __future__ import annotations + +import tempfile +from pathlib import Path + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from cleveragents.application.services.repo_indexing_service import ( + RepoIndexingService, +) +from cleveragents.application.services.resource_registry_service import ( + ResourceRegistryService, +) +from cleveragents.infrastructure.database.models import Base + + +def _make_services() -> tuple[ResourceRegistryService, RepoIndexingService]: + """Create fresh services backed by an in-memory database.""" + engine = create_engine("sqlite:///:memory:", echo=False) + Base.metadata.create_all(engine) + factory = sessionmaker(bind=engine, expire_on_commit=False) + resource_svc = ResourceRegistryService(session_factory=factory) + resource_svc.bootstrap_builtin_types() + indexing_svc = RepoIndexingService(session_factory=factory) + return resource_svc, indexing_svc + + +def test_e2e_add_index_status() -> str: + """End-to-end: add resource -> index -> check status.""" + resource_svc, indexing_svc = _make_services() + + # Create a temp directory with sample files + tmp_dir = tempfile.mkdtemp() + readme = Path(tmp_dir) / "README.md" + readme.write_text("# Test\nSample content.\n") + main_py = Path(tmp_dir) / "main.py" + main_py.write_text("print('hello')\n") + + # Register resource + resource = resource_svc.register_resource( + type_name="git-checkout", + name="local/e2e-repo", + location=tmp_dir, + properties={"path": tmp_dir}, + ) + + # Index + repo_index = indexing_svc.index_resource(resource.resource_id, tmp_dir) + assert repo_index.metadata.status.value == "ready" + assert repo_index.metadata.file_count >= 2 + + # Check status + status = indexing_svc.get_index_status(resource.resource_id) + assert status is not None + assert status.status.value == "ready" + assert status.file_count == repo_index.metadata.file_count + + return "E2E add-index-status PASSED" + + +def test_incremental_reindex_after_change() -> str: + """Incremental re-index after file change.""" + resource_svc, indexing_svc = _make_services() + + tmp_dir = tempfile.mkdtemp() + readme = Path(tmp_dir) / "README.md" + readme.write_text("# Original\n") + + resource = resource_svc.register_resource( + type_name="git-checkout", + name="local/incr-repo", + location=tmp_dir, + properties={"path": tmp_dir}, + ) + + # Initial index + idx1 = indexing_svc.index_resource(resource.resource_id, tmp_dir) + count1 = idx1.metadata.file_count + + # Add a new file + new_file = Path(tmp_dir) / "app.py" + new_file.write_text("x = 1\n") + + # Incremental refresh + idx2 = indexing_svc.refresh_index(resource.resource_id, tmp_dir) + assert idx2.metadata.file_count == count1 + 1 + assert idx2.metadata.status.value == "ready" + + return "Incremental reindex PASSED" + + +def test_full_reindex() -> str: + """Full re-index replaces the entire index.""" + resource_svc, indexing_svc = _make_services() + + tmp_dir = tempfile.mkdtemp() + readme = Path(tmp_dir) / "README.md" + readme.write_text("# Test\n") + + resource = resource_svc.register_resource( + type_name="git-checkout", + name="local/full-repo", + location=tmp_dir, + properties={"path": tmp_dir}, + ) + + # Initial index + idx1 = indexing_svc.index_resource(resource.resource_id, tmp_dir) + + # Full re-index + idx2 = indexing_svc.index_resource(resource.resource_id, tmp_dir) + assert idx2.metadata.status.value == "ready" + assert idx2.metadata.file_count == idx1.metadata.file_count + + return "Full reindex PASSED" + + +def test_status_no_index() -> str: + """Status returns None when no index exists.""" + resource_svc, indexing_svc = _make_services() + + tmp_dir = tempfile.mkdtemp() + + resource = resource_svc.register_resource( + type_name="git-checkout", + name="local/no-index-repo", + location=tmp_dir, + properties={"path": tmp_dir}, + ) + + status = indexing_svc.get_index_status(resource.resource_id) + assert status is None + + return "Status no-index PASSED" + + +def test_remove_index() -> str: + """Remove index cleans up all data.""" + resource_svc, indexing_svc = _make_services() + + tmp_dir = tempfile.mkdtemp() + readme = Path(tmp_dir) / "README.md" + readme.write_text("# Test\n") + + resource = resource_svc.register_resource( + type_name="git-checkout", + name="local/remove-repo", + location=tmp_dir, + properties={"path": tmp_dir}, + ) + + indexing_svc.index_resource(resource.resource_id, tmp_dir) + assert indexing_svc.get_index_status(resource.resource_id) is not None + + removed = indexing_svc.remove_index(resource.resource_id) + assert removed is True + assert indexing_svc.get_index_status(resource.resource_id) is None + + return "Remove index PASSED" + + +if __name__ == "__main__": + import sys + + _dispatch = { + "e2e": test_e2e_add_index_status, + "incremental": test_incremental_reindex_after_change, + "full": test_full_reindex, + "no-index": test_status_no_index, + "remove": test_remove_index, + } + if len(sys.argv) < 2 or sys.argv[1] not in _dispatch: + # Run all tests + for fn in _dispatch.values(): + print(fn()) + else: + print(_dispatch[sys.argv[1]]()) diff --git a/robot/repo_indexing_cli.robot b/robot/repo_indexing_cli.robot new file mode 100644 index 000000000..76b600b45 --- /dev/null +++ b/robot/repo_indexing_cli.robot @@ -0,0 +1,46 @@ +*** Settings *** +Documentation Repository Indexing CLI Integration Tests +Library Process +Library OperatingSystem + +*** Variables *** +${PYTHON} python +${HELPER} ${CURDIR}/helper_repo_indexing_cli.py + +*** Test Cases *** +E2E Add Resource Index And Check Status + [Documentation] End-to-end: add resource, index it, check status + ${result}= Run Process ${PYTHON} ${HELPER} e2e + ... on_timeout=kill timeout=60s + Should Be Equal As Integers ${result.rc} 0 E2E test failed: ${result.stderr} + Should Contain ${result.stdout} E2E add-index-status PASSED + +Incremental Reindex After File Change + [Documentation] Incremental re-index detects new files + ${result}= Run Process ${PYTHON} ${HELPER} incremental + ... on_timeout=kill timeout=60s + Should Be Equal As Integers ${result.rc} 0 Incremental test failed: ${result.stderr} + Should Contain ${result.stdout} Incremental reindex PASSED + +Full Reindex Replaces Entire Index + [Documentation] Full re-index replaces the entire index + ${result}= Run Process ${PYTHON} ${HELPER} full + ... on_timeout=kill timeout=60s + Should Be Equal As Integers ${result.rc} 0 Full reindex test failed: ${result.stderr} + Should Contain ${result.stdout} Full reindex PASSED + +Status Returns None When No Index Exists + [Documentation] Status check with no index returns None + ${result}= Run Process ${PYTHON} ${HELPER} no-index + ... on_timeout=kill timeout=60s + Should Be Equal As Integers ${result.rc} 0 Status no-index test failed: ${result.stderr} + Should Contain ${result.stdout} Status no-index PASSED + +Remove Index Cleans Up All Data + [Documentation] Removing an index cleans up all data + ${result}= Run Process ${PYTHON} ${HELPER} remove + ... on_timeout=kill timeout=60s + Should Be Equal As Integers ${result.rc} 0 Remove index test failed: ${result.stderr} + Should Contain ${result.stdout} Remove index PASSED + +*** Keywords *** diff --git a/robot/scientific_paper_e2e_test.robot b/robot/scientific_paper_e2e_test.robot index 9887c251c..f596d757b 100644 --- a/robot/scientific_paper_e2e_test.robot +++ b/robot/scientific_paper_e2e_test.robot @@ -65,7 +65,7 @@ Scientific Paper Writer Full Workflow # Test 8: Advance toward later stages (using structure or latex_generation as targets) Log Advancing toward later stages... - ${result}= Run Paper Command !next structure timeout=120s on_timeout=kill + ${result}= Run Paper Command !next structure timeout=120s Should Be Equal As Integers ${result.rc} 0 # Test 9: Verify stage list command works @@ -90,7 +90,7 @@ Scientific Paper Writer LaTeX Generation ... ${V2_PAPER_CONTEXTS_DIR}/03_brainstorming.json ... --context-dir ${CONTEXT_DIR} ... stderr=STDOUT - ... timeout=120s on_timeout=kill + ... timeout=30s Should Be Equal As Integers ${result.rc} 0 Log Context imported: ${result.stdout} @@ -142,7 +142,7 @@ Scientific Paper Writer LaTeX Generation ... --allow-rxpy-in-run-mode ... -p !context ... stderr=STDOUT - ... timeout=120s on_timeout=kill + ... timeout=30s Should Contain ${result.stdout} latex_source Log LaTeX generation test completed @@ -229,7 +229,7 @@ Cleanup Test Environment Log Test environment cleaned up Run Paper Command - [Arguments] ${command} ${timeout}=120s + [Arguments] ${command} ${timeout}=30s [Documentation] Run a command against the paper writer with the persistent context ${result}= Run Process ${PYTHON} -m cleveragents actor run ... -c ${CONFIG_FILE} @@ -239,7 +239,7 @@ Run Paper Command ... --allow-rxpy-in-run-mode ... -p ${command} ... stderr=STDOUT - ... timeout=${timeout} on_timeout=kill + ... timeout=${timeout} Log Command: ${command} Log Output: ${result.stdout} RETURN ${result} diff --git a/src/cleveragents/cli/commands/repo.py b/src/cleveragents/cli/commands/repo.py new file mode 100644 index 000000000..8c8ac60e5 --- /dev/null +++ b/src/cleveragents/cli/commands/repo.py @@ -0,0 +1,238 @@ +"""Repository indexing CLI commands for CleverAgents. + +The ``agents repo`` command group manages repository indexing: + +| Command | Description | +|-----------------------|------------------------------------------| +| ``agents repo index`` | Index or re-index a repository resource | +| ``agents repo status``| Show indexing status for a resource | + +## Example Usage + +```bash +# Index a resource (incremental by default) +agents repo index local/my-repo + +# Force full re-index +agents repo index local/my-repo --full + +# Check indexing status +agents repo status local/my-repo + +# JSON output +agents repo status local/my-repo --format json +``` + +Wires the existing :class:`RepoIndexingService` backend to the CLI layer. + +Based on issue #856: feat(cli): repo indexing CLI functional. +""" + +from __future__ import annotations + +import logging +from typing import Annotated + +import typer +from rich.console import Console +from rich.panel import Panel + +from cleveragents.application.container import get_container +from cleveragents.cli.formatting import format_output +from cleveragents.core.exceptions import NotFoundError + +logger = logging.getLogger(__name__) + +app = typer.Typer( + help="Repository indexing management.", +) + +console = Console() + +_FORMAT_HELP = "Output format: text or json (default: text)" + + +def _resolve_resource( + resource_name: str, +) -> tuple[str, str]: + """Resolve a resource name to (resource_id, root_path). + + Uses the ``ResourceRegistryService`` to look up the resource by + namespaced name or ULID, then extracts the filesystem path from + the resource's ``location`` or ``properties["path"]``. + + Returns: + Tuple of (resource_id, root_path). + + Raises: + typer.Exit: If the resource is not found or has no path. + """ + container = get_container() + registry_service = container.resource_registry_service() + + try: + resource = registry_service.show_resource(resource_name) + except NotFoundError as exc: + console.print(f"[red]Error:[/red] Resource not found: {resource_name}") + raise typer.Exit(code=1) from exc + + # Extract filesystem path from location or properties + root_path = resource.location + if not root_path and resource.properties: + path_val = resource.properties.get("path") + root_path = str(path_val) if path_val is not None else None + + if not root_path: + console.print( + f"[red]Error:[/red] Resource '{resource_name}' has no filesystem path" + ) + raise typer.Exit(code=1) + + return resource.resource_id, root_path + + +@app.command("index") +def index_command( + resource_name: Annotated[ + str, + typer.Argument(help="Resource name or ULID to index (e.g., local/my-repo)"), + ], + full: Annotated[ + bool, + typer.Option("--full", help="Force full re-index (not incremental)"), + ] = False, + fmt: Annotated[ + str, + typer.Option("--format", "-f", help=_FORMAT_HELP), + ] = "text", +) -> None: + """Index or re-index a repository resource. + + By default performs an incremental refresh (only changed files). + Use ``--full`` to force a complete re-index. + + Examples: + agents repo index local/my-repo + agents repo index local/my-repo --full + agents repo index local/my-repo --format json + """ + resource_id, root_path = _resolve_resource(resource_name) + + container = get_container() + indexing_service = container.repo_indexing_service() + + try: + if full: + repo_index = indexing_service.index_resource(resource_id, root_path) + else: + repo_index = indexing_service.refresh_index(resource_id, root_path) + except FileNotFoundError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(code=1) from exc + except ValueError as exc: + console.print(f"[red]Error:[/red] {exc}") + raise typer.Exit(code=1) from exc + + metadata = repo_index.metadata + + if fmt.lower() == "json": + data: dict[str, object] = { + "resource": resource_name, + "resource_id": metadata.resource_id, + "index_id": metadata.index_id, + "status": metadata.status.value, + "file_count": metadata.file_count, + "token_estimate": metadata.token_estimate, + "primary_language": metadata.primary_language, + "indexed_at": metadata.indexed_at.isoformat(), + "mode": "full" if full else "incremental", + } + console.print(format_output(data, "json")) + return + + # Text output + mode_label = "Full index" if full else "Incremental refresh" + console.print(f"[green]{mode_label} complete:[/green] {resource_name}") + details = ( + f"[bold]Status:[/bold] {metadata.status.value}\n" + f"[bold]Files:[/bold] {metadata.file_count}\n" + f"[bold]Tokens:[/bold] {metadata.token_estimate:,}\n" + f"[bold]Language:[/bold] {metadata.primary_language}\n" + f"[bold]Indexed at:[/bold] {metadata.indexed_at.isoformat()}" + ) + console.print(Panel(details, title="Index Result", expand=False)) + + +@app.command("status") +def status_command( + resource_name: Annotated[ + str, + typer.Argument( + help="Resource name or ULID to check status (e.g., local/my-repo)" + ), + ], + fmt: Annotated[ + str, + typer.Option("--format", "-f", help=_FORMAT_HELP), + ] = "text", +) -> None: + """Show indexing status for a repository resource. + + Displays index metadata including file count, token estimate, + primary language, status, and last indexed timestamp. + + Examples: + agents repo status local/my-repo + agents repo status local/my-repo --format json + """ + resource_id, _root_path = _resolve_resource(resource_name) + + container = get_container() + indexing_service = container.repo_indexing_service() + + metadata = indexing_service.get_index_status(resource_id) + + if metadata is None: + if fmt.lower() == "json": + data: dict[str, object] = { + "resource": resource_name, + "status": "not_indexed", + "message": "No index found for this resource", + } + console.print(format_output(data, "json")) + else: + console.print( + f"[yellow]No index found for resource:[/yellow] {resource_name}" + ) + return + + if fmt.lower() == "json": + data = { + "resource": resource_name, + "resource_id": metadata.resource_id, + "index_id": metadata.index_id, + "status": metadata.status.value, + "file_count": metadata.file_count, + "token_estimate": metadata.token_estimate, + "primary_language": metadata.primary_language, + "indexed_at": metadata.indexed_at.isoformat(), + } + if metadata.error_message: + data["error_message"] = metadata.error_message + console.print(format_output(data, "json")) + return + + # Text output + console.print(f"[bold]Index status for:[/bold] {resource_name}") + error_line = "" + if metadata.error_message: + error_line = f"\n[bold]Error:[/bold] {metadata.error_message}" + details = ( + f"[bold]Status:[/bold] {metadata.status.value}\n" + f"[bold]Files:[/bold] {metadata.file_count}\n" + f"[bold]Tokens:[/bold] {metadata.token_estimate:,}\n" + f"[bold]Language:[/bold] {metadata.primary_language}\n" + f"[bold]Indexed at:[/bold] {metadata.indexed_at.isoformat()}" + f"{error_line}" + ) + console.print(Panel(details, title="Index Status", expand=False)) diff --git a/src/cleveragents/cli/main.py b/src/cleveragents/cli/main.py index c1da3a6d7..21152987d 100644 --- a/src/cleveragents/cli/main.py +++ b/src/cleveragents/cli/main.py @@ -88,6 +88,7 @@ def _register_subcommands() -> None: lsp, plan, project, + repo, resource, session, skill, @@ -194,6 +195,11 @@ def _register_subcommands() -> None: name="server", help="Server connection management (stub)", ) + app.add_typer( + repo.app, + name="repo", + help="Repository indexing management", + ) _subcommands_registered = True @@ -611,6 +617,7 @@ def main(args: list[str] | None = None) -> int: "invariant", # Invariant constraint management "repl", # Interactive REPL "server", # Server connection management + "repo", # Repository indexing management "tell", # Shortcut for plan tell "build", # Shortcut for plan build "apply", # Shortcut for plan apply