// ast-grep CLI wrapper. import { execFile } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' let sgPath export function findAstGrep(extra = []) { if (sgPath !== undefined) return sgPath const dirs = [...extra, ...(process.env.PATH || '').split(path.delimiter), path.join(process.env.HOME || '', '.local/bin'), '/usr/local/bin'] sgPath = null for (const d of dirs) { if (!d) continue const p = path.join(d, 'ast-grep') // `sg` collides with util-linux's setgroups on many distros try { fs.accessSync(p, fs.constants.X_OK); sgPath = p; break } catch {} } return sgPath } export function run(bin, args, { cwd, signal, timeoutMs = 120_000, maxBuffer = 64 * 1024 * 1024 } = {}) { return new Promise((resolve) => { execFile(bin, args, { cwd, signal, timeout: timeoutMs, maxBuffer, encoding: 'utf8' }, (error, stdout, stderr) => { resolve({ code: error ? (typeof error.code === 'number' ? error.code : 1) : 0, stdout: stdout ?? '', stderr: stderr ?? '', error }) }) }) } /** * Run a pattern (optionally with rewrite) and return parsed matches. * @returns {{matches: object[], warnings: string[]}} */ export async function search({ bin, pattern, rewrite, lang, paths, cwd, globs, strictness, selector, signal }) { const args = ['run', '--pattern', pattern, '--json=compact', '--color=never'] if (rewrite !== undefined) args.push('--rewrite', rewrite) if (lang) args.push('--lang', lang) if (strictness) args.push('--strictness', strictness) if (selector) args.push('--selector', selector) for (const g of globs ?? []) args.push('--globs', g) args.push('--', ...(paths?.length ? paths : ['.'])) const r = await run(bin, args, { cwd, signal }) const warnings = r.stderr.split('\n').map((l) => l.trim()).filter((l) => l && !/^See also|^Help: .*playground/i.test(l)) if (r.code !== 0 && !r.stdout.trim().startsWith('[')) { throw new Error(`ast-grep failed: ${warnings.join(' ') || r.error?.message || `exit ${r.code}`}`) } let matches = [] try { matches = JSON.parse(r.stdout || '[]') } catch (e) { throw new Error(`ast-grep returned malformed JSON: ${e.message}`) } return { matches, warnings } } /** Apply replacements (byte offsets) to file contents; returns new text. */ export function applyReplacements(buf, reps) { const sorted = [...reps].sort((a, b) => b.start - a.start) let out = buf let lastStart = Infinity for (const r of sorted) { if (r.end > lastStart) continue // overlapping: keep the outermost-first earlier one out = Buffer.concat([out.subarray(0, r.start), Buffer.from(r.text, 'utf8'), out.subarray(r.end)]) lastStart = r.start } return out }