From c21822afbaca986e3f73978bbe77c73b3c8caaa9 Mon Sep 17 00:00:00 2001 From: Abin Thomas Date: Fri, 2 Aug 2024 00:59:33 +0530 Subject: [PATCH] More features --- .gitignore | 3 + MANIFEST.in | 3 + repopack/__init__.py | 5 + repopack/__main__.py | 5 + repopack/cli.py | 46 + repopack/config.py | 28 + repopack/output_generator.py | 29 + repopack/packager.py | 28 + repopack/repopack-output.txt | 2484 ++++++++++++++++++++++++++++++ repopack/utils/cli_output.py | 16 + repopack/utils/file_handler.py | 44 + repopack/utils/ignore_utils.py | 33 + repopack/utils/logger.py | 48 + repopack/utils/spinner.py | 18 + repopack/utils/tree_generator.py | 39 + repopack/version.py | 2 + requirements.txt | 4 + setup.py | 41 + 18 files changed, 2876 insertions(+) create mode 100644 MANIFEST.in create mode 100644 repopack/__init__.py create mode 100644 repopack/__main__.py create mode 100644 repopack/cli.py create mode 100644 repopack/config.py create mode 100644 repopack/output_generator.py create mode 100644 repopack/packager.py create mode 100644 repopack/repopack-output.txt create mode 100644 repopack/utils/cli_output.py create mode 100644 repopack/utils/file_handler.py create mode 100644 repopack/utils/ignore_utils.py create mode 100644 repopack/utils/logger.py create mode 100644 repopack/utils/spinner.py create mode 100644 repopack/utils/tree_generator.py create mode 100644 repopack/version.py create mode 100644 requirements.txt create mode 100644 setup.py diff --git a/.gitignore b/.gitignore index 68bc17f..c278cf0 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,6 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +# Mac +.DS_Store diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..86abfd2 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,3 @@ +include README.md +include LICENSE +recursive-include repopack *.py diff --git a/repopack/__init__.py b/repopack/__init__.py new file mode 100644 index 0000000..abe3849 --- /dev/null +++ b/repopack/__init__.py @@ -0,0 +1,5 @@ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] diff --git a/repopack/__main__.py b/repopack/__main__.py new file mode 100644 index 0000000..2a0ed68 --- /dev/null +++ b/repopack/__main__.py @@ -0,0 +1,5 @@ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + \ No newline at end of file diff --git a/repopack/cli.py b/repopack/cli.py new file mode 100644 index 0000000..6f90172 --- /dev/null +++ b/repopack/cli.py @@ -0,0 +1,46 @@ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.cli_output import print_summary, print_completion +from .utils.logger import logger +from .utils.spinner import Spinner +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + spinner = Spinner("Packing files...") + try: + spinner.start() + pack_result = pack(os.path.abspath(args.directory), merged_config) + spinner.succeed("Packing completed successfully!") + + print_summary(pack_result['total_files'], pack_result['total_characters'], merged_config['output']['file_path']) + print_completion() + except Exception as e: + spinner.fail(f"Error during packing: {str(e)}") + logger.error(str(e)) + exit(1) + +if __name__ == "__main__": + run_cli() diff --git a/repopack/config.py b/repopack/config.py new file mode 100644 index 0000000..9c298cc --- /dev/null +++ b/repopack/config.py @@ -0,0 +1,28 @@ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged diff --git a/repopack/output_generator.py b/repopack/output_generator.py new file mode 100644 index 0000000..ede7d16 --- /dev/null +++ b/repopack/output_generator.py @@ -0,0 +1,29 @@ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") diff --git a/repopack/packager.py b/repopack/packager.py new file mode 100644 index 0000000..3994753 --- /dev/null +++ b/repopack/packager.py @@ -0,0 +1,28 @@ +import os +from typing import Dict, Any +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + return { + "total_files": total_files, + "total_characters": total_characters, + } diff --git a/repopack/repopack-output.txt b/repopack/repopack-output.txt new file mode 100644 index 0000000..bcb429a --- /dev/null +++ b/repopack/repopack-output.txt @@ -0,0 +1,2484 @@ +================================================================ +Repopack Output File +================================================================ + +This file was generated by Repopack on: 2024-08-02T00:21:29.751151 + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +Repository Structure: +--------------------- +utils/ + cli_output.py + file_handler.py + ignore_utils.py + logger.py + spinner.py + tree_generator.py +__init__.py +__main__.py +cli.py +config.py +output_generator.py +packager.py +repopack-output.txt +version.py + +================================================================ +Repository Files +================================================================ + +================ +File: config.py +================ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged + +================ +File: version.py +================ +# repopack/version.py +__version__ = "0.1.0" + +================ +File: packager.py +================ +import os +from typing import Dict, Any +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + return { + "total_files": total_files, + "total_characters": total_characters, + } + +================ +File: __init__.py +================ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] + +================ +File: repopack-output.txt +================ +================================================================ +Repopack Output File +================================================================ + +This file was generated by Repopack on: 2024-08-02T00:21:21.654047 + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +Repository Structure: +--------------------- +utils/ + cli_output.py + file_handler.py + ignore_utils.py + logger.py + spinner.py + tree_generator.py +__init__.py +__main__.py +cli.py +config.py +output_generator.py +packager.py +repopack-output.txt +version.py + +================================================================ +Repository Files +================================================================ + +================ +File: config.py +================ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged + +================ +File: version.py +================ +# repopack/version.py +__version__ = "0.1.0" + +================ +File: packager.py +================ +import os +from typing import Dict, Any +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + return { + "total_files": total_files, + "total_characters": total_characters, + } + +================ +File: __init__.py +================ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] + +================ +File: repopack-output.txt +================ +================================================================ +Repopack Output File +================================================================ + +This file was generated by Repopack on: 2024-08-02T00:21:18.766686 + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +Repository Structure: +--------------------- +utils/ + cli_output.py + file_handler.py + ignore_utils.py + logger.py + spinner.py + tree_generator.py +__init__.py +__main__.py +cli.py +config.py +output_generator.py +packager.py +repopack-output.txt +version.py + +================================================================ +Repository Files +================================================================ + +================ +File: config.py +================ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged + +================ +File: version.py +================ +# repopack/version.py +__version__ = "0.1.0" + +================ +File: packager.py +================ +import os +from typing import Dict, Any +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + return { + "total_files": total_files, + "total_characters": total_characters, + } + +================ +File: __init__.py +================ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] + +================ +File: repopack-output.txt +================ +================================================================ +Repopack Output File +================================================================ + +This file was generated by Repopack on: 2024-08-02T00:21:01.716563 + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +Repository Structure: +--------------------- +utils/ + cli_output.py + file_handler.py + ignore_utils.py + logger.py + spinner.py + tree_generator.py +__init__.py +__main__.py +cli.py +config.py +output_generator.py +packager.py +repopack-output.txt +version.py + +================================================================ +Repository Files +================================================================ + +================ +File: config.py +================ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged + +================ +File: version.py +================ +# repopack/version.py +__version__ = "0.1.0" + +================ +File: packager.py +================ +import os +from typing import Dict, Any +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + return { + "total_files": total_files, + "total_characters": total_characters, + } + +================ +File: __init__.py +================ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] + +================ +File: repopack-output.txt +================ +================================================================ +Repopack Output File +================================================================ + +This file was generated by Repopack on: 2024-08-02T00:07:50.951163 + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +Repository Structure: +--------------------- +utils/ + cli_output.py + file_handler.py + ignore_utils.py + logger.py + tree_generator.py +__init__.py +__main__.py +cli.py +config.py +output_generator.py +packager.py +repopack-output.txt +version.py + +================================================================ +Repository Files +================================================================ + +================ +File: config.py +================ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged + +================ +File: version.py +================ +# repopack/version.py +__version__ = "0.1.0" + +================ +File: packager.py +================ +import os +from typing import Dict, Any +from .utils.cli_output import print_summary, print_completion +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + result ={ + "total_files": total_files, + "total_characters": total_characters, + } + + print_summary(total_files, total_characters, config['output']['file_path']) + print_completion() + + return result + +================ +File: __init__.py +================ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] + +================ +File: repopack-output.txt +================ +================================================================ +Repopack Output File +================================================================ + +This file was generated by Repopack on: 2024-08-02T00:02:21.567312 + +Purpose: +-------- +This file contains a packed representation of the entire repository's contents. +It is designed to be easily consumable by AI systems for analysis, code review, +or other automated processes. + +Repository Structure: +--------------------- +utils/ + file_handler.py + ignore_utils.py + logger.py + tree_generator.py +__init__.py +__main__.py +cli.py +config.py +output_generator.py +packager.py +version.py + +================================================================ +Repository Files +================================================================ + +================ +File: config.py +================ +import json +from typing import Dict, Any + +DEFAULT_CONFIG = { + "output": { + "file_path": "repopack-output.txt", + "style": "plain", + "remove_comments": False, + "remove_empty_lines": False, + }, + "ignore": { + "use_gitignore": True, + "use_default_patterns": True, + "custom_patterns": [], + }, +} + +def load_config(config_path: str = None) -> Dict[str, Any]: + if config_path: + with open(config_path, 'r') as f: + return json.load(f) + return {} + +def merge_configs(file_config: Dict[str, Any], cli_config: Dict[str, Any]) -> Dict[str, Any]: + merged = DEFAULT_CONFIG.copy() + merged.update(file_config) + merged.update(cli_config) + return merged + +================ +File: version.py +================ +# repopack/version.py +__version__ = "0.1.0" + +================ +File: packager.py +================ +import os +from typing import Dict, Any +from .utils.file_handler import sanitize_files +from .utils.ignore_utils import get_all_ignore_patterns, create_ignore_filter +from .output_generator import generate_output + +def pack(root_dir: str, config: Dict[str, Any]) -> Dict[str, Any]: + ignore_patterns = get_all_ignore_patterns(root_dir, config) + ignore_filter = create_ignore_filter(ignore_patterns) + + all_file_paths = [] + for root, _, files in os.walk(root_dir): + for file in files: + file_path = os.path.relpath(os.path.join(root, file), root_dir) + if ignore_filter(file_path): + all_file_paths.append(file_path) + + sanitized_files = sanitize_files(all_file_paths, root_dir, config) + + generate_output(root_dir, config, sanitized_files, all_file_paths) + + total_files = len(sanitized_files) + total_characters = sum(len(file['content']) for file in sanitized_files) + + return { + "total_files": total_files, + "total_characters": total_characters, + } + +================ +File: __init__.py +================ +from .packager import pack +from .cli import run_cli +from .version import __version__ + +__all__ = ['pack', 'run_cli', '__version__'] + +================ +File: cli.py +================ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.logger import logger +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + try: + pack_result = pack(os.path.abspath(args.directory), merged_config) + print(f"Packed {pack_result['total_files']} files ({pack_result['total_characters']} characters) to {merged_config['output']['file_path']}") + except Exception as e: + logger.error(f"Error during packing: {str(e)}") + exit(1) + +if __name__ == "__main__": + run_cli() + +================ +File: output_generator.py +================ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") + +================ +File: __main__.py +================ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + +================ +File: utils/tree_generator.py +================ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() + +================ +File: utils/file_handler.py +================ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) + +================ +File: utils/logger.py +================ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() + +================ +File: utils/ignore_utils.py +================ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) + +================ +File: cli.py +================ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.logger import logger +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + try: + pack_result = pack(os.path.abspath(args.directory), merged_config) + print(f"Packed {pack_result['total_files']} files ({pack_result['total_characters']} characters) to {merged_config['output']['file_path']}") + except Exception as e: + logger.error(f"Error during packing: {str(e)}") + exit(1) + +if __name__ == "__main__": + run_cli() + +================ +File: output_generator.py +================ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") + +================ +File: __main__.py +================ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + +================ +File: utils/tree_generator.py +================ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() + +================ +File: utils/file_handler.py +================ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) + +================ +File: utils/logger.py +================ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() + +================ +File: utils/ignore_utils.py +================ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) + +================ +File: utils/cli_output.py +================ +import colorama +from colorama import Fore, Style + +colorama.init(autoreset=True) + +def print_summary(total_files, total_characters, output_path): + print(f"\n{Fore.CYAN}📊 Pack Summary:") + print(f"{Fore.CYAN}────────────────") + print(f"{Fore.WHITE}Total Files: {total_files}") + print(f"{Fore.WHITE}Total Chars: {total_characters}") + print(f"{Fore.WHITE} Output: {output_path}") + +def print_completion(): + print(f"\n{Fore.GREEN}🎉 All Done!") + print(f"{Fore.WHITE}Your repository has been successfully packed.") + +================ +File: cli.py +================ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.cli_output import print_summary, print_completion +from .utils.logger import logger +from .utils.spinner import Spinner +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + spinner = Spinner("Packing files...") + try: + spinner.start() + pack_result = pack(os.path.abspath(args.directory), merged_config) + spinner.succeed("Packing completed successfully!") + + print_summary(pack_result['total_files'], pack_result['total_characters'], merged_config['output']['file_path']) + print_completion() + except Exception as e: + spinner.fail(f"Error during packing: {str(e)}") + logger.error(str(e)) + exit(1) + +if __name__ == "__main__": + run_cli() + +================ +File: output_generator.py +================ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") + +================ +File: __main__.py +================ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + +================ +File: utils/tree_generator.py +================ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() + +================ +File: utils/file_handler.py +================ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) + +================ +File: utils/logger.py +================ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() + +================ +File: utils/spinner.py +================ +from halo import Halo + +class Spinner: + def __init__(self, message): + self.spinner = Halo(text=message, spinner='dots') + + def start(self): + self.spinner.start() + + def stop(self): + self.spinner.stop() + + def succeed(self, message): + self.spinner.succeed(message) + + def fail(self, message): + self.spinner.fail(message) + +================ +File: utils/ignore_utils.py +================ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) + +================ +File: utils/cli_output.py +================ +import colorama +from colorama import Fore, Style + +colorama.init(autoreset=True) + +def print_summary(total_files, total_characters, output_path): + print(f"\n{Fore.CYAN}📊 Pack Summary:") + print(f"{Fore.CYAN}────────────────") + print(f"{Fore.WHITE}Total Files: {total_files}") + print(f"{Fore.WHITE}Total Chars: {total_characters}") + print(f"{Fore.WHITE} Output: {output_path}") + +def print_completion(): + print(f"\n{Fore.GREEN}🎉 All Done!") + print(f"{Fore.WHITE}Your repository has been successfully packed.") + +================ +File: cli.py +================ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.cli_output import print_summary, print_completion +from .utils.logger import logger +from .utils.spinner import Spinner +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + spinner = Spinner("Packing files...") + try: + spinner.start() + pack_result = pack(os.path.abspath(args.directory), merged_config) + spinner.succeed("Packing completed successfully!") + + print_summary(pack_result['total_files'], pack_result['total_characters'], merged_config['output']['file_path']) + print_completion() + except Exception as e: + spinner.fail(f"Error during packing: {str(e)}") + logger.error(str(e)) + exit(1) + +if __name__ == "__main__": + run_cli() + +================ +File: output_generator.py +================ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") + +================ +File: __main__.py +================ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + +================ +File: utils/tree_generator.py +================ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() + +================ +File: utils/file_handler.py +================ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) + +================ +File: utils/logger.py +================ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() + +================ +File: utils/spinner.py +================ +from halo import Halo + +class Spinner: + def __init__(self, message): + self.spinner = Halo(text=message, spinner='dots') + + def start(self): + self.spinner.start() + + def stop(self): + self.spinner.stop() + + def succeed(self, message): + self.spinner.succeed(message) + + def fail(self, message): + self.spinner.fail(message) + +================ +File: utils/ignore_utils.py +================ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) + +================ +File: utils/cli_output.py +================ +import colorama +from colorama import Fore, Style + +colorama.init(autoreset=True) + +def print_summary(total_files, total_characters, output_path): + print(f"\n{Fore.CYAN}📊 Pack Summary:") + print(f"{Fore.CYAN}────────────────") + print(f"{Fore.WHITE}Total Files: {total_files}") + print(f"{Fore.WHITE}Total Chars: {total_characters}") + print(f"{Fore.WHITE} Output: {output_path}") + +def print_completion(): + print(f"\n{Fore.GREEN}🎉 All Done!") + print(f"{Fore.WHITE}Your repository has been successfully packed.") + +================ +File: cli.py +================ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.cli_output import print_summary, print_completion +from .utils.logger import logger +from .utils.spinner import Spinner +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + spinner = Spinner("Packing files...") + try: + spinner.start() + pack_result = pack(os.path.abspath(args.directory), merged_config) + spinner.succeed("Packing completed successfully!") + + print_summary(pack_result['total_files'], pack_result['total_characters'], merged_config['output']['file_path']) + print_completion() + except Exception as e: + spinner.fail(f"Error during packing: {str(e)}") + logger.error(str(e)) + exit(1) + +if __name__ == "__main__": + run_cli() + +================ +File: output_generator.py +================ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") + +================ +File: __main__.py +================ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + +================ +File: utils/tree_generator.py +================ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() + +================ +File: utils/file_handler.py +================ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) + +================ +File: utils/logger.py +================ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() + +================ +File: utils/spinner.py +================ +from halo import Halo + +class Spinner: + def __init__(self, message): + self.spinner = Halo(text=message, spinner='dots') + + def start(self): + self.spinner.start() + + def stop(self): + self.spinner.stop() + + def succeed(self, message): + self.spinner.succeed(message) + + def fail(self, message): + self.spinner.fail(message) + +================ +File: utils/ignore_utils.py +================ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) + +================ +File: utils/cli_output.py +================ +import colorama +from colorama import Fore, Style + +colorama.init(autoreset=True) + +def print_summary(total_files, total_characters, output_path): + print(f"\n{Fore.CYAN}📊 Pack Summary:") + print(f"{Fore.CYAN}────────────────") + print(f"{Fore.WHITE}Total Files: {total_files}") + print(f"{Fore.WHITE}Total Chars: {total_characters}") + print(f"{Fore.WHITE} Output: {output_path}") + +def print_completion(): + print(f"\n{Fore.GREEN}🎉 All Done!") + print(f"{Fore.WHITE}Your repository has been successfully packed.") + +================ +File: cli.py +================ +import argparse +import os +from .packager import pack +from .config import load_config, merge_configs +from .utils.cli_output import print_summary, print_completion +from .utils.logger import logger +from .utils.spinner import Spinner +from .version import __version__ + + +def run_cli(): + parser = argparse.ArgumentParser(description="Repopack - Pack your repository into a single AI-friendly file") + parser.add_argument("directory", nargs="?", default=".", help="Directory to pack") + parser.add_argument("-o", "--output", help="Specify the output file name") + parser.add_argument("-i", "--ignore", help="Additional ignore patterns (comma-separated)") + parser.add_argument("-c", "--config", help="Path to a custom config file") + parser.add_argument("--verbose", action="store_true", help="Enable verbose logging") + parser.add_argument("-v", "--version", action="version", version=f"Repopack v{__version__}") + args = parser.parse_args() + + logger.set_verbose(args.verbose) + + config = load_config(args.config) + cli_config = {} + if args.output: + cli_config["output"] = {"file_path": args.output} + if args.ignore: + cli_config["ignore"] = {"custom_patterns": args.ignore.split(",")} + + merged_config = merge_configs(config, cli_config) + + spinner = Spinner("Packing files...") + try: + spinner.start() + pack_result = pack(os.path.abspath(args.directory), merged_config) + spinner.succeed("Packing completed successfully!") + + print_summary(pack_result['total_files'], pack_result['total_characters'], merged_config['output']['file_path']) + print_completion() + except Exception as e: + spinner.fail(f"Error during packing: {str(e)}") + logger.error(str(e)) + exit(1) + +if __name__ == "__main__": + run_cli() + +================ +File: output_generator.py +================ +import os +from datetime import datetime +from typing import Dict, Any, List +from .utils.tree_generator import generate_tree_string + +def generate_output(root_dir: str, config: Dict[str, Any], sanitized_files: List[Dict[str, str]], all_file_paths: List[str]): + output_path = os.path.join(root_dir, config['output']['file_path']) + tree_string = generate_tree_string(all_file_paths) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write("=" * 64 + "\n") + f.write("Repopack Output File\n") + f.write("=" * 64 + "\n\n") + f.write(f"This file was generated by Repopack on: {datetime.now().isoformat()}\n\n") + f.write("Purpose:\n--------\n") + f.write("This file contains a packed representation of the entire repository's contents.\n") + f.write("It is designed to be easily consumable by AI systems for analysis, code review,\n") + f.write("or other automated processes.\n\n") + f.write("Repository Structure:\n---------------------\n") + f.write(tree_string + "\n\n") + f.write("=" * 64 + "\n") + f.write("Repository Files\n") + f.write("=" * 64 + "\n\n") + + for file in sanitized_files: + f.write("=" * 16 + "\n") + f.write(f"File: {file['path']}\n") + f.write("=" * 16 + "\n") + f.write(file['content'] + "\n\n") + +================ +File: __main__.py +================ +from .cli import run_cli + +if __name__ == "__main__": + run_cli() + +================ +File: utils/tree_generator.py +================ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() + +================ +File: utils/file_handler.py +================ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) + +================ +File: utils/logger.py +================ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() + +================ +File: utils/spinner.py +================ +from halo import Halo + +class Spinner: + def __init__(self, message): + self.spinner = Halo(text=message, spinner='dots') + + def start(self): + self.spinner.start() + + def stop(self): + self.spinner.stop() + + def succeed(self, message): + self.spinner.succeed(message) + + def fail(self, message): + self.spinner.fail(message) + +================ +File: utils/ignore_utils.py +================ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) + +================ +File: utils/cli_output.py +================ +import colorama +from colorama import Fore, Style + +colorama.init(autoreset=True) + +def print_summary(total_files, total_characters, output_path): + print(f"\n{Fore.CYAN}📊 Pack Summary:") + print(f"{Fore.CYAN}────────────────") + print(f"{Fore.WHITE}Total Files: {total_files}") + print(f"{Fore.WHITE}Total Chars: {total_characters}") + print(f"{Fore.WHITE} Output: {output_path}") + +def print_completion(): + print(f"\n{Fore.GREEN}🎉 All Done!") + print(f"{Fore.WHITE}Your repository has been successfully packed.") + diff --git a/repopack/utils/cli_output.py b/repopack/utils/cli_output.py new file mode 100644 index 0000000..ca52f69 --- /dev/null +++ b/repopack/utils/cli_output.py @@ -0,0 +1,16 @@ +import colorama +from colorama import Fore, Style + +colorama.init(autoreset=True) + +def print_summary(total_files, total_characters, output_path): + print(f"\n{Fore.CYAN}📊 Pack Summary:") + print(f"{Fore.CYAN}────────────────") + print(f"{Fore.WHITE}Total Files: {total_files}") + print(f"{Fore.WHITE}Total Chars: {total_characters}") + print(f"{Fore.WHITE} Output: {output_path}") + +def print_completion(): + print(f"\n{Fore.GREEN}🎉 All Done!") + print(f"{Fore.WHITE}Your repository has been successfully packed.") + \ No newline at end of file diff --git a/repopack/utils/file_handler.py b/repopack/utils/file_handler.py new file mode 100644 index 0000000..baf1f44 --- /dev/null +++ b/repopack/utils/file_handler.py @@ -0,0 +1,44 @@ +import os +import chardet +from typing import List, Dict, Any + +def is_binary(file_path: str) -> bool: + """Check if a file is binary.""" + try: + with open(file_path, 'tr') as check_file: + check_file.read() + return False + except: + return True + +def sanitize_files(file_paths: List[str], root_dir: str, config: Dict[str, Any]) -> List[Dict[str, str]]: + """Sanitize files based on the given configuration.""" + sanitized_files = [] + for file_path in file_paths: + full_path = os.path.join(root_dir, file_path) + if not is_binary(full_path): + content = sanitize_file(full_path, config) + if content: + sanitized_files.append({"path": file_path, "content": content}) + return sanitized_files + +def sanitize_file(file_path: str, config: Dict[str, Any]) -> str: + """Sanitize a single file.""" + with open(file_path, 'rb') as f: + raw_content = f.read() + + encoding = chardet.detect(raw_content)['encoding'] or 'utf-8' + content = raw_content.decode(encoding) + + if config['output']['remove_comments']: + # Implement comment removal logic here + pass + + if config['output']['remove_empty_lines']: + content = remove_empty_lines(content) + + return content.strip() + +def remove_empty_lines(content: str) -> str: + """Remove empty lines from the content.""" + return '\n'.join(line for line in content.splitlines() if line.strip()) diff --git a/repopack/utils/ignore_utils.py b/repopack/utils/ignore_utils.py new file mode 100644 index 0000000..487a607 --- /dev/null +++ b/repopack/utils/ignore_utils.py @@ -0,0 +1,33 @@ +import os +from typing import List, Dict, Any +from pathspec import PathSpec +from pathspec.patterns import GitWildMatchPattern + +DEFAULT_IGNORE_LIST = [ + '.git', '.gitignore', 'node_modules', '*.pyc', '__pycache__', + '.vscode', '.idea', '*.log', '*.swp', '*.swo' +] + +def get_ignore_patterns(filename: str, root_dir: str) -> List[str]: + """Get ignore patterns from a file.""" + ignore_path = os.path.join(root_dir, filename) + if os.path.exists(ignore_path): + with open(ignore_path, 'r') as f: + return [line.strip() for line in f if line.strip() and not line.startswith('#')] + return [] + +def get_all_ignore_patterns(root_dir: str, config: Dict[str, Any]) -> List[str]: + """Get all ignore patterns based on the configuration.""" + patterns = [] + if config['ignore']['use_default_patterns']: + patterns.extend(DEFAULT_IGNORE_LIST) + if config['ignore']['use_gitignore']: + patterns.extend(get_ignore_patterns('.gitignore', root_dir)) + patterns.extend(get_ignore_patterns('.repopackignore', root_dir)) + patterns.extend(config['ignore']['custom_patterns']) + return patterns + +def create_ignore_filter(patterns: List[str]): + """Create an ignore filter function based on the given patterns.""" + spec = PathSpec.from_lines(GitWildMatchPattern, patterns) + return lambda path: not spec.match_file(path) diff --git a/repopack/utils/logger.py b/repopack/utils/logger.py new file mode 100644 index 0000000..39c4bb9 --- /dev/null +++ b/repopack/utils/logger.py @@ -0,0 +1,48 @@ +import logging +from colorama import Fore, Style, init + +init(autoreset=True) + +class ColoredFormatter(logging.Formatter): + COLORS = { + 'DEBUG': Fore.BLUE, + 'INFO': Fore.CYAN, + 'WARNING': Fore.YELLOW, + 'ERROR': Fore.RED, + 'CRITICAL': Fore.RED + Style.BRIGHT, + } + + def format(self, record): + levelname = record.levelname + if levelname in self.COLORS: + record.levelname = f"{self.COLORS[levelname]}{levelname}{Style.RESET_ALL}" + return super().format(record) + +class Logger: + def __init__(self): + self.logger = logging.getLogger('repopack') + self.logger.setLevel(logging.INFO) + + console_handler = logging.StreamHandler() + console_handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s')) + self.logger.addHandler(console_handler) + + def set_verbose(self, verbose: bool): + self.logger.setLevel(logging.DEBUG if verbose else logging.INFO) + + def debug(self, message): + self.logger.debug(message) + + def info(self, message): + self.logger.info(message) + + def warning(self, message): + self.logger.warning(message) + + def error(self, message): + self.logger.error(message) + + def critical(self, message): + self.logger.critical(message) + +logger = Logger() diff --git a/repopack/utils/spinner.py b/repopack/utils/spinner.py new file mode 100644 index 0000000..2cbd44d --- /dev/null +++ b/repopack/utils/spinner.py @@ -0,0 +1,18 @@ +from halo import Halo + +class Spinner: + def __init__(self, message): + self.spinner = Halo(text=message, spinner='dots') + + def start(self): + self.spinner.start() + + def stop(self): + self.spinner.stop() + + def succeed(self, message): + self.spinner.succeed(message) + + def fail(self, message): + self.spinner.fail(message) + \ No newline at end of file diff --git a/repopack/utils/tree_generator.py b/repopack/utils/tree_generator.py new file mode 100644 index 0000000..78d3b3a --- /dev/null +++ b/repopack/utils/tree_generator.py @@ -0,0 +1,39 @@ +from typing import List, Dict + +class TreeNode: + def __init__(self, name: str, is_directory: bool = False): + self.name = name + self.children = [] + self.is_directory = is_directory + +def generate_file_tree(files: List[str]) -> TreeNode: + root = TreeNode('root', True) + for file in files: + parts = file.split('/') + current_node = root + for i, part in enumerate(parts): + is_last_part = i == len(parts) - 1 + child = next((c for c in current_node.children if c.name == part), None) + if not child: + child = TreeNode(part, not is_last_part) + current_node.children.append(child) + current_node = child + return root + +def sort_tree_nodes(node: TreeNode): + node.children.sort(key=lambda x: (not x.is_directory, x.name)) + for child in node.children: + sort_tree_nodes(child) + +def tree_to_string(node: TreeNode, prefix: str = '') -> str: + sort_tree_nodes(node) + result = '' + for child in node.children: + result += f"{prefix}{child.name}{'/' if child.is_directory else ''}\n" + if child.is_directory: + result += tree_to_string(child, prefix + ' ') + return result + +def generate_tree_string(files: List[str]) -> str: + tree = generate_file_tree(files) + return tree_to_string(tree).strip() diff --git a/repopack/version.py b/repopack/version.py new file mode 100644 index 0000000..13aeaa8 --- /dev/null +++ b/repopack/version.py @@ -0,0 +1,2 @@ +# repopack/version.py +__version__ = "0.1.0" diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f41b0c3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +chardet +colorama +halo +pathspec diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ba7f55a --- /dev/null +++ b/setup.py @@ -0,0 +1,41 @@ +from setuptools import setup, find_packages +import os + +# Read the contents of your README file +from pathlib import Path +this_directory = Path(__file__).parent +long_description = (this_directory / "README.md").read_text() + +# Get version +version = {} +with open(os.path.join("repopack", "version.py")) as fp: + exec(fp.read(), version) + +setup( + name="repopack", + version=version['__version__'], + author="Abin Thomas", + author_email="abinthomasonline@gmail.com", + description="A tool to pack repository contents into a single file for AI analysis", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/abinthomasonline/repopack-py", + packages=find_packages(), + install_requires=[ + "chardet", + "pathspec", + "colorama", + # Add any other dependencies your project needs + ], + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires=">=3.7", + entry_points={ + "console_scripts": [ + "repopack=repopack.cli:run_cli", + ], + }, +) \ No newline at end of file