UNPKG

shipdeck

Version:

Ship MVPs in 48 hours. Fix bugs in 30 seconds. The command deck for developers who ship.

348 lines (300 loc) 9.62 kB
/** * Template Processor for Shipdeck Ultimate * Loads, parses, and processes YAML templates for agent intelligence */ const fs = require('fs'); const path = require('path'); const yaml = require('js-yaml'); class TemplateProcessor { constructor() { this.templateCache = new Map(); this.baseTemplatePath = path.join(__dirname, '..', 'templates'); this.rulesPath = path.join(__dirname, '..', 'rules'); this.prepTemplatePath = path.join(__dirname, '..', 'prep-templates'); } /** * Load a template from file system * @param {string} templatePath - Path relative to templates directory * @returns {Object} Parsed template object */ async loadTemplate(templatePath) { const cacheKey = templatePath; // Check cache first if (this.templateCache.has(cacheKey)) { return this.templateCache.get(cacheKey); } const fullPath = path.join(this.baseTemplatePath, templatePath); try { const content = fs.readFileSync(fullPath, 'utf8'); let template; if (fullPath.endsWith('.yaml') || fullPath.endsWith('.yml')) { template = yaml.load(content); } else if (fullPath.endsWith('.md')) { template = this.parseMarkdownTemplate(content); } else if (fullPath.endsWith('.json')) { template = JSON.parse(content); } else { throw new Error(`Unsupported template format: ${path.extname(fullPath)}`); } // Enhance template with metadata template.metadata = { path: templatePath, loadedAt: new Date().toISOString(), type: this.detectTemplateType(template) }; // Cache the template this.templateCache.set(cacheKey, template); return template; } catch (error) { console.error(`Failed to load template ${templatePath}:`, error.message); throw error; } } /** * Parse markdown template format (legacy ai-task-templates) * @param {string} content - Markdown content * @returns {Object} Parsed template structure */ parseMarkdownTemplate(content) { const template = { sections: {}, variables: {}, steps: [] }; // Parse sections (## headers) const sectionRegex = /^##\s+(.+)$/gm; const sections = content.split(sectionRegex); for (let i = 1; i < sections.length; i += 2) { const sectionName = sections[i].trim(); const sectionContent = sections[i + 1].trim(); template.sections[sectionName.toLowerCase().replace(/\s+/g, '_')] = sectionContent; } // Parse variables ({{variable}}) const variableRegex = /\{\{(\w+)\}\}/g; let match; while ((match = variableRegex.exec(content)) !== null) { template.variables[match[1]] = null; } // Parse steps (numbered lists) const stepRegex = /^\d+\.\s+(.+)$/gm; while ((match = stepRegex.exec(content)) !== null) { template.steps.push({ action: match[1], completed: false }); } return template; } /** * Detect template type based on structure * @param {Object} template - Template object * @returns {string} Template type */ detectTemplateType(template) { if (template.lifecycle) return 'lifecycle'; if (template.agent) return 'agent'; if (template.workflow) return 'workflow'; if (template.rules) return 'rules'; if (template.prep) return 'prep'; return 'generic'; } /** * Fill template parameters with context * @param {Object} template - Template object * @param {Object} context - Context with values * @returns {Object} Filled template */ fillParameters(template, context) { const filled = JSON.parse(JSON.stringify(template)); // Deep clone // Fill variables if (filled.variables) { for (const [key, value] of Object.entries(context)) { if (filled.variables[key] !== undefined) { filled.variables[key] = value; } } } // Replace placeholders in all string values const replacePlaceholders = (obj) => { for (const [key, value] of Object.entries(obj)) { if (typeof value === 'string') { obj[key] = value.replace(/\{\{(\w+)\}\}/g, (match, varName) => { return context[varName] || match; }); } else if (typeof value === 'object' && value !== null) { replacePlaceholders(value); } } }; replacePlaceholders(filled); return filled; } /** * Create workflow instance from template * @param {Object} template - Template object * @param {Object} params - Parameters for workflow * @returns {Object} Workflow instance */ createWorkflow(template, params) { const workflow = { id: this.generateWorkflowId(), templateId: template.metadata?.path || 'unknown', status: 'initialized', createdAt: new Date().toISOString(), params: params, currentStep: 0, steps: [], results: {}, context: {} }; // Build workflow steps based on template type if (template.steps && Array.isArray(template.steps)) { workflow.steps = template.steps.map(step => ({ ...step, status: 'pending', startedAt: null, completedAt: null, result: null })); } else if (template.phases) { // Lifecycle template with phases for (const phase of template.phases) { workflow.steps.push({ name: phase.name, description: phase.description, agents: phase.agents || [], status: 'pending', parallel: phase.parallel || false }); } } return workflow; } /** * Load all rules from cursor rules directory * @returns {Array} Array of rule objects */ async loadRules() { const rules = []; const rulesDir = this.rulesPath; if (!fs.existsSync(rulesDir)) { console.warn('Rules directory not found:', rulesDir); return rules; } const categories = fs.readdirSync(rulesDir) .filter(file => fs.statSync(path.join(rulesDir, file)).isDirectory()); for (const category of categories) { const categoryPath = path.join(rulesDir, category); const ruleFiles = fs.readdirSync(categoryPath) .filter(file => file.endsWith('.mdc') || file.endsWith('.md')); for (const ruleFile of ruleFiles) { const rulePath = path.join(categoryPath, ruleFile); const content = fs.readFileSync(rulePath, 'utf8'); rules.push({ category: category, name: path.basename(ruleFile, path.extname(ruleFile)), path: rulePath, content: content, active: true }); } } return rules; } /** * Load prep templates for project initialization * @returns {Array} Array of prep template objects */ async loadPrepTemplates() { const prepTemplates = []; const prepDir = this.prepTemplatePath; if (!fs.existsSync(prepDir)) { console.warn('Prep templates directory not found:', prepDir); return prepTemplates; } const files = fs.readdirSync(prepDir) .filter(file => file.match(/^\d+_/)) .sort(); for (const file of files) { const filePath = path.join(prepDir, file); const content = fs.readFileSync(filePath, 'utf8'); const order = parseInt(file.match(/^(\d+)_/)[1]); const name = file.replace(/^\d+_/, '').replace(/\.\w+$/, ''); prepTemplates.push({ order: order, name: name, file: file, path: filePath, content: content }); } return prepTemplates; } /** * Generate unique workflow ID * @returns {string} Workflow ID */ generateWorkflowId() { return `wf-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`; } /** * Validate template structure * @param {Object} template - Template to validate * @returns {Object} Validation result */ validateTemplate(template) { const errors = []; const warnings = []; // Check required fields based on type const type = template.metadata?.type || this.detectTemplateType(template); switch (type) { case 'lifecycle': if (!template.phases || !Array.isArray(template.phases)) { errors.push('Lifecycle template must have phases array'); } break; case 'agent': if (!template.agent || !template.agent.name) { errors.push('Agent template must have agent.name'); } break; case 'workflow': if (!template.steps || !Array.isArray(template.steps)) { errors.push('Workflow template must have steps array'); } break; } // Check for undefined variables if (template.variables) { for (const [key, value] of Object.entries(template.variables)) { if (value === null || value === undefined) { warnings.push(`Variable ${key} is not initialized`); } } } return { valid: errors.length === 0, errors: errors, warnings: warnings }; } /** * Clear template cache */ clearCache() { this.templateCache.clear(); console.log('Template cache cleared'); } /** * Get template statistics * @returns {Object} Statistics about loaded templates */ getStats() { return { cachedTemplates: this.templateCache.size, cacheKeys: Array.from(this.templateCache.keys()), memoryUsage: process.memoryUsage() }; } } module.exports = TemplateProcessor;