diff --git a/ra_aid/file_listing.py b/ra_aid/file_listing.py index 2155f05..9b5dfef 100644 --- a/ra_aid/file_listing.py +++ b/ra_aid/file_listing.py @@ -74,14 +74,15 @@ def get_file_listing( directory: str, limit: Optional[int] = None, include_hidden: bool = False ) -> Tuple[List[str], int]: """ - Get a list of tracked files in a git repository. + Get a list of files in a directory. - Uses `git ls-files` for efficient file listing that respects .gitignore rules. + For git repositories, uses `git ls-files` for efficient file listing that respects .gitignore rules. + For non-git directories, falls back to manual file listing using Python's standard library. Returns a tuple containing the list of files (truncated if limit is specified) and the total count of files. Args: - directory: Path to the git repository + directory: Path to the directory limit: Optional maximum number of files to return include_hidden: Whether to include hidden files (starting with .) in the results @@ -104,50 +105,74 @@ def get_file_listing( raise DirectoryNotFoundError(f"Not a directory: {directory}") # Check if it's a git repository - if not is_git_repo(directory): - return [], 0 - - # Get list of files from git ls-files - try: - # Get both tracked and untracked files - tracked_files_process = subprocess.run( - ["git", "ls-files"], - cwd=directory, - capture_output=True, - text=True, - check=True, - ) - untracked_files_process = subprocess.run( - ["git", "ls-files", "--others", "--exclude-standard"], - cwd=directory, - capture_output=True, - text=True, - check=True, - ) - except subprocess.CalledProcessError as e: - raise GitCommandError(f"Git command failed: {e}") - except PermissionError as e: - raise DirectoryAccessError(f"Permission denied: {e}") - - # Combine and process the files + is_git = is_git_repo(directory) + all_files = [] - for file in ( - tracked_files_process.stdout.splitlines() - + untracked_files_process.stdout.splitlines() - ): - file = file.strip() - if not file: - continue - # Skip hidden files unless explicitly included - if not include_hidden and ( - file.startswith(".") - or any(part.startswith(".") for part in file.split("/")) + + if is_git: + # Get list of files from git ls-files + try: + # Get both tracked and untracked files + tracked_files_process = subprocess.run( + ["git", "ls-files"], + cwd=directory, + capture_output=True, + text=True, + check=True, + ) + untracked_files_process = subprocess.run( + ["git", "ls-files", "--others", "--exclude-standard"], + cwd=directory, + capture_output=True, + text=True, + check=True, + ) + except subprocess.CalledProcessError as e: + raise GitCommandError(f"Git command failed: {e}") + except PermissionError as e: + raise DirectoryAccessError(f"Permission denied: {e}") + + # Combine and process the files + for file in ( + tracked_files_process.stdout.splitlines() + + untracked_files_process.stdout.splitlines() ): - continue - # Skip .aider files - if ".aider" in file: - continue - all_files.append(file) + file = file.strip() + if not file: + continue + # Skip hidden files unless explicitly included + if not include_hidden and ( + file.startswith(".") + or any(part.startswith(".") for part in file.split("/")) + ): + continue + # Skip .aider files + if ".aider" in file: + continue + all_files.append(file) + else: + # Not a git repository, use manual file listing + base_path = Path(directory) + excluded_dirs = {'.ra-aid', '.venv', '.git', '.aider', '__pycache__'} + + for root, dirs, files in os.walk(directory): + # Filter out excluded directories + dirs[:] = [d for d in dirs if d not in excluded_dirs and (include_hidden or not d.startswith('.'))] + + # Calculate relative path + rel_root = os.path.relpath(root, directory) + if rel_root == '.': + rel_root = '' + + # Process files + for file in files: + # Skip hidden files unless explicitly included + if not include_hidden and file.startswith('.'): + continue + + # Create relative path + rel_path = os.path.join(rel_root, file) if rel_root else file + all_files.append(rel_path) # Remove duplicates and sort all_files = sorted(set(all_files)) diff --git a/ra_aid/project_state.py b/ra_aid/project_state.py index d8e3121..9f62fd5 100644 --- a/ra_aid/project_state.py +++ b/ra_aid/project_state.py @@ -28,7 +28,7 @@ def is_new_project(directory: str) -> bool: A project is considered new if it either: - Is an empty directory - - Contains only .git directory, .gitignore file, and/or .ra-aid directory + - Contains only .git directory, .gitignore file, .venv directory, and/or .ra-aid directory Args: directory: String path to the directory to check diff --git a/ra_aid/prompts/ciayn_prompts.py b/ra_aid/prompts/ciayn_prompts.py index f7f6162..cd7589f 100644 --- a/ra_aid/prompts/ciayn_prompts.py +++ b/ra_aid/prompts/ciayn_prompts.py @@ -23,7 +23,7 @@ You are a ReAct agent. You run in a loop and use ONE of the available functions The result of that function call will be given to you in the next message. Call one function at a time. Function arguments can be complex objects, long strings, etc. if needed. Each tool call you make shall be different from the previous. -The user cannot see the results of function calls, so you have to explicitly use a tool like ask_human if you want them to see something. +The user cannot see the results of function calls, so you have to explicitly use a tool (function call) if you want them to see something. If you don't know what to do, just make a best guess on what function to call. YOU MUST ALWAYS RESPOND WITH A SINGLE LINE OF PYTHON THAT CALLS ONE OF THE AVAILABLE TOOLS. NEVER RETURN AN EMPTY MESSAGE. diff --git a/ra_aid/tools/list_directory.py b/ra_aid/tools/list_directory.py index 1cc7085..ad17ced 100644 --- a/ra_aid/tools/list_directory.py +++ b/ra_aid/tools/list_directory.py @@ -185,9 +185,10 @@ def list_directory_tree( exclude_patterns: List[str] = None, ) -> str: """List directory contents in a tree format with optional metadata. + If a file path is provided, returns information about just that file. Args: - path: Directory path to list + path: Directory or file path to list max_depth: Maximum depth to traverse (default: 1 for no recursion) follow_links: Whether to follow symbolic links show_size: Show file sizes (default: False) @@ -200,24 +201,38 @@ def list_directory_tree( root_path = Path(path).resolve() if not root_path.exists(): raise ValueError(f"Path does not exist: {path}") - if not root_path.is_dir(): - raise ValueError(f"Path is not a directory: {path}") - - # Load .gitignore patterns if present - spec = load_gitignore_patterns(root_path) - - # Create tree - tree = Tree(f"📁 {root_path}/") - config = DirScanConfig( - max_depth=max_depth, - follow_links=follow_links, - show_size=show_size, - show_modified=show_modified, - exclude_patterns=DEFAULT_EXCLUDE_PATTERNS + (exclude_patterns or []), - ) - - # Build tree - build_tree(root_path, tree, config, 0, spec) + + # Load .gitignore patterns if present (only needed for directories) + spec = None + if root_path.is_dir(): + spec = load_gitignore_patterns(root_path) + # Create tree for directory + tree = Tree(f"📁 {root_path}/") + config = DirScanConfig( + max_depth=max_depth, + follow_links=follow_links, + show_size=show_size, + show_modified=show_modified, + exclude_patterns=DEFAULT_EXCLUDE_PATTERNS + (exclude_patterns or []), + ) + # Build tree + build_tree(root_path, tree, config, 0, spec) + else: + # Create a simple tree for a single file + tree = Tree(f"🗋 {root_path.parent}/") + file_text = root_path.name + + # Add size information if requested + if show_size: + size_str = format_size(root_path.stat().st_size) + file_text = f"{file_text} ({size_str})" + + # Add modified time if requested + if show_modified: + mod_time = format_time(root_path.stat().st_mtime) + file_text = f"{file_text} [Modified: {mod_time}]" + + tree.add(file_text) # Capture tree output with console.capture() as capture: diff --git a/tests/ra_aid/tools/test_list_directory.py b/tests/ra_aid/tools/test_list_directory.py index 91a8d66..ce11ee9 100644 --- a/tests/ra_aid/tools/test_list_directory.py +++ b/tests/ra_aid/tools/test_list_directory.py @@ -130,8 +130,5 @@ def test_invalid_path(): """Test error handling for invalid paths""" with pytest.raises(ValueError, match="Path does not exist"): list_directory_tree.invoke({"path": "/nonexistent/path"}) - - with pytest.raises(ValueError, match="Path is not a directory"): - list_directory_tree.invoke( - {"path": __file__} - ) # Try to list the test file itself + + # We now allow files to be passed to list_directory_tree, so we don't test for this case anymore diff --git a/tests/test_list_file.py b/tests/test_list_file.py new file mode 100644 index 0000000..8b52c02 --- /dev/null +++ b/tests/test_list_file.py @@ -0,0 +1,39 @@ +"""Test for list_directory_tree with file path support.""" + +import tempfile +import os +from pathlib import Path + +from ra_aid.tools import list_directory_tree + + +def test_list_directory_tree_with_file(): + """Test that list_directory_tree works with a file path.""" + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(b"Some test content") + tmp_file_path = tmp_file.name + + try: + # Test with file path + result = list_directory_tree.invoke({"path": tmp_file_path}) + + # Basic verification that the output contains the filename + filename = os.path.basename(tmp_file_path) + assert filename in result + + # Test with size option + result_with_size = list_directory_tree.invoke({"path": tmp_file_path, "show_size": True}) + assert "(" in result_with_size # Size information should be present + + # Test with modified time option + result_with_time = list_directory_tree.invoke({"path": tmp_file_path, "show_modified": True}) + assert "Modified:" in result_with_time + finally: + # Clean up the temporary file + if os.path.exists(tmp_file_path): + os.unlink(tmp_file_path) + + +if __name__ == "__main__": + test_list_directory_tree_with_file() + print("All tests passed!") diff --git a/tests/test_non_git_file_listing.py b/tests/test_non_git_file_listing.py new file mode 100644 index 0000000..4e0e707 --- /dev/null +++ b/tests/test_non_git_file_listing.py @@ -0,0 +1,81 @@ +"""Test file listing for non-git directories.""" + +import os +import tempfile +from pathlib import Path + +import pytest + +from ra_aid.file_listing import get_file_listing + + +def test_non_git_file_listing(): + """Test that file listing works correctly for non-git directories.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create a few test files + file1 = Path(temp_dir) / "file1.txt" + file2 = Path(temp_dir) / "file2.py" + file3 = Path(temp_dir) / ".hidden_file" # Hidden file + + # Create a subdirectory with files + subdir = Path(temp_dir) / "subdir" + os.makedirs(subdir) + file4 = subdir / "file4.js" + + # Create excluded directories + ra_aid_dir = Path(temp_dir) / ".ra-aid" + venv_dir = Path(temp_dir) / ".venv" + os.makedirs(ra_aid_dir) + os.makedirs(venv_dir) + + # Create files in excluded directories + ra_aid_file = ra_aid_dir / "config.json" + venv_file = venv_dir / "activate" + + # Write content to all files + for file_path in [file1, file2, file3, file4, ra_aid_file, venv_file]: + with open(file_path, "w") as f: + f.write("test content") + + # Test regular file listing (should exclude hidden files and directories) + files, count = get_file_listing(temp_dir) + assert count == 3 # file1.txt, file2.py, subdir/file4.js + assert set(files) == {"file1.txt", "file2.py", os.path.join("subdir", "file4.js")} + + # Test with include_hidden=True + files_with_hidden, count_with_hidden = get_file_listing(temp_dir, include_hidden=True) + assert count_with_hidden == 4 # Including .hidden_file + assert ".hidden_file" in files_with_hidden + + # Test with limit + files_limited, count_limited = get_file_listing(temp_dir, limit=2) + assert len(files_limited) == 2 + assert count_limited == 3 # Total count should still be 3 + + +def test_non_git_directory_with_excluded_dirs(): + """Test that excluded directories are properly handled in non-git directories.""" + with tempfile.TemporaryDirectory() as temp_dir: + # Create regular files + file1 = Path(temp_dir) / "file1.txt" + with open(file1, "w") as f: + f.write("test content") + + # Create excluded directories with files + excluded_dirs = [".ra-aid", ".venv", ".git", ".aider", "__pycache__"] + for excluded_dir in excluded_dirs: + dir_path = Path(temp_dir) / excluded_dir + os.makedirs(dir_path) + with open(dir_path / "test_file.txt", "w") as f: + f.write("test content") + + # Get file listing + files, count = get_file_listing(temp_dir) + + # Should only include the regular file + assert count == 1 + assert files == ["file1.txt"] + + +if __name__ == "__main__": + pytest.main(["-xvs", __file__])