Compare commits
6 Commits
chore/crea
...
feat/imple
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a24b997c3 | ||
|
|
da3cf08d6e | ||
|
|
b991c2b48b | ||
|
|
f17f01fddf | ||
|
|
4639eee097 | ||
|
|
b78de8dbb4 |
9
.changeset/add-kiro-profile.md
Normal file
9
.changeset/add-kiro-profile.md
Normal file
@@ -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
|
||||
5
.changeset/update-mcp-readme.md
Normal file
5
.changeset/update-mcp-readme.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"task-master-ai": patch
|
||||
---
|
||||
|
||||
Correct MCP server name and use 'Add to Cursor' button with updated placeholder keys.
|
||||
10
README.md
10
README.md
@@ -25,11 +25,7 @@ For more detailed information, check out the documentation in the `docs` directo
|
||||
|
||||
#### Quick Install for Cursor 1.0+ (One-Click)
|
||||
|
||||
📋 Click the copy button (top-right of code block) then paste into your browser:
|
||||
|
||||
```text
|
||||
cursor://anysphere.cursor-deeplink/mcp/install?name=taskmaster-ai&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIi0tcGFja2FnZT10YXNrLW1hc3Rlci1haSIsInRhc2stbWFzdGVyLWFpIl0sImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQo=
|
||||
```
|
||||
[](https://cursor.com/install-mcp?name=task-master-ai&config=eyJjb21tYW5kIjoibnB4IC15IC0tcGFja2FnZT10YXNrLW1hc3Rlci1haSB0YXNrLW1hc3Rlci1haSIsImVudiI6eyJBTlRIUk9QSUNfQVBJX0tFWSI6IllPVVJfQU5USFJPUElDX0FQSV9LRVlfSEVSRSIsIlBFUlBMRVhJVFlfQVBJX0tFWSI6IllPVVJfUEVSUExFWElUWV9BUElfS0VZX0hFUkUiLCJPUEVOQUlfQVBJX0tFWSI6IllPVVJfT1BFTkFJX0tFWV9IRVJFIiwiR09PR0xFX0FQSV9LRVkiOiJZT1VSX0dPT0dMRV9LRVlfSEVSRSIsIk1JU1RSQUxfQVBJX0tFWSI6IllPVVJfTUlTVFJBTF9LRVlfSEVSRSIsIkdST1FfQVBJX0tFWSI6IllPVVJfR1JPUV9LRVlfSEVSRSIsIk9QRU5ST1VURVJfQVBJX0tFWSI6IllPVVJfT1BFTlJPVVRFUl9LRVlfSEVSRSIsIlhBSV9BUElfS0VZIjoiWU9VUl9YQUlfS0VZX0hFUkUiLCJBWlVSRV9PUEVOQUlfQVBJX0tFWSI6IllPVVJfQVpVUkVfS0VZX0hFUkUiLCJPTExBTUFfQVBJX0tFWSI6IllPVVJfT0xMQU1BX0FQSV9LRVlfSEVSRSJ9fQ%3D%3D)
|
||||
|
||||
> **Note:** After clicking the link, you'll still need to add your API keys to the configuration. The link installs the MCP server with placeholder keys that you'll need to replace with your actual API keys.
|
||||
|
||||
@@ -73,7 +69,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"taskmaster-ai": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
@@ -102,7 +98,7 @@ MCP (Model Control Protocol) lets you run Task Master directly from your editor.
|
||||
```json
|
||||
{
|
||||
"servers": {
|
||||
"taskmaster-ai": {
|
||||
"task-master-ai": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "--package=task-master-ai", "task-master-ai"],
|
||||
"env": {
|
||||
|
||||
@@ -190,8 +190,45 @@ function parseUpdatedTaskFromText(text, expectedTaskId, logFn, isMCP) {
|
||||
throw new Error('Parsed AI response is not a valid JSON object.');
|
||||
}
|
||||
|
||||
// Preprocess the task to ensure subtasks have proper structure
|
||||
const preprocessedTask = {
|
||||
...parsedTask,
|
||||
status: parsedTask.status || 'pending',
|
||||
dependencies: Array.isArray(parsedTask.dependencies)
|
||||
? parsedTask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof parsedTask.details === 'string'
|
||||
? parsedTask.details
|
||||
: String(parsedTask.details || ''),
|
||||
testStrategy:
|
||||
typeof parsedTask.testStrategy === 'string'
|
||||
? parsedTask.testStrategy
|
||||
: String(parsedTask.testStrategy || ''),
|
||||
// Ensure subtasks is an array and each subtask has required fields
|
||||
subtasks: Array.isArray(parsedTask.subtasks)
|
||||
? parsedTask.subtasks.map((subtask) => ({
|
||||
...subtask,
|
||||
title: subtask.title || '',
|
||||
description: subtask.description || '',
|
||||
status: subtask.status || 'pending',
|
||||
dependencies: Array.isArray(subtask.dependencies)
|
||||
? subtask.dependencies
|
||||
: [],
|
||||
details:
|
||||
typeof subtask.details === 'string'
|
||||
? subtask.details
|
||||
: String(subtask.details || ''),
|
||||
testStrategy:
|
||||
typeof subtask.testStrategy === 'string'
|
||||
? subtask.testStrategy
|
||||
: String(subtask.testStrategy || '')
|
||||
}))
|
||||
: []
|
||||
};
|
||||
|
||||
// Validate the parsed task object using Zod
|
||||
const validationResult = updatedTaskSchema.safeParse(parsedTask);
|
||||
const validationResult = updatedTaskSchema.safeParse(preprocessedTask);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task object failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
|
||||
@@ -196,7 +196,18 @@ function parseUpdatedTasksFromText(text, expectedCount, logFn, isMCP) {
|
||||
);
|
||||
}
|
||||
|
||||
const validationResult = updatedTaskArraySchema.safeParse(parsedTasks);
|
||||
// Preprocess tasks to ensure required fields have proper defaults
|
||||
const preprocessedTasks = parsedTasks.map((task) => ({
|
||||
...task,
|
||||
// Ensure subtasks is always an array (not null or undefined)
|
||||
subtasks: Array.isArray(task.subtasks) ? task.subtasks : [],
|
||||
// Ensure status has a default value if missing
|
||||
status: task.status || 'pending',
|
||||
// Ensure dependencies is always an array
|
||||
dependencies: Array.isArray(task.dependencies) ? task.dependencies : []
|
||||
}));
|
||||
|
||||
const validationResult = updatedTaskArraySchema.safeParse(preprocessedTasks);
|
||||
if (!validationResult.success) {
|
||||
report('error', 'Parsed task array failed Zod validation.');
|
||||
validationResult.error.errors.forEach((err) => {
|
||||
@@ -442,7 +453,17 @@ async function updateTasks(
|
||||
data.tasks.forEach((task, index) => {
|
||||
if (updatedTasksMap.has(task.id)) {
|
||||
// Only update if the task was part of the set sent to AI
|
||||
data.tasks[index] = updatedTasksMap.get(task.id);
|
||||
const updatedTask = updatedTasksMap.get(task.id);
|
||||
// Merge the updated task with the existing one to preserve fields like subtasks
|
||||
data.tasks[index] = {
|
||||
...task, // Keep all existing fields
|
||||
...updatedTask, // Override with updated fields
|
||||
// Ensure subtasks field is preserved if not provided by AI
|
||||
subtasks:
|
||||
updatedTask.subtasks !== undefined
|
||||
? updatedTask.subtasks
|
||||
: task.subtasks
|
||||
};
|
||||
actualUpdateCount++;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
42
src/profiles/kiro.js
Normal file
42
src/profiles/kiro.js
Normal file
@@ -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' }
|
||||
]
|
||||
});
|
||||
142
tests/unit/profiles/kiro-integration.test.js
Normal file
142
tests/unit/profiles/kiro-integration.test.js
Normal file
@@ -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.'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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('/');
|
||||
|
||||
215
tests/unit/profiles/rule-transformer-kiro.test.js
Normal file
215
tests/unit/profiles/rule-transformer-kiro.test.js
Normal file
@@ -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'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user