From 1346f291a35c98b83f4883c50310436f76b2c578 Mon Sep 17 00:00:00 2001 From: Tim Gent Date: Fri, 2 May 2025 16:15:10 +0100 Subject: [PATCH] Handle rules files in nested directory or root directory --- .changeset/brown-cycles-open.md | 5 ++ .changeset/purple-places-do.md | 5 ++ src/prompting/common.ts | 27 ++++---- src/rules/rules.test.ts | 106 ++++++++++++++++++++++++++++++++ src/rules/rules.ts | 12 ++-- 5 files changed, 138 insertions(+), 17 deletions(-) create mode 100644 .changeset/brown-cycles-open.md create mode 100644 .changeset/purple-places-do.md diff --git a/.changeset/brown-cycles-open.md b/.changeset/brown-cycles-open.md new file mode 100644 index 0000000..060bb53 --- /dev/null +++ b/.changeset/brown-cycles-open.md @@ -0,0 +1,5 @@ +--- +"@multiverse-io/cari": minor +--- + +Handle rules file in root directory or in nested directories diff --git a/.changeset/purple-places-do.md b/.changeset/purple-places-do.md new file mode 100644 index 0000000..27e309d --- /dev/null +++ b/.changeset/purple-places-do.md @@ -0,0 +1,5 @@ +--- +"@multiverse-io/cari": patch +--- + +Gracefully handle nested rule folders and root level rules diff --git a/src/prompting/common.ts b/src/prompting/common.ts index caebf81..c0f8b30 100644 --- a/src/prompting/common.ts +++ b/src/prompting/common.ts @@ -33,18 +33,21 @@ export const directoryChoice = ( repo: string, directory: string, ruleRelativeFilePaths: RuleFilePath[] -): PromptChoice => ({ - name: `---> Whole directory: ${directory}`, - value: { - type: "directory", - org, - repo, - directory: directory, - ruleRelativeFilePaths, - }, - short: `Dir: ${directory}`, - disabled: false, -}); +): PromptChoice => { + const directoryForPrompt = directory === "" ? "root" : directory; + return { + name: `---> Files in directory: ${directoryForPrompt}`, + value: { + type: "directory", + org, + repo, + directory: directory, + ruleRelativeFilePaths, + }, + short: `Dir: ${directory}`, + disabled: false, + }; +}; export const fileChoice = ( org: string, diff --git a/src/rules/rules.test.ts b/src/rules/rules.test.ts index aa9aeed..2b9e171 100644 --- a/src/rules/rules.test.ts +++ b/src/rules/rules.test.ts @@ -83,6 +83,66 @@ describe("writeRulesToProject", () => { ) ).toBe("some more rule content"); }); + + it("should handle rules that are directly in the rules directory", async () => { + mockFs({ + [projectDir]: {}, + // File directly in the rules directory + "/home/user/.cari/org/repo/rules/root-rule.mdc": "root rule content", + }); + const rules: RepoRules[] = [ + { + org: "org", + repo: "repo", + relativeFilePaths: [ + { + fileName: "root-rule.mdc", + categoryFolderName: "", // Empty for root-level rules + }, + ], + }, + ]; + await writeRulesToProject(rules); + + expect( + fs.readFileSync( + path.join(projectDir, ".cursor/rules/org/repo/root-rule.mdc"), + "utf8" + ) + ).toBe("root rule content"); + }); + + it("should handle rules with nested category folders", async () => { + mockFs({ + [projectDir]: {}, + // File in nested category directories + "/home/user/.cari/org/repo/rules/typescript/events/nested-rule.mdc": + "nested rule content", + }); + const rules: RepoRules[] = [ + { + org: "org", + repo: "repo", + relativeFilePaths: [ + { + fileName: "nested-rule.mdc", + categoryFolderName: "typescript/events", // Nested path + }, + ], + }, + ]; + await writeRulesToProject(rules); + + expect( + fs.readFileSync( + path.join( + projectDir, + ".cursor/rules/org/repo/typescript/events/nested-rule.mdc" + ), + "utf8" + ) + ).toBe("nested rule content"); + }); }); describe("getCentralRules", () => { @@ -154,6 +214,52 @@ describe("getCentralRules", () => { ]); }); + it("should extract categoryFolderName as path relative to rules directory", async () => { + const mockFolderStructure = { + "/home/user/.cari/my-org/rules-repo": { + rules: { + // File directly in rules directory + "root-rule.mdc": "root rule content", + // File in category directory + category: { + "category-rule.mdc": "category rule content", + }, + // File in nested category directories + typescript: { + events: { + "events-rule.mdc": "events rule content", + }, + }, + }, + }, + }; + + mockFs(mockFolderStructure); + const centralRules = await getCentralRules([ + { + orgName: "my-org", + repoName: "rules-repo", + repoDir: "/home/user/.cari/my-org/rules-repo", + }, + ]); + + // Find each rule by filename and check its categoryFolderName + const rootRule = centralRules[0].relativeFilePaths.find( + (rule) => rule.fileName === "root-rule.mdc" + ); + const categoryRule = centralRules[0].relativeFilePaths.find( + (rule) => rule.fileName === "category-rule.mdc" + ); + const eventsRule = centralRules[0].relativeFilePaths.find( + (rule) => rule.fileName === "events-rule.mdc" + ); + + // Assert that categoryFolderName is correctly set + expect(rootRule?.categoryFolderName).toBe(""); + expect(categoryRule?.categoryFolderName).toBe("category"); + expect(eventsRule?.categoryFolderName).toBe("typescript/events"); + }); + it("should log a warning when a repo has no rules", async () => { const mockFolderStructure = { "/home/user/.cari/my-org/empty-repo": { diff --git a/src/rules/rules.ts b/src/rules/rules.ts index 19b0082..a13b38d 100644 --- a/src/rules/rules.ts +++ b/src/rules/rules.ts @@ -30,7 +30,10 @@ export const getCentralRules = async ( const fileNames = relativeFilePaths.map((filePath) => { const fileName = path.basename(filePath); - const categoryFolderName = path.basename(path.dirname(filePath)); + const rulesDir = path.join(repoDir, "rules"); + const fileDir = path.dirname(filePath); + const categoryFolderName = + fileDir === rulesDir ? "" : path.relative(rulesDir, fileDir); return { fileName, categoryFolderName, @@ -91,11 +94,10 @@ const getCentralRepoRulePath = ( repo: string, relativeFilePath: RuleFilePath ) => { + const basePath = path.join(getAriHomeDir(), org, repo, "rules"); + return path.join( - getAriHomeDir(), - org, - repo, - "rules", + basePath, relativeFilePath.categoryFolderName, relativeFilePath.fileName );