Skip to main content

lib/arrea/cli/verify.ex

defmodule Arrea.CLI.Verify do
  @moduledoc false

  @doc """
  Verifies that all runtime versions requested via --asdf-* and --mise-*
  exist in their respective tool managers.
  """
  @spec runtime_opts!(map() | keyword()) :: :ok | no_return()
  def runtime_opts!(runtime_opts) do
    Enum.each(runtime_opts, fn {key, version} ->
      key_str = to_string(key)

      cond do
        String.starts_with?(key_str, "asdf_") ->
          runtime = String.replace_prefix(key_str, "asdf_", "")
          tool = find_asdf()
          verify_asdf_version(tool, runtime, version)

        String.starts_with?(key_str, "mise_") ->
          runtime = String.replace_prefix(key_str, "mise_", "")
          verify_mise_version!(runtime, version)

        true ->
          :ok
      end
    end)
  end

  defp find_asdf do
    case System.find_executable("asdf") do
      nil ->
        case System.find_executable("mise") do
          nil -> nil
          _ -> :mise
        end

      _ ->
        :asdf
    end
  end

  defp verify_asdf_version(:asdf, runtime, version) do
    case System.cmd("asdf", ["plugin", "list"], stderr_to_stdout: true) do
      {output, 0} ->
        plugins = output |> String.split("\n") |> Enum.map(&String.trim/1)

        if runtime in plugins do
          verify_asdf_version(runtime, version)
        else
          IO.puts(:stderr, "Error: asdf: plugin #{runtime} not installed")
          IO.puts(:stderr, "  Run: asdf plugin add #{runtime}")
          System.halt(1)
        end

      {_output, _} ->
        IO.puts(:stderr, "Error: asdf: command not found or not working")
        System.halt(1)
    end
  rescue
    _ ->
      IO.puts(:stderr, "Error: asdf: command not found. Is asdf installed?")
      System.halt(1)
  end

  defp verify_asdf_version(:mise, runtime, version) do
    verify_mise_version!(runtime, version)
  end

  defp verify_asdf_version(nil, runtime, version) do
    IO.puts(
      :stderr,
      "Error: --asdf-#{runtime} #{version} requires asdf or mise, but neither is installed"
    )

    System.halt(1)
  end

  defp verify_asdf_version(runtime, version) do
    case System.cmd("asdf", ["list", runtime], stderr_to_stdout: true) do
      {output, 0} ->
        versions = output |> String.split("\n") |> Enum.map(&String.trim/1)

        if version in versions do
          :ok
        else
          IO.puts(:stderr, "Error: asdf: version #{version} not found for #{runtime}")
          IO.puts(:stderr, "  Available: #{Enum.join(versions, ", ")}")
          System.halt(1)
        end

      {_output, _} ->
        IO.puts(:stderr, "Error: asdf: no versions installed for #{runtime}")
        System.halt(1)
    end
  end

  defp verify_mise_version!(runtime, version) do
    if System.find_executable("mise") == nil do
      IO.puts(:stderr, "Error: mise is not installed. Please install mise or use asdf.")
      System.halt(1)
    end

    case System.cmd("mise", ["ls", runtime, "--json"], stderr_to_stdout: true) do
      {output, 0} ->
        versions =
          output
          |> Jason.decode!()
          |> Enum.map(fn %{"version" => v} -> v end)

        if version in versions do
          :ok
        else
          IO.puts(:stderr, "Error: mise: version #{version} not found for #{runtime}")
          IO.puts(:stderr, "  Available: #{Enum.join(versions, ", ")}")
          System.halt(1)
        end

      {output, exit_code} ->
        diagnose_mise_error(runtime, version, output, exit_code)
    end
  rescue
    _ ->
      IO.puts(:stderr, "Error: mise is not installed. Please install mise or use asdf.")
      System.halt(1)
  end

  @spec diagnose_mise_error(String.t(), String.t(), String.t(), non_neg_integer()) :: no_return()
  defp diagnose_mise_error(runtime, version, _output, _exit_code) do
    case System.cmd("mise", ["ls"], stderr_to_stdout: true) do
      {ls_output, 0} ->
        cond do
          String.contains?(ls_output, "plugin not installed") or
              String.contains?(ls_output, "missing plugin") ->
            IO.puts(:stderr, "Error: mise: plugin #{runtime} not installed")
            IO.puts(:stderr, "  Run: mise plugins install #{runtime}")

          String.contains?(ls_output, runtime) ->
            IO.puts(:stderr, "Error: mise: version #{version} not found for #{runtime}")
            list_mise_versions(runtime)

          true ->
            IO.puts(:stderr, "Error: mise: plugin #{runtime} not installed")
            IO.puts(:stderr, "  Run: mise plugins install #{runtime}")
        end

      _ ->
        IO.puts(:stderr, "Error: mise is not installed. Please install mise or use asdf.")
    end

    System.halt(1)
  end

  defp list_mise_versions(runtime) do
    case System.cmd("mise", ["ls", runtime, "--json"], stderr_to_stdout: true) do
      {output, 0} ->
        versions =
          output
          |> Jason.decode!()
          |> Enum.map(fn %{"version" => v} -> v end)

        if versions != [] do
          IO.puts(:stderr, "  Available: #{Enum.join(versions, ", ")}")
        end

      _ ->
        :ok
    end
  end
end