Skip to main content

lib/mix/tasks/host_kit.clean.ex

defmodule Mix.Tasks.HostKit.Clean do
  @moduledoc """
  Builds and optionally applies a conservative cleanup plan from HostKit release metadata.

  Cleanup uses the same target selection flags as `host_kit.plan` and the same
  confirmation model as `host_kit.apply`. Use `--dry-run` to inspect without
  deleting anything, or `--confirm` to apply the generated cleanup commands.
  """

  use Mix.Task

  alias Mix.Tasks.HostKit.Options

  @shortdoc "Clean stale HostKit-managed release artifacts"

  @impl true
  def run(args) do
    Mix.Task.run("app.start")

    {opts, positional} =
      OptionParser.parse!(args,
        strict: [
          local: :boolean,
          host: :string,
          remote: :string,
          user: :string,
          port: :integer,
          identity_file: :string,
          password: :string,
          password_env: :string,
          silently_accept_hosts: :boolean,
          sudo: :boolean,
          require: :keep,
          service: :keep,
          keep: :integer,
          dry_run: :boolean,
          confirm: :boolean,
          quiet: :boolean,
          verbose: :boolean
        ],
        aliases: [dry_run: :dry_run]
      )

    path = List.first(positional) || "infra/config.exs"
    project = HostKit.load!(path, require: Keyword.get_values(opts, :require))

    Options.with_target_opts(opts, project, fn target_opts ->
      case HostKit.clean(project, clean_opts(opts, target_opts)) do
        {:ok, plan} -> run_plan(plan, opts, target_opts)
        {:error, reason} -> Mix.raise("HostKit clean failed: #{inspect(reason)}")
      end
    end)
  end

  defp run_plan(plan, opts, target_opts) do
    cond do
      Keyword.get(opts, :dry_run, false) ->
        IO.puts(HostKit.format_plan(plan))

      Keyword.get(opts, :confirm, false) ->
        case HostKit.apply(plan, apply_opts(opts, target_opts)) do
          {:ok, results} -> print_results(results)
          {:error, reason} -> Mix.raise("HostKit clean apply failed: #{inspect(reason)}")
        end

      true ->
        IO.puts(HostKit.format_plan(plan))
        Mix.raise("pass --dry-run to inspect or --confirm to apply cleanup")
    end
  end

  defp clean_opts(opts, target_opts) do
    target_opts
    |> Options.expand_target_opts()
    |> put_present(:services, Options.selected_services(opts))
    |> put_present(:keep, Keyword.get(opts, :keep))
  end

  defp apply_opts(opts, target_opts) do
    target_opts
    |> Options.expand_target_opts()
    |> Keyword.merge(confirm: true, dry_run: false, track: false)
    |> put_present(:keep, Keyword.get(opts, :keep))
  end

  defp print_results(results) do
    results
    |> Enum.map_join("\n", fn %{change: change, status: status} ->
      "#{status} #{HostKit.Plan.Format.format_change(change)}"
    end)
    |> IO.puts()
  end

  defp put_present(opts, _key, nil), do: opts
  defp put_present(opts, key, value), do: Keyword.put(opts, key, value)
end