stackharbinger 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0625ffb650537306abc4297407d0373c204c69412c7610254488c1006461bbcc
4
- data.tar.gz: 65106dadaa487141d44e7772c57d9b0c59e18ab478517f0e94cad1440b8ab8b4
3
+ metadata.gz: 663f135c9d728ba59e2f33d33ddd73e3be1b37143c7533db0534db995301aa8e
4
+ data.tar.gz: de00d8ba69996d960b57bc96d49c79da0f9221a90ac870805fd9f5aad16b506f
5
5
  SHA512:
6
- metadata.gz: 3922e4f871e0782ebf821ef3e01f91aebba249c908f81a3b8f94e550d0d7d68a8525118d61f78b326b801aeecb801fbd89f975b48c2c95bbafd12c7703ebcf7a
7
- data.tar.gz: c50866bca2f2b69d995d01735d62da1cb44f436aa546e5a069d7f8b50b8d25e63d95969f4df85ed008cd7d893a7e8d92959efe12e29e7834199f7322455a9cb9
6
+ metadata.gz: 2505777f55661fe67224a94351e8eb00d56cb2ef834c19a86f00e9be6fd45cf4aea5abb872242750c5f4dc3608e4707e0ecb8899538407d348ff7fdec7254a3c
7
+ data.tar.gz: 1ee3dcbca6a3cd75ede6a720fdb50bc8969a070c4bbb0c8a17cb4abacc0b9cb4536dd78fe0b78cb7609d56e15fc5affdcebac1ec5a906911cd3e323341b882bf
data/README.md CHANGED
@@ -86,6 +86,31 @@ PostgreSQL 16.11:
86
86
  ```bash
87
87
  # Show dashboard of all tracked projects
88
88
  harbinger show
89
+
90
+ # Filter to specific project(s) by name or path
91
+ harbinger show budget
92
+ harbinger show job
93
+
94
+ # Show project paths with verbose mode
95
+ harbinger show -v
96
+ harbinger show job --verbose
97
+ ```
98
+
99
+ ### Export data
100
+
101
+ ```bash
102
+ # Export to JSON (stdout)
103
+ harbinger show --format json
104
+
105
+ # Export to CSV (stdout)
106
+ harbinger show --format csv
107
+
108
+ # Save to file
109
+ harbinger show --format json -o report.json
110
+ harbinger show --format csv --output eol-report.csv
111
+
112
+ # Export filtered projects
113
+ harbinger show myproject --format json
89
114
  ```
90
115
 
91
116
  **Example output:**
@@ -113,6 +138,13 @@ harbinger rescan
113
138
  harbinger rescan --verbose
114
139
  ```
115
140
 
141
+ ### Remove a project
142
+
143
+ ```bash
144
+ # Remove a project from tracking
145
+ harbinger remove my-project
146
+ ```
147
+
116
148
  ### Update EOL data
117
149
 
118
150
  ```bash
@@ -197,19 +229,19 @@ bundle exec exe/harbinger scan .
197
229
 
198
230
  ## Roadmap
199
231
 
200
- ### V0.3.0 - Current
232
+ ### V0.4.0 - Current
233
+ - ✅ Export reports to JSON/CSV
234
+ - ✅ Docker Compose database version detection
235
+ - ✅ Redis version detection
236
+ - ✅ MongoDB version detection
237
+
238
+ ### V0.3.0
201
239
  - ✅ PostgreSQL version detection with local/remote database handling
202
240
  - ✅ MySQL version detection (mysql2 and trilogy adapters)
203
241
  - ✅ Rescan command to update all tracked projects
204
242
  - ✅ Enhanced dashboard with database columns
205
243
  - ✅ EOL tracking for PostgreSQL and MySQL
206
244
 
207
- ### V0.4.0 - Planned
208
- - 📋 Export reports to JSON/CSV
209
- - 🐳 Docker Compose database version detection
210
- - 🔴 Redis version detection
211
- - 🍃 MongoDB version detection
212
-
213
245
  ### V1.0 - Future
214
246
  - 🐍 Python support (pyproject.toml, requirements.txt)
215
247
  - 📦 Node.js support (package.json, .nvmrc)
data/docs/index.html CHANGED
@@ -17,7 +17,7 @@
17
17
  <div class="max-w-6xl mx-auto px-4 py-20">
18
18
  <h1 class="text-5xl md:text-7xl font-black mb-6">Harbinger</h1>
19
19
  <p class="text-2xl md:text-3xl mb-8 text-blue-100">Track End-of-Life dates for your tech stack</p>
20
- <p class="text-xl mb-12 text-blue-100 max-w-2xl">Never get caught off-guard by unsupported dependencies. Harbinger scans your Ruby and Rails projects and warns you before support ends.</p>
20
+ <p class="text-xl mb-12 text-blue-100 max-w-2xl">Never get caught off-guard by unsupported dependencies. Harbinger scans your Ruby, Rails, PostgreSQL, and MySQL versions and warns you before support ends.</p>
21
21
 
22
22
  <div class="flex flex-col sm:flex-row gap-4">
23
23
  <div class="bg-gray-900 rounded-lg p-4 font-mono text-sm">
@@ -38,7 +38,7 @@
38
38
  <div class="bg-white p-6 rounded-lg shadow-sm">
39
39
  <div class="text-4xl mb-4">🔍</div>
40
40
  <h3 class="text-xl font-semibold mb-2">Auto-Detection</h3>
41
- <p class="text-gray-600">Automatically detects Ruby and Rails versions from .ruby-version, Gemfile, and Gemfile.lock</p>
41
+ <p class="text-gray-600">Detects Ruby, Rails, PostgreSQL, and MySQL versions from project files and database configs</p>
42
42
  </div>
43
43
  <div class="bg-white p-6 rounded-lg shadow-sm">
44
44
  <div class="text-4xl mb-4">📅</div>
@@ -62,8 +62,8 @@
62
62
  </div>
63
63
  <div class="bg-white p-6 rounded-lg shadow-sm">
64
64
  <div class="text-4xl mb-4">📊</div>
65
- <h3 class="text-xl font-semibold mb-2">Dashboard View</h3>
66
- <p class="text-gray-600">Track multiple projects and see EOL status at a glance with harbinger show</p>
65
+ <h3 class="text-xl font-semibold mb-2">Smart Dashboard</h3>
66
+ <p class="text-gray-600">Dynamic columns show only relevant data - columns and projects without matches are hidden automatically</p>
67
67
  </div>
68
68
  <div class="bg-white p-6 rounded-lg shadow-sm">
69
69
  <div class="text-4xl mb-4">🔄</div>
@@ -73,7 +73,7 @@
73
73
  <div class="bg-white p-6 rounded-lg shadow-sm">
74
74
  <div class="text-4xl mb-4">🧪</div>
75
75
  <h3 class="text-xl font-semibold mb-2">Well Tested</h3>
76
- <p class="text-gray-600">52 RSpec tests with 100% pass rate, built with TDD</p>
76
+ <p class="text-gray-600">116 RSpec tests with 100% pass rate, built with TDD</p>
77
77
  </div>
78
78
  </div>
79
79
  </div>
@@ -114,7 +114,11 @@
114
114
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --path [PATH]</code> - Scan a project and show EOL status</li>
115
115
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --save</code> - Save project for tracking</li>
116
116
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger scan --recursive</code> - Scan all projects in a directory</li>
117
- <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show</code> - View dashboard of all tracked projects</li>
117
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show [PROJECT] [-v]</code> - View dashboard (filter by name/path, -v shows paths)</li>
118
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show --format json|csv</code> - Export data to JSON or CSV</li>
119
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger show --format json -o report.json</code> - Save export to file</li>
120
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger rescan</code> - Re-scan all tracked projects and update versions</li>
121
+ <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger remove PROJECT</code> - Remove a project from tracking</li>
118
122
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger update</code> - Force refresh EOL data from API</li>
119
123
  <li><code class="bg-gray-100 px-2 py-1 rounded">harbinger version</code> - Show harbinger version</li>
120
124
  </ul>
@@ -127,21 +131,21 @@
127
131
  <h2 class="text-3xl font-bold mb-8 text-center">Roadmap</h2>
128
132
  <div class="grid md:grid-cols-3 gap-8">
129
133
  <div>
130
- <h3 class="text-xl font-semibold mb-4 text-green-600">✅ V0.2.0 (Current)</h3>
134
+ <h3 class="text-xl font-semibold mb-4 text-green-600">✅ V0.4.0 (Current)</h3>
131
135
  <ul class="space-y-2 text-gray-600">
132
- <li>• Dashboard view</li>
133
- <li>• Project tracking</li>
134
- <li>• Recursive scanning</li>
135
- <li>• Homebrew distribution</li>
136
+ <li>• Export to JSON/CSV</li>
137
+ <li>• Docker Compose detection</li>
138
+ <li>• Redis detection</li>
139
+ <li>• MongoDB detection</li>
136
140
  </ul>
137
141
  </div>
138
142
  <div>
139
- <h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V0.3.0 (Planned)</h3>
143
+ <h3 class="text-xl font-semibold mb-4 text-blue-600">📋 V0.3.0</h3>
140
144
  <ul class="space-y-2 text-gray-600">
141
145
  <li>• PostgreSQL detection</li>
142
146
  <li>• MySQL detection</li>
143
147
  <li>• Rescan command</li>
144
- <li>• Export to JSON/CSV</li>
148
+ <li>• Smart dashboard filtering</li>
145
149
  </ul>
146
150
  </div>
147
151
  <div>
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "English"
3
4
  require "yaml"
4
5
 
5
6
  module Harbinger
@@ -17,7 +18,11 @@ module Harbinger
17
18
  def detect
18
19
  return nil unless database_detected?
19
20
 
20
- # Try shell command first (actual database version)
21
+ # Try docker-compose.yml first (most accurate for Docker-based projects)
22
+ version = detect_from_docker_compose
23
+ return version if version
24
+
25
+ # Try shell command (actual database version for non-Docker setups)
21
26
  version = detect_from_shell
22
27
  return version if version
23
28
 
@@ -68,6 +73,11 @@ module Harbinger
68
73
  raise NotImplementedError, "Subclasses must implement detect_from_shell"
69
74
  end
70
75
 
76
+ # Optional method - subclasses can override to detect from docker-compose.yml
77
+ def detect_from_docker_compose
78
+ nil
79
+ end
80
+
71
81
  # Abstract method - must be implemented by subclasses
72
82
  # Detects version from Gemfile.lock gem version
73
83
  def detect_from_gemfile_lock
@@ -81,7 +91,7 @@ module Harbinger
81
91
 
82
92
  content = File.read(database_yml_path)
83
93
  YAML.safe_load(content, aliases: true)
84
- rescue Psych::SyntaxError, StandardError
94
+ rescue StandardError
85
95
  nil
86
96
  end
87
97
 
@@ -112,7 +122,7 @@ module Harbinger
112
122
  # Execute shell command safely
113
123
  def execute_command(command)
114
124
  output = `#{command} 2>&1`.strip
115
- return nil unless $?.success?
125
+ return nil unless $CHILD_STATUS.success?
116
126
 
117
127
  output
118
128
  rescue StandardError
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Harbinger
6
+ module Analyzers
7
+ # Detects versions from docker-compose.yml and Dockerfile
8
+ class DockerComposeDetector
9
+ attr_reader :project_path
10
+
11
+ def initialize(project_path)
12
+ @project_path = project_path
13
+ end
14
+
15
+ # Extract version from a Docker image in docker-compose.yml
16
+ # e.g., "postgres:16-alpine" => "16", "mysql:8.0" => "8.0"
17
+ def image_version(image_pattern)
18
+ compose = parse_docker_compose
19
+ return nil unless compose
20
+
21
+ services = compose["services"]
22
+ return nil unless services
23
+
24
+ services.each_value do |service|
25
+ image = service["image"]
26
+ next unless image
27
+
28
+ if image.match?(/^#{image_pattern}[:\d]/)
29
+ version = extract_version_from_image(image)
30
+ return version if version
31
+ end
32
+ end
33
+
34
+ nil
35
+ end
36
+
37
+ # Extract Ruby version from Dockerfile
38
+ # e.g., "FROM ruby:3.4.7-slim" => "3.4.7"
39
+ def ruby_version_from_dockerfile
40
+ dockerfile = read_dockerfile
41
+ return nil unless dockerfile
42
+
43
+ # Match patterns like:
44
+ # FROM ruby:3.4.7
45
+ # FROM ruby:3.4.7-slim
46
+ # FROM ruby:3.4.7-alpine
47
+ match = dockerfile.match(/^FROM\s+ruby:(\d+\.\d+(?:\.\d+)?)/i)
48
+ match[1] if match
49
+ end
50
+
51
+ # Extract Rails version from Dockerfile (rare, but possible)
52
+ def rails_version_from_dockerfile
53
+ dockerfile = read_dockerfile
54
+ return nil unless dockerfile
55
+
56
+ # Match patterns like:
57
+ # FROM rails:7.0
58
+ # ARG RAILS_VERSION=7.0.8
59
+ match = dockerfile.match(/^FROM\s+rails:(\d+\.\d+(?:\.\d+)?)/i)
60
+ return match[1] if match
61
+
62
+ match = dockerfile.match(/RAILS_VERSION[=:](\d+\.\d+(?:\.\d+)?)/i)
63
+ match[1] if match
64
+ end
65
+
66
+ # Check if docker-compose.yml exists
67
+ def docker_compose_exists?
68
+ docker_compose_path != nil
69
+ end
70
+
71
+ # Check if Dockerfile exists
72
+ def dockerfile_exists?
73
+ File.exist?(File.join(project_path, "Dockerfile"))
74
+ end
75
+
76
+ private
77
+
78
+ def docker_compose_path
79
+ paths = [
80
+ File.join(project_path, "docker-compose.yml"),
81
+ File.join(project_path, "docker-compose.yaml"),
82
+ File.join(project_path, "compose.yml"),
83
+ File.join(project_path, "compose.yaml")
84
+ ]
85
+ paths.find { |p| File.exist?(p) }
86
+ end
87
+
88
+ def parse_docker_compose
89
+ path = docker_compose_path
90
+ return nil unless path
91
+
92
+ YAML.safe_load(File.read(path), aliases: true)
93
+ rescue StandardError
94
+ nil
95
+ end
96
+
97
+ def read_dockerfile
98
+ path = File.join(project_path, "Dockerfile")
99
+ return nil unless File.exist?(path)
100
+
101
+ File.read(path)
102
+ rescue StandardError
103
+ nil
104
+ end
105
+
106
+ # Extract version number from Docker image tag
107
+ # "postgres:16-alpine" => "16"
108
+ # "postgres:16.2" => "16.2"
109
+ # "mysql:8.0.33" => "8.0.33"
110
+ # "redis:7-alpine" => "7"
111
+ def extract_version_from_image(image)
112
+ return nil unless image.include?(":")
113
+
114
+ tag = image.split(":").last
115
+ # Extract leading version number (digits and dots)
116
+ match = tag.match(/^(\d+(?:\.\d+)*)/)
117
+ match[1] if match
118
+ end
119
+ end
120
+ end
121
+ end
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "database_detector"
4
+ require_relative "docker_compose_detector"
4
5
 
5
6
  module Harbinger
6
7
  module Analyzers
@@ -10,7 +11,13 @@ module Harbinger
10
11
  protected
11
12
 
12
13
  def adapter_name
13
- ["mysql2", "trilogy"]
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")
14
21
  end
15
22
 
16
23
  def detect_from_shell
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "database_detector"
4
+ require_relative "docker_compose_detector"
4
5
 
5
6
  module Harbinger
6
7
  module Analyzers
@@ -12,6 +13,11 @@ module Harbinger
12
13
  "postgresql"
13
14
  end
14
15
 
16
+ def detect_from_docker_compose
17
+ docker = DockerComposeDetector.new(project_path)
18
+ docker.image_version("postgres")
19
+ end
20
+
15
21
  def detect_from_shell
16
22
  # Skip shell command if database is remote
17
23
  # (shell gives client version, not server version)
@@ -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