constants.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import { createRequire } from "node:module"
  2. import { dirname, join } from "node:path"
  3. import { existsSync, statSync } from "node:fs"
  4. import { spawnSync } from "node:child_process"
  5. import { getCachedBinaryPath } from "./downloader"
  6. import { CLI_LANGUAGES } from "./types"
  7. type Platform = "darwin" | "linux" | "win32" | "unsupported"
  8. // Minimum expected size for a valid sg binary (filters out stub files)
  9. const MIN_BINARY_SIZE = 10_000
  10. function isValidBinary(filePath: string): boolean {
  11. try {
  12. return statSync(filePath).size > MIN_BINARY_SIZE
  13. } catch {
  14. return false
  15. }
  16. }
  17. function getPlatformPackageName(): string | null {
  18. const platform = process.platform as Platform
  19. const arch = process.arch
  20. const platformMap: Record<string, string> = {
  21. "darwin-arm64": "@ast-grep/cli-darwin-arm64",
  22. "darwin-x64": "@ast-grep/cli-darwin-x64",
  23. "linux-arm64": "@ast-grep/cli-linux-arm64-gnu",
  24. "linux-x64": "@ast-grep/cli-linux-x64-gnu",
  25. "win32-x64": "@ast-grep/cli-win32-x64-msvc",
  26. "win32-arm64": "@ast-grep/cli-win32-arm64-msvc",
  27. "win32-ia32": "@ast-grep/cli-win32-ia32-msvc",
  28. }
  29. return platformMap[`${platform}-${arch}`] ?? null
  30. }
  31. // Single source of truth for resolved CLI path
  32. let resolvedCliPath: string | null = null
  33. export function findSgCliPathSync(): string | null {
  34. const binaryName = process.platform === "win32" ? "sg.exe" : "sg"
  35. const cachedPath = getCachedBinaryPath()
  36. if (cachedPath && isValidBinary(cachedPath)) {
  37. return cachedPath
  38. }
  39. try {
  40. const require = createRequire(import.meta.url)
  41. const cliPkgPath = require.resolve("@ast-grep/cli/package.json")
  42. const cliDir = dirname(cliPkgPath)
  43. const sgPath = join(cliDir, binaryName)
  44. if (existsSync(sgPath) && isValidBinary(sgPath)) {
  45. return sgPath
  46. }
  47. } catch {
  48. // @ast-grep/cli not installed
  49. }
  50. const platformPkg = getPlatformPackageName()
  51. if (platformPkg) {
  52. try {
  53. const require = createRequire(import.meta.url)
  54. const pkgPath = require.resolve(`${platformPkg}/package.json`)
  55. const pkgDir = dirname(pkgPath)
  56. const astGrepName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep"
  57. const binaryPath = join(pkgDir, astGrepName)
  58. if (existsSync(binaryPath) && isValidBinary(binaryPath)) {
  59. return binaryPath
  60. }
  61. } catch {
  62. // Platform-specific package not installed
  63. }
  64. }
  65. if (process.platform === "darwin") {
  66. const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"]
  67. for (const path of homebrewPaths) {
  68. if (existsSync(path) && isValidBinary(path)) {
  69. return path
  70. }
  71. }
  72. }
  73. return null
  74. }
  75. export function getSgCliPath(): string {
  76. if (resolvedCliPath !== null) {
  77. return resolvedCliPath
  78. }
  79. const syncPath = findSgCliPathSync()
  80. if (syncPath) {
  81. resolvedCliPath = syncPath
  82. return syncPath
  83. }
  84. return "sg"
  85. }
  86. export function setSgCliPath(path: string): void {
  87. resolvedCliPath = path
  88. }
  89. // Re-export language constants
  90. export { CLI_LANGUAGES }
  91. // Defaults
  92. export const DEFAULT_TIMEOUT_MS = 300_000
  93. export const DEFAULT_MAX_OUTPUT_BYTES = 1 * 1024 * 1024
  94. export const DEFAULT_MAX_MATCHES = 500
  95. export const LANG_EXTENSIONS: Record<string, string[]> = {
  96. bash: [".bash", ".sh", ".zsh", ".bats"],
  97. c: [".c", ".h"],
  98. cpp: [".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h"],
  99. csharp: [".cs"],
  100. css: [".css"],
  101. elixir: [".ex", ".exs"],
  102. go: [".go"],
  103. haskell: [".hs", ".lhs"],
  104. html: [".html", ".htm"],
  105. java: [".java"],
  106. javascript: [".js", ".jsx", ".mjs", ".cjs"],
  107. json: [".json"],
  108. kotlin: [".kt", ".kts"],
  109. lua: [".lua"],
  110. nix: [".nix"],
  111. php: [".php"],
  112. python: [".py", ".pyi"],
  113. ruby: [".rb", ".rake"],
  114. rust: [".rs"],
  115. scala: [".scala", ".sc"],
  116. solidity: [".sol"],
  117. swift: [".swift"],
  118. typescript: [".ts", ".cts", ".mts"],
  119. tsx: [".tsx"],
  120. yaml: [".yml", ".yaml"],
  121. }
  122. export interface EnvironmentCheckResult {
  123. cli: {
  124. available: boolean
  125. path: string
  126. error?: string
  127. }
  128. }
  129. /**
  130. * Check if ast-grep CLI is available.
  131. * Call this at startup to provide early feedback about missing dependencies.
  132. */
  133. export function checkEnvironment(): EnvironmentCheckResult {
  134. const cliPath = getSgCliPath()
  135. const result: EnvironmentCheckResult = {
  136. cli: {
  137. available: false,
  138. path: cliPath,
  139. },
  140. }
  141. if (existsSync(cliPath)) {
  142. result.cli.available = true
  143. } else if (cliPath === "sg") {
  144. try {
  145. const whichResult = spawnSync(process.platform === "win32" ? "where" : "which", ["sg"], {
  146. encoding: "utf-8",
  147. timeout: 5000,
  148. })
  149. result.cli.available = whichResult.status === 0 && !!whichResult.stdout?.trim()
  150. if (!result.cli.available) {
  151. result.cli.error = "sg binary not found in PATH"
  152. }
  153. } catch {
  154. result.cli.error = "Failed to check sg availability"
  155. }
  156. } else {
  157. result.cli.error = `Binary not found: ${cliPath}`
  158. }
  159. return result
  160. }
  161. /**
  162. * Format environment check result as user-friendly message.
  163. */
  164. export function formatEnvironmentCheck(result: EnvironmentCheckResult): string {
  165. const lines: string[] = ["ast-grep Environment Status:", ""]
  166. if (result.cli.available) {
  167. lines.push(`✓ CLI: Available (${result.cli.path})`)
  168. } else {
  169. lines.push(`✗ CLI: Not available`)
  170. if (result.cli.error) {
  171. lines.push(` Error: ${result.cli.error}`)
  172. }
  173. lines.push(` Install: bun add -D @ast-grep/cli`)
  174. }
  175. lines.push("")
  176. lines.push(`CLI supports ${CLI_LANGUAGES.length} languages`)
  177. return lines.join("\n")
  178. }