Initial commit: Obsidian Web Interface for Claude Code
- 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>
This commit is contained in:
298
services/response-processor.js
Normal file
298
services/response-processor.js
Normal file
@@ -0,0 +1,298 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user