- 
                Notifications
    
You must be signed in to change notification settings  - Fork 430
 
feat: add ai command for cli to start the project #7352
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 23 commits
82710fd
              6864489
              a403e15
              614fe51
              eb21fb0
              8edaf5f
              1e31e7d
              8633645
              775fa44
              04a41a3
              6a36601
              ad065d7
              4ccd1ba
              127f8b4
              165f6e3
              5285a5d
              983aef7
              eb18730
              29228e6
              05b325a
              b7bb36c
              723222a
              d50b455
              9e0514f
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,255 @@ | ||||||||||
| import { resolve } from 'node:path' | ||||||||||
| import { promises as fs } from 'node:fs' | ||||||||||
| import type { NetlifyAPI } from '@netlify/api' | ||||||||||
| 
     | 
||||||||||
| import { chalk, log, logAndThrowError, type APIError } from '../../utils/command-helpers.js' | ||||||||||
| import { normalizeRepoUrl } from '../../utils/normalize-repo-url.js' | ||||||||||
| import { runGit } from '../../utils/run-git.js' | ||||||||||
| import { startSpinner } from '../../lib/spinner.js' | ||||||||||
| import { detectIDE } from '../../recipes/ai-context/index.js' | ||||||||||
| import { type ConsumerConfig } from '../../recipes/ai-context/context.js' | ||||||||||
| import { | ||||||||||
| generateMcpConfig, | ||||||||||
| configureMcpForVSCode, | ||||||||||
| configureMcpForCursor, | ||||||||||
| configureMcpForWindsurf, | ||||||||||
| showGenericMcpConfig, | ||||||||||
| } from '../../utils/mcp-utils.js' | ||||||||||
| import type BaseCommand from '../base-command.js' | ||||||||||
| import type { SiteInfo } from '../../utils/types.js' | ||||||||||
| import inquirer from 'inquirer' | ||||||||||
| 
     | 
||||||||||
| /** | ||||||||||
| * Project information interface for AI projects | ||||||||||
| */ | ||||||||||
| interface ProjectInfo { | ||||||||||
| success: boolean | ||||||||||
| projectId: string | ||||||||||
| prompt: string | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| // Trigger IDE-specific MCP configuration | ||||||||||
| const triggerMcpConfiguration = async (ide: ConsumerConfig, projectPath: string): Promise<boolean> => { | ||||||||||
| log(`\n${chalk.blue('π§ MCP Configuration for')} ${chalk.cyan(ide.presentedName)}`) | ||||||||||
| 
     | 
||||||||||
| const { shouldConfigure } = await inquirer.prompt<{ shouldConfigure: boolean }>([ | ||||||||||
| { | ||||||||||
| type: 'confirm', | ||||||||||
| name: 'shouldConfigure', | ||||||||||
| message: `Would you like to automatically configure the MCP server for ${ide.presentedName}?`, | ||||||||||
| default: true, | ||||||||||
| }, | ||||||||||
| ]) | ||||||||||
| 
     | 
||||||||||
| if (!shouldConfigure) { | ||||||||||
| log(` ${chalk.gray('You can configure MCP manually later for enhanced AI capabilities:')}`) | ||||||||||
| log( | ||||||||||
| ` ${chalk.gray('Documentation:')} ${chalk.cyan( | ||||||||||
| 'https://docs.netlify.com/welcome/build-with-ai/netlify-mcp-server/', | ||||||||||
| )}`, | ||||||||||
| ) | ||||||||||
| 
     | 
||||||||||
| return false | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| try { | ||||||||||
| const config = generateMcpConfig(ide) | ||||||||||
| 
     | 
||||||||||
| // IDE-specific configuration actions | ||||||||||
| switch (ide.key) { | ||||||||||
| case 'vscode': | ||||||||||
| await configureMcpForVSCode(config, projectPath) | ||||||||||
| break | ||||||||||
| case 'cursor': | ||||||||||
| await configureMcpForCursor(config, projectPath) | ||||||||||
| break | ||||||||||
| case 'windsurf': | ||||||||||
| await configureMcpForWindsurf(config, projectPath) | ||||||||||
| break | ||||||||||
| default: | ||||||||||
| showGenericMcpConfig(config, ide.presentedName) | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| log(`${chalk.green('β ')} MCP configuration completed for ${chalk.cyan(ide.presentedName)}`) | ||||||||||
| return true | ||||||||||
| } catch (error) { | ||||||||||
| log(`${chalk.red('β')} Failed to configure MCP: ${error instanceof Error ? error.message : 'Unknown error'}`) | ||||||||||
| return false | ||||||||||
| } | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| // Helper functions reused from ai-start.ts | ||||||||||
| const decodeHash = (hash: string): string => { | ||||||||||
| try { | ||||||||||
| return atob(hash) | ||||||||||
| } catch (error) { | ||||||||||
| throw new Error(`Failed to decode hash: ${error instanceof Error ? error.message : 'Invalid base64 or URL'}`) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| const fetchProjectInfo = async (url: string): Promise<ProjectInfo> => { | ||||||||||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be great to have a comment explaining what is the URL we're hitting β is it the Netlify API, something else?  | 
||||||||||
| try { | ||||||||||
| const response = await fetch(url, { | ||||||||||
| headers: { | ||||||||||
| 'content-type': 'text/plain', | ||||||||||
| }, | ||||||||||
| }) | ||||||||||
| 
     | 
||||||||||
| const data = (await response.text()) as unknown as string | ||||||||||
| const parsedData = JSON.parse(data) as unknown as ProjectInfo | ||||||||||
| return parsedData | ||||||||||
| } catch (error) { | ||||||||||
| throw new Error(`Failed to fetch project information: ${error instanceof Error ? error.message : 'Unknown error'}`) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| const getRepoUrlFromProjectId = async (api: NetlifyAPI, projectId: string): Promise<string> => { | ||||||||||
| try { | ||||||||||
| const siteInfo = (await api.getSite({ siteId: projectId })) as SiteInfo | ||||||||||
| 
         There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we not already have the site information populated by the base command? Or is this a different site? If we could use that, we'd avoid making an extra API call.  | 
||||||||||
| const repoUrl = siteInfo.build_settings?.repo_url | ||||||||||
| 
     | 
||||||||||
| if (!repoUrl) { | ||||||||||
| throw new Error(`No repository URL found for project ID: ${projectId}`) | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| return repoUrl | ||||||||||
| } catch (error) { | ||||||||||
| if ((error as APIError).status === 404) { | ||||||||||
| throw new Error(`Project with ID ${projectId} not found`) | ||||||||||
| } | ||||||||||
| throw new Error(`Failed to fetch project data: ${(error as Error).message}`) | ||||||||||
| } | ||||||||||
| } | ||||||||||
| 
     | 
||||||||||
| const savePrompt = async (instructions: string, ntlContext: string | null, targetDir: string): Promise<void> => { | ||||||||||
| try { | ||||||||||
| const filePath = resolve(targetDir, 'AI-instructions.md') | ||||||||||
| await fs.writeFile(filePath, `Context: ${ntlContext ?? ''}\n\n${instructions}`, 'utf-8') | ||||||||||
| log(`${chalk.green('β ')} AI instructions saved to ${chalk.cyan('AI-instructions.md')}`) | ||||||||||
| } catch (error) { | ||||||||||
| const errorMessage = error instanceof Error ? error.message : 'Unknown error' | ||||||||||
| log(`${chalk.yellow('β οΈ')} Warning: Failed to save AI instructions: ${errorMessage}`) | ||||||||||
                
       | 
||||||||||
| export const warn = (message = '') => { | |
| const bang = chalk.yellow(BANG) | |
| log(` ${bang} Warning: ${message}`) | |
| } | 
        
          
              
                Outdated
          
        
      There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
in future we don't wanna fetch context if we installing MCP
        
          
              
                Outdated
          
        
      There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are you setting Content-Type on a GET request? Did you mean to use Accept?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What is the purpose of this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
part of code here is duplication, but we still not setelled the command and want to see how it feels so then we can extract stuff into common utils after we work on making command fully public
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure why trialing the new command should mean more code duplication? If you move the generic methods you're reusing to a separate file (which I would argue should already be the case), you're making it easier for this PR to be reviewed, you're going to have less code to maintain, and you reduce the number of places where you might need to adjust things if an issue or an improvement come up.
If you then decide that you want to keep the new command in its current shape, great β you already have the code in the structure you want and no further changes are needed; if you decide you want to bin it, you can just delete it and the code you abstracted away into a separate file still makes sense on its own.