lib/mix/tasks/graft.remove.ex

defmodule Mix.Tasks.Graft.Remove do
  @shortdoc "Remove siblings from the graft manifest"

  @moduledoc """
  Remove sibling entries from `graft.exs`.

      mix graft.remove req_llm --dry-run
      mix graft.remove req_llm
      mix graft.remove req_llm --delete
      mix graft.remove req_llm --delete --force
      mix graft.remove req_llm --json

  By default this only edits `graft.exs`. The sibling directory is deleted only
  when `--delete` is supplied. Dirty git repositories are refused for deletion
  unless `--force` is also supplied.
  """

  use Mix.Task

  alias Graft.{Error, Remove}
  alias Graft.CLI.Errors

  @switches [
    root: :string,
    dry_run: :boolean,
    delete: :boolean,
    force: :boolean,
    json: :boolean
  ]

  @impl Mix.Task
  def run(argv) do
    case execute(argv) do
      {:ok, output} ->
        Mix.shell().info(output)

      {:error, output, :stdout} ->
        Mix.shell().info(output)
        exit({:shutdown, 1})

      {:error, output, :stderr} ->
        Mix.shell().error(output)
        exit({:shutdown, 1})
    end
  end

  @spec execute([String.t()]) :: {:ok, String.t()} | {:error, String.t(), :stdout | :stderr}
  def execute(argv) do
    case parse_opts(argv) do
      {:ok, opts, targets} ->
        root = opts[:root] || File.cwd!()
        format = if opts[:json], do: :json, else: :text

        case Remove.remove(targets, root,
               dry_run: Keyword.get(opts, :dry_run, false),
               delete: Keyword.get(opts, :delete, false),
               force: Keyword.get(opts, :force, false)
             ) do
          {:ok, result} ->
            {:ok, Remove.render(result, format)}

          {:error, %Remove.Result{} = result} ->
            stream = if format == :json, do: :stdout, else: :stderr
            {:error, Remove.render(result, format), stream}

          {:error, %Error{} = err} ->
            Errors.format(err, format, "graft.remove")
        end

      {:error, message} ->
        {:error, "graft.remove: #{message}", :stderr}
    end
  end

  defp parse_opts(argv) do
    case OptionParser.parse!(argv, strict: @switches) do
      {_opts, []} ->
        {:error, "at least one sibling name is required"}

      {opts, targets} ->
        {:ok, opts, targets}
    end
  rescue
    e in OptionParser.ParseError ->
      {:error, Exception.message(e)}
  end
end