defmodule NPM.Node.BinResolver do
@moduledoc """
Resolve executable binaries from `node_modules/.bin/`.
Provides lookup and listing of available npm binaries,
matching the behavior of `npx` and `npm exec`.
"""
@doc """
List all available binaries in `node_modules/.bin/`.
Returns a sorted list of `{command_name, target_path}` tuples.
"""
@spec list(String.t()) :: [{String.t(), String.t()}]
def list(node_modules_dir \\ "node_modules") do
bin_dir = Path.join(node_modules_dir, ".bin")
case File.ls(bin_dir) do
{:ok, entries} ->
entries
|> Enum.map(fn name -> {name, resolve_target(bin_dir, name)} end)
|> Enum.sort()
{:error, _} ->
[]
end
end
@doc """
Find the path to a specific binary command.
Returns `{:ok, path}` if found, `:error` if not.
"""
@spec find(String.t(), String.t()) :: {:ok, String.t()} | :error
def find(command, node_modules_dir \\ "node_modules") do
path = Path.join([node_modules_dir, ".bin", command])
if File.exists?(path) do
{:ok, resolve_target(Path.join(node_modules_dir, ".bin"), command)}
else
:error
end
end
@doc """
Check if a binary command is available.
"""
@spec available?(String.t(), String.t()) :: boolean()
def available?(command, node_modules_dir \\ "node_modules") do
Path.join([node_modules_dir, ".bin", command]) |> File.exists?()
end
defp resolve_target(bin_dir, name) do
link = Path.join(bin_dir, name)
case File.read_link(link) do
{:ok, target} -> Path.expand(target, bin_dir)
{:error, _} -> link
end
end
end