← back to Animate Museum Posts

ralphy.js

354 lines

#!/usr/bin/env node
/**
 * Ralphy Boy - Free Local Task Runner
 * A simple task management system for autonomous development
 * No paid services, no amp CLI - just pure local goodness!
 *
 * Usage:
 *   node ralph.js next          - Show next available task
 *   node ralph.js start T001    - Mark task as in_progress
 *   node ralph.js done T001     - Mark task as completed
 *   node ralph.js status        - Show overall progress
 *   node ralph.js list          - List all tasks
 *   node ralph.js phase 1       - Show tasks for phase 1
 *   node ralph.js reset         - Reset all tasks to pending
 */

import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const TASKS_FILE = path.join(__dirname, 'scripts/ralph/tasks.json');
const PROGRESS_FILE = path.join(__dirname, 'scripts/ralph/progress.txt');

// ANSI colors
const colors = {
  reset: '\x1b[0m',
  bright: '\x1b[1m',
  red: '\x1b[31m',
  green: '\x1b[32m',
  yellow: '\x1b[33m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  cyan: '\x1b[36m',
  gray: '\x1b[90m'
};

function loadTasks() {
  const data = fs.readFileSync(TASKS_FILE, 'utf-8');
  return JSON.parse(data);
}

function saveTasks(data) {
  fs.writeFileSync(TASKS_FILE, JSON.stringify(data, null, 2));
}

function logProgress(message) {
  const timestamp = new Date().toISOString();
  const entry = `[${timestamp}] ${message}\n`;
  fs.appendFileSync(PROGRESS_FILE, entry);
}

function getTaskById(tasks, id) {
  return tasks.find(t => t.id === id);
}

function isTaskReady(task, tasks) {
  if (task.status !== 'pending') return false;

  for (const depId of task.dependsOn) {
    const dep = getTaskById(tasks, depId);
    if (!dep || dep.status !== 'completed') {
      return false;
    }
  }
  return true;
}

function getNextTask(tasks) {
  // Find first ready task (pending with all deps completed)
  return tasks.find(t => isTaskReady(t, tasks));
}

function getReadyTasks(tasks) {
  return tasks.filter(t => isTaskReady(t, tasks));
}

function printTask(task, verbose = true) {
  const statusColors = {
    pending: colors.gray,
    in_progress: colors.yellow,
    completed: colors.green
  };

  const statusIcon = {
    pending: '○',
    in_progress: '◐',
    completed: '●'
  };

  console.log(`\n${colors.bright}${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
  console.log(`${colors.bright}Task: ${task.id}${colors.reset} - Phase ${task.phase}`);
  console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}`);
  console.log(`${colors.bright}Title:${colors.reset} ${task.title}`);
  console.log(`${colors.bright}Status:${colors.reset} ${statusColors[task.status]}${statusIcon[task.status]} ${task.status}${colors.reset}`);

  if (task.dependsOn.length > 0) {
    console.log(`${colors.bright}Depends on:${colors.reset} ${task.dependsOn.join(', ')}`);
  }

  if (verbose) {
    console.log(`\n${colors.bright}Description:${colors.reset}`);
    console.log(`  ${task.description}`);

    console.log(`\n${colors.bright}Files to modify:${colors.reset}`);
    task.files.forEach(f => console.log(`  - ${f}`));

    console.log(`\n${colors.bright}Acceptance Criteria:${colors.reset}`);
    task.acceptance.forEach(a => console.log(`  [ ] ${a}`));

    if (task.commands) {
      console.log(`\n${colors.bright}Commands:${colors.reset}`);
      if (task.commands.run) console.log(`  ${colors.green}Run:${colors.reset} ${task.commands.run}`);
      if (task.commands.verify) console.log(`  ${colors.blue}Verify:${colors.reset} ${task.commands.verify}`);
    }
  }

  console.log(`${colors.cyan}═══════════════════════════════════════════════════════════${colors.reset}\n`);
}

function printStatus(data) {
  const tasks = data.tasks;
  const completed = tasks.filter(t => t.status === 'completed').length;
  const inProgress = tasks.filter(t => t.status === 'in_progress').length;
  const pending = tasks.filter(t => t.status === 'pending').length;
  const ready = getReadyTasks(tasks).length;

  const pct = Math.round((completed / tasks.length) * 100);
  const bar = '█'.repeat(Math.floor(pct / 5)) + '░'.repeat(20 - Math.floor(pct / 5));

  console.log(`\n${colors.bright}${colors.cyan}╔═══════════════════════════════════════════════════════════╗${colors.reset}`);
  console.log(`${colors.cyan}║${colors.reset}  ${colors.bright}RALPHY BOY - Museum Art Bot Tasks${colors.reset}                       ${colors.cyan}║${colors.reset}`);
  console.log(`${colors.cyan}╠═══════════════════════════════════════════════════════════╣${colors.reset}`);
  console.log(`${colors.cyan}║${colors.reset}  Progress: [${colors.green}${bar}${colors.reset}] ${pct}%                    ${colors.cyan}║${colors.reset}`);
  console.log(`${colors.cyan}╠═══════════════════════════════════════════════════════════╣${colors.reset}`);
  console.log(`${colors.cyan}║${colors.reset}  ${colors.green}● Completed:${colors.reset}    ${String(completed).padStart(2)} / ${tasks.length}                                  ${colors.cyan}║${colors.reset}`);
  console.log(`${colors.cyan}║${colors.reset}  ${colors.yellow}◐ In Progress:${colors.reset}  ${String(inProgress).padStart(2)}                                       ${colors.cyan}║${colors.reset}`);
  console.log(`${colors.cyan}║${colors.reset}  ${colors.blue}○ Ready:${colors.reset}        ${String(ready).padStart(2)}  (dependencies met)                  ${colors.cyan}║${colors.reset}`);
  console.log(`${colors.cyan}║${colors.reset}  ${colors.gray}○ Blocked:${colors.reset}      ${String(pending - ready).padStart(2)}  (waiting on dependencies)          ${colors.cyan}║${colors.reset}`);
  console.log(`${colors.cyan}╚═══════════════════════════════════════════════════════════╝${colors.reset}`);

  // Show by phase
  console.log(`\n${colors.bright}By Phase:${colors.reset}`);
  for (let phase = 1; phase <= 5; phase++) {
    const phaseTasks = tasks.filter(t => t.phase === phase);
    const phaseCompleted = phaseTasks.filter(t => t.status === 'completed').length;
    const phaseNames = ['', 'Stability', 'Museums', 'Animation', 'Content', 'Monitoring'];
    console.log(`  Phase ${phase} (${phaseNames[phase]}): ${phaseCompleted}/${phaseTasks.length}`);
  }

  // Show next task
  const next = getNextTask(tasks);
  if (next) {
    console.log(`\n${colors.bright}Next task:${colors.reset} ${next.id} - ${next.title}`);
    console.log(`${colors.gray}Run: node ralph.js next${colors.reset}`);
  } else if (completed === tasks.length) {
    console.log(`\n${colors.green}${colors.bright}All tasks completed!${colors.reset}`);
  } else {
    console.log(`\n${colors.yellow}No tasks ready - complete in-progress tasks first${colors.reset}`);
  }

  console.log('');
}

function printList(data, phaseFilter = null) {
  const tasks = phaseFilter
    ? data.tasks.filter(t => t.phase === phaseFilter)
    : data.tasks;

  const statusIcon = {
    pending: `${colors.gray}○${colors.reset}`,
    in_progress: `${colors.yellow}◐${colors.reset}`,
    completed: `${colors.green}●${colors.reset}`
  };

  console.log(`\n${colors.bright}Task List${phaseFilter ? ` (Phase ${phaseFilter})` : ''}:${colors.reset}\n`);

  let currentPhase = 0;
  for (const task of tasks) {
    if (task.phase !== currentPhase) {
      currentPhase = task.phase;
      const phaseNames = ['', 'Stability', 'Museums', 'Animation', 'Content', 'Monitoring'];
      console.log(`\n${colors.bright}Phase ${currentPhase}: ${phaseNames[currentPhase]}${colors.reset}`);
    }

    const ready = isTaskReady(task, data.tasks);
    const readyMarker = ready && task.status === 'pending' ? `${colors.blue}[READY]${colors.reset} ` : '';

    console.log(`  ${statusIcon[task.status]} ${task.id}: ${readyMarker}${task.title}`);
  }
  console.log('');
}

// Main CLI
const args = process.argv.slice(2);
const command = args[0] || 'status';

try {
  const data = loadTasks();

  switch (command) {
    case 'next': {
      const next = getNextTask(data.tasks);
      if (next) {
        printTask(next);
        console.log(`${colors.bright}To start this task:${colors.reset} node ralph.js start ${next.id}`);
      } else {
        const inProgress = data.tasks.filter(t => t.status === 'in_progress');
        if (inProgress.length > 0) {
          console.log(`\n${colors.yellow}Tasks in progress:${colors.reset}`);
          inProgress.forEach(t => console.log(`  ${t.id}: ${t.title}`));
          console.log(`\n${colors.gray}Complete these first: node ralph.js done <ID>${colors.reset}`);
        } else {
          console.log(`\n${colors.green}All tasks completed!${colors.reset}`);
        }
      }
      break;
    }

    case 'start': {
      const taskId = args[1];
      if (!taskId) {
        console.log(`${colors.red}Error: Specify task ID${colors.reset}`);
        console.log('Usage: node ralph.js start T001');
        process.exit(1);
      }

      const task = getTaskById(data.tasks, taskId);
      if (!task) {
        console.log(`${colors.red}Error: Task ${taskId} not found${colors.reset}`);
        process.exit(1);
      }

      if (!isTaskReady(task, data.tasks) && task.status === 'pending') {
        console.log(`${colors.red}Error: Task ${taskId} has unmet dependencies${colors.reset}`);
        console.log(`Depends on: ${task.dependsOn.join(', ')}`);
        process.exit(1);
      }

      task.status = 'in_progress';
      task.startedAt = new Date().toISOString();
      saveTasks(data);
      logProgress(`STARTED: ${taskId} - ${task.title}`);

      console.log(`${colors.green}Started task ${taskId}${colors.reset}`);
      printTask(task);
      break;
    }

    case 'done': {
      const taskId = args[1];
      if (!taskId) {
        console.log(`${colors.red}Error: Specify task ID${colors.reset}`);
        console.log('Usage: node ralph.js done T001');
        process.exit(1);
      }

      const task = getTaskById(data.tasks, taskId);
      if (!task) {
        console.log(`${colors.red}Error: Task ${taskId} not found${colors.reset}`);
        process.exit(1);
      }

      task.status = 'completed';
      task.completedAt = new Date().toISOString();
      saveTasks(data);
      logProgress(`COMPLETED: ${taskId} - ${task.title}`);

      console.log(`${colors.green}Completed task ${taskId}!${colors.reset}`);

      // Show next task
      const next = getNextTask(data.tasks);
      if (next) {
        console.log(`\n${colors.bright}Next available task:${colors.reset} ${next.id} - ${next.title}`);
      }
      break;
    }

    case 'status':
      printStatus(data);
      break;

    case 'list':
      printList(data);
      break;

    case 'phase': {
      const phase = parseInt(args[1]);
      if (isNaN(phase) || phase < 1 || phase > 5) {
        console.log(`${colors.red}Error: Specify phase 1-5${colors.reset}`);
        process.exit(1);
      }
      printList(data, phase);
      break;
    }

    case 'show': {
      const taskId = args[1];
      if (!taskId) {
        console.log(`${colors.red}Error: Specify task ID${colors.reset}`);
        process.exit(1);
      }
      const task = getTaskById(data.tasks, taskId);
      if (!task) {
        console.log(`${colors.red}Error: Task ${taskId} not found${colors.reset}`);
        process.exit(1);
      }
      printTask(task);
      break;
    }

    case 'reset': {
      data.tasks.forEach(t => {
        t.status = 'pending';
        delete t.startedAt;
        delete t.completedAt;
      });
      saveTasks(data);
      logProgress('RESET: All tasks reset to pending');
      console.log(`${colors.yellow}All tasks reset to pending${colors.reset}`);
      break;
    }

    case 'help':
    default:
      console.log(`
${colors.bright}Ralphy Boy - Free Local Task Runner${colors.reset}

${colors.cyan}Commands:${colors.reset}
  node ralph.js next          Show next available task
  node ralph.js start T001    Mark task as in_progress
  node ralph.js done T001     Mark task as completed
  node ralph.js show T001     Show task details
  node ralph.js status        Show overall progress
  node ralph.js list          List all tasks
  node ralph.js phase 1       Show tasks for phase 1-5
  node ralph.js reset         Reset all tasks to pending
  node ralph.js help          Show this help

${colors.cyan}Workflow:${colors.reset}
  1. Run 'node ralph.js next' to see next task
  2. Run 'node ralph.js start T001' to begin
  3. Implement the task
  4. Run 'node ralph.js done T001' when complete
  5. Repeat!
`);
  }
} catch (error) {
  console.error(`${colors.red}Error: ${error.message}${colors.reset}`);
  process.exit(1);
}