@@ -44,8 +44,11 @@ export async function isCliAvailable(): Promise<boolean> {
4444
4545/**
4646 * Run ast-grep CLI command
47+ *
48+ * @param options - Command options
49+ * @param retried - Internal flag to prevent infinite retry loops
4750 */
48- export async function runSg ( options : RunSgOptions ) : Promise < SgResult > {
51+ export async function runSg ( options : RunSgOptions , retried = false ) : Promise < SgResult > {
4952 const cliPath = await getAstGrepPath ( )
5053
5154 if ( ! cliPath ) {
@@ -117,7 +120,8 @@ export async function runSg(options: RunSgOptions): Promise<SgResult> {
117120 proc . kill ( )
118121 reject ( new Error ( `Search timeout after ${ timeout } ms` ) )
119122 } , timeout )
120- proc . exited . then ( ( ) => clearTimeout ( id ) )
123+ // Use .finally() to ensure cleanup even if proc.exited rejects
124+ proc . exited . finally ( ( ) => clearTimeout ( id ) )
121125 } )
122126
123127 let stdout : string
@@ -147,20 +151,20 @@ export async function runSg(options: RunSgOptions): Promise<SgResult> {
147151 || nodeError . message ?. includes ( 'ENOENT' )
148152 || nodeError . message ?. includes ( 'not found' )
149153 ) {
150- // Binary not found, try to download
151- const downloadedPath = await ensureAstGrepBinary ( )
152- if ( downloadedPath ) {
153- resolvedCliPath = downloadedPath
154- return runSg ( options ) // Retry
155- }
156- else {
157- return {
158- matches : [ ] ,
159- totalMatches : 0 ,
160- truncated : false ,
161- error : getInstallInstructions ( ) ,
154+ // Binary not found, try to download (only once to prevent infinite loops)
155+ if ( ! retried ) {
156+ const downloadedPath = await ensureAstGrepBinary ( )
157+ if ( downloadedPath ) {
158+ resolvedCliPath = downloadedPath
159+ return runSg ( options , true ) // Retry once
162160 }
163161 }
162+ return {
163+ matches : [ ] ,
164+ totalMatches : 0 ,
165+ truncated : false ,
166+ error : getInstallInstructions ( ) ,
167+ }
164168 }
165169
166170 return {
@@ -179,7 +183,13 @@ export async function runSg(options: RunSgOptions): Promise<SgResult> {
179183 if ( stderr . trim ( ) ) {
180184 return { matches : [ ] , totalMatches : 0 , truncated : false , error : stderr . trim ( ) }
181185 }
182- return { matches : [ ] , totalMatches : 0 , truncated : false }
186+ // Non-zero exit with no output - include exit code for diagnosis
187+ return {
188+ matches : [ ] ,
189+ totalMatches : 0 ,
190+ truncated : false ,
191+ error : `ast-grep exited with code ${ exitCode } (no output)` ,
192+ }
183193 }
184194
185195 // No output
@@ -196,7 +206,7 @@ export async function runSg(options: RunSgOptions): Promise<SgResult> {
196206 try {
197207 matches = JSON . parse ( outputToProcess ) as CliMatch [ ]
198208 }
199- catch {
209+ catch ( parseError ) {
200210 if ( outputTruncated ) {
201211 // Try to parse partial JSON
202212 try {
@@ -209,18 +219,27 @@ export async function runSg(options: RunSgOptions): Promise<SgResult> {
209219 }
210220 }
211221 }
212- catch {
222+ catch ( recoveryError ) {
223+ const errorMsg = recoveryError instanceof Error ? recoveryError . message : 'unknown'
213224 return {
214225 matches : [ ] ,
215226 totalMatches : 0 ,
216227 truncated : true ,
217228 truncatedReason : 'max_output_bytes' ,
218- error : ' Output too large and could not be parsed' ,
229+ error : ` Output too large and could not be parsed: ${ errorMsg } ` ,
219230 }
220231 }
221232 }
222233 else {
223- return { matches : [ ] , totalMatches : 0 , truncated : false }
234+ // Non-truncated output but failed to parse - log and return error
235+ const errorMsg = parseError instanceof Error ? parseError . message : 'unknown'
236+ console . error ( `[ast-grep] Failed to parse output: ${ errorMsg } ` )
237+ return {
238+ matches : [ ] ,
239+ totalMatches : 0 ,
240+ truncated : false ,
241+ error : `Failed to parse ast-grep output: ${ errorMsg } ` ,
242+ }
224243 }
225244 }
226245
0 commit comments