Unify and streamline profile system architecture (#853)

* move claude rules and commands to assets/claude

* update claude profile to copy assets/claude to .claude

* fix formatting

* feat(profiles): Implement unified profile system

- Convert Claude and Codex profiles to use createProfile() factory
- Remove simple vs complex profile distinction in rule transformer
- Unify convertAllRulesToProfileRules() to handle all profiles consistently
- Fix mcpConfigPath construction in base-profile.js for null mcpConfigName
- Update terminology from 'simpleProfiles' to 'assetOnlyProfiles' throughout
- Ensure Claude .claude directory copying works in both CLI and MCP contexts
- All profiles now follow same execution flow with proper lifecycle functions

Changes:
- src/profiles/claude.js: Convert to createProfile() factory pattern
- src/profiles/codex.js: Convert to createProfile() factory pattern
- src/utils/rule-transformer.js: Unified profile handling logic
- src/utils/profiles.js: Remove simple profile categorization
- src/profiles/base-profile.js: Fix mcpConfigPath construction
- scripts/modules/commands.js: Update variable naming
- tests/: Update all tests for unified system and terminology

Fixes Claude profile asset copying issue in MCP context.
All tests passing (617 passed, 11 skipped).

* re-checkin claude files

* fix formatting

* chore: clean up test Claude rules files

* chore: add changeset for unified profile system

* add claude files back

* add changeset

* restore proper gitignore

* remove claude agents file from root

* remove incorrect doc

* simplify profiles and update tests

* update changeset

* update changeset

* remove profile specific code

* streamline profiles with defaults and update tests

* update changeset

* add newline at end of gitignore

* restore changes

* streamline profiles with defaults; update tests and add vscode test

* update rule profile tests

* update wording for clearer profile management

* refactor and clarify terminology

* use original projectRoot var name

* revert param desc

* use updated claude assets from neno

* add "YOUR_" before api key here

* streamline codex profile

* add gemini profile

* update gemini profile

* update tests

* relocate function

* update rules interactive setup Gemini desc

* remove duplicative code

* add comma
This commit is contained in:
Joe Danziger
2025-07-09 07:22:11 -04:00
committed by GitHub
parent 5f009a5e1f
commit 95c299df64
82 changed files with 4827 additions and 720 deletions

View File

@@ -16,8 +16,9 @@ import path from 'path';
* @param {string} [editorConfig.targetExtension='.md'] - Target file extension
* @param {Object} [editorConfig.toolMappings={}] - Tool name mappings
* @param {Array} [editorConfig.customReplacements=[]] - Custom text replacements
* @param {Object} [editorConfig.customFileMap={}] - Custom file name mappings
* @param {Object} [editorConfig.fileMap={}] - Custom file name mappings
* @param {boolean} [editorConfig.supportsRulesSubdirectories=false] - Whether to use taskmaster/ subdirectory for taskmaster-specific rules (only Cursor uses this by default)
* @param {boolean} [editorConfig.includeDefaultRules=true] - Whether to include default rule files
* @param {Function} [editorConfig.onAdd] - Lifecycle hook for profile addition
* @param {Function} [editorConfig.onRemove] - Lifecycle hook for profile removal
* @param {Function} [editorConfig.onPostConvert] - Lifecycle hook for post-conversion
@@ -29,34 +30,38 @@ export function createProfile(editorConfig) {
displayName = name,
url,
docsUrl,
profileDir,
profileDir = `.${name.toLowerCase()}`,
rulesDir = `${profileDir}/rules`,
mcpConfig = true,
mcpConfigName = 'mcp.json',
mcpConfigName = mcpConfig ? 'mcp.json' : null,
fileExtension = '.mdc',
targetExtension = '.md',
toolMappings = {},
customReplacements = [],
customFileMap = {},
fileMap = {},
supportsRulesSubdirectories = false,
includeDefaultRules = true,
onAdd,
onRemove,
onPostConvert
} = editorConfig;
const mcpConfigPath = `${profileDir}/${mcpConfigName}`;
const mcpConfigPath = mcpConfigName ? `${profileDir}/${mcpConfigName}` : null;
// Standard file mapping with custom overrides
// Use taskmaster subdirectory only if profile supports it
const taskmasterPrefix = supportsRulesSubdirectories ? 'taskmaster/' : '';
const defaultFileMap = {
'cursor_rules.mdc': `${name.toLowerCase()}_rules${targetExtension}`,
'dev_workflow.mdc': `${taskmasterPrefix}dev_workflow${targetExtension}`,
'self_improve.mdc': `self_improve${targetExtension}`,
'taskmaster.mdc': `${taskmasterPrefix}taskmaster${targetExtension}`
'rules/cursor_rules.mdc': `${name.toLowerCase()}_rules${targetExtension}`,
'rules/dev_workflow.mdc': `${taskmasterPrefix}dev_workflow${targetExtension}`,
'rules/self_improve.mdc': `self_improve${targetExtension}`,
'rules/taskmaster.mdc': `${taskmasterPrefix}taskmaster${targetExtension}`
};
const fileMap = { ...defaultFileMap, ...customFileMap };
// Build final fileMap - merge defaults with custom entries when includeDefaultRules is true
const finalFileMap = includeDefaultRules
? { ...defaultFileMap, ...fileMap }
: fileMap;
// Base global replacements that work for all editors
const baseGlobalReplacements = [
@@ -187,7 +192,8 @@ export function createProfile(editorConfig) {
replacement: (match, text, filePath) => {
const baseName = path.basename(filePath, '.mdc');
const newFileName =
fileMap[`${baseName}.mdc`] || `${baseName}${targetExtension}`;
finalFileMap[`rules/${baseName}.mdc`] ||
`${baseName}${targetExtension}`;
// Update the link text to match the new filename (strip directory path for display)
const newLinkText = path.basename(newFileName);
// For Cursor, keep the mdc: protocol; for others, use standard relative paths
@@ -201,8 +207,8 @@ export function createProfile(editorConfig) {
};
function getTargetRuleFilename(sourceFilename) {
if (fileMap[sourceFilename]) {
return fileMap[sourceFilename];
if (finalFileMap[sourceFilename]) {
return finalFileMap[sourceFilename];
}
return targetExtension !== fileExtension
? sourceFilename.replace(
@@ -221,7 +227,8 @@ export function createProfile(editorConfig) {
mcpConfigName,
mcpConfigPath,
supportsRulesSubdirectories,
fileMap,
includeDefaultRules,
fileMap: finalFileMap,
globalReplacements: baseGlobalReplacements,
conversionConfig,
getTargetRuleFilename,

View File

@@ -2,58 +2,96 @@
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';
// Helper function to recursively copy directory (adopted from Roo profile)
function copyRecursiveSync(src, dest) {
const exists = fs.existsSync(src);
const stats = exists && fs.statSync(src);
const isDirectory = exists && stats.isDirectory();
if (isDirectory) {
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
fs.readdirSync(src).forEach((childItemName) => {
copyRecursiveSync(
path.join(src, childItemName),
path.join(dest, childItemName)
);
});
} else {
fs.copyFileSync(src, dest);
}
}
// Helper function to recursively remove directory
function removeDirectoryRecursive(dirPath) {
if (fs.existsSync(dirPath)) {
try {
fs.rmSync(dirPath, { recursive: true, force: true });
return true;
} catch (err) {
log('error', `Failed to remove directory ${dirPath}: ${err.message}`);
return false;
}
}
return true;
}
// Lifecycle functions for Claude Code profile
function onAddRulesProfile(targetDir, assetsDir) {
// Use the provided assets directory to find the source file
const sourceFile = path.join(assetsDir, 'AGENTS.md');
const destFile = path.join(targetDir, 'CLAUDE.md');
// Copy .claude directory recursively
const claudeSourceDir = path.join(assetsDir, 'claude');
const claudeDestDir = path.join(targetDir, '.claude');
if (fs.existsSync(sourceFile)) {
try {
fs.copyFileSync(sourceFile, destFile);
log('debug', `[Claude] Copied AGENTS.md to ${destFile}`);
} catch (err) {
log('error', `[Claude] Failed to copy AGENTS.md: ${err.message}`);
}
if (!fs.existsSync(claudeSourceDir)) {
log(
'error',
`[Claude] Source directory does not exist: ${claudeSourceDir}`
);
return;
}
try {
copyRecursiveSync(claudeSourceDir, claudeDestDir);
log('debug', `[Claude] Copied .claude directory to ${claudeDestDir}`);
} catch (err) {
log(
'error',
`[Claude] An error occurred during directory copy: ${err.message}`
);
}
}
function onRemoveRulesProfile(targetDir) {
const claudeFile = path.join(targetDir, 'CLAUDE.md');
if (fs.existsSync(claudeFile)) {
try {
fs.rmSync(claudeFile, { force: true });
log('debug', `[Claude] Removed CLAUDE.md from ${claudeFile}`);
} catch (err) {
log('error', `[Claude] Failed to remove CLAUDE.md: ${err.message}`);
}
// Remove .claude directory recursively
const claudeDir = path.join(targetDir, '.claude');
if (removeDirectoryRecursive(claudeDir)) {
log('debug', `[Claude] Removed .claude directory from ${claudeDir}`);
}
}
function onPostConvertRulesProfile(targetDir, assetsDir) {
// For Claude, post-convert is the same as add since we don't transform rules
onAddRulesProfile(targetDir, assetsDir);
}
// Simple filename function
function getTargetRuleFilename(sourceFilename) {
return sourceFilename;
}
// Simple profile configuration - bypasses base-profile system
export const claudeProfile = {
profileName: 'claude',
// Create and export claude profile using the base factory
export const claudeProfile = createProfile({
name: 'claude',
displayName: 'Claude Code',
url: 'claude.ai',
docsUrl: 'docs.anthropic.com/en/docs/claude-code',
profileDir: '.', // Root directory
rulesDir: '.', // No rules directory needed
mcpConfig: false, // No MCP config needed
rulesDir: '.', // No specific rules directory needed
mcpConfig: false,
mcpConfigName: null,
mcpConfigPath: null,
conversionConfig: {},
fileMap: {},
globalReplacements: [],
getTargetRuleFilename,
onAddRulesProfile,
onRemoveRulesProfile,
onPostConvertRulesProfile
};
includeDefaultRules: false,
fileMap: {
'AGENTS.md': 'CLAUDE.md'
},
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile,
onPostConvert: onPostConvertRulesProfile
});
// Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile };

View File

@@ -9,12 +9,5 @@ export const clineProfile = createProfile({
docsUrl: 'docs.cline.bot',
profileDir: '.clinerules',
rulesDir: '.clinerules',
mcpConfig: false,
mcpConfigName: 'cline_mcp_settings.json',
fileExtension: '.mdc',
targetExtension: '.md',
toolMappings: COMMON_TOOL_MAPPINGS.STANDARD, // Cline uses standard tool names
customFileMap: {
'cursor_rules.mdc': 'cline_rules.md'
}
mcpConfig: false
});

View File

@@ -1,59 +1,18 @@
// Codex profile for rule-transformer
import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js';
// Lifecycle functions for Codex profile
function onAddRulesProfile(targetDir, assetsDir) {
// Use the provided assets directory to find the source file
const sourceFile = path.join(assetsDir, 'AGENTS.md');
const destFile = path.join(targetDir, 'AGENTS.md');
if (fs.existsSync(sourceFile)) {
try {
fs.copyFileSync(sourceFile, destFile);
log('debug', `[Codex] Copied AGENTS.md to ${destFile}`);
} catch (err) {
log('error', `[Codex] Failed to copy AGENTS.md: ${err.message}`);
}
}
}
function onRemoveRulesProfile(targetDir) {
const agentsFile = path.join(targetDir, 'AGENTS.md');
if (fs.existsSync(agentsFile)) {
try {
fs.rmSync(agentsFile, { force: true });
log('debug', `[Codex] Removed AGENTS.md from ${agentsFile}`);
} catch (err) {
log('error', `[Codex] Failed to remove AGENTS.md: ${err.message}`);
}
}
}
function onPostConvertRulesProfile(targetDir, assetsDir) {
onAddRulesProfile(targetDir, assetsDir);
}
// Simple filename function
function getTargetRuleFilename(sourceFilename) {
return sourceFilename;
}
// Simple profile configuration - bypasses base-profile system
export const codexProfile = {
profileName: 'codex',
// Create and export codex profile using the base factory
export const codexProfile = createProfile({
name: 'codex',
displayName: 'Codex',
url: 'codex.ai',
docsUrl: 'platform.openai.com/docs/codex',
profileDir: '.', // Root directory
rulesDir: '.', // No rules directory needed
mcpConfig: false, // No MCP config needed
rulesDir: '.', // No specific rules directory needed
mcpConfig: false,
mcpConfigName: null,
mcpConfigPath: null,
conversionConfig: {},
fileMap: {},
globalReplacements: [],
getTargetRuleFilename,
onAddRulesProfile,
onRemoveRulesProfile,
onPostConvertRulesProfile
};
includeDefaultRules: false,
fileMap: {
'AGENTS.md': 'AGENTS.md'
}
});

View File

@@ -7,15 +7,6 @@ export const cursorProfile = createProfile({
displayName: 'Cursor',
url: 'cursor.so',
docsUrl: 'docs.cursor.com',
profileDir: '.cursor',
rulesDir: '.cursor/rules',
mcpConfig: true,
mcpConfigName: 'mcp.json',
fileExtension: '.mdc',
targetExtension: '.mdc', // Cursor keeps .mdc extension
toolMappings: COMMON_TOOL_MAPPINGS.STANDARD,
supportsRulesSubdirectories: true,
customFileMap: {
'cursor_rules.mdc': 'cursor_rules.mdc' // Keep the same name for cursor
}
supportsRulesSubdirectories: true
});

17
src/profiles/gemini.js Normal file
View File

@@ -0,0 +1,17 @@
// Gemini profile for rule-transformer
import { createProfile } from './base-profile.js';
// Create and export gemini profile using the base factory
export const geminiProfile = createProfile({
name: 'gemini',
displayName: 'Gemini',
url: 'codeassist.google',
docsUrl: 'github.com/google-gemini/gemini-cli',
profileDir: '.gemini', // Keep .gemini for settings.json
rulesDir: '.', // Root directory for GEMINI.md
mcpConfigName: 'settings.json', // Override default 'mcp.json'
includeDefaultRules: false,
fileMap: {
'AGENTS.md': 'GEMINI.md'
}
});

View File

@@ -3,6 +3,7 @@ export { claudeProfile } from './claude.js';
export { clineProfile } from './cline.js';
export { codexProfile } from './codex.js';
export { cursorProfile } from './cursor.js';
export { geminiProfile } from './gemini.js';
export { rooProfile } from './roo.js';
export { traeProfile } from './trae.js';
export { vscodeProfile } from './vscode.js';

View File

@@ -110,16 +110,7 @@ export const rooProfile = createProfile({
displayName: 'Roo Code',
url: 'roocode.com',
docsUrl: 'docs.roocode.com',
profileDir: '.roo',
rulesDir: '.roo/rules',
mcpConfig: true,
mcpConfigName: 'mcp.json',
fileExtension: '.mdc',
targetExtension: '.md',
toolMappings: COMMON_TOOL_MAPPINGS.ROO_STYLE,
customFileMap: {
'cursor_rules.mdc': 'roo_rules.md'
},
onAdd: onAddRulesProfile,
onRemove: onRemoveRulesProfile,
onPostConvert: onPostConvertRulesProfile

View File

@@ -7,11 +7,5 @@ export const traeProfile = createProfile({
displayName: 'Trae',
url: 'trae.ai',
docsUrl: 'docs.trae.ai',
profileDir: '.trae',
rulesDir: '.trae/rules',
mcpConfig: false,
mcpConfigName: 'trae_mcp_settings.json',
fileExtension: '.mdc',
targetExtension: '.md',
toolMappings: COMMON_TOOL_MAPPINGS.STANDARD // Trae uses standard tool names
mcpConfig: false
});

View File

@@ -7,16 +7,7 @@ export const vscodeProfile = createProfile({
displayName: 'VS Code',
url: 'code.visualstudio.com',
docsUrl: 'code.visualstudio.com/docs',
profileDir: '.vscode', // MCP config location
rulesDir: '.github/instructions', // VS Code instructions location
mcpConfig: true,
mcpConfigName: 'mcp.json',
fileExtension: '.mdc',
targetExtension: '.md',
toolMappings: COMMON_TOOL_MAPPINGS.STANDARD, // VS Code uses standard tool names
customFileMap: {
'cursor_rules.mdc': 'vscode_rules.md' // Rename cursor_rules to vscode_rules
},
customReplacements: [
// Core VS Code directory structure changes
{ from: /\.cursor\/rules/g, to: '.github/instructions' },

View File

@@ -6,12 +6,5 @@ export const windsurfProfile = createProfile({
name: 'windsurf',
displayName: 'Windsurf',
url: 'windsurf.com',
docsUrl: 'docs.windsurf.com',
profileDir: '.windsurf',
rulesDir: '.windsurf/rules',
mcpConfig: true,
mcpConfigName: 'mcp.json',
fileExtension: '.mdc',
targetExtension: '.md',
toolMappings: COMMON_TOOL_MAPPINGS.STANDARD // Windsurf uses standard tool names
docsUrl: 'docs.windsurf.com'
});