defmodule Mix.Tasks.Raxol.Config do @moduledoc """ Configuration management for Raxol. ## Usage mix raxol.config [command] [options] ## Commands * `init` - Initialize a new configuration file * `validate` - Validate existing configuration * `generate` - Generate configuration templates * `docs` - Generate configuration documentation * `show` - Show current configuration * `get` - Get a specific configuration value * `set` - Set a configuration value * `backup` - Backup current configuration * `migrate` - Migrate configuration to new format ## Options * `--file` - Configuration file path (default: config/raxol.toml) * `--format` - Output format: toml, json, yaml (default: toml) * `--env` - Environment: development, production, test * `--minimal` - Generate minimal configuration * `--with-comments` - Include comments in generated config * `--with-examples` - Include examples in generated config * `--output` - Output file or directory ## Examples # Initialize a new configuration file mix raxol.config init # Initialize with specific format and options mix raxol.config init --format json --with-comments # Validate current configuration mix raxol.config validate # Generate template configuration mix raxol.config generate --output config/template.toml # Show current configuration mix raxol.config show # Get specific configuration value mix raxol.config get terminal.width # Set configuration value mix raxol.config set terminal.width 120 # Generate documentation mix raxol.config docs --output docs/config.md # Generate environment-specific config mix raxol.config init --env production --output config/prod.toml """ use Mix.Task alias Raxol.Config.{Generator, Loader, Schema} @shortdoc "Manage Raxol configuration" @impl Mix.Task def run(args) do {opts, args, _} = OptionParser.parse(args, switches: [ file: :string, format: :string, env: :string, minimal: :boolean, with_comments: :boolean, with_examples: :boolean, output: :string, help: :boolean ], aliases: [ f: :file, o: :output, h: :help ] ) if opts[:help] do Mix.shell().info(@moduledoc) :ok end command = List.first(args) || "help" Mix.Task.run("app.start") case command do "init" -> handle_init(opts) "validate" -> handle_validate(opts) "generate" -> handle_generate(opts) "docs" -> handle_docs(opts) "show" -> handle_show(opts) "get" -> handle_get(args, opts) "set" -> handle_set(args, opts) "backup" -> handle_backup(opts) "migrate" -> handle_migrate(opts) "help" -> Mix.shell().info(@moduledoc) _ -> Mix.shell().error("Unknown command: #{command}") Mix.shell().info("Run 'mix raxol.config help' for usage information") end end defp handle_init(opts) do file_path = get_config_path(opts) format = get_format(opts, file_path) env = String.to_atom(opts[:env] || "development") Mix.shell().info("Initializing Raxol configuration...") if File.exists?(file_path) and not confirm_overwrite(file_path) do Mix.shell().info("Configuration initialization cancelled.") :ok end result = generate_config(file_path, format, env, opts) handle_init_result(result) end defp generate_config(file_path, format, env, opts) do if opts[:minimal] do Generator.generate_minimal_config(file_path) else generate_env_or_default_config(file_path, format, env, opts) end end defp generate_env_or_default_config(file_path, format, env, opts) do case env do env when env in [:development, :production, :test] -> Generator.generate_env_config(env, file_path, opts) _ -> Generator.generate_default_config(file_path, format: format, comments: opts[:with_comments], examples: opts[:with_examples] ) end end defp handle_init_result({:ok, path}) do Mix.shell().info("✅ Configuration initialized at #{path}") show_next_steps(path) end defp handle_init_result({:error, reason}) do Mix.shell().error( "❌ Failed to initialize configuration: #{inspect(reason)}" ) end defp handle_validate(opts) do file_path = get_config_path(opts) Mix.shell().info("Validating configuration at #{file_path}...") case Loader.load_file(file_path) do {:ok, config} -> case Schema.validate_config(config) do {:ok, :valid} -> Mix.shell().info("✅ Configuration is valid") show_config_summary(config) {:error, errors} -> Mix.shell().error("❌ Configuration validation failed:") show_validation_errors(errors) end {:error, reason} -> Mix.shell().error("❌ Failed to load configuration: #{inspect(reason)}") end end defp handle_generate(opts) do output_path = opts[:output] || "config/template.toml" Mix.shell().info("Generating configuration template...") case Generator.generate_template(output_path, opts) do :ok -> Mix.shell().info("✅ Configuration template generated at #{output_path}") {:error, reason} -> Mix.shell().error("❌ Failed to generate template: #{inspect(reason)}") end end defp handle_docs(opts) do output_path = opts[:output] || "docs/configuration.md" Mix.shell().info("Generating configuration documentation...") result = Generator.generate_config_docs(output_path) handle_docs_result(result) end defp handle_docs_result({:ok, path}) do Mix.shell().info("✅ Configuration documentation generated at #{path}") end defp handle_show(opts) do file_path = get_config_path(opts) format = String.to_atom(opts[:format] || "pretty") Mix.shell().info("Current configuration from #{file_path}:") Mix.shell().info(String.duplicate("=", 50)) result = Loader.load_file(file_path) handle_show_result(result, format) end defp handle_show_result({:ok, config}, format) do show_config(config, format) end defp handle_show_result({:error, reason}, _format) do Mix.shell().error("❌ Failed to load configuration: #{inspect(reason)}") Mix.shell().info( "💡 Run 'mix raxol.config init' to create a configuration file" ) end defp handle_get([_, key | _], opts) do file_path = get_config_path(opts) case Loader.load_file(file_path) do {:ok, config} -> key_path = parse_key_path(key) value = get_nested_value(config, key_path) if value != nil do Mix.shell().info("#{key}: #{inspect(value)}") else Mix.shell().error("Configuration key not found: #{key}") end {:error, reason} -> Mix.shell().error("❌ Failed to load configuration: #{inspect(reason)}") end end defp handle_get(_, _) do Mix.shell().error("Usage: mix raxol.config get ") Mix.shell().info("Example: mix raxol.config get terminal.width") end defp handle_set([_, key, value | _], opts) do file_path = get_config_path(opts) Mix.shell().info("Setting #{key} = #{value} in #{file_path}") Mix.shell().info("❌ Config modification not yet implemented") Mix.shell().info("💡 Please edit the configuration file manually") end defp handle_set(_, _) do Mix.shell().error("Usage: mix raxol.config set ") Mix.shell().info("Example: mix raxol.config set terminal.width 120") end defp handle_backup(opts) do file_path = get_config_path(opts) case Loader.backup_config(file_path) do {:ok, backup_path} -> Mix.shell().info("✅ Configuration backed up to #{backup_path}") {:error, :file_not_found} -> Mix.shell().error("❌ Configuration file not found: #{file_path}") {:error, reason} -> Mix.shell().error("❌ Backup failed: #{inspect(reason)}") end end defp handle_migrate(opts) do file_path = get_config_path(opts) Mix.shell().info("Configuration migration not yet implemented") Mix.shell().info("Current file: #{file_path}") end defp get_config_path(opts) do opts[:file] || "config/raxol.toml" end defp get_format(opts, file_path) do case opts[:format] do nil -> detect_format_from_path(file_path) format -> String.to_atom(format) end end defp detect_format_from_path(path) do case String.downcase(Path.extname(path)) do ".toml" -> :toml ".json" -> :json ".yaml" -> :yaml ".yml" -> :yaml _ -> :toml end end defp confirm_overwrite(path) do Mix.shell().yes?("Configuration file #{path} already exists. Overwrite?") end defp show_next_steps(path) do Mix.shell().info(""" Next steps: 1. Review and customize the configuration in #{path} 2. Validate your configuration: mix raxol.config validate 3. View current settings: mix raxol.config show """) end defp show_config_summary(config) do sections = Map.keys(config) total_options = count_config_options(config) Mix.shell().info("") Mix.shell().info("Configuration summary:") Mix.shell().info(" Sections: #{Enum.join(sections, ", ")}") Mix.shell().info(" Total options: #{total_options}") end defp count_config_options(config) when is_map(config) do Enum.reduce(config, 0, fn {_key, value}, acc -> if is_map(value) do acc + count_config_options(value) else acc + 1 end end) end defp show_validation_errors(errors) do Enum.each(errors, fn {path, error} -> path_str = Enum.join(path, ".") Mix.shell().error(" • #{path_str}: #{error}") end) end defp show_config(config, :json) do case Jason.encode(config, pretty: true) do {:ok, json} -> Mix.shell().info(json) {:error, _} -> show_config(config, :pretty) end end defp show_config(config, :compact) do Mix.shell().info(inspect(config)) end defp show_config(config, _) do show_config_section(config, [], 0) end defp show_config_section(config, path, indent) when is_map(config) do indent_str = String.duplicate(" ", indent) Enum.each(config, fn {key, value} -> current_path = path ++ [key] case value do v when is_map(v) -> Mix.shell().info("#{indent_str}#{key}:") show_config_section(v, current_path, indent + 1) v -> Mix.shell().info("#{indent_str}#{key}: #{inspect(v)}") end end) end defp parse_key_path(key) do key |> String.split(".") |> Enum.map(&String.to_atom/1) end defp get_nested_value(map, []), do: map defp get_nested_value(map, [key | rest]) when is_map(map) do case Map.get(map, key) do nil -> nil value -> get_nested_value(value, rest) end end defp get_nested_value(_, _), do: nil end