feat(ast-grep): add CLI path resolution and auto-download functionality
- Add automatic CLI binary path detection and resolution - Implement lazy binary download with caching - Add environment check utilities for CLI and NAPI availability - Improve error handling and fallback mechanisms - Export new utilities from index.ts
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { spawn } from "bun"
|
||||
import { SG_CLI_PATH } from "./constants"
|
||||
import { existsSync } from "fs"
|
||||
import { getSgCliPath, setSgCliPath, findSgCliPathSync } from "./constants"
|
||||
import { ensureAstGrepBinary } from "./downloader"
|
||||
import type { CliMatch, CliLanguage } from "./types"
|
||||
|
||||
export interface RunOptions {
|
||||
@@ -12,6 +14,65 @@ export interface RunOptions {
|
||||
updateAll?: boolean
|
||||
}
|
||||
|
||||
let resolvedCliPath: string | null = null
|
||||
let initPromise: Promise<string | null> | null = null
|
||||
|
||||
export async function getAstGrepPath(): Promise<string | null> {
|
||||
if (resolvedCliPath !== null && existsSync(resolvedCliPath)) {
|
||||
return resolvedCliPath
|
||||
}
|
||||
|
||||
if (initPromise) {
|
||||
return initPromise
|
||||
}
|
||||
|
||||
initPromise = (async () => {
|
||||
const syncPath = findSgCliPathSync()
|
||||
if (syncPath && existsSync(syncPath)) {
|
||||
resolvedCliPath = syncPath
|
||||
setSgCliPath(syncPath)
|
||||
return syncPath
|
||||
}
|
||||
|
||||
const downloadedPath = await ensureAstGrepBinary()
|
||||
if (downloadedPath) {
|
||||
resolvedCliPath = downloadedPath
|
||||
setSgCliPath(downloadedPath)
|
||||
return downloadedPath
|
||||
}
|
||||
|
||||
return null
|
||||
})()
|
||||
|
||||
return initPromise
|
||||
}
|
||||
|
||||
export function startBackgroundInit(): void {
|
||||
if (!initPromise) {
|
||||
initPromise = getAstGrepPath()
|
||||
initPromise.catch(() => {})
|
||||
}
|
||||
}
|
||||
|
||||
interface SpawnResult {
|
||||
stdout: string
|
||||
stderr: string
|
||||
exitCode: number
|
||||
}
|
||||
|
||||
async function spawnSg(cliPath: string, args: string[]): Promise<SpawnResult> {
|
||||
const proc = spawn([cliPath, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
const exitCode = await proc.exited
|
||||
|
||||
return { stdout, stderr, exitCode }
|
||||
}
|
||||
|
||||
export async function runSg(options: RunOptions): Promise<CliMatch[]> {
|
||||
const args = ["run", "-p", options.pattern, "--lang", options.lang, "--json=compact"]
|
||||
|
||||
@@ -35,14 +96,45 @@ export async function runSg(options: RunOptions): Promise<CliMatch[]> {
|
||||
const paths = options.paths && options.paths.length > 0 ? options.paths : ["."]
|
||||
args.push(...paths)
|
||||
|
||||
const proc = spawn([SG_CLI_PATH, ...args], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
})
|
||||
let cliPath = getSgCliPath()
|
||||
|
||||
const stdout = await new Response(proc.stdout).text()
|
||||
const stderr = await new Response(proc.stderr).text()
|
||||
const exitCode = await proc.exited
|
||||
if (!existsSync(cliPath) && cliPath !== "sg") {
|
||||
const downloadedPath = await getAstGrepPath()
|
||||
if (downloadedPath) {
|
||||
cliPath = downloadedPath
|
||||
}
|
||||
}
|
||||
|
||||
let result: SpawnResult
|
||||
try {
|
||||
result = await spawnSg(cliPath, args)
|
||||
} catch (e) {
|
||||
const error = e as NodeJS.ErrnoException
|
||||
if (
|
||||
error.code === "ENOENT" ||
|
||||
error.message?.includes("ENOENT") ||
|
||||
error.message?.includes("not found")
|
||||
) {
|
||||
const downloadedPath = await ensureAstGrepBinary()
|
||||
if (downloadedPath) {
|
||||
resolvedCliPath = downloadedPath
|
||||
setSgCliPath(downloadedPath)
|
||||
result = await spawnSg(downloadedPath, args)
|
||||
} else {
|
||||
throw new Error(
|
||||
`ast-grep CLI binary not found.\n\n` +
|
||||
`Auto-download failed. Manual install options:\n` +
|
||||
` bun add -D @ast-grep/cli\n` +
|
||||
` cargo install ast-grep --locked\n` +
|
||||
` brew install ast-grep`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
throw new Error(`Failed to spawn ast-grep: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
const { stdout, stderr, exitCode } = result
|
||||
|
||||
if (exitCode !== 0 && stdout.trim() === "") {
|
||||
if (stderr.includes("No files found")) {
|
||||
@@ -64,3 +156,13 @@ export async function runSg(options: RunOptions): Promise<CliMatch[]> {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export function isCliAvailable(): boolean {
|
||||
const path = findSgCliPathSync()
|
||||
return path !== null && existsSync(path)
|
||||
}
|
||||
|
||||
export async function ensureCliAvailable(): Promise<boolean> {
|
||||
const path = await getAstGrepPath()
|
||||
return path !== null && existsSync(path)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user