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
JavaScript
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);
});