lib/mix/tasks/graft.workspace.dossier.ex

defmodule Mix.Tasks.Graft.Workspace.Dossier do
  @shortdoc "Print a JSON workspace dossier for the given path"

  @moduledoc """
  Print a durable workspace status dossier as JSON.

      mix contrib.workspace.dossier
      mix contrib.workspace.dossier --root path/to/workspace

  The dossier is a read-only summary of the workspace state including
  repo paths, git summary, health, attention flags, and detected
  session metadata (tmux, agent).

  Read-only, side-effect free.
  """

  use Mix.Task

  alias Graft.{Error, Workspace}
  alias Graft.Workspace.Dossier
  alias Graft.Workspace.Dossier.Builder

  @switches [root: :string]

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

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

  @doc """
  Pure entry point for testing: parses argv, builds the snapshot + dossier,
  encodes to JSON, and returns `{:ok, json}` or `{:error, message, :stderr}`.
  """
  @spec execute([String.t()]) :: {:ok, String.t()} | {:error, String.t(), :stderr}
  def execute(argv) do
    case parse_opts(argv) do
      {:ok, opts} ->
        root = opts[:root] || File.cwd!()

        case Workspace.snapshot(root) do
          {:ok, snapshot} ->
            dossier = Builder.build(snapshot)
            json = Jason.encode!(Dossier.to_map(dossier), pretty: true)
            {:ok, json}

          {:error, %Error{} = err} ->
            {:error, "graft.workspace.dossier: #{err.message}", :stderr}
        end

      {:error, message} ->
        {:error, "graft.workspace.dossier: #{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
end