Refactor: Modularize Task Master CLI into Modules Directory
Simplified the Task Master CLI by organizing code into modules within the directory. **Why:** - **Better Organization:** Code is now grouped by function (AI, commands, dependencies, tasks, UI, utilities). - **Easier to Maintain:** Smaller modules are simpler to update and fix. - **Scalable:** New features can be added more easily in a structured way. **What Changed:** - Moved code from single _____ _ __ __ _ |_ _|_ _ ___| | __ | \/ | __ _ ___| |_ ___ _ __ | |/ _` / __| |/ / | |\/| |/ _` / __| __/ _ \ '__| | | (_| \__ \ < | | | | (_| \__ \ || __/ | |_|\__,_|___/_|\_\ |_| |_|\__,_|___/\__\___|_| by https://x.com/eyaltoledano ╭────────────────────────────────────────────╮ │ │ │ Version: 0.9.16 Project: Task Master │ │ │ ╰────────────────────────────────────────────╯ ╭─────────────────────╮ │ │ │ Task Master CLI │ │ │ ╰─────────────────────╯ ╭───────────────────╮ │ Task Generation │ ╰───────────────────╯ parse-prd --input=<file.txt> [--tasks=10] Generate tasks from a PRD document generate Create individual task files from tasks… ╭───────────────────╮ │ Task Management │ ╰───────────────────╯ list [--status=<status>] [--with-subtas… List all tasks with their status set-status --id=<id> --status=<status> Update task status (done, pending, etc.) update --from=<id> --prompt="<context>" Update tasks based on new requirements add-task --prompt="<text>" [--dependencies=… Add a new task using AI add-dependency --id=<id> --depends-on=<id> Add a dependency to a task remove-dependency --id=<id> --depends-on=<id> Remove a dependency from a task ╭──────────────────────────╮ │ Task Analysis & Detail │ ╰──────────────────────────╯ analyze-complexity [--research] [--threshold=5] Analyze tasks and generate expansion re… complexity-report [--file=<path>] Display the complexity analysis report expand --id=<id> [--num=5] [--research] [… Break down tasks into detailed subtasks expand --all [--force] [--research] Expand all pending tasks with subtasks clear-subtasks --id=<id> Remove subtasks from specified tasks ╭─────────────────────────────╮ │ Task Navigation & Viewing │ ╰─────────────────────────────╯ next Show the next task to work on based on … show <id> Display detailed information about a sp… ╭─────────────────────────╮ │ Dependency Management │ ╰─────────────────────────╯ validate-dependenci… Identify invalid dependencies without f… fix-dependencies Fix invalid dependencies automatically ╭─────────────────────────╮ │ Environment Variables │ ╰─────────────────────────╯ ANTHROPIC_API_KEY Your Anthropic API key Required MODEL Claude model to use Default: claude-3-7-sonn… MAX_TOKENS Maximum tokens for responses Default: 4000 TEMPERATURE Temperature for model responses Default: 0.7 PERPLEXITY_API_KEY Perplexity API key for research Optional PERPLEXITY_MODEL Perplexity model to use Default: sonar-small-onl… DEBUG Enable debug logging Default: false LOG_LEVEL Console output level (debug,info,warn,error) Default: info DEFAULT_SUBTASKS Default number of subtasks to generate Default: 3 DEFAULT_PRIORITY Default task priority Default: medium PROJECT_NAME Project name displayed in UI Default: Task Master file into these new modules: - : AI interactions (Claude, Perplexity) - : CLI command definitions (Commander.js) - : Task dependency handling - : Core task operations (create, list, update, etc.) - : User interface elements (display, formatting) - : Utility functions and configuration - : Exports all modules - Replaced direct use of _____ _ __ __ _ |_ _|_ _ ___| | __ | \/ | __ _ ___| |_ ___ _ __ | |/ _` / __| |/ / | |\/| |/ _` / __| __/ _ \ '__| | | (_| \__ \ < | | | | (_| \__ \ || __/ | |_|\__,_|___/_|\_\ |_| |_|\__,_|___/\__\___|_| by https://x.com/eyaltoledano ╭────────────────────────────────────────────╮ │ │ │ Version: 0.9.16 Project: Task Master │ │ │ ╰────────────────────────────────────────────╯ ╭─────────────────────╮ │ │ │ Task Master CLI │ │ │ ╰─────────────────────╯ ╭───────────────────╮ │ Task Generation │ ╰───────────────────╯ parse-prd --input=<file.txt> [--tasks=10] Generate tasks from a PRD document generate Create individual task files from tasks… ╭───────────────────╮ │ Task Management │ ╰───────────────────╯ list [--status=<status>] [--with-subtas… List all tasks with their status set-status --id=<id> --status=<status> Update task status (done, pending, etc.) update --from=<id> --prompt="<context>" Update tasks based on new requirements add-task --prompt="<text>" [--dependencies=… Add a new task using AI add-dependency --id=<id> --depends-on=<id> Add a dependency to a task remove-dependency --id=<id> --depends-on=<id> Remove a dependency from a task ╭──────────────────────────╮ │ Task Analysis & Detail │ ╰──────────────────────────╯ analyze-complexity [--research] [--threshold=5] Analyze tasks and generate expansion re… complexity-report [--file=<path>] Display the complexity analysis report expand --id=<id> [--num=5] [--research] [… Break down tasks into detailed subtasks expand --all [--force] [--research] Expand all pending tasks with subtasks clear-subtasks --id=<id> Remove subtasks from specified tasks ╭─────────────────────────────╮ │ Task Navigation & Viewing │ ╰─────────────────────────────╯ next Show the next task to work on based on … show <id> Display detailed information about a sp… ╭─────────────────────────╮ │ Dependency Management │ ╰─────────────────────────╯ validate-dependenci… Identify invalid dependencies without f… fix-dependencies Fix invalid dependencies automatically ╭─────────────────────────╮ │ Environment Variables │ ╰─────────────────────────╯ ANTHROPIC_API_KEY Your Anthropic API key Required MODEL Claude model to use Default: claude-3-7-sonn… MAX_TOKENS Maximum tokens for responses Default: 4000 TEMPERATURE Temperature for model responses Default: 0.7 PERPLEXITY_API_KEY Perplexity API key for research Optional PERPLEXITY_MODEL Perplexity model to use Default: sonar-small-onl… DEBUG Enable debug logging Default: false LOG_LEVEL Console output level (debug,info,warn,error) Default: info DEFAULT_SUBTASKS Default number of subtasks to generate Default: 3 DEFAULT_PRIORITY Default task priority Default: medium PROJECT_NAME Project name displayed in UI Default: Task Master with the global command (see ). - Updated documentation () to reflect the new command. **Benefits:** Code is now cleaner, easier to work with, and ready for future growth. Use the command (or ) to run the CLI. See for command details.
This commit is contained in:
5468
scripts/dev.js
5468
scripts/dev.js
File diff suppressed because it is too large
Load Diff
538
scripts/modules/ai-services.js
Normal file
538
scripts/modules/ai-services.js
Normal file
@@ -0,0 +1,538 @@
|
||||
/**
|
||||
* ai-services.js
|
||||
* AI service interactions for the Task Master CLI
|
||||
*/
|
||||
|
||||
import { Anthropic } from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
import dotenv from 'dotenv';
|
||||
import { CONFIG, log, sanitizePrompt } from './utils.js';
|
||||
import { startLoadingIndicator, stopLoadingIndicator } from './ui.js';
|
||||
|
||||
// Load environment variables
|
||||
dotenv.config();
|
||||
|
||||
// Configure Anthropic client
|
||||
const anthropic = new Anthropic({
|
||||
apiKey: process.env.ANTHROPIC_API_KEY,
|
||||
});
|
||||
|
||||
// Lazy-loaded Perplexity client
|
||||
let perplexity = null;
|
||||
|
||||
/**
|
||||
* Get or initialize the Perplexity client
|
||||
* @returns {OpenAI} Perplexity client
|
||||
*/
|
||||
function getPerplexityClient() {
|
||||
if (!perplexity) {
|
||||
if (!process.env.PERPLEXITY_API_KEY) {
|
||||
throw new Error("PERPLEXITY_API_KEY environment variable is missing. Set it to use research-backed features.");
|
||||
}
|
||||
perplexity = new OpenAI({
|
||||
apiKey: process.env.PERPLEXITY_API_KEY,
|
||||
baseURL: 'https://api.perplexity.ai',
|
||||
});
|
||||
}
|
||||
return perplexity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Claude to generate tasks from a PRD
|
||||
* @param {string} prdContent - PRD content
|
||||
* @param {string} prdPath - Path to the PRD file
|
||||
* @param {number} numTasks - Number of tasks to generate
|
||||
* @param {number} retryCount - Retry count
|
||||
* @returns {Object} Claude's response
|
||||
*/
|
||||
async function callClaude(prdContent, prdPath, numTasks, retryCount = 0) {
|
||||
try {
|
||||
log('info', 'Calling Claude...');
|
||||
|
||||
// Build the system prompt
|
||||
const systemPrompt = `You are an AI assistant helping to break down a Product Requirements Document (PRD) into a set of sequential development tasks.
|
||||
Your goal is to create ${numTasks} well-structured, actionable development tasks based on the PRD provided.
|
||||
|
||||
Each task should follow this JSON structure:
|
||||
{
|
||||
"id": number,
|
||||
"title": string,
|
||||
"description": string,
|
||||
"status": "pending",
|
||||
"dependencies": number[] (IDs of tasks this depends on),
|
||||
"priority": "high" | "medium" | "low",
|
||||
"details": string (implementation details),
|
||||
"testStrategy": string (validation approach)
|
||||
}
|
||||
|
||||
Guidelines:
|
||||
1. Create exactly ${numTasks} tasks, numbered from 1 to ${numTasks}
|
||||
2. Each task should be atomic and focused on a single responsibility
|
||||
3. Order tasks logically - consider dependencies and implementation sequence
|
||||
4. Early tasks should focus on setup, core functionality first, then advanced features
|
||||
5. Include clear validation/testing approach for each task
|
||||
6. Set appropriate dependency IDs (a task can only depend on tasks with lower IDs)
|
||||
7. Assign priority (high/medium/low) based on criticality and dependency order
|
||||
8. Include detailed implementation guidance in the "details" field
|
||||
|
||||
Expected output format:
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"id": 1,
|
||||
"title": "Setup Project Repository",
|
||||
"description": "...",
|
||||
...
|
||||
},
|
||||
...
|
||||
],
|
||||
"metadata": {
|
||||
"projectName": "PRD Implementation",
|
||||
"totalTasks": ${numTasks},
|
||||
"sourceFile": "${prdPath}",
|
||||
"generatedAt": "YYYY-MM-DD"
|
||||
}
|
||||
}
|
||||
|
||||
Important: Your response must be valid JSON only, with no additional explanation or comments.`;
|
||||
|
||||
// Use streaming request to handle large responses and show progress
|
||||
return await handleStreamingRequest(prdContent, prdPath, numTasks, CONFIG.maxTokens, systemPrompt);
|
||||
} catch (error) {
|
||||
log('error', 'Error calling Claude:', error.message);
|
||||
|
||||
// Retry logic
|
||||
if (retryCount < 2) {
|
||||
log('info', `Retrying (${retryCount + 1}/2)...`);
|
||||
return await callClaude(prdContent, prdPath, numTasks, retryCount + 1);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle streaming request to Claude
|
||||
* @param {string} prdContent - PRD content
|
||||
* @param {string} prdPath - Path to the PRD file
|
||||
* @param {number} numTasks - Number of tasks to generate
|
||||
* @param {number} maxTokens - Maximum tokens
|
||||
* @param {string} systemPrompt - System prompt
|
||||
* @returns {Object} Claude's response
|
||||
*/
|
||||
async function handleStreamingRequest(prdContent, prdPath, numTasks, maxTokens, systemPrompt) {
|
||||
const loadingIndicator = startLoadingIndicator('Generating tasks from PRD...');
|
||||
let responseText = '';
|
||||
|
||||
try {
|
||||
const message = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: `Here's the Product Requirements Document (PRD) to break down into ${numTasks} tasks:\n\n${prdContent}`
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
responseText = message.content[0].text;
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
return processClaudeResponse(responseText, numTasks, 0, prdContent, prdPath);
|
||||
} catch (error) {
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process Claude's response
|
||||
* @param {string} textContent - Text content from Claude
|
||||
* @param {number} numTasks - Number of tasks
|
||||
* @param {number} retryCount - Retry count
|
||||
* @param {string} prdContent - PRD content
|
||||
* @param {string} prdPath - Path to the PRD file
|
||||
* @returns {Object} Processed response
|
||||
*/
|
||||
function processClaudeResponse(textContent, numTasks, retryCount, prdContent, prdPath) {
|
||||
try {
|
||||
// Attempt to parse the JSON response
|
||||
let jsonStart = textContent.indexOf('{');
|
||||
let jsonEnd = textContent.lastIndexOf('}');
|
||||
|
||||
if (jsonStart === -1 || jsonEnd === -1) {
|
||||
throw new Error("Could not find valid JSON in Claude's response");
|
||||
}
|
||||
|
||||
let jsonContent = textContent.substring(jsonStart, jsonEnd + 1);
|
||||
let parsedData = JSON.parse(jsonContent);
|
||||
|
||||
// Validate the structure of the generated tasks
|
||||
if (!parsedData.tasks || !Array.isArray(parsedData.tasks)) {
|
||||
throw new Error("Claude's response does not contain a valid tasks array");
|
||||
}
|
||||
|
||||
// Ensure we have the correct number of tasks
|
||||
if (parsedData.tasks.length !== numTasks) {
|
||||
log('warn', `Expected ${numTasks} tasks, but received ${parsedData.tasks.length}`);
|
||||
}
|
||||
|
||||
// Add metadata if missing
|
||||
if (!parsedData.metadata) {
|
||||
parsedData.metadata = {
|
||||
projectName: "PRD Implementation",
|
||||
totalTasks: parsedData.tasks.length,
|
||||
sourceFile: prdPath,
|
||||
generatedAt: new Date().toISOString().split('T')[0]
|
||||
};
|
||||
}
|
||||
|
||||
return parsedData;
|
||||
} catch (error) {
|
||||
log('error', "Error processing Claude's response:", error.message);
|
||||
|
||||
// Retry logic
|
||||
if (retryCount < 2) {
|
||||
log('info', `Retrying to parse response (${retryCount + 1}/2)...`);
|
||||
|
||||
// Try again with Claude for a cleaner response
|
||||
if (retryCount === 1) {
|
||||
log('info', "Calling Claude again for a cleaner response...");
|
||||
return callClaude(prdContent, prdPath, numTasks, retryCount + 1);
|
||||
}
|
||||
|
||||
return processClaudeResponse(textContent, numTasks, retryCount + 1, prdContent, prdPath);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate subtasks for a task
|
||||
* @param {Object} task - Task to generate subtasks for
|
||||
* @param {number} numSubtasks - Number of subtasks to generate
|
||||
* @param {number} nextSubtaskId - Next subtask ID
|
||||
* @param {string} additionalContext - Additional context
|
||||
* @returns {Array} Generated subtasks
|
||||
*/
|
||||
async function generateSubtasks(task, numSubtasks, nextSubtaskId, additionalContext = '') {
|
||||
try {
|
||||
log('info', `Generating ${numSubtasks} subtasks for task ${task.id}: ${task.title}`);
|
||||
|
||||
const loadingIndicator = startLoadingIndicator(`Generating subtasks for task ${task.id}...`);
|
||||
|
||||
const systemPrompt = `You are an AI assistant helping with task breakdown for software development.
|
||||
You need to break down a high-level task into ${numSubtasks} specific subtasks that can be implemented one by one.
|
||||
|
||||
Subtasks should:
|
||||
1. Be specific and actionable implementation steps
|
||||
2. Follow a logical sequence
|
||||
3. Each handle a distinct part of the parent task
|
||||
4. Include clear guidance on implementation approach
|
||||
5. Have appropriate dependency chains between subtasks
|
||||
6. Collectively cover all aspects of the parent task
|
||||
|
||||
For each subtask, provide:
|
||||
- A clear, specific title
|
||||
- Detailed implementation steps
|
||||
- Dependencies on previous subtasks
|
||||
- Testing approach
|
||||
|
||||
Each subtask should be implementable in a focused coding session.`;
|
||||
|
||||
const contextPrompt = additionalContext ?
|
||||
`\n\nAdditional context to consider: ${additionalContext}` : '';
|
||||
|
||||
const userPrompt = `Please break down this task into ${numSubtasks} specific, actionable subtasks:
|
||||
|
||||
Task ID: ${task.id}
|
||||
Title: ${task.title}
|
||||
Description: ${task.description}
|
||||
Current details: ${task.details || 'None provided'}
|
||||
${contextPrompt}
|
||||
|
||||
Return exactly ${numSubtasks} subtasks with the following JSON structure:
|
||||
[
|
||||
{
|
||||
"id": ${nextSubtaskId},
|
||||
"title": "First subtask title",
|
||||
"description": "Detailed description",
|
||||
"dependencies": [],
|
||||
"details": "Implementation details"
|
||||
},
|
||||
...more subtasks...
|
||||
]
|
||||
|
||||
Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use an empty array if there are no dependencies.`;
|
||||
|
||||
const message = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: userPrompt
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
return parseSubtasksFromText(message.content[0].text, nextSubtaskId, numSubtasks, task.id);
|
||||
} catch (error) {
|
||||
log('error', `Error generating subtasks: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate subtasks with research from Perplexity
|
||||
* @param {Object} task - Task to generate subtasks for
|
||||
* @param {number} numSubtasks - Number of subtasks to generate
|
||||
* @param {number} nextSubtaskId - Next subtask ID
|
||||
* @param {string} additionalContext - Additional context
|
||||
* @returns {Array} Generated subtasks
|
||||
*/
|
||||
async function generateSubtasksWithPerplexity(task, numSubtasks = 3, nextSubtaskId = 1, additionalContext = '') {
|
||||
try {
|
||||
// First, perform research to get context
|
||||
log('info', `Researching context for task ${task.id}: ${task.title}`);
|
||||
const perplexityClient = getPerplexityClient();
|
||||
|
||||
const PERPLEXITY_MODEL = process.env.PERPLEXITY_MODEL || 'sonar-small-online';
|
||||
const researchLoadingIndicator = startLoadingIndicator('Researching best practices with Perplexity AI...');
|
||||
|
||||
// Formulate research query based on task
|
||||
const researchQuery = `I need to implement "${task.title}" which involves: "${task.description}".
|
||||
What are current best practices, libraries, design patterns, and implementation approaches?
|
||||
Include concrete code examples and technical considerations where relevant.`;
|
||||
|
||||
// Query Perplexity for research
|
||||
const researchResponse = await perplexityClient.chat.completions.create({
|
||||
model: PERPLEXITY_MODEL,
|
||||
messages: [{
|
||||
role: 'user',
|
||||
content: researchQuery
|
||||
}],
|
||||
temperature: 0.1 // Lower temperature for more factual responses
|
||||
});
|
||||
|
||||
const researchResult = researchResponse.choices[0].message.content;
|
||||
|
||||
stopLoadingIndicator(researchLoadingIndicator);
|
||||
log('info', 'Research completed, now generating subtasks with additional context');
|
||||
|
||||
// Use the research result as additional context for Claude to generate subtasks
|
||||
const combinedContext = `
|
||||
RESEARCH FINDINGS:
|
||||
${researchResult}
|
||||
|
||||
ADDITIONAL CONTEXT PROVIDED BY USER:
|
||||
${additionalContext || "No additional context provided."}
|
||||
`;
|
||||
|
||||
// Now generate subtasks with Claude
|
||||
const loadingIndicator = startLoadingIndicator(`Generating research-backed subtasks for task ${task.id}...`);
|
||||
|
||||
const systemPrompt = `You are an AI assistant helping with task breakdown for software development.
|
||||
You need to break down a high-level task into ${numSubtasks} specific subtasks that can be implemented one by one.
|
||||
|
||||
You have been provided with research on current best practices and implementation approaches.
|
||||
Use this research to inform and enhance your subtask breakdown.
|
||||
|
||||
Subtasks should:
|
||||
1. Be specific and actionable implementation steps
|
||||
2. Follow a logical sequence
|
||||
3. Each handle a distinct part of the parent task
|
||||
4. Include clear guidance on implementation approach, referencing the research where relevant
|
||||
5. Have appropriate dependency chains between subtasks
|
||||
6. Collectively cover all aspects of the parent task
|
||||
|
||||
For each subtask, provide:
|
||||
- A clear, specific title
|
||||
- Detailed implementation steps that incorporate best practices from the research
|
||||
- Dependencies on previous subtasks
|
||||
- Testing approach
|
||||
|
||||
Each subtask should be implementable in a focused coding session.`;
|
||||
|
||||
const userPrompt = `Please break down this task into ${numSubtasks} specific, actionable subtasks,
|
||||
using the research findings to inform your breakdown:
|
||||
|
||||
Task ID: ${task.id}
|
||||
Title: ${task.title}
|
||||
Description: ${task.description}
|
||||
Current details: ${task.details || 'None provided'}
|
||||
|
||||
${combinedContext}
|
||||
|
||||
Return exactly ${numSubtasks} subtasks with the following JSON structure:
|
||||
[
|
||||
{
|
||||
"id": ${nextSubtaskId},
|
||||
"title": "First subtask title",
|
||||
"description": "Detailed description",
|
||||
"dependencies": [],
|
||||
"details": "Implementation details"
|
||||
},
|
||||
...more subtasks...
|
||||
]
|
||||
|
||||
Note on dependencies: Subtasks can depend on other subtasks with lower IDs. Use an empty array if there are no dependencies.`;
|
||||
|
||||
const message = await anthropic.messages.create({
|
||||
model: CONFIG.model,
|
||||
max_tokens: CONFIG.maxTokens,
|
||||
temperature: CONFIG.temperature,
|
||||
system: systemPrompt,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: userPrompt
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
stopLoadingIndicator(loadingIndicator);
|
||||
|
||||
return parseSubtasksFromText(message.content[0].text, nextSubtaskId, numSubtasks, task.id);
|
||||
} catch (error) {
|
||||
log('error', `Error generating research-backed subtasks: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse subtasks from Claude's response text
|
||||
* @param {string} text - Response text
|
||||
* @param {number} startId - Starting subtask ID
|
||||
* @param {number} expectedCount - Expected number of subtasks
|
||||
* @param {number} parentTaskId - Parent task ID
|
||||
* @returns {Array} Parsed subtasks
|
||||
*/
|
||||
function parseSubtasksFromText(text, startId, expectedCount, parentTaskId) {
|
||||
try {
|
||||
// Locate JSON array in the text
|
||||
const jsonStartIndex = text.indexOf('[');
|
||||
const jsonEndIndex = text.lastIndexOf(']');
|
||||
|
||||
if (jsonStartIndex === -1 || jsonEndIndex === -1 || jsonEndIndex < jsonStartIndex) {
|
||||
throw new Error("Could not locate valid JSON array in the response");
|
||||
}
|
||||
|
||||
// Extract and parse the JSON
|
||||
const jsonText = text.substring(jsonStartIndex, jsonEndIndex + 1);
|
||||
let subtasks = JSON.parse(jsonText);
|
||||
|
||||
// Validate
|
||||
if (!Array.isArray(subtasks)) {
|
||||
throw new Error("Parsed content is not an array");
|
||||
}
|
||||
|
||||
// Log warning if count doesn't match expected
|
||||
if (subtasks.length !== expectedCount) {
|
||||
log('warn', `Expected ${expectedCount} subtasks, but parsed ${subtasks.length}`);
|
||||
}
|
||||
|
||||
// Normalize subtask IDs if they don't match
|
||||
subtasks = subtasks.map((subtask, index) => {
|
||||
// Assign the correct ID if it doesn't match
|
||||
if (subtask.id !== startId + index) {
|
||||
log('warn', `Correcting subtask ID from ${subtask.id} to ${startId + index}`);
|
||||
subtask.id = startId + index;
|
||||
}
|
||||
|
||||
// Convert dependencies to numbers if they are strings
|
||||
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
|
||||
subtask.dependencies = subtask.dependencies.map(dep => {
|
||||
return typeof dep === 'string' ? parseInt(dep, 10) : dep;
|
||||
});
|
||||
} else {
|
||||
subtask.dependencies = [];
|
||||
}
|
||||
|
||||
// Ensure status is 'pending'
|
||||
subtask.status = 'pending';
|
||||
|
||||
// Add parentTaskId
|
||||
subtask.parentTaskId = parentTaskId;
|
||||
|
||||
return subtask;
|
||||
});
|
||||
|
||||
return subtasks;
|
||||
} catch (error) {
|
||||
log('error', `Error parsing subtasks: ${error.message}`);
|
||||
|
||||
// Create a fallback array of empty subtasks if parsing fails
|
||||
log('warn', 'Creating fallback subtasks');
|
||||
|
||||
const fallbackSubtasks = [];
|
||||
|
||||
for (let i = 0; i < expectedCount; i++) {
|
||||
fallbackSubtasks.push({
|
||||
id: startId + i,
|
||||
title: `Subtask ${startId + i}`,
|
||||
description: "Auto-generated fallback subtask",
|
||||
dependencies: [],
|
||||
details: "This is a fallback subtask created because parsing failed. Please update with real details.",
|
||||
status: 'pending',
|
||||
parentTaskId: parentTaskId
|
||||
});
|
||||
}
|
||||
|
||||
return fallbackSubtasks;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a prompt for complexity analysis
|
||||
* @param {Array} tasksData - Tasks data
|
||||
* @returns {string} Generated prompt
|
||||
*/
|
||||
function generateComplexityAnalysisPrompt(tasksData) {
|
||||
return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown:
|
||||
|
||||
${tasksData.map(task => `
|
||||
Task ID: ${task.id}
|
||||
Title: ${task.title}
|
||||
Description: ${task.description}
|
||||
Details: ${task.details}
|
||||
Dependencies: ${JSON.stringify(task.dependencies || [])}
|
||||
Priority: ${task.priority || 'medium'}
|
||||
`).join('\n---\n')}
|
||||
|
||||
Analyze each task and return a JSON array with the following structure for each task:
|
||||
[
|
||||
{
|
||||
"taskId": number,
|
||||
"taskTitle": string,
|
||||
"complexityScore": number (1-10),
|
||||
"recommendedSubtasks": number (${Math.max(3, CONFIG.defaultSubtasks - 1)}-${Math.min(8, CONFIG.defaultSubtasks + 2)}),
|
||||
"expansionPrompt": string (a specific prompt for generating good subtasks),
|
||||
"reasoning": string (brief explanation of your assessment)
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
IMPORTANT: Make sure to include an analysis for EVERY task listed above, with the correct taskId matching each task's ID.
|
||||
`;
|
||||
}
|
||||
|
||||
// Export AI service functions
|
||||
export {
|
||||
getPerplexityClient,
|
||||
callClaude,
|
||||
handleStreamingRequest,
|
||||
processClaudeResponse,
|
||||
generateSubtasks,
|
||||
generateSubtasksWithPerplexity,
|
||||
parseSubtasksFromText,
|
||||
generateComplexityAnalysisPrompt
|
||||
};
|
||||
465
scripts/modules/commands.js
Normal file
465
scripts/modules/commands.js
Normal file
@@ -0,0 +1,465 @@
|
||||
/**
|
||||
* commands.js
|
||||
* Command-line interface for the Task Master CLI
|
||||
*/
|
||||
|
||||
import { program } from 'commander';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import fs from 'fs';
|
||||
|
||||
import { CONFIG, log, readJSON } from './utils.js';
|
||||
import {
|
||||
parsePRD,
|
||||
updateTasks,
|
||||
generateTaskFiles,
|
||||
setTaskStatus,
|
||||
listTasks,
|
||||
expandTask,
|
||||
expandAllTasks,
|
||||
clearSubtasks,
|
||||
addTask,
|
||||
analyzeTaskComplexity
|
||||
} from './task-manager.js';
|
||||
|
||||
import {
|
||||
addDependency,
|
||||
removeDependency,
|
||||
validateDependenciesCommand,
|
||||
fixDependenciesCommand
|
||||
} from './dependency-manager.js';
|
||||
|
||||
import {
|
||||
displayBanner,
|
||||
displayHelp,
|
||||
displayNextTask,
|
||||
displayTaskById,
|
||||
displayComplexityReport,
|
||||
} from './ui.js';
|
||||
|
||||
/**
|
||||
* Configure and register CLI commands
|
||||
* @param {Object} program - Commander program instance
|
||||
*/
|
||||
function registerCommands(programInstance) {
|
||||
// Default help
|
||||
programInstance.on('--help', function() {
|
||||
displayHelp();
|
||||
});
|
||||
|
||||
// parse-prd command
|
||||
programInstance
|
||||
.command('parse-prd')
|
||||
.description('Parse a PRD file and generate tasks')
|
||||
.argument('<file>', 'Path to the PRD file')
|
||||
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
||||
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
||||
.action(async (file, options) => {
|
||||
const numTasks = parseInt(options.numTasks, 10);
|
||||
const outputPath = options.output;
|
||||
|
||||
console.log(chalk.blue(`Parsing PRD file: ${file}`));
|
||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||
|
||||
await parsePRD(file, outputPath, numTasks);
|
||||
});
|
||||
|
||||
// update command
|
||||
programInstance
|
||||
.command('update')
|
||||
.description('Update tasks based on new information or implementation changes')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('--from <id>', 'Task ID to start updating from (tasks with ID >= this value will be updated)', '1')
|
||||
.option('-p, --prompt <text>', 'Prompt explaining the changes or new context (required)')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const fromId = parseInt(options.from, 10);
|
||||
const prompt = options.prompt;
|
||||
|
||||
if (!prompt) {
|
||||
console.error(chalk.red('Error: --prompt parameter is required. Please provide information about the changes.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`Updating tasks from ID >= ${fromId} with prompt: "${prompt}"`));
|
||||
console.log(chalk.blue(`Tasks file: ${tasksPath}`));
|
||||
|
||||
await updateTasks(tasksPath, fromId, prompt);
|
||||
});
|
||||
|
||||
// generate command
|
||||
programInstance
|
||||
.command('generate')
|
||||
.description('Generate task files from tasks.json')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-o, --output <dir>', 'Output directory', 'tasks')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const outputDir = options.output;
|
||||
|
||||
console.log(chalk.blue(`Generating task files from: ${tasksPath}`));
|
||||
console.log(chalk.blue(`Output directory: ${outputDir}`));
|
||||
|
||||
await generateTaskFiles(tasksPath, outputDir);
|
||||
});
|
||||
|
||||
// set-status command
|
||||
programInstance
|
||||
.command('set-status')
|
||||
.description('Set the status of a task')
|
||||
.option('-i, --id <id>', 'Task ID (can be comma-separated for multiple tasks)')
|
||||
.option('-s, --status <status>', 'New status (todo, in-progress, review, done)')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const taskId = options.id;
|
||||
const status = options.status;
|
||||
|
||||
if (!taskId || !status) {
|
||||
console.error(chalk.red('Error: Both --id and --status are required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`));
|
||||
|
||||
await setTaskStatus(tasksPath, taskId, status);
|
||||
});
|
||||
|
||||
// list command
|
||||
programInstance
|
||||
.command('list')
|
||||
.description('List all tasks')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-s, --status <status>', 'Filter by status')
|
||||
.option('--with-subtasks', 'Show subtasks for each task')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const statusFilter = options.status;
|
||||
const withSubtasks = options.withSubtasks || false;
|
||||
|
||||
console.log(chalk.blue(`Listing tasks from: ${tasksPath}`));
|
||||
if (statusFilter) {
|
||||
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
|
||||
}
|
||||
if (withSubtasks) {
|
||||
console.log(chalk.blue('Including subtasks in listing'));
|
||||
}
|
||||
|
||||
await listTasks(tasksPath, statusFilter, withSubtasks);
|
||||
});
|
||||
|
||||
// expand command
|
||||
programInstance
|
||||
.command('expand')
|
||||
.description('Break down tasks into detailed subtasks')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <id>', 'Task ID to expand')
|
||||
.option('-a, --all', 'Expand all tasks')
|
||||
.option('-n, --num <number>', 'Number of subtasks to generate', CONFIG.defaultSubtasks.toString())
|
||||
.option('--research', 'Enable Perplexity AI for research-backed subtask generation')
|
||||
.option('-p, --prompt <text>', 'Additional context to guide subtask generation')
|
||||
.option('--force', 'Force regeneration of subtasks for tasks that already have them')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const idArg = options.id ? parseInt(options.id, 10) : null;
|
||||
const allFlag = options.all;
|
||||
const numSubtasks = parseInt(options.num, 10);
|
||||
const forceFlag = options.force;
|
||||
const useResearch = options.research === true;
|
||||
const additionalContext = options.prompt || '';
|
||||
|
||||
// Debug log to verify the value
|
||||
log('debug', `Research enabled: ${useResearch}`);
|
||||
|
||||
if (allFlag) {
|
||||
console.log(chalk.blue(`Expanding all tasks with ${numSubtasks} subtasks each...`));
|
||||
if (useResearch) {
|
||||
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
|
||||
} else {
|
||||
console.log(chalk.yellow('Research-backed subtask generation disabled'));
|
||||
}
|
||||
if (additionalContext) {
|
||||
console.log(chalk.blue(`Additional context: "${additionalContext}"`));
|
||||
}
|
||||
await expandAllTasks(numSubtasks, useResearch, additionalContext, forceFlag);
|
||||
} else if (idArg) {
|
||||
console.log(chalk.blue(`Expanding task ${idArg} with ${numSubtasks} subtasks...`));
|
||||
if (useResearch) {
|
||||
console.log(chalk.blue('Using Perplexity AI for research-backed subtask generation'));
|
||||
} else {
|
||||
console.log(chalk.yellow('Research-backed subtask generation disabled'));
|
||||
}
|
||||
if (additionalContext) {
|
||||
console.log(chalk.blue(`Additional context: "${additionalContext}"`));
|
||||
}
|
||||
await expandTask(idArg, numSubtasks, useResearch, additionalContext);
|
||||
} else {
|
||||
console.error(chalk.red('Error: Please specify a task ID with --id=<id> or use --all to expand all tasks.'));
|
||||
}
|
||||
});
|
||||
|
||||
// analyze-complexity command
|
||||
programInstance
|
||||
.command('analyze-complexity')
|
||||
.description(`Analyze tasks and generate expansion recommendations${chalk.reset('')}`)
|
||||
.option('-o, --output <file>', 'Output file path for the report', 'scripts/task-complexity-report.json')
|
||||
.option('-m, --model <model>', 'LLM model to use for analysis (defaults to configured model)')
|
||||
.option('-t, --threshold <number>', 'Minimum complexity score to recommend expansion (1-10)', '5')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-r, --research', 'Use Perplexity AI for research-backed complexity analysis')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file || 'tasks/tasks.json';
|
||||
const outputPath = options.output;
|
||||
const modelOverride = options.model;
|
||||
const thresholdScore = parseFloat(options.threshold);
|
||||
const useResearch = options.research || false;
|
||||
|
||||
console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`));
|
||||
console.log(chalk.blue(`Output report will be saved to: ${outputPath}`));
|
||||
|
||||
if (useResearch) {
|
||||
console.log(chalk.blue('Using Perplexity AI for research-backed complexity analysis'));
|
||||
}
|
||||
|
||||
await analyzeTaskComplexity(options);
|
||||
});
|
||||
|
||||
// clear-subtasks command
|
||||
programInstance
|
||||
.command('clear-subtasks')
|
||||
.description('Clear subtasks from specified tasks')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-i, --id <ids>', 'Task IDs (comma-separated) to clear subtasks from')
|
||||
.option('--all', 'Clear subtasks from all tasks')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const taskIds = options.id;
|
||||
const all = options.all;
|
||||
|
||||
if (!taskIds && !all) {
|
||||
console.error(chalk.red('Error: Please specify task IDs with --id=<ids> or use --all to clear all tasks'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (all) {
|
||||
// If --all is specified, get all task IDs
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
console.error(chalk.red('Error: No valid tasks found'));
|
||||
process.exit(1);
|
||||
}
|
||||
const allIds = data.tasks.map(t => t.id).join(',');
|
||||
clearSubtasks(tasksPath, allIds);
|
||||
} else {
|
||||
clearSubtasks(tasksPath, taskIds);
|
||||
}
|
||||
});
|
||||
|
||||
// add-task command
|
||||
programInstance
|
||||
.command('add-task')
|
||||
.description('Add a new task using AI')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.option('-p, --prompt <text>', 'Description of the task to add (required)')
|
||||
.option('-d, --dependencies <ids>', 'Comma-separated list of task IDs this task depends on')
|
||||
.option('--priority <priority>', 'Task priority (high, medium, low)', 'medium')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const prompt = options.prompt;
|
||||
const dependencies = options.dependencies ? options.dependencies.split(',').map(id => parseInt(id.trim(), 10)) : [];
|
||||
const priority = options.priority;
|
||||
|
||||
if (!prompt) {
|
||||
console.error(chalk.red('Error: --prompt parameter is required. Please provide a task description.'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(chalk.blue(`Adding new task with description: "${prompt}"`));
|
||||
console.log(chalk.blue(`Dependencies: ${dependencies.length > 0 ? dependencies.join(', ') : 'None'}`));
|
||||
console.log(chalk.blue(`Priority: ${priority}`));
|
||||
|
||||
await addTask(tasksPath, prompt, dependencies, priority);
|
||||
});
|
||||
|
||||
// next command
|
||||
programInstance
|
||||
.command('next')
|
||||
.description(`Show the next task to work on based on dependencies and status${chalk.reset('')}`)
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
await displayNextTask(tasksPath);
|
||||
});
|
||||
|
||||
// show command
|
||||
programInstance
|
||||
.command('show')
|
||||
.description(`Display detailed information about a specific task${chalk.reset('')}`)
|
||||
.argument('[id]', 'Task ID to show')
|
||||
.option('-i, --id <id>', 'Task ID to show')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (taskId, options) => {
|
||||
const idArg = taskId || options.id;
|
||||
|
||||
if (!idArg) {
|
||||
console.error(chalk.red('Error: Please provide a task ID'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const tasksPath = options.file;
|
||||
await displayTaskById(tasksPath, idArg);
|
||||
});
|
||||
|
||||
// add-dependency command
|
||||
programInstance
|
||||
.command('add-dependency')
|
||||
.description('Add a dependency to a task')
|
||||
.option('-i, --id <id>', 'Task ID to add dependency to')
|
||||
.option('-d, --depends-on <id>', 'Task ID that will become a dependency')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const taskId = options.id;
|
||||
const dependencyId = options.dependsOn;
|
||||
|
||||
if (!taskId || !dependencyId) {
|
||||
console.error(chalk.red('Error: Both --id and --depends-on are required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await addDependency(tasksPath, parseInt(taskId, 10), parseInt(dependencyId, 10));
|
||||
});
|
||||
|
||||
// remove-dependency command
|
||||
programInstance
|
||||
.command('remove-dependency')
|
||||
.description('Remove a dependency from a task')
|
||||
.option('-i, --id <id>', 'Task ID to remove dependency from')
|
||||
.option('-d, --depends-on <id>', 'Task ID to remove as a dependency')
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file;
|
||||
const taskId = options.id;
|
||||
const dependencyId = options.dependsOn;
|
||||
|
||||
if (!taskId || !dependencyId) {
|
||||
console.error(chalk.red('Error: Both --id and --depends-on are required'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await removeDependency(tasksPath, parseInt(taskId, 10), parseInt(dependencyId, 10));
|
||||
});
|
||||
|
||||
// validate-dependencies command
|
||||
programInstance
|
||||
.command('validate-dependencies')
|
||||
.description(`Identify invalid dependencies without fixing them${chalk.reset('')}`)
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (options) => {
|
||||
await validateDependenciesCommand(options.file);
|
||||
});
|
||||
|
||||
// fix-dependencies command
|
||||
programInstance
|
||||
.command('fix-dependencies')
|
||||
.description(`Fix invalid dependencies automatically${chalk.reset('')}`)
|
||||
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
|
||||
.action(async (options) => {
|
||||
await fixDependenciesCommand(options.file);
|
||||
});
|
||||
|
||||
// complexity-report command
|
||||
programInstance
|
||||
.command('complexity-report')
|
||||
.description(`Display the complexity analysis report${chalk.reset('')}`)
|
||||
.option('-f, --file <file>', 'Path to the report file', 'scripts/task-complexity-report.json')
|
||||
.action(async (options) => {
|
||||
await displayComplexityReport(options.file);
|
||||
});
|
||||
|
||||
// Add more commands as needed...
|
||||
|
||||
return programInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup the CLI application
|
||||
* @returns {Object} Configured Commander program
|
||||
*/
|
||||
function setupCLI() {
|
||||
// Create a new program instance
|
||||
const programInstance = program
|
||||
.name('dev')
|
||||
.description('AI-driven development task management')
|
||||
.version(() => {
|
||||
// Read version directly from package.json
|
||||
try {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
return packageJson.version;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fall back to default version
|
||||
}
|
||||
return CONFIG.projectVersion; // Default fallback
|
||||
})
|
||||
.helpOption('-h, --help', 'Display help')
|
||||
.addHelpCommand(false) // Disable default help command
|
||||
.on('--help', () => {
|
||||
displayHelp(); // Use your custom help display instead
|
||||
})
|
||||
.on('-h', () => {
|
||||
displayHelp();
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// Modify the help option to use your custom display
|
||||
programInstance.helpInformation = () => {
|
||||
displayHelp();
|
||||
return '';
|
||||
};
|
||||
|
||||
// Register commands
|
||||
registerCommands(programInstance);
|
||||
|
||||
return programInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse arguments and run the CLI
|
||||
* @param {Array} argv - Command-line arguments
|
||||
*/
|
||||
async function runCLI(argv = process.argv) {
|
||||
try {
|
||||
// Display banner if not in a pipe
|
||||
if (process.stdout.isTTY) {
|
||||
displayBanner();
|
||||
}
|
||||
|
||||
// If no arguments provided, show help
|
||||
if (argv.length <= 2) {
|
||||
displayHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Setup and parse
|
||||
const programInstance = setupCLI();
|
||||
await programInstance.parseAsync(argv);
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
|
||||
if (CONFIG.debug) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
registerCommands,
|
||||
setupCLI,
|
||||
runCLI
|
||||
};
|
||||
1326
scripts/modules/dependency-manager.js
Normal file
1326
scripts/modules/dependency-manager.js
Normal file
File diff suppressed because it is too large
Load Diff
11
scripts/modules/index.js
Normal file
11
scripts/modules/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* index.js
|
||||
* Main export point for all Task Master CLI modules
|
||||
*/
|
||||
|
||||
// Export all modules
|
||||
export * from './utils.js';
|
||||
export * from './ui.js';
|
||||
export * from './ai-services.js';
|
||||
export * from './task-manager.js';
|
||||
export * from './commands.js';
|
||||
2111
scripts/modules/task-manager.js
Normal file
2111
scripts/modules/task-manager.js
Normal file
File diff suppressed because it is too large
Load Diff
903
scripts/modules/ui.js
Normal file
903
scripts/modules/ui.js
Normal file
@@ -0,0 +1,903 @@
|
||||
/**
|
||||
* ui.js
|
||||
* User interface functions for the Task Master CLI
|
||||
*/
|
||||
|
||||
import chalk from 'chalk';
|
||||
import figlet from 'figlet';
|
||||
import boxen from 'boxen';
|
||||
import ora from 'ora';
|
||||
import Table from 'cli-table3';
|
||||
import gradient from 'gradient-string';
|
||||
import { CONFIG, log, findTaskById, readJSON, readComplexityReport, truncate } from './utils.js';
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { findNextTask, analyzeTaskComplexity } from './task-manager.js';
|
||||
|
||||
// Create a color gradient for the banner
|
||||
const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']);
|
||||
const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);
|
||||
|
||||
/**
|
||||
* Display a fancy banner for the CLI
|
||||
*/
|
||||
function displayBanner() {
|
||||
console.clear();
|
||||
const bannerText = figlet.textSync('Task Master', {
|
||||
font: 'Standard',
|
||||
horizontalLayout: 'default',
|
||||
verticalLayout: 'default'
|
||||
});
|
||||
|
||||
console.log(coolGradient(bannerText));
|
||||
|
||||
// Add creator credit line below the banner
|
||||
console.log(chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano'));
|
||||
|
||||
// Read version directly from package.json
|
||||
let version = CONFIG.projectVersion; // Default fallback
|
||||
try {
|
||||
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
||||
if (fs.existsSync(packageJsonPath)) {
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
version = packageJson.version;
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fall back to default version
|
||||
}
|
||||
|
||||
console.log(boxen(chalk.white(`${chalk.bold('Version:')} ${version} ${chalk.bold('Project:')} ${CONFIG.projectName}`), {
|
||||
padding: 1,
|
||||
margin: { top: 0, bottom: 1 },
|
||||
borderStyle: 'round',
|
||||
borderColor: 'cyan'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a loading indicator with an animated spinner
|
||||
* @param {string} message - Message to display next to the spinner
|
||||
* @returns {Object} Spinner object
|
||||
*/
|
||||
function startLoadingIndicator(message) {
|
||||
const spinner = ora({
|
||||
text: message,
|
||||
color: 'cyan'
|
||||
}).start();
|
||||
|
||||
return spinner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a loading indicator
|
||||
* @param {Object} spinner - Spinner object to stop
|
||||
*/
|
||||
function stopLoadingIndicator(spinner) {
|
||||
if (spinner && spinner.stop) {
|
||||
spinner.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a progress bar using ASCII characters
|
||||
* @param {number} percent - Progress percentage (0-100)
|
||||
* @param {number} length - Length of the progress bar in characters
|
||||
* @returns {string} Formatted progress bar
|
||||
*/
|
||||
function createProgressBar(percent, length = 30) {
|
||||
const filled = Math.round(percent * length / 100);
|
||||
const empty = length - filled;
|
||||
|
||||
const filledBar = '█'.repeat(filled);
|
||||
const emptyBar = '░'.repeat(empty);
|
||||
|
||||
return `${filledBar}${emptyBar} ${percent.toFixed(0)}%`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a colored status string based on the status value
|
||||
* @param {string} status - Task status (e.g., "done", "pending", "in-progress")
|
||||
* @returns {string} Colored status string
|
||||
*/
|
||||
function getStatusWithColor(status) {
|
||||
if (!status) {
|
||||
return chalk.gray('unknown');
|
||||
}
|
||||
|
||||
const statusColors = {
|
||||
'done': chalk.green,
|
||||
'completed': chalk.green,
|
||||
'pending': chalk.yellow,
|
||||
'in-progress': chalk.blue,
|
||||
'deferred': chalk.gray,
|
||||
'blocked': chalk.red,
|
||||
'review': chalk.magenta
|
||||
};
|
||||
|
||||
const colorFunc = statusColors[status.toLowerCase()] || chalk.white;
|
||||
return colorFunc(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format dependencies list with status indicators
|
||||
* @param {Array} dependencies - Array of dependency IDs
|
||||
* @param {Array} allTasks - Array of all tasks
|
||||
* @param {boolean} forConsole - Whether the output is for console display
|
||||
* @returns {string} Formatted dependencies string
|
||||
*/
|
||||
function formatDependenciesWithStatus(dependencies, allTasks, forConsole = false) {
|
||||
if (!dependencies || !Array.isArray(dependencies) || dependencies.length === 0) {
|
||||
return forConsole ? chalk.gray('None') : 'None';
|
||||
}
|
||||
|
||||
const formattedDeps = dependencies.map(depId => {
|
||||
const depTask = findTaskById(allTasks, depId);
|
||||
|
||||
if (!depTask) {
|
||||
return forConsole ?
|
||||
chalk.red(`${depId} (Not found)`) :
|
||||
`${depId} (Not found)`;
|
||||
}
|
||||
|
||||
const status = depTask.status || 'pending';
|
||||
const isDone = status.toLowerCase() === 'done' || status.toLowerCase() === 'completed';
|
||||
|
||||
if (forConsole) {
|
||||
return isDone ?
|
||||
chalk.green(`${depId}`) :
|
||||
chalk.red(`${depId}`);
|
||||
}
|
||||
|
||||
const statusIcon = isDone ? '✅' : '⏱️';
|
||||
return `${statusIcon} ${depId} (${status})`;
|
||||
});
|
||||
|
||||
return formattedDeps.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a comprehensive help guide
|
||||
*/
|
||||
function displayHelp() {
|
||||
displayBanner();
|
||||
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Task Master CLI'),
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
|
||||
));
|
||||
|
||||
// Command categories
|
||||
const commandCategories = [
|
||||
{
|
||||
title: 'Task Generation',
|
||||
color: 'cyan',
|
||||
commands: [
|
||||
{ name: 'parse-prd', args: '--input=<file.txt> [--tasks=10]',
|
||||
desc: 'Generate tasks from a PRD document' },
|
||||
{ name: 'generate', args: '',
|
||||
desc: 'Create individual task files from tasks.json' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Task Management',
|
||||
color: 'green',
|
||||
commands: [
|
||||
{ name: 'list', args: '[--status=<status>] [--with-subtasks]',
|
||||
desc: 'List all tasks with their status' },
|
||||
{ name: 'set-status', args: '--id=<id> --status=<status>',
|
||||
desc: 'Update task status (done, pending, etc.)' },
|
||||
{ name: 'update', args: '--from=<id> --prompt="<context>"',
|
||||
desc: 'Update tasks based on new requirements' },
|
||||
{ name: 'add-task', args: '--prompt="<text>" [--dependencies=<ids>] [--priority=<priority>]',
|
||||
desc: 'Add a new task using AI' },
|
||||
{ name: 'add-dependency', args: '--id=<id> --depends-on=<id>',
|
||||
desc: 'Add a dependency to a task' },
|
||||
{ name: 'remove-dependency', args: '--id=<id> --depends-on=<id>',
|
||||
desc: 'Remove a dependency from a task' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Task Analysis & Detail',
|
||||
color: 'yellow',
|
||||
commands: [
|
||||
{ name: 'analyze-complexity', args: '[--research] [--threshold=5]',
|
||||
desc: 'Analyze tasks and generate expansion recommendations' },
|
||||
{ name: 'complexity-report', args: '[--file=<path>]',
|
||||
desc: 'Display the complexity analysis report' },
|
||||
{ name: 'expand', args: '--id=<id> [--num=5] [--research] [--prompt="<context>"]',
|
||||
desc: 'Break down tasks into detailed subtasks' },
|
||||
{ name: 'expand --all', args: '[--force] [--research]',
|
||||
desc: 'Expand all pending tasks with subtasks' },
|
||||
{ name: 'clear-subtasks', args: '--id=<id>',
|
||||
desc: 'Remove subtasks from specified tasks' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Task Navigation & Viewing',
|
||||
color: 'magenta',
|
||||
commands: [
|
||||
{ name: 'next', args: '',
|
||||
desc: 'Show the next task to work on based on dependencies' },
|
||||
{ name: 'show', args: '<id>',
|
||||
desc: 'Display detailed information about a specific task' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Dependency Management',
|
||||
color: 'blue',
|
||||
commands: [
|
||||
{ name: 'validate-dependencies', args: '',
|
||||
desc: 'Identify invalid dependencies without fixing them' },
|
||||
{ name: 'fix-dependencies', args: '',
|
||||
desc: 'Fix invalid dependencies automatically' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// Display each category
|
||||
commandCategories.forEach(category => {
|
||||
console.log(boxen(
|
||||
chalk[category.color].bold(category.title),
|
||||
{
|
||||
padding: { left: 2, right: 2, top: 0, bottom: 0 },
|
||||
margin: { top: 1, bottom: 0 },
|
||||
borderColor: category.color,
|
||||
borderStyle: 'round'
|
||||
}
|
||||
));
|
||||
|
||||
const commandTable = new Table({
|
||||
colWidths: [25, 40, 45],
|
||||
chars: {
|
||||
'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
|
||||
'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
|
||||
'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '',
|
||||
'right': '', 'right-mid': '', 'middle': ' '
|
||||
},
|
||||
style: { border: [], 'padding-left': 4 }
|
||||
});
|
||||
|
||||
category.commands.forEach((cmd, index) => {
|
||||
commandTable.push([
|
||||
`${chalk.yellow.bold(cmd.name)}${chalk.reset('')}`,
|
||||
`${chalk.white(cmd.args)}${chalk.reset('')}`,
|
||||
`${chalk.dim(cmd.desc)}${chalk.reset('')}`
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(commandTable.toString());
|
||||
console.log('');
|
||||
});
|
||||
|
||||
// Display environment variables section
|
||||
console.log(boxen(
|
||||
chalk.cyan.bold('Environment Variables'),
|
||||
{
|
||||
padding: { left: 2, right: 2, top: 0, bottom: 0 },
|
||||
margin: { top: 1, bottom: 0 },
|
||||
borderColor: 'cyan',
|
||||
borderStyle: 'round'
|
||||
}
|
||||
));
|
||||
|
||||
const envTable = new Table({
|
||||
colWidths: [30, 50, 30],
|
||||
chars: {
|
||||
'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
|
||||
'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
|
||||
'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '',
|
||||
'right': '', 'right-mid': '', 'middle': ' '
|
||||
},
|
||||
style: { border: [], 'padding-left': 4 }
|
||||
});
|
||||
|
||||
envTable.push(
|
||||
[`${chalk.yellow('ANTHROPIC_API_KEY')}${chalk.reset('')}`,
|
||||
`${chalk.white('Your Anthropic API key')}${chalk.reset('')}`,
|
||||
`${chalk.dim('Required')}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('MODEL')}${chalk.reset('')}`,
|
||||
`${chalk.white('Claude model to use')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.model}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('MAX_TOKENS')}${chalk.reset('')}`,
|
||||
`${chalk.white('Maximum tokens for responses')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.maxTokens}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('TEMPERATURE')}${chalk.reset('')}`,
|
||||
`${chalk.white('Temperature for model responses')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.temperature}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('PERPLEXITY_API_KEY')}${chalk.reset('')}`,
|
||||
`${chalk.white('Perplexity API key for research')}${chalk.reset('')}`,
|
||||
`${chalk.dim('Optional')}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('PERPLEXITY_MODEL')}${chalk.reset('')}`,
|
||||
`${chalk.white('Perplexity model to use')}${chalk.reset('')}`,
|
||||
`${chalk.dim('Default: sonar-small-online')}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('DEBUG')}${chalk.reset('')}`,
|
||||
`${chalk.white('Enable debug logging')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.debug}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('LOG_LEVEL')}${chalk.reset('')}`,
|
||||
`${chalk.white('Console output level (debug,info,warn,error)')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.logLevel}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('DEFAULT_SUBTASKS')}${chalk.reset('')}`,
|
||||
`${chalk.white('Default number of subtasks to generate')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.defaultSubtasks}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('DEFAULT_PRIORITY')}${chalk.reset('')}`,
|
||||
`${chalk.white('Default task priority')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.defaultPriority}`)}${chalk.reset('')}`],
|
||||
[`${chalk.yellow('PROJECT_NAME')}${chalk.reset('')}`,
|
||||
`${chalk.white('Project name displayed in UI')}${chalk.reset('')}`,
|
||||
`${chalk.dim(`Default: ${CONFIG.projectName}`)}${chalk.reset('')}`]
|
||||
);
|
||||
|
||||
console.log(envTable.toString());
|
||||
console.log('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get colored complexity score
|
||||
* @param {number} score - Complexity score (1-10)
|
||||
* @returns {string} Colored complexity score
|
||||
*/
|
||||
function getComplexityWithColor(score) {
|
||||
if (score <= 3) return chalk.green(score.toString());
|
||||
if (score <= 6) return chalk.yellow(score.toString());
|
||||
return chalk.red(score.toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the next task to work on
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
*/
|
||||
async function displayNextTask(tasksPath) {
|
||||
displayBanner();
|
||||
|
||||
// Read the tasks file
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
log('error', "No valid tasks found.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find the next task
|
||||
const nextTask = findNextTask(data.tasks);
|
||||
|
||||
if (!nextTask) {
|
||||
console.log(boxen(
|
||||
chalk.yellow('No eligible tasks found!\n\n') +
|
||||
'All pending tasks have unsatisfied dependencies, or all tasks are completed.',
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Display the task in a nice format
|
||||
console.log(boxen(
|
||||
chalk.white.bold(`Next Task: #${nextTask.id} - ${nextTask.title}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
|
||||
// Create a table with task details
|
||||
const taskTable = new Table({
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
'padding-top': 0,
|
||||
'padding-bottom': 0,
|
||||
compact: true
|
||||
},
|
||||
chars: {
|
||||
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
|
||||
},
|
||||
colWidths: [15, 75]
|
||||
});
|
||||
|
||||
// Priority with color
|
||||
const priorityColors = {
|
||||
'high': chalk.red.bold,
|
||||
'medium': chalk.yellow,
|
||||
'low': chalk.gray
|
||||
};
|
||||
const priorityColor = priorityColors[nextTask.priority || 'medium'] || chalk.white;
|
||||
|
||||
// Add task details to table
|
||||
taskTable.push(
|
||||
[chalk.cyan.bold('ID:'), nextTask.id.toString()],
|
||||
[chalk.cyan.bold('Title:'), nextTask.title],
|
||||
[chalk.cyan.bold('Priority:'), priorityColor(nextTask.priority || 'medium')],
|
||||
[chalk.cyan.bold('Dependencies:'), formatDependenciesWithStatus(nextTask.dependencies, data.tasks, true)],
|
||||
[chalk.cyan.bold('Description:'), nextTask.description]
|
||||
);
|
||||
|
||||
console.log(taskTable.toString());
|
||||
|
||||
// If task has details, show them in a separate box
|
||||
if (nextTask.details && nextTask.details.trim().length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Implementation Details:') + '\n\n' +
|
||||
nextTask.details,
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
}
|
||||
|
||||
// Show subtasks if they exist
|
||||
if (nextTask.subtasks && nextTask.subtasks.length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Subtasks'),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 0 }, borderColor: 'magenta', borderStyle: 'round' }
|
||||
));
|
||||
|
||||
// Create a table for subtasks
|
||||
const subtaskTable = new Table({
|
||||
head: [
|
||||
chalk.magenta.bold('ID'),
|
||||
chalk.magenta.bold('Status'),
|
||||
chalk.magenta.bold('Title'),
|
||||
chalk.magenta.bold('Dependencies')
|
||||
],
|
||||
colWidths: [6, 12, 50, 20],
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
'padding-top': 0,
|
||||
'padding-bottom': 0,
|
||||
compact: true
|
||||
},
|
||||
chars: {
|
||||
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
|
||||
}
|
||||
});
|
||||
|
||||
// Add subtasks to table
|
||||
nextTask.subtasks.forEach(st => {
|
||||
const statusColor = {
|
||||
'done': chalk.green,
|
||||
'completed': chalk.green,
|
||||
'pending': chalk.yellow,
|
||||
'in-progress': chalk.blue
|
||||
}[st.status || 'pending'] || chalk.white;
|
||||
|
||||
// Format subtask dependencies
|
||||
let subtaskDeps = 'None';
|
||||
if (st.dependencies && st.dependencies.length > 0) {
|
||||
// Format dependencies with correct notation
|
||||
const formattedDeps = st.dependencies.map(depId => {
|
||||
if (typeof depId === 'number' && depId < 100) {
|
||||
return `${nextTask.id}.${depId}`;
|
||||
}
|
||||
return depId;
|
||||
});
|
||||
subtaskDeps = formatDependenciesWithStatus(formattedDeps, data.tasks, true);
|
||||
}
|
||||
|
||||
subtaskTable.push([
|
||||
`${nextTask.id}.${st.id}`,
|
||||
statusColor(st.status || 'pending'),
|
||||
st.title,
|
||||
subtaskDeps
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(subtaskTable.toString());
|
||||
} else {
|
||||
// Suggest expanding if no subtasks
|
||||
console.log(boxen(
|
||||
chalk.yellow('No subtasks found. Consider breaking down this task:') + '\n' +
|
||||
chalk.white(`Run: ${chalk.cyan(`task-master expand --id=${nextTask.id}`)}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
}
|
||||
|
||||
// Show action suggestions
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Suggested Actions:') + '\n' +
|
||||
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` +
|
||||
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` +
|
||||
(nextTask.subtasks && nextTask.subtasks.length > 0
|
||||
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${nextTask.id}.1 --status=done`)}`
|
||||
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a specific task by ID
|
||||
* @param {string} tasksPath - Path to the tasks.json file
|
||||
* @param {string|number} taskId - The ID of the task to display
|
||||
*/
|
||||
async function displayTaskById(tasksPath, taskId) {
|
||||
displayBanner();
|
||||
|
||||
// Read the tasks file
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
log('error', "No valid tasks found.");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Find the task by ID
|
||||
const task = findTaskById(data.tasks, taskId);
|
||||
|
||||
if (!task) {
|
||||
console.log(boxen(
|
||||
chalk.yellow(`Task with ID ${taskId} not found!`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle subtask display specially
|
||||
if (task.isSubtask || task.parentTask) {
|
||||
console.log(boxen(
|
||||
chalk.white.bold(`Subtask: #${task.parentTask.id}.${task.id} - ${task.title}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'magenta', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
|
||||
// Create a table with subtask details
|
||||
const taskTable = new Table({
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
'padding-top': 0,
|
||||
'padding-bottom': 0,
|
||||
compact: true
|
||||
},
|
||||
chars: {
|
||||
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
|
||||
},
|
||||
colWidths: [15, 75]
|
||||
});
|
||||
|
||||
// Add subtask details to table
|
||||
taskTable.push(
|
||||
[chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`],
|
||||
[chalk.cyan.bold('Parent Task:'), `#${task.parentTask.id} - ${task.parentTask.title}`],
|
||||
[chalk.cyan.bold('Title:'), task.title],
|
||||
[chalk.cyan.bold('Status:'), getStatusWithColor(task.status || 'pending')],
|
||||
[chalk.cyan.bold('Description:'), task.description || 'No description provided.']
|
||||
);
|
||||
|
||||
console.log(taskTable.toString());
|
||||
|
||||
// Show action suggestions for subtask
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Suggested Actions:') + '\n' +
|
||||
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=in-progress`)}\n` +
|
||||
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=done`)}\n` +
|
||||
`${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${task.parentTask.id}`)}`,
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Display a regular task
|
||||
console.log(boxen(
|
||||
chalk.white.bold(`Task: #${task.id} - ${task.title}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
|
||||
// Create a table with task details
|
||||
const taskTable = new Table({
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
'padding-top': 0,
|
||||
'padding-bottom': 0,
|
||||
compact: true
|
||||
},
|
||||
chars: {
|
||||
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
|
||||
},
|
||||
colWidths: [15, 75]
|
||||
});
|
||||
|
||||
// Priority with color
|
||||
const priorityColors = {
|
||||
'high': chalk.red.bold,
|
||||
'medium': chalk.yellow,
|
||||
'low': chalk.gray
|
||||
};
|
||||
const priorityColor = priorityColors[task.priority || 'medium'] || chalk.white;
|
||||
|
||||
// Add task details to table
|
||||
taskTable.push(
|
||||
[chalk.cyan.bold('ID:'), task.id.toString()],
|
||||
[chalk.cyan.bold('Title:'), task.title],
|
||||
[chalk.cyan.bold('Status:'), getStatusWithColor(task.status || 'pending')],
|
||||
[chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')],
|
||||
[chalk.cyan.bold('Dependencies:'), formatDependenciesWithStatus(task.dependencies, data.tasks, true)],
|
||||
[chalk.cyan.bold('Description:'), task.description]
|
||||
);
|
||||
|
||||
console.log(taskTable.toString());
|
||||
|
||||
// If task has details, show them in a separate box
|
||||
if (task.details && task.details.trim().length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Implementation Details:') + '\n\n' +
|
||||
task.details,
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
}
|
||||
|
||||
// Show test strategy if available
|
||||
if (task.testStrategy && task.testStrategy.trim().length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Test Strategy:') + '\n\n' +
|
||||
task.testStrategy,
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
}
|
||||
|
||||
// Show subtasks if they exist
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Subtasks'),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 0 }, borderColor: 'magenta', borderStyle: 'round' }
|
||||
));
|
||||
|
||||
// Create a table for subtasks
|
||||
const subtaskTable = new Table({
|
||||
head: [
|
||||
chalk.magenta.bold('ID'),
|
||||
chalk.magenta.bold('Status'),
|
||||
chalk.magenta.bold('Title'),
|
||||
chalk.magenta.bold('Dependencies')
|
||||
],
|
||||
colWidths: [6, 12, 50, 20],
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
'padding-top': 0,
|
||||
'padding-bottom': 0,
|
||||
compact: true
|
||||
},
|
||||
chars: {
|
||||
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
|
||||
}
|
||||
});
|
||||
|
||||
// Add subtasks to table
|
||||
task.subtasks.forEach(st => {
|
||||
const statusColor = {
|
||||
'done': chalk.green,
|
||||
'completed': chalk.green,
|
||||
'pending': chalk.yellow,
|
||||
'in-progress': chalk.blue
|
||||
}[st.status || 'pending'] || chalk.white;
|
||||
|
||||
// Format subtask dependencies
|
||||
let subtaskDeps = 'None';
|
||||
if (st.dependencies && st.dependencies.length > 0) {
|
||||
// Format dependencies with correct notation
|
||||
const formattedDeps = st.dependencies.map(depId => {
|
||||
if (typeof depId === 'number' && depId < 100) {
|
||||
return `${task.id}.${depId}`;
|
||||
}
|
||||
return depId;
|
||||
});
|
||||
subtaskDeps = formatDependenciesWithStatus(formattedDeps, data.tasks, true);
|
||||
}
|
||||
|
||||
subtaskTable.push([
|
||||
`${task.id}.${st.id}`,
|
||||
statusColor(st.status || 'pending'),
|
||||
st.title,
|
||||
subtaskDeps
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(subtaskTable.toString());
|
||||
} else {
|
||||
// Suggest expanding if no subtasks
|
||||
console.log(boxen(
|
||||
chalk.yellow('No subtasks found. Consider breaking down this task:') + '\n' +
|
||||
chalk.white(`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
|
||||
));
|
||||
}
|
||||
|
||||
// Show action suggestions
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Suggested Actions:') + '\n' +
|
||||
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` +
|
||||
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` +
|
||||
(task.subtasks && task.subtasks.length > 0
|
||||
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}`
|
||||
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`),
|
||||
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the complexity analysis report in a nice format
|
||||
* @param {string} reportPath - Path to the complexity report file
|
||||
*/
|
||||
async function displayComplexityReport(reportPath) {
|
||||
displayBanner();
|
||||
|
||||
// Check if the report exists
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
console.log(boxen(
|
||||
chalk.yellow(`No complexity report found at ${reportPath}\n\n`) +
|
||||
'Would you like to generate one now?',
|
||||
{ padding: 1, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
|
||||
const readline = require('readline').createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
const answer = await new Promise(resolve => {
|
||||
readline.question(chalk.cyan('Generate complexity report? (y/n): '), resolve);
|
||||
});
|
||||
readline.close();
|
||||
|
||||
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
||||
// Call the analyze-complexity command
|
||||
console.log(chalk.blue('Generating complexity report...'));
|
||||
await analyzeTaskComplexity({
|
||||
output: reportPath,
|
||||
research: false, // Default to no research for speed
|
||||
file: 'tasks/tasks.json'
|
||||
});
|
||||
// Read the newly generated report
|
||||
return displayComplexityReport(reportPath);
|
||||
} else {
|
||||
console.log(chalk.yellow('Report generation cancelled.'));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Read the report
|
||||
let report;
|
||||
try {
|
||||
report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
|
||||
} catch (error) {
|
||||
log('error', `Error reading complexity report: ${error.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Display report header
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Task Complexity Analysis Report'),
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
|
||||
));
|
||||
|
||||
// Display metadata
|
||||
const metaTable = new Table({
|
||||
style: {
|
||||
head: [],
|
||||
border: [],
|
||||
'padding-top': 0,
|
||||
'padding-bottom': 0,
|
||||
compact: true
|
||||
},
|
||||
chars: {
|
||||
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
|
||||
},
|
||||
colWidths: [20, 50]
|
||||
});
|
||||
|
||||
metaTable.push(
|
||||
[chalk.cyan.bold('Generated:'), new Date(report.meta.generatedAt).toLocaleString()],
|
||||
[chalk.cyan.bold('Tasks Analyzed:'), report.meta.tasksAnalyzed],
|
||||
[chalk.cyan.bold('Threshold Score:'), report.meta.thresholdScore],
|
||||
[chalk.cyan.bold('Project:'), report.meta.projectName],
|
||||
[chalk.cyan.bold('Research-backed:'), report.meta.usedResearch ? 'Yes' : 'No']
|
||||
);
|
||||
|
||||
console.log(metaTable.toString());
|
||||
|
||||
// Sort tasks by complexity score (highest first)
|
||||
const sortedTasks = [...report.complexityAnalysis].sort((a, b) => b.complexityScore - a.complexityScore);
|
||||
|
||||
// Determine which tasks need expansion based on threshold
|
||||
const tasksNeedingExpansion = sortedTasks.filter(task => task.complexityScore >= report.meta.thresholdScore);
|
||||
const simpleTasks = sortedTasks.filter(task => task.complexityScore < report.meta.thresholdScore);
|
||||
|
||||
// Create progress bar to show complexity distribution
|
||||
const complexityDistribution = [0, 0, 0]; // Low (0-4), Medium (5-7), High (8-10)
|
||||
sortedTasks.forEach(task => {
|
||||
if (task.complexityScore < 5) complexityDistribution[0]++;
|
||||
else if (task.complexityScore < 8) complexityDistribution[1]++;
|
||||
else complexityDistribution[2]++;
|
||||
});
|
||||
|
||||
const percentLow = Math.round((complexityDistribution[0] / sortedTasks.length) * 100);
|
||||
const percentMedium = Math.round((complexityDistribution[1] / sortedTasks.length) * 100);
|
||||
const percentHigh = Math.round((complexityDistribution[2] / sortedTasks.length) * 100);
|
||||
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Complexity Distribution\n\n') +
|
||||
`${chalk.green.bold('Low (1-4):')} ${complexityDistribution[0]} tasks (${percentLow}%)\n` +
|
||||
`${chalk.yellow.bold('Medium (5-7):')} ${complexityDistribution[1]} tasks (${percentMedium}%)\n` +
|
||||
`${chalk.red.bold('High (8-10):')} ${complexityDistribution[2]} tasks (${percentHigh}%)`,
|
||||
{ padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
|
||||
));
|
||||
|
||||
// Create table for tasks that need expansion
|
||||
if (tasksNeedingExpansion.length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.yellow.bold(`Tasks Recommended for Expansion (${tasksNeedingExpansion.length})`),
|
||||
{ padding: { left: 2, right: 2, top: 0, bottom: 0 }, margin: { top: 1, bottom: 0 }, borderColor: 'yellow', borderStyle: 'round' }
|
||||
));
|
||||
|
||||
const complexTable = new Table({
|
||||
head: [
|
||||
chalk.yellow.bold('ID'),
|
||||
chalk.yellow.bold('Title'),
|
||||
chalk.yellow.bold('Score'),
|
||||
chalk.yellow.bold('Subtasks'),
|
||||
chalk.yellow.bold('Expansion Command')
|
||||
],
|
||||
colWidths: [5, 40, 8, 10, 45],
|
||||
style: { head: [], border: [] }
|
||||
});
|
||||
|
||||
tasksNeedingExpansion.forEach(task => {
|
||||
complexTable.push([
|
||||
task.taskId,
|
||||
truncate(task.taskTitle, 37),
|
||||
getComplexityWithColor(task.complexityScore),
|
||||
task.recommendedSubtasks,
|
||||
chalk.cyan(`task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}`)
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(complexTable.toString());
|
||||
}
|
||||
|
||||
// Create table for simple tasks
|
||||
if (simpleTasks.length > 0) {
|
||||
console.log(boxen(
|
||||
chalk.green.bold(`Simple Tasks (${simpleTasks.length})`),
|
||||
{ padding: { left: 2, right: 2, top: 0, bottom: 0 }, margin: { top: 1, bottom: 0 }, borderColor: 'green', borderStyle: 'round' }
|
||||
));
|
||||
|
||||
const simpleTable = new Table({
|
||||
head: [
|
||||
chalk.green.bold('ID'),
|
||||
chalk.green.bold('Title'),
|
||||
chalk.green.bold('Score'),
|
||||
chalk.green.bold('Reasoning')
|
||||
],
|
||||
colWidths: [5, 40, 8, 50],
|
||||
style: { head: [], border: [] }
|
||||
});
|
||||
|
||||
simpleTasks.forEach(task => {
|
||||
simpleTable.push([
|
||||
task.taskId,
|
||||
truncate(task.taskTitle, 37),
|
||||
getComplexityWithColor(task.complexityScore),
|
||||
truncate(task.reasoning, 47)
|
||||
]);
|
||||
});
|
||||
|
||||
console.log(simpleTable.toString());
|
||||
}
|
||||
|
||||
// Show action suggestions
|
||||
console.log(boxen(
|
||||
chalk.white.bold('Suggested Actions:') + '\n\n' +
|
||||
`${chalk.cyan('1.')} Expand all complex tasks: ${chalk.yellow(`task-master expand --all`)}\n` +
|
||||
`${chalk.cyan('2.')} Expand a specific task: ${chalk.yellow(`task-master expand --id=<id>`)}\n` +
|
||||
`${chalk.cyan('3.')} Regenerate with research: ${chalk.yellow(`task-master analyze-complexity --research`)}`,
|
||||
{ padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } }
|
||||
));
|
||||
}
|
||||
|
||||
// Export UI functions
|
||||
export {
|
||||
displayBanner,
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator,
|
||||
createProgressBar,
|
||||
getStatusWithColor,
|
||||
formatDependenciesWithStatus,
|
||||
displayHelp,
|
||||
getComplexityWithColor,
|
||||
displayNextTask,
|
||||
displayTaskById,
|
||||
displayComplexityReport,
|
||||
};
|
||||
283
scripts/modules/utils.js
Normal file
283
scripts/modules/utils.js
Normal file
@@ -0,0 +1,283 @@
|
||||
/**
|
||||
* utils.js
|
||||
* Utility functions for the Task Master CLI
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
// Configuration and constants
|
||||
const CONFIG = {
|
||||
model: process.env.MODEL || 'claude-3-7-sonnet-20250219',
|
||||
maxTokens: parseInt(process.env.MAX_TOKENS || '4000'),
|
||||
temperature: parseFloat(process.env.TEMPERATURE || '0.7'),
|
||||
debug: process.env.DEBUG === "true",
|
||||
logLevel: process.env.LOG_LEVEL || "info",
|
||||
defaultSubtasks: parseInt(process.env.DEFAULT_SUBTASKS || "3"),
|
||||
defaultPriority: process.env.DEFAULT_PRIORITY || "medium",
|
||||
projectName: process.env.PROJECT_NAME || "Task Master",
|
||||
projectVersion: "1.5.0" // Hardcoded version - ALWAYS use this value, ignore environment variable
|
||||
};
|
||||
|
||||
// Set up logging based on log level
|
||||
const LOG_LEVELS = {
|
||||
debug: 0,
|
||||
info: 1,
|
||||
warn: 2,
|
||||
error: 3
|
||||
};
|
||||
|
||||
/**
|
||||
* Logs a message at the specified level
|
||||
* @param {string} level - The log level (debug, info, warn, error)
|
||||
* @param {...any} args - Arguments to log
|
||||
*/
|
||||
function log(level, ...args) {
|
||||
const icons = {
|
||||
debug: chalk.gray('🔍'),
|
||||
info: chalk.blue('ℹ️'),
|
||||
warn: chalk.yellow('⚠️'),
|
||||
error: chalk.red('❌'),
|
||||
success: chalk.green('✅')
|
||||
};
|
||||
|
||||
if (LOG_LEVELS[level] >= LOG_LEVELS[CONFIG.logLevel]) {
|
||||
const icon = icons[level] || '';
|
||||
console.log(`${icon} ${args.join(' ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses a JSON file
|
||||
* @param {string} filepath - Path to the JSON file
|
||||
* @returns {Object} Parsed JSON data
|
||||
*/
|
||||
function readJSON(filepath) {
|
||||
try {
|
||||
const rawData = fs.readFileSync(filepath, 'utf8');
|
||||
return JSON.parse(rawData);
|
||||
} catch (error) {
|
||||
log('error', `Error reading JSON file ${filepath}:`, error.message);
|
||||
if (CONFIG.debug) {
|
||||
console.error(error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes data to a JSON file
|
||||
* @param {string} filepath - Path to the JSON file
|
||||
* @param {Object} data - Data to write
|
||||
*/
|
||||
function writeJSON(filepath, data) {
|
||||
try {
|
||||
fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
|
||||
} catch (error) {
|
||||
log('error', `Error writing JSON file ${filepath}:`, error.message);
|
||||
if (CONFIG.debug) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes a prompt string for use in a shell command
|
||||
* @param {string} prompt The prompt to sanitize
|
||||
* @returns {string} Sanitized prompt
|
||||
*/
|
||||
function sanitizePrompt(prompt) {
|
||||
// Replace double quotes with escaped double quotes
|
||||
return prompt.replace(/"/g, '\\"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads and parses the complexity report if it exists
|
||||
* @param {string} customPath - Optional custom path to the report
|
||||
* @returns {Object|null} The parsed complexity report or null if not found
|
||||
*/
|
||||
function readComplexityReport(customPath = null) {
|
||||
try {
|
||||
const reportPath = customPath || path.join(process.cwd(), 'scripts', 'task-complexity-report.json');
|
||||
if (!fs.existsSync(reportPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reportData = fs.readFileSync(reportPath, 'utf8');
|
||||
return JSON.parse(reportData);
|
||||
} catch (error) {
|
||||
log('warn', `Could not read complexity report: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a task analysis in the complexity report
|
||||
* @param {Object} report - The complexity report
|
||||
* @param {number} taskId - The task ID to find
|
||||
* @returns {Object|null} The task analysis or null if not found
|
||||
*/
|
||||
function findTaskInComplexityReport(report, taskId) {
|
||||
if (!report || !report.complexityAnalysis || !Array.isArray(report.complexityAnalysis)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return report.complexityAnalysis.find(task => task.taskId === taskId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a task exists in the tasks array
|
||||
* @param {Array} tasks - The tasks array
|
||||
* @param {string|number} taskId - The task ID to check
|
||||
* @returns {boolean} True if the task exists, false otherwise
|
||||
*/
|
||||
function taskExists(tasks, taskId) {
|
||||
if (!taskId || !tasks || !Array.isArray(tasks)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Handle both regular task IDs and subtask IDs (e.g., "1.2")
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
const [parentId, subtaskId] = taskId.split('.').map(id => parseInt(id, 10));
|
||||
const parentTask = tasks.find(t => t.id === parentId);
|
||||
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return parentTask.subtasks.some(st => st.id === subtaskId);
|
||||
}
|
||||
|
||||
const id = parseInt(taskId, 10);
|
||||
return tasks.some(t => t.id === id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a task ID as a string
|
||||
* @param {string|number} id - The task ID to format
|
||||
* @returns {string} The formatted task ID
|
||||
*/
|
||||
function formatTaskId(id) {
|
||||
if (typeof id === 'string' && id.includes('.')) {
|
||||
return id; // Already formatted as a string with a dot (e.g., "1.2")
|
||||
}
|
||||
|
||||
if (typeof id === 'number') {
|
||||
return id.toString();
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a task by ID in the tasks array
|
||||
* @param {Array} tasks - The tasks array
|
||||
* @param {string|number} taskId - The task ID to find
|
||||
* @returns {Object|null} The task object or null if not found
|
||||
*/
|
||||
function findTaskById(tasks, taskId) {
|
||||
if (!taskId || !tasks || !Array.isArray(tasks)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's a subtask ID (e.g., "1.2")
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
const [parentId, subtaskId] = taskId.split('.').map(id => parseInt(id, 10));
|
||||
const parentTask = tasks.find(t => t.id === parentId);
|
||||
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const subtask = parentTask.subtasks.find(st => st.id === subtaskId);
|
||||
if (subtask) {
|
||||
// Add reference to parent task for context
|
||||
subtask.parentTask = {
|
||||
id: parentTask.id,
|
||||
title: parentTask.title,
|
||||
status: parentTask.status
|
||||
};
|
||||
subtask.isSubtask = true;
|
||||
}
|
||||
|
||||
return subtask || null;
|
||||
}
|
||||
|
||||
const id = parseInt(taskId, 10);
|
||||
return tasks.find(t => t.id === id) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncates text to a specified length
|
||||
* @param {string} text - The text to truncate
|
||||
* @param {number} maxLength - The maximum length
|
||||
* @returns {string} The truncated text
|
||||
*/
|
||||
function truncate(text, maxLength) {
|
||||
if (!text || text.length <= maxLength) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return text.slice(0, maxLength - 3) + '...';
|
||||
}
|
||||
|
||||
/**
|
||||
* Find cycles in a dependency graph using DFS
|
||||
* @param {string} subtaskId - Current subtask ID
|
||||
* @param {Map} dependencyMap - Map of subtask IDs to their dependencies
|
||||
* @param {Set} visited - Set of visited nodes
|
||||
* @param {Set} recursionStack - Set of nodes in current recursion stack
|
||||
* @returns {Array} - List of dependency edges that need to be removed to break cycles
|
||||
*/
|
||||
function findCycles(subtaskId, dependencyMap, visited = new Set(), recursionStack = new Set(), path = []) {
|
||||
// Mark the current node as visited and part of recursion stack
|
||||
visited.add(subtaskId);
|
||||
recursionStack.add(subtaskId);
|
||||
path.push(subtaskId);
|
||||
|
||||
const cyclesToBreak = [];
|
||||
|
||||
// Get all dependencies of the current subtask
|
||||
const dependencies = dependencyMap.get(subtaskId) || [];
|
||||
|
||||
// For each dependency
|
||||
for (const depId of dependencies) {
|
||||
// If not visited, recursively check for cycles
|
||||
if (!visited.has(depId)) {
|
||||
const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [...path]);
|
||||
cyclesToBreak.push(...cycles);
|
||||
}
|
||||
// If the dependency is in the recursion stack, we found a cycle
|
||||
else if (recursionStack.has(depId)) {
|
||||
// Find the position of the dependency in the path
|
||||
const cycleStartIndex = path.indexOf(depId);
|
||||
// The last edge in the cycle is what we want to remove
|
||||
const cycleEdges = path.slice(cycleStartIndex);
|
||||
// We'll remove the last edge in the cycle (the one that points back)
|
||||
cyclesToBreak.push(depId);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the node from recursion stack before returning
|
||||
recursionStack.delete(subtaskId);
|
||||
|
||||
return cyclesToBreak;
|
||||
}
|
||||
|
||||
// Export all utility functions and configuration
|
||||
export {
|
||||
CONFIG,
|
||||
LOG_LEVELS,
|
||||
log,
|
||||
readJSON,
|
||||
writeJSON,
|
||||
sanitizePrompt,
|
||||
readComplexityReport,
|
||||
findTaskInComplexityReport,
|
||||
taskExists,
|
||||
formatTaskId,
|
||||
findTaskById,
|
||||
truncate,
|
||||
findCycles,
|
||||
};
|
||||
Reference in New Issue
Block a user