UNPKG

@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),

658 lines (582 loc) • 19.3 kB
/** * VERSATIL Framework v3.0.0 - Java Language Adapter * * Enables VERSATIL to work with Java projects, supporting Maven, Gradle, JUnit, * Checkstyle, SpotBugs, and the entire Java ecosystem. * * OPERA agents can now orchestrate Java 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 JavaAdapter extends BaseLanguageAdapter { private buildTool: 'maven' | 'gradle' | null = null; private javaVersion?: string; /** * Detect if this is a Java project */ async detect(): Promise<boolean> { const indicators = [ 'pom.xml', // Maven 'build.gradle', // Gradle 'build.gradle.kts', // Gradle Kotlin DSL 'gradlew', 'mvnw' ]; // Check for Java project indicators for (const file of indicators) { if (existsSync(join(this.rootPath, file))) { return true; } } // Check for Java source files const javaDirs = ['src/main/java', 'src/test/java', 'src/']; for (const dir of javaDirs) { const dirPath = join(this.rootPath, dir); if (existsSync(dirPath)) { try { const { stdout } = await execAsync(`find "${dirPath}" -name "*.java" | head -1`, { cwd: this.rootPath }); if (stdout.trim()) { return true; } } catch { continue; } } } return false; } /** * Get Java-specific capabilities */ getCapabilities(): LanguageCapabilities { return { testing: true, // JUnit, TestNG linting: true, // Checkstyle, SpotBugs formatting: true, // google-java-format typeChecking: true, // Native Java type checking packageManagement: true, // Maven, Gradle buildSystem: true // Maven, Gradle }; } /** * Analyze Java project structure */ async analyzeProject(): Promise<ProjectStructure> { // Detect build tool await this.detectBuildTool(); // Get Java version try { const { stdout } = await execAsync('java -version 2>&1'); this.javaVersion = stdout.split('\n')[0]; } catch { this.javaVersion = 'Unknown'; } // Find Java source files const mainFiles = await this.findJavaFiles([ 'src/main/java/**/*.java', 'src/**/*.java' ]); // Find test files const testFiles = await this.findJavaFiles([ 'src/test/java/**/*.java', 'test/**/*.java', '**/Test*.java', '**/*Test.java' ]); // Find config files const configFiles: string[] = []; const configs = [ 'pom.xml', 'build.gradle', 'build.gradle.kts', 'settings.gradle', 'settings.gradle.kts', 'gradle.properties', 'checkstyle.xml', 'spotbugs.xml', 'junit-platform.properties' ]; for (const config of configs) { if (existsSync(join(this.rootPath, config))) { configFiles.push(config); } } // Parse dependencies const dependencies = await this.parseDependencies(); return { rootPath: this.rootPath, language: 'java', packageManager: this.buildTool || undefined, mainFiles: mainFiles.filter(f => !f.includes('/test/') && !f.includes('Test.java')), testFiles, configFiles, buildOutput: this.buildTool === 'maven' ? 'target/' : 'build/', dependencies }; } /** * Run Java tests using JUnit */ async runTests(options?: { pattern?: string; coverage?: boolean; watch?: boolean; }): Promise<TestResult> { try { let command = ''; const coverageArg = options?.coverage ? 'jacoco:report' : ''; if (this.buildTool === 'maven') { command = `mvn test ${coverageArg}`; } else if (this.buildTool === 'gradle') { command = options?.coverage ? 'gradle test jacocoTestReport' : 'gradle test'; } else { throw new Error('No build tool detected'); } const { stdout, stderr } = await execAsync(command, { cwd: this.rootPath }); // Parse test results let passed = 0; let failed = 0; let skipped = 0; if (this.buildTool === 'maven') { // Parse Maven test output const passedMatch = stdout.match(/Tests run: (\d+), Failures: (\d+), Errors: (\d+), Skipped: (\d+)/); if (passedMatch) { const total = parseInt(passedMatch[1]); const failures = parseInt(passedMatch[2]); const errors = parseInt(passedMatch[3]); skipped = parseInt(passedMatch[4]); failed = failures + errors; passed = total - failed - skipped; } } else { // Parse Gradle test output const resultMatch = stdout.match(/(\d+) tests? completed, (\d+) failed, (\d+) skipped/); if (resultMatch) { const total = parseInt(resultMatch[1]); failed = parseInt(resultMatch[2]); skipped = parseInt(resultMatch[3]); passed = total - failed - skipped; } } // Get coverage if requested let coverage: number | undefined; if (options?.coverage) { try { const coverageFile = this.buildTool === 'maven' ? join(this.rootPath, 'target/site/jacoco/index.html') : join(this.rootPath, 'build/reports/jacoco/test/html/index.html'); if (existsSync(coverageFile)) { const content = readFileSync(coverageFile, 'utf8'); const coverageMatch = content.match(/Total.*?(\d+)%/); coverage = coverageMatch ? parseInt(coverageMatch[1]) : undefined; } } catch { coverage = undefined; } } return { passed, failed, skipped, coverage, duration: 0, details: [] }; } catch (error: any) { // Parse error output return { passed: 0, failed: 1, skipped: 0, duration: 0, details: [{ name: 'test execution', status: 'failed', duration: 0, error: error.message }] }; } } /** * Build Java project using Maven or Gradle */ async build(options?: { mode?: 'development' | 'production'; target?: string; optimization?: boolean; }): Promise<BuildResult> { const startTime = Date.now(); const errors: string[] = []; const warnings: string[] = []; const artifacts: string[] = []; try { let buildCommand = ''; if (this.buildTool === 'maven') { const skipTests = options?.mode === 'production' ? '' : '-DskipTests'; buildCommand = `mvn clean package ${skipTests}`; } else if (this.buildTool === 'gradle') { const buildType = options?.mode === 'production' ? 'assembleRelease' : 'assemble'; buildCommand = `gradle clean ${buildType}`; } else { throw new Error('No build tool detected'); } const { stdout, stderr } = await execAsync(buildCommand, { cwd: this.rootPath }); // Parse warnings const warningPattern = /\[WARNING\]|warning:/gi; if (warningPattern.test(stderr)) { warnings.push('Build completed with warnings'); } // Check for artifacts const artifactDir = this.buildTool === 'maven' ? 'target' : 'build/libs'; const artifactPath = join(this.rootPath, artifactDir); if (existsSync(artifactPath)) { try { const { stdout: files } = await execAsync(`ls "${artifactPath}"`, { cwd: this.rootPath }); const jars = files.split('\n').filter(f => f.endsWith('.jar') || f.endsWith('.war')); artifacts.push(...jars.map(j => `${artifactDir}/${j}`)); } catch { // No artifacts found } } return { success: true, output: stdout, errors, warnings, artifacts, duration: Date.now() - startTime }; } catch (error: any) { // Parse error output const errorPattern = /\[ERROR\]|error:/gi; if (errorPattern.test(error.stderr || '')) { errors.push('Build failed with errors'); } return { success: false, output: error.stdout || '', errors: [...errors, error.message], warnings, artifacts, duration: Date.now() - startTime }; } } /** * Lint Java code using Checkstyle and SpotBugs */ async lint(options?: { fix?: boolean; files?: string[]; }): Promise<LintResult> { try { let command = ''; if (this.buildTool === 'maven') { command = 'mvn checkstyle:check spotbugs:check'; } else if (this.buildTool === 'gradle') { command = 'gradle checkstyleMain checkstyleTest spotbugsMain spotbugsTest'; } else { throw new Error('No build tool detected'); } const { stdout, stderr } = await execAsync(command, { cwd: this.rootPath }); // Parse checkstyle/spotbugs output const issues: any[] = []; let errors = 0; let warnings = 0; // Try to parse XML reports if available const checkstyleReport = join(this.rootPath, this.buildTool === 'maven' ? 'target/checkstyle-result.xml' : 'build/reports/checkstyle/main.xml' ); if (existsSync(checkstyleReport)) { try { const content = readFileSync(checkstyleReport, 'utf8'); const errorPattern = /<error[^>]+severity="error"[^>]+message="([^"]+)"/g; const warningPattern = /<error[^>]+severity="warning"[^>]+message="([^"]+)"/g; let match; while ((match = errorPattern.exec(content)) !== null) { errors++; issues.push({ file: 'unknown', line: 0, column: 0, severity: 'error' as const, message: match[1], rule: 'checkstyle' }); } while ((match = warningPattern.exec(content)) !== null) { warnings++; issues.push({ file: 'unknown', line: 0, column: 0, severity: 'warning' as const, message: match[1], rule: 'checkstyle' }); } } catch { // Could not parse report } } return { errors, warnings, issues }; } catch (error: any) { // Linting failed, return minimal info return { errors: 1, warnings: 0, issues: [{ file: 'unknown', line: 0, column: 0, severity: 'error' as const, message: error.message, rule: 'lint' }] }; } } /** * Format Java code using google-java-format */ async format(options?: { files?: string[]; check?: boolean; }): Promise<{ formatted: number; errors: string[] }> { try { // Check if google-java-format is available const files = options?.files?.join(' ') || 'src/**/*.java'; const checkArg = options?.check ? '--dry-run' : '-i'; try { await execAsync(`google-java-format ${checkArg} ${files}`, { cwd: this.rootPath }); return { formatted: 1, errors: [] }; } catch { // google-java-format not available, try Maven/Gradle plugins if (this.buildTool === 'maven') { await execAsync('mvn spotless:apply', { cwd: this.rootPath }); } else if (this.buildTool === 'gradle') { await execAsync('gradle spotlessApply', { cwd: this.rootPath }); } return { formatted: 1, errors: [] }; } } catch (error: any) { return { formatted: 0, errors: [error.message] }; } } /** * Install Java dependencies */ async installDependencies(): Promise<{ success: boolean; installed: string[]; errors: string[]; }> { try { let command = ''; if (this.buildTool === 'maven') { command = 'mvn dependency:resolve'; } else if (this.buildTool === 'gradle') { command = 'gradle dependencies'; } else { throw new Error('No build tool detected'); } const { stdout } = await execAsync(command, { cwd: this.rootPath }); // Parse installed dependencies const installed = await this.parseDependencyNames(); return { success: true, installed, errors: [] }; } catch (error: any) { return { success: false, installed: [], errors: [error.message] }; } } /** * Get recommended OPERA agents for Java projects */ getRecommendedAgents(): string[] { return [ 'maria-qa', // Testing with JUnit 'marcus-backend', // Backend development (Spring Boot, etc.) 'architecture-dan', // Enterprise architecture 'devops-dan', // Deployment (Docker, Kubernetes) 'security-sam' // Security (OWASP, SpotBugs) ]; } /** * Get Java-specific quality metrics */ async getQualityMetrics(): Promise<{ testCoverage: number; lintScore: number; complexityScore: number; maintainability: number; }> { // Run coverage let testCoverage = 0; try { const coverageFile = this.buildTool === 'maven' ? join(this.rootPath, 'target/site/jacoco/index.html') : join(this.rootPath, 'build/reports/jacoco/test/html/index.html'); if (existsSync(coverageFile)) { const content = readFileSync(coverageFile, 'utf8'); const coverageMatch = content.match(/Total.*?(\d+)%/); testCoverage = coverageMatch ? parseInt(coverageMatch[1]) : 0; } } catch { testCoverage = 0; } // Run lint for score let lintScore = 100; try { const lintResult = await this.lint(); const totalIssues = lintResult.errors + lintResult.warnings; lintScore = Math.max(0, 100 - (totalIssues * 5)); // Deduct 5 points per issue } catch { lintScore = 100; } // Run PMD for cyclomatic complexity (alternative: checkstyle) let complexityScore = 80; try { // PMD can report cyclomatic complexity const { stdout: pmdOutput } = await execAsync( 'pmd check -d . -f text -R category/java/design.xml/CyclomaticComplexity', { cwd: this.rootPath } ); // Count violations by severity const violations = pmdOutput.split('\n').filter(line => line.includes('CyclomaticComplexity')); const violationCount = violations.length; // Convert to score: 0 violations=100, 1-5=85, 6-10=70, 11-20=55, 20+=40 if (violationCount === 0) complexityScore = 100; else if (violationCount <= 5) complexityScore = 85; else if (violationCount <= 10) complexityScore = 70; else if (violationCount <= 20) complexityScore = 55; else complexityScore = 40; } catch { // PMD not installed or failed - try checkstyle as fallback try { const { stdout: checkstyleOutput } = await execAsync( 'checkstyle -c /google_checks.xml .', { cwd: this.rootPath } ); // Parse checkstyle complexity warnings const complexityWarnings = checkstyleOutput.split('\n').filter(line => line.includes('CyclomaticComplexity') || line.includes('complexity') ); const warningCount = complexityWarnings.length; if (warningCount === 0) complexityScore = 100; else if (warningCount <= 5) complexityScore = 85; else if (warningCount <= 10) complexityScore = 70; else if (warningCount <= 20) complexityScore = 55; else complexityScore = 40; } catch { // Neither PMD nor checkstyle available - use default complexityScore = 80; } } return { testCoverage, lintScore, complexityScore, maintainability: (testCoverage + lintScore + complexityScore) / 3 }; } /** * Execute Java-specific command */ async executeCommand(command: string, args?: string[]): Promise<{ exitCode: number; stdout: string; stderr: string; }> { 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 HELPER METHODS // ============================================================================ private async detectBuildTool(): Promise<void> { if (existsSync(join(this.rootPath, 'pom.xml'))) { this.buildTool = 'maven'; } else if (existsSync(join(this.rootPath, 'build.gradle')) || existsSync(join(this.rootPath, 'build.gradle.kts'))) { this.buildTool = 'gradle'; } else { this.buildTool = null; } } private async findJavaFiles(patterns: string[]): Promise<string[]> { const files: string[] = []; for (const pattern of patterns) { try { const { stdout } = await execAsync(`find . -name "*.java" -type f`, { cwd: this.rootPath }); files.push(...stdout.split('\n').filter(f => f.trim() && f.endsWith('.java'))); } catch { // Pattern not found, continue } } return Array.from(new Set(files)); // Remove duplicates } private async parseDependencies(): Promise<Record<string, string>> { const deps: Record<string, string> = {}; if (this.buildTool === 'maven') { const pomPath = join(this.rootPath, 'pom.xml'); if (existsSync(pomPath)) { const content = readFileSync(pomPath, 'utf8'); const depPattern = /<dependency>[\s\S]*?<groupId>([^<]+)<\/groupId>[\s\S]*?<artifactId>([^<]+)<\/artifactId>[\s\S]*?<version>([^<]+)<\/version>[\s\S]*?<\/dependency>/g; let match; while ((match = depPattern.exec(content)) !== null) { deps[`${match[1]}:${match[2]}`] = match[3]; } } } else if (this.buildTool === 'gradle') { const buildFile = existsSync(join(this.rootPath, 'build.gradle')) ? join(this.rootPath, 'build.gradle') : join(this.rootPath, 'build.gradle.kts'); if (existsSync(buildFile)) { const content = readFileSync(buildFile, 'utf8'); const depPattern = /implementation\s+['"]([^'"]+)['"]/g; let match; while ((match = depPattern.exec(content)) !== null) { const parts = match[1].split(':'); if (parts.length >= 3) { deps[`${parts[0]}:${parts[1]}`] = parts[2]; } } } } return deps; } private async parseDependencyNames(): Promise<string[]> { const deps = await this.parseDependencies(); return Object.keys(deps); } }