| 164 | } |
| 165 | |
| 166 | function makeTmpdirFs(root: string): TextEditorFsCallbacks { |
| 167 | const resolvePath = (rel: string): string => { |
| 168 | const abs = resolve(root, rel); |
| 169 | const relCheck = relative(root, abs); |
| 170 | if (relCheck.startsWith('..') || isAbsolute(relCheck)) { |
| 171 | throw new Error(`Path escapes workspace: ${rel}`); |
| 172 | } |
| 173 | return abs; |
| 174 | }; |
| 175 | return { |
| 176 | view(path) { |
| 177 | const abs = resolvePath(path); |
| 178 | if (!existsSync(abs)) return null; |
| 179 | const stat = statSync(abs); |
| 180 | if (!stat.isFile()) return null; |
| 181 | const content = readFileSync(abs, 'utf8'); |
| 182 | return { content, numLines: content.split('\n').length }; |
| 183 | }, |
| 184 | create(path, content) { |
| 185 | const abs = resolvePath(path); |
| 186 | mkdirSync(dirname(abs), { recursive: true }); |
| 187 | writeFileSync(abs, content); |
| 188 | return { path }; |
| 189 | }, |
| 190 | strReplace(path, oldStr, newStr) { |
| 191 | const abs = resolvePath(path); |
| 192 | const current = readFileSync(abs, 'utf8'); |
| 193 | const idx = current.indexOf(oldStr); |
| 194 | if (idx === -1) throw new Error(`old_str not found in ${path}`); |
| 195 | if (current.indexOf(oldStr, idx + 1) !== -1) { |
| 196 | throw new Error(`old_str matches multiple locations in ${path}`); |
| 197 | } |
| 198 | writeFileSync(abs, current.slice(0, idx) + newStr + current.slice(idx + oldStr.length)); |
| 199 | return { path }; |
| 200 | }, |
| 201 | insert(path, line, text) { |
| 202 | const abs = resolvePath(path); |
| 203 | const current = existsSync(abs) ? readFileSync(abs, 'utf8') : ''; |
| 204 | const lines = current.split('\n'); |
| 205 | const at = Math.max(0, Math.min(line, lines.length)); |
| 206 | lines.splice(at, 0, text); |
| 207 | writeFileSync(abs, lines.join('\n')); |
| 208 | return { path }; |
| 209 | }, |
| 210 | listDir(dir) { |
| 211 | const abs = resolvePath(dir); |
| 212 | if (!existsSync(abs) || !statSync(abs).isDirectory()) return []; |
| 213 | return readdirSync(abs).sort(); |
| 214 | }, |
| 215 | }; |
| 216 | } |
| 217 | |
| 218 | async function runOne(model: SmokeModel, prompt: SmokePrompt, apiKey: string): Promise<RunResult> { |
| 219 | const t0 = Date.now(); |