defmodule Marea do
@moduledoc """
Entry point for the `marea` escript.
Marea is a thin, transparent helper around `mix`, `docker`, `helm`,
`kubectl` and `aws`: it issues those commands consistently from a
single `marea.yaml` and prints every one before running it. The
current focus is Helm + Kubernetes, with optional plugins for the AWS
pieces (ECR / Route53 / S3); other deploy targets, registries and
providers are added as plugins, not by patching core.
`main/1` is invoked by the escript wrapper. It reads `marea.yaml`,
loads the configured `Malla.Plugin` modules on top of the base
plugins ([`Setup`](plugins/setup.md), [`Build`](plugins/build.md),
[`Run`](plugins/run.md), [`Base`](plugins/base.md)), starts `Marea.Service`,
and dispatches the parsed command through the plugin chain.
See the [Introduction](introduction.html) and
[Architecture](architecture.html) guides for the bigger picture and
the full startup/dispatch flow.
"""
alias Marea.{Service, Lib}
@base_plugins [
Marea.Plugins.Setup,
Marea.Plugins.Build,
Marea.Plugins.Run,
Marea.Plugins.Mcp,
Marea.Plugins.Base
]
@doc """
Returns the full list of plugin modules for the current working
directory: `@base_plugins` plus the modules listed under `plugins:`
in `marea.yaml`.
Reads the YAML but does not start the service, so callers (notably
the MCP server) can introspect the plugin chain without committing
to a service lifecycle.
"""
@spec plugins() :: [module()] | no_return()
def plugins() do
{:ok, _} = Application.ensure_all_started(:marea)
plugin_names =
case Marea.Config.Yaml.find_config() do
{:ok, {_path, terms}} -> Map.get(terms, "plugins", [])
:not_found -> []
end
user_modules =
for name <- plugin_names do
mod = Module.safe_concat(String.split(name, "."))
case Code.ensure_compiled(mod) do
{:module, _} -> mod
_ -> Lib.stop("Unknown plugin: #{name} (module not found)")
end
end
@base_plugins ++ user_modules
end
@doc """
Escript entry point.
Sets up signal traps, clears any leftover `.marea/next_cmd`, starts the
service, and runs the command. Any deferred shell command produced by
the chain is written to `.marea/next_cmd` for the wrapper to exec.
"""
@spec main([String.t()]) :: :ok | no_return()
def main(args) do
System.trap_signal(:sigstop, :stop, &signal_stop/0)
System.trap_signal(:sigquit, :quit, &signal_quit/0)
System.trap_signal(:sigusr1, :usr1, &signal_usr1/0)
clear_next_cmd()
start_service(args)
Service.execute()
end
@doc """
Entry point used by `mix marea`.
Same as `main/1` but returns deferred commands as `{:exec, cmd}` so the
Mix task can run them via Port instead of relying on the escript wrapper.
"""
@spec main_inline([String.t()]) :: :ok | {:exec, String.t()} | no_return()
def main_inline(args) do
clear_next_cmd()
start_service(args)
Service.execute_inline()
end
defp start_service(args) do
plugins = plugins()
{path, terms, found?} =
case Marea.Config.Yaml.find_config() do
{:ok, {path, terms}} -> {path, terms, true}
:not_found -> {nil, %{}, false}
end
{:ok, _} =
Service.start_link(
args: args,
yaml_path: path,
yaml_terms: terms,
config_found: found?,
plugins: plugins
)
end
defp clear_next_cmd do
path = Path.join(".marea", "next_cmd")
if File.exists?(path), do: File.write!(path, "#!/bin/sh\n")
end
@doc false
def signal_stop(), do: IO.puts("SIGNAL STOP")
@doc false
def signal_quit(), do: IO.puts("SIGNAL QUIT")
@doc false
def signal_usr1(), do: IO.puts("SIGNAL USR1")
end