stackharbinger 0.2.0 → 0.4.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.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "docker_compose_detector"
4
+
5
+ module Harbinger
6
+ module Analyzers
7
+ # Detects MongoDB version from projects
8
+ class MongoDetector
9
+ attr_reader :project_path
10
+
11
+ def initialize(project_path)
12
+ @project_path = project_path
13
+ end
14
+
15
+ # Main detection method - returns version string or nil
16
+ def detect
17
+ return nil unless mongo_detected?
18
+
19
+ # Try docker-compose.yml first
20
+ version = detect_from_docker_compose
21
+ return version if version
22
+
23
+ # Try shell command
24
+ version = detect_from_shell
25
+ return version if version
26
+
27
+ # Fallback to gem version
28
+ detect_from_gemfile_lock
29
+ end
30
+
31
+ # Check if MongoDB is used in this project
32
+ def mongo_detected?
33
+ gemfile_has_mongo? || docker_compose_has_mongo?
34
+ end
35
+
36
+ private
37
+
38
+ def detect_from_docker_compose
39
+ docker = DockerComposeDetector.new(project_path)
40
+ docker.image_version("mongo")
41
+ end
42
+
43
+ def detect_from_shell
44
+ # Try mongosh first (modern shell, MongoDB 5+)
45
+ output = `mongosh --version 2>&1`.strip
46
+ return output if $CHILD_STATUS.success? && output.match?(/^\d+\.\d+/)
47
+
48
+ # Try legacy mongo shell
49
+ output = `mongo --version 2>&1`.strip
50
+ if $CHILD_STATUS.success?
51
+ match = output.match(/MongoDB shell version v?(\d+\.\d+(?:\.\d+)?)/)
52
+ return match[1] if match
53
+ end
54
+
55
+ # Fall back to mongod (server)
56
+ output = `mongod --version 2>&1`.strip
57
+ return nil unless $CHILD_STATUS.success?
58
+
59
+ match = output.match(/db version v(\d+\.\d+(?:\.\d+)?)/)
60
+ match[1] if match
61
+ rescue StandardError
62
+ nil
63
+ end
64
+
65
+ def detect_from_gemfile_lock
66
+ content = read_gemfile_lock
67
+ return nil unless content
68
+
69
+ # Look for mongoid gem version first (most common for Rails)
70
+ match = content.match(/^\s{4}mongoid\s+\(([^)]+)\)/)
71
+ return "#{match[1]} (mongoid gem)" if match
72
+
73
+ # Fall back to mongo gem
74
+ match = content.match(/^\s{4}mongo\s+\(([^)]+)\)/)
75
+ return "#{match[1]} (mongo gem)" if match
76
+
77
+ nil
78
+ end
79
+
80
+ def gemfile_has_mongo?
81
+ content = read_gemfile_lock
82
+ return false unless content
83
+
84
+ content.include?("mongoid (") || content.include?("mongo (")
85
+ end
86
+
87
+ def docker_compose_has_mongo?
88
+ docker = DockerComposeDetector.new(project_path)
89
+ return false unless docker.docker_compose_exists?
90
+
91
+ docker.image_version("mongo") != nil
92
+ end
93
+
94
+ def read_gemfile_lock
95
+ path = File.join(project_path, "Gemfile.lock")
96
+ return nil unless File.exist?(path)
97
+
98
+ File.read(path)
99
+ rescue StandardError
100
+ nil
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database_detector"
4
+ require_relative "docker_compose_detector"
5
+
6
+ module Harbinger
7
+ module Analyzers
8
+ # Detects MySQL version from Rails projects
9
+ # Supports both mysql2 and trilogy adapters
10
+ class MysqlDetector < DatabaseDetector
11
+ protected
12
+
13
+ def adapter_name
14
+ %w[mysql2 trilogy]
15
+ end
16
+
17
+ def detect_from_docker_compose
18
+ docker = DockerComposeDetector.new(project_path)
19
+ # Try mysql first, then mariadb
20
+ docker.image_version("mysql") || docker.image_version("mariadb")
21
+ end
22
+
23
+ def detect_from_shell
24
+ # Skip shell command if database is remote
25
+ return nil if remote_database?
26
+
27
+ # Try mysql command first, then mysqld
28
+ output = execute_command("mysql --version") || execute_command("mysqld --version")
29
+ return nil unless output
30
+
31
+ # Parse: "mysql Ver 8.0.33" or "mysqld Ver 8.0.33"
32
+ # Also handles MariaDB: "mysql Ver 15.1 Distrib 10.11.2-MariaDB"
33
+ match = output.match(/Ver\s+(?:\d+\.\d+\s+Distrib\s+)?(\d+\.\d+\.\d+)/)
34
+ match[1] if match
35
+ end
36
+
37
+ def detect_from_gemfile_lock
38
+ content = parse_gemfile_lock
39
+ return nil unless content
40
+
41
+ # Check which adapter is being used
42
+ config = parse_database_yml
43
+ return nil unless config
44
+
45
+ section = config["production"] || config["default"] || config[config.keys.first]
46
+ return nil unless section
47
+
48
+ adapter = extract_adapter_from_section(section)
49
+
50
+ # Return appropriate gem version based on adapter
51
+ if adapter == "trilogy"
52
+ version = extract_gem_version(content, "trilogy")
53
+ version ? "#{version} (trilogy gem)" : nil
54
+ else
55
+ version = extract_gem_version(content, "mysql2")
56
+ version ? "#{version} (mysql2 gem)" : nil
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # Check if database configuration indicates a remote database
63
+ # (same logic as PostgresDetector)
64
+ def remote_database?
65
+ config = parse_database_yml
66
+ return false unless config
67
+
68
+ section = config["production"] || config["default"] || config[config.keys.first]
69
+ return false unless section
70
+
71
+ db_config = if section["adapter"]
72
+ section
73
+ else
74
+ section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
75
+ end
76
+
77
+ return false unless db_config
78
+
79
+ host = db_config["host"]
80
+
81
+ # No host specified = localhost (Unix socket)
82
+ return false if host.nil? || host.empty?
83
+
84
+ # Explicit localhost indicators
85
+ local_hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
86
+ !local_hosts.include?(host.to_s.downcase)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "database_detector"
4
+ require_relative "docker_compose_detector"
5
+
6
+ module Harbinger
7
+ module Analyzers
8
+ # Detects PostgreSQL version from Rails projects
9
+ class PostgresDetector < DatabaseDetector
10
+ protected
11
+
12
+ def adapter_name
13
+ "postgresql"
14
+ end
15
+
16
+ def detect_from_docker_compose
17
+ docker = DockerComposeDetector.new(project_path)
18
+ docker.image_version("postgres")
19
+ end
20
+
21
+ def detect_from_shell
22
+ # Skip shell command if database is remote
23
+ # (shell gives client version, not server version)
24
+ return nil if remote_database?
25
+
26
+ output = execute_command("psql --version")
27
+ return nil unless output
28
+
29
+ # Parse: "psql (PostgreSQL) 15.3" or "psql (PostgreSQL) 15.3 (Ubuntu 15.3-1)"
30
+ match = output.match(/PostgreSQL\)\s+(\d+\.\d+)/)
31
+ match[1] if match
32
+ end
33
+
34
+ def detect_from_gemfile_lock
35
+ content = parse_gemfile_lock
36
+ version = extract_gem_version(content, "pg")
37
+ version ? "#{version} (pg gem)" : nil
38
+ end
39
+
40
+ private
41
+
42
+ # Check if database configuration indicates a remote database
43
+ def remote_database?
44
+ config = parse_database_yml
45
+ return false unless config
46
+
47
+ # Get the section with database config
48
+ section = config["production"] || config["default"] || config[config.keys.first]
49
+ return false unless section
50
+
51
+ # Handle multi-database config
52
+ db_config = if section["adapter"]
53
+ section
54
+ else
55
+ section["primary"] || section.values.find { |v| v.is_a?(Hash) && v["adapter"] }
56
+ end
57
+
58
+ return false unless db_config
59
+
60
+ host = db_config["host"]
61
+
62
+ # No host specified = localhost (Unix socket)
63
+ return false if host.nil? || host.empty?
64
+
65
+ # Explicit localhost indicators
66
+ local_hosts = ["localhost", "127.0.0.1", "::1", "0.0.0.0"]
67
+ !local_hosts.include?(host.to_s.downcase)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "docker_compose_detector"
4
+
5
+ module Harbinger
6
+ module Analyzers
7
+ # Detects Redis version from projects
8
+ class RedisDetector
9
+ attr_reader :project_path
10
+
11
+ def initialize(project_path)
12
+ @project_path = project_path
13
+ end
14
+
15
+ # Main detection method - returns version string or nil
16
+ def detect
17
+ return nil unless redis_detected?
18
+
19
+ # Try docker-compose.yml first
20
+ version = detect_from_docker_compose
21
+ return version if version
22
+
23
+ # Try shell command
24
+ version = detect_from_shell
25
+ return version if version
26
+
27
+ # Fallback to gem version
28
+ detect_from_gemfile_lock
29
+ end
30
+
31
+ # Check if Redis is used in this project
32
+ def redis_detected?
33
+ gemfile_has_redis? || docker_compose_has_redis?
34
+ end
35
+
36
+ private
37
+
38
+ def detect_from_docker_compose
39
+ docker = DockerComposeDetector.new(project_path)
40
+ docker.image_version("redis")
41
+ end
42
+
43
+ def detect_from_shell
44
+ # Try redis-cli first (more commonly available)
45
+ output = `redis-cli -v 2>&1`.strip
46
+ if $CHILD_STATUS.success?
47
+ # Parse: "redis-cli 7.0.5"
48
+ match = output.match(/redis-cli\s+(\d+\.\d+(?:\.\d+)?)/)
49
+ return match[1] if match
50
+ end
51
+
52
+ # Fall back to redis-server
53
+ output = `redis-server --version 2>&1`.strip
54
+ return nil unless $CHILD_STATUS.success?
55
+
56
+ # Parse: "Redis server v=7.2.4 sha=..."
57
+ match = output.match(/v=(\d+\.\d+(?:\.\d+)?)/)
58
+ match[1] if match
59
+ rescue StandardError
60
+ nil
61
+ end
62
+
63
+ def detect_from_gemfile_lock
64
+ content = read_gemfile_lock
65
+ return nil unless content
66
+
67
+ # Look for redis gem version
68
+ match = content.match(/^\s{4}redis\s+\(([^)]+)\)/)
69
+ return "#{match[1]} (gem)" if match
70
+
71
+ nil
72
+ end
73
+
74
+ def gemfile_has_redis?
75
+ content = read_gemfile_lock
76
+ return false unless content
77
+
78
+ content.include?("redis (")
79
+ end
80
+
81
+ def docker_compose_has_redis?
82
+ docker = DockerComposeDetector.new(project_path)
83
+ return false unless docker.docker_compose_exists?
84
+
85
+ docker.image_version("redis") != nil
86
+ end
87
+
88
+ def read_gemfile_lock
89
+ path = File.join(project_path, "Gemfile.lock")
90
+ return nil unless File.exist?(path)
91
+
92
+ File.read(path)
93
+ rescue StandardError
94
+ nil
95
+ end
96
+ end
97
+ end
98
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "docker_compose_detector"
4
+
3
5
  module Harbinger
4
6
  module Analyzers
5
7
  class RubyDetector
@@ -10,7 +12,8 @@ module Harbinger
10
12
  def detect
11
13
  detect_from_ruby_version ||
12
14
  detect_from_gemfile ||
13
- detect_from_gemfile_lock
15
+ detect_from_gemfile_lock ||
16
+ detect_from_dockerfile
14
17
  end
15
18
 
16
19
  def ruby_detected?
@@ -61,6 +64,11 @@ module Harbinger
61
64
  # Remove patch level suffix (e.g., "p223")
62
65
  version.sub(/p\d+$/, "")
63
66
  end
67
+
68
+ def detect_from_dockerfile
69
+ docker = DockerComposeDetector.new(project_path)
70
+ docker.ruby_version_from_dockerfile
71
+ end
64
72
  end
65
73
  end
66
74
  end