Skip to main content

lib/mix/tasks/cairnloop/install.ex

defmodule Mix.Tasks.Cairnloop.Install do
  @shortdoc "Installs Cairnloop: adds the dep, generates the base migration, and prints next steps"

  @moduledoc """
  Igniter installer for Cairnloop.

  Adds the dependency, generates the base `cairnloop_*` tables migration against your repo, and
  prints the remaining host-owned wiring steps (router mount, auditor config) plus a pointer to
  `mix cairnloop.doctor` to verify. Cairnloop is host-owned, so the router mount and auth stay
  yours — the installer guides them rather than silently injecting routes.
  """

  use Igniter.Mix.Task

  @impl Igniter.Mix.Task
  def info(_argv, _composing_task) do
    %Igniter.Mix.Task.Info{
      group: :cairnloop,
      schema: [],
      defaults: []
    }
  end

  @impl Igniter.Mix.Task
  def igniter(igniter) do
    igniter
    |> Igniter.Project.Deps.add_dep({:cairnloop, "~> 0.3"})
    |> add_base_migration()
    |> Igniter.add_notice(next_steps_notice())
  end

  defp add_base_migration(igniter) do
    case Igniter.Libs.Ecto.select_repo(igniter) do
      {igniter, nil} ->
        Igniter.add_issue(
          igniter,
          "No Ecto repo found. Please create a migration manually for cairnloop tables."
        )

      {igniter, repo} ->
        Igniter.Libs.Ecto.gen_migration(
          igniter,
          repo,
          "create_cairnloop_tables",
          body: """
            def change do
              create table(:cairnloop_conversations) do
                add :status, :string, null: false
                add :subject, :string
                add :host_user_id, :string
                add :resolved_at, :utc_datetime_usec
                add :csat_rating, :string

                timestamps()
              end

              create table(:cairnloop_messages) do
                add :content, :text, null: false
                add :role, :string, null: false
                add :metadata, :map
                add :conversation_id, references(:cairnloop_conversations, on_delete: :delete_all), null: false

                timestamps()
              end

              create index(:cairnloop_messages, [:conversation_id])
            end
          """,
          on_exists: :skip
        )
    end
  end

  defp next_steps_notice do
    """
    Cairnloop is host-owned. To finish wiring it up:

      1. Mount the operator surfaces in your router (lib/my_app_web/router.ex):

           require Cairnloop.Router

           # Liveness/metrics probes — outside auth so infra can reach them.
           scope "/" do
             Cairnloop.Router.cairnloop_operations()
           end

           # Operator dashboard — wrap in your own auth pipeline.
           scope "/support" do
             pipe_through [:browser]   # add your :require_admin pipeline here
             # Inject the SIGNED-IN operator per request via an MFA tuple. `host_user_id`
             # is the audit actor + search scope — a static map freezes it at compile time
             # so every operator shares one id. Define cairnloop_session/1 to read the
             # authenticated user off the conn (see the Auth & Operator Identity guide):
             #   def cairnloop_session(conn), do: %{"host_user_id" => to_string(conn.assigns.current_user.id)}
             Cairnloop.Router.cairnloop_dashboard "/",
               session: {MyAppWeb.UserAuth, :cairnloop_session, []}
           end

      2. Surface governed-action events in the audit log:

           config :cairnloop, :auditor, Cairnloop.Auditor.Governance

         (Optional) scaffold a Notifier:  mix cairnloop.gen.notifier

      3. Run the migration:  mix ecto.migrate

      4. Verify the wiring:  mix cairnloop.doctor
    """
  end
end