require "option_parser" require "yaml" # require "completion" require "./models/root_config" require "./version" module CodePreloader class Config enum Subcommand None Init Pack Help Version end class HelpOptions property parser_snapshot : OptionParser? = nil end class InitOptions property config_path : String? = nil end class PackOptions property config_path : String? = nil property source_list : Array(String) = [] of String property exclude_list : Array(String) = [] of String property include_list : Array(String) = [] of String property output_path : String? property prompt_template_path : String? property prompt_header_path : String? property prompt_footer_path : String? end getter? verbose : Bool = false getter? trace : Bool = false getter parser : OptionParser? getter subcommand : Subcommand = Subcommand::None getter pack_options : PackOptions? getter init_options : InitOptions? getter help_options : HelpOptions? def initialize() end def parse_init_options(parser) @init_options = InitOptions.new parser.banner = [ "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_path = arg end end parser.on( "-c FILE", "--config=FILE", "Load parameters from FILE" ) do |config_file| @init_options.try { |opt| opt.config_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 # complete_with "code-preloader init", parser end def parse_pack_options(parser) @pack_options = PackOptions.new unless ENV["CODE_PRELOADER_DETECT"]? =~ /(no|false|0)/i config_file = detect_config_file config_file.try { |path| load_pack_config(path) } end parser.banner = [ "Usage: code-preloader pack [options] DIR ...\n", "Global options:" ].join("\n") parser.separator "\nPack options:" parser.on( "-c FILE", "--config=FILE", "Load parameters from FILE\n(default: autodetect)" ) do |config_file| @pack_options.try { |opt| load_pack_config(config_file) } end parser.on( "-F FILE", "--prompt-footer=FILE", "Load prompt footer from FILE (default: none)" ) do |prompt_footer_path| @pack_options.try { |opt| opt.prompt_footer_path = prompt_footer_path } end parser.on( "-H FILE", "--prompt-header=FILE", "Load prompt header from FILE (default: none)" ) do |prompt_header_path| @pack_options.try { |opt| opt.prompt_header_path = prompt_header_path } end parser.on( "-i REGEXP", "--include=REGEXP", "Include file or directory. Can be used\nmultiple times (default: none)" ) do |include_regexp| @pack_options.try { |opt| opt.include_list << include_regexp } end parser.on( "-e REGEXP", "--exclude=REGEXP", "Exclude file or directory. Can be used\nmultiple times (default: none)" ) do |exclude_regexp| @pack_options.try { |opt| opt.exclude_list << exclude_regexp } end parser.on( "-o FILE", "--output=FILE", "Write output to FILE (default: \"-\", STDOUT)" ) do |output_file| @pack_options.try { |opt| opt.output_path = output_file } end parser.on( "-t FILE", "--template=FILE", "Load template from FILE (default: internal)" ) do |prompt_template_path| @pack_options.try { |opt| opt.prompt_template_path = prompt_template_path } end parser.separator "" parser.unknown_args do |remaining_args, _| remaining_args.each do |arg| @pack_options.try { |opt| opt.source_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 # complete_with "code-preloader pack", parser end def parse_arguments(args : Array(String)) @parser = OptionParser.new do |parser| parser.banner = [ "Usage: code-preloader [options] [DIR] [...]\n", "Global options:" ].join("\n") parser.on("-h", "--help", "Show this help") do @subcommand = Subcommand::Help @help_options = HelpOptions.new @help_options.try do |opts| opts.parser_snapshot = parser.dup end end parser.on("-v", "--verbose", "Enable verbose mode") do @verbose = true end parser.on("-t", "--trace", "Show detailed traces for exceptions") do @trace = true end parser.on("--version", "Show version") do @subcommand = Subcommand::Version 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 # complete_with "code-preloader", parser end @parser.try &.parse(args) validate end def detect_config_file() : String? home_dir = ENV["HOME"] possible_files = [ File.join(".code_preloader.yaml"), File.join(".code_preloader.yml"), File.join(home_dir, ".config", "code_preloader", "config.yaml"), File.join(home_dir, ".config", "code_preloader", "config.yml"), File.join(home_dir, ".config", "code_preloader.yaml"), File.join(home_dir, ".config", "code_preloader.yml"), File.join("/etc", "code_preloader", "config.yaml"), File.join("/etc", "code_preloader", "config.yml"), ] possible_files.each do |file_path| return file_path if File.exists?(file_path) end return nil end private def validate case @subcommand when Subcommand::Init then validate_init when Subcommand::Pack then validate_pack when Subcommand::None, Subcommand::Help, Subcommand::Version # do nothing 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 opts = @pack_options abort("No pack options defined!") if opts.nil? abort("Missing repository path.") if opts.source_list.empty? end # Reads and returns a list of paths to ignore from the given file. def self.get_ignore_list(ignore_path : String) : Array(String) File.exists?(ignore_path) ? File.read_lines(ignore_path).map(&.strip) : [] of String rescue e : IO::Error STDERR.puts "Error reading ignore file: #{e.message}" exit(1) end private def load_pack_config(config_path : String) opts = @pack_options abort("No pack options defined!") if opts.nil? config_str = File.read(config_path) root = Models::RootConfig.from_yaml(config_str) opts.config_path = config_path if opts.source_list.nil? || opts.source_list.try &.empty? root.source_list.try { |value| opts.source_list = value } end if opts.exclude_list.nil? || opts.exclude_list.try &.empty? root.exclude_list.try { |value| opts.exclude_list = value } end if opts.include_list.nil? || opts.include_list.try &.empty? root.include_list.try { |value| opts.include_list = value } end if opts.output_path.nil? opts.output_path = root.output_path end if opts.prompt_header_path.nil? root.prompt.try &.header_path.try { |value| opts.prompt_header_path = value } end if opts.prompt_footer_path.nil? root.prompt.try &.footer_path.try { |value| opts.prompt_footer_path = value } end if opts.prompt_template_path.nil? root.prompt.try &.template_path.try { |value| opts.prompt_template_path = value } end rescue ex : Exception STDERR.puts "Failed to load config file: #{ex.message}" exit(1) end end end