mirror-magi-meta-agent
Version:
AI-powered development planning and execution system with Supabase integration
342 lines (285 loc) ⢠10.2 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
const { exec } = require('child_process');
const { promisify } = require('util');
const execAsync = promisify(exec);
const ProjectPlanner = require('../../core/project-planner');
const SupabasePreflightChecker = require('./supabase-preflight');
// Git helper functions
const git = {
async status() {
try {
const { stdout } = await execAsync('git status --porcelain');
return stdout.trim().split('\n').filter(line => line.length > 0);
} catch (error) {
throw new Error('Not a git repository');
}
},
async hasChanges() {
const changes = await this.status();
return changes.length > 0;
},
async stash(message) {
await execAsync(`git stash push -m "${message}"`);
},
async add(files = '.') {
await execAsync(`git add ${files}`);
},
async commit(message) {
await execAsync(`git commit -m "${message}"`);
},
async getCurrentBranch() {
const { stdout } = await execAsync('git branch --show-current');
return stdout.trim();
},
async diff(staged = false) {
const { stdout } = await execAsync(`git diff ${staged ? '--staged' : ''}`);
return stdout;
}
};
// Load user preferences
async function loadUserPreferences() {
try {
const prefsPath = path.join(__dirname, '../../state/user-preferences.json');
const prefs = await fs.readFile(prefsPath, 'utf8');
return JSON.parse(prefs);
} catch {
return {
autoCommit: false,
commitPrefix: 'feat',
includeTaskId: true,
interactive: true
};
}
}
// Save user preferences
async function saveUserPreferences(prefs) {
const prefsPath = path.join(__dirname, '../../state/user-preferences.json');
await fs.writeFile(prefsPath, JSON.stringify(prefs, null, 2));
}
// Generate commit message based on task
function generateCommitMessage(task, customMessage = null) {
if (customMessage) return customMessage;
const typeMap = {
'supabase_migration': 'feat(db)',
'supabase_rls': 'feat(security)',
'component_creation': 'feat(ui)',
'api_integration': 'feat(api)',
'configuration': 'chore(config)',
'testing': 'test',
'bug_fix': 'fix',
'documentation': 'docs',
'styling': 'style',
'refactoring': 'refactor',
'feature_development': 'feat'
};
const prefix = typeMap[task.type] || 'feat';
const description = task.description;
const taskRef = ` (${task.id})`;
return `${prefix}: ${description}${taskRef}`;
}
// Interactive commit prompt
async function interactiveCommit(task, changes) {
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const question = (prompt) => new Promise(resolve => readline.question(prompt, resolve));
console.log('\nš Changed files:');
changes.forEach(change => console.log(` ${change}`));
console.log('\nš¬ Suggested commit message:');
const suggested = generateCommitMessage(task);
console.log(` ${suggested}`);
const choice = await question('\nOptions:\n1. Use suggested message\n2. Custom message\n3. Skip commit\n> ');
let message = null;
if (choice === '1') {
message = suggested;
} else if (choice === '2') {
message = await question('Enter commit message: ');
}
readline.close();
return message;
}
// Handle the complete command with optional commit
async function handleComplete(taskId, options = {}) {
if (!taskId) {
console.log('Usage: node plan-project.js complete <task_id> [--commit] [--message "..."]');
process.exit(1);
}
try {
// Load configurations
const config = JSON.parse(await fs.readFile(path.join(__dirname, '../../config/project-config.json'), 'utf8'));
const projectState = JSON.parse(await fs.readFile(path.join(__dirname, '../../state/project-state.json'), 'utf8'));
const preferences = await loadUserPreferences();
const planner = new ProjectPlanner(config, projectState);
await planner.loadPlan();
// Find the task
const task = planner.findTask(taskId);
if (!task) {
console.error(`ā Task ${taskId} not found`);
process.exit(1);
}
// Check for git changes if commit is requested
const shouldCommit = options.commit || (preferences.autoCommit && !options.noCommit);
if (shouldCommit) {
const hasChanges = await git.hasChanges();
if (hasChanges) {
const changes = await git.status();
// Check for unrelated changes
const unrelatedChanges = [];
const taskChanges = [];
// In a real implementation, we'd analyze which files are related to the task
// For now, we'll assume all changes are task-related
taskChanges.push(...changes);
if (preferences.interactive && !options.message) {
// Interactive mode
const commitMessage = await interactiveCommit(task, taskChanges);
if (commitMessage) {
console.log('\nš Committing changes...');
await git.add();
await git.commit(commitMessage);
console.log('ā
Changes committed!');
const { stdout } = await execAsync('git log -1 --oneline');
console.log(` ${stdout.trim()}`);
}
} else {
// Automatic mode
const commitMessage = options.message || generateCommitMessage(task);
console.log('\nš Auto-committing changes...');
await git.add();
await git.commit(commitMessage);
console.log('ā
Changes committed!');
const { stdout } = await execAsync('git log -1 --oneline');
console.log(` ${stdout.trim()}`);
}
} else {
console.log('ā¹ļø No changes to commit');
}
}
// Mark task as completed
const progress = await planner.markTaskCompleted(taskId);
console.log(`\nā
Task ${taskId} marked as complete!`);
console.log(`š Progress: ${progress.completed}/${progress.total} tasks (${Math.round(progress.percentage)}%)`);
// Check if phase is complete
const phase = planner.getCurrentPhase();
const phaseProgress = planner.calculatePhaseProgress(phase.id);
if (phaseProgress.percentage === 100) {
console.log(`\nš Phase "${phase.name}" completed!`);
}
// Save preferences if they were updated
if (options.savePrefs) {
await saveUserPreferences(preferences);
}
} catch (error) {
console.error('ā Error:', error.message);
process.exit(1);
}
}
// Handle the config command
async function handleConfig(setting, value) {
const preferences = await loadUserPreferences();
if (!setting) {
console.log('Current preferences:');
console.log(JSON.stringify(preferences, null, 2));
return;
}
switch (setting) {
case 'autoCommit':
preferences.autoCommit = value === 'true';
break;
case 'commitPrefix':
preferences.commitPrefix = value;
break;
case 'includeTaskId':
preferences.includeTaskId = value === 'true';
break;
case 'interactive':
preferences.interactive = value === 'true';
break;
default:
console.log(`Unknown setting: ${setting}`);
console.log('Available settings: autoCommit, commitPrefix, includeTaskId, interactive');
return;
}
await saveUserPreferences(preferences);
console.log(`ā
Updated ${setting} to ${value}`);
}
// Main function remains largely the same
async function main() {
console.log('šÆ Mirror Magi Project Planner\n');
console.log('Create a specific, actionable development plan for your project goal.\n');
try {
// Load configurations
const config = JSON.parse(await fs.readFile(path.join(__dirname, '../../config/project-config.json'), 'utf8'));
const projectState = JSON.parse(await fs.readFile(path.join(__dirname, '../../state/project-state.json'), 'utf8'));
const planner = new ProjectPlanner(config, projectState);
// Check if there's an existing plan
const existingPlan = await planner.loadPlan();
if (existingPlan) {
console.log('š Found existing plan:', existingPlan.goal);
console.log('Would you like to:');
console.log('1. Continue with existing plan');
console.log('2. Create a new plan');
console.log('3. Show plan progress\n');
// For demo purposes, we'll continue with existing plan
const choice = process.argv[2] || '1';
if (choice === '3') {
await showPlanProgress(planner);
return;
} else if (choice === '2') {
await createNewPlan(planner);
return;
}
// Continue with existing plan
await continueExistingPlan(planner);
return;
}
// Create new plan
await createNewPlan(planner);
} catch (error) {
console.error('ā Error:', error.message);
if (error.message.includes('project-config.json')) {
console.log('š” Run "npm run setup" first to create configuration files');
}
}
}
// Parse command line arguments
const args = process.argv.slice(2);
const command = args[0];
// Parse options
const options = {};
for (let i = 1; i < args.length; i++) {
if (args[i].startsWith('--')) {
const key = args[i].substring(2);
if (args[i + 1] && !args[i + 1].startsWith('--')) {
options[key] = args[i + 1];
i++;
} else {
options[key] = true;
}
}
}
// Route to appropriate handler
if (command === 'complete') {
handleComplete(args[1], options);
} else if (command === 'config') {
handleConfig(args[1], args[2]);
} else if (command === 'next' || command === 'continue') {
main();
} else if (command === 'progress' || command === 'status') {
process.argv[2] = '3';
main();
} else {
main();
}
// Export the continue and create functions (rest of the file remains the same)
async function createNewPlan(planner) {
// ... same as original
}
async function continueExistingPlan(planner) {
// ... same as original
}
async function showPlanProgress(planner) {
// ... same as original
}