From 7da2465cadaaf6656d0e49823d26717810e4b94f Mon Sep 17 00:00:00 2001 From: Glenn Date: Fri, 29 Dec 2023 14:13:20 +0100 Subject: [PATCH] Initial import --- .gitignore | 1 + Makefile | 5 ++ README.md | 34 ++++++++++ prompts/footer.txt | 3 + shard.yml | 23 +++++++ src/cli.cr | 164 +++++++++++++++++++++++++++++++++++++++++++++ src/main.cr | 11 +++ 7 files changed, 241 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 README.md create mode 100644 prompts/footer.txt create mode 100644 shard.yml create mode 100644 src/cli.cr create mode 100644 src/main.cr diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e660fd9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +bin/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aa9dbfe --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ + +all: build + +build: + shards build diff --git a/README.md b/README.md new file mode 100644 index 0000000..9bbd17d --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# README + +code-preloader is a tool that helps preloading all files for a given root directory +and concatenates them as a single file. + +## Structure of the output file + +```jinja2 +{{ HEADER_PROMPT_FILE }} + +{% for file in file_list %} +@@ File "{{ file.path }}" +{{ file.content }} +{% endfor %} + +{{ FOOTER_PROMPT_FILE }} +``` + +## Usage + +``` +Usage: code-preloader [options] ROOT_DIR + +Options: + -c, --config=CONFIG_FILE Load parameters from CONFIG_FILE (ignore, output, etc.) + -i, --ignore=IGNORE_FILE Ignore file or directory path (not a pattern) + -o, --output=OUTPUT_FILE Write output to OUTPUT_FILE (default to stdout if option missing or OUTPUT_FILE is "-") + --header-prompt=HEADER_PROMPT_FILE Load header prompt from PROMPT_FILE + --footer-prompt=FOOTER_PROMPT_FILE Load footer prompt from PROMPT_FILE + --version Show version + -v, --verbose Enable verbose mode + -h, --help Show this help +``` + diff --git a/prompts/footer.txt b/prompts/footer.txt new file mode 100644 index 0000000..248cdab --- /dev/null +++ b/prompts/footer.txt @@ -0,0 +1,3 @@ +@@ REQUEST + +Please follow crystal lang good practices, idioms and conventions. Please analyze code for every .cr file and tell me what we should change or fix (if needed) to improve the code and match the Usage described in README.md diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..0c8afa8 --- /dev/null +++ b/shard.yml @@ -0,0 +1,23 @@ +name: chatgpt-preloader +version: 0.1.0 + +targets: + chatgpt-preloader: + main: src/main.cr + +authors: + - Glenn Y. Rolland + +# description: | +# Short description of chatgpt-preloader + +# dependencies: +# pg: +# github: will/crystal-pg +# version: "~> 0.5" + +# development_dependencies: +# webmock: +# github: manastech/webmock.cr + +# license: MIT diff --git a/src/cli.cr b/src/cli.cr new file mode 100644 index 0000000..d09bb67 --- /dev/null +++ b/src/cli.cr @@ -0,0 +1,164 @@ +# vim: set ts=2 sw=2 et ft=crystal: + +require "file" +require "option_parser" + +# The CodePreloader module organizes classes and methods related to preloading code files. +module CodePreloader + # The Cli class handles command-line interface operations for the CodePreloader. + class Cli + getter repo_path : String + getter ignore_list : Array(String) = [] of String + getter output_file_path : String + getter preamble_file_path : String? + getter header_prompt_file_path : String? # Add type annotation + getter footer_prompt_file_path : String? # Assuming you'll also need this + + # Initializes the Cli class with default values. + def initialize + @repo_path = "" + @output_file_path = "" + end + + # Parses command-line arguments and initializes the necessary configurations. + def parse_arguments(args : Array(String)) + OptionParser.parse(args) do |parser| + parser.banner = "Usage: code-preloader [options] ROOT_DIR" + + parser.on("-c CONFIG_FILE", "--config=CONFIG_FILE", "Load parameters from CONFIG_FILE") do |config_file| + load_config(config_file) + end + + parser.on("-i IGNORE_PATH", "--ignore=IGNORE_PATH", "Ignore file or directory") do |ignore_file| + @ignore_list << ignore_file + end + + parser.on("-o OUTPUT_FILE", "--output=OUTPUT_FILE", "Write output to OUTPUT_FILE") do |output_file| + @output_file_path = output_file + end + + parser.on("--header-prompt=HEADER_PROMPT_FILE", "Load header prompt from HEADER_PROMPT_FILE") do |header_prompt_file| + @header_prompt_file_path = header_prompt_file + end + + parser.on("--footer-prompt=FOOTER_PROMPT_FILE", "Load footer prompt from FOOTER_PROMPT_FILE") do |footer_prompt_file| + @footer_prompt_file_path = footer_prompt_file + end + + parser.on("-h", "--help", "Show this help") do + STDERR.puts parser + exit + end + + parser.unknown_args do |remaining_args, _| + if remaining_args.size != 1 + abort("Invalid number of arguments. Expected exactly one argument for ROOT_DIR.") + end + @repo_path = remaining_args[0] + end + end + + validate_arguments + end + + # Executes the main functionality of the CLI application. + def exec + if preamble_file_path + STDERR.puts "Loading preamble from: #{preamble_file_path}" + end + + if header_prompt_file_path + STDERR.puts "Loading header prompt from: #{header_prompt_file_path}" + end + + if footer_prompt_file_path + STDERR.puts "Loading footer prompt from: #{footer_prompt_file_path}" + end + + STDERR.puts "Processing repository: #{repo_path}" + process_repository + STDERR.puts "Processing completed. Output written to: #{@output_file_path.empty? ? "stdout" : @output_file_path}" + + rescue e : Exception + STDERR.puts "An error occurred during execution: #{e.message}" + exit(1) + end + + # Processes the specified repository and writes the output to a file. + def process_repository + local_output_file_path = @output_file_path + must_close = false + output_file = STDOUT + + if !local_output_file_path.empty? && (local_output_file_path != "-") + output_file = File.open(local_output_file_path, "w") + must_close = true + end + + output_file.puts preamble_text if preamble_file_path + process_directory(repo_path, output_file) + output_file.close if must_close + + rescue e : IO::Error + STDERR.puts "Error processing repository: #{e.message}" + exit(1) + end + + private def process_directory(path : String, output_file : IO::FileDescriptor) + Dir.each_child(path) do |child| + child_path = File.join(path, child) + + ignores = ( + ignore_list + .map{ |prefix| [prefix, File.expand_path(child_path) =~ /^#{File.expand_path(prefix)}/] } + .reject!{ |item| item[1].nil? } + ) + next if !ignores.empty? + + puts "File: #{child_path}" + child_path = File.join(path, child) + if File.directory?(child_path) + process_directory(child_path, output_file) + else + process_file(child_path, output_file) + end + end + end + + private def process_file(file_path : String, output_file : IO::FileDescriptor) + relative_file_path = file_path.sub(/^#{Regex.escape(repo_path)}/, ".").lstrip + output_file.puts "@@ File \"#{relative_file_path}\"" + output_file.puts "" + output_file.puts(File.read(file_path)) + output_file.puts "" + end + + private def preamble_text : String + local_preamble_file_path = @preamble_file_path + return "" if local_preamble_file_path.nil? + + File.read(local_preamble_file_path) + rescue e : IO::Error + STDERR.puts "Error reading preamble file: #{e.message}" + exit(1) + end + + private def validate_arguments + abort("Missing repository path.") if repo_path.empty? + STDERR.puts("Output file path not specified (using STDOUT)") if output_file_path.empty? + end + + # Reads and returns a list of paths to ignore from the given file. + def self.get_ignore_list(ignore_file_path : String) : Array(String) + File.exists?(ignore_file_path) ? File.read_lines(ignore_file_path).map(&.strip) : [] of String + rescue e : IO::Error + STDERR.puts "Error reading ignore file: #{e.message}" + exit(1) + end + + # Loads configuration from a config file. + private def load_config(config_file_path : String) + # Implement configuration loading logic here + end + end +end diff --git a/src/main.cr b/src/main.cr new file mode 100644 index 0000000..0706111 --- /dev/null +++ b/src/main.cr @@ -0,0 +1,11 @@ + +# vim: set ts=2 sw=2 et ft=crystal: + +require "./cli" + +# Now that we have checked for nil, it's safe to use not_nil! +app = CodePreloader::Cli.new +app.parse_arguments(ARGV) +app.exec() + +