( operations: BatchOperation[], projectRoot: string, )
| 1260 | } |
| 1261 | |
| 1262 | export function executeBatch( |
| 1263 | operations: BatchOperation[], |
| 1264 | projectRoot: string, |
| 1265 | ): BatchResult { |
| 1266 | const results: OperationResult[] = new Array(operations.length); |
| 1267 | const undoEntries: BatchResult["undoEntries"] = []; |
| 1268 | |
| 1269 | // Group operations by file |
| 1270 | const byFile = new Map<string, Array<{ index: number; op: BatchOperation }>>(); |
| 1271 | |
| 1272 | for (let i = 0; i < operations.length; i++) { |
| 1273 | const op = operations[i]; |
| 1274 | const file = op.file; |
| 1275 | const group = byFile.get(file) ?? []; |
| 1276 | group.push({ index: i, op }); |
| 1277 | byFile.set(file, group); |
| 1278 | } |
| 1279 | |
| 1280 | // Process each file atomically |
| 1281 | for (const [file, ops] of byFile) { |
| 1282 | // Validate file path |
| 1283 | if (!isProjectFilePathSafe(file, projectRoot)) { |
| 1284 | for (const { index, op } of ops) { |
| 1285 | results[index] = { |
| 1286 | op: op.op, |
| 1287 | file, |
| 1288 | line: getOpLine(op), |
| 1289 | success: false, |
| 1290 | error: "File path is outside the project root", |
| 1291 | }; |
| 1292 | } |
| 1293 | continue; |
| 1294 | } |
| 1295 | |
| 1296 | const resolvedPath = resolveProjectFilePath(file, projectRoot); |
| 1297 | if (!resolvedPath) { |
| 1298 | for (const { index, op } of ops) { |
| 1299 | results[index] = { |
| 1300 | op: op.op, |
| 1301 | file, |
| 1302 | line: getOpLine(op), |
| 1303 | success: false, |
| 1304 | error: "Could not resolve file path", |
| 1305 | }; |
| 1306 | } |
| 1307 | continue; |
| 1308 | } |
| 1309 | |
| 1310 | let source: string; |
| 1311 | try { |
| 1312 | source = fs.readFileSync(resolvedPath, "utf-8"); |
| 1313 | } catch (err) { |
| 1314 | for (const { index, op } of ops) { |
| 1315 | results[index] = { |
| 1316 | op: op.op, |
| 1317 | file, |
| 1318 | line: getOpLine(op), |
| 1319 | success: false, |
no test coverage detected