@versatil/sdlc-framework
Version:
🚀 AI-Native SDLC framework with 11-MCP ecosystem, RAG memory, OPERA orchestration, and 6 specialized agents achieving ZERO CONTEXT LOSS. Features complete CI/CD pipeline with 7 GitHub workflows (MCP testing, security scanning, performance benchmarking),
263 lines (223 loc) • 7.62 kB
text/typescript
/**
* VERSATIL Framework v3.0.0 - Ruby Language Adapter
*
* Enables VERSATIL to work with Ruby projects, supporting Bundler, RSpec,
* RuboCop, and the entire Ruby ecosystem.
*
* OPERA agents can now orchestrate Ruby development workflows.
*/
import { exec } from 'child_process';
import { promisify } from 'util';
import { existsSync, readFileSync } from 'fs';
import { join } from 'path';
import {
BaseLanguageAdapter,
ProjectStructure,
LanguageCapabilities,
TestResult,
BuildResult,
LintResult
} from './base-language-adapter.js';
const execAsync = promisify(exec);
export class RubyAdapter extends BaseLanguageAdapter {
private rubyVersion?: string;
private gemName?: string;
async detect(): Promise<boolean> {
const indicators = ['Gemfile', 'Rakefile', '*.gemspec', '.ruby-version'];
for (const file of indicators) {
if (existsSync(join(this.rootPath, file))) {
return true;
}
}
try {
const { stdout } = await execAsync(`find . -name "*.rb" | head -1`, {
cwd: this.rootPath
});
if (stdout.trim()) return true;
} catch {}
return false;
}
getCapabilities(): LanguageCapabilities {
return {
testing: true,
linting: true,
formatting: true,
typeChecking: false,
packageManagement: true,
buildSystem: true
};
}
async analyzeProject(): Promise<ProjectStructure> {
try {
const { stdout } = await execAsync('ruby --version');
this.rubyVersion = stdout.trim();
} catch {
this.rubyVersion = 'Unknown';
}
const mainFiles = await this.findRubyFiles(['lib/**/*.rb', 'app/**/*.rb']);
const testFiles = await this.findRubyFiles(['spec/**/*.rb', 'test/**/*.rb']);
const configFiles: string[] = [];
for (const config of ['Gemfile', 'Gemfile.lock', 'Rakefile', '.rubocop.yml', '.rspec']) {
if (existsSync(join(this.rootPath, config))) configFiles.push(config);
}
return {
rootPath: this.rootPath,
language: 'ruby',
packageManager: 'bundler',
mainFiles,
testFiles,
configFiles,
buildOutput: 'pkg/',
dependencies: await this.parseDependencies()
};
}
async runTests(options?: any): Promise<TestResult> {
try {
const command = 'bundle exec rspec --format json';
const { stdout } = await execAsync(command, { cwd: this.rootPath });
const result = JSON.parse(stdout);
const examples = result.examples || [];
return {
passed: examples.filter((e: any) => e.status === 'passed').length,
failed: examples.filter((e: any) => e.status === 'failed').length,
skipped: examples.filter((e: any) => e.status === 'pending').length,
coverage: undefined,
duration: result.summary?.duration || 0,
details: []
};
} catch (error: any) {
return {
passed: 0,
failed: 1,
skipped: 0,
duration: 0,
details: [{ name: 'test execution', status: 'failed', duration: 0, error: error.message }]
};
}
}
async build(options?: any): Promise<BuildResult> {
const startTime = Date.now();
try {
const { stdout } = await execAsync('gem build', { cwd: this.rootPath });
const artifacts: string[] = [];
try {
const { stdout: files } = await execAsync('ls pkg/');
artifacts.push(...files.split('\n').filter(f => f.endsWith('.gem')).map(f => `pkg/${f}`));
} catch {}
return {
success: true,
output: stdout,
errors: [],
warnings: [],
artifacts,
duration: Date.now() - startTime
};
} catch (error: any) {
return {
success: false,
output: error.stdout || '',
errors: [error.message],
warnings: [],
artifacts: [],
duration: Date.now() - startTime
};
}
}
async lint(options?: any): Promise<LintResult> {
try {
const command = 'bundle exec rubocop --format json';
const { stdout } = await execAsync(command, { cwd: this.rootPath });
const result = JSON.parse(stdout);
const files = result.files || [];
const issues = files.flatMap((file: any) =>
(file.offenses || []).map((offense: any) => ({
file: file.path,
line: offense.location.line,
column: offense.location.column,
severity: offense.severity === 'error' ? 'error' : 'warning',
message: offense.message,
rule: offense.cop_name
}))
);
return {
errors: issues.filter(i => i.severity === 'error').length,
warnings: issues.filter(i => i.severity === 'warning').length,
issues
};
} catch (error: any) {
return { errors: 0, warnings: 0, issues: [] };
}
}
async format(options?: any): Promise<{ formatted: number; errors: string[] }> {
try {
const checkArg = options?.check ? '--dry-run' : '--autocorrect-all';
await execAsync(`bundle exec rubocop ${checkArg}`, { cwd: this.rootPath });
return { formatted: 1, errors: [] };
} catch (error: any) {
return { formatted: 0, errors: [error.message] };
}
}
async installDependencies(): Promise<{ success: boolean; installed: string[]; errors: string[] }> {
try {
const { stdout } = await execAsync('bundle install', { cwd: this.rootPath });
const installed = await this.parseDependencyNames();
return { success: true, installed, errors: [] };
} catch (error: any) {
return { success: false, installed: [], errors: [error.message] };
}
}
getRecommendedAgents(): string[] {
return ['maria-qa', 'marcus-backend', 'james-frontend', 'devops-dan', 'security-sam'];
}
async getQualityMetrics(): Promise<any> {
let testCoverage = 0;
let lintScore = 100;
try {
const lintResult = await this.lint();
lintScore = Math.max(0, 100 - (lintResult.errors + lintResult.warnings) * 5);
} catch {}
return {
testCoverage,
lintScore,
complexityScore: 75,
maintainability: (testCoverage + lintScore + 75) / 3
};
}
async executeCommand(command: string, args?: string[]): Promise<any> {
const fullCommand = args ? `${command} ${args.join(' ')}` : command;
try {
const { stdout, stderr } = await execAsync(fullCommand, { cwd: this.rootPath });
return { exitCode: 0, stdout, stderr };
} catch (error: any) {
return {
exitCode: error.code || 1,
stdout: error.stdout || '',
stderr: error.stderr || error.message
};
}
}
private async findRubyFiles(patterns: string[]): Promise<string[]> {
const files: string[] = [];
try {
const { stdout } = await execAsync(`find . -name "*.rb" -type f`, { cwd: this.rootPath });
files.push(...stdout.split('\n').filter(f => f.trim() && f.endsWith('.rb')));
} catch {}
return Array.from(new Set(files));
}
private async parseDependencies(): Promise<Record<string, string>> {
const deps: Record<string, string> = {};
const gemfilePath = join(this.rootPath, 'Gemfile');
if (!existsSync(gemfilePath)) return deps;
const content = readFileSync(gemfilePath, 'utf8');
const gemPattern = /gem\s+['"]([^'"]+)['"],\s*['"]~>\s*([^'"]+)['"]/g;
let match;
while ((match = gemPattern.exec(content)) !== null) {
deps[match[1]] = match[2];
}
return deps;
}
private async parseDependencyNames(): Promise<string[]> {
const deps = await this.parseDependencies();
return Object.keys(deps);
}
}