Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
a8e2d72
Version Packages
github-actions[bot] Jul 12, 2025
f662654
fix: prevent CLAUDE.md overwrite by using imports (#949)
ben-vargas Jul 14, 2025
7b4803a
fix: task master (tm) custom slash commands w/ proper syntax (#968)
Crunchyman-ralph Jul 14, 2025
36dc129
chore: create extension scaffolding (#989)
Crunchyman-ralph Jul 16, 2025
cc4fe20
feat(profiles): Add MCP configuration to Claude Code rules (#980)
joedanz Jul 16, 2025
ab2e946
fix: show command no longer requires complexity report to exist (#979)
ben-vargas Jul 16, 2025
fedfd6a
feat: complete Groq provider integration and add Kimi K2 model (#978)
ben-vargas Jul 16, 2025
c327273
docs: Auto-update and format models.md
github-actions[bot] Jul 16, 2025
6d05e86
feat: Add Amp rule profile with AGENT.md and MCP config (#973)
joedanz Jul 16, 2025
5b0eda0
feat: Add Zed editor rule profile with agent rules and MCP config (#974)
joedanz Jul 16, 2025
1c7badf
fix: Add missing API keys to .env.example and README.md (#972)
joedanz Jul 16, 2025
b87499b
feat: Add OpenCode rule profile with AGENTS.md and MCP config (#970)
joedanz Jul 16, 2025
256d7cf
chore: add coderabbit configuration (#992)
Crunchyman-ralph Jul 16, 2025
b78de8d
docs: Update MCP server name for consistency and use 'Add to Cursor' …
joedanz Jul 17, 2025
4639eee
fix(ai-validation): comprehensive fixes for AI response validation is…
Crunchyman-ralph Jul 17, 2025
75a36ea
feat: add kiro profile (#1001)
Crunchyman-ralph Jul 17, 2025
6d0654c
refactor: remove unused resource and resource template initialization…
Crunchyman-ralph Jul 17, 2025
4f360ee
move folder
joedanz Jul 18, 2025
843cb81
validate approach
joedanz Jul 18, 2025
50d5f37
verify new patterns
joedanz Jul 18, 2025
5c04137
profile migration
joedanz Jul 18, 2025
0ffd691
legacy cleanup
joedanz Jul 18, 2025
3347287
fix formatting
joedanz Jul 18, 2025
e7cc081
fix issues
joedanz Jul 18, 2025
eb7d6c9
fix formatting
joedanz Jul 18, 2025
cae3a47
fix profiles and tests
joedanz Jul 18, 2025
3a2f5d8
update/fix tests
joedanz Jul 18, 2025
cc8d96e
fix tests
joedanz Jul 18, 2025
cd6bcf4
Merge branch 'next' of https://github.com/eyaltoledano/claude-task-ma…
joedanz Jul 23, 2025
be7e2ff
Merge branch 'next' of https://github.com/eyaltoledano/claude-task-ma…
joedanz Jul 23, 2025
c73bc2f
fix formatting
joedanz Jul 23, 2025
2f6cae2
fix formatting & test
joedanz Jul 25, 2025
9b12859
Merge branch 'next' of https://github.com/eyaltoledano/claude-task-ma…
joedanz Jul 25, 2025
2600ef4
first formatting
joedanz Jul 25, 2025
073339e
remove duplicate docUrls property in conversion configuration
joedanz Jul 27, 2025
4fefab9
Fix test: assertions misplaced outside any test case.
joedanz Jul 27, 2025
f377adb
verify numeric key edge case more explicitly
joedanz Jul 27, 2025
591c5ee
remove legacy conversions
joedanz Jul 27, 2025
e0d2856
Improve ProfileBuilder implementation validation
joedanz Jul 27, 2025
50202e8
Add missing targetExtension property to ProfileInit
joedanz Jul 27, 2025
0554129
remove unnecessary exports and update comments
joedanz Jul 27, 2025
00e2a0b
refactor
joedanz Jul 27, 2025
fceefdc
enhance summary method to provide more detailed feedback for each ope…
joedanz Jul 27, 2025
c46a81a
Use ProfileError for consistency
joedanz Jul 27, 2025
2fed8fa
remove duplicate docUrl
joedanz Jul 27, 2025
e97f182
remove duplicate docUrl
joedanz Jul 27, 2025
2dc6b63
remove duplicate docUrl
joedanz Jul 27, 2025
7a3ec17
improve jsdoc
joedanz Jul 27, 2025
78042cb
Fix malformed markdown link replacement
joedanz Jul 27, 2025
1f249ff
remove empty globalReplacements - handled later
joedanz Jul 27, 2025
c207e67
Fix markdown link replacement pattern
joedanz Jul 27, 2025
fcbb3f9
remove dupe docUrl
joedanz Jul 27, 2025
4ce7eb5
remove dupe docUrl
joedanz Jul 27, 2025
b1133c5
remove config file if empty
joedanz Jul 27, 2025
be6f8b9
Fix markdown link transformation
joedanz Jul 27, 2025
598fb46
Implement schema integration logic
joedanz Jul 27, 2025
82e68fd
fix formatting
joedanz Jul 27, 2025
808f803
update test
joedanz Jul 27, 2025
4c1c830
fix formatting
joedanz Jul 27, 2025
005442f
fix schema cleanup
joedanz Jul 27, 2025
6dbe3e0
update mock setup to match actual test usage
joedanz Jul 27, 2025
9e79c73
don't check for .taskmaster folder
joedanz Aug 5, 2025
b225cc8
reorganize
joedanz Aug 5, 2025
6f5001a
Merge branch 'next' of https://github.com/eyaltoledano/claude-task-ma…
joedanz Aug 5, 2025
a6a14ef
update amp profile and tests
joedanz Aug 5, 2025
72af52f
Enhance directory creation safety and add granular error handling
joedanz Aug 5, 2025
fd5236d
ensure directory exists for safety
joedanz Aug 5, 2025
64fba90
regex optimizations
joedanz Aug 5, 2025
b72fc56
fix formatting
joedanz Aug 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
368 changes: 368 additions & 0 deletions src/profile/Profile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,368 @@
/**
* @fileoverview Immutable Profile class representing a complete profile configuration
*/

import { ProfileOperationError } from './ProfileError.js';

/**
* Immutable Profile class representing a complete profile configuration
*
* @class Profile
*/
export default class Profile {
/**
* Creates a new Profile instance
* @param {import('./types.js').ProfileInit} config - Profile configuration
*/
constructor(config) {
// Required properties
this.profileName = config.profileName;
this.rulesDir = config.rulesDir;
this.profileDir = config.profileDir;

// Optional properties with defaults
this.displayName = config.displayName ?? config.profileName;
this.fileMap = Object.freeze(config.fileMap ?? {});
this.conversionConfig = Object.freeze(config.conversionConfig ?? {});
this.globalReplacements = Object.freeze(config.globalReplacements ?? []);
this.hooks = Object.freeze(config.hooks ?? {});

// MCP configuration
this._mcpConfigRaw = config.mcpConfig;
this.mcpConfig = this._deriveMcpConfigBoolean(config.mcpConfig);

// Core profile behavior
this.includeDefaultRules = config.includeDefaultRules ?? true;
this.supportsRulesSubdirectories =
config.supportsRulesSubdirectories ?? false;
this.targetExtension = config.targetExtension ?? '.md';

// Computed properties
this.mcpConfigName = this._computeMcpConfigName();
this.mcpConfigPath = this._computeMcpConfigPath();

// Freeze the instance for immutability
Object.freeze(this);
}

/**
* Handle operation errors consistently
* @private
* @param {string} operation - Operation type
* @param {Error} error - Error object
* @throws {ProfileOperationError}
*/
_handleOperationError(operation, error) {
throw new ProfileOperationError(
operation,
this.profileName,
error.message,
error
);
}

/**
* Install this profile to a project directory
* Template method that delegates to hooks
*
* @param {string} projectRoot - Target project directory
* @param {string} assetsDir - Source assets directory
* @returns {Promise<import('./types.js').ProfileOperationResult>}
*/
async install(projectRoot, assetsDir) {
try {
if (this.hooks.onAdd) {
await Promise.resolve(this.hooks.onAdd(projectRoot, assetsDir));
}
return {
success: true,
filesProcessed: Object.keys(this.fileMap).length
};
} catch (error) {
this._handleOperationError('install', error);
}
}

/**
* Remove this profile from a project directory
* Template method that delegates to hooks
*
* @param {string} projectRoot - Target project directory
* @returns {Promise<import('./types.js').ProfileOperationResult>}
*/
async remove(projectRoot) {
try {
if (this.hooks.onRemove) {
await Promise.resolve(this.hooks.onRemove(projectRoot));
}
return {
success: true
};
} catch (error) {
this._handleOperationError('remove', error);
}
}

/**
* Post-conversion processing for this profile
* Template method that delegates to hooks
*
* @param {string} projectRoot - Target project directory
* @param {string} assetsDir - Source assets directory
* @returns {Promise<import('./types.js').ProfileOperationResult>}
*/
async postConvert(projectRoot, assetsDir) {
try {
if (this.hooks.onPost) {
await Promise.resolve(this.hooks.onPost(projectRoot, assetsDir));
}
return {
success: true
};
} catch (error) {
this._handleOperationError('convert', error);
}
}

/**
* Generate a human-readable summary for an operation
*
* @param {import('./types.js').ProfileOperation} operation - Type of operation
* @param {import('./types.js').ProfileOperationResult} result - Operation result
* @returns {string} Formatted summary message
*/
summary(operation, result) {
if (!result.success) {
const errorDetails = result.error || 'Unknown error';
const context = result.context ? ` (${result.context})` : '';
return `${this.displayName}: [ERROR] Failed - ${errorDetails}${context}`;
}

// Operation-specific summary functions
const operationSummaries = {
add: () => {
if (!this.includeDefaultRules) {
// Integration guide profiles
const mcpStatus = this.hasMcpConfig()
? ' with MCP configuration'
: '';
const notice = result.notice ? ` (${result.notice})` : '';
return `${this.displayName}: [OK] Integration guide installed${mcpStatus}${notice}`;
}

// Standard rule profiles
const processed = result.filesProcessed || 0;
const skipped = result.filesSkipped || 0;
const total = processed + skipped;
const existing = result.filesExisting || 0;
const updated = result.filesUpdated || 0;

// Handle edge cases
if (processed === 0 && skipped === 0) {
return `${this.displayName}: [WARN] No files processed - profile may already be installed`;
}

if (processed === 0 && skipped > 0) {
return `${this.displayName}: [WARN] All ${skipped} files skipped - profile may already be installed`;
}

// Build detailed summary
let summary = `${this.displayName}: [OK] ${processed} file${processed !== 1 ? 's' : ''} processed`;

if (updated > 0) {
summary += ` (${updated} updated)`;
}

if (skipped > 0) {
summary += `, ${skipped} skipped`;
}

if (existing > 0) {
summary += ` (${existing} already existed)`;
}

// Add MCP configuration status
if (this.hasMcpConfig() && result.mcpConfigInstalled) {
summary += ', MCP config installed';
}

const notice = result.notice ? ` - ${result.notice}` : '';
return summary + notice;
},

remove: () => {
const removedCount = result.filesRemoved || 0;
const notFoundCount = result.filesNotFound || 0;
const total = removedCount + notFoundCount;

// Handle edge cases
if (removedCount === 0 && notFoundCount === 0) {
const profileType = this.includeDefaultRules
? 'rule profile'
: 'integration guide';
return `${this.displayName}: [WARN] No files found to remove - ${profileType} may not be installed`;
}

if (removedCount === 0 && notFoundCount > 0) {
const profileType = this.includeDefaultRules
? 'rule profile'
: 'integration guide';
return `${this.displayName}: [WARN] ${profileType} not found - may already be removed`;
}

// Build detailed summary
const profileType = this.includeDefaultRules
? 'rule profile'
: 'integration guide';
let summary = `${this.displayName}: [OK] ${profileType} removed`;

if (removedCount > 0) {
summary += ` (${removedCount} file${removedCount !== 1 ? 's' : ''} deleted)`;
}

if (notFoundCount > 0) {
summary += `, ${notFoundCount} file${notFoundCount !== 1 ? 's' : ''} not found`;
}

// Add MCP configuration removal status
if (this.hasMcpConfig() && result.mcpConfigRemoved) {
summary += ', MCP config removed';
}

const notice = result.notice ? ` - ${result.notice}` : '';
return summary + notice;
},

convert: () => {
const converted = result.filesConverted || 0;
const skipped = result.filesSkipped || 0;
const errors = result.conversionErrors || 0;

// Handle edge cases
if (converted === 0 && skipped === 0 && errors === 0) {
return `${this.displayName}: [WARN] No files found to convert`;
}

if (converted === 0 && errors > 0) {
return `${this.displayName}: [ERROR] Conversion failed for ${errors} file${errors !== 1 ? 's' : ''}`;
}

// Build detailed summary
let summary = `${this.displayName}: [OK] Rules converted successfully`;

if (converted > 0) {
summary += ` (${converted} file${converted !== 1 ? 's' : ''})`;
}

if (skipped > 0) {
summary += `, ${skipped} skipped`;
}

if (errors > 0) {
summary += `, ${errors} error${errors !== 1 ? 's' : ''}`;
}

const notice = result.notice ? ` - ${result.notice}` : '';
return summary + notice;
},

default: () => {
const status = result.success ? '[OK]' : '[ERROR]';
const notice = result.notice ? ` - ${result.notice}` : '';
const duration = result.duration ? ` (${result.duration}ms)` : '';
return `${this.displayName}: ${status} ${operation} completed${duration}${notice}`;
}
};

const summaryFn =
operationSummaries[operation] || operationSummaries.default;
return summaryFn();
}

/**
* Check if this profile has any lifecycle hooks defined
*
* @returns {boolean} True if profile has hooks
*/
hasHooks() {
return Object.keys(this.hooks).length > 0;
}

/**
* Check if this profile includes default rule files
*
* @returns {boolean} True if profile includes default rules
*/
hasDefaultRules() {
return this.includeDefaultRules;
}

/**
* Check if this profile has MCP configuration enabled
*
* @returns {boolean} True if MCP config is enabled
*/
hasMcpConfig() {
return Boolean(this.mcpConfig);
}

/**
* Get the number of files this profile will process
*
* @returns {number} Number of files in fileMap
*/
getFileCount() {
return Object.keys(this.fileMap).length;
}

// Private helper methods

/**
* Normalize file paths by joining segments and removing duplicate slashes
* @private
* @param {...string} segments - Path segments to join
* @returns {string} Normalized path
*/
_normalizePath(...segments) {
return segments
.filter(Boolean)
.join('/')
.replace(/\/+/g, '/')
.replace(/\/$/, '');
}

/**
* Compute MCP config name from configuration
* @private
*/
_computeMcpConfigName() {
if (!this.mcpConfig) return null;
const { configName = 'mcp.json' } =
typeof this._mcpConfigRaw === 'object' ? this._mcpConfigRaw : {};
return configName;
}

/**
* Compute MCP config path from configuration
* @private
*/
_computeMcpConfigPath() {
return this.mcpConfigName
? this._normalizePath(
this.profileDir === '.' ? '' : this.profileDir,
this.mcpConfigName
)
: null;
}

/**
* Derive a boolean value from the MCP config.
* Returns true if MCP is enabled (either true or a config object), false otherwise.
* @private
*/
_deriveMcpConfigBoolean(config) {
if (config === true) return true;
if (config === false || config == null) return false;
return typeof config === 'object';
}
}
Loading