         :ok
     end
   end
+
+  @spec print_plugins([Plugin.t()]) :: :ok
+  defp print_plugins([]), do: :ok
+
+  defp print_plugins(plugins) do
+    IO.puts("\x1b[93m=== Loaded Plugins ===\x1b[0m")
+
+    Enum.each(plugins, fn plugin ->
+      mod_name =
+        plugin.module
+        |> to_string()
+        |> String.replace_prefix("Elixir.", "")
+
+      IO.puts("\x1b[1m#{plugin.name} v#{plugin.version}\x1b[0m (#{mod_name})")
+      IO.puts("    " <> plugin.description)
+    end)
+
+    IO.puts("")
+  end
 end

  alias Serum.Build.FileProcessor
  alias Serum.Build.FragmentGenerator
  alias Serum.Plugin
  alias Serum.Result

  @doc """


  ## Build Procedure

  1. Checks if a project definition file exists under `src`, checks if it's
    well-formed and valid, then loads it.

  2. Tries to load plugins listed in the project definition file.

  3. Checks if the system timezone is properly set.

      Timex requires the local timezone information to format the date/time
      string. If it's not set or invalid, Timex will fail.

  4. Checks if the current user has enough permission on the destination
    directory and cleans it if it already exists.

  5. Loads source files. See `Serum.Build.FileLoader`.

  6. Processes source files and produces intermediate data structures.
    See `Serum.Build.FileProcessor`.

  7. Generates HTML fragments from the intermediate data.
    See `Serum.Build.FragmentGenerator`.

  8. Renders full HTML pages from fragments and writes them to files.
    See `Serum.Build.FileEmitter`.

  9. Copies `assets/` and `media/` directories if they exist.
  """
  @spec build(binary(), binary()) :: Result.t(binary())
  def build(src, dest) do
    with {:ok, %Project{} = proj} <- ProjectLoader.load(src, dest),
         {:ok, plugins} <- Plugin.load_plugins(proj.plugins),
         :ok <- print_plugins(plugins),
         :ok <- Plugin.build_started(src, dest),
         :ok <- check_tz(),
         :ok <- check_dest_perm(dest),
         :ok <- clean_dest(dest),
         {:ok, files} <- FileLoader.load_files(proj),
         {:ok, map} <- FileProcessor.process_files(files, proj),
         {:ok, fragments} <- FragmentGenerator.to_fragment(map, proj),
         :ok <- FileEmitter.run(fragments),
         :ok <- copy_assets(src, dest),
         :ok <- Plugin.build_succeeded(src, dest),
         :ok <- Plugin.finalizing(src, dest) do
      {:ok, dest}
    else
      {:error, _} = error ->
        with :ok <- Plugin.build_failed(src, dest, error),
             :ok <- Plugin.finalizing(src, dest) do
          error
        else
          {:error, _} = plugin_error -> plugin_error
