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
JavaScript
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);
}