2023-12-29 15:53:54 +01:00
|
|
|
require "option_parser"
|
|
|
|
require "yaml"
|
2024-01-04 12:42:34 +01:00
|
|
|
# require "completion"
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2023-12-29 16:31:17 +01:00
|
|
|
require "./models/root_config"
|
2024-01-03 17:03:40 +01:00
|
|
|
require "./version"
|
2023-12-29 16:31:17 +01:00
|
|
|
|
2023-12-29 15:53:54 +01:00
|
|
|
module CodePreloader
|
|
|
|
class Config
|
2024-01-04 11:53:50 +01:00
|
|
|
|
|
|
|
enum Subcommand
|
|
|
|
None
|
|
|
|
Init
|
|
|
|
Pack
|
|
|
|
Help
|
|
|
|
Version
|
|
|
|
end
|
|
|
|
|
2024-01-04 22:51:04 +01:00
|
|
|
class HelpOptions
|
|
|
|
property parser_snapshot : OptionParser? = nil
|
|
|
|
end
|
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
class InitOptions
|
2024-01-04 22:51:04 +01:00
|
|
|
property config_path : String? = nil
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
class PackOptions
|
2024-01-04 22:51:04 +01:00
|
|
|
property config_path : String? = nil
|
|
|
|
property source_list : Array(String) = [] of String
|
2025-06-16 16:14:09 +02:00
|
|
|
property exclude_list : Array(String) = [] of String
|
|
|
|
property include_list : Array(String) = [] of String
|
2024-01-04 22:51:04 +01:00
|
|
|
property output_path : String?
|
|
|
|
property prompt_template_path : String?
|
|
|
|
property prompt_header_path : String?
|
|
|
|
property prompt_footer_path : String?
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
2025-06-16 16:14:09 +02:00
|
|
|
getter? verbose : Bool = false
|
|
|
|
getter? trace : Bool = false
|
2024-01-04 11:53:50 +01:00
|
|
|
getter parser : OptionParser?
|
2024-01-04 22:51:04 +01:00
|
|
|
getter subcommand : Subcommand = Subcommand::None
|
|
|
|
getter pack_options : PackOptions?
|
|
|
|
getter init_options : InitOptions?
|
|
|
|
getter help_options : HelpOptions?
|
2023-12-29 15:53:54 +01:00
|
|
|
|
|
|
|
def initialize()
|
|
|
|
end
|
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
def parse_init_options(parser)
|
|
|
|
@init_options = InitOptions.new
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
parser.banner = [
|
|
|
|
"Usage: code-preloader init [options]\n",
|
|
|
|
"Global options:"
|
|
|
|
].join("\n")
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
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|
|
2024-01-04 22:51:04 +01:00
|
|
|
@init_options.try &.config_path = arg
|
2023-12-29 15:53:54 +01:00
|
|
|
end
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2025-06-16 16:14:09 +02:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-c FILE",
|
|
|
|
"--config=FILE",
|
2024-01-04 11:53:50 +01:00
|
|
|
"Load parameters from FILE"
|
|
|
|
) do |config_file|
|
2024-01-04 22:51:04 +01:00
|
|
|
@init_options.try { |opt| opt.config_path = config_file }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
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
|
2024-01-04 12:42:34 +01:00
|
|
|
|
|
|
|
# complete_with "code-preloader init", parser
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
2025-06-16 16:14:09 +02:00
|
|
|
def parse_pack_options(parser)
|
2024-01-04 11:53:50 +01:00
|
|
|
@pack_options = PackOptions.new
|
|
|
|
|
2024-01-05 11:47:52 +01:00
|
|
|
unless ENV["CODE_PRELOADER_DETECT"]? =~ /(no|false|0)/i
|
2025-06-16 16:14:09 +02:00
|
|
|
config_file = detect_config_file
|
2024-01-05 11:47:52 +01:00
|
|
|
config_file.try { |path| load_pack_config(path) }
|
|
|
|
end
|
2024-01-04 22:51:04 +01:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
parser.banner = [
|
|
|
|
"Usage: code-preloader pack [options] DIR ...\n",
|
|
|
|
"Global options:"
|
|
|
|
].join("\n")
|
|
|
|
|
|
|
|
parser.separator "\nPack options:"
|
2024-01-04 22:51:04 +01:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-c FILE",
|
|
|
|
"--config=FILE",
|
2024-01-05 11:47:52 +01:00
|
|
|
"Load parameters from FILE\n(default: autodetect)"
|
2024-01-04 22:51:04 +01:00
|
|
|
) do |config_file|
|
|
|
|
@pack_options.try { |opt| load_pack_config(config_file) }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-F FILE",
|
|
|
|
"--prompt-footer=FILE",
|
2024-01-04 22:51:04 +01:00
|
|
|
"Load prompt footer from FILE (default: none)"
|
|
|
|
) do |prompt_footer_path|
|
|
|
|
@pack_options.try { |opt| opt.prompt_footer_path = prompt_footer_path }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-H FILE",
|
|
|
|
"--prompt-header=FILE",
|
2024-01-04 22:51:04 +01:00
|
|
|
"Load prompt header from FILE (default: none)"
|
|
|
|
) do |prompt_header_path|
|
|
|
|
@pack_options.try { |opt| opt.prompt_header_path = prompt_header_path }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-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 }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-o FILE",
|
|
|
|
"--output=FILE",
|
2024-01-04 22:51:04 +01:00
|
|
|
"Write output to FILE (default: \"-\", STDOUT)"
|
|
|
|
) do |output_file|
|
|
|
|
@pack_options.try { |opt| opt.output_path = output_file }
|
|
|
|
end
|
|
|
|
|
|
|
|
parser.on(
|
2025-06-16 16:14:09 +02:00
|
|
|
"-t FILE",
|
|
|
|
"--template=FILE",
|
2024-01-04 22:51:04 +01:00
|
|
|
"Load template from FILE (default: internal)"
|
|
|
|
) do |prompt_template_path|
|
|
|
|
@pack_options.try { |opt| opt.prompt_template_path = prompt_template_path }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
parser.separator ""
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
parser.unknown_args do |remaining_args, _|
|
|
|
|
remaining_args.each do |arg|
|
2024-01-04 22:51:04 +01:00
|
|
|
@pack_options.try { |opt| opt.source_list << arg }
|
2023-12-29 15:53:54 +01:00
|
|
|
end
|
2024-01-04 11:53:50 +01:00
|
|
|
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
|
2024-01-04 12:42:34 +01:00
|
|
|
|
|
|
|
# complete_with "code-preloader pack", parser
|
2024-01-04 11:53:50 +01:00
|
|
|
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")
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 22:51:04 +01:00
|
|
|
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
|
|
|
|
|
2025-06-16 16:14:09 +02:00
|
|
|
parser.on("-t", "--trace", "Show detailed traces for exceptions") do
|
|
|
|
@trace = true
|
|
|
|
end
|
|
|
|
|
2025-06-16 16:47:08 +02:00
|
|
|
parser.on("-v", "--version", "Show version") do
|
2024-01-04 13:05:41 +01:00
|
|
|
@subcommand = Subcommand::Version
|
2024-01-03 17:03:40 +01:00
|
|
|
end
|
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
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}")
|
2023-12-29 15:53:54 +01:00
|
|
|
end
|
2024-01-04 12:42:34 +01:00
|
|
|
|
|
|
|
# complete_with "code-preloader", parser
|
2023-12-29 15:53:54 +01:00
|
|
|
end
|
|
|
|
|
2024-01-04 11:53:50 +01:00
|
|
|
@parser.try &.parse(args)
|
2024-01-02 12:32:00 +01:00
|
|
|
validate
|
2023-12-29 15:53:54 +01:00
|
|
|
end
|
|
|
|
|
2024-01-04 22:51:04 +01:00
|
|
|
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
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
|
|
|
|
2024-01-02 12:32:00 +01:00
|
|
|
private def validate
|
2024-01-04 11:53:50 +01:00
|
|
|
case @subcommand
|
|
|
|
when Subcommand::Init then validate_init
|
|
|
|
when Subcommand::Pack then validate_pack
|
2024-01-04 12:42:34 +01:00
|
|
|
when Subcommand::None, Subcommand::Help, Subcommand::Version
|
|
|
|
# do nothing
|
2024-01-04 11:53:50 +01:00
|
|
|
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
|
2024-01-04 16:35:39 +01:00
|
|
|
opts = @pack_options
|
|
|
|
abort("No pack options defined!") if opts.nil?
|
2024-01-04 22:51:04 +01:00
|
|
|
abort("Missing repository path.") if opts.source_list.empty?
|
2023-12-29 15:53:54 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# Reads and returns a list of paths to ignore from the given file.
|
2024-01-04 22:51:04 +01:00
|
|
|
def self.get_ignore_list(ignore_path : String) : Array(String)
|
|
|
|
File.exists?(ignore_path) ? File.read_lines(ignore_path).map(&.strip) : [] of String
|
2023-12-29 15:53:54 +01:00
|
|
|
rescue e : IO::Error
|
|
|
|
STDERR.puts "Error reading ignore file: #{e.message}"
|
|
|
|
exit(1)
|
|
|
|
end
|
|
|
|
|
2024-01-04 22:51:04 +01:00
|
|
|
private def load_pack_config(config_path : String)
|
2024-01-04 16:35:39 +01:00
|
|
|
opts = @pack_options
|
2024-01-04 22:51:04 +01:00
|
|
|
abort("No pack options defined!") if opts.nil?
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 22:51:04 +01:00
|
|
|
config_str = File.read(config_path)
|
2023-12-29 16:31:17 +01:00
|
|
|
root = Models::RootConfig.from_yaml(config_str)
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-04 22:51:04 +01:00
|
|
|
opts.config_path = config_path
|
|
|
|
if opts.source_list.nil? || opts.source_list.try &.empty?
|
|
|
|
root.source_list.try { |value| opts.source_list = value }
|
2024-01-04 16:35:39 +01:00
|
|
|
end
|
2025-06-16 16:14:09 +02:00
|
|
|
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 }
|
2024-01-04 16:35:39 +01:00
|
|
|
end
|
2024-01-04 22:51:04 +01:00
|
|
|
if opts.output_path.nil?
|
2025-06-16 16:14:09 +02:00
|
|
|
opts.output_path = root.output_path
|
2024-01-04 22:51:04 +01:00
|
|
|
end
|
|
|
|
if opts.prompt_header_path.nil?
|
|
|
|
root.prompt.try &.header_path.try { |value| opts.prompt_header_path = value }
|
2024-01-04 16:35:39 +01:00
|
|
|
end
|
2024-01-04 22:51:04 +01:00
|
|
|
if opts.prompt_footer_path.nil?
|
|
|
|
root.prompt.try &.footer_path.try { |value| opts.prompt_footer_path = value }
|
2024-01-04 16:35:39 +01:00
|
|
|
end
|
2024-01-04 22:51:04 +01:00
|
|
|
if opts.prompt_template_path.nil?
|
|
|
|
root.prompt.try &.template_path.try { |value| opts.prompt_template_path = value }
|
2024-01-04 11:53:50 +01:00
|
|
|
end
|
2023-12-29 15:53:54 +01:00
|
|
|
|
2024-01-02 12:32:00 +01:00
|
|
|
rescue ex : Exception
|
2023-12-29 15:53:54 +01:00
|
|
|
STDERR.puts "Failed to load config file: #{ex.message}"
|
|
|
|
exit(1)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|