{"repo": "b08x/blueprints-cli", "files": {"lib/blueprintsCLI/setup/provider_detector.rb": {"64a20b367209eac0d73cc0df9909e85d962ad6d5": {"t": 1785745748.9876447, "n": 13572, "h": "ecf09b296423498f", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule BlueprintsCLI\n module Setup\n # ProviderDetector handles AI provider detection, configuration, and testing.\n # It scans environment variables for API keys, guides users through provider\n # selection, and tests connections to ensure providers are working correctly.\n class ProviderDetector\n # Provider information including environment variables and capabilities\n PROVIDERS = {\n openai: {\n name: \"OpenAI\",\n env_vars: %w[OPENAI_API_KEY],\n description: \"Industry-leading GPT models with excellent performance\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-3.5-turbo\"],\n capabilities: %w[chat embedding image_generation tools],\n pricing: \"Mid-range\",\n notes: \"Direct OpenAI API access\",\n },\n openrouter: {\n name: \"OpenRouter\",\n env_vars: %w[OPENROUTER_API_KEY],\n description: \"Access to multiple providers through single API\",\n models: [\"gpt-4o\", \"claude-3-5-sonnet\", \"llama-3.1-70b\"],\n capabilities: %w[chat embedding],\n pricing: \"Variable by model\",\n notes: \"Unified access to multiple AI providers\",\n },\n anthropic: {\n name: \"Anthropic\",\n env_vars: %w[ANTHROPIC_API_KEY],\n description: \"Claude models known for safety and reasoning\",\n models: [\"claude-3-5-sonnet\", \"claude-3-haiku\", \"claude-3-opus\"],\n capabilities: %w[chat tools],\n pricing: \"Mid-range\",\n notes: \"Direct Anthropic API access\",\n },\n gemini: {\n name: \"Google Gemini\",\n env_vars: %w[GEMINI_API_KEY GOOGLE_API_KEY],\n description: \"Google's multimodal AI with competitive pricing\",\n models: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n capabilities: %w[chat embedding image_generation tools vision],\n pricing: \"Low-cost\",\n notes: \"Excellent for cost-conscious applications\",\n },\n deepseek: {\n name: \"DeepSeek\",\n env_vars: %w[DEEPSEEK_API_KEY],\n description: \"High-performance reasoning models at low cost\",\n models: %w[deepseek-chat deepseek-coder],\n capabilities: %w[chat tools],\n pricing: \"Very low-cost\",\n notes: \"Excellent reasoning capabilities\",\n },\n }.freeze\n\n # Initialize the provider detector\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @detected_providers = {}\n end\n\n # Detect available providers and guide user through configuration\n #\n # @return [Boolean] True if at least one provider was configured\n def detect_and_configure\n @logger.info(\"Scanning for AI provider API keys...\")\n\n scan_environment_variables\n display_detected_providers\n\n if @detected_providers.any?\n configure_detected_providers\n else\n guide_manual_configuration\n end\n\n finalize_provider_configuration\n end\n\n private def scan_environment_variables\n PROVIDERS.each do |provider_key, provider_info|\n provider_info[:env_vars].each do |env_var|\n next unless ENV[env_var] && !ENV[env_var].empty?\n\n @detected_providers[provider_key] = {\n info: provider_info,\n api_key: ENV.fetch(env_var, nil),\n env_var:,\n }\n @logger.success(\"Found #{provider_info[:name]} API key (#{env_var})\")\n break # Use first found key for this provider\n end\n end\n end\n\n # Display information about detected providers\n private def display_detected_providers\n if @detected_providers.any?\n puts \"\\nšŸ” Detected AI Providers:\"\n @detected_providers.each_value do |config|\n info = config[:info]\n puts \" āœ“ #{info[:name]} - #{info[:description]}\"\n puts \" Models: #{info[:models].join(', ')}\"\n puts \" Pricing: #{info[:pricing]}\"\n puts \"\"\n end\n else\n puts \"\\nāŒ No AI provider API keys found in environment variables.\"\n puts \"Don't worry! We'll help you configure providers manually.\"\n end\n end\n\n # Configure detected providers\n #\n # @return [Boolean] True if configuration completed\n private def configure_detected_providers\n selected_providers = {}\n\n if @detected_providers.size == 1\n # Auto-select single provider\n provider_key = @detected_providers.keys.first\n selected_providers[provider_key] = @detected_providers[provider_key]\n @logger.info(\"Auto-selecting #{@detected_providers[provider_key][:info][:name]}\")\n else\n # Let user choose from detected providers\n selected_providers = prompt_provider_selection\n end\n\n configure_providers(selected_providers)\n end\n\n # Prompt user to select from detected providers\n #\n # @return [Hash] Selected providers configuration\n private def prompt_provider_selection\n selected = {}\n\n puts \"\\nšŸ¤– Multiple AI providers detected. Choose which ones to configure:\"\n\n @detected_providers.each do |provider_key, config|\n info = config[:info]\n use_provider = @prompt.yes?(\"Configure #{info[:name]}?\", default: true)\n\n next unless use_provider\n\n selected[provider_key] = config\n\n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n end\n end\n\n selected\n end\n\n # Guide user through manual provider configuration\n #\n # @return [Boolean] True if manual configuration completed\n private def guide_manual_configuration\n puts \"\\nšŸ”§ Manual Provider Configuration\"\n puts \"Please choose an AI provider to configure:\"\n\n # Display all available providers\n provider_choices = PROVIDERS.map do |key, info|\n { name: \"#{info[:name]} - #{info[:description]}\", value: key }\n end\n provider_choices << { name: \"Skip provider setup for now\", value: :skip }\n\n selected_provider = @prompt.select(\"Select a provider:\", provider_choices)\n return true if selected_provider == :skip\n\n configure_manual_provider(selected_provider)\n end\n\n # Configure a provider manually\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Boolean] True if configuration completed\n private def configure_manual_provider(provider_key)\n provider_info = PROVIDERS[provider_key]\n\n puts \"\\nšŸ“‹ Configuring #{provider_info[:name]}\"\n puts \"Description: #{provider_info[:description]}\"\n puts \"Required environment variable: #{provider_info[:env_vars].first}\"\n puts \"\"\n\n api_key = @prompt.mask(\"Enter your #{provider_info[:name]} API key:\")\n\n if api_key.empty?\n @logger.warn(\"No API key provided for #{provider_info[:name]}\")\n return false\n end\n\n # Test the connection\n if test_provider_connection(provider_key, api_key)\n @logger.success(\"#{provider_info[:name]} connection verified!\")\n store_manual_provider(provider_key, api_key)\n true\n else\n @logger.failure(\"Failed to connect to #{provider_info[:name]}\")\n retry_config = @prompt.yes?(\"Retry configuration?\", default: true)\n retry_config ? configure_manual_provider(provider_key) : false\n end\n end\n\n # Store manually configured provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n private def store_manual_provider(provider_key, api_key)\n @detected_providers[provider_key] = {\n info: PROVIDERS[provider_key],\n api_key:,\n env_var: PROVIDERS[provider_key][:env_vars].first,\n }\n end\n\n # Test provider connection\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key to test\n # @return [Boolean] True if connection successful\n private def test_provider_connection(provider_key, api_key)\n @logger.info(\"Testing #{PROVIDERS[provider_key][:name]} connection...\")\n\n # Configure RubyLLM for testing\n original_config = backup_rubyllm_config\n configure_rubyllm_for_test(provider_key, api_key)\n\n begin\n # Simple test request\n chat = RubyLLM.chat(\n model: get_test_model(provider_key),\n provider: map_provider_for_rubyllm(provider_key)\n )\n response = chat.ask(\"Hello! Please respond with just 'OK'\")\n\n success = response&.content&.include?(\"OK\")\n @logger.debug(\"Test response: #{response&.content}\") if ENV[\"DEBUG\"]\n success\n rescue => e\n @logger.debug(\"Connection test failed: #{e.message}\") if ENV[\"DEBUG\"]\n false\n ensure\n restore_rubyllm_config(original_config)\n end\n end\n\n # Get appropriate test model for provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [String] Model name for testing\n private def get_test_model(provider_key)\n case provider_key\n when :openai, :openrouter\n \"gpt-4o-mini\"\n when :anthropic\n \"claude-3-haiku-20240307\"\n when :gemini\n \"gemini-2.0-flash\"\n when :deepseek\n \"deepseek-chat\"\n else\n \"gpt-4o-mini\"\n end\n end\n\n # Map provider key to RubyLLM provider format\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Symbol] RubyLLM provider symbol\n private def map_provider_for_rubyllm(provider_key)\n case provider_key\n when :openrouter\n :openai # OpenRouter uses OpenAI API format\n else\n provider_key\n end\n end\n\n # Configure RubyLLM for testing\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n private def configure_rubyllm_for_test(provider_key, api_key)\n RubyLLM.configure do |config|\n case provider_key\n when :openai\n config.openai_api_key = api_key\n when :openrouter\n config.openai_api_key = api_key\n config.openai_api_base = \"https://openrouter.ai/api/v1\"\n when :anthropic\n config.anthropic_api_key = api_key\n when :gemini\n config.gemini_api_key = api_key\n when :deepseek\n config.deepseek_api_key = api_key\n end\n end\n end\n\n # Backup current RubyLLM configuration\n #\n # @return [Hash] Current configuration\n private def backup_rubyllm_config\n {\n openai_api_key: RubyLLM.config.openai_api_key,\n openai_api_base: RubyLLM.config.openai_api_base,\n anthropic_api_key: RubyLLM.config.anthropic_api_key,\n gemini_api_key: RubyLLM.config.gemini_api_key,\n deepseek_api_key: RubyLLM.config.deepseek_api_key,\n }\n end\n\n # Restore RubyLLM configuration\n #\n # @param config [Hash] Configuration to restore\n private def restore_rubyllm_config(config)\n RubyLLM.configure do |rubyllm_config|\n config.each do |key, value|\n rubyllm_config.public_send(\"#{key}=\", value) if value\n end\n end\n end\n\n # Configure selected providers\n #\n # @param providers [Hash] Selected providers configuration\n private def configure_providers(providers)\n @setup_data[:providers] = {}\n\n providers.each do |provider_key, config|\n @setup_data[:providers][provider_key] = {\n name: config[:info][:name],\n api_key: config[:api_key],\n env_var: config[:env_var],\n capabilities: config[:info][:capabilities],\n models: config[:info][:models],\n }\n end\n end\n\n # Finalize provider configuration\n #\n # @return [Boolean] True if at least one provider configured\n private def finalize_provider_configuration\n if @setup_data[:providers]&.any?\n # Select primary provider\n if @setup_data[:providers].size == 1\n primary_provider = @setup_data[:providers].keys.first\n else\n choices = @setup_data[:providers].map do |key, config|\n { name: config[:name], value: key }\n end\n primary_provider = @prompt.select(\"Select primary AI provider:\", choices)\n end\n\n @setup_data[:primary_provider] = primary_provider\n @logger.success(\"Provider configuration completed!\")\n @logger.info(\"Primary provider: #{@setup_data[:providers][primary_provider][:name]}\")\n\n true\n else\n @logger.warn(\"No AI providers configured. Some features may not work.\")\n false\n end\n end\n end\n end\nend\n"}, "84c01180385ad460755977240718d6e31767b1b5": {"t": 1785756132.5590918, "n": 13572, "h": "ecf09b296423498f", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule BlueprintsCLI\n module Setup\n # ProviderDetector handles AI provider detection, configuration, and testing.\n # It scans environment variables for API keys, guides users through provider\n # selection, and tests connections to ensure providers are working correctly.\n class ProviderDetector\n # Provider information including environment variables and capabilities\n PROVIDERS = {\n openai: {\n name: \"OpenAI\",\n env_vars: %w[OPENAI_API_KEY],\n description: \"Industry-leading GPT models with excellent performance\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-3.5-turbo\"],\n capabilities: %w[chat embedding image_generation tools],\n pricing: \"Mid-range\",\n notes: \"Direct OpenAI API access\",\n },\n openrouter: {\n name: \"OpenRouter\",\n env_vars: %w[OPENROUTER_API_KEY],\n description: \"Access to multiple providers through single API\",\n models: [\"gpt-4o\", \"claude-3-5-sonnet\", \"llama-3.1-70b\"],\n capabilities: %w[chat embedding],\n pricing: \"Variable by model\",\n notes: \"Unified access to multiple AI providers\",\n },\n anthropic: {\n name: \"Anthropic\",\n env_vars: %w[ANTHROPIC_API_KEY],\n description: \"Claude models known for safety and reasoning\",\n models: [\"claude-3-5-sonnet\", \"claude-3-haiku\", \"claude-3-opus\"],\n capabilities: %w[chat tools],\n pricing: \"Mid-range\",\n notes: \"Direct Anthropic API access\",\n },\n gemini: {\n name: \"Google Gemini\",\n env_vars: %w[GEMINI_API_KEY GOOGLE_API_KEY],\n description: \"Google's multimodal AI with competitive pricing\",\n models: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n capabilities: %w[chat embedding image_generation tools vision],\n pricing: \"Low-cost\",\n notes: \"Excellent for cost-conscious applications\",\n },\n deepseek: {\n name: \"DeepSeek\",\n env_vars: %w[DEEPSEEK_API_KEY],\n description: \"High-performance reasoning models at low cost\",\n models: %w[deepseek-chat deepseek-coder],\n capabilities: %w[chat tools],\n pricing: \"Very low-cost\",\n notes: \"Excellent reasoning capabilities\",\n },\n }.freeze\n\n # Initialize the provider detector\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @detected_providers = {}\n end\n\n # Detect available providers and guide user through configuration\n #\n # @return [Boolean] True if at least one provider was configured\n def detect_and_configure\n @logger.info(\"Scanning for AI provider API keys...\")\n\n scan_environment_variables\n display_detected_providers\n\n if @detected_providers.any?\n configure_detected_providers\n else\n guide_manual_configuration\n end\n\n finalize_provider_configuration\n end\n\n private def scan_environment_variables\n PROVIDERS.each do |provider_key, provider_info|\n provider_info[:env_vars].each do |env_var|\n next unless ENV[env_var] && !ENV[env_var].empty?\n\n @detected_providers[provider_key] = {\n info: provider_info,\n api_key: ENV.fetch(env_var, nil),\n env_var:,\n }\n @logger.success(\"Found #{provider_info[:name]} API key (#{env_var})\")\n break # Use first found key for this provider\n end\n end\n end\n\n # Display information about detected providers\n private def display_detected_providers\n if @detected_providers.any?\n puts \"\\nšŸ” Detected AI Providers:\"\n @detected_providers.each_value do |config|\n info = config[:info]\n puts \" āœ“ #{info[:name]} - #{info[:description]}\"\n puts \" Models: #{info[:models].join(', ')}\"\n puts \" Pricing: #{info[:pricing]}\"\n puts \"\"\n end\n else\n puts \"\\nāŒ No AI provider API keys found in environment variables.\"\n puts \"Don't worry! We'll help you configure providers manually.\"\n end\n end\n\n # Configure detected providers\n #\n # @return [Boolean] True if configuration completed\n private def configure_detected_providers\n selected_providers = {}\n\n if @detected_providers.size == 1\n # Auto-select single provider\n provider_key = @detected_providers.keys.first\n selected_providers[provider_key] = @detected_providers[provider_key]\n @logger.info(\"Auto-selecting #{@detected_providers[provider_key][:info][:name]}\")\n else\n # Let user choose from detected providers\n selected_providers = prompt_provider_selection\n end\n\n configure_providers(selected_providers)\n end\n\n # Prompt user to select from detected providers\n #\n # @return [Hash] Selected providers configuration\n private def prompt_provider_selection\n selected = {}\n\n puts \"\\nšŸ¤– Multiple AI providers detected. Choose which ones to configure:\"\n\n @detected_providers.each do |provider_key, config|\n info = config[:info]\n use_provider = @prompt.yes?(\"Configure #{info[:name]}?\", default: true)\n\n next unless use_provider\n\n selected[provider_key] = config\n\n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n end\n end\n\n selected\n end\n\n # Guide user through manual provider configuration\n #\n # @return [Boolean] True if manual configuration completed\n private def guide_manual_configuration\n puts \"\\nšŸ”§ Manual Provider Configuration\"\n puts \"Please choose an AI provider to configure:\"\n\n # Display all available providers\n provider_choices = PROVIDERS.map do |key, info|\n { name: \"#{info[:name]} - #{info[:description]}\", value: key }\n end\n provider_choices << { name: \"Skip provider setup for now\", value: :skip }\n\n selected_provider = @prompt.select(\"Select a provider:\", provider_choices)\n return true if selected_provider == :skip\n\n configure_manual_provider(selected_provider)\n end\n\n # Configure a provider manually\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Boolean] True if configuration completed\n private def configure_manual_provider(provider_key)\n provider_info = PROVIDERS[provider_key]\n\n puts \"\\nšŸ“‹ Configuring #{provider_info[:name]}\"\n puts \"Description: #{provider_info[:description]}\"\n puts \"Required environment variable: #{provider_info[:env_vars].first}\"\n puts \"\"\n\n api_key = @prompt.mask(\"Enter your #{provider_info[:name]} API key:\")\n\n if api_key.empty?\n @logger.warn(\"No API key provided for #{provider_info[:name]}\")\n return false\n end\n\n # Test the connection\n if test_provider_connection(provider_key, api_key)\n @logger.success(\"#{provider_info[:name]} connection verified!\")\n store_manual_provider(provider_key, api_key)\n true\n else\n @logger.failure(\"Failed to connect to #{provider_info[:name]}\")\n retry_config = @prompt.yes?(\"Retry configuration?\", default: true)\n retry_config ? configure_manual_provider(provider_key) : false\n end\n end\n\n # Store manually configured provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n private def store_manual_provider(provider_key, api_key)\n @detected_providers[provider_key] = {\n info: PROVIDERS[provider_key],\n api_key:,\n env_var: PROVIDERS[provider_key][:env_vars].first,\n }\n end\n\n # Test provider connection\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key to test\n # @return [Boolean] True if connection successful\n private def test_provider_connection(provider_key, api_key)\n @logger.info(\"Testing #{PROVIDERS[provider_key][:name]} connection...\")\n\n # Configure RubyLLM for testing\n original_config = backup_rubyllm_config\n configure_rubyllm_for_test(provider_key, api_key)\n\n begin\n # Simple test request\n chat = RubyLLM.chat(\n model: get_test_model(provider_key),\n provider: map_provider_for_rubyllm(provider_key)\n )\n response = chat.ask(\"Hello! Please respond with just 'OK'\")\n\n success = response&.content&.include?(\"OK\")\n @logger.debug(\"Test response: #{response&.content}\") if ENV[\"DEBUG\"]\n success\n rescue => e\n @logger.debug(\"Connection test failed: #{e.message}\") if ENV[\"DEBUG\"]\n false\n ensure\n restore_rubyllm_config(original_config)\n end\n end\n\n # Get appropriate test model for provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [String] Model name for testing\n private def get_test_model(provider_key)\n case provider_key\n when :openai, :openrouter\n \"gpt-4o-mini\"\n when :anthropic\n \"claude-3-haiku-20240307\"\n when :gemini\n \"gemini-2.0-flash\"\n when :deepseek\n \"deepseek-chat\"\n else\n \"gpt-4o-mini\"\n end\n end\n\n # Map provider key to RubyLLM provider format\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Symbol] RubyLLM provider symbol\n private def map_provider_for_rubyllm(provider_key)\n case provider_key\n when :openrouter\n :openai # OpenRouter uses OpenAI API format\n else\n provider_key\n end\n end\n\n # Configure RubyLLM for testing\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n private def configure_rubyllm_for_test(provider_key, api_key)\n RubyLLM.configure do |config|\n case provider_key\n when :openai\n config.openai_api_key = api_key\n when :openrouter\n config.openai_api_key = api_key\n config.openai_api_base = \"https://openrouter.ai/api/v1\"\n when :anthropic\n config.anthropic_api_key = api_key\n when :gemini\n config.gemini_api_key = api_key\n when :deepseek\n config.deepseek_api_key = api_key\n end\n end\n end\n\n # Backup current RubyLLM configuration\n #\n # @return [Hash] Current configuration\n private def backup_rubyllm_config\n {\n openai_api_key: RubyLLM.config.openai_api_key,\n openai_api_base: RubyLLM.config.openai_api_base,\n anthropic_api_key: RubyLLM.config.anthropic_api_key,\n gemini_api_key: RubyLLM.config.gemini_api_key,\n deepseek_api_key: RubyLLM.config.deepseek_api_key,\n }\n end\n\n # Restore RubyLLM configuration\n #\n # @param config [Hash] Configuration to restore\n private def restore_rubyllm_config(config)\n RubyLLM.configure do |rubyllm_config|\n config.each do |key, value|\n rubyllm_config.public_send(\"#{key}=\", value) if value\n end\n end\n end\n\n # Configure selected providers\n #\n # @param providers [Hash] Selected providers configuration\n private def configure_providers(providers)\n @setup_data[:providers] = {}\n\n providers.each do |provider_key, config|\n @setup_data[:providers][provider_key] = {\n name: config[:info][:name],\n api_key: config[:api_key],\n env_var: config[:env_var],\n capabilities: config[:info][:capabilities],\n models: config[:info][:models],\n }\n end\n end\n\n # Finalize provider configuration\n #\n # @return [Boolean] True if at least one provider configured\n private def finalize_provider_configuration\n if @setup_data[:providers]&.any?\n # Select primary provider\n if @setup_data[:providers].size == 1\n primary_provider = @setup_data[:providers].keys.first\n else\n choices = @setup_data[:providers].map do |key, config|\n { name: config[:name], value: key }\n end\n primary_provider = @prompt.select(\"Select primary AI provider:\", choices)\n end\n\n @setup_data[:primary_provider] = primary_provider\n @logger.success(\"Provider configuration completed!\")\n @logger.info(\"Primary provider: #{@setup_data[:providers][primary_provider][:name]}\")\n\n true\n else\n @logger.warn(\"No AI providers configured. Some features may not work.\")\n false\n end\n end\n end\n end\nend\n"}, "48eef306e5e764c0ea0da7069ba82b107409bba0": {"t": 1785756133.9631445, "n": 14124, "h": "161af8dce307f0eb", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule BlueprintsCLI\n module Setup\n # ProviderDetector handles AI provider detection, configuration, and testing.\n # It scans environment variables for API keys, guides users through provider\n # selection, and tests connections to ensure providers are working correctly.\n class ProviderDetector\n # Provider information including environment variables and capabilities\n PROVIDERS = {\n openai: {\n name: \"OpenAI\",\n env_vars: %w[OPENAI_API_KEY],\n description: \"Industry-leading GPT models with excellent performance\",\n models: [\"gpt-4o\", \"gpt-4o-mini\", \"gpt-3.5-turbo\"],\n capabilities: %w[chat embedding image_generation tools],\n pricing: \"Mid-range\",\n notes: \"Direct OpenAI API access\",\n },\n openrouter: {\n name: \"OpenRouter\",\n env_vars: %w[OPENROUTER_API_KEY],\n description: \"Access to multiple providers through single API\",\n models: [\"gpt-4o\", \"claude-3-5-sonnet\", \"llama-3.1-70b\"],\n capabilities: %w[chat embedding],\n pricing: \"Variable by model\",\n notes: \"Unified access to multiple AI providers\",\n },\n anthropic: {\n name: \"Anthropic\",\n env_vars: %w[ANTHROPIC_API_KEY],\n description: \"Claude models known for safety and reasoning\",\n models: [\"claude-3-5-sonnet\", \"claude-3-haiku\", \"claude-3-opus\"],\n capabilities: %w[chat tools],\n pricing: \"Mid-range\",\n notes: \"Direct Anthropic API access\",\n },\n gemini: {\n name: \"Google Gemini\",\n env_vars: %w[GEMINI_API_KEY GOOGLE_API_KEY],\n description: \"Google's multimodal AI with competitive pricing\",\n models: [\"gemini-2.0-flash\", \"gemini-1.5-pro\", \"gemini-1.5-flash\"],\n capabilities: %w[chat embedding image_generation tools vision],\n pricing: \"Low-cost\",\n notes: \"Excellent for cost-conscious applications\",\n },\n deepseek: {\n name: \"DeepSeek\",\n env_vars: %w[DEEPSEEK_API_KEY],\n description: \"High-performance reasoning models at low cost\",\n models: %w[deepseek-chat deepseek-coder],\n capabilities: %w[chat tools],\n pricing: \"Very low-cost\",\n notes: \"Excellent reasoning capabilities\",\n },\n }.freeze\n\n # Initialize the provider detector\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @detected_providers = {}\n end\n\n # Detect available providers and guide user through configuration\n #\n # @return [Boolean] True if at least one provider was configured\n def detect_and_configure\n @logger.info(\"Scanning for AI provider API keys...\")\n\n scan_environment_variables\n display_detected_providers\n\n\n if @detected_providers.any?\n configure_detected_providers\n else\n guide_manual_configuration\n end\n\n finalize_provider_configuration\n end\n\n private def scan_environment_variables\n PROVIDERS.each do |provider_key, provider_info|\n provider_info[:env_vars].each do |env_var|\n next unless ENV[env_var] && !ENV[env_var].empty?\n\n @detected_providers[provider_key] = {\n info: provider_info,\n api_key: ENV.fetch(env_var, nil),\n env_var:,\n }\n @logger.success(\"Found #{provider_info[:name]} API key (#{env_var})\")\n break # Use first found key for this provider\n end\n end\n end\n\n # Display information about detected providers\n private def display_detected_providers\n if @detected_providers.any?\n puts \"\\nšŸ” Detected AI Providers:\"\n @detected_providers.each_value do |config|\n @detected_providers.each_value do |config|\n info = config[:info]\n puts \" āœ“ #{info[:name]} - #{info[:description]}\"\n puts \" Models: #{info[:models].join(', ')}\"\n puts \" Pricing: #{info[:pricing]}\"\n puts ''\n end\n else\n puts \"\\nāŒ No AI provider API keys found in environment variables.\"\n puts \"Don't worry! We'll help you configure providers manually.\"\n end\n end\n\n # Configure detected providers\n #\n # @return [Boolean] True if configuration completed\n private def configure_detected_providers\n selected_providers = {}\n\n if @detected_providers.size == 1\n # Auto-select single provider\n provider_key = @detected_providers.keys.first\n selected_providers[provider_key] = @detected_providers[provider_key]\n @logger.info(\"Auto-selecting #{@detected_providers[provider_key][:info][:name]}\")\n else\n # Let user choose from detected providers\n selected_providers = prompt_provider_selection\n end\n\n configure_providers(selected_providers)\n end\n\n # Prompt user to select from detected providers\n #\n # @return [Hash] Selected providers configuration\n private def prompt_provider_selection\n selected = {}\n\n\n puts \"\\nšŸ¤– Multiple AI providers detected. Choose which ones to configure:\"\n\n\n @detected_providers.each do |provider_key, config|\n info = config[:info]\n use_provider = @prompt.yes?(\"Configure #{info[:name]}?\", default: true)\n\n next unless use_provider\n\n selected[provider_key] = config\n\n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n\n next unless use_provider\n\n selected[provider_key] = config\n\n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n end\n end\n\n selected\n end\n\n # Guide user through manual provider configuration\n #\n # @return [Boolean] True if manual configuration completed\n private def guide_manual_configuration\n puts \"\\nšŸ”§ Manual Provider Configuration\"\n puts 'Please choose an AI provider to configure:'\n\n # Display all available providers\n provider_choices = PROVIDERS.map do |key, info|\n { name: \"#{info[:name]} - #{info[:description]}\", value: key }\n end\n provider_choices << { name: 'Skip provider setup for now', value: :skip }\n\n selected_provider = @prompt.select('Select a provider:', provider_choices)\n return true if selected_provider == :skip\n\n configure_manual_provider(selected_provider)\n end\n\n # Configure a provider manually\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Boolean] True if configuration completed\n private def configure_manual_provider(provider_key)\n provider_info = PROVIDERS[provider_key]\n\n\n puts \"\\nšŸ“‹ Configuring #{provider_info[:name]}\"\n puts \"Description: #{provider_info[:description]}\"\n puts \"Required environment variable: #{provider_info[:env_vars].first}\"\n puts ''\n\n api_key = @prompt.mask(\"Enter your #{provider_info[:name]} API key:\")\n\n\n if api_key.empty?\n @logger.warn(\"No API key provided for #{provider_info[:name]}\")\n return false\n end\n\n # Test the connection\n if test_provider_connection(provider_key, api_key)\n @logger.success(\"#{provider_info[:name]} connection verified!\")\n store_manual_provider(provider_key, api_key)\n true\n else\n @logger.failure(\"Failed to connect to #{provider_info[:name]}\")\n retry_config = @prompt.yes?('Retry configuration?', default: true)\n retry_config ? configure_manual_provider(provider_key) : false\n end\n end\n\n # Store manually configured provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n private def store_manual_provider(provider_key, api_key)\n @detected_providers[provider_key] = {\n info: PROVIDERS[provider_key],\n api_key:,\n env_var: PROVIDERS[provider_key][:env_vars].first,\n }\n end\n\n # Test provider connection\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key to test\n # @return [Boolean] True if connection successful\n private def test_provider_connection(provider_key, api_key)\n @logger.info(\"Testing #{PROVIDERS[provider_key][:name]} connection...\")\n\n\n # Configure RubyLLM for testing\n original_config = backup_rubyllm_config\n configure_rubyllm_for_test(provider_key, api_key)\n\n begin\n # Simple test request\n chat = RubyLLM.chat(\n model: get_test_model(provider_key),\n provider: map_provider_for_rubyllm(provider_key)\n )\n response = chat.ask(\"Hello! Please respond with just 'OK'\")\n\n success = response&.content&.include?(\"OK\")\n @logger.debug(\"Test response: #{response&.content}\") if ENV[\"DEBUG\"]\n success\n rescue => e\n @logger.debug(\"Connection test failed: #{e.message}\") if ENV[\"DEBUG\"]\n false\n ensure\n restore_rubyllm_config(original_config)\n end\n end\n\n # Get appropriate test model for provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [String] Model name for testing\n private def get_test_model(provider_key)\n case provider_key\n when :openai, :openrouter\n \"gpt-4o-mini\"\n when :anthropic\n \"claude-3-haiku-20240307\"\n when :gemini\n \"gemini-2.0-flash\"\n when :deepseek\n \"deepseek-chat\"\n else\n \"gpt-4o-mini\"\n end\n end\n\n # Map provider key to RubyLLM provider format\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Symbol] RubyLLM provider symbol\n private def map_provider_for_rubyllm(provider_key)\n case provider_key\n when :openrouter\n :openai # OpenRouter uses OpenAI API format\n else\n provider_key\n end\n end\n\n # Configure RubyLLM for testing\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n private def configure_rubyllm_for_test(provider_key, api_key)\n RubyLLM.configure do |config|\n case provider_key\n when :openai\n config.openai_api_key = api_key\n when :openrouter\n config.openai_api_key = api_key\n config.openai_api_base = \"https://openrouter.ai/api/v1\"\n when :anthropic\n config.anthropic_api_key = api_key\n when :gemini\n config.gemini_api_key = api_key\n when :deepseek\n config.deepseek_api_key = api_key\n end\n end\n end\n\n # Backup current RubyLLM configuration\n #\n # @return [Hash] Current configuration\n private def backup_rubyllm_config\n {\n openai_api_key: RubyLLM.config.openai_api_key,\n openai_api_base: RubyLLM.config.openai_api_base,\n anthropic_api_key: RubyLLM.config.anthropic_api_key,\n gemini_api_key: RubyLLM.config.gemini_api_key,\n deepseek_api_key: RubyLLM.config.deepseek_api_key,\n }\n end\n\n # Restore RubyLLM configuration\n #\n # @param config [Hash] Configuration to restore\n private def restore_rubyllm_config(config)\n RubyLLM.configure do |rubyllm_config|\n config.each do |key, value|\n rubyllm_config.public_send(\"#{key}=\", value) if value\n end\n end\n end\n\n # Configure selected providers\n #\n # @param providers [Hash] Selected providers configuration\n private def configure_providers(providers)\n @setup_data[:providers] = {}\n\n providers.each do |provider_key, config|\n @setup_data[:providers][provider_key] = {\n name: config[:info][:name],\n api_key: config[:api_key],\n env_var: config[:env_var],\n capabilities: config[:info][:capabilities],\n models: config[:info][:models],\n }\n end\n end\n\n # Finalize provider configuration\n #\n # @return [Boolean] True if at least one provider configured\n private def finalize_provider_configuration\n if @setup_data[:providers]&.any?\n # Select primary provider\n if @setup_data[:providers].size == 1\n primary_provider = @setup_data[:providers].keys.first\n else\n choices = @setup_data[:providers].map do |key, config|\n { name: config[:name], value: key }\n end\n primary_provider = @prompt.select('Select primary AI provider:', choices)\n end\n\n @setup_data[:primary_provider] = primary_provider\n @logger.success('Provider configuration completed!')\n @logger.info(\"Primary provider: #{@setup_data[:providers][primary_provider][:name]}\")\n\n\n true\n else\n @logger.warn('No AI providers configured. Some features may not work.')\n false\n end\n end\n end\n end\nend\n\n"}, "7006eb6a43f96981217237f06f61de5bf3583969": {"t": 1785756135.3601928, "n": 13545, "h": "1215fd8a1923f02e", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule BlueprintsCLI\n module Setup\n # ProviderDetector handles AI provider detection, configuration, and testing.\n # It scans environment variables for API keys, guides users through provider\n # selection, and tests connections to ensure providers are working correctly.\n class ProviderDetector\n # Provider information including environment variables and capabilities\n PROVIDERS = {\n openai: {\n name: 'OpenAI',\n env_vars: %w[OPENAI_API_KEY],\n description: 'Industry-leading GPT models with excellent performance',\n models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],\n capabilities: %w[chat embedding image_generation tools],\n pricing: 'Mid-range',\n notes: 'Direct OpenAI API access'\n },\n openrouter: {\n name: 'OpenRouter',\n env_vars: %w[OPENROUTER_API_KEY],\n description: 'Access to multiple providers through single API',\n models: ['gpt-4o', 'claude-3-5-sonnet', 'llama-3.1-70b'],\n capabilities: %w[chat embedding],\n pricing: 'Variable by model',\n notes: 'Unified access to multiple AI providers'\n },\n anthropic: {\n name: 'Anthropic',\n env_vars: %w[ANTHROPIC_API_KEY],\n description: 'Claude models known for safety and reasoning',\n models: ['claude-3-5-sonnet', 'claude-3-haiku', 'claude-3-opus'],\n capabilities: %w[chat tools],\n pricing: 'Mid-range',\n notes: 'Direct Anthropic API access'\n },\n gemini: {\n name: 'Google Gemini',\n env_vars: %w[GEMINI_API_KEY GOOGLE_API_KEY],\n description: 'Google\\'s multimodal AI with competitive pricing',\n models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],\n capabilities: %w[chat embedding image_generation tools vision],\n pricing: 'Low-cost',\n notes: 'Excellent for cost-conscious applications'\n },\n deepseek: {\n name: 'DeepSeek',\n env_vars: %w[DEEPSEEK_API_KEY],\n description: 'High-performance reasoning models at low cost',\n models: %w[deepseek-chat deepseek-coder],\n capabilities: %w[chat tools],\n pricing: 'Very low-cost',\n notes: 'Excellent reasoning capabilities'\n }\n }.freeze\n\n # Initialize the provider detector\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @detected_providers = {}\n end\n\n # Detect available providers and guide user through configuration\n #\n # @return [Boolean] True if at least one provider was configured\n def detect_and_configure\n @logger.info('Scanning for AI provider API keys...')\n\n scan_environment_variables\n display_detected_providers\n\n if @detected_providers.any?\n configure_detected_providers\n else\n guide_manual_configuration\n end\n\n finalize_provider_configuration\n end\n\n private\n\n # Scan environment variables for provider API keys\n def scan_environment_variables\n PROVIDERS.each do |provider_key, provider_info|\n provider_info[:env_vars].each do |env_var|\n next unless ENV[env_var] && !ENV[env_var].empty?\n\n @detected_providers[provider_key] = {\n info: provider_info,\n api_key: ENV.fetch(env_var, nil),\n env_var: env_var\n }\n @logger.success(\"Found #{provider_info[:name]} API key (#{env_var})\")\n break # Use first found key for this provider\n end\n end\n end\n\n # Display information about detected providers\n def display_detected_providers\n if @detected_providers.any?\n puts \"\\nšŸ” Detected AI Providers:\"\n @detected_providers.each_value do |config|\n info = config[:info]\n puts \" āœ“ #{info[:name]} - #{info[:description]}\"\n puts \" Models: #{info[:models].join(', ')}\"\n puts \" Pricing: #{info[:pricing]}\"\n puts ''\n end\n else\n puts \"\\nāŒ No AI provider API keys found in environment variables.\"\n puts \"Don't worry! We'll help you configure providers manually.\"\n end\n end\n\n # Configure detected providers\n #\n # @return [Boolean] True if configuration completed\n def configure_detected_providers\n selected_providers = {}\n\n if @detected_providers.size == 1\n # Auto-select single provider\n provider_key = @detected_providers.keys.first\n selected_providers[provider_key] = @detected_providers[provider_key]\n @logger.info(\"Auto-selecting #{@detected_providers[provider_key][:info][:name]}\")\n else\n # Let user choose from detected providers\n selected_providers = prompt_provider_selection\n end\n\n configure_providers(selected_providers)\n end\n\n # Prompt user to select from detected providers\n #\n # @return [Hash] Selected providers configuration\n def prompt_provider_selection\n selected = {}\n\n puts \"\\nšŸ¤– Multiple AI providers detected. Choose which ones to configure:\"\n\n @detected_providers.each do |provider_key, config|\n info = config[:info]\n use_provider = @prompt.yes?(\"Configure #{info[:name]}?\", default: true)\n\n next unless use_provider\n\n selected[provider_key] = config\n\n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n end\n end\n\n selected\n end\n\n # Guide user through manual provider configuration\n #\n # @return [Boolean] True if manual configuration completed\n def guide_manual_configuration\n puts \"\\nšŸ”§ Manual Provider Configuration\"\n puts 'Please choose an AI provider to configure:'\n\n # Display all available providers\n provider_choices = PROVIDERS.map do |key, info|\n { name: \"#{info[:name]} - #{info[:description]}\", value: key }\n end\n provider_choices << { name: 'Skip provider setup for now', value: :skip }\n\n selected_provider = @prompt.select('Select a provider:', provider_choices)\n return true if selected_provider == :skip\n\n configure_manual_provider(selected_provider)\n end\n\n # Configure a provider manually\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Boolean] True if configuration completed\n def configure_manual_provider(provider_key)\n provider_info = PROVIDERS[provider_key]\n\n puts \"\\nšŸ“‹ Configuring #{provider_info[:name]}\"\n puts \"Description: #{provider_info[:description]}\"\n puts \"Required environment variable: #{provider_info[:env_vars].first}\"\n puts ''\n\n api_key = @prompt.mask(\"Enter your #{provider_info[:name]} API key:\")\n\n if api_key.empty?\n @logger.warn(\"No API key provided for #{provider_info[:name]}\")\n return false\n end\n\n # Test the connection\n if test_provider_connection(provider_key, api_key)\n @logger.success(\"#{provider_info[:name]} connection verified!\")\n store_manual_provider(provider_key, api_key)\n true\n else\n @logger.failure(\"Failed to connect to #{provider_info[:name]}\")\n retry_config = @prompt.yes?('Retry configuration?', default: true)\n retry_config ? configure_manual_provider(provider_key) : false\n end\n end\n\n # Store manually configured provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n def store_manual_provider(provider_key, api_key)\n @detected_providers[provider_key] = {\n info: PROVIDERS[provider_key],\n api_key: api_key,\n env_var: PROVIDERS[provider_key][:env_vars].first\n }\n end\n\n # Test provider connection\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key to test\n # @return [Boolean] True if connection successful\n def test_provider_connection(provider_key, api_key)\n @logger.info(\"Testing #{PROVIDERS[provider_key][:name]} connection...\")\n\n # Configure RubyLLM for testing\n original_config = backup_rubyllm_config\n configure_rubyllm_for_test(provider_key, api_key)\n\n begin\n # Simple test request\n chat = RubyLLM.chat(\n model: get_test_model(provider_key),\n provider: map_provider_for_rubyllm(provider_key)\n )\n response = chat.ask(\"Hello! Please respond with just 'OK'\")\n\n success = response&.content&.include?('OK')\n @logger.debug(\"Test response: #{response&.content}\") if ENV['DEBUG']\n success\n rescue StandardError => e\n @logger.debug(\"Connection test failed: #{e.message}\") if ENV['DEBUG']\n false\n ensure\n restore_rubyllm_config(original_config)\n end\n end\n\n # Get appropriate test model for provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [String] Model name for testing\n def get_test_model(provider_key)\n case provider_key\n when :openai, :openrouter\n 'gpt-4o-mini'\n when :anthropic\n 'claude-3-haiku-20240307'\n when :gemini\n 'gemini-2.0-flash'\n when :deepseek\n 'deepseek-chat'\n else\n 'gpt-4o-mini'\n end\n end\n\n # Map provider key to RubyLLM provider format\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Symbol] RubyLLM provider symbol\n def map_provider_for_rubyllm(provider_key)\n case provider_key\n when :openrouter\n :openai # OpenRouter uses OpenAI API format\n else\n provider_key\n end\n end\n\n # Configure RubyLLM for testing\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n def configure_rubyllm_for_test(provider_key, api_key)\n RubyLLM.configure do |config|\n case provider_key\n when :openai\n config.openai_api_key = api_key\n when :openrouter\n config.openai_api_key = api_key\n config.openai_api_base = 'https://openrouter.ai/api/v1'\n when :anthropic\n config.anthropic_api_key = api_key\n when :gemini\n config.gemini_api_key = api_key\n when :deepseek\n config.deepseek_api_key = api_key\n end\n end\n end\n\n # Backup current RubyLLM configuration\n #\n # @return [Hash] Current configuration\n def backup_rubyllm_config\n {\n openai_api_key: RubyLLM.config.openai_api_key,\n openai_api_base: RubyLLM.config.openai_api_base,\n anthropic_api_key: RubyLLM.config.anthropic_api_key,\n gemini_api_key: RubyLLM.config.gemini_api_key,\n deepseek_api_key: RubyLLM.config.deepseek_api_key\n }\n end\n\n # Restore RubyLLM configuration\n #\n # @param config [Hash] Configuration to restore\n def restore_rubyllm_config(config)\n RubyLLM.configure do |rubyllm_config|\n config.each do |key, value|\n rubyllm_config.public_send(\"#{key}=\", value) if value\n end\n end\n end\n\n # Configure selected providers\n #\n # @param providers [Hash] Selected providers configuration\n def configure_providers(providers)\n @setup_data[:providers] = {}\n\n providers.each do |provider_key, config|\n @setup_data[:providers][provider_key] = {\n name: config[:info][:name],\n api_key: config[:api_key],\n env_var: config[:env_var],\n capabilities: config[:info][:capabilities],\n models: config[:info][:models]\n }\n end\n end\n\n # Finalize provider configuration\n #\n # @return [Boolean] True if at least one provider configured\n def finalize_provider_configuration\n if @setup_data[:providers]&.any?\n # Select primary provider\n if @setup_data[:providers].size == 1\n primary_provider = @setup_data[:providers].keys.first\n else\n choices = @setup_data[:providers].map do |key, config|\n { name: config[:name], value: key }\n end\n primary_provider = @prompt.select('Select primary AI provider:', choices)\n end\n\n @setup_data[:primary_provider] = primary_provider\n @logger.success('Provider configuration completed!')\n @logger.info(\"Primary provider: #{@setup_data[:providers][primary_provider][:name]}\")\n\n true\n else\n @logger.warn('No AI providers configured. Some features may not work.')\n false\n end\n end\n end\n end\nend\n"}, "d3c27d43989e4525f6c128e73a7e6ebcf57e8781": {"t": 1785756136.7127755, "n": 13554, "h": "631a8793ac0d3df6", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule BlueprintsCLI\n module Setup\n # ProviderDetector handles AI provider detection, configuration, and testing.\n # It scans environment variables for API keys, guides users through provider\n # selection, and tests connections to ensure providers are working correctly.\n class ProviderDetector\n # Provider information including environment variables and capabilities\n PROVIDERS = {\n openai: {\n name: 'OpenAI',\n env_vars: %w[OPENAI_API_KEY],\n description: 'Industry-leading GPT models with excellent performance',\n models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],\n capabilities: %w[chat embedding image_generation tools],\n pricing: 'Mid-range',\n notes: 'Direct OpenAI API access'\n },\n openrouter: {\n name: 'OpenRouter',\n env_vars: %w[OPENROUTER_API_KEY],\n description: 'Access to multiple providers through single API',\n models: ['gpt-4o', 'claude-3-5-sonnet', 'llama-3.1-70b'],\n capabilities: %w[chat embedding],\n pricing: 'Variable by model',\n notes: 'Unified access to multiple AI providers'\n },\n anthropic: {\n name: 'Anthropic',\n env_vars: %w[ANTHROPIC_API_KEY],\n description: 'Claude models known for safety and reasoning',\n models: ['claude-3-5-sonnet', 'claude-3-haiku', 'claude-3-opus'],\n capabilities: %w[chat tools],\n pricing: 'Mid-range',\n notes: 'Direct Anthropic API access'\n },\n gemini: {\n name: 'Google Gemini',\n env_vars: %w[GEMINI_API_KEY GOOGLE_API_KEY],\n description: 'Google\\'s multimodal AI with competitive pricing',\n models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],\n capabilities: %w[chat embedding image_generation tools vision],\n pricing: 'Low-cost',\n notes: 'Excellent for cost-conscious applications'\n },\n deepseek: {\n name: 'DeepSeek',\n env_vars: %w[DEEPSEEK_API_KEY],\n description: 'High-performance reasoning models at low cost',\n models: %w[deepseek-chat deepseek-coder],\n capabilities: %w[chat tools],\n pricing: 'Very low-cost',\n notes: 'Excellent reasoning capabilities'\n }\n }.freeze\n\n # Initialize the provider detector\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @detected_providers = {}\n end\n\n # Detect available providers and guide user through configuration\n #\n # @return [Boolean] True if at least one provider was configured\n def detect_and_configure\n @logger.info('Scanning for AI provider API keys...')\n\n scan_environment_variables\n display_detected_providers\n\n if @detected_providers.any?\n configure_detected_providers\n else\n guide_manual_configuration\n end\n\n finalize_provider_configuration\n end\n\n private\n\n # Scan environment variables for provider API keys\n def scan_environment_variables\n PROVIDERS.each do |provider_key, provider_info|\n provider_info[:env_vars].each do |env_var|\n next unless ENV[env_var] && !ENV[env_var].empty?\n\n @detected_providers[provider_key] = {\n info: provider_info,\n api_key: ENV.fetch(env_var, nil),\n env_var: env_var\n }\n @logger.success(\"Found #{provider_info[:name]} API key (#{env_var})\")\n break # Use first found key for this provider\n end\n end\n end\n\n # Display information about detected providers\n def display_detected_providers\n if @detected_providers.any?\n puts \"\\nšŸ” Detected AI Providers:\"\n @detected_providers.each do |_provider_key, config|\n info = config[:info]\n puts \" āœ“ #{info[:name]} - #{info[:description]}\"\n puts \" Models: #{info[:models].join(', ')}\"\n puts \" Pricing: #{info[:pricing]}\"\n puts ''\n end\n else\n puts \"\\nāŒ No AI provider API keys found in environment variables.\"\n puts \"Don't worry! We'll help you configure providers manually.\"\n end\n end\n\n # Configure detected providers\n #\n # @return [Boolean] True if configuration completed\n def configure_detected_providers\n selected_providers = {}\n\n if @detected_providers.size == 1\n # Auto-select single provider\n provider_key = @detected_providers.keys.first\n selected_providers[provider_key] = @detected_providers[provider_key]\n @logger.info(\"Auto-selecting #{@detected_providers[provider_key][:info][:name]}\")\n else\n # Let user choose from detected providers\n selected_providers = prompt_provider_selection\n end\n\n configure_providers(selected_providers)\n end\n\n # Prompt user to select from detected providers\n #\n # @return [Hash] Selected providers configuration\n def prompt_provider_selection\n selected = {}\n\n puts \"\\nšŸ¤– Multiple AI providers detected. Choose which ones to configure:\"\n\n @detected_providers.each do |provider_key, config|\n info = config[:info]\n use_provider = @prompt.yes?(\"Configure #{info[:name]}?\", default: true)\n\n next unless use_provider\n\n selected[provider_key] = config\n\n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n end\n end\n\n selected\n end\n\n # Guide user through manual provider configuration\n #\n # @return [Boolean] True if manual configuration completed\n def guide_manual_configuration\n puts \"\\nšŸ”§ Manual Provider Configuration\"\n puts 'Please choose an AI provider to configure:'\n\n # Display all available providers\n provider_choices = PROVIDERS.map do |key, info|\n { name: \"#{info[:name]} - #{info[:description]}\", value: key }\n end\n provider_choices << { name: 'Skip provider setup for now', value: :skip }\n\n selected_provider = @prompt.select('Select a provider:', provider_choices)\n return true if selected_provider == :skip\n\n configure_manual_provider(selected_provider)\n end\n\n # Configure a provider manually\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Boolean] True if configuration completed\n def configure_manual_provider(provider_key)\n provider_info = PROVIDERS[provider_key]\n\n puts \"\\nšŸ“‹ Configuring #{provider_info[:name]}\"\n puts \"Description: #{provider_info[:description]}\"\n puts \"Required environment variable: #{provider_info[:env_vars].first}\"\n puts ''\n\n api_key = @prompt.mask(\"Enter your #{provider_info[:name]} API key:\")\n\n if api_key.empty?\n @logger.warn(\"No API key provided for #{provider_info[:name]}\")\n return false\n end\n\n # Test the connection\n if test_provider_connection(provider_key, api_key)\n @logger.success(\"#{provider_info[:name]} connection verified!\")\n store_manual_provider(provider_key, api_key)\n true\n else\n @logger.failure(\"Failed to connect to #{provider_info[:name]}\")\n retry_config = @prompt.yes?('Retry configuration?', default: true)\n retry_config ? configure_manual_provider(provider_key) : false\n end\n end\n\n # Store manually configured provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n def store_manual_provider(provider_key, api_key)\n @detected_providers[provider_key] = {\n info: PROVIDERS[provider_key],\n api_key: api_key,\n env_var: PROVIDERS[provider_key][:env_vars].first\n }\n end\n\n # Test provider connection\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key to test\n # @return [Boolean] True if connection successful\n def test_provider_connection(provider_key, api_key)\n @logger.info(\"Testing #{PROVIDERS[provider_key][:name]} connection...\")\n\n # Configure RubyLLM for testing\n original_config = backup_rubyllm_config\n configure_rubyllm_for_test(provider_key, api_key)\n\n begin\n # Simple test request\n chat = RubyLLM.chat(\n model: get_test_model(provider_key),\n provider: map_provider_for_rubyllm(provider_key)\n )\n response = chat.ask(\"Hello! Please respond with just 'OK'\")\n\n success = response&.content&.include?('OK')\n @logger.debug(\"Test response: #{response&.content}\") if ENV['DEBUG']\n success\n rescue StandardError => e\n @logger.debug(\"Connection test failed: #{e.message}\") if ENV['DEBUG']\n false\n ensure\n restore_rubyllm_config(original_config)\n end\n end\n\n # Get appropriate test model for provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [String] Model name for testing\n def get_test_model(provider_key)\n case provider_key\n when :openai, :openrouter\n 'gpt-4o-mini'\n when :anthropic\n 'claude-3-haiku-20240307'\n when :gemini\n 'gemini-2.0-flash'\n when :deepseek\n 'deepseek-chat'\n else\n 'gpt-4o-mini'\n end\n end\n\n # Map provider key to RubyLLM provider format\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Symbol] RubyLLM provider symbol\n def map_provider_for_rubyllm(provider_key)\n case provider_key\n when :openrouter\n :openai # OpenRouter uses OpenAI API format\n else\n provider_key\n end\n end\n\n # Configure RubyLLM for testing\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n def configure_rubyllm_for_test(provider_key, api_key)\n RubyLLM.configure do |config|\n case provider_key\n when :openai\n config.openai_api_key = api_key\n when :openrouter\n config.openai_api_key = api_key\n config.openai_api_base = 'https://openrouter.ai/api/v1'\n when :anthropic\n config.anthropic_api_key = api_key\n when :gemini\n config.gemini_api_key = api_key\n when :deepseek\n config.deepseek_api_key = api_key\n end\n end\n end\n\n # Backup current RubyLLM configuration\n #\n # @return [Hash] Current configuration\n def backup_rubyllm_config\n {\n openai_api_key: RubyLLM.config.openai_api_key,\n openai_api_base: RubyLLM.config.openai_api_base,\n anthropic_api_key: RubyLLM.config.anthropic_api_key,\n gemini_api_key: RubyLLM.config.gemini_api_key,\n deepseek_api_key: RubyLLM.config.deepseek_api_key\n }\n end\n\n # Restore RubyLLM configuration\n #\n # @param config [Hash] Configuration to restore\n def restore_rubyllm_config(config)\n RubyLLM.configure do |rubyllm_config|\n config.each do |key, value|\n rubyllm_config.public_send(\"#{key}=\", value) if value\n end\n end\n end\n\n # Configure selected providers\n #\n # @param providers [Hash] Selected providers configuration\n def configure_providers(providers)\n @setup_data[:providers] = {}\n\n providers.each do |provider_key, config|\n @setup_data[:providers][provider_key] = {\n name: config[:info][:name],\n api_key: config[:api_key],\n env_var: config[:env_var],\n capabilities: config[:info][:capabilities],\n models: config[:info][:models]\n }\n end\n end\n\n # Finalize provider configuration\n #\n # @return [Boolean] True if at least one provider configured\n def finalize_provider_configuration\n if @setup_data[:providers]&.any?\n # Select primary provider\n if @setup_data[:providers].size == 1\n primary_provider = @setup_data[:providers].keys.first\n else\n choices = @setup_data[:providers].map do |key, config|\n { name: config[:name], value: key }\n end\n primary_provider = @prompt.select('Select primary AI provider:', choices)\n end\n\n @setup_data[:primary_provider] = primary_provider\n @logger.success('Provider configuration completed!')\n @logger.info(\"Primary provider: #{@setup_data[:providers][primary_provider][:name]}\")\n\n true\n else\n @logger.warn('No AI providers configured. Some features may not work.')\n false\n end\n end\n end\n end\nend\n"}, "8522ea5dda5577bfc73651772d4845803d49c31c": {"t": 1785756137.9573908, "n": 13684, "h": "93442b60bc25d561", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule BlueprintsCLI\n module Setup\n # ProviderDetector handles AI provider detection, configuration, and testing.\n # It scans environment variables for API keys, guides users through provider\n # selection, and tests connections to ensure providers are working correctly.\n class ProviderDetector\n # Provider information including environment variables and capabilities\n PROVIDERS = {\n openai: {\n name: 'OpenAI',\n env_vars: %w[OPENAI_API_KEY],\n description: 'Industry-leading GPT models with excellent performance',\n models: ['gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo'],\n capabilities: %w[chat embedding image_generation tools],\n pricing: 'Mid-range',\n notes: 'Direct OpenAI API access'\n },\n openrouter: {\n name: 'OpenRouter',\n env_vars: %w[OPENROUTER_API_KEY],\n description: 'Access to multiple providers through single API',\n models: ['gpt-4o', 'claude-3-5-sonnet', 'llama-3.1-70b'],\n capabilities: %w[chat embedding],\n pricing: 'Variable by model',\n notes: 'Unified access to multiple AI providers'\n },\n anthropic: {\n name: 'Anthropic',\n env_vars: %w[ANTHROPIC_API_KEY],\n description: 'Claude models known for safety and reasoning',\n models: ['claude-3-5-sonnet', 'claude-3-haiku', 'claude-3-opus'],\n capabilities: %w[chat tools],\n pricing: 'Mid-range',\n notes: 'Direct Anthropic API access'\n },\n gemini: {\n name: 'Google Gemini',\n env_vars: %w[GEMINI_API_KEY GOOGLE_API_KEY],\n description: 'Google\\'s multimodal AI with competitive pricing',\n models: ['gemini-2.0-flash', 'gemini-1.5-pro', 'gemini-1.5-flash'],\n capabilities: %w[chat embedding image_generation tools vision],\n pricing: 'Low-cost',\n notes: 'Excellent for cost-conscious applications'\n },\n deepseek: {\n name: 'DeepSeek',\n env_vars: %w[DEEPSEEK_API_KEY],\n description: 'High-performance reasoning models at low cost',\n models: ['deepseek-chat', 'deepseek-coder'],\n capabilities: %w[chat tools],\n pricing: 'Very low-cost',\n notes: 'Excellent reasoning capabilities'\n }\n }.freeze\n\n # Initialize the provider detector\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @detected_providers = {}\n end\n\n # Detect available providers and guide user through configuration\n #\n # @return [Boolean] True if at least one provider was configured\n def detect_and_configure\n @logger.info(\"Scanning for AI provider API keys...\")\n \n scan_environment_variables\n display_detected_providers\n \n if @detected_providers.any?\n configure_detected_providers\n else\n guide_manual_configuration\n end\n\n finalize_provider_configuration\n end\n\n private\n\n # Scan environment variables for provider API keys\n def scan_environment_variables\n PROVIDERS.each do |provider_key, provider_info|\n provider_info[:env_vars].each do |env_var|\n if ENV[env_var] && !ENV[env_var].empty?\n @detected_providers[provider_key] = {\n info: provider_info,\n api_key: ENV[env_var],\n env_var: env_var\n }\n @logger.success(\"Found #{provider_info[:name]} API key (#{env_var})\")\n break # Use first found key for this provider\n end\n end\n end\n end\n\n # Display information about detected providers\n def display_detected_providers\n if @detected_providers.any?\n puts \"\\nšŸ” Detected AI Providers:\"\n @detected_providers.each do |provider_key, config|\n info = config[:info]\n puts \" āœ“ #{info[:name]} - #{info[:description]}\"\n puts \" Models: #{info[:models].join(', ')}\"\n puts \" Pricing: #{info[:pricing]}\"\n puts \"\"\n end\n else\n puts \"\\nāŒ No AI provider API keys found in environment variables.\"\n puts \"Don't worry! We'll help you configure providers manually.\"\n end\n end\n\n # Configure detected providers\n #\n # @return [Boolean] True if configuration completed\n def configure_detected_providers\n selected_providers = {}\n\n if @detected_providers.size == 1\n # Auto-select single provider\n provider_key = @detected_providers.keys.first\n selected_providers[provider_key] = @detected_providers[provider_key]\n @logger.info(\"Auto-selecting #{@detected_providers[provider_key][:info][:name]}\")\n else\n # Let user choose from detected providers\n selected_providers = prompt_provider_selection\n end\n\n configure_providers(selected_providers)\n end\n\n # Prompt user to select from detected providers\n #\n # @return [Hash] Selected providers configuration\n def prompt_provider_selection\n selected = {}\n \n puts \"\\nšŸ¤– Multiple AI providers detected. Choose which ones to configure:\"\n \n @detected_providers.each do |provider_key, config|\n info = config[:info]\n use_provider = @prompt.yes?(\"Configure #{info[:name]}?\", default: true)\n \n if use_provider\n selected[provider_key] = config\n \n # Test the provider connection\n if test_provider_connection(provider_key, config[:api_key])\n @logger.success(\"#{info[:name]} connection verified!\")\n else\n @logger.failure(\"#{info[:name]} connection failed\")\n use_anyway = @prompt.yes?(\"Continue with #{info[:name]} anyway?\", default: false)\n selected.delete(provider_key) unless use_anyway\n end\n end\n end\n\n selected\n end\n\n # Guide user through manual provider configuration\n #\n # @return [Boolean] True if manual configuration completed\n def guide_manual_configuration\n puts \"\\nšŸ”§ Manual Provider Configuration\"\n puts \"Please choose an AI provider to configure:\"\n\n # Display all available providers\n provider_choices = PROVIDERS.map do |key, info|\n { name: \"#{info[:name]} - #{info[:description]}\", value: key }\n end\n provider_choices << { name: \"Skip provider setup for now\", value: :skip }\n\n selected_provider = @prompt.select(\"Select a provider:\", provider_choices)\n return true if selected_provider == :skip\n\n configure_manual_provider(selected_provider)\n end\n\n # Configure a provider manually\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Boolean] True if configuration completed\n def configure_manual_provider(provider_key)\n provider_info = PROVIDERS[provider_key]\n \n puts \"\\nšŸ“‹ Configuring #{provider_info[:name]}\"\n puts \"Description: #{provider_info[:description]}\"\n puts \"Required environment variable: #{provider_info[:env_vars].first}\"\n puts \"\"\n\n api_key = @prompt.mask(\"Enter your #{provider_info[:name]} API key:\")\n \n if api_key.empty?\n @logger.warn(\"No API key provided for #{provider_info[:name]}\")\n return false\n end\n\n # Test the connection\n if test_provider_connection(provider_key, api_key)\n @logger.success(\"#{provider_info[:name]} connection verified!\")\n store_manual_provider(provider_key, api_key)\n true\n else\n @logger.failure(\"Failed to connect to #{provider_info[:name]}\")\n retry_config = @prompt.yes?(\"Retry configuration?\", default: true)\n retry_config ? configure_manual_provider(provider_key) : false\n end\n end\n\n # Store manually configured provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n def store_manual_provider(provider_key, api_key)\n @detected_providers[provider_key] = {\n info: PROVIDERS[provider_key],\n api_key: api_key,\n env_var: PROVIDERS[provider_key][:env_vars].first\n }\n end\n\n # Test provider connection\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key to test\n # @return [Boolean] True if connection successful\n def test_provider_connection(provider_key, api_key)\n @logger.info(\"Testing #{PROVIDERS[provider_key][:name]} connection...\")\n \n # Configure RubyLLM for testing\n original_config = backup_rubyllm_config\n configure_rubyllm_for_test(provider_key, api_key)\n\n begin\n # Simple test request\n chat = RubyLLM.chat(\n model: get_test_model(provider_key),\n provider: map_provider_for_rubyllm(provider_key)\n )\n response = chat.ask(\"Hello! Please respond with just 'OK'\")\n \n success = response&.content&.include?('OK')\n @logger.debug(\"Test response: #{response&.content}\") if ENV['DEBUG']\n success\n rescue StandardError => e\n @logger.debug(\"Connection test failed: #{e.message}\") if ENV['DEBUG']\n false\n ensure\n restore_rubyllm_config(original_config)\n end\n end\n\n # Get appropriate test model for provider\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [String] Model name for testing\n def get_test_model(provider_key)\n case provider_key\n when :openai, :openrouter\n 'gpt-4o-mini'\n when :anthropic\n 'claude-3-haiku-20240307'\n when :gemini\n 'gemini-2.0-flash'\n when :deepseek\n 'deepseek-chat'\n else\n 'gpt-4o-mini'\n end\n end\n\n # Map provider key to RubyLLM provider format\n #\n # @param provider_key [Symbol] Provider identifier\n # @return [Symbol] RubyLLM provider symbol\n def map_provider_for_rubyllm(provider_key)\n case provider_key\n when :openrouter\n :openai # OpenRouter uses OpenAI API format\n else\n provider_key\n end\n end\n\n # Configure RubyLLM for testing\n #\n # @param provider_key [Symbol] Provider identifier\n # @param api_key [String] API key\n def configure_rubyllm_for_test(provider_key, api_key)\n RubyLLM.configure do |config|\n case provider_key\n when :openai\n config.openai_api_key = api_key\n when :openrouter\n config.openai_api_key = api_key\n config.openai_api_base = 'https://openrouter.ai/api/v1'\n when :anthropic\n config.anthropic_api_key = api_key\n when :gemini\n config.gemini_api_key = api_key\n when :deepseek\n config.deepseek_api_key = api_key\n end\n end\n end\n\n # Backup current RubyLLM configuration\n #\n # @return [Hash] Current configuration\n def backup_rubyllm_config\n {\n openai_api_key: RubyLLM.config.openai_api_key,\n openai_api_base: RubyLLM.config.openai_api_base,\n anthropic_api_key: RubyLLM.config.anthropic_api_key,\n gemini_api_key: RubyLLM.config.gemini_api_key,\n deepseek_api_key: RubyLLM.config.deepseek_api_key\n }\n end\n\n # Restore RubyLLM configuration\n #\n # @param config [Hash] Configuration to restore\n def restore_rubyllm_config(config)\n RubyLLM.configure do |rubyllm_config|\n config.each do |key, value|\n rubyllm_config.public_send(\"#{key}=\", value) if value\n end\n end\n end\n\n # Configure selected providers\n #\n # @param providers [Hash] Selected providers configuration\n def configure_providers(providers)\n @setup_data[:providers] = {}\n\n providers.each do |provider_key, config|\n @setup_data[:providers][provider_key] = {\n name: config[:info][:name],\n api_key: config[:api_key],\n env_var: config[:env_var],\n capabilities: config[:info][:capabilities],\n models: config[:info][:models]\n }\n end\n end\n\n # Finalize provider configuration\n #\n # @return [Boolean] True if at least one provider configured\n def finalize_provider_configuration\n if @setup_data[:providers]&.any?\n # Select primary provider\n if @setup_data[:providers].size == 1\n primary_provider = @setup_data[:providers].keys.first\n else\n choices = @setup_data[:providers].map do |key, config|\n { name: config[:name], value: key }\n end\n primary_provider = @prompt.select(\"Select primary AI provider:\", choices)\n end\n\n @setup_data[:primary_provider] = primary_provider\n @logger.success(\"Provider configuration completed!\")\n @logger.info(\"Primary provider: #{@setup_data[:providers][primary_provider][:name]}\")\n \n true\n else\n @logger.warn(\"No AI providers configured. Some features may not work.\")\n false\n end\n end\n end\n end\nend"}}, "lib/blueprintsCLI/configuration.rb": {"64a20b367209eac0d73cc0df9909e85d962ad6d5": {"t": 1785745749.4073381, "n": 24262, "h": "5c5f384bd2a3d063", "c": "# frozen_string_literal: true\n\nrequire \"dry/monads\"\nrequire \"tty-config\"\nrequire \"fileutils\"\nrequire \"ruby_llm\"\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:database, :url) # Returns env var value\n class Configuration\n include Dry::Monads[:result, :try]\n\n # Error raised when configuration validation fails\n class ValidationError < StandardError\n end\n\n # Error raised when required configuration is missing\n class MissingConfigError < StandardError\n end\n\n # Default configuration file name\n DEFAULT_FILENAME = \"config\"\n\n # Default configuration file extension\n DEFAULT_EXTENSION = \".yml\"\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = \"BLUEPRINTS\"\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default:)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value:)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force:, create:)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [true] Returns true on success, raises ValidationError on failure\n def validate!\n validate_blueprints\n .bind { validate_ai }\n .bind { validate_logger }\n .or { |msg| raise ValidationError, msg }\n .value!\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:database, :url) ||\n ENV[\"BLUEPRINT_DATABASE_URL\"] ||\n ENV[\"DATABASE_URL\"] ||\n build_database_url\n end\n\n # Build database URL from individual components\n #\n # @return [String] Constructed database URL\n def build_database_url\n host = ENV[\"DB_HOST\"] || \"localhost\"\n port = ENV[\"DB_PORT\"] || \"5432\"\n user = ENV[\"DB_USER\"] || \"postgres\"\n password = ENV[\"DB_PASSWORD\"] || \"blueprints\"\n database = ENV[\"DB_NAME\"] || \"blueprints\"\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when \"gemini\", \"google\"\n ENV[\"GEMINI_API_KEY\"] || ENV.fetch(\"GOOGLE_API_KEY\", nil)\n when \"openai\"\n ENV.fetch(\"OPENAI_API_KEY\", nil)\n when \"openrouter\"\n ENV.fetch(\"OPENROUTER_API_KEY\", nil)\n when \"anthropic\"\n ENV.fetch(\"ANTHROPIC_API_KEY\", nil)\n when \"deepseek\"\n ENV.fetch(\"DEEPSEEK_API_KEY\", nil)\n end\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :rubyllm, :openai_api_base) if fetch(:ai, :rubyllm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require \"tty-prompt\"\n prompt = TTY::Prompt.new\n\n puts \"šŸ”§ BlueprintsCLI Interactive Configuration Setup\"\n puts \"=\" * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask(\"Temporary directory:\", default: fetch(:editor, :temp_dir) || \"/tmp\")\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?(\"Enable colored output?\", default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?(\"Enable interactive prompts?\",\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask(\"Pager command:\", default: fetch(:ui, :pager) || \"most\")\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n result = Try(:StandardError) do\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.openrouter_api_key = ai_api_key(:openrouter)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoints\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n config.ollama_api_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n end\n\n @rubyllm_configured = true\n end.to_result\n\n result.or { |e| BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\") }\n end\n\n private def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n private def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask(\"Database URL:\", default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n private def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select(\"Select AI provider:\", %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :provider, value: provider)\n\n case provider.downcase\n when \"gemini\"\n api_key = prompt.mask(\"Gemini API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when \"openai\"\n api_key = prompt.mask(\"OpenAI API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when \"anthropic\"\n api_key = prompt.mask(\"Anthropic API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when \"deepseek\"\n api_key = prompt.mask(\"DeepSeek API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n private def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select(\"Console log level:\", %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?(\"Enable file logging?\")\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select(\"File log level:\", %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask(\"Log file path:\", default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n private def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\"\n editor = prompt.ask(\"Default editor:\", default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?(\"Enable auto-save for edits?\")\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n private def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = \"_\"\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, \".config\", \"BlueprintsCLI\"),\n File.join(Dir.home, \".blueprintsCLI\"),\n File.join(__dir__, \"config\"),\n Dir.pwd,\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n private def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n end\n\n # Set default configuration values\n private def set_defaults\n # Database defaults (top-level key matching config.yml)\n @config.set_if_empty(:database, :url,\n value: \"postgresql://postgres:blueprints@localhost:5432/blueprints\")\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :provider, value: \"Gemini\")\n @config.set_if_empty(:ai, :model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :embedding_model, value: \"embeddinggemma\")\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: \"embeddinggemma\")\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: \"imagen-3.0-generate-002\")\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: \"info\")\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: \"info\")\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: \"debug\")\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n private def setup_env_mappings\n # Database\n @config.set_from_env(:database, :url) { \"BLUEPRINT_DATABASE_URL\" }\n @config.set_from_env(:database, :url) { \"DATABASE_URL\" }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { \"EDITOR\" }\n @config.set_from_env(:blueprints, :editor) { \"VISUAL\" }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { \"DEBUG\" }\n @config.set_from_env(:blueprints, :debug) { \"BLUEPRINTS_DEBUG\" }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openrouter) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GEMINI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GOOGLE_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { \"ANTHROPIC_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { \"DEEPSEEK_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { \"OPENAI_ACCESS_TOKEN\" }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { \"OPENAI_BASE_URI\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openrouter_api_key) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { \"OPENAI_API_BASE\" }\n @config.set_from_env(:ai, :rubyllm, :ollama_api_base) { \"OLLAMA_API_BASE\" }\n end\n\n # Setup validation rules\n private def setup_validations\n # Database URL validation\n @config.validate(:database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?(\"postgres://\") || value.start_with?(\"postgresql://\")\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n #\n # @return [Dry::Monads::Result] Success or Failure\n private def validate_blueprints\n db_url = fetch(:database, :url)\n # Only validate if we're not using the env-var fallback\n if db_url.nil? || (db_url.empty? && !ENV[\"BLUEPRINT_DATABASE_URL\"] && !ENV[\"DATABASE_URL\"])\n return Failure(\"Database URL is required\")\n end\n\n Success(true)\n end\n\n # Validate AI section\n #\n # @return [Dry::Monads::Result] Success or Failure\n private def validate_ai\n provider = fetch(:ai, :provider)\n model = fetch(:ai, :model)\n\n return Failure(\"AI provider is required\") if provider.nil? || provider.empty?\n return Failure(\"AI model is required\") if model.nil? || model.empty?\n\n # Check if API key is available for the provider (warn only, not a hard failure)\n api_key = ai_api_key(provider)\n if api_key.nil? || api_key.empty?\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate Ollama-specific configuration (warns only)\n validate_ollama_config!\n\n Success(true)\n end\n\n # Validate Ollama-specific configuration\n private def validate_ollama_config!\n embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n\n # Check if we're using an Ollama embedding model\n return unless embedding_model&.match?(/^(embeddinggemma|nomic-embed|mxbai-embed)/i)\n\n ollama_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n return unless ollama_base.nil? || ollama_base.empty?\n\n BlueprintsCLI.logger.warn(\"Using Ollama embedding model '#{embedding_model}' but OLLAMA_API_BASE is not configured.\")\n BlueprintsCLI.logger.warn(\"Set OLLAMA_API_BASE environment variable (e.g., http://localhost:11434) or ensure Ollama is running on the default port.\")\n end\n\n # Validate logger section\n #\n # @return [Dry::Monads::Result] Success or Failure\n private def validate_logger\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n return Failure(\"Invalid logger level: #{level}\")\n end\n\n if fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n return Failure(\"Logger file path is required when file logging is enabled\")\n end\n\n Success(true)\n end\n\n # Get default log file path\n private def default_log_path\n state_home = ENV[\"XDG_STATE_HOME\"] || File.expand_path(\"~/.local/state\")\n File.join(state_home, \"BlueprintsCLI\", \"app.log\")\n end\n end\nend\n"}, "ac09a99eaa88c83e498c8742e3bcda6ef52c2a0d": {"t": 1785756131.964107, "n": 24262, "h": "5c5f384bd2a3d063", "c": "# frozen_string_literal: true\n\nrequire \"dry/monads\"\nrequire \"tty-config\"\nrequire \"fileutils\"\nrequire \"ruby_llm\"\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:database, :url) # Returns env var value\n class Configuration\n include Dry::Monads[:result, :try]\n\n # Error raised when configuration validation fails\n class ValidationError < StandardError\n end\n\n # Error raised when required configuration is missing\n class MissingConfigError < StandardError\n end\n\n # Default configuration file name\n DEFAULT_FILENAME = \"config\"\n\n # Default configuration file extension\n DEFAULT_EXTENSION = \".yml\"\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = \"BLUEPRINTS\"\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default:)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value:)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force:, create:)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [true] Returns true on success, raises ValidationError on failure\n def validate!\n validate_blueprints\n .bind { validate_ai }\n .bind { validate_logger }\n .or { |msg| raise ValidationError, msg }\n .value!\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:database, :url) ||\n ENV[\"BLUEPRINT_DATABASE_URL\"] ||\n ENV[\"DATABASE_URL\"] ||\n build_database_url\n end\n\n # Build database URL from individual components\n #\n # @return [String] Constructed database URL\n def build_database_url\n host = ENV[\"DB_HOST\"] || \"localhost\"\n port = ENV[\"DB_PORT\"] || \"5432\"\n user = ENV[\"DB_USER\"] || \"postgres\"\n password = ENV[\"DB_PASSWORD\"] || \"blueprints\"\n database = ENV[\"DB_NAME\"] || \"blueprints\"\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when \"gemini\", \"google\"\n ENV[\"GEMINI_API_KEY\"] || ENV.fetch(\"GOOGLE_API_KEY\", nil)\n when \"openai\"\n ENV.fetch(\"OPENAI_API_KEY\", nil)\n when \"openrouter\"\n ENV.fetch(\"OPENROUTER_API_KEY\", nil)\n when \"anthropic\"\n ENV.fetch(\"ANTHROPIC_API_KEY\", nil)\n when \"deepseek\"\n ENV.fetch(\"DEEPSEEK_API_KEY\", nil)\n end\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :rubyllm, :openai_api_base) if fetch(:ai, :rubyllm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require \"tty-prompt\"\n prompt = TTY::Prompt.new\n\n puts \"šŸ”§ BlueprintsCLI Interactive Configuration Setup\"\n puts \"=\" * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask(\"Temporary directory:\", default: fetch(:editor, :temp_dir) || \"/tmp\")\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?(\"Enable colored output?\", default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?(\"Enable interactive prompts?\",\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask(\"Pager command:\", default: fetch(:ui, :pager) || \"most\")\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n result = Try(:StandardError) do\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.openrouter_api_key = ai_api_key(:openrouter)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoints\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n config.ollama_api_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n end\n\n @rubyllm_configured = true\n end.to_result\n\n result.or { |e| BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\") }\n end\n\n private def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n private def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask(\"Database URL:\", default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n private def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select(\"Select AI provider:\", %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :provider, value: provider)\n\n case provider.downcase\n when \"gemini\"\n api_key = prompt.mask(\"Gemini API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when \"openai\"\n api_key = prompt.mask(\"OpenAI API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when \"anthropic\"\n api_key = prompt.mask(\"Anthropic API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when \"deepseek\"\n api_key = prompt.mask(\"DeepSeek API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n private def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select(\"Console log level:\", %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?(\"Enable file logging?\")\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select(\"File log level:\", %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask(\"Log file path:\", default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n private def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\"\n editor = prompt.ask(\"Default editor:\", default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?(\"Enable auto-save for edits?\")\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n private def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = \"_\"\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, \".config\", \"BlueprintsCLI\"),\n File.join(Dir.home, \".blueprintsCLI\"),\n File.join(__dir__, \"config\"),\n Dir.pwd,\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n private def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n end\n\n # Set default configuration values\n private def set_defaults\n # Database defaults (top-level key matching config.yml)\n @config.set_if_empty(:database, :url,\n value: \"postgresql://postgres:blueprints@localhost:5432/blueprints\")\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :provider, value: \"Gemini\")\n @config.set_if_empty(:ai, :model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :embedding_model, value: \"embeddinggemma\")\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: \"embeddinggemma\")\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: \"imagen-3.0-generate-002\")\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: \"info\")\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: \"info\")\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: \"debug\")\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n private def setup_env_mappings\n # Database\n @config.set_from_env(:database, :url) { \"BLUEPRINT_DATABASE_URL\" }\n @config.set_from_env(:database, :url) { \"DATABASE_URL\" }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { \"EDITOR\" }\n @config.set_from_env(:blueprints, :editor) { \"VISUAL\" }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { \"DEBUG\" }\n @config.set_from_env(:blueprints, :debug) { \"BLUEPRINTS_DEBUG\" }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openrouter) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GEMINI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GOOGLE_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { \"ANTHROPIC_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { \"DEEPSEEK_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { \"OPENAI_ACCESS_TOKEN\" }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { \"OPENAI_BASE_URI\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openrouter_api_key) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { \"OPENAI_API_BASE\" }\n @config.set_from_env(:ai, :rubyllm, :ollama_api_base) { \"OLLAMA_API_BASE\" }\n end\n\n # Setup validation rules\n private def setup_validations\n # Database URL validation\n @config.validate(:database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?(\"postgres://\") || value.start_with?(\"postgresql://\")\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n #\n # @return [Dry::Monads::Result] Success or Failure\n private def validate_blueprints\n db_url = fetch(:database, :url)\n # Only validate if we're not using the env-var fallback\n if db_url.nil? || (db_url.empty? && !ENV[\"BLUEPRINT_DATABASE_URL\"] && !ENV[\"DATABASE_URL\"])\n return Failure(\"Database URL is required\")\n end\n\n Success(true)\n end\n\n # Validate AI section\n #\n # @return [Dry::Monads::Result] Success or Failure\n private def validate_ai\n provider = fetch(:ai, :provider)\n model = fetch(:ai, :model)\n\n return Failure(\"AI provider is required\") if provider.nil? || provider.empty?\n return Failure(\"AI model is required\") if model.nil? || model.empty?\n\n # Check if API key is available for the provider (warn only, not a hard failure)\n api_key = ai_api_key(provider)\n if api_key.nil? || api_key.empty?\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate Ollama-specific configuration (warns only)\n validate_ollama_config!\n\n Success(true)\n end\n\n # Validate Ollama-specific configuration\n private def validate_ollama_config!\n embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n\n # Check if we're using an Ollama embedding model\n return unless embedding_model&.match?(/^(embeddinggemma|nomic-embed|mxbai-embed)/i)\n\n ollama_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n return unless ollama_base.nil? || ollama_base.empty?\n\n BlueprintsCLI.logger.warn(\"Using Ollama embedding model '#{embedding_model}' but OLLAMA_API_BASE is not configured.\")\n BlueprintsCLI.logger.warn(\"Set OLLAMA_API_BASE environment variable (e.g., http://localhost:11434) or ensure Ollama is running on the default port.\")\n end\n\n # Validate logger section\n #\n # @return [Dry::Monads::Result] Success or Failure\n private def validate_logger\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n return Failure(\"Invalid logger level: #{level}\")\n end\n\n if fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n return Failure(\"Logger file path is required when file logging is enabled\")\n end\n\n Success(true)\n end\n\n # Get default log file path\n private def default_log_path\n state_home = ENV[\"XDG_STATE_HOME\"] || File.expand_path(\"~/.local/state\")\n File.join(state_home, \"BlueprintsCLI\", \"app.log\")\n end\n end\nend\n"}, "84c01180385ad460755977240718d6e31767b1b5": {"t": 1785756133.357016, "n": 25324, "h": "62e8ba219a71e204", "c": "# frozen_string_literal: true\n\nrequire \"tty-config\"\nrequire \"fileutils\"\nrequire \"ruby_llm\"\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n class ValidationError < StandardError\n end\n\n # Error raised when required configuration is missing\n class MissingConfigError < StandardError\n end\n\n # Default configuration file name\n DEFAULT_FILENAME = \"config\"\n\n # Default configuration file extension\n DEFAULT_EXTENSION = \".yml\"\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = \"BLUEPRINTS\"\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default:)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value:)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force:, create:)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV[\"BLUEPRINT_DATABASE_URL\"] ||\n ENV[\"DATABASE_URL\"] ||\n build_database_url\n end\n\n # Build database URL from individual components\n #\n # @return [String] Constructed database URL\n def build_database_url\n host = ENV[\"DB_HOST\"] || \"localhost\"\n port = ENV[\"DB_PORT\"] || \"5432\"\n user = ENV[\"DB_USER\"] || \"postgres\"\n password = ENV[\"DB_PASSWORD\"] || \"blueprints\"\n database = ENV[\"DB_NAME\"] || \"blueprints\"\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when \"gemini\", \"google\"\n ENV[\"GEMINI_API_KEY\"] || ENV.fetch(\"GOOGLE_API_KEY\", nil)\n when \"openai\"\n ENV.fetch(\"OPENAI_API_KEY\", nil)\n when \"openrouter\"\n ENV.fetch(\"OPENROUTER_API_KEY\", nil)\n when \"anthropic\"\n ENV.fetch(\"ANTHROPIC_API_KEY\", nil)\n when \"deepseek\"\n ENV.fetch(\"DEEPSEEK_API_KEY\", nil)\n end\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require \"tty-prompt\"\n prompt = TTY::Prompt.new\n\n puts \"šŸ”§ BlueprintsCLI Interactive Configuration Setup\"\n puts \"=\" * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask(\"Temporary directory:\", default: fetch(:editor, :temp_dir) || \"/tmp\")\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?(\"Enable colored output?\", default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?(\"Enable interactive prompts?\",\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask(\"Pager command:\", default: fetch(:ui, :pager) || \"most\")\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n # configure_rubyllm # MOVED TO POST-INIT - was causing circular dependency\n configure_openai_gem\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.openrouter_api_key = ai_api_key(:openrouter)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoints\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n config.ollama_api_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n end\n\n @rubyllm_configured = true\n rescue => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n private def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n private def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask(\"Database URL:\", default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n private def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select(\"Select AI provider:\", %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when \"gemini\"\n api_key = prompt.mask(\"Gemini API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when \"openai\"\n api_key = prompt.mask(\"OpenAI API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when \"anthropic\"\n api_key = prompt.mask(\"Anthropic API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when \"deepseek\"\n api_key = prompt.mask(\"DeepSeek API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n private def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select(\"Console log level:\", %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?(\"Enable file logging?\")\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select(\"File log level:\", %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask(\"Log file path:\", default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n private def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\"\n editor = prompt.ask(\"Default editor:\", default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?(\"Enable auto-save for edits?\")\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n private def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = \"_\"\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, \".config\", \"BlueprintsCLI\"),\n File.join(Dir.home, \".blueprintsCLI\"),\n File.join(__dir__, \"config\"),\n Dir.pwd,\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n private def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n private def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: \"postgresql://postgres:blueprints@localhost:5432/blueprints\")\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :provider, value: \"Gemini\")\n @config.set_if_empty(:ai, :sublayer, :model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :embedding_model, value: \"embeddinggemma:latest\")\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: \"embeddinggemma:latest\")\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: \"imagen-3.0-generate-002\")\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: \"info\")\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: \"info\")\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: \"debug\")\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n private def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { \"BLUEPRINT_DATABASE_URL\" }\n @config.set_from_env(:blueprints, :database, :url) { \"DATABASE_URL\" }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { \"EDITOR\" }\n @config.set_from_env(:blueprints, :editor) { \"VISUAL\" }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { \"DEBUG\" }\n @config.set_from_env(:blueprints, :debug) { \"BLUEPRINTS_DEBUG\" }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openrouter) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GEMINI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GOOGLE_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { \"ANTHROPIC_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { \"DEEPSEEK_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { \"OPENAI_ACCESS_TOKEN\" }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { \"OPENAI_BASE_URI\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openrouter_api_key) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { \"OPENAI_API_BASE\" }\n @config.set_from_env(:ai, :rubyllm, :ollama_api_base) { \"OLLAMA_API_BASE\" }\n end\n\n # Setup validation rules\n private def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?(\"postgres://\") || value.start_with?(\"postgresql://\")\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n private def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV[\"BLUEPRINT_DATABASE_URL\"] && !ENV[\"DATABASE_URL\"])\n\n raise ValidationError, \"Database URL is required\"\n end\n\n # Validate AI section\n private def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, \"AI provider is required\" if provider.nil? || provider.empty?\n\n raise ValidationError, \"AI model is required\" if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n if api_key.nil? || api_key.empty?\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate Ollama-specific configuration\n validate_ollama_config!\n end\n\n # Validate Ollama-specific configuration\n private def validate_ollama_config!\n embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n\n # Check if we're using an Ollama embedding model\n return unless embedding_model&.match?(/^(embeddinggemma|nomic-embed|mxbai-embed)/i)\n\n ollama_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n return unless ollama_base.nil? || ollama_base.empty?\n\n BlueprintsCLI.logger.warn(\"Using Ollama embedding model '#{embedding_model}' but OLLAMA_API_BASE is not configured.\")\n BlueprintsCLI.logger.warn(\"Set OLLAMA_API_BASE environment variable (e.g., http://localhost:11434) or ensure Ollama is running on the default port.\")\n end\n\n # Validate logger section\n private def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, \"Logger file path is required when file logging is enabled\"\n end\n\n # Get default log file path\n private def default_log_path\n state_home = ENV[\"XDG_STATE_HOME\"] || File.expand_path(\"~/.local/state\")\n File.join(state_home, \"BlueprintsCLI\", \"app.log\")\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV[\"OPENAI_ACCESS_TOKEN\"] || ai_api_key(:openai)\n base_uri = ENV.fetch(\"OPENAI_BASE_URI\", nil)\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue => e\n BlueprintsCLI.logger.failure(\"Error configuring OpenAI gem: #{e.message}\")\n end\n end\nend\n"}, "48eef306e5e764c0ea0da7069ba82b107409bba0": {"t": 1785756135.0507302, "n": 46210, "h": "e30eb29aba878c48", "c": "# frozen_string_literal: true\n\nrequire \"tty-config\"\nrequire \"fileutils\"\nrequire \"ruby_llm\"\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n class ValidationError < StandardError\n end\n\n # Error raised when required configuration is missing\n class MissingConfigError < StandardError\n end\n\n # Default configuration file name\n DEFAULT_FILENAME = \"config\"\n\n # Default configuration file extension\n DEFAULT_EXTENSION = \".yml\"\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = \"BLUEPRINTS\"\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default:)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value:)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force:, create:)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV[\"BLUEPRINT_DATABASE_URL\"] ||\n ENV[\"DATABASE_URL\"] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV[\"DB_HOST\"] || \"localhost\"\n port = ENV[\"DB_PORT\"] || \"5432\"\n user = ENV[\"DB_USER\"] || \"postgres\"\n password = ENV[\"DB_PASSWORD\"] || \"blueprints\"\n database = ENV[\"DB_NAME\"] || \"blueprints\"\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when \"gemini\", \"google\"\n ENV[\"GEMINI_API_KEY\"] || ENV.fetch(\"GOOGLE_API_KEY\", nil)\n when \"openai\"\n ENV.fetch(\"OPENAI_API_KEY\", nil)\n when \"openrouter\"\n ENV.fetch(\"OPENROUTER_API_KEY\", nil)\n when \"anthropic\"\n ENV.fetch(\"ANTHROPIC_API_KEY\", nil)\n when \"deepseek\"\n ENV.fetch(\"DEEPSEEK_API_KEY\", nil)\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require \"tty-prompt\"\n prompt = TTY::Prompt.new\n\n puts \"šŸ”§ BlueprintsCLI Interactive Configuration Setup\"\n puts \"=\" * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask(\"Temporary directory:\", default: fetch(:editor, :temp_dir) || \"/tmp\")\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?(\"Enable colored output?\", default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?(\"Enable interactive prompts?\",\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask(\"Pager command:\", default: fetch(:ui, :pager) || \"most\")\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n # configure_rubyllm # MOVED TO POST-INIT - was causing circular dependency\n configure_openai_gem\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.openrouter_api_key = ai_api_key(:openrouter)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoints\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n config.ollama_api_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n end\n\n @rubyllm_configured = true\n rescue => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n private def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n private def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask(\"Database URL:\", default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n private def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select(\"Select AI provider:\", %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when \"gemini\"\n api_key = prompt.mask(\"Gemini API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when \"openai\"\n api_key = prompt.mask(\"OpenAI API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when \"anthropic\"\n api_key = prompt.mask(\"Anthropic API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when \"deepseek\"\n api_key = prompt.mask(\"DeepSeek API Key (leave empty to use environment variable):\")\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n private def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select(\"Console log level:\", %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?(\"Enable file logging?\")\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select(\"File log level:\", %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask(\"Log file path:\", default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n private def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\"\n editor = prompt.ask(\"Default editor:\", default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?(\"Enable auto-save for edits?\")\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n private def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = \"_\"\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, \".config\", \"BlueprintsCLI\"),\n File.join(Dir.home, \".blueprintsCLI\"),\n File.join(__dir__, \"config\"),\n Dir.pwd,\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n private def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n private def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: \"postgresql://postgres:blueprints@localhost:5432/blueprints\")\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :provider, value: \"Gemini\")\n @config.set_if_empty(:ai, :sublayer, :model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :embedding_model, value: \"embeddinggemma:latest\")\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: \"gemini-2.0-flash\")\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: \"embeddinggemma:latest\")\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: \"imagen-3.0-generate-002\")\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: \"info\")\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: \"info\")\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: \"debug\")\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n private def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { \"BLUEPRINT_DATABASE_URL\" }\n @config.set_from_env(:blueprints, :database, :url) { \"DATABASE_URL\" }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { \"EDITOR\" }\n @config.set_from_env(:blueprints, :editor) { \"VISUAL\" }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { \"DEBUG\" }\n @config.set_from_env(:blueprints, :debug) { \"BLUEPRINTS_DEBUG\" }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openrouter) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GEMINI_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :gemini) { \"GOOGLE_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { \"ANTHROPIC_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { \"DEEPSEEK_API_KEY\" }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { \"OPENAI_ACCESS_TOKEN\" }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { \"OPENAI_BASE_URI\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { \"OPENAI_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openrouter_api_key) { \"OPENROUTER_API_KEY\" }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { \"OPENAI_API_BASE\" }\n @config.set_from_env(:ai, :rubyllm, :ollama_api_base) { \"OLLAMA_API_BASE\" }\n end\n\n # Setup validation rules\n private def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?(\"postgres://\") || value.start_with?(\"postgresql://\")\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n private def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV[\"BLUEPRINT_DATABASE_URL\"] && !ENV[\"DATABASE_URL\"])\n\n raise ValidationError, \"Database URL is required\"\n end\n\n # Validate AI section\n private def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, \"AI provider is required\" if provider.nil? || provider.empty?\n\n raise ValidationError, \"AI model is required\" if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n if api_key.nil? || api_key.empty?\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate Ollama-specific configuration\n validate_ollama_config!\n end\n\n # Validate Ollama-specific configuration\n private def validate_ollama_config!\n embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n\n # Check if we're using an Ollama embedding model\n return unless embedding_model&.match?(/^(embeddinggemma|nomic-embed|mxbai-embed)/i)\n\n ollama_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n return unless ollama_base.nil? || ollama_base.empty?\n\n BlueprintsCLI.logger.warn(\"Using Ollama embedding model '#{embedding_model}' but OLLAMA_API_BASE is not configured.\")\n BlueprintsCLI.logger.warn(\"Set OLLAMA_API_BASE environment variable (e.g., http://localhost:11434) or ensure Ollama is running on the default port.\")\n end\n\n # Validate logger section\n private def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, \"Logger file path is required when file logging is enabled\"\n end\n\n # Get default log file path\n private def default_log_path\n state_home = ENV[\"XDG_STATE_HOME\"] || File.expand_path(\"~/.local/state\")\n File.join(state_home, \"BlueprintsCLI\", \"app.log\")\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV[\"OPENAI_ACCESS_TOKEN\"] || ai_api_key(:openai)\n base_uri = ENV.fetch(\"OPENAI_BASE_URI\", nil)\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue => e\n BlueprintsCLI.logger.failure(\"Error configuring OpenAI gem: #{e.message}\")\n end\n end\nend\n"}, "e086c65d5a6425245237cdab72897fbf3d9d87f0": {"t": 1785756136.780054, "n": 53899, "h": "8035eb8dadcdc2f5", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n ENV['OPENAI_API_KEY']\n when 'openrouter'\n ENV['OPENROUTER_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n # configure_rubyllm # MOVED TO POST-INIT - was causing circular dependency\n configure_openai_gem\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.openrouter_api_key = ai_api_key(:openrouter)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoints\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n config.ollama_api_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n end\n\n @rubyllm_configured = true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'embeddinggemma:latest')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'embeddinggemma:latest')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openrouter) { 'OPENROUTER_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openrouter_api_key) { 'OPENROUTER_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n @config.set_from_env(:ai, :rubyllm, :ollama_api_base) { 'OLLAMA_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n if api_key.nil? || api_key.empty?\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate Ollama-specific configuration\n validate_ollama_config!\n end\n\n # Validate Ollama-specific configuration\n def validate_ollama_config!\n embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n\n # Check if we're using an Ollama embedding model\n if embedding_model&.match?(/^(embeddinggemma|nomic-embed|mxbai-embed)/i)\n ollama_base = fetch(:ai, :rubyllm, :ollama_api_base)\n\n if ollama_base.nil? || ollama_base.empty?\n BlueprintsCLI.logger.warn(\"Using Ollama embedding model '#{embedding_model}' but OLLAMA_API_BASE is not configured.\")\n BlueprintsCLI.logger.warn(\"Set OLLAMA_API_BASE environment variable (e.g., http://localhost:11434) or ensure Ollama is running on the default port.\")\n end\n end\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n private\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "0083213af819b674d3b2b52a35a0ea691643f5b3": {"t": 1785756138.2449987, "n": 52790, "h": "ce82813f1b7145b7", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n # configure_rubyllm # MOVED TO POST-INIT - was causing circular dependency\n configure_openai_gem\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n end\n\n @rubyllm_configured = true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n private\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "5ce7d6554fca20a1e835cd9fd90668eb2e3d5ddf": {"t": 1785756139.7052839, "n": 53214, "h": "7d0c3e8e7fd58c5e", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n \n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n \n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n #\n # Configures external AI provider libraries (RubyLLM and OpenAI gem)\n # using values from the unified configuration system. This ensures\n # that all provider libraries are properly initialized with the\n # correct API keys and settings.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_providers\n # configure_rubyllm # MOVED TO POST-INIT - was causing circular dependency\n configure_openai_gem\n end\n\n # Configure RubyLLM on-demand (lazy loading to avoid circular dependencies)\n def configure_rubyllm!\n return if @rubyllm_configured\n\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n \n end\n\n @rubyllm_configured = true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "95477b41e128e10245b73074106d27250e3ceef7": {"t": 1785756141.452241, "n": 53607, "h": "50a6340969ab8cf0", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n \n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n \n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n #\n # Configures external AI provider libraries (RubyLLM and OpenAI gem)\n # using values from the unified configuration system. This ensures\n # that all provider libraries are properly initialized with the\n # correct API keys and settings.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n #\n # Initializes the RubyLLM library with API keys, model defaults,\n # connection settings, and logging configuration from the unified\n # configuration system. Handles errors gracefully to avoid blocking\n # application startup.\n #\n # Configuration includes:\n # - API keys for all supported providers\n # - Default models for text, embedding, and image generation\n # - Connection timeouts and retry settings\n # - Logging configuration\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n \n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "8dbee67f443937e40259e73e0983be1418bb0e5a": {"t": 1785756142.8884127, "n": 53765, "h": "67454371ca190cfe", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n \n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n \n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n #\n # Configures external AI provider libraries (RubyLLM and OpenAI gem)\n # using values from the unified configuration system. This ensures\n # that all provider libraries are properly initialized with the\n # correct API keys and settings.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n #\n # Initializes the RubyLLM library with API keys, model defaults,\n # connection settings, and logging configuration from the unified\n # configuration system. Handles errors gracefully to avoid blocking\n # application startup.\n #\n # Configuration includes:\n # - API keys for all supported providers\n # - Default models for text, embedding, and image generation\n # - Connection timeouts and retry settings\n # - Logging configuration\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n \n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "f88aa0463f3684614405fcb574a75872302ca3ee": {"t": 1785756144.4632218, "n": 54101, "h": "c4e97f07889e7b07", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\n # puts \"Configuration setup completed successfully\"\n # else\n # puts \"Setup failed, check error messages\"\n # end\n #\n # @example First-time user experience\n # unless config.exist?\n # puts \"Welcome to BlueprintsCLI! Let's set up your configuration.\"\n # config.interactive_setup\n # end\n #\n # @raise [StandardError] Various errors may be raised during setup,\n # which are caught and logged as failure messages.\n #\n # @since 0.1.0\n # @see TTY::Prompt\n # @see #save_config\n # @see #validate!\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n #\n # Configures external AI provider libraries (RubyLLM and OpenAI gem)\n # using values from the unified configuration system. This ensures\n # that all provider libraries are properly initialized with the\n # correct API keys and settings.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n #\n # Initializes the RubyLLM library with API keys, model defaults,\n # connection settings, and logging configuration from the unified\n # configuration system. Handles errors gracefully to avoid blocking\n # application startup.\n #\n # Configuration includes:\n # - API keys for all supported providers\n # - Default models for text, embedding, and image generation\n # - Connection timeouts and retry settings\n # - Logging configuration\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "8a920d3fd816e278e8ca49ee9b19c77f5adb06ae": {"t": 1785756146.076216, "n": 52814, "h": "a2ea37c60ca92ff8", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\\n # puts \\\"Configuration setup completed successfully\\\"\\n # else\\n # puts \\\"Setup failed, check error messages\\\"\\n # end\\n #\\n # @example First-time user experience\\n # unless config.exist?\\n # puts \\\"Welcome to BlueprintsCLI! Let's set up your configuration.\\\"\\n # config.interactive_setup\\n # end\\n #\\n # @raise [StandardError] Various errors may be raised during setup,\\n # which are caught and logged as failure messages.\\n #\\n # @since 0.1.0\\n # @see TTY::Prompt\\n # @see #save_config\\n # @see #validate!\\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n #\n # Configures external AI provider libraries (RubyLLM and OpenAI gem)\n # using values from the unified configuration system. This ensures\n # that all provider libraries are properly initialized with the\n # correct API keys and settings.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n #\n # Initializes the RubyLLM library with API keys, model defaults,\n # connection settings, and logging configuration from the unified\n # configuration system. Handles errors gracefully to avoid blocking\n # application startup.\n #\n # Configuration includes:\n # - API keys for all supported providers\n # - Default models for text, embedding, and image generation\n # - Connection timeouts and retry settings\n # - Logging configuration\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\nend\n"}, "fb1824ba809788b9364c29194e18bff79d091a70": {"t": 1785756147.7038085, "n": 52710, "h": "26a171af31207be5", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management system using TTY::Config for BlueprintsCLI\n #\n # This class provides a comprehensive configuration management system that handles\n # configuration for the entire BlueprintsCLI application. It manages settings for\n # the application itself, AI providers (Sublayer and RubyLLM), database connections,\n # logging, and user interface preferences.\n #\n # The configuration system supports multiple configuration sources including:\n # - YAML configuration files in standard locations\n # - Environment variables with automatic prefix mapping\n # - Interactive setup through TTY::Prompt\n # - Programmatic configuration via the API\n #\n # Configuration files are searched in the following order:\n # 1. ~/.config/BlueprintsCLI/config.yml\n # 2. ~/.blueprintsCLI/config.yml\n # 3. lib/blueprintsCLI/config/config.yml\n # 4. ./config.yml (current directory)\n #\n # Environment variables are automatically mapped using the BLUEPRINTS_ prefix,\n # with nested configuration keys separated by underscores.\n #\n # @example Basic configuration usage\n # config = BlueprintsCLI::Configuration.new\n # database_url = config.fetch(:blueprints, :database, :url)\n # config.set(:logger, :level, value: 'debug')\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://localhost/blueprints'\n # config.fetch(:blueprints, :database, :url) # Returns the environment variable value\n #\n # @example Interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # config.interactive_setup\n # config.save_config\n #\n # @example Validation\n # config = BlueprintsCLI::Configuration.new\n # if config.valid?\n # puts \"Configuration is valid\"\n # else\n # config.validate! # Raises ValidationError with details\n # end\n #\n # @since 0.1.0\n # @see TTY::Config\n # @see BlueprintsCLI::CLI for command-line interface usage\n class Configuration\n # Error raised when configuration validation fails\n #\n # This error is raised when the configuration contains invalid values\n # that don't meet the validation criteria defined in the validation rules.\n #\n # @example Handling validation errors\n # begin\n # config.validate!\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Configuration is invalid: #{e.message}\"\n # end\n #\n # @since 0.1.0\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n #\n # This error is raised when attempting to access configuration values\n # that are required but not present in any configuration source.\n #\n # @example Handling missing configuration\n # begin\n # config.fetch(:required, :key)\n # rescue BlueprintsCLI::Configuration::MissingConfigError => e\n # puts \"Missing required configuration: #{e.message}\"\n # end\n #\n # @since 0.1.0\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name (without extension)\n #\n # This constant defines the default filename used when searching for\n # configuration files in the configured search paths.\n #\n # @since 0.1.0\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n #\n # This constant defines the file extension used for configuration files.\n # The configuration system will look for files with this extension.\n #\n # @since 0.1.0\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n #\n # This prefix is used to automatically map environment variables to\n # configuration keys. Environment variables starting with this prefix\n # will be automatically mapped to configuration values.\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_DATABASE_URL'] maps to config.fetch(:blueprints, :database, :url)\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] maps to config.fetch(:logger, :level)\n #\n # @since 0.1.0\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The underlying TTY::Config instance\n #\n # This accessor provides direct access to the TTY::Config instance for\n # advanced configuration operations that are not covered by the wrapper methods.\n #\n # @return [TTY::Config] The TTY::Config instance managing configuration data\n # @since 0.1.0\n # @see TTY::Config\n attr_reader :config\n\n # Initialize a new Configuration instance\n #\n # Creates a new configuration management instance with support for multiple\n # configuration sources including YAML files and environment variables.\n # The configuration system automatically searches for configuration files\n # in standard locations and sets up environment variable mapping.\n #\n # @param config_paths [Array] Additional filesystem paths to search for configuration files.\n # These paths are added to the default search paths and will be checked for configuration files.\n # @param filename [String] The base filename (without extension) to search for in configuration paths.\n # Defaults to 'config', resulting in searches for 'config.yml' files.\n # @param auto_load [Boolean] Whether to automatically load configuration files and set up defaults.\n # When true, performs full initialization including file loading, default values, and validation setup.\n # When false, creates the instance but requires manual loading via {#load_configuration}.\n #\n # @example Basic initialization\n # config = BlueprintsCLI::Configuration.new\n # # Searches default paths for 'config.yml' and loads automatically\n #\n # @example Custom paths and filename\n # config = BlueprintsCLI::Configuration.new(\n # config_paths: ['/etc/blueprints', '/usr/local/etc'],\n # filename: 'blueprints_config'\n # )\n #\n # @example Deferred loading\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # # Perform custom setup...\n # config.load_configuration\n #\n # @raise [TTY::Config::ReadError] if configuration file exists but cannot be read\n # @raise [ValidationError] if auto_load is true and configuration validation fails\n #\n # @since 0.1.0\n # @see #load_configuration\n # @see #setup_config\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # Retrieves configuration values using a sequence of nested keys, with support\n # for default values when the configuration path doesn't exist. This method\n # searches through all configuration sources including YAML files and environment\n # variables mapped through the configured prefix.\n #\n # @param keys [Array] Sequence of nested keys to navigate the configuration hierarchy.\n # Keys can be symbols or strings and represent the path to the desired configuration value.\n # @param default [Object] Default value to return if the configuration path doesn't exist.\n # Can be any Ruby object. If not provided, returns nil for missing keys.\n #\n # @return [Object] The configuration value found at the specified path, or the default value\n # if the path doesn't exist.\n #\n # @example Basic value retrieval\n # config.fetch(:blueprints, :database, :url)\n # # => \"postgresql://localhost:5432/blueprints\"\n #\n # @example With default value\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n # # => \"gemini\" (if not configured)\n #\n # @example Environment variable mapping\n # ENV['BLUEPRINTS_LOGGER_LEVEL'] = 'debug'\n # config.fetch(:logger, :level)\n # # => \"debug\"\n #\n # @example Nested configuration access\n # config.fetch(:blueprints, :features, :auto_description)\n # # => true\n #\n # @since 0.1.0\n # @see #set\n # @see TTY::Config#fetch\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # Sets configuration values at the specified nested key path. This method\n # allows programmatic configuration of values, which is useful for dynamic\n # configuration updates or during interactive setup processes.\n #\n # Note that validation is not performed during set operations to avoid\n # issues during initial configuration setup. Validation should be performed\n # explicitly using {#validate!} after configuration is complete.\n #\n # @param keys [Array] Sequence of nested keys defining where to set the value.\n # Creates the nested structure if it doesn't exist.\n # @param value [Object] The value to set at the specified configuration path.\n # Can be any Ruby object that can be serialized to YAML.\n #\n # @return [Object] The value that was set, allowing for method chaining.\n #\n # @example Setting database configuration\n # config.set(:blueprints, :database, :url, value: 'postgres://localhost/blueprints')\n # # => \"postgres://localhost/blueprints\"\n #\n # @example Setting logger level\n # config.set(:logger, :level, value: 'debug')\n # # => \"debug\"\n #\n # @example Setting complex nested values\n # config.set(:ai, :rubyllm, :max_retries, value: 5)\n # # => 5\n #\n # @note Validation is not performed during set operations. Use {#validate!} to check configuration validity.\n #\n # @since 0.1.0\n # @see #fetch\n # @see #validate!\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # Determines whether a configuration file exists in any of the configured\n # search paths. This is useful for determining whether to prompt for\n # initial setup or load existing configuration.\n #\n # @return [Boolean] true if a configuration file exists in any search path,\n # false if no configuration file is found.\n #\n # @example Check for existing configuration\n # if config.exist?\n # puts \"Loading existing configuration\"\n # else\n # puts \"No configuration found, running setup\"\n # config.interactive_setup\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#exist?\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # Persists the current configuration state to a YAML file in the first\n # writable location from the configured search paths. Creates necessary\n # directories if they don't exist and the create option is enabled.\n #\n # @param force [Boolean] Whether to overwrite an existing configuration file.\n # When false, will not overwrite existing files and may raise an error.\n # @param create [Boolean] Whether to create missing parent directories.\n # When true, creates the directory structure needed for the configuration file.\n #\n # @return [Boolean] true if the configuration was successfully written to file,\n # false if the write operation failed.\n #\n # @example Write configuration with defaults\n # config.write\n # # => true (if successful)\n #\n # @example Force overwrite existing file\n # config.write(force: true)\n # # => true\n #\n # @example Write without creating directories\n # config.write(create: false)\n # # => false (if directory doesn't exist)\n #\n # @raise [TTY::Config::WriteError] when write operation fails and error handling is disabled\n #\n # @since 0.1.0\n # @see TTY::Config#write\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # Completely reinitializes the configuration system by creating a new\n # TTY::Config instance and reloading all configuration sources including\n # files and environment variables. This is useful when configuration\n # files have been modified externally or environment variables have changed.\n #\n # @return [self] Returns self to allow method chaining.\n #\n # @example Reload after external configuration changes\n # config.reload!\n # updated_value = config.fetch(:blueprints, :database, :url)\n #\n # @example Method chaining\n # config.reload!.validate!\n #\n # @since 0.1.0\n # @see #initialize\n # @see #load_configuration\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get the path to the currently loaded configuration file\n #\n # Returns the filesystem path to the configuration file that was loaded,\n # or nil if no configuration file was found during initialization.\n # This is useful for displaying the source of configuration to users\n # or for backup/editing operations.\n #\n # @return [String, nil] The absolute path to the configuration file if one\n # was loaded, or nil if configuration is only from defaults and environment variables.\n #\n # @example Display configuration source\n # path = config.config_file_path\n # if path\n # puts \"Configuration loaded from: #{path}\"\n # else\n # puts \"Using default configuration\"\n # end\n #\n # @since 0.1.0\n # @see TTY::Config#source_file\n def config_file_path\n @config.source_file\n end\n\n # Convert the entire configuration to a hash\n #\n # Returns a complete hash representation of the current configuration,\n # including all nested structures. This is useful for debugging,\n # serialization, or when you need to work with the configuration\n # data as a standard Ruby hash.\n #\n # @return [Hash] A hash containing all configuration data with symbol keys\n # representing the nested configuration structure.\n #\n # @example Export configuration\n # config_hash = config.to_hash\n # puts config_hash[:blueprints][:database][:url]\n #\n # @example Serialize configuration\n # File.write('config_backup.yml', config.to_hash.to_yaml)\n #\n # @since 0.1.0\n # @see #to_h\n # @see TTY::Config#to_hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate the entire configuration\n #\n # Performs comprehensive validation of all configuration sections including\n # blueprints, AI providers, and logger settings. Validates data types,\n # required fields, value ranges, and cross-section dependencies.\n #\n # This method collects all validation errors and raises a single\n # ValidationError containing all issues found, making it easy to\n # present complete validation feedback to users.\n #\n # @return [true] Returns true if all validation passes.\n #\n # @raise [ValidationError] if any validation rules fail. The error message\n # contains details about all validation failures found.\n #\n # @example Validate configuration\n # begin\n # config.validate!\n # puts \"Configuration is valid\"\n # rescue BlueprintsCLI::Configuration::ValidationError => e\n # puts \"Validation failed:\\n#{e.message}\"\n # end\n #\n # @since 0.1.0\n # @see #valid?\n # @see #validate_blueprints!\n # @see #validate_ai!\n # @see #validate_logger!\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid without raising exceptions\n #\n # Performs the same validation as {#validate!} but returns a boolean\n # result instead of raising exceptions. This is useful for conditional\n # logic where you want to check validity without handling exceptions.\n #\n # @return [Boolean] true if all configuration validation passes,\n # false if any validation rules fail.\n #\n # @example Conditional validation check\n # if config.valid?\n # proceed_with_operation\n # else\n # show_configuration_errors\n # end\n #\n # @since 0.1.0\n # @see #validate!\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with intelligent fallback strategy\n #\n # Attempts to retrieve the database URL from multiple sources in order\n # of preference: configuration file, BLUEPRINT_DATABASE_URL environment\n # variable, DATABASE_URL environment variable, and finally constructs\n # a URL from individual database environment variables.\n #\n # @return [String, nil] The database URL to use for connections, or nil\n # if no database configuration can be determined.\n #\n # @example Get database URL\n # url = config.database_url\n # # => \"postgresql://user:pass@localhost:5432/blueprints\"\n #\n # @example Environment variable precedence\n # ENV['BLUEPRINT_DATABASE_URL'] = 'postgres://custom/db'\n # config.database_url\n # # => \"postgres://custom/db\"\n #\n # @since 0.1.0\n # @see #build_database_url\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual environment variables\n #\n # Constructs a PostgreSQL connection URL using individual database\n # configuration environment variables. This provides a fallback when\n # no complete database URL is available from other sources.\n #\n # Uses the following environment variables with sensible defaults:\n # - DB_HOST (default: 'localhost')\n # - DB_PORT (default: '5432')\n # - DB_USER (default: 'postgres')\n # - DB_PASSWORD (default: 'blueprints')\n # - DB_NAME (default: 'blueprints')\n #\n # @return [String] A complete PostgreSQL connection URL constructed from\n # environment variables and defaults.\n #\n # @example Default construction\n # config.build_database_url\n # # => \"postgresql://postgres:blueprints@localhost:5432/blueprints\"\n #\n # @example With environment variables\n # ENV['DB_HOST'] = 'db.example.com'\n # ENV['DB_USER'] = 'myapp'\n # ENV['DB_PASSWORD'] = 'secret'\n # config.build_database_url\n # # => \"postgresql://myapp:secret@db.example.com:5432/blueprints\"\n #\n # @since 0.1.0\n # @see #database_url\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get API key for the specified AI provider\n #\n # Retrieves API keys for supported AI providers from environment variables.\n # Supports multiple environment variable names for each provider to\n # accommodate different naming conventions and legacy compatibility.\n #\n # Supported providers and their environment variables:\n # - gemini/google: GEMINI_API_KEY, GOOGLE_API_KEY\n # - openai: OPENROUTER_API_KEY, OPENAI_API_KEY\n # - anthropic: ANTHROPIC_API_KEY\n # - deepseek: DEEPSEEK_API_KEY\n #\n # @param provider [String, Symbol] The AI provider name. Case-insensitive.\n # Supported values: 'gemini', 'google', 'openai', 'anthropic', 'deepseek'.\n #\n # @return [String, nil] The API key for the specified provider, or nil\n # if no API key is found in environment variables.\n #\n # @example Get Gemini API key\n # config.ai_api_key(:gemini)\n # # => \"your-gemini-api-key\" (from GEMINI_API_KEY or GOOGLE_API_KEY)\n #\n # @example Get OpenAI API key with fallback\n # config.ai_api_key('openai')\n # # => \"your-openai-key\" (from OPENROUTER_API_KEY or OPENAI_API_KEY)\n #\n # @example Case insensitive\n # config.ai_api_key('ANTHROPIC')\n # # => \"your-anthropic-key\"\n #\n # @since 0.1.0\n # @see #ruby_llm_config\n # @see #sublayer_config\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer AI framework configuration\n #\n # Returns a hash containing all configuration values needed to initialize\n # the Sublayer AI framework. Sublayer is used for AI-powered code analysis\n # and generation within BlueprintsCLI.\n #\n # @return [Hash] A hash containing Sublayer configuration with the following keys:\n # - :project_name [String] The project name for Sublayer (default: 'blueprintsCLI')\n # - :project_template [String] The project template type (default: 'CLI')\n # - :ai_provider [String] The AI provider to use (default: 'Gemini')\n # - :ai_model [String] The specific AI model (default: 'gemini-2.0-flash')\n #\n # @example Get Sublayer configuration\n # sublayer_config = config.sublayer_config\n # # => {\n # # project_name: 'blueprintsCLI',\n # # project_template: 'CLI',\n # # ai_provider: 'Gemini',\n # # ai_model: 'gemini-2.0-flash'\n # # }\n #\n # @example Use with Sublayer initialization\n # Sublayer.configure(config.sublayer_config)\n #\n # @since 0.1.0\n # @see Sublayer\n # @see #ai_api_key\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get RubyLLM library configuration\n #\n # Returns a hash containing all API keys and configuration values needed\n # to initialize the RubyLLM library. Only includes API keys that are\n # actually available in environment variables to avoid configuration errors.\n #\n # The RubyLLM library provides a unified interface to multiple AI providers\n # including OpenAI, Anthropic, Gemini, and DeepSeek.\n #\n # @return [Hash] A hash containing available RubyLLM configuration. May include:\n # - :openai_api_key [String] OpenAI API key (if available)\n # - :anthropic_api_key [String] Anthropic API key (if available)\n # - :gemini_api_key [String] Gemini API key (if available)\n # - :deepseek_api_key [String] DeepSeek API key (if available)\n # - :openai_api_base [String] Custom OpenAI endpoint (if configured)\n #\n # @example Get RubyLLM configuration\n # ruby_llm_config = config.ruby_llm_config\n # # => { gemini_api_key: 'your-key', openai_api_key: 'another-key' }\n #\n # @example Use with RubyLLM initialization\n # RubyLLM.configure(config.ruby_llm_config)\n #\n # @since 0.1.0\n # @see RubyLLM\n # @see #ai_api_key\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Run interactive configuration setup wizard\n #\n # Provides a guided, interactive configuration setup process using TTY::Prompt.\n # This method walks users through configuring all major aspects of BlueprintsCLI\n # including database connection, AI providers, logging, and editor preferences.\n #\n # The setup process includes:\n # 1. Database configuration (URL or connection parameters)\n # 2. AI provider selection and API key configuration\n # 3. Logger settings (level, file logging options)\n # 4. Editor preferences and auto-save settings\n # 5. Automatic configuration file creation and validation\n #\n # @return [Boolean] true if setup completed successfully, false if errors occurred.\n #\n # @example Run interactive setup\n # config = BlueprintsCLI::Configuration.new(auto_load: false)\n # if config.interactive_setup\\n # puts \\\"Configuration setup completed successfully\\\"\\n # else\\n # puts \\\"Setup failed, check error messages\\\"\\n # end\\n #\\n # @example First-time user experience\\n # unless config.exist?\\n # puts \\\"Welcome to BlueprintsCLI! Let's set up your configuration.\\\"\\n # config.interactive_setup\\n # end\\n #\\n # @raise [StandardError] Various errors may be raised during setup,\\n # which are caught and logged as failure messages.\\n #\\n # @since 0.1.0\\n # @see TTY::Prompt\\n # @see #save_config\\n # @see #validate!\\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Configure logger settings interactively\n #\n # Provides an interactive interface for configuring logger settings including\n # console log level, file logging options, and log file paths. This method\n # is used both during initial setup and when editing specific configuration sections.\n #\n # @return [void]\n #\n # @example Configure logger independently\n # config.configure_logger\n # # Prompts for logger level, file logging, etc.\n #\n # @since 0.1.0\n # @see #configure_logger_interactive\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n # Configure filesystem path settings interactively\n #\n # Provides an interactive interface for configuring filesystem paths\n # used by BlueprintsCLI, such as temporary directories for editor operations.\n #\n # @return [void]\n #\n # @example Configure paths\n # config.configure_paths\n # # Prompts for temporary directory path\n #\n # @since 0.1.0\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n # Configure display and UI settings interactively\n #\n # Provides an interactive interface for configuring user interface settings\n # including colored output, interactive prompts, and pager configuration.\n #\n # @return [void]\n #\n # @example Configure display settings\n # config.configure_display\n # # Prompts for colors, interactive mode, pager choice\n #\n # @since 0.1.0\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n # Configure Restic backup settings interactively (placeholder)\n #\n # This method is a placeholder for future Restic backup configuration.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future backup configuration functionality.\n #\n # @since 0.1.0\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n # Configure terminal settings interactively (placeholder)\n #\n # This method is a placeholder for future terminal configuration options.\n # Currently displays a message indicating the feature is not implemented.\n #\n # @return [void]\n #\n # @note This method is not yet implemented and serves as a placeholder\n # for future terminal configuration functionality.\n #\n # @since 0.1.0\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file with force overwrite\n #\n # Convenience method that saves the current configuration state to file\n # with force overwrite enabled. This is typically used after interactive\n # setup or configuration changes to persist the updates.\n #\n # @return [Boolean] true if save was successful, false otherwise.\n #\n # @example Save configuration after changes\n # config.set(:logger, :level, value: 'debug')\n # config.save_config\n # # => true\n #\n # @since 0.1.0\n # @see #write\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability (placeholder)\n #\n # This method is a placeholder for validating that configured terminal\n # commands are available on the system. Currently returns true as a\n # basic implementation.\n #\n # @return [Boolean] Always returns true in current implementation.\n #\n # @note This is a placeholder implementation that could be extended\n # to check if configured terminal commands are available in PATH.\n #\n # @since 0.1.0\n # @api private\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n #\n # Prompts the user for database configuration including the database URL.\n # Uses existing configuration values as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n #\n # Prompts the user to select an AI provider and optionally configure\n # API keys. Supports Gemini, OpenAI, Anthropic, and DeepSeek providers.\n # API keys can be left empty to use environment variables.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n #\n # Prompts the user for logger configuration including console log level,\n # file logging options, file log level, and log file path.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n #\n # Prompts the user for editor configuration including default editor\n # selection and auto-save preferences. Uses EDITOR or VISUAL environment\n # variables as defaults when available.\n #\n # @param prompt [TTY::Prompt] The prompt instance for user interaction.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n #\n # Initializes the TTY::Config instance with configuration file paths,\n # environment variable mapping, and search paths. This method sets up\n # the foundational configuration system before loading any actual values.\n #\n # @param config_paths [Array] Additional paths to search for configuration files.\n # @param filename [String] Base filename for configuration files (without extension).\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n #\n # Performs the complete configuration loading process including reading\n # configuration files, setting default values, mapping environment variables,\n # and configuring external provider libraries.\n #\n # This method handles errors gracefully and will continue with defaults\n # if configuration files cannot be read.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n #\n # Establishes default values for all configuration sections using\n # TTY::Config's set_if_empty method. This ensures that the application\n # has sensible defaults even when no configuration file is present.\n #\n # Default values include:\n # - Database connection settings\n # - AI provider configurations\n # - Logger settings\n # - UI preferences\n # - Performance tuning parameters\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n #\n # Configures automatic mapping between environment variables and\n # configuration keys. This allows environment variables to override\n # configuration file values and provides flexible deployment options.\n #\n # Mappings include:\n # - Database URLs (BLUEPRINT_DATABASE_URL, DATABASE_URL)\n # - Editor preferences (EDITOR, VISUAL)\n # - Debug settings (DEBUG, BLUEPRINTS_DEBUG)\n # - AI provider API keys (OPENAI_API_KEY, GEMINI_API_KEY, etc.)\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n #\n # Establishes comprehensive validation rules for all configuration sections.\n # These rules are used by the validate! method to ensure configuration\n # integrity and provide meaningful error messages for invalid values.\n #\n # Validation rules include:\n # - Database URL format validation\n # - Boolean value validation for feature flags\n # - Numeric range validation for performance settings\n # - Enum validation for AI providers and log levels\n # - Required field validation\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints configuration section\n #\n # Validates the blueprints section of the configuration, particularly\n # focusing on database URL availability. Only validates when not using\n # default fallback URLs to avoid false positives.\n #\n # @return [void]\n #\n # @raise [ValidationError] if database URL is required but missing.\n #\n # @since 0.1.0\n # @api private\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI configuration section\n #\n # Validates AI provider configuration including provider name, model\n # specification, and API key availability. Issues warnings for missing\n # API keys rather than raising errors to allow for environment-specific\n # configuration.\n #\n # @return [void]\n #\n # @raise [ValidationError] if required AI configuration is missing.\n #\n # @since 0.1.0\n # @api private\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger configuration section\n #\n # Validates logger settings including log level values and file logging\n # configuration. Ensures that file paths are specified when file logging\n # is enabled.\n #\n # @return [void]\n #\n # @raise [ValidationError] if logger configuration is invalid.\n #\n # @since 0.1.0\n # @api private\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n #\n # Constructs a default log file path using XDG Base Directory specification.\n # Falls back to ~/.local/state if XDG_STATE_HOME is not set.\n #\n # @return [String] The default log file path.\n #\n # @example Default log path\n # config.send(:default_log_path)\n # # => \"/home/user/.local/state/BlueprintsCLI/app.log\"\n #\n # @since 0.1.0\n # @api private\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n #\n # Configures external AI provider libraries (RubyLLM and OpenAI gem)\n # using values from the unified configuration system. This ensures\n # that all provider libraries are properly initialized with the\n # correct API keys and settings.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n #\n # Initializes the RubyLLM library with API keys, model defaults,\n # connection settings, and logging configuration from the unified\n # configuration system. Handles errors gracefully to avoid blocking\n # application startup.\n #\n # Configuration includes:\n # - API keys for all supported providers\n # - Default models for text, embedding, and image generation\n # - Connection timeouts and retry settings\n # - Logging configuration\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n #\n # Configures the legacy OpenAI gem if it's available in the application.\n # This provides backward compatibility for code that uses the OpenAI gem\n # directly rather than through RubyLLM.\n #\n # Only configures the gem if it's actually loaded to avoid dependency\n # issues in environments where it's not needed.\n #\n # @return [void]\n #\n # @since 0.1.0\n # @api private\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\nend\n"}, "dc4aec47ba4078c81e321dbeee7cd4848a6b34f2": {"t": 1785756149.1030622, "n": 25341, "h": "b2278af76d1f9a80", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual components\n #\n # @return [String] Constructed database URL\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Failed to read configuration file: #{e.message}\"\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI section\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\"\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring RubyLLM: #{e.message}\"\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n # Can't use BlueprintsCLI.logger here as it may not be initialized yet\n warn \"Error configuring OpenAI gem: #{e.message}\"\n end\n end\nend\n"}, "c05f3ed8c9df60a688906653e13b6300db1aff9e": {"t": 1785756150.7967763, "n": 25125, "h": "bc2d5becccefde5c", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL'] ||\n build_database_url\n end\n\n # Build database URL from individual components\n #\n # @return [String] Constructed database URL\n def build_database_url\n host = ENV['DB_HOST'] || 'localhost'\n port = ENV['DB_PORT'] || '5432'\n user = ENV['DB_USER'] || 'postgres'\n password = ENV['DB_PASSWORD'] || 'blueprints'\n database = ENV['DB_NAME'] || 'blueprints'\n\n \"postgresql://#{user}:#{password}@#{host}:#{port}/#{database}\"\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n # Support both OpenRouter and direct OpenAI\n ENV['OPENROUTER_API_KEY'] || ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@localhost:5432/blueprints')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_key) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI section\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring OpenAI gem: #{e.message}\")\n end\n end\nend\n"}, "a97b68230ef6db15000ca29f261ec038fd174e1c": {"t": 1785756152.183709, "n": 24510, "h": "b96eb0cf05b325f1", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n unless errors.empty?\n raise ValidationError,\n \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL']\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai,\n :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?',\n default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@ninjabot:5433/blueprints_development')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError,\n \"#{key} must be a non-empty string\"\n end\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n unless value.is_a?(Integer) && value.positive?\n raise ValidationError,\n \"#{key} must be a positive integer\"\n end\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n unless value.is_a?(Integer) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative integer\"\n end\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n unless value.is_a?(Numeric) && value >= 0\n raise ValidationError,\n \"#{key} must be a non-negative number\"\n end\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI section\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring OpenAI gem: #{e.message}\")\n end\n end\nend\n"}, "ff5ac887c9a66fa9eb8e4217517a1ce1b15e82cf": {"t": 1785756153.6479619, "n": 24126, "h": "d4c75cd31602fcfd", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\n# Provider configurations are now handled by the unified config system\n# See BlueprintsCLI::Configuration#configure_providers method\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n raise ValidationError, \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\" unless errors.empty?\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL']\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:gemini_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai, :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n # Interactive configuration setup\n def interactive_setup\n require 'tty-prompt'\n prompt = TTY::Prompt.new\n\n puts 'šŸ”§ BlueprintsCLI Interactive Configuration Setup'\n puts '=' * 50\n\n # Database configuration\n configure_database_interactive(prompt)\n\n # AI provider configuration\n configure_ai_interactive(prompt)\n\n # Logger configuration\n configure_logger_interactive(prompt)\n\n # Editor configuration\n configure_editor_interactive(prompt)\n\n # Save configuration\n save_config\n\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error during interactive setup: #{e.message}\")\n false\n end\n\n # Individual configuration methods for edit command\n def configure_logger\n configure_logger_interactive(TTY::Prompt.new)\n end\n\n def configure_paths\n prompt = TTY::Prompt.new\n puts \"\\nšŸ“ Path Configuration\"\n\n temp_dir = prompt.ask('Temporary directory:', default: fetch(:editor, :temp_dir) || '/tmp')\n set(:editor, :temp_dir, value: temp_dir)\n end\n\n def configure_display\n prompt = TTY::Prompt.new\n puts \"\\nšŸŽØ Display Configuration\"\n\n colors = prompt.yes?('Enable colored output?', default: fetch(:ui, :colors, default: true))\n set(:ui, :colors, value: colors)\n\n interactive_mode = prompt.yes?('Enable interactive prompts?', default: fetch(:ui, :interactive, default: true))\n set(:ui, :interactive, value: interactive_mode)\n\n pager = prompt.ask('Pager command:', default: fetch(:ui, :pager) || 'most')\n set(:ui, :pager, value: pager)\n end\n\n def configure_restic\n # Placeholder for restic configuration if needed\n puts \"\\nšŸ’¾ Restic Configuration (not implemented yet)\"\n end\n\n def configure_terminals\n # Placeholder for terminal configuration if needed\n puts \"\\nšŸ–„ļø Terminal Configuration (not implemented yet)\"\n end\n\n # Save current configuration to file\n def save_config\n write(force: true)\n end\n\n private\n\n # Validate terminal command availability\n def validate_terminal_command\n # This is a placeholder validation - just return true for now\n # Could be extended to check if configured terminal commands are available\n true\n end\n\n # Configure database settings interactively\n def configure_database_interactive(prompt)\n puts \"\\nšŸ“Š Database Configuration\"\n\n current_url = fetch(:database, :url) || fetch(:blueprints, :database, :url)\n database_url = prompt.ask('Database URL:', default: current_url)\n\n set(:database, :url, value: database_url) if database_url\n end\n\n # Configure AI provider settings interactively\n def configure_ai_interactive(prompt)\n puts \"\\nšŸ¤– AI Provider Configuration\"\n\n provider = prompt.select('Select AI provider:', %w[Gemini OpenAI Anthropic DeepSeek])\n set(:ai, :sublayer, :provider, value: provider)\n\n case provider.downcase\n when 'gemini'\n api_key = prompt.mask('Gemini API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :gemini, value: api_key) unless api_key.empty?\n when 'openai'\n api_key = prompt.mask('OpenAI API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :openai, value: api_key) unless api_key.empty?\n when 'anthropic'\n api_key = prompt.mask('Anthropic API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :anthropic, value: api_key) unless api_key.empty?\n when 'deepseek'\n api_key = prompt.mask('DeepSeek API Key (leave empty to use environment variable):')\n set(:ai, :provider_keys, :deepseek, value: api_key) unless api_key.empty?\n end\n end\n\n # Configure logger settings interactively\n def configure_logger_interactive(prompt)\n puts \"\\nšŸ“ Logger Configuration\"\n\n level = prompt.select('Console log level:', %w[debug info warn error fatal])\n set(:logger, :level, value: level)\n\n file_logging = prompt.yes?('Enable file logging?')\n set(:logger, :file_logging, value: file_logging)\n\n return unless file_logging\n\n file_level = prompt.select('File log level:', %w[debug info warn error fatal])\n set(:logger, :file_level, value: file_level)\n\n file_path = prompt.ask('Log file path:', default: default_log_path)\n set(:logger, :file_path, value: file_path)\n end\n\n # Configure editor settings interactively\n def configure_editor_interactive(prompt)\n puts \"\\nāœļø Editor Configuration\"\n\n current_editor = fetch(:editor, :default) || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt.ask('Default editor:', default: current_editor)\n set(:editor, :default, value: editor)\n\n auto_save = prompt.yes?('Enable auto-save for edits?')\n set(:editor, :auto_save, value: auto_save)\n end\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n\n # Configure external providers\n configure_providers\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@ninjabot:5433/blueprints_development')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # RubyLLM defaults\n @config.set_if_empty(:ai, :rubyllm, :default_model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :rubyllm, :default_embedding_model, value: 'text-embedding-004')\n @config.set_if_empty(:ai, :rubyllm, :default_image_model, value: 'imagen-3.0-generate-002')\n @config.set_if_empty(:ai, :rubyllm, :request_timeout, value: 120)\n @config.set_if_empty(:ai, :rubyllm, :max_retries, value: 3)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :retry_backoff_factor, value: 2)\n @config.set_if_empty(:ai, :rubyllm, :retry_interval_randomness, value: 0.5)\n @config.set_if_empty(:ai, :rubyllm, :log_level, value: 'info')\n @config.set_if_empty(:ai, :rubyllm, :log_assume_model_exists, value: false)\n\n # OpenAI gem defaults\n @config.set_if_empty(:ai, :openai, :log_errors, value: true)\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n\n # AI Provider API keys\n @config.set_from_env(:ai, :provider_keys, :openai) { 'OPENAI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GEMINI_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :gemini) { 'GOOGLE_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :anthropic) { 'ANTHROPIC_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :deepseek) { 'DEEPSEEK_API_KEY' }\n @config.set_from_env(:ai, :provider_keys, :openai_access_token) { 'OPENAI_ACCESS_TOKEN' }\n @config.set_from_env(:ai, :provider_keys, :openai_base_uri) { 'OPENAI_BASE_URI' }\n @config.set_from_env(:ai, :rubyllm, :openai_api_base) { 'OPENAI_API_BASE' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n raise ValidationError, \"#{key} must be a non-empty string\" unless value.is_a?(String) && !value.empty?\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value.positive?\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value.positive?\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM timeout validation\n @config.validate(:ai, :rubyllm, :request_timeout) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value.positive?\n end\n\n # RubyLLM retry validation\n @config.validate(:ai, :rubyllm, :max_retries) do |key, value|\n raise ValidationError, \"#{key} must be a non-negative integer\" unless value.is_a?(Integer) && value >= 0\n end\n\n # RubyLLM retry interval validation\n @config.validate(:ai, :rubyllm, :retry_interval) do |key, value|\n raise ValidationError, \"#{key} must be a non-negative number\" unless value.is_a?(Numeric) && value >= 0\n end\n\n # RubyLLM log level validation\n @config.validate(:ai, :rubyllm, :log_level) do |key, value|\n valid_levels = %w[debug info warn]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n\n # RubyLLM boolean validation\n @config.validate(:ai, :rubyllm, :log_assume_model_exists) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI section\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n\n # Configure external provider libraries based on unified configuration\n def configure_providers\n configure_rubyllm\n configure_openai_gem\n end\n\n # Configure RubyLLM with settings from unified config system\n def configure_rubyllm\n RubyLLM.configure do |config|\n # API Keys - use the existing ai_api_key method that checks environment variables\n config.openai_api_key = ai_api_key(:openai)\n config.gemini_api_key = ai_api_key(:gemini)\n config.anthropic_api_key = ai_api_key(:anthropic)\n config.deepseek_api_key = ai_api_key(:deepseek)\n\n # Custom endpoint\n config.openai_api_base = fetch(:ai, :rubyllm, :openai_api_base)\n\n # Default models\n config.default_model = fetch(:ai, :rubyllm, :default_model)\n config.default_embedding_model = fetch(:ai, :rubyllm, :default_embedding_model)\n config.default_image_model = fetch(:ai, :rubyllm, :default_image_model)\n\n # Connection settings\n config.request_timeout = fetch(:ai, :rubyllm, :request_timeout)\n config.max_retries = fetch(:ai, :rubyllm, :max_retries)\n config.retry_interval = fetch(:ai, :rubyllm, :retry_interval)\n config.retry_backoff_factor = fetch(:ai, :rubyllm, :retry_backoff_factor)\n config.retry_interval_randomness = fetch(:ai, :rubyllm, :retry_interval_randomness)\n\n # Logging settings\n log_file = fetch(:ai, :rubyllm, :log_file)\n config.log_file = log_file unless log_file.nil?\n config.log_level = fetch(:ai, :rubyllm, :log_level)&.to_sym || :info\n config.log_assume_model_exists = fetch(:ai, :rubyllm, :log_assume_model_exists)\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring RubyLLM: #{e.message}\")\n end\n\n # Configure legacy OpenAI gem with settings from unified config system\n def configure_openai_gem\n return unless defined?(OpenAI)\n\n OpenAI.configure do |config|\n # Use environment variables directly for OpenAI gem\n access_token = ENV['OPENAI_ACCESS_TOKEN'] || ai_api_key(:openai)\n base_uri = ENV['OPENAI_BASE_URI']\n\n config.access_token = access_token if access_token\n config.uri_base = base_uri if base_uri\n config.log_errors = fetch(:ai, :openai, :log_errors, default: true)\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error configuring OpenAI gem: #{e.message}\")\n end\n end\nend\n"}, "9107c4ab514d6740c44dbd3fda82000a777a1501": {"t": 1785756154.9334219, "n": 14627, "h": "d3c8ef2481e14f21", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\nrequire 'ruby_llm'\n\nOpenAI.configure do |config|\n config.access_token = ENV.fetch('OPENAI_ACCESS_TOKEN')\n config.uri_base = ENV.fetch('OPENAI_BASE_URI')\n config.log_errors = true # Highly recommended in development, so you can see what errors OpenAI is returning. Not recommended in production because it could leak private data to your logs.\nend\n\nRubyLLM.configure do |config|\n config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)\n config.gemini_api_key = ENV.fetch('GEMINI_API_KEY', nil)\n config.openai_api_base = ENV.fetch('OPENAI_API_BASE', nil)\n config.default_model = 'gemini-2.0-flash'\n config.default_embedding_model = 'text-embedding-004'\n config.default_image_model = 'imagen-3.0-generate-002'\n config.request_timeout = 120\n config.max_retries = 3\n config.retry_interval = 0.5\n config.retry_backoff_factor = 2\n config.retry_interval_randomness = 0.5\nend\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n raise ValidationError, \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\" unless errors.empty?\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL']\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n else\n nil\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:google_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai, :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n private\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@ninjabot:5433/blueprints_development')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n raise ValidationError, \"#{key} must be a non-empty string\" unless value.is_a?(String) && !value.empty?\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value > 0\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value > 0\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI section\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n end\nend\n"}, "9432b439d0a5ec790e50fc9d5741a9ffc8d857f3": {"t": 1785756156.247596, "n": 13745, "h": "6798d408d4f0a311", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n #\n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration\n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n\n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n\n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n\n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n\n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # NOTE: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n\n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n\n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n\n raise ValidationError, \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\" unless errors.empty?\n\n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL']\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n else\n nil\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash\n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n\n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:google_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n\n # Add custom API base if configured\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base) if fetch(:ai, :ruby_llm, :openai_api_base)\n\n config_hash\n end\n\n private\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n\n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n\n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n\n # Set default values\n set_defaults\n\n # Map common environment variables\n setup_env_mappings\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url,\n value: 'postgresql://postgres:blueprints@ninjabot:5433/blueprints_development')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n\n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI')\n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n\n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n\n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n\n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n raise ValidationError, \"#{key} must be a non-empty string\" unless value.is_a?(String) && !value.empty?\n\n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n\n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n raise ValidationError, \"#{key} must be true or false\" unless [true, false].include?(value)\n end\n\n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value > 0\n end\n\n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n raise ValidationError, \"#{key} must be a positive integer\" unless value.is_a?(Integer) && value > 0\n end\n\n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n\n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n return unless database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n\n raise ValidationError, 'Database URL is required'\n end\n\n # Validate AI section\n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n\n raise ValidationError, 'AI provider is required' if provider.nil? || provider.empty?\n\n raise ValidationError, 'AI model is required' if model.nil? || model.empty?\n\n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n return unless api_key.nil? || api_key.empty?\n\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n\n return unless fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n\n raise ValidationError, 'Logger file path is required when file logging is enabled'\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n end\nend\n"}, "9c1cd3923b2e207e3b4f366b825cb569aa0f759d": {"t": 1785756157.8258362, "n": 14064, "h": "890bbd90254bbd10", "c": "# frozen_string_literal: true\n\nrequire 'tty-config'\nrequire 'fileutils'\n\nmodule BlueprintsCLI\n # Unified configuration management using TTY::Config\n # \n # Handles configuration for:\n # - BlueprintsCLI application settings\n # - Sublayer AI provider configuration \n # - Ruby LLM provider settings\n # - Logger configuration\n #\n # @example Basic usage\n # config = BlueprintsCLI::Configuration.new\n # config.fetch(:blueprints, :database, :url)\n #\n # @example Environment variables\n # ENV['BLUEPRINTS_DATABASE_URL'] = 'postgres://...'\n # config.fetch(:blueprints, :database, :url) # Returns env var value\n class Configuration\n # Error raised when configuration validation fails\n ValidationError = Class.new(StandardError)\n \n # Error raised when required configuration is missing\n MissingConfigError = Class.new(StandardError)\n\n # Default configuration file name\n DEFAULT_FILENAME = 'config'\n \n # Default configuration file extension\n DEFAULT_EXTENSION = '.yml'\n \n # Environment variable prefix for auto-mapping\n ENV_PREFIX = 'BLUEPRINTS'\n \n # The TTY::Config instance\n attr_reader :config\n\n # Initialize configuration with optional custom paths\n #\n # @param config_paths [Array] Additional paths to search for config files\n # @param filename [String] Configuration filename (without extension)\n # @param auto_load [Boolean] Whether to automatically load configuration files\n def initialize(config_paths: [], filename: DEFAULT_FILENAME, auto_load: true)\n @config = TTY::Config.new\n setup_config(config_paths, filename)\n load_configuration if auto_load\n setup_validations\n end\n\n # Fetch a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to fetch\n # @param default [Object] Default value if key not found\n # @return [Object] The configuration value\n #\n # @example\n # config.fetch(:blueprints, :database, :url)\n # config.fetch(:ai, :sublayer, :provider, default: 'gemini')\n def fetch(*keys, default: nil)\n @config.fetch(*keys, default: default)\n end\n\n # Set a configuration value using nested keys\n #\n # @param keys [Array] Nested keys to set\n # @param value [Object] Value to set\n # @return [Object] The set value\n #\n # @example\n # config.set(:blueprints, :database, :url, value: 'postgres://...')\n def set(*keys, value:)\n @config.set(*keys, value: value)\n # Note: We don't validate on set because it may cause issues during initial setup\n value\n end\n\n # Check if configuration file exists\n #\n # @return [Boolean] True if config file exists\n def exist?\n @config.exist?\n end\n\n # Write current configuration to file\n #\n # @param force [Boolean] Whether to overwrite existing file\n # @param create [Boolean] Whether to create missing directories\n # @return [Boolean] True if write succeeded\n def write(force: false, create: true)\n @config.write(force: force, create: create)\n true\n rescue TTY::Config::WriteError => e\n BlueprintsCLI.logger.failure(\"Failed to write configuration: #{e.message}\")\n false\n end\n\n # Reload configuration from file and environment\n #\n # @return [self]\n def reload!\n @config = TTY::Config.new\n setup_config([], DEFAULT_FILENAME)\n load_configuration\n setup_validations\n self\n end\n\n # Get configuration file path\n #\n # @return [String, nil] Path to configuration file or nil if not found\n def config_file_path\n @config.source_file\n end\n\n # Convert configuration to hash\n #\n # @return [Hash] Configuration as hash\n def to_hash\n @config.to_hash\n end\n alias to_h to_hash\n\n # Validate entire configuration\n #\n # @return [Array] Array of validation error messages (empty if valid)\n def validate!\n errors = []\n \n begin\n validate_blueprints!\n rescue ValidationError => e\n errors << e.message\n end\n \n begin \n validate_ai!\n rescue ValidationError => e\n errors << e.message\n end\n \n begin\n validate_logger!\n rescue ValidationError => e\n errors << e.message\n end\n \n unless errors.empty?\n raise ValidationError, \"Configuration validation failed:\\n#{errors.join(\"\\n\")}\"\n end\n \n true\n end\n\n # Check if configuration is valid\n #\n # @return [Boolean] True if configuration is valid\n def valid?\n validate!\n true\n rescue ValidationError\n false\n end\n\n # Get database URL with environment variable fallback\n #\n # @return [String, nil] Database URL\n def database_url\n fetch(:blueprints, :database, :url) ||\n ENV['BLUEPRINT_DATABASE_URL'] ||\n ENV['DATABASE_URL']\n end\n\n # Get AI provider API key for given provider\n #\n # @param provider [String, Symbol] AI provider name\n # @return [String, nil] API key\n def ai_api_key(provider)\n case provider.to_s.downcase\n when 'gemini', 'google'\n ENV['GEMINI_API_KEY'] || ENV['GOOGLE_API_KEY']\n when 'openai'\n ENV['OPENAI_API_KEY']\n when 'anthropic'\n ENV['ANTHROPIC_API_KEY']\n when 'deepseek'\n ENV['DEEPSEEK_API_KEY']\n else\n nil\n end\n end\n\n # Get Sublayer configuration hash\n #\n # @return [Hash] Sublayer configuration\n def sublayer_config\n {\n project_name: fetch(:ai, :sublayer, :project_name, default: 'blueprintsCLI'),\n project_template: fetch(:ai, :sublayer, :project_template, default: 'CLI'),\n ai_provider: fetch(:ai, :sublayer, :provider, default: 'Gemini'),\n ai_model: fetch(:ai, :sublayer, :model, default: 'gemini-2.0-flash')\n }\n end\n\n # Get Ruby LLM configuration hash \n #\n # @return [Hash] Ruby LLM configuration\n def ruby_llm_config\n config_hash = {}\n \n # Add API keys that are available\n config_hash[:openai_api_key] = ai_api_key(:openai) if ai_api_key(:openai)\n config_hash[:anthropic_api_key] = ai_api_key(:anthropic) if ai_api_key(:anthropic)\n config_hash[:google_api_key] = ai_api_key(:gemini) if ai_api_key(:gemini)\n config_hash[:deepseek_api_key] = ai_api_key(:deepseek) if ai_api_key(:deepseek)\n \n # Add custom API base if configured\n if fetch(:ai, :ruby_llm, :openai_api_base)\n config_hash[:openai_api_base] = fetch(:ai, :ruby_llm, :openai_api_base)\n end\n \n config_hash\n end\n\n private\n\n # Setup TTY::Config with paths and environment mapping\n def setup_config(config_paths, filename)\n @config.filename = filename\n @config.extname = DEFAULT_EXTENSION\n @config.env_prefix = ENV_PREFIX\n @config.env_separator = '_'\n @config.autoload_env\n \n # Add default search paths\n default_paths = [\n File.join(Dir.home, '.config', 'BlueprintsCLI'),\n File.join(Dir.home, '.blueprintsCLI'),\n File.join(__dir__, 'config'),\n Dir.pwd\n ]\n \n (default_paths + config_paths).each do |path|\n @config.append_path(path) if Dir.exist?(path)\n end\n end\n\n # Load configuration from file and set defaults\n def load_configuration\n # Try to read existing configuration file\n begin\n @config.read if @config.exist?\n rescue TTY::Config::ReadError => e\n BlueprintsCLI.logger.warn(\"Failed to read configuration file: #{e.message}\")\n end\n \n # Set default values\n set_defaults\n \n # Map common environment variables\n setup_env_mappings\n end\n\n # Set default configuration values\n def set_defaults\n # Blueprints defaults\n @config.set_if_empty(:blueprints, :database, :url, value: 'postgres://localhost/blueprints_development')\n @config.set_if_empty(:blueprints, :features, :auto_description, value: true)\n @config.set_if_empty(:blueprints, :features, :auto_categorize, value: true)\n @config.set_if_empty(:blueprints, :features, :improvement_analysis, value: true)\n @config.set_if_empty(:blueprints, :editor, value: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n @config.set_if_empty(:blueprints, :auto_save_edits, value: false)\n @config.set_if_empty(:blueprints, :search, :default_limit, value: 10)\n @config.set_if_empty(:blueprints, :search, :semantic_search, value: true)\n @config.set_if_empty(:blueprints, :export, :include_metadata, value: false)\n @config.set_if_empty(:blueprints, :export, :auto_detect_extension, value: true)\n @config.set_if_empty(:blueprints, :performance, :batch_size, value: 100)\n @config.set_if_empty(:blueprints, :performance, :connection_pool_size, value: 5)\n @config.set_if_empty(:blueprints, :ui, :colors, value: true)\n @config.set_if_empty(:blueprints, :ui, :interactive, value: true)\n @config.set_if_empty(:blueprints, :ui, :pager, value: true)\n \n # AI defaults\n @config.set_if_empty(:ai, :sublayer, :project_name, value: 'blueprintsCLI')\n @config.set_if_empty(:ai, :sublayer, :project_template, value: 'CLI') \n @config.set_if_empty(:ai, :sublayer, :provider, value: 'Gemini')\n @config.set_if_empty(:ai, :sublayer, :model, value: 'gemini-2.0-flash')\n @config.set_if_empty(:ai, :embedding_model, value: 'text-embedding-004')\n \n # Logger defaults\n @config.set_if_empty(:logger, :level, value: 'info')\n @config.set_if_empty(:logger, :file_logging, value: false)\n @config.set_if_empty(:logger, :file_level, value: 'debug')\n @config.set_if_empty(:logger, :file_path, value: default_log_path)\n end\n\n # Setup environment variable mappings\n def setup_env_mappings\n # Database\n @config.set_from_env(:blueprints, :database, :url) { 'BLUEPRINT_DATABASE_URL' }\n @config.set_from_env(:blueprints, :database, :url) { 'DATABASE_URL' }\n \n # Editor\n @config.set_from_env(:blueprints, :editor) { 'EDITOR' }\n @config.set_from_env(:blueprints, :editor) { 'VISUAL' }\n \n # Debug mode\n @config.set_from_env(:blueprints, :debug) { 'DEBUG' }\n @config.set_from_env(:blueprints, :debug) { 'BLUEPRINTS_DEBUG' }\n end\n\n # Setup validation rules\n def setup_validations\n # Database URL validation\n @config.validate(:blueprints, :database, :url) do |key, value|\n unless value.is_a?(String) && !value.empty?\n raise ValidationError, \"#{key} must be a non-empty string\"\n end\n \n unless value.start_with?('postgres://') || value.start_with?('postgresql://')\n raise ValidationError, \"#{key} must be a PostgreSQL URL (postgres:// or postgresql://)\"\n end\n end\n \n # Feature flags validation\n @config.validate(:blueprints, :features, :auto_description) do |key, value|\n unless [true, false].include?(value)\n raise ValidationError, \"#{key} must be true or false\"\n end\n end\n \n @config.validate(:blueprints, :features, :auto_categorize) do |key, value|\n unless [true, false].include?(value)\n raise ValidationError, \"#{key} must be true or false\"\n end\n end\n \n # Numeric validations\n @config.validate(:blueprints, :search, :default_limit) do |key, value|\n unless value.is_a?(Integer) && value > 0\n raise ValidationError, \"#{key} must be a positive integer\"\n end\n end\n \n @config.validate(:blueprints, :performance, :batch_size) do |key, value|\n unless value.is_a?(Integer) && value > 0\n raise ValidationError, \"#{key} must be a positive integer\"\n end\n end\n \n # AI provider validation\n @config.validate(:ai, :sublayer, :provider) do |key, value|\n valid_providers = %w[Gemini OpenAI Anthropic DeepSeek]\n unless valid_providers.include?(value)\n raise ValidationError, \"#{key} must be one of: #{valid_providers.join(', ')}\"\n end\n end\n \n # Logger level validation\n @config.validate(:logger, :level) do |key, value|\n valid_levels = %w[debug info warn error fatal]\n unless valid_levels.include?(value.to_s.downcase)\n raise ValidationError, \"#{key} must be one of: #{valid_levels.join(', ')}\"\n end\n end\n end\n\n # Validate blueprints section\n def validate_blueprints!\n database_url = fetch(:blueprints, :database, :url)\n # Only validate if we're not using the default fallback\n if database_url.nil? || (database_url.empty? && !ENV['BLUEPRINT_DATABASE_URL'] && !ENV['DATABASE_URL'])\n raise ValidationError, \"Database URL is required\"\n end\n end\n\n # Validate AI section \n def validate_ai!\n provider = fetch(:ai, :sublayer, :provider)\n model = fetch(:ai, :sublayer, :model)\n \n if provider.nil? || provider.empty?\n raise ValidationError, \"AI provider is required\"\n end\n \n if model.nil? || model.empty?\n raise ValidationError, \"AI model is required\"\n end\n \n # Check if API key is available for the provider\n api_key = ai_api_key(provider)\n if api_key.nil? || api_key.empty?\n BlueprintsCLI.logger.warn(\"No API key found for AI provider '#{provider}'. Set the appropriate environment variable.\")\n end\n end\n\n # Validate logger section\n def validate_logger!\n level = fetch(:logger, :level)\n if level && !%w[debug info warn error fatal].include?(level.to_s.downcase)\n raise ValidationError, \"Invalid logger level: #{level}\"\n end\n \n if fetch(:logger, :file_logging) && fetch(:logger, :file_path).nil?\n raise ValidationError, \"Logger file path is required when file logging is enabled\"\n end\n end\n\n # Get default log file path\n def default_log_path\n state_home = ENV['XDG_STATE_HOME'] || File.expand_path('~/.local/state')\n File.join(state_home, 'BlueprintsCLI', 'app.log')\n end\n end\nend"}}, "lib/blueprintsCLI/actions/config.rb": {"64a20b367209eac0d73cc0df9909e85d962ad6d5": {"t": 1785745750.282235, "n": 22550, "h": "a428ecca02be0510", "c": "# frozen_string_literal: true\n\nrequire_relative \"../configuration\"\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: \"show\")\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when \"show\", \"view\"\n show_configuration\n when \"setup\", \"init\", \"edit\"\n setup_configuration\n when \"test\", \"validate\"\n test_configuration\n when \"reset\"\n reset_configuration\n when \"migrate\"\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private def show_configuration\n BlueprintsCLI.logger.step(\"Blueprint Configuration\")\n puts \"=\" * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts \"\"\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts \"Database Configuration:\".colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts \"\"\n\n # AI Configuration\n puts \"AI Configuration:\".colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :model, default: 'Not set')}\"\n ai_provider = @config.fetch(:ai, :provider, default: \"\").downcase\n api_key_status = @config.ai_api_key(ai_provider) ? \"Set\" : \"Not set\"\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts \"\"\n\n # AI Configuration - Ruby LLM\n puts \"AI Configuration (Ruby LLM):\".colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?(\"_api_key\")\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select do |k|\n k.to_s.end_with?('_api_key')\n end.join(', ')}\"\n else\n puts \" No Ruby LLM configuration found\"\n end\n puts \"\"\n\n # Editor Configuration\n puts \"Editor Configuration:\".colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts \"\"\n\n # Feature Flags\n puts \"Feature Flags:\".colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description,\n default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize,\n default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts \"\"\n\n # Logger Configuration\n puts \"Logger Configuration:\".colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts \"\"\n\n # Search Configuration\n puts \"Search Configuration:\".colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit,\n default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search,\n default: 'Not set')}\"\n puts \"\"\n\n # Performance Configuration\n puts \"Performance Configuration:\".colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size,\n default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure(\"No configuration found\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts \"=\" * 60\n puts \"\"\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n private def setup_configuration\n BlueprintsCLI.logger.step(\"Blueprint Configuration Setup\")\n puts \"=\" * 50\n puts \"\"\n\n config = load_configuration || {}\n\n # Database configuration\n puts \"šŸ“Š Database Configuration\".colorize(:cyan)\n current_db = config.dig(\"database\", \"url\") || BlueprintsCLI.configuration.database_url\n db_url = prompt_for_input(\"Database URL\", current_db)\n\n config[\"database\"] = { \"url\" => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig(\"ai\", \"provider\") || \"gemini\"\n provider = prompt_for_choice(\"AI Provider\", %w[gemini openai], current_provider)\n\n current_model = config.dig(\"ai\",\n \"model\") || ((provider == \"gemini\") ? \"text-embedding-004\" : \"text-embedding-3-small\")\n model = prompt_for_input(\"AI Model\", current_model)\n\n puts \"šŸ’” Set API key via environment variable:\".colorize(:yellow)\n puts \" export GEMINI_API_KEY=your_key_here\" if provider == \"gemini\"\n puts \" export OPENAI_API_KEY=your_key_here\" if provider == \"openai\"\n\n config[\"ai\"] = {\n \"provider\" => provider,\n \"model\" => model,\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor,\n default: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n editor = prompt_for_input(\"Preferred editor\", current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean(\"Auto-save edits\", current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean(\"Auto-generate descriptions\", current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean(\"Auto-generate categories\", current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis,\n default: true)\n improvement = prompt_for_boolean(\"Enable AI improvement analysis\", current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: \"info\")\n log_level = prompt_for_choice(\"Log level\", %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean(\"Enable file logging\", current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path,\n default: @config.send(:default_log_path))\n file_path = prompt_for_input(\"Log file path\", current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success(\"Configuration saved successfully!\",\n file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure(\"Failed to save configuration\")\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n private def test_configuration\n BlueprintsCLI.logger.step(\"Testing Blueprint Configuration\")\n puts \"=\" * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success(\"Configuration structure is valid\")\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n#{'=' * 50}\"\n if all_tests_passed\n BlueprintsCLI.logger.success(\"All configuration tests passed!\")\n else\n BlueprintsCLI.logger.failure(\"Some configuration tests failed\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n private def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print \"āš ļø This will delete the existing configuration. Continue? (y/N): \"\n response = $stdin.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success(\"Configuration reset successfully\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn(\"Reset cancelled\")\n false\n end\n else\n BlueprintsCLI.logger.info(\"No configuration file found to reset\")\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n private def migrate_configuration\n BlueprintsCLI.logger.step(\"Migrating Configuration Files\")\n puts \"=\" * 50\n puts \"\"\n\n puts \"ā„¹ļø Migration is no longer needed - using unified configuration system\"\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n private def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n private def show_environment_variables\n puts \"šŸŒ Environment Variables:\".colorize(:blue)\n\n env_vars = {\n \"GEMINI_API_KEY\" => ENV.fetch(\"GEMINI_API_KEY\", nil),\n \"GOOGLE_API_KEY\" => ENV.fetch(\"GOOGLE_API_KEY\", nil),\n \"OPENAI_API_KEY\" => ENV.fetch(\"OPENAI_API_KEY\", nil),\n \"ANTHROPIC_API_KEY\" => ENV.fetch(\"ANTHROPIC_API_KEY\", nil),\n \"DEEPSEEK_API_KEY\" => ENV.fetch(\"DEEPSEEK_API_KEY\", nil),\n \"BLUEPRINT_DATABASE_URL\" => ENV.fetch(\"BLUEPRINT_DATABASE_URL\", nil),\n \"DATABASE_URL\" => ENV.fetch(\"DATABASE_URL\", nil),\n \"BLUEPRINTS_DEBUG\" => ENV.fetch(\"BLUEPRINTS_DEBUG\", nil),\n \"DEBUG\" => ENV.fetch(\"DEBUG\", nil),\n \"EDITOR\" => ENV.fetch(\"EDITOR\", nil),\n \"VISUAL\" => ENV.fetch(\"VISUAL\", nil),\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?(\"KEY\") ? \"Set (hidden)\" : value\n else\n \"Not set\"\n end\n puts \" #{key}: #{status}\"\n end\n puts \"\"\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n private def test_database_connection\n require \"sequel\"\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure(\"No database URL configured\")\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success(\"Database connection successful\")\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests AI provider connections by checking for configured API keys.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n private def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test configured AI provider\n ai_provider = @config.fetch(:ai, :provider, default: \"\").downcase\n unless ai_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(ai_provider)\n if api_key\n BlueprintsCLI.logger.success(\"AI API key found for #{ai_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"AI API key not found for #{ai_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?(\"_api_key\") && v }\n\n if api_keys.any?\n api_keys.each_key do |key|\n provider = key.to_s.gsub(\"_api_key\", \"\")\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn(\"No Ruby LLM API keys configured\")\n end\n\n if total_tests.zero?\n BlueprintsCLI.logger.warn(\"No AI providers configured\")\n false\n else\n success_count.positive?\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n private def test_editor\n editor = @config.fetch(:blueprints, :editor, default: \"vim\")\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure(\"No editor configured\")\n return false\n end\n\n if TTY::Which.exist?(editor)\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n private def test_logger_config\n level = @config.fetch(:logger, :level, default: \"info\")\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure(\"File logging enabled but no file path configured\")\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success(\"Logger configuration is valid\")\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n private def prompt_for_input(prompt, default = nil)\n print prompt\n print \" [#{default}]\" if default\n print \": \"\n\n input = $stdin.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n private def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : \": \"\n\n input = $stdin.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n private def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then \" [Y/n]\"\n when false then \" [y/N]\"\n else \" [y/n]\"\n end\n\n print \"#{prompt}#{default_text}: \"\n input = $stdin.gets.chomp.downcase\n\n case input\n when \"y\", \"yes\", \"true\"\n true\n when \"n\", \"no\", \"false\"\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n private def mask_password(url)\n return url unless url.include?(\":\") && url.include?(\"@\")\n\n url.gsub(/:[^:@]*@/, \":***@\")\n end\n end\n end\nend\n"}, "ac09a99eaa88c83e498c8742e3bcda6ef52c2a0d": {"t": 1785756130.617982, "n": 22550, "h": "a428ecca02be0510", "c": "# frozen_string_literal: true\n\nrequire_relative \"../configuration\"\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: \"show\")\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when \"show\", \"view\"\n show_configuration\n when \"setup\", \"init\", \"edit\"\n setup_configuration\n when \"test\", \"validate\"\n test_configuration\n when \"reset\"\n reset_configuration\n when \"migrate\"\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private def show_configuration\n BlueprintsCLI.logger.step(\"Blueprint Configuration\")\n puts \"=\" * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts \"\"\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts \"Database Configuration:\".colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts \"\"\n\n # AI Configuration\n puts \"AI Configuration:\".colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :model, default: 'Not set')}\"\n ai_provider = @config.fetch(:ai, :provider, default: \"\").downcase\n api_key_status = @config.ai_api_key(ai_provider) ? \"Set\" : \"Not set\"\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts \"\"\n\n # AI Configuration - Ruby LLM\n puts \"AI Configuration (Ruby LLM):\".colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?(\"_api_key\")\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select do |k|\n k.to_s.end_with?('_api_key')\n end.join(', ')}\"\n else\n puts \" No Ruby LLM configuration found\"\n end\n puts \"\"\n\n # Editor Configuration\n puts \"Editor Configuration:\".colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts \"\"\n\n # Feature Flags\n puts \"Feature Flags:\".colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description,\n default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize,\n default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts \"\"\n\n # Logger Configuration\n puts \"Logger Configuration:\".colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts \"\"\n\n # Search Configuration\n puts \"Search Configuration:\".colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit,\n default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search,\n default: 'Not set')}\"\n puts \"\"\n\n # Performance Configuration\n puts \"Performance Configuration:\".colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size,\n default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure(\"No configuration found\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts \"=\" * 60\n puts \"\"\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n private def setup_configuration\n BlueprintsCLI.logger.step(\"Blueprint Configuration Setup\")\n puts \"=\" * 50\n puts \"\"\n\n config = load_configuration || {}\n\n # Database configuration\n puts \"šŸ“Š Database Configuration\".colorize(:cyan)\n current_db = config.dig(\"database\", \"url\") || BlueprintsCLI.configuration.database_url\n db_url = prompt_for_input(\"Database URL\", current_db)\n\n config[\"database\"] = { \"url\" => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig(\"ai\", \"provider\") || \"gemini\"\n provider = prompt_for_choice(\"AI Provider\", %w[gemini openai], current_provider)\n\n current_model = config.dig(\"ai\",\n \"model\") || ((provider == \"gemini\") ? \"text-embedding-004\" : \"text-embedding-3-small\")\n model = prompt_for_input(\"AI Model\", current_model)\n\n puts \"šŸ’” Set API key via environment variable:\".colorize(:yellow)\n puts \" export GEMINI_API_KEY=your_key_here\" if provider == \"gemini\"\n puts \" export OPENAI_API_KEY=your_key_here\" if provider == \"openai\"\n\n config[\"ai\"] = {\n \"provider\" => provider,\n \"model\" => model,\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor,\n default: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n editor = prompt_for_input(\"Preferred editor\", current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean(\"Auto-save edits\", current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean(\"Auto-generate descriptions\", current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean(\"Auto-generate categories\", current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis,\n default: true)\n improvement = prompt_for_boolean(\"Enable AI improvement analysis\", current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: \"info\")\n log_level = prompt_for_choice(\"Log level\", %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean(\"Enable file logging\", current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path,\n default: @config.send(:default_log_path))\n file_path = prompt_for_input(\"Log file path\", current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success(\"Configuration saved successfully!\",\n file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure(\"Failed to save configuration\")\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n private def test_configuration\n BlueprintsCLI.logger.step(\"Testing Blueprint Configuration\")\n puts \"=\" * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success(\"Configuration structure is valid\")\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n#{'=' * 50}\"\n if all_tests_passed\n BlueprintsCLI.logger.success(\"All configuration tests passed!\")\n else\n BlueprintsCLI.logger.failure(\"Some configuration tests failed\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n private def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print \"āš ļø This will delete the existing configuration. Continue? (y/N): \"\n response = $stdin.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success(\"Configuration reset successfully\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn(\"Reset cancelled\")\n false\n end\n else\n BlueprintsCLI.logger.info(\"No configuration file found to reset\")\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n private def migrate_configuration\n BlueprintsCLI.logger.step(\"Migrating Configuration Files\")\n puts \"=\" * 50\n puts \"\"\n\n puts \"ā„¹ļø Migration is no longer needed - using unified configuration system\"\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n private def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n private def show_environment_variables\n puts \"šŸŒ Environment Variables:\".colorize(:blue)\n\n env_vars = {\n \"GEMINI_API_KEY\" => ENV.fetch(\"GEMINI_API_KEY\", nil),\n \"GOOGLE_API_KEY\" => ENV.fetch(\"GOOGLE_API_KEY\", nil),\n \"OPENAI_API_KEY\" => ENV.fetch(\"OPENAI_API_KEY\", nil),\n \"ANTHROPIC_API_KEY\" => ENV.fetch(\"ANTHROPIC_API_KEY\", nil),\n \"DEEPSEEK_API_KEY\" => ENV.fetch(\"DEEPSEEK_API_KEY\", nil),\n \"BLUEPRINT_DATABASE_URL\" => ENV.fetch(\"BLUEPRINT_DATABASE_URL\", nil),\n \"DATABASE_URL\" => ENV.fetch(\"DATABASE_URL\", nil),\n \"BLUEPRINTS_DEBUG\" => ENV.fetch(\"BLUEPRINTS_DEBUG\", nil),\n \"DEBUG\" => ENV.fetch(\"DEBUG\", nil),\n \"EDITOR\" => ENV.fetch(\"EDITOR\", nil),\n \"VISUAL\" => ENV.fetch(\"VISUAL\", nil),\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?(\"KEY\") ? \"Set (hidden)\" : value\n else\n \"Not set\"\n end\n puts \" #{key}: #{status}\"\n end\n puts \"\"\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n private def test_database_connection\n require \"sequel\"\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure(\"No database URL configured\")\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success(\"Database connection successful\")\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests AI provider connections by checking for configured API keys.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n private def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test configured AI provider\n ai_provider = @config.fetch(:ai, :provider, default: \"\").downcase\n unless ai_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(ai_provider)\n if api_key\n BlueprintsCLI.logger.success(\"AI API key found for #{ai_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"AI API key not found for #{ai_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?(\"_api_key\") && v }\n\n if api_keys.any?\n api_keys.each_key do |key|\n provider = key.to_s.gsub(\"_api_key\", \"\")\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn(\"No Ruby LLM API keys configured\")\n end\n\n if total_tests.zero?\n BlueprintsCLI.logger.warn(\"No AI providers configured\")\n false\n else\n success_count.positive?\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n private def test_editor\n editor = @config.fetch(:blueprints, :editor, default: \"vim\")\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure(\"No editor configured\")\n return false\n end\n\n if TTY::Which.exist?(editor)\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n private def test_logger_config\n level = @config.fetch(:logger, :level, default: \"info\")\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure(\"File logging enabled but no file path configured\")\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success(\"Logger configuration is valid\")\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n private def prompt_for_input(prompt, default = nil)\n print prompt\n print \" [#{default}]\" if default\n print \": \"\n\n input = $stdin.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n private def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : \": \"\n\n input = $stdin.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n private def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then \" [Y/n]\"\n when false then \" [y/N]\"\n else \" [y/n]\"\n end\n\n print \"#{prompt}#{default_text}: \"\n input = $stdin.gets.chomp.downcase\n\n case input\n when \"y\", \"yes\", \"true\"\n true\n when \"n\", \"no\", \"false\"\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n private def mask_password(url)\n return url unless url.include?(\":\") && url.include?(\"@\")\n\n url.gsub(/:[^:@]*@/, \":***@\")\n end\n end\n end\nend\n"}, "48eef306e5e764c0ea0da7069ba82b107409bba0": {"t": 1785756131.9784443, "n": 22611, "h": "74dbb4b82ac91a21", "c": "# frozen_string_literal: true\n\nrequire_relative \"../configuration\"\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: \"show\")\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when \"show\", \"view\"\n show_configuration\n when \"setup\", \"init\", \"edit\"\n setup_configuration\n when \"test\", \"validate\"\n test_configuration\n when \"reset\"\n reset_configuration\n when \"migrate\"\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private def show_configuration\n BlueprintsCLI.logger.step(\"Blueprint Configuration\")\n puts \"=\" * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts \"\"\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts \"Database Configuration:\".colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts \"\"\n\n # AI Configuration\n puts \"AI Configuration:\".colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n ai_provider = @config.fetch(:ai, :sublayer, :provider, default: \"\").downcase\n api_key_status = @config.ai_api_key(ai_provider) ? \"Set\" : \"Not set\"\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts \"\"\n\n # AI Configuration - Ruby LLM\n puts \"AI Configuration (Ruby LLM):\".colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?(\"_api_key\")\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select do |k|\n k.to_s.end_with?('_api_key')\n end.join(', ')}\"\n else\n puts \" No Ruby LLM configuration found\"\n end\n puts \"\"\n\n # Editor Configuration\n puts \"Editor Configuration:\".colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts \"\"\n\n # Feature Flags\n puts \"Feature Flags:\".colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description,\n default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize,\n default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts \"\"\n\n # Logger Configuration\n puts \"Logger Configuration:\".colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts \"\"\n\n # Search Configuration\n puts \"Search Configuration:\".colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit,\n default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search,\n default: 'Not set')}\"\n puts \"\"\n\n # Performance Configuration\n puts \"Performance Configuration:\".colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size,\n default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure(\"No configuration found\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts \"=\" * 60\n puts \"\"\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n private def setup_configuration\n BlueprintsCLI.logger.step(\"Blueprint Configuration Setup\")\n puts \"=\" * 50\n puts \"\"\n\n config = load_configuration || {}\n\n # Database configuration\n puts \"šŸ“Š Database Configuration\".colorize(:cyan)\n current_db = config.dig(\"database\", \"url\") || BlueprintsCLI.configuration.database_url\n db_url = prompt_for_input(\"Database URL\", current_db)\n\n config[\"database\"] = { \"url\" => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig(\"ai\", \"provider\") || \"gemini\"\n provider = prompt_for_choice(\"AI Provider\", %w[gemini openai], current_provider)\n\n current_model = config.dig(\"ai\",\n \"model\") || ((provider == \"gemini\") ? \"text-embedding-004\" : \"text-embedding-3-small\")\n model = prompt_for_input(\"AI Model\", current_model)\n\n puts \"šŸ’” Set API key via environment variable:\".colorize(:yellow)\n puts \" export GEMINI_API_KEY=your_key_here\" if provider == \"gemini\"\n puts \" export OPENAI_API_KEY=your_key_here\" if provider == \"openai\"\n\n config[\"ai\"] = {\n \"provider\" => provider,\n \"model\" => model,\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor,\n default: ENV[\"EDITOR\"] || ENV[\"VISUAL\"] || \"vim\")\n editor = prompt_for_input(\"Preferred editor\", current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean(\"Auto-save edits\", current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean(\"Auto-generate descriptions\", current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean(\"Auto-generate categories\", current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis,\n default: true)\n improvement = prompt_for_boolean(\"Enable AI improvement analysis\", current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: \"info\")\n log_level = prompt_for_choice(\"Log level\", %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean(\"Enable file logging\", current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path,\n default: @config.send(:default_log_path))\n file_path = prompt_for_input(\"Log file path\", current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success(\"Configuration saved successfully!\",\n file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure(\"Failed to save configuration\")\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n private def test_configuration\n BlueprintsCLI.logger.step(\"Testing Blueprint Configuration\")\n puts \"=\" * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success(\"Configuration structure is valid\")\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n#{'=' * 50}\"\n if all_tests_passed\n BlueprintsCLI.logger.success(\"All configuration tests passed!\")\n else\n BlueprintsCLI.logger.failure(\"Some configuration tests failed\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n private def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print \"āš ļø This will delete the existing configuration. Continue? (y/N): \"\n response = $stdin.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success(\"Configuration reset successfully\")\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn(\"Reset cancelled\")\n false\n end\n else\n BlueprintsCLI.logger.info(\"No configuration file found to reset\")\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n private def migrate_configuration\n BlueprintsCLI.logger.step(\"Migrating Configuration Files\")\n puts \"=\" * 50\n puts \"\"\n\n puts \"ā„¹ļø Migration is no longer needed - using unified configuration system\"\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n private def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n private def show_environment_variables\n puts \"šŸŒ Environment Variables:\".colorize(:blue)\n\n env_vars = {\n \"GEMINI_API_KEY\" => ENV.fetch(\"GEMINI_API_KEY\", nil),\n \"GOOGLE_API_KEY\" => ENV.fetch(\"GOOGLE_API_KEY\", nil),\n \"OPENAI_API_KEY\" => ENV.fetch(\"OPENAI_API_KEY\", nil),\n \"ANTHROPIC_API_KEY\" => ENV.fetch(\"ANTHROPIC_API_KEY\", nil),\n \"DEEPSEEK_API_KEY\" => ENV.fetch(\"DEEPSEEK_API_KEY\", nil),\n \"BLUEPRINT_DATABASE_URL\" => ENV.fetch(\"BLUEPRINT_DATABASE_URL\", nil),\n \"DATABASE_URL\" => ENV.fetch(\"DATABASE_URL\", nil),\n \"BLUEPRINTS_DEBUG\" => ENV.fetch(\"BLUEPRINTS_DEBUG\", nil),\n \"DEBUG\" => ENV.fetch(\"DEBUG\", nil),\n \"EDITOR\" => ENV.fetch(\"EDITOR\", nil),\n \"VISUAL\" => ENV.fetch(\"VISUAL\", nil),\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?(\"KEY\") ? \"Set (hidden)\" : value\n else\n \"Not set\"\n end\n puts \" #{key}: #{status}\"\n end\n puts \"\"\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n private def test_database_connection\n require \"sequel\"\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure(\"No database URL configured\")\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success(\"Database connection successful\")\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests AI provider connections by checking for configured API keys.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n private def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test configured AI provider\n ai_provider = @config.fetch(:ai, :sublayer, :provider, default: \"\").downcase\n unless ai_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(ai_provider)\n if api_key\n BlueprintsCLI.logger.success(\"AI API key found for #{ai_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"AI API key not found for #{ai_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?(\"_api_key\") && v }\n\n if api_keys.any?\n api_keys.each_key do |key|\n provider = key.to_s.gsub(\"_api_key\", \"\")\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn(\"No Ruby LLM API keys configured\")\n end\n\n if total_tests.zero?\n BlueprintsCLI.logger.warn(\"No AI providers configured\")\n false\n else\n success_count.positive?\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n private def test_editor\n editor = @config.fetch(:blueprints, :editor, default: \"vim\")\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure(\"No editor configured\")\n return false\n end\n\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n private def test_logger_config\n level = @config.fetch(:logger, :level, default: \"info\")\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure(\"File logging enabled but no file path configured\")\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success(\"Logger configuration is valid\")\n true\n rescue => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n private def prompt_for_input(prompt, default = nil)\n print prompt\n print \" [#{default}]\" if default\n print \": \"\n\n input = $stdin.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n private def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : \": \"\n\n input = $stdin.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n private def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then \" [Y/n]\"\n when false then \" [y/N]\"\n else \" [y/n]\"\n end\n\n print \"#{prompt}#{default_text}: \"\n input = $stdin.gets.chomp.downcase\n\n case input\n when \"y\", \"yes\", \"true\"\n true\n when \"n\", \"no\", \"false\"\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n private def mask_password(url)\n return url unless url.include?(\":\") && url.include?(\"@\")\n\n url.gsub(/:[^:@]*@/, \":***@\")\n end\n end\n end\nend\n"}, "95477b41e128e10245b73074106d27250e3ceef7": {"t": 1785756133.467823, "n": 23353, "h": "1053e9e6ee98d412", "c": "# frozen_string_literal: true\n\nrequire_relative '../configuration'\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: 'show')\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test', 'validate'\n test_configuration\n when 'reset'\n reset_configuration\n when 'migrate'\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts ''\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts ''\n\n # AI Configuration\n puts 'AI Configuration:'.colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n ai_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n api_key_status = @config.ai_api_key(ai_provider) ? 'Set' : 'Not set'\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts ''\n\n # AI Configuration - Ruby LLM\n puts 'AI Configuration (Ruby LLM):'.colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?('_api_key')\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select do |k|\n k.to_s.end_with?('_api_key')\n end.join(', ')}\"\n else\n puts ' No Ruby LLM configuration found'\n end\n puts ''\n\n # Editor Configuration\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts ''\n\n # Feature Flags\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description,\n default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize,\n default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts ''\n\n # Logger Configuration\n puts 'Logger Configuration:'.colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts ''\n\n # Search Configuration\n puts 'Search Configuration:'.colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit,\n default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search,\n default: 'Not set')}\"\n puts ''\n\n # Performance Configuration\n puts 'Performance Configuration:'.colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size,\n default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || BlueprintsCLI.configuration.database_url\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor,\n default: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n editor = prompt_for_input('Preferred editor', current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis,\n default: true)\n improvement = prompt_for_boolean('Enable AI improvement analysis', current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: 'info')\n log_level = prompt_for_choice('Log level', %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean('Enable file logging', current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path,\n default: @config.send(:default_log_path))\n file_path = prompt_for_input('Log file path', current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!',\n file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success('Configuration structure is valid')\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n def migrate_configuration\n BlueprintsCLI.logger.step('Migrating Configuration Files')\n puts '=' * 50\n puts ''\n\n puts 'ā„¹ļø Migration is no longer needed - using unified configuration system'\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'GOOGLE_API_KEY' => ENV.fetch('GOOGLE_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'ANTHROPIC_API_KEY' => ENV.fetch('ANTHROPIC_API_KEY', nil),\n 'DEEPSEEK_API_KEY' => ENV.fetch('DEEPSEEK_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'BLUEPRINTS_DEBUG' => ENV.fetch('BLUEPRINTS_DEBUG', nil),\n 'DEBUG' => ENV.fetch('DEBUG', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?('KEY') ? 'Set (hidden)' : value\n else\n 'Not set'\n end\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection\n require 'sequel'\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure('No database URL configured')\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests AI provider connections by checking for configured API keys.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test configured AI provider\n ai_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n unless ai_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(ai_provider)\n if api_key\n BlueprintsCLI.logger.success(\"AI API key found for #{ai_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"AI API key not found for #{ai_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?('_api_key') && v }\n\n if api_keys.any?\n api_keys.each do |key, _|\n provider = key.to_s.gsub('_api_key', '')\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn('No Ruby LLM API keys configured')\n end\n\n if total_tests == 0\n BlueprintsCLI.logger.warn('No AI providers configured')\n false\n else\n success_count > 0\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor\n editor = @config.fetch(:blueprints, :editor, default: 'vim')\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure('No editor configured')\n return false\n end\n\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n def test_logger_config\n level = @config.fetch(:logger, :level, default: 'info')\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure('File logging enabled but no file path configured')\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success('Logger configuration is valid')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}, "c05f3ed8c9df60a688906653e13b6300db1aff9e": {"t": 1785756135.239689, "n": 23443, "h": "ecfe680e6e3ef0ce", "c": "# frozen_string_literal: true\n\nrequire_relative '../configuration'\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config < Sublayer::Actions::Base\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: 'show')\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test', 'validate'\n test_configuration\n when 'reset'\n reset_configuration\n when 'migrate'\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts ''\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts ''\n\n # AI Configuration - Sublayer\n puts 'AI Configuration (Sublayer):'.colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n api_key_status = @config.ai_api_key(sublayer_provider) ? 'Set' : 'Not set'\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts ''\n\n # AI Configuration - Ruby LLM\n puts 'AI Configuration (Ruby LLM):'.colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?('_api_key')\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select do |k|\n k.to_s.end_with?('_api_key')\n end.join(', ')}\"\n else\n puts ' No Ruby LLM configuration found'\n end\n puts ''\n\n # Editor Configuration\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts ''\n\n # Feature Flags\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description,\n default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize,\n default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts ''\n\n # Logger Configuration\n puts 'Logger Configuration:'.colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts ''\n\n # Search Configuration\n puts 'Search Configuration:'.colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit,\n default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search,\n default: 'Not set')}\"\n puts ''\n\n # Performance Configuration\n puts 'Performance Configuration:'.colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size,\n default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || BlueprintsCLI.configuration.database_url\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor,\n default: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n editor = prompt_for_input('Preferred editor', current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis,\n default: true)\n improvement = prompt_for_boolean('Enable AI improvement analysis', current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: 'info')\n log_level = prompt_for_choice('Log level', %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean('Enable file logging', current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path,\n default: @config.send(:default_log_path))\n file_path = prompt_for_input('Log file path', current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!',\n file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success('Configuration structure is valid')\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n def migrate_configuration\n BlueprintsCLI.logger.step('Migrating Configuration Files')\n puts '=' * 50\n puts ''\n\n puts 'ā„¹ļø Migration is no longer needed - using unified configuration system'\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'GOOGLE_API_KEY' => ENV.fetch('GOOGLE_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'ANTHROPIC_API_KEY' => ENV.fetch('ANTHROPIC_API_KEY', nil),\n 'DEEPSEEK_API_KEY' => ENV.fetch('DEEPSEEK_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'BLUEPRINTS_DEBUG' => ENV.fetch('BLUEPRINTS_DEBUG', nil),\n 'DEBUG' => ENV.fetch('DEBUG', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?('KEY') ? 'Set (hidden)' : value\n else\n 'Not set'\n end\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection\n require 'sequel'\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure('No database URL configured')\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests both Sublayer and Ruby LLM configurations.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test Sublayer configuration\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n unless sublayer_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(sublayer_provider)\n if api_key\n BlueprintsCLI.logger.success(\"Sublayer AI API key found for #{sublayer_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"Sublayer AI API key not found for #{sublayer_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?('_api_key') && v }\n\n if api_keys.any?\n api_keys.each do |key, _|\n provider = key.to_s.gsub('_api_key', '')\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn('No Ruby LLM API keys configured')\n end\n\n if total_tests == 0\n BlueprintsCLI.logger.warn('No AI providers configured')\n false\n else\n success_count > 0\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor\n editor = @config.fetch(:blueprints, :editor, default: 'vim')\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure('No editor configured')\n return false\n end\n\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n def test_logger_config\n level = @config.fetch(:logger, :level, default: 'info')\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure('File logging enabled but no file path configured')\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success('Logger configuration is valid')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}, "a97b68230ef6db15000ca29f261ec038fd174e1c": {"t": 1785756136.7921722, "n": 23448, "h": "bf04124d43bb5b4b", "c": "# frozen_string_literal: true\n\nrequire_relative '../configuration'\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config < Sublayer::Actions::Base\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: 'show')\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test', 'validate'\n test_configuration\n when 'reset'\n reset_configuration\n when 'migrate'\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts ''\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts ''\n\n # AI Configuration - Sublayer\n puts 'AI Configuration (Sublayer):'.colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n api_key_status = @config.ai_api_key(sublayer_provider) ? 'Set' : 'Not set'\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts ''\n\n # AI Configuration - Ruby LLM\n puts 'AI Configuration (Ruby LLM):'.colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?('_api_key')\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select do |k|\n k.to_s.end_with?('_api_key')\n end.join(', ')}\"\n else\n puts ' No Ruby LLM configuration found'\n end\n puts ''\n\n # Editor Configuration\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts ''\n\n # Feature Flags\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description,\n default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize,\n default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts ''\n\n # Logger Configuration\n puts 'Logger Configuration:'.colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts ''\n\n # Search Configuration\n puts 'Search Configuration:'.colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit,\n default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search,\n default: 'Not set')}\"\n puts ''\n\n # Performance Configuration\n puts 'Performance Configuration:'.colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size,\n default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || 'postgres://localhost/blueprints_development'\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor,\n default: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n editor = prompt_for_input('Preferred editor', current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis,\n default: true)\n improvement = prompt_for_boolean('Enable AI improvement analysis', current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: 'info')\n log_level = prompt_for_choice('Log level', %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean('Enable file logging', current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path,\n default: @config.send(:default_log_path))\n file_path = prompt_for_input('Log file path', current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!',\n file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success('Configuration structure is valid')\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n def migrate_configuration\n BlueprintsCLI.logger.step('Migrating Configuration Files')\n puts '=' * 50\n puts ''\n\n puts 'ā„¹ļø Migration is no longer needed - using unified configuration system'\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'GOOGLE_API_KEY' => ENV.fetch('GOOGLE_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'ANTHROPIC_API_KEY' => ENV.fetch('ANTHROPIC_API_KEY', nil),\n 'DEEPSEEK_API_KEY' => ENV.fetch('DEEPSEEK_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'BLUEPRINTS_DEBUG' => ENV.fetch('BLUEPRINTS_DEBUG', nil),\n 'DEBUG' => ENV.fetch('DEBUG', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?('KEY') ? 'Set (hidden)' : value\n else\n 'Not set'\n end\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection\n require 'sequel'\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure('No database URL configured')\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests both Sublayer and Ruby LLM configurations.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test Sublayer configuration\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n unless sublayer_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(sublayer_provider)\n if api_key\n BlueprintsCLI.logger.success(\"Sublayer AI API key found for #{sublayer_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"Sublayer AI API key not found for #{sublayer_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?('_api_key') && v }\n\n if api_keys.any?\n api_keys.each do |key, _|\n provider = key.to_s.gsub('_api_key', '')\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn('No Ruby LLM API keys configured')\n end\n\n if total_tests == 0\n BlueprintsCLI.logger.warn('No AI providers configured')\n false\n else\n success_count > 0\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor\n editor = @config.fetch(:blueprints, :editor, default: 'vim')\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure('No editor configured')\n return false\n end\n\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n def test_logger_config\n level = @config.fetch(:logger, :level, default: 'info')\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure('File logging enabled but no file path configured')\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success('Logger configuration is valid')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}, "ff5ac887c9a66fa9eb8e4217517a1ce1b15e82cf": {"t": 1785756138.1156902, "n": 23000, "h": "df1424b5fcd20dba", "c": "# frozen_string_literal: true\n\nrequire_relative '../configuration'\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config < Sublayer::Actions::Base\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: 'show')\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test', 'validate'\n test_configuration\n when 'reset'\n reset_configuration\n when 'migrate'\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts ''\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts ''\n\n # AI Configuration - Sublayer\n puts 'AI Configuration (Sublayer):'.colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n api_key_status = @config.ai_api_key(sublayer_provider) ? 'Set' : 'Not set'\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts ''\n\n # AI Configuration - Ruby LLM\n puts 'AI Configuration (Ruby LLM):'.colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?('_api_key')\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select { |k| k.to_s.end_with?('_api_key') }.join(', ')}\"\n else\n puts ' No Ruby LLM configuration found'\n end\n puts ''\n\n # Editor Configuration\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts ''\n\n # Feature Flags\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description, default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize, default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts ''\n\n # Logger Configuration\n puts 'Logger Configuration:'.colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts ''\n\n # Search Configuration\n puts 'Search Configuration:'.colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit, default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search, default: 'Not set')}\"\n puts ''\n\n # Performance Configuration\n puts 'Performance Configuration:'.colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size, default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || 'postgres://localhost/blueprints_development'\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor, default: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n editor = prompt_for_input('Preferred editor', current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis, default: true)\n improvement = prompt_for_boolean('Enable AI improvement analysis', current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: 'info')\n log_level = prompt_for_choice('Log level', %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean('Enable file logging', current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path, default: @config.send(:default_log_path))\n file_path = prompt_for_input('Log file path', current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!', file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success('Configuration structure is valid')\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n def migrate_configuration\n BlueprintsCLI.logger.step('Migrating Configuration Files')\n puts '=' * 50\n puts ''\n\n puts 'ā„¹ļø Migration is no longer needed - using unified configuration system'\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'GOOGLE_API_KEY' => ENV.fetch('GOOGLE_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'ANTHROPIC_API_KEY' => ENV.fetch('ANTHROPIC_API_KEY', nil),\n 'DEEPSEEK_API_KEY' => ENV.fetch('DEEPSEEK_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'BLUEPRINTS_DEBUG' => ENV.fetch('BLUEPRINTS_DEBUG', nil),\n 'DEBUG' => ENV.fetch('DEBUG', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?('KEY') ? 'Set (hidden)' : value\n else\n 'Not set'\n end\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection\n require 'sequel'\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure('No database URL configured')\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests both Sublayer and Ruby LLM configurations.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test Sublayer configuration\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n unless sublayer_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(sublayer_provider)\n if api_key\n BlueprintsCLI.logger.success(\"Sublayer AI API key found for #{sublayer_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"Sublayer AI API key not found for #{sublayer_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?('_api_key') && v }\n\n if api_keys.any?\n api_keys.each do |key, _|\n provider = key.to_s.gsub('_api_key', '')\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn('No Ruby LLM API keys configured')\n end\n\n if total_tests == 0\n BlueprintsCLI.logger.warn('No AI providers configured')\n false\n else\n success_count > 0\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor\n editor = @config.fetch(:blueprints, :editor, default: 'vim')\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure('No editor configured')\n return false\n end\n\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n def test_logger_config\n level = @config.fetch(:logger, :level, default: 'info')\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure('File logging enabled but no file path configured')\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success('Logger configuration is valid')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}, "9432b439d0a5ec790e50fc9d5741a9ffc8d857f3": {"t": 1785756139.4526649, "n": 28336, "h": "cdb2aa9980f8c480", "c": "# frozen_string_literal: true\n\nrequire_relative '../configuration'\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config < Sublayer::Actions::Base\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: 'show')\n super\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test', 'validate'\n test_configuration\n when 'reset'\n reset_configuration\n when 'migrate'\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts ''\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts ''\n\n # AI Configuration - Sublayer\n puts 'AI Configuration (Sublayer):'.colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n api_key_status = @config.ai_api_key(sublayer_provider) ? 'Set' : 'Not set'\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts ''\n\n # AI Configuration - Ruby LLM\n puts 'AI Configuration (Ruby LLM):'.colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?('_api_key')\n\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select { |k| k.to_s.end_with?('_api_key') }.join(', ')}\"\n else\n puts ' No Ruby LLM configuration found'\n end\n puts ''\n\n # Editor Configuration\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts ''\n\n # Feature Flags\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description, default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize, default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis,\n default: 'Not set')}\"\n puts ''\n\n # Logger Configuration\n puts 'Logger Configuration:'.colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts ''\n\n # Search Configuration\n puts 'Search Configuration:'.colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit, default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search, default: 'Not set')}\"\n puts ''\n\n # Performance Configuration\n puts 'Performance Configuration:'.colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size, default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size,\n default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || 'postgres://localhost/blueprints_development'\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor, default: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n editor = prompt_for_input('Preferred editor', current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis, default: true)\n improvement = prompt_for_boolean('Enable AI improvement analysis', current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: 'info')\n log_level = prompt_for_choice('Log level', %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean('Enable file logging', current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path, default: @config.send(:default_log_path))\n file_path = prompt_for_input('Log file path', current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!', file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success('Configuration structure is valid')\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n config_file = @config.config_file_path\n\n if config_file && File.exist?(config_file)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n def migrate_configuration\n BlueprintsCLI.logger.step('Migrating Configuration Files')\n puts '=' * 50\n puts ''\n\n # Check for existing config files\n old_config_path = File.join(__dir__, '..', 'config', 'blueprints.yml')\n old_sublayer_path = File.join(__dir__, '..', 'config', 'sublayer.yml')\n\n migrated_any = false\n\n # Migrate old blueprints.yml\n if File.exist?(old_config_path)\n puts 'šŸ“„ Found existing blueprints.yml configuration'\n begin\n old_config = YAML.load_file(old_config_path)\n migrate_blueprints_config(old_config)\n migrated_any = true\n puts 'āœ… Migrated blueprints.yml configuration'\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Failed to migrate blueprints.yml: #{e.message}\")\n end\n end\n\n # Migrate old sublayer.yml\n if File.exist?(old_sublayer_path)\n puts 'šŸ“„ Found existing sublayer.yml configuration'\n begin\n old_sublayer = YAML.load_file(old_sublayer_path)\n migrate_sublayer_config(old_sublayer)\n migrated_any = true\n puts 'āœ… Migrated sublayer.yml configuration'\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Failed to migrate sublayer.yml: #{e.message}\")\n end\n end\n\n if migrated_any\n # Save the migrated configuration\n if @config.write(force: true, create: true)\n BlueprintsCLI.logger.success('Migration completed successfully!', file: @config.config_file_path)\n puts \"\\nšŸ’” You can now delete the old configuration files:\"\n puts \" rm #{old_config_path}\" if File.exist?(old_config_path)\n puts \" rm #{old_sublayer_path}\" if File.exist?(old_sublayer_path)\n else\n BlueprintsCLI.logger.failure('Failed to save migrated configuration')\n return false\n end\n else\n puts 'ā„¹ļø No existing configuration files found to migrate'\n end\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup#{' '}\n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n ##\n # Migrate old blueprints.yml configuration to new format\n #\n # @param old_config [Hash] The old configuration hash\n def migrate_blueprints_config(old_config)\n # Database configuration\n if old_config['database'] && old_config['database']['url']\n @config.set(:blueprints, :database, :url,\n value: old_config['database']['url'])\n end\n\n # Editor configuration\n @config.set(:blueprints, :editor, value: old_config['editor']) if old_config['editor']\n if old_config.key?('auto_save_edits')\n @config.set(:blueprints, :auto_save_edits,\n value: old_config['auto_save_edits'])\n end\n\n # Feature flags\n if old_config['features']\n if old_config['features'].key?('auto_description')\n @config.set(:blueprints, :features, :auto_description,\n value: old_config['features']['auto_description'])\n end\n if old_config['features'].key?('auto_categorize')\n @config.set(:blueprints, :features, :auto_categorize,\n value: old_config['features']['auto_categorize'])\n end\n if old_config['features'].key?('improvement_analysis')\n @config.set(:blueprints, :features, :improvement_analysis,\n value: old_config['features']['improvement_analysis'])\n end\n end\n\n # Search configuration\n if old_config['search']\n if old_config['search']['default_limit']\n @config.set(:blueprints, :search, :default_limit,\n value: old_config['search']['default_limit'])\n end\n if old_config['search'].key?('semantic_search')\n @config.set(:blueprints, :search, :semantic_search,\n value: old_config['search']['semantic_search'])\n end\n end\n\n # Performance configuration\n if old_config['performance']\n if old_config['performance']['batch_size']\n @config.set(:blueprints, :performance, :batch_size,\n value: old_config['performance']['batch_size'])\n end\n if old_config['performance']['connection_pool_size']\n @config.set(:blueprints, :performance, :connection_pool_size,\n value: old_config['performance']['connection_pool_size'])\n end\n end\n\n # AI configuration (if present in old format)\n return unless old_config['ai']\n\n @config.set(:ai, :sublayer, :provider, value: old_config['ai']['provider']) if old_config['ai']['provider']\n @config.set(:ai, :sublayer, :model, value: old_config['ai']['model']) if old_config['ai']['model']\n end\n\n ##\n # Migrate old sublayer.yml configuration to new format\n #\n # @param old_sublayer [Hash] The old sublayer configuration hash\n def migrate_sublayer_config(old_sublayer)\n @config.set(:ai, :sublayer, :project_name, value: old_sublayer[:project_name]) if old_sublayer[:project_name]\n if old_sublayer[:project_template]\n @config.set(:ai, :sublayer, :project_template,\n value: old_sublayer[:project_template])\n end\n @config.set(:ai, :sublayer, :provider, value: old_sublayer[:ai_provider]) if old_sublayer[:ai_provider]\n @config.set(:ai, :sublayer, :model, value: old_sublayer[:ai_model]) if old_sublayer[:ai_model]\n end\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'GOOGLE_API_KEY' => ENV.fetch('GOOGLE_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'ANTHROPIC_API_KEY' => ENV.fetch('ANTHROPIC_API_KEY', nil),\n 'DEEPSEEK_API_KEY' => ENV.fetch('DEEPSEEK_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'BLUEPRINTS_DEBUG' => ENV.fetch('BLUEPRINTS_DEBUG', nil),\n 'DEBUG' => ENV.fetch('DEBUG', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = if value\n key.include?('KEY') ? 'Set (hidden)' : value\n else\n 'Not set'\n end\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection\n require 'sequel'\n db_url = @config.database_url\n\n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure('No database URL configured')\n return false\n end\n\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests both Sublayer and Ruby LLM configurations.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n def test_ai_connections\n success_count = 0\n total_tests = 0\n\n # Test Sublayer configuration\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n unless sublayer_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(sublayer_provider)\n if api_key\n BlueprintsCLI.logger.success(\"Sublayer AI API key found for #{sublayer_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"Sublayer AI API key not found for #{sublayer_provider}\")\n end\n end\n\n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?('_api_key') && v }\n\n if api_keys.any?\n api_keys.each do |key, _|\n provider = key.to_s.gsub('_api_key', '')\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn('No Ruby LLM API keys configured')\n end\n\n if total_tests == 0\n BlueprintsCLI.logger.warn('No AI providers configured')\n false\n else\n success_count > 0\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor\n editor = @config.fetch(:blueprints, :editor, default: 'vim')\n\n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure('No editor configured')\n return false\n end\n\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n def test_logger_config\n level = @config.fetch(:logger, :level, default: 'info')\n valid_levels = %w[debug info warn error fatal]\n\n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n\n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure('File logging enabled but no file path configured')\n return false\n end\n\n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n\n BlueprintsCLI.logger.success('Logger configuration is valid')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}, "9c1cd3923b2e207e3b4f366b825cb569aa0f759d": {"t": 1785756140.9862595, "n": 27780, "h": "90367401fd69edae", "c": "# frozen_string_literal: true\n\nrequire_relative '../configuration'\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration\n # using the new TTY::Config-based BlueprintsCLI::Configuration system.\n # Handles settings for the database, AI providers, logger, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config < Sublayer::Actions::Base\n\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'validate', 'reset'.\n def initialize(subcommand: 'show')\n @subcommand = subcommand\n @config = BlueprintsCLI::Configuration.new\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test', 'validate'\n test_configuration\n when 'reset'\n reset_configuration\n when 'migrate'\n migrate_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{@config.config_file_path || 'Not found'}\"\n puts \"File exists: #{@config.exist? ? 'Yes' : 'No'}\"\n puts ''\n\n if @config.exist? || @config.to_hash.any?\n # Database Configuration\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(@config.database_url || 'Not set')}\"\n puts ''\n\n # AI Configuration - Sublayer\n puts 'AI Configuration (Sublayer):'.colorize(:cyan)\n puts \" Provider: #{@config.fetch(:ai, :sublayer, :provider, default: 'Not set')}\"\n puts \" Model: #{@config.fetch(:ai, :sublayer, :model, default: 'Not set')}\"\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n api_key_status = @config.ai_api_key(sublayer_provider) ? 'Set' : 'Not set'\n puts \" API Key: #{api_key_status}\"\n puts \" Embedding Model: #{@config.fetch(:ai, :embedding_model, default: 'Not set')}\"\n puts ''\n\n # AI Configuration - Ruby LLM\n puts 'AI Configuration (Ruby LLM):'.colorize(:cyan)\n ruby_llm_config = @config.ruby_llm_config\n if ruby_llm_config.any?\n ruby_llm_config.each do |key, value|\n next if key.to_s.end_with?('_api_key')\n puts \" #{key}: #{value}\"\n end\n puts \" Available API Keys: #{ruby_llm_config.keys.select { |k| k.to_s.end_with?('_api_key') }.join(', ')}\"\n else\n puts ' No Ruby LLM configuration found'\n end\n puts ''\n\n # Editor Configuration\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{@config.fetch(:blueprints, :editor, default: 'Not set')}\"\n puts \" Auto-save: #{@config.fetch(:blueprints, :auto_save_edits, default: 'Not set')}\"\n puts ''\n\n # Feature Flags\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{@config.fetch(:blueprints, :features, :auto_description, default: 'Not set')}\"\n puts \" Auto-categorization: #{@config.fetch(:blueprints, :features, :auto_categorize, default: 'Not set')}\"\n puts \" Improvement analysis: #{@config.fetch(:blueprints, :features, :improvement_analysis, default: 'Not set')}\"\n puts ''\n\n # Logger Configuration\n puts 'Logger Configuration:'.colorize(:cyan)\n puts \" Level: #{@config.fetch(:logger, :level, default: 'Not set')}\"\n puts \" File logging: #{@config.fetch(:logger, :file_logging, default: 'Not set')}\"\n puts \" File path: #{@config.fetch(:logger, :file_path, default: 'Not set')}\"\n puts ''\n\n # Search Configuration\n puts 'Search Configuration:'.colorize(:cyan)\n puts \" Default limit: #{@config.fetch(:blueprints, :search, :default_limit, default: 'Not set')}\"\n puts \" Semantic search: #{@config.fetch(:blueprints, :search, :semantic_search, default: 'Not set')}\"\n puts ''\n\n # Performance Configuration\n puts 'Performance Configuration:'.colorize(:cyan)\n puts \" Batch size: #{@config.fetch(:blueprints, :performance, :batch_size, default: 'Not set')}\"\n puts \" Connection pool size: #{@config.fetch(:blueprints, :performance, :connection_pool_size, default: 'Not set')}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || 'postgres://localhost/blueprints_development'\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = @config.fetch(:blueprints, :editor, default: ENV['EDITOR'] || ENV['VISUAL'] || 'vim')\n editor = prompt_for_input('Preferred editor', current_editor)\n @config.set(:blueprints, :editor, value: editor)\n\n current_auto_save = @config.fetch(:blueprints, :auto_save_edits, default: false)\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save)\n @config.set(:blueprints, :auto_save_edits, value: auto_save)\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = @config.fetch(:blueprints, :features, :auto_description, default: true)\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc)\n @config.set(:blueprints, :features, :auto_description, value: auto_desc)\n\n current_auto_cat = @config.fetch(:blueprints, :features, :auto_categorize, default: true)\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat)\n @config.set(:blueprints, :features, :auto_categorize, value: auto_cat)\n\n current_improvement = @config.fetch(:blueprints, :features, :improvement_analysis, default: true)\n improvement = prompt_for_boolean('Enable AI improvement analysis', current_improvement)\n @config.set(:blueprints, :features, :improvement_analysis, value: improvement)\n\n # Logger configuration\n puts \"\\nšŸ“ Logger Configuration\".colorize(:cyan)\n current_level = @config.fetch(:logger, :level, default: 'info')\n log_level = prompt_for_choice('Log level', %w[debug info warn error fatal], current_level)\n @config.set(:logger, :level, value: log_level)\n\n current_file_logging = @config.fetch(:logger, :file_logging, default: false)\n file_logging = prompt_for_boolean('Enable file logging', current_file_logging)\n @config.set(:logger, :file_logging, value: file_logging)\n\n if file_logging\n current_file_path = @config.fetch(:logger, :file_path, default: @config.send(:default_log_path))\n file_path = prompt_for_input('Log file path', current_file_path)\n @config.set(:logger, :file_path, value: file_path)\n end\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = @config.write(force: true, create: true)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!', file: @config.config_file_path)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Validates configuration structure and tests connectivity to database,\n # AI providers, and editor availability.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n all_tests_passed = true\n\n # Test configuration validation\n puts \"\\nšŸ“‹ Validating configuration structure...\".colorize(:cyan)\n begin\n @config.validate!\n BlueprintsCLI.logger.success('Configuration structure is valid')\n rescue BlueprintsCLI::Configuration::ValidationError => e\n BlueprintsCLI.logger.failure(\"Configuration validation failed: #{e.message}\")\n all_tests_passed = false\n end\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection\n all_tests_passed &&= db_success\n\n # Test AI API connections\n puts \"\\nšŸ¤– Testing AI API connections...\".colorize(:cyan)\n ai_success = test_ai_connections\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor\n all_tests_passed &&= editor_success\n\n # Test logger configuration\n puts \"\\nšŸ“ Testing logger configuration...\".colorize(:cyan)\n logger_success = test_logger_config\n all_tests_passed &&= logger_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the configuration file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n config_file = @config.config_file_path\n \n if config_file && File.exist?(config_file)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(config_file)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Migrate existing configuration files to new format\n #\n # @return [Boolean] Returns `true` if migration succeeded\n def migrate_configuration\n BlueprintsCLI.logger.step('Migrating Configuration Files')\n puts '=' * 50\n puts ''\n\n # Check for existing config files\n old_config_path = File.join(__dir__, '..', 'config', 'blueprints.yml')\n old_sublayer_path = File.join(__dir__, '..', 'config', 'sublayer.yml')\n \n migrated_any = false\n\n # Migrate old blueprints.yml\n if File.exist?(old_config_path)\n puts \"šŸ“„ Found existing blueprints.yml configuration\"\n begin\n old_config = YAML.load_file(old_config_path)\n migrate_blueprints_config(old_config)\n migrated_any = true\n puts \"āœ… Migrated blueprints.yml configuration\"\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Failed to migrate blueprints.yml: #{e.message}\")\n end\n end\n\n # Migrate old sublayer.yml\n if File.exist?(old_sublayer_path)\n puts \"šŸ“„ Found existing sublayer.yml configuration\"\n begin\n old_sublayer = YAML.load_file(old_sublayer_path)\n migrate_sublayer_config(old_sublayer)\n migrated_any = true\n puts \"āœ… Migrated sublayer.yml configuration\"\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Failed to migrate sublayer.yml: #{e.message}\")\n end\n end\n\n if migrated_any\n # Save the migrated configuration\n if @config.write(force: true, create: true)\n BlueprintsCLI.logger.success('Migration completed successfully!', file: @config.config_file_path)\n puts \"\\nšŸ’” You can now delete the old configuration files:\"\n puts \" rm #{old_config_path}\" if File.exist?(old_config_path)\n puts \" rm #{old_sublayer_path}\" if File.exist?(old_sublayer_path)\n else\n BlueprintsCLI.logger.failure('Failed to save migrated configuration')\n return false\n end\n else\n puts \"ā„¹ļø No existing configuration files found to migrate\"\n end\n\n true\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup \n blueprint config test Test configuration connectivity and validation\n blueprint config migrate Migrate old configuration files to new format\n blueprint config reset Reset configuration to defaults\n\n Configuration is managed by TTY::Config and stored in:\n #{@config.config_file_path || '~/.config/BlueprintsCLI/config.yml'}\n HELP\n end\n\n ##\n # Migrate old blueprints.yml configuration to new format\n #\n # @param old_config [Hash] The old configuration hash\n def migrate_blueprints_config(old_config)\n # Database configuration\n if old_config['database']\n @config.set(:blueprints, :database, :url, value: old_config['database']['url']) if old_config['database']['url']\n end\n\n # Editor configuration\n @config.set(:blueprints, :editor, value: old_config['editor']) if old_config['editor']\n @config.set(:blueprints, :auto_save_edits, value: old_config['auto_save_edits']) if old_config.key?('auto_save_edits')\n\n # Feature flags\n if old_config['features']\n @config.set(:blueprints, :features, :auto_description, value: old_config['features']['auto_description']) if old_config['features'].key?('auto_description')\n @config.set(:blueprints, :features, :auto_categorize, value: old_config['features']['auto_categorize']) if old_config['features'].key?('auto_categorize')\n @config.set(:blueprints, :features, :improvement_analysis, value: old_config['features']['improvement_analysis']) if old_config['features'].key?('improvement_analysis')\n end\n\n # Search configuration\n if old_config['search']\n @config.set(:blueprints, :search, :default_limit, value: old_config['search']['default_limit']) if old_config['search']['default_limit']\n @config.set(:blueprints, :search, :semantic_search, value: old_config['search']['semantic_search']) if old_config['search'].key?('semantic_search')\n end\n\n # Performance configuration\n if old_config['performance']\n @config.set(:blueprints, :performance, :batch_size, value: old_config['performance']['batch_size']) if old_config['performance']['batch_size']\n @config.set(:blueprints, :performance, :connection_pool_size, value: old_config['performance']['connection_pool_size']) if old_config['performance']['connection_pool_size']\n end\n\n # AI configuration (if present in old format)\n if old_config['ai']\n @config.set(:ai, :sublayer, :provider, value: old_config['ai']['provider']) if old_config['ai']['provider']\n @config.set(:ai, :sublayer, :model, value: old_config['ai']['model']) if old_config['ai']['model']\n end\n end\n\n ##\n # Migrate old sublayer.yml configuration to new format\n #\n # @param old_sublayer [Hash] The old sublayer configuration hash\n def migrate_sublayer_config(old_sublayer)\n @config.set(:ai, :sublayer, :project_name, value: old_sublayer[:project_name]) if old_sublayer[:project_name]\n @config.set(:ai, :sublayer, :project_template, value: old_sublayer[:project_template]) if old_sublayer[:project_template]\n @config.set(:ai, :sublayer, :provider, value: old_sublayer[:ai_provider]) if old_sublayer[:ai_provider]\n @config.set(:ai, :sublayer, :model, value: old_sublayer[:ai_model]) if old_sublayer[:ai_model]\n end\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'GOOGLE_API_KEY' => ENV.fetch('GOOGLE_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'ANTHROPIC_API_KEY' => ENV.fetch('ANTHROPIC_API_KEY', nil),\n 'DEEPSEEK_API_KEY' => ENV.fetch('DEEPSEEK_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'BLUEPRINTS_DEBUG' => ENV.fetch('BLUEPRINTS_DEBUG', nil),\n 'DEBUG' => ENV.fetch('DEBUG', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = value ? (key.include?('KEY') ? 'Set (hidden)' : value) : 'Not set'\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection\n require 'sequel'\n db_url = @config.database_url\n \n if db_url.nil? || db_url.empty?\n BlueprintsCLI.logger.failure('No database URL configured')\n return false\n end\n \n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connections by checking for API keys.\n #\n # Tests both Sublayer and Ruby LLM configurations.\n #\n # @return [Boolean] `true` if at least one API key is found, `false` otherwise.\n def test_ai_connections\n success_count = 0\n total_tests = 0\n \n # Test Sublayer configuration\n sublayer_provider = @config.fetch(:ai, :sublayer, :provider, default: '').downcase\n if !sublayer_provider.empty?\n total_tests += 1\n api_key = @config.ai_api_key(sublayer_provider)\n if api_key\n BlueprintsCLI.logger.success(\"Sublayer AI API key found for #{sublayer_provider}\")\n success_count += 1\n else\n BlueprintsCLI.logger.failure(\"Sublayer AI API key not found for #{sublayer_provider}\")\n end\n end\n \n # Test Ruby LLM configuration\n ruby_llm_config = @config.ruby_llm_config\n api_keys = ruby_llm_config.select { |k, v| k.to_s.end_with?('_api_key') && v }\n \n if api_keys.any?\n api_keys.each do |key, _|\n provider = key.to_s.gsub('_api_key', '')\n BlueprintsCLI.logger.success(\"Ruby LLM API key found for #{provider}\")\n success_count += 1\n total_tests += 1\n end\n else\n BlueprintsCLI.logger.warn('No Ruby LLM API keys configured')\n end\n \n if total_tests == 0\n BlueprintsCLI.logger.warn('No AI providers configured')\n false\n else\n success_count > 0\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor\n editor = @config.fetch(:blueprints, :editor, default: 'vim')\n \n if editor.nil? || editor.empty?\n BlueprintsCLI.logger.failure('No editor configured')\n return false\n end\n \n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n \n ##\n # Tests the logger configuration.\n #\n # @return [Boolean] `true` if logger config is valid, `false` otherwise.\n def test_logger_config\n level = @config.fetch(:logger, :level, default: 'info')\n valid_levels = %w[debug info warn error fatal]\n \n unless valid_levels.include?(level.to_s.downcase)\n BlueprintsCLI.logger.failure(\"Invalid logger level: #{level}\")\n return false\n end\n \n if @config.fetch(:logger, :file_logging, default: false)\n file_path = @config.fetch(:logger, :file_path)\n if file_path.nil? || file_path.empty?\n BlueprintsCLI.logger.failure('File logging enabled but no file path configured')\n return false\n end\n \n # Check if directory is writable\n dir = File.dirname(File.expand_path(file_path))\n unless File.directory?(dir) || File.writable?(File.dirname(dir))\n BlueprintsCLI.logger.failure(\"Log directory not writable: #{dir}\")\n return false\n end\n end\n \n BlueprintsCLI.logger.success('Logger configuration is valid')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Logger configuration test failed: #{e.message}\")\n false\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}, "4b5279751be640b14a689d95edce16b72de919e2": {"t": 1785756142.3559816, "n": 17065, "h": "7fe6bb69dc1f7fee", "c": "# frozen_string_literal: true\n\nmodule BlueprintsCLI\n module Actions\n # Manages the application's configuration through a command-line interface.\n # This action allows users to show, set up, test, and reset the configuration,\n # which is stored in a YAML file. It handles settings for the database,\n # AI provider, editor, and various feature flags.\n #\n # @example Show the current configuration\n # BlueprintsCLI::Actions::Config.new(subcommand: 'show').call\n #\n # @example Run the interactive setup\n # BlueprintsCLI::Actions::Config.new(subcommand: 'setup').call\n class Config < Sublayer::Actions::Base\n # The path to the YAML file where configuration is stored.\n CONFIG_PATH = File.join(__dir__, '..', 'config', 'blueprints.yml')\n\n ##\n # Initializes the configuration action.\n #\n # @param subcommand [String] The configuration command to execute.\n # Defaults to 'show'. Supported values: 'show', 'view', 'setup',\n # 'init', 'edit', 'test', 'reset'.\n def initialize(subcommand: 'show')\n @subcommand = subcommand\n end\n\n ##\n # Executes the specified configuration subcommand.\n #\n # This is the main entry point for the action. It routes to the appropriate\n # method based on the subcommand provided during initialization. It also\n # includes error handling to catch and report issues during execution.\n #\n # @return [Boolean] Returns `true` on success and `false` on failure or\n # if an unknown subcommand is provided.\n def call\n case @subcommand\n when 'show', 'view'\n show_configuration\n when 'setup', 'init', 'edit'\n setup_configuration\n when 'test'\n test_configuration\n when 'reset'\n reset_configuration\n else\n BlueprintsCLI.logger.failure(\"Unknown config subcommand: '#{@subcommand}'\")\n show_config_help\n false\n end\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error managing configuration: #{e.message}\")\n BlueprintsCLI.logger.debug(e) # tty-logger will format the exception and backtrace\n false\n end\n\n private\n\n ##\n # Displays the current configuration settings.\n #\n # Reads from the configuration file and prints a formatted summary of all\n # settings, including database, AI, editor, and feature flags. Also shows\n # the status of relevant environment variables.\n #\n # @return [Boolean] Returns `true`.\n def show_configuration\n config = load_configuration\n\n BlueprintsCLI.logger.step('Blueprint Configuration')\n puts '=' * 60\n puts \"Config file: #{CONFIG_PATH}\"\n puts \"File exists: #{File.exist?(CONFIG_PATH) ? 'Yes' : 'No'}\"\n puts ''\n\n if config\n puts 'Database Configuration:'.colorize(:cyan)\n puts \" URL: #{mask_password(config.dig('database', 'url') || 'Not set')}\"\n puts ''\n\n puts 'AI Configuration:'.colorize(:cyan)\n puts \" Provider: #{config.dig('ai', 'provider') || 'Not set'}\"\n puts \" Model: #{config.dig('ai', 'model') || 'Not set'}\"\n puts \" API Key: #{config.dig('ai', 'api_key') ? 'Set' : 'Not set'}\"\n puts ''\n\n puts 'Editor Configuration:'.colorize(:cyan)\n puts \" Editor: #{config.dig('editor') || 'Not set'}\"\n puts \" Auto-save: #{config.dig('auto_save_edits') || 'Not set'}\"\n puts ''\n\n puts 'Feature Flags:'.colorize(:cyan)\n puts \" Auto-description: #{config.dig('features', 'auto_description') || 'Not set'}\"\n puts \" Auto-categorization: #{config.dig('features', 'auto_categorize') || 'Not set'}\"\n puts \" Debug mode: #{config.dig('debug') || 'Not set'}\"\n else\n BlueprintsCLI.logger.failure('No configuration found')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create configuration\")\n end\n\n puts '=' * 60\n puts ''\n\n # Show environment variables\n show_environment_variables\n\n true\n end\n\n ##\n # Runs an interactive setup process for configuration.\n #\n # Prompts the user for all necessary configuration values, such as database\n # URL, AI provider/model, and editor preferences. It then saves the\n # resulting configuration to the YAML file.\n #\n # @return [Boolean] Returns `true` if the configuration is saved\n # successfully, `false` otherwise.\n def setup_configuration\n BlueprintsCLI.logger.step('Blueprint Configuration Setup')\n puts '=' * 50\n puts ''\n\n config = load_configuration || {}\n\n # Database configuration\n puts 'šŸ“Š Database Configuration'.colorize(:cyan)\n current_db = config.dig('database', 'url') || 'postgres://localhost/blueprints_development'\n db_url = prompt_for_input('Database URL', current_db)\n\n config['database'] = { 'url' => db_url }\n\n # AI configuration\n puts \"\\nšŸ¤– AI Configuration\".colorize(:cyan)\n current_provider = config.dig('ai', 'provider') || 'gemini'\n provider = prompt_for_choice('AI Provider', %w[gemini openai], current_provider)\n\n current_model = config.dig('ai',\n 'model') || (provider == 'gemini' ? 'text-embedding-004' : 'text-embedding-3-small')\n model = prompt_for_input('AI Model', current_model)\n\n puts 'šŸ’” Set API key via environment variable:'.colorize(:yellow)\n puts ' export GEMINI_API_KEY=your_key_here' if provider == 'gemini'\n puts ' export OPENAI_API_KEY=your_key_here' if provider == 'openai'\n\n config['ai'] = {\n 'provider' => provider,\n 'model' => model\n }\n\n # Editor configuration\n puts \"\\nāœļø Editor Configuration\".colorize(:cyan)\n current_editor = config.dig('editor') || ENV['EDITOR'] || ENV['VISUAL'] || 'vim'\n editor = prompt_for_input('Preferred editor', current_editor)\n\n current_auto_save = config.dig('auto_save_edits')\n auto_save = prompt_for_boolean('Auto-save edits', current_auto_save.nil? || current_auto_save)\n\n config['editor'] = editor\n config['auto_save_edits'] = auto_save\n\n # Feature flags\n puts \"\\nšŸŽ›ļø Feature Configuration\".colorize(:cyan)\n current_auto_desc = config.dig('features', 'auto_description')\n auto_desc = prompt_for_boolean('Auto-generate descriptions', current_auto_desc.nil? || current_auto_desc)\n\n current_auto_cat = config.dig('features', 'auto_categorize')\n auto_cat = prompt_for_boolean('Auto-generate categories', current_auto_cat.nil? || current_auto_cat)\n\n current_debug = config.dig('debug')\n debug = prompt_for_boolean('Debug mode', current_debug || false)\n\n config['features'] = {\n 'auto_description' => auto_desc,\n 'auto_categorize' => auto_cat\n }\n config['debug'] = debug\n\n # Save configuration\n puts \"\\nšŸ’¾ Saving Configuration\".colorize(:blue)\n save_success = save_configuration(config)\n\n if save_success\n BlueprintsCLI.logger.success('Configuration saved successfully!', file: CONFIG_PATH)\n BlueprintsCLI.logger.tip(\"Run 'blueprint config test' to validate the configuration\")\n else\n BlueprintsCLI.logger.failure('Failed to save configuration')\n end\n\n save_success\n end\n\n ##\n # Tests the validity and connectivity of the current configuration.\n #\n # Checks the database connection, AI provider API key, and editor\n # availability based on the settings in the configuration file.\n #\n # @return [Boolean] Returns `true` if all tests pass, `false` otherwise.\n def test_configuration\n BlueprintsCLI.logger.step('Testing Blueprint Configuration')\n puts '=' * 50\n\n config = load_configuration\n unless config\n BlueprintsCLI.logger.failure('No configuration found')\n return false\n end\n\n all_tests_passed = true\n\n # Test database connection\n puts \"\\nšŸ“Š Testing database connection...\".colorize(:cyan)\n db_success = test_database_connection(config)\n all_tests_passed &&= db_success\n\n # Test AI API\n puts \"\\nšŸ¤– Testing AI API connection...\".colorize(:cyan)\n ai_success = test_ai_connection(config)\n all_tests_passed &&= ai_success\n\n # Test editor\n puts \"\\nāœļø Testing editor availability...\".colorize(:cyan)\n editor_success = test_editor(config)\n all_tests_passed &&= editor_success\n\n puts \"\\n\" + ('=' * 50)\n if all_tests_passed\n BlueprintsCLI.logger.success('All configuration tests passed!')\n else\n BlueprintsCLI.logger.failure('Some configuration tests failed')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to fix issues\")\n end\n\n all_tests_passed\n end\n\n ##\n # Deletes the configuration file, resetting to defaults.\n #\n # Prompts the user for confirmation before deleting the `blueprints.yml` file.\n #\n # @return [Boolean] Returns `true` if the file is deleted or if it didn't\n # exist initially. Returns `false` if the user cancels the operation.\n def reset_configuration\n if File.exist?(CONFIG_PATH)\n print 'āš ļø This will delete the existing configuration. Continue? (y/N): '\n response = STDIN.gets.chomp.downcase\n\n if %w[y yes].include?(response)\n File.delete(CONFIG_PATH)\n BlueprintsCLI.logger.success('Configuration reset successfully')\n BlueprintsCLI.logger.tip(\"Run 'blueprint config setup' to create new configuration\")\n true\n else\n BlueprintsCLI.logger.warn('Reset cancelled')\n false\n end\n else\n BlueprintsCLI.logger.info('No configuration file found to reset')\n true\n end\n end\n\n ##\n # Displays help text for the configuration command.\n # @return [nil]\n def show_config_help\n puts <<~HELP\n Blueprint Configuration Commands:\n\n blueprint config show Show current configuration\n blueprint config setup Interactive configuration setup\n blueprint config test Test configuration connectivity\n blueprint config reset Reset configuration to defaults\n\n Configuration is stored in: #{CONFIG_PATH}\n HELP\n end\n\n ##\n # Loads the configuration from the YAML file.\n #\n # @return [Hash, nil] A hash containing the configuration, or `nil` if the\n # file does not exist or an error occurs during loading.\n def load_configuration\n return nil unless File.exist?(CONFIG_PATH)\n\n YAML.load_file(CONFIG_PATH)\n rescue StandardError => e\n BlueprintsCLI.logger.warn(\"Error loading configuration: #{e.message}\")\n nil\n end\n\n ##\n # Saves the given configuration hash to the YAML file.\n #\n # Ensures the directory exists before writing the file.\n #\n # @param config [Hash] The configuration hash to save.\n # @return [Boolean] `true` on successful save, `false` otherwise.\n def save_configuration(config)\n # Ensure directory exists\n config_dir = File.dirname(CONFIG_PATH)\n FileUtils.mkdir_p(config_dir) unless Dir.exist?(config_dir)\n\n File.write(CONFIG_PATH, config.to_yaml)\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Error saving configuration: #{e.message}\")\n false\n end\n\n ##\n # Displays the status of relevant environment variables.\n # @return [void]\n def show_environment_variables\n puts 'šŸŒ Environment Variables:'.colorize(:blue)\n\n env_vars = {\n 'GEMINI_API_KEY' => ENV.fetch('GEMINI_API_KEY', nil),\n 'OPENAI_API_KEY' => ENV.fetch('OPENAI_API_KEY', nil),\n 'BLUEPRINT_DATABASE_URL' => ENV.fetch('BLUEPRINT_DATABASE_URL', nil),\n 'DATABASE_URL' => ENV.fetch('DATABASE_URL', nil),\n 'EDITOR' => ENV.fetch('EDITOR', nil),\n 'VISUAL' => ENV.fetch('VISUAL', nil)\n }\n\n env_vars.each do |key, value|\n status = value ? 'Set' : 'Not set'\n puts \" #{key}: #{status}\"\n end\n puts ''\n end\n\n ##\n # Tests the database connection using the URL from the configuration.\n #\n # @param config [Hash] The loaded configuration hash.\n # @return [Boolean] `true` if the connection is successful, `false` otherwise.\n def test_database_connection(config)\n require 'sequel'\n db_url = config.dig('database', 'url')\n db = Sequel.connect(db_url)\n db.test_connection\n BlueprintsCLI.logger.success('Database connection successful')\n true\n rescue StandardError => e\n BlueprintsCLI.logger.failure(\"Database connection failed: #{e.message}\")\n false\n end\n\n ##\n # Tests the AI provider connection by checking for an API key.\n #\n # This is a simplified test that only checks if the relevant environment\n # variable for the configured provider is set.\n #\n # @param config [Hash] The loaded configuration hash.\n # @return [Boolean] `true` if the API key is found, `false` otherwise.\n def test_ai_connection(config)\n # This is a simplified test - in reality you'd make an actual API call\n provider = config.dig('ai', 'provider')\n api_key = case provider\n when 'gemini'\n ENV.fetch('GEMINI_API_KEY', nil)\n when 'openai'\n ENV.fetch('OPENAI_API_KEY', nil)\n end\n\n if api_key\n BlueprintsCLI.logger.success(\"AI API key found for #{provider}\")\n true\n else\n BlueprintsCLI.logger.failure(\"AI API key not found for #{provider}\")\n false\n end\n end\n\n ##\n # Tests if the configured editor is available in the system's PATH.\n #\n # @param config [Hash] The loaded configuration hash.\n # @return [Boolean] `true` if the editor command is found, `false` otherwise.\n def test_editor(config)\n editor = config.dig('editor')\n if system(\"which #{editor} > /dev/null 2>&1\")\n BlueprintsCLI.logger.success(\"Editor '#{editor}' found\")\n true\n else\n BlueprintsCLI.logger.failure(\"Editor '#{editor}' not found\")\n false\n end\n end\n\n ##\n # Prompts the user for text input via STDIN.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [String, nil] The default value to use if the user enters nothing.\n # @return [String] The user's input or the default value.\n def prompt_for_input(prompt, default = nil)\n print \"#{prompt}\"\n print \" [#{default}]\" if default\n print ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user to select from a list of choices.\n #\n # @param prompt [String] The message to display to the user.\n # @param choices [Array] A list of available options.\n # @param default [String, nil] The default choice if the user enters nothing.\n # @return [String] The user's selection or the default value.\n def prompt_for_choice(prompt, choices, default = nil)\n puts \"#{prompt} (#{choices.join('/')})\"\n print default ? \"[#{default}]: \" : ': '\n\n input = STDIN.gets.chomp\n input.empty? ? default : input\n end\n\n ##\n # Prompts the user for a boolean (yes/no) response.\n #\n # @param prompt [String] The message to display to the user.\n # @param default [Boolean, nil] The default value (`true`, `false`, or `nil`)\n # to use if the user enters nothing.\n # @return [Boolean, nil] Returns `true` for 'y', `false` for 'n', or the\n # default value for any other input.\n def prompt_for_boolean(prompt, default = nil)\n default_text = case default\n when true then ' [Y/n]'\n when false then ' [y/N]'\n else ' [y/n]'\n end\n\n print \"#{prompt}#{default_text}: \"\n input = STDIN.gets.chomp.downcase\n\n case input\n when 'y', 'yes', 'true'\n true\n when 'n', 'no', 'false'\n false\n else\n default\n end\n end\n\n ##\n # Masks the password portion of a database URL for safe display.\n #\n # @param url [String] The database URL to process.\n # @return [String] The URL with the password replaced by '***'.\n def mask_password(url)\n return url unless url.include?(':') && url.include?('@')\n\n url.gsub(/:[^:@]*@/, ':***@')\n end\n end\n end\nend\n"}}, "lib/blueprintsCLI/setup/model_configurator.rb": {"64a20b367209eac0d73cc0df9909e85d962ad6d5": {"t": 1785745750.4862425, "n": 17168, "h": "452eaa8d21d51698", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule BlueprintsCLI\n module Setup\n # ModelConfigurator handles AI model discovery, selection, and configuration.\n # It uses RubyLLM's model registry to discover available models from configured\n # providers and helps users select appropriate models for different tasks.\n class ModelConfigurator\n # Model categories and their purposes\n MODEL_CATEGORIES = {\n chat: {\n name: \"Chat Models\",\n description: \"For conversational AI, code generation, and general tasks\",\n required: true,\n },\n embedding: {\n name: \"Embedding Models\",\n description: \"For vector search and semantic similarity\",\n required: true,\n },\n image: {\n name: \"Image Generation Models\",\n description: \"For creating images from text descriptions\",\n required: false,\n },\n }.freeze\n\n # Initialize the model configurator\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @available_models = {}\n @selected_models = {}\n end\n\n # Discover and configure AI models\n #\n # @return [Boolean] True if model configuration completed successfully\n def discover_and_configure\n return false unless providers_configured?\n\n @logger.info(\"Discovering available AI models...\")\n\n configure_rubyllm_from_setup\n discover_models\n display_model_summary\n configure_model_preferences\n finalize_model_configuration\n\n true\n end\n\n private def providers_configured?\n if @setup_data[:providers]&.any?\n true\n else\n @logger.failure(\"No AI providers configured. Please run provider setup first.\")\n false\n end\n end\n\n # Configure RubyLLM with setup data\n private def configure_rubyllm_from_setup\n @logger.info(\"Configuring RubyLLM with detected providers...\")\n\n RubyLLM.configure do |config|\n @setup_data[:providers].each do |provider_key, provider_config|\n case provider_key\n when :openai\n config.openai_api_key = provider_config[:api_key]\n when :openrouter\n config.openai_api_key = provider_config[:api_key]\n config.openai_api_base = \"https://openrouter.ai/api/v1\"\n when :anthropic\n config.anthropic_api_key = provider_config[:api_key]\n when :gemini\n config.gemini_api_key = provider_config[:api_key]\n when :deepseek\n config.deepseek_api_key = provider_config[:api_key]\n end\n end\n end\n end\n\n # Discover available models from configured providers\n private def discover_models\n @logger.info(\"Refreshing model registry...\")\n RubyLLM.models.refresh!\n\n # Categorize available models\n @available_models[:chat] = filter_chat_models\n @available_models[:embedding] = filter_embedding_models\n @available_models[:image] = filter_image_models\n\n @logger.success(\"Model discovery completed!\")\n rescue => e\n @logger.failure(\"Model discovery failed: #{e.message}\")\n @logger.debug(e.backtrace.join(\"\\n\")) if ENV[\"DEBUG\"]\n fallback_to_default_models\n end\n\n # Filter chat models from available providers\n #\n # @return [Array] Available chat models with metadata\n private def filter_chat_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.chat_models.select do |model|\n provider_symbol = model.provider.to_sym\n # Map openrouter to openai for filtering\n provider_symbol = :openrouter if provider_symbol == :openai &&\n @setup_data[:providers].key?(:openrouter) &&\n !@setup_data[:providers].key?(:openai)\n\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n context_window: model.context_window,\n supports_vision: model.supports_vision?,\n supports_tools: model.supports_functions?,\n input_price: model.input_price_per_million,\n output_price: model.output_price_per_million,\n family: model.family,\n }\n end\n end\n\n # Filter embedding models from available providers\n #\n # @return [Array] Available embedding models with metadata\n private def filter_embedding_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.embedding_models.select do |model|\n provider_symbol = model.provider.to_sym\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n dimensions: model.respond_to?(:dimensions) ? model.dimensions : \"Unknown\",\n input_price: model.input_price_per_million,\n }\n end\n end\n\n # Filter image generation models from available providers\n #\n # @return [Array] Available image models with metadata\n private def filter_image_models\n provider_keys = @setup_data[:providers].keys\n\n # NOTE: RubyLLM may not have a specific image_models filter\n # This is a placeholder implementation\n all_models = begin\n RubyLLM.models.all.select do |model|\n model.respond_to?(:type) &&\n model.type == \"image\" &&\n provider_keys.include?(model.provider.to_sym)\n end\n rescue\n []\n end\n\n all_models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n max_resolution: model.respond_to?(:max_resolution) ? model.max_resolution : \"Unknown\",\n }\n end\n end\n\n # Display summary of discovered models\n private def display_model_summary\n puts \"\\nšŸ“Š Model Discovery Summary:\"\n\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n status = models.any? ? \"āœ“\" : \"āœ—\"\n count = models.size\n\n puts \" #{status} #{info[:name]}: #{count} models available\"\n\n if models.any? && count <= 5\n models.each do |model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" - #{model[:name]} (#{provider_name})\"\n end\n elsif count > 5\n puts \" - #{models.first(3).map do |m|\n m[:name]\n end.join(', ')}, and #{count - 3} more...\"\n end\n end\n puts \"\"\n end\n\n # Configure model preferences interactively\n private def configure_model_preferences\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n\n if models.empty?\n @logger.warn(\"No #{info[:name].downcase} available. Some features may not work.\") if info[:required]\n next\n end\n\n configure_category_models(category, info, models)\n end\n end\n\n # Configure models for a specific category\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models [Array] Available models for category\n private def configure_category_models(category, info, models)\n puts \"\\nšŸ¤– #{info[:name]} Configuration\"\n puts \"Purpose: #{info[:description]}\"\n\n if models.size == 1\n # Auto-select single model\n model = models.first\n @selected_models[category] = model\n @logger.info(\"Auto-selected: #{model[:name]} (#{model[:provider]})\")\n return\n end\n\n # Group models by provider\n models_by_provider = group_models_by_provider(models)\n\n if models_by_provider.size == 1\n # Only one provider, show models directly\n provider_key = models_by_provider.keys.first\n select_model_from_provider(category, info, provider_key, models_by_provider[provider_key])\n else\n # Multiple providers, let user choose provider first\n select_model_with_provider_separation(category, info, models_by_provider)\n end\n\n # Show additional model info\n return unless @selected_models[category]\n\n display_model_details(@selected_models[category]) if @prompt.yes?(\"Show model details?\",\n default: false)\n end\n\n # Group models by provider for organized selection\n #\n # @param models [Array] Available models\n # @return [Hash] Models grouped by provider\n private def group_models_by_provider(models)\n models.group_by { |model| model[:provider] }\n end\n\n # Select model when only one provider is available\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param provider [String] Provider name\n # @param provider_models [Array] Models from this provider\n private def select_model_from_provider(category, info, provider, provider_models)\n provider_name = get_provider_display_name(provider)\n puts \"\\nAvailable #{info[:name].downcase} from #{provider_name}:\"\n\n choices = build_model_choices(provider_models)\n\n selected = @prompt.select(\n \"Choose #{info[:name].downcase}:\",\n choices,\n cycle: true,\n filter: true,\n help: \"(Use ↑/↓ arrows to navigate, Enter to select, type to filter)\"\n )\n\n @selected_models[category] = selected\n end\n\n # Select model with provider separation for multiple providers\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models_by_provider [Hash] Models grouped by provider\n private def select_model_with_provider_separation(category, info, models_by_provider)\n # First, let user choose provider\n provider_choices = models_by_provider.map do |provider, provider_models|\n provider_name = get_provider_display_name(provider)\n model_count = provider_models.size\n {\n name: \"#{provider_name} (#{model_count} models)\",\n value: provider,\n }\n end\n\n selected_provider = @prompt.select(\n \"Choose AI provider for #{info[:name].downcase}:\",\n provider_choices,\n cycle: true,\n help: \"(Use ↑/↓ arrows to navigate, Enter to select)\"\n )\n\n # Then select model from chosen provider\n select_model_from_provider(category, info, selected_provider,\n models_by_provider[selected_provider])\n end\n\n # Build model choices with pricing and capability info\n #\n # @param models [Array] Models to build choices for\n # @return [Array] Formatted choices for TTY::Prompt\n private def build_model_choices(models)\n models.map do |model|\n price_info = if model[:input_price]\n \" ($#{model[:input_price]}/1M tokens)\"\n else\n \"\"\n end\n\n # Add capability indicators\n capabilities = []\n capabilities << \"šŸ‘ļø\" if model[:supports_vision]\n capabilities << \"šŸ”§\" if model[:supports_tools]\n capability_info = capabilities.any? ? \" #{capabilities.join(' ')}\" : \"\"\n\n description = \"#{model[:name]}#{price_info}#{capability_info}\"\n { name: description, value: model }\n end\n end\n\n # Display detailed information about a model\n #\n # @param model [Hash] Model information\n private def display_model_details(model)\n puts \"\\nšŸ“‹ Model Details:\"\n puts \" ID: #{model[:id]}\"\n puts \" Provider: #{model[:provider]}\"\n\n if model[:context_window]\n formatted_window = model[:context_window].to_s.reverse.gsub(/(\\d{3})(?=\\d)/,\n '\\\\1,').reverse\n puts \" Context Window: #{formatted_window} tokens\"\n end\n\n puts \" Vision Support: Yes\" if model[:supports_vision]\n\n puts \" Function Calling: Yes\" if model[:supports_tools]\n\n puts \" Input Cost: $#{model[:input_price]}/1M tokens\" if model[:input_price]\n\n puts \" Output Cost: $#{model[:output_price]}/1M tokens\" if model[:output_price]\n\n puts \"\"\n end\n\n # Finalize model configuration\n private def finalize_model_configuration\n @setup_data[:models] = {}\n\n @selected_models.each do |category, model|\n @setup_data[:models][category] = {\n id: model[:id],\n name: model[:name],\n provider: model[:provider],\n capabilities: extract_model_capabilities(model),\n }\n end\n\n # Set default models for AI configuration\n if @selected_models[:chat]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_model] = @selected_models[:chat][:id]\n @setup_data[:ai][:default_provider] = @selected_models[:chat][:provider]\n end\n\n if @selected_models[:embedding]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_embedding_model] = @selected_models[:embedding][:id]\n end\n\n @logger.success(\"Model configuration completed!\")\n display_final_model_summary\n end\n\n # Extract capabilities from model information\n #\n # @param model [Hash] Model information\n # @return [Array] List of capabilities\n private def extract_model_capabilities(model)\n capabilities = []\n capabilities << \"vision\" if model[:supports_vision]\n capabilities << \"tools\" if model[:supports_tools]\n capabilities << \"embedding\" if model[:dimensions]\n capabilities\n end\n\n # Display final model configuration summary\n private def display_final_model_summary\n puts \"\\nāœ… Selected Models:\"\n @selected_models.each do |category, model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" #{category.to_s.capitalize}: #{model[:name]} (#{provider_name})\"\n end\n puts \"\"\n end\n\n # Fallback to default models if discovery fails\n private def fallback_to_default_models\n @logger.warn(\"Using fallback default models...\")\n\n primary_provider = @setup_data[:primary_provider]\n @setup_data[:providers][primary_provider]\n\n case primary_provider\n when :openai, :openrouter\n @available_models[:chat] = [\n{\n id: \"gpt-4o-mini\",\n name: \"GPT-4o Mini\",\n provider: \"openai\",\n supports_vision: true,\n supports_tools: true,\n},\n]\n @available_models[:embedding] = [\n{\n id: \"text-embedding-3-small\",\n name: \"Text Embedding 3 Small\",\n provider: \"openai\",\n},\n]\n when :anthropic\n @available_models[:chat] = [\n{\n id: \"claude-3-haiku-20240307\",\n name: \"Claude 3 Haiku\",\n provider: \"anthropic\",\n supports_tools: true,\n},\n]\n when :gemini\n @available_models[:chat] = [\n{\n id: \"gemini-2.0-flash\",\n name: \"Gemini 2.0 Flash\",\n provider: \"gemini\",\n supports_vision: true,\n supports_tools: true,\n},\n]\n @available_models[:embedding] = [\n{\n id: \"text-embedding-004\",\n name: \"Text Embedding 004\",\n provider: \"gemini\",\n},\n]\n when :deepseek\n @available_models[:chat] = [\n{\n id: \"deepseek-chat\",\n name: \"DeepSeek Chat\",\n provider: \"deepseek\",\n supports_tools: true,\n},\n]\n end\n\n @available_models[:image] = []\n end\n\n # Get display name for a provider, handling mismatches safely\n #\n # @param provider [String, Symbol] Provider identifier from model\n # @return [String] Human-readable provider name\n private def get_provider_display_name(provider)\n provider_key = map_provider_to_key(provider)\n\n if @setup_data[:providers] && @setup_data[:providers][provider_key]\n @setup_data[:providers][provider_key][:name]\n else\n # Fallback to capitalized provider name\n provider.to_s.capitalize\n end\n end\n\n # Map model provider to setup data key\n #\n # @param provider [String, Symbol] Provider from model\n # @return [Symbol] Provider key for setup data\n private def map_provider_to_key(provider)\n case provider.to_s.downcase\n when \"openai\"\n # Could be either openai or openrouter\n if @setup_data[:providers]&.key?(:openrouter) && !@setup_data[:providers]&.key?(:openai)\n :openrouter\n else\n :openai\n end\n when \"google\", \"gemini\"\n :gemini\n when \"anthropic\"\n :anthropic\n when \"deepseek\"\n :deepseek\n else\n provider.to_sym\n end\n end\n end\n end\nend\n"}, "48eef306e5e764c0ea0da7069ba82b107409bba0": {"t": 1785756131.647955, "n": 17168, "h": "452eaa8d21d51698", "c": "# frozen_string_literal: true\n\nrequire \"ruby_llm\"\n\nmodule BlueprintsCLI\n module Setup\n # ModelConfigurator handles AI model discovery, selection, and configuration.\n # It uses RubyLLM's model registry to discover available models from configured\n # providers and helps users select appropriate models for different tasks.\n class ModelConfigurator\n # Model categories and their purposes\n MODEL_CATEGORIES = {\n chat: {\n name: \"Chat Models\",\n description: \"For conversational AI, code generation, and general tasks\",\n required: true,\n },\n embedding: {\n name: \"Embedding Models\",\n description: \"For vector search and semantic similarity\",\n required: true,\n },\n image: {\n name: \"Image Generation Models\",\n description: \"For creating images from text descriptions\",\n required: false,\n },\n }.freeze\n\n # Initialize the model configurator\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @available_models = {}\n @selected_models = {}\n end\n\n # Discover and configure AI models\n #\n # @return [Boolean] True if model configuration completed successfully\n def discover_and_configure\n return false unless providers_configured?\n\n @logger.info(\"Discovering available AI models...\")\n\n configure_rubyllm_from_setup\n discover_models\n display_model_summary\n configure_model_preferences\n finalize_model_configuration\n\n true\n end\n\n private def providers_configured?\n if @setup_data[:providers]&.any?\n true\n else\n @logger.failure(\"No AI providers configured. Please run provider setup first.\")\n false\n end\n end\n\n # Configure RubyLLM with setup data\n private def configure_rubyllm_from_setup\n @logger.info(\"Configuring RubyLLM with detected providers...\")\n\n RubyLLM.configure do |config|\n @setup_data[:providers].each do |provider_key, provider_config|\n case provider_key\n when :openai\n config.openai_api_key = provider_config[:api_key]\n when :openrouter\n config.openai_api_key = provider_config[:api_key]\n config.openai_api_base = \"https://openrouter.ai/api/v1\"\n when :anthropic\n config.anthropic_api_key = provider_config[:api_key]\n when :gemini\n config.gemini_api_key = provider_config[:api_key]\n when :deepseek\n config.deepseek_api_key = provider_config[:api_key]\n end\n end\n end\n end\n\n # Discover available models from configured providers\n private def discover_models\n @logger.info(\"Refreshing model registry...\")\n RubyLLM.models.refresh!\n\n # Categorize available models\n @available_models[:chat] = filter_chat_models\n @available_models[:embedding] = filter_embedding_models\n @available_models[:image] = filter_image_models\n\n @logger.success(\"Model discovery completed!\")\n rescue => e\n @logger.failure(\"Model discovery failed: #{e.message}\")\n @logger.debug(e.backtrace.join(\"\\n\")) if ENV[\"DEBUG\"]\n fallback_to_default_models\n end\n\n # Filter chat models from available providers\n #\n # @return [Array] Available chat models with metadata\n private def filter_chat_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.chat_models.select do |model|\n provider_symbol = model.provider.to_sym\n # Map openrouter to openai for filtering\n provider_symbol = :openrouter if provider_symbol == :openai &&\n @setup_data[:providers].key?(:openrouter) &&\n !@setup_data[:providers].key?(:openai)\n\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n context_window: model.context_window,\n supports_vision: model.supports_vision?,\n supports_tools: model.supports_functions?,\n input_price: model.input_price_per_million,\n output_price: model.output_price_per_million,\n family: model.family,\n }\n end\n end\n\n # Filter embedding models from available providers\n #\n # @return [Array] Available embedding models with metadata\n private def filter_embedding_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.embedding_models.select do |model|\n provider_symbol = model.provider.to_sym\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n dimensions: model.respond_to?(:dimensions) ? model.dimensions : \"Unknown\",\n input_price: model.input_price_per_million,\n }\n end\n end\n\n # Filter image generation models from available providers\n #\n # @return [Array] Available image models with metadata\n private def filter_image_models\n provider_keys = @setup_data[:providers].keys\n\n # NOTE: RubyLLM may not have a specific image_models filter\n # This is a placeholder implementation\n all_models = begin\n RubyLLM.models.all.select do |model|\n model.respond_to?(:type) &&\n model.type == \"image\" &&\n provider_keys.include?(model.provider.to_sym)\n end\n rescue\n []\n end\n\n all_models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n max_resolution: model.respond_to?(:max_resolution) ? model.max_resolution : \"Unknown\",\n }\n end\n end\n\n # Display summary of discovered models\n private def display_model_summary\n puts \"\\nšŸ“Š Model Discovery Summary:\"\n\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n status = models.any? ? \"āœ“\" : \"āœ—\"\n count = models.size\n\n puts \" #{status} #{info[:name]}: #{count} models available\"\n\n if models.any? && count <= 5\n models.each do |model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" - #{model[:name]} (#{provider_name})\"\n end\n elsif count > 5\n puts \" - #{models.first(3).map do |m|\n m[:name]\n end.join(', ')}, and #{count - 3} more...\"\n end\n end\n puts \"\"\n end\n\n # Configure model preferences interactively\n private def configure_model_preferences\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n\n if models.empty?\n @logger.warn(\"No #{info[:name].downcase} available. Some features may not work.\") if info[:required]\n next\n end\n\n configure_category_models(category, info, models)\n end\n end\n\n # Configure models for a specific category\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models [Array] Available models for category\n private def configure_category_models(category, info, models)\n puts \"\\nšŸ¤– #{info[:name]} Configuration\"\n puts \"Purpose: #{info[:description]}\"\n\n if models.size == 1\n # Auto-select single model\n model = models.first\n @selected_models[category] = model\n @logger.info(\"Auto-selected: #{model[:name]} (#{model[:provider]})\")\n return\n end\n\n # Group models by provider\n models_by_provider = group_models_by_provider(models)\n\n if models_by_provider.size == 1\n # Only one provider, show models directly\n provider_key = models_by_provider.keys.first\n select_model_from_provider(category, info, provider_key, models_by_provider[provider_key])\n else\n # Multiple providers, let user choose provider first\n select_model_with_provider_separation(category, info, models_by_provider)\n end\n\n # Show additional model info\n return unless @selected_models[category]\n\n display_model_details(@selected_models[category]) if @prompt.yes?(\"Show model details?\",\n default: false)\n end\n\n # Group models by provider for organized selection\n #\n # @param models [Array] Available models\n # @return [Hash] Models grouped by provider\n private def group_models_by_provider(models)\n models.group_by { |model| model[:provider] }\n end\n\n # Select model when only one provider is available\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param provider [String] Provider name\n # @param provider_models [Array] Models from this provider\n private def select_model_from_provider(category, info, provider, provider_models)\n provider_name = get_provider_display_name(provider)\n puts \"\\nAvailable #{info[:name].downcase} from #{provider_name}:\"\n\n choices = build_model_choices(provider_models)\n\n selected = @prompt.select(\n \"Choose #{info[:name].downcase}:\",\n choices,\n cycle: true,\n filter: true,\n help: \"(Use ↑/↓ arrows to navigate, Enter to select, type to filter)\"\n )\n\n @selected_models[category] = selected\n end\n\n # Select model with provider separation for multiple providers\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models_by_provider [Hash] Models grouped by provider\n private def select_model_with_provider_separation(category, info, models_by_provider)\n # First, let user choose provider\n provider_choices = models_by_provider.map do |provider, provider_models|\n provider_name = get_provider_display_name(provider)\n model_count = provider_models.size\n {\n name: \"#{provider_name} (#{model_count} models)\",\n value: provider,\n }\n end\n\n selected_provider = @prompt.select(\n \"Choose AI provider for #{info[:name].downcase}:\",\n provider_choices,\n cycle: true,\n help: \"(Use ↑/↓ arrows to navigate, Enter to select)\"\n )\n\n # Then select model from chosen provider\n select_model_from_provider(category, info, selected_provider,\n models_by_provider[selected_provider])\n end\n\n # Build model choices with pricing and capability info\n #\n # @param models [Array] Models to build choices for\n # @return [Array] Formatted choices for TTY::Prompt\n private def build_model_choices(models)\n models.map do |model|\n price_info = if model[:input_price]\n \" ($#{model[:input_price]}/1M tokens)\"\n else\n \"\"\n end\n\n # Add capability indicators\n capabilities = []\n capabilities << \"šŸ‘ļø\" if model[:supports_vision]\n capabilities << \"šŸ”§\" if model[:supports_tools]\n capability_info = capabilities.any? ? \" #{capabilities.join(' ')}\" : \"\"\n\n description = \"#{model[:name]}#{price_info}#{capability_info}\"\n { name: description, value: model }\n end\n end\n\n # Display detailed information about a model\n #\n # @param model [Hash] Model information\n private def display_model_details(model)\n puts \"\\nšŸ“‹ Model Details:\"\n puts \" ID: #{model[:id]}\"\n puts \" Provider: #{model[:provider]}\"\n\n if model[:context_window]\n formatted_window = model[:context_window].to_s.reverse.gsub(/(\\d{3})(?=\\d)/,\n '\\\\1,').reverse\n puts \" Context Window: #{formatted_window} tokens\"\n end\n\n puts \" Vision Support: Yes\" if model[:supports_vision]\n\n puts \" Function Calling: Yes\" if model[:supports_tools]\n\n puts \" Input Cost: $#{model[:input_price]}/1M tokens\" if model[:input_price]\n\n puts \" Output Cost: $#{model[:output_price]}/1M tokens\" if model[:output_price]\n\n puts \"\"\n end\n\n # Finalize model configuration\n private def finalize_model_configuration\n @setup_data[:models] = {}\n\n @selected_models.each do |category, model|\n @setup_data[:models][category] = {\n id: model[:id],\n name: model[:name],\n provider: model[:provider],\n capabilities: extract_model_capabilities(model),\n }\n end\n\n # Set default models for AI configuration\n if @selected_models[:chat]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_model] = @selected_models[:chat][:id]\n @setup_data[:ai][:default_provider] = @selected_models[:chat][:provider]\n end\n\n if @selected_models[:embedding]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_embedding_model] = @selected_models[:embedding][:id]\n end\n\n @logger.success(\"Model configuration completed!\")\n display_final_model_summary\n end\n\n # Extract capabilities from model information\n #\n # @param model [Hash] Model information\n # @return [Array] List of capabilities\n private def extract_model_capabilities(model)\n capabilities = []\n capabilities << \"vision\" if model[:supports_vision]\n capabilities << \"tools\" if model[:supports_tools]\n capabilities << \"embedding\" if model[:dimensions]\n capabilities\n end\n\n # Display final model configuration summary\n private def display_final_model_summary\n puts \"\\nāœ… Selected Models:\"\n @selected_models.each do |category, model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" #{category.to_s.capitalize}: #{model[:name]} (#{provider_name})\"\n end\n puts \"\"\n end\n\n # Fallback to default models if discovery fails\n private def fallback_to_default_models\n @logger.warn(\"Using fallback default models...\")\n\n primary_provider = @setup_data[:primary_provider]\n @setup_data[:providers][primary_provider]\n\n case primary_provider\n when :openai, :openrouter\n @available_models[:chat] = [\n{\n id: \"gpt-4o-mini\",\n name: \"GPT-4o Mini\",\n provider: \"openai\",\n supports_vision: true,\n supports_tools: true,\n},\n]\n @available_models[:embedding] = [\n{\n id: \"text-embedding-3-small\",\n name: \"Text Embedding 3 Small\",\n provider: \"openai\",\n},\n]\n when :anthropic\n @available_models[:chat] = [\n{\n id: \"claude-3-haiku-20240307\",\n name: \"Claude 3 Haiku\",\n provider: \"anthropic\",\n supports_tools: true,\n},\n]\n when :gemini\n @available_models[:chat] = [\n{\n id: \"gemini-2.0-flash\",\n name: \"Gemini 2.0 Flash\",\n provider: \"gemini\",\n supports_vision: true,\n supports_tools: true,\n},\n]\n @available_models[:embedding] = [\n{\n id: \"text-embedding-004\",\n name: \"Text Embedding 004\",\n provider: \"gemini\",\n},\n]\n when :deepseek\n @available_models[:chat] = [\n{\n id: \"deepseek-chat\",\n name: \"DeepSeek Chat\",\n provider: \"deepseek\",\n supports_tools: true,\n},\n]\n end\n\n @available_models[:image] = []\n end\n\n # Get display name for a provider, handling mismatches safely\n #\n # @param provider [String, Symbol] Provider identifier from model\n # @return [String] Human-readable provider name\n private def get_provider_display_name(provider)\n provider_key = map_provider_to_key(provider)\n\n if @setup_data[:providers] && @setup_data[:providers][provider_key]\n @setup_data[:providers][provider_key][:name]\n else\n # Fallback to capitalized provider name\n provider.to_s.capitalize\n end\n end\n\n # Map model provider to setup data key\n #\n # @param provider [String, Symbol] Provider from model\n # @return [Symbol] Provider key for setup data\n private def map_provider_to_key(provider)\n case provider.to_s.downcase\n when \"openai\"\n # Could be either openai or openrouter\n if @setup_data[:providers]&.key?(:openrouter) && !@setup_data[:providers]&.key?(:openai)\n :openrouter\n else\n :openai\n end\n when \"google\", \"gemini\"\n :gemini\n when \"anthropic\"\n :anthropic\n when \"deepseek\"\n :deepseek\n else\n provider.to_sym\n end\n end\n end\n end\nend\n"}, "d3c27d43989e4525f6c128e73a7e6ebcf57e8781": {"t": 1785756133.0399547, "n": 17720, "h": "aa5094f817b24b30", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule BlueprintsCLI\n module Setup\n # ModelConfigurator handles AI model discovery, selection, and configuration.\n # It uses RubyLLM's model registry to discover available models from configured\n # providers and helps users select appropriate models for different tasks.\n class ModelConfigurator\n # Model categories and their purposes\n MODEL_CATEGORIES = {\n chat: {\n name: 'Chat Models',\n description: 'For conversational AI, code generation, and general tasks',\n required: true\n },\n embedding: {\n name: 'Embedding Models',\n description: 'For vector search and semantic similarity',\n required: true\n },\n image: {\n name: 'Image Generation Models',\n description: 'For creating images from text descriptions',\n required: false\n }\n }.freeze\n\n # Initialize the model configurator\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @available_models = {}\n @selected_models = {}\n end\n\n # Discover and configure AI models\n #\n # @return [Boolean] True if model configuration completed successfully\n def discover_and_configure\n return false unless providers_configured?\n\n @logger.info('Discovering available AI models...')\n\n configure_rubyllm_from_setup\n discover_models\n display_model_summary\n configure_model_preferences\n finalize_model_configuration\n\n true\n end\n\n private\n\n # Check if AI providers are configured\n #\n # @return [Boolean] True if providers are available\n def providers_configured?\n if @setup_data[:providers]&.any?\n true\n else\n @logger.failure('No AI providers configured. Please run provider setup first.')\n false\n end\n end\n\n # Configure RubyLLM with setup data\n def configure_rubyllm_from_setup\n @logger.info('Configuring RubyLLM with detected providers...')\n\n RubyLLM.configure do |config|\n @setup_data[:providers].each do |provider_key, provider_config|\n case provider_key\n when :openai\n config.openai_api_key = provider_config[:api_key]\n when :openrouter\n config.openai_api_key = provider_config[:api_key]\n config.openai_api_base = 'https://openrouter.ai/api/v1'\n when :anthropic\n config.anthropic_api_key = provider_config[:api_key]\n when :gemini\n config.gemini_api_key = provider_config[:api_key]\n when :deepseek\n config.deepseek_api_key = provider_config[:api_key]\n end\n end\n end\n end\n\n # Discover available models from configured providers\n def discover_models\n @logger.info('Refreshing model registry...')\n RubyLLM.models.refresh!\n\n # Categorize available models\n @available_models[:chat] = filter_chat_models\n @available_models[:embedding] = filter_embedding_models\n @available_models[:image] = filter_image_models\n\n @logger.success('Model discovery completed!')\n rescue StandardError => e\n @logger.failure(\"Model discovery failed: #{e.message}\")\n @logger.debug(e.backtrace.join(\"\\n\")) if ENV['DEBUG']\n fallback_to_default_models\n end\n\n # Filter chat models from available providers\n #\n # @return [Array] Available chat models with metadata\n def filter_chat_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.chat_models.select do |model|\n provider_symbol = model.provider.to_sym\n # Map openrouter to openai for filtering\n provider_symbol = :openrouter if provider_symbol == :openai &&\n @setup_data[:providers].key?(:openrouter) &&\n !@setup_data[:providers].key?(:openai)\n\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n context_window: model.context_window,\n supports_vision: model.supports_vision?,\n supports_tools: model.supports_functions?,\n input_price: model.input_price_per_million,\n output_price: model.output_price_per_million,\n family: model.family\n }\n end\n end\n\n # Filter embedding models from available providers\n #\n # @return [Array] Available embedding models with metadata\n def filter_embedding_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.embedding_models.select do |model|\n provider_symbol = model.provider.to_sym\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n dimensions: model.respond_to?(:dimensions) ? model.dimensions : 'Unknown',\n input_price: model.input_price_per_million\n }\n end\n end\n\n # Filter image generation models from available providers\n #\n # @return [Array] Available image models with metadata\n def filter_image_models\n provider_keys = @setup_data[:providers].keys\n\n # NOTE: RubyLLM may not have a specific image_models filter\n # This is a placeholder implementation\n all_models = begin\n RubyLLM.models.all.select do |model|\n model.respond_to?(:type) &&\n model.type == 'image' &&\n provider_keys.include?(model.provider.to_sym)\n end\n rescue StandardError\n []\n end\n\n all_models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n max_resolution: model.respond_to?(:max_resolution) ? model.max_resolution : 'Unknown'\n }\n end\n end\n\n # Display summary of discovered models\n def display_model_summary\n puts \"\\nšŸ“Š Model Discovery Summary:\"\n\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n status = models.any? ? 'āœ“' : 'āœ—'\n count = models.size\n\n puts \" #{status} #{info[:name]}: #{count} models available\"\n\n if models.any? && count <= 5\n models.each do |model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" - #{model[:name]} (#{provider_name})\"\n end\n elsif count > 5\n puts \" - #{models.first(3).map do |m|\n m[:name]\n end.join(', ')}, and #{count - 3} more...\"\n end\n end\n puts ''\n end\n\n # Configure model preferences interactively\n def configure_model_preferences\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n\n if models.empty?\n if info[:required]\n @logger.warn(\"No #{info[:name].downcase} available. Some features may not work.\")\n end\n next\n end\n\n configure_category_models(category, info, models)\n end\n end\n\n # Configure models for a specific category\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models [Array] Available models for category\n def configure_category_models(category, info, models)\n puts \"\\nšŸ¤– #{info[:name]} Configuration\"\n puts \"Purpose: #{info[:description]}\"\n\n if models.size == 1\n # Auto-select single model\n model = models.first\n @selected_models[category] = model\n @logger.info(\"Auto-selected: #{model[:name]} (#{model[:provider]})\")\n return\n end\n\n # Group models by provider\n models_by_provider = group_models_by_provider(models)\n\n if models_by_provider.size == 1\n # Only one provider, show models directly\n provider_key = models_by_provider.keys.first\n select_model_from_provider(category, info, provider_key, models_by_provider[provider_key])\n else\n # Multiple providers, let user choose provider first\n select_model_with_provider_separation(category, info, models_by_provider)\n end\n\n # Show additional model info\n return unless @selected_models[category]\n\n display_model_details(@selected_models[category]) if @prompt.yes?('Show model details?',\n default: false)\n end\n\n # Group models by provider for organized selection\n #\n # @param models [Array] Available models\n # @return [Hash] Models grouped by provider\n def group_models_by_provider(models)\n models.group_by { |model| model[:provider] }\n end\n\n # Select model when only one provider is available\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param provider [String] Provider name\n # @param provider_models [Array] Models from this provider\n def select_model_from_provider(category, info, provider, provider_models)\n provider_name = get_provider_display_name(provider)\n puts \"\\nAvailable #{info[:name].downcase} from #{provider_name}:\"\n\n choices = build_model_choices(provider_models)\n\n selected = @prompt.select(\n \"Choose #{info[:name].downcase}:\",\n choices,\n cycle: true,\n filter: true,\n help: '(Use ↑/↓ arrows to navigate, Enter to select, type to filter)'\n )\n\n @selected_models[category] = selected\n end\n\n # Select model with provider separation for multiple providers\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models_by_provider [Hash] Models grouped by provider\n def select_model_with_provider_separation(category, info, models_by_provider)\n # First, let user choose provider\n provider_choices = models_by_provider.map do |provider, provider_models|\n provider_name = get_provider_display_name(provider)\n model_count = provider_models.size\n {\n name: \"#{provider_name} (#{model_count} models)\",\n value: provider\n }\n end\n\n selected_provider = @prompt.select(\n \"Choose AI provider for #{info[:name].downcase}:\",\n provider_choices,\n cycle: true,\n help: '(Use ↑/↓ arrows to navigate, Enter to select)'\n )\n\n # Then select model from chosen provider\n select_model_from_provider(category, info, selected_provider,\n models_by_provider[selected_provider])\n end\n\n # Build model choices with pricing and capability info\n #\n # @param models [Array] Models to build choices for\n # @return [Array] Formatted choices for TTY::Prompt\n def build_model_choices(models)\n models.map do |model|\n price_info = if model[:input_price]\n \" ($#{model[:input_price]}/1M tokens)\"\n else\n ''\n end\n\n # Add capability indicators\n capabilities = []\n capabilities << 'šŸ‘ļø' if model[:supports_vision]\n capabilities << 'šŸ”§' if model[:supports_tools]\n capability_info = capabilities.any? ? \" #{capabilities.join(' ')}\" : ''\n\n description = \"#{model[:name]}#{price_info}#{capability_info}\"\n { name: description, value: model }\n end\n end\n\n # Display detailed information about a model\n #\n # @param model [Hash] Model information\n def display_model_details(model)\n puts \"\\nšŸ“‹ Model Details:\"\n puts \" ID: #{model[:id]}\"\n puts \" Provider: #{model[:provider]}\"\n\n if model[:context_window]\n formatted_window = model[:context_window].to_s.reverse.gsub(/(\\d{3})(?=\\d)/,\n '\\\\1,').reverse\n puts \" Context Window: #{formatted_window} tokens\"\n end\n\n puts ' Vision Support: Yes' if model[:supports_vision]\n\n puts ' Function Calling: Yes' if model[:supports_tools]\n\n puts \" Input Cost: $#{model[:input_price]}/1M tokens\" if model[:input_price]\n\n puts \" Output Cost: $#{model[:output_price]}/1M tokens\" if model[:output_price]\n\n puts ''\n end\n\n # Finalize model configuration\n def finalize_model_configuration\n @setup_data[:models] = {}\n\n @selected_models.each do |category, model|\n @setup_data[:models][category] = {\n id: model[:id],\n name: model[:name],\n provider: model[:provider],\n capabilities: extract_model_capabilities(model)\n }\n end\n\n # Set default models for AI configuration\n if @selected_models[:chat]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_model] = @selected_models[:chat][:id]\n @setup_data[:ai][:default_provider] = @selected_models[:chat][:provider]\n end\n\n if @selected_models[:embedding]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_embedding_model] = @selected_models[:embedding][:id]\n end\n\n @logger.success('Model configuration completed!')\n display_final_model_summary\n end\n\n # Extract capabilities from model information\n #\n # @param model [Hash] Model information\n # @return [Array] List of capabilities\n def extract_model_capabilities(model)\n capabilities = []\n capabilities << 'vision' if model[:supports_vision]\n capabilities << 'tools' if model[:supports_tools]\n capabilities << 'embedding' if model[:dimensions]\n capabilities\n end\n\n # Display final model configuration summary\n def display_final_model_summary\n puts \"\\nāœ… Selected Models:\"\n @selected_models.each do |category, model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" #{category.to_s.capitalize}: #{model[:name]} (#{provider_name})\"\n end\n puts ''\n end\n\n # Fallback to default models if discovery fails\n def fallback_to_default_models\n @logger.warn('Using fallback default models...')\n\n primary_provider = @setup_data[:primary_provider]\n @setup_data[:providers][primary_provider]\n\n case primary_provider\n when :openai, :openrouter\n @available_models[:chat] = [{\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini',\n provider: 'openai',\n supports_vision: true,\n supports_tools: true\n }]\n @available_models[:embedding] = [{\n id: 'text-embedding-3-small',\n name: 'Text Embedding 3 Small',\n provider: 'openai'\n }]\n when :anthropic\n @available_models[:chat] = [{\n id: 'claude-3-haiku-20240307',\n name: 'Claude 3 Haiku',\n provider: 'anthropic',\n supports_tools: true\n }]\n when :gemini\n @available_models[:chat] = [{\n id: 'gemini-2.0-flash',\n name: 'Gemini 2.0 Flash',\n provider: 'gemini',\n supports_vision: true,\n supports_tools: true\n }]\n @available_models[:embedding] = [{\n id: 'text-embedding-004',\n name: 'Text Embedding 004',\n provider: 'gemini'\n }]\n when :deepseek\n @available_models[:chat] = [{\n id: 'deepseek-chat',\n name: 'DeepSeek Chat',\n provider: 'deepseek',\n supports_tools: true\n }]\n end\n\n @available_models[:image] = []\n end\n\n # Get display name for a provider, handling mismatches safely\n #\n # @param provider [String, Symbol] Provider identifier from model\n # @return [String] Human-readable provider name\n def get_provider_display_name(provider)\n provider_key = map_provider_to_key(provider)\n\n if @setup_data[:providers] && @setup_data[:providers][provider_key]\n @setup_data[:providers][provider_key][:name]\n else\n # Fallback to capitalized provider name\n provider.to_s.capitalize\n end\n end\n\n # Map model provider to setup data key\n #\n # @param provider [String, Symbol] Provider from model\n # @return [Symbol] Provider key for setup data\n def map_provider_to_key(provider)\n case provider.to_s.downcase\n when 'openai'\n # Could be either openai or openrouter\n if @setup_data[:providers]&.key?(:openrouter) && !@setup_data[:providers]&.key?(:openai)\n :openrouter\n else\n :openai\n end\n when 'google', 'gemini'\n :gemini\n when 'anthropic'\n :anthropic\n when 'deepseek'\n :deepseek\n else\n provider.to_sym\n end\n end\n end\n end\nend\n"}, "c05f3ed8c9df60a688906653e13b6300db1aff9e": {"t": 1785756134.436676, "n": 17738, "h": "6daebd510a6678e3", "c": "# frozen_string_literal: true\n\nrequire 'ruby_llm'\n\nmodule BlueprintsCLI\n module Setup\n # ModelConfigurator handles AI model discovery, selection, and configuration.\n # It uses RubyLLM's model registry to discover available models from configured\n # providers and helps users select appropriate models for different tasks.\n class ModelConfigurator\n # Model categories and their purposes\n MODEL_CATEGORIES = {\n chat: {\n name: 'Chat Models',\n description: 'For conversational AI, code generation, and general tasks',\n required: true\n },\n embedding: {\n name: 'Embedding Models',\n description: 'For vector search and semantic similarity',\n required: true\n },\n image: {\n name: 'Image Generation Models',\n description: 'For creating images from text descriptions',\n required: false\n }\n }.freeze\n\n # Initialize the model configurator\n #\n # @param prompt [TTY::Prompt] TTY prompt instance\n # @param setup_data [Hash] Setup data storage\n def initialize(prompt, setup_data)\n @prompt = prompt\n @setup_data = setup_data\n @logger = BlueprintsCLI.logger\n @available_models = {}\n @selected_models = {}\n end\n\n # Discover and configure AI models\n #\n # @return [Boolean] True if model configuration completed successfully\n def discover_and_configure\n return false unless providers_configured?\n\n @logger.info('Discovering available AI models...')\n\n configure_rubyllm_from_setup\n discover_models\n display_model_summary\n configure_model_preferences\n finalize_model_configuration\n\n true\n end\n\n private\n\n # Check if AI providers are configured\n #\n # @return [Boolean] True if providers are available\n def providers_configured?\n if @setup_data[:providers]&.any?\n true\n else\n @logger.failure('No AI providers configured. Please run provider setup first.')\n false\n end\n end\n\n # Configure RubyLLM with setup data\n def configure_rubyllm_from_setup\n @logger.info('Configuring RubyLLM with detected providers...')\n\n RubyLLM.configure do |config|\n @setup_data[:providers].each do |provider_key, provider_config|\n case provider_key\n when :openai\n config.openai_api_key = provider_config[:api_key]\n when :openrouter\n config.openai_api_key = provider_config[:api_key]\n config.openai_api_base = 'https://openrouter.ai/api/v1'\n when :anthropic\n config.anthropic_api_key = provider_config[:api_key]\n when :gemini\n config.gemini_api_key = provider_config[:api_key]\n when :deepseek\n config.deepseek_api_key = provider_config[:api_key]\n end\n end\n end\n end\n\n # Discover available models from configured providers\n def discover_models\n @logger.info('Refreshing model registry...')\n RubyLLM.models.refresh!\n\n # Categorize available models\n @available_models[:chat] = filter_chat_models\n @available_models[:embedding] = filter_embedding_models\n @available_models[:image] = filter_image_models\n\n @logger.success('Model discovery completed!')\n rescue StandardError => e\n @logger.failure(\"Model discovery failed: #{e.message}\")\n @logger.debug(e.backtrace.join(\"\\n\")) if ENV['DEBUG']\n fallback_to_default_models\n end\n\n # Filter chat models from available providers\n #\n # @return [Array] Available chat models with metadata\n def filter_chat_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.chat_models.select do |model|\n provider_symbol = model.provider.to_sym\n # Map openrouter to openai for filtering\n provider_symbol = :openrouter if provider_symbol == :openai &&\n @setup_data[:providers].key?(:openrouter) &&\n !@setup_data[:providers].key?(:openai)\n\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n context_window: model.context_window,\n supports_vision: model.supports_vision?,\n supports_tools: model.supports_functions?,\n input_price: model.input_price_per_million,\n output_price: model.output_price_per_million,\n family: model.family\n }\n end\n end\n\n # Filter embedding models from available providers\n #\n # @return [Array] Available embedding models with metadata\n def filter_embedding_models\n provider_keys = @setup_data[:providers].keys\n\n models = RubyLLM.models.embedding_models.select do |model|\n provider_symbol = model.provider.to_sym\n provider_keys.include?(provider_symbol)\n end\n\n models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n dimensions: model.respond_to?(:dimensions) ? model.dimensions : 'Unknown',\n input_price: model.input_price_per_million\n }\n end\n end\n\n # Filter image generation models from available providers\n #\n # @return [Array] Available image models with metadata\n def filter_image_models\n provider_keys = @setup_data[:providers].keys\n\n # NOTE: RubyLLM may not have a specific image_models filter\n # This is a placeholder implementation\n all_models = begin\n RubyLLM.models.all.select do |model|\n model.respond_to?(:type) &&\n model.type == 'image' &&\n provider_keys.include?(model.provider.to_sym)\n end\n rescue StandardError\n []\n end\n\n all_models.map do |model|\n {\n id: model.id,\n name: model.name,\n provider: model.provider,\n max_resolution: model.respond_to?(:max_resolution) ? model.max_resolution : 'Unknown'\n }\n end\n end\n\n # Display summary of discovered models\n def display_model_summary\n puts \"\\nšŸ“Š Model Discovery Summary:\"\n\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n status = models.any? ? 'āœ“' : 'āœ—'\n count = models.size\n\n puts \" #{status} #{info[:name]}: #{count} models available\"\n\n if models.any? && count <= 5\n models.each do |model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" - #{model[:name]} (#{provider_name})\"\n end\n elsif count > 5\n puts \" - #{models.first(3).map do |m|\n m[:name]\n end.join(', ')}, and #{count - 3} more...\"\n end\n end\n puts ''\n end\n\n # Configure model preferences interactively\n def configure_model_preferences\n MODEL_CATEGORIES.each do |category, info|\n models = @available_models[category] || []\n\n if models.empty?\n if info[:required]\n @logger.warn(\"No #{info[:name].downcase} available. Some features may not work.\")\n end\n next\n end\n\n configure_category_models(category, info, models)\n end\n end\n\n # Configure models for a specific category\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models [Array] Available models for category\n def configure_category_models(category, info, models)\n puts \"\\nšŸ¤– #{info[:name]} Configuration\"\n puts \"Purpose: #{info[:description]}\"\n\n if models.size == 1\n # Auto-select single model\n model = models.first\n @selected_models[category] = model\n @logger.info(\"Auto-selected: #{model[:name]} (#{model[:provider]})\")\n return\n end\n\n # Group models by provider\n models_by_provider = group_models_by_provider(models)\n\n if models_by_provider.size == 1\n # Only one provider, show models directly\n provider_key = models_by_provider.keys.first\n select_model_from_provider(category, info, provider_key, models_by_provider[provider_key])\n else\n # Multiple providers, let user choose provider first\n select_model_with_provider_separation(category, info, models_by_provider)\n end\n\n # Show additional model info\n return unless @selected_models[category]\n\n display_model_details(@selected_models[category]) if @prompt.yes?('Show model details?',\n default: false)\n end\n\n # Group models by provider for organized selection\n #\n # @param models [Array] Available models\n # @return [Hash] Models grouped by provider\n def group_models_by_provider(models)\n models.group_by { |model| model[:provider] }\n end\n\n # Select model when only one provider is available\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param provider [String] Provider name\n # @param provider_models [Array] Models from this provider\n def select_model_from_provider(category, info, provider, provider_models)\n provider_name = get_provider_display_name(provider)\n puts \"\\nAvailable #{info[:name].downcase} from #{provider_name}:\"\n\n choices = build_model_choices(provider_models)\n\n selected = @prompt.select(\n \"Choose #{info[:name].downcase}:\",\n choices,\n cycle: true,\n filter: true,\n help: '(Use ↑/↓ arrows to navigate, Enter to select, type to filter)'\n )\n\n @selected_models[category] = selected\n end\n\n # Select model with provider separation for multiple providers\n #\n # @param category [Symbol] Model category\n # @param info [Hash] Category information\n # @param models_by_provider [Hash] Models grouped by provider\n def select_model_with_provider_separation(category, info, models_by_provider)\n # First, let user choose provider\n provider_choices = models_by_provider.map do |provider, provider_models|\n provider_name = get_provider_display_name(provider)\n model_count = provider_models.size\n {\n name: \"#{provider_name} (#{model_count} models)\",\n value: provider\n }\n end\n\n selected_provider = @prompt.select(\n \"Choose AI provider for #{info[:name].downcase}:\",\n provider_choices,\n cycle: true,\n help: '(Use ↑/↓ arrows to navigate, Enter to select)'\n )\n\n # Then select model from chosen provider\n select_model_from_provider(category, info, selected_provider,\n models_by_provider[selected_provider])\n end\n\n # Build model choices with pricing and capability info\n #\n # @param models [Array] Models to build choices for\n # @return [Array] Formatted choices for TTY::Prompt\n def build_model_choices(models)\n models.map do |model|\n price_info = if model[:input_price]\n \" ($#{model[:input_price]}/1M tokens)\"\n else\n ''\n end\n\n # Add capability indicators\n capabilities = []\n capabilities << 'šŸ‘ļø' if model[:supports_vision]\n capabilities << 'šŸ”§' if model[:supports_tools]\n capability_info = capabilities.any? ? \" #{capabilities.join(' ')}\" : ''\n\n description = \"#{model[:name]}#{price_info}#{capability_info}\"\n { name: description, value: model }\n end\n end\n\n # Display detailed information about a model\n #\n # @param model [Hash] Model information\n def display_model_details(model)\n puts \"\\nšŸ“‹ Model Details:\"\n puts \" ID: #{model[:id]}\"\n puts \" Provider: #{model[:provider]}\"\n\n if model[:context_window]\n formatted_window = model[:context_window].to_s.reverse.gsub(/(\\d{3})(?=\\d)/,\n '\\\\1,').reverse\n puts \" Context Window: #{formatted_window} tokens\"\n end\n\n puts ' Vision Support: Yes' if model[:supports_vision]\n\n puts ' Function Calling: Yes' if model[:supports_tools]\n\n puts \" Input Cost: $#{model[:input_price]}/1M tokens\" if model[:input_price]\n\n puts \" Output Cost: $#{model[:output_price]}/1M tokens\" if model[:output_price]\n\n puts ''\n end\n\n # Finalize model configuration\n def finalize_model_configuration\n @setup_data[:models] = {}\n\n @selected_models.each do |category, model|\n @setup_data[:models][category] = {\n id: model[:id],\n name: model[:name],\n provider: model[:provider],\n capabilities: extract_model_capabilities(model)\n }\n end\n\n # Set default models for AI configuration\n if @selected_models[:chat]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_model] = @selected_models[:chat][:id]\n @setup_data[:ai][:default_provider] = @selected_models[:chat][:provider]\n end\n\n if @selected_models[:embedding]\n @setup_data[:ai] ||= {}\n @setup_data[:ai][:default_embedding_model] = @selected_models[:embedding][:id]\n end\n\n @logger.success('Model configuration completed!')\n display_final_model_summary\n end\n\n # Extract capabilities from model information\n #\n # @param model [Hash] Model information\n # @return [Array] List of capabilities\n def extract_model_capabilities(model)\n capabilities = []\n capabilities << 'vision' if model[:supports_vision]\n capabilities << 'tools' if model[:supports_tools]\n capabilities << 'embedding' if model[:dimensions]\n capabilities\n end\n\n # Display final model configuration summary\n def display_final_model_summary\n puts \"\\nāœ… Selected Models:\"\n @selected_models.each do |category, model|\n provider_name = get_provider_display_name(model[:provider])\n puts \" #{category.to_s.capitalize}: #{model[:name]} (#{provider_name})\"\n end\n puts ''\n end\n\n # Fallback to default models if discovery fails\n def fallback_to_default_models\n @logger.warn('Using fallback default models...')\n\n primary_provider = @setup_data[:primary_provider]\n provider_config = @setup_data[:providers][primary_provider]\n\n case primary_provider\n when :openai, :openrouter\n @available_models[:chat] = [{\n id: 'gpt-4o-mini',\n name: 'GPT-4o Mini',\n provider: 'openai',\n supports_vision: true,\n supports_tools: true\n }]\n @available_models[:embedding] = [{\n id: 'text-embedding-3-small',\n name: 'Text Embedding 3 Small',\n provider: 'openai'\n }]\n when :anthropic\n @available_models[:chat] = [{\n id: 'claude-3-haiku-20240307',\n name: 'Claude 3 Haiku',\n provider: 'anthropic',\n supports_tools: true\n }]\n when :gemini\n @available_models[:chat] = [{\n id: 'gemini-2.0-flash',\n name: 'Gemini 2.0 Flash',\n provider: 'gemini',\n supports_vision: true,\n supports_tools: true\n }]\n @available_models[:embedding] = [{\n id: 'text-embedding-004',\n name: 'Text Embedding 004',\n provider: 'gemini'\n }]\n when :deepseek\n @available_models[:chat] = [{\n id: 'deepseek-chat',\n name: 'DeepSeek Chat',\n provider: 'deepseek',\n supports_tools: true\n }]\n end\n\n @available_models[:image] = []\n end\n\n # Get display name for a provider, handling mismatches safely\n #\n # @param provider [String, Symbol] Provider identifier from model\n # @return [String] Human-readable provider name\n def get_provider_display_name(provider)\n provider_key = map_provider_to_key(provider)\n\n if @setup_data[:providers] && @setup_data[:providers][provider_key]\n @setup_data[:providers][provider_key][:name]\n else\n # Fallback to capitalized provider name\n provider.to_s.capitalize\n end\n end\n\n # Map model provider to setup data key\n #\n # @param provider [String, Symbol] Provider from model\n # @return [Symbol] Provider key for setup data\n def map_provider_to_key(provider)\n case provider.to_s.downcase\n when 'openai'\n # Could be either openai or openrouter\n if @setup_data[:providers]&.key?(:openrouter) && !@setup_data[:providers]&.key?(:openai)\n :openrouter\n else\n :openai\n end\n when 'google', 'gemini'\n :gemini\n when 'anthropic'\n :anthropic\n when 'deepseek'\n :deepseek\n else\n provider.to_sym\n end\n end\n end\n end\nend\n"}}}}