n2b 0.7.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/n2b/base.rb CHANGED
@@ -3,12 +3,36 @@ require_relative 'model_config'
3
3
  module N2B
4
4
  class Base
5
5
 
6
- CONFIG_FILE = ENV['N2B_CONFIG_FILE'] || File.expand_path('~/.n2b/config.yml')
7
- HISTORY_FILE = ENV['N2B_HISTORY_FILE'] || File.expand_path('~/.n2b/history')
6
+ def self.config_file
7
+ # Bulletproof test environment detection
8
+ if test_environment?
9
+ File.expand_path('~/.n2b_test/config.yml')
10
+ else
11
+ ENV['N2B_CONFIG_FILE'] || File.expand_path('~/.n2b/config.yml')
12
+ end
13
+ end
14
+
15
+ def self.history_file
16
+ if test_environment?
17
+ File.expand_path('~/.n2b_test/history')
18
+ else
19
+ ENV['N2B_HISTORY_FILE'] || File.expand_path('~/.n2b/history')
20
+ end
21
+ end
22
+
23
+ def self.test_environment?
24
+ # Multiple ways to detect test environment for maximum safety
25
+ ENV['RAILS_ENV'] == 'test' ||
26
+ ENV['RACK_ENV'] == 'test' ||
27
+ ENV['N2B_TEST_MODE'] == 'true' ||
28
+ $PROGRAM_NAME.include?('rake') ||
29
+ $PROGRAM_NAME.include?('test') ||
30
+ caller.any? { |line| line.include?('test/') || line.include?('minitest') || line.include?('rake') }
31
+ end
8
32
 
9
33
  def load_config
10
- if File.exist?(CONFIG_FILE)
11
- YAML.load_file(CONFIG_FILE)
34
+ if File.exist?(self.class.config_file)
35
+ YAML.load_file(self.class.config_file)
12
36
  else
13
37
  { }
14
38
  end
@@ -22,13 +46,19 @@ module N2B
22
46
  # Determine if it's effectively a first-time setup for core LLM details
23
47
  is_first_time_core_setup = config['llm'].nil?
24
48
 
25
- if api_key.nil? || api_key == '' || config['llm'].nil? || reconfigure
49
+ # Check if configuration is incomplete based on provider type
50
+ config_incomplete = config['llm'].nil? || reconfigure ||
51
+ (config['llm'] == 'vertexai' && (config['vertex_credential_file'].nil? || config['vertex_credential_file'].empty?)) ||
52
+ (config['llm'] == 'ollama' && false) || # Ollama doesn't require API key, so never incomplete due to missing key
53
+ (['claude', 'openai', 'gemini', 'openrouter'].include?(config['llm']) && (api_key.nil? || api_key == ''))
54
+
55
+ if config_incomplete
26
56
  puts "--- N2B Core LLM Configuration ---"
27
- print "Choose a language model to use (1:claude, 2:openai, 3:gemini, 4:openrouter, 5:ollama) [current: #{config['llm']}]: "
57
+ print "Choose a language model to use (1:claude, 2:openai, 3:gemini (API Key), 4:openrouter, 5:ollama, 6:vertexai (Credential File)) [current: #{config['llm']}]: "
28
58
  llm_choice = $stdin.gets.chomp
29
59
  llm_choice = config['llm'] if llm_choice.empty? && !config['llm'].nil? # Keep current if input is empty
30
60
 
31
- unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', '1', '2', '3', '4', '5'].include?(llm_choice)
61
+ unless ['claude', 'openai', 'gemini', 'openrouter', 'ollama', 'vertexai', '1', '2', '3', '4', '5', '6'].include?(llm_choice)
32
62
  puts "Invalid language model selection. Defaulting to 'claude' or previous if available."
33
63
  llm_choice = config['llm'] || 'claude' # Fallback
34
64
  end
@@ -36,10 +66,11 @@ module N2B
36
66
  selected_llm = case llm_choice
37
67
  when '1', 'claude' then 'claude'
38
68
  when '2', 'openai' then 'openai'
39
- when '3', 'gemini' then 'gemini'
69
+ when '3', 'gemini' then 'gemini' # This is now explicitly API Key based Gemini
40
70
  when '4', 'openrouter' then 'openrouter'
41
71
  when '5', 'ollama' then 'ollama'
42
- else config['llm'] || 'claude' # Should not happen due to validation
72
+ when '6', 'vertexai' then 'vertexai' # New Vertex AI (credential file)
73
+ else config['llm'] || 'claude'
43
74
  end
44
75
  config['llm'] = selected_llm
45
76
 
@@ -47,18 +78,76 @@ module N2B
47
78
  puts "\n--- Ollama Specific Configuration ---"
48
79
  puts "Ollama typically runs locally and doesn't require an API key."
49
80
 
50
- # Use new model configuration system
51
81
  current_model = config['model']
52
- model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed to model_choice to avoid conflict
82
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
53
83
  config['model'] = model_choice
54
84
 
55
85
  current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
56
86
  print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
57
87
  ollama_api_url_input = $stdin.gets.chomp
58
88
  config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
59
- config.delete('access_key') # Remove access_key if switching to ollama
60
- else
61
- # Configuration for API key based LLMs
89
+
90
+ config.delete('access_key')
91
+ config.delete('gemini_credential_file') # old key
92
+ config.delete('vertex_credential_file') # new key for Vertex
93
+
94
+ elsif selected_llm == 'vertexai'
95
+ puts "\n--- Vertex AI (Credential File) Specific Configuration ---"
96
+ current_vertex_credential_file = config['vertex_credential_file']
97
+ print "Enter your Google Cloud credential file path for Vertex AI #{current_vertex_credential_file ? '[leave blank to keep current]' : ''}: "
98
+ vertex_credential_file_input = $stdin.gets.chomp
99
+ if !vertex_credential_file_input.empty?
100
+ config['vertex_credential_file'] = vertex_credential_file_input
101
+ elsif current_vertex_credential_file
102
+ config['vertex_credential_file'] = current_vertex_credential_file
103
+ else
104
+ config['vertex_credential_file'] = nil
105
+ end
106
+
107
+ # Ask for Vertex AI region
108
+ current_vertex_location = config['vertex_location'] || 'us-central1'
109
+ puts "\nVertex AI Region Selection:"
110
+ puts "Common regions:"
111
+ puts " 1. us-central1 (Iowa, USA) [default]"
112
+ puts " 2. us-east1 (South Carolina, USA)"
113
+ puts " 3. us-west1 (Oregon, USA)"
114
+ puts " 4. europe-west1 (Belgium)"
115
+ puts " 5. europe-west4 (Netherlands)"
116
+ puts " 6. asia-northeast1 (Tokyo, Japan)"
117
+ puts " 7. asia-southeast1 (Singapore)"
118
+ puts " 8. custom (enter your own region)"
119
+ print "Choose region (1-8) or enter region name [current: #{current_vertex_location}]: "
120
+
121
+ region_choice = $stdin.gets.chomp
122
+ if region_choice.empty?
123
+ config['vertex_location'] = current_vertex_location
124
+ else
125
+ case region_choice
126
+ when '1' then config['vertex_location'] = 'us-central1'
127
+ when '2' then config['vertex_location'] = 'us-east1'
128
+ when '3' then config['vertex_location'] = 'us-west1'
129
+ when '4' then config['vertex_location'] = 'europe-west1'
130
+ when '5' then config['vertex_location'] = 'europe-west4'
131
+ when '6' then config['vertex_location'] = 'asia-northeast1'
132
+ when '7' then config['vertex_location'] = 'asia-southeast1'
133
+ when '8'
134
+ print "Enter custom region (e.g., europe-west2): "
135
+ custom_region = $stdin.gets.chomp
136
+ config['vertex_location'] = custom_region.empty? ? current_vertex_location : custom_region
137
+ else
138
+ # Treat as direct region input
139
+ config['vertex_location'] = region_choice
140
+ end
141
+ end
142
+
143
+ config.delete('access_key')
144
+ config.delete('ollama_api_url')
145
+ config.delete('gemini_credential_file') # old key
146
+
147
+ current_model = config['model']
148
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
149
+ config['model'] = model_choice
150
+ else # API Key based LLMs: claude, openai, gemini (API Key), openrouter
62
151
  puts "\n--- #{selected_llm.capitalize} Specific Configuration ---"
63
152
  current_api_key = config['access_key']
64
153
  print "Enter your #{selected_llm} API key #{ current_api_key.nil? || current_api_key.empty? ? '' : '[leave blank to keep current]' }: "
@@ -70,9 +159,12 @@ module N2B
70
159
  puts "API key is required for #{selected_llm}."
71
160
  exit 1
72
161
  end
162
+ config.delete('vertex_credential_file') # new key for Vertex
163
+ config.delete('ollama_api_url')
164
+ config.delete('gemini_credential_file') # old key
73
165
 
74
166
  current_model = config['model']
75
- model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed
167
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
76
168
  config['model'] = model_choice
77
169
 
78
170
  if selected_llm == 'openrouter'
@@ -94,26 +186,46 @@ module N2B
94
186
 
95
187
  if prompt_for_advanced
96
188
  puts "\n--- Advanced Settings ---"
97
- print "Would you like to configure advanced settings (e.g., Jira integration, privacy)? (y/n) [default: n]: "
189
+ print "Would you like to configure advanced settings (e.g., Jira or GitHub integration, privacy)? (y/n) [default: n]: "
98
190
  choice = $stdin.gets.chomp.downcase
99
191
 
100
192
  if choice == 'y'
101
- # Jira Configuration
102
- puts "\n--- Jira Integration ---"
103
- puts "You can generate a Jira API token here: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/"
104
- config['jira'] ||= {}
105
- print "Jira Domain (e.g., your-company.atlassian.net) [current: #{config['jira']['domain']}]: "
106
- config['jira']['domain'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['domain'] : val }
107
-
108
- print "Jira Email Address [current: #{config['jira']['email']}]: "
109
- config['jira']['email'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['email'] : val }
110
-
111
- print "Jira API Token #{config['jira']['api_key'] ? '[leave blank to keep current]' : ''}: "
112
- api_token_input = $stdin.gets.chomp
113
- config['jira']['api_key'] = api_token_input if !api_token_input.empty?
114
-
115
- print "Default Jira Project Key (optional, e.g., MYPROJ) [current: #{config['jira']['default_project']}]: "
116
- config['jira']['default_project'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['default_project'] : val }
193
+ current_tracker = config['issue_tracker'] || 'none'
194
+ print "\nSelect issue tracker to integrate (none, jira, github) [current: #{current_tracker}]: "
195
+ tracker_choice = $stdin.gets.chomp.downcase
196
+ tracker_choice = current_tracker if tracker_choice.empty?
197
+ config['issue_tracker'] = tracker_choice
198
+
199
+ case tracker_choice
200
+ when 'jira'
201
+ puts "\n--- Jira Integration ---"
202
+ puts "You can generate a Jira API token here: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/"
203
+ config['jira'] ||= {}
204
+ print "Jira Domain (e.g., your-company.atlassian.net) [current: #{config['jira']['domain']}]: "
205
+ config['jira']['domain'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['domain'] : val }
206
+
207
+ print "Jira Email Address [current: #{config['jira']['email']}]: "
208
+ config['jira']['email'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['email'] : val }
209
+
210
+ print "Jira API Token #{config['jira']['api_key'] ? '[leave blank to keep current]' : ''}: "
211
+ api_token_input = $stdin.gets.chomp
212
+ config['jira']['api_key'] = api_token_input if !api_token_input.empty?
213
+
214
+ print "Default Jira Project Key (optional, e.g., MYPROJ) [current: #{config['jira']['default_project']}]: "
215
+ config['jira']['default_project'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['default_project'] : val }
216
+ when 'github'
217
+ puts "\n--- GitHub Integration ---"
218
+ config['github'] ||= {}
219
+ print "GitHub Repository (owner/repo) [current: #{config['github']['repo']}]: "
220
+ config['github']['repo'] = $stdin.gets.chomp.then { |val| val.empty? ? config['github']['repo'] : val }
221
+
222
+ print "GitHub Access Token #{config['github']['access_token'] ? '[leave blank to keep current]' : ''}: "
223
+ token_input = $stdin.gets.chomp
224
+ config['github']['access_token'] = token_input if !token_input.empty?
225
+ else
226
+ config['jira'] ||= {}
227
+ config['github'] ||= {}
228
+ end
117
229
 
118
230
  # Privacy Settings
119
231
  puts "\n--- Privacy Settings ---"
@@ -132,9 +244,13 @@ module N2B
132
244
  config['append_to_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['append_to_shell_history'] : (val.downcase == 'true') }
133
245
  config['privacy']['append_to_shell_history'] = config['append_to_shell_history'] # Also place under privacy for consistency
134
246
 
247
+ # Editor Configuration
248
+ prompt_for_editor_config(config)
249
+
135
250
  else # User chose 'n' for advanced settings
136
- # Ensure Jira config is empty or defaults are cleared if user opts out of advanced
137
- config['jira'] ||= {} # Ensure it exists
251
+ config['jira'] ||= {}
252
+ config['github'] ||= {}
253
+ config['issue_tracker'] ||= 'none'
138
254
  # If they opt out, we don't clear existing, just don't prompt.
139
255
  # If it's a fresh setup and they opt out, these will remain empty/nil.
140
256
 
@@ -148,7 +264,9 @@ module N2B
148
264
  end
149
265
  else # Not prompting for advanced (neither advanced_flow nor first_time_core_setup)
150
266
  # Ensure defaults for privacy if they don't exist from a previous config
151
- config['jira'] ||= {} # Ensure Jira key exists
267
+ config['jira'] ||= {}
268
+ config['github'] ||= {}
269
+ config['issue_tracker'] ||= 'none'
152
270
  config['privacy'] ||= {}
153
271
  config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
154
272
  config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
@@ -157,6 +275,12 @@ module N2B
157
275
  config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
158
276
  end
159
277
 
278
+ # Editor Configuration
279
+ config['editor'] ||= {}
280
+ config['editor']['command'] ||= nil # or 'nano', 'vi'
281
+ config['editor']['type'] ||= nil # 'text_editor' or 'diff_tool'
282
+ config['editor']['configured'] ||= false
283
+
160
284
  # Validate configuration before saving
161
285
  validation_errors = validate_config(config)
162
286
  if validation_errors.any?
@@ -165,13 +289,15 @@ module N2B
165
289
  puts "Configuration saved with warnings."
166
290
  end
167
291
 
168
- puts "\nConfiguration saved to #{CONFIG_FILE}"
169
- FileUtils.mkdir_p(File.dirname(CONFIG_FILE)) unless File.exist?(File.dirname(CONFIG_FILE))
170
- File.write(CONFIG_FILE, config.to_yaml)
292
+ puts "\nConfiguration saved to #{self.class.config_file}"
293
+ FileUtils.mkdir_p(File.dirname(self.class.config_file)) unless File.exist?(File.dirname(self.class.config_file))
294
+ File.write(self.class.config_file, config.to_yaml)
171
295
  else
172
296
  # If not reconfiguring, still ensure privacy and jira keys exist with defaults if missing
173
297
  # This handles configs from before these settings were introduced
174
298
  config['jira'] ||= {}
299
+ config['github'] ||= {}
300
+ config['issue_tracker'] ||= 'none'
175
301
  config['privacy'] ||= {}
176
302
  config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
177
303
  config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
@@ -181,46 +307,211 @@ module N2B
181
307
  current_append_setting = config.key?('append_to_shell_history') ? config['append_to_shell_history'] : false
182
308
  config['append_to_shell_history'] = current_append_setting
183
309
  config['privacy']['append_to_shell_history'] = config['privacy']['append_to_shell_history'] || current_append_setting
310
+
311
+ # Ensure editor config is initialized if missing (for older configs)
312
+ config['editor'] ||= {}
313
+ config['editor']['command'] ||= nil
314
+ config['editor']['type'] ||= nil
315
+ config['editor']['configured'] ||= false
184
316
  end
185
317
  config
186
318
  end
187
319
 
188
320
  private
189
321
 
322
+ def command_exists?(command)
323
+ # Check for Windows or Unix-like systems for the correct command
324
+ null_device = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ ? 'NUL' : '/dev/null'
325
+ system("which #{command} > #{null_device} 2>&1") || system("where #{command} > #{null_device} 2>&1")
326
+ end
327
+
328
+ def prompt_for_editor_config(config)
329
+ puts "\n--- Editor Configuration ---"
330
+ current_editor_command = config.dig('editor', 'command')
331
+ current_editor_type = config.dig('editor', 'type')
332
+ current_status = if current_editor_command
333
+ "Current: #{current_editor_command} (#{current_editor_type || 'not set'})"
334
+ else
335
+ "Current: Not configured"
336
+ end
337
+ puts current_status
338
+
339
+ known_editors = [
340
+ { name: 'nano', command: 'nano', description: 'Simple terminal editor', type: 'text_editor' },
341
+ { name: 'vim', command: 'vim', description: 'Powerful terminal editor', type: 'text_editor' },
342
+ { name: 'vi', command: 'vi', description: 'Standard Unix terminal editor', type: 'text_editor' },
343
+ { name: 'code', command: 'code', description: 'Visual Studio Code (requires "code" in PATH)', type: 'text_editor' },
344
+ { name: 'subl', command: 'subl', description: 'Sublime Text (requires "subl" in PATH)', type: 'text_editor' },
345
+ { name: 'meld', command: 'meld', description: 'Visual diff and merge tool', type: 'diff_tool' },
346
+ { name: 'kdiff3', command: 'kdiff3', description: 'Visual diff and merge tool (KDE)', type: 'diff_tool' },
347
+ { name: 'opendiff', command: 'opendiff', description: 'File comparison tool (macOS)', type: 'diff_tool' },
348
+ { name: 'vimdiff', command: 'vimdiff', description: 'Diff tool using Vim', type: 'diff_tool' }
349
+ ]
350
+
351
+ available_editors = known_editors.select { |editor| command_exists?(editor[:command]) }
352
+
353
+ if available_editors.empty?
354
+ puts "No standard editors detected automatically."
355
+ else
356
+ puts "Choose your preferred editor/diff tool:"
357
+ available_editors.each_with_index do |editor, index|
358
+ puts "#{index + 1}. #{editor[:name]} (#{editor[:description]})"
359
+ end
360
+ puts "#{available_editors.length + 1}. Custom (enter your own command)"
361
+ print "Enter choice (1-#{available_editors.length + 1}) or leave blank to skip: "
362
+ end
363
+
364
+ choice_input = $stdin.gets.chomp
365
+ return if choice_input.empty? && current_editor_command # Skip if already configured and user wants to skip
366
+
367
+ choice = choice_input.to_i
368
+
369
+ selected_editor = nil
370
+ custom_command = nil
371
+
372
+ if choice > 0 && choice <= available_editors.length
373
+ selected_editor = available_editors[choice - 1]
374
+ config['editor']['command'] = selected_editor[:command]
375
+ config['editor']['type'] = selected_editor[:type]
376
+ config['editor']['configured'] = true
377
+ puts "✓ Using #{selected_editor[:name]} as your editor/diff tool."
378
+ elsif choice == available_editors.length + 1 || (available_editors.empty? && !choice_input.empty?)
379
+ print "Enter custom editor command: "
380
+ custom_command = $stdin.gets.chomp
381
+ if custom_command.empty?
382
+ puts "No command entered. Editor configuration skipped."
383
+ return
384
+ end
385
+
386
+ print "Is this a 'text_editor' or a 'diff_tool'? (text_editor/diff_tool): "
387
+ custom_type = $stdin.gets.chomp.downcase
388
+ unless ['text_editor', 'diff_tool'].include?(custom_type)
389
+ puts "Invalid type. Defaulting to 'text_editor'."
390
+ custom_type = 'text_editor'
391
+ end
392
+ config['editor']['command'] = custom_command
393
+ config['editor']['type'] = custom_type
394
+ config['editor']['configured'] = true
395
+ puts "✓ Using custom command '#{custom_command}' (#{custom_type}) as your editor/diff tool."
396
+ else
397
+ puts "Invalid choice. Editor configuration skipped."
398
+ # Keep existing config if invalid choice, or clear if they wanted to change but failed?
399
+ # For now, just skipping and keeping whatever was there.
400
+ return
401
+ end
402
+ end
403
+
190
404
  def validate_config(config)
191
405
  errors = []
192
406
 
193
- # Validate LLM configuration
407
+ # Validate LLM provider selection
194
408
  if config['llm'].nil? || config['llm'].empty?
195
409
  errors << "LLM provider not specified"
196
410
  end
197
411
 
198
- # Validate model name
412
+ # Validate model name (existing logic can likely stay as is)
199
413
  if config['model'].nil? || config['model'].empty?
200
414
  errors << "Model not specified"
201
- elsif config['model'].length < 3
415
+ elsif config['model'].length < 3 # Example of an existing check
202
416
  errors << "Model name '#{config['model']}' seems too short - might be invalid"
203
- elsif %w[y n yes no true false].include?(config['model'].downcase)
417
+ elsif %w[y n yes no true false].include?(config['model'].downcase) # Example
204
418
  errors << "Model name '#{config['model']}' appears to be a boolean response rather than a model name"
205
419
  end
206
420
 
207
- # Validate API key for non-Ollama providers
208
- if config['llm'] != 'ollama' && (config['access_key'].nil? || config['access_key'].empty?)
209
- errors << "API key missing for #{config['llm']} provider"
210
- end
421
+ # Provider-specific validation
422
+ case config['llm']
423
+ when 'gemini' # API Key based Gemini
424
+ if config['access_key'].nil? || config['access_key'].empty?
425
+ errors << "API key missing for gemini provider"
426
+ end
427
+ if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
428
+ errors << "Vertex AI credential file should not be present when gemini (API Key) provider is selected"
429
+ end
430
+ # Ensure old gemini_credential_file key is also not present
431
+ if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
432
+ errors << "Old gemini_credential_file key should not be present. Use 'gemini' (API key) or 'vertexai' (credential file)."
433
+ end
211
434
 
212
- # Validate Jira configuration if present
213
- if config['jira'] && !config['jira'].empty?
214
- jira_config = config['jira']
215
- if jira_config['domain'] && !jira_config['domain'].empty?
216
- # If domain is set, email and api_key should also be set
217
- if jira_config['email'].nil? || jira_config['email'].empty?
218
- errors << "Jira email missing when domain is configured"
435
+ when 'vertexai' # Credential File based Vertex AI
436
+ if config['vertex_credential_file'].nil? || config['vertex_credential_file'].empty?
437
+ errors << "Credential file path for Vertex AI not provided"
438
+ else
439
+ # Ensure File.exist? is only called if the path is not nil and not empty
440
+ unless File.exist?(config['vertex_credential_file'])
441
+ errors << "Credential file missing or invalid at #{config['vertex_credential_file']} for Vertex AI"
219
442
  end
220
- if jira_config['api_key'].nil? || jira_config['api_key'].empty?
221
- errors << "Jira API key missing when domain is configured"
443
+ end
444
+ if config['access_key'] && !config['access_key'].empty?
445
+ errors << "API key (access_key) should not be present when Vertex AI provider is selected"
446
+ end
447
+ # Ensure old gemini_credential_file key is also not present
448
+ if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
449
+ errors << "Old gemini_credential_file key should not be present when Vertex AI is selected."
450
+ end
451
+
452
+ when 'ollama'
453
+ # No specific key/credential validation for Ollama here, but ensure others are not present
454
+ if config['access_key'] && !config['access_key'].empty?
455
+ errors << "API key (access_key) should not be present when Ollama provider is selected"
456
+ end
457
+ if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
458
+ errors << "Vertex AI credential file should not be present when Ollama provider is selected"
459
+ end
460
+ if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
461
+ errors << "Old gemini_credential_file key should not be present when Ollama is selected."
462
+ end
463
+ # Ollama specific validations like ollama_api_url could be here if needed
464
+
465
+ when 'claude', 'openai', 'openrouter' # Other API key based providers
466
+ if config['access_key'].nil? || config['access_key'].empty?
467
+ errors << "API key missing for #{config['llm']} provider"
468
+ end
469
+ if config['vertex_credential_file'] && !config['vertex_credential_file'].empty?
470
+ errors << "Vertex AI credential file should not be present for #{config['llm']} provider"
471
+ end
472
+ if config['gemini_credential_file'] && !config['gemini_credential_file'].empty?
473
+ errors << "Old gemini_credential_file key should not be present for #{config['llm']} provider."
474
+ end
475
+ # else
476
+ # This case means llm is nil or an unknown provider, already handled by the first check.
477
+ end
478
+
479
+ # Validate editor configuration (optional, so more like warnings or info)
480
+ # Example: Check if command is set if configured is true
481
+ # if config['editor'] && config['editor']['configured'] && (config['editor']['command'].nil? || config['editor']['command'].empty?)
482
+ # errors << "Editor is marked as configured, but no command is set."
483
+ # end
484
+
485
+ tracker = config['issue_tracker'] || 'none'
486
+ case tracker
487
+ when 'jira'
488
+ if config['jira'] && !config['jira'].empty?
489
+ jira_config = config['jira']
490
+ if jira_config['domain'] && !jira_config['domain'].empty?
491
+ if jira_config['email'].nil? || jira_config['email'].empty?
492
+ errors << "Jira email missing when domain is configured"
493
+ end
494
+ if jira_config['api_key'].nil? || jira_config['api_key'].empty?
495
+ errors << "Jira API key missing when domain is configured"
496
+ end
497
+ else
498
+ errors << "Jira domain missing when issue_tracker is set to 'jira'"
222
499
  end
500
+ else
501
+ errors << "Jira configuration missing when issue_tracker is set to 'jira'"
223
502
  end
503
+ when 'github'
504
+ if config['github'] && !config['github'].empty?
505
+ gh = config['github']
506
+ errors << "GitHub repository missing when issue_tracker is set to 'github'" if gh['repo'].nil? || gh['repo'].empty?
507
+ errors << "GitHub access token missing when issue_tracker is set to 'github'" if gh['access_token'].nil? || gh['access_token'].empty?
508
+ else
509
+ errors << "GitHub configuration missing when issue_tracker is set to 'github'"
510
+ end
511
+ when 'none'
512
+ # No validation needed for 'none' tracker
513
+ else
514
+ errors << "Invalid issue_tracker '#{tracker}' - must be 'jira', 'github', or 'none'"
224
515
  end
225
516
 
226
517
  errors