feat: unified watch server under org-garden
This commit is contained in:
118
org-garden/lib/org_garden/quartz.ex
Normal file
118
org-garden/lib/org_garden/quartz.ex
Normal file
@@ -0,0 +1,118 @@
|
||||
defmodule OrgGarden.Quartz do
|
||||
@moduledoc """
|
||||
Manages Quartz Node.js process as an Erlang Port.
|
||||
|
||||
Required environment:
|
||||
- QUARTZ_PATH: path to quartz repo (with node_modules)
|
||||
- NODE_PATH: path to node executable (default: "node")
|
||||
|
||||
Starts Quartz in serve mode (`npx quartz build --serve`) and forwards
|
||||
all stdout/stderr output to the Logger with a `[quartz]` prefix.
|
||||
|
||||
If Quartz exits, this GenServer will stop, which triggers the supervisor
|
||||
to restart the entire supervision tree (strategy: :one_for_all).
|
||||
"""
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
|
||||
defstruct [:port, :quartz_path, :content_dir, :http_port, :ws_port]
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# Client API
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@doc """
|
||||
Start the Quartz process as a linked GenServer.
|
||||
|
||||
## Options
|
||||
|
||||
* `:content_dir` — directory where markdown files are located (required)
|
||||
* `:port` — HTTP server port (default: 8080)
|
||||
* `:ws_port` — WebSocket hot reload port (default: 3001)
|
||||
"""
|
||||
def start_link(opts) do
|
||||
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
|
||||
end
|
||||
|
||||
# -------------------------------------------------------------------
|
||||
# GenServer callbacks
|
||||
# -------------------------------------------------------------------
|
||||
|
||||
@impl true
|
||||
def init(opts) do
|
||||
quartz_path =
|
||||
System.get_env("QUARTZ_PATH") ||
|
||||
raise "QUARTZ_PATH environment variable not set"
|
||||
|
||||
node_path = System.get_env("NODE_PATH", "node")
|
||||
|
||||
content_dir = Keyword.fetch!(opts, :content_dir)
|
||||
http_port = Keyword.get(opts, :port, 8080)
|
||||
ws_port = Keyword.get(opts, :ws_port, 3001)
|
||||
|
||||
cli_path = Path.join(quartz_path, "quartz/bootstrap-cli.mjs")
|
||||
|
||||
unless File.exists?(cli_path) do
|
||||
raise "Quartz CLI not found at #{cli_path}. Check QUARTZ_PATH."
|
||||
end
|
||||
|
||||
args = [
|
||||
cli_path,
|
||||
"build",
|
||||
"--serve",
|
||||
"--directory", content_dir,
|
||||
"--port", to_string(http_port),
|
||||
"--wsPort", to_string(ws_port)
|
||||
]
|
||||
|
||||
Logger.info("[quartz] Starting: #{node_path} #{Enum.join(args, " ")}")
|
||||
Logger.info("[quartz] Working directory: #{quartz_path}")
|
||||
|
||||
port =
|
||||
Port.open({:spawn_executable, node_path}, [
|
||||
:binary,
|
||||
:exit_status,
|
||||
:stderr_to_stdout,
|
||||
args: args,
|
||||
cd: quartz_path,
|
||||
env: [{~c"NODE_NO_WARNINGS", ~c"1"}]
|
||||
])
|
||||
|
||||
state = %__MODULE__{
|
||||
port: port,
|
||||
quartz_path: quartz_path,
|
||||
content_dir: content_dir,
|
||||
http_port: http_port,
|
||||
ws_port: ws_port
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({port, {:data, data}}, %{port: port} = state) do
|
||||
data
|
||||
|> String.split("\n", trim: true)
|
||||
|> Enum.each(&Logger.info("[quartz] #{&1}"))
|
||||
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info({port, {:exit_status, status}}, %{port: port} = state) do
|
||||
Logger.error("[quartz] Process exited with status #{status}")
|
||||
{:stop, {:quartz_exit, status}, state}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def terminate(_reason, %{port: port}) when is_port(port) do
|
||||
# Attempt graceful shutdown
|
||||
Port.close(port)
|
||||
:ok
|
||||
rescue
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
def terminate(_reason, _state), do: :ok
|
||||
end
|
||||
Reference in New Issue
Block a user