-
-
Save Uninen/2515d89755f681f374355e6ee687f9c3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Inspired by https://github.com/jefftriplett/files-to-claude-xml | |
| """ | |
| import logging | |
| from contextlib import contextmanager | |
| from pathlib import Path | |
| from typing import Optional, Set | |
| from xml.sax import saxutils | |
| import glob2 | |
| import typer | |
| import yaml | |
| from pydantic import BaseModel, Field, ValidationError | |
| from rich import print | |
| from rich.logging import RichHandler | |
| from rich.progress import track | |
| logging.basicConfig(level=logging.INFO, format="%(message)s", handlers=[RichHandler(rich_tracebacks=True)]) | |
| log = logging.getLogger("claude-xml") | |
| class ProjectConfig(BaseModel): | |
| include: list[str] = Field( | |
| ..., | |
| description="Glob patterns for files to include", | |
| ) | |
| exclude: list[str] = Field(default=[], description="Glob patterns for files to exclude") | |
| project_details_path: Optional[Path] = Field(default=None, description="Optional path to project details file") | |
| class XMLCompilationError(Exception): | |
| pass | |
| @contextmanager | |
| def error_handler(operation: str): | |
| try: | |
| yield None | |
| except ValidationError as e: | |
| log.error("Configuration validation failed") | |
| for error in e.errors(): | |
| log.error(f" - {error['loc']}: {error['msg']}") | |
| raise typer.Exit(1) | |
| except XMLCompilationError as e: | |
| log.error(f"XML compilation failed: {e}") | |
| raise typer.Exit(1) | |
| except Exception as e: | |
| log.error(f"Error during {operation}: {str(e)}") | |
| raise typer.Exit(1) | |
| def resolve_paths(patterns: list[str], base_dir: Path) -> Set[Path]: | |
| paths: Set[Path] = set() | |
| for pattern in patterns: | |
| if Path(pattern).is_absolute(): | |
| full_pattern = pattern | |
| else: | |
| full_pattern = str(base_dir / pattern) | |
| paths.update(Path(p).resolve() for p in glob2.glob(full_pattern, recursive=True)) | |
| return paths | |
| def get_project_files(config: ProjectConfig, base_dir: Path) -> Set[Path]: | |
| included = resolve_paths(config.include, base_dir) | |
| excluded = resolve_paths(config.exclude, base_dir) | |
| return {p for p in (included - excluded) if p.is_file()} | |
| def load_config(config_path: Path) -> ProjectConfig: | |
| with open(config_path) as f: | |
| raw_config = yaml.safe_load(f) | |
| return ProjectConfig.model_validate(raw_config) | |
| def compile_xml(files: Set[Path], show_progress: bool = True) -> str: | |
| xml_parts = ['<?xml version="1.0" encoding="UTF-8"?>', "<documents>"] | |
| errors = [] | |
| file_iter = track(sorted(files)) if show_progress else sorted(files) | |
| for index, file in enumerate(file_iter, start=1): | |
| log.debug(f"Processing file: {file}") | |
| try: | |
| content = file.read_text(encoding="utf-8") | |
| # escape XML special characters | |
| escaped_content = saxutils.escape(content) | |
| relative_path = file.relative_to(Path.cwd()) | |
| escaped_path = saxutils.escape(relative_path.as_posix()) | |
| xml_parts.extend( | |
| [ | |
| f'<document index="{index}">', | |
| f"<source>{escaped_path}</source>", | |
| "<document_content>", | |
| escaped_content, | |
| "</document_content>", | |
| "</document>", | |
| ] | |
| ) | |
| except Exception as e: | |
| error_msg = f"Error processing {file}: {str(e)}" | |
| log.error(error_msg) | |
| errors.append(error_msg) | |
| # continue processing other files | |
| xml_parts.append("</documents>") | |
| if errors: | |
| raise XMLCompilationError(f"Failed to process {len(errors)} files. Check logs for details.") | |
| return "\n".join(xml_parts) | |
| app = typer.Typer(help=__doc__) | |
| @app.command() | |
| def main( | |
| config_path: Path = typer.Argument(default=".claude_project/config.yml", help="Path to configuration file"), | |
| output_path: Path = typer.Argument(default=".claude_project/project.xml", help="Output XML file path"), | |
| verbose: bool = typer.Option(False, "--verbose", "-v", help="Enable verbose logging"), | |
| quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress progress display"), | |
| ) -> None: | |
| log.setLevel(logging.DEBUG if verbose else logging.INFO) | |
| if not config_path.exists(): | |
| print(f"[red]Error:[/red] Configuration file {config_path} not found") | |
| raise typer.Exit(1) | |
| base_dir = Path.cwd() | |
| with error_handler("XML generation"): | |
| config = load_config(config_path) | |
| log.debug(f"Loaded configuration from {config_path}") | |
| project_files = get_project_files(config, base_dir) | |
| if not project_files: | |
| log.warning("No files matched the patterns") | |
| raise typer.Exit(0) | |
| log.info(f"Processing {len(project_files)} files...") | |
| xml_content = compile_xml(project_files, show_progress=not quiet) | |
| output_path.write_text(xml_content, encoding="utf-8") | |
| log.info(f"Successfully wrote XML to {output_path}") | |
| if __name__ == "__main__": | |
| app() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment