defmodule HostKit do
import Kernel, except: [apply: 2]
@moduledoc """
Elixir-native host infrastructure declarations, planning, and runtime control.
HostKit keeps systemd and transient unit execution as core primitives while
integrations such as Caddy are provided by providers. DSL files compile to plain
structs that can be inspected and consumed through the runtime API.
"""
alias HostKit.{Apply, Clean, Loader, Plan, Project, Target}
defmacro __using__(opts \\ []) do
quote do
use HostKit.DSL, unquote(opts)
end
end
@doc "Loads a HostKit project from an `.exs` file."
@spec load(Path.t(), keyword()) :: {:ok, Project.t()} | {:error, term()}
def load(path, opts \\ []), do: Loader.load(path, opts)
@doc "Loads a HostKit project from an `.exs` file or raises."
@spec load!(Path.t(), keyword()) :: Project.t()
def load!(path, opts \\ []) do
case load(path, opts) do
{:ok, project} ->
project
{:error, reason} ->
raise ArgumentError, "could not load HostKit project: #{inspect(reason)}"
end
end
@doc """
Builds an initial plan from project resources.
This first implementation is intentionally structural: it preserves resources
and dependency ordering without touching a host.
"""
@spec plan(Project.t(), keyword()) ::
{:ok, Plan.t()} | {:error, HostKit.Diagnostics.t() | term()}
def plan(%Project{} = project, opts \\ []) do
opts = expand_target_opts(opts)
HostKit.Telemetry.span([:plan], %{project: project.name}, fn ->
Plan.build(project, opts)
end)
end
@doc "Applies supported changes from a HostKit plan."
@spec apply(Plan.t(), keyword()) :: {:ok, [Apply.result()]} | {:error, term()}
def apply(%Plan{} = plan, opts \\ []) do
opts = expand_target_opts(opts)
HostKit.Telemetry.span([:apply], %{project: plan.project && plan.project.name}, fn ->
Apply.run(plan, opts)
end)
end
@doc "Applies supported changes from a HostKit plan or raises."
@spec apply!(Plan.t(), keyword()) :: [Apply.result()]
def apply!(%Plan{} = plan, opts \\ []) do
case apply(plan, opts) do
{:ok, results} ->
results
{:error, reason} ->
raise ArgumentError, "could not apply HostKit plan: #{inspect(reason)}"
end
end
@doc "Builds a cleanup plan from project release metadata."
@spec clean(Project.t(), keyword()) :: {:ok, Plan.t()} | {:error, term()}
def clean(%Project{} = project, opts \\ []) do
opts = expand_target_opts(opts)
HostKit.Telemetry.span([:clean], %{project: project.name}, fn ->
Clean.plan(project, opts)
end)
end
@doc "Builds a down/rollback plan from an existing HostKit plan."
@spec down(Plan.t(), keyword()) :: {:ok, Plan.t()} | {:error, term()}
def down(%Plan{} = plan, opts \\ []), do: Plan.down(plan, opts)
@doc "Applies the down/rollback plan for an existing HostKit plan."
@spec rollback(Plan.t(), keyword()) :: {:ok, [Apply.result()]} | {:error, term()}
def rollback(%Plan{} = plan, opts \\ []) do
opts = expand_target_opts(opts)
with {:ok, down_plan} <- down(plan, opts) do
apply(down_plan, Keyword.put(opts, :confirm, true))
end
end
@doc "Formats a HostKit plan for human-readable output."
@spec format_plan(Plan.t()) :: String.t()
def format_plan(%Plan{} = plan), do: Plan.Format.format(plan)
defp expand_target_opts(opts) do
case Keyword.pop(opts, :target) do
{%Target{} = target, opts} -> Target.opts(target, opts)
{nil, opts} -> opts
end
end
end