|
30 | 30 | */ |
31 | 31 |
|
32 | 32 | import { randomBytes } from "node:crypto"; |
33 | | -import { readFileSync, renameSync, writeFileSync } from "node:fs"; |
| 33 | +import { lstatSync, readFileSync, renameSync, writeFileSync } from "node:fs"; |
34 | 34 | import { isAbsolute, resolve } from "node:path"; |
35 | 35 |
|
36 | 36 | import { |
@@ -82,6 +82,7 @@ export type ConflictReason = |
82 | 82 | | "line out of range" |
83 | 83 | | "line content drifted" |
84 | 84 | | "path escapes project root" |
| 85 | + | "path is a symlink" |
85 | 86 | | "duplicate edit on same line"; |
86 | 87 |
|
87 | 88 | /** Q5 envelope shape — single shape across `dry-run` and `apply` modes. */ |
@@ -172,8 +173,23 @@ export function applyDiffPayload(opts: ApplyDiffPayloadOpts): ApplyJsonPayload { |
172 | 173 |
|
173 | 174 | let source = sourceCache.get(canonicalPath); |
174 | 175 | if (source === undefined) { |
| 176 | + const absPath = resolve(resolvedRoot, canonicalPath); |
175 | 177 | try { |
176 | | - source = readFileSync(resolve(resolvedRoot, canonicalPath), "utf8"); |
| 178 | + if (lstatSync(absPath).isSymbolicLink()) { |
| 179 | + conflicts.push({ |
| 180 | + file_path: canonicalPath, |
| 181 | + line_start: lineStart, |
| 182 | + before_pattern: before, |
| 183 | + actual_at_line: "", |
| 184 | + reason: "path is a symlink", |
| 185 | + }); |
| 186 | + continue; |
| 187 | + } |
| 188 | + } catch { |
| 189 | + // Missing path — readFileSync below reports "file missing". |
| 190 | + } |
| 191 | + try { |
| 192 | + source = readFileSync(absPath, "utf8"); |
177 | 193 | } catch { |
178 | 194 | conflicts.push({ |
179 | 195 | file_path: canonicalPath, |
|
0 commit comments