code-preloader/src/config.cr
Glenn e057581c51
Some checks failed
continuous-integration/drone/push Build is failing
fix(config): add short option for version command
Enhancing user experience by providing a shorthand option for version
command, reducing potential user errors and improving command-line
efficiency.

- Added `-v` as a shorthand for the `--version` command in the parser.
- Ensures quicker access to version information, aligning with common
  CLI practices.

Signed-off-by: Glenn <glenux@glenux.net>
2025-06-16 16:47:08 +02:00

319 lines
8.9 KiB
Crystal

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 <subcommand> [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("-v", "--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