rails_lens 0.0.0 → 0.2.2

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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE.txt +2 -2
  4. data/README.md +463 -9
  5. data/exe/rails_lens +25 -0
  6. data/lib/rails_lens/analyzers/association_analyzer.rb +111 -0
  7. data/lib/rails_lens/analyzers/base.rb +35 -0
  8. data/lib/rails_lens/analyzers/best_practices_analyzer.rb +114 -0
  9. data/lib/rails_lens/analyzers/column_analyzer.rb +97 -0
  10. data/lib/rails_lens/analyzers/composite_keys.rb +62 -0
  11. data/lib/rails_lens/analyzers/database_constraints.rb +35 -0
  12. data/lib/rails_lens/analyzers/delegated_types.rb +129 -0
  13. data/lib/rails_lens/analyzers/enums.rb +34 -0
  14. data/lib/rails_lens/analyzers/error_handling.rb +66 -0
  15. data/lib/rails_lens/analyzers/foreign_key_analyzer.rb +47 -0
  16. data/lib/rails_lens/analyzers/generated_columns.rb +56 -0
  17. data/lib/rails_lens/analyzers/index_analyzer.rb +128 -0
  18. data/lib/rails_lens/analyzers/inheritance.rb +212 -0
  19. data/lib/rails_lens/analyzers/notes.rb +325 -0
  20. data/lib/rails_lens/analyzers/performance_analyzer.rb +110 -0
  21. data/lib/rails_lens/annotation_pipeline.rb +87 -0
  22. data/lib/rails_lens/cli.rb +176 -0
  23. data/lib/rails_lens/cli_error_handler.rb +86 -0
  24. data/lib/rails_lens/commands.rb +164 -0
  25. data/lib/rails_lens/connection.rb +133 -0
  26. data/lib/rails_lens/erd/column_type_formatter.rb +32 -0
  27. data/lib/rails_lens/erd/domain_color_mapper.rb +40 -0
  28. data/lib/rails_lens/erd/mysql_column_type_formatter.rb +19 -0
  29. data/lib/rails_lens/erd/postgresql_column_type_formatter.rb +19 -0
  30. data/lib/rails_lens/erd/visualizer.rb +329 -0
  31. data/lib/rails_lens/errors.rb +78 -0
  32. data/lib/rails_lens/extension_loader.rb +261 -0
  33. data/lib/rails_lens/extensions/base.rb +194 -0
  34. data/lib/rails_lens/extensions/closure_tree_ext.rb +157 -0
  35. data/lib/rails_lens/file_insertion_helper.rb +168 -0
  36. data/lib/rails_lens/mailer/annotator.rb +226 -0
  37. data/lib/rails_lens/mailer/extractor.rb +201 -0
  38. data/lib/rails_lens/model_detector.rb +252 -0
  39. data/lib/rails_lens/parsers/class_info.rb +46 -0
  40. data/lib/rails_lens/parsers/module_info.rb +33 -0
  41. data/lib/rails_lens/parsers/parser_result.rb +55 -0
  42. data/lib/rails_lens/parsers/prism_parser.rb +90 -0
  43. data/lib/rails_lens/parsers.rb +10 -0
  44. data/lib/rails_lens/providers/association_notes_provider.rb +11 -0
  45. data/lib/rails_lens/providers/base.rb +37 -0
  46. data/lib/rails_lens/providers/best_practices_notes_provider.rb +11 -0
  47. data/lib/rails_lens/providers/column_notes_provider.rb +11 -0
  48. data/lib/rails_lens/providers/composite_keys_provider.rb +11 -0
  49. data/lib/rails_lens/providers/database_constraints_provider.rb +11 -0
  50. data/lib/rails_lens/providers/delegated_types_provider.rb +11 -0
  51. data/lib/rails_lens/providers/enums_provider.rb +11 -0
  52. data/lib/rails_lens/providers/extension_notes_provider.rb +20 -0
  53. data/lib/rails_lens/providers/extensions_provider.rb +22 -0
  54. data/lib/rails_lens/providers/foreign_key_notes_provider.rb +11 -0
  55. data/lib/rails_lens/providers/generated_columns_provider.rb +11 -0
  56. data/lib/rails_lens/providers/index_notes_provider.rb +20 -0
  57. data/lib/rails_lens/providers/inheritance_provider.rb +23 -0
  58. data/lib/rails_lens/providers/notes_provider_base.rb +25 -0
  59. data/lib/rails_lens/providers/performance_notes_provider.rb +11 -0
  60. data/lib/rails_lens/providers/schema_provider.rb +61 -0
  61. data/lib/rails_lens/providers/section_provider_base.rb +28 -0
  62. data/lib/rails_lens/railtie.rb +17 -0
  63. data/lib/rails_lens/rake_bootstrapper.rb +18 -0
  64. data/lib/rails_lens/route/annotator.rb +268 -0
  65. data/lib/rails_lens/route/extractor.rb +133 -0
  66. data/lib/rails_lens/route/parser.rb +59 -0
  67. data/lib/rails_lens/schema/adapters/base.rb +345 -0
  68. data/lib/rails_lens/schema/adapters/database_info.rb +118 -0
  69. data/lib/rails_lens/schema/adapters/mysql.rb +279 -0
  70. data/lib/rails_lens/schema/adapters/postgresql.rb +197 -0
  71. data/lib/rails_lens/schema/adapters/sqlite3.rb +96 -0
  72. data/lib/rails_lens/schema/annotation.rb +144 -0
  73. data/lib/rails_lens/schema/annotation_manager.rb +202 -0
  74. data/lib/rails_lens/tasks/annotate.rake +35 -0
  75. data/lib/rails_lens/tasks/erd.rake +24 -0
  76. data/lib/rails_lens/tasks/mailers.rake +27 -0
  77. data/lib/rails_lens/tasks/routes.rake +27 -0
  78. data/lib/rails_lens/tasks/schema.rake +108 -0
  79. data/lib/rails_lens/version.rb +5 -0
  80. data/lib/rails_lens.rb +138 -5
  81. metadata +215 -11
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ module Schema
5
+ class Annotation
6
+ MARKER_FORMAT = 'rails-lens:schema'
7
+
8
+ attr_reader :content
9
+
10
+ def initialize
11
+ @content = []
12
+ end
13
+
14
+ def start_marker
15
+ "# <#{MARKER_FORMAT}:begin>"
16
+ end
17
+
18
+ def end_marker
19
+ "# <#{MARKER_FORMAT}:end>"
20
+ end
21
+
22
+ def add_line(line)
23
+ @content << line
24
+ end
25
+
26
+ def add_lines(lines)
27
+ @content.concat(lines)
28
+ end
29
+
30
+ def to_s
31
+ return '' if @content.empty?
32
+
33
+ lines = []
34
+ lines << start_marker
35
+
36
+ @content.each do |line|
37
+ lines << (line.empty? ? '#' : "# #{line}")
38
+ end
39
+
40
+ lines << end_marker
41
+ lines.join("\n")
42
+ end
43
+
44
+ def self.extract(file_content)
45
+ start_marker = "# <#{MARKER_FORMAT}:begin>"
46
+ end_marker = "# <#{MARKER_FORMAT}:end>"
47
+
48
+ start_index = file_content.index(start_marker)
49
+ return nil unless start_index
50
+
51
+ end_index = file_content.index(end_marker, start_index)
52
+ return nil unless end_index
53
+
54
+ {
55
+ start_index: start_index,
56
+ end_index: end_index + end_marker.length,
57
+ content: file_content[start_index..(end_index + end_marker.length - 1)]
58
+ }
59
+ end
60
+
61
+ def self.remove(file_content)
62
+ # Remove all occurrences of the annotation blocks
63
+ result = file_content.dup
64
+
65
+ while (annotation = extract(result))
66
+ # Preserve one newline if the annotation was followed by a newline
67
+ replacement = ''
68
+ replacement = "\n" if annotation[:end_index] < result.length && result[annotation[:end_index]] == "\n"
69
+
70
+ result[annotation[:start_index]...annotation[:end_index]] = replacement
71
+ end
72
+
73
+ # Clean up multiple consecutive blank lines
74
+ result.gsub(/\n\n\n+/, "\n\n")
75
+ end
76
+
77
+ def self.insert_after_line(file_content, line_pattern, annotation_text)
78
+ lines = file_content.split("\n")
79
+ insert_index = nil
80
+
81
+ lines.each_with_index do |line, index|
82
+ if line.match?(line_pattern)
83
+ insert_index = index + 1
84
+ break
85
+ end
86
+ end
87
+
88
+ return file_content unless insert_index
89
+
90
+ # Insert the annotation
91
+ lines.insert(insert_index, annotation_text)
92
+ lines.join("\n")
93
+ end
94
+
95
+ def self.update_or_insert(file_content, annotation_text, line_pattern)
96
+ # First, try to find and update existing annotation
97
+ if (existing = extract(file_content))
98
+ # Replace existing annotation
99
+ before = file_content[0...existing[:start_index]]
100
+ after = file_content[existing[:end_index]..]
101
+
102
+ # Ensure proper spacing
103
+ before = "#{before.rstrip}\n"
104
+ after = "\n#{after.lstrip}" if after && !after.start_with?("\n")
105
+
106
+ return before + annotation_text + after
107
+ end
108
+
109
+ # No existing annotation, insert new one
110
+ insert_after_line(file_content, line_pattern, annotation_text)
111
+ end
112
+
113
+ def self.parse_content(annotation_block)
114
+ return {} unless annotation_block
115
+
116
+ lines = annotation_block.split("\n").map { |line| line.sub(/^#\s?/, '') }
117
+
118
+ # Remove marker lines
119
+ lines = lines[1..-2] if lines.first&.match?(/<.*:begin>/) && lines.last&.match?(/<.*:end>/)
120
+
121
+ sections = {}
122
+ current_section = nil
123
+ current_content = []
124
+
125
+ lines.each do |line|
126
+ if line.match?(/^==\s+(.+)/)
127
+ # Save previous section if any
128
+ sections[current_section] = current_content.join("\n").strip if current_section
129
+
130
+ current_section = line.strip
131
+ current_content = []
132
+ else
133
+ current_content << line
134
+ end
135
+ end
136
+
137
+ # Save last section
138
+ sections[current_section] = current_content.join("\n").strip if current_section
139
+
140
+ sections
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../file_insertion_helper'
4
+
5
+ module RailsLens
6
+ module Schema
7
+ class AnnotationManager
8
+ attr_reader :model_class
9
+
10
+ def initialize(model_class)
11
+ @model_class = model_class
12
+ end
13
+
14
+ def annotate_file(file_path = nil, allow_external_files: false)
15
+ file_path ||= model_file_path
16
+ return unless file_path && File.exist?(file_path)
17
+
18
+ # Only annotate files within the Rails application (unless explicitly allowed)
19
+ if !allow_external_files && defined?(Rails.root) && !file_path.start_with?(Rails.root.to_s)
20
+ return
21
+ end
22
+
23
+ annotation_text = generate_annotation
24
+
25
+ # First remove any existing annotations
26
+ content = File.read(file_path)
27
+ content = Annotation.remove(content) if Annotation.extract(content)
28
+
29
+ # Try AST-based insertion first
30
+ class_name = model_class.name.split('::').last
31
+
32
+ # Use Prism-based insertion
33
+ if FileInsertionHelper.insert_at_class_definition(file_path, class_name, annotation_text)
34
+ true
35
+ else
36
+ # Final fallback to old method
37
+ annotated_content = add_annotation(content, file_path)
38
+ if annotated_content == content
39
+ false
40
+ else
41
+ File.write(file_path, annotated_content)
42
+ true
43
+ end
44
+ end
45
+ end
46
+
47
+ def remove_annotations(file_path = nil)
48
+ file_path ||= model_file_path
49
+ return unless file_path && File.exist?(file_path)
50
+
51
+ content = File.read(file_path)
52
+ cleaned_content = Annotation.remove(content)
53
+
54
+ if cleaned_content == content
55
+ false
56
+ else
57
+ File.write(file_path, cleaned_content)
58
+ true
59
+ end
60
+ end
61
+
62
+ def generate_annotation
63
+ pipeline = AnnotationPipeline.new
64
+ results = pipeline.process(model_class)
65
+
66
+ annotation = Annotation.new
67
+
68
+ # Add schema content
69
+ annotation.add_lines(results[:schema].split("\n")) if results[:schema]
70
+
71
+ # Add sections
72
+ results[:sections].each do |section|
73
+ next unless section && section[:content]
74
+
75
+ annotation.add_line('')
76
+ # The provider can optionally provide a title
77
+ annotation.add_line(section[:title]) if section[:title]
78
+ annotation.add_lines(section[:content].split("\n"))
79
+ end
80
+
81
+ # Add notes
82
+ if results[:notes].any?
83
+ annotation.add_line('')
84
+ annotation.add_line('== Notes')
85
+ results[:notes].uniq.each do |note|
86
+ annotation.add_line("- #{note}")
87
+ end
88
+ end
89
+
90
+ annotation.to_s
91
+ end
92
+
93
+ def self.annotate_all(options = {})
94
+ models = ModelDetector.detect_models(options)
95
+
96
+ # Filter abstract classes based on options
97
+ if options[:include_abstract]
98
+ # Include all models
99
+ elsif options[:abstract_only]
100
+ models = models.select(&:abstract_class?)
101
+ else
102
+ # Default: exclude abstract classes
103
+ models = models.reject(&:abstract_class?)
104
+ end
105
+
106
+ results = { annotated: [], skipped: [], failed: [] }
107
+
108
+ models.each do |model|
109
+ # Ensure model is actually a class, not a hash or other object
110
+ unless model.is_a?(Class)
111
+ results[:failed] << { model: model.inspect, error: "Expected Class, got #{model.class}" }
112
+ next
113
+ end
114
+
115
+ # Skip models without tables or with missing tables (but not abstract classes)
116
+ unless model.abstract_class? || model.table_exists?
117
+ results[:skipped] << model.name
118
+ warn "Skipping #{model.name} - table does not exist" if options[:verbose]
119
+ next
120
+ end
121
+
122
+ manager = new(model)
123
+
124
+ # Determine file path based on options
125
+ file_path = if options[:models_path]
126
+ File.join(options[:models_path], "#{model.name.underscore}.rb")
127
+ else
128
+ nil # Use default model_file_path
129
+ end
130
+
131
+ # Allow external files when models_path is provided (for testing)
132
+ allow_external = options[:models_path].present?
133
+
134
+ if manager.annotate_file(file_path, allow_external_files: allow_external)
135
+ results[:annotated] << model.name
136
+ else
137
+ results[:skipped] << model.name
138
+ end
139
+ rescue ActiveRecord::StatementInvalid => e
140
+ # Handle database-related errors (missing tables, schemas, etc.)
141
+ results[:skipped] << model.name
142
+ warn "Skipping #{model.name} - database error: #{e.message}" if options[:verbose]
143
+ rescue StandardError => e
144
+ model_name = if model.is_a?(Class) && model.respond_to?(:name)
145
+ model.name
146
+ else
147
+ model.inspect
148
+ end
149
+ results[:failed] << { model: model_name, error: e.message }
150
+ end
151
+
152
+ results
153
+ end
154
+
155
+ def self.remove_all(options = {})
156
+ models = ModelDetector.detect_models(options)
157
+ results = { removed: [], skipped: [], failed: [] }
158
+
159
+ models.each do |model|
160
+ manager = new(model)
161
+ if manager.remove_annotations
162
+ results[:removed] << model.name
163
+ else
164
+ results[:skipped] << model.name
165
+ end
166
+ rescue StandardError => e
167
+ results[:failed] << { model: model.name, error: e.message }
168
+ end
169
+
170
+ results
171
+ end
172
+
173
+ private
174
+
175
+ def add_annotation(content, _file_path = nil)
176
+ annotation_text = generate_annotation
177
+
178
+ # First check if annotation already exists and remove it
179
+ existing = Annotation.extract(content)
180
+ content = Annotation.remove(content) if existing
181
+
182
+ # Use the file insertion helper to insert after frozen_string_literal
183
+ FileInsertionHelper.insert_after_frozen_string_literal(content, annotation_text)
184
+ end
185
+
186
+ def model_file_path
187
+ # First try const_source_location as it's more reliable for finding model files
188
+ const_source_location = Object.const_source_location(model_class.name)
189
+ return const_source_location.first if const_source_location
190
+
191
+ # Fallback to instance method source location (though this often points to ActiveRecord)
192
+ model_class.instance_method(:initialize).source_location.first
193
+ rescue StandardError
194
+ # As a last resort, try to construct the path from Rails conventions
195
+ if defined?(Rails.root) && model_class.name
196
+ model_path = Rails.root.join('app', 'models', "#{model_class.name.underscore}.rb").to_s
197
+ model_path if File.exist?(model_path)
198
+ end
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_lens do
4
+ desc 'Annotate Rails models with schema information'
5
+ task :annotate, [:models] => :environment do |_t, args|
6
+ require 'rails_lens/schema/annotation_manager'
7
+
8
+ options = {
9
+ models: args[:models]&.split(',')
10
+ }
11
+
12
+ results = RailsLens::Schema::AnnotationManager.annotate_all(options)
13
+
14
+ puts "Annotated #{results[:annotated].length} models"
15
+ puts "Skipped #{results[:skipped].length} models" if results[:skipped].any?
16
+ if results[:failed].any?
17
+ puts "Failed to annotate #{results[:failed].length} models:"
18
+ results[:failed].each do |failure|
19
+ puts " - #{failure[:model]}: #{failure[:error]}"
20
+ end
21
+ end
22
+ end
23
+
24
+ desc 'Annotate all Rails files (models, routes, and mailers)'
25
+ task all: :environment do
26
+ # Annotate models
27
+ Rake::Task['rails_lens:annotate'].invoke
28
+
29
+ # Annotate routes
30
+ Rake::Task['rails_lens:routes:annotate'].invoke
31
+
32
+ # Annotate mailers
33
+ Rake::Task['rails_lens:mailers:annotate'].invoke
34
+ end
35
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_lens do
4
+ desc 'Generate Entity Relationship Diagram (Mermaid format)'
5
+ task erd: :environment do
6
+ gem 'mermaid', '>= 0.0.5'
7
+ require 'mermaid'
8
+
9
+ puts 'Generating ERD...'
10
+ visualizer = RailsLens::ERD::Visualizer.new
11
+ filename = visualizer.generate
12
+ puts "ERD generated successfully: #{filename}"
13
+ puts ''
14
+ puts 'To view the ERD:'
15
+ puts '1. Install Mermaid CLI: npm install -g @mermaid-js/mermaid-cli'
16
+ puts "2. Generate image: mmdc -i #{filename} -o erd.png"
17
+ puts '3. Or view online: https://mermaid.live/'
18
+ rescue Gem::LoadError
19
+ puts 'Error: Mermaid gem (>= 0.0.5) is required for ERD generation.'
20
+ puts "Add to your Gemfile: gem 'mermaid', '>= 0.0.5'"
21
+ puts 'Then run: bundle install'
22
+ exit 1
23
+ end
24
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_lens do
4
+ namespace :mailers do
5
+ desc 'Annotate mailer files with mailer information'
6
+ task annotate: :environment do
7
+ require 'rails_lens/mailer/annotator'
8
+
9
+ annotator = RailsLens::Mailer::Annotator.new
10
+ changed_files = annotator.annotate_all
11
+
12
+ puts "Annotated #{changed_files.length} mailer files with mailer information"
13
+ changed_files.each { |file| puts " - #{file}" }
14
+ end
15
+
16
+ desc 'Remove mailer annotations from mailer files'
17
+ task remove: :environment do
18
+ require 'rails_lens/mailer/annotator'
19
+
20
+ annotator = RailsLens::Mailer::Annotator.new
21
+ changed_files = annotator.remove_all
22
+
23
+ puts "Removed mailer annotations from #{changed_files.length} mailer files"
24
+ changed_files.each { |file| puts " - #{file}" }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_lens do
4
+ namespace :routes do
5
+ desc 'Annotate controller files with route information'
6
+ task annotate: :environment do
7
+ require 'rails_lens/route/annotator'
8
+
9
+ annotator = RailsLens::Route::Annotator.new
10
+ changed_files = annotator.annotate_all
11
+
12
+ puts "Annotated #{changed_files.length} controller files with route information"
13
+ changed_files.each { |file| puts " - #{file}" }
14
+ end
15
+
16
+ desc 'Remove route annotations from controller files'
17
+ task remove: :environment do
18
+ require 'rails_lens/route/annotator'
19
+
20
+ annotator = RailsLens::Route::Annotator.new
21
+ changed_files = annotator.remove_all
22
+
23
+ puts "Removed route annotations from #{changed_files.length} controller files"
24
+ changed_files.each { |file| puts " - #{file}" }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :rails_lens do
4
+ namespace :schema do
5
+ desc 'Annotate models with database schema information'
6
+ task annotate: :environment do
7
+ require 'rails_lens'
8
+
9
+ options = {}
10
+ options[:include_abstract] = true if ENV['INCLUDE_ABSTRACT'] == 'true'
11
+
12
+ puts 'Annotating models with schema information...'
13
+ results = RailsLens.annotate_models(options)
14
+
15
+ if results[:annotated].any?
16
+ puts "Annotated #{results[:annotated].length} models"
17
+ puts '(including abstract classes)' if options[:include_abstract]
18
+ end
19
+
20
+ puts "\nSkipped #{results[:skipped].length} models (no changes needed)" if results[:skipped].any?
21
+
22
+ if results[:failed].any?
23
+ puts "\nFailed to annotate #{results[:failed].length} models:"
24
+ results[:failed].each do |failure|
25
+ puts " ✗ #{failure[:model]}: #{failure[:error]}"
26
+ end
27
+ end
28
+ end
29
+
30
+ desc 'Remove schema annotations from models'
31
+ task remove: :environment do
32
+ require 'rails_lens'
33
+
34
+ puts 'Removing schema annotations...'
35
+ results = RailsLens.remove_annotations
36
+
37
+ if results[:removed].any?
38
+ puts "Removed annotations from #{results[:removed].length} models:"
39
+ results[:removed].each { |model| puts " ✓ #{model}" }
40
+ end
41
+
42
+ puts "\nSkipped #{results[:skipped].length} models (no annotations found)" if results[:skipped].any?
43
+
44
+ if results[:failed].any?
45
+ puts "\nFailed to process #{results[:failed].length} models:"
46
+ results[:failed].each do |failure|
47
+ puts " ✗ #{failure[:model]}: #{failure[:error]}"
48
+ end
49
+ end
50
+ end
51
+
52
+ desc 'Analyze models and show notes'
53
+ task analyze: :environment do
54
+ require 'rails_lens'
55
+
56
+ puts 'Analyzing models...'
57
+
58
+ pipeline = RailsLens::AnnotationPipeline.new
59
+ models = RailsLens::ModelDetector.detect_models
60
+
61
+ models.each do |model|
62
+ next if model.abstract_class? || !model.table_exists?
63
+
64
+ results = pipeline.process(model)
65
+ notes = results[:notes]
66
+
67
+ if notes.any?
68
+ puts "\n#{model.name}:"
69
+ notes.uniq.each { |note| puts " - #{note}" }
70
+ end
71
+ rescue StandardError => e
72
+ puts "\nError analyzing #{model.name}: #{e.message}"
73
+ end
74
+ end
75
+
76
+ desc 'List detected extensions'
77
+ task extensions: :environment do
78
+ require 'rails_lens'
79
+
80
+ puts 'Detected extensions:'
81
+
82
+ extensions = RailsLens::ExtensionLoader.load_extensions
83
+ if extensions.any?
84
+ extensions.each do |ext|
85
+ status = ext.detect? ? '✓ Active' : '○ Inactive'
86
+ version = begin
87
+ ext.interface_version
88
+ rescue StandardError
89
+ 'Unknown'
90
+ end
91
+ puts " #{status} #{ext.gem_name} (interface v#{version})"
92
+ end
93
+ else
94
+ puts ' No extensions detected'
95
+ end
96
+
97
+ config = RailsLens.config.extensions
98
+ puts "\nExtension configuration:"
99
+ puts " Enabled: #{config[:enabled]}"
100
+ puts " Autoload: #{config[:autoload]}"
101
+ puts " Interface version: #{config[:interface_version]}"
102
+
103
+ puts " Ignored gems: #{config[:ignore].join(', ')}" if config[:ignore].any?
104
+
105
+ puts " Custom paths: #{config[:custom_paths].join(', ')}" if config[:custom_paths].any?
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsLens
4
+ VERSION = '0.2.2'
5
+ end