Current section
Files
Jump to
Current section
Files
lib/mix/tasks/ragex.configure.ex
defmodule Mix.Tasks.Ragex.Configure do
@moduledoc """
Interactive configuration wizard for Ragex setup.
## Usage
# Launch interactive wizard
mix ragex.configure
# Show current configuration
mix ragex.configure --show
Creates a `.ragex.exs` configuration file in your project root with:
- Project type detection (Elixir, Erlang, Python, JS/TS, polyglot)
- Embedding model selection with comparison table
- AI provider configuration (OpenAI, Anthropic, DeepSeek, Ollama)
- Analysis options (exclusions, cache settings)
- Custom analysis rules
The wizard detects your project structure and suggests optimal defaults.
"""
@shortdoc "Interactive configuration wizard"
use Mix.Task
alias Ragex.CLI.{Colors, Output, Prompt}
alias Ragex.Embeddings.Registry
@config_file ".ragex.exs"
@ai_providers [
{:openai, "OpenAI (GPT-4, GPT-3.5)", "OPENAI_API_KEY"},
{:anthropic, "Anthropic (Claude)", "ANTHROPIC_API_KEY"},
{:deepseek, "DeepSeek (DeepSeek-Coder)", "DEEPSEEK_API_KEY"},
{:deepseek_r1, "DeepSeek R1 (Reasoning)", "DEEPSEEK_R1_API_KEY"},
{:ollama, "Ollama (Local Models)", nil}
]
@impl Mix.Task
def run(args) do
{:ok, _} = Application.ensure_all_started(:ragex)
{opts, _, _} =
OptionParser.parse(args,
switches: [show: :boolean, help: :boolean],
aliases: [s: :show, h: :help]
)
cond do
opts[:help] ->
show_help()
opts[:show] ->
show_current_config()
true ->
run_wizard()
end
end
defp run_wizard do
Output.section("Ragex Configuration Wizard")
IO.puts(Colors.muted("This wizard will help you set up Ragex for your project."))
IO.puts(Colors.muted("You can change these settings later by editing #{@config_file}"))
IO.puts("")
# Check if config already exists
if File.exists?(@config_file) do
IO.puts(Colors.warning("⚠ Configuration file already exists: #{@config_file}"))
unless Prompt.confirm("Overwrite existing configuration?", default: :no) do
IO.puts(Colors.muted("Configuration cancelled."))
IO.puts("")
System.halt(0)
end
IO.puts("")
end
# Gather configuration
config = %{
project_type: detect_and_confirm_project_type(),
embedding_model: select_embedding_model(),
ai_providers: configure_ai_providers(),
analysis_options: configure_analysis_options(),
cache_settings: configure_cache_settings()
}
# Preview configuration
unless preview_config(config) do
IO.puts(Colors.muted("Configuration cancelled."))
IO.puts("")
System.halt(0)
end
# Write configuration file
write_config_file(config)
Output.section("Setup Complete")
IO.puts(Colors.success("✓ Configuration saved to #{@config_file}"))
IO.puts("")
IO.puts(Colors.bold("Next steps:"))
Output.list(
[
"Analyze your codebase: #{Colors.highlight("mix ragex.cache.refresh --path .")}",
"Start the MCP server: #{Colors.highlight("mix ragex.server")}",
"Or run interactive refactoring: #{Colors.highlight("mix ragex.refactor")}"
],
indent: 2,
bullet: "→"
)
IO.puts("")
end
defp detect_and_confirm_project_type do
Output.section("Project Type Detection")
detected = detect_project_languages()
IO.puts(Colors.info("Detected languages:"))
if detected == [] do
IO.puts(Colors.muted(" (none detected)"))
else
Enum.each(detected, fn lang ->
IO.puts(" • #{Colors.highlight(lang)}")
end)
end
IO.puts("")
# Let user select primary language
language_options = [
"elixir - Elixir projects",
"erlang - Erlang/OTP projects",
"python - Python projects",
"javascript - JavaScript/TypeScript projects",
"polyglot - Multiple languages"
]
index =
Prompt.select("Select primary project type:", language_options,
default: default_language_index(detected)
)
primary_type =
case index do
0 -> :elixir
1 -> :erlang
2 -> :python
3 -> :javascript
4 -> :polyglot
end
IO.puts("")
IO.puts(Colors.success("Selected: #{primary_type}"))
IO.puts("")
primary_type
end
defp detect_project_languages do
languages = []
# Check for Elixir
languages =
if File.exists?("mix.exs") or File.dir?("lib") do
["Elixir" | languages]
else
languages
end
# Check for Erlang
languages =
if File.exists?("rebar.config") or File.dir?("src") do
["Erlang" | languages]
else
languages
end
# Check for Python
languages =
if File.exists?("setup.py") or File.exists?("pyproject.toml") or
File.exists?("requirements.txt") do
["Python" | languages]
else
languages
end
# Check for JavaScript/TypeScript
languages =
if File.exists?("package.json") or File.exists?("tsconfig.json") do
["JavaScript/TypeScript" | languages]
else
languages
end
Enum.reverse(languages)
end
defp default_language_index(detected) do
cond do
"Elixir" in detected -> 0
"Erlang" in detected -> 1
"Python" in detected -> 2
"JavaScript/TypeScript" in detected -> 3
length(detected) > 1 -> 4
true -> 0
end
end
defp select_embedding_model do
Output.section("Embedding Model Selection")
IO.puts(Colors.bold("Available models:"))
IO.puts("")
models = Registry.all()
# Display model comparison
for {index, model} <- Enum.with_index(models) do
marker = if model.id == Registry.default(), do: " (default)", else: ""
IO.puts("#{index + 1}. #{Colors.info(to_string(model.id))}#{marker}")
IO.puts(Colors.muted(" #{model.name}"))
Output.key_value(
[
{"Dimensions", model.dimensions},
{"Type", model.type},
{"Best for", model_use_case(model.id)}
],
indent: 3
)
IO.puts("")
end
IO.puts(Colors.muted("Recommendation: all_minilm_l6_v2 (default) for balanced speed/quality"))
IO.puts("")
model_names = Enum.map(models, &to_string(&1.id))
selected_name = Prompt.select("Select embedding model:", model_names, default: 0)
selected_model = Enum.find(models, hd(models), fn m -> to_string(m.id) == selected_name end)
IO.puts("")
IO.puts(Colors.success("Selected: #{selected_model.id}"))
IO.puts("")
selected_model.id
end
defp model_use_case(:all_minilm_l6_v2), do: "General purpose, fast"
defp model_use_case(:all_mpnet_base_v2), do: "High quality, slower"
defp model_use_case(:codebert_base), do: "Code-specific, technical"
defp model_use_case(:paraphrase_multilingual), do: "Multi-language support"
defp model_use_case(_), do: "General purpose"
defp configure_ai_providers do
Output.section("AI Provider Configuration")
IO.puts(Colors.muted("Configure AI providers for enhanced analysis features."))
IO.puts(Colors.muted("Leave API keys empty to skip provider setup."))
IO.puts("")
providers =
for {provider_id, provider_name, env_var} <- @ai_providers do
IO.puts(Colors.bold(provider_name))
if env_var do
IO.puts(Colors.muted("Environment variable: #{env_var}"))
# Check if already set
current_key = System.get_env(env_var)
if current_key do
IO.puts(Colors.success("✓ API key already set in environment"))
use_existing = Prompt.confirm("Use existing key?", default: :yes)
if use_existing do
IO.puts("")
{provider_id, %{enabled: true, use_env: true}}
else
api_key = Prompt.input("API key (or empty to skip)", default: "", masked: true)
IO.puts("")
if api_key == "" do
{provider_id, %{enabled: false}}
else
{provider_id, %{enabled: true, api_key: api_key}}
end
end
else
api_key = Prompt.input("API key (or empty to skip)", default: "", masked: true)
IO.puts("")
if api_key == "" do
{provider_id, %{enabled: false}}
else
{provider_id, %{enabled: true, api_key: api_key}}
end
end
else
# Ollama - no API key needed
enabled = Prompt.confirm("Enable Ollama (local models)?", default: :no)
IO.puts("")
if enabled do
base_url = Prompt.input("Ollama base URL", default: "http://localhost:11434")
IO.puts("")
{provider_id, %{enabled: true, base_url: base_url}}
else
{provider_id, %{enabled: false}}
end
end
end
Map.new(providers)
end
defp configure_analysis_options do
Output.section("Analysis Options")
IO.puts(Colors.muted("Configure which files and directories to analyze."))
IO.puts("")
# Exclusions
IO.puts(Colors.bold("File/directory exclusions:"))
default_exclusions = suggest_exclusions()
IO.puts(Colors.muted("Default exclusions: #{Enum.join(default_exclusions, ", ")}"))
use_defaults = Prompt.confirm("Use default exclusions?", default: :yes)
IO.puts("")
exclusions =
if use_defaults do
default_exclusions
else
IO.puts("Enter patterns to exclude (comma-separated):")
input = Prompt.input("Exclusions", default: Enum.join(default_exclusions, ","))
String.split(input, ",") |> Enum.map(&String.trim/1)
end
# Max file size
IO.puts(Colors.bold("Maximum file size:"))
max_size = Prompt.number("Max file size (KB)", default: 1024, min: 1)
IO.puts("")
# Auto-analyze on save
auto_analyze = Prompt.confirm("Enable auto-analysis on file changes?", default: :no)
IO.puts("")
%{
exclusions: exclusions,
max_file_size_kb: max_size,
auto_analyze: auto_analyze
}
end
defp suggest_exclusions do
base_exclusions = ["_build", "deps", ".git", "node_modules", ".elixir_ls"]
# Add language-specific exclusions
extra =
cond do
File.exists?("mix.exs") -> ["cover"]
File.exists?("package.json") -> ["dist", "build", ".next"]
File.exists?("setup.py") -> ["__pycache__", "venv", ".pytest_cache"]
true -> []
end
base_exclusions ++ extra
end
defp configure_cache_settings do
Output.section("Cache Settings")
IO.puts(Colors.muted("Configure caching behavior for embeddings and analysis."))
IO.puts("")
# Cache directory
default_cache_dir = "~/.ragex/cache"
IO.puts("Cache directory: #{default_cache_dir}")
use_default = Prompt.confirm("Use default cache directory?", default: :yes)
IO.puts("")
cache_dir =
if use_default do
default_cache_dir
else
Prompt.input("Cache directory", default: default_cache_dir)
end
# TTL
IO.puts(Colors.bold("Cache Time-To-Live (TTL):"))
ttl_hours = Prompt.number("Hours until cache expires", default: 24, min: 1, max: 720)
IO.puts("")
# Auto-refresh
auto_refresh = Prompt.confirm("Auto-refresh stale cache on startup?", default: :yes)
IO.puts("")
%{
cache_dir: cache_dir,
ttl_hours: ttl_hours,
auto_refresh: auto_refresh
}
end
defp preview_config(config) do
Output.section("Configuration Preview")
IO.puts(Colors.bold("Project Type:"))
IO.puts(" #{config.project_type}")
IO.puts("")
IO.puts(Colors.bold("Embedding Model:"))
IO.puts(" #{config.embedding_model}")
IO.puts("")
IO.puts(Colors.bold("AI Providers:"))
enabled_providers =
config.ai_providers
|> Enum.filter(fn {_id, settings} -> settings.enabled end)
|> Enum.map(fn {id, _settings} -> id end)
if enabled_providers == [] do
IO.puts(Colors.muted(" (none configured)"))
else
Enum.each(enabled_providers, fn provider ->
IO.puts(" • #{Colors.success(to_string(provider))}")
end)
end
IO.puts("")
IO.puts(Colors.bold("Analysis Options:"))
Output.key_value(
[
{"Exclusions", Enum.join(config.analysis_options.exclusions, ", ")},
{"Max file size", "#{config.analysis_options.max_file_size_kb} KB"},
{"Auto-analyze", if(config.analysis_options.auto_analyze, do: "yes", else: "no")}
],
indent: 2
)
IO.puts("")
IO.puts(Colors.bold("Cache Settings:"))
Output.key_value(
[
{"Directory", config.cache_settings.cache_dir},
{"TTL", "#{config.cache_settings.ttl_hours} hours"},
{"Auto-refresh", if(config.cache_settings.auto_refresh, do: "yes", else: "no")}
],
indent: 2
)
IO.puts("")
Prompt.confirm("Save this configuration?", default: :yes)
end
defp write_config_file(config) do
content = generate_config_content(config)
File.write!(@config_file, content)
end
defp generate_config_content(config) do
"""
# Ragex Configuration
# Generated by mix ragex.configure on #{DateTime.utc_now() |> DateTime.to_string()}
import Config
# Project type: #{config.project_type}
config :ragex, :project_type, :#{config.project_type}
# Embedding model
config :ragex, :embedding_model, :#{config.embedding_model}
#{generate_ai_providers_config(config.ai_providers)}
# Analysis options
config :ragex, :analysis,
exclusions: #{inspect(config.analysis_options.exclusions)},
max_file_size_kb: #{config.analysis_options.max_file_size_kb},
auto_analyze: #{config.analysis_options.auto_analyze}
# Cache settings
config :ragex, :cache,
cache_dir: "#{config.cache_settings.cache_dir}",
ttl_hours: #{config.cache_settings.ttl_hours},
auto_refresh: #{config.cache_settings.auto_refresh}
# MCP Server settings
config :ragex, :mcp,
stdio: true,
log_level: :info
# Additional custom configuration can be added below
"""
end
defp generate_ai_providers_config(providers) do
enabled =
providers
|> Enum.filter(fn {_id, settings} -> settings.enabled end)
if enabled == [] do
"# AI providers (none configured)\n# config :ragex, :ai_providers, []"
else
configs =
Enum.map(enabled, fn {provider_id, settings} ->
if Map.has_key?(settings, :use_env) do
" #{provider_id}: %{api_key: {:system, \"#{get_env_var_name(provider_id)}\"}}"
else
case provider_id do
:ollama ->
" #{provider_id}: %{base_url: \"#{settings.base_url}\"}"
_ ->
" #{provider_id}: %{api_key: \"#{settings.api_key}\"}"
end
end
end)
"""
# AI providers
config :ragex, :ai_providers, [
#{Enum.join(configs, ",\n")}
]
"""
end
end
defp get_env_var_name(provider_id) do
case provider_id do
:openai -> "OPENAI_API_KEY"
:anthropic -> "ANTHROPIC_API_KEY"
:deepseek -> "DEEPSEEK_API_KEY"
:deepseek_r1 -> "DEEPSEEK_R1_API_KEY"
_ -> "#{String.upcase(to_string(provider_id))}_API_KEY"
end
end
defp show_current_config do
Output.section("Current Configuration")
if File.exists?(@config_file) do
IO.puts(Colors.success("✓ Configuration file exists: #{@config_file}"))
IO.puts("")
# Read and display key settings
display_config_summary()
else
IO.puts(Colors.muted("No configuration file found: #{@config_file}"))
IO.puts("")
IO.puts("Run #{Colors.highlight("mix ragex.configure")} to create one.")
IO.puts("")
end
end
defp display_config_summary do
# Display key configuration values from application env
IO.puts(Colors.bold("Active Configuration:"))
IO.puts("")
Output.key_value(
[
{"Project type", Application.get_env(:ragex, :project_type, :elixir)},
{"Embedding model", Application.get_env(:ragex, :embedding_model, Registry.default())},
{"Cache enabled", Application.get_env(:ragex, :cache_enabled, true)}
],
indent: 2
)
IO.puts("")
# AI providers
providers = Application.get_env(:ragex, :ai_providers, [])
IO.puts(Colors.bold("Configured AI Providers:"))
if providers == [] do
IO.puts(Colors.muted(" (none)"))
else
Enum.each(providers, fn {provider, _settings} ->
IO.puts(" • #{provider}")
end)
end
IO.puts("")
end
defp show_help do
IO.puts("""
#{Colors.bold("Ragex Configuration Wizard")}
#{Colors.info("Interactive configuration:")}
mix ragex.configure
#{Colors.info("Show current configuration:")}
mix ragex.configure --show
#{Colors.info("What gets configured:")}
• Project type detection (Elixir, Erlang, Python, JS/TS, polyglot)
• Embedding model selection with comparison
• AI provider setup (OpenAI, Anthropic, DeepSeek, Ollama)
• Analysis options (exclusions, file size limits)
• Cache settings (directory, TTL, auto-refresh)
The wizard creates a #{Colors.highlight(".ragex.exs")} file in your project root.
#{Colors.muted("You can edit this file manually or re-run the wizard to update.")}
""")
end
end