lib/mix/tasks/graft.list.ex

defmodule Mix.Tasks.Graft.List do
  @shortdoc "List declared workspace siblings"

  @moduledoc """
  Render the workspace manifest as a concise, read-only listing.

      mix graft.list
      mix graft.list --json
      mix graft.list --root path/to/workspace

  Unlike `mix graft.status`, this does **not** snapshot the workspace,
  parse `mix.exs`, call `git`, or check Hex. It only reads `graft.exs`
  and checks whether each declared sibling path exists on disk.

  Exits 0 on success, non-zero on error. In `--json` mode, errors are
  written to stdout as a structured JSON object.
  """

  use Mix.Task

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

  @switches [json: :boolean, root: :string]

  @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

  @doc """
  Pure entry point for testing: parses argv, loads the manifest, renders,
  and returns either `{:ok, output}` or `{:error, output, stream}`.
  """
  @spec execute([String.t()]) :: {:ok, String.t()} | {:error, String.t(), :stdout | :stderr}
  def execute(argv) do
    case parse_opts(argv) do
      {:ok, opts} ->
        format = if opts[:json], do: :json, else: :text
        root = opts[:root] || File.cwd!()

        case List.render(root, format) do
          {:ok, output} -> {:ok, output}
          {:error, %Error{} = err} -> format_error(err, format)
        end

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

  ## ─── Argument parsing ───────────────────────────────────────────────

  defp parse_opts(argv) do
    case OptionParser.parse!(argv, strict: @switches) do
      {opts, []} ->
        {:ok, opts}

      {_opts, extra} ->
        {:error, "unexpected arguments: #{Enum.join(extra, " ")}"}
    end
  rescue
    e in OptionParser.ParseError ->
      {:error, Exception.message(e)}
  end

  ## ─── Error formatting ───────────────────────────────────────────────

  defp format_error(%Error{} = err, format), do: Errors.format(err, format, "graft.list")
end