From 1fae608bf74583301e56f900f300654c4aaa9438 Mon Sep 17 00:00:00 2001 From: Ghina Ajour Date: Wed, 22 Apr 2026 11:30:13 +0300 Subject: [PATCH] Detect Rails in tool-detector #25 --- .../core/src/analyzer/tool-detector.test.ts | 42 +++++++++++++++++++ packages/core/src/analyzer/tool-detector.ts | 24 +++++++++-- 2 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 packages/core/src/analyzer/tool-detector.test.ts diff --git a/packages/core/src/analyzer/tool-detector.test.ts b/packages/core/src/analyzer/tool-detector.test.ts new file mode 100644 index 0000000..f610078 --- /dev/null +++ b/packages/core/src/analyzer/tool-detector.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, rmSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; +import { detectTools } from './tool-detector.js'; + +let root: string; + +beforeEach(() => { + root = join(tmpdir(), `tool-detector-test-${Date.now()}`); + mkdirSync(root, { recursive: true }); +}); + +afterEach(() => rmSync(root, { recursive: true, force: true })); + +describe('Rails detection', () => { + it('detects via Gemfile containing rails', () => { + writeFileSync(join(root, 'Gemfile'), `gem "rails", "~> 7.1"`); + const tools = detectTools(root); + expect(tools.find(t => t.id === 'rails')).toBeDefined(); + }); + + it('does not detect rails from a non-rails Gemfile', () => { + writeFileSync(join(root, 'Gemfile'), `gem "sinatra"`); + const tools = detectTools(root); + expect(tools.find(t => t.id === 'rails')).toBeUndefined(); + }); + + it('detects via config/application.rb with Rails::Application', () => { + mkdirSync(join(root, 'config'), { recursive: true }); + writeFileSync(join(root, 'config/application.rb'), `class Application < Rails::Application\nend`); + const tools = detectTools(root); + expect(tools.find(t => t.id === 'rails')).toBeDefined(); + }); + + it('detects via bin/rails existence alone', () => { + mkdirSync(join(root, 'bin'), { recursive: true }); + writeFileSync(join(root, 'bin/rails'), '#!/usr/bin/env ruby'); + const tools = detectTools(root); + expect(tools.find(t => t.id === 'rails')).toBeDefined(); + }); +}); diff --git a/packages/core/src/analyzer/tool-detector.ts b/packages/core/src/analyzer/tool-detector.ts index 4919675..ad4afc0 100644 --- a/packages/core/src/analyzer/tool-detector.ts +++ b/packages/core/src/analyzer/tool-detector.ts @@ -36,6 +36,8 @@ interface ToolSignature { versionFrom?: 'package.json' | 'config'; /** Package name in package.json (for version extraction) */ packageName?: string; + /** If a signal file matches an entry here, its contents must match the pattern */ + fileContains?: Array<{ file: string; pattern: RegExp }>; /** Context hint template */ contextHint: string; } @@ -63,6 +65,16 @@ const TOOL_SIGNATURES: ToolSignature[] = [ contextHint: 'Supabase: PostgreSQL + Auth + Storage + Realtime, config in supabase/', }, // Frameworks + { + id: 'rails', name: 'Ruby on Rails', category: 'framework', + signals: ['Gemfile', 'config/application.rb', 'bin/rails'], + fileContains: [ + { file: 'Gemfile', pattern: /gem\s+['"]rails['"]/i }, + { file: 'config/application.rb', pattern: /Rails::Application/ }, + ], + filePatterns: ['app/controllers/**', 'app/models/**', 'app/views/**', 'config/**', 'db/**', 'Gemfile'], + contextHint: 'Ruby on Rails: MVC framework, app/ has models/views/controllers, db/ has migrations, config/routes.rb defines routes', + }, { id: 'nextjs', name: 'Next.js', category: 'framework', signals: ['next.config.js', 'next.config.mjs', 'next.config.ts'], @@ -209,10 +221,16 @@ export function detectTools(repoRoot: string, scanResult?: ScanEntry): DetectedT // Check file signals for (const signal of sig.signals) { const fullPath = join(repoRoot, signal); - if (existsSync(fullPath)) { - configPath = signal; - break; + if (!existsSync(fullPath)) continue; + const contentCheck = sig.fileContains?.find(c => c.file === signal); + if (contentCheck) { + try { + const contents = readFileSync(fullPath, 'utf-8'); + if (!contentCheck.pattern.test(contents)) continue; + } catch { continue; } } + configPath = signal; + break; } // Check package.json deps (for tools without config files, like Express)