n2b 2.0.0 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '059751bf0eb02d00fa853b62ad738a873223766049608e6627c31dfbd1927363'
4
- data.tar.gz: 72406255d6d2d63943e1e9dcea7017f8e56d27d9a2b9c7cb4afcd09867ce51f7
3
+ metadata.gz: 904b66daf8fd1cdb0a4fba961fc0431dbf5bf841c022d3b31b52daa7c549f02d
4
+ data.tar.gz: e89ae0125af9f1c2c40ac2ad14f0a5ced1dc6bd882e88aa81ccf34b2d958528f
5
5
  SHA512:
6
- metadata.gz: b3ef595ab79f72892164caf89308cd74264580d297afe0a988cae15614b276507726baa403f2ee87111fa60b9e7ca7dc532fdded04132a6a5ff8b271b3bffb69
7
- data.tar.gz: a34b6395591167c64a73db80afd592fa5dca021fe856335137964303c655bd19f09bb145f27fe3f205389311bc8a4a48792168fc66843960bfb523cb22ccfb05
6
+ metadata.gz: 528eefd2847a246bcc5282b1ea360477737c9c85e8643fadce3be1eaaa6c7b64464e9cdba1d1a59d8bcad4eec25fd912dd5e9d04e1abbd5f31ab39c646b9b363
7
+ data.tar.gz: abcee779b759722282f5b48046c97418d215e0ff528b9c6ea76be4cca09335256606150eb7c18c3a2bff978559f5560768b0fdf02c35bbcd05a65023de3ff4b0
data/lib/n2b/base.rb CHANGED
@@ -46,13 +46,19 @@ module N2B
46
46
  # Determine if it's effectively a first-time setup for core LLM details
47
47
  is_first_time_core_setup = config['llm'].nil?
48
48
 
49
- 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
50
56
  puts "--- N2B Core LLM Configuration ---"
51
- 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']}]: "
52
58
  llm_choice = $stdin.gets.chomp
53
59
  llm_choice = config['llm'] if llm_choice.empty? && !config['llm'].nil? # Keep current if input is empty
54
60
 
55
- 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)
56
62
  puts "Invalid language model selection. Defaulting to 'claude' or previous if available."
57
63
  llm_choice = config['llm'] || 'claude' # Fallback
58
64
  end
@@ -60,10 +66,11 @@ module N2B
60
66
  selected_llm = case llm_choice
61
67
  when '1', 'claude' then 'claude'
62
68
  when '2', 'openai' then 'openai'
63
- when '3', 'gemini' then 'gemini'
69
+ when '3', 'gemini' then 'gemini' # This is now explicitly API Key based Gemini
64
70
  when '4', 'openrouter' then 'openrouter'
65
71
  when '5', 'ollama' then 'ollama'
66
- 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'
67
74
  end
68
75
  config['llm'] = selected_llm
69
76
 
@@ -71,18 +78,76 @@ module N2B
71
78
  puts "\n--- Ollama Specific Configuration ---"
72
79
  puts "Ollama typically runs locally and doesn't require an API key."
73
80
 
74
- # Use new model configuration system
75
81
  current_model = config['model']
76
- 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)
77
83
  config['model'] = model_choice
78
84
 
79
85
  current_ollama_api_url = config['ollama_api_url'] || N2M::Llm::Ollama::DEFAULT_OLLAMA_API_URI
80
86
  print "Enter Ollama API base URL [current: #{current_ollama_api_url}]: "
81
87
  ollama_api_url_input = $stdin.gets.chomp
82
88
  config['ollama_api_url'] = ollama_api_url_input.empty? ? current_ollama_api_url : ollama_api_url_input
83
- config.delete('access_key') # Remove access_key if switching to ollama
84
- else
85
- # 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
86
151
  puts "\n--- #{selected_llm.capitalize} Specific Configuration ---"
87
152
  current_api_key = config['access_key']
88
153
  print "Enter your #{selected_llm} API key #{ current_api_key.nil? || current_api_key.empty? ? '' : '[leave blank to keep current]' }: "
@@ -94,9 +159,12 @@ module N2B
94
159
  puts "API key is required for #{selected_llm}."
95
160
  exit 1
96
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
97
165
 
98
166
  current_model = config['model']
99
- model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model) # Renamed
167
+ model_choice = N2B::ModelConfig.get_model_choice(selected_llm, current_model)
100
168
  config['model'] = model_choice
101
169
 
102
170
  if selected_llm == 'openrouter'
@@ -336,23 +404,76 @@ module N2B
336
404
  def validate_config(config)
337
405
  errors = []
338
406
 
339
- # Validate LLM configuration
407
+ # Validate LLM provider selection
340
408
  if config['llm'].nil? || config['llm'].empty?
341
409
  errors << "LLM provider not specified"
342
410
  end
343
411
 
344
- # Validate model name
412
+ # Validate model name (existing logic can likely stay as is)
345
413
  if config['model'].nil? || config['model'].empty?
346
414
  errors << "Model not specified"
347
- elsif config['model'].length < 3
415
+ elsif config['model'].length < 3 # Example of an existing check
348
416
  errors << "Model name '#{config['model']}' seems too short - might be invalid"
349
- 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
350
418
  errors << "Model name '#{config['model']}' appears to be a boolean response rather than a model name"
351
419
  end
352
420
 
353
- # Validate API key for non-Ollama providers
354
- if config['llm'] != 'ollama' && (config['access_key'].nil? || config['access_key'].empty?)
355
- errors << "API key missing for #{config['llm']} provider"
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
434
+
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"
442
+ end
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.
356
477
  end
357
478
 
358
479
  # Validate editor configuration (optional, so more like warnings or info)
data/lib/n2b/cli.rb CHANGED
@@ -296,13 +296,14 @@ REQUIREMENTS_BLOCK
296
296
  end
297
297
 
298
298
  def append_to_llm_history_file(commands)
299
- File.open(HISTORY_FILE, 'a') do |file|
299
+ File.open(self.class.history_file, 'a') do |file|
300
300
  file.puts(commands)
301
301
  end
302
302
  end
303
-
303
+
304
304
  def read_llm_history_file
305
- history = File.read(HISTORY_FILE) if File.exist?(HISTORY_FILE)
305
+ history_file_path = self.class.history_file
306
+ history = File.read(history_file_path) if File.exist?(history_file_path)
306
307
  history ||= ''
307
308
  # limit to 20 most recent commands
308
309
  history.split("\n").last(20).join("\n")
@@ -316,8 +317,10 @@ REQUIREMENTS_BLOCK
316
317
  N2B::Llm::OpenAi.new(config)
317
318
  when 'claude'
318
319
  N2B::Llm::Claude.new(config)
319
- when 'gemini'
320
+ when 'gemini' # This is for API Key based Gemini
320
321
  N2B::Llm::Gemini.new(config)
322
+ when 'vertexai' # This is for Credential File based Vertex AI
323
+ N2B::Llm::VertexAi.new(config)
321
324
  when 'openrouter'
322
325
  N2B::Llm::OpenRouter.new(config)
323
326
  when 'ollama'
@@ -25,9 +25,25 @@ openai:
25
25
 
26
26
  gemini:
27
27
  suggested:
28
- gemini-2.5-flash: "gemini-2.5-flash-preview-05-20"
29
- gemini-2.5-pro: "gemini-2.5-pro-preview-05-06"
30
- default: "gemini-2.5-flash"
28
+ gemini-2.0-flash: "gemini-2.0-flash-001"
29
+ gemini-2.0-flash-lite: "gemini-2.0-flash-lite-001"
30
+ gemini-2.5-flash: "gemini-2.5-flash-preview"
31
+ gemini-2.5-pro: "gemini-2.5-pro-preview"
32
+ # Legacy models (may not be available in all regions)
33
+ gemini-2.5-flash-05-20: "gemini-2.5-flash-preview-05-20"
34
+ gemini-2.5-pro-05-06: "gemini-2.5-pro-preview-05-06"
35
+ default: "gemini-2.0-flash"
36
+
37
+ vertexai:
38
+ suggested:
39
+ gemini-2.0-flash: "gemini-2.0-flash-001"
40
+ gemini-2.0-flash-lite: "gemini-2.0-flash-lite-001"
41
+ gemini-2.5-flash: "gemini-2.5-flash-preview"
42
+ gemini-2.5-pro: "gemini-2.5-pro-preview"
43
+ # Legacy models (may not be available in all regions)
44
+ gemini-2.5-flash-05-20: "gemini-2.5-flash-preview-05-20"
45
+ gemini-2.5-pro-05-06: "gemini-2.5-pro-preview-05-06"
46
+ default: "gemini-2.0-flash"
31
47
 
32
48
  openrouter:
33
49
  suggested:
@@ -2,6 +2,7 @@ require 'net/http'
2
2
  require 'uri'
3
3
  require 'json'
4
4
  require 'base64'
5
+ require 'time'
5
6
  require_relative 'template_engine'
6
7
 
7
8
  module N2B
@@ -390,7 +391,7 @@ module N2B
390
391
  else
391
392
  formatted_date = created
392
393
  end
393
- rescue
394
+ rescue => e
394
395
  formatted_date = created
395
396
  end
396
397
 
@@ -1,6 +1,7 @@
1
1
  require 'net/http'
2
2
  require 'json'
3
3
  require 'uri'
4
+ # Removed googleauth require
4
5
  require_relative '../model_config'
5
6
 
6
7
  module N2B
@@ -9,7 +10,7 @@ module N2B
9
10
  API_URI = URI.parse('https://generativelanguage.googleapis.com/v1beta/models')
10
11
 
11
12
  def initialize(config)
12
- @config = config
13
+ @config = config # Used for access_key and model
13
14
  end
14
15
 
15
16
  def get_model_name
@@ -29,6 +30,8 @@ module N2B
29
30
  request = Net::HTTP::Post.new(uri)
30
31
  request.content_type = 'application/json'
31
32
 
33
+ # Removed Authorization header and token fetching logic
34
+
32
35
  request.body = JSON.dump({
33
36
  "contents" => [{
34
37
  "parts" => [{
@@ -82,6 +85,8 @@ module N2B
82
85
  request = Net::HTTP::Post.new(uri)
83
86
  request.content_type = 'application/json'
84
87
 
88
+ # Removed Authorization header and token fetching logic
89
+
85
90
  request.body = JSON.dump({
86
91
  "contents" => [{
87
92
  "parts" => [{
@@ -0,0 +1,225 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'googleauth' # For service account authentication
5
+ require_relative '../model_config'
6
+ require_relative '../errors'
7
+
8
+ module N2B
9
+ module Llm
10
+ class VertexAi
11
+ # Vertex AI API endpoint format
12
+ DEFAULT_LOCATION = 'us-central1'
13
+ COMMON_LOCATIONS = [
14
+ 'us-central1', # Iowa, USA
15
+ 'us-east1', # South Carolina, USA
16
+ 'us-west1', # Oregon, USA
17
+ 'europe-west1', # Belgium
18
+ 'europe-west4', # Netherlands
19
+ 'asia-northeast1', # Tokyo, Japan
20
+ 'asia-southeast1' # Singapore
21
+ ].freeze
22
+
23
+ # HTTP timeout in seconds
24
+ REQUEST_TIMEOUT = 60
25
+
26
+ def initialize(config)
27
+ @config = config # Contains 'vertex_credential_file' and 'model'
28
+ @project_id = nil
29
+ @location = DEFAULT_LOCATION
30
+ load_project_info
31
+ end
32
+
33
+ private
34
+
35
+ def load_project_info
36
+ # Extract project_id from the credential file
37
+ credential_data = JSON.parse(File.read(@config['vertex_credential_file']))
38
+ @project_id = credential_data['project_id']
39
+
40
+ # Allow location override from config, with intelligent defaults
41
+ @location = determine_location
42
+ rescue JSON::ParserError => e
43
+ raise N2B::LlmApiError.new("Invalid JSON in credential file: #{e.message}")
44
+ rescue Errno::ENOENT => e
45
+ raise N2B::LlmApiError.new("Credential file not found: #{e.message}")
46
+ rescue => e
47
+ raise N2B::LlmApiError.new("Failed to load project info from credential file: #{e.message}")
48
+ end
49
+
50
+ def determine_location
51
+ # 1. Use explicit config if provided
52
+ return @config['vertex_location'] if @config['vertex_location']
53
+
54
+ # 2. Try to detect from project_id patterns (some projects have region hints)
55
+ # 3. Default to us-central1 but provide helpful error message if it fails
56
+ DEFAULT_LOCATION
57
+ end
58
+
59
+ def build_api_uri(model)
60
+ "https://#{@location}-aiplatform.googleapis.com/v1/projects/#{@project_id}/locations/#{@location}/publishers/google/models/#{model}:generateContent"
61
+ end
62
+
63
+ public
64
+
65
+ def get_model_name
66
+ # Resolve model name using the centralized configuration for 'vertexai'
67
+ model_name = N2B::ModelConfig.resolve_model('vertexai', @config['model'])
68
+ if model_name.nil? || model_name.empty?
69
+ # Fallback to default if no model specified for vertexai
70
+ model_name = N2B::ModelConfig.resolve_model('vertexai', N2B::ModelConfig.default_model('vertexai'))
71
+ end
72
+ # If still no model, a generic default could be used, or an error raised.
73
+ # For now, assume ModelConfig handles returning a usable default or nil.
74
+ # If ModelConfig.resolve_model can return nil and that's an issue, add handling here.
75
+ # For example, if model_name is still nil, raise an error or use a hardcoded default.
76
+ # Let's assume ModelConfig provides a valid model or a sensible default from models.yml.
77
+ model_name
78
+ end
79
+
80
+ def make_request(content)
81
+ model = get_model_name
82
+ raise N2B::LlmApiError.new("No model configured for Vertex AI.") if model.nil? || model.empty?
83
+
84
+ uri = URI.parse(build_api_uri(model))
85
+
86
+ request = Net::HTTP::Post.new(uri)
87
+ request.content_type = 'application/json'
88
+
89
+ begin
90
+ scope = 'https://www.googleapis.com/auth/cloud-platform'
91
+ authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
92
+ json_key_io: File.open(@config['vertex_credential_file']),
93
+ scope: scope
94
+ )
95
+ access_token = authorizer.fetch_access_token!['access_token']
96
+ request['Authorization'] = "Bearer #{access_token}"
97
+ rescue StandardError => e
98
+ raise N2B::LlmApiError.new("Vertex AI - Failed to obtain Google Cloud access token: #{e.message}")
99
+ end
100
+
101
+ request.body = JSON.dump({
102
+ "contents" => [{
103
+ "role" => "user",
104
+ "parts" => [{
105
+ "text" => content
106
+ }]
107
+ }],
108
+ "generationConfig" => {
109
+ "responseMimeType" => "application/json" # Requesting JSON output from the LLM
110
+ }
111
+ })
112
+
113
+ begin
114
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
115
+ http.read_timeout = REQUEST_TIMEOUT
116
+ http.open_timeout = 30
117
+ http.request(request)
118
+ end
119
+ rescue Net::TimeoutError, Net::ReadTimeout, Net::OpenTimeout => e
120
+ error_msg = "Vertex AI request timed out (region: #{@location}): #{e.message}"
121
+ error_msg += "\n\nThis might be a region issue. Try reconfiguring with 'n2b -c' and select a different region."
122
+ error_msg += "\nFor EU users, try: europe-west1 (Belgium) or europe-west4 (Netherlands)"
123
+ error_msg += "\nCommon regions: #{COMMON_LOCATIONS.join(', ')}"
124
+ raise N2B::LlmApiError.new(error_msg)
125
+ rescue => e
126
+ raise N2B::LlmApiError.new("Vertex AI network error: #{e.message}")
127
+ end
128
+
129
+ if response.code != '200'
130
+ error_msg = "Vertex AI LLM API Error: #{response.code} #{response.message} - #{response.body}"
131
+ if response.code == '404'
132
+ error_msg += "\n\nThis might be a region or model availability issue. Current region: #{@location}"
133
+ error_msg += "\nNote: Google models via Vertex AI are not available in all regions."
134
+ error_msg += "\nTry reconfiguring with 'n2b -c' and:"
135
+ error_msg += "\n 1. Select a different region (Common regions: #{COMMON_LOCATIONS.join(', ')})"
136
+ error_msg += "\n 2. Choose a different model (some models are only available in specific regions)"
137
+ end
138
+ raise N2B::LlmApiError.new(error_msg)
139
+ end
140
+
141
+ parsed_response = JSON.parse(response.body)
142
+ # Vertex AI response structure is the same as Gemini API
143
+ answer = parsed_response['candidates'].first['content']['parts'].first['text']
144
+
145
+ begin
146
+ if answer.strip.start_with?('{') && answer.strip.end_with?('}')
147
+ answer = JSON.parse(answer) # LLM returned JSON as a string
148
+ else
149
+ # If not JSON, wrap it as per existing Gemini class (for CLI compatibility)
150
+ answer = { 'explanation' => answer, 'code' => nil }
151
+ end
152
+ rescue JSON::ParserError
153
+ answer = { 'explanation' => answer, 'code' => nil }
154
+ end
155
+ answer
156
+ end
157
+
158
+ def analyze_code_diff(prompt_content)
159
+ model = get_model_name
160
+ raise N2B::LlmApiError.new("No model configured for Vertex AI.") if model.nil? || model.empty?
161
+
162
+ uri = URI.parse(build_api_uri(model))
163
+
164
+ request = Net::HTTP::Post.new(uri)
165
+ request.content_type = 'application/json'
166
+
167
+ begin
168
+ scope = 'https://www.googleapis.com/auth/cloud-platform'
169
+ authorizer = Google::Auth::ServiceAccountCredentials.make_creds(
170
+ json_key_io: File.open(@config['vertex_credential_file']),
171
+ scope: scope
172
+ )
173
+ access_token = authorizer.fetch_access_token!['access_token']
174
+ request['Authorization'] = "Bearer #{access_token}"
175
+ rescue StandardError => e
176
+ raise N2B::LlmApiError.new("Vertex AI - Failed to obtain Google Cloud access token for diff analysis: #{e.message}")
177
+ end
178
+
179
+ request.body = JSON.dump({
180
+ "contents" => [{
181
+ "role" => "user",
182
+ "parts" => [{
183
+ "text" => prompt_content
184
+ }]
185
+ }],
186
+ "generationConfig" => {
187
+ "responseMimeType" => "application/json" # Expecting JSON response from LLM
188
+ }
189
+ })
190
+
191
+ begin
192
+ response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
193
+ http.read_timeout = REQUEST_TIMEOUT
194
+ http.open_timeout = 30
195
+ http.request(request)
196
+ end
197
+ rescue Net::TimeoutError, Net::ReadTimeout, Net::OpenTimeout => e
198
+ error_msg = "Vertex AI diff analysis timed out (region: #{@location}): #{e.message}"
199
+ error_msg += "\n\nThis might be a region issue. Try reconfiguring with 'n2b -c' and select a different region."
200
+ error_msg += "\nFor EU users, try: europe-west1 (Belgium) or europe-west4 (Netherlands)"
201
+ error_msg += "\nCommon regions: #{COMMON_LOCATIONS.join(', ')}"
202
+ raise N2B::LlmApiError.new(error_msg)
203
+ rescue => e
204
+ raise N2B::LlmApiError.new("Vertex AI network error during diff analysis: #{e.message}")
205
+ end
206
+
207
+ if response.code != '200'
208
+ error_msg = "Vertex AI LLM API Error for diff analysis: #{response.code} #{response.message} - #{response.body}"
209
+ if response.code == '404'
210
+ error_msg += "\n\nThis might be a region or model availability issue. Current region: #{@location}"
211
+ error_msg += "\nNote: Google models via Vertex AI are not available in all regions."
212
+ error_msg += "\nTry reconfiguring with 'n2b -c' and:"
213
+ error_msg += "\n 1. Select a different region (Common regions: #{COMMON_LOCATIONS.join(', ')})"
214
+ error_msg += "\n 2. Choose a different model (some models are only available in specific regions)"
215
+ end
216
+ raise N2B::LlmApiError.new(error_msg)
217
+ end
218
+
219
+ parsed_response = JSON.parse(response.body)
220
+ # Return the raw JSON string from the 'text' field, CLI will parse it.
221
+ parsed_response['candidates'].first['content']['parts'].first['text']
222
+ end
223
+ end
224
+ end
225
+ end
data/lib/n2b/merge_cli.rb CHANGED
@@ -844,6 +844,7 @@ REQUIREMENTS_BLOCK
844
844
  when 'openai' then N2B::Llm::OpenAi.new(config)
845
845
  when 'claude' then N2B::Llm::Claude.new(config)
846
846
  when 'gemini' then N2B::Llm::Gemini.new(config)
847
+ when 'vertexai' then N2B::Llm::VertexAi.new(config)
847
848
  when 'openrouter' then N2B::Llm::OpenRouter.new(config)
848
849
  when 'ollama' then N2B::Llm::Ollama.new(config)
849
850
  else raise N2B::Error, "Unsupported LLM service for analysis: #{llm_service_name}"
@@ -1083,6 +1084,8 @@ REQUIREMENTS_BLOCK
1083
1084
  N2B::Llm::Claude.new(config)
1084
1085
  when 'gemini'
1085
1086
  N2B::Llm::Gemini.new(config)
1087
+ when 'vertexai'
1088
+ N2B::Llm::VertexAi.new(config)
1086
1089
  when 'openrouter'
1087
1090
  N2B::Llm::OpenRouter.new(config)
1088
1091
  when 'ollama'
@@ -17,6 +17,14 @@ module N2B
17
17
  'claude' => { 'suggested' => { 'sonnet' => 'claude-3-sonnet-20240229' }, 'default' => 'sonnet' },
18
18
  'openai' => { 'suggested' => { 'gpt-4o-mini' => 'gpt-4o-mini' }, 'default' => 'gpt-4o-mini' },
19
19
  'gemini' => { 'suggested' => { 'gemini-flash' => 'gemini-2.0-flash' }, 'default' => 'gemini-flash' },
20
+ 'vertexai' => {
21
+ 'suggested' => {
22
+ 'gemini-2.0-flash' => 'gemini-2.0-flash-001',
23
+ 'gemini-2.5-flash' => 'gemini-2.5-flash-preview',
24
+ 'gemini-2.5-flash-05-20' => 'gemini-2.5-flash-preview-05-20'
25
+ },
26
+ 'default' => 'gemini-2.0-flash'
27
+ },
20
28
  'openrouter' => { 'suggested' => { 'gpt-4o' => 'openai/gpt-4o' }, 'default' => 'gpt-4o' },
21
29
  'ollama' => { 'suggested' => { 'llama3' => 'llama3' }, 'default' => 'llama3' }
22
30
  }
data/lib/n2b/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # lib/n2b/version.rb
2
2
  module N2B
3
- VERSION = "2.0.0"
3
+ VERSION = "2.1.0"
4
4
  end
data/lib/n2b.rb CHANGED
@@ -9,6 +9,7 @@ require 'n2b/version'
9
9
  require 'n2b/llm/claude'
10
10
  require 'n2b/llm/open_ai'
11
11
  require 'n2b/llm/gemini'
12
+ require 'n2b/llm/vertex_ai'
12
13
  require 'n2b/errors' # Load custom errors
13
14
  require 'n2b/base'
14
15
  require 'n2b/cli'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n2b
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Stefan Nothegger
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-06-09 00:00:00.000000000 Z
11
+ date: 2025-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: googleauth
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.2'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: bundler
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -81,7 +95,6 @@ extensions: []
81
95
  extra_rdoc_files: []
82
96
  files:
83
97
  - README.md
84
- - bin/branch-audit.sh
85
98
  - bin/n2b
86
99
  - bin/n2b-diff
87
100
  - bin/n2b-test-github
@@ -99,6 +112,7 @@ files:
99
112
  - lib/n2b/llm/ollama.rb
100
113
  - lib/n2b/llm/open_ai.rb
101
114
  - lib/n2b/llm/open_router.rb
115
+ - lib/n2b/llm/vertex_ai.rb
102
116
  - lib/n2b/merge_cli.rb
103
117
  - lib/n2b/merge_conflict_parser.rb
104
118
  - lib/n2b/message_utils.rb
@@ -133,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
133
147
  - !ruby/object:Gem::Version
134
148
  version: '0'
135
149
  requirements: []
136
- rubygems_version: 3.5.3
150
+ rubygems_version: 3.5.22
137
151
  signing_key:
138
152
  specification_version: 4
139
153
  summary: AI-powered development toolkit with merge conflict resolution and Jira integration
data/bin/branch-audit.sh DELETED
@@ -1,397 +0,0 @@
1
- #!/bin/bash
2
-
3
- # Branch Audit Script - Safely analyze all branches for unmerged code
4
- # Author: Augment Agent
5
- # Purpose: Find valuable code in branches without touching main
6
-
7
- set -e # Exit on any error
8
-
9
- # Colors for output
10
- RED='\033[0;31m'
11
- GREEN='\033[0;32m'
12
- YELLOW='\033[1;33m'
13
- BLUE='\033[0;34m'
14
- NC='\033[0m' # No Color
15
-
16
- # Configuration
17
- MAIN_BRANCH="main"
18
- REPORT_FILE="branch_audit_report_$(date +%Y-%m-%d_%H-%M-%S).md"
19
- TEMP_DIR="temp_branch_audit_$$"
20
- WORKTREE_DIR="worktree_audit_$$"
21
-
22
- # Arrays to store results
23
- declare -a MERGEABLE_BRANCHES=()
24
- declare -a CONFLICTED_BRANCHES=()
25
- declare -a EMPTY_BRANCHES=()
26
- declare -a ERROR_BRANCHES=()
27
-
28
- # Cleanup function
29
- cleanup() {
30
- echo -e "${YELLOW}🧹 Cleaning up temporary files...${NC}"
31
- if [ -d "$TEMP_DIR" ]; then
32
- rm -rf "$TEMP_DIR"
33
- fi
34
- if [ -d "$WORKTREE_DIR" ]; then
35
- git worktree remove "$WORKTREE_DIR" 2>/dev/null || true
36
- rm -rf "$WORKTREE_DIR" 2>/dev/null || true
37
- fi
38
- }
39
-
40
- # Set up cleanup trap
41
- trap cleanup EXIT
42
-
43
- # Function to log with timestamp
44
- log() {
45
- echo -e "${BLUE}[$(date '+%H:%M:%S')]${NC} $1"
46
- }
47
-
48
- # Function to check if branch exists
49
- branch_exists() {
50
- git show-ref --verify --quiet "refs/heads/$1" 2>/dev/null
51
- }
52
-
53
- # Function to check if remote branch exists
54
- remote_branch_exists() {
55
- git show-ref --verify --quiet "refs/remotes/$1" 2>/dev/null
56
- }
57
-
58
- # Function to get unique commits in branch vs main
59
- get_unique_commits() {
60
- local branch="$1"
61
- git log --oneline "${MAIN_BRANCH}..${branch}" 2>/dev/null || echo ""
62
- }
63
-
64
- # Function to get file changes summary
65
- get_file_changes() {
66
- local branch="$1"
67
- git diff --name-status "${MAIN_BRANCH}...${branch}" 2>/dev/null || echo ""
68
- }
69
-
70
- # Function to check if fast-forward merge is possible
71
- can_fast_forward() {
72
- local branch="$1"
73
- local merge_base=$(git merge-base "$MAIN_BRANCH" "$branch" 2>/dev/null || echo "")
74
- local main_commit=$(git rev-parse "$MAIN_BRANCH" 2>/dev/null || echo "")
75
-
76
- if [ -z "$merge_base" ] || [ -z "$main_commit" ]; then
77
- return 1
78
- fi
79
-
80
- # If merge base equals main commit, then it's a fast-forward
81
- [ "$merge_base" = "$main_commit" ]
82
- }
83
-
84
- # Function to analyze a single branch
85
- analyze_branch() {
86
- local branch="$1"
87
- local branch_type="$2" # "local" or "remote"
88
-
89
- log "Analyzing ${branch_type} branch: $branch"
90
-
91
- # Skip main branch and its variants
92
- if [[ "$branch" =~ ^(main|master|origin/main|origin/master)$ ]]; then
93
- log "Skipping main branch: $branch"
94
- return
95
- fi
96
-
97
- # For remote branches, create local tracking branch if needed
98
- local local_branch="$branch"
99
- if [ "$branch_type" = "remote" ]; then
100
- local_branch="${branch#origin/}"
101
- if ! branch_exists "$local_branch"; then
102
- log "Creating local tracking branch for $branch"
103
- git branch "$local_branch" "$branch" 2>/dev/null || {
104
- ERROR_BRANCHES+=("$branch (failed to create local branch)")
105
- return
106
- }
107
- fi
108
- fi
109
-
110
- # Get unique commits
111
- local unique_commits=$(get_unique_commits "$local_branch")
112
- local commit_count=0
113
- if [ -n "$unique_commits" ] && [ "$unique_commits" != "" ]; then
114
- commit_count=$(echo "$unique_commits" | wc -l | tr -d ' ')
115
- fi
116
-
117
- if [ "$commit_count" -eq 0 ]; then
118
- log "Branch $branch has no unique commits"
119
- EMPTY_BRANCHES+=("$branch")
120
- return
121
- fi
122
-
123
- # Check if fast-forward is possible
124
- if can_fast_forward "$local_branch"; then
125
- log "✅ Branch $branch can be fast-forward merged ($commit_count commits)"
126
- MERGEABLE_BRANCHES+=("$branch|$commit_count|$unique_commits")
127
- else
128
- log "⚠️ Branch $branch has conflicts or diverged ($commit_count commits)"
129
- CONFLICTED_BRANCHES+=("$branch|$commit_count|$unique_commits")
130
- fi
131
- }
132
-
133
- # Main execution starts here
134
- main() {
135
- echo -e "${GREEN}🔍 Starting Branch Audit Analysis${NC}"
136
- echo -e "${BLUE}Repository: $(pwd)${NC}"
137
- echo -e "${BLUE}Main branch: $MAIN_BRANCH${NC}"
138
- echo -e "${BLUE}Report file: $REPORT_FILE${NC}"
139
- echo ""
140
-
141
- # Verify we're in a git repository
142
- if ! git rev-parse --git-dir > /dev/null 2>&1; then
143
- echo -e "${RED}❌ Error: Not in a git repository${NC}"
144
- exit 1
145
- fi
146
-
147
- # Verify main branch exists
148
- if ! git show-ref --verify --quiet "refs/heads/$MAIN_BRANCH"; then
149
- echo -e "${RED}❌ Error: Main branch '$MAIN_BRANCH' not found${NC}"
150
- exit 1
151
- fi
152
-
153
- # Fetch latest from remote
154
- log "Fetching latest changes from remote..."
155
- git fetch --all --prune
156
-
157
- # Create temporary directory
158
- mkdir -p "$TEMP_DIR"
159
-
160
- # Get current branch to restore later
161
- CURRENT_BRANCH=$(git branch --show-current)
162
-
163
- # Ensure we're on main branch for analysis
164
- log "Switching to main branch for analysis..."
165
- git checkout "$MAIN_BRANCH" >/dev/null 2>&1
166
-
167
- # Get all local branches
168
- log "Analyzing local branches..."
169
- while IFS= read -r branch; do
170
- branch=$(echo "$branch" | sed 's/^[* ] //' | xargs)
171
- analyze_branch "$branch" "local"
172
- done < <(git branch | grep -v "^\*.*$MAIN_BRANCH$")
173
-
174
- # Get all remote branches
175
- log "Analyzing remote branches..."
176
- while IFS= read -r branch; do
177
- branch=$(echo "$branch" | sed 's|^ remotes/||' | xargs)
178
- # Skip if we already have this as a local branch
179
- local_name="${branch#origin/}"
180
- if ! branch_exists "$local_name"; then
181
- analyze_branch "$branch" "remote"
182
- fi
183
- done < <(git branch -r | grep -v "HEAD\|$MAIN_BRANCH$")
184
-
185
- # Restore original branch
186
- if [ -n "$CURRENT_BRANCH" ] && [ "$CURRENT_BRANCH" != "$MAIN_BRANCH" ]; then
187
- log "Restoring original branch: $CURRENT_BRANCH"
188
- git checkout "$CURRENT_BRANCH" >/dev/null 2>&1
189
- fi
190
-
191
- # Generate report
192
- generate_report
193
-
194
- echo ""
195
- echo -e "${GREEN}✅ Branch audit completed!${NC}"
196
- echo -e "${BLUE}📄 Report saved to: $REPORT_FILE${NC}"
197
- }
198
-
199
- # Function to generate detailed report
200
- generate_report() {
201
- log "Generating detailed report..."
202
-
203
- cat > "$REPORT_FILE" << EOF
204
- # Branch Audit Report
205
- **Generated:** $(date)
206
- **Repository:** $(pwd)
207
- **Main Branch:** $MAIN_BRANCH
208
-
209
- ## 📊 Executive Summary
210
-
211
- - **Mergeable Branches (Fast-Forward):** ${#MERGEABLE_BRANCHES[@]}
212
- - **Conflicted Branches (Manual Review):** ${#CONFLICTED_BRANCHES[@]}
213
- - **Empty Branches (No Changes):** ${#EMPTY_BRANCHES[@]}
214
- - **Error Branches:** ${#ERROR_BRANCHES[@]}
215
- - **Total Analyzed:** $((${#MERGEABLE_BRANCHES[@]} + ${#CONFLICTED_BRANCHES[@]} + ${#EMPTY_BRANCHES[@]} + ${#ERROR_BRANCHES[@]}))
216
-
217
- ---
218
-
219
- EOF
220
-
221
- # Mergeable branches section
222
- if [ ${#MERGEABLE_BRANCHES[@]} -gt 0 ]; then
223
- cat >> "$REPORT_FILE" << EOF
224
- ## ✅ Mergeable Branches (Fast-Forward Possible)
225
-
226
- These branches can be safely merged into main without conflicts:
227
-
228
- EOF
229
- for branch_info in "${MERGEABLE_BRANCHES[@]}"; do
230
- IFS='|' read -r branch commit_count commits <<< "$branch_info"
231
- cat >> "$REPORT_FILE" << EOF
232
- ### 🌿 \`$branch\`
233
- - **Unique Commits:** $commit_count
234
- - **File Changes:** $(get_file_changes "$branch" | wc -l) files
235
- - **Recent Commits:**
236
- \`\`\`
237
- $commits
238
- \`\`\`
239
- - **Files Modified:**
240
- \`\`\`
241
- $(get_file_changes "$branch" | head -10)
242
- $([ $(get_file_changes "$branch" | wc -l) -gt 10 ] && echo "... and $(($(get_file_changes "$branch" | wc -l) - 10)) more files")
243
- \`\`\`
244
-
245
- **Recommendation:** ✅ Safe to merge with \`git merge $branch\`
246
-
247
- ---
248
-
249
- EOF
250
- done
251
- fi
252
-
253
- # Conflicted branches section
254
- if [ ${#CONFLICTED_BRANCHES[@]} -gt 0 ]; then
255
- cat >> "$REPORT_FILE" << EOF
256
- ## ⚠️ Conflicted Branches (Manual Review Required)
257
-
258
- These branches have diverged from main and may have conflicts:
259
-
260
- EOF
261
- for branch_info in "${CONFLICTED_BRANCHES[@]}"; do
262
- IFS='|' read -r branch commit_count commits <<< "$branch_info"
263
- cat >> "$REPORT_FILE" << EOF
264
- ### 🔀 \`$branch\`
265
- - **Unique Commits:** $commit_count
266
- - **File Changes:** $(get_file_changes "$branch" | wc -l) files
267
- - **Recent Commits:**
268
- \`\`\`
269
- $commits
270
- \`\`\`
271
- - **Files Modified:**
272
- \`\`\`
273
- $(get_file_changes "$branch" | head -10)
274
- $([ $(get_file_changes "$branch" | wc -l) -gt 10 ] && echo "... and $(($(get_file_changes "$branch" | wc -l) - 10)) more files")
275
- \`\`\`
276
-
277
- **Recommendation:** ⚠️ Manual review required - check for conflicts before merging
278
-
279
- ---
280
-
281
- EOF
282
- done
283
- fi
284
-
285
- # Empty branches section
286
- if [ ${#EMPTY_BRANCHES[@]} -gt 0 ]; then
287
- cat >> "$REPORT_FILE" << EOF
288
- ## 🗑️ Empty Branches (Safe to Delete)
289
-
290
- These branches have no unique commits compared to main:
291
-
292
- EOF
293
- for branch in "${EMPTY_BRANCHES[@]}"; do
294
- cat >> "$REPORT_FILE" << EOF
295
- - \`$branch\` - No unique changes
296
- EOF
297
- done
298
- cat >> "$REPORT_FILE" << EOF
299
-
300
- **Cleanup Commands:**
301
- \`\`\`bash
302
- # Delete local empty branches
303
- EOF
304
- for branch in "${EMPTY_BRANCHES[@]}"; do
305
- if [[ ! "$branch" =~ ^origin/ ]]; then
306
- echo "git branch -d $branch" >> "$REPORT_FILE"
307
- fi
308
- done
309
- cat >> "$REPORT_FILE" << EOF
310
-
311
- # Delete remote empty branches (be careful!)
312
- EOF
313
- for branch in "${EMPTY_BRANCHES[@]}"; do
314
- if [[ "$branch" =~ ^origin/ ]]; then
315
- remote_branch="${branch#origin/}"
316
- echo "git push origin --delete $remote_branch" >> "$REPORT_FILE"
317
- fi
318
- done
319
- cat >> "$REPORT_FILE" << EOF
320
- \`\`\`
321
-
322
- ---
323
-
324
- EOF
325
- fi
326
-
327
- # Error branches section
328
- if [ ${#ERROR_BRANCHES[@]} -gt 0 ]; then
329
- cat >> "$REPORT_FILE" << EOF
330
- ## ❌ Error Branches
331
-
332
- These branches encountered errors during analysis:
333
-
334
- EOF
335
- for branch in "${ERROR_BRANCHES[@]}"; do
336
- cat >> "$REPORT_FILE" << EOF
337
- - \`$branch\`
338
- EOF
339
- done
340
- cat >> "$REPORT_FILE" << EOF
341
-
342
- **Recommendation:** Manual investigation required
343
-
344
- ---
345
-
346
- EOF
347
- fi
348
-
349
- # Summary and recommendations
350
- cat >> "$REPORT_FILE" << EOF
351
- ## 🎯 Recommended Actions
352
-
353
- ### Immediate Actions (Safe)
354
- 1. **Merge fast-forward branches:** ${#MERGEABLE_BRANCHES[@]} branches ready
355
- 2. **Delete empty branches:** ${#EMPTY_BRANCHES[@]} branches can be removed
356
- 3. **Review conflicted branches:** ${#CONFLICTED_BRANCHES[@]} branches need manual review
357
-
358
- ### Cleanup Script
359
- \`\`\`bash
360
- # Run this script to clean up empty branches
361
- EOF
362
-
363
- # Generate cleanup commands for empty branches
364
- for branch in "${EMPTY_BRANCHES[@]}"; do
365
- if [[ ! "$branch" =~ ^origin/ ]]; then
366
- echo "git branch -d $branch" >> "$REPORT_FILE"
367
- fi
368
- done
369
-
370
- cat >> "$REPORT_FILE" << EOF
371
- \`\`\`
372
-
373
- ### Merge Script for Fast-Forward Branches
374
- \`\`\`bash
375
- # Run these commands to merge ready branches
376
- EOF
377
-
378
- # Generate merge commands for mergeable branches
379
- for branch_info in "${MERGEABLE_BRANCHES[@]}"; do
380
- IFS='|' read -r branch commit_count commits <<< "$branch_info"
381
- if [[ ! "$branch" =~ ^origin/ ]]; then
382
- echo "git merge $branch # $commit_count commits" >> "$REPORT_FILE"
383
- fi
384
- done
385
-
386
- cat >> "$REPORT_FILE" << EOF
387
- \`\`\`
388
-
389
- ---
390
- *Report generated by branch-audit.sh on $(date)*
391
- EOF
392
-
393
- log "Report generated: $REPORT_FILE"
394
- }
395
-
396
- # Run main function
397
- main "$@"