rails_console_pro 0.1.2 → 0.1.3

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec_status +261 -240
  3. data/CHANGELOG.md +4 -0
  4. data/QUICK_START.md +8 -0
  5. data/README.md +16 -0
  6. data/docs/FORMATTING.md +5 -0
  7. data/docs/MODEL_STATISTICS.md +4 -0
  8. data/docs/OBJECT_DIFFING.md +6 -0
  9. data/docs/PROFILING.md +91 -0
  10. data/docs/QUEUE_INSIGHTS.md +82 -0
  11. data/docs/SCHEMA_INSPECTION.md +5 -0
  12. data/docs/SNIPPETS.md +71 -0
  13. data/lib/rails_console_pro/commands/jobs_command.rb +212 -0
  14. data/lib/rails_console_pro/commands/profile_command.rb +84 -0
  15. data/lib/rails_console_pro/commands/snippets_command.rb +141 -0
  16. data/lib/rails_console_pro/commands.rb +15 -0
  17. data/lib/rails_console_pro/configuration.rb +39 -0
  18. data/lib/rails_console_pro/format_exporter.rb +8 -0
  19. data/lib/rails_console_pro/global_methods.rb +12 -0
  20. data/lib/rails_console_pro/initializer.rb +23 -0
  21. data/lib/rails_console_pro/model_validator.rb +1 -1
  22. data/lib/rails_console_pro/printers/profile_printer.rb +180 -0
  23. data/lib/rails_console_pro/printers/queue_insights_printer.rb +150 -0
  24. data/lib/rails_console_pro/printers/snippet_collection_printer.rb +68 -0
  25. data/lib/rails_console_pro/printers/snippet_printer.rb +64 -0
  26. data/lib/rails_console_pro/profile_result.rb +109 -0
  27. data/lib/rails_console_pro/pry_commands.rb +106 -0
  28. data/lib/rails_console_pro/queue_insights_result.rb +110 -0
  29. data/lib/rails_console_pro/serializers/profile_serializer.rb +73 -0
  30. data/lib/rails_console_pro/services/profile_collector.rb +245 -0
  31. data/lib/rails_console_pro/services/queue_action_service.rb +176 -0
  32. data/lib/rails_console_pro/services/queue_insight_fetcher.rb +600 -0
  33. data/lib/rails_console_pro/services/snippet_repository.rb +191 -0
  34. data/lib/rails_console_pro/snippets/collection_result.rb +44 -0
  35. data/lib/rails_console_pro/snippets/single_result.rb +30 -0
  36. data/lib/rails_console_pro/snippets/snippet.rb +112 -0
  37. data/lib/rails_console_pro/snippets.rb +12 -0
  38. data/lib/rails_console_pro/version.rb +1 -1
  39. data/rails_console_pro.gemspec +1 -1
  40. metadata +26 -8
data/QUICK_START.md CHANGED
@@ -59,6 +59,13 @@ export schema(User) user_schema.json
59
59
  ```
60
60
  [Learn more →](docs/EXPORT.md)
61
61
 
62
+ ### Snippet Library
63
+ ```ruby
64
+ snippets(:add, "User.where(active: true).count", description: "Active users", tags: %w[users metrics])
65
+ snippets(:list)
66
+ ```
67
+ [Learn more →](docs/SNIPPETS.md)
68
+
62
69
  ### Beautiful Formatting
63
70
  ```ruby
64
71
  User.first # Automatically formatted with colors
@@ -103,6 +110,7 @@ rails generate rails_console_pro:install
103
110
  - [Association Navigation](docs/ASSOCIATION_NAVIGATION.md)
104
111
  - [Object Diffing](docs/OBJECT_DIFFING.md)
105
112
  - [Export](docs/EXPORT.md)
113
+ - [Snippets](docs/SNIPPETS.md)
106
114
  - [Formatting](docs/FORMATTING.md)
107
115
 
108
116
  ## Need Help?
data/README.md CHANGED
@@ -14,9 +14,12 @@ Rails Console Pro transforms your Rails console into a powerful debugging enviro
14
14
  - 🔍 **SQL Explain** - Analyze query execution plans with performance recommendations
15
15
  - 🧭 **Association Navigator** - Interactive navigation through model associations
16
16
  - 📈 **Model Statistics** - Record counts, growth rates, table sizes, and index usage
17
+ - 🔬 **Adaptive Profiling** - Profile blocks or relations with query, cache, and performance metrics
18
+ - 🧵 **ActiveJob Insights** - Inspect and manage queues across adapters (Sidekiq, SolidQueue, Test, Async) with filters and inline actions
17
19
  - 🔄 **Object Diffing** - Compare ActiveRecord objects and highlight differences
18
20
  - 💾 **Export Capabilities** - Export to JSON, YAML, and HTML formats
19
21
  - 📄 **Smart Pagination** - Automatic pagination for large collections
22
+ - 📝 **Snippet Library** - Capture, search, and reuse console snippets across sessions
20
23
 
21
24
  ## 🚀 Installation
22
25
 
@@ -62,6 +65,15 @@ diff user1, user2
62
65
 
63
66
  # Export
64
67
  export schema(User) user_schema.json
68
+
69
+ # Profiling
70
+ profile('Load users') { User.active.includes(:posts).limit(10).to_a }
71
+
72
+ # Queue insights
73
+ jobs(limit: 10, queue: 'mailers')
74
+ jobs status=retry class=ReminderJob
75
+ jobs retry=abcdef123456
76
+ jobs details=abcdef123456
65
77
  ```
66
78
 
67
79
  See [QUICK_START.md](QUICK_START.md) for more examples and detailed documentation for each feature.
@@ -78,6 +90,7 @@ RailsConsolePro.configure do |config|
78
90
  config.schema_command_enabled = true
79
91
  config.explain_command_enabled = true
80
92
  config.stats_command_enabled = true
93
+ config.queue_command_enabled = true
81
94
 
82
95
  # Color scheme
83
96
  config.color_scheme = :dark # or :light
@@ -107,7 +120,10 @@ end
107
120
  - [Association Navigation](docs/ASSOCIATION_NAVIGATION.md) - Navigate model associations
108
121
  - [Object Diffing](docs/OBJECT_DIFFING.md) - Compare objects
109
122
  - [Export](docs/EXPORT.md) - Export to JSON, YAML, HTML
123
+ - [Snippets](docs/SNIPPETS.md) - Build a reusable console snippet library
110
124
  - [Formatting](docs/FORMATTING.md) - Beautiful console output
125
+ - [Adaptive Profiling](docs/PROFILING.md) - Measure queries, cache hits, and potential N+1 issues
126
+ - [Queue Insights](docs/QUEUE_INSIGHTS.md) - Inspect jobs across ActiveJob adapters
111
127
 
112
128
  ## 🤝 Contributing
113
129
 
data/docs/FORMATTING.md CHANGED
@@ -84,3 +84,8 @@ end
84
84
  - **Clean Layout**: Well-organized, readable output
85
85
  - **Dark/Light Themes**: Choose your preferred color scheme
86
86
 
87
+ ## Screenshots
88
+
89
+ <img width="1117" height="551" alt="Screenshot 2025-11-07 at 11 42 52 AM" src="https://github.com/user-attachments/assets/2e77e0d5-8fa1-4249-8b0f-f8d16e829d99" />
90
+
91
+
@@ -70,3 +70,7 @@ export stats(User) user_stats.json
70
70
  - **Index Usage**: Which indexes are used frequently
71
71
  - **Column Statistics**: Unique values, distributions, ranges (for smaller tables)
72
72
 
73
+ ## Screenshots
74
+
75
+ <img width="1119" height="714" alt="Screenshot 2025-11-07 at 11 44 01 AM" src="https://github.com/user-attachments/assets/a175cb8c-1bea-4819-a80b-3f0bbb0d1a75" />
76
+
@@ -85,3 +85,9 @@ diff(user1, user2).to_json
85
85
  - **Multiple Object Types**: Works with ActiveRecord, Hash, and more
86
86
  - **Export Support**: Export diff results to JSON/YAML/HTML
87
87
 
88
+ ## Screenshots
89
+
90
+ <img width="1057" height="650" alt="Screenshot 2025-11-07 at 11 27 23 AM" src="https://github.com/user-attachments/assets/ca9fce82-8ed8-4506-907b-75e07735ec66" />
91
+
92
+
93
+
data/docs/PROFILING.md ADDED
@@ -0,0 +1,91 @@
1
+ # Adaptive Profiling
2
+
3
+ Rails Console Pro includes an adaptive profiler that wraps any block, callable, or ActiveRecord relation and reports real-time performance metrics without leaving the console.
4
+
5
+ ## Why Use It?
6
+
7
+ - Understand how long a console experiment takes end-to-end
8
+ - See how much time is spent inside SQL queries
9
+ - Detect cache hits/misses and duplicated queries (potential N+1s)
10
+ - Keep a sample of executed SQL statements with bind values
11
+ - Capture errors while still getting timing information
12
+
13
+ ## Basic Usage
14
+
15
+ ```ruby
16
+ # Profile a block
17
+ profile { User.active.limit(25).to_a }
18
+
19
+ # Add a label for the session
20
+ profile('Load active users') { User.active.includes(:posts).limit(25).to_a }
21
+
22
+ # Profile a relation (loads it automatically)
23
+ relation = User.includes(:posts).limit(10)
24
+ profile relation
25
+
26
+ # Profile any callable object
27
+ profile -> { HeavyService.call(user) }
28
+ ```
29
+
30
+ `profile` returns a `RailsConsolePro::ProfileResult` instance, so you can further inspect or export the collected metrics.
31
+
32
+ ## Sample Output
33
+
34
+ ```
35
+ 🧪 PROFILE: Load active users
36
+ ⏱ Execution Summary:
37
+ Total time 35.42 ms
38
+ SQL time 28.12 ms
39
+ (79.36% of total time spent in SQL)
40
+
41
+ 🗂 Query Breakdown:
42
+ Total queries 4
43
+ Read queries 4
44
+ Write queries 0
45
+ Cached queries 1
46
+
47
+ 🐢 Slow Queries (100.0ms+):
48
+ 1. 120.44 ms SELECT "users".* FROM ...
49
+ ```
50
+
51
+ The printer also highlights cache activity, sample queries, potential N+1 issues, and any error raised during execution.
52
+
53
+ ## Configuration
54
+
55
+ Tune profiling behaviour via the initializer:
56
+
57
+ ```ruby
58
+ RailsConsolePro.configure do |config|
59
+ # Enable/disable the profile command
60
+ config.profile_command_enabled = true
61
+
62
+ # Flag queries above this threshold (milliseconds)
63
+ config.profile_slow_query_threshold = 120.0
64
+
65
+ # Minimum occurrences before a query is treated as a possible N+1
66
+ config.profile_duplicate_query_threshold = 3
67
+
68
+ # Number of query samples to keep in memory for reporting
69
+ config.profile_max_saved_queries = 10
70
+ end
71
+ ```
72
+
73
+ ## Exporting
74
+
75
+ Profile results can be exported like any other value object:
76
+
77
+ ```ruby
78
+ result = profile { User.active.limit(10).to_a }
79
+
80
+ result.to_json
81
+ result.to_yaml
82
+ result.export_to_file('profile.html', format: :html)
83
+ ```
84
+
85
+ ## Tips
86
+
87
+ - Combine profiling with Rails scopes to analyse real data paths
88
+ - Lower `profile_slow_query_threshold` when testing on local databases
89
+ - Use labels to differentiate multiple runs in the same console session
90
+ - Errors are captured and displayed; you still get timings even when the block fails
91
+
@@ -0,0 +1,82 @@
1
+ # Queue Insights
2
+
3
+ Rails Console Pro introduces a `jobs` command that surfaces ActiveJob activity without leaving the console. It aggregates state from the underlying queue adapter so you can quickly review what is enqueued, retrying, or currently executing.
4
+
5
+ ## Supported Adapters
6
+
7
+ The command automatically detects the adapter backing `ActiveJob::Base.queue_adapter` and falls back gracefully when a feature is unavailable.
8
+
9
+ | Adapter | Enqueued | Retries | Recent Executions | Notes |
10
+ | ------- | -------- | ------- | ----------------- | ----- |
11
+ | Sidekiq | ✅ | ✅ (`RetrySet`) | ✅ (`Workers`) | Requires Redis connection |
12
+ | SolidQueue | ✅ (`ready`) | ✅ (`retryable`/`failed`) | ✅ (`Execution`/`CompletedExecution`) | Works with default SolidQueue tables |
13
+ | Test / Inline / Async | ✅ (`enqueued_jobs`) | ❌ | ✅ (`performed_jobs` when available) | Primarily for development/test |
14
+
15
+ > **Tip:** When an adapter does not expose a given dataset, the section is omitted and a warning is printed where appropriate.
16
+ >
17
+ > When Sidekiq or SolidQueue are loaded but ActiveJob is still using the default async adapter, Rails Console Pro automatically falls back to the native queue APIs so you still see those jobs. For Sidekiq, ensure `sidekiq/api` is required in the console (most Rails apps do this automatically). Inline actions (`retry=`, `delete=`, `details=`) currently target Sidekiq queues.
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ # Everything (auto-detected adapter)
23
+ jobs
24
+
25
+ # Limit the number of entries
26
+ jobs(limit: 10)
27
+
28
+ # Focus on a specific queue (if the adapter supports it)
29
+ jobs(queue: "mailers")
30
+
31
+ # Combine options
32
+ jobs(limit: 5, queue: "critical")
33
+
34
+ # Filter by status or job class
35
+ jobs status=retry
36
+ jobs status=enqueued,retry class=ReminderJob
37
+
38
+ # Inline queue actions
39
+ jobs retry=abcdef123456
40
+ jobs delete=abcdef123456
41
+ jobs details=abcdef123456
42
+ ```
43
+
44
+ In Pry you can use CLI-style arguments:
45
+
46
+ ```
47
+ jobs limit=5 queue=critical
48
+ jobs 10 mailers
49
+ jobs status=retry class=ReminderJob
50
+ jobs --retry-only
51
+ jobs retry=abcdef123456
52
+ jobs delete=abcdef123456
53
+ jobs details=abcdef123456
54
+ ```
55
+
56
+ ## Output
57
+
58
+ The printer renders three sections when data is available:
59
+
60
+ 1. **📬 Enqueued Jobs** – Most recent enqueued jobs with timestamps and arguments.
61
+ 2. **🔁 Retry Set** – Jobs scheduled for retry or marked as failed.
62
+ 3. **⚙️ Recent Executions** – Currently running jobs (Sidekiq) or recently performed jobs (other adapters).
63
+
64
+ Adapter statistics (e.g., processed, failed) appear under **ℹ️ Adapter Stats** when provided by the backend.
65
+
66
+ ## Configuration
67
+
68
+ The feature is enabled by default. Toggle it in your initializer if needed:
69
+
70
+ ```ruby
71
+ RailsConsolePro.configure do |config|
72
+ config.queue_command_enabled = true # or false to disable
73
+ end
74
+ ```
75
+
76
+ ## Troubleshooting
77
+
78
+ - **Missing data:** Ensure the backing queue system is reachable (e.g., Redis for Sidekiq).
79
+ - **Adapter-specific warnings:** These indicate the adapter API is unavailable or the command lacks sufficient access. The command continues with available sections.
80
+ - **Custom adapters:** The command falls back to ActiveJob's generic `enqueued_jobs` / `performed_jobs` APIs when present.
81
+
82
+
@@ -57,4 +57,9 @@ schema(User).to_json
57
57
  # Export to file
58
58
  export schema(User) user_schema.json
59
59
  ```
60
+ ## Screenshots
60
61
 
62
+
63
+ <img width="1114" height="722" alt="Screenshot 2025-11-07 at 11 27 52 AM" src="https://github.com/user-attachments/assets/ab93a9b9-4b59-4032-b15e-06fe1ef069c6" />
64
+
65
+ <img width="1107" height="498" alt="Screenshot 2025-11-07 at 11 28 01 AM" src="https://github.com/user-attachments/assets/27f9e85a-8e5d-48f6-9a75-b69fde3dbb5c" />
data/docs/SNIPPETS.md ADDED
@@ -0,0 +1,71 @@
1
+ # Snippet Library
2
+
3
+ Rails Console Pro ships with a developer-friendly snippet library that helps you capture, search, and reuse the console commands you rely on every day.
4
+
5
+ ## Overview
6
+
7
+ - Store frequently used console expressions with a short description and optional tags
8
+ - Persist snippets across console sessions (default path: `tmp/rails_console_pro/snippets.yml`)
9
+ - Fuzzy search by text or tags to quickly locate prior commands
10
+ - Mark favorites for even faster recall
11
+ - Use Ruby blocks to add multi-line snippets with nice formatting
12
+
13
+ ## Quick Actions
14
+
15
+ ```ruby
16
+ # Capture a snippet
17
+ snippets(:add, "User.where(active: true).count", description: "Active users", tags: %w[users metrics])
18
+
19
+ # Capture multiline snippet with a block
20
+ snippets(:add, description: "Backfill user slugs") do
21
+ <<~RUBY
22
+ User.where(slug: nil).find_each do |user|
23
+ user.update!(slug: user.name.parameterize)
24
+ end
25
+ RUBY
26
+ end
27
+
28
+ # List (defaults to the 10 most recent)
29
+ snippets(:list)
30
+
31
+ # Search by keyword
32
+ snippets(:search, "active users")
33
+
34
+ # Search by tags
35
+ snippets(:search, tags: %w[metrics])
36
+
37
+ # Mark a favorite and list favorites
38
+ snippets(:favorite, "active-users")
39
+ snippets(:list, favorites: true)
40
+
41
+ # Show a specific snippet with full body
42
+ snippets(:show, "active-users")
43
+
44
+ # Remove a snippet
45
+ snippets(:delete, "active-users")
46
+ ```
47
+
48
+ ## Configuration
49
+
50
+ You can adjust snippet behavior via the standard configuration block:
51
+
52
+ ```ruby
53
+ RailsConsolePro.configure do |config|
54
+ # Disable / enable the snippet commands
55
+ config.snippets_command_enabled = true
56
+
57
+ # Override the storage path if you prefer a shared location
58
+ config.snippet_store_path = Rails.root.join("tmp", "rails_console_pro", "snippets.yml")
59
+ end
60
+ ```
61
+
62
+ If you want to share snippets across a team, point `snippet_store_path` to a shared directory (for example within your repo) and commit the file if appropriate.
63
+
64
+ ## Tips
65
+
66
+ - Use meaningful IDs (`id:` option) to recall snippets by name (`snippets(:show, "backfill-slugs")`)
67
+ - Tag by model or domain (`tags: %w[user data-migrations]`) to quickly filter the list
68
+ - Keep destructive snippets safe by adding warnings to the description or tags (`tags: %w[danger prod-only]`)
69
+ - Since snippets are plain Ruby strings, you can paste them directly back into the console when you need them
70
+
71
+
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Commands
5
+ class JobsCommand < BaseCommand
6
+ DEFAULT_LIMIT = 20
7
+
8
+ def execute(options = nil)
9
+ unless feature_enabled?
10
+ puts pastel.yellow("Jobs command is disabled. Enable it with: RailsConsolePro.configure { |c| c.queue_command_enabled = true }")
11
+ return nil
12
+ end
13
+
14
+ unless active_job_available?
15
+ puts pastel.red("ActiveJob is not loaded. Queue insights require ActiveJob to be available.")
16
+ return nil
17
+ end
18
+
19
+ normalized_options = normalize_options(options)
20
+
21
+ fetch_options, filter_options, action_options = split_options(normalized_options)
22
+ action_results = perform_actions(action_options, fetch_options)
23
+ action_results.each { |result| print_action_result(result) }
24
+
25
+ result = fetcher.fetch(**fetch_options)
26
+ return result unless result
27
+
28
+ apply_filters(result, filter_options)
29
+ rescue => e
30
+ RailsConsolePro::ErrorHandler.handle(e, context: :jobs)
31
+ end
32
+
33
+ private
34
+
35
+ def feature_enabled?
36
+ RailsConsolePro.config.queue_command_enabled
37
+ end
38
+
39
+ def active_job_available?
40
+ defined?(ActiveJob::Base)
41
+ end
42
+
43
+ def fetcher
44
+ @fetcher ||= Services::QueueInsightFetcher.new
45
+ end
46
+
47
+ def action_service
48
+ @action_service ||= Services::QueueActionService.new
49
+ end
50
+
51
+ def perform_actions(action_options, fetch_options)
52
+ return [] if action_options.empty?
53
+
54
+ action_options.each_with_object([]) do |(action_key, value), results|
55
+ next if value.nil? || (value.respond_to?(:empty?) && value.empty?)
56
+
57
+ result = action_service.perform(
58
+ action: action_key,
59
+ jid: value,
60
+ queue: fetch_options[:queue]
61
+ )
62
+ results << result if result
63
+ end
64
+ end
65
+
66
+ def apply_filters(result, filter_options)
67
+ filters = prepare_filters(filter_options)
68
+ filtered_enqueued = result.enqueued_jobs
69
+ filtered_retry = result.retry_jobs
70
+ filtered_recent = result.recent_executions
71
+ additional_warnings = []
72
+
73
+ statuses = filters[:statuses]
74
+ unless statuses.empty?
75
+ show_enqueued = statuses.include?(:enqueued) || statuses.include?(:scheduled) || statuses.include?(:all)
76
+ show_retry = statuses.include?(:retry) || statuses.include?(:retries) || statuses.include?(:all)
77
+ show_recent = statuses.include?(:recent) || statuses.include?(:executing) || statuses.include?(:running) || statuses.include?(:all)
78
+
79
+ filtered_enqueued = [] unless show_enqueued
80
+ filtered_retry = [] unless show_retry
81
+ filtered_recent = [] unless show_recent
82
+ end
83
+
84
+ if filters[:job_class]
85
+ matcher = filters[:job_class]
86
+ filtered_enqueued = filtered_enqueued.select { |job| job_class_matches?(job, matcher) }
87
+ filtered_retry = filtered_retry.select { |job| job_class_matches?(job, matcher) }
88
+ filtered_recent = filtered_recent.select { |job| job_class_matches?(job, matcher) }
89
+ if filtered_enqueued.empty? && filtered_retry.empty? && filtered_recent.empty?
90
+ additional_warnings << "No jobs matching class filter '#{matcher}'."
91
+ end
92
+ end
93
+
94
+ filtered_result = result.with_overrides(
95
+ enqueued_jobs: filtered_enqueued,
96
+ retry_jobs: filtered_retry,
97
+ recent_executions: filtered_recent,
98
+ warnings: result.warnings + additional_warnings
99
+ )
100
+
101
+ filtered_result
102
+ end
103
+
104
+ def normalize_options(options)
105
+ case options
106
+ when Numeric
107
+ { limit: options.to_i }
108
+ when Hash
109
+ symbolized = options.each_with_object({}) do |(key, value), acc|
110
+ sym_key = key.to_sym
111
+ sym_key = :job_class if sym_key == :class
112
+ acc[sym_key] = value
113
+ end
114
+ symbolized
115
+ when nil
116
+ {}
117
+ else
118
+ {}
119
+ end.then do |opts|
120
+ limit = opts.fetch(:limit, DEFAULT_LIMIT)
121
+ limit = limit.to_i
122
+ limit = DEFAULT_LIMIT if limit <= 0
123
+ opts[:limit] = limit
124
+ opts[:job_class] = opts[:job_class].to_s.strip if opts.key?(:job_class) && opts[:job_class]
125
+ [:retry, :delete, :details].each do |key|
126
+ opts[key] = opts[key].to_s.strip if opts.key?(key) && opts[key]
127
+ end
128
+ opts
129
+ end
130
+ end
131
+
132
+ def split_options(options)
133
+ fetch_options = {
134
+ limit: options[:limit],
135
+ queue: options[:queue]
136
+ }.compact
137
+
138
+ filter_options = {
139
+ statuses: options[:statuses] || options[:status],
140
+ job_class: options[:job_class]
141
+ }.compact
142
+
143
+ action_options = {
144
+ retry: options[:retry],
145
+ delete: options[:delete],
146
+ details: options[:details]
147
+ }.compact
148
+
149
+ [fetch_options, filter_options, action_options]
150
+ end
151
+
152
+ def prepare_filters(filter_options)
153
+ statuses =
154
+ Array(filter_options[:statuses]).flat_map { |s| s.to_s.split(',') }
155
+ .map(&:strip)
156
+ .reject(&:empty?)
157
+ .map { |s| s.downcase.to_sym }
158
+ .uniq
159
+
160
+ {
161
+ statuses: statuses,
162
+ job_class: filter_options[:job_class],
163
+ limit: filter_options[:limit]
164
+ }
165
+ end
166
+
167
+ def job_class_matches?(job, matcher)
168
+ job_class = job.job_class.to_s
169
+ return false if job_class.empty?
170
+
171
+ matcher_str = matcher.to_s
172
+ job_class.casecmp?(matcher_str) ||
173
+ job_class.split('::').last.casecmp?(matcher_str) ||
174
+ job_class.include?(matcher_str)
175
+ end
176
+
177
+ def print_action_result(result)
178
+ return unless result
179
+
180
+ if result.warning
181
+ puts pastel.yellow("⚠️ #{result.warning}")
182
+ end
183
+
184
+ if result.message
185
+ color_method = result.success ? :green : :cyan
186
+ puts pastel.public_send(color_method, result.message)
187
+ end
188
+
189
+ if result.details
190
+ puts pastel.cyan("Details:")
191
+ formatted = format_details(result.details)
192
+ puts formatted
193
+ end
194
+ end
195
+
196
+ def format_details(details)
197
+ case details
198
+ when String
199
+ " #{details}"
200
+ when Hash
201
+ details.map { |key, value| " #{key}: #{value.inspect}" }.join("\n")
202
+ when Array
203
+ details.map { |value| " - #{value.inspect}" }.join("\n")
204
+ else
205
+ details.inspect
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
211
+
212
+
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsConsolePro
4
+ module Commands
5
+ # Command for profiling arbitrary blocks, callables, or ActiveRecord relations
6
+ class ProfileCommand < BaseCommand
7
+ def execute(target = nil, *args, label: nil, &block)
8
+ return disabled_message unless enabled?
9
+
10
+ label, target, block = normalize_arguments(label, target, block)
11
+ execution = build_execution(target, args, block)
12
+ return execution if execution.nil? || execution.is_a?(String)
13
+
14
+ collector = Services::ProfileCollector.new(config)
15
+ collector.profile(label: label || execution[:label]) do
16
+ execution[:callable].call
17
+ end
18
+ rescue => e
19
+ RailsConsolePro::ErrorHandler.handle(e, context: :profile)
20
+ end
21
+
22
+ private
23
+
24
+ def enabled?
25
+ RailsConsolePro.config.enabled && RailsConsolePro.config.profile_command_enabled
26
+ end
27
+
28
+ def disabled_message
29
+ pastel.yellow('Profile command is disabled. Enable it via RailsConsolePro.configure { |c| c.profile_command_enabled = true }')
30
+ end
31
+
32
+ def config
33
+ RailsConsolePro.config
34
+ end
35
+
36
+ def normalize_arguments(label, target, block)
37
+ if block_provided?(block) && (target.is_a?(String) || target.is_a?(Symbol))
38
+ label ||= target.to_s
39
+ target = nil
40
+ end
41
+ [label, target, block]
42
+ end
43
+
44
+ def block_provided?(block)
45
+ !block.nil?
46
+ end
47
+
48
+ def build_execution(target, args, block)
49
+ if block_provided?(block)
50
+ { callable: block, label: nil }
51
+ elsif relation?(target)
52
+ relation = target
53
+ { callable: -> { relation.load }, label: "#{relation.klass.name} relation" }
54
+ elsif callable?(target)
55
+ callable = target
56
+ { callable: -> { callable.call(*args) }, label: callable_label(callable) }
57
+ elsif target.nil?
58
+ pastel.red('Nothing to profile. Provide a block, callable, or ActiveRecord relation.')
59
+ else
60
+ { callable: -> { target }, label: target.class.name }
61
+ end
62
+ end
63
+
64
+ def relation?(object)
65
+ defined?(ActiveRecord::Relation) && object.is_a?(ActiveRecord::Relation)
66
+ end
67
+
68
+ def callable?(object)
69
+ object.respond_to?(:call)
70
+ end
71
+
72
+ def callable_label(callable)
73
+ if callable.respond_to?(:name) && callable.name
74
+ callable.name
75
+ elsif callable.respond_to?(:receiver) && callable.respond_to?(:name)
76
+ "#{callable.receiver.class}##{callable.name}"
77
+ else
78
+ callable.class.name
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
84
+