YeonGyu-Kim 725ec9b91d feat(ast-grep): add safety limits to prevent token overflow
- Add timeout (5min), output limit (1MB), match limit (500)
- Add SgResult type with truncation info
- Update formatSearchResult/formatReplaceResult for truncation display
- cli.ts: timeout + output truncation + graceful JSON recovery
2025-12-05 22:52:33 +09:00

265 lines
6.5 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 {
if (resolvedCliPath !== null) {
return resolvedCliPath
}
const syncPath = findSgCliPathSync()
if (syncPath) {
resolvedCliPath = syncPath
return syncPath
}
return "sg"
}
export function setSgCliPath(path: string): void {
resolvedCliPath = path
}
export const SG_CLI_PATH = getSgCliPath()
// 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 result: EnvironmentCheckResult = {
cli: {
available: false,
path: SG_CLI_PATH,
},
napi: {
available: false,
},
}
// Check CLI availability
if (existsSync(SG_CLI_PATH)) {
result.cli.available = true
} else if (SG_CLI_PATH === "sg") {
// Fallback path - try which/where to find in PATH
try {
const { spawnSync } = require("child_process")
const whichResult = spawnSync(process.platform === "win32" ? "where" : "which", ["sg"], {
encoding: "utf-8",
timeout: 5000,
})
result.cli.available = whichResult.status === 0 && !!whichResult.stdout?.trim()
if (!result.cli.available) {
result.cli.error = "sg binary not found in PATH"
}
} catch {
result.cli.error = "Failed to check sg availability"
}
} else {
result.cli.error = `Binary not found: ${SG_CLI_PATH}`
}
// 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(`✓ CLI: Available (${result.cli.path})`)
} else {
lines.push(`✗ 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(`✓ NAPI: Available`)
} else {
lines.push(`✗ 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")
}