Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
import { readFile, writeFile, mkdir, readdir, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import path from 'node:path';
import { resolveInRepo, isKicadFile } from '../util/paths.js';
export async function toolReadFile(
repoRoot: string,
p: string,
startLine?: number,
endLine?: number,
): Promise<string> {
const abs = resolveInRepo(repoRoot, p);
const text = await readFile(abs, 'utf8');
if (startLine === undefined) return text;
const lines = text.split('\n');
const from = Math.max(1, startLine);
const to = Math.min(lines.length, endLine ?? lines.length);
return lines
.slice(from - 1, to)
.map((l, i) => `${from + i}: ${l}`)
.join('\n');
}
/** New files only; refuses to overwrite anything and to create KiCad files (SPEC §4.2). */
export async function toolWriteFile(repoRoot: string, p: string, content: string): Promise<string> {
const abs = resolveInRepo(repoRoot, p);
if (isKicadFile(abs)) {
throw new Error(`write_file refuses KiCad files (${p}); use edit_file with anchors instead`);
}
if (existsSync(abs)) {
throw new Error(`write_file refuses to overwrite existing file ${p}; use edit_file`);
}
await mkdir(path.dirname(abs), { recursive: true });
await writeFile(abs, content, 'utf8');
return `wrote ${p}`;
}
/**
* Exact-match anchored replace; fails with an actionable error unless unique.
* `replaceAll` replaces every occurrence (the rename case, AC-3.1) while still
* being a surgical text edit on the s-expression source.
*/
export async function toolEditFile(
repoRoot: string,
p: string,
oldString: string,
newString: string,
replaceAll = false,
): Promise<string> {
const abs = resolveInRepo(repoRoot, p);
const text = await readFile(abs, 'utf8');
const first = text.indexOf(oldString);
if (first === -1) {
throw new Error(`edit_file: anchor not found in ${p}; re-read the file and use an exact excerpt`);
}
const count = text.split(oldString).length - 1;
if (replaceAll) {
await writeFile(abs, text.split(oldString).join(newString), 'utf8');
return `edited ${p} (${count} occurrence(s) replaced)`;
}
if (count > 1) {
throw new Error(
`edit_file: anchor matched ${count} times in ${p}; widen the anchor with surrounding lines until it is unique, or pass replace_all: true to replace every occurrence`,
);
}
await writeFile(abs, text.slice(0, first) + newString + text.slice(first + oldString.length), 'utf8');
return `edited ${p}`;
}
export interface SearchMatch {
file: string;
line: number;
text: string;
}
const SKIP_DIRS = new Set(['.git', 'node_modules', 'dist', '.copperhead']);
export function globToRegex(glob: string): RegExp {
let out = '';
for (let i = 0; i < glob.length; i++) {
if (glob.startsWith('**/', i)) {
out += '(?:.*/)?'; // zero or more directories
i += 2;
} else if (glob.startsWith('**', i)) {
out += '.*';
i += 1;
} else if (glob[i] === '*') {
out += '[^/]*';
} else if (glob[i] === '?') {
out += '.';
} else {
out += glob[i]!.replace(/[.+^${}()|[\]\\]/, '\\$&');
}
}
return new RegExp(`^${out}$`);
}
/** ripgrep-style regex search implemented natively (no rg dependency). */
export async function toolSearch(
repoRoot: string,
pattern: string,
glob?: string,
maxMatches = 200,
): Promise<SearchMatch[]> {
const re = new RegExp(pattern);
const globRe = glob ? globToRegex(glob) : null;
const matches: SearchMatch[] = [];
async function walk(dir: string): Promise<void> {
if (matches.length >= maxMatches) return;
for (const entry of await readdir(dir)) {
if (SKIP_DIRS.has(entry)) continue;
const abs = path.join(dir, entry);
const rel = path.relative(repoRoot, abs);
const st = await stat(abs);
if (st.isDirectory()) {
await walk(abs);
} else if (st.size < 5_000_000 && (!globRe || globRe.test(rel))) {
let text: string;
try {
text = await readFile(abs, 'utf8');
} catch {
continue;
}
if (text.includes('\u0000')) continue; // binary
const lines = text.split('\n');
for (let i = 0; i < lines.length && matches.length < maxMatches; i++) {
if (re.test(lines[i]!)) matches.push({ file: rel, line: i + 1, text: lines[i]!.trim() });
}
}
if (matches.length >= maxMatches) return;
}
}
await walk(repoRoot);
return matches;
}
8ecc6ed96This file is pinned to the selected release. Download the original to open it in your design tool.