7fdbabb264
On Linux systems, 'sg' is a mailutils command, not ast-grep. The previous fallback would silently run the wrong binary when ast-grep wasn't found. Changes: - getSgCliPath() now returns string | null instead of string - Fallback changed from 'sg' to null - Call sites now check for null and return user-facing error with installation instructions - checkEnvironment() updated to handle null path Fixes #1365
250 lines
6.0 KiB
TypeScript
250 lines
6.0 KiB
TypeScript
import { createRequire } from "module"
|
|
import { dirname, join } from "path"
|
|
import { existsSync, statSync } from "fs"
|
|
import { getCachedBinaryPath } from "./downloader"
|
|
|
|
type Platform = "darwin" | "linux" | "win32" | "unsupported"
|
|
|
|
function isValidBinary(filePath: string): boolean {
|
|
try {
|
|
return statSync(filePath).size > 10000
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function getPlatformPackageName(): string | null {
|
|
const platform = process.platform as Platform
|
|
const arch = process.arch
|
|
|
|
const platformMap: Record<string, string> = {
|
|
"darwin-arm64": "@ast-grep/cli-darwin-arm64",
|
|
"darwin-x64": "@ast-grep/cli-darwin-x64",
|
|
"linux-arm64": "@ast-grep/cli-linux-arm64-gnu",
|
|
"linux-x64": "@ast-grep/cli-linux-x64-gnu",
|
|
"win32-x64": "@ast-grep/cli-win32-x64-msvc",
|
|
"win32-arm64": "@ast-grep/cli-win32-arm64-msvc",
|
|
"win32-ia32": "@ast-grep/cli-win32-ia32-msvc",
|
|
}
|
|
|
|
return platformMap[`${platform}-${arch}`] ?? null
|
|
}
|
|
|
|
export function findSgCliPathSync(): string | null {
|
|
const binaryName = process.platform === "win32" ? "sg.exe" : "sg"
|
|
|
|
const cachedPath = getCachedBinaryPath()
|
|
if (cachedPath && isValidBinary(cachedPath)) {
|
|
return cachedPath
|
|
}
|
|
|
|
try {
|
|
const require = createRequire(import.meta.url)
|
|
const cliPkgPath = require.resolve("@ast-grep/cli/package.json")
|
|
const cliDir = dirname(cliPkgPath)
|
|
const sgPath = join(cliDir, binaryName)
|
|
|
|
if (existsSync(sgPath) && isValidBinary(sgPath)) {
|
|
return sgPath
|
|
}
|
|
} catch {
|
|
// @ast-grep/cli not installed
|
|
}
|
|
|
|
const platformPkg = getPlatformPackageName()
|
|
if (platformPkg) {
|
|
try {
|
|
const require = createRequire(import.meta.url)
|
|
const pkgPath = require.resolve(`${platformPkg}/package.json`)
|
|
const pkgDir = dirname(pkgPath)
|
|
const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep"
|
|
const binaryPath = join(pkgDir, astGrepName)
|
|
|
|
if (existsSync(binaryPath) && isValidBinary(binaryPath)) {
|
|
return binaryPath
|
|
}
|
|
} catch {
|
|
// Platform-specific package not installed
|
|
}
|
|
}
|
|
|
|
if (process.platform === "darwin") {
|
|
const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"]
|
|
for (const path of homebrewPaths) {
|
|
if (existsSync(path) && isValidBinary(path)) {
|
|
return path
|
|
}
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
let resolvedCliPath: string | null = null
|
|
|
|
export function getSgCliPath(): string | null {
|
|
if (resolvedCliPath !== null) {
|
|
return resolvedCliPath
|
|
}
|
|
|
|
const syncPath = findSgCliPathSync()
|
|
if (syncPath) {
|
|
resolvedCliPath = syncPath
|
|
return syncPath
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function setSgCliPath(path: string): void {
|
|
resolvedCliPath = path
|
|
}
|
|
|
|
// CLI supported languages (25 total)
|
|
export const CLI_LANGUAGES = [
|
|
"bash",
|
|
"c",
|
|
"cpp",
|
|
"csharp",
|
|
"css",
|
|
"elixir",
|
|
"go",
|
|
"haskell",
|
|
"html",
|
|
"java",
|
|
"javascript",
|
|
"json",
|
|
"kotlin",
|
|
"lua",
|
|
"nix",
|
|
"php",
|
|
"python",
|
|
"ruby",
|
|
"rust",
|
|
"scala",
|
|
"solidity",
|
|
"swift",
|
|
"typescript",
|
|
"tsx",
|
|
"yaml",
|
|
] as const
|
|
|
|
// NAPI supported languages (5 total - native bindings)
|
|
export const NAPI_LANGUAGES = ["html", "javascript", "tsx", "css", "typescript"] as const
|
|
|
|
// Language to file extensions mapping
|
|
export const DEFAULT_TIMEOUT_MS = 300_000
|
|
export const DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024
|
|
export const DEFAULT_MAX_MATCHES = 500
|
|
|
|
export const LANG_EXTENSIONS: Record<string, string[]> = {
|
|
bash: [".bash", ".sh", ".zsh", ".bats"],
|
|
c: [".c", ".h"],
|
|
cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h"],
|
|
csharp: [".cs"],
|
|
css: [".css"],
|
|
elixir: [".ex", ".exs"],
|
|
go: [".go"],
|
|
haskell: [".hs", ".lhs"],
|
|
html: [".html", ".htm"],
|
|
java: [".java"],
|
|
javascript: [".js", ".jsx", ".mjs", ".cjs"],
|
|
json: [".json"],
|
|
kotlin: [".kt", ".kts"],
|
|
lua: [".lua"],
|
|
nix: [".nix"],
|
|
php: [".php"],
|
|
python: [".py", ".pyi"],
|
|
ruby: [".rb", ".rake"],
|
|
rust: [".rs"],
|
|
scala: [".scala", ".sc"],
|
|
solidity: [".sol"],
|
|
swift: [".swift"],
|
|
typescript: [".ts", ".cts", ".mts"],
|
|
tsx: [".tsx"],
|
|
yaml: [".yml", ".yaml"],
|
|
}
|
|
|
|
export interface EnvironmentCheckResult {
|
|
cli: {
|
|
available: boolean
|
|
path: string
|
|
error?: string
|
|
}
|
|
napi: {
|
|
available: boolean
|
|
error?: string
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if ast-grep CLI and NAPI are available.
|
|
* Call this at startup to provide early feedback about missing dependencies.
|
|
*/
|
|
export function checkEnvironment(): EnvironmentCheckResult {
|
|
const cliPath = getSgCliPath()
|
|
const result: EnvironmentCheckResult = {
|
|
cli: {
|
|
available: false,
|
|
path: cliPath ?? "not found",
|
|
},
|
|
napi: {
|
|
available: false,
|
|
},
|
|
}
|
|
|
|
if (cliPath && existsSync(cliPath)) {
|
|
result.cli.available = true
|
|
} else if (!cliPath) {
|
|
result.cli.error = "ast-grep binary not found. Install with: bun add -D @ast-grep/cli"
|
|
} else {
|
|
result.cli.error = `Binary not found: ${cliPath}`
|
|
}
|
|
|
|
// Check NAPI availability
|
|
try {
|
|
require("@ast-grep/napi")
|
|
result.napi.available = true
|
|
} catch (e) {
|
|
result.napi.available = false
|
|
result.napi.error = `@ast-grep/napi not installed: ${e instanceof Error ? e.message : String(e)}`
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
/**
|
|
* Format environment check result as user-friendly message.
|
|
*/
|
|
export function formatEnvironmentCheck(result: EnvironmentCheckResult): string {
|
|
const lines: string[] = ["ast-grep Environment Status:", ""]
|
|
|
|
// CLI status
|
|
if (result.cli.available) {
|
|
lines.push(`[OK] CLI: Available (${result.cli.path})`)
|
|
} else {
|
|
lines.push(`[X] CLI: Not available`)
|
|
if (result.cli.error) {
|
|
lines.push(` Error: ${result.cli.error}`)
|
|
}
|
|
lines.push(` Install: bun add -D @ast-grep/cli`)
|
|
}
|
|
|
|
// NAPI status
|
|
if (result.napi.available) {
|
|
lines.push(`[OK] NAPI: Available`)
|
|
} else {
|
|
lines.push(`[X] NAPI: Not available`)
|
|
if (result.napi.error) {
|
|
lines.push(` Error: ${result.napi.error}`)
|
|
}
|
|
lines.push(` Install: bun add -D @ast-grep/napi`)
|
|
}
|
|
|
|
lines.push("")
|
|
lines.push(`CLI supports ${CLI_LANGUAGES.length} languages`)
|
|
lines.push(`NAPI supports ${NAPI_LANGUAGES.length} languages: ${NAPI_LANGUAGES.join(", ")}`)
|
|
|
|
return lines.join("\n")
|
|
}
|