···2929 def run(target, opts \\ [])
30303131 def run(target, opts) when is_binary(target) do
3232- run(Nix.Eval.Request.parse(target, Keyword.get(opts, :attr)), opts)
3232+ request_opts = Keyword.take(opts, [:attr, :type])
3333+ run(Nix.Eval.Request.parse(target, request_opts), opts)
3334 end
34353536 def run(%Nix.Eval.Request{} = request, opts) do
+44-22
apps/nix/lib/nix/eval/request.ex
···11defmodule Nix.Eval.Request do
22+ @moduledoc """
33+ Represents a Nix evaluation request.
44+55+ Supports both flake and path-based evaluation targets.
66+ """
77+28 use TypedStruct
39410 require Logger
1111+1212+ alias Nix.Eval.Type
513614 typedstruct do
715 field :id, String.t()
···1119 field :root_id, String.t() | nil, default: nil
1220 end
13211414- def parse(path, attr \\ nil) do
1515- type = detect_type(path)
2222+ @doc """
2323+ Parse a path into an evaluation request.
2424+2525+ ## Options
2626+ - `:attr` - Attribute path to evaluate (e.g., "packages.x86_64-linux")
2727+ - `:type` - Explicit type (:flake, :path, or :auto for auto-detection). Default: :auto
2828+2929+ ## Examples
3030+3131+ iex> Nix.Eval.Request.parse(".")
3232+ %Nix.Eval.Request{type: :flake, path: ".", ...}
16331717- {path, attribute} = parse_path(type, path, attr)
3434+ iex> Nix.Eval.Request.parse("./default.nix", type: :path)
3535+ %Nix.Eval.Request{type: :path, path: "/absolute/path/default.nix", ...}
3636+ """
3737+ def parse(path, opts \\ [])
3838+3939+ def parse(path, opts) when is_list(opts) do
4040+ attr = Keyword.get(opts, :attr)
4141+ explicit_type = Keyword.get(opts, :type, :auto)
4242+4343+ type = resolve_type(path, explicit_type)
4444+ {parsed_path, attribute} = parse_path(type, path, attr)
18451946 %__MODULE__{
2047 id: new_id(),
2148 type: type,
2222- path: path,
4949+ path: parsed_path,
2350 attr: attribute
2451 }
2552 end
26532727- def new_id(), do: "eval_#{Cuid2Ex.create()}"
5454+ # Backwards compatibility: parse(path, attr) where attr is a string
5555+ def parse(path, attr) when is_binary(attr) or is_nil(attr) do
5656+ parse(path, attr: attr)
5757+ end
28582929- def detect_type(path) do
3030- cond do
3131- String.match?(path, ~r{#}) ->
3232- :flake
5959+ def new_id, do: "eval_#{Cuid2Ex.create()}"
33603434- String.match?(path, ~r{://}) ->
3535- :flake
3636-3737- String.ends_with?(path, ".nix") ->
3838- :path
3939-4040- File.exists?(Path.expand("flake.nix", path)) ->
4141- :flake
4242-4343- File.exists?(Path.expand("flake.nix", path)) ->
4444- :path
4545-4646- true ->
6161+ defp resolve_type(path, :auto) do
6262+ case Type.detect(path) do
6363+ {:error, :unknown_type} ->
4764 Logger.error(msg: "Failed to detect type", path: path)
4865 raise RuntimeError
6666+6767+ type ->
6868+ type
4969 end
5070 end
7171+7272+ defp resolve_type(_path, type) when type in [:flake, :path], do: type
51735274 def parse_path(:flake, path, nil) do
5375 case String.split(path, "#") do
+74
apps/nix/lib/nix/eval/type.ex
···11+defmodule Nix.Eval.Type do
22+ @moduledoc """
33+ Type detection for Nix evaluation targets.
44+55+ Determines whether a target should be evaluated as a flake or path-based expression.
66+ """
77+88+ @type t :: :flake | :path
99+1010+ @doc """
1111+ Detect the evaluation type for a given path.
1212+1313+ Returns `:flake` or `:path` based on the path characteristics:
1414+ - Contains `#` → flake (has attribute fragment)
1515+ - Contains `://` → flake (URL scheme)
1616+ - Ends with `.nix` → path
1717+ - Has `flake.nix` in directory → flake
1818+ - Has `default.nix` in directory → path
1919+ - Otherwise → `{:error, :unknown_type}`
2020+2121+ ## Examples
2222+2323+ iex> Nix.Eval.Type.detect(".")
2424+ :flake # if flake.nix exists
2525+2626+ iex> Nix.Eval.Type.detect(".#packages")
2727+ :flake
2828+2929+ iex> Nix.Eval.Type.detect("github:NixOS/nixpkgs")
3030+ :flake
3131+3232+ iex> Nix.Eval.Type.detect("./default.nix")
3333+ :path
3434+ """
3535+ def detect(path) do
3636+ cond do
3737+ String.match?(path, ~r{#}) ->
3838+ :flake
3939+4040+ String.match?(path, ~r{://}) ->
4141+ :flake
4242+4343+ String.ends_with?(path, ".nix") ->
4444+ :path
4545+4646+ File.exists?(Path.expand("flake.nix", path)) ->
4747+ :flake
4848+4949+ File.exists?(Path.expand("default.nix", path)) ->
5050+ :path
5151+5252+ true ->
5353+ {:error, :unknown_type}
5454+ end
5555+ end
5656+5757+ @doc """
5858+ Check if a type is valid.
5959+6060+ ## Examples
6161+6262+ iex> Nix.Eval.Type.valid?(:flake)
6363+ true
6464+6565+ iex> Nix.Eval.Type.valid?(:path)
6666+ true
6767+6868+ iex> Nix.Eval.Type.valid?(:invalid)
6969+ false
7070+ """
7171+ def valid?(:flake), do: true
7272+ def valid?(:path), do: true
7373+ def valid?(_), do: false
7474+end
+3-3
apps/nix/test/nix/eval/request_test.exs
···11defmodule Nix.Eval.RequestTest do
22 use ExUnit.Case
3344- test "detect_type/1" do
55- assert Nix.Eval.Request.detect_type("/tmp#") == :flake
66- assert Nix.Eval.Request.detect_type(Path.expand("../../../../..", __DIR__)) == :flake
44+ test "Nix.Eval.Type.detect/1" do
55+ assert Nix.Eval.Type.detect("/tmp#") == :flake
66+ assert Nix.Eval.Type.detect(Path.expand("../../../../..", __DIR__)) == :flake
77 end
8899 test "parse_path/3" do
+2-1
apps/sower_cli/.formatter.exs
···11# Used by "mix format"
22[
33- inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
33+ inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
44+ import_deps: [:typedstruct]
45]