Packages

Headless local MCP node providing notebook, references and script capability to LLM workers

Current section

Files

Jump to
ymer_node mix.exs
Raw

mix.exs

defmodule YmerNode.MixProject do
  use Mix.Project

  def project do
    [
      app: :ymer_node,
      version: "0.2.2",
      elixir: "~> 1.19",
      elixirc_paths: elixirc_paths(Mix.env()),
      start_permanent: Mix.env() == :prod,
      aliases: aliases(),
      deps: deps(),
      releases: releases(),
      description:
        "Headless local MCP node providing notebook, references and script capability " <>
          "to LLM workers",
      package: package(),
      name: "Ymer Node",
      docs: docs()
    ]
  end

  def application do
    [
      mod: {YmerNode.Application, []},
      # `:xmerl` is named here although sweet_xml's own application closure
      # already carries it: XPath over `:xmerl` is a promise the node makes in
      # `YmerNode.Script`'s What a script may call, so the application is the
      # node's to name rather than a dependency's side effect — the same reason
      # jsv below is a direct dependency. A release ships only the OTP
      # applications its closure names, and this line is what keeps `:xmerl`
      # in it if that closure ever stops needing it. `:eex` is named for the
      # same reason and one more: typst renders every document through
      # `EEx.eval_string/3` while its own application names no `:eex`, which
      # reaches the release today only through other dependencies' closures,
      # plug's and ecto's among them — so this line is what keeps every render
      # working if those ever stop needing it.
      extra_applications: [:logger, :xmerl, :eex]
    ]
  end

  def cli do
    [preferred_envs: [precommit: :test]]
  end

  # The repository's published home, named once: hex requires `links`, and
  # ExDoc's `source_url` below is what gives every module page on hexdocs its
  # "view source" link. Both point here so the two cannot drift apart.
  @source_url "https://github.com/kristiangronberg/ymer-node"

  defp package do
    [
      licenses: ["Apache-2.0"],
      links: %{"GitHub" => @source_url},
      files: ~w(lib priv .formatter.exs mix.exs README.md LICENSE)
    ]
  end

  defp docs do
    [
      main: "readme",
      source_url: @source_url,
      extras: [{"README.md", title: "README"}, "docs/glossary.md", "docs/scripts.md"],
      groups_for_modules: [Scripts: ~r/^YmerNode\.Scripts?(\.|$)/]
    ]
  end

  defp elixirc_paths(:test), do: ["lib", "test/support"]
  defp elixirc_paths(_), do: ["lib"]

  defp deps do
    [
      {:bandit, "~> 1.12.5"},
      {:credo, "~> 1.7", only: [:dev, :test], runtime: false},
      {:ecto_sql, "~> 3.13"},
      {:ecto_sqlite3, "~> 0.25"},
      {:elixlsx, "~> 0.6.0"},
      {:ex_doc, "~> 0.40.4", only: [:dev, :test], runtime: false},
      {:ex_slop, "~> 0.4.5", only: [:dev, :test], runtime: false},
      # HTML a script reads into markdown — a wiki page, a mail body. Pure
      # Elixir, with no dependencies of its own.
      {:floki, "~> 0.38.4"},
      # Direct although wymcp already locks it transitively: the node validates a
      # run's arguments against the action's own schema, so jsv is a dependency
      # the node relies on and therefore names. The alternative — wymcp's
      # public but undocumented `validate_schema/2` — would bind us to an
      # internal, which is a different thing from relying on a transitive
      # requirement the way the :plug comment below describes.
      {:jsv, "~> 0.22"},
      # Markdown a script turns into what a system accepts — HTML, or a tree a
      # renderer walks into a wiki's markup. A NIF: the build downloads its
      # precompiled artifact for the building machine's target, so no Rust
      # toolchain reaches the image.
      {:mdex, "~> 0.13.5"},
      {:mix_audit, "~> 2.1", only: [:dev, :test], runtime: false},
      {:mix_test_watch, "~> 1.0", only: :dev, runtime: false},
      # CSV a script reads or writes — `NimbleCSV.RFC4180` ready-made, and a
      # separator variant `NimbleCSV.define/2` builds as a module nested under
      # the script's (`__MODULE__.Semi`), so the compiler's purge reaches it.
      # Pure Elixir, with no dependencies of its own; with it in the
      # release Req's opt-in `:csv` decoder works too, which is Req's option
      # and not a promise the node makes.
      {:nimble_csv, "~> 1.3"},
      # The HTTP battery every script receives through its context. Runtime, and
      # for a caller's sake rather than the node's: a script's whole point is
      # reaching a system, and the node itself never makes an outbound call.
      {:req, "~> 0.7.4"},
      # XML a script writes — a SOAP envelope built with `Saxy.XML` and encoded
      # with `Saxy.encode!/2`. Pure Elixir, no dependencies, and in the release
      # regardless as xlsx_reader's parser; named here because writing XML is a
      # job the node promises it for.
      {:saxy, "~> 1.6"},
      # XML a script reads — an RSS feed, a SOAP answer — as XPath over OTP's
      # `:xmerl`, which `application/0` names above. Pure Elixir, with no
      # dependencies of its own.
      {:sweet_xml, "~> 0.7.5"},
      {:typst, "~> 0.4.4"},
      # The zone database behind every zone-aware `DateTime` call a script makes,
      # made Elixir's own by one line in `config/config.exs`. The IANA data is
      # compiled into the package, so the node makes no call for zone data:
      # castore and mint serve only tz's updater, which nothing here starts —
      # mint is in the tree for Req, castore is not in it at all. The zones are
      # the IANA release this tz bundles, and IANA moves faster than tz does, so
      # a change that touches this file may take a newer tz. Should a zone in use
      # change before tz catches up, vendor a pinned IANA version instead —
      # `mix tz.download <version>` into `config :tz, :data_dir`, with
      # `:iana_version` — and move the Dockerfile's `COPY priv` above its
      # `mix deps.compile`, which today runs first.
      {:tz, "~> 0.28.2"},
      # No direct :plug dep although `YmerNode.Mcp.Endpoint` uses `Plug.Router`:
      # bandit and wymcp are both built around Plug and pull it in — the
      # transitive requirement is relied on deliberately, not restated here.
      # Three-segment on purpose: `~> 0.8` would float onto 0.9.0, and pre-1.0
      # wymcp treats a minor as the breaking bump, so `~> 0.8.0` admits 0.8.x
      # only. Plain hex in every environment — a path dep to a sibling checkout
      # would make the build depend on this machine's layout, and the clone
      # gate exists to prove it does not.
      {:wymcp, "~> 0.8.2"},
      # The XLSX a script reads — a spreadsheet a colleague dropped in the files
      # directory. Pure Elixir; it parses through saxy, and reads numbers as
      # floats unless told otherwise, which `YmerNode.Script`'s What a script
      # may call says beneath its table.
      {:xlsx_reader, "~> 0.8.12"}
    ]
  end

  defp releases do
    [
      ymer_node: [
        include_executables_for: [:unix],
        # The `Docs` chunk is what `YmerNode.Scripts.Guide` renders the guide
        # from at the call; a release strips it by default, and the guide then
        # refuses rather than answering in part. Release-wide, like the strip.
        strip_beams: [keep: ["Docs"]]
      ]
    ]
  end

  defp aliases do
    [
      precommit: [
        "compile --warnings-as-errors",
        "deps.unlock --unused",
        "format",
        "credo --strict",
        "deps.audit",
        "docs --warnings-as-errors",
        "test --warnings-as-errors"
      ]
    ]
  end
end