lib/mix/tasks/foundry.project.context.ex

defmodule Mix.Tasks.Foundry.Project.Context do
  use Mix.Task
  @shortdoc "Generate or query the project context"

  @moduledoc """
  Generates project context for Foundry modules.

  ## Usage

      mix foundry.project.context <Module>  # Per-module context
      mix foundry.project.context            # Bulk project context
      mix foundry.project.context --check    # Check lock freshness

  ## Forms

  - `mix foundry.project.context <Module>`: Returns JSON context for a single module
  - `mix foundry.project.context`: Returns bulk context for all modules in project
  - `mix foundry.project.context --check`: Checks if mix.lock is current
  """

  def run(["--check"]), do: run_check()
  def run([module_name]) when is_binary(module_name), do: run_single(module_name)
  def run([]), do: run_bulk()
  def run(_), do: Mix.raise("Usage: mix foundry.project.context [<Module> | --check]")

  defp run_single(module_name) do
    Mix.Task.run("app.start")
    project_root = File.cwd!()
    {:ok, manifest} = Foundry.Manifest.Parser.read(project_root)
    {:ok, pending_set} = Foundry.Context.PendingMigrations.check(project_root)

    module =
      try do
        String.to_existing_atom("Elixir." <> module_name)
      rescue
        _ -> emit_error_and_halt("module_not_found", module_name)
      end

    unless Code.ensure_loaded?(module) do
      emit_error_and_halt("module_not_found", module_name)
    end

    info = Foundry.SparkMeta.walk(module)
    pending = Foundry.Context.PendingMigrations.pending?(module, pending_set)
    node = Foundry.Context.NodeBuilder.build(info, manifest, pending)

    IO.puts(Jason.encode!(node, pretty: true))
  end

  defp emit_error_and_halt(error, module_name) do
    IO.puts(Jason.encode!(%{error: error, module: module_name}))
    exit({:shutdown, 1})
  end

  defp run_bulk() do
    Mix.Task.run("app.start")
    project_root = File.cwd!()

    Foundry.Context.ProjectMap.build_all(project_root)
    |> Jason.encode!(pretty: true)
    |> IO.puts()

    # Write lock file for --check to use
    Foundry.Context.LockFile.write(project_root)
  end

  defp run_check() do
    project_root = File.cwd!()

    case Foundry.Context.LockFile.check(project_root) do
      :ok ->
        IO.puts("context.lock is current.")

      {:error, :missing} ->
        IO.puts(:stderr, "error: .foundry/context.lock absent. Run: mix foundry.project.context")
        exit({:shutdown, 1})

      {:error, :stale} ->
        IO.puts(:stderr, "error: context.lock is stale. Run: mix foundry.project.context")
        exit({:shutdown, 1})
    end
  end
end