@@ -3,6 +3,7 @@ import type {
33 OutputPluginContext ,
44 OutputWriteContext ,
55 Project ,
6+ RulePrompt ,
67 SkillPrompt ,
78 WriteResult ,
89 WriteResults
@@ -22,6 +23,7 @@ const RULES_SUBDIR = 'rules'
2223const GLOBAL_RULE_FILE = 'global.mdc'
2324const SKILLS_CURSOR_SUBDIR = 'skills-cursor'
2425const SKILL_FILE_NAME = 'SKILL.md'
26+ const RULE_FILE_PREFIX = 'rule-'
2527
2628const PRESERVED_SKILLS = new Set < string > ( [
2729 'create-rule' ,
@@ -75,7 +77,7 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
7577 async registerGlobalOutputDirs ( ctx : OutputPluginContext ) : Promise < RelativePath [ ] > {
7678 const results : RelativePath [ ] = [ ]
7779 const globalDir = this . getGlobalConfigDir ( )
78- const { fastCommands, skills} = ctx . collectedInputContext
80+ const { fastCommands, skills, rules } = ctx . collectedInputContext
7981
8082 if ( fastCommands != null && fastCommands . length > 0 ) {
8183 const commandsDir = this . getGlobalCommandsDir ( )
@@ -104,6 +106,17 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
104106 }
105107 }
106108
109+ const globalRules = rules ?. filter ( r => r . scope === 'global' )
110+ if ( globalRules == null || globalRules . length === 0 ) return results
111+
112+ const globalRulesDir = path . join ( globalDir , RULES_SUBDIR )
113+ results . push ( {
114+ pathKind : FilePathKind . Relative ,
115+ path : RULES_SUBDIR ,
116+ basePath : globalDir ,
117+ getDirectoryName : ( ) => RULES_SUBDIR ,
118+ getAbsolutePath : ( ) => globalRulesDir
119+ } )
107120 return results
108121 }
109122
@@ -140,6 +153,22 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
140153 }
141154 }
142155
156+ const globalRules = ctx . collectedInputContext . rules ?. filter ( r => r . scope === 'global' )
157+ if ( globalRules != null && globalRules . length > 0 ) {
158+ const globalRulesDir = path . join ( globalDir , RULES_SUBDIR )
159+ for ( const rule of globalRules ) {
160+ const fileName = this . buildRuleFileName ( rule )
161+ const fullPath = path . join ( globalRulesDir , fileName )
162+ results . push ( {
163+ pathKind : FilePathKind . Relative ,
164+ path : path . join ( RULES_SUBDIR , fileName ) ,
165+ basePath : globalDir ,
166+ getDirectoryName : ( ) => RULES_SUBDIR ,
167+ getAbsolutePath : ( ) => fullPath
168+ } )
169+ }
170+ }
171+
143172 if ( skills == null || skills . length === 0 ) return results
144173
145174 const skillsCursorDir = this . getSkillsCursorDir ( )
@@ -196,8 +225,10 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
196225
197226 async registerProjectOutputDirs ( ctx : OutputPluginContext ) : Promise < RelativePath [ ] > {
198227 const results : RelativePath [ ] = [ ]
199- const { workspace, globalMemory} = ctx . collectedInputContext
200- if ( globalMemory == null ) return results
228+ const { workspace, globalMemory, rules} = ctx . collectedInputContext
229+ const hasProjectRules = rules ?. some ( r => r . scope === 'project' ) ?? false
230+
231+ if ( globalMemory == null && ! hasProjectRules ) return results
201232
202233 for ( const project of workspace . projects ) {
203234 const projectDir = project . dirFromWorkspacePath
@@ -209,35 +240,53 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
209240
210241 async registerProjectOutputFiles ( ctx : OutputPluginContext ) : Promise < RelativePath [ ] > {
211242 const results : RelativePath [ ] = [ ]
212- const { workspace, globalMemory} = ctx . collectedInputContext
213- if ( globalMemory == null ) return results
243+ const { workspace, globalMemory, rules} = ctx . collectedInputContext
244+ const projectRules = rules ?. filter ( r => r . scope === 'project' )
245+ const hasProjectRules = projectRules != null && projectRules . length > 0
214246
215- for ( const project of workspace . projects ) {
216- const projectDir = project . dirFromWorkspacePath
217- if ( projectDir == null ) continue
218- results . push ( this . createProjectRuleFileRelativePath ( projectDir , GLOBAL_RULE_FILE ) )
247+ if ( globalMemory == null && ! hasProjectRules ) return results
248+
249+ if ( globalMemory != null ) {
250+ for ( const project of workspace . projects ) {
251+ const projectDir = project . dirFromWorkspacePath
252+ if ( projectDir == null ) continue
253+ results . push ( this . createProjectRuleFileRelativePath ( projectDir , GLOBAL_RULE_FILE ) )
254+ }
255+ }
256+
257+ if ( hasProjectRules ) {
258+ for ( const project of workspace . projects ) {
259+ const projectDir = project . dirFromWorkspacePath
260+ if ( projectDir == null ) continue
261+ for ( const rule of projectRules ) {
262+ const fileName = this . buildRuleFileName ( rule )
263+ results . push ( this . createProjectRuleFileRelativePath ( projectDir , fileName ) )
264+ }
265+ }
219266 }
267+
220268 results . push ( ...this . registerProjectIgnoreOutputFiles ( workspace . projects ) )
221269 return results
222270 }
223271
224272 async canWrite ( ctx : OutputWriteContext ) : Promise < boolean > {
225- const { workspace, skills, fastCommands, globalMemory, aiAgentIgnoreConfigFiles} = ctx . collectedInputContext
273+ const { workspace, skills, fastCommands, globalMemory, rules , aiAgentIgnoreConfigFiles} = ctx . collectedInputContext
226274 const hasSkills = ( skills ?. length ?? 0 ) > 0
227275 const hasFastCommands = ( fastCommands ?. length ?? 0 ) > 0
276+ const hasRules = ( rules ?. length ?? 0 ) > 0
228277 const hasGlobalRuleOutput
229278 = globalMemory != null
230279 && workspace . projects . some ( p => p . dirFromWorkspacePath != null )
231280 const hasCursorIgnore = aiAgentIgnoreConfigFiles ?. some ( f => f . fileName === '.cursorignore' ) ?? false
232281
233- if ( hasSkills || hasFastCommands || hasGlobalRuleOutput || hasCursorIgnore ) return true
282+ if ( hasSkills || hasFastCommands || hasGlobalRuleOutput || hasRules || hasCursorIgnore ) return true
234283
235284 this . log . trace ( { action : 'skip' , reason : 'noOutputs' } )
236285 return false
237286 }
238287
239288 async writeGlobalOutputs ( ctx : OutputWriteContext ) : Promise < WriteResults > {
240- const { skills, fastCommands} = ctx . collectedInputContext
289+ const { skills, fastCommands, rules } = ctx . collectedInputContext
241290 const fileResults : WriteResult [ ] = [ ]
242291 const dirResults : WriteResult [ ] = [ ]
243292
@@ -255,20 +304,30 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
255304 }
256305 }
257306
258- if ( fastCommands == null || fastCommands . length === 0 ) return { files : fileResults , dirs : dirResults }
307+ if ( fastCommands != null && fastCommands . length > 0 ) {
308+ const commandsDir = this . getGlobalCommandsDir ( )
309+ for ( const cmd of fastCommands ) {
310+ const result = await this . writeGlobalFastCommand ( ctx , commandsDir , cmd )
311+ fileResults . push ( result )
312+ }
313+ }
259314
260- const commandsDir = this . getGlobalCommandsDir ( )
261- for ( const cmd of fastCommands ) {
262- const result = await this . writeGlobalFastCommand ( ctx , commandsDir , cmd )
263- fileResults . push ( result )
315+ const globalRules = rules ?. filter ( r => r . scope === 'global' )
316+ if ( globalRules != null && globalRules . length > 0 ) {
317+ const globalRulesDir = path . join ( this . getGlobalConfigDir ( ) , RULES_SUBDIR )
318+ for ( const rule of globalRules ) {
319+ const result = await this . writeRuleMdcFile ( ctx , globalRulesDir , rule , this . getGlobalConfigDir ( ) )
320+ fileResults . push ( result )
321+ }
264322 }
323+
265324 return { files : fileResults , dirs : dirResults }
266325 }
267326
268327 async writeProjectOutputs ( ctx : OutputWriteContext ) : Promise < WriteResults > {
269328 const fileResults : WriteResult [ ] = [ ]
270329 const dirResults : WriteResult [ ] = [ ]
271- const { workspace, globalMemory} = ctx . collectedInputContext
330+ const { workspace, globalMemory, rules } = ctx . collectedInputContext
272331 if ( globalMemory != null ) {
273332 const content = this . buildGlobalRuleContent ( globalMemory . content as string )
274333 for ( const project of workspace . projects ) {
@@ -279,6 +338,19 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
279338 }
280339 }
281340
341+ const projectRules = rules ?. filter ( r => r . scope === 'project' )
342+ if ( projectRules != null && projectRules . length > 0 ) {
343+ for ( const project of workspace . projects ) {
344+ const projectDir = project . dirFromWorkspacePath
345+ if ( projectDir == null ) continue
346+ const rulesDir = path . join ( projectDir . basePath , projectDir . path , GLOBAL_CONFIG_DIR , RULES_SUBDIR )
347+ for ( const rule of projectRules ) {
348+ const result = await this . writeRuleMdcFile ( ctx , rulesDir , rule , projectDir . basePath )
349+ fileResults . push ( result )
350+ }
351+ }
352+ }
353+
282354 const ignoreResults = await this . writeProjectIgnoreFiles ( ctx )
283355 fileResults . push ( ...ignoreResults )
284356
@@ -662,4 +734,55 @@ export class CursorOutputPlugin extends AbstractOutputPlugin {
662734 return { path : relativePath , success : false , error : error as Error }
663735 }
664736 }
737+
738+ private buildRuleFileName ( rule : RulePrompt ) : string {
739+ return `${ RULE_FILE_PREFIX } ${ rule . series } -${ rule . ruleName } .mdc`
740+ }
741+
742+ private buildRuleMdcContent ( rule : RulePrompt ) : string {
743+ const description = rule . yamlFrontMatter ?. description ?? ''
744+ const fmData : Record < string , unknown > = {
745+ description,
746+ globs : [ ...rule . globs ] ,
747+ alwaysApply : false
748+ }
749+ return buildMarkdownWithFrontMatter ( fmData , rule . content )
750+ }
751+
752+ private async writeRuleMdcFile (
753+ ctx : OutputWriteContext ,
754+ rulesDir : string ,
755+ rule : RulePrompt ,
756+ basePath : string
757+ ) : Promise < WriteResult > {
758+ const fileName = this . buildRuleFileName ( rule )
759+ const fullPath = path . join ( rulesDir , fileName )
760+
761+ const relativePath : RelativePath = {
762+ pathKind : FilePathKind . Relative ,
763+ path : path . join ( GLOBAL_CONFIG_DIR , RULES_SUBDIR , fileName ) ,
764+ basePath,
765+ getDirectoryName : ( ) => RULES_SUBDIR ,
766+ getAbsolutePath : ( ) => fullPath
767+ }
768+
769+ const content = this . buildRuleMdcContent ( rule )
770+
771+ if ( ctx . dryRun === true ) {
772+ this . log . trace ( { action : 'dryRun' , type : 'ruleFile' , path : fullPath } )
773+ return { path : relativePath , success : true , skipped : false }
774+ }
775+
776+ try {
777+ this . ensureDirectory ( rulesDir )
778+ this . writeFileSync ( fullPath , content )
779+ this . log . trace ( { action : 'write' , type : 'ruleFile' , path : fullPath } )
780+ return { path : relativePath , success : true }
781+ }
782+ catch ( error ) {
783+ const errMsg = error instanceof Error ? error . message : String ( error )
784+ this . log . error ( { action : 'write' , type : 'ruleFile' , path : fullPath , error : errMsg } )
785+ return { path : relativePath , success : false , error : error as Error }
786+ }
787+ }
665788}
0 commit comments