UNPKG

taskmanager-ai

Version:

A CLI tool to manage your projects tasks with AI

503 lines (411 loc) 14.4 kB
/** * Utilitário de gerenciamento de tarefas para o TaskManager * * Este módulo gerencia operações relacionadas às tarefas, como * criar, listar, atualizar e remover tarefas do projeto. */ import fs from 'fs/promises'; import path from 'path'; import { existsSync } from 'fs'; import { getTasksFilePath, loadConfig } from './config.js'; /** * Carrega as tarefas do arquivo tasks.json * @returns {Object} Objeto contendo as tarefas e metadados */ export async function loadTasks() { const tasksPath = getTasksFilePath(); if (!existsSync(tasksPath)) { return { tasks: [], metadata: { projectType: 'unknown', createdAt: new Date().toISOString(), lastUpdated: new Date().toISOString(), taskCount: 0, completedCount: 0, pendingCount: 0, inProgressCount: 0, deferredCount: 0, cancelledCount: 0, highPriorityCount: 0, mediumPriorityCount: 0, lowPriorityCount: 0 } }; } try { const tasksData = await fs.readFile(tasksPath, 'utf-8'); return JSON.parse(tasksData); } catch (error) { console.error(`Erro ao ler arquivo de tarefas: ${error.message}`); throw error; } } /** * Salva as tarefas no arquivo tasks.json * @param {Object} tasksData - Objeto contendo as tarefas e metadados * @returns {Boolean} True se a operação foi bem-sucedida */ export async function saveTasks(tasksData) { const tasksPath = getTasksFilePath(); try { // Atualiza o timestamp de última atualização tasksData.metadata.lastUpdated = new Date().toISOString(); // Atualiza as contagens de metadados updateTaskMetadata(tasksData); await fs.writeFile( tasksPath, JSON.stringify(tasksData, null, 2) ); return true; } catch (error) { console.error(`Erro ao salvar arquivo de tarefas: ${error.message}`); throw error; } } /** * Atualiza os metadados das tarefas * @param {Object} tasksData - Objeto contendo as tarefas e metadados */ function updateTaskMetadata(tasksData) { // Inicializa contadores let taskCount = 0; let completedCount = 0; let pendingCount = 0; let inProgressCount = 0; let deferredCount = 0; let cancelledCount = 0; let highPriorityCount = 0; let mediumPriorityCount = 0; let lowPriorityCount = 0; // Conta as tarefas por status e prioridade for (const task of tasksData.tasks) { taskCount++; // Contagem por status if (task.status === 'done') completedCount++; else if (task.status === 'pending') pendingCount++; else if (task.status === 'in-progress') inProgressCount++; else if (task.status === 'deferred') deferredCount++; else if (task.status === 'cancelled') cancelledCount++; // Contagem por prioridade if (task.priority === 'high') highPriorityCount++; else if (task.priority === 'medium') mediumPriorityCount++; else if (task.priority === 'low') lowPriorityCount++; // Conta subtarefas (opcional) if (task.subtasks && Array.isArray(task.subtasks)) { for (const subtask of task.subtasks) { if (subtask.status === 'done') completedCount++; else if (subtask.status === 'pending') pendingCount++; else if (subtask.status === 'in-progress') inProgressCount++; else if (subtask.status === 'deferred') deferredCount++; else if (subtask.status === 'cancelled') cancelledCount++; } } } // Atualiza os metadados tasksData.metadata.taskCount = taskCount; tasksData.metadata.completedCount = completedCount; tasksData.metadata.pendingCount = pendingCount; tasksData.metadata.inProgressCount = inProgressCount; tasksData.metadata.deferredCount = deferredCount; tasksData.metadata.cancelledCount = cancelledCount; tasksData.metadata.highPriorityCount = highPriorityCount; tasksData.metadata.mediumPriorityCount = mediumPriorityCount; tasksData.metadata.lowPriorityCount = lowPriorityCount; } /** * Adiciona uma nova tarefa * @param {Object} taskData - Dados da tarefa a ser adicionada * @returns {Object} Tarefa adicionada com ID */ export async function addTask(taskData) { const tasksData = await loadTasks(); const config = await loadConfig(); // Gera um novo ID para a tarefa const newId = tasksData.tasks.length > 0 ? Math.max(...tasksData.tasks.map(t => t.id)) + 1 : 1; // Define valores padrão se não fornecidos const defaultPriority = config?.taskSettings?.defaultPriority || 'medium'; // Cria a nova tarefa const newTask = { id: newId, title: taskData.title || 'Nova Tarefa', description: taskData.description || '', status: taskData.status || 'pending', priority: taskData.priority || defaultPriority, dependencies: taskData.dependencies || [], details: taskData.details || '', testStrategy: taskData.testStrategy || '', category: taskData.category || 'feature', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), subtasks: [] }; // Adiciona a tarefa ao array tasksData.tasks.push(newTask); // Salva o arquivo atualizado await saveTasks(tasksData); return newTask; } /** * Atualiza uma tarefa existente * @param {Number} taskId - ID da tarefa a ser atualizada * @param {Object} taskData - Dados atualizados da tarefa * @returns {Object} Tarefa atualizada ou null se não encontrada */ export async function updateTask(taskId, taskData) { const tasksData = await loadTasks(); // Encontra o índice da tarefa const taskIndex = tasksData.tasks.findIndex(t => t.id === taskId); if (taskIndex === -1) { return null; } // Atualiza os campos da tarefa const updatedTask = { ...tasksData.tasks[taskIndex], ...taskData, updatedAt: new Date().toISOString() }; // Mantém o ID original updatedTask.id = taskId; // Atualiza a tarefa no array tasksData.tasks[taskIndex] = updatedTask; // Salva o arquivo atualizado await saveTasks(tasksData); return updatedTask; } /** * Remove uma tarefa * @param {Number} taskId - ID da tarefa a ser removida * @returns {Boolean} True se a tarefa foi removida com sucesso */ export async function removeTask(taskId) { const tasksData = await loadTasks(); // Encontra o índice da tarefa const taskIndex = tasksData.tasks.findIndex(t => t.id === taskId); if (taskIndex === -1) { return false; } // Remove a tarefa do array tasksData.tasks.splice(taskIndex, 1); // Salva o arquivo atualizado await saveTasks(tasksData); return true; } /** * Obtém uma tarefa por ID * @param {Number} taskId - ID da tarefa * @returns {Object} Tarefa encontrada ou null se não existir */ export async function getTaskById(taskId) { const tasksData = await loadTasks(); // Encontra a tarefa pelo ID const task = tasksData.tasks.find(t => t.id === taskId); return task || null; } /** * Encontra a próxima tarefa a ser trabalhada * @returns {Object} Próxima tarefa ou null se não houver */ export async function getNextTask() { const tasksData = await loadTasks(); // Filtra tarefas pendentes ou em progresso const availableTasks = tasksData.tasks.filter(t => (t.status === 'pending' || t.status === 'in-progress') && !hasPendingDependencies(t, tasksData.tasks) ); if (availableTasks.length === 0) { return null; } // Ordena por prioridade (high > medium > low) const priorityOrder = { high: 1, medium: 2, low: 3 }; const sortedTasks = [...availableTasks].sort((a, b) => { // Primeiro, ordena por prioridade if (priorityOrder[a.priority] !== priorityOrder[b.priority]) { return priorityOrder[a.priority] - priorityOrder[b.priority]; } // Se a prioridade for igual, tarefas em andamento têm precedência if (a.status !== b.status) { return a.status === 'in-progress' ? -1 : 1; } // Se o status for igual, ordena por ID (mais antigo primeiro) return a.id - b.id; }); return sortedTasks[0]; } /** * Verifica se uma tarefa tem dependências pendentes * @param {Object} task - Tarefa a ser verificada * @param {Array} allTasks - Lista de todas as tarefas * @returns {Boolean} True se a tarefa tiver dependências pendentes */ function hasPendingDependencies(task, allTasks) { if (!task.dependencies || task.dependencies.length === 0) { return false; } // Verifica cada dependência for (const depId of task.dependencies) { const depTask = allTasks.find(t => t.id === depId); // Se a dependência não existir ou não estiver concluída if (!depTask || depTask.status !== 'done') { return true; } } return false; } /** * Adiciona uma subtarefa a uma tarefa existente * @param {Number} taskId - ID da tarefa pai * @param {Object} subtaskData - Dados da subtarefa * @returns {Object} Subtarefa adicionada ou null se a tarefa pai não existir */ export async function addSubtask(taskId, subtaskData) { const tasksData = await loadTasks(); // Encontra o índice da tarefa pai const taskIndex = tasksData.tasks.findIndex(t => t.id === taskId); if (taskIndex === -1) { return null; } // Inicializa o array de subtarefas se não existir if (!tasksData.tasks[taskIndex].subtasks) { tasksData.tasks[taskIndex].subtasks = []; } // Gera um novo ID para a subtarefa const subtasks = tasksData.tasks[taskIndex].subtasks; const newSubtaskId = subtasks.length > 0 ? Math.max(...subtasks.map(st => st.id)) + 1 : 1; // Cria a nova subtarefa const newSubtask = { id: newSubtaskId, title: subtaskData.title || 'Nova Subtarefa', description: subtaskData.description || '', status: subtaskData.status || 'pending', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() }; // Adiciona a subtarefa ao array tasksData.tasks[taskIndex].subtasks.push(newSubtask); // Atualiza a data de modificação da tarefa pai tasksData.tasks[taskIndex].updatedAt = new Date().toISOString(); // Salva o arquivo atualizado await saveTasks(tasksData); return newSubtask; } /** * Atualiza o status de uma tarefa * @param {Number} taskId - ID da tarefa * @param {String} status - Novo status * @param {Number} subtaskId - ID da subtarefa (opcional) * @returns {Boolean} True se a atualização foi bem-sucedida */ export async function updateTaskStatus(taskId, status, subtaskId = null) { const tasksData = await loadTasks(); // Encontra o índice da tarefa const taskIndex = tasksData.tasks.findIndex(t => t.id === taskId); if (taskIndex === -1) { return false; } if (subtaskId !== null) { // Atualiza o status da subtarefa const subtasks = tasksData.tasks[taskIndex].subtasks || []; const subtaskIndex = subtasks.findIndex(st => st.id === subtaskId); if (subtaskIndex === -1) { return false; } tasksData.tasks[taskIndex].subtasks[subtaskIndex].status = status; tasksData.tasks[taskIndex].subtasks[subtaskIndex].updatedAt = new Date().toISOString(); } else { // Atualiza o status da tarefa tasksData.tasks[taskIndex].status = status; tasksData.tasks[taskIndex].updatedAt = new Date().toISOString(); // Se a tarefa for marcada como concluída, marca todas as subtarefas como concluídas if (status === 'done' && tasksData.tasks[taskIndex].subtasks) { for (let i = 0; i < tasksData.tasks[taskIndex].subtasks.length; i++) { tasksData.tasks[taskIndex].subtasks[i].status = 'done'; tasksData.tasks[taskIndex].subtasks[i].updatedAt = new Date().toISOString(); } } } // Salva o arquivo atualizado await saveTasks(tasksData); return true; } /** * Lista todas as tarefas ou filtra por status * @param {String} status - Status para filtrar (opcional) * @returns {Array} Lista de tarefas */ export async function listTasks(status = null) { const tasksData = await loadTasks(); if (status) { return tasksData.tasks.filter(t => t.status === status); } return tasksData.tasks; } /** * Adiciona uma dependência entre tarefas * @param {Number} taskId - ID da tarefa * @param {Number} dependsOnId - ID da tarefa dependência * @returns {Boolean} True se a operação foi bem-sucedida */ export async function addDependency(taskId, dependsOnId) { const tasksData = await loadTasks(); // Encontra os índices das tarefas const taskIndex = tasksData.tasks.findIndex(t => t.id === taskId); const depIndex = tasksData.tasks.findIndex(t => t.id === dependsOnId); // Verifica se ambas as tarefas existem if (taskIndex === -1 || depIndex === -1 || taskId === dependsOnId) { return false; } // Inicializa o array de dependências se não existir if (!tasksData.tasks[taskIndex].dependencies) { tasksData.tasks[taskIndex].dependencies = []; } // Adiciona a dependência se ainda não existir if (!tasksData.tasks[taskIndex].dependencies.includes(dependsOnId)) { tasksData.tasks[taskIndex].dependencies.push(dependsOnId); tasksData.tasks[taskIndex].updatedAt = new Date().toISOString(); // Salva o arquivo atualizado await saveTasks(tasksData); } return true; } /** * Remove uma dependência entre tarefas * @param {Number} taskId - ID da tarefa * @param {Number} dependsOnId - ID da tarefa dependência * @returns {Boolean} True se a operação foi bem-sucedida */ export async function removeDependency(taskId, dependsOnId) { const tasksData = await loadTasks(); // Encontra o índice da tarefa const taskIndex = tasksData.tasks.findIndex(t => t.id === taskId); if (taskIndex === -1 || !tasksData.tasks[taskIndex].dependencies) { return false; } // Encontra o índice da dependência no array const depIndex = tasksData.tasks[taskIndex].dependencies.indexOf(dependsOnId); if (depIndex === -1) { return false; } // Remove a dependência tasksData.tasks[taskIndex].dependencies.splice(depIndex, 1); tasksData.tasks[taskIndex].updatedAt = new Date().toISOString(); // Salva o arquivo atualizado await saveTasks(tasksData); return true; } /** * Obtém todas as tarefas do projeto * @returns {Array} Array de tarefas */ export async function getTasks() { return await loadTasks(); } function isValidStatus(status) { const validStatuses = ['pending', 'in-progress', 'done', 'cancelled', 'postponed']; return validStatuses.includes(status); } export { isValidStatus };