const fs = require('fs').promises; const fsSync = require('fs'); const path = require('path'); const { spawn } = require('child_process'); const { extractAllTags } = require('./tag-parser'); /** * Response Processor - Executes dyad-style tags * Handles file operations, package installation, and commands */ class ResponseProcessor { constructor(vaultPath) { this.vaultPath = vaultPath; } /** * Process all tags from a response * Executes operations in order: delete → rename → write → dependencies → commands */ async processResponse(sessionId, response, options = {}) { const { workingDir = this.vaultPath, autoApprove = false, onProgress = null } = options; const tags = extractAllTags(response); // Track operations const results = { sessionId, workingDir, operations: [], errors: [], timestamp: new Date().toISOString() }; // Execute in order: deletes first, then renames, then writes for (const tag of tags.deletes) { try { if (onProgress) onProgress({ type: 'delete', path: tag.path }); const result = await this.executeDelete(workingDir, tag); results.operations.push(result); } catch (error) { results.errors.push({ type: 'delete', tag, error: error.message }); } } for (const tag of tags.renames) { try { if (onProgress) onProgress({ type: 'rename', from: tag.from, to: tag.to }); const result = await this.executeRename(workingDir, tag); results.operations.push(result); } catch (error) { results.errors.push({ type: 'rename', tag, error: error.message }); } } for (const tag of tags.writes) { try { if (onProgress) onProgress({ type: 'write', path: tag.path }); const result = await this.executeWrite(workingDir, tag); results.operations.push(result); } catch (error) { results.errors.push({ type: 'write', tag, error: error.message }); } } for (const tag of tags.dependencies) { try { if (onProgress) onProgress({ type: 'install', packages: tag.packages }); const result = await this.executeAddDependency(workingDir, tag); results.operations.push(result); } catch (error) { results.errors.push({ type: 'install', tag, error: error.message }); } } for (const tag of tags.commands) { try { if (onProgress) onProgress({ type: 'command', command: tag.command }); const result = await this.executeCommand(workingDir, tag); results.operations.push(result); } catch (error) { results.errors.push({ type: 'command', tag, error: error.message }); } } return results; } /** * Execute dyad-write tag */ async executeWrite(workingDir, tag) { const fullPath = path.join(workingDir, tag.path); // Security check if (!fullPath.startsWith(workingDir)) { throw new Error('Access denied: path outside working directory'); } // Create directory if needed const dir = path.dirname(fullPath); if (!fsSync.existsSync(dir)) { await fs.mkdir(dir, { recursive: true }); } // Write file await fs.writeFile(fullPath, tag.content, 'utf-8'); return { type: 'write', path: tag.path, fullPath, success: true }; } /** * Execute dyad-rename tag */ async executeRename(workingDir, tag) { const fromPath = path.join(workingDir, tag.from); const toPath = path.join(workingDir, tag.to); // Security check if (!fromPath.startsWith(workingDir) || !toPath.startsWith(workingDir)) { throw new Error('Access denied: path outside working directory'); } // Check if source exists if (!fsSync.existsSync(fromPath)) { throw new Error(`Source file not found: ${tag.from}`); } // Create target directory if needed const toDir = path.dirname(toPath); if (!fsSync.existsSync(toDir)) { await fs.mkdir(toDir, { recursive: true }); } // Rename/move file await fs.rename(fromPath, toPath); return { type: 'rename', from: tag.from, to: tag.to, fromPath, toPath, success: true }; } /** * Execute dyad-delete tag */ async executeDelete(workingDir, tag) { const fullPath = path.join(workingDir, tag.path); // Security check if (!fullPath.startsWith(workingDir)) { throw new Error('Access denied: path outside working directory'); } // Check if exists if (!fsSync.existsSync(fullPath)) { throw new Error(`File not found: ${tag.path}`); } // Delete file or directory const stat = await fs.stat(fullPath); if (stat.isDirectory()) { await fs.rm(fullPath, { recursive: true, force: true }); } else { await fs.unlink(fullPath); } return { type: 'delete', path: tag.path, fullPath, success: true }; } /** * Execute dyad-add-dependency tag */ async executeAddDependency(workingDir, tag) { const packageJsonPath = path.join(workingDir, 'package.json'); // Check if package.json exists if (!fsSync.existsSync(packageJsonPath)) { throw new Error('No package.json found in working directory'); } // Install packages using npm return new Promise((resolve, reject) => { const packages = tag.packages.join(' '); const npm = spawn('npm', ['install', '--save', ...tag.packages], { cwd: workingDir, stdio: ['ignore', 'pipe', 'pipe'], shell: true }); let output = ''; let errorOutput = ''; npm.stdout.on('data', (data) => { output += data.toString(); }); npm.stderr.on('data', (data) => { errorOutput += data.toString(); }); npm.on('close', (code) => { if (code === 0) { resolve({ type: 'install', packages: tag.packages, output, success: true }); } else { reject(new Error(`npm install failed: ${errorOutput}`)); } }); npm.on('error', (error) => { reject(new Error(`Failed to spawn npm: ${error.message}`)); }); }); } /** * Execute dyad-command tag * Currently supports: rebuild, restart, refresh */ async executeCommand(workingDir, tag) { const commands = { rebuild: async () => { // Rebuild logic - could be project-specific return { type: 'command', command: tag.command, result: 'Rebuild triggered', success: true }; }, restart: async () => { // Restart dev server logic return { type: 'command', command: tag.command, result: 'Restart triggered', success: true }; }, refresh: async () => { // Refresh preview logic return { type: 'command', command: tag.command, result: 'Refresh triggered', success: true }; } }; const handler = commands[tag.command]; if (!handler) { throw new Error(`Unknown command: ${tag.command}`); } return await handler(); } /** * Preview operations without executing */ async previewOperations(response, workingDir = this.vaultPath) { const tags = extractAllTags(response); const { generateOperationSummary } = require('./tag-parser'); return { workingDir, operations: generateOperationSummary(tags), hasChanges: tags.writes.length > 0 || tags.renames.length > 0 || tags.deletes.length > 0, timestamp: new Date().toISOString() }; } } module.exports = ResponseProcessor;