@@ -4,6 +4,179 @@ import * as path from 'path'
44import * as os from 'os'
55import { Config } from '../common/config'
66import { get , set } from 'lodash'
7+ import { parseGitRemote } from '../common/parseGitRemote'
8+
9+ interface Rule {
10+ when : string
11+ enforce : string [ ]
12+ }
13+
14+ interface RuleConfig {
15+ name : string
16+ description : string
17+ rules : Rule [ ]
18+ }
19+
20+ const newRulesTemplate = ( provider ?: string , organization ?: string , repository ?: string ) : RuleConfig => ( {
21+ name : 'Codacy Rules' ,
22+ description : 'Configuration for AI behavior when interacting with Codacy' ,
23+ rules : [
24+ {
25+ when : 'using any tool that has either one of the following arguments: provider, organization or repository' ,
26+ enforce : [
27+ `use ${ provider } for provider, ${ organization } for organization and ${ repository } for repository, and avoid calling 'git remote -v' unless really necessary` ,
28+ ] ,
29+ } ,
30+ {
31+ when : 'after ANY successful edit_file or reapply tool call' ,
32+ enforce : [
33+ 'IMMEDIATELY run codacy_cli_analyze tool with:' ,
34+ 'rootPath set to the workspace path' ,
35+ 'file set to the edited file path' ,
36+ 'If issues are found, propose fixes for them' ,
37+ ] ,
38+ } ,
39+ ] ,
40+ } )
41+ // Function to parse the json content inside an MDC file. An MDC file contains text elements that would generate errors when parsed as JSON
42+ const parseMdcContent = ( content : string ) : RuleConfig => {
43+ const parts = content . split ( '---' )
44+
45+ if ( parts . length < 3 ) {
46+ throw new Error ( 'Invalid MDC file format: missing frontmatter' )
47+ }
48+
49+ const jsonContent = parts [ 2 ] . trim ( )
50+
51+ try {
52+ return JSON . parse ( jsonContent )
53+ } catch ( error ) {
54+ throw new Error ( 'Invalid JSON content in MDC file' )
55+ }
56+ }
57+
58+ const convertRulesToMarkdown = ( rules : RuleConfig , existingContent ?: string ) : string => {
59+ const codacyRules : string = existingContent ?. split ( '---' ) . filter ( ( part ) => part . includes ( rules . name ) ) [ 0 ] || ''
60+ const newCodacyRules = `---\n# ${ rules . name } \n${ rules . description } \n${ rules . rules
61+ . map ( ( rule ) => `## When ${ rule . when } \n${ rule . enforce . join ( '\n - ' ) } ` )
62+ . join ( '\n\n' ) } \n---`
63+ return existingContent ? existingContent ?. replace ( `---${ codacyRules } ---` , newCodacyRules ) : newCodacyRules
64+ }
65+
66+ const rulesPrefixForMdc = `---
67+ description:
68+ globs:
69+ alwaysApply: true
70+ ---
71+ \n`
72+
73+ function getCorrectRulesInfo ( ) : { path : string ; format : string } {
74+ const ideInfo = getCurrentIDE ( )
75+ // Get the workspace folder path
76+ const workspacePath = vscode . workspace . workspaceFolders ?. [ 0 ] ?. uri . fsPath
77+ if ( ! workspacePath ) {
78+ throw new Error ( 'No workspace folder found' )
79+ }
80+ if ( ideInfo === 'cursor' ) {
81+ return { path : path . join ( workspacePath , '.cursor' , 'rules' , 'codacy.mdc' ) , format : 'mdc' }
82+ }
83+ if ( ideInfo === 'windsurf' ) {
84+ return { path : path . join ( workspacePath , '.windsurfrules' ) , format : 'md' }
85+ }
86+ return { path : path . join ( workspacePath , '.github' , 'copilot-instructions.md' ) , format : 'md' }
87+ }
88+
89+ const addRulesToGitignore = ( rulesPath : string ) => {
90+ const currentIDE = getCurrentIDE ( )
91+ const workspacePath = vscode . workspace . workspaceFolders ?. [ 0 ] ?. uri . fsPath || ''
92+ const gitignorePath = path . join ( workspacePath , '.gitignore' )
93+ const relativeRulesPath = path . relative ( workspacePath , rulesPath )
94+ const gitignoreContent = `\n\n#Ignore ${ currentIDE } AI rules\n${ relativeRulesPath } \n`
95+ let existingGitignore = ''
96+
97+ if ( fs . existsSync ( gitignorePath ) ) {
98+ existingGitignore = fs . readFileSync ( gitignorePath , 'utf8' )
99+
100+ if ( ! existingGitignore . split ( '\n' ) . some ( ( line ) => line . trim ( ) === relativeRulesPath . trim ( ) ) ) {
101+ fs . appendFileSync ( gitignorePath , gitignoreContent )
102+ vscode . window . showInformationMessage ( `Added ${ relativeRulesPath } to .gitignore` )
103+ }
104+ } else {
105+ fs . writeFileSync ( gitignorePath , gitignoreContent )
106+ vscode . window . showInformationMessage ( 'Created .gitignore and added rules path' )
107+ }
108+ }
109+ export async function createRules ( ) {
110+ // Get git info
111+ const git = vscode . extensions . getExtension ( 'vscode.git' ) ?. exports . getAPI ( 1 )
112+ const repo = git ?. repositories [ 0 ]
113+ let provider , organization , repository
114+
115+ if ( repo ?. state . remotes [ 0 ] ?. pushUrl ) {
116+ const gitInfo = parseGitRemote ( repo . state . remotes [ 0 ] . pushUrl )
117+ provider = gitInfo . provider
118+ organization = gitInfo . organization
119+ repository = gitInfo . repository
120+ }
121+
122+ const newRules = newRulesTemplate ( provider , organization , repository )
123+
124+ try {
125+ const { path : rulesPath , format } = getCorrectRulesInfo ( )
126+ const isMdc = format === 'mdc'
127+ const dirPath = path . dirname ( rulesPath )
128+
129+ // Create directories if they don't exist
130+ if ( ! fs . existsSync ( dirPath ) ) {
131+ fs . mkdirSync ( dirPath , { recursive : true } )
132+ }
133+
134+ if ( ! fs . existsSync ( rulesPath ) ) {
135+ fs . writeFileSync (
136+ rulesPath ,
137+ `${ isMdc ? rulesPrefixForMdc : '' } ${
138+ isMdc ? JSON . stringify ( newRules , null , 2 ) : convertRulesToMarkdown ( newRules )
139+ } `
140+ )
141+ vscode . window . showInformationMessage ( `Created new rules file at ${ rulesPath } ` )
142+ addRulesToGitignore ( rulesPath )
143+ } else {
144+ try {
145+ const existingContent = fs . readFileSync ( rulesPath , 'utf8' )
146+
147+ if ( isMdc ) {
148+ const existingRules = parseMdcContent ( existingContent )
149+ const mergedRules = {
150+ ...existingRules ,
151+ rules : [
152+ ...( existingRules . rules || [ ] ) ,
153+ ...newRules . rules . filter (
154+ ( newRule ) =>
155+ ! existingRules . rules ?. some (
156+ ( existingRule : Rule ) =>
157+ existingRule . when === newRule . when &&
158+ existingRule . enforce . every ( ( e ) => newRule . enforce . includes ( e ) )
159+ )
160+ ) ,
161+ ] ,
162+ }
163+ fs . writeFileSync ( rulesPath , `${ rulesPrefixForMdc } ${ JSON . stringify ( mergedRules , null , 2 ) } ` )
164+ } else {
165+ fs . writeFileSync ( rulesPath , convertRulesToMarkdown ( newRules , existingContent ) )
166+ }
167+
168+ vscode . window . showInformationMessage ( `Updated rules in ${ rulesPath } ` )
169+ } catch ( parseError ) {
170+ vscode . window . showWarningMessage ( `Error parsing existing rules file. Creating new one.` )
171+ fs . writeFileSync ( rulesPath , JSON . stringify ( newRules , null , 2 ) )
172+ }
173+ }
174+ } catch ( error : unknown ) {
175+ const errorMessage = error instanceof Error ? error . message : 'Unknown error occurred'
176+ vscode . window . showErrorMessage ( `Failed to create rules: ${ errorMessage } ` )
177+ throw error
178+ }
179+ }
7180
8181function getCurrentIDE ( ) : string {
9182 const isCursor = vscode . env . appName . toLowerCase ( ) . includes ( 'cursor' )
@@ -105,6 +278,7 @@ export async function configureMCP() {
105278 fs . writeFileSync ( filePath , JSON . stringify ( modifiedConfig , null , 2 ) )
106279
107280 vscode . window . showInformationMessage ( 'Codacy MCP server added successfully' )
281+ await createRules ( )
108282 } catch ( error : unknown ) {
109283 const errorMessage = error instanceof Error ? error . message : 'Unknown error occurred'
110284 vscode . window . showErrorMessage ( `Failed to configure MCP server: ${ errorMessage } ` )
0 commit comments