mirror-magi-meta-agent
Version:
AI-powered development planning and execution system with Supabase integration
472 lines (386 loc) • 14.3 kB
JavaScript
const fs = require('fs').promises;
const path = require('path');
const readline = require('readline');
/**
* Plan Editor - Edit and modify existing development plans
*/
class PlanEditor {
constructor() {
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
this.plan = null;
this.originalPlan = null;
}
async start() {
console.log('✏️ Plan Editor');
console.log('═'.repeat(30));
console.log('Edit and modify your development plans\n');
try {
await this.loadPlan();
await this.editPlan();
} catch (error) {
console.error('❌ Error:', error.message);
} finally {
this.rl.close();
}
}
async loadPlan() {
try {
const planPath = path.join('state', 'master-plan.json');
const planData = await fs.readFile(planPath, 'utf8');
this.plan = JSON.parse(planData);
this.originalPlan = JSON.parse(planData); // Keep backup
console.log('📋 Loaded plan:', this.plan.goal);
console.log('Status:', this.plan.status);
console.log('Created:', new Date(this.plan.created).toLocaleString());
} catch (error) {
throw new Error('No plan found. Create a plan first with "node interactive-planner.js"');
}
}
async editPlan() {
while (true) {
this.displayPlanOverview();
const choice = await this.askQuestion('\nWhat would you like to edit?\n1. Goal/description\n2. Phases\n3. Tasks\n4. Context/settings\n5. Save changes\n6. Discard changes\n\nChoose (1-6): ');
switch (choice.trim()) {
case '1':
await this.editGoal();
break;
case '2':
await this.editPhases();
break;
case '3':
await this.editTasks();
break;
case '4':
await this.editContext();
break;
case '5':
await this.saveChanges();
return;
case '6':
await this.discardChanges();
return;
default:
console.log('Invalid choice. Please try again.');
}
}
}
displayPlanOverview() {
console.log('\n📋 CURRENT PLAN OVERVIEW');
console.log('─'.repeat(40));
console.log(`🎯 Goal: ${this.plan.goal}`);
console.log(`📊 Phases: ${this.plan.phases.length}`);
console.log(`📈 Status: ${this.plan.status}`);
const totalTasks = this.plan.phases.reduce((sum, phase) =>
sum + (phase.tasks ? phase.tasks.length : 0), 0
);
console.log(`📝 Total tasks: ${totalTasks}`);
}
async editGoal() {
console.log('\n✏️ EDIT GOAL');
console.log('─'.repeat(20));
console.log('Current goal:', this.plan.goal);
const newGoal = await this.askQuestion('\nEnter new goal (or press Enter to keep current): ');
if (newGoal.trim()) {
this.plan.goal = newGoal.trim();
this.plan.lastModified = new Date().toISOString();
console.log('✅ Goal updated');
}
}
async editPhases() {
console.log('\n📋 EDIT PHASES');
console.log('─'.repeat(20));
while (true) {
console.log('\nCurrent phases:');
this.plan.phases.forEach((phase, index) => {
const taskCount = phase.tasks ? phase.tasks.length : 0;
console.log(`${index + 1}. ${phase.name} (${taskCount} tasks)`);
console.log(` ${phase.description}`);
});
const action = await this.askQuestion('\n1. Edit phase\n2. Add phase\n3. Remove phase\n4. Reorder phases\n5. Back to main menu\n\nChoose (1-5): ');
switch (action.trim()) {
case '1':
await this.editSinglePhase();
break;
case '2':
await this.addPhase();
break;
case '3':
await this.removePhase();
break;
case '4':
await this.reorderPhases();
break;
case '5':
return;
default:
console.log('Invalid choice.');
}
}
}
async editSinglePhase() {
const phaseNum = await this.askQuestion('Which phase number to edit? ');
const index = parseInt(phaseNum) - 1;
if (index < 0 || index >= this.plan.phases.length) {
console.log('Invalid phase number.');
return;
}
const phase = this.plan.phases[index];
console.log(`\nEditing: ${phase.name}`);
const newName = await this.askQuestion(`New name (current: ${phase.name}): `);
if (newName.trim()) {
phase.name = newName.trim();
}
const newDesc = await this.askQuestion(`New description (current: ${phase.description}): `);
if (newDesc.trim()) {
phase.description = newDesc.trim();
}
console.log('✅ Phase updated');
}
async addPhase() {
const name = await this.askQuestion('Phase name: ');
const description = await this.askQuestion('Phase description: ');
const position = await this.askQuestion(`Insert at position (1-${this.plan.phases.length + 1}, default: end): `);
const newPhase = {
id: `phase_${Date.now()}`,
name: name.trim(),
description: description.trim(),
tasks: []
};
const pos = parseInt(position) - 1;
if (pos >= 0 && pos <= this.plan.phases.length) {
this.plan.phases.splice(pos, 0, newPhase);
} else {
this.plan.phases.push(newPhase);
}
console.log('✅ Phase added');
}
async removePhase() {
const phaseNum = await this.askQuestion('Which phase number to remove? ');
const index = parseInt(phaseNum) - 1;
if (index < 0 || index >= this.plan.phases.length) {
console.log('Invalid phase number.');
return;
}
const phase = this.plan.phases[index];
const confirm = await this.askQuestion(`Really remove "${phase.name}"? (y/n): `);
if (confirm.toLowerCase().startsWith('y')) {
this.plan.phases.splice(index, 1);
console.log('✅ Phase removed');
}
}
async editTasks() {
console.log('\n📝 EDIT TASKS');
console.log('─'.repeat(20));
// Show phases with task counts
console.log('\nPhases:');
this.plan.phases.forEach((phase, index) => {
const taskCount = phase.tasks ? phase.tasks.length : 0;
console.log(`${index + 1}. ${phase.name} (${taskCount} tasks)`);
});
const phaseNum = await this.askQuestion('\nWhich phase to edit tasks for? ');
const phaseIndex = parseInt(phaseNum) - 1;
if (phaseIndex < 0 || phaseIndex >= this.plan.phases.length) {
console.log('Invalid phase number.');
return;
}
await this.editPhaseTasks(this.plan.phases[phaseIndex]);
}
async editPhaseTasks(phase) {
console.log(`\n🔧 EDITING TASKS FOR: ${phase.name}`);
if (!phase.tasks) {
phase.tasks = [];
}
while (true) {
console.log(`\nCurrent tasks (${phase.tasks.length}):`);
if (phase.tasks.length === 0) {
console.log(' No tasks yet');
} else {
phase.tasks.forEach((task, idx) => {
console.log(`${idx + 1}. ${task.description} (${task.type || 'general'})`);
});
}
const action = await this.askQuestion('\n1. Add task\n2. Edit task\n3. Remove task\n4. Reorder tasks\n5. Back\n\nChoose (1-5): ');
switch (action.trim()) {
case '1':
await this.addTaskToPhase(phase);
break;
case '2':
await this.editTask(phase);
break;
case '3':
await this.removeTask(phase);
break;
case '4':
await this.reorderTasks(phase);
break;
case '5':
return;
default:
console.log('Invalid choice.');
}
}
}
async addTaskToPhase(phase) {
const description = await this.askQuestion('Task description: ');
const type = await this.askQuestion('Task type (component_creation, api_integration, configuration, etc.): ');
const task = {
id: `task_${Date.now()}_${Math.random().toString(36).substr(2, 5)}`,
description: description.trim(),
type: type.trim() || 'feature_development',
dependencies: [],
specifics: {}
};
phase.tasks.push(task);
console.log('✅ Task added successfully');
}
async editTask(phase) {
if (phase.tasks.length === 0) {
console.log('No tasks to edit.');
return;
}
const taskNum = await this.askQuestion('Which task number to edit? ');
const index = parseInt(taskNum) - 1;
if (index < 0 || index >= phase.tasks.length) {
console.log('Invalid task number.');
return;
}
const task = phase.tasks[index];
console.log(`\nEditing: ${task.description}`);
const newDesc = await this.askQuestion(`New description (current: ${task.description}): `);
if (newDesc.trim()) {
task.description = newDesc.trim();
}
const newType = await this.askQuestion(`New type (current: ${task.type}): `);
if (newType.trim()) {
task.type = newType.trim();
}
console.log('✅ Task updated');
}
async removeTask(phase) {
if (phase.tasks.length === 0) {
console.log('No tasks to remove.');
return;
}
const taskNum = await this.askQuestion('Which task number to remove? ');
const index = parseInt(taskNum) - 1;
if (index < 0 || index >= phase.tasks.length) {
console.log('Invalid task number.');
return;
}
const task = phase.tasks[index];
const confirm = await this.askQuestion(`Really remove "${task.description}"? (y/n): `);
if (confirm.toLowerCase().startsWith('y')) {
phase.tasks.splice(index, 1);
console.log('✅ Task removed');
}
}
async editContext() {
console.log('\n⚙️ EDIT CONTEXT & SETTINGS');
console.log('─'.repeat(30));
if (this.plan.context) {
console.log('Current context:');
Object.entries(this.plan.context).forEach(([key, value]) => {
console.log(` ${key}: ${JSON.stringify(value)}`);
});
}
const editChoice = await this.askQuestion('\n1. Edit required features\n2. Edit design system\n3. Edit data source\n4. Edit user requirements\n5. Edit priority\n6. Back\n\nChoose (1-6): ');
if (!this.plan.context) {
this.plan.context = {};
}
switch (editChoice.trim()) {
case '1':
const features = await this.askQuestion('Required features (comma-separated): ');
this.plan.context.requiredFeatures = features.split(',').map(f => f.trim()).filter(f => f);
break;
case '2':
const design = await this.askQuestion('Design system: ');
if (design.trim()) this.plan.context.designSystem = design.trim();
break;
case '3':
const dataSource = await this.askQuestion('Data source: ');
if (dataSource.trim()) this.plan.context.dataSource = dataSource.trim();
break;
case '4':
const userReqs = await this.askQuestion('User requirements (comma-separated): ');
this.plan.context.userRequirements = userReqs.split(',').map(r => r.trim()).filter(r => r);
break;
case '5':
const priority = await this.askQuestion('Priority (1=Low, 2=Medium, 3=High): ');
if (priority.trim()) this.plan.context.priority = priority.trim();
break;
case '6':
return;
}
console.log('✅ Context updated');
}
async saveChanges() {
console.log('\n💾 SAVING CHANGES');
console.log('─'.repeat(20));
// Update metadata
this.plan.lastModified = new Date().toISOString();
this.plan.status = 'modified';
// Recalculate estimate
this.plan.estimatedHours = this.calculateEstimate();
// Show changes summary
this.showChangesSummary();
const confirm = await this.askQuestion('\n✅ Save these changes? (y/n): ');
if (confirm.toLowerCase().startsWith('y')) {
const planPath = path.join('state', 'master-plan.json');
await fs.writeFile(planPath, JSON.stringify(this.plan, null, 2));
console.log('\n🎉 Plan saved successfully!');
console.log('📁 Location: state/master-plan.json');
console.log('\n💡 Next steps:');
console.log('• Continue plan: node plan-project.js continue');
console.log('• View progress: node plan-project.js progress');
} else {
console.log('Changes not saved.');
}
}
async discardChanges() {
const confirm = await this.askQuestion('\n⚠️ Discard all changes? (y/n): ');
if (confirm.toLowerCase().startsWith('y')) {
this.plan = this.originalPlan;
console.log('✅ Changes discarded');
}
}
showChangesSummary() {
console.log('\n📋 CHANGES SUMMARY:');
if (this.plan.goal !== this.originalPlan.goal) {
console.log(`🎯 Goal changed: ${this.originalPlan.goal} → ${this.plan.goal}`);
}
if (this.plan.phases.length !== this.originalPlan.phases.length) {
console.log(`📊 Phase count changed: ${this.originalPlan.phases.length} → ${this.plan.phases.length}`);
}
const originalTasks = this.originalPlan.phases.reduce((sum, p) => sum + (p.tasks?.length || 0), 0);
const currentTasks = this.plan.phases.reduce((sum, p) => sum + (p.tasks?.length || 0), 0);
if (currentTasks !== originalTasks) {
console.log(`📝 Task count changed: ${originalTasks} → ${currentTasks}`);
}
console.log(`📊 Current structure: ${this.plan.phases.length} phases, ${currentTasks} tasks`);
}
calculateEstimate() {
// Simple task count for reference (no time estimation)
let totalTasks = 0;
this.plan.phases.forEach(phase => {
if (phase.tasks) {
totalTasks += phase.tasks.length;
}
});
return totalTasks; // Return task count instead of time
}
askQuestion(question) {
return new Promise((resolve) => {
this.rl.question(question, resolve);
});
}
}
// Run the plan editor
if (require.main === module) {
const editor = new PlanEditor();
editor.start().catch(console.error);
}
module.exports = PlanEditor;