({
config,
filePath,
operation,
abortSignal,
}: ExecuteFileEditOperationOptions<TMetadata>)
| 105 | * Handles validation, file IO, diff generation, and common error handling. |
| 106 | */ |
| 107 | export async function executeFileEditOperation<TMetadata>({ |
| 108 | config, |
| 109 | filePath, |
| 110 | operation, |
| 111 | abortSignal, |
| 112 | }: ExecuteFileEditOperationOptions<TMetadata>): Promise< |
| 113 | FileEditErrorResult | (FileEditDiffSuccessBase & TMetadata) |
| 114 | > { |
| 115 | try { |
| 116 | const { |
| 117 | correctedPath: validatedPath, |
| 118 | warning: pathWarning, |
| 119 | resolvedPath, |
| 120 | } = resolvePathWithinCwd(filePath, config.cwd, config.runtime); |
| 121 | filePath = validatedPath; |
| 122 | |
| 123 | // Validate plan mode access restrictions |
| 124 | const planModeError = await validatePlanModeAccess(filePath, config); |
| 125 | if (planModeError) { |
| 126 | return planModeError; |
| 127 | } |
| 128 | |
| 129 | const abortedBeforeLock = getAbortedFileEditResult(abortSignal); |
| 130 | if (abortedBeforeLock) { |
| 131 | return abortedBeforeLock; |
| 132 | } |
| 133 | |
| 134 | // Serialize same-file edits so a later operation re-reads the file after the |
| 135 | // earlier write lands instead of racing on stale content or temp-file writes. |
| 136 | let writeStarted = false; |
| 137 | const lockedEditPromise: Promise<ExecuteFileEditReturn<TMetadata>> = getFileEditLocks( |
| 138 | config.runtime |
| 139 | ).withLock(resolvedPath, async () => { |
| 140 | // Abort can fire while we wait on the lock, and local runtime file I/O does |
| 141 | // not observe abort signals on its own, so re-check before touching disk. |
| 142 | const abortedAfterLock = getAbortedFileEditResult(abortSignal); |
| 143 | if (abortedAfterLock) { |
| 144 | return abortedAfterLock; |
| 145 | } |
| 146 | |
| 147 | // Check if file exists and get stats using runtime |
| 148 | let fileStat; |
| 149 | try { |
| 150 | fileStat = await config.runtime.stat(resolvedPath, abortSignal); |
| 151 | } catch (err) { |
| 152 | if (err instanceof RuntimeError) { |
| 153 | return { |
| 154 | success: false, |
| 155 | error: err.message, |
| 156 | }; |
| 157 | } |
| 158 | throw err; |
| 159 | } |
| 160 | |
| 161 | if (fileStat.isDirectory) { |
| 162 | return { |
| 163 | success: false, |
| 164 | error: `Path is a directory, not a file: ${resolvedPath}`, |
no test coverage detected