diff --git a/src/cli.cr b/src/cli.cr index 747912b..57209f1 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -18,20 +18,52 @@ module CodePreloader def initialize(args) @output_file_path = "" @config = Config.new() + @config.detect_config() @config.parse_arguments(args) end # Executes the main functionality of the CLI application. def exec - # get local values for typing - output_file_path = @output_file_path - repository_path_list = @config.repository_path_list - header_prompt_file_path = @config.header_prompt_file_path - footer_prompt_file_path = @config.footer_prompt_file_path + case @config.subcommand + when Config::Subcommand::Init then exec_init(@config.init_options) + when Config::Subcommand::Pack then exec_pack(@config.pack_options) + when Config::Subcommand::Version then exec_version + when Config::Subcommand::Help then exec_help + when Config::Subcommand::None then exec_none + else + abort("Unknown subcommand #{@config.subcommand}!") + end + end + + def exec_init(init_options) + abort("Unexpected nil value for init_options!") if init_options.nil? + abort("FIXME: Not implemented!") + end + + def exec_version + abort("FIXME: Not implemented!") + end + + def exec_none + abort("No command specified!") + end + + def exec_help + abort("FIXME: Not implemented!") + end + + def exec_pack(pack_options) + abort("Unexpected nil value for pack_options!") if pack_options.nil? + + output_file_path = pack_options.output_file_path + repository_path_list = pack_options.repository_path_list + header_prompt_file_path = pack_options.header_prompt_file_path + footer_prompt_file_path = pack_options.footer_prompt_file_path + regular_output_file = false filelist = FileList.new() filelist.add(repository_path_list) - @config.ignore_list.each do |ignore_pattern| + pack_options.ignore_list.each do |ignore_pattern| filelist.reject { |path| !!(path =~ Regex.new(ignore_pattern)) } end @@ -45,27 +77,28 @@ module CodePreloader footer_prompt = File.read(footer_prompt_file_path) end - unless output_file_path.nil? || output_file_path.try(&.empty?) || (output_file_path != "-") - output_file = File.open(output_file_path, "w") - invalid_output_file = false - end + output_file_path.try do |path| + break if path.empty? + break if path == "-" + regular_output_file = true + output_file = File.open(path, "w") + end - invalid_output_file = true output_file = STDOUT header_prompt = "" footer_prompt = "" - output_file.puts header_prompt if @config.header_prompt_file_path + output_file.puts header_prompt if header_prompt_file_path - STDERR.puts "Processing repository: #{@config.repository_path_list}" + STDERR.puts "Processing repository: #{repository_path_list}" filelist.each do |file_path| process_file(file_path, output_file) end - output_file.puts footer_prompt if @config.footer_prompt_file_path + output_file.puts footer_prompt if footer_prompt_file_path - output_file.close if !invalid_output_file - STDERR.puts "Processing completed. Output written to: #{invalid_output_file ? "stdout" : output_file_path}" + output_file.close if regular_output_file + STDERR.puts "Processing completed. Output written to: #{regular_output_file ? output_file_path : "stdout" }" rescue e : Exception STDERR.puts "An error occurred during execution: #{e.message}" diff --git a/src/config.cr b/src/config.cr index 9a866f5..b2e69c8 100644 --- a/src/config.cr +++ b/src/config.cr @@ -6,63 +6,151 @@ require "./version" module CodePreloader class Config - property repository_path_list : Array(String) = [] of String - property ignore_list : Array(String) = [] of String - property output_file_path : String? - property header_prompt_file_path : String? - property footer_prompt_file_path : String? + + enum Subcommand + None + Init + Pack + Help + Version + end + + class InitOptions + property config_file_path : String? = nil + end + + class PackOptions + property config_file_path : String? = nil + property repository_path_list : Array(String) = [] of String + property ignore_list : Array(String) = [] of String + property output_file_path : String? + property header_prompt_file_path : String? + property footer_prompt_file_path : String? + end + + getter parser : OptionParser? + property subcommand : Subcommand = Subcommand::None + property pack_options : PackOptions? + property init_options : InitOptions? def initialize() end + def parse_init_options(parser) + @init_options = InitOptions.new + + parser.banner = [ + "#{PROGRAM_NAME} v#{VERSION}", + "Usage: code-preloader init [options]\n", + "Global options:" + ].join("\n") + + parser.separator "\nInit options:" + parser.unknown_args do |remaining_args, _| + # FIXME: detect and make error if there are more or less than one + remaining_args.each do |arg| + @init_options.try &.config_file_path = arg + end + end + + parser.on( + "-c FILE", + "--config=FILE", + "Load parameters from FILE" + ) do |config_file| + @init_options.try { |opt| opt.config_file_path = config_file } + end + + parser.separator "" + + parser.missing_option do |opt| + puts parser + abort("ERROR: Missing parameter for option #{opt}!") + end + + parser.invalid_option do |opt| + puts parser + abort("ERROR: Invalid option #{opt}!") + end + end + + def parse_pack_options(parser) + @pack_options = PackOptions.new + + parser.banner = [ + "#{PROGRAM_NAME} v#{VERSION}", + "Usage: code-preloader pack [options] DIR ...\n", + "Global options:" + ].join("\n") + + parser.separator "\nPack options:" + parser.on( + "-i REGEXP", + "--ignore=REGEXP", + "Ignore file or directory" + ) do |ignore_file| + @pack_options.try { |opt| opt.ignore_list << ignore_file } + end + + parser.on( + "-o FILE", + "--output=FILE", + "Write output to FILE" + ) do |output_file| + @pack_options.try { |opt| opt.output_file_path = output_file } + end + + parser.on( + "-H FILE", + "--header-prompt=FILE", + "Load header prompt from FILE" + ) do |header_prompt_file| + @pack_options.try { |opt| opt.header_prompt_file_path = header_prompt_file } + end + + parser.on( + "-F FILE", + "--footer-prompt=FILE", + "Load footer prompt from FILE" + ) do |footer_prompt_file| + @pack_options.try { |opt| opt.footer_prompt_file_path = footer_prompt_file } + end + + parser.on( + "-c FILE", + "--config=FILE", + "Load parameters from FILE" + ) do |config_file| + @pack_options.try { |opt| load_pack_config(config_file) } + end + + parser.separator "" + + parser.unknown_args do |remaining_args, _| + remaining_args.each do |arg| + @pack_options.try { |opt| opt.repository_path_list << arg } + end + end + + parser.missing_option do |opt| + puts parser + abort("ERROR: Missing parameter for option #{opt}!") + end + + parser.invalid_option do |ex| + puts parser + abort("ERROR: Invalid option #{ex}") + end + end + def parse_arguments(args : Array(String)) - OptionParser.parse(args) do |parser| + @parser = OptionParser.new do |parser| parser.banner = [ "#{PROGRAM_NAME} v#{VERSION}", - "Usage: code-preloader [options] DIR ...\n", - "Options:" + "Usage: code-preloader [options] [DIR] [...]\n", + "Global options:" ].join("\n") - parser.on( - "-c FILE", - "--config=FILE", - "Load parameters from FILE" - ) do |config_file| - load_config(config_file) - end - - parser.on( - "-i REGEXP", - "--ignore=REGEXP", - "Ignore file or directory" - ) do |ignore_file| - @ignore_list << ignore_file - end - - parser.on( - "-o FILE", - "--output=FILE", - "Write output to FILE" - ) do |output_file| - @output_file_path = output_file - end - - parser.on( - "-H FILE", - "--header-prompt=FILE", - "Load header prompt from FILE" - ) do |header_prompt_file| - @header_prompt_file_path = header_prompt_file - end - - parser.on( - "-F FILE", - "--footer-prompt=FILE", - "Load footer prompt from FILE" - ) do |footer_prompt_file| - @footer_prompt_file_path = footer_prompt_file - end - parser.on("--version", "Show version") do STDOUT.puts "#{PROGRAM_NAME} #{VERSION}" exit(0) @@ -73,20 +161,56 @@ module CodePreloader exit end - parser.unknown_args do |remaining_args, _| - remaining_args.each do |arg| - @repository_path_list << arg - end + parser.separator "\nSubcommands:" + + parser.on("init", "Create an example .code_preloader.yml file") do + @subcommand = Subcommand::Init + parse_init_options(parser) + end + + parser.on("pack", "Create the packed version of a directory for LLM prompting") do + @subcommand = Subcommand::Pack + parse_pack_options(parser) + end + + parser.separator "" + + parser.invalid_option do |ex| + puts parser + abort("ERROR: Invalid option #{ex}") end end + @parser.try &.parse(args) validate end + def detect_config + # FIXME: detect config name, if any + end + private def validate - abort("Missing repository path.") if @repository_path_list.empty? - - STDERR.puts("Output file path not specified (using STDOUT)") if @output_file_path.nil? || @output_file_path.try(&.empty?) + case @subcommand + when Subcommand::Init then validate_init + when Subcommand::Pack then validate_pack + else + abort("Unknown subcommand #{@subcommand}") + end + end + + private def validate_init + abort("No init options defined!") if @init_options.nil? + end + + private def validate_pack + abort("No pack options defined!") if @pack_options.nil? + @pack_options.try do |opts| + abort("Missing repository path.") if opts.repository_path_list.empty? + + if opts.output_file_path.nil? || opts.output_file_path.try(&.empty?) + STDERR.puts("Output file path not specified (using STDOUT)") + end + end end # Reads and returns a list of paths to ignore from the given file. @@ -97,16 +221,19 @@ module CodePreloader exit(1) end - private def load_config(config_file_path : String) + private def load_pack_config(config_file_path : String) config_str = File.read(config_file_path) root = Models::RootConfig.from_yaml(config_str) - @repository_path = root.repository_path_list || @repository_path_list - @ignore_list = root.ignore_list || @ignore_list - @output_file_path = root.output_file_path || @output_file_path - @header_prompt_file_path = root.header_prompt_file_path || @header_prompt_file_path - @footer_prompt_file_path = root.footer_prompt_file_path || @footer_prompt_file_path + @pack_options.try do |opts| + opts.config_file_path = config_file_path + opts.repository_path_list = root.repository_path_list || opts.repository_path_list + opts.ignore_list = root.ignore_list || opts.ignore_list + opts.output_file_path = root.output_file_path || opts.output_file_path + opts.header_prompt_file_path = root.header_prompt_file_path || opts.header_prompt_file_path + opts.footer_prompt_file_path = root.footer_prompt_file_path || opts.footer_prompt_file_path + end rescue ex : Exception STDERR.puts "Failed to load config file: #{ex.message}"