UNPKG

woaru

Version:

Universal Project Setup Autopilot - Analyze and automatically configure development tools for ANY programming language

205 lines 7.25 kB
import * as path from 'path'; export class BaseAction { async runCommand(command, args, cwd, options = {}) { const { spawn } = await import('child_process'); const { timeout = 30000 } = options; return new Promise((resolve, reject) => { // Validate command to prevent injection if (!this.isValidCommand(command)) { reject(new Error(`Invalid command: ${command}`)); return; } // Sanitize arguments const sanitizedArgs = args.map(arg => this.sanitizeArgument(arg)); const child = spawn(command, sanitizedArgs, { cwd: this.sanitizePath(cwd), stdio: 'pipe', shell: false, // Disable shell to prevent injection env: { ...process.env, ...options.env }, }); let output = ''; let errorOutput = ''; let timeoutId = null; // Set up timeout if (timeout > 0) { timeoutId = setTimeout(() => { child.kill('SIGTERM'); reject(new Error(`Command timed out after ${timeout}ms`)); }, timeout); } child.stdout?.on('data', data => { output += data.toString(); }); child.stderr?.on('data', data => { errorOutput += data.toString(); }); child.on('error', error => { if (timeoutId) clearTimeout(timeoutId); reject(new Error(`Command execution failed: ${error.message}`)); }); child.on('close', code => { if (timeoutId) clearTimeout(timeoutId); resolve({ success: code === 0, output, error: errorOutput || undefined, }); }); }); } // Legacy method for backward compatibility - use with caution async runCommandLegacy(command, cwd) { console.warn('runCommandLegacy is deprecated and unsafe. Use runCommand with explicit args instead.'); // Parse command safely const parts = this.parseCommand(command); if (parts.length === 0) { throw new Error('Invalid command'); } const [cmd, ...args] = parts; const result = await this.runCommand(cmd, args, cwd); return { success: result.success, output: result.output + (result.error || ''), }; } isValidCommand(command) { // Whitelist of allowed commands const allowedCommands = [ 'npm', 'yarn', 'pnpm', 'node', 'npx', 'git', 'cargo', 'rustc', 'python', 'pip', 'go', 'dotnet', 'java', 'javac', 'mvn', 'eslint', 'prettier', 'tsc', 'jest', ]; return allowedCommands.includes(command.toLowerCase()); } sanitizeArgument(arg) { // Remove potentially dangerous characters return arg.replace(/[;&|`$(){}[\]<>]/g, ''); } sanitizePath(filePath) { try { return path.resolve(filePath); } catch { throw new Error(`Invalid path: ${filePath}`); } } parseCommand(command) { // Simple command parser that respects quotes const parts = []; let current = ''; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < command.length; i++) { const char = command[i]; if ((char === '"' || char === "'") && !inQuotes) { inQuotes = true; quoteChar = char; } else if (char === quoteChar && inQuotes) { inQuotes = false; quoteChar = ''; } else if (char === ' ' && !inQuotes) { if (current.length > 0) { parts.push(current); current = ''; } } else { current += char; } } if (current.length > 0) { parts.push(current); } return parts; } async fileExists(filePath) { const fs = await import('fs-extra'); return fs.pathExists(filePath); } async readJsonFile(filePath) { const fs = await import('fs-extra'); try { // Validate file path to prevent directory traversal const sanitizedPath = this.sanitizePath(filePath); return (await fs.readJson(sanitizedPath)); } catch (error) { console.warn(`Failed to read JSON file ${filePath}:`, error); return null; } } async writeJsonFile(filePath, data) { const fs = await import('fs-extra'); try { // Validate file path and ensure directory exists const sanitizedPath = this.sanitizePath(filePath); const path = await import('path'); await fs.ensureDir(path.dirname(sanitizedPath)); // Atomic write using temporary file const tempFile = `${sanitizedPath}.tmp`; await fs.writeJson(tempFile, data, { spaces: 2 }); await fs.move(tempFile, sanitizedPath, { overwrite: true }); } catch (error) { throw new Error(`Failed to write JSON file ${filePath}: ${error}`); } } async createBackup(filePath) { const fs = await import('fs-extra'); try { const sanitizedPath = this.sanitizePath(filePath); const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `${sanitizedPath}.woaru-backup-${timestamp}`; if (await fs.pathExists(sanitizedPath)) { await fs.copy(sanitizedPath, backupPath); // Limit number of backup files (keep last 5) await this.cleanupOldBackups(sanitizedPath); } return backupPath; } catch (error) { throw new Error(`Failed to create backup for ${filePath}: ${error}`); } } async cleanupOldBackups(originalPath) { const fs = await import('fs-extra'); const path = await import('path'); try { const dir = path.dirname(originalPath); const basename = path.basename(originalPath); const files = await fs.readdir(dir); const backupFiles = files .filter(file => file.startsWith(`${basename}.woaru-backup-`)) .sort() .reverse(); // Most recent first // Keep only the 5 most recent backups for (let i = 5; i < backupFiles.length; i++) { await fs.remove(path.join(dir, backupFiles[i])); } } catch (error) { console.warn('Failed to cleanup old backups:', error); } } } //# sourceMappingURL=BaseAction.js.map