lib/aino/watcher.ex

defmodule Aino.Watcher do
  @moduledoc """
  Launch external processes along with the Aino

  Example:

  ```
  watchers = [
    [
      command: "node_modules/yarn/bin/yarn",
      args: ["build:js:watch"],
      directory: "assets/"
    ],
    [
      command: "node_modules/yarn/bin/yarn",
      args: ["build:css:watch"],
      directory: "assets/"
    ]
  ]

  children = [
    {Aino.Watcher, name: MyApp.Watcher, watchers: watchers}
  ]
  ```
  """

  use Supervisor

  def start_link(opts) do
    Supervisor.start_link(__MODULE__, opts)
  end

  def init(opts) do
    children =
      opts[:watchers]
      |> Enum.with_index()
      |> Enum.map(fn {watcher, index} ->
        Supervisor.child_spec({Aino.Watcher.ExternalProcess, watcher}, id: {__MODULE__, index})
      end)

    opts = [strategy: :one_for_one, name: opts[:name]]
    Supervisor.init(children, opts)
  end
end

defmodule Aino.Watcher.ExternalProcess do
  @moduledoc false

  use GenServer

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts)
  end

  def init(opts) do
    opts = Enum.into(opts, %{})

    {:ok, opts, {:continue, :spawn}}
  end

  def handle_continue(:spawn, state) do
    command_directory = Path.join(File.cwd!(), state.directory)
    command = Path.join(command_directory, state.command)

    {:ok, _pid, external_pid} =
      :exec.run_link([command | state.args], [
        {:cd, command_directory},
        {:stdout, self()},
        {:stderr, self()}
      ])

    {:noreply, Map.put(state, :external_pid, external_pid)}
  end

  def handle_info({:stdout, pid, output}, %{external_pid: pid} = state) do
    IO.puts(output)

    {:noreply, state}
  end

  def handle_info({:stderr, pid, output}, %{external_pid: pid} = state) do
    IO.puts(output)

    {:noreply, state}
  end
end