| |
| |
| |
| |
| |
|
|
|
|
| import * as fs from 'fs'
|
| import * as path from 'path'
|
| import {execSync} from 'child_process'
|
|
|
| export interface PatchResult {
|
| success: boolean
|
| error?: string
|
| originalCode: string
|
| patchedCode: string
|
| backupPath?: string
|
| }
|
|
|
| export interface PatchRequest {
|
| filename: string
|
|
|
| functionName?: string
|
| functionStartLine?: number
|
| functionEndLine?: number
|
|
|
| lineStart?: number
|
| lineEnd?: number
|
|
|
| newCode: string
|
| language: string
|
| }
|
|
|
| |
| |
| |
| |
|
|
| export function applyPatch(
|
| workingDir: string,
|
| request: PatchRequest
|
| ): PatchResult {
|
| const absPath = path.resolve(workingDir, request.filename)
|
|
|
| if (!fs.existsSync(absPath)) {
|
| return {
|
| success: false,
|
| error: `File not found: ${absPath}`,
|
| originalCode: '',
|
| patchedCode: ''
|
| }
|
| }
|
|
|
| try {
|
| const originalCode = fs.readFileSync(absPath, 'utf8')
|
| const lines = originalCode.split('\n')
|
|
|
|
|
| if (
|
| request.functionName &&
|
| request.functionStartLine &&
|
| request.functionEndLine
|
| ) {
|
| const result = applyFunctionLevelPatch(lines, request)
|
| if (result.success) {
|
|
|
| const backupPath = `${absPath}.backup.${Date.now()}`
|
| fs.writeFileSync(backupPath, originalCode)
|
|
|
|
|
| fs.writeFileSync(absPath, result.patchedCode)
|
|
|
| return {
|
| ...result,
|
| originalCode,
|
| backupPath
|
| }
|
| }
|
|
|
| }
|
|
|
|
|
| if (request.lineStart && request.lineEnd) {
|
| const result = applyLineRangePatch(lines, request)
|
| if (result.success) {
|
| const backupPath = `${absPath}.backup.${Date.now()}`
|
| fs.writeFileSync(backupPath, originalCode)
|
| fs.writeFileSync(absPath, result.patchedCode)
|
|
|
| return {
|
| ...result,
|
| originalCode,
|
| backupPath
|
| }
|
| }
|
| return result
|
| }
|
|
|
| return {
|
| success: false,
|
| error: 'No valid patch strategy available',
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| } catch (err: any) {
|
| return {
|
| success: false,
|
| error: `Patch application failed: ${err.message}`,
|
| originalCode: '',
|
| patchedCode: ''
|
| }
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| function applyFunctionLevelPatch(
|
| lines: string[],
|
| request: PatchRequest
|
| ): PatchResult {
|
| const originalCode = lines.join('\n')
|
|
|
| if (!request.functionStartLine || !request.functionEndLine) {
|
| return {
|
| success: false,
|
| error: 'Function boundaries not specified',
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
|
|
| const startIdx = request.functionStartLine - 1
|
| const endIdx = request.functionEndLine
|
|
|
| if (startIdx < 0 || endIdx > lines.length || startIdx >= endIdx) {
|
| return {
|
| success: false,
|
| error: `Invalid function boundaries: ${request.functionStartLine}-${request.functionEndLine}`,
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
|
|
| const originalFunction = lines.slice(startIdx, endIdx).join('\n')
|
| const functionName = request.functionName || 'unknown'
|
|
|
|
|
| const firstLine = lines[startIdx].toLowerCase()
|
| const expectedName = functionName.toLowerCase()
|
|
|
|
|
| const hasFunctionPattern =
|
| firstLine.includes('function') ||
|
| firstLine.includes('=>') ||
|
| firstLine.includes('def ') ||
|
| firstLine.includes(`${expectedName}`)
|
|
|
| if (!hasFunctionPattern) {
|
| return {
|
| success: false,
|
| error: `Function "${functionName}" not found at expected location. Heuristic check failed.`,
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
|
|
| const newCode = normalizeIndentation(request.newCode, lines[startIdx])
|
|
|
|
|
| const beforeFunction = lines.slice(0, startIdx)
|
| const afterFunction = lines.slice(endIdx)
|
|
|
| const patchedLines = [...beforeFunction, newCode, ...afterFunction]
|
|
|
| const patchedCode = patchedLines.join('\n')
|
|
|
|
|
| const syntaxError = quickSyntaxCheck(patchedCode, request.language)
|
| if (syntaxError) {
|
| return {
|
| success: false,
|
| error: `Syntax check failed: ${syntaxError}`,
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
| return {
|
| success: true,
|
| originalCode,
|
| patchedCode
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| function applyLineRangePatch(
|
| lines: string[],
|
| request: PatchRequest
|
| ): PatchResult {
|
| const originalCode = lines.join('\n')
|
|
|
| if (!request.lineStart || !request.lineEnd) {
|
| return {
|
| success: false,
|
| error: 'Line range not specified',
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
|
|
| const startIdx = request.lineStart - 1
|
| const endIdx = request.lineEnd
|
|
|
| if (startIdx < 0 || endIdx > lines.length || startIdx >= endIdx) {
|
| return {
|
| success: false,
|
| error: `Invalid line range: ${request.lineStart}-${request.lineEnd}`,
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
|
|
| const baseIndent = getIndentation(lines[startIdx])
|
|
|
|
|
| const newCode = normalizeIndentation(request.newCode, lines[startIdx])
|
| const newCodeLines = newCode.split('\n')
|
|
|
|
|
| const beforeRange = lines.slice(0, startIdx)
|
| const afterRange = lines.slice(endIdx)
|
|
|
| const patchedLines = [...beforeRange, ...newCodeLines, ...afterRange]
|
|
|
| const patchedCode = patchedLines.join('\n')
|
|
|
|
|
| const syntaxError = quickSyntaxCheck(patchedCode, request.language)
|
| if (syntaxError) {
|
| return {
|
| success: false,
|
| error: `Syntax check failed: ${syntaxError}`,
|
| originalCode,
|
| patchedCode: originalCode
|
| }
|
| }
|
|
|
| return {
|
| success: true,
|
| originalCode,
|
| patchedCode
|
| }
|
| }
|
|
|
| |
| |
|
|
| function getIndentation(line: string): string {
|
| const match = line.match(/^(\s*)/)
|
| return match ? match[1] : ''
|
| }
|
|
|
| |
| |
|
|
| function normalizeIndentation(newCode: string, referenceLine: string): string {
|
| const baseIndent = getIndentation(referenceLine)
|
| const lines = newCode.split('\n')
|
|
|
|
|
| let minIndent = Infinity
|
| for (const line of lines) {
|
| if (line.trim()) {
|
| const indent = getIndentation(line).length
|
| minIndent = Math.min(minIndent, indent)
|
| }
|
| }
|
|
|
| if (minIndent === Infinity) return newCode
|
|
|
|
|
| const normalized = lines.map(line => {
|
| if (!line.trim()) return ''
|
| const currentIndent = getIndentation(line).length
|
| const relativeIndent = currentIndent - minIndent
|
| return baseIndent + ' '.repeat(relativeIndent) + line.trimStart()
|
| })
|
|
|
| return normalized.join('\n')
|
| }
|
|
|
| |
| |
| |
|
|
| export interface ValidationResult {
|
| passed: boolean
|
| command: string
|
| stdout: string
|
| stderr: string
|
| exitCode: number
|
| }
|
|
|
| export async function validatePatchSemantically(
|
| workingDir: string,
|
| filename: string,
|
| language: string
|
| ): Promise<ValidationResult> {
|
|
|
| const validationCommands: Array<{cmd: string; label: string}> = []
|
|
|
| if (language === 'typescript') {
|
|
|
| validationCommands.push(
|
| {cmd: `npx tsc --noEmit ${filename}`, label: 'TypeScript compiler'},
|
| {cmd: 'npx tsc --noEmit', label: 'Project-wide TypeScript check'}
|
| )
|
| }
|
|
|
|
|
| validationCommands.push(
|
| {cmd: `npx eslint ${filename} --max-warnings=0`, label: 'ESLint'},
|
| {cmd: 'npx eslint . --max-warnings=0', label: 'Project-wide ESLint'}
|
| )
|
|
|
|
|
| validationCommands.push({
|
| cmd: 'npm test -- --passWithNoTests --testPathPattern=' + filename,
|
| label: 'npm test'
|
| })
|
|
|
| for (const {cmd, label} of validationCommands) {
|
| try {
|
| console.log(`[VALIDATION] Running ${label}...`)
|
| const stdout = execSync(cmd, {
|
| cwd: workingDir,
|
| encoding: 'utf8',
|
| timeout: 60000,
|
| stdio: ['pipe', 'pipe', 'pipe']
|
| })
|
|
|
| console.log(`[VALIDATION] ✓ ${label} passed`)
|
| return {
|
| passed: true,
|
| command: cmd,
|
| stdout,
|
| stderr: '',
|
| exitCode: 0
|
| }
|
| } catch (err: any) {
|
|
|
| if (
|
| err.status === 127 ||
|
| err.message?.includes('not found') ||
|
| err.message?.includes('ENOENT')
|
| ) {
|
| console.log(`[VALIDATION] ${label} not available, skipping...`)
|
| continue
|
| }
|
|
|
|
|
| console.log(`[VALIDATION] ✗ ${label} failed (exit ${err.status})`)
|
| return {
|
| passed: false,
|
| command: cmd,
|
| stdout: err.stdout || '',
|
| stderr: err.stderr || '',
|
| exitCode: err.status || 1
|
| }
|
| }
|
| }
|
|
|
|
|
| console.log(`[VALIDATION] ⚠ No semantic validation tools available`)
|
| return {
|
| passed: true,
|
| command: 'none',
|
| stdout: '',
|
| stderr: 'No validation tools available',
|
| exitCode: 0
|
| }
|
| }
|
|
|
| |
| |
| |
|
|
| function quickSyntaxCheck(code: string, language: string): string | null {
|
|
|
| let braceCount = 0
|
| let parenCount = 0
|
| let bracketCount = 0
|
| let inString = false
|
| let stringChar = ''
|
|
|
| for (let i = 0; i < code.length; i++) {
|
| const char = code[i]
|
| const prevChar = i > 0 ? code[i - 1] : ''
|
|
|
|
|
| if (!inString && (char === '"' || char === "'" || char === '`')) {
|
| inString = true
|
| stringChar = char
|
| continue
|
| }
|
| if (inString && char === stringChar && prevChar !== '\\') {
|
| inString = false
|
| continue
|
| }
|
|
|
| if (inString) continue
|
|
|
|
|
| if (char === '{') braceCount++
|
| else if (char === '}') braceCount--
|
| else if (char === '(') parenCount++
|
| else if (char === ')') parenCount--
|
| else if (char === '[') bracketCount++
|
| else if (char === ']') bracketCount--
|
|
|
|
|
| if (braceCount < -2 || parenCount < -2 || bracketCount < -2) {
|
| return `Unbalanced ${braceCount < 0 ? 'braces' : parenCount < 0 ? 'parentheses' : 'brackets'}`
|
| }
|
| }
|
|
|
| if (braceCount !== 0)
|
| return `Unbalanced braces: ${braceCount > 0 ? 'missing' : 'extra'} ${Math.abs(braceCount)} }`
|
| if (parenCount !== 0)
|
| return `Unbalanced parentheses: ${parenCount > 0 ? 'missing' : 'extra'} ${Math.abs(parenCount)} )`
|
| if (bracketCount !== 0)
|
| return `Unbalanced brackets: ${bracketCount > 0 ? 'missing' : 'extra'} ${Math.abs(bracketCount)} ]`
|
|
|
|
|
| if (language === 'typescript' || language === 'javascript') {
|
|
|
| if (/\bfunction\s*\([^)]*\)\s*[^=>{]/.test(code)) {
|
| return 'Function without body'
|
| }
|
| }
|
|
|
| return null
|
| }
|
|
|
| |
| |
|
|
| export function rollbackPatch(
|
| backupPath: string,
|
| originalPath: string
|
| ): boolean {
|
| try {
|
| if (!fs.existsSync(backupPath)) {
|
| console.error(`[PATCH] Backup not found: ${backupPath}`)
|
| return false
|
| }
|
|
|
| fs.copyFileSync(backupPath, originalPath)
|
| console.log(`[PATCH] Rolled back ${originalPath} from ${backupPath}`)
|
| return true
|
| } catch (err: any) {
|
| console.error(`[PATCH] Rollback failed: ${err.message}`)
|
| return false
|
| }
|
| }
|
|
|
| |
| |
|
|
| export function cleanupOldBackups(
|
| workingDir: string,
|
| maxAgeMs: number = 24 * 60 * 60 * 1000
|
| ): void {
|
| try {
|
| const files = fs.readdirSync(workingDir)
|
| const now = Date.now()
|
|
|
| for (const file of files) {
|
| if (file.includes('.backup.')) {
|
| const backupPath = path.join(workingDir, file)
|
| const stats = fs.statSync(backupPath)
|
| const age = now - stats.mtimeMs
|
|
|
| if (age > maxAgeMs) {
|
| fs.unlinkSync(backupPath)
|
| console.log(`[PATCH] Cleaned up old backup: ${file}`)
|
| }
|
| }
|
| }
|
| } catch (err: any) {
|
| console.error(`[PATCH] Cleanup failed: ${err.message}`)
|
| }
|
| }
|
|
|