( text: string, node: TaskNode, cwd: string, )
| 266 | * and the common "// File: path" / "### path" preambles. |
| 267 | */ |
| 268 | export async function extractFileEdits( |
| 269 | text: string, |
| 270 | node: TaskNode, |
| 271 | cwd: string, |
| 272 | ): Promise<WorkerResult['fileEdits']> { |
| 273 | const edits: WorkerResult['fileEdits'] = []; |
| 274 | const seen = new Set<string>(); |
| 275 | |
| 276 | // Pattern A: fenced block with path= attribute. |
| 277 | const fenceRe = /```[a-zA-Z]*\s+path=([^\s`]+)\s*\n([\s\S]*?)```/g; |
| 278 | let m: RegExpExecArray | null; |
| 279 | while ((m = fenceRe.exec(text)) !== null) { |
| 280 | const p = normalizeRel(m[1]!, cwd); |
| 281 | if (seen.has(p)) continue; |
| 282 | seen.add(p); |
| 283 | edits.push({ path: p, content: m[2]!.replace(/\n$/, ''), isNew: await isNewFile(cwd, p) }); |
| 284 | } |
| 285 | |
| 286 | // Pattern B: "// File: path" or "### path" header followed by a fenced block. |
| 287 | if (edits.length === 0) { |
| 288 | const headerRe = /(?:\/\/\s*File:|###|####|File:)\s*([^\s`]+\.[a-zA-Z]+)\s*\n+```[a-zA-Z]*\n([\s\S]*?)```/g; |
| 289 | while ((m = headerRe.exec(text)) !== null) { |
| 290 | const p = normalizeRel(m[1]!, cwd); |
| 291 | if (seen.has(p)) continue; |
| 292 | seen.add(p); |
| 293 | edits.push({ path: p, content: m[2]!.replace(/\n$/, ''), isNew: await isNewFile(cwd, p) }); |
| 294 | } |
| 295 | } |
| 296 | |
| 297 | // Pattern C: exactly one target file + one fenced block, no path marker. |
| 298 | if (edits.length === 0 && node.targetFiles.length === 1) { |
| 299 | const block = text.match(/```[a-zA-Z]*\n([\s\S]*?)```/); |
| 300 | if (block) { |
| 301 | const p = node.targetFiles[0]!; |
| 302 | edits.push({ path: p, content: block[1]!.replace(/\n$/, ''), isNew: await isNewFile(cwd, p) }); |
| 303 | } |
| 304 | } |
| 305 | |
| 306 | return edits; |
| 307 | } |
| 308 | |
| 309 | function normalizeRel(p: string, cwd: string): string { |
| 310 | let rel = p.trim().replace(/^['"]|['"]$/g, ''); |
no test coverage detected