Current section

Files

Jump to
metastatic lib metastatic cli.ex
Raw

lib/metastatic/cli.ex

defmodule Metastatic.CLI do
@moduledoc """
Shared utilities for Metastatic CLI commands.
Provides common functionality for Mix tasks including:
- Adapter lookup by language or file extension
- File path handling
- Error formatting
- Success/error reporting
"""
alias Metastatic.Languages
@type language :: atom()
@type file_path :: String.t()
@doc """
Get adapter for a given language.
Delegates to `Metastatic.Languages.get_adapter/1`.
## Examples
iex> Metastatic.CLI.get_adapter(:python)
{:ok, Metastatic.Adapters.Python}
iex> Metastatic.CLI.get_adapter(:unknown)
{:error, "Unsupported language: unknown. Supported: elixir, erlang, haskell, python, ruby"}
"""
@spec get_adapter(language()) :: {:ok, module()} | {:error, String.t()}
defdelegate get_adapter(language), to: Languages
@doc """
Detect language from file extension.
Delegates to `Metastatic.Languages.detect_language/1`.
## Examples
iex> Metastatic.CLI.detect_language("foo.py")
{:ok, :python}
iex> Metastatic.CLI.detect_language("foo.ex")
{:ok, :elixir}
iex> Metastatic.CLI.detect_language("foo.erl")
{:ok, :erlang}
iex> Metastatic.CLI.detect_language("foo.rb")
{:ok, :ruby}
iex> Metastatic.CLI.detect_language("foo.txt")
{:error, "Cannot detect language from extension: .txt"}
"""
@spec detect_language(file_path()) :: {:ok, language()} | {:error, String.t()}
defdelegate detect_language(path), to: Languages
@doc """
Parse a language string from CLI options into an atom.
Delegates to `Metastatic.Languages.parse_language/1`.
## Examples
iex> Metastatic.CLI.parse_language("python")
{:ok, :python}
iex> Metastatic.CLI.parse_language(nil)
nil
iex> Metastatic.CLI.parse_language("brainfuck")
{:error, "Invalid language: brainfuck. Supported: elixir, erlang, haskell, python, ruby"}
"""
@spec parse_language(String.t() | nil) :: {:ok, atom()} | {:error, String.t()} | nil
defdelegate parse_language(lang_str), to: Languages
@doc """
Read file contents.
## Examples
iex> Metastatic.CLI.read_file("test.py")
{:ok, "print('hello')"}
iex> Metastatic.CLI.read_file("nonexistent.py")
{:error, "Cannot read file test.py: enoent"}
"""
@spec read_file(file_path()) :: {:ok, String.t()} | {:error, String.t()}
def read_file(path) do
case File.read(path) do
{:ok, content} -> {:ok, content}
{:error, reason} -> {:error, "Cannot read file #{path}: #{reason}"}
end
end
@doc """
Write file contents, creating directories if needed.
## Examples
iex> Metastatic.CLI.write_file("output/test.py", "print('hello')")
:ok
"""
@spec write_file(file_path(), String.t()) :: :ok | {:error, String.t()}
def write_file(path, content) do
case File.mkdir_p(Path.dirname(path)) do
:ok ->
case File.write(path, content) do
:ok -> :ok
{:error, reason} -> {:error, "Cannot write file #{path}: #{reason}"}
end
{:error, reason} ->
{:error, "Cannot create directory for #{path}: #{reason}"}
end
end
@doc """
Format error message for CLI output.
Prefixes with red "Error: " for terminal output.
"""
@spec format_error(String.t()) :: String.t()
def format_error(message) do
IO.ANSI.format([:red, :bright, "Error: ", :reset, message]) |> IO.iodata_to_binary()
end
@doc """
Format success message for CLI output.
Prefixes with green checkmark for terminal output.
"""
@spec format_success(String.t()) :: String.t()
def format_success(message) do
IO.ANSI.format([:green, :bright, "✓ ", :reset, message]) |> IO.iodata_to_binary()
end
@doc """
Format info message for CLI output.
Prefixes with blue info icon for terminal output.
"""
@spec format_info(String.t()) :: String.t()
def format_info(message) do
IO.ANSI.format([:blue, :bright, "→ ", :reset, message]) |> IO.iodata_to_binary()
end
@doc """
Print error and exit with status code 1.
"""
@spec fatal(String.t()) :: no_return()
def fatal(message) do
Mix.shell().error(format_error(message))
exit({:shutdown, 1})
end
end