- Full IDE with terminal integration using xterm.js - Session management with local and web sessions - HTML preview functionality - Multi-terminal support with session picker Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
299 lines
7.9 KiB
JavaScript
299 lines
7.9 KiB
JavaScript
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;
|