diff --git a/src/cli.cr b/src/cli.cr index 0ace4160..22807769 100644 --- a/src/cli.cr +++ b/src/cli.cr @@ -3,6 +3,8 @@ require "./commands/*" module Shards BUILTIN_COMMANDS = %w[ + add + rm build run check @@ -23,6 +25,8 @@ module Shards Commands: build [] [] - Build the specified in `bin` path, all build_options are delegated to `crystal build`. check - Verify all dependencies are installed. + add [] [] - Add a shard to `shard.yml`, then install it. + rm [] - Remove a shard from `shard.yml`, then prune. init - Initialize a `shard.yml` file. install - Install dependencies, creating or using the `shard.lock` file. list [--tree] - List installed dependencies. @@ -90,6 +94,20 @@ module Shards Commands::Run.run(path, targets, run_options, options) when "check" Commands::Check.run(path) + when "add" + urls = args[1..-1] + version = nil + if urls.size > 1 && !urls.last.includes?("://") && !urls.last.includes?('@') + version = urls.pop + end + Commands::Add.new(path).run(urls, version) + when "rm" + url = args[1] + if url.nil? + Log.error{"ERROR: missing dependency URL"} + exit 1 + end + Commands::Remove.new(path).run(url) when "init" Commands::Init.run(path) when "install" diff --git a/src/commands/add.cr b/src/commands/add.cr new file mode 100644 index 00000000..2628729b --- /dev/null +++ b/src/commands/add.cr @@ -0,0 +1,92 @@ +require "./command" +require "./io" + +module Shards + module Commands + class Add < Command + def run(urls : Array(String), version : String? = nil) + spec_path = File.join(path, SPEC_FILENAME) + raise Error.new("#{SPEC_FILENAME} not found") unless File.exists?(spec_path) + + urls.each do |url| + dep = Commands.git_url_to_dependency(url) + Log.info { "Adding dependency: #{dep[:name]} from #{dep[:provider]}: #{dep[:repo]}" } + + lines = File.read_lines(spec_path) + dependencies_index = -1 + dependencies_indentation = "" + + lines.each_with_index do |line, index| + if line =~ /^(\s*)dependencies\s*:/ + dependencies_index = index + dependencies_indentation = $1 + break + end + end + + if dependencies_index == -1 + lines << "" if lines.last != "" + lines << "dependencies:" + dependencies_index = lines.size - 1 + dependencies_indentation = "" + end + + dep_name = dep[:name] + dep_start_index = -1 + dep_end_index = -1 + + (dependencies_index + 1).upto(lines.size - 1) do |i| + break if i >= lines.size || lines[i] =~ /^\S/ && !lines[i].starts_with?("#") + if lines[i] =~ /^\s+#{Regex.escape(dep_name)}\s*:/ + dep_start_index = i + + j = i + 1 + while j < lines.size && (lines[j].empty? || lines[j].starts_with?("#") || lines[j] =~ /^\s+/) + if lines[j] =~ /^(\s+)/ && $1.size > lines[i].index(/\S/).not_nil! + dep_end_index = j + end + j += 1 + end + + break + end + end + + dep_indentation = "#{dependencies_indentation} " + prop_indentation = "#{dependencies_indentation} " + dep_lines = ["#{dep_indentation}#{dep_name}:"] + + dep_lines << "#{prop_indentation}#{dep[:provider]}: #{dep[:repo]}" + dep_lines << "#{prop_indentation}version: #{version}" if version + + if dep_start_index != -1 + lines.delete_at(dep_start_index..dep_end_index) + dep_lines.each_with_index do |line, idx| + lines.insert(dep_start_index + idx, line) + end + else + insert_index = dependencies_index + 1 + + while insert_index < lines.size && + (lines[insert_index].empty? || + lines[insert_index].starts_with?("#") || + lines[insert_index] =~ /^\s+/) + insert_index += 1 + end + + dep_lines.each_with_index do |line, idx| + lines.insert(insert_index + idx, line) + end + end + + File.write(spec_path, lines.join("\n")) + Log.info { "Added dependency #{dep[:name]} from #{dep[:provider]}: #{dep[:repo]}#{version ? " with version #{version}" : ""}" } + end + + Commands::Lock.new(path).run([] of String) + Commands::Install.new(path).run + end + + end + end +end \ No newline at end of file diff --git a/src/commands/io.cr b/src/commands/io.cr new file mode 100644 index 00000000..af510bd6 --- /dev/null +++ b/src/commands/io.cr @@ -0,0 +1,41 @@ +module Shards + module Commands + def self.read_shard_yml : Array(String) + begin + File.read_lines("shard.yml") + rescue + Log.error{"No shard.yml was found in the current directory."} + exit 1 + end + end + + def self.write_shard_yml(lines : Array(String)) + File.write("shard.yml", lines.join("\n")) + end + + def self.git_url_to_dependency(url : String) : NamedTuple(name: String, repo: String, provider: String) + hosts = { + "github.com" => "github", + "gitlab.com" => "gitlab", + "codeberg.org" => "codeberg" + } + + uri = URI.parse(url) + provider = hosts[uri.host]? + parts = uri.path.split("/").reject(&.empty?) + + if parts.size < 2 + raise Error.new("Invalid git URL format") + end + if !provider + provider = "github" + end + + return { + name: parts.last.gsub(".git", "").downcase, + repo: "#{parts[0]}/#{parts[1]}", + provider: provider + } + end + end +end diff --git a/src/commands/remove.cr b/src/commands/remove.cr new file mode 100644 index 00000000..7a7fb4ba --- /dev/null +++ b/src/commands/remove.cr @@ -0,0 +1,84 @@ +require "./command" +require "./io" + +module Shards + module Commands + class Remove < Command + def run(url : String) + dep = Commands.git_url_to_dependency(url) + Log.info{"Removing dependency: #{dep[:name]}"} + + lines = Commands.read_shard_yml + dependencies_index = -1 + + lines.each_with_index do |line, index| + if line =~ /^(\s*)dependencies\s*:/ + dependencies_index = index + break + end + end + + if dependencies_index == -1 + Log.warn{"Dependency #{dep[:name]} not found, nothing to remove."} + return + end + + dep_name = dep[:name] + dep_start_index = -1 + dep_end_index = -1 + dep_indentation = nil + + (dependencies_index + 1).upto(lines.size - 1) do |i| + break if i >= lines.size || (lines[i] =~ /^\S/ && !lines[i].starts_with?("#")) + + if lines[i] =~ /^(\s+)#{Regex.escape(dep_name)}\s*:/ + dep_start_index = i + dep_indentation = $1.size + + j = i + 1 + while j < lines.size + if !lines[j].empty? && !lines[j].starts_with?("#") && lines[j] =~ /^(\s*)\S/ + current_indent = $1.size + if current_indent <= dep_indentation + break + end + dep_end_index = j + end + j += 1 + end + + break + end + end + + if dep_start_index != -1 + if dep_end_index != -1 + lines.delete_at(dep_start_index..dep_end_index) + else + lines.delete_at(dep_start_index) + end + + has_other_deps = false + (dependencies_index + 1).upto(lines.size - 1) do |i| + break if i >= lines.size || (lines[i] =~ /^\S/ && !lines[i].starts_with?("#")) + if lines[i] =~ /^\s+\S+\s*:/ + has_other_deps = true + break + end + end + + if !has_other_deps + lines.delete_at(dependencies_index) + end + + Commands.write_shard_yml(lines) + Commands::Prune.new(path).run + + Log.info{"Removed dependency #{dep[:name]}."} + else + Log.warn{"Dependency: #{dep[:name]} not found, nothing to remove."} + end + end + end + end +end \ No newline at end of file