From 9c58a922436c0c5e7ff1b20ed2edbc269990c772 Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Fri, 18 Jul 2025 01:02:30 +0300 Subject: [PATCH] feat: add kiro profile (#1001) * feat: add kiro profile * chore: fix format * chore: implement requested changes * chore: fix CI --- .changeset/add-kiro-profile.md | 9 + src/constants/profiles.js | 4 +- src/profiles/index.js | 1 + src/profiles/kiro.js | 42 ++++ tests/unit/profiles/kiro-integration.test.js | 142 ++++++++++++ .../profiles/mcp-config-validation.test.js | 19 ++ .../profiles/rule-transformer-kiro.test.js | 215 ++++++++++++++++++ tests/unit/profiles/rule-transformer.test.js | 6 + 8 files changed, 437 insertions(+), 1 deletion(-) create mode 100644 .changeset/add-kiro-profile.md create mode 100644 src/profiles/kiro.js create mode 100644 tests/unit/profiles/kiro-integration.test.js create mode 100644 tests/unit/profiles/rule-transformer-kiro.test.js diff --git a/.changeset/add-kiro-profile.md b/.changeset/add-kiro-profile.md new file mode 100644 index 00000000..a23ff26c --- /dev/null +++ b/.changeset/add-kiro-profile.md @@ -0,0 +1,9 @@ +--- +"task-master-ai": minor +--- + +Add Kiro editor rule profile support + +- Add support for Kiro IDE with custom rule files and MCP configuration +- Generate rule files in `.kiro/steering/` directory with markdown format +- Include MCP server configuration with enhanced file inclusion patterns \ No newline at end of file diff --git a/src/constants/profiles.js b/src/constants/profiles.js index 8521b4d8..9c24648e 100644 --- a/src/constants/profiles.js +++ b/src/constants/profiles.js @@ -1,5 +1,5 @@ /** - * @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile + * @typedef {'amp' | 'claude' | 'cline' | 'codex' | 'cursor' | 'gemini' | 'kiro' | 'opencode' | 'roo' | 'trae' | 'windsurf' | 'vscode' | 'zed'} RulesProfile */ /** @@ -16,6 +16,7 @@ * - codex: Codex integration * - cursor: Cursor IDE rules * - gemini: Gemini integration + * - kiro: Kiro IDE rules * - opencode: OpenCode integration * - roo: Roo Code IDE rules * - trae: Trae IDE rules @@ -35,6 +36,7 @@ export const RULE_PROFILES = [ 'codex', 'cursor', 'gemini', + 'kiro', 'opencode', 'roo', 'trae', diff --git a/src/profiles/index.js b/src/profiles/index.js index 202f2663..d906e474 100644 --- a/src/profiles/index.js +++ b/src/profiles/index.js @@ -5,6 +5,7 @@ export { clineProfile } from './cline.js'; export { codexProfile } from './codex.js'; export { cursorProfile } from './cursor.js'; export { geminiProfile } from './gemini.js'; +export { kiroProfile } from './kiro.js'; export { opencodeProfile } from './opencode.js'; export { rooProfile } from './roo.js'; export { traeProfile } from './trae.js'; diff --git a/src/profiles/kiro.js b/src/profiles/kiro.js new file mode 100644 index 00000000..5dff0604 --- /dev/null +++ b/src/profiles/kiro.js @@ -0,0 +1,42 @@ +// Kiro profile for rule-transformer +import { createProfile } from './base-profile.js'; + +// Create and export kiro profile using the base factory +export const kiroProfile = createProfile({ + name: 'kiro', + displayName: 'Kiro', + url: 'kiro.dev', + docsUrl: 'kiro.dev/docs', + profileDir: '.kiro', + rulesDir: '.kiro/steering', // Kiro rules location (full path) + mcpConfig: true, + mcpConfigName: 'settings/mcp.json', // Create directly in settings subdirectory + includeDefaultRules: true, // Include default rules to get all the standard files + targetExtension: '.md', + fileMap: { + // Override specific mappings - the base profile will create: + // 'rules/cursor_rules.mdc': 'kiro_rules.md' + // 'rules/dev_workflow.mdc': 'dev_workflow.md' + // 'rules/self_improve.mdc': 'self_improve.md' + // 'rules/taskmaster.mdc': 'taskmaster.md' + // We can add additional custom mappings here if needed + }, + customReplacements: [ + // Core Kiro directory structure changes + { from: /\.cursor\/rules/g, to: '.kiro/steering' }, + { from: /\.cursor\/mcp\.json/g, to: '.kiro/settings/mcp.json' }, + + // Fix any remaining kiro/rules references that might be created during transformation + { from: /\.kiro\/rules/g, to: '.kiro/steering' }, + + // Essential markdown link transformations for Kiro structure + { + from: /\[(.+?)\]\(mdc:\.cursor\/rules\/(.+?)\.mdc\)/g, + to: '[$1](.kiro/steering/$2.md)' + }, + + // Kiro specific terminology + { from: /rules directory/g, to: 'steering directory' }, + { from: /cursor rules/gi, to: 'Kiro steering files' } + ] +}); diff --git a/tests/unit/profiles/kiro-integration.test.js b/tests/unit/profiles/kiro-integration.test.js new file mode 100644 index 00000000..5f1e9e59 --- /dev/null +++ b/tests/unit/profiles/kiro-integration.test.js @@ -0,0 +1,142 @@ +import { jest } from '@jest/globals'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; + +// Mock external modules +jest.mock('child_process', () => ({ + execSync: jest.fn() +})); + +// Mock console methods +jest.mock('console', () => ({ + log: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + clear: jest.fn() +})); + +describe('Kiro Integration', () => { + let tempDir; + + beforeEach(() => { + jest.clearAllMocks(); + + // Create a temporary directory for testing + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'task-master-test-')); + + // Spy on fs methods + jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + jest.spyOn(fs, 'readFileSync').mockImplementation((filePath) => { + if (filePath.toString().includes('mcp.json')) { + return JSON.stringify({ mcpServers: {} }, null, 2); + } + return '{}'; + }); + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + jest.spyOn(fs, 'mkdirSync').mockImplementation(() => {}); + }); + + afterEach(() => { + // Clean up the temporary directory + try { + fs.rmSync(tempDir, { recursive: true, force: true }); + } catch (err) { + console.error(`Error cleaning up: ${err.message}`); + } + }); + + // Test function that simulates the createProjectStructure behavior for Kiro files + function mockCreateKiroStructure() { + // This function simulates the actual kiro profile creation logic + // It explicitly calls the mocked fs methods to ensure consistency with the test environment + + // Simulate directory creation calls - these will call the mocked mkdirSync + fs.mkdirSync(path.join(tempDir, '.kiro'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, '.kiro', 'steering'), { recursive: true }); + fs.mkdirSync(path.join(tempDir, '.kiro', 'settings'), { recursive: true }); + + // Create MCP config file at .kiro/settings/mcp.json + // This will call the mocked writeFileSync + fs.writeFileSync( + path.join(tempDir, '.kiro', 'settings', 'mcp.json'), + JSON.stringify({ mcpServers: {} }, null, 2) + ); + + // Create kiro rule files in steering directory + // All these will call the mocked writeFileSync + fs.writeFileSync( + path.join(tempDir, '.kiro', 'steering', 'kiro_rules.md'), + '# Kiro Rules\n\nKiro-specific rules and instructions.' + ); + fs.writeFileSync( + path.join(tempDir, '.kiro', 'steering', 'dev_workflow.md'), + '# Development Workflow\n\nDevelopment workflow instructions.' + ); + fs.writeFileSync( + path.join(tempDir, '.kiro', 'steering', 'self_improve.md'), + '# Self Improvement\n\nSelf improvement guidelines.' + ); + fs.writeFileSync( + path.join(tempDir, '.kiro', 'steering', 'taskmaster.md'), + '# Task Master\n\nTask Master integration instructions.' + ); + } + + test('creates all required .kiro directories', () => { + // Act + mockCreateKiroStructure(); + + // Assert + expect(fs.mkdirSync).toHaveBeenCalledWith(path.join(tempDir, '.kiro'), { + recursive: true + }); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'steering'), + { + recursive: true + } + ); + expect(fs.mkdirSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'settings'), + { + recursive: true + } + ); + }); + + test('creates Kiro mcp.json with mcpServers format', () => { + // Act + mockCreateKiroStructure(); + + // Assert + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'settings', 'mcp.json'), + JSON.stringify({ mcpServers: {} }, null, 2) + ); + }); + + test('creates rule files in steering directory', () => { + // Act + mockCreateKiroStructure(); + + // Assert + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'steering', 'kiro_rules.md'), + '# Kiro Rules\n\nKiro-specific rules and instructions.' + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'steering', 'dev_workflow.md'), + '# Development Workflow\n\nDevelopment workflow instructions.' + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'steering', 'self_improve.md'), + '# Self Improvement\n\nSelf improvement guidelines.' + ); + expect(fs.writeFileSync).toHaveBeenCalledWith( + path.join(tempDir, '.kiro', 'steering', 'taskmaster.md'), + '# Task Master\n\nTask Master integration instructions.' + ); + }); +}); diff --git a/tests/unit/profiles/mcp-config-validation.test.js b/tests/unit/profiles/mcp-config-validation.test.js index 6e3aff24..edf3ac78 100644 --- a/tests/unit/profiles/mcp-config-validation.test.js +++ b/tests/unit/profiles/mcp-config-validation.test.js @@ -41,6 +41,12 @@ describe('MCP Configuration Validation', () => { expectedConfigName: 'settings.json', expectedPath: '.gemini/settings.json' }, + kiro: { + shouldHaveMcp: true, + expectedDir: '.kiro', + expectedConfigName: 'settings/mcp.json', + expectedPath: '.kiro/settings/mcp.json' + }, opencode: { shouldHaveMcp: true, expectedDir: '.', @@ -128,6 +134,7 @@ describe('MCP Configuration Validation', () => { test('should ensure all MCP-enabled profiles use proper directory structure', () => { const rootProfiles = ['opencode', 'claude', 'codex']; // Profiles that use root directory for config + const nestedConfigProfiles = ['kiro']; // Profiles that use nested directories for config RULE_PROFILES.forEach((profileName) => { const profile = getRulesProfile(profileName); @@ -140,6 +147,11 @@ describe('MCP Configuration Validation', () => { // Other root profiles normalize to just the filename (no ./ prefix) expect(profile.mcpConfigPath).toMatch(/^[\w_.]+$/); } + } else if (nestedConfigProfiles.includes(profileName)) { + // Profiles with nested config directories + expect(profile.mcpConfigPath).toMatch( + /^\.[\w-]+\/[\w-]+\/[\w_.]+$/ + ); } else { // Other profiles should have config files in their specific directories expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/); @@ -347,6 +359,13 @@ describe('MCP Configuration Validation', () => { // Other root profiles normalize to just the filename expect(profile.mcpConfigPath).toBe(profile.mcpConfigName); } + } else if (profileName === 'kiro') { + // Kiro has a nested config structure + const parts = profile.mcpConfigPath.split('/'); + expect(parts).toHaveLength(3); // Should be profileDir/settings/mcp.json + expect(parts[0]).toBe(profile.profileDir); + expect(parts[1]).toBe('settings'); + expect(parts[2]).toBe('mcp.json'); } else { // Non-root profiles should have profileDir/configName structure const parts = profile.mcpConfigPath.split('/'); diff --git a/tests/unit/profiles/rule-transformer-kiro.test.js b/tests/unit/profiles/rule-transformer-kiro.test.js new file mode 100644 index 00000000..b1a2ce81 --- /dev/null +++ b/tests/unit/profiles/rule-transformer-kiro.test.js @@ -0,0 +1,215 @@ +import { jest } from '@jest/globals'; + +// Mock fs module before importing anything that uses it +jest.mock('fs', () => ({ + readFileSync: jest.fn(), + writeFileSync: jest.fn(), + existsSync: jest.fn(), + mkdirSync: jest.fn() +})); + +// Import modules after mocking +import fs from 'fs'; +import { convertRuleToProfileRule } from '../../../src/utils/rule-transformer.js'; +import { kiroProfile } from '../../../src/profiles/kiro.js'; + +describe('Kiro Rule Transformer', () => { + // Set up spies on the mocked modules + const mockReadFileSync = jest.spyOn(fs, 'readFileSync'); + const mockWriteFileSync = jest.spyOn(fs, 'writeFileSync'); + const mockExistsSync = jest.spyOn(fs, 'existsSync'); + const mockMkdirSync = jest.spyOn(fs, 'mkdirSync'); + const mockConsoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => {}); + + beforeEach(() => { + jest.clearAllMocks(); + // Setup default mocks + mockReadFileSync.mockReturnValue(''); + mockWriteFileSync.mockImplementation(() => {}); + mockExistsSync.mockReturnValue(true); + mockMkdirSync.mockImplementation(() => {}); + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should correctly convert basic terms', () => { + const testContent = `--- +description: Test Cursor rule for basic terms +globs: **/* +alwaysApply: true +--- + +This is a Cursor rule that references cursor.so and uses the word Cursor multiple times. +Also has references to .mdc files.`; + + // Mock file read to return our test content + mockReadFileSync.mockReturnValue(testContent); + + // Mock file system operations + mockExistsSync.mockReturnValue(true); + + // Call the function + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'test-target.md', + kiroProfile + ); + + // Verify the result + expect(result).toBe(true); + expect(mockWriteFileSync).toHaveBeenCalledTimes(1); + + // Get the transformed content + const transformedContent = mockWriteFileSync.mock.calls[0][1]; + + // Verify Cursor -> Kiro transformations + expect(transformedContent).toContain('kiro.dev'); + expect(transformedContent).toContain('Kiro'); + expect(transformedContent).not.toContain('cursor.so'); + expect(transformedContent).not.toContain('Cursor'); + expect(transformedContent).toContain('.md'); + expect(transformedContent).not.toContain('.mdc'); + }); + + it('should handle URL transformations', () => { + const testContent = `Visit https://cursor.so/docs for more information. +Also check out cursor.so and www.cursor.so for updates.`; + + mockReadFileSync.mockReturnValue(testContent); + mockExistsSync.mockReturnValue(true); + + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'test-target.md', + kiroProfile + ); + + expect(result).toBe(true); + const transformedContent = mockWriteFileSync.mock.calls[0][1]; + + // Verify URL transformations + expect(transformedContent).toContain('https://kiro.dev'); + expect(transformedContent).toContain('kiro.dev'); + expect(transformedContent).not.toContain('cursor.so'); + }); + + it('should handle file extension transformations', () => { + const testContent = `This rule references file.mdc and another.mdc file. +Use the .mdc extension for all rule files.`; + + mockReadFileSync.mockReturnValue(testContent); + mockExistsSync.mockReturnValue(true); + + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'test-target.md', + kiroProfile + ); + + expect(result).toBe(true); + const transformedContent = mockWriteFileSync.mock.calls[0][1]; + + // Verify file extension transformations + expect(transformedContent).toContain('file.md'); + expect(transformedContent).toContain('another.md'); + expect(transformedContent).toContain('.md extension'); + expect(transformedContent).not.toContain('.mdc'); + }); + + it('should handle case variations', () => { + const testContent = `CURSOR, Cursor, cursor should all be transformed.`; + + mockReadFileSync.mockReturnValue(testContent); + mockExistsSync.mockReturnValue(true); + + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'test-target.md', + kiroProfile + ); + + expect(result).toBe(true); + const transformedContent = mockWriteFileSync.mock.calls[0][1]; + + // Verify case transformations + // Due to regex order, the case-insensitive rule runs first: + // CURSOR -> Kiro (because it starts with 'C'), Cursor -> Kiro, cursor -> kiro + expect(transformedContent).toContain('Kiro'); + expect(transformedContent).toContain('kiro'); + expect(transformedContent).not.toContain('CURSOR'); + expect(transformedContent).not.toContain('Cursor'); + expect(transformedContent).not.toContain('cursor'); + }); + + it('should create target directory if it does not exist', () => { + const testContent = 'Test content'; + mockReadFileSync.mockReturnValue(testContent); + mockExistsSync.mockReturnValue(false); + + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'nested/path/test-target.md', + kiroProfile + ); + + expect(result).toBe(true); + expect(mockMkdirSync).toHaveBeenCalledWith('nested/path', { + recursive: true + }); + }); + + it('should handle file system errors gracefully', () => { + mockReadFileSync.mockImplementation(() => { + throw new Error('File not found'); + }); + + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'test-target.md', + kiroProfile + ); + + expect(result).toBe(false); + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error converting rule file: File not found' + ); + }); + + it('should handle write errors gracefully', () => { + mockReadFileSync.mockReturnValue('Test content'); + mockWriteFileSync.mockImplementation(() => { + throw new Error('Write permission denied'); + }); + + const result = convertRuleToProfileRule( + 'test-source.mdc', + 'test-target.md', + kiroProfile + ); + + expect(result).toBe(false); + expect(mockConsoleError).toHaveBeenCalledWith( + 'Error converting rule file: Write permission denied' + ); + }); + + it('should verify profile configuration', () => { + expect(kiroProfile.profileName).toBe('kiro'); + expect(kiroProfile.displayName).toBe('Kiro'); + expect(kiroProfile.profileDir).toBe('.kiro'); + expect(kiroProfile.mcpConfig).toBe(true); + expect(kiroProfile.mcpConfigName).toBe('settings/mcp.json'); + expect(kiroProfile.mcpConfigPath).toBe('.kiro/settings/mcp.json'); + expect(kiroProfile.includeDefaultRules).toBe(true); + expect(kiroProfile.fileMap).toEqual({ + 'rules/cursor_rules.mdc': 'kiro_rules.md', + 'rules/dev_workflow.mdc': 'dev_workflow.md', + 'rules/self_improve.mdc': 'self_improve.md', + 'rules/taskmaster.mdc': 'taskmaster.md' + }); + }); +}); diff --git a/tests/unit/profiles/rule-transformer.test.js b/tests/unit/profiles/rule-transformer.test.js index c93f957c..4e2fbcee 100644 --- a/tests/unit/profiles/rule-transformer.test.js +++ b/tests/unit/profiles/rule-transformer.test.js @@ -19,6 +19,7 @@ describe('Rule Transformer - General', () => { 'codex', 'cursor', 'gemini', + 'kiro', 'opencode', 'roo', 'trae', @@ -212,6 +213,11 @@ describe('Rule Transformer - General', () => { mcpConfigName: 'settings.json', expectedPath: '.gemini/settings.json' }, + kiro: { + mcpConfig: true, + mcpConfigName: 'settings/mcp.json', + expectedPath: '.kiro/settings/mcp.json' + }, opencode: { mcpConfig: true, mcpConfigName: 'opencode.json',