defmodule Pi.Dev do
@moduledoc "Small dogfood/dev reload helpers callable from elixir_eval."
alias Pi.LLM.Broker
@default_prefixes ["Elixir.Pi"]
@doc "Returns compact development reload status."
def status(_opts \\ []) do
%{
app: Mix.Project.config()[:app],
env: Mix.env(),
bridge_version: Application.spec(:pi_bridge, :vsn) |> to_string(),
loaded_modules: loaded() |> length(),
restart_hint:
"Use /elixir:restart to restart embedded BEAM; /elixir:refresh to refresh pi + BEAM."
}
end
@doc "Compiles the current Mix project."
def compile(opts \\ []) do
args = if Keyword.get(opts, :force, false), do: ["--force"], else: []
compile_args = Keyword.get(opts, :args, args)
Mix.Task.clear()
try do
modules = Mix.Task.run("compile", compile_args) |> normalize_modules()
{:ok, %{modules: modules, count: length(modules)}}
rescue
exception in [Mix.Error, CompileError, RuntimeError, ArgumentError] ->
{:error,
%{kind: :compile_error, message: Exception.format(:error, exception, __STACKTRACE__)}}
catch
:exit, reason -> {:error, %{kind: :compile_exit, reason: inspect(reason)}}
end
end
@doc "Compiles and soft-reloads matching BEAM modules."
def reload(opts \\ []) do
with {:ok, %{modules: modules} = compile_report} <-
compile(Keyword.put_new(opts, :force, true)) do
modules = reloadable_modules(modules, opts)
warnings = Enum.flat_map(modules, &reload_module/1)
{:ok, Map.merge(compile_report, %{reloaded: modules, warnings: warnings})}
end
end
@doc "Returns loaded modules matching optional prefixes."
def loaded(opts \\ []) do
prefixes = Keyword.get(opts, :prefixes, @default_prefixes)
:code.all_loaded()
|> Enum.map(&elem(&1, 0))
|> Enum.filter(&prefixed?(&1, prefixes))
|> Enum.sort()
end
@doc "Requests pi to perform a process or extension reload after the current eval returns."
def request(action, opts \\ []) when action in [:beam_restart, :pi_reload, :dev_reload] do
timeout = Keyword.get(opts, :timeout, 1_000)
Broker.request(:dev_reload, %{action: Atom.to_string(action)}, timeout)
end
@doc "Requests pi to restart the embedded BEAM bridge after the current eval returns."
def restart(opts \\ []), do: request(:beam_restart, opts)
@doc "Requests pi to refresh the dev environment after the current eval returns."
def refresh(opts \\ []), do: request(:dev_reload, opts)
defp normalize_modules({:ok, modules}) when is_list(modules), do: modules
defp normalize_modules(modules) when is_list(modules), do: modules
defp normalize_modules(_other), do: []
defp reloadable_modules([], opts), do: loaded(opts)
defp reloadable_modules(modules, opts),
do: Enum.filter(modules, &prefixed?(&1, Keyword.get(opts, :prefixes, @default_prefixes)))
defp prefixed?(module, prefixes) do
name = to_string(module)
Enum.any?(prefixes, &String.starts_with?(name, &1))
end
defp reload_module(module) do
if :code.soft_purge(module) do
:code.delete(module)
:code.ensure_loaded(module)
[]
else
[{:old_code_still_running, module}]
end
end
end