UNPKG

flagtrack

Version:

CTF Progress Tracker and Task Management Tool

454 lines (386 loc) 15.2 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const simpleGit = require('simple-git'); const inquirer = require('inquirer'); const { getCurrentBranch, getGitUserName } = require('../utils/gitHelpers'); const { loadConfig } = require('../utils/configManager'); const { validateLocation } = require('../utils/helpers'); async function solve() { console.log(chalk.blue('🏁 Checking task completion status')); try { // Load config const config = await loadConfig(); if (!config) { console.log(chalk.yellow('⚠️ No configuration found. Run `flagtrack setup` first.')); process.exit(1); } // Find current task from the branch name const currentBranch = await getCurrentBranch(); if (!currentBranch) { console.log(chalk.yellow('⚠️ Not in a git repository or unable to determine current branch.')); process.exit(1); } // Don't allow solve on main/master branch if (currentBranch === 'main' || currentBranch === 'master') { console.log(chalk.red('❌ Cannot run solve on main/master branch.')); console.log(chalk.yellow('Please checkout a task branch first.')); process.exit(1); } // Try to find task from branch name let taskInfo = await findTaskFromBranch(currentBranch, config); if (!taskInfo) { // If task not found from branch, allow manual selection console.log(chalk.yellow('⚠️ Could not determine task from branch name.')); console.log(chalk.blue('Please select the task manually:')); const taskPath = await selectTaskManually(config); if (!taskPath) { console.log(chalk.red('❌ No task selected.')); process.exit(1); } taskInfo = { path: taskPath }; } // Check if there's a writeup.md file in the task folder const writeupPath = path.join(taskInfo.path, 'writeup.md'); if (!await fs.pathExists(writeupPath)) { console.log(chalk.red(`❌ No writeup.md found at ${writeupPath}`)); process.exit(1); } // Read the writeup and check for required fields const writeupContent = await fs.readFile(writeupPath, 'utf-8'); // Check for flag, points, and solver const flagMatch = writeupContent.match(/\*\*Flag:\*\* `(.+)`/); const flag = flagMatch ? flagMatch[1] : null; const isFlagMissing = !flag || flag === 'TBD'; const pointsMatch = writeupContent.match(/\*\*Points:\*\* (\d+|TBD)/); const points = pointsMatch ? pointsMatch[1] : null; const isPointsMissing = !points || points === 'TBD'; const solverMatch = writeupContent.match(/\*\*Solver:\*\* (.+)/); const solver = solverMatch ? solverMatch[1].trim() : null; const isSolverMissing = !solver || solver === 'TBD'; // If everything is filled, ask if they want to finish the task if (!isFlagMissing && !isPointsMissing && !isSolverMissing) { console.log(chalk.green('✅ Task is complete:')); console.log(chalk.green(`🏆 Flag: ${flag}`)); console.log(chalk.green(`💯 Points: ${points}`)); console.log(chalk.green(`👤 Solver: ${solver}`)); const { finishTask } = await inquirer.prompt([ { type: 'confirm', name: 'finishTask', message: 'Do you want to merge this branch to main and delete it?', default: true } ]); if (finishTask) { await mergeAndDeleteBranch(currentBranch); console.log(chalk.green('🎉 Task completed and branch cleaned up!')); } else { console.log(chalk.blue('Task remains open. Run `flagtrack solve` again when ready to merge.')); } return; } // First, ask if they solved the flag const { solvedFlag } = await inquirer.prompt([ { type: 'confirm', name: 'solvedFlag', message: 'Did you solve the flag?', default: true } ]); // If they didn't solve it, don't continue if (!solvedFlag && isFlagMissing) { console.log(chalk.yellow('🔍 Task remains unsolved. Run `flagtrack solve` when the flag is found.')); return; } // Prepare to update the writeup let updatedContent = writeupContent; let isUpdated = false; // Update flag if missing if (isFlagMissing) { const { flagValue } = await inquirer.prompt([ { type: 'input', name: 'flagValue', message: 'Enter the flag:', validate: input => input.trim() ? true : 'Flag cannot be empty' } ]); if (flagMatch) { // Replace existing TBD flag updatedContent = updatedContent.replace(/\*\*Flag:\*\* `TBD`/, `**Flag:** \`${flagValue}\``); } else { // Add flag after category updatedContent = updatedContent.replace(/\*\*Category:\*\* (.+)/, `**Category:** $1\n**Flag:** \`${flagValue}\``); } isUpdated = true; } // Update solver if missing if (isSolverMissing) { const { solverName } = await inquirer.prompt([ { type: 'input', name: 'solverName', message: 'Who solved this challenge?', default: solvedFlag ? (await getGitUserName() || 'Unknown') : 'Team effort' } ]); if (solverMatch) { // Replace existing TBD solver updatedContent = updatedContent.replace(/\*\*Solver:\*\* TBD/, `**Solver:** ${solverName}`); } else { // Add solver after flag updatedContent = updatedContent.replace(/\*\*Flag:\*\* `(.+)`/, `**Flag:** \`$1\`\n**Solver:** ${solverName}`); } isUpdated = true; } // Update points if missing if (isPointsMissing) { const { pointsValue } = await inquirer.prompt([ { type: 'input', name: 'pointsValue', message: 'Enter the points for this challenge:', validate: input => { const number = parseInt(input.trim()); return !isNaN(number) && number > 0 ? true : 'Please enter a valid positive number'; } } ]); if (pointsMatch) { // Replace existing TBD points updatedContent = updatedContent.replace(/\*\*Points:\*\* TBD/, `**Points:** ${pointsValue}`); } else { // Add points after category updatedContent = updatedContent.replace(/\*\*Category:\*\* (.+)/, `**Category:** $1\n**Points:** ${pointsValue}`); } isUpdated = true; } // If content was updated, write back to file if (isUpdated) { await fs.writeFile(writeupPath, updatedContent, 'utf-8'); console.log(chalk.green('✅ Writeup updated with missing information.')); // Commit the changes try { const git = simpleGit(); await git.add(writeupPath); // Create appropriate commit message let commitMessage = 'Update task'; if (isFlagMissing && solvedFlag) { const solverName = updatedContent.match(/\*\*Solver:\*\* (.+)/)[1].trim(); commitMessage = `Add flag solution by ${solverName}`; } else if (isPointsMissing) { commitMessage = 'Add points to task'; } await git.commit(commitMessage); console.log(chalk.green('✅ Changes committed.')); // Push changes try { await git.push(); console.log(chalk.green('✅ Changes pushed to remote.')); } catch (pushError) { console.log(chalk.yellow(`⚠️ Could not push changes: ${pushError.message}`)); } } catch (gitError) { console.log(chalk.yellow(`⚠️ Could not commit changes: ${gitError.message}`)); } } // If everything is now complete, ask about merging if (updatedContent.match(/\*\*Flag:\*\* `(.+?)`/) && updatedContent.match(/\*\*Points:\*\* (\d+)/) && updatedContent.match(/\*\*Solver:\*\* (.+)/) && !updatedContent.includes('TBD')) { const { finishTask } = await inquirer.prompt([ { type: 'confirm', name: 'finishTask', message: 'Task is now complete! Merge this branch to main and delete it?', default: true } ]); if (finishTask) { await mergeAndDeleteBranch(currentBranch); console.log(chalk.green('🎉 Task completed and branch cleaned up!')); } else { console.log(chalk.blue('Task marked as complete but branch remains open.')); console.log(chalk.blue('Run `flagtrack solve` again when ready to merge.')); } } else { console.log(chalk.blue('Task is still missing some information.')); console.log(chalk.blue('Run `flagtrack solve` again to complete the task.')); } } catch (error) { console.error(chalk.red('❌ Error checking task:'), error.message); process.exit(1); } } async function mergeAndDeleteBranch(currentBranch) { try { const git = simpleGit(); // Check if we have any uncommitted changes const status = await git.status(); if (status.files.length > 0) { console.log(chalk.yellow('⚠️ You have uncommitted changes. Please commit or stash them first.')); return false; } // Get main branch name (either main or master) const branches = await git.branch(); const mainBranch = branches.all.includes('main') ? 'main' : 'master'; // Make sure we have the latest changes from main console.log(chalk.blue(`📥 Fetching latest changes from ${mainBranch}...`)); await git.fetch('origin', mainBranch); // Checkout main branch console.log(chalk.blue(`🔄 Switching to ${mainBranch} branch...`)); await git.checkout(mainBranch); // Pull latest changes await git.pull('origin', mainBranch); // Merge the task branch console.log(chalk.blue(`🔀 Merging ${currentBranch} into ${mainBranch}...`)); await git.merge([currentBranch, '--no-ff', '-m', `Merge task branch '${currentBranch}'`]); // Push the changes to main console.log(chalk.blue(`📤 Pushing changes to ${mainBranch}...`)); await git.push('origin', mainBranch); // Delete the branch locally console.log(chalk.blue(`🗑️ Deleting local branch ${currentBranch}...`)); await git.deleteLocalBranch(currentBranch, true); // Delete the branch on remote console.log(chalk.blue(`🗑️ Deleting remote branch ${currentBranch}...`)); try { await git.push('origin', `:${currentBranch}`); } catch (error) { console.log(chalk.yellow(`⚠️ Could not delete remote branch: ${error.message}`)); console.log(chalk.yellow('This is often normal for protected branches or if the branch was never pushed.')); } return true; } catch (error) { console.log(chalk.red(`❌ Error during merge: ${error.message}`)); console.log(chalk.yellow('You may need to resolve conflicts or complete the merge manually.')); return false; } } async function findTaskFromBranch(branchName, config) { // Try to parse branch name in format: category-tasknum-name const branchPattern = /^([a-z]+)-(\d+)-(.+)$/; const match = branchName.match(branchPattern); if (!match) { return null; } const category = match[1]; const taskNum = match[2].padStart(2, '0'); // Find the category folder const ctfRoot = await validateLocation(config); // Find the category number from name let categoryNum = null; for (const [num, name] of Object.entries(config.categories)) { if (name.toLowerCase() === category) { categoryNum = num.padStart(2, '0'); break; } } if (!categoryNum) { return null; } // Look for matching task folder const categoryPath = path.join(ctfRoot, `${categoryNum}_${capitalizeName(category)}`); if (!await fs.pathExists(categoryPath)) { return null; } // List all task folders and find one that starts with the task number const taskFolders = await fs.readdir(categoryPath, { withFileTypes: true }); for (const item of taskFolders) { if (item.isDirectory() && item.name.startsWith(`${taskNum}_`)) { return { path: path.join(categoryPath, item.name), category, taskNum }; } } return null; } function capitalizeName(str) { return str.charAt(0).toUpperCase() + str.slice(1); } async function selectTaskManually(config) { try { const ctfRoot = await validateLocation(config); // Get all categories const categories = []; for (const [num, name] of Object.entries(config.categories)) { const categoryPath = path.join(ctfRoot, `${num.padStart(2, '0')}_${name}`); if (await fs.pathExists(categoryPath)) { categories.push({ num: num.padStart(2, '0'), name, path: categoryPath }); } } if (categories.length === 0) { console.log(chalk.yellow('⚠️ No categories found.')); return null; } // Let user select category const { categoryIndex } = await inquirer.prompt([ { type: 'list', name: 'categoryIndex', message: 'Select a category:', choices: categories.map((cat, index) => ({ name: cat.name, value: index })) } ]); const selectedCategory = categories[categoryIndex]; // Get all tasks in the selected category const tasks = []; const taskFolders = await fs.readdir(selectedCategory.path, { withFileTypes: true }); for (const item of taskFolders) { if (item.isDirectory()) { const taskPath = path.join(selectedCategory.path, item.name); const writeupPath = path.join(taskPath, 'writeup.md'); if (await fs.pathExists(writeupPath)) { // Extract task name from writeup if possible let taskName = item.name; try { const content = await fs.readFile(writeupPath, 'utf-8'); const nameMatch = content.match(/# 🧩 (.+)/); if (nameMatch) { taskName = nameMatch[1]; } } catch (e) { // Ignore error and use folder name } tasks.push({ name: taskName, path: taskPath, folderName: item.name }); } } } if (tasks.length === 0) { console.log(chalk.yellow(`⚠️ No tasks found in category ${selectedCategory.name}.`)); return null; } // Let user select task const { taskIndex } = await inquirer.prompt([ { type: 'list', name: 'taskIndex', message: 'Select a task:', choices: tasks.map((task, index) => ({ name: `${task.folderName} - ${task.name}`, value: index })) } ]); return tasks[taskIndex].path; } catch (error) { console.error(chalk.red('❌ Error selecting task:'), error.message); return null; } } module.exports = solve;