1
1
import { resolve } from 'node:path'
2
2
import { promises as fs } from 'node:fs'
3
- import { homedir } from 'node:os'
4
3
import type { NetlifyAPI } from '@netlify/api'
5
4
6
5
import { chalk , log , logAndThrowError , type APIError } from '../../utils/command-helpers.js'
7
6
import { normalizeRepoUrl } from '../../utils/normalize-repo-url.js'
8
7
import { runGit } from '../../utils/run-git.js'
9
8
import { startSpinner } from '../../lib/spinner.js'
10
- import { getContextConsumers , type ConsumerConfig } from '../../recipes/ai-context/context.js'
11
- import execa from '../../utils/execa.js'
12
- import { version } from '../../utils/command-helpers.js'
9
+ import { detectIDE } from '../../recipes/ai-context/index.js'
10
+ import { type ConsumerConfig } from '../../recipes/ai-context/context.js'
11
+ import {
12
+ generateMcpConfig ,
13
+ configureMcpForVSCode ,
14
+ configureMcpForCursor ,
15
+ configureMcpForWindsurf ,
16
+ showGenericMcpConfig
17
+ } from '../../utils/mcp-utils.js'
13
18
import type BaseCommand from '../base-command.js'
14
19
import type { SiteInfo } from '../../utils/types.js'
15
20
import inquirer from 'inquirer'
16
21
22
+ /**
23
+ * Project information interface for AI projects
24
+ */
17
25
interface ProjectInfo {
18
26
success : boolean
19
27
projectId : string
20
28
prompt : string
21
29
}
22
30
23
- // Check if a command belongs to a known IDE (reusing ai-context logic)
24
- const getConsumerKeyFromCommand = ( command : string , consumers : ConsumerConfig [ ] ) : string | null => {
25
- const match = consumers . find (
26
- ( consumer ) => consumer . consumerProcessCmd && command . includes ( consumer . consumerProcessCmd ) ,
27
- )
28
- return match ? match . key : null
29
- }
30
-
31
- // Get command and parent PID (same logic as ai-context)
32
- const getCommandAndParentPID = async (
33
- pid : number ,
34
- ) : Promise < {
35
- parentPID : number
36
- command : string
37
- consumerKey : string | null
38
- } > => {
39
- const { stdout } = await execa ( 'ps' , [ '-p' , String ( pid ) , '-o' , 'ppid=,comm=' ] )
40
- const output = stdout . trim ( )
41
- const spaceIndex = output . indexOf ( ' ' )
42
- const parentPID = output . substring ( 0 , spaceIndex )
43
- const command = output . substring ( spaceIndex + 1 ) . toLowerCase ( )
44
-
45
- const consumers = await getContextConsumers ( version ) // Use current CLI version
46
-
47
- return {
48
- parentPID : Number ( parentPID ) ,
49
- command,
50
- consumerKey : getConsumerKeyFromCommand ( command , consumers ) ,
51
- }
52
- }
53
-
54
- // Detect IDE by walking up process tree (same logic as ai-context)
55
- const detectIDE = async ( ) : Promise < ConsumerConfig | null > => {
56
- if ( process . env . AI_CONTEXT_SKIP_DETECTION === 'true' ) {
57
- return null
58
- }
59
-
60
- const ppid = process . ppid
61
- let result : Awaited < ReturnType < typeof getCommandAndParentPID > >
62
- try {
63
- result = await getCommandAndParentPID ( ppid )
64
- while ( result . parentPID !== 1 && ! result . consumerKey ) {
65
- result = await getCommandAndParentPID ( result . parentPID )
66
- }
67
- } catch {
68
- // Process detection failed
69
- return null
70
- }
71
-
72
- if ( result . consumerKey ) {
73
- const consumers = await getContextConsumers ( version )
74
- const contextConsumer = consumers . find ( ( consumer ) => consumer . key === result . consumerKey )
75
- if ( contextConsumer ) {
76
- return contextConsumer
77
- }
78
- }
79
-
80
- return null
81
- }
82
-
83
- // Generate MCP configuration for the detected IDE
84
- const generateMcpConfig = ( ide : ConsumerConfig ) : Record < string , unknown > => {
85
- const configs : Record < string , Record < string , unknown > > = {
86
- vscode : {
87
- servers : {
88
- netlify : {
89
- type : 'stdio' ,
90
- command : 'npx' ,
91
- args : [ '-y' , '@netlify/mcp' ] ,
92
- } ,
93
- } ,
94
- } ,
95
- cursor : {
96
- mcpServers : {
97
- netlify : {
98
- command : 'npx' ,
99
- args : [ '-y' , '@netlify/mcp' ] ,
100
- } ,
101
- } ,
102
- } ,
103
- windsurf : {
104
- mcpServers : {
105
- netlify : {
106
- command : 'npx' ,
107
- args : [ '-y' , '@netlify/mcp' ] ,
108
- } ,
109
- } ,
110
- } ,
111
- }
112
-
113
- return (
114
- configs [ ide . key ] ?? {
115
- mcpServers : {
116
- netlify : {
117
- command : 'npx' ,
118
- args : [ '-y' , '@netlify/mcp' ] ,
119
- } ,
120
- } ,
121
- }
122
- )
123
- }
124
-
125
- // VS Code specific MCP configuration
126
- const configureMcpForVSCode = async ( config : Record < string , unknown > , projectPath : string ) : Promise < void > => {
127
- const vscodeDirPath = resolve ( projectPath , '.vscode' )
128
- const configPath = resolve ( vscodeDirPath , 'mcp.json' )
129
-
130
- try {
131
- // Create .vscode directory if it doesn't exist
132
- await fs . mkdir ( vscodeDirPath , { recursive : true } )
133
-
134
- // Write or update mcp.json
135
- let existingConfig : Record < string , unknown > = { }
136
- try {
137
- const existingContent = await fs . readFile ( configPath , 'utf-8' )
138
- existingConfig = JSON . parse ( existingContent ) as Record < string , unknown >
139
- } catch {
140
- // File doesn't exist or is invalid JSON
141
- }
142
-
143
- const updatedConfig = { ...existingConfig , ...config }
144
-
145
- await fs . writeFile ( configPath , JSON . stringify ( updatedConfig , null , 2 ) , 'utf-8' )
146
- log ( `${ chalk . green ( '✅' ) } VS Code MCP configuration saved to ${ chalk . cyan ( '.vscode/mcp.json' ) } ` )
147
- } catch ( error ) {
148
- throw new Error ( `Failed to configure VS Code MCP: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
149
- }
150
- }
151
-
152
- // Cursor specific MCP configuration
153
- const configureMcpForCursor = async ( config : Record < string , unknown > , projectPath : string ) : Promise < void > => {
154
- const configPath = resolve ( projectPath , '.cursor' , 'mcp.json' )
155
-
156
- try {
157
- await fs . mkdir ( resolve ( projectPath , '.cursor' ) , { recursive : true } )
158
- await fs . writeFile ( configPath , JSON . stringify ( config , null , 2 ) , 'utf-8' )
159
- log ( `${ chalk . green ( '✅' ) } Cursor MCP configuration saved to ${ chalk . cyan ( '.cursor/mcp.json' ) } ` )
160
- } catch ( error ) {
161
- throw new Error ( `Failed to configure Cursor MCP: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
162
- }
163
- }
164
-
165
- // Windsurf specific MCP configuration
166
- const configureMcpForWindsurf = async ( config : Record < string , unknown > , _projectPath : string ) : Promise < void > => {
167
- const windsurfDirPath = resolve ( homedir ( ) , '.codeium' , 'windsurf' )
168
- const configPath = resolve ( windsurfDirPath , 'mcp_config.json' )
169
-
170
- try {
171
- // Create .codeium/windsurf directory if it doesn't exist
172
- await fs . mkdir ( windsurfDirPath , { recursive : true } )
173
-
174
- // Read existing config or create new one
175
- let existingConfig : Record < string , unknown > = { }
176
- try {
177
- const existingContent = await fs . readFile ( configPath , 'utf-8' )
178
- existingConfig = JSON . parse ( existingContent ) as Record < string , unknown >
179
- } catch {
180
- // File doesn't exist or is invalid JSON
181
- }
182
-
183
- // Merge mcpServers from both configs
184
- const existingServers = ( existingConfig . mcpServers as Record < string , unknown > | undefined ) ?? { }
185
- const newServers = ( config . mcpServers as Record < string , unknown > | undefined ) ?? { }
186
-
187
- const updatedConfig = {
188
- ...existingConfig ,
189
- mcpServers : {
190
- ...existingServers ,
191
- ...newServers ,
192
- } ,
193
- }
194
-
195
- await fs . writeFile ( configPath , JSON . stringify ( updatedConfig , null , 2 ) , 'utf-8' )
196
- log ( `${ chalk . green ( '✅' ) } Windsurf MCP configuration saved` )
197
- log ( `${ chalk . gray ( '💡' ) } Restart Windsurf to activate the MCP server` )
198
- } catch ( error ) {
199
- throw new Error ( `Failed to configure Windsurf MCP: ${ error instanceof Error ? error . message : 'Unknown error' } ` )
200
- }
201
- }
202
-
203
- // Generic MCP configuration display
204
- const showGenericMcpConfig = ( config : Record < string , unknown > , ideName : string ) : void => {
205
- log ( `\n${ chalk . yellow ( '📋 Manual configuration required' ) } ` )
206
- log ( `Please add the following configuration to your ${ ideName } settings:` )
207
- log ( `\n${ chalk . gray ( '--- Configuration ---' ) } ` )
208
- log ( JSON . stringify ( config , null , 2 ) )
209
- log ( `${ chalk . gray ( '--- End Configuration ---' ) } \n` )
210
- }
211
-
212
31
// Trigger IDE-specific MCP configuration
213
32
const triggerMcpConfiguration = async ( ide : ConsumerConfig , projectPath : string ) : Promise < boolean > => {
214
33
log ( `\n${ chalk . blue ( '🔧 MCP Configuration for' ) } ${ chalk . cyan ( ide . presentedName ) } ` )
@@ -272,9 +91,6 @@ const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => {
272
91
} ,
273
92
} )
274
93
275
- if ( ! response . ok ) {
276
- throw new Error ( `Failed to fetch project information: ${ response . statusText } ` )
277
- }
278
94
const data = ( await response . text ( ) ) as unknown as string
279
95
const parsedData = JSON . parse ( data ) as unknown as ProjectInfo
280
96
return parsedData
@@ -286,7 +102,7 @@ const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => {
286
102
const getRepoUrlFromProjectId = async ( api : NetlifyAPI , projectId : string ) : Promise < string > => {
287
103
try {
288
104
const siteInfo = ( await api . getSite ( { siteId : projectId } ) ) as SiteInfo
289
- const repoUrl = SiteInfo . build_settings ?. repo_url
105
+ const repoUrl = siteInfo . build_settings ?. repo_url
290
106
291
107
if ( ! repoUrl ) {
292
108
throw new Error ( `No repository URL found for project ID: ${ projectId } ` )
@@ -410,7 +226,9 @@ export const initWithAiRules = async (hash: string, command: BaseCommand): Promi
410
226
} else {
411
227
log ( chalk . yellowBright ( `🔧 Step 2: Manual MCP Configuration` ) )
412
228
log ( ` ${ chalk . cyan ( detectedIDE . key ) } detected - MCP setup was skipped` )
413
- log ( ` ${ chalk . gray ( 'You can configure MCP manually later for enhanced AI capabilities' ) } ` )
229
+ log ( ` ${ chalk . gray ( 'You can configure MCP manually later for enhanced AI capabilities:' ) } ` )
230
+ log ( ` ${ chalk . gray ( 'Documentation:' ) } ${ chalk . cyan ( 'https://docs.netlify.com/welcome/build-with-ai/netlify-mcp-server/' ) } ` )
231
+
414
232
}
415
233
log ( )
416
234
}
0 commit comments