UNPKG

superuser-task-runner

Version:

A powerful task runner designed to manage complex development workflows across both monorepo and single-project configurations. Built as a more flexible alternative to Turborepo with enhanced customization capabilities, this tool focuses on unique deploym

428 lines (370 loc) 14.4 kB
const readline = require('readline'); const { spawn } = require('child_process'); const fs = require('fs'); const path = require('path'); const { ensureMethJsonExists } = require('./helpers.js'); const colors = require('./colors.js'); // Initialize tasks from str.json const tasksFilePath = ensureMethJsonExists(); let tasks; try { const fileContent = fs.readFileSync(tasksFilePath, 'utf-8'); tasks = JSON.parse(fileContent).tasks; } catch (err) { console.error('Failed to load str.json:', err.message); process.exit(1); } // Configure stdin to handle arrow keys readline.emitKeypressEvents(process.stdin); if (process.stdin.isTTY) { process.stdin.setRawMode(true); } // Organize tasks into categories function organizeTasksByCategory() { const categories = {}; for (const taskName in tasks) { const parts = taskName.split(':'); const category = parts.length > 1 ? parts[0] : 'general'; if (!categories[category]) { categories[category] = []; } categories[category].push(taskName); } return categories; } // Clear the console function clearConsole() { process.stdout.write('\x1Bc'); } // Display header function displayHeader() { console.log(`${colors.cyan}${colors.bright}╔════════════════════════════════════════╗${colors.reset}`); console.log(`${colors.cyan}${colors.bright}║ Superuser Task Runner ║${colors.reset}`); console.log(`${colors.cyan}${colors.bright}╚════════════════════════════════════════╝${colors.reset}`); console.log(); } // Display instructions function displayInstructions() { console.log(`${colors.dim}Navigation: Use arrow keys ↑↓ to select, → to expand category, ← to go back${colors.reset}`); console.log(`${colors.dim}Actions: Enter to execute task, 'r' to refresh, 'q' to quit${colors.reset}`); console.log(`${colors.dim}Options: 'n' to skip cache, 'i' for task info${colors.reset}`); console.log(`${colors.dim}All commands are loaded dynamically from str.json${colors.reset}`); console.log(); } // Display task info function displayTaskInfo(taskName) { clearConsole(); displayHeader(); const taskConfig = tasks[taskName]; console.log(`${colors.bright}${colors.yellow}Task Information: ${taskName}${colors.reset}\n`); if (taskConfig) { console.log(`${colors.bright}Directory:${colors.reset} ${taskConfig.directory || 'N/A'}`); if (taskConfig.command) { console.log(`\n${colors.bright}Command:${colors.reset}`); if (Array.isArray(taskConfig.command)) { taskConfig.command.forEach((cmd, i) => { console.log(` ${i + 1}. ${cmd}`); }); } else { console.log(` ${taskConfig.command}`); } } if (taskConfig.description) { console.log(`\n${colors.bright}Description:${colors.reset} ${taskConfig.description}`); } if (taskConfig.tasks) { console.log(`\n${colors.bright}Subtasks:${colors.reset}`); taskConfig.tasks.forEach((subtask, i) => { console.log(` ${i + 1}. ${subtask}`); }); } } else { console.log(`${colors.red}Task not found in configuration.${colors.reset}`); } console.log(`\n${colors.dim}Press any key to go back...${colors.reset}`); // Wait for keypress return new Promise((resolve) => { process.stdin.once('keypress', () => { resolve(); }); }); } // Get frequently used tasks dynamically from available tasks function getFrequentTasks() { const commonTaskNames = ['build', 'dev', 'i', 'start', 'test', 'lint', 'clean']; const availableFrequentTasks = []; // Check which common tasks are available in the current str.json commonTaskNames.forEach(taskName => { if (tasks[taskName]) { availableFrequentTasks.push(taskName); } }); return availableFrequentTasks; } // Display categories and tasks function displayMenu(categories, selectedCategory, selectedTask, expandedCategory = null) { clearConsole(); displayHeader(); displayInstructions(); // Display frequently used tasks at the top (dynamically determined) const frequentTasks = getFrequentTasks(); if (frequentTasks.length > 0) { console.log(`${colors.bright}${colors.yellow}Frequently Used Tasks:${colors.reset}`); frequentTasks.forEach((task, index) => { const isSelected = selectedCategory === 'frequent' && selectedTask === index; const taskCommand = tasks[task].command || 'No command defined'; const displayCommand = taskCommand.length > 50 ? taskCommand.substring(0, 47) + '...' : taskCommand; console.log(` ${isSelected ? colors.bg.cyan + colors.bright : ''}${task}${colors.reset} ${colors.dim}(${displayCommand})${colors.reset}`); }); console.log(); } // Display all categories Object.keys(categories).forEach((category, catIndex) => { const isSelectedCategory = selectedCategory === category; const isExpanded = expandedCategory === category; console.log(`${isSelectedCategory && !isExpanded ? colors.bg.blue + colors.bright : colors.bright}${category}${isExpanded ? ' ▼' : ' ▶'}${colors.reset}`); if (isExpanded) { categories[category].forEach((task, taskIndex) => { const isSelectedTask = isSelectedCategory && selectedTask === taskIndex; const taskCommand = tasks[task].command || 'No command defined'; const displayCommand = taskCommand.length > 40 ? taskCommand.substring(0, 37) + '...' : taskCommand; console.log(` ${isSelectedTask ? colors.bg.cyan + colors.bright : ''}${task}${colors.reset} ${colors.dim}(${displayCommand})${colors.reset}`); }); } }); } async function runTask(taskName, skipCache = false) { clearConsole(); displayHeader(); console.log(`${colors.bright}${colors.green}Running task: ${taskName}${skipCache ? ' (skip cache)' : ''}${colors.reset}\n`); const methPath = path.resolve(__dirname, 'str.js'); const args = [methPath, taskName]; if (skipCache) { args.push('--no-cache'); } return new Promise((resolve, reject) => { const node = process.argv[0]; // Get the path to the node executable const child = spawn(node, args, { stdio: ['inherit', 'pipe', 'pipe'], // Change from 'inherit' to 'pipe' for stdout and stderr shell: true }); // Relay stdout in real-time child.stdout.on('data', (data) => { process.stdout.write(data.toString()); }); // Relay stderr in real-time child.stderr.on('data', (data) => { process.stderr.write(data.toString()); }); child.on('close', (code) => { if (code === 0) { console.log(`\n${colors.bright}${colors.green}Task completed successfully!${colors.reset}`); } else { console.error(`\n${colors.bright}${colors.red}Task failed with code ${code}${colors.reset}`); } console.log(`\n${colors.dim}Press any key to return to menu...${colors.reset}`); // Wait for a single keypress before returning to menu const waitForKeypress = () => { process.stdin.once('keypress', () => { resolve(); }); }; waitForKeypress(); }); child.on('error', (err) => { console.error(`\n${colors.bright}${colors.red}Failed to start task: ${err.message}${colors.reset}`); console.log(`\n${colors.dim}Press any key to return to menu...${colors.reset}`); process.stdin.once('keypress', () => { reject(err); }); }); }); } // Main interactive menu function async function interactiveMenu() { // Set up initial state let categories = organizeTasksByCategory(); let selectedCategory = Object.keys(categories)[0]; let selectedTask = 0; let expandedCategory = null; let skipCache = false; // Display initial menu displayMenu(categories, selectedCategory, selectedTask); // Process keypress events process.stdin.on('keypress', async (str, key) => { // Handle Ctrl+C or q to quit if ((key.ctrl && key.name === 'c') || key.name === 'q') { console.log('\nExiting...'); process.exit(0); } // Toggle skip cache option if (key.name === 'n') { skipCache = !skipCache; displayMenu(categories, selectedCategory, selectedTask, expandedCategory); console.log(`${colors.yellow}Skip cache: ${skipCache ? 'Enabled' : 'Disabled'}${colors.reset}`); return; } // Show task info if (key.name === 'i') { let taskName = null; if (selectedCategory === 'frequent') { const frequentTasks = getFrequentTasks(); if (selectedTask < frequentTasks.length) { taskName = frequentTasks[selectedTask]; } } else if (expandedCategory) { taskName = categories[selectedCategory][selectedTask]; } if (taskName) { await displayTaskInfo(taskName); displayMenu(categories, selectedCategory, selectedTask, expandedCategory); } return; } // Refresh menu if (key.name === 'r') { try { const fileContent = fs.readFileSync(tasksFilePath, 'utf-8'); tasks = JSON.parse(fileContent).tasks; categories = organizeTasksByCategory(); selectedCategory = Object.keys(categories)[0]; selectedTask = 0; expandedCategory = null; displayMenu(categories, selectedCategory, selectedTask); console.log(`${colors.green}Tasks refreshed from str.json${colors.reset}`); } catch (err) { console.error(`${colors.red}Failed to refresh tasks: ${err.message}${colors.reset}`); } return; } // Navigation switch (key.name) { case 'up': if (selectedCategory === 'frequent') { const frequentTasks = getFrequentTasks(); if (selectedTask > 0) { selectedTask--; } else { // Move to last category const categoryKeys = Object.keys(categories); selectedCategory = categoryKeys[categoryKeys.length - 1]; expandedCategory = null; selectedTask = 0; } } else if (expandedCategory) { if (selectedTask > 0) { selectedTask--; } else { // Move to category above or frequent tasks const categoryKeys = Object.keys(categories); const currentIndex = categoryKeys.indexOf(selectedCategory); if (currentIndex > 0) { selectedCategory = categoryKeys[currentIndex - 1]; expandedCategory = null; selectedTask = 0; } else { // Move to frequent tasks if available const frequentTasks = getFrequentTasks(); if (frequentTasks.length > 0) { selectedCategory = 'frequent'; selectedTask = frequentTasks.length - 1; expandedCategory = null; } } } } else { const categoryKeys = Object.keys(categories); const currentIndex = categoryKeys.indexOf(selectedCategory); if (currentIndex > 0) { selectedCategory = categoryKeys[currentIndex - 1]; } else { // Move to frequent tasks if available const frequentTasks = getFrequentTasks(); if (frequentTasks.length > 0) { selectedCategory = 'frequent'; selectedTask = 0; } } } break; case 'down': if (selectedCategory === 'frequent') { const frequentTasks = getFrequentTasks(); if (selectedTask < frequentTasks.length - 1) { selectedTask++; } else { // Move to first category const categoryKeys = Object.keys(categories); if (categoryKeys.length > 0) { selectedCategory = categoryKeys[0]; expandedCategory = null; selectedTask = 0; } } } else if (expandedCategory) { if (selectedTask < categories[selectedCategory].length - 1) { selectedTask++; } else { // Move to category below const categoryKeys = Object.keys(categories); const currentIndex = categoryKeys.indexOf(selectedCategory); if (currentIndex < categoryKeys.length - 1) { selectedCategory = categoryKeys[currentIndex + 1]; expandedCategory = null; selectedTask = 0; } } } else { const categoryKeys = Object.keys(categories); const currentIndex = categoryKeys.indexOf(selectedCategory); if (currentIndex < categoryKeys.length - 1) { selectedCategory = categoryKeys[currentIndex + 1]; } } break; case 'right': if (selectedCategory !== 'frequent') { expandedCategory = selectedCategory; selectedTask = 0; } break; case 'left': if (selectedCategory === 'frequent') { // Can't go left from frequent tasks return; } expandedCategory = null; break; case 'return': let taskToRun = null; if (selectedCategory === 'frequent') { const frequentTasks = getFrequentTasks(); if (selectedTask < frequentTasks.length) { taskToRun = frequentTasks[selectedTask]; } } else if (expandedCategory) { taskToRun = categories[selectedCategory][selectedTask]; } else { expandedCategory = selectedCategory; selectedTask = 0; } if (taskToRun) { try { await runTask(taskToRun, skipCache); displayMenu(categories, selectedCategory, selectedTask, expandedCategory); } catch (err) { console.error(`Error running task: ${err.message}`); displayMenu(categories, selectedCategory, selectedTask, expandedCategory); } } break; } displayMenu(categories, selectedCategory, selectedTask, expandedCategory); }); } // Start the interactive menu console.log('Starting interactive menu...'); interactiveMenu().catch(err => { console.error('Error in interactive menu:', err); process.exit(1); });