11import { fileExistsAtPath , isDirectory , readDirectory } from "@utils/fs"
2+ import { ensureRulesDirectoryExists , GlobalFileNames } from "@core/storage/disk"
3+ import { getGlobalState , getWorkspaceState , updateGlobalState , updateWorkspaceState } from "@core/storage/state"
24import * as path from "path"
35import fs from "fs/promises"
46import { ClineRulesToggles } from "@shared/cline-rules"
7+ import * as vscode from "vscode"
58
69/**
710 * Recursively traverses directory and finds all files, including checking for optional whitelisted file extension
811 */
9- export async function readDirectoryRecursive ( directoryPath : string , allowedFileExtension : string ) : Promise < string [ ] > {
12+ export async function readDirectoryRecursive (
13+ directoryPath : string ,
14+ allowedFileExtension : string ,
15+ excludedPaths : string [ ] [ ] = [ ] ,
16+ ) : Promise < string [ ] > {
1017 try {
11- const entries = await readDirectory ( directoryPath )
18+ const entries = await readDirectory ( directoryPath , excludedPaths )
1219 let results : string [ ] = [ ]
1320 for ( const entry of entries ) {
1421 if ( allowedFileExtension !== "" ) {
@@ -33,6 +40,7 @@ export async function synchronizeRuleToggles(
3340 rulesDirectoryPath : string ,
3441 currentToggles : ClineRulesToggles ,
3542 allowedFileExtension : string = "" ,
43+ excludedPaths : string [ ] [ ] = [ ] ,
3644) : Promise < ClineRulesToggles > {
3745 // Create a copy of toggles to modify
3846 const updatedToggles = { ...currentToggles }
@@ -45,7 +53,7 @@ export async function synchronizeRuleToggles(
4553
4654 if ( isDir ) {
4755 // DIRECTORY CASE
48- const filePaths = await readDirectoryRecursive ( rulesDirectoryPath , allowedFileExtension )
56+ const filePaths = await readDirectoryRecursive ( rulesDirectoryPath , allowedFileExtension , excludedPaths )
4957 const existingRulePaths = new Set < string > ( )
5058
5159 for ( const filePath of filePaths ) {
@@ -119,3 +127,155 @@ export const getRuleFilesTotalContent = async (rulesFilePaths: string[], basePat
119127 ) . then ( ( contents ) => contents . filter ( Boolean ) . join ( "\n\n" ) )
120128 return ruleFilesTotalContent
121129}
130+
131+ /**
132+ * Handles converting any directory into a file (specifically used for .clinerules and .clinerules/workflows)
133+ * The old .clinerules file or .clinerules/workflows file will be renamed to a default filename
134+ * Doesn't do anything if the dir already exists or doesn't exist
135+ * Returns whether there are any uncaught errors
136+ */
137+ export async function ensureLocalClineDirExists ( clinerulePath : string , defaultRuleFilename : string ) : Promise < boolean > {
138+ try {
139+ const exists = await fileExistsAtPath ( clinerulePath )
140+
141+ if ( exists && ! ( await isDirectory ( clinerulePath ) ) ) {
142+ // logic to convert .clinerules file into directory, and rename the rules file to {defaultRuleFilename}
143+ const content = await fs . readFile ( clinerulePath , "utf8" )
144+ const tempPath = clinerulePath + ".bak"
145+ await fs . rename ( clinerulePath , tempPath ) // create backup
146+ try {
147+ await fs . mkdir ( clinerulePath , { recursive : true } )
148+ await fs . writeFile ( path . join ( clinerulePath , defaultRuleFilename ) , content , "utf8" )
149+ await fs . unlink ( tempPath ) . catch ( ( ) => { } ) // delete backup
150+
151+ return false // conversion successful with no errors
152+ } catch ( conversionError ) {
153+ // attempt to restore backup on conversion failure
154+ try {
155+ await fs . rm ( clinerulePath , { recursive : true , force : true } ) . catch ( ( ) => { } )
156+ await fs . rename ( tempPath , clinerulePath ) // restore backup
157+ } catch ( restoreError ) { }
158+ return true // in either case here we consider this an error
159+ }
160+ }
161+ // exists and is a dir or doesn't exist, either of these cases we dont need to handle here
162+ return false
163+ } catch ( error ) {
164+ return true
165+ }
166+ }
167+
168+ /**
169+ * Create a rule file or workflow file
170+ */
171+ export const createRuleFile = async ( isGlobal : boolean , filename : string , cwd : string , type : string ) => {
172+ try {
173+ let filePath : string
174+ if ( isGlobal ) {
175+ // global means its implicitly clinerules
176+ const globalClineRulesFilePath = await ensureRulesDirectoryExists ( )
177+ filePath = path . join ( globalClineRulesFilePath , filename )
178+ } else {
179+ const localClineRulesFilePath = path . resolve ( cwd , GlobalFileNames . clineRules )
180+
181+ const hasError = await ensureLocalClineDirExists ( localClineRulesFilePath , "default-rules.md" )
182+ if ( hasError === true ) {
183+ return { filePath : null , fileExists : false }
184+ }
185+
186+ await fs . mkdir ( localClineRulesFilePath , { recursive : true } )
187+
188+ if ( type === "workflow" ) {
189+ const localWorkflowsFilePath = path . resolve ( cwd , GlobalFileNames . workflows )
190+
191+ const hasError = await ensureLocalClineDirExists ( localWorkflowsFilePath , "default-workflows.md" )
192+ if ( hasError === true ) {
193+ return { filePath : null , fileExists : false }
194+ }
195+
196+ await fs . mkdir ( localWorkflowsFilePath , { recursive : true } )
197+
198+ filePath = path . join ( localWorkflowsFilePath , filename )
199+ } else {
200+ // clinerules file creation
201+ filePath = path . join ( localClineRulesFilePath , filename )
202+ }
203+ }
204+
205+ const fileExists = await fileExistsAtPath ( filePath )
206+
207+ if ( fileExists ) {
208+ return { filePath, fileExists }
209+ }
210+
211+ await fs . writeFile ( filePath , "" , "utf8" )
212+
213+ return { filePath, fileExists : false }
214+ } catch ( error ) {
215+ return { filePath : null , fileExists : false }
216+ }
217+ }
218+
219+ /**
220+ * Delete a rule file or workflow file
221+ */
222+ export async function deleteRuleFile (
223+ context : vscode . ExtensionContext ,
224+ rulePath : string ,
225+ isGlobal : boolean ,
226+ type : string ,
227+ ) : Promise < { success : boolean ; message : string } > {
228+ try {
229+ // Check if file exists
230+ const fileExists = await fileExistsAtPath ( rulePath )
231+ if ( ! fileExists ) {
232+ return {
233+ success : false ,
234+ message : `File does not exist: ${ rulePath } ` ,
235+ }
236+ }
237+
238+ // Delete the file from disk
239+ await fs . unlink ( rulePath )
240+
241+ // Get the filename for messages
242+ const fileName = path . basename ( rulePath )
243+
244+ // Update the appropriate toggles
245+ if ( isGlobal ) {
246+ const toggles = ( ( await getGlobalState ( context , "globalClineRulesToggles" ) ) as ClineRulesToggles ) || { }
247+ delete toggles [ rulePath ]
248+ await updateGlobalState ( context , "globalClineRulesToggles" , toggles )
249+ } else {
250+ if ( type === "workflow" ) {
251+ const toggles = ( ( await getWorkspaceState ( context , "workflowToggles" ) ) as ClineRulesToggles ) || { }
252+ delete toggles [ rulePath ]
253+ await updateWorkspaceState ( context , "workflowToggles" , toggles )
254+ } else if ( type === "cursor" ) {
255+ const toggles = ( ( await getWorkspaceState ( context , "localCursorRulesToggles" ) ) as ClineRulesToggles ) || { }
256+ delete toggles [ rulePath ]
257+ await updateWorkspaceState ( context , "localCursorRulesToggles" , toggles )
258+ } else if ( type === "windsurf" ) {
259+ const toggles = ( ( await getWorkspaceState ( context , "localWindsurfRulesToggles" ) ) as ClineRulesToggles ) || { }
260+ delete toggles [ rulePath ]
261+ await updateWorkspaceState ( context , "localWindsurfRulesToggles" , toggles )
262+ } else {
263+ const toggles = ( ( await getWorkspaceState ( context , "localClineRulesToggles" ) ) as ClineRulesToggles ) || { }
264+ delete toggles [ rulePath ]
265+ await updateWorkspaceState ( context , "localClineRulesToggles" , toggles )
266+ }
267+ }
268+
269+ return {
270+ success : true ,
271+ message : `File "${ fileName } " deleted successfully` ,
272+ }
273+ } catch ( error ) {
274+ const errorMessage = error instanceof Error ? error . message : String ( error )
275+ console . error ( `Error deleting file: ${ errorMessage } ` , error )
276+ return {
277+ success : false ,
278+ message : `Failed to delete file.` ,
279+ }
280+ }
281+ }
0 commit comments