UNPKG

vibe-coder-mcp

Version:

Production-ready MCP server with complete agent integration, multi-transport support, and comprehensive development automation tools for AI-assisted workflows.

354 lines (353 loc) 14.4 kB
import { getStorageManager } from '../core/storage/storage-manager.js'; import logger from '../../../logger.js'; import { promises as fs } from 'fs'; import path from 'path'; import { getVibeTaskManagerOutputDir } from './config-loader.js'; const DEFAULT_CONFIG = { projectPrefix: 'PID', epicPrefix: 'E', taskPrefix: 'T', projectIdLength: 3, epicIdLength: 3, taskIdLength: 4, maxRetries: 100 }; export class IdGenerator { static instance; config; counterFilePath; counterLock = Promise.resolve(); constructor(config) { this.config = { ...DEFAULT_CONFIG, ...config }; this.counterFilePath = path.join(getVibeTaskManagerOutputDir(), 'id-counters.json'); } static getInstance(config) { if (!IdGenerator.instance) { IdGenerator.instance = new IdGenerator(config); } return IdGenerator.instance; } async generateProjectId(projectName) { try { logger.debug({ projectName }, 'Generating project ID'); const nameValidation = this.validateProjectName(projectName); if (!nameValidation.valid) { return { success: false, error: `Invalid project name: ${nameValidation.errors.join(', ')}` }; } const baseId = this.createProjectBaseId(projectName); const storageManager = await getStorageManager(); for (let counter = 1; counter <= this.config.maxRetries; counter++) { const projectId = `${baseId}-${counter.toString().padStart(this.config.projectIdLength, '0')}`; const exists = await storageManager.projectExists(projectId); if (!exists) { logger.debug({ projectId, attempts: counter }, 'Generated unique project ID'); return { success: true, id: projectId, attempts: counter }; } } return { success: false, error: `Failed to generate unique project ID after ${this.config.maxRetries} attempts`, attempts: this.config.maxRetries }; } catch (error) { logger.error({ err: error, projectName }, 'Failed to generate project ID'); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } async generateEpicId(projectId) { return new Promise((resolve) => { this.counterLock = this.counterLock.then(async () => { try { logger.debug({ projectId }, 'Generating epic ID with counter lock'); const storageManager = await getStorageManager(); const projectExists = await storageManager.projectExists(projectId); if (!projectExists) { resolve({ success: false, error: `Project ${projectId} not found` }); return; } const counters = await this.loadCounters(); const currentEpicCounter = counters.epics || 0; for (let counter = currentEpicCounter + 1; counter <= currentEpicCounter + this.config.maxRetries; counter++) { const epicId = `${this.config.epicPrefix}${counter.toString().padStart(this.config.epicIdLength, '0')}`; const exists = await storageManager.epicExists(epicId); if (!exists) { counters.epics = counter; await this.saveCounters(counters); logger.debug({ epicId, projectId, attempts: counter - currentEpicCounter }, 'Generated unique epic ID'); resolve({ success: true, id: epicId, attempts: counter - currentEpicCounter }); return; } } resolve({ success: false, error: `Failed to generate unique epic ID after ${this.config.maxRetries} attempts`, attempts: this.config.maxRetries }); } catch (error) { logger.error({ err: error, projectId }, 'Failed to generate epic ID'); resolve({ success: false, error: error instanceof Error ? error.message : String(error) }); } }); }); } async loadCounters() { try { const data = await fs.readFile(this.counterFilePath, 'utf-8'); return JSON.parse(data); } catch { return {}; } } async saveCounters(counters) { const tempPath = `${this.counterFilePath}.tmp`; await fs.writeFile(tempPath, JSON.stringify(counters, null, 2), 'utf-8'); await fs.rename(tempPath, this.counterFilePath); } async generateTaskId() { return new Promise((resolve) => { this.counterLock = this.counterLock.then(async () => { try { logger.debug('Generating globally unique task ID with counter lock'); const counters = await this.loadCounters(); const currentTaskCounter = counters.tasks || 0; const storageManager = await getStorageManager(); for (let counter = currentTaskCounter + 1; counter <= currentTaskCounter + this.config.maxRetries; counter++) { const taskId = `${this.config.taskPrefix}${counter.toString().padStart(this.config.taskIdLength, '0')}`; const exists = await storageManager.taskExists(taskId); if (!exists) { counters.tasks = counter; await this.saveCounters(counters); logger.debug({ taskId, attempts: counter - currentTaskCounter }, 'Generated globally unique task ID'); resolve({ success: true, id: taskId, attempts: counter - currentTaskCounter }); return; } } resolve({ success: false, error: `Failed to generate unique task ID after ${this.config.maxRetries} attempts`, attempts: this.config.maxRetries }); } catch (error) { logger.error({ err: error }, 'Failed to generate task ID'); resolve({ success: false, error: error instanceof Error ? error.message : String(error) }); } }); }); } async generateDependencyId(fromTaskId, toTaskId) { try { logger.debug({ fromTaskId, toTaskId }, 'Generating dependency ID'); if (!this.isValidTaskId(fromTaskId)) { return { success: false, error: `Invalid from task ID format: ${fromTaskId}` }; } if (!this.isValidTaskId(toTaskId)) { return { success: false, error: `Invalid to task ID format: ${toTaskId}` }; } const baseId = `DEP-${fromTaskId}-${toTaskId}`; const storageManager = await getStorageManager(); for (let counter = 1; counter <= this.config.maxRetries; counter++) { const dependencyId = `${baseId}-${counter.toString().padStart(3, '0')}`; const exists = await storageManager.dependencyExists(dependencyId); if (!exists) { logger.debug({ dependencyId, fromTaskId, toTaskId, attempts: counter }, 'Generated unique dependency ID'); return { success: true, id: dependencyId, attempts: counter }; } } return { success: false, error: `Failed to generate unique dependency ID after ${this.config.maxRetries} attempts`, attempts: this.config.maxRetries }; } catch (error) { logger.error({ err: error, fromTaskId, toTaskId }, 'Failed to generate dependency ID'); return { success: false, error: error instanceof Error ? error.message : String(error) }; } } validateId(id, type) { const errors = []; if (!id || typeof id !== 'string') { errors.push('ID must be a non-empty string'); return { valid: false, errors }; } switch (type) { case 'project': if (!this.isValidProjectId(id)) { errors.push('Invalid project ID format'); } break; case 'epic': if (!this.isValidEpicId(id)) { errors.push('Invalid epic ID format'); } break; case 'task': if (!this.isValidTaskId(id)) { errors.push('Invalid task ID format'); } break; case 'dependency': if (!this.isValidDependencyId(id)) { errors.push('Invalid dependency ID format'); } break; default: errors.push(`Unknown ID type: ${type}`); } return { valid: errors.length === 0, errors }; } parseId(id) { const projectMatch = id.match(/^(PID)-([A-Z0-9-]+)-(\d{3})$/); if (projectMatch) { return { type: 'project', components: { prefix: projectMatch[1], name: projectMatch[2], counter: projectMatch[3] } }; } const epicMatch = id.match(/^(E)(\d{3})$/); if (epicMatch) { return { type: 'epic', components: { prefix: epicMatch[1], counter: epicMatch[2] } }; } const taskMatch = id.match(/^(T)(\d{4})$/); if (taskMatch) { return { type: 'task', components: { prefix: taskMatch[1], counter: taskMatch[2] } }; } const depMatch = id.match(/^(DEP)-(T\d{4})-(T\d{4})-(\d{3})$/); if (depMatch) { return { type: 'dependency', components: { prefix: depMatch[1], fromTask: depMatch[2], toTask: depMatch[3], counter: depMatch[4] } }; } return null; } createProjectBaseId(projectName) { return `${this.config.projectPrefix}-${projectName .toUpperCase() .replace(/[^A-Z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .substring(0, 20)}`; } suggestShorterName(projectName) { const commonWords = ['a', 'an', 'the', 'for', 'with', 'using', 'that', 'this', 'based', 'web', 'app', 'application', 'system', 'platform']; const words = projectName .toLowerCase() .split(/\s+/) .filter(word => word.length > 2 && !commonWords.includes(word)) .map(word => word.charAt(0).toUpperCase() + word.slice(1)); const suggested = words.slice(0, Math.min(4, words.length)).join(' '); if (suggested.length > 35) { const abbreviated = words.slice(0, 2).join(' '); return abbreviated.length <= 35 ? abbreviated : words[0]; } return suggested || projectName.substring(0, 30).trim(); } validateProjectName(projectName) { const errors = []; if (!projectName || typeof projectName !== 'string') { errors.push('Project name must be a non-empty string'); } else { if (projectName.length < 2) { errors.push('Project name must be at least 2 characters long'); } if (projectName.length > 50) { errors.push(`Project name is too long (${projectName.length} characters). ` + `Please use 50 characters or less for optimal file system compatibility. ` + `Suggestion: Use a shorter, descriptive name like "${this.suggestShorterName(projectName)}" ` + `instead of "${projectName}".`); } if (!/^[a-zA-Z0-9\s\-_]+$/.test(projectName)) { errors.push('Project name can only contain letters, numbers, spaces, hyphens, and underscores'); } } return { valid: errors.length === 0, errors }; } isValidProjectId(id) { return /^PID-[A-Z0-9-]+-\d{3}$/.test(id); } isValidEpicId(id) { return new RegExp(`^${this.config.epicPrefix}\\d{${this.config.epicIdLength}}$`).test(id); } isValidTaskId(id) { return new RegExp(`^${this.config.taskPrefix}\\d{${this.config.taskIdLength}}$`).test(id); } isValidDependencyId(id) { return /^DEP-T\d{4}-T\d{4}-\d{3}$/.test(id); } } export function getIdGenerator(config) { return IdGenerator.getInstance(config); }