Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | /** * Command Runner Utility * Executes shell commands for dev tools (tests, lint, typescript) */ import { spawn, ChildProcess } from 'child_process'; import path from 'path'; import type { CommandResult } from './types'; // Allowed commands whitelist for security const ALLOWED_COMMANDS = [ 'npm', 'npx', 'node', ] as const; const ALLOWED_NPM_SCRIPTS = [ 'test', 'lint', 'lint:fix', 'check:ts', 'check:lint', ] as const; // Store running processes for cancellation const runningProcesses = new Map<string, ChildProcess>(); /** * Validates that the command is in the whitelist */ function validateCommand(command: string, args: string[]): boolean { if (!ALLOWED_COMMANDS.includes(command as typeof ALLOWED_COMMANDS[number])) { return false; } // For npm commands, validate the script if (command === 'npm' && args[0] === 'run') { const script = args[1]; if (!ALLOWED_NPM_SCRIPTS.includes(script as typeof ALLOWED_NPM_SCRIPTS[number])) { return false; } } // For npm test, allow it if (command === 'npm' && args[0] === 'test') { return true; } // For npx, only allow specific commands if (command === 'npx') { const npxCommand = args[0]; if (!['tsc', 'eslint', 'jest'].includes(npxCommand)) { return false; } } return true; } /** * Get the project root directory */ function getProjectRoot(): string { return process.cwd(); } /** * Run a command and return the result */ export async function runCommand( command: string, args: string[], options: { timeout?: number; cwd?: string; processId?: string; } = {} ): Promise<CommandResult> { const { timeout = 300000, cwd = getProjectRoot(), processId } = options; // Validate command if (!validateCommand(command, args)) { return { success: false, output: '', error: `Command not allowed: ${command} ${args.join(' ')}`, exitCode: 1, duration: 0}; } return new Promise((resolve) => { const startTime = Date.now(); let stdout = ''; let stderr = ''; let resolved = false; // Use shell: true on Windows for npm commands const isWindows = process.platform === 'win32'; const spawnOptions = { cwd, shell: isWindows, env: { ...process.env, FORCE_COLOR: '0' }, // Disable colors for cleaner parsing }; const childProcess = spawn(command, args, spawnOptions); // Store process for potential cancellation if (processId) { runningProcesses.set(processId, childProcess); } // Set timeout const timeoutId = setTimeout(() => { if (!resolved) { resolved = true; childProcess.kill('SIGTERM'); runningProcesses.delete(processId || ''); resolve({ success: false, output: stdout, error: 'Command timed out', exitCode: 124, duration: Date.now() - startTime}); } }, timeout); childProcess.stdout?.on('data', (data: Buffer) => { stdout += data.toString(); }); childProcess.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); }); childProcess.on('close', (code) => { if (!resolved) { resolved = true; clearTimeout(timeoutId); runningProcesses.delete(processId || ''); resolve({ success: code === 0, output: stdout, error: stderr, exitCode: code ?? 1, duration: Date.now() - startTime}); } }); childProcess.on('error', (err) => { if (!resolved) { resolved = true; clearTimeout(timeoutId); runningProcesses.delete(processId || ''); resolve({ success: false, output: stdout, error: err.message, exitCode: 1, duration: Date.now() - startTime}); } }); }); } /** * Kill a running process by ID */ export function killProcess(processId: string): boolean { const process = runningProcesses.get(processId); if (process) { process.kill('SIGTERM'); runningProcesses.delete(processId); return true; } return false; } /** * Check if a process is running */ export function isProcessRunning(processId: string): boolean { return runningProcesses.has(processId); } /** * Run npm test command */ export async function runTests(options: { pattern?: string; coverage?: boolean; processId?: string; } = {}): Promise<CommandResult> { const args = ['test', '--']; if (options.pattern) { args.push('--testPathPattern', options.pattern); } if (options.coverage) { args.push('--coverage'); } // Run in CI mode for cleaner output args.push('--ci', '--reporters=default'); return runCommand('npm', args, { processId: options.processId }); } /** * Run ESLint */ export async function runLint(options: { path?: string; fix?: boolean; processId?: string; } = {}): Promise<CommandResult> { // Note: path option is available for future use with custom eslint args const args = ['run', options.fix ? 'lint:fix' : 'lint']; return runCommand('npm', args, { processId: options.processId }); } /** * Run TypeScript type checking */ export async function runTypeCheck(options: { processId?: string; } = {}): Promise<CommandResult> { return runCommand('npm', ['run', 'check:ts'], { processId: options.processId }); } /** * Sanitize path to prevent directory traversal */ export function sanitizePath(inputPath: string, allowedBase: string): string | null { const resolved = path.resolve(allowedBase, inputPath); const normalized = path.normalize(resolved); // Ensure the path is within the allowed base directory if (!normalized.startsWith(path.normalize(allowedBase))) { return null; } return normalized; } |