mirror-magi-meta-agent
Version:
AI-powered development planning and execution system with Supabase integration
460 lines (382 loc) ⢠15.5 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');
const git = require('./git-helpers');
const { buildInteractiveCommitMessage, generateCommitMessage, generateCommitBody } = require('./commit-utils');
const ClaudeResponseManager = require('./claude-response-manager');
// Initialize response manager
const responseManager = new ClaudeResponseManager();
// 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,
interactive: true,
includeTaskId: true,
showGitInstructions: 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));
}
// Handle the complete command with git integration
async function handleComplete(taskId, options = {}) {
if (!taskId) {
console.log('Usage: npm run plan:complete <task_id> [--commit] [--message "..."] [--no-commit]');
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 if we should commit
const shouldCommit = options.commit || (preferences.autoCommit && !options.noCommit);
if (shouldCommit) {
// Check if we're in a git repository
const isGitRepo = await git.isGitRepo();
if (!isGitRepo) {
console.log('ā ļø Not a git repository. Skipping commit.');
} else {
const hasChanges = await git.hasChanges();
if (hasChanges) {
const changes = await git.status();
// Get Claude's response for this task if available
const claudeResponse = await responseManager.getResponse(taskId);
let commitMessage;
if (preferences.interactive && !options.message) {
// Interactive mode
commitMessage = await buildInteractiveCommitMessage(task, claudeResponse, changes);
} else {
// Automatic mode
if (options.message) {
commitMessage = options.message;
} else if (claudeResponse && claudeResponse.summary) {
// Use Claude's summary
commitMessage = generateCommitMessage(task, {
claudeSummary: claudeResponse.summary,
includeTaskId: preferences.includeTaskId
});
const body = generateCommitBody(task, claudeResponse.summary, changes);
if (body) {
commitMessage = `${commitMessage}\n\n${body}`;
}
} else {
// Fall back to task description
commitMessage = generateCommitMessage(task, {
includeTaskId: preferences.includeTaskId
});
}
}
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 {
console.log('āļø Skipping commit');
}
} 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 currentPhase = planner.getCurrentPhase();
if (currentPhase) {
const phaseProgress = planner.calculatePhaseProgress(currentPhase.id);
if (phaseProgress.percentage === 100) {
console.log(`\nš Phase "${currentPhase.name}" completed!`);
}
}
// Show next task preview
const nextTask = planner.findNextTask();
if (nextTask) {
console.log(`\nšÆ Next task: ${nextTask.description}`);
console.log('Run: npm run plan:continue');
} else {
console.log('\nš All tasks completed! Great work!');
}
} 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('\nš Current Git Integration Settings:\n');
console.log(JSON.stringify(preferences, null, 2));
console.log('\nAvailable settings:');
console.log(' autoCommit - Automatically commit on task completion (true/false)');
console.log(' interactive - Show commit prompt (true/false)');
console.log(' includeTaskId - Include task ID in commit messages (true/false)');
console.log(' showGitInstructions - Show git instructions after task (true/false)');
console.log('\nUsage: npm run plan:config <setting> <value>');
return;
}
switch (setting) {
case 'autoCommit':
preferences.autoCommit = value === 'true';
break;
case 'interactive':
preferences.interactive = value === 'true';
break;
case 'includeTaskId':
preferences.includeTaskId = value === 'true';
break;
case 'showGitInstructions':
preferences.showGitInstructions = value === 'true';
break;
default:
console.log(`ā Unknown setting: ${setting}`);
return;
}
await saveUserPreferences(preferences);
console.log(`ā
Updated ${setting} to ${value}`);
}
// Save Claude's response after showing command
async function saveClaudeInstructions(taskId) {
console.log('\nš After Claude implements this task, you can save the response:');
console.log(`npm run plan:save-response ${taskId} "Claude's response here"`);
console.log('Or paste the response into a file and run:');
console.log(`npm run plan:save-response ${taskId} --file response.txt\n`);
}
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');
}
}
}
async function createNewPlan(planner) {
const goal = process.argv[2] || 'Create an investment dashboard page with portfolio overview and transaction history';
console.log('šÆ PROJECT GOAL:', goal);
console.log('\nš Creating master development plan...\n');
// For a real implementation, you'd want to gather more context
const context = {
requiredFeatures: ['charts', 'data_table', 'filtering'],
designSystem: 'Tailwind CSS',
dataSource: 'REST API',
userRequirements: [
'View portfolio performance over time',
'Filter transactions by date range',
'Export data to CSV'
]
};
const plan = await planner.createMasterPlan(goal, context);
console.log('ā
Master Plan Created!');
console.log(`š ${plan.phases.length} phases planned`);
console.log(`ā±ļø Estimated timeline: ${plan.estimatedTimeline} hours`);
console.log('\nš DEVELOPMENT PHASES:');
plan.phases.forEach((phase, index) => {
console.log(`\n${index + 1}. ${phase.name}`);
console.log(` ${phase.description}`);
if (phase.tasks) {
console.log(` š ${phase.tasks.length} tasks`);
}
});
console.log('\nš Ready to start development!');
console.log('Run: npm run plan:continue');
}
async function continueExistingPlan(planner) {
console.log('š Generating next specific command...\n');
const nextCommand = await planner.generateNextCommand();
if (nextCommand.status === 'complete') {
console.log('š All tasks completed!');
console.log(nextCommand.message);
return;
}
const { task, command, progress } = nextCommand;
// Check if this is a Supabase task
const isSupabaseTask = ['supabase_migration', 'supabase_function', 'supabase_rls'].includes(task.type);
if (isSupabaseTask) {
console.log('šļø SUPABASE TASK DETECTED');
console.log('Running pre-flight checks...\n');
const checker = new SupabasePreflightChecker();
const passed = await checker.runChecks();
if (!passed) {
console.log('\nā Pre-flight checks failed!');
console.log('Fix the errors above before proceeding with Supabase tasks.');
return;
}
console.log('\nā
Pre-flight checks passed!');
console.log('ā'.repeat(40));
}
console.log(`\nš Progress: ${progress.completed}/${progress.total} tasks (${Math.round(progress.percentage)}%)`);
console.log(`\nšÆ NEXT TASK: ${task.description}`);
console.log(`š Type: ${task.type}`);
console.log(`š Task ID: ${task.id}`);
if (task.specifics) {
console.log('\nš SPECIFIC DETAILS:');
Object.entries(task.specifics).forEach(([key, value]) => {
console.log(` ${key}: ${JSON.stringify(value)}`);
});
}
console.log('\nš GENERATED COMMAND:');
console.log('=' .repeat(80));
console.log(command.command);
console.log('=' .repeat(80));
console.log('\nā
VALIDATION STEPS:');
command.validationSteps.forEach((step, index) => {
console.log(`${index + 1}. ${step}`);
});
console.log('\nšÆ SUCCESS CRITERIA:');
command.successCriteria.forEach((criteria, index) => {
console.log(`${index + 1}. ${criteria}`);
});
// Git integration instructions
const preferences = await loadUserPreferences();
if (preferences.showGitInstructions || preferences.autoCommit) {
console.log('\nš GIT INTEGRATION:');
if (preferences.autoCommit) {
console.log('ā
Auto-commit is enabled. Changes will be committed automatically when you complete this task.');
} else {
console.log('š” To commit changes when done:');
console.log(` npm run plan:complete ${task.id} --commit`);
}
console.log('\nš To save Claude\'s response for better commit messages:');
console.log(` npm run plan:save-response ${task.id} "Claude's final message"`);
}
console.log('\nš” After completing this task, run:');
console.log(`npm run plan:complete ${task.id}`);
}
async function showPlanProgress(planner) {
const plan = planner.masterPlan;
console.log(`š PROJECT: ${plan.goal}`);
console.log(`š
Created: ${new Date(plan.created).toLocaleDateString()}`);
const progress = planner.calculateProgress();
console.log(`\nšÆ PROGRESS: ${progress.completed}/${progress.total} tasks (${Math.round(progress.percentage)}%)`);
console.log('\nš PHASES:');
plan.phases.forEach((phase, index) => {
const phaseProgress = planner.calculatePhaseProgress(phase.id);
const status = phaseProgress.percentage === 100 ? 'ā
' :
phaseProgress.percentage > 0 ? 'š' : 'ā³';
console.log(`${status} ${index + 1}. ${phase.name} (${Math.round(phaseProgress.percentage)}%)`);
});
const nextTask = planner.findNextTask();
if (nextTask) {
console.log(`\nšÆ NEXT: ${nextTask.description}`);
console.log('Run: npm run plan:continue');
}
// Show git preferences
const preferences = await loadUserPreferences();
console.log('\nāļø Git Integration:', preferences.autoCommit ? 'Enabled' : 'Disabled');
}
// Handle saving Claude's response
async function handleSaveResponse(taskId, response, options = {}) {
if (!taskId) {
console.log('Usage: npm run plan:save-response <task_id> "response" | --file <file>');
process.exit(1);
}
try {
let responseText = response;
if (options.file) {
responseText = await fs.readFile(options.file, 'utf8');
}
if (!responseText) {
console.error('ā No response provided');
process.exit(1);
}
await responseManager.saveResponse(taskId, responseText);
const savedResponse = await responseManager.getResponse(taskId);
console.log(`ā
Saved Claude's response for task ${taskId}`);
if (savedResponse.summary) {
console.log(`š Extracted summary: "${savedResponse.summary}"`);
console.log('This will be used for commit messages!');
}
} catch (error) {
console.error('ā Error saving response:', error.message);
process.exit(1);
}
}
// Parse command line arguments
const args = process.argv.slice(2);
const command = args[0];
// Parse options
const options = {};
let mainArg = args[1];
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(mainArg, options);
} else if (command === 'config') {
handleConfig(args[1], args[2]);
} else if (command === 'save-response') {
handleSaveResponse(mainArg, args[2], options);
} else if (command === 'next' || command === 'continue') {
main();
} else if (command === 'progress' || command === 'status') {
process.argv[2] = '3';
main();
} else {
main();
}