caclawphony/elixir/lib/symphony_elixir/cli.ex
Alex Kotliarskyi fa75ec68c2 🎼
2026-03-04 09:29:11 -08:00

192 lines
5.4 KiB
Elixir

defmodule SymphonyElixir.CLI do
@moduledoc """
Escript entrypoint for running Symphony with an explicit WORKFLOW.md path.
"""
alias SymphonyElixir.LogFile
@acknowledgement_switch :i_understand_that_this_will_be_running_without_the_usual_guardrails
@switches [{@acknowledgement_switch, :boolean}, logs_root: :string, port: :integer]
@type ensure_started_result :: {:ok, [atom()]} | {:error, term()}
@type deps :: %{
file_regular?: (String.t() -> boolean()),
set_workflow_file_path: (String.t() -> :ok | {:error, term()}),
set_logs_root: (String.t() -> :ok | {:error, term()}),
set_server_port_override: (non_neg_integer() | nil -> :ok | {:error, term()}),
ensure_all_started: (-> ensure_started_result())
}
@spec main([String.t()]) :: no_return()
def main(args) do
case evaluate(args) do
:ok ->
wait_for_shutdown()
{:error, message} ->
IO.puts(:stderr, message)
System.halt(1)
end
end
@spec evaluate([String.t()], deps()) :: :ok | {:error, String.t()}
def evaluate(args, deps \\ runtime_deps()) do
case OptionParser.parse(args, strict: @switches) do
{opts, [], []} ->
with :ok <- require_guardrails_acknowledgement(opts),
:ok <- maybe_set_logs_root(opts, deps),
:ok <- maybe_set_server_port(opts, deps) do
run(Path.expand("WORKFLOW.md"), deps)
end
{opts, [workflow_path], []} ->
with :ok <- require_guardrails_acknowledgement(opts),
:ok <- maybe_set_logs_root(opts, deps),
:ok <- maybe_set_server_port(opts, deps) do
run(workflow_path, deps)
end
_ ->
{:error, usage_message()}
end
end
@spec run(String.t(), deps()) :: :ok | {:error, String.t()}
def run(workflow_path, deps) do
expanded_path = Path.expand(workflow_path)
if deps.file_regular?.(expanded_path) do
:ok = deps.set_workflow_file_path.(expanded_path)
case deps.ensure_all_started.() do
{:ok, _started_apps} ->
:ok
{:error, reason} ->
{:error, "Failed to start Symphony with workflow #{expanded_path}: #{inspect(reason)}"}
end
else
{:error, "Workflow file not found: #{expanded_path}"}
end
end
@spec usage_message() :: String.t()
defp usage_message do
"Usage: symphony [--logs-root <path>] [--port <port>] [path-to-WORKFLOW.md]"
end
@spec runtime_deps() :: deps()
defp runtime_deps do
%{
file_regular?: &File.regular?/1,
set_workflow_file_path: &SymphonyElixir.Workflow.set_workflow_file_path/1,
set_logs_root: &set_logs_root/1,
set_server_port_override: &set_server_port_override/1,
ensure_all_started: fn -> Application.ensure_all_started(:symphony_elixir) end
}
end
defp maybe_set_logs_root(opts, deps) do
case Keyword.get_values(opts, :logs_root) do
[] ->
:ok
values ->
logs_root = values |> List.last() |> String.trim()
if logs_root == "" do
{:error, usage_message()}
else
:ok = deps.set_logs_root.(Path.expand(logs_root))
end
end
end
defp require_guardrails_acknowledgement(opts) do
if Keyword.get(opts, @acknowledgement_switch, false) do
:ok
else
{:error, acknowledgement_banner()}
end
end
@spec acknowledgement_banner() :: String.t()
defp acknowledgement_banner do
lines = [
"This Symphony implementation is a low key engineering preview.",
"Codex will run without any guardrails.",
"SymphonyElixir is not a supported product and is presented as-is.",
"To proceed, start with `--i-understand-that-this-will-be-running-without-the-usual-guardrails` CLI argument"
]
width = Enum.max(Enum.map(lines, &String.length/1))
border = String.duplicate("", width + 2)
top = "" <> border <> ""
bottom = "" <> border <> ""
spacer = "" <> String.duplicate(" ", width) <> ""
content =
[
top,
spacer
| Enum.map(lines, fn line ->
"" <> String.pad_trailing(line, width) <> ""
end)
] ++ [spacer, bottom]
[
IO.ANSI.red(),
IO.ANSI.bright(),
Enum.join(content, "\n"),
IO.ANSI.reset()
]
|> IO.iodata_to_binary()
end
defp set_logs_root(logs_root) do
Application.put_env(:symphony_elixir, :log_file, LogFile.default_log_file(logs_root))
:ok
end
defp maybe_set_server_port(opts, deps) do
case Keyword.get_values(opts, :port) do
[] ->
:ok
values ->
port = List.last(values)
if is_integer(port) and port >= 0 do
:ok = deps.set_server_port_override.(port)
else
{:error, usage_message()}
end
end
end
defp set_server_port_override(port) when is_integer(port) and port >= 0 do
Application.put_env(:symphony_elixir, :server_port_override, port)
:ok
end
@spec wait_for_shutdown() :: no_return()
defp wait_for_shutdown do
case Process.whereis(SymphonyElixir.Supervisor) do
nil ->
IO.puts(:stderr, "Symphony supervisor is not running")
System.halt(1)
pid ->
ref = Process.monitor(pid)
receive do
{:DOWN, ^ref, :process, ^pid, reason} ->
case reason do
:normal -> System.halt(0)
_ -> System.halt(1)
end
end
end
end
end