together-code
Version:
AI-powered coding assistant that plans, then builds
133 lines (110 loc) • 3.62 kB
text/typescript
import fs from 'fs-extra';
import path from 'path';
export interface FileToWrite {
path: string;
content: string;
}
export class FileWriter {
private basePath: string;
constructor(basePath: string = process.cwd()) {
this.basePath = basePath;
}
async writeFiles(files: FileToWrite[]): Promise<void> {
for (const file of files) {
await this.writeFile(file.path, file.content);
}
}
async writeFile(filePath: string, content: string): Promise<void> {
const fullPath = path.resolve(this.basePath, filePath);
const dir = path.dirname(fullPath);
// Ensure directory exists
await fs.ensureDir(dir);
// Write file
await fs.writeFile(fullPath, content, 'utf8');
}
parseCodeBlocks(aiResponse: string): FileToWrite[] {
const files: FileToWrite[] = [];
// Match code blocks with filename: prefix
const codeBlockRegex = /```(?:filename:([^\n]+)\n)?([\s\S]*?)```/g;
let match;
while ((match = codeBlockRegex.exec(aiResponse)) !== null) {
const [, filename, content] = match;
if (filename && content) {
files.push({
path: filename.trim(),
content: content.trim()
});
} else if (content && files.length === 0) {
// If no filename specified and it's the first code block, try to infer from content
const inferredName = this.inferFilename(content);
if (inferredName) {
files.push({
path: inferredName,
content: content.trim()
});
}
}
}
return files;
}
private inferFilename(content: string): string | null {
// Simple inference based on content
if (content.includes('import React') || content.includes('export default')) {
return 'component.tsx';
}
if (content.includes('function ') || content.includes('const ') || content.includes('export ')) {
return 'index.js';
}
if (content.includes('<!DOCTYPE html') || content.includes('<html')) {
return 'index.html';
}
if (content.includes('body {') || content.includes('.class')) {
return 'style.css';
}
if (content.includes('"name":') && content.includes('"version":')) {
return 'package.json';
}
return null;
}
async fileExists(filePath: string): Promise<boolean> {
const fullPath = path.resolve(this.basePath, filePath);
return fs.pathExists(fullPath);
}
async createBackup(filePath: string): Promise<void> {
const fullPath = path.resolve(this.basePath, filePath);
const backupPath = `${fullPath}.backup.${Date.now()}`;
if (await this.fileExists(filePath)) {
await fs.copy(fullPath, backupPath);
}
}
async checkExistingFiles(files: FileToWrite[]): Promise<string[]> {
const existingFiles: string[] = [];
for (const file of files) {
if (await this.fileExists(file.path)) {
existingFiles.push(file.path);
}
}
return existingFiles;
}
async getFileContent(filePath: string): Promise<string | null> {
try {
const fullPath = path.resolve(this.basePath, filePath);
if (await this.fileExists(filePath)) {
return await fs.readFile(fullPath, 'utf8');
}
return null;
} catch (error) {
return null;
}
}
async writeFilesWithBackup(files: FileToWrite[]): Promise<void> {
// Create backups for existing files
for (const file of files) {
if (await this.fileExists(file.path)) {
await this.createBackup(file.path);
}
}
// Write the files
await this.writeFiles(files);
}
}