n2b 0.7.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/cli.rb CHANGED
@@ -1,5 +1,3 @@
1
- require_relative 'jira_client' # For N2B::JiraClient
2
-
3
1
  module N2B
4
2
  class CLI < Base
5
3
  def self.run(args)
@@ -16,281 +14,27 @@ module N2B
16
14
  config = get_config(reconfigure: @options[:config], advanced_flow: @options[:advanced_config])
17
15
  user_input = @args.join(' ') # All remaining args form user input/prompt addition
18
16
 
19
- if @options[:diff]
20
- handle_diff_analysis(config)
21
- elsif user_input.empty? # No input text after options
22
- puts "Enter your natural language command:"
23
- input_text = $stdin.gets.chomp
24
- process_natural_language_command(input_text, config)
25
- else # Natural language command
17
+ # Diff functionality has been removed from N2B::CLI
18
+ # if @options[:diff]
19
+ # handle_diff_analysis(config)
20
+ # els
21
+ if user_input.empty? # No input text after options
22
+ # If config mode was chosen, it's handled by get_config.
23
+ # If not, and no input, prompt for it.
24
+ unless @options[:config] # Don't prompt if only -c or --advanced-config was used
25
+ puts "Enter your natural language command (or type 'exit' or 'quit'):"
26
+ input_text = $stdin.gets.chomp
27
+ exit if ['exit', 'quit'].include?(input_text.downcase)
28
+ process_natural_language_command(input_text, config)
29
+ end
30
+ else # Natural language command provided as argument
26
31
  process_natural_language_command(user_input, config)
27
32
  end
28
33
  end
29
34
 
30
35
  protected
31
36
 
32
- def handle_diff_analysis(config)
33
- vcs_type = get_vcs_type
34
- if vcs_type == :none
35
- puts "Error: Not a git or hg repository."
36
- exit 1
37
- end
38
-
39
- # Get requirements file from parsed options
40
- requirements_filepath = @options[:requirements]
41
- user_prompt_addition = @args.join(' ') # All remaining args are user prompt addition
42
-
43
- # Jira Ticket Information
44
- jira_ticket = @options[:jira_ticket]
45
- jira_update_flag = @options[:jira_update] # true, false, or nil
46
-
47
- requirements_content = nil # Initialize requirements_content
48
-
49
- if jira_ticket
50
- puts "Jira ticket specified: #{jira_ticket}"
51
- if config['jira'] && config['jira']['domain'] && config['jira']['email'] && config['jira']['api_key']
52
- begin
53
- jira_client = N2B::JiraClient.new(config) # Pass the whole config
54
- puts "Fetching Jira ticket details..."
55
- # If a requirements file is also provided, the Jira ticket will take precedence.
56
- # Or, we could append/prepend. For now, Jira overwrites.
57
- requirements_content = jira_client.fetch_ticket(jira_ticket)
58
- puts "Successfully fetched Jira ticket details."
59
- # The fetched content is now in requirements_content and will be passed to analyze_diff
60
- rescue N2B::JiraClient::JiraApiError => e
61
- puts "Error fetching Jira ticket: #{e.message}"
62
- puts "Proceeding with diff analysis without Jira ticket details."
63
- rescue ArgumentError => e # Catches missing Jira config in JiraClient.new
64
- puts "Jira configuration error: #{e.message}"
65
- puts "Please ensure Jira is configured correctly using 'n2b -c'."
66
- puts "Proceeding with diff analysis without Jira ticket details."
67
- rescue StandardError => e
68
- puts "An unexpected error occurred while fetching Jira ticket: #{e.message}"
69
- puts "Proceeding with diff analysis without Jira ticket details."
70
- end
71
- else
72
- puts "Jira configuration is missing or incomplete in N2B settings."
73
- puts "Please configure Jira using 'n2b -c' to fetch ticket details."
74
- puts "Proceeding with diff analysis without Jira ticket details."
75
- end
76
- # Handling of jira_update_flag can be done elsewhere, e.g., after analysis
77
- if jira_update_flag == true
78
- puts "Note: Jira ticket update (--jira-update) is flagged."
79
- # Actual update logic will be separate
80
- elsif jira_update_flag == false
81
- puts "Note: Jira ticket will not be updated (--jira-no-update)."
82
- end
83
- end
84
-
85
- # Load requirements from file if no Jira ticket was fetched or if specifically desired even with Jira.
86
- # Current logic: Jira fetch, if successful, populates requirements_content.
87
- # If Jira not specified, or fetch failed, try to load from file.
88
- if requirements_content.nil? && requirements_filepath
89
- if File.exist?(requirements_filepath)
90
- puts "Loading requirements from file: #{requirements_filepath}"
91
- requirements_content = File.read(requirements_filepath)
92
- else
93
- puts "Error: Requirements file not found: #{requirements_filepath}"
94
- # Decide if to exit or proceed. For now, proceed.
95
- puts "Proceeding with diff analysis without file-based requirements."
96
- end
97
- elsif requirements_content && requirements_filepath
98
- puts "Note: Both Jira ticket and requirements file were provided. Using Jira ticket content for analysis."
99
- end
100
-
101
- diff_output = execute_vcs_diff(vcs_type, @options[:branch])
102
- analysis_result = analyze_diff(diff_output, config, user_prompt_addition, requirements_content) # Store the result
103
-
104
- # --- Jira Update Logic ---
105
- if jira_ticket && analysis_result && !analysis_result.empty?
106
- # Check if Jira config is valid for updating
107
- if config['jira'] && config['jira']['domain'] && config['jira']['email'] && config['jira']['api_key']
108
- jira_comment_data = format_analysis_for_jira(analysis_result)
109
- proceed_with_update = false
110
-
111
- if jira_update_flag == true # --jira-update used
112
- proceed_with_update = true
113
- elsif jira_update_flag.nil? # Neither --jira-update nor --jira-no-update used
114
- puts "\nWould you like to update Jira ticket #{jira_ticket} with this analysis? (y/n)"
115
- user_choice = $stdin.gets.chomp.downcase
116
- proceed_with_update = user_choice == 'y'
117
- end # If jira_update_flag is false, proceed_with_update remains false
118
-
119
- if proceed_with_update
120
- begin
121
- # Re-instantiate JiraClient or use an existing one if available and in scope
122
- # For safety and simplicity here, re-instantiate with current config.
123
- update_jira_client = N2B::JiraClient.new(config)
124
- puts "Updating Jira ticket #{jira_ticket}..."
125
- if update_jira_client.update_ticket(jira_ticket, jira_comment_data)
126
- puts "Jira ticket #{jira_ticket} updated successfully."
127
- else
128
- # update_ticket currently returns true/false, but might raise error for http issues
129
- puts "Failed to update Jira ticket #{jira_ticket}. The client did not report an error, but the update may not have completed."
130
- end
131
- rescue N2B::JiraClient::JiraApiError => e
132
- puts "Error updating Jira ticket: #{e.message}"
133
- rescue ArgumentError => e # From JiraClient.new if config is suddenly invalid
134
- puts "Jira configuration error before update: #{e.message}"
135
- rescue StandardError => e
136
- puts "An unexpected error occurred while updating Jira ticket: #{e.message}"
137
- end
138
- else
139
- puts "Jira ticket update skipped."
140
- end
141
- else
142
- puts "Jira configuration is missing or incomplete. Cannot proceed with Jira update."
143
- end
144
- elsif jira_ticket && (analysis_result.nil? || analysis_result.empty?)
145
- puts "Skipping Jira update as analysis result was empty or not generated."
146
- end
147
- # --- End of Jira Update Logic ---
148
-
149
- analysis_result # Return analysis_result from handle_diff_analysis
150
- end
151
-
152
- def get_vcs_type
153
- if Dir.exist?(File.join(Dir.pwd, '.git'))
154
- :git
155
- elsif Dir.exist?(File.join(Dir.pwd, '.hg'))
156
- :hg
157
- else
158
- :none
159
- end
160
- end
161
-
162
- def execute_vcs_diff(vcs_type, branch_option = nil)
163
- case vcs_type
164
- when :git
165
- if branch_option
166
- target_branch = branch_option == 'auto' ? detect_git_default_branch : branch_option
167
- if target_branch
168
- # Validate that the target branch exists
169
- unless validate_git_branch_exists(target_branch)
170
- puts "Error: Branch '#{target_branch}' does not exist."
171
- puts "Available branches:"
172
- puts `git branch -a`.lines.map(&:strip).reject(&:empty?)
173
- exit 1
174
- end
175
-
176
- puts "Comparing current branch against '#{target_branch}'..."
177
- `git diff #{target_branch}...HEAD`
178
- else
179
- puts "Could not detect default branch, falling back to HEAD diff..."
180
- `git diff HEAD`
181
- end
182
- else
183
- `git diff HEAD`
184
- end
185
- when :hg
186
- if branch_option
187
- target_branch = branch_option == 'auto' ? detect_hg_default_branch : branch_option
188
- if target_branch
189
- # Validate that the target branch exists
190
- unless validate_hg_branch_exists(target_branch)
191
- puts "Error: Branch '#{target_branch}' does not exist."
192
- puts "Available branches:"
193
- puts `hg branches`.lines.map(&:strip).reject(&:empty?)
194
- exit 1
195
- end
196
-
197
- puts "Comparing current branch against '#{target_branch}'..."
198
- `hg diff -r #{target_branch}`
199
- else
200
- puts "Could not detect default branch, falling back to standard diff..."
201
- `hg diff`
202
- end
203
- else
204
- `hg diff`
205
- end
206
- else
207
- "" # Should not happen if get_vcs_type logic is correct and checked before calling
208
- end
209
- end
210
-
211
- def detect_git_default_branch
212
- # Try multiple methods to detect the default branch
213
-
214
- # Method 1: Check origin/HEAD symbolic ref
215
- result = `git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null`.strip
216
- if $?.success? && !result.empty?
217
- return result.split('/').last
218
- end
219
-
220
- # Method 2: Check remote show origin
221
- result = `git remote show origin 2>/dev/null | grep "HEAD branch"`.strip
222
- if $?.success? && !result.empty?
223
- match = result.match(/HEAD branch:\s*(\w+)/)
224
- return match[1] if match
225
- end
226
-
227
- # Method 3: Check if common default branches exist
228
- ['main', 'master'].each do |branch|
229
- result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
230
- if $?.success?
231
- return branch
232
- end
233
- end
234
-
235
- # Method 4: Fallback - check local branches
236
- ['main', 'master'].each do |branch|
237
- result = `git rev-parse --verify #{branch} 2>/dev/null`
238
- if $?.success?
239
- return branch
240
- end
241
- end
242
-
243
- # If all else fails, return nil
244
- nil
245
- end
246
-
247
- def detect_hg_default_branch
248
- # Method 1: Check current branch (if it's 'default', that's the main branch)
249
- result = `hg branch 2>/dev/null`.strip
250
- if $?.success? && result == 'default'
251
- return 'default'
252
- end
253
-
254
- # Method 2: Look for 'default' branch in branch list
255
- result = `hg branches 2>/dev/null`
256
- if $?.success? && result.include?('default')
257
- return 'default'
258
- end
259
-
260
- # Method 3: Check if there are any branches at all
261
- result = `hg branches 2>/dev/null`.strip
262
- if $?.success? && !result.empty?
263
- # Get the first branch (usually the main one)
264
- first_branch = result.lines.first&.split&.first
265
- return first_branch if first_branch
266
- end
267
-
268
- # Fallback to 'default' (standard hg main branch name)
269
- 'default'
270
- end
271
-
272
- def validate_git_branch_exists(branch)
273
- # Check if branch exists locally
274
- _result = `git rev-parse --verify #{branch} 2>/dev/null`
275
- return true if $?.success?
276
-
277
- # Check if branch exists on remote
278
- _result = `git rev-parse --verify origin/#{branch} 2>/dev/null`
279
- return true if $?.success?
280
-
281
- false
282
- end
283
-
284
- def validate_hg_branch_exists(branch)
285
- # Check if branch exists in hg branches
286
- result = `hg branches 2>/dev/null`
287
- if $?.success?
288
- return result.lines.any? { |line| line.strip.start_with?(branch) }
289
- end
290
-
291
- # If we can't list branches, assume it exists (hg is more permissive)
292
- true
293
- end
37
+ # All diff-related methods like handle_diff_analysis, get_vcs_type, etc., are removed.
294
38
 
295
39
  private
296
40
 
@@ -450,6 +194,19 @@ REQUIREMENTS_BLOCK
450
194
  improvements.map(&:strip).reject(&:empty?)
451
195
  end
452
196
 
197
+ def format_analysis_for_github(analysis_result)
198
+ return "No analysis result available." if analysis_result.nil? || analysis_result.empty?
199
+
200
+ {
201
+ implementation_summary: analysis_result['ticket_implementation_summary']&.strip,
202
+ technical_summary: analysis_result['summary']&.strip,
203
+ issues: format_issues_for_adf(analysis_result['errors']),
204
+ improvements: format_improvements_for_adf(analysis_result['improvements']),
205
+ test_coverage: analysis_result['test_coverage']&.strip,
206
+ requirements_evaluation: analysis_result['requirements_evaluation']&.strip
207
+ }
208
+ end
209
+
453
210
  def extract_json_from_response(response)
454
211
  # First try to parse the response as-is
455
212
  begin
@@ -538,41 +295,6 @@ REQUIREMENTS_BLOCK
538
295
  context_sections
539
296
  end
540
297
 
541
- def call_llm_for_diff_analysis(prompt, config)
542
- begin
543
- llm_service_name = config['llm']
544
- llm = case llm_service_name
545
- when 'openai'
546
- N2M::Llm::OpenAi.new(config)
547
- when 'claude'
548
- N2M::Llm::Claude.new(config)
549
- when 'gemini'
550
- N2M::Llm::Gemini.new(config)
551
- when 'openrouter'
552
- N2M::Llm::OpenRouter.new(config)
553
- when 'ollama'
554
- N2M::Llm::Ollama.new(config)
555
- else
556
- # Should not happen if config is validated, but as a safeguard:
557
- raise N2B::Error, "Unsupported LLM service: #{llm_service_name}"
558
- end
559
-
560
- puts "🔍 AI is analyzing your code diff..."
561
- response_json_str = analyze_diff_with_spinner(llm, prompt)
562
- response_json_str
563
- rescue N2B::LlmApiError => e # This catches errors from analyze_code_diff
564
- puts "Error communicating with the LLM: #{e.message}"
565
-
566
- # Check if it might be a model-related error
567
- if e.message.include?('model') || e.message.include?('Model') || e.message.include?('invalid') || e.message.include?('not found')
568
- puts "\nThis might be due to an invalid or unsupported model configuration."
569
- puts "Run 'n2b -c' to reconfigure your model settings."
570
- end
571
-
572
- return '{"summary": "Error: Could not analyze diff due to LLM API error.", "errors": [], "improvements": []}'
573
- end
574
- end
575
-
576
298
  def append_to_llm_history_file(commands)
577
299
  File.open(HISTORY_FILE, 'a') do |file|
578
300
  file.puts(commands)
@@ -591,19 +313,19 @@ REQUIREMENTS_BLOCK
591
313
  llm_service_name = config['llm']
592
314
  llm = case llm_service_name
593
315
  when 'openai'
594
- N2M::Llm::OpenAi.new(config)
316
+ N2B::Llm::OpenAi.new(config)
595
317
  when 'claude'
596
- N2M::Llm::Claude.new(config)
318
+ N2B::Llm::Claude.new(config)
597
319
  when 'gemini'
598
- N2M::Llm::Gemini.new(config)
320
+ N2B::Llm::Gemini.new(config)
599
321
  when 'openrouter'
600
- N2M::Llm::OpenRouter.new(config)
322
+ N2B::Llm::OpenRouter.new(config)
601
323
  when 'ollama'
602
- N2M::Llm::Ollama.new(config)
324
+ N2B::Llm::Ollama.new(config)
603
325
  else
604
326
  # Fallback or error, though config validation should prevent this
605
327
  puts "Warning: Unsupported LLM service '#{llm_service_name}' configured. Falling back to Claude."
606
- N2M::Llm::Claude.new(config)
328
+ N2B::Llm::Claude.new(config)
607
329
  end
608
330
 
609
331
  # This content is specific to bash command generation
@@ -782,31 +504,6 @@ REQUIREMENTS_BLOCK
782
504
  end
783
505
  end
784
506
 
785
- def analyze_diff_with_spinner(llm, prompt)
786
- spinner_chars = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
787
- spinner_thread = Thread.new do
788
- i = 0
789
- while true
790
- print "\r🔍 #{spinner_chars[i % spinner_chars.length]} Analyzing diff..."
791
- $stdout.flush
792
- sleep(0.1)
793
- i += 1
794
- end
795
- end
796
-
797
- begin
798
- result = llm.analyze_code_diff(prompt)
799
- spinner_thread.kill
800
- print "\r#{' ' * 30}\r" # Clear the spinner line
801
- puts "✅ Diff analysis complete!"
802
- result
803
- rescue => e
804
- spinner_thread.kill
805
- print "\r#{' ' * 30}\r" # Clear the spinner line
806
- raise e
807
- end
808
- end
809
-
810
507
  def attempt_json_repair_for_commands(malformed_response, llm)
811
508
  repair_prompt = <<~PROMPT
812
509
  The following response was supposed to be valid JSON with keys "commands" (array) and "explanation" (string), but it has formatting issues. Please fix it and return ONLY the corrected JSON:
@@ -850,63 +547,41 @@ REQUIREMENTS_BLOCK
850
547
  options = {
851
548
  execute: false,
852
549
  config: nil,
853
- diff: false,
854
- requirements: nil,
855
- branch: nil,
856
- jira_ticket: nil,
857
- jira_update: nil, # Using nil as default, true for --jira-update, false for --jira-no-update
858
- advanced_config: false # New option for advanced configuration flow
550
+ # diff: false, # Removed
551
+ # requirements: nil, # Removed
552
+ # branch: nil, # Removed
553
+ # jira_ticket: nil, # Removed
554
+ # jira_update: nil, # Removed
555
+ advanced_config: false
859
556
  }
860
557
 
861
558
  parser = OptionParser.new do |opts|
862
559
  opts.banner = "Usage: n2b [options] [natural language command]"
863
560
 
864
- opts.on('-x', '--execute', 'Execute the commands after confirmation') do
561
+ opts.on('-x', '--execute', 'Execute the translated commands after confirmation.') do
865
562
  options[:execute] = true
866
563
  end
867
564
 
868
- opts.on('-d', '--diff', 'Analyze git/hg diff with AI') do
869
- options[:diff] = true
870
- end
871
-
872
- opts.on('-b', '--branch [BRANCH]', 'Compare against branch (default: auto-detect main/master)') do |branch|
873
- options[:branch] = branch || 'auto'
874
- end
875
-
876
- opts.on('-r', '--requirements FILE', 'Requirements file for diff analysis') do |file|
877
- options[:requirements] = file
878
- end
565
+ # Removed options: -d, --diff, -b, --branch, -r, --requirements, -j, --jira, --jira-update, --jira-no-update
879
566
 
880
- opts.on('-j', '--jira JIRA_ID_OR_URL', 'Jira ticket ID or URL for context or update') do |jira|
881
- options[:jira_ticket] = jira
882
- end
883
-
884
- opts.on('--jira-update', 'Update the linked Jira ticket (requires -j)') do
885
- options[:jira_update] = true
567
+ opts.on('-c', '--config', 'Configure N2B (API key, model, privacy settings, etc.).') do
568
+ options[:config] = true
886
569
  end
887
570
 
888
- opts.on('--jira-no-update', 'Do not update the linked Jira ticket (requires -j)') do
889
- options[:jira_update] = false
571
+ opts.on('--advanced-config', 'Access advanced configuration options.') do
572
+ options[:advanced_config] = true
573
+ options[:config] = true # --advanced-config implies -c
890
574
  end
891
575
 
892
- opts.on('-h', '--help', 'Print this help') do
576
+ opts.on_tail('-h', '--help', 'Show this help message.') do
893
577
  puts opts
894
578
  exit
895
579
  end
896
580
 
897
- opts.on('-v', '--version', 'Show version') do
898
- puts "n2b version #{N2B::VERSION}"
581
+ opts.on_tail('-v', '--version', 'Show version.') do
582
+ puts "n2b version #{N2B::VERSION}" # Assuming N2B::VERSION is defined
899
583
  exit
900
584
  end
901
-
902
- opts.on('-c', '--config', 'Configure the API key and model') do
903
- options[:config] = true
904
- end
905
-
906
- opts.on('--advanced-config', 'Access advanced configuration options including Jira and privacy settings') do
907
- options[:advanced_config] = true
908
- options[:config] = true # Forcing config mode if advanced is chosen
909
- end
910
585
  end
911
586
 
912
587
  begin
@@ -916,36 +591,14 @@ REQUIREMENTS_BLOCK
916
591
  puts ""
917
592
  puts parser.help
918
593
  exit 1
919
- end
920
-
921
- # Validate option combinations
922
- if options[:branch] && !options[:diff]
923
- puts "Error: --branch option can only be used with --diff"
924
- puts ""
925
- puts parser.help
926
- exit 1
927
- end
928
-
929
- if options[:jira_update] == true && options[:jira_ticket].nil?
930
- puts "Error: --jira-update option requires a Jira ticket to be specified with -j or --jira."
931
- puts ""
932
- puts parser.help
933
- exit 1
934
- end
935
-
936
- if options[:jira_update] == false && options[:jira_ticket].nil?
937
- puts "Error: --jira-no-update option requires a Jira ticket to be specified with -j or --jira."
594
+ rescue OptionParser::MissingArgument => e
595
+ puts "Error: #{e.message}"
938
596
  puts ""
939
597
  puts parser.help
940
598
  exit 1
941
599
  end
942
600
 
943
- if options[:jira_update] == true && options[:jira_update] == false
944
- puts "Error: --jira-update and --jira-no-update are mutually exclusive."
945
- puts ""
946
- puts parser.help
947
- exit 1
948
- end
601
+ # Removed validation logic for diff/jira related options
949
602
 
950
603
  options
951
604
  end