n2b 0.5.1 → 2.0.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
@@ -94,26 +118,46 @@ module N2B
94
118
 
95
119
  if prompt_for_advanced
96
120
  puts "\n--- Advanced Settings ---"
97
- print "Would you like to configure advanced settings (e.g., Jira integration, privacy)? (y/n) [default: n]: "
121
+ print "Would you like to configure advanced settings (e.g., Jira or GitHub integration, privacy)? (y/n) [default: n]: "
98
122
  choice = $stdin.gets.chomp.downcase
99
123
 
100
124
  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 }
125
+ current_tracker = config['issue_tracker'] || 'none'
126
+ print "\nSelect issue tracker to integrate (none, jira, github) [current: #{current_tracker}]: "
127
+ tracker_choice = $stdin.gets.chomp.downcase
128
+ tracker_choice = current_tracker if tracker_choice.empty?
129
+ config['issue_tracker'] = tracker_choice
130
+
131
+ case tracker_choice
132
+ when 'jira'
133
+ puts "\n--- Jira Integration ---"
134
+ puts "You can generate a Jira API token here: https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/"
135
+ config['jira'] ||= {}
136
+ print "Jira Domain (e.g., your-company.atlassian.net) [current: #{config['jira']['domain']}]: "
137
+ config['jira']['domain'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['domain'] : val }
138
+
139
+ print "Jira Email Address [current: #{config['jira']['email']}]: "
140
+ config['jira']['email'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['email'] : val }
141
+
142
+ print "Jira API Token #{config['jira']['api_key'] ? '[leave blank to keep current]' : ''}: "
143
+ api_token_input = $stdin.gets.chomp
144
+ config['jira']['api_key'] = api_token_input if !api_token_input.empty?
145
+
146
+ print "Default Jira Project Key (optional, e.g., MYPROJ) [current: #{config['jira']['default_project']}]: "
147
+ config['jira']['default_project'] = $stdin.gets.chomp.then { |val| val.empty? ? config['jira']['default_project'] : val }
148
+ when 'github'
149
+ puts "\n--- GitHub Integration ---"
150
+ config['github'] ||= {}
151
+ print "GitHub Repository (owner/repo) [current: #{config['github']['repo']}]: "
152
+ config['github']['repo'] = $stdin.gets.chomp.then { |val| val.empty? ? config['github']['repo'] : val }
153
+
154
+ print "GitHub Access Token #{config['github']['access_token'] ? '[leave blank to keep current]' : ''}: "
155
+ token_input = $stdin.gets.chomp
156
+ config['github']['access_token'] = token_input if !token_input.empty?
157
+ else
158
+ config['jira'] ||= {}
159
+ config['github'] ||= {}
160
+ end
117
161
 
118
162
  # Privacy Settings
119
163
  puts "\n--- Privacy Settings ---"
@@ -132,9 +176,13 @@ module N2B
132
176
  config['append_to_shell_history'] = $stdin.gets.chomp.then { |val| val.empty? ? config['append_to_shell_history'] : (val.downcase == 'true') }
133
177
  config['privacy']['append_to_shell_history'] = config['append_to_shell_history'] # Also place under privacy for consistency
134
178
 
179
+ # Editor Configuration
180
+ prompt_for_editor_config(config)
181
+
135
182
  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
183
+ config['jira'] ||= {}
184
+ config['github'] ||= {}
185
+ config['issue_tracker'] ||= 'none'
138
186
  # If they opt out, we don't clear existing, just don't prompt.
139
187
  # If it's a fresh setup and they opt out, these will remain empty/nil.
140
188
 
@@ -148,7 +196,9 @@ module N2B
148
196
  end
149
197
  else # Not prompting for advanced (neither advanced_flow nor first_time_core_setup)
150
198
  # Ensure defaults for privacy if they don't exist from a previous config
151
- config['jira'] ||= {} # Ensure Jira key exists
199
+ config['jira'] ||= {}
200
+ config['github'] ||= {}
201
+ config['issue_tracker'] ||= 'none'
152
202
  config['privacy'] ||= {}
153
203
  config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
154
204
  config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
@@ -157,6 +207,12 @@ module N2B
157
207
  config['privacy']['append_to_shell_history'] = config['append_to_shell_history']
158
208
  end
159
209
 
210
+ # Editor Configuration
211
+ config['editor'] ||= {}
212
+ config['editor']['command'] ||= nil # or 'nano', 'vi'
213
+ config['editor']['type'] ||= nil # 'text_editor' or 'diff_tool'
214
+ config['editor']['configured'] ||= false
215
+
160
216
  # Validate configuration before saving
161
217
  validation_errors = validate_config(config)
162
218
  if validation_errors.any?
@@ -165,13 +221,15 @@ module N2B
165
221
  puts "Configuration saved with warnings."
166
222
  end
167
223
 
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)
224
+ puts "\nConfiguration saved to #{self.class.config_file}"
225
+ FileUtils.mkdir_p(File.dirname(self.class.config_file)) unless File.exist?(File.dirname(self.class.config_file))
226
+ File.write(self.class.config_file, config.to_yaml)
171
227
  else
172
228
  # If not reconfiguring, still ensure privacy and jira keys exist with defaults if missing
173
229
  # This handles configs from before these settings were introduced
174
230
  config['jira'] ||= {}
231
+ config['github'] ||= {}
232
+ config['issue_tracker'] ||= 'none'
175
233
  config['privacy'] ||= {}
176
234
  config['privacy']['send_shell_history'] = config['privacy']['send_shell_history'] || false
177
235
  config['privacy']['send_llm_history'] = config['privacy']['send_llm_history'] || true
@@ -181,12 +239,100 @@ module N2B
181
239
  current_append_setting = config.key?('append_to_shell_history') ? config['append_to_shell_history'] : false
182
240
  config['append_to_shell_history'] = current_append_setting
183
241
  config['privacy']['append_to_shell_history'] = config['privacy']['append_to_shell_history'] || current_append_setting
242
+
243
+ # Ensure editor config is initialized if missing (for older configs)
244
+ config['editor'] ||= {}
245
+ config['editor']['command'] ||= nil
246
+ config['editor']['type'] ||= nil
247
+ config['editor']['configured'] ||= false
184
248
  end
185
249
  config
186
250
  end
187
251
 
188
252
  private
189
253
 
254
+ def command_exists?(command)
255
+ # Check for Windows or Unix-like systems for the correct command
256
+ null_device = RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/ ? 'NUL' : '/dev/null'
257
+ system("which #{command} > #{null_device} 2>&1") || system("where #{command} > #{null_device} 2>&1")
258
+ end
259
+
260
+ def prompt_for_editor_config(config)
261
+ puts "\n--- Editor Configuration ---"
262
+ current_editor_command = config.dig('editor', 'command')
263
+ current_editor_type = config.dig('editor', 'type')
264
+ current_status = if current_editor_command
265
+ "Current: #{current_editor_command} (#{current_editor_type || 'not set'})"
266
+ else
267
+ "Current: Not configured"
268
+ end
269
+ puts current_status
270
+
271
+ known_editors = [
272
+ { name: 'nano', command: 'nano', description: 'Simple terminal editor', type: 'text_editor' },
273
+ { name: 'vim', command: 'vim', description: 'Powerful terminal editor', type: 'text_editor' },
274
+ { name: 'vi', command: 'vi', description: 'Standard Unix terminal editor', type: 'text_editor' },
275
+ { name: 'code', command: 'code', description: 'Visual Studio Code (requires "code" in PATH)', type: 'text_editor' },
276
+ { name: 'subl', command: 'subl', description: 'Sublime Text (requires "subl" in PATH)', type: 'text_editor' },
277
+ { name: 'meld', command: 'meld', description: 'Visual diff and merge tool', type: 'diff_tool' },
278
+ { name: 'kdiff3', command: 'kdiff3', description: 'Visual diff and merge tool (KDE)', type: 'diff_tool' },
279
+ { name: 'opendiff', command: 'opendiff', description: 'File comparison tool (macOS)', type: 'diff_tool' },
280
+ { name: 'vimdiff', command: 'vimdiff', description: 'Diff tool using Vim', type: 'diff_tool' }
281
+ ]
282
+
283
+ available_editors = known_editors.select { |editor| command_exists?(editor[:command]) }
284
+
285
+ if available_editors.empty?
286
+ puts "No standard editors detected automatically."
287
+ else
288
+ puts "Choose your preferred editor/diff tool:"
289
+ available_editors.each_with_index do |editor, index|
290
+ puts "#{index + 1}. #{editor[:name]} (#{editor[:description]})"
291
+ end
292
+ puts "#{available_editors.length + 1}. Custom (enter your own command)"
293
+ print "Enter choice (1-#{available_editors.length + 1}) or leave blank to skip: "
294
+ end
295
+
296
+ choice_input = $stdin.gets.chomp
297
+ return if choice_input.empty? && current_editor_command # Skip if already configured and user wants to skip
298
+
299
+ choice = choice_input.to_i
300
+
301
+ selected_editor = nil
302
+ custom_command = nil
303
+
304
+ if choice > 0 && choice <= available_editors.length
305
+ selected_editor = available_editors[choice - 1]
306
+ config['editor']['command'] = selected_editor[:command]
307
+ config['editor']['type'] = selected_editor[:type]
308
+ config['editor']['configured'] = true
309
+ puts "✓ Using #{selected_editor[:name]} as your editor/diff tool."
310
+ elsif choice == available_editors.length + 1 || (available_editors.empty? && !choice_input.empty?)
311
+ print "Enter custom editor command: "
312
+ custom_command = $stdin.gets.chomp
313
+ if custom_command.empty?
314
+ puts "No command entered. Editor configuration skipped."
315
+ return
316
+ end
317
+
318
+ print "Is this a 'text_editor' or a 'diff_tool'? (text_editor/diff_tool): "
319
+ custom_type = $stdin.gets.chomp.downcase
320
+ unless ['text_editor', 'diff_tool'].include?(custom_type)
321
+ puts "Invalid type. Defaulting to 'text_editor'."
322
+ custom_type = 'text_editor'
323
+ end
324
+ config['editor']['command'] = custom_command
325
+ config['editor']['type'] = custom_type
326
+ config['editor']['configured'] = true
327
+ puts "✓ Using custom command '#{custom_command}' (#{custom_type}) as your editor/diff tool."
328
+ else
329
+ puts "Invalid choice. Editor configuration skipped."
330
+ # Keep existing config if invalid choice, or clear if they wanted to change but failed?
331
+ # For now, just skipping and keeping whatever was there.
332
+ return
333
+ end
334
+ end
335
+
190
336
  def validate_config(config)
191
337
  errors = []
192
338
 
@@ -209,18 +355,42 @@ module N2B
209
355
  errors << "API key missing for #{config['llm']} provider"
210
356
  end
211
357
 
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"
219
- end
220
- if jira_config['api_key'].nil? || jira_config['api_key'].empty?
221
- errors << "Jira API key missing when domain is configured"
358
+ # Validate editor configuration (optional, so more like warnings or info)
359
+ # Example: Check if command is set if configured is true
360
+ # if config['editor'] && config['editor']['configured'] && (config['editor']['command'].nil? || config['editor']['command'].empty?)
361
+ # errors << "Editor is marked as configured, but no command is set."
362
+ # end
363
+
364
+ tracker = config['issue_tracker'] || 'none'
365
+ case tracker
366
+ when 'jira'
367
+ if config['jira'] && !config['jira'].empty?
368
+ jira_config = config['jira']
369
+ if jira_config['domain'] && !jira_config['domain'].empty?
370
+ if jira_config['email'].nil? || jira_config['email'].empty?
371
+ errors << "Jira email missing when domain is configured"
372
+ end
373
+ if jira_config['api_key'].nil? || jira_config['api_key'].empty?
374
+ errors << "Jira API key missing when domain is configured"
375
+ end
376
+ else
377
+ errors << "Jira domain missing when issue_tracker is set to 'jira'"
222
378
  end
379
+ else
380
+ errors << "Jira configuration missing when issue_tracker is set to 'jira'"
381
+ end
382
+ when 'github'
383
+ if config['github'] && !config['github'].empty?
384
+ gh = config['github']
385
+ errors << "GitHub repository missing when issue_tracker is set to 'github'" if gh['repo'].nil? || gh['repo'].empty?
386
+ errors << "GitHub access token missing when issue_tracker is set to 'github'" if gh['access_token'].nil? || gh['access_token'].empty?
387
+ else
388
+ errors << "GitHub configuration missing when issue_tracker is set to 'github'"
223
389
  end
390
+ when 'none'
391
+ # No validation needed for 'none' tracker
392
+ else
393
+ errors << "Invalid issue_tracker '#{tracker}' - must be 'jira', 'github', or 'none'"
224
394
  end
225
395
 
226
396
  errors