@reinteractive/rails-insight 1.0.1

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 (90) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +210 -0
  3. package/bin/railsinsight.js +128 -0
  4. package/package.json +62 -0
  5. package/src/core/blast-radius.js +496 -0
  6. package/src/core/constants.js +39 -0
  7. package/src/core/context-loader.js +227 -0
  8. package/src/core/drift-detector.js +168 -0
  9. package/src/core/formatter.js +197 -0
  10. package/src/core/graph.js +510 -0
  11. package/src/core/indexer.js +595 -0
  12. package/src/core/patterns/api.js +27 -0
  13. package/src/core/patterns/auth.js +25 -0
  14. package/src/core/patterns/authorization.js +24 -0
  15. package/src/core/patterns/caching.js +19 -0
  16. package/src/core/patterns/component.js +18 -0
  17. package/src/core/patterns/config.js +15 -0
  18. package/src/core/patterns/controller.js +42 -0
  19. package/src/core/patterns/email.js +20 -0
  20. package/src/core/patterns/factory.js +31 -0
  21. package/src/core/patterns/gemfile.js +9 -0
  22. package/src/core/patterns/helper.js +10 -0
  23. package/src/core/patterns/job.js +12 -0
  24. package/src/core/patterns/model.js +123 -0
  25. package/src/core/patterns/realtime.js +17 -0
  26. package/src/core/patterns/route.js +27 -0
  27. package/src/core/patterns/schema.js +25 -0
  28. package/src/core/patterns/stimulus.js +13 -0
  29. package/src/core/patterns/storage.js +16 -0
  30. package/src/core/patterns/uploader.js +16 -0
  31. package/src/core/patterns/view.js +20 -0
  32. package/src/core/patterns/worker.js +12 -0
  33. package/src/core/patterns.js +27 -0
  34. package/src/core/scanner.js +394 -0
  35. package/src/core/version-detector.js +295 -0
  36. package/src/extractors/api.js +284 -0
  37. package/src/extractors/auth.js +853 -0
  38. package/src/extractors/authorization.js +785 -0
  39. package/src/extractors/caching.js +84 -0
  40. package/src/extractors/component.js +221 -0
  41. package/src/extractors/config.js +81 -0
  42. package/src/extractors/controller.js +273 -0
  43. package/src/extractors/coverage-snapshot.js +296 -0
  44. package/src/extractors/email.js +123 -0
  45. package/src/extractors/factory-registry.js +225 -0
  46. package/src/extractors/gemfile.js +440 -0
  47. package/src/extractors/helper.js +55 -0
  48. package/src/extractors/jobs.js +122 -0
  49. package/src/extractors/model.js +506 -0
  50. package/src/extractors/realtime.js +102 -0
  51. package/src/extractors/routes.js +251 -0
  52. package/src/extractors/schema.js +178 -0
  53. package/src/extractors/stimulus.js +149 -0
  54. package/src/extractors/storage.js +100 -0
  55. package/src/extractors/test-conventions.js +340 -0
  56. package/src/extractors/tier2.js +417 -0
  57. package/src/extractors/tier3.js +84 -0
  58. package/src/extractors/uploader.js +138 -0
  59. package/src/extractors/views.js +131 -0
  60. package/src/extractors/worker.js +62 -0
  61. package/src/git/diff-parser.js +132 -0
  62. package/src/providers/interface.js +12 -0
  63. package/src/providers/local-fs.js +318 -0
  64. package/src/server.js +71 -0
  65. package/src/tools/blast-radius-tools.js +129 -0
  66. package/src/tools/free-tools.js +44 -0
  67. package/src/tools/handlers/get-controller.js +93 -0
  68. package/src/tools/handlers/get-coverage-gaps.js +100 -0
  69. package/src/tools/handlers/get-deep-analysis.js +294 -0
  70. package/src/tools/handlers/get-domain-clusters.js +113 -0
  71. package/src/tools/handlers/get-factory-registry.js +43 -0
  72. package/src/tools/handlers/get-full-index.js +28 -0
  73. package/src/tools/handlers/get-model.js +108 -0
  74. package/src/tools/handlers/get-overview.js +153 -0
  75. package/src/tools/handlers/get-routes.js +18 -0
  76. package/src/tools/handlers/get-schema.js +40 -0
  77. package/src/tools/handlers/get-subgraph.js +82 -0
  78. package/src/tools/handlers/get-test-conventions.js +18 -0
  79. package/src/tools/handlers/get-well-tested-examples.js +51 -0
  80. package/src/tools/handlers/helpers.js +115 -0
  81. package/src/tools/handlers/index-project.js +36 -0
  82. package/src/tools/handlers/search-patterns.js +104 -0
  83. package/src/tools/index.js +34 -0
  84. package/src/tools/pro-tools.js +13 -0
  85. package/src/utils/file-reader.js +20 -0
  86. package/src/utils/inflector.js +223 -0
  87. package/src/utils/ruby-parser.js +115 -0
  88. package/src/utils/spec-style-detector.js +26 -0
  89. package/src/utils/token-counter.js +46 -0
  90. package/src/utils/yaml-parser.js +135 -0
@@ -0,0 +1,440 @@
1
+ /**
2
+ * Gemfile Extractor (#16)
3
+ * Parses Gemfile and Gemfile.lock, categorises gems.
4
+ */
5
+
6
+ import { GEMFILE_PATTERNS } from '../core/patterns.js'
7
+
8
+ /** @type {Record<string, string[]>} */
9
+ const GEM_CATEGORIES = {
10
+ core: [
11
+ 'rails',
12
+ 'railties',
13
+ 'activesupport',
14
+ 'activerecord',
15
+ 'actionpack',
16
+ 'actionview',
17
+ 'actionmailer',
18
+ 'activejob',
19
+ 'actioncable',
20
+ 'activestorage',
21
+ 'actiontext',
22
+ 'actionmailbox',
23
+ 'puma',
24
+ 'unicorn',
25
+ 'bootsnap',
26
+ 'tzinfo-data',
27
+ 'sprockets',
28
+ 'sprockets-rails',
29
+ ],
30
+ frontend: [
31
+ 'importmap-rails',
32
+ 'jsbundling-rails',
33
+ 'cssbundling-rails',
34
+ 'tailwindcss-rails',
35
+ 'sass-rails',
36
+ 'webpacker',
37
+ 'shakapacker',
38
+ 'turbo-rails',
39
+ 'stimulus-rails',
40
+ 'hotwire-rails',
41
+ 'propshaft',
42
+ 'dartsass-rails',
43
+ 'vite_rails',
44
+ 'esbuild',
45
+ 'rollup',
46
+ ],
47
+ auth: [
48
+ 'devise',
49
+ 'devise-jwt',
50
+ 'devise_invitable',
51
+ 'omniauth',
52
+ 'omniauth-rails_csrf_protection',
53
+ 'sorcery',
54
+ 'clearance',
55
+ 'rodauth-rails',
56
+ 'doorkeeper',
57
+ 'jwt',
58
+ 'bcrypt',
59
+ 'has_secure_password',
60
+ 'warden',
61
+ 'rack-attack',
62
+ ],
63
+ authorization: [
64
+ 'pundit',
65
+ 'cancancan',
66
+ 'action_policy',
67
+ 'rolify',
68
+ 'authority',
69
+ ],
70
+ background: [
71
+ 'sidekiq',
72
+ 'sidekiq-pro',
73
+ 'sidekiq-enterprise',
74
+ 'resque',
75
+ 'delayed_job',
76
+ 'good_job',
77
+ 'solid_queue',
78
+ 'que',
79
+ 'sneakers',
80
+ 'shoryuken',
81
+ 'mission_control-jobs',
82
+ ],
83
+ caching: [
84
+ 'redis',
85
+ 'redis-rails',
86
+ 'redis-actionpack',
87
+ 'hiredis',
88
+ 'solid_cache',
89
+ 'dalli',
90
+ 'identity_cache',
91
+ 'readthis',
92
+ ],
93
+ search: [
94
+ 'searchkick',
95
+ 'pg_search',
96
+ 'meilisearch-rails',
97
+ 'elasticsearch-rails',
98
+ 'chewy',
99
+ 'ransack',
100
+ 'thinking-sphinx',
101
+ ],
102
+ payments: [
103
+ 'pay',
104
+ 'stripe',
105
+ 'stripe-rails',
106
+ 'braintree',
107
+ 'shopify_api',
108
+ 'solidus',
109
+ 'spree',
110
+ ],
111
+ uploads: [
112
+ 'image_processing',
113
+ 'mini_magick',
114
+ 'shrine',
115
+ 'carrierwave',
116
+ 'paperclip',
117
+ 'aws-sdk-s3',
118
+ 'google-cloud-storage',
119
+ 'azure-storage-blob',
120
+ ],
121
+ monitoring: [
122
+ 'sentry-ruby',
123
+ 'sentry-rails',
124
+ 'newrelic_rpm',
125
+ 'honeybadger',
126
+ 'bugsnag',
127
+ 'rollbar',
128
+ 'airbrake',
129
+ 'scout_apm',
130
+ 'skylight',
131
+ 'datadog',
132
+ 'lograge',
133
+ 'ahoy_matey',
134
+ ],
135
+ deployment: [
136
+ 'kamal',
137
+ 'capistrano',
138
+ 'capistrano-rails',
139
+ 'capistrano-bundler',
140
+ 'capistrano-rbenv',
141
+ 'thruster',
142
+ 'mina',
143
+ 'sshkit',
144
+ ],
145
+ code_quality: [
146
+ 'rubocop',
147
+ 'rubocop-rails',
148
+ 'rubocop-rspec',
149
+ 'rubocop-performance',
150
+ 'rubocop-minitest',
151
+ 'rubocop-rails-omakase',
152
+ 'standard',
153
+ 'erb_lint',
154
+ 'brakeman',
155
+ 'bundler-audit',
156
+ 'overcommit',
157
+ 'annotate',
158
+ 'reek',
159
+ 'flog',
160
+ 'flay',
161
+ ],
162
+ testing: [
163
+ 'rspec-rails',
164
+ 'factory_bot_rails',
165
+ 'faker',
166
+ 'capybara',
167
+ 'selenium-webdriver',
168
+ 'cuprite',
169
+ 'capybara-playwright-driver',
170
+ 'shoulda-matchers',
171
+ 'simplecov',
172
+ 'webmock',
173
+ 'vcr',
174
+ 'timecop',
175
+ 'database_cleaner',
176
+ 'parallel_tests',
177
+ 'minitest',
178
+ 'mocha',
179
+ 'rspec-mocks',
180
+ ],
181
+ data: [
182
+ 'pg',
183
+ 'mysql2',
184
+ 'sqlite3',
185
+ 'trilogy',
186
+ 'activerecord-import',
187
+ 'scenic',
188
+ 'strong_migrations',
189
+ 'active_record_doctor',
190
+ 'paranoia',
191
+ 'discard',
192
+ 'acts_as_paranoid',
193
+ 'paper_trail',
194
+ 'audited',
195
+ 'aasm',
196
+ 'statesman',
197
+ 'state_machines-activerecord',
198
+ 'friendly_id',
199
+ 'acts_as_list',
200
+ 'acts_as_tree',
201
+ 'ancestry',
202
+ 'closure_tree',
203
+ ],
204
+ admin: [
205
+ 'activeadmin',
206
+ 'administrate',
207
+ 'avo',
208
+ 'rails_admin',
209
+ 'trestle',
210
+ 'motor-admin',
211
+ ],
212
+ api: [
213
+ 'grape',
214
+ 'graphql',
215
+ 'graphql-ruby',
216
+ 'graphiql-rails',
217
+ 'jbuilder',
218
+ 'jsonapi-serializer',
219
+ 'active_model_serializers',
220
+ 'blueprinter',
221
+ 'alba',
222
+ 'fast_jsonapi',
223
+ 'rack-cors',
224
+ 'versionist',
225
+ 'apipie-rails',
226
+ 'rswag',
227
+ ],
228
+ realtime: ['actioncable', 'anycable-rails', 'hotwire-rails'],
229
+ mail: [
230
+ 'letter_opener',
231
+ 'letter_opener_web',
232
+ 'premailer-rails',
233
+ 'mailgun-ruby',
234
+ 'postmark-rails',
235
+ 'sendgrid-ruby',
236
+ 'aws-sdk-ses',
237
+ ],
238
+ pdf: [
239
+ 'wicked_pdf',
240
+ 'grover',
241
+ 'prawn',
242
+ 'prawn-table',
243
+ 'hexapdf',
244
+ 'wkhtmltopdf-binary',
245
+ ],
246
+ spreadsheet: [
247
+ 'caxlsx',
248
+ 'axlsx_rails',
249
+ 'roo',
250
+ 'spreadsheet',
251
+ 'rubyXL',
252
+ 'creek',
253
+ 'csv',
254
+ ],
255
+ image: ['ruby-vips', 'fastimage'],
256
+ i18n: ['i18n-tasks', 'rails-i18n', 'mobility', 'globalize', 'i18n-js'],
257
+ multi_tenancy: ['acts_as_tenant', 'apartment', 'ros-apartment', 'milia'],
258
+ dev_tools: [
259
+ 'pry',
260
+ 'pry-rails',
261
+ 'pry-byebug',
262
+ 'byebug',
263
+ 'debug',
264
+ 'better_errors',
265
+ 'binding_of_caller',
266
+ 'web-console',
267
+ 'rack-mini-profiler',
268
+ 'bullet',
269
+ 'prosopite',
270
+ 'pghero',
271
+ 'spring',
272
+ 'listen',
273
+ 'foreman',
274
+ ],
275
+ }
276
+
277
+ /** Build reverse lookup: gem name → category */
278
+ const GEM_TO_CATEGORY = {}
279
+ for (const [category, gems] of Object.entries(GEM_CATEGORIES)) {
280
+ for (const gem of gems) {
281
+ GEM_TO_CATEGORY[gem] = category
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Categorize a gem name.
287
+ * @param {string} name
288
+ * @returns {string}
289
+ */
290
+ function categorizeGem(name) {
291
+ return GEM_TO_CATEGORY[name] || 'other'
292
+ }
293
+
294
+ /**
295
+ * Parse resolved versions from Gemfile.lock content.
296
+ * @param {string|null} lockContent
297
+ * @returns {Map<string, string>}
298
+ */
299
+ function parseLockVersions(lockContent) {
300
+ const versions = new Map()
301
+ if (!lockContent) return versions
302
+
303
+ let inSpecs = false
304
+ for (const line of lockContent.split('\n')) {
305
+ const trimmed = line.trimEnd()
306
+ if (trimmed === ' specs:' || trimmed === ' specs:') {
307
+ inSpecs = true
308
+ continue
309
+ }
310
+ if (inSpecs && /^\S/.test(trimmed)) {
311
+ inSpecs = false
312
+ continue
313
+ }
314
+ if (inSpecs) {
315
+ // Lines like " rails (7.1.3)" or " activesupport (= 7.1.3)"
316
+ const match = trimmed.match(/^\s{4}(\S+)\s+\(([^)]+)\)/)
317
+ if (match) {
318
+ versions.set(match[1], match[2])
319
+ }
320
+ }
321
+ }
322
+ return versions
323
+ }
324
+
325
+ /**
326
+ * Parse Gemfile content into structured gem entries.
327
+ * @param {string|null} gemfileContent
328
+ * @param {Map<string, string>} lockVersions
329
+ * @returns {{ gems: Array<{name: string, version: string|null, resolved: string|null, category: string, group: string}>, source: string|null, rubyVersion: string|null, groups: string[] }}
330
+ */
331
+ function parseGemfile(gemfileContent, lockVersions) {
332
+ const result = {
333
+ gems: [],
334
+ source: null,
335
+ rubyVersion: null,
336
+ groups: [],
337
+ }
338
+
339
+ if (!gemfileContent) return result
340
+
341
+ // Extract source
342
+ const sourceMatch = gemfileContent.match(GEMFILE_PATTERNS.source)
343
+ if (sourceMatch) {
344
+ result.source = sourceMatch[1]
345
+ }
346
+
347
+ // Extract ruby version
348
+ const rubyMatch = gemfileContent.match(GEMFILE_PATTERNS.ruby)
349
+ if (rubyMatch) {
350
+ result.rubyVersion = rubyMatch[1]
351
+ }
352
+
353
+ // Parse line by line, tracking group context
354
+ const lines = gemfileContent.split('\n')
355
+ const groupStack = []
356
+ const seenGroups = new Set()
357
+
358
+ for (const line of lines) {
359
+ // Detect group blocks
360
+ const groupMatch = line.match(GEMFILE_PATTERNS.group)
361
+ if (groupMatch) {
362
+ // Parse group symbols: :development, :test or :development, :test
363
+ const groupSymbols = groupMatch[1].match(/:(\w+)/g)
364
+ if (groupSymbols) {
365
+ const groups = groupSymbols.map((g) => g.slice(1))
366
+ groupStack.push(groups)
367
+ for (const g of groups) seenGroups.add(g)
368
+ }
369
+ continue
370
+ }
371
+
372
+ // Detect end of group block
373
+ if (/^\s*end\b/.test(line) && groupStack.length > 0) {
374
+ groupStack.pop()
375
+ continue
376
+ }
377
+
378
+ // Parse gem declarations
379
+ const gemMatch = line.match(GEMFILE_PATTERNS.gem)
380
+ if (gemMatch) {
381
+ const name = gemMatch[1]
382
+ const version = gemMatch[2] || null
383
+
384
+ // Determine group from context or inline group option
385
+ let group = 'default'
386
+ if (groupStack.length > 0) {
387
+ group = groupStack[groupStack.length - 1].join(', ')
388
+ } else if (gemMatch[3]) {
389
+ // Check for inline group: option
390
+ const inlineGroup = gemMatch[3].match(
391
+ /group:\s*(?::(\w+)|\[([^\]]+)\])/,
392
+ )
393
+ if (inlineGroup) {
394
+ group =
395
+ inlineGroup[1] ||
396
+ inlineGroup[2]
397
+ .replace(/:/g, '')
398
+ .replace(/\s/g, '')
399
+ .split(',')
400
+ .join(', ')
401
+ }
402
+ }
403
+
404
+ result.gems.push({
405
+ name,
406
+ version,
407
+ resolved: lockVersions.get(name) || null,
408
+ category: categorizeGem(name),
409
+ group,
410
+ })
411
+ }
412
+ }
413
+
414
+ result.groups = [...seenGroups]
415
+ return result
416
+ }
417
+
418
+ /**
419
+ * Extract Gemfile data from the project.
420
+ * @param {import('../providers/interface.js').FileProvider} provider
421
+ * @returns {{ gems: Array<{name: string, version: string|null, resolved: string|null, category: string, group: string}>, source: string|null, rubyVersion: string|null, groups: string[], byCategory: Record<string, Array<{name: string, version: string|null, resolved: string|null, category: string, group: string}>> }}
422
+ */
423
+ export function extractGemfile(provider) {
424
+ const gemfileContent = provider.readFile('Gemfile')
425
+ const lockContent = provider.readFile('Gemfile.lock')
426
+
427
+ const lockVersions = parseLockVersions(lockContent)
428
+ const result = parseGemfile(gemfileContent, lockVersions)
429
+
430
+ // Build byCategory index
431
+ const byCategory = {}
432
+ for (const gem of result.gems) {
433
+ if (!byCategory[gem.category]) {
434
+ byCategory[gem.category] = []
435
+ }
436
+ byCategory[gem.category].push(gem)
437
+ }
438
+
439
+ return { ...result, byCategory }
440
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Helper Extractor (#7 — Views sub-type)
3
+ * Extracts helper module names, public methods, and controller associations.
4
+ */
5
+
6
+ import { HELPER_PATTERNS } from '../core/patterns.js'
7
+
8
+ /**
9
+ * Extract helper information from a single helper file.
10
+ * @param {import('../providers/interface.js').FileProvider} provider
11
+ * @param {string} filePath
12
+ * @returns {object|null}
13
+ */
14
+ export function extractHelper(provider, filePath) {
15
+ const content = provider.readFile(filePath)
16
+ if (!content) return null
17
+
18
+ const moduleMatch = content.match(HELPER_PATTERNS.moduleDeclaration)
19
+ if (!moduleMatch) return null
20
+
21
+ const moduleName = moduleMatch[1]
22
+
23
+ // Derive controller by convention: PostsHelper → PostsController
24
+ const controller = moduleName.endsWith('Helper')
25
+ ? moduleName.replace(/Helper$/, 'Controller')
26
+ : null
27
+
28
+ // Find where private section begins (if any)
29
+ const privateMatch = content.match(HELPER_PATTERNS.privateKeyword)
30
+ const privateIndex = privateMatch ? privateMatch.index : content.length
31
+
32
+ // Extract public methods (before private keyword)
33
+ const publicContent = content.slice(0, privateIndex)
34
+ const methods = []
35
+ const methodRe = new RegExp(HELPER_PATTERNS.methodDefinition.source, 'gm')
36
+ let m
37
+ while ((m = methodRe.exec(publicContent))) {
38
+ methods.push(m[1])
39
+ }
40
+
41
+ // Extract included helpers
42
+ const includes = []
43
+ const includeRe = new RegExp(HELPER_PATTERNS.includeHelper.source, 'g')
44
+ while ((m = includeRe.exec(content))) {
45
+ includes.push(m[1])
46
+ }
47
+
48
+ return {
49
+ module: moduleName,
50
+ file: filePath,
51
+ controller,
52
+ methods,
53
+ includes,
54
+ }
55
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Jobs Extractor (#10)
3
+ * Extracts background job metadata from app/jobs/*.rb.
4
+ */
5
+
6
+ import { JOB_PATTERNS } from '../core/patterns.js'
7
+
8
+ /**
9
+ * Extract a single job's metadata.
10
+ * @param {import('../providers/interface.js').FileProvider} provider
11
+ * @param {string} filePath
12
+ * @returns {object|null}
13
+ */
14
+ export function extractJob(provider, filePath) {
15
+ const content = provider.readFile(filePath)
16
+ if (!content) return null
17
+
18
+ const classMatch = content.match(JOB_PATTERNS.classDeclaration)
19
+ if (!classMatch) return null
20
+
21
+ const superclass = classMatch[2]
22
+ if (
23
+ !superclass.includes('Job') &&
24
+ !superclass.includes('ApplicationJob') &&
25
+ superclass !== 'ActiveJob::Base'
26
+ ) {
27
+ return null
28
+ }
29
+
30
+ const result = {
31
+ class: classMatch[1],
32
+ file: filePath,
33
+ superclass,
34
+ queue: 'default',
35
+ retry_on: [],
36
+ discard_on: [],
37
+ sidekiq_options: null,
38
+ }
39
+
40
+ // Queue
41
+ const queueMatch = content.match(JOB_PATTERNS.queueAs)
42
+ if (queueMatch) {
43
+ result.queue = queueMatch[1]
44
+ }
45
+
46
+ // Retry on
47
+ const retryRe = new RegExp(JOB_PATTERNS.retryOn.source, 'gm')
48
+ let m
49
+ while ((m = retryRe.exec(content))) {
50
+ result.retry_on.push({
51
+ exception: m[1],
52
+ options: m[2]?.trim() || null,
53
+ })
54
+ }
55
+
56
+ // Discard on
57
+ const discardRe = new RegExp(JOB_PATTERNS.discardOn.source, 'gm')
58
+ while ((m = discardRe.exec(content))) {
59
+ result.discard_on.push(m[1])
60
+ }
61
+
62
+ // Sidekiq options
63
+ const sidekiqMatch = content.match(JOB_PATTERNS.sidekiqOptions)
64
+ if (sidekiqMatch) {
65
+ result.sidekiq_options = sidekiqMatch[1].trim()
66
+ }
67
+
68
+ return result
69
+ }
70
+
71
+ /**
72
+ * Extract all jobs and detect adapter/recurring config.
73
+ * @param {import('../providers/interface.js').FileProvider} provider
74
+ * @param {Array<{path: string}>} entries
75
+ * @param {{gems?: object}} gemInfo
76
+ * @returns {object}
77
+ */
78
+ export function extractJobs(provider, entries, gemInfo = {}) {
79
+ const gems = gemInfo.gems || {}
80
+ const result = {
81
+ adapter: null,
82
+ jobs: [],
83
+ queues_detected: new Set(),
84
+ recurring_jobs: null,
85
+ }
86
+
87
+ // Detect adapter
88
+ if (gems.solid_queue) result.adapter = 'solid_queue'
89
+ else if (gems.sidekiq) result.adapter = 'sidekiq'
90
+ else if (gems.delayed_job) result.adapter = 'delayed_job'
91
+ else if (gems.resque) result.adapter = 'resque'
92
+ else if (gems.good_job) result.adapter = 'good_job'
93
+
94
+ for (const entry of entries) {
95
+ const job = extractJob(provider, entry.path)
96
+ if (job) {
97
+ result.jobs.push(job)
98
+ result.queues_detected.add(job.queue)
99
+ }
100
+ }
101
+
102
+ // Recurring jobs from config/recurring.yml (Solid Queue)
103
+ const recurringContent = provider.readFile('config/recurring.yml')
104
+ if (recurringContent) {
105
+ const jobNames = []
106
+ const classRe = /class:\s*(\w+)/g
107
+ let m
108
+ while ((m = classRe.exec(recurringContent))) {
109
+ jobNames.push(m[1])
110
+ }
111
+ if (jobNames.length > 0) {
112
+ result.recurring_jobs = {
113
+ source: 'config/recurring.yml',
114
+ jobs: jobNames,
115
+ }
116
+ }
117
+ }
118
+
119
+ result.queues_detected = [...result.queues_detected]
120
+
121
+ return result
122
+ }