Skip to content

Commit 29228e6

Browse files
committed
extracting common utils
1 parent eb18730 commit 29228e6

File tree

2 files changed

+23
-202
lines changed

2 files changed

+23
-202
lines changed

src/commands/init/ai-rules.ts

Lines changed: 16 additions & 198 deletions
Original file line numberDiff line numberDiff line change
@@ -1,214 +1,33 @@
11
import { resolve } from 'node:path'
22
import { promises as fs } from 'node:fs'
3-
import { homedir } from 'node:os'
43
import type { NetlifyAPI } from '@netlify/api'
54

65
import { chalk, log, logAndThrowError, type APIError } from '../../utils/command-helpers.js'
76
import { normalizeRepoUrl } from '../../utils/normalize-repo-url.js'
87
import { runGit } from '../../utils/run-git.js'
98
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'
1318
import type BaseCommand from '../base-command.js'
1419
import type { SiteInfo } from '../../utils/types.js'
1520
import inquirer from 'inquirer'
1621

22+
/**
23+
* Project information interface for AI projects
24+
*/
1725
interface ProjectInfo {
1826
success: boolean
1927
projectId: string
2028
prompt: string
2129
}
2230

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-
21231
// Trigger IDE-specific MCP configuration
21332
const triggerMcpConfiguration = async (ide: ConsumerConfig, projectPath: string): Promise<boolean> => {
21433
log(`\n${chalk.blue('🔧 MCP Configuration for')} ${chalk.cyan(ide.presentedName)}`)
@@ -272,9 +91,6 @@ const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => {
27291
},
27392
})
27493

275-
if (!response.ok) {
276-
throw new Error(`Failed to fetch project information: ${response.statusText}`)
277-
}
27894
const data = (await response.text()) as unknown as string
27995
const parsedData = JSON.parse(data) as unknown as ProjectInfo
28096
return parsedData
@@ -286,7 +102,7 @@ const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => {
286102
const getRepoUrlFromProjectId = async (api: NetlifyAPI, projectId: string): Promise<string> => {
287103
try {
288104
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
290106

291107
if (!repoUrl) {
292108
throw new Error(`No repository URL found for project ID: ${projectId}`)
@@ -410,7 +226,9 @@ export const initWithAiRules = async (hash: string, command: BaseCommand): Promi
410226
} else {
411227
log(chalk.yellowBright(`🔧 Step 2: Manual MCP Configuration`))
412228
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+
414232
}
415233
log()
416234
}

src/recipes/ai-context/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ const promptForContextConsumerSelection = async (): Promise<ConsumerConfig> => {
7777
* Checks if a command belongs to a known IDEs by checking if it includes a specific string.
7878
* For example, the command that starts windsurf looks something like "/applications/windsurf.app/contents/...".
7979
*/
80-
const getConsumerKeyFromCommand = (command: string): string | null => {
80+
export const getConsumerKeyFromCommand = (command: string): string | null => {
8181
// The actual command is something like "/applications/windsurf.app/contents/...", but we are only looking for windsurf
8282
const match = cliContextConsumers.find(
8383
(consumer) => consumer.consumerProcessCmd && command.includes(consumer.consumerProcessCmd),
@@ -88,7 +88,7 @@ const getConsumerKeyFromCommand = (command: string): string | null => {
8888
/**
8989
* Receives a process ID (pid) and returns both the command that the process was run with and its parent process ID. If the process is a known IDE, also returns information about that IDE.
9090
*/
91-
const getCommandAndParentPID = async (
91+
export const getCommandAndParentPID = async (
9292
pid: number,
9393
): Promise<{
9494
parentPID: number
@@ -107,7 +107,10 @@ const getCommandAndParentPID = async (
107107
}
108108
}
109109

110-
const getPathByDetectingIDE = async (): Promise<ConsumerConfig | null> => {
110+
/**
111+
* Detects the IDE by walking up the process tree and matching against known consumer processes
112+
*/
113+
export const detectIDE = async (): Promise<ConsumerConfig | null> => {
111114
// Go up the chain of ancestor process IDs and find if one of their commands matches an IDE.
112115
const ppid = process.ppid
113116
let result: Awaited<ReturnType<typeof getCommandAndParentPID>>
@@ -142,7 +145,7 @@ export const run = async (runOptions: RunRecipeOptions) => {
142145
}
143146

144147
if (!consumer && process.env.AI_CONTEXT_SKIP_DETECTION !== 'true') {
145-
consumer = await getPathByDetectingIDE()
148+
consumer = await detectIDE()
146149
}
147150

148151
if (!consumer) {

0 commit comments

Comments
 (0)