11import { createRequire } from "module"
22import { dirname , join } from "path"
33import { existsSync } from "fs"
4+ import { getCachedBinaryPath } from "./downloader"
45
56type Platform = "darwin" | "linux" | "win32" | "unsupported"
67
@@ -21,30 +22,30 @@ function getPlatformPackageName(): string | null {
2122 return platformMap [ `${ platform } -${ arch } ` ] ?? null
2223}
2324
24- function findSgCliPath ( ) : string {
25- // 1. Try to find from @ast-grep/cli package (installed via npm)
25+ export function findSgCliPathSync ( ) : string | null {
26+ const binaryName = process . platform === "win32" ? "sg.exe" : "sg"
27+
2628 try {
2729 const require = createRequire ( import . meta. url )
2830 const cliPkgPath = require . resolve ( "@ast-grep/cli/package.json" )
2931 const cliDir = dirname ( cliPkgPath )
30- const sgPath = join ( cliDir , process . platform === "win32" ? "sg.exe" : "sg" )
32+ const sgPath = join ( cliDir , binaryName )
3133
3234 if ( existsSync ( sgPath ) ) {
3335 return sgPath
3436 }
3537 } catch {
36- // @ast -grep/cli not installed, try platform-specific package
38+ // @ast -grep/cli not installed
3739 }
3840
39- // 2. Try platform-specific package directly
4041 const platformPkg = getPlatformPackageName ( )
4142 if ( platformPkg ) {
4243 try {
4344 const require = createRequire ( import . meta. url )
4445 const pkgPath = require . resolve ( `${ platformPkg } /package.json` )
4546 const pkgDir = dirname ( pkgPath )
46- const binaryName = process . platform === "win32" ? "ast-grep.exe" : "ast-grep"
47- const binaryPath = join ( pkgDir , binaryName )
47+ const astGrepName = process . platform === "win32" ? "ast-grep.exe" : "ast-grep"
48+ const binaryPath = join ( pkgDir , astGrepName )
4849
4950 if ( existsSync ( binaryPath ) ) {
5051 return binaryPath
@@ -54,12 +55,44 @@ function findSgCliPath(): string {
5455 }
5556 }
5657
57- // 3. Fallback to system PATH
58+ if ( process . platform === "darwin" ) {
59+ const homebrewPaths = [ "/opt/homebrew/bin/sg" , "/usr/local/bin/sg" ]
60+ for ( const path of homebrewPaths ) {
61+ if ( existsSync ( path ) ) {
62+ return path
63+ }
64+ }
65+ }
66+
67+ const cachedPath = getCachedBinaryPath ( )
68+ if ( cachedPath ) {
69+ return cachedPath
70+ }
71+
72+ return null
73+ }
74+
75+ let resolvedCliPath : string | null = null
76+
77+ export function getSgCliPath ( ) : string {
78+ if ( resolvedCliPath !== null ) {
79+ return resolvedCliPath
80+ }
81+
82+ const syncPath = findSgCliPathSync ( )
83+ if ( syncPath ) {
84+ resolvedCliPath = syncPath
85+ return syncPath
86+ }
87+
5888 return "sg"
5989}
6090
61- // ast-grep CLI path (auto-detected from node_modules or system PATH)
62- export const SG_CLI_PATH = findSgCliPath ( )
91+ export function setSgCliPath ( path : string ) : void {
92+ resolvedCliPath = path
93+ }
94+
95+ export const SG_CLI_PATH = getSgCliPath ( )
6396
6497// CLI supported languages (25 total)
6598export const CLI_LANGUAGES = [
@@ -121,3 +154,99 @@ export const LANG_EXTENSIONS: Record<string, string[]> = {
121154 tsx : [ ".tsx" ] ,
122155 yaml : [ ".yml" , ".yaml" ] ,
123156}
157+
158+ export interface EnvironmentCheckResult {
159+ cli : {
160+ available : boolean
161+ path : string
162+ error ?: string
163+ }
164+ napi : {
165+ available : boolean
166+ error ?: string
167+ }
168+ }
169+
170+ /**
171+ * Check if ast-grep CLI and NAPI are available.
172+ * Call this at startup to provide early feedback about missing dependencies.
173+ */
174+ export function checkEnvironment ( ) : EnvironmentCheckResult {
175+ const result : EnvironmentCheckResult = {
176+ cli : {
177+ available : false ,
178+ path : SG_CLI_PATH ,
179+ } ,
180+ napi : {
181+ available : false ,
182+ } ,
183+ }
184+
185+ // Check CLI availability
186+ if ( existsSync ( SG_CLI_PATH ) ) {
187+ result . cli . available = true
188+ } else if ( SG_CLI_PATH === "sg" ) {
189+ // Fallback path - try which/where to find in PATH
190+ try {
191+ const { spawnSync } = require ( "child_process" )
192+ const whichResult = spawnSync ( process . platform === "win32" ? "where" : "which" , [ "sg" ] , {
193+ encoding : "utf-8" ,
194+ timeout : 5000 ,
195+ } )
196+ result . cli . available = whichResult . status === 0 && ! ! whichResult . stdout ?. trim ( )
197+ if ( ! result . cli . available ) {
198+ result . cli . error = "sg binary not found in PATH"
199+ }
200+ } catch {
201+ result . cli . error = "Failed to check sg availability"
202+ }
203+ } else {
204+ result . cli . error = `Binary not found: ${ SG_CLI_PATH } `
205+ }
206+
207+ // Check NAPI availability
208+ try {
209+ require ( "@ast-grep/napi" )
210+ result . napi . available = true
211+ } catch ( e ) {
212+ result . napi . available = false
213+ result . napi . error = `@ast-grep/napi not installed: ${ e instanceof Error ? e . message : String ( e ) } `
214+ }
215+
216+ return result
217+ }
218+
219+ /**
220+ * Format environment check result as user-friendly message.
221+ */
222+ export function formatEnvironmentCheck ( result : EnvironmentCheckResult ) : string {
223+ const lines : string [ ] = [ "ast-grep Environment Status:" , "" ]
224+
225+ // CLI status
226+ if ( result . cli . available ) {
227+ lines . push ( `✓ CLI: Available (${ result . cli . path } )` )
228+ } else {
229+ lines . push ( `✗ CLI: Not available` )
230+ if ( result . cli . error ) {
231+ lines . push ( ` Error: ${ result . cli . error } ` )
232+ }
233+ lines . push ( ` Install: bun add -D @ast-grep/cli` )
234+ }
235+
236+ // NAPI status
237+ if ( result . napi . available ) {
238+ lines . push ( `✓ NAPI: Available` )
239+ } else {
240+ lines . push ( `✗ NAPI: Not available` )
241+ if ( result . napi . error ) {
242+ lines . push ( ` Error: ${ result . napi . error } ` )
243+ }
244+ lines . push ( ` Install: bun add -D @ast-grep/napi` )
245+ }
246+
247+ lines . push ( "" )
248+ lines . push ( `CLI supports ${ CLI_LANGUAGES . length } languages` )
249+ lines . push ( `NAPI supports ${ NAPI_LANGUAGES . length } languages: ${ NAPI_LANGUAGES . join ( ", " ) } ` )
250+
251+ return lines . join ( "\n" )
252+ }
0 commit comments