1- import { ChildProcess , spawn } from 'child_process' ;
21import {
32 LlmConstrainedOutputGenerateResponse ,
4- LlmGenerateFilesContext ,
53 LlmGenerateFilesRequestOptions ,
6- LlmGenerateFilesResponse ,
74 LlmGenerateTextResponse ,
85 LlmRunner ,
96} from '../llm-runner.js' ;
10- import { join , relative } from 'path' ;
11- import { existsSync , mkdirSync } from 'fs' ;
7+ import { join } from 'path' ;
8+ import { mkdirSync } from 'fs' ;
129import { writeFile } from 'fs/promises' ;
1310import {
1411 getGeminiIgnoreFile ,
1512 getGeminiInstructionsFile ,
1613 getGeminiSettingsFile ,
1714} from './gemini-files.js' ;
18- import { DirectorySnapshot } from '../directory-snapshot.js' ;
19- import { LlmResponseFile } from '../../shared-interfaces.js' ;
2015import { UserFacingError } from '../../utils/errors.js' ;
21- import assert from 'assert ' ;
16+ import { BaseCliAgentRunner } from '../base-cli-agent-runner.js ' ;
2217
2318const SUPPORTED_MODELS = [ 'gemini-2.5-pro' , 'gemini-2.5-flash' , 'gemini-2.5-flash-lite' ] ;
2419
2520/** Runner that generates code using the Gemini CLI. */
26- export class GeminiCliRunner implements LlmRunner {
21+ export class GeminiCliRunner extends BaseCliAgentRunner implements LlmRunner {
2722 readonly id = 'gemini-cli' ;
2823 readonly displayName = 'Gemini CLI' ;
2924 readonly hasBuiltInRepairLoop = true ;
30- private pendingTimeouts = new Set < ReturnType < typeof setTimeout > > ( ) ;
31- private pendingProcesses = new Set < ChildProcess > ( ) ;
32- private binaryPath = this . resolveBinaryPath ( ) ;
33- private evalIgnoredPatterns = [
34- '**/node_modules/**' ,
35- '**/dist/**' ,
36- '**/.angular/**' ,
37- '**/GEMINI.md' ,
38- '**/.geminiignore' ,
39- ] ;
40-
41- async generateFiles ( options : LlmGenerateFilesRequestOptions ) : Promise < LlmGenerateFilesResponse > {
42- const { context, model} = options ;
43-
44- // TODO: Consider removing these assertions when we have better types here.
45- // These fields are always set when running in a local environment, and this
46- // is a requirement for selecting the `gemini-cli` runner.
47- assert (
48- context . buildCommand ,
49- 'Expected a `buildCommand` to be set in the LLM generate request context' ,
50- ) ;
51- assert (
52- context . packageManager ,
53- 'Expected a `packageManager` to be set in the LLM generate request context' ,
54- ) ;
55-
56- const ignoreFilePath = join ( context . directory , '.geminiignore' ) ;
57- const instructionFilePath = join ( context . directory , 'GEMINI.md' ) ;
58- const settingsDir = join ( context . directory , '.gemini' ) ;
59- const initialSnapshot = await DirectorySnapshot . forDirectory (
60- context . directory ,
61- this . evalIgnoredPatterns ,
62- ) ;
63-
64- mkdirSync ( settingsDir ) ;
65-
66- await Promise . all ( [
67- writeFile ( ignoreFilePath , getGeminiIgnoreFile ( ) ) ,
68- writeFile (
69- instructionFilePath ,
70- getGeminiInstructionsFile ( context . systemInstructions , context . buildCommand ) ,
71- ) ,
72- writeFile (
73- join ( settingsDir , 'settings.json' ) ,
74- getGeminiSettingsFile ( context . packageManager , context . possiblePackageManagers ) ,
75- ) ,
76- ] ) ;
77-
78- const reasoning = await this . runGeminiProcess ( model , context , 2 , 10 ) ;
79- const finalSnapshot = await DirectorySnapshot . forDirectory (
80- context . directory ,
81- this . evalIgnoredPatterns ,
82- ) ;
83-
84- const diff = finalSnapshot . getChangedOrAddedFiles ( initialSnapshot ) ;
85- const files : LlmResponseFile [ ] = [ ] ;
86-
87- for ( const [ absolutePath , code ] of diff ) {
88- files . push ( {
89- filePath : relative ( context . directory , absolutePath ) ,
90- code,
91- } ) ;
92- }
93-
94- return { files, reasoning, toolLogs : [ ] } ;
95- }
25+ protected ignoredFilePatterns = [ '**/GEMINI.md' , '**/.geminiignore' ] ;
26+ protected binaryName = 'gemini' ;
9627
9728 generateText ( ) : Promise < LlmGenerateTextResponse > {
9829 // Technically we can make this work, but we don't need it at the time of writing.
@@ -109,144 +40,42 @@ export class GeminiCliRunner implements LlmRunner {
10940 return SUPPORTED_MODELS ;
11041 }
11142
112- async dispose ( ) : Promise < void > {
113- for ( const timeout of this . pendingTimeouts ) {
114- clearTimeout ( timeout ) ;
115- }
116-
117- for ( const childProcess of this . pendingProcesses ) {
118- childProcess . kill ( 'SIGKILL' ) ;
119- }
120-
121- this . pendingTimeouts . clear ( ) ;
122- this . pendingProcesses . clear ( ) ;
123- }
124-
125- private resolveBinaryPath ( ) : string {
126- let dir = import . meta. dirname ;
127- let closestRoot : string | null = null ;
128-
129- // Attempt to resolve the Gemini CLI binary by starting at the current file and going up until
130- // we find the closest `node_modules`. Note that we can't rely on `import.meta.resolve` here,
131- // because that'll point us to the Gemini CLI bundle, but not its binary. In some package
132- // managers (pnpm specifically) the `node_modules` in which the file is installed is different
133- // from the one in which the binary is placed.
134- while ( dir . length > 1 ) {
135- if ( existsSync ( join ( dir , 'node_modules' ) ) ) {
136- closestRoot = dir ;
137- break ;
138- }
139-
140- const parent = join ( dir , '..' ) ;
141-
142- if ( parent === dir ) {
143- // We've reached the root, stop traversing.
144- break ;
145- } else {
146- dir = parent ;
147- }
148- }
149-
150- const binaryPath = closestRoot ? join ( closestRoot , 'node_modules/.bin/gemini' ) : null ;
151-
152- if ( ! binaryPath || ! existsSync ( binaryPath ) ) {
153- throw new UserFacingError ( 'Gemini CLI is not installed inside the current project' ) ;
154- }
155-
156- return binaryPath ;
43+ protected getCommandLineFlags ( options : LlmGenerateFilesRequestOptions ) : string [ ] {
44+ return [
45+ '--prompt' ,
46+ options . context . executablePrompt ,
47+ '--model' ,
48+ options . model ,
49+ // Skip all confirmations.
50+ '--approval-mode' ,
51+ 'yolo' ,
52+ ] ;
15753 }
15854
159- private runGeminiProcess (
160- model : string ,
161- context : LlmGenerateFilesContext ,
162- inactivityTimeoutMins : number ,
163- totalRequestTimeoutMins : number ,
164- ) : Promise < string > {
165- return new Promise < string > ( resolve => {
166- let stdoutBuffer = '' ;
167- let stdErrBuffer = '' ;
168- let isDone = false ;
169- const msPerMin = 1000 * 60 ;
170- const finalize = ( finalMessage : string ) => {
171- if ( isDone ) {
172- return ;
173- }
174-
175- isDone = true ;
176-
177- if ( inactivityTimeout ) {
178- clearTimeout ( inactivityTimeout ) ;
179- this . pendingTimeouts . delete ( inactivityTimeout ) ;
180- }
181-
182- clearTimeout ( globalTimeout ) ;
183- childProcess . kill ( 'SIGKILL' ) ;
184- this . pendingTimeouts . delete ( globalTimeout ) ;
185- this . pendingProcesses . delete ( childProcess ) ;
186-
187- const separator = '\n--------------------------------------------------\n' ;
188-
189- if ( stdErrBuffer . length > 0 ) {
190- stdoutBuffer += separator + 'Stderr output:\n' + stdErrBuffer ;
191- }
192-
193- stdoutBuffer += separator + finalMessage ;
194- resolve ( stdoutBuffer ) ;
195- } ;
196-
197- const noOutputCallback = ( ) => {
198- finalize (
199- `There was no output from Gemini CLI for ${ inactivityTimeoutMins } minute(s). ` +
200- `Stopping the process...` ,
201- ) ;
202- } ;
55+ protected async writeAgentFiles ( options : LlmGenerateFilesRequestOptions ) : Promise < void > {
56+ const { context} = options ;
57+ const ignoreFilePath = join ( context . directory , '.geminiignore' ) ;
58+ const instructionFilePath = join ( context . directory , 'GEMINI.md' ) ;
59+ const settingsDir = join ( context . directory , '.gemini' ) ;
20360
204- // Gemini can get into a state where it stops outputting code, but it also doesn't exit
205- // the process. Stop if there hasn't been any output for a certain amount of time.
206- let inactivityTimeout = setTimeout ( noOutputCallback , inactivityTimeoutMins * msPerMin ) ;
207- this . pendingTimeouts . add ( inactivityTimeout ) ;
61+ mkdirSync ( settingsDir ) ;
20862
209- // Also add a timeout for the entire codegen process.
210- const globalTimeout = setTimeout ( ( ) => {
211- finalize (
212- `Gemini CLI didn't finish within ${ totalRequestTimeoutMins } minute(s). ` +
213- `Stopping the process...` ,
214- ) ;
215- } , totalRequestTimeoutMins * msPerMin ) ;
63+ const promises : Promise < unknown > [ ] = [ writeFile ( ignoreFilePath , getGeminiIgnoreFile ( ) ) ] ;
21664
217- const childProcess = spawn (
218- this . binaryPath ,
219- [
220- '--prompt' ,
221- context . executablePrompt ,
222- '--model' ,
223- model ,
224- // Skip all confirmations.
225- '--approval-mode' ,
226- 'yolo' ,
227- ] ,
228- {
229- cwd : context . directory ,
230- env : { ...process . env } ,
231- } ,
65+ if ( context . buildCommand ) {
66+ promises . push (
67+ writeFile (
68+ instructionFilePath ,
69+ getGeminiInstructionsFile ( context . systemInstructions , context . buildCommand ) ,
70+ ) ,
23271 ) ;
72+ }
23373
234- childProcess . on ( 'close' , code =>
235- finalize ( 'Gemini CLI process has exited' + ( code == null ? '.' : ` with ${ code } code.` ) ) ,
74+ if ( context . packageManager ) {
75+ writeFile (
76+ join ( settingsDir , 'settings.json' ) ,
77+ getGeminiSettingsFile ( context . packageManager , context . possiblePackageManagers ) ,
23678 ) ;
237- childProcess . stdout . on ( 'data' , data => {
238- if ( inactivityTimeout ) {
239- this . pendingTimeouts . delete ( inactivityTimeout ) ;
240- clearTimeout ( inactivityTimeout ) ;
241- }
242-
243- stdoutBuffer += data . toString ( ) ;
244- inactivityTimeout = setTimeout ( noOutputCallback , inactivityTimeoutMins * msPerMin ) ;
245- this . pendingTimeouts . add ( inactivityTimeout ) ;
246- } ) ;
247- childProcess . stderr . on ( 'data' , data => {
248- stdErrBuffer += data . toString ( ) ;
249- } ) ;
250- } ) ;
79+ }
25180 }
25281}
0 commit comments