mak3r-hub
Version:
Universal Claude Code force multiplier for website development
374 lines (311 loc) ⢠11.3 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const cron = require('node-cron');
const TemplateManager = require('./template-manager');
/**
* Automated Template Update Scheduler
* Handles scheduled template updates, monitoring, and notifications
*/
class TemplateScheduler {
constructor() {
this.templateManager = new TemplateManager();
this.schedulerConfig = path.join(__dirname, '..', 'config', 'template-scheduler.json');
this.activeJobs = new Map();
this.initialized = false;
}
/**
* Initialize scheduler with configuration
*/
async initialize() {
if (this.initialized) return;
await fs.ensureDir(path.dirname(this.schedulerConfig));
// Create default configuration if it doesn't exist
if (!await fs.pathExists(this.schedulerConfig)) {
const defaultConfig = {
schedules: {
weekly: {
cron: '0 9 * * 1', // Monday at 9 AM
repositories: ['ConvertStar', 'MAK3R-HUB', 'SharpCET', 'Trinity'],
auto_commit: false,
enabled: true,
last_run: null,
next_run: null
},
monthly: {
cron: '0 10 1 * *', // 1st of month at 10 AM
repositories: ['ConvertStar', 'MAK3R-HUB', 'SharpCET', 'Trinity'],
auto_commit: false,
enabled: false,
last_run: null,
next_run: null
}
},
notifications: {
enabled: true,
slack_webhook: null,
email: null
},
update_settings: {
preserve_manual_edits: true,
backup_before_update: true,
rollback_on_failure: true,
max_age_days: 30
}
};
await fs.writeJson(this.schedulerConfig, defaultConfig, { spaces: 2 });
}
this.initialized = true;
await this.loadSchedules();
}
/**
* Load and activate schedules from configuration
*/
async loadSchedules() {
const config = await fs.readJson(this.schedulerConfig);
for (const [name, schedule] of Object.entries(config.schedules)) {
if (schedule.enabled && cron.validate(schedule.cron)) {
await this.scheduleJob(name, schedule);
}
}
console.log(chalk.green(`ā
Template scheduler initialized with ${this.activeJobs.size} active jobs`));
}
/**
* Schedule a template update job
*/
async scheduleJob(name, schedule) {
// Stop existing job if running
if (this.activeJobs.has(name)) {
this.activeJobs.get(name).stop();
}
const job = cron.schedule(schedule.cron, async () => {
console.log(chalk.blue(`š Running scheduled template update: ${name}`));
await this.executeScheduledUpdate(name, schedule);
}, {
scheduled: true,
timezone: 'America/New_York' // Adjust as needed
});
this.activeJobs.set(name, job);
// Update next run time
await this.updateScheduleMetadata(name, {
next_run: this.getNextRunTime(schedule.cron)
});
console.log(chalk.cyan(`š
Scheduled ${name} template updates: ${schedule.cron}`));
}
/**
* Execute a scheduled template update
*/
async executeScheduledUpdate(scheduleName, schedule) {
const startTime = new Date();
try {
console.log(chalk.blue(`\nš Template Update Execution: ${scheduleName}`));
console.log('ā'.repeat(60));
// Update last run time
await this.updateScheduleMetadata(scheduleName, {
last_run: startTime.toISOString(),
status: 'running'
});
// Get current template status
const scanResults = await this.templateManager.scanRepositories();
// Filter by scheduled repositories
const targetRepos = schedule.repositories || [];
const staleTemplates = [];
for (const [repoName, repoData] of Object.entries(scanResults)) {
if (targetRepos.includes(repoName) || targetRepos.includes('all')) {
const staleFiles = repoData.files.filter(f => f.isStale);
if (staleFiles.length > 0) {
staleTemplates.push({ repo: repoName, files: staleFiles });
}
}
}
if (staleTemplates.length === 0) {
console.log(chalk.green('ā
All templates are up to date'));
await this.sendNotification(`Template Update Complete: ${scheduleName}`, {
status: 'success',
message: 'All templates are up to date',
repositories: targetRepos,
execution_time: Date.now() - startTime.getTime()
});
return;
}
// Backup current templates if enabled
if (schedule.backup_before_update !== false) {
await this.backupTemplates(staleTemplates);
}
// Execute updates (placeholder for future implementation)
const updateResults = await this.performTemplateUpdates(staleTemplates, schedule);
// Commit changes if auto-commit is enabled
if (schedule.auto_commit && updateResults.success) {
await this.commitTemplateUpdates(updateResults, scheduleName);
}
// Update schedule metadata
await this.updateScheduleMetadata(scheduleName, {
status: 'completed',
last_execution_time: Date.now() - startTime.getTime(),
templates_updated: updateResults.updated_count,
next_run: this.getNextRunTime(schedule.cron)
});
// Send success notification
await this.sendNotification(`Template Update Complete: ${scheduleName}`, {
status: 'success',
updated_templates: updateResults.updated_count,
repositories: targetRepos,
execution_time: Date.now() - startTime.getTime(),
auto_committed: schedule.auto_commit && updateResults.success
});
console.log(chalk.green(`ā
Scheduled update completed: ${updateResults.updated_count} templates updated`));
} catch (error) {
console.error(chalk.red(`ā Scheduled update failed: ${error.message}`));
// Update schedule metadata with error
await this.updateScheduleMetadata(scheduleName, {
status: 'failed',
last_error: error.message,
last_execution_time: Date.now() - startTime.getTime()
});
// Send error notification
await this.sendNotification(`Template Update Failed: ${scheduleName}`, {
status: 'error',
error: error.message,
execution_time: Date.now() - startTime.getTime()
});
}
}
/**
* Backup templates before updating
*/
async backupTemplates(staleTemplates) {
const backupDir = path.join(__dirname, '..', 'backups', 'templates', new Date().toISOString().split('T')[0]);
await fs.ensureDir(backupDir);
for (const { repo, files } of staleTemplates) {
const repoBackupDir = path.join(backupDir, repo);
await fs.ensureDir(repoBackupDir);
for (const file of files) {
const sourceFile = file.path;
const backupFile = path.join(repoBackupDir, file.file);
if (await fs.pathExists(sourceFile)) {
await fs.copy(sourceFile, backupFile);
}
}
}
console.log(chalk.cyan(`š¾ Templates backed up to: ${backupDir}`));
}
/**
* Perform template updates (placeholder)
*/
async performTemplateUpdates(staleTemplates, schedule) {
// This is a placeholder for the actual update implementation
// In a real implementation, this would:
// 1. Fetch latest templates from Trinity
// 2. Preserve manual edits
// 3. Apply updates safely
// 4. Validate changes
console.log(chalk.yellow('š§ Template update implementation coming soon...'));
let updatedCount = 0;
for (const { files } of staleTemplates) {
updatedCount += files.length;
}
return {
success: true,
updated_count: updatedCount,
failed_count: 0,
details: staleTemplates
};
}
/**
* Commit template updates to git
*/
async commitTemplateUpdates(updateResults, scheduleName) {
const { execSync } = require('child_process');
for (const { repo } of updateResults.details) {
const repoPath = this.templateManager.repositories.find(r => r.includes(repo));
if (repoPath && await fs.pathExists(repoPath)) {
try {
// Stage template files
execSync('git add *.md', { cwd: repoPath, stdio: 'inherit' });
// Commit with scheduler info
const commitMessage = `š¤ Automated template updates via ${scheduleName}
Updated ${updateResults.updated_count} templates
Scheduler: MAK3R-HUB Template Scheduler
Execution: ${new Date().toISOString()}
š Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>`;
execSync(`git commit -m "${commitMessage}"`, {
cwd: repoPath,
stdio: 'inherit'
});
console.log(chalk.green(`ā
Changes committed to ${repo}`));
} catch (error) {
console.error(chalk.yellow(`ā ļø Failed to commit ${repo}: ${error.message}`));
}
}
}
}
/**
* Send notifications
*/
async sendNotification(title, details) {
const config = await fs.readJson(this.schedulerConfig);
if (!config.notifications.enabled) return;
const message = {
title,
timestamp: new Date().toISOString(),
...details
};
// Log to console (always)
console.log(chalk.blue(`š¬ Notification: ${title}`));
console.log(chalk.gray(JSON.stringify(details, null, 2)));
// TODO: Implement actual notification channels (Slack, email, etc.)
if (config.notifications.slack_webhook) {
// Send to Slack
}
if (config.notifications.email) {
// Send email
}
}
/**
* Update schedule metadata
*/
async updateScheduleMetadata(scheduleName, updates) {
const config = await fs.readJson(this.schedulerConfig);
if (config.schedules[scheduleName]) {
Object.assign(config.schedules[scheduleName], updates);
await fs.writeJson(this.schedulerConfig, config, { spaces: 2 });
}
}
/**
* Get next run time for cron expression
*/
getNextRunTime(cronExpression) {
// This is a simplified implementation
// In production, use a proper cron parser like 'cron-parser'
return new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(); // Next week
}
/**
* Get scheduler status
*/
async getStatus() {
if (!this.initialized) {
await this.initialize();
}
const config = await fs.readJson(this.schedulerConfig);
return {
initialized: this.initialized,
active_jobs: this.activeJobs.size,
schedules: config.schedules,
notifications: config.notifications.enabled,
update_settings: config.update_settings
};
}
/**
* Stop all scheduled jobs
*/
stopAll() {
for (const [name, job] of this.activeJobs) {
job.stop();
console.log(chalk.yellow(`ā¹ļø Stopped scheduled job: ${name}`));
}
this.activeJobs.clear();
console.log(chalk.green('ā
All template update jobs stopped'));
}
}
module.exports = TemplateScheduler;