16
16
17
17
import { Runtime } from '@genkit-ai/tools-common/manager' ;
18
18
import { logger } from '@genkit-ai/tools-common/utils' ;
19
- import { select } from '@inquirer/prompts' ;
20
19
import { existsSync , readFileSync } from 'fs' ;
21
- import { mkdir , writeFile } from 'fs/promises' ;
22
- import path from 'path' ;
23
- import { AIToolConfigResult , AIToolModule , InitConfigOptions } from '../types' ;
24
20
import {
25
- GENKIT_PROMPT_PATH ,
26
- initGenkitFile ,
27
- initOrReplaceFile ,
28
- updateContentInPlace ,
29
- } from '../utils' ;
21
+ copyFile ,
22
+ lstat ,
23
+ mkdir ,
24
+ readlink ,
25
+ symlink ,
26
+ writeFile ,
27
+ } from 'fs/promises' ;
28
+ import * as path from 'path' ;
29
+ import { AIToolConfigResult , AIToolModule , InitConfigOptions } from '../types' ;
30
+ import { GENKIT_PROMPT_PATH , initGenkitFile } from '../utils' ;
30
31
31
32
// GEMINI specific paths
32
33
const GEMINI_DIR = '.gemini' ;
33
34
const GEMINI_SETTINGS_PATH = path . join ( GEMINI_DIR , 'settings.json' ) ;
34
- const GEMINI_MD_PATH = path . join ( 'GEMINI.md' ) ;
35
+ const GENKIT_MD_SYMLINK_PATH = path . join ( GEMINI_DIR , GENKIT_PROMPT_PATH ) ;
35
36
36
37
// GENKIT specific constants
37
- const GENKIT_EXT_DIR = path . join ( GEMINI_DIR , 'extensions' , 'genkit' ) ;
38
- const GENKIT_MD_REL_PATH = path . join ( '..' , '..' , '..' , GENKIT_PROMPT_PATH ) ;
39
- const GENKIT_EXTENSION_CONFIG = {
40
- name : 'genkit' ,
41
- version : '1.0.0' ,
42
- mcpServers : {
43
- genkit : {
44
- command : 'genkit' ,
45
- args : [ 'mcp' , '--no-update-notification' ] ,
46
- cwd : '.' ,
47
- timeout : 30000 ,
48
- trust : false ,
49
- excludeTools : [
50
- 'run_shell_command(genkit start)' ,
51
- 'run_shell_command(npx genkit start)' ,
52
- ] ,
53
- } ,
54
- } ,
55
- contextFileName : GENKIT_MD_REL_PATH ,
38
+ const GENKIT_MCP_CONFIG = {
39
+ command : 'genkit' ,
40
+ args : [ 'mcp' , '--no-update-notification' ] ,
41
+ cwd : '.' ,
42
+ timeout : 30000 ,
43
+ trust : false ,
44
+ excludeTools : [
45
+ 'run_shell_command(genkit start)' ,
46
+ 'run_shell_command(npx genkit start)' ,
47
+ ] ,
56
48
} ;
57
49
58
- const EXT_INSTALLATION = 'extension' ;
59
- const MD_INSTALLATION = 'geminimd' ;
60
- type InstallationType = typeof EXT_INSTALLATION | typeof MD_INSTALLATION ;
61
-
62
50
/** Configuration module for Gemini CLI */
63
51
export const gemini : AIToolModule = {
64
52
name : 'gemini' ,
@@ -71,33 +59,8 @@ export const gemini: AIToolModule = {
71
59
runtime : Runtime ,
72
60
options ?: InitConfigOptions
73
61
) : Promise < AIToolConfigResult > {
74
- let installationMethod : InstallationType = EXT_INSTALLATION ;
75
- if ( ! options ?. yesMode ) {
76
- installationMethod = await select ( {
77
- message : 'Select your preferred installation method' ,
78
- choices : [
79
- {
80
- name : 'Gemini CLI Extension' ,
81
- value : 'extension' ,
82
- description :
83
- 'Use Gemini Extension to install Genkit context in a modular fashion' ,
84
- } ,
85
- {
86
- name : 'GEMINI.md' ,
87
- value : 'geminimd' ,
88
- description : 'Incorporate Genkit context within the GEMINI.md file' ,
89
- } ,
90
- ] ,
91
- } ) ;
92
- }
93
-
94
- if ( installationMethod === EXT_INSTALLATION ) {
95
- logger . info ( 'Installing as part of GEMINI.md' ) ;
96
- return await installAsExtension ( runtime ) ;
97
- } else {
98
- logger . info ( 'Installing as Gemini CLI extension' ) ;
99
- return await installInMdFile ( runtime ) ;
100
- }
62
+ logger . info ( 'Installing as part of GEMINI.md' ) ;
63
+ return await installInMdFile ( runtime ) ;
101
64
} ,
102
65
} ;
103
66
@@ -125,8 +88,19 @@ async function installInMdFile(runtime: Runtime): Promise<AIToolConfigResult> {
125
88
if ( ! existingConfig . mcpServers ) {
126
89
existingConfig . mcpServers = { } ;
127
90
}
128
- existingConfig . mcpServers . genkit =
129
- GENKIT_EXTENSION_CONFIG . mcpServers . genkit ;
91
+ existingConfig . mcpServers . genkit = GENKIT_MCP_CONFIG ;
92
+
93
+ if ( existingConfig . contextFileName ) {
94
+ const contextFiles = Array . isArray ( existingConfig . contextFileName )
95
+ ? [ ...existingConfig . contextFileName ]
96
+ : [ existingConfig . contextFileName ] ;
97
+ if ( ! contextFiles . includes ( 'GENKIT.md' ) ) {
98
+ contextFiles . push ( 'GENKIT.md' ) ;
99
+ }
100
+ existingConfig . contextFileName = contextFiles ;
101
+ } else {
102
+ existingConfig . contextFileName = 'GENKIT.md' ;
103
+ }
130
104
await writeFile (
131
105
GEMINI_SETTINGS_PATH ,
132
106
JSON . stringify ( existingConfig , null , 2 )
@@ -140,48 +114,104 @@ async function installInMdFile(runtime: Runtime): Promise<AIToolConfigResult> {
140
114
const baseResult = await initGenkitFile ( runtime ) ;
141
115
files . push ( { path : GENKIT_PROMPT_PATH , updated : baseResult . updated } ) ;
142
116
143
- logger . info ( 'Updating GEMINI.md to include Genkit context' ) ;
144
- const geminiImportTag = `\nGenkit Framework Instructions:\n - @./GENKIT.md\n` ;
145
- const { updated : mdUpdated } = await updateContentInPlace (
146
- GEMINI_MD_PATH ,
147
- geminiImportTag ,
148
- { hash : baseResult . hash }
149
- ) ;
150
- files . push ( { path : GEMINI_MD_PATH , updated : mdUpdated } ) ;
117
+ // Create link/copy of GENKIT.md in .gemini folder
118
+ const linkResult = await createGenkitMdLink ( ) ;
119
+ files . push ( linkResult ) ;
151
120
152
121
return { files } ;
153
122
}
154
123
155
- async function installAsExtension (
156
- runtime : Runtime
157
- ) : Promise < AIToolConfigResult > {
158
- const files : AIToolConfigResult [ 'files' ] = [ ] ;
159
- // Part 1: Generate GENKIT.md file.
160
- const baseResult = await initGenkitFile ( runtime ) ;
161
- files . push ( { path : GENKIT_PROMPT_PATH , updated : baseResult . updated } ) ;
124
+ /**
125
+ * Creates a link to GENKIT.md in the .gemini folder.
126
+ * On Windows, copies the file instead of creating a symlink.
127
+ * On Unix systems, creates a symlink.
128
+ */
129
+ async function createGenkitMdLink ( ) : Promise < {
130
+ path : string ;
131
+ updated : boolean ;
132
+ } > {
133
+ const sourcePath = GENKIT_PROMPT_PATH ;
134
+ const targetPath = GENKIT_MD_SYMLINK_PATH ;
135
+ const isWindows = process . platform === 'win32' ;
136
+
137
+ if ( isWindows ) {
138
+ logger . info (
139
+ 'Copying GENKIT.md to .gemini folder (Windows compatibility mode)'
140
+ ) ;
162
141
163
- // Part 2: Configure the main gemini-extension.json file, and gemini config directory if needed.
164
- logger . info ( 'Configuring extentions files in user workspace' ) ;
165
- await mkdir ( GENKIT_EXT_DIR , { recursive : true } ) ;
166
- const extensionPath = path . join ( GENKIT_EXT_DIR , 'gemini-extension.json' ) ;
142
+ // Check if file already exists
143
+ try {
144
+ const stats = await lstat ( targetPath ) ;
145
+ if ( stats . isFile ( ) ) {
146
+ logger . info ( 'GENKIT.md copy already exists in .gemini folder' ) ;
147
+ return { path : targetPath , updated : false } ;
148
+ }
149
+ } catch ( error : any ) {
150
+ if ( error . code !== 'ENOENT' ) {
151
+ logger . error ( 'Error checking GENKIT.md copy:' , error ) ;
152
+ throw error ;
153
+ }
154
+ // File doesn't exist, proceed with copying
155
+ }
167
156
168
- let extensionUpdated = false ;
169
- try {
170
- const { updated } = await initOrReplaceFile (
171
- extensionPath ,
172
- JSON . stringify ( GENKIT_EXTENSION_CONFIG , null , 2 )
173
- ) ;
174
- extensionUpdated = updated ;
175
- if ( extensionUpdated ) {
176
- logger . info (
177
- `Genkit extension for Gemini CLI initialized at ${ extensionPath } `
178
- ) ;
157
+ // Copy the file
158
+ try {
159
+ await copyFile ( sourcePath , targetPath ) ;
160
+ logger . info ( 'Successfully copied GENKIT.md to .gemini folder' ) ;
161
+ return { path : targetPath , updated : true } ;
162
+ } catch ( error ) {
163
+ logger . error ( 'Error copying GENKIT.md:' , error ) ;
164
+ throw error ;
165
+ }
166
+ } else {
167
+ // Unix-like systems: create symlink
168
+ logger . info ( 'Adding a symlink for GENKIT.md in the .gemini folder' ) ;
169
+
170
+ // Check if symlink already exists
171
+ try {
172
+ const stats = await lstat ( targetPath ) ;
173
+ if ( stats . isSymbolicLink ( ) ) {
174
+ // Verify the symlink points to the correct target
175
+ const linkTarget = await readlink ( targetPath ) ;
176
+
177
+ // Resolve the link target to absolute path
178
+ const resolvedLinkTarget = path . resolve (
179
+ path . dirname ( targetPath ) ,
180
+ linkTarget
181
+ ) ;
182
+ // Resolve the source path to absolute
183
+ const resolvedSourcePath = path . resolve ( sourcePath ) ;
184
+
185
+ // Compare absolute paths
186
+ if ( resolvedLinkTarget === resolvedSourcePath ) {
187
+ logger . info (
188
+ 'Symlink already exists and points to the correct location'
189
+ ) ;
190
+ return { path : targetPath , updated : false } ;
191
+ } else {
192
+ logger . warn (
193
+ `Symlink exists but points to wrong location. Expected: ${ resolvedSourcePath } , Found: ${ resolvedLinkTarget } . Please remove this file and try again if this was not intentional.`
194
+ ) ;
195
+ return { path : targetPath , updated : false } ;
196
+ }
197
+ } else {
198
+ // File exists but is not a symlink
199
+ logger . warn (
200
+ `${ targetPath } exists but is not a symlink, skipping symlink creation`
201
+ ) ;
202
+ return { path : targetPath , updated : false } ;
203
+ }
204
+ } catch ( error : any ) {
205
+ if ( error . code === 'ENOENT' ) {
206
+ // Symlink doesn't exist, create it
207
+ await symlink ( path . relative ( GEMINI_DIR , sourcePath ) , targetPath ) ;
208
+ logger . info ( 'Successfully created symlink for GENKIT.md' ) ;
209
+ return { path : targetPath , updated : true } ;
210
+ } else {
211
+ // Some other error occurred
212
+ logger . error ( 'Error checking symlink:' , error ) ;
213
+ throw error ;
214
+ }
179
215
}
180
- } catch ( err ) {
181
- logger . error ( err ) ;
182
- process . exit ( 1 ) ;
183
216
}
184
- files . push ( { path : extensionPath , updated : extensionUpdated } ) ;
185
-
186
- return { files } ;
187
217
}
0 commit comments