Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat backend frontend code gen fault check #117

Draft
wants to merge 11 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export interface FileTask {
filePath: string; // e.g. "src/components/MyComponent.ts"
fileContents: string; // the code you got from the LLM
dependenciesPath?: string;
}

export class CodeTaskQueue {
private tasks: FileTask[] = [];

enqueue(task: FileTask) {
this.tasks.push(task);
}

dequeue(): FileTask | undefined {
return this.tasks.shift();
}

get size(): number {
return this.tasks.length;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { Logger } from '@nestjs/common';
import { writeFile, rename, readFile } from 'fs/promises';
import path from 'path';

export interface FileOperation {
action: 'write' | 'rename' | 'read';
originalPath?: string;
renamePath?: string;
code?: string;
}

export class FileOperationManager {
private readonly projectRoot: string;
private readonly allowedPaths: string[];
private logger = new Logger('FileOperationManager');

constructor(
projectRoot: string,
private renameMap: Map<string, string>,
) {
this.projectRoot = path.normalize(projectRoot);
this.allowedPaths = [this.projectRoot];
}

private operationCount = 0;
async executeOperations(operations: FileOperation[]): Promise<string | null> {
// if (operations.length > 5) {
// throw new Error('Maximum 5 operations per fix');
// }

let newFilePath: string | null = null;

for (const op of operations) {
try {
switch (op.action) {
case 'write':
await this.handleWrite(op);
break;
case 'rename':
await this.handleRename(op);
newFilePath = op.renamePath || null;

// add new file path
if (op.originalPath && op.renamePath) {
// **Check if originalPath was previously renamed**
const latestPath =
this.renameMap.get(op.originalPath) || op.originalPath;

// **Update mapping for the latest renamed file**
this.renameMap.set(latestPath, op.renamePath);
}
break;
case 'read':
await this.handleRead(op);
newFilePath = op.renamePath || null;
break;
}
} catch (error) {
this.logger.error(
`Failed to ${op.action} ${op.originalPath}: ${error}`,
);
throw error;
}
}

return newFilePath;
}

private async handleWrite(op: FileOperation): Promise<void> {
const originalPath = path.resolve(this.projectRoot, op.originalPath);
this.safetyChecks(originalPath);

this.logger.debug('start update file to: ' + originalPath);
await writeFile(originalPath, op.code, 'utf-8');
}

private async handleRead(op: FileOperation): Promise<string | null> {
try {
const originalPath = path.resolve(this.projectRoot, op.originalPath);
this.safetyChecks(originalPath);

this.logger.debug(`Reading file: ${originalPath}`);

// Read the file content
const fileContent = await readFile(originalPath, 'utf-8');

this.logger.debug(`File read successfully: ${originalPath}`);

return fileContent;
} catch (error) {
this.logger.error(
`Failed to read file: ${op.originalPath}, Error: ${error.message}`,
);
return null; // Return null if the file cannot be read
}
}

private async handleRename(op: FileOperation): Promise<void> {
const originalPath = path.resolve(this.projectRoot, op.originalPath);
const RenamePath = path.resolve(this.projectRoot, op.renamePath);

this.safetyChecks(originalPath);
this.safetyChecks(RenamePath);

this.logger.debug('start rename: ' + originalPath);
this.logger.debug('change to name: ' + RenamePath);
// Perform the actual rename
await rename(originalPath, RenamePath);
}

private safetyChecks(filePath: string) {
const targetPath = path.resolve(this.projectRoot, filePath); // Normalize path

// Prevent path traversal attacks
if (!targetPath.startsWith(this.projectRoot)) {
throw new Error('Unauthorized file access detected');
}

// Prevent package.json modifications
if (targetPath.includes('package.json')) {
throw new Error('Modifying package.json requires special approval');
}

// Security check
if (!this.isPathAllowed(targetPath)) {
throw new Error(`Attempted to access restricted path: ${targetPath}`);
}

// Limit write anddelete write operations
// if (path.startsWith('src/')) {
// throw new Error('Can only delete or write files in src/ directory');
// }
}

private isPathAllowed(targetPath: string): boolean {
return this.allowedPaths.some(
(allowedPath) =>
targetPath.startsWith(allowedPath) &&
!targetPath.includes('node_modules') &&
!targetPath.includes('.env'),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { FileOperation } from './FileOperationManager';

export class FixResponseParser {
private logger = console;

// parse the gpt json input
parse(json: string, filePath: string): FileOperation[] {
this.logger.log('Parsing JSON:', json);

let parsedData;
try {
parsedData = JSON.parse(json);
} catch (error) {
this.logger.error('Error parsing JSON:', error);
throw new Error('Invalid JSON format');
}

if (!parsedData.fix || !parsedData.fix.operations) {
throw new Error("Invalid JSON structure: Missing 'fix.operations'");
}

const operations: FileOperation[] = parsedData.fix.operations
.map((op: any) => {
if (op.type === 'WRITE') {
return {
action: 'write',
originalPath: filePath,
code: parsedData.fix.generate?.trim(),
};
} else if (op.type === 'RENAME') {
return {
action: 'rename',
originalPath: op.original_path,
renamePath: op.path,
code: parsedData.fix.generate?.trim(),
};
} else if (op.type === 'READ') {
return {
action: 'read',
originalPath: op.original_path,
};
}
return null;
})
.filter(Boolean);

// this.logger.log('Extracted operations:', operations);
return operations;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { spawn } from 'child_process';
import { Logger } from '@nestjs/common';

export interface ValidationResult {
success: boolean;
error?: string;
}

/**
* FrontendCodeValidator is responsible for checking the correctness of the generated frontend code.
* It runs an npm build command (or any custom build script) in the given project directory (frontendPath)
* and captures any errors produced during the build process.
*/
export class FrontendCodeValidator {
private readonly logger = new Logger('FrontendCodeValidator');

/**
* @param frontendPath - The absolute path to the generated frontend project.
*/
constructor(private readonly frontendPath: string) {}

/**
* Runs the build command (npm run build) inside the frontend project directory.
* This method returns a promise that resolves with a ValidationResult, indicating whether
* the build succeeded or failed along with any error messages.
*
* @returns A promise that resolves with the ValidationResult.
*/
public async validate(): Promise<ValidationResult> {
return new Promise<ValidationResult>((resolve, reject) => {
this.logger.log('Starting frontend code validation...');
// Spawn the npm build process in the provided frontend project path.
const npmProcess = spawn('npm', ['run', 'build'], {
cwd: this.frontendPath,
shell: true,
});

this.logger.log('Running npm build command in', this.frontendPath);
let stdoutBuffer = '';
let stderrBuffer = '';

npmProcess.stdout.on('data', (data: Buffer) => {
const output = data.toString();
stdoutBuffer += output;
// this.logger.log(output);
});

npmProcess.stderr.on('data', (data: Buffer) => {
const output = data.toString();
stderrBuffer += output;
// this.logger.log(output);
});

npmProcess.on('close', (code: number) => {
if (code !== 0) {
// Build failed – use stderr if available, else fallback to stdout.
const errorMessage = stderrBuffer || stdoutBuffer;
// this.logger.error(
// `Build process exited with code ${code}. Error: ${errorMessage}`,
// );

this.logger.error(`Build process exited with code ${code}.`);
resolve({
success: false,
error: errorMessage,
});
} else {
// Build succeeded
this.logger.log('Build process completed successfully.');
resolve({
success: true,
});
}
});

npmProcess.on('error', (err: Error) => {
this.logger.error('Failed to run npm build command:', err);
reject(err);
});
});
}

public async installDependencies(): Promise<ValidationResult> {
return new Promise<ValidationResult>((resolve, reject) => {
this.logger.log('Starting npm install in', this.frontendPath);

const npmInstall = spawn('npm', ['install'], {
cwd: this.frontendPath,
});

let stdoutBuffer = '';
let stderrBuffer = '';

npmInstall.stdout.on('data', (data: Buffer) => {
stdoutBuffer += data.toString();
});

npmInstall.stderr.on('data', (data: Buffer) => {
stderrBuffer += data.toString();
});

npmInstall.on('close', (code: number) => {
if (code !== 0) {
const errorMessage = stderrBuffer || stdoutBuffer;
this.logger.error(`npm install exited with code ${code}.`);
resolve({
success: false,
error: errorMessage,
});
} else {
this.logger.log('npm install completed successfully.');
resolve({ success: true });
}
});

npmInstall.on('error', (err: Error) => {
this.logger.error('Failed to run npm install command:', err);
reject(err);
});
});
}
}
Loading
Loading