1
- import { ChildProcess , spawn } from 'child_process' ;
2
1
import {
3
2
LlmConstrainedOutputGenerateResponse ,
4
- LlmGenerateFilesContext ,
5
3
LlmGenerateFilesRequestOptions ,
6
- LlmGenerateFilesResponse ,
7
4
LlmGenerateTextResponse ,
8
5
LlmRunner ,
9
6
} 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' ;
12
9
import { writeFile } from 'fs/promises' ;
13
10
import {
14
11
getGeminiIgnoreFile ,
15
12
getGeminiInstructionsFile ,
16
13
getGeminiSettingsFile ,
17
14
} from './gemini-files.js' ;
18
- import { DirectorySnapshot } from '../directory-snapshot.js' ;
19
- import { LlmResponseFile } from '../../shared-interfaces.js' ;
20
15
import { UserFacingError } from '../../utils/errors.js' ;
21
- import assert from 'assert ' ;
16
+ import { BaseCliAgentRunner } from '../base-cli-agent-runner.js ' ;
22
17
23
18
const SUPPORTED_MODELS = [ 'gemini-2.5-pro' , 'gemini-2.5-flash' , 'gemini-2.5-flash-lite' ] ;
24
19
25
20
/** Runner that generates code using the Gemini CLI. */
26
- export class GeminiCliRunner implements LlmRunner {
21
+ export class GeminiCliRunner extends BaseCliAgentRunner implements LlmRunner {
27
22
readonly id = 'gemini-cli' ;
28
23
readonly displayName = 'Gemini CLI' ;
29
24
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' ;
96
27
97
28
generateText ( ) : Promise < LlmGenerateTextResponse > {
98
29
// 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 {
109
40
return SUPPORTED_MODELS ;
110
41
}
111
42
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
+ ] ;
157
53
}
158
54
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' ) ;
203
60
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 ) ;
208
62
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 ( ) ) ] ;
216
64
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
+ ) ,
232
71
) ;
72
+ }
233
73
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 ) ,
236
78
) ;
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
+ }
251
80
}
252
81
}
0 commit comments