import argparse
import os
import sys
from promptprep.aggregator import CodeAggregator
from promptprep.config import ConfigManager
if sys.platform != "win32":
# Import TUI function only if not on Windows
from promptprep.tui import select_files_interactive
else:
# Define a dummy function for Windows
# It should match the signature and return type of the real one
def select_files_interactive(directory: str) -> tuple[set[str], set[str], bool]:
print("Error: Interactive mode is not supported on Windows.", file=sys.stderr)
# Return values indicating cancellation/no selection
return set(), set(), False
[docs]
def parse_arguments() -> argparse.Namespace:
"""Sets up and handles command-line interface options for the code aggregation tool."""
parser = argparse.ArgumentParser(
description="Aggregate code files into a master file with a directory tree."
)
# Group for exclusive commands: standard aggregation or diff
action_group = parser.add_mutually_exclusive_group()
# Add the main --diff trigger to the *mutually exclusive* group
action_group.add_argument(
"--diff",
metavar="PREV_FILE",
dest="prev_file",
type=str,
help="Compare a previous aggregation file with the current one. Cannot be used with other action flags.", # Example help text
)
# Create a separate argument group for *visual* organization of diff options in help
diff_options_group = parser.add_argument_group(
"File Diff Options (used with --diff)"
)
# Add the diff-specific options to the *visual* group
diff_options_group.add_argument(
"--diff-context",
type=int,
default=3,
help="Number of context lines to include in diff (default: 3)",
)
diff_options_group.add_argument(
"--diff-output", type=str, help="Write diff to specified file instead of stdout"
)
# Standard arguments
parser.add_argument(
"-c",
"--clipboard",
action="store_true",
help="Copy aggregated content to the clipboard instead of writing to a file.",
)
parser.add_argument(
"-d",
"--directory",
type=str,
default=os.getcwd(),
help="The directory to start aggregation from. Defaults to the current directory.",
)
parser.add_argument(
"-o",
"--output-file",
type=str,
default="full_code.txt",
help="Name of the output file. Defaults to full_code.txt.",
)
parser.add_argument(
"-i",
"--include-files",
type=str,
default="",
help="Comma-separated list of files to include. If not provided, all files are included.",
)
parser.add_argument(
"-x",
"--extensions",
type=str,
default="",
help="Comma-separated list of programming extensions to use. Replaces the default set if provided.",
)
parser.add_argument(
"-e",
"--exclude-dirs",
type=str,
default="",
help="Comma-separated list of directories to exclude. Replaces the default set if provided.",
)
parser.add_argument(
"-m",
"--max-file-size",
type=float,
default=100.0,
help="Maximum file size in MB to include. Files larger than this will be skipped. Defaults to 100 MB.",
)
parser.add_argument(
"--interactive",
action="store_true",
help="Use interactive TUI mode to select files/directories to include or exclude.",
)
parser.add_argument(
"--incremental",
action="store_true",
help="Only process files that have changed since the last run.",
)
parser.add_argument(
"--last-run-timestamp",
type=float,
default=None,
help="Timestamp of the last run (Unix epoch time). Required when using --incremental.",
)
parser.add_argument(
"--summary-mode",
action="store_true",
help="Include only function/class declarations and docstrings.",
)
parser.add_argument(
"--include-comments",
action="store_true",
default=True,
help="Include comments in the aggregated output. Defaults to True.",
)
parser.add_argument(
"--no-include-comments",
dest="include_comments",
action="store_false",
help="Exclude comments from the aggregated output.",
)
parser.add_argument(
"--metadata",
action="store_true",
help="Collect and append codebase metadata (LOC, comment ratio, etc.).",
)
parser.add_argument(
"--count-tokens",
action="store_true",
help="Count tokens in the output file and include in metadata.",
)
parser.add_argument(
"--token-model",
type=str,
default="cl100k_base",
help="The tokenizer model to use for counting tokens. Common options: cl100k_base (for GPT-4), p50k_base (for GPT-3).",
)
parser.add_argument(
"--format",
type=str,
choices=["plain", "markdown", "html", "highlighted", "custom"],
default="plain",
help="Output format. Options: plain (default), markdown, html, highlighted and custom.",
)
parser.add_argument(
"--line-numbers",
action="store_true",
help="Include line numbers in the output. Defaults to False.",
)
parser.add_argument(
"--template-file",
type=str,
default=None,
help="Path to a custom template file (required for --format custom).",
)
parser.add_argument(
"--save-config",
type=str,
metavar="CONFIG_FILE",
nargs="?",
const="default",
help="Save current configuration to a file. Uses default location (~/.promptprep/config.json) if no path is provided.",
)
parser.add_argument(
"--load-config",
type=str,
metavar="CONFIG_FILE",
nargs="?",
const="default",
help="Load configuration from a file. Uses default location (~/.promptprep/config.json) if no path is provided.",
)
return parser.parse_args()
[docs]
def main() -> None:
"""Main entry point for the code aggregation tool."""
args = parse_arguments()
# Load saved configuration if requested
if args.load_config:
try:
config_file = None if args.load_config == "default" else args.load_config
config_dict = ConfigManager.load_config(config_file)
args = ConfigManager.apply_config_to_args(config_dict, args)
if config_file:
print(f"Configuration loaded from '{config_file}'.")
else:
print("Configuration loaded from default location.")
except FileNotFoundError as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
include_files = set()
exclude_dirs = set()
should_continue = True
# Ensure template is provided for custom format
if args.format == "custom" and not args.template_file:
print(
"Error: --template-file is required when using --format custom",
file=sys.stderr,
)
sys.exit(1)
# Let users select files interactively if requested
if args.interactive:
print("Starting interactive file selection...")
include_files, exclude_dirs, should_continue = select_files_interactive(
args.directory
)
if should_continue:
print(
f"Selected {len(include_files)} files to include and {len(exclude_dirs)} directories to exclude."
)
else:
print("Interactive selection canceled. No files will be processed.")
return
else:
include_files = {f.strip() for f in args.include_files.split(",") if f.strip()}
exclude_dirs = {d.strip() for d in args.exclude_dirs.split(",") if d.strip()}
programming_extensions = {
e.strip() for e in args.extensions.split(",") if e.strip()
}
# Save current configuration if requested
if args.save_config:
try:
config_file = None if args.save_config == "default" else args.save_config
saved_path = ConfigManager.save_config(args, config_file)
print(f"Configuration saved to '{saved_path}'.")
# Exit if only saving config
if len(sys.argv) == 2 and "--save-config" in sys.argv[1]:
return
if len(sys.argv) == 3 and "--save-config" in sys.argv[1:]:
return
except IOError as e:
print(f"Error saving configuration: {e}", file=sys.stderr)
sys.exit(1)
try:
aggregator = CodeAggregator(
directory=args.directory,
output_file=args.output_file,
include_files=include_files,
programming_extensions=(
programming_extensions if programming_extensions else None
),
exclude_dirs=exclude_dirs if exclude_dirs else None,
max_file_size_mb=args.max_file_size,
summary_mode=args.summary_mode,
include_comments=args.include_comments,
collect_metadata=args.metadata,
count_tokens=args.count_tokens,
token_model=args.token_model,
output_format=args.format,
line_numbers=args.line_numbers,
template_file=args.template_file,
incremental=args.incremental,
last_run_timestamp=args.last_run_timestamp,
)
# Handle file comparison if requested
if hasattr(args, "prev_file") and args.prev_file:
if not os.path.exists(args.prev_file):
print(
f"Error: Previous file not found: {args.prev_file}", file=sys.stderr
)
sys.exit(1)
try:
# Generate current output if needed
if not os.path.exists(args.output_file):
print(
f"Current output file '{args.output_file}' does not exist. Generating it..."
)
aggregator.write_to_file()
# Show differences between files
diff_result = aggregator.compare_files(
file1=args.prev_file,
file2=args.output_file,
output_file=args.diff_output,
context_lines=args.diff_context,
)
if args.diff_output:
print(diff_result)
else:
print(
f"Diff between {os.path.basename(args.prev_file)} and {os.path.basename(args.output_file)}:"
)
print(diff_result)
return
except Exception as e:
print(f"Error generating diff: {e}", file=sys.stderr)
sys.exit(1)
# Handle regular aggregation
elif args.clipboard:
if aggregator.copy_to_clipboard():
print("Aggregated content copied to the clipboard successfully.")
else:
print("Failed to copy content to the clipboard.")
raise SystemExit(1)
else:
aggregator.write_to_file()
print(f"Aggregated file '{args.output_file}' created successfully.")
except FileNotFoundError as e:
print(f"Error: Directory not found: {e}", file=sys.stderr)
sys.exit(1)
except IOError as e:
print(f"Error: File error: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main() # pragma: no cover