chore: passes tests and linting

This commit is contained in:
Eyal Toledano
2025-06-07 20:30:51 -04:00
parent 27edbd8f3f
commit faae0b419d
13 changed files with 6781 additions and 6777 deletions

View File

@@ -24,9 +24,9 @@ import {
getAzureBaseURL,
getBedrockBaseURL,
getVertexProjectId,
getVertexLocation,
} from "./config-manager.js";
import { log, findProjectRoot, resolveEnvVariable } from "./utils.js";
getVertexLocation
} from './config-manager.js';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
// Import provider classes
import {
@@ -39,8 +39,8 @@ import {
OllamaAIProvider,
BedrockAIProvider,
AzureProvider,
VertexAIProvider,
} from "../../src/ai-providers/index.js";
VertexAIProvider
} from '../../src/ai-providers/index.js';
// Create provider instances
const PROVIDERS = {
@@ -53,36 +53,36 @@ const PROVIDERS = {
ollama: new OllamaAIProvider(),
bedrock: new BedrockAIProvider(),
azure: new AzureProvider(),
vertex: new VertexAIProvider(),
vertex: new VertexAIProvider()
};
// Helper function to get cost for a specific model
function _getCostForModel(providerName, modelId) {
if (!MODEL_MAP || !MODEL_MAP[providerName]) {
log(
"warn",
'warn',
`Provider "${providerName}" not found in MODEL_MAP. Cannot determine cost for model ${modelId}.`
);
return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost
return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost
}
const modelData = MODEL_MAP[providerName].find((m) => m.id === modelId);
if (!modelData || !modelData.cost_per_1m_tokens) {
log(
"debug",
'debug',
`Cost data not found for model "${modelId}" under provider "${providerName}". Assuming zero cost.`
);
return { inputCost: 0, outputCost: 0, currency: "USD" }; // Default to zero cost
return { inputCost: 0, outputCost: 0, currency: 'USD' }; // Default to zero cost
}
// Ensure currency is part of the returned object, defaulting if not present
const currency = modelData.cost_per_1m_tokens.currency || "USD";
const currency = modelData.cost_per_1m_tokens.currency || 'USD';
return {
inputCost: modelData.cost_per_1m_tokens.input || 0,
outputCost: modelData.cost_per_1m_tokens.output || 0,
currency: currency,
currency: currency
};
}
@@ -92,13 +92,13 @@ const INITIAL_RETRY_DELAY_MS = 1000;
// Helper function to check if an error is retryable
function isRetryableError(error) {
const errorMessage = error.message?.toLowerCase() || "";
const errorMessage = error.message?.toLowerCase() || '';
return (
errorMessage.includes("rate limit") ||
errorMessage.includes("overloaded") ||
errorMessage.includes("service temporarily unavailable") ||
errorMessage.includes("timeout") ||
errorMessage.includes("network error") ||
errorMessage.includes('rate limit') ||
errorMessage.includes('overloaded') ||
errorMessage.includes('service temporarily unavailable') ||
errorMessage.includes('timeout') ||
errorMessage.includes('network error') ||
error.status === 429 ||
error.status >= 500
);
@@ -123,7 +123,7 @@ function _extractErrorMessage(error) {
}
// Attempt 3: Look for nested error message in response body if it's JSON string
if (typeof error?.responseBody === "string") {
if (typeof error?.responseBody === 'string') {
try {
const body = JSON.parse(error.responseBody);
if (body?.error?.message) {
@@ -135,20 +135,20 @@ function _extractErrorMessage(error) {
}
// Attempt 4: Use the top-level message if it exists
if (typeof error?.message === "string" && error.message) {
if (typeof error?.message === 'string' && error.message) {
return error.message;
}
// Attempt 5: Handle simple string errors
if (typeof error === "string") {
if (typeof error === 'string') {
return error;
}
// Fallback
return "An unknown AI service error occurred.";
return 'An unknown AI service error occurred.';
} catch (e) {
// Safety net
return "Failed to extract error message.";
return 'Failed to extract error message.';
}
}
@@ -162,17 +162,17 @@ function _extractErrorMessage(error) {
*/
function _resolveApiKey(providerName, session, projectRoot = null) {
const keyMap = {
openai: "OPENAI_API_KEY",
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_API_KEY",
perplexity: "PERPLEXITY_API_KEY",
mistral: "MISTRAL_API_KEY",
azure: "AZURE_OPENAI_API_KEY",
openrouter: "OPENROUTER_API_KEY",
xai: "XAI_API_KEY",
ollama: "OLLAMA_API_KEY",
bedrock: "AWS_ACCESS_KEY_ID",
vertex: "GOOGLE_API_KEY",
openai: 'OPENAI_API_KEY',
anthropic: 'ANTHROPIC_API_KEY',
google: 'GOOGLE_API_KEY',
perplexity: 'PERPLEXITY_API_KEY',
mistral: 'MISTRAL_API_KEY',
azure: 'AZURE_OPENAI_API_KEY',
openrouter: 'OPENROUTER_API_KEY',
xai: 'XAI_API_KEY',
ollama: 'OLLAMA_API_KEY',
bedrock: 'AWS_ACCESS_KEY_ID',
vertex: 'GOOGLE_API_KEY'
};
const envVarName = keyMap[providerName];
@@ -185,7 +185,7 @@ function _resolveApiKey(providerName, session, projectRoot = null) {
const apiKey = resolveEnvVariable(envVarName, session, projectRoot);
// Special handling for providers that can use alternative auth
if (providerName === "ollama" || providerName === "bedrock") {
if (providerName === 'ollama' || providerName === 'bedrock') {
return apiKey || null;
}
@@ -223,7 +223,7 @@ async function _attemptProviderCallWithRetries(
try {
if (getDebugFlag()) {
log(
"info",
'info',
`Attempt ${retries + 1}/${MAX_RETRIES + 1} calling ${fnName} (Provider: ${providerName}, Model: ${modelId}, Role: ${attemptRole})`
);
}
@@ -233,14 +233,14 @@ async function _attemptProviderCallWithRetries(
if (getDebugFlag()) {
log(
"info",
'info',
`${fnName} succeeded for role ${attemptRole} (Provider: ${providerName}) on attempt ${retries + 1}`
);
}
return result;
} catch (error) {
log(
"warn",
'warn',
`Attempt ${retries + 1} failed for role ${attemptRole} (${fnName} / ${providerName}): ${error.message}`
);
@@ -248,13 +248,13 @@ async function _attemptProviderCallWithRetries(
retries++;
const delay = INITIAL_RETRY_DELAY_MS * Math.pow(2, retries - 1);
log(
"info",
'info',
`Something went wrong on the provider side. Retrying in ${delay / 1000}s...`
);
await new Promise((resolve) => setTimeout(resolve, delay));
} else {
log(
"error",
'error',
`Something went wrong on the provider side. Max retries reached for role ${attemptRole} (${fnName} / ${providerName}).`
);
throw error;
@@ -296,11 +296,11 @@ async function _unifiedServiceRunner(serviceType, params) {
...restApiParams
} = params;
if (getDebugFlag()) {
log("info", `${serviceType}Service called`, {
log('info', `${serviceType}Service called`, {
role: initialRole,
commandName,
outputType,
projectRoot,
projectRoot
});
}
@@ -308,23 +308,23 @@ async function _unifiedServiceRunner(serviceType, params) {
const userId = getUserId(effectiveProjectRoot);
let sequence;
if (initialRole === "main") {
sequence = ["main", "fallback", "research"];
} else if (initialRole === "research") {
sequence = ["research", "fallback", "main"];
} else if (initialRole === "fallback") {
sequence = ["fallback", "main", "research"];
if (initialRole === 'main') {
sequence = ['main', 'fallback', 'research'];
} else if (initialRole === 'research') {
sequence = ['research', 'fallback', 'main'];
} else if (initialRole === 'fallback') {
sequence = ['fallback', 'main', 'research'];
} else {
log(
"warn",
'warn',
`Unknown initial role: ${initialRole}. Defaulting to main -> fallback -> research sequence.`
);
sequence = ["main", "fallback", "research"];
sequence = ['main', 'fallback', 'research'];
}
let lastError = null;
let lastCleanErrorMessage =
"AI service call failed for all configured roles.";
'AI service call failed for all configured roles.';
for (const currentRole of sequence) {
let providerName,
@@ -337,20 +337,20 @@ async function _unifiedServiceRunner(serviceType, params) {
telemetryData = null;
try {
log("info", `New AI service call with role: ${currentRole}`);
log('info', `New AI service call with role: ${currentRole}`);
if (currentRole === "main") {
if (currentRole === 'main') {
providerName = getMainProvider(effectiveProjectRoot);
modelId = getMainModelId(effectiveProjectRoot);
} else if (currentRole === "research") {
} else if (currentRole === 'research') {
providerName = getResearchProvider(effectiveProjectRoot);
modelId = getResearchModelId(effectiveProjectRoot);
} else if (currentRole === "fallback") {
} else if (currentRole === 'fallback') {
providerName = getFallbackProvider(effectiveProjectRoot);
modelId = getFallbackModelId(effectiveProjectRoot);
} else {
log(
"error",
'error',
`Unknown role encountered in _unifiedServiceRunner: ${currentRole}`
);
lastError =
@@ -360,7 +360,7 @@ async function _unifiedServiceRunner(serviceType, params) {
if (!providerName || !modelId) {
log(
"warn",
'warn',
`Skipping role '${currentRole}': Provider or Model ID not configured.`
);
lastError =
@@ -375,7 +375,7 @@ async function _unifiedServiceRunner(serviceType, params) {
provider = PROVIDERS[providerName?.toLowerCase()];
if (!provider) {
log(
"warn",
'warn',
`Skipping role '${currentRole}': Provider '${providerName}' not supported.`
);
lastError =
@@ -385,10 +385,10 @@ async function _unifiedServiceRunner(serviceType, params) {
}
// Check API key if needed
if (providerName?.toLowerCase() !== "ollama") {
if (providerName?.toLowerCase() !== 'ollama') {
if (!isApiKeySet(providerName, session, effectiveProjectRoot)) {
log(
"warn",
'warn',
`Skipping role '${currentRole}' (Provider: ${providerName}): API key not set or invalid.`
);
lastError =
@@ -404,17 +404,17 @@ async function _unifiedServiceRunner(serviceType, params) {
baseURL = getBaseUrlForRole(currentRole, effectiveProjectRoot);
// For Azure, use the global Azure base URL if role-specific URL is not configured
if (providerName?.toLowerCase() === "azure" && !baseURL) {
if (providerName?.toLowerCase() === 'azure' && !baseURL) {
baseURL = getAzureBaseURL(effectiveProjectRoot);
log("debug", `Using global Azure base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === "ollama" && !baseURL) {
log('debug', `Using global Azure base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === 'ollama' && !baseURL) {
// For Ollama, use the global Ollama base URL if role-specific URL is not configured
baseURL = getOllamaBaseURL(effectiveProjectRoot);
log("debug", `Using global Ollama base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === "bedrock" && !baseURL) {
log('debug', `Using global Ollama base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) {
// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
baseURL = getBedrockBaseURL(effectiveProjectRoot);
log("debug", `Using global Bedrock base URL: ${baseURL}`);
log('debug', `Using global Bedrock base URL: ${baseURL}`);
}
// Get AI parameters for the current role
@@ -429,12 +429,12 @@ async function _unifiedServiceRunner(serviceType, params) {
let providerSpecificParams = {};
// Handle Vertex AI specific configuration
if (providerName?.toLowerCase() === "vertex") {
if (providerName?.toLowerCase() === 'vertex') {
// Get Vertex project ID and location
const projectId =
getVertexProjectId(effectiveProjectRoot) ||
resolveEnvVariable(
"VERTEX_PROJECT_ID",
'VERTEX_PROJECT_ID',
session,
effectiveProjectRoot
);
@@ -442,15 +442,15 @@ async function _unifiedServiceRunner(serviceType, params) {
const location =
getVertexLocation(effectiveProjectRoot) ||
resolveEnvVariable(
"VERTEX_LOCATION",
'VERTEX_LOCATION',
session,
effectiveProjectRoot
) ||
"us-central1";
'us-central1';
// Get credentials path if available
const credentialsPath = resolveEnvVariable(
"GOOGLE_APPLICATION_CREDENTIALS",
'GOOGLE_APPLICATION_CREDENTIALS',
session,
effectiveProjectRoot
);
@@ -459,18 +459,18 @@ async function _unifiedServiceRunner(serviceType, params) {
providerSpecificParams = {
projectId,
location,
...(credentialsPath && { credentials: { credentialsFromEnv: true } }),
...(credentialsPath && { credentials: { credentialsFromEnv: true } })
};
log(
"debug",
'debug',
`Using Vertex AI configuration: Project ID=${projectId}, Location=${location}`
);
}
const messages = [];
if (systemPrompt) {
messages.push({ role: "system", content: systemPrompt });
messages.push({ role: 'system', content: systemPrompt });
}
// IN THE FUTURE WHEN DOING CONTEXT IMPROVEMENTS
@@ -492,9 +492,9 @@ async function _unifiedServiceRunner(serviceType, params) {
// }
if (prompt) {
messages.push({ role: "user", content: prompt });
messages.push({ role: 'user', content: prompt });
} else {
throw new Error("User prompt content is missing.");
throw new Error('User prompt content is missing.');
}
const callParams = {
@@ -504,9 +504,9 @@ async function _unifiedServiceRunner(serviceType, params) {
temperature: roleParams.temperature,
messages,
...(baseURL && { baseURL }),
...(serviceType === "generateObject" && { schema, objectName }),
...(serviceType === 'generateObject' && { schema, objectName }),
...providerSpecificParams,
...restApiParams,
...restApiParams
};
providerResponse = await _attemptProviderCallWithRetries(
@@ -527,7 +527,7 @@ async function _unifiedServiceRunner(serviceType, params) {
modelId,
inputTokens: providerResponse.usage.inputTokens,
outputTokens: providerResponse.usage.outputTokens,
outputType,
outputType
});
} catch (telemetryError) {
// logAiUsage already logs its own errors and returns null on failure
@@ -535,21 +535,21 @@ async function _unifiedServiceRunner(serviceType, params) {
}
} else if (userId && providerResponse && !providerResponse.usage) {
log(
"warn",
'warn',
`Cannot log telemetry for ${commandName} (${providerName}/${modelId}): AI result missing 'usage' data. (May be expected for streams)`
);
}
let finalMainResult;
if (serviceType === "generateText") {
if (serviceType === 'generateText') {
finalMainResult = providerResponse.text;
} else if (serviceType === "generateObject") {
} else if (serviceType === 'generateObject') {
finalMainResult = providerResponse.object;
} else if (serviceType === "streamText") {
} else if (serviceType === 'streamText') {
finalMainResult = providerResponse;
} else {
log(
"error",
'error',
`Unknown serviceType in _unifiedServiceRunner: ${serviceType}`
);
finalMainResult = providerResponse;
@@ -557,38 +557,38 @@ async function _unifiedServiceRunner(serviceType, params) {
return {
mainResult: finalMainResult,
telemetryData: telemetryData,
telemetryData: telemetryData
};
} catch (error) {
const cleanMessage = _extractErrorMessage(error);
log(
"error",
`Service call failed for role ${currentRole} (Provider: ${providerName || "unknown"}, Model: ${modelId || "unknown"}): ${cleanMessage}`
'error',
`Service call failed for role ${currentRole} (Provider: ${providerName || 'unknown'}, Model: ${modelId || 'unknown'}): ${cleanMessage}`
);
lastError = error;
lastCleanErrorMessage = cleanMessage;
if (serviceType === "generateObject") {
if (serviceType === 'generateObject') {
const lowerCaseMessage = cleanMessage.toLowerCase();
if (
lowerCaseMessage.includes(
"no endpoints found that support tool use"
'no endpoints found that support tool use'
) ||
lowerCaseMessage.includes("does not support tool_use") ||
lowerCaseMessage.includes("tool use is not supported") ||
lowerCaseMessage.includes("tools are not supported") ||
lowerCaseMessage.includes("function calling is not supported") ||
lowerCaseMessage.includes("tool use is not supported")
lowerCaseMessage.includes('does not support tool_use') ||
lowerCaseMessage.includes('tool use is not supported') ||
lowerCaseMessage.includes('tools are not supported') ||
lowerCaseMessage.includes('function calling is not supported') ||
lowerCaseMessage.includes('tool use is not supported')
) {
const specificErrorMsg = `Model '${modelId || "unknown"}' via provider '${providerName || "unknown"}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
log("error", `[Tool Support Error] ${specificErrorMsg}`);
const specificErrorMsg = `Model '${modelId || 'unknown'}' via provider '${providerName || 'unknown'}' does not support the 'tool use' required by generateObjectService. Please configure a model that supports tool/function calling for the '${currentRole}' role, or use generateTextService if structured output is not strictly required.`;
log('error', `[Tool Support Error] ${specificErrorMsg}`);
throw new Error(specificErrorMsg);
}
}
}
}
log("error", `All roles in the sequence [${sequence.join(", ")}] failed.`);
log('error', `All roles in the sequence [${sequence.join(', ')}] failed.`);
throw new Error(lastCleanErrorMessage);
}
@@ -608,10 +608,10 @@ async function _unifiedServiceRunner(serviceType, params) {
*/
async function generateTextService(params) {
// Ensure default outputType if not provided
const defaults = { outputType: "cli" };
const defaults = { outputType: 'cli' };
const combinedParams = { ...defaults, ...params };
// TODO: Validate commandName exists?
return _unifiedServiceRunner("generateText", combinedParams);
return _unifiedServiceRunner('generateText', combinedParams);
}
/**
@@ -629,13 +629,13 @@ async function generateTextService(params) {
* @returns {Promise<object>} Result object containing the stream and usage data.
*/
async function streamTextService(params) {
const defaults = { outputType: "cli" };
const defaults = { outputType: 'cli' };
const combinedParams = { ...defaults, ...params };
// TODO: Validate commandName exists?
// NOTE: Telemetry for streaming might be tricky as usage data often comes at the end.
// The current implementation logs *after* the stream is returned.
// We might need to adjust how usage is captured/logged for streams.
return _unifiedServiceRunner("streamText", combinedParams);
return _unifiedServiceRunner('streamText', combinedParams);
}
/**
@@ -657,13 +657,13 @@ async function streamTextService(params) {
*/
async function generateObjectService(params) {
const defaults = {
objectName: "generated_object",
objectName: 'generated_object',
maxRetries: 3,
outputType: "cli",
outputType: 'cli'
};
const combinedParams = { ...defaults, ...params };
// TODO: Validate commandName exists?
return _unifiedServiceRunner("generateObject", combinedParams);
return _unifiedServiceRunner('generateObject', combinedParams);
}
// --- Telemetry Function ---
@@ -685,10 +685,10 @@ async function logAiUsage({
modelId,
inputTokens,
outputTokens,
outputType,
outputType
}) {
try {
const isMCP = outputType === "mcp";
const isMCP = outputType === 'mcp';
const timestamp = new Date().toISOString();
const totalTokens = (inputTokens || 0) + (outputTokens || 0);
@@ -712,19 +712,19 @@ async function logAiUsage({
outputTokens: outputTokens || 0,
totalTokens,
totalCost: parseFloat(totalCost.toFixed(6)),
currency, // Add currency to the telemetry data
currency // Add currency to the telemetry data
};
if (getDebugFlag()) {
log("info", "AI Usage Telemetry:", telemetryData);
log('info', 'AI Usage Telemetry:', telemetryData);
}
// TODO (Subtask 77.2): Send telemetryData securely to the external endpoint.
return telemetryData;
} catch (error) {
log("error", `Failed to log AI usage telemetry: ${error.message}`, {
error,
log('error', `Failed to log AI usage telemetry: ${error.message}`, {
error
});
// Don't re-throw; telemetry failure shouldn't block core functionality.
return null;
@@ -735,5 +735,5 @@ export {
generateTextService,
streamTextService,
generateObjectService,
logAiUsage,
logAiUsage
};

View File

@@ -3,9 +3,9 @@
* Manages task dependencies and relationships
*/
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import {
log,
@@ -14,12 +14,12 @@ import {
taskExists,
formatTaskId,
findCycles,
isSilentMode,
} from "./utils.js";
isSilentMode
} from './utils.js';
import { displayBanner } from "./ui.js";
import { displayBanner } from './ui.js';
import { generateTaskFiles } from "./task-manager.js";
import { generateTaskFiles } from './task-manager.js';
/**
* Add a dependency to a task
@@ -28,17 +28,17 @@ import { generateTaskFiles } from "./task-manager.js";
* @param {number|string} dependencyId - ID of the task to add as dependency
*/
async function addDependency(tasksPath, taskId, dependencyId) {
log("info", `Adding dependency ${dependencyId} to task ${taskId}...`);
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log("error", "No valid tasks found in tasks.json");
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId =
typeof taskId === "string" && taskId.includes(".")
typeof taskId === 'string' && taskId.includes('.')
? taskId
: parseInt(taskId, 10);
@@ -47,7 +47,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Check if the dependency task or subtask actually exists
if (!taskExists(data.tasks, formattedDependencyId)) {
log(
"error",
'error',
`Dependency target ${formattedDependencyId} does not exist in tasks.json`
);
process.exit(1);
@@ -57,20 +57,20 @@ async function addDependency(tasksPath, taskId, dependencyId) {
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId
.split(".")
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
log("error", `Parent task ${parentId} not found.`);
log('error', `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log("error", `Parent task ${parentId} has no subtasks.`);
log('error', `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
@@ -78,7 +78,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
isSubtask = true;
if (!targetTask) {
log("error", `Subtask ${formattedTaskId} not found.`);
log('error', `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
@@ -86,7 +86,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) {
log("error", `Task ${formattedTaskId} not found.`);
log('error', `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
@@ -104,7 +104,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
})
) {
log(
"warn",
'warn',
`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
);
return;
@@ -112,7 +112,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
if (String(formattedTaskId) === String(formattedDependencyId)) {
log("error", `Task ${formattedTaskId} cannot depend on itself.`);
log('error', `Task ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
@@ -121,30 +121,30 @@ async function addDependency(tasksPath, taskId, dependencyId) {
let isSelfDependency = false;
if (
typeof formattedTaskId === "string" &&
typeof formattedDependencyId === "string" &&
formattedTaskId.includes(".") &&
formattedDependencyId.includes(".")
typeof formattedTaskId === 'string' &&
typeof formattedDependencyId === 'string' &&
formattedTaskId.includes('.') &&
formattedDependencyId.includes('.')
) {
const [taskParentId] = formattedTaskId.split(".");
const [depParentId] = formattedDependencyId.split(".");
const [taskParentId] = formattedTaskId.split('.');
const [depParentId] = formattedDependencyId.split('.');
// Only treat it as a self-dependency if both the parent ID and subtask ID are identical
isSelfDependency = formattedTaskId === formattedDependencyId;
// Log for debugging
log(
"debug",
'debug',
`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
);
log(
"debug",
'debug',
`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
);
}
if (isSelfDependency) {
log("error", `Subtask ${formattedTaskId} cannot depend on itself.`);
log('error', `Subtask ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
@@ -158,13 +158,13 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Sort dependencies numerically or by parent task ID first, then subtask ID
targetTask.dependencies.sort((a, b) => {
if (typeof a === "number" && typeof b === "number") {
if (typeof a === 'number' && typeof b === 'number') {
return a - b;
} else if (typeof a === "string" && typeof b === "string") {
const [aParent, aChild] = a.split(".").map(Number);
const [bParent, bChild] = b.split(".").map(Number);
} else if (typeof a === 'string' && typeof b === 'string') {
const [aParent, aChild] = a.split('.').map(Number);
const [bParent, bChild] = b.split('.').map(Number);
return aParent !== bParent ? aParent - bParent : aChild - bChild;
} else if (typeof a === "number") {
} else if (typeof a === 'number') {
return -1; // Numbers come before strings
} else {
return 1; // Strings come after numbers
@@ -174,7 +174,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Save changes
writeJSON(tasksPath, data);
log(
"success",
'success',
`Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
);
@@ -186,9 +186,9 @@ async function addDependency(tasksPath, taskId, dependencyId) {
`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
@@ -197,10 +197,10 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Generate updated task files
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
log("info", "Task files regenerated with updated dependencies.");
log('info', 'Task files regenerated with updated dependencies.');
} else {
log(
"error",
'error',
`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
);
process.exit(1);
@@ -214,18 +214,18 @@ async function addDependency(tasksPath, taskId, dependencyId) {
* @param {number|string} dependencyId - ID of the task to remove as dependency
*/
async function removeDependency(tasksPath, taskId, dependencyId) {
log("info", `Removing dependency ${dependencyId} from task ${taskId}...`);
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);
// Read tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log("error", "No valid tasks found.");
log('error', 'No valid tasks found.');
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId =
typeof taskId === "string" && taskId.includes(".")
typeof taskId === 'string' && taskId.includes('.')
? taskId
: parseInt(taskId, 10);
@@ -235,20 +235,20 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId
.split(".")
.split('.')
.map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) {
log("error", `Parent task ${parentId} not found.`);
log('error', `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log("error", `Parent task ${parentId} has no subtasks.`);
log('error', `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
@@ -256,7 +256,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
isSubtask = true;
if (!targetTask) {
log("error", `Subtask ${formattedTaskId} not found.`);
log('error', `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
@@ -264,7 +264,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) {
log("error", `Task ${formattedTaskId} not found.`);
log('error', `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
@@ -272,7 +272,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
// Check if the task has any dependencies
if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
log(
"info",
'info',
`Task ${formattedTaskId} has no dependencies, nothing to remove.`
);
return;
@@ -287,10 +287,10 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
let depStr = String(dep);
// Special handling for numeric IDs that might be subtask references
if (typeof dep === "number" && dep < 100 && isSubtask) {
if (typeof dep === 'number' && dep < 100 && isSubtask) {
// It's likely a reference to another subtask in the same parent task
// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
const [parentId] = formattedTaskId.split(".");
const [parentId] = formattedTaskId.split('.');
depStr = `${parentId}.${dep}`;
}
@@ -299,7 +299,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
if (dependencyIndex === -1) {
log(
"info",
'info',
`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
);
return;
@@ -313,7 +313,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
// Success message
log(
"success",
'success',
`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
);
@@ -325,9 +325,9 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
@@ -358,8 +358,8 @@ function isCircularDependency(tasks, taskId, chain = []) {
let parentIdForSubtask = null;
// Check if this is a subtask reference (e.g., "1.2")
if (taskIdStr.includes(".")) {
const [parentId, subtaskId] = taskIdStr.split(".").map(Number);
if (taskIdStr.includes('.')) {
const [parentId, subtaskId] = taskIdStr.split('.').map(Number);
const parentTask = tasks.find((t) => t.id === parentId);
parentIdForSubtask = parentId; // Store parent ID if it's a subtask
@@ -385,7 +385,7 @@ function isCircularDependency(tasks, taskId, chain = []) {
return task.dependencies.some((depId) => {
let normalizedDepId = String(depId);
// Normalize relative subtask dependencies
if (typeof depId === "number" && parentIdForSubtask !== null) {
if (typeof depId === 'number' && parentIdForSubtask !== null) {
// If the current task is a subtask AND the dependency is a number,
// assume it refers to a sibling subtask.
normalizedDepId = `${parentIdForSubtask}.${depId}`;
@@ -413,9 +413,9 @@ function validateTaskDependencies(tasks) {
// Check for self-dependencies
if (String(depId) === String(task.id)) {
issues.push({
type: "self",
type: 'self',
taskId: task.id,
message: `Task ${task.id} depends on itself`,
message: `Task ${task.id} depends on itself`
});
return;
}
@@ -423,10 +423,10 @@ function validateTaskDependencies(tasks) {
// Check if dependency exists
if (!taskExists(tasks, depId)) {
issues.push({
type: "missing",
type: 'missing',
taskId: task.id,
dependencyId: depId,
message: `Task ${task.id} depends on non-existent task ${depId}`,
message: `Task ${task.id} depends on non-existent task ${depId}`
});
}
});
@@ -434,9 +434,9 @@ function validateTaskDependencies(tasks) {
// Check for circular dependencies
if (isCircularDependency(tasks, task.id)) {
issues.push({
type: "circular",
type: 'circular',
taskId: task.id,
message: `Task ${task.id} is part of a circular dependency chain`,
message: `Task ${task.id} is part of a circular dependency chain`
});
}
@@ -454,12 +454,12 @@ function validateTaskDependencies(tasks) {
// Check for self-dependencies in subtasks
if (
String(depId) === String(fullSubtaskId) ||
(typeof depId === "number" && depId === subtask.id)
(typeof depId === 'number' && depId === subtask.id)
) {
issues.push({
type: "self",
type: 'self',
taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} depends on itself`,
message: `Subtask ${fullSubtaskId} depends on itself`
});
return;
}
@@ -467,10 +467,10 @@ function validateTaskDependencies(tasks) {
// Check if dependency exists
if (!taskExists(tasks, depId)) {
issues.push({
type: "missing",
type: 'missing',
taskId: fullSubtaskId,
dependencyId: depId,
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`,
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`
});
}
});
@@ -478,9 +478,9 @@ function validateTaskDependencies(tasks) {
// Check for circular dependencies in subtasks
if (isCircularDependency(tasks, fullSubtaskId)) {
issues.push({
type: "circular",
type: 'circular',
taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`,
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`
});
}
});
@@ -489,7 +489,7 @@ function validateTaskDependencies(tasks) {
return {
valid: issues.length === 0,
issues,
issues
};
}
@@ -508,13 +508,13 @@ function removeDuplicateDependencies(tasksData) {
const uniqueDeps = [...new Set(task.dependencies)];
return {
...task,
dependencies: uniqueDeps,
dependencies: uniqueDeps
};
});
return {
...tasksData,
tasks,
tasks
};
}
@@ -554,7 +554,7 @@ function cleanupSubtaskDependencies(tasksData) {
return {
...tasksData,
tasks,
tasks
};
}
@@ -563,12 +563,12 @@ function cleanupSubtaskDependencies(tasksData) {
* @param {string} tasksPath - Path to tasks.json
*/
async function validateDependenciesCommand(tasksPath, options = {}) {
log("info", "Checking for invalid dependencies in task files...");
log('info', 'Checking for invalid dependencies in task files...');
// Read tasks data
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log("error", "No valid tasks found in tasks.json");
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
@@ -582,7 +582,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
});
log(
"info",
'info',
`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
);
@@ -592,7 +592,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
if (!validationResult.valid) {
log(
"error",
'error',
`Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
);
validationResult.issues.forEach((issue) => {
@@ -600,7 +600,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
if (issue.dependencyId) {
errorMsg += ` (Dependency: ${issue.dependencyId})`;
}
log("error", errorMsg); // Log each issue as an error
log('error', errorMsg); // Log each issue as an error
});
// Optionally exit if validation fails, depending on desired behavior
@@ -611,22 +611,22 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
console.log(
boxen(
chalk.red(`Dependency Validation FAILED\n\n`) +
`${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
`${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
`${chalk.red("Issues found:")} ${validationResult.issues.length}`, // Display count from result
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result
{
padding: 1,
borderColor: "red",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
borderColor: 'red',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
} else {
log(
"success",
"No invalid dependencies found - all dependencies are valid"
'success',
'No invalid dependencies found - all dependencies are valid'
);
// Show validation summary - only if not in silent mode
@@ -634,21 +634,21 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
console.log(
boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
`${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
`${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
}
} catch (error) {
log("error", "Error validating dependencies:", error);
log('error', 'Error validating dependencies:', error);
process.exit(1);
}
}
@@ -686,13 +686,13 @@ function countAllDependencies(tasks) {
* @param {Object} options - Options object
*/
async function fixDependenciesCommand(tasksPath, options = {}) {
log("info", "Checking for and fixing invalid dependencies in tasks.json...");
log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
try {
// Read tasks data
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log("error", "No valid tasks found in tasks.json");
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
@@ -706,7 +706,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
duplicateDependenciesRemoved: 0,
circularDependenciesFixed: 0,
tasksFixed: 0,
subtasksFixed: 0,
subtasksFixed: 0
};
// First phase: Remove duplicate dependencies in tasks
@@ -718,7 +718,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log(
"info",
'info',
`Removing duplicate dependency from task ${task.id}: ${depId}`
);
stats.duplicateDependenciesRemoved++;
@@ -740,12 +740,12 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const originalLength = subtask.dependencies.length;
subtask.dependencies = subtask.dependencies.filter((depId) => {
let depIdStr = String(depId);
if (typeof depId === "number" && depId < 100) {
if (typeof depId === 'number' && depId < 100) {
depIdStr = `${task.id}.${depId}`;
}
if (uniqueDeps.has(depIdStr)) {
log(
"info",
'info',
`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
);
stats.duplicateDependenciesRemoved++;
@@ -778,13 +778,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (task.dependencies && Array.isArray(task.dependencies)) {
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter((depId) => {
const isSubtask = typeof depId === "string" && depId.includes(".");
const isSubtask = typeof depId === 'string' && depId.includes('.');
if (isSubtask) {
// Check if the subtask exists
if (!validSubtaskIds.has(depId)) {
log(
"info",
'info',
`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
);
stats.nonExistentDependenciesRemoved++;
@@ -794,10 +794,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
} else {
// Check if the task exists
const numericId =
typeof depId === "string" ? parseInt(depId, 10) : depId;
typeof depId === 'string' ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) {
log(
"info",
'info',
`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
);
stats.nonExistentDependenciesRemoved++;
@@ -821,9 +821,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// First check for self-dependencies
const hasSelfDependency = subtask.dependencies.some((depId) => {
if (typeof depId === "string" && depId.includes(".")) {
if (typeof depId === 'string' && depId.includes('.')) {
return depId === subtaskId;
} else if (typeof depId === "number" && depId < 100) {
} else if (typeof depId === 'number' && depId < 100) {
return depId === subtask.id;
}
return false;
@@ -832,13 +832,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (hasSelfDependency) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId =
typeof depId === "number" && depId < 100
typeof depId === 'number' && depId < 100
? `${task.id}.${depId}`
: String(depId);
if (normalizedDepId === subtaskId) {
log(
"info",
'info',
`Removing self-dependency from subtask ${subtaskId}`
);
stats.selfDependenciesRemoved++;
@@ -850,10 +850,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Then check for non-existent dependencies
subtask.dependencies = subtask.dependencies.filter((depId) => {
if (typeof depId === "string" && depId.includes(".")) {
if (typeof depId === 'string' && depId.includes('.')) {
if (!validSubtaskIds.has(depId)) {
log(
"info",
'info',
`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
);
stats.nonExistentDependenciesRemoved++;
@@ -864,7 +864,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Handle numeric dependencies
const numericId =
typeof depId === "number" ? depId : parseInt(depId, 10);
typeof depId === 'number' ? depId : parseInt(depId, 10);
// Small numbers likely refer to subtasks in the same task
if (numericId < 100) {
@@ -872,7 +872,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (!validSubtaskIds.has(fullSubtaskId)) {
log(
"info",
'info',
`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
);
stats.nonExistentDependenciesRemoved++;
@@ -885,7 +885,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Otherwise it's a task reference
if (!validTaskIds.has(numericId)) {
log(
"info",
'info',
`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
);
stats.nonExistentDependenciesRemoved++;
@@ -904,7 +904,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
});
// Third phase: Check for circular dependencies
log("info", "Checking for circular dependencies...");
log('info', 'Checking for circular dependencies...');
// Build the dependency map for subtasks
const subtaskDependencyMap = new Map();
@@ -915,9 +915,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const normalizedDeps = subtask.dependencies.map((depId) => {
if (typeof depId === "string" && depId.includes(".")) {
if (typeof depId === 'string' && depId.includes('.')) {
return depId;
} else if (typeof depId === "number" && depId < 100) {
} else if (typeof depId === 'number' && depId < 100) {
return `${task.id}.${depId}`;
}
return String(depId);
@@ -945,7 +945,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (cycleEdges.length > 0) {
const [taskId, subtaskNum] = subtaskId
.split(".")
.split('.')
.map((part) => Number(part));
const task = data.tasks.find((t) => t.id === taskId);
@@ -956,9 +956,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const originalLength = subtask.dependencies.length;
const edgesToRemove = cycleEdges.map((edge) => {
if (edge.includes(".")) {
if (edge.includes('.')) {
const [depTaskId, depSubtaskId] = edge
.split(".")
.split('.')
.map((part) => Number(part));
if (depTaskId === taskId) {
@@ -973,7 +973,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId =
typeof depId === "number" && depId < 100
typeof depId === 'number' && depId < 100
? `${taskId}.${depId}`
: String(depId);
@@ -982,7 +982,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
edgesToRemove.includes(normalizedDepId)
) {
log(
"info",
'info',
`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
);
stats.circularDependenciesFixed++;
@@ -1005,13 +1005,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (dataChanged) {
// Save the changes
writeJSON(tasksPath, data);
log("success", "Fixed dependency issues in tasks.json");
log('success', 'Fixed dependency issues in tasks.json');
// Regenerate task files
log("info", "Regenerating task files to reflect dependency changes...");
log('info', 'Regenerating task files to reflect dependency changes...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
} else {
log("info", "No changes needed to fix dependencies");
log('info', 'No changes needed to fix dependencies');
}
// Show detailed statistics report
@@ -1023,48 +1023,48 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (!isSilentMode()) {
if (totalFixedAll > 0) {
log("success", `Fixed ${totalFixedAll} dependency issues in total!`);
log('success', `Fixed ${totalFixedAll} dependency issues in total!`);
console.log(
boxen(
chalk.green(`Dependency Fixes Summary:\n\n`) +
`${chalk.cyan("Invalid dependencies removed:")} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan("Self-dependencies removed:")} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan("Duplicate dependencies removed:")} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan("Circular dependencies fixed:")} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan("Tasks fixed:")} ${stats.tasksFixed}\n` +
`${chalk.cyan("Subtasks fixed:")} ${stats.subtasksFixed}\n`,
`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
} else {
log(
"success",
"No dependency issues found - all dependencies are valid"
'success',
'No dependency issues found - all dependencies are valid'
);
console.log(
boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan("Tasks checked:")} ${data.tasks.length}\n` +
`${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
}
)
);
}
}
} catch (error) {
log("error", "Error in fix-dependencies command:", error);
log('error', 'Error in fix-dependencies command:', error);
process.exit(1);
}
}
@@ -1103,7 +1103,7 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
if (task.subtasks.length > 0) {
const firstSubtask = task.subtasks[0];
log(
"debug",
'debug',
`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
);
firstSubtask.dependencies = [];
@@ -1124,11 +1124,11 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
*/
function validateAndFixDependencies(tasksData, tasksPath = null) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
log("error", "Invalid tasks data");
log('error', 'Invalid tasks data');
return false;
}
log("debug", "Validating and fixing dependencies...");
log('debug', 'Validating and fixing dependencies...');
// Create a deep copy for comparison
const originalData = JSON.parse(JSON.stringify(tasksData));
@@ -1174,7 +1174,7 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
if (subtask.dependencies) {
subtask.dependencies = subtask.dependencies.filter((depId) => {
// Handle numeric subtask references
if (typeof depId === "number" && depId < 100) {
if (typeof depId === 'number' && depId < 100) {
const fullSubtaskId = `${task.id}.${depId}`;
return taskExists(tasksData.tasks, fullSubtaskId);
}
@@ -1210,9 +1210,9 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
if (tasksPath && changesDetected) {
try {
writeJSON(tasksPath, tasksData);
log("debug", "Saved dependency fixes to tasks.json");
log('debug', 'Saved dependency fixes to tasks.json');
} catch (error) {
log("error", "Failed to save dependency fixes to tasks.json", error);
log('error', 'Failed to save dependency fixes to tasks.json', error);
}
}
@@ -1229,5 +1229,5 @@ export {
removeDuplicateDependencies,
cleanupSubtaskDependencies,
ensureAtLeastOneIndependentSubtask,
validateAndFixDependencies,
validateAndFixDependencies
};

View File

@@ -1,9 +1,9 @@
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import { z } from "zod";
import Fuse from "fuse.js"; // Import Fuse.js for advanced fuzzy search
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { z } from 'zod';
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search
import {
displayBanner,
@@ -12,31 +12,31 @@ import {
stopLoadingIndicator,
succeedLoadingIndicator,
failLoadingIndicator,
displayAiUsageSummary,
} from "../ui.js";
import { readJSON, writeJSON, log as consoleLog, truncate } from "../utils.js";
import { generateObjectService } from "../ai-services-unified.js";
import { getDefaultPriority } from "../config-manager.js";
import generateTaskFiles from "./generate-task-files.js";
displayAiUsageSummary
} from '../ui.js';
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js';
import { generateObjectService } from '../ai-services-unified.js';
import { getDefaultPriority } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
// Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({
title: z.string().describe("Clear, concise title for the task"),
title: z.string().describe('Clear, concise title for the task'),
description: z
.string()
.describe("A one or two sentence description of the task"),
.describe('A one or two sentence description of the task'),
details: z
.string()
.describe("In-depth implementation details, considerations, and guidance"),
.describe('In-depth implementation details, considerations, and guidance'),
testStrategy: z
.string()
.describe("Detailed approach for verifying task completion"),
.describe('Detailed approach for verifying task completion'),
dependencies: z
.array(z.number())
.optional()
.describe(
"Array of task IDs that this task depends on (must be completed before this task can start)"
),
'Array of task IDs that this task depends on (must be completed before this task can start)'
)
});
/**
@@ -64,7 +64,7 @@ async function addTask(
dependencies = [],
priority = null,
context = {},
outputFormat = "text", // Default to text for CLI
outputFormat = 'text', // Default to text for CLI
manualTaskData = null,
useResearch = false
) {
@@ -76,27 +76,27 @@ async function addTask(
? mcpLog // Use MCP logger if provided
: {
// Create a wrapper around consoleLog for CLI
info: (...args) => consoleLog("info", ...args),
warn: (...args) => consoleLog("warn", ...args),
error: (...args) => consoleLog("error", ...args),
debug: (...args) => consoleLog("debug", ...args),
success: (...args) => consoleLog("success", ...args),
info: (...args) => consoleLog('info', ...args),
warn: (...args) => consoleLog('warn', ...args),
error: (...args) => consoleLog('error', ...args),
debug: (...args) => consoleLog('debug', ...args),
success: (...args) => consoleLog('success', ...args)
};
const effectivePriority = priority || getDefaultPriority(projectRoot);
logFn.info(
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(", ") || "None"}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
);
let loadingIndicator = null;
let aiServiceResponse = null; // To store the full response from AI service
// Create custom reporter that checks for MCP log
const report = (message, level = "info") => {
const report = (message, level = 'info') => {
if (mcpLog) {
mcpLog[level](message);
} else if (outputFormat === "text") {
} else if (outputFormat === 'text') {
consoleLog(level, message);
}
};
@@ -158,7 +158,7 @@ async function addTask(
title: task.title,
description: task.description,
status: task.status,
dependencies: dependencyData,
dependencies: dependencyData
};
}
@@ -168,14 +168,14 @@ async function addTask(
// If tasks.json doesn't exist or is invalid, create a new one
if (!data || !data.tasks) {
report("tasks.json not found or invalid. Creating a new one.", "info");
report('tasks.json not found or invalid. Creating a new one.', 'info');
// Create default tasks data structure
data = {
tasks: [],
tasks: []
};
// Ensure the directory exists and write the new file
writeJSON(tasksPath, data);
report("Created new tasks.json file with empty tasks array.", "info");
report('Created new tasks.json file with empty tasks array.', 'info');
}
// Find the highest task ID to determine the next ID
@@ -184,13 +184,13 @@ async function addTask(
const newTaskId = highestId + 1;
// Only show UI box for CLI mode
if (outputFormat === "text") {
if (outputFormat === 'text') {
console.log(
boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
padding: 1,
borderColor: "blue",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
})
);
}
@@ -204,10 +204,10 @@ async function addTask(
if (invalidDeps.length > 0) {
report(
`The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`,
"warn"
`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`,
'warn'
);
report("Removing invalid dependencies...", "info");
report('Removing invalid dependencies...', 'info');
dependencies = dependencies.filter(
(depId) => !invalidDeps.includes(depId)
);
@@ -242,28 +242,28 @@ async function addTask(
// Check if manual task data is provided
if (manualTaskData) {
report("Using manually provided task data", "info");
report('Using manually provided task data', 'info');
taskData = manualTaskData;
report("DEBUG: Taking MANUAL task data path.", "debug");
report('DEBUG: Taking MANUAL task data path.', 'debug');
// Basic validation for manual data
if (
!taskData.title ||
typeof taskData.title !== "string" ||
typeof taskData.title !== 'string' ||
!taskData.description ||
typeof taskData.description !== "string"
typeof taskData.description !== 'string'
) {
throw new Error(
"Manual task data must include at least a title and description."
'Manual task data must include at least a title and description.'
);
}
} else {
report("DEBUG: Taking AI task generation path.", "debug");
report('DEBUG: Taking AI task generation path.', 'debug');
// --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, "info");
report(`Generating task data with AI with prompt:\n${prompt}`, 'info');
// Create context string for task creation prompt
let contextTasks = "";
let contextTasks = '';
// Create a dependency map for better understanding of the task relationships
const taskMap = {};
@@ -274,18 +274,18 @@ async function addTask(
title: t.title,
description: t.description,
dependencies: t.dependencies || [],
status: t.status,
status: t.status
};
});
// CLI-only feedback for the dependency analysis
if (outputFormat === "text") {
if (outputFormat === 'text') {
console.log(
boxen(chalk.cyan.bold("Task Context Analysis"), {
boxen(chalk.cyan.bold('Task Context Analysis'), {
padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0 },
borderColor: "cyan",
borderStyle: "round",
borderColor: 'cyan',
borderStyle: 'round'
})
);
}
@@ -316,7 +316,7 @@ async function addTask(
const directDeps = data.tasks.filter((t) =>
numericDependencies.includes(t.id)
);
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join("\n")}`;
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`;
// Add an overview of indirect dependencies if present
const indirectDeps = dependentTasks.filter(
@@ -327,7 +327,7 @@ async function addTask(
contextTasks += `\n${indirectDeps
.slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join("\n")}`;
.join('\n')}`;
if (indirectDeps.length > 5) {
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
}
@@ -338,15 +338,15 @@ async function addTask(
for (const depTask of uniqueDetailedTasks) {
const depthInfo = depthMap.get(depTask.id)
? ` (depth: ${depthMap.get(depTask.id)})`
: "";
: '';
const isDirect = numericDependencies.includes(depTask.id)
? " [DIRECT DEPENDENCY]"
: "";
? ' [DIRECT DEPENDENCY]'
: '';
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
contextTasks += `Description: ${depTask.description}\n`;
contextTasks += `Status: ${depTask.status || "pending"}\n`;
contextTasks += `Priority: ${depTask.priority || "medium"}\n`;
contextTasks += `Status: ${depTask.status || 'pending'}\n`;
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`;
// List its dependencies
if (depTask.dependencies && depTask.dependencies.length > 0) {
@@ -356,7 +356,7 @@ async function addTask(
? `Task ${dId}: ${depDepTask.title}`
: `Task ${dId}`;
});
contextTasks += `Dependencies: ${depDeps.join(", ")}\n`;
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`;
} else {
contextTasks += `Dependencies: None\n`;
}
@@ -365,7 +365,7 @@ async function addTask(
if (depTask.details) {
const truncatedDetails =
depTask.details.length > 400
? depTask.details.substring(0, 400) + "... (truncated)"
? depTask.details.substring(0, 400) + '... (truncated)'
: depTask.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
@@ -373,19 +373,19 @@ async function addTask(
// Add dependency chain visualization
if (dependencyGraphs.length > 0) {
contextTasks += "\n\nDependency Chain Visualization:";
contextTasks += '\n\nDependency Chain Visualization:';
// Helper function to format dependency chain as text
function formatDependencyChain(
node,
prefix = "",
prefix = '',
isLast = true,
depth = 0
) {
if (depth > 3) return ""; // Limit depth to avoid excessive nesting
if (depth > 3) return ''; // Limit depth to avoid excessive nesting
const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? " " : "";
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '';
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
@@ -411,7 +411,7 @@ async function addTask(
}
// Show dependency analysis in CLI mode
if (outputFormat === "text") {
if (outputFormat === 'text') {
if (directDeps.length > 0) {
console.log(chalk.gray(` Explicitly specified dependencies:`));
directDeps.forEach((t) => {
@@ -451,14 +451,14 @@ async function addTask(
// Convert dependency graph to ASCII art for terminal
function visualizeDependencyGraph(
node,
prefix = "",
prefix = '',
isLast = true,
depth = 0
) {
if (depth > 2) return; // Limit depth for display
const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? " " : "";
const connector = isLast ? '└── ' : '├── ';
const childPrefix = isLast ? ' ' : '';
console.log(
chalk.blue(
@@ -494,18 +494,18 @@ async function addTask(
includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [
{ name: "title", weight: 1.5 }, // Title is most important
{ name: "description", weight: 2 }, // Description is very important
{ name: "details", weight: 3 }, // Details is most important
{ name: 'title', weight: 1.5 }, // Title is most important
{ name: 'description', weight: 2 }, // Description is very important
{ name: 'details', weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things
{ name: "dependencyTitles", weight: 0.5 },
{ name: 'dependencyTitles', weight: 0.5 }
],
// Sort matches by score (lower is better)
shouldSort: true,
// Allow searching in nested properties
useExtendedSearch: true,
// Return up to 50 matches
limit: 50,
limit: 50
};
// Prepare task data with dependencies expanded as titles for better semantic search
@@ -516,15 +516,15 @@ async function addTask(
? task.dependencies
.map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId);
return depTask ? depTask.title : "";
return depTask ? depTask.title : '';
})
.filter((title) => title)
.join(" ")
: "";
.join(' ')
: '';
return {
...task,
dependencyTitles,
dependencyTitles
};
});
@@ -534,7 +534,7 @@ async function addTask(
// Extract significant words and phrases from the prompt
const promptWords = prompt
.toLowerCase()
.replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces
.split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars
@@ -602,21 +602,21 @@ async function addTask(
if (relatedTasks.length > 0) {
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
.map((t, i) => {
const relevanceMarker = i < highRelevance.length ? "" : "";
const relevanceMarker = i < highRelevance.length ? '' : '';
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
})
.join("\n")}`;
.join('\n')}`;
}
if (
recentTasks.length > 0 &&
!contextTasks.includes("Recently created tasks")
!contextTasks.includes('Recently created tasks')
) {
contextTasks += `\n\nRecently created tasks:\n${recentTasks
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join("\n")}`;
.join('\n')}`;
}
// Add detailed information about the most relevant tasks
@@ -630,8 +630,8 @@ async function addTask(
for (const task of uniqueDetailedTasks) {
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
contextTasks += `Description: ${task.description}\n`;
contextTasks += `Status: ${task.status || "pending"}\n`;
contextTasks += `Priority: ${task.priority || "medium"}\n`;
contextTasks += `Status: ${task.status || 'pending'}\n`;
contextTasks += `Priority: ${task.priority || 'medium'}\n`;
if (task.dependencies && task.dependencies.length > 0) {
// Format dependency list with titles
const depList = task.dependencies.map((depId) => {
@@ -640,13 +640,13 @@ async function addTask(
? `Task ${depId} (${depTask.title})`
: `Task ${depId}`;
});
contextTasks += `Dependencies: ${depList.join(", ")}\n`;
contextTasks += `Dependencies: ${depList.join(', ')}\n`;
}
// Add implementation details but truncate if too long
if (task.details) {
const truncatedDetails =
task.details.length > 400
? task.details.substring(0, 400) + "... (truncated)"
? task.details.substring(0, 400) + '... (truncated)'
: task.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`;
}
@@ -654,7 +654,7 @@ async function addTask(
}
// Add a concise view of the task dependency structure
contextTasks += "\n\nSummary of task dependencies in the project:";
contextTasks += '\n\nSummary of task dependencies in the project:';
// Get pending/in-progress tasks that might be most relevant based on fuzzy search
// Prioritize tasks from our similarity search
@@ -662,7 +662,7 @@ async function addTask(
const relevantPendingTasks = data.tasks
.filter(
(t) =>
(t.status === "pending" || t.status === "in-progress") &&
(t.status === 'pending' || t.status === 'in-progress') &&
// Either in our relevant set OR has relevant words in title/description
(relevantTaskIds.has(t.id) ||
promptWords.some(
@@ -676,8 +676,8 @@ async function addTask(
for (const task of relevantPendingTasks) {
const depsStr =
task.dependencies && task.dependencies.length > 0
? task.dependencies.join(", ")
: "None";
? task.dependencies.join(', ')
: 'None';
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
}
@@ -709,7 +709,7 @@ async function addTask(
.slice(0, 10);
if (commonDeps.length > 0) {
contextTasks += "\nMost common dependencies for similar tasks:";
contextTasks += '\nMost common dependencies for similar tasks:';
commonDeps.forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) {
@@ -720,7 +720,7 @@ async function addTask(
}
// Show fuzzy search analysis in CLI mode
if (outputFormat === "text") {
if (outputFormat === 'text') {
console.log(
chalk.gray(
` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
@@ -777,7 +777,7 @@ async function addTask(
const isHighRelevance = highRelevance.some(
(ht) => ht.id === t.id
);
const relevanceIndicator = isHighRelevance ? "" : "";
const relevanceIndicator = isHighRelevance ? '' : '';
console.log(
chalk.cyan(
`${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
@@ -805,14 +805,14 @@ async function addTask(
}
// Add a visual transition to show we're moving to AI generation - only for CLI
if (outputFormat === "text") {
if (outputFormat === 'text') {
console.log(
boxen(
chalk.white.bold("AI Task Generation") +
`\n\n${chalk.gray("Analyzing context and generating task details using AI...")}` +
`\n${chalk.cyan("Context size: ")}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan("Dependency detection: ")}${chalk.yellow(numericDependencies.length > 0 ? "Explicit dependencies" : "Auto-discovery mode")}` +
`\n${chalk.cyan("Detailed tasks: ")}${chalk.yellow(
chalk.white.bold('AI Task Generation') +
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` +
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` +
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow(
numericDependencies.length > 0
? dependentTasks.length // Use length of tasks from explicit dependency path
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
@@ -820,8 +820,8 @@ async function addTask(
{
padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 },
borderColor: "white",
borderStyle: "round",
borderColor: 'white',
borderStyle: 'round'
}
)
);
@@ -831,15 +831,15 @@ async function addTask(
// System Prompt - Enhanced for dependency awareness
const systemPrompt =
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
"When determining dependencies for a new task, follow these principles:\n" +
"1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
"2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
"3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n" +
"4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n" +
"5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n" +
'When determining dependencies for a new task, follow these principles:\n' +
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' +
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' +
'3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n' +
'4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n' +
'5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n' +
"6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" +
"7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n" +
"The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n";
'7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n' +
'The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n';
// Task Structure Description (for user prompt)
const taskStructureDesc = `
@@ -853,7 +853,7 @@ async function addTask(
`;
// Add any manually provided details to the prompt for context
let contextFromArgs = "";
let contextFromArgs = '';
if (manualTaskData?.title)
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
if (manualTaskData?.description)
@@ -867,7 +867,7 @@ async function addTask(
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
${contextTasks}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ""}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''}
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
@@ -878,15 +878,15 @@ async function addTask(
`;
// Start the loading indicator - only for text mode
if (outputFormat === "text") {
if (outputFormat === 'text') {
loadingIndicator = startLoadingIndicator(
`Generating new task with ${useResearch ? "Research" : "Main"} AI... \n`
`Generating new task with ${useResearch ? 'Research' : 'Main'} AI... \n`
);
}
try {
const serviceRole = useResearch ? "research" : "main";
report("DEBUG: Calling generateObjectService...", "debug");
const serviceRole = useResearch ? 'research' : 'main';
report('DEBUG: Calling generateObjectService...', 'debug');
aiServiceResponse = await generateObjectService({
// Capture the full response
@@ -894,17 +894,17 @@ async function addTask(
session: session,
projectRoot: projectRoot,
schema: AiTaskDataSchema,
objectName: "newTaskData",
objectName: 'newTaskData',
systemPrompt: systemPrompt,
prompt: userPrompt,
commandName: commandName || "add-task", // Use passed commandName or default
outputType: outputType || (isMCP ? "mcp" : "cli"), // Use passed outputType or derive
commandName: commandName || 'add-task', // Use passed commandName or default
outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive
});
report("DEBUG: generateObjectService returned successfully.", "debug");
report('DEBUG: generateObjectService returned successfully.', 'debug');
if (!aiServiceResponse || !aiServiceResponse.mainResult) {
throw new Error(
"AI service did not return the expected object structure."
'AI service did not return the expected object structure.'
);
}
@@ -921,33 +921,33 @@ async function addTask(
) {
taskData = aiServiceResponse.mainResult.object;
} else {
throw new Error("AI service did not return a valid task object.");
throw new Error('AI service did not return a valid task object.');
}
report("Successfully generated task data from AI.", "success");
report('Successfully generated task data from AI.', 'success');
// Success! Show checkmark
if (loadingIndicator) {
succeedLoadingIndicator(
loadingIndicator,
"Task generated successfully"
'Task generated successfully'
);
loadingIndicator = null; // Clear it
}
} catch (error) {
// Failure! Show X
if (loadingIndicator) {
failLoadingIndicator(loadingIndicator, "AI generation failed");
failLoadingIndicator(loadingIndicator, 'AI generation failed');
loadingIndicator = null;
}
report(
`DEBUG: generateObjectService caught error: ${error.message}`,
"debug"
'debug'
);
report(`Error generating task with AI: ${error.message}`, "error");
report(`Error generating task with AI: ${error.message}`, 'error');
throw error; // Re-throw error after logging
} finally {
report("DEBUG: generateObjectService finally block reached.", "debug");
report('DEBUG: generateObjectService finally block reached.', 'debug');
// Clean up if somehow still running
if (loadingIndicator) {
stopLoadingIndicator(loadingIndicator);
@@ -961,14 +961,14 @@ async function addTask(
id: newTaskId,
title: taskData.title,
description: taskData.description,
details: taskData.details || "",
testStrategy: taskData.testStrategy || "",
status: "pending",
details: taskData.details || '',
testStrategy: taskData.testStrategy || '',
status: 'pending',
dependencies: taskData.dependencies?.length
? taskData.dependencies
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
priority: effectivePriority,
subtasks: [], // Initialize with empty subtasks array
subtasks: [] // Initialize with empty subtasks array
};
// Additional check: validate all dependencies in the AI response
@@ -980,8 +980,8 @@ async function addTask(
if (!allValidDeps) {
report(
"AI suggested invalid dependencies. Filtering them out...",
"warn"
'AI suggested invalid dependencies. Filtering them out...',
'warn'
);
newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10);
@@ -993,48 +993,48 @@ async function addTask(
// Add the task to the tasks array
data.tasks.push(newTask);
report("DEBUG: Writing tasks.json...", "debug");
report('DEBUG: Writing tasks.json...', 'debug');
// Write the updated tasks to the file
writeJSON(tasksPath, data);
report("DEBUG: tasks.json written.", "debug");
report('DEBUG: tasks.json written.', 'debug');
// Generate markdown task files
report("Generating task files...", "info");
report("DEBUG: Calling generateTaskFiles...", "debug");
report('Generating task files...', 'info');
report('DEBUG: Calling generateTaskFiles...', 'debug');
// Pass mcpLog if available to generateTaskFiles
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
report("DEBUG: generateTaskFiles finished.", "debug");
report('DEBUG: generateTaskFiles finished.', 'debug');
// Show success message - only for text output (CLI)
if (outputFormat === "text") {
if (outputFormat === 'text') {
const table = new Table({
head: [
chalk.cyan.bold("ID"),
chalk.cyan.bold("Title"),
chalk.cyan.bold("Description"),
chalk.cyan.bold('ID'),
chalk.cyan.bold('Title'),
chalk.cyan.bold('Description')
],
colWidths: [5, 30, 50], // Adjust widths as needed
colWidths: [5, 30, 50] // Adjust widths as needed
});
table.push([
newTask.id,
truncate(newTask.title, 27),
truncate(newTask.description, 47),
truncate(newTask.description, 47)
]);
console.log(chalk.green("✓ New task created successfully:"));
console.log(chalk.green('✓ New task created successfully:'));
console.log(table.toString());
// Helper to get priority color
const getPriorityColor = (p) => {
switch (p?.toLowerCase()) {
case "high":
return "red";
case "low":
return "gray";
case "medium":
case 'high':
return 'red';
case 'low':
return 'gray';
case 'medium':
default:
return "yellow";
return 'yellow';
}
};
@@ -1058,49 +1058,49 @@ async function addTask(
});
// Prepare dependency display string
let dependencyDisplay = "";
let dependencyDisplay = '';
if (newTask.dependencies.length > 0) {
dependencyDisplay = chalk.white("Dependencies:") + "\n";
dependencyDisplay = chalk.white('Dependencies:') + '\n';
newTask.dependencies.forEach((dep) => {
const isAiAdded = aiAddedDeps.includes(dep);
const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : "";
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : '';
dependencyDisplay +=
chalk.white(
` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}`
) + "\n";
` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}`
) + '\n';
});
} else {
dependencyDisplay = chalk.white("Dependencies: None") + "\n";
dependencyDisplay = chalk.white('Dependencies: None') + '\n';
}
// Add info about removed dependencies if any
if (aiRemovedDeps.length > 0) {
dependencyDisplay +=
chalk.gray("\nUser-specified dependencies that were not used:") +
"\n";
chalk.gray('\nUser-specified dependencies that were not used:') +
'\n';
aiRemovedDeps.forEach((dep) => {
const depTask = data.tasks.find((t) => t.id === dep);
const title = depTask ? truncate(depTask.title, 30) : "Unknown task";
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + "\n";
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task';
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n';
});
}
// Add dependency analysis summary
let dependencyAnalysis = "";
let dependencyAnalysis = '';
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
dependencyAnalysis =
"\n" + chalk.white.bold("Dependency Analysis:") + "\n";
'\n' + chalk.white.bold('Dependency Analysis:') + '\n';
if (aiAddedDeps.length > 0) {
dependencyAnalysis +=
chalk.green(
`AI identified ${aiAddedDeps.length} additional dependencies`
) + "\n";
) + '\n';
}
if (aiRemovedDeps.length > 0) {
dependencyAnalysis +=
chalk.yellow(
`AI excluded ${aiRemovedDeps.length} user-provided dependencies`
) + "\n";
) + '\n';
}
}
@@ -1108,32 +1108,32 @@ async function addTask(
console.log(
boxen(
chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
"\n\n" +
'\n\n' +
chalk.white(`Title: ${newTask.title}`) +
"\n" +
'\n' +
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
"\n" +
'\n' +
chalk.white(
`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
) +
"\n\n" +
'\n\n' +
dependencyDisplay +
dependencyAnalysis +
"\n" +
chalk.white.bold("Next Steps:") +
"\n" +
'\n' +
chalk.white.bold('Next Steps:') +
'\n' +
chalk.cyan(
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
) +
"\n" +
'\n' +
chalk.cyan(
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
) +
"\n" +
'\n' +
chalk.cyan(
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
),
{ padding: 1, borderColor: "green", borderStyle: "round" }
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);
@@ -1141,19 +1141,19 @@ async function addTask(
if (
aiServiceResponse &&
aiServiceResponse.telemetryData &&
(outputType === "cli" || outputType === "text")
(outputType === 'cli' || outputType === 'text')
) {
displayAiUsageSummary(aiServiceResponse.telemetryData, "cli");
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
}
}
report(
`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
"debug"
'debug'
);
return {
newTaskId: newTaskId,
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null
};
} catch (error) {
// Stop any loading indicator on error
@@ -1161,8 +1161,8 @@ async function addTask(
stopLoadingIndicator(loadingIndicator);
}
report(`Error adding task: ${error.message}`, "error");
if (outputFormat === "text") {
report(`Error adding task: ${error.message}`, 'error');
if (outputFormat === 'text') {
console.error(chalk.red(`Error: ${error.message}`));
}
// In MCP mode, we let the direct function handler catch and format

View File

@@ -1,11 +1,11 @@
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import { log, readJSON, writeJSON, truncate, isSilentMode } from "../utils.js";
import { displayBanner } from "../ui.js";
import generateTaskFiles from "./generate-task-files.js";
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js';
import { displayBanner } from '../ui.js';
import generateTaskFiles from './generate-task-files.js';
/**
* Clear subtasks from specified tasks
@@ -13,58 +13,58 @@ import generateTaskFiles from "./generate-task-files.js";
* @param {string} taskIds - Task IDs to clear subtasks from
*/
function clearSubtasks(tasksPath, taskIds) {
log("info", `Reading tasks from ${tasksPath}...`);
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log("error", "No valid tasks found.");
log('error', 'No valid tasks found.');
process.exit(1);
}
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold("Clearing Subtasks"), {
boxen(chalk.white.bold('Clearing Subtasks'), {
padding: 1,
borderColor: "blue",
borderStyle: "round",
margin: { top: 1, bottom: 1 },
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 1, bottom: 1 }
})
);
}
// Handle multiple task IDs (comma-separated)
const taskIdArray = taskIds.split(",").map((id) => id.trim());
const taskIdArray = taskIds.split(',').map((id) => id.trim());
let clearedCount = 0;
// Create a summary table for the cleared subtasks
const summaryTable = new Table({
head: [
chalk.cyan.bold("Task ID"),
chalk.cyan.bold("Task Title"),
chalk.cyan.bold("Subtasks Cleared"),
chalk.cyan.bold('Task ID'),
chalk.cyan.bold('Task Title'),
chalk.cyan.bold('Subtasks Cleared')
],
colWidths: [10, 50, 20],
style: { head: [], border: [] },
style: { head: [], border: [] }
});
taskIdArray.forEach((taskId) => {
const id = parseInt(taskId, 10);
if (isNaN(id)) {
log("error", `Invalid task ID: ${taskId}`);
log('error', `Invalid task ID: ${taskId}`);
return;
}
const task = data.tasks.find((t) => t.id === id);
if (!task) {
log("error", `Task ${id} not found`);
log('error', `Task ${id} not found`);
return;
}
if (!task.subtasks || task.subtasks.length === 0) {
log("info", `Task ${id} has no subtasks to clear`);
log('info', `Task ${id} has no subtasks to clear`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.yellow("No subtasks"),
chalk.yellow('No subtasks')
]);
return;
}
@@ -72,12 +72,12 @@ function clearSubtasks(tasksPath, taskIds) {
const subtaskCount = task.subtasks.length;
task.subtasks = [];
clearedCount++;
log("info", `Cleared ${subtaskCount} subtasks from task ${id}`);
log('info', `Cleared ${subtaskCount} subtasks from task ${id}`);
summaryTable.push([
id.toString(),
truncate(task.title, 47),
chalk.green(`${subtaskCount} subtasks cleared`),
chalk.green(`${subtaskCount} subtasks cleared`)
]);
});
@@ -87,18 +87,18 @@ function clearSubtasks(tasksPath, taskIds) {
// Show summary table
if (!isSilentMode()) {
console.log(
boxen(chalk.white.bold("Subtask Clearing Summary:"), {
boxen(chalk.white.bold('Subtask Clearing Summary:'), {
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: "blue",
borderStyle: "round",
borderColor: 'blue',
borderStyle: 'round'
})
);
console.log(summaryTable.toString());
}
// Regenerate task files to reflect changes
log("info", "Regenerating task files...");
log('info', 'Regenerating task files...');
generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Success message
@@ -110,9 +110,9 @@ function clearSubtasks(tasksPath, taskIds) {
),
{
padding: 1,
borderColor: "green",
borderStyle: "round",
margin: { top: 1 },
borderColor: 'green',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
@@ -120,15 +120,15 @@ function clearSubtasks(tasksPath, taskIds) {
// Next steps suggestion
console.log(
boxen(
chalk.white.bold("Next Steps:") +
"\n\n" +
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --id=<id>")} to generate new subtasks\n` +
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master list --with-subtasks")} to verify changes`,
chalk.white.bold('Next Steps:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`,
{
padding: 1,
borderColor: "cyan",
borderStyle: "round",
margin: { top: 1 },
borderColor: 'cyan',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
@@ -136,11 +136,11 @@ function clearSubtasks(tasksPath, taskIds) {
} else {
if (!isSilentMode()) {
console.log(
boxen(chalk.yellow("No subtasks were cleared"), {
boxen(chalk.yellow('No subtasks were cleared'), {
padding: 1,
borderColor: "yellow",
borderStyle: "round",
margin: { top: 1 },
borderColor: 'yellow',
borderStyle: 'round',
margin: { top: 1 }
})
);
}

View File

@@ -1,23 +1,23 @@
import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import chalk from 'chalk';
import boxen from 'boxen';
import Table from 'cli-table3';
import {
log,
readJSON,
truncate,
readComplexityReport,
addComplexityToTask,
} from "../utils.js";
import findNextTask from "./find-next-task.js";
addComplexityToTask
} from '../utils.js';
import findNextTask from './find-next-task.js';
import {
displayBanner,
getStatusWithColor,
formatDependenciesWithStatus,
getComplexityWithColor,
createProgressBar,
} from "../ui.js";
createProgressBar
} from '../ui.js';
/**
* List all tasks
@@ -33,7 +33,7 @@ function listTasks(
statusFilter,
reportPath = null,
withSubtasks = false,
outputFormat = "text"
outputFormat = 'text'
) {
try {
const data = readJSON(tasksPath); // Reads the whole tasks.json
@@ -50,7 +50,7 @@ function listTasks(
// Filter tasks by status if specified
const filteredTasks =
statusFilter && statusFilter.toLowerCase() !== "all" // <-- Added check for 'all'
statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all'
? data.tasks.filter(
(task) =>
task.status &&
@@ -61,7 +61,7 @@ function listTasks(
// Calculate completion statistics
const totalTasks = data.tasks.length;
const completedTasks = data.tasks.filter(
(task) => task.status === "done" || task.status === "completed"
(task) => task.status === 'done' || task.status === 'completed'
).length;
const completionPercentage =
totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
@@ -69,19 +69,19 @@ function listTasks(
// Count statuses for tasks
const doneCount = completedTasks;
const inProgressCount = data.tasks.filter(
(task) => task.status === "in-progress"
(task) => task.status === 'in-progress'
).length;
const pendingCount = data.tasks.filter(
(task) => task.status === "pending"
(task) => task.status === 'pending'
).length;
const blockedCount = data.tasks.filter(
(task) => task.status === "blocked"
(task) => task.status === 'blocked'
).length;
const deferredCount = data.tasks.filter(
(task) => task.status === "deferred"
(task) => task.status === 'deferred'
).length;
const cancelledCount = data.tasks.filter(
(task) => task.status === "cancelled"
(task) => task.status === 'cancelled'
).length;
// Count subtasks and their statuses
@@ -97,22 +97,22 @@ function listTasks(
if (task.subtasks && task.subtasks.length > 0) {
totalSubtasks += task.subtasks.length;
completedSubtasks += task.subtasks.filter(
(st) => st.status === "done" || st.status === "completed"
(st) => st.status === 'done' || st.status === 'completed'
).length;
inProgressSubtasks += task.subtasks.filter(
(st) => st.status === "in-progress"
(st) => st.status === 'in-progress'
).length;
pendingSubtasks += task.subtasks.filter(
(st) => st.status === "pending"
(st) => st.status === 'pending'
).length;
blockedSubtasks += task.subtasks.filter(
(st) => st.status === "blocked"
(st) => st.status === 'blocked'
).length;
deferredSubtasks += task.subtasks.filter(
(st) => st.status === "deferred"
(st) => st.status === 'deferred'
).length;
cancelledSubtasks += task.subtasks.filter(
(st) => st.status === "cancelled"
(st) => st.status === 'cancelled'
).length;
}
});
@@ -121,7 +121,7 @@ function listTasks(
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
// For JSON output, return structured data
if (outputFormat === "json") {
if (outputFormat === 'json') {
// *** Modification: Remove 'details' field for JSON output ***
const tasksWithoutDetails = filteredTasks.map((task) => {
// <-- USES filteredTasks!
@@ -141,7 +141,7 @@ function listTasks(
return {
tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
filter: statusFilter || "all", // Return the actual filter used
filter: statusFilter || 'all', // Return the actual filter used
stats: {
total: totalTasks,
completed: doneCount,
@@ -159,9 +159,9 @@ function listTasks(
blocked: blockedSubtasks,
deferred: deferredSubtasks,
cancelled: cancelledSubtasks,
completionPercentage: subtaskCompletionPercentage,
},
},
completionPercentage: subtaskCompletionPercentage
}
}
};
}
@@ -169,22 +169,22 @@ function listTasks(
// Calculate status breakdowns as percentages of total
const taskStatusBreakdown = {
"in-progress": totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0
};
const subtaskStatusBreakdown = {
"in-progress":
'in-progress':
totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
deferred:
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
cancelled:
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0
};
// Create progress bars with status breakdowns
@@ -202,21 +202,21 @@ function listTasks(
// Calculate dependency statistics
const completedTaskIds = new Set(
data.tasks
.filter((t) => t.status === "done" || t.status === "completed")
.filter((t) => t.status === 'done' || t.status === 'completed')
.map((t) => t.id)
);
const tasksWithNoDeps = data.tasks.filter(
(t) =>
t.status !== "done" &&
t.status !== "completed" &&
t.status !== 'done' &&
t.status !== 'completed' &&
(!t.dependencies || t.dependencies.length === 0)
).length;
const tasksWithAllDepsSatisfied = data.tasks.filter(
(t) =>
t.status !== "done" &&
t.status !== "completed" &&
t.status !== 'done' &&
t.status !== 'completed' &&
t.dependencies &&
t.dependencies.length > 0 &&
t.dependencies.every((depId) => completedTaskIds.has(depId))
@@ -224,8 +224,8 @@ function listTasks(
const tasksWithUnsatisfiedDeps = data.tasks.filter(
(t) =>
t.status !== "done" &&
t.status !== "completed" &&
t.status !== 'done' &&
t.status !== 'completed' &&
t.dependencies &&
t.dependencies.length > 0 &&
!t.dependencies.every((depId) => completedTaskIds.has(depId))
@@ -278,7 +278,7 @@ function listTasks(
terminalWidth = process.stdout.columns;
} catch (e) {
// Fallback if columns cannot be determined
log("debug", "Could not determine terminal width, using default");
log('debug', 'Could not determine terminal width, using default');
}
// Ensure we have a reasonable default if detection fails
terminalWidth = terminalWidth || 80;
@@ -288,35 +288,35 @@ function listTasks(
// Create dashboard content
const projectDashboardContent =
chalk.white.bold("Project Dashboard") +
"\n" +
chalk.white.bold('Project Dashboard') +
'\n' +
`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
`Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
chalk.cyan.bold("Priority Breakdown:") +
"\n" +
`${chalk.red("•")} ${chalk.white("High priority:")} ${data.tasks.filter((t) => t.priority === "high").length}\n` +
`${chalk.yellow("•")} ${chalk.white("Medium priority:")} ${data.tasks.filter((t) => t.priority === "medium").length}\n` +
`${chalk.green("•")} ${chalk.white("Low priority:")} ${data.tasks.filter((t) => t.priority === "low").length}`;
chalk.cyan.bold('Priority Breakdown:') +
'\n' +
`${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` +
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` +
`${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`;
const dependencyDashboardContent =
chalk.white.bold("Dependency Status & Next Task") +
"\n" +
chalk.cyan.bold("Dependency Metrics:") +
"\n" +
`${chalk.green("•")} ${chalk.white("Tasks with no dependencies:")} ${tasksWithNoDeps}\n` +
`${chalk.green("•")} ${chalk.white("Tasks ready to work on:")} ${tasksReadyToWork}\n` +
`${chalk.yellow("•")} ${chalk.white("Tasks blocked by dependencies:")} ${tasksWithUnsatisfiedDeps}\n` +
`${chalk.magenta("•")} ${chalk.white("Most depended-on task:")} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray("None")}\n` +
`${chalk.blue("•")} ${chalk.white("Avg dependencies per task:")} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
chalk.cyan.bold("Next Task to Work On:") +
"\n" +
`ID: ${chalk.cyan(nextItem ? nextItem.id : "N/A")} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow("No task available")}
chalk.white.bold('Dependency Status & Next Task') +
'\n' +
chalk.cyan.bold('Dependency Metrics:') +
'\n' +
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` +
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` +
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` +
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` +
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
chalk.cyan.bold('Next Task to Work On:') +
'\n' +
`ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')}
` +
`Priority: ${nextItem ? chalk.white(nextItem.priority || "medium") : ""} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ""}
`Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''}
` +
`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray("N/A")}`;
`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`;
// Calculate width for side-by-side display
// Box borders, padding take approximately 4 chars on each side
@@ -336,23 +336,23 @@ function listTasks(
// Create boxen options with precise widths
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: "blue",
borderStyle: "round",
borderColor: 'blue',
borderStyle: 'round',
width: boxContentWidth,
dimBorder: false,
dimBorder: false
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: "magenta",
borderStyle: "round",
borderColor: 'magenta',
borderStyle: 'round',
width: boxContentWidth,
dimBorder: false,
dimBorder: false
});
// Create a better side-by-side layout with exact spacing
const dashboardLines = dashboardBox.split("\n");
const dependencyLines = dependencyBox.split("\n");
const dashboardLines = dashboardBox.split('\n');
const dependencyLines = dependencyBox.split('\n');
// Make sure both boxes have the same height
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
@@ -362,35 +362,35 @@ function listTasks(
const combinedLines = [];
for (let i = 0; i < maxHeight; i++) {
// Get the dashboard line (or empty string if we've run out of lines)
const dashLine = i < dashboardLines.length ? dashboardLines[i] : "";
const dashLine = i < dashboardLines.length ? dashboardLines[i] : '';
// Get the dependency line (or empty string if we've run out of lines)
const depLine = i < dependencyLines.length ? dependencyLines[i] : "";
const depLine = i < dependencyLines.length ? dependencyLines[i] : '';
// Remove any trailing spaces from dashLine before padding to exact width
const trimmedDashLine = dashLine.trimEnd();
// Pad the dashboard line to exactly halfWidth chars with no extra spaces
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, " ");
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' ');
// Join the lines with no space in between
combinedLines.push(paddedDashLine + depLine);
}
// Join all lines and output
console.log(combinedLines.join("\n"));
console.log(combinedLines.join('\n'));
} else {
// Terminal too narrow, show boxes stacked vertically
const dashboardBox = boxen(projectDashboardContent, {
padding: 1,
borderColor: "blue",
borderStyle: "round",
margin: { top: 0, bottom: 1 },
borderColor: 'blue',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
});
const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1,
borderColor: "magenta",
borderStyle: "round",
margin: { top: 0, bottom: 1 },
borderColor: 'magenta',
borderStyle: 'round',
margin: { top: 0, bottom: 1 }
});
// Display stacked vertically
@@ -403,8 +403,8 @@ function listTasks(
boxen(
statusFilter
? chalk.yellow(`No tasks with status '${statusFilter}' found`)
: chalk.yellow("No tasks found"),
{ padding: 1, borderColor: "yellow", borderStyle: "round" }
: chalk.yellow('No tasks found'),
{ padding: 1, borderColor: 'yellow', borderStyle: 'round' }
)
);
return;
@@ -453,12 +453,12 @@ function listTasks(
// Create a table with correct borders and spacing
const table = new Table({
head: [
chalk.cyan.bold("ID"),
chalk.cyan.bold("Title"),
chalk.cyan.bold("Status"),
chalk.cyan.bold("Priority"),
chalk.cyan.bold("Dependencies"),
chalk.cyan.bold("Complexity"),
chalk.cyan.bold('ID'),
chalk.cyan.bold('Title'),
chalk.cyan.bold('Status'),
chalk.cyan.bold('Priority'),
chalk.cyan.bold('Dependencies'),
chalk.cyan.bold('Complexity')
],
colWidths: [
idWidth,
@@ -466,21 +466,21 @@ function listTasks(
statusWidth,
priorityWidth,
depsWidth,
complexityWidth, // Added complexity column width
complexityWidth // Added complexity column width
],
style: {
head: [], // No special styling for header
border: [], // No special styling for border
compact: false, // Use default spacing
compact: false // Use default spacing
},
wordWrap: true,
wrapOnWordBoundary: true,
wrapOnWordBoundary: true
});
// Process tasks for the table
filteredTasks.forEach((task) => {
// Format dependencies with status indicators (colored)
let depText = "None";
let depText = 'None';
if (task.dependencies && task.dependencies.length > 0) {
// Use the proper formatDependenciesWithStatus function for colored status
depText = formatDependenciesWithStatus(
@@ -490,19 +490,19 @@ function listTasks(
complexityReport
);
} else {
depText = chalk.gray("None");
depText = chalk.gray('None');
}
// Clean up any ANSI codes or confusing characters
const cleanTitle = task.title.replace(/\n/g, " ");
const cleanTitle = task.title.replace(/\n/g, ' ');
// Get priority color
const priorityColor =
{
high: chalk.red,
medium: chalk.yellow,
low: chalk.gray,
}[task.priority || "medium"] || chalk.white;
low: chalk.gray
}[task.priority || 'medium'] || chalk.white;
// Format status
const status = getStatusWithColor(task.status, true);
@@ -512,38 +512,38 @@ function listTasks(
task.id.toString(),
truncate(cleanTitle, titleWidth - 3),
status,
priorityColor(truncate(task.priority || "medium", priorityWidth - 2)),
priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)),
depText,
task.complexityScore
? getComplexityWithColor(task.complexityScore)
: chalk.gray("N/A"),
: chalk.gray('N/A')
]);
// Add subtasks if requested
if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
// Format subtask dependencies with status indicators
let subtaskDepText = "None";
let subtaskDepText = 'None';
if (subtask.dependencies && subtask.dependencies.length > 0) {
// Handle both subtask-to-subtask and subtask-to-task dependencies
const formattedDeps = subtask.dependencies
.map((depId) => {
// Check if it's a dependency on another subtask
if (typeof depId === "number" && depId < 100) {
if (typeof depId === 'number' && depId < 100) {
const foundSubtask = task.subtasks.find(
(st) => st.id === depId
);
if (foundSubtask) {
const isDone =
foundSubtask.status === "done" ||
foundSubtask.status === "completed";
const isInProgress = foundSubtask.status === "in-progress";
foundSubtask.status === 'done' ||
foundSubtask.status === 'completed';
const isInProgress = foundSubtask.status === 'in-progress';
// Use consistent color formatting instead of emojis
if (isDone) {
return chalk.green.bold(`${task.id}.${depId}`);
} else if (isInProgress) {
return chalk.hex("#FFA500").bold(`${task.id}.${depId}`);
return chalk.hex('#FFA500').bold(`${task.id}.${depId}`);
} else {
return chalk.red.bold(`${task.id}.${depId}`);
}
@@ -555,22 +555,22 @@ function listTasks(
// Add complexity to depTask before checking status
addComplexityToTask(depTask, complexityReport);
const isDone =
depTask.status === "done" || depTask.status === "completed";
const isInProgress = depTask.status === "in-progress";
depTask.status === 'done' || depTask.status === 'completed';
const isInProgress = depTask.status === 'in-progress';
// Use the same color scheme as in formatDependenciesWithStatus
if (isDone) {
return chalk.green.bold(`${depId}`);
} else if (isInProgress) {
return chalk.hex("#FFA500").bold(`${depId}`);
return chalk.hex('#FFA500').bold(`${depId}`);
} else {
return chalk.red.bold(`${depId}`);
}
}
return chalk.cyan(depId.toString());
})
.join(", ");
.join(', ');
subtaskDepText = formattedDeps || chalk.gray("None");
subtaskDepText = formattedDeps || chalk.gray('None');
}
// Add the subtask row without truncating dependencies
@@ -578,11 +578,11 @@ function listTasks(
`${task.id}.${subtask.id}`,
chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
getStatusWithColor(subtask.status, true),
chalk.dim("-"),
chalk.dim('-'),
subtaskDepText,
subtask.complexityScore
? chalk.gray(`${subtask.complexityScore}`)
: chalk.gray("N/A"),
: chalk.gray('N/A')
]);
});
}
@@ -592,12 +592,12 @@ function listTasks(
try {
console.log(table.toString());
} catch (err) {
log("error", `Error rendering table: ${err.message}`);
log('error', `Error rendering table: ${err.message}`);
// Fall back to simpler output
console.log(
chalk.yellow(
"\nFalling back to simple task list due to terminal width constraints:"
'\nFalling back to simple task list due to terminal width constraints:'
)
);
filteredTasks.forEach((task) => {
@@ -619,13 +619,13 @@ function listTasks(
const priorityColors = {
high: chalk.red.bold,
medium: chalk.yellow,
low: chalk.gray,
low: chalk.gray
};
// Show next task box in a prominent color
if (nextItem) {
// Prepare subtasks section if they exist (Only tasks have .subtasks property)
let subtasksSection = "";
let subtasksSection = '';
// Check if the nextItem is a top-level task before looking for subtasks
const parentTaskForSubtasks = data.tasks.find(
(t) => String(t.id) === String(nextItem.id)
@@ -635,75 +635,75 @@ function listTasks(
parentTaskForSubtasks.subtasks &&
parentTaskForSubtasks.subtasks.length > 0
) {
subtasksSection = `\n\n${chalk.white.bold("Subtasks:")}\n`;
subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`;
subtasksSection += parentTaskForSubtasks.subtasks
.map((subtask) => {
// Add complexity to subtask before display
addComplexityToTask(subtask, complexityReport);
// Using a more simplified format for subtask status display
const status = subtask.status || "pending";
const status = subtask.status || 'pending';
const statusColors = {
done: chalk.green,
completed: chalk.green,
pending: chalk.yellow,
"in-progress": chalk.blue,
'in-progress': chalk.blue,
deferred: chalk.gray,
blocked: chalk.red,
cancelled: chalk.gray,
cancelled: chalk.gray
};
const statusColor =
statusColors[status.toLowerCase()] || chalk.white;
// Ensure subtask ID is displayed correctly using parent ID from the original task object
return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
})
.join("\n");
.join('\n');
}
console.log(
boxen(
chalk.hex("#FF8800").bold(
chalk.hex('#FF8800').bold(
// Use nextItem.id and nextItem.title
`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
) +
"\n\n" +
'\n\n' +
// Use nextItem.priority, nextItem.status, nextItem.dependencies
`${chalk.white("Priority:")} ${priorityColors[nextItem.priority || "medium"](nextItem.priority || "medium")} ${chalk.white("Status:")} ${getStatusWithColor(nextItem.status, true)}\n` +
`${chalk.white("Dependencies:")} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray("None")}\n\n` +
`${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` +
`${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` +
// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
// *** Fetching original item for description and details ***
`${chalk.white("Description:")} ${getWorkItemDescription(nextItem, data.tasks)}` +
`${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` +
subtasksSection + // <-- Subtasks are handled above now
"\n\n" +
'\n\n' +
// Use nextItem.id
`${chalk.cyan("Start working:")} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
// Use nextItem.id
`${chalk.cyan("View details:")} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
{
padding: { left: 2, right: 2, top: 1, bottom: 1 },
borderColor: "#FF8800",
borderStyle: "round",
borderColor: '#FF8800',
borderStyle: 'round',
margin: { top: 1, bottom: 1 },
title: "⚡ RECOMMENDED NEXT TASK ⚡",
titleAlignment: "center",
title: '⚡ RECOMMENDED NEXT TASK ⚡',
titleAlignment: 'center',
width: terminalWidth - 4,
fullscreen: false,
fullscreen: false
}
)
);
} else {
console.log(
boxen(
chalk.hex("#FF8800").bold("No eligible next task found") +
"\n\n" +
"All pending tasks have dependencies that are not yet completed, or all tasks are done.",
chalk.hex('#FF8800').bold('No eligible next task found') +
'\n\n' +
'All pending tasks have dependencies that are not yet completed, or all tasks are done.',
{
padding: 1,
borderColor: "#FF8800",
borderStyle: "round",
borderColor: '#FF8800',
borderStyle: 'round',
margin: { top: 1, bottom: 1 },
title: "⚡ NEXT TASK ⚡",
titleAlignment: "center",
width: terminalWidth - 4, // Use full terminal width minus a small margin
title: '⚡ NEXT TASK ⚡',
titleAlignment: 'center',
width: terminalWidth - 4 // Use full terminal width minus a small margin
}
)
);
@@ -712,28 +712,28 @@ function listTasks(
// Show next steps
console.log(
boxen(
chalk.white.bold("Suggested Next Steps:") +
"\n\n" +
`${chalk.cyan("1.")} Run ${chalk.yellow("task-master next")} to see what to work on next\n` +
`${chalk.cyan("2.")} Run ${chalk.yellow("task-master expand --id=<id>")} to break down a task into subtasks\n` +
`${chalk.cyan("3.")} Run ${chalk.yellow("task-master set-status --id=<id> --status=done")} to mark a task as complete`,
chalk.white.bold('Suggested Next Steps:') +
'\n\n' +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`,
{
padding: 1,
borderColor: "gray",
borderStyle: "round",
margin: { top: 1 },
borderColor: 'gray',
borderStyle: 'round',
margin: { top: 1 }
}
)
);
} catch (error) {
log("error", `Error listing tasks: ${error.message}`);
log('error', `Error listing tasks: ${error.message}`);
if (outputFormat === "json") {
if (outputFormat === 'json') {
// Return structured error for JSON output
throw {
code: "TASK_LIST_ERROR",
code: 'TASK_LIST_ERROR',
message: error.message,
details: error.stack,
details: error.stack
};
}
@@ -744,18 +744,18 @@ function listTasks(
// *** Helper function to get description for task or subtask ***
function getWorkItemDescription(item, allTasks) {
if (!item) return "N/A";
if (!item) return 'N/A';
if (item.parentId) {
// It's a subtask
const parent = allTasks.find((t) => t.id === item.parentId);
const subtask = parent?.subtasks?.find(
(st) => `${parent.id}.${st.id}` === item.id
);
return subtask?.description || "No description available.";
return subtask?.description || 'No description available.';
} else {
// It's a top-level task
const task = allTasks.find((t) => String(t.id) === String(item.id));
return task?.description || "No description available.";
return task?.description || 'No description available.';
}
}

View File

@@ -3,8 +3,8 @@
* Core functionality for managing AI model configurations
*/
import https from "https";
import http from "http";
import https from 'https';
import http from 'http';
import {
getMainModelId,
getResearchModelId,
@@ -19,10 +19,10 @@ import {
writeConfig,
isConfigFilePresent,
getAllProviders,
getBaseUrlForRole,
} from "../config-manager.js";
import { findConfigPath } from "../../../src/utils/path-utils.js";
import { log } from "../utils.js";
getBaseUrlForRole
} from '../config-manager.js';
import { findConfigPath } from '../../../src/utils/path-utils.js';
import { log } from '../utils.js';
/**
* Fetches the list of models from OpenRouter API.
@@ -31,26 +31,26 @@ import { log } from "../utils.js";
function fetchOpenRouterModels() {
return new Promise((resolve) => {
const options = {
hostname: "openrouter.ai",
path: "/api/v1/models",
method: "GET",
hostname: 'openrouter.ai',
path: '/api/v1/models',
method: 'GET',
headers: {
Accept: "application/json",
},
Accept: 'application/json'
}
};
const req = https.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on("end", () => {
res.on('end', () => {
if (res.statusCode === 200) {
try {
const parsedData = JSON.parse(data);
resolve(parsedData.data || []); // Return the array of models
} catch (e) {
console.error("Error parsing OpenRouter response:", e);
console.error('Error parsing OpenRouter response:', e);
resolve(null); // Indicate failure
}
} else {
@@ -62,8 +62,8 @@ function fetchOpenRouterModels() {
});
});
req.on("error", (e) => {
console.error("Error fetching OpenRouter models:", e);
req.on('error', (e) => {
console.error('Error fetching OpenRouter models:', e);
resolve(null); // Indicate failure
});
req.end();
@@ -75,14 +75,14 @@ function fetchOpenRouterModels() {
* @param {string} baseURL - The base URL for the Ollama API (e.g., "http://localhost:11434/api")
* @returns {Promise<Array|null>} A promise that resolves with the list of model objects or null if fetch fails.
*/
function fetchOllamaModels(baseURL = "http://localhost:11434/api") {
function fetchOllamaModels(baseURL = 'http://localhost:11434/api') {
return new Promise((resolve) => {
try {
// Parse the base URL to extract hostname, port, and base path
const url = new URL(baseURL);
const isHttps = url.protocol === "https:";
const isHttps = url.protocol === 'https:';
const port = url.port || (isHttps ? 443 : 80);
const basePath = url.pathname.endsWith("/")
const basePath = url.pathname.endsWith('/')
? url.pathname.slice(0, -1)
: url.pathname;
@@ -90,25 +90,25 @@ function fetchOllamaModels(baseURL = "http://localhost:11434/api") {
hostname: url.hostname,
port: parseInt(port, 10),
path: `${basePath}/tags`,
method: "GET",
method: 'GET',
headers: {
Accept: "application/json",
},
Accept: 'application/json'
}
};
const requestLib = isHttps ? https : http;
const req = requestLib.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on("end", () => {
res.on('end', () => {
if (res.statusCode === 200) {
try {
const parsedData = JSON.parse(data);
resolve(parsedData.models || []); // Return the array of models
} catch (e) {
console.error("Error parsing Ollama response:", e);
console.error('Error parsing Ollama response:', e);
resolve(null); // Indicate failure
}
} else {
@@ -120,13 +120,13 @@ function fetchOllamaModels(baseURL = "http://localhost:11434/api") {
});
});
req.on("error", (e) => {
console.error("Error fetching Ollama models:", e);
req.on('error', (e) => {
console.error('Error fetching Ollama models:', e);
resolve(null); // Indicate failure
});
req.end();
} catch (e) {
console.error("Error parsing Ollama base URL:", e);
console.error('Error parsing Ollama base URL:', e);
resolve(null); // Indicate failure
}
});
@@ -144,13 +144,13 @@ async function getModelConfiguration(options = {}) {
const { mcpLog, projectRoot, session } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === "function") {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
if (!projectRoot) {
throw new Error("Project root is required but not found.");
throw new Error('Project root is required but not found.');
}
// Use centralized config path finding instead of hardcoded path
@@ -158,11 +158,11 @@ async function getModelConfiguration(options = {}) {
const configExists = isConfigFilePresent(projectRoot);
log(
"debug",
'debug',
`Checking for config file using findConfigPath, found: ${configPath}`
);
log(
"debug",
'debug',
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
);
@@ -221,8 +221,8 @@ async function getModelConfiguration(options = {}) {
cost: mainModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: mainCliKeyOk,
mcp: mainMcpKeyOk,
},
mcp: mainMcpKeyOk
}
},
research: {
provider: researchProvider,
@@ -231,8 +231,8 @@ async function getModelConfiguration(options = {}) {
cost: researchModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: researchCliKeyOk,
mcp: researchMcpKeyOk,
},
mcp: researchMcpKeyOk
}
},
fallback: fallbackProvider
? {
@@ -242,22 +242,22 @@ async function getModelConfiguration(options = {}) {
cost: fallbackModelData?.cost_per_1m_tokens || null,
keyStatus: {
cli: fallbackCliKeyOk,
mcp: fallbackMcpKeyOk,
},
mcp: fallbackMcpKeyOk
}
: null,
},
message: "Successfully retrieved current model configuration",
}
: null
},
message: 'Successfully retrieved current model configuration'
}
};
} catch (error) {
report("error", `Error getting model configuration: ${error.message}`);
report('error', `Error getting model configuration: ${error.message}`);
return {
success: false,
error: {
code: "CONFIG_ERROR",
message: error.message,
},
code: 'CONFIG_ERROR',
message: error.message
}
};
}
}
@@ -274,13 +274,13 @@ async function getAvailableModelsList(options = {}) {
const { mcpLog, projectRoot } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === "function") {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
if (!projectRoot) {
throw new Error("Project root is required but not found.");
throw new Error('Project root is required but not found.');
}
// Use centralized config path finding instead of hardcoded path
@@ -288,11 +288,11 @@ async function getAvailableModelsList(options = {}) {
const configExists = isConfigFilePresent(projectRoot);
log(
"debug",
'debug',
`Checking for config file using findConfigPath, found: ${configPath}`
);
log(
"debug",
'debug',
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
);
@@ -311,8 +311,8 @@ async function getAvailableModelsList(options = {}) {
success: true,
data: {
models: [],
message: "No available models found",
},
message: 'No available models found'
}
};
}
@@ -326,28 +326,28 @@ async function getAvailableModelsList(options = {}) {
Boolean
);
const otherAvailableModels = allAvailableModels.map((model) => ({
provider: model.provider || "N/A",
provider: model.provider || 'N/A',
modelId: model.id,
sweScore: model.swe_score || null,
cost: model.cost_per_1m_tokens || null,
allowedRoles: model.allowed_roles || [],
allowedRoles: model.allowed_roles || []
}));
return {
success: true,
data: {
models: otherAvailableModels,
message: `Successfully retrieved ${otherAvailableModels.length} available models`,
},
message: `Successfully retrieved ${otherAvailableModels.length} available models`
}
};
} catch (error) {
report("error", `Error getting available models: ${error.message}`);
report('error', `Error getting available models: ${error.message}`);
return {
success: false,
error: {
code: "MODELS_LIST_ERROR",
message: error.message,
},
code: 'MODELS_LIST_ERROR',
message: error.message
}
};
}
}
@@ -367,13 +367,13 @@ async function setModel(role, modelId, options = {}) {
const { mcpLog, projectRoot, providerHint } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === "function") {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
if (!projectRoot) {
throw new Error("Project root is required but not found.");
throw new Error('Project root is required but not found.');
}
// Use centralized config path finding instead of hardcoded path
@@ -381,11 +381,11 @@ async function setModel(role, modelId, options = {}) {
const configExists = isConfigFilePresent(projectRoot);
log(
"debug",
'debug',
`Checking for config file using findConfigPath, found: ${configPath}`
);
log(
"debug",
'debug',
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
);
@@ -396,24 +396,24 @@ async function setModel(role, modelId, options = {}) {
}
// Validate role
if (!["main", "research", "fallback"].includes(role)) {
if (!['main', 'research', 'fallback'].includes(role)) {
return {
success: false,
error: {
code: "INVALID_ROLE",
message: `Invalid role: ${role}. Must be one of: main, research, fallback.`,
},
code: 'INVALID_ROLE',
message: `Invalid role: ${role}. Must be one of: main, research, fallback.`
}
};
}
// Validate model ID
if (typeof modelId !== "string" || modelId.trim() === "") {
if (typeof modelId !== 'string' || modelId.trim() === '') {
return {
success: false,
error: {
code: "INVALID_MODEL_ID",
message: `Invalid model ID: ${modelId}. Must be a non-empty string.`,
},
code: 'INVALID_MODEL_ID',
message: `Invalid model ID: ${modelId}. Must be a non-empty string.`
}
};
}
@@ -434,40 +434,40 @@ async function setModel(role, modelId, options = {}) {
// Found internally AND provider matches the hint
determinedProvider = providerHint;
report(
"info",
'info',
`Model ${modelId} found internally with matching provider hint ${determinedProvider}.`
);
} else {
// Either not found internally, OR found but under a DIFFERENT provider than hinted.
// Proceed with custom logic based ONLY on the hint.
if (providerHint === "openrouter") {
if (providerHint === 'openrouter') {
// Check OpenRouter ONLY because hint was openrouter
report("info", `Checking OpenRouter for ${modelId} (as hinted)...`);
report('info', `Checking OpenRouter for ${modelId} (as hinted)...`);
const openRouterModels = await fetchOpenRouterModels();
if (
openRouterModels &&
openRouterModels.some((m) => m.id === modelId)
) {
determinedProvider = "openrouter";
determinedProvider = 'openrouter';
// Check if this is a free model (ends with :free)
if (modelId.endsWith(":free")) {
warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(":free", "")}' for full functionality.`;
if (modelId.endsWith(':free')) {
warningMessage = `Warning: OpenRouter free model '${modelId}' selected. Free models have significant limitations including lower context windows, reduced rate limits, and may not support advanced features like tool_use. Consider using the paid version '${modelId.replace(':free', '')}' for full functionality.`;
} else {
warningMessage = `Warning: Custom OpenRouter model '${modelId}' set. This model is not officially validated by Taskmaster and may not function as expected.`;
}
report("warn", warningMessage);
report('warn', warningMessage);
} else {
// Hinted as OpenRouter but not found in live check
throw new Error(
`Model ID "${modelId}" not found in the live OpenRouter model list. Please verify the ID and ensure it's available on OpenRouter.`
);
}
} else if (providerHint === "ollama") {
} else if (providerHint === 'ollama') {
// Check Ollama ONLY because hint was ollama
report("info", `Checking Ollama for ${modelId} (as hinted)...`);
report('info', `Checking Ollama for ${modelId} (as hinted)...`);
// Get the Ollama base URL from config
const ollamaBaseURL = getBaseUrlForRole(role, projectRoot);
@@ -479,9 +479,9 @@ async function setModel(role, modelId, options = {}) {
`Unable to connect to Ollama server at ${ollamaBaseURL}. Please ensure Ollama is running and try again.`
);
} else if (ollamaModels.some((m) => m.model === modelId)) {
determinedProvider = "ollama";
determinedProvider = 'ollama';
warningMessage = `Warning: Custom Ollama model '${modelId}' set. Ensure your Ollama server is running and has pulled this model. Taskmaster cannot guarantee compatibility.`;
report("warn", warningMessage);
report('warn', warningMessage);
} else {
// Server is running but model not found
const tagsUrl = `${ollamaBaseURL}/tags`;
@@ -489,11 +489,11 @@ async function setModel(role, modelId, options = {}) {
`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
);
}
} else if (providerHint === "bedrock") {
} else if (providerHint === 'bedrock') {
// Set provider without model validation since Bedrock models are managed by AWS
determinedProvider = "bedrock";
determinedProvider = 'bedrock';
warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
report("warn", warningMessage);
report('warn', warningMessage);
} else {
// Invalid provider hint - should not happen
throw new Error(`Invalid provider hint received: ${providerHint}`);
@@ -505,7 +505,7 @@ async function setModel(role, modelId, options = {}) {
// Found internally, use the provider from the internal list
determinedProvider = modelData.provider;
report(
"info",
'info',
`Model ${modelId} found internally with provider ${determinedProvider}.`
);
} else {
@@ -513,9 +513,9 @@ async function setModel(role, modelId, options = {}) {
return {
success: false,
error: {
code: "MODEL_NOT_FOUND_NO_HINT",
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.`,
},
code: 'MODEL_NOT_FOUND_NO_HINT',
message: `Model ID "${modelId}" not found in Taskmaster's supported models. If this is a custom model, please specify the provider using --openrouter or --ollama.`
}
};
}
}
@@ -528,9 +528,9 @@ async function setModel(role, modelId, options = {}) {
return {
success: false,
error: {
code: "PROVIDER_UNDETERMINED",
message: `Could not determine the provider for model ID "${modelId}".`,
},
code: 'PROVIDER_UNDETERMINED',
message: `Could not determine the provider for model ID "${modelId}".`
}
};
}
@@ -538,7 +538,7 @@ async function setModel(role, modelId, options = {}) {
currentConfig.models[role] = {
...currentConfig.models[role], // Keep existing params like maxTokens
provider: determinedProvider,
modelId: modelId,
modelId: modelId
};
// Write updated configuration
@@ -547,14 +547,14 @@ async function setModel(role, modelId, options = {}) {
return {
success: false,
error: {
code: "CONFIG_WRITE_ERROR",
message: "Error writing updated configuration to configuration file",
},
code: 'CONFIG_WRITE_ERROR',
message: 'Error writing updated configuration to configuration file'
}
};
}
const successMessage = `Successfully set ${role} model to ${modelId} (Provider: ${determinedProvider})`;
report("info", successMessage);
report('info', successMessage);
return {
success: true,
@@ -563,17 +563,17 @@ async function setModel(role, modelId, options = {}) {
provider: determinedProvider,
modelId,
message: successMessage,
warning: warningMessage, // Include warning in the response data
},
warning: warningMessage // Include warning in the response data
}
};
} catch (error) {
report("error", `Error setting ${role} model: ${error.message}`);
report('error', `Error setting ${role} model: ${error.message}`);
return {
success: false,
error: {
code: "SET_MODEL_ERROR",
message: error.message,
},
code: 'SET_MODEL_ERROR',
message: error.message
}
};
}
}
@@ -589,7 +589,7 @@ async function setModel(role, modelId, options = {}) {
async function getApiKeyStatusReport(options = {}) {
const { mcpLog, projectRoot, session } = options;
const report = (level, ...args) => {
if (mcpLog && typeof mcpLog[level] === "function") {
if (mcpLog && typeof mcpLog[level] === 'function') {
mcpLog[level](...args);
}
};
@@ -597,7 +597,7 @@ async function getApiKeyStatusReport(options = {}) {
try {
const providers = getAllProviders();
const providersToCheck = providers.filter(
(p) => p.toLowerCase() !== "ollama"
(p) => p.toLowerCase() !== 'ollama'
); // Ollama is not a provider, it's a service, doesn't need an api key usually
const statusReport = providersToCheck.map((provider) => {
// Use provided projectRoot for MCP status check
@@ -606,26 +606,26 @@ async function getApiKeyStatusReport(options = {}) {
return {
provider,
cli: cliOk,
mcp: mcpOk,
mcp: mcpOk
};
});
report("info", "Successfully generated API key status report.");
report('info', 'Successfully generated API key status report.');
return {
success: true,
data: {
report: statusReport,
message: "API key status report generated.",
},
message: 'API key status report generated.'
}
};
} catch (error) {
report("error", `Error generating API key status report: ${error.message}`);
report('error', `Error generating API key status report: ${error.message}`);
return {
success: false,
error: {
code: "API_KEY_STATUS_ERROR",
message: error.message,
},
code: 'API_KEY_STATUS_ERROR',
message: error.message
}
};
}
}
@@ -634,5 +634,5 @@ export {
getModelConfiguration,
getAvailableModelsList,
setModel,
getApiKeyStatusReport,
getApiKeyStatusReport
};

View File

@@ -1,17 +1,17 @@
import path from "path";
import chalk from "chalk";
import boxen from "boxen";
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import { log, readJSON, writeJSON, findTaskById } from "../utils.js";
import { displayBanner } from "../ui.js";
import { validateTaskDependencies } from "../dependency-manager.js";
import { getDebugFlag } from "../config-manager.js";
import updateSingleTaskStatus from "./update-single-task-status.js";
import generateTaskFiles from "./generate-task-files.js";
import { log, readJSON, writeJSON, findTaskById } from '../utils.js';
import { displayBanner } from '../ui.js';
import { validateTaskDependencies } from '../dependency-manager.js';
import { getDebugFlag } from '../config-manager.js';
import updateSingleTaskStatus from './update-single-task-status.js';
import generateTaskFiles from './generate-task-files.js';
import {
isValidTaskStatus,
TASK_STATUS_OPTIONS,
} from "../../../src/constants/task-status.js";
TASK_STATUS_OPTIONS
} from '../../../src/constants/task-status.js';
/**
* Set the status of a task
@@ -25,7 +25,7 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
try {
if (!isValidTaskStatus(newStatus)) {
throw new Error(
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}`
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}`
);
}
// Determine if we're in MCP mode by checking for mcpLog
@@ -36,20 +36,20 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
console.log(
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
padding: 1,
borderColor: "blue",
borderStyle: "round",
borderColor: 'blue',
borderStyle: 'round'
})
);
}
log("info", `Reading tasks from ${tasksPath}...`);
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`);
}
// Handle multiple task IDs (comma-separated)
const taskIds = taskIdInput.split(",").map((id) => id.trim());
const taskIds = taskIdInput.split(',').map((id) => id.trim());
const updatedTasks = [];
// Update each task
@@ -62,13 +62,13 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
writeJSON(tasksPath, data);
// Validate dependencies after status update
log("info", "Validating dependencies after status update...");
log('info', 'Validating dependencies after status update...');
validateTaskDependencies(data.tasks);
// Generate individual task files
log("info", "Regenerating task files...");
log('info', 'Regenerating task files...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
mcpLog: options.mcpLog,
mcpLog: options.mcpLog
});
// Display success message - only in CLI mode
@@ -80,10 +80,10 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
console.log(
boxen(
chalk.white.bold(`Successfully updated task ${id} status:`) +
"\n" +
`From: ${chalk.yellow(task ? task.status : "unknown")}\n` +
'\n' +
`From: ${chalk.yellow(task ? task.status : 'unknown')}\n` +
`To: ${chalk.green(newStatus)}`,
{ padding: 1, borderColor: "green", borderStyle: "round" }
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
)
);
}
@@ -94,11 +94,11 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
success: true,
updatedTasks: updatedTasks.map((id) => ({
id,
status: newStatus,
})),
status: newStatus
}))
};
} catch (error) {
log("error", `Error setting task status: ${error.message}`);
log('error', `Error setting task status: ${error.message}`);
// Only show error UI in CLI mode
if (!options?.mcpLog) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
import { generateText, streamText, generateObject } from "ai";
import { log } from "../../scripts/modules/index.js";
import { generateText, streamText, generateObject } from 'ai';
import { log } from '../../scripts/modules/index.js';
/**
* Base class for all AI providers
@@ -7,7 +7,7 @@ import { log } from "../../scripts/modules/index.js";
export class BaseAIProvider {
constructor() {
if (this.constructor === BaseAIProvider) {
throw new Error("BaseAIProvider cannot be instantiated directly");
throw new Error('BaseAIProvider cannot be instantiated directly');
}
// Each provider must set their name
@@ -51,10 +51,10 @@ export class BaseAIProvider {
params.temperature !== undefined &&
(params.temperature < 0 || params.temperature > 1)
) {
throw new Error("Temperature must be between 0 and 1");
throw new Error('Temperature must be between 0 and 1');
}
if (params.maxTokens !== undefined && params.maxTokens <= 0) {
throw new Error("maxTokens must be greater than 0");
throw new Error('maxTokens must be greater than 0');
}
}
@@ -63,13 +63,13 @@ export class BaseAIProvider {
*/
validateMessages(messages) {
if (!messages || !Array.isArray(messages) || messages.length === 0) {
throw new Error("Invalid or empty messages array provided");
throw new Error('Invalid or empty messages array provided');
}
for (const msg of messages) {
if (!msg.role || !msg.content) {
throw new Error(
"Invalid message format. Each message must have role and content"
'Invalid message format. Each message must have role and content'
);
}
}
@@ -79,9 +79,9 @@ export class BaseAIProvider {
* Common error handler
*/
handleError(operation, error) {
const errorMessage = error.message || "Unknown error occurred";
log("error", `${this.name} ${operation} failed: ${errorMessage}`, {
error,
const errorMessage = error.message || 'Unknown error occurred';
log('error', `${this.name} ${operation} failed: ${errorMessage}`, {
error
});
throw new Error(
`${this.name} API error during ${operation}: ${errorMessage}`
@@ -93,7 +93,7 @@ export class BaseAIProvider {
* @abstract
*/
getClient(params) {
throw new Error("getClient must be implemented by provider");
throw new Error('getClient must be implemented by provider');
}
/**
@@ -105,7 +105,7 @@ export class BaseAIProvider {
this.validateMessages(params.messages);
log(
"debug",
'debug',
`Generating ${this.name} text with model: ${params.modelId}`
);
@@ -114,11 +114,11 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
maxTokens: params.maxTokens,
temperature: params.temperature,
temperature: params.temperature
});
log(
"debug",
'debug',
`${this.name} generateText completed successfully for model: ${params.modelId}`
);
@@ -127,11 +127,11 @@ export class BaseAIProvider {
usage: {
inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens,
},
totalTokens: result.usage?.totalTokens
}
};
} catch (error) {
this.handleError("text generation", error);
this.handleError('text generation', error);
}
}
@@ -143,24 +143,24 @@ export class BaseAIProvider {
this.validateParams(params);
this.validateMessages(params.messages);
log("debug", `Streaming ${this.name} text with model: ${params.modelId}`);
log('debug', `Streaming ${this.name} text with model: ${params.modelId}`);
const client = this.getClient(params);
const stream = await streamText({
model: client(params.modelId),
messages: params.messages,
maxTokens: params.maxTokens,
temperature: params.temperature,
temperature: params.temperature
});
log(
"debug",
'debug',
`${this.name} streamText initiated successfully for model: ${params.modelId}`
);
return stream;
} catch (error) {
this.handleError("text streaming", error);
this.handleError('text streaming', error);
}
}
@@ -173,14 +173,14 @@ export class BaseAIProvider {
this.validateMessages(params.messages);
if (!params.schema) {
throw new Error("Schema is required for object generation");
throw new Error('Schema is required for object generation');
}
if (!params.objectName) {
throw new Error("Object name is required for object generation");
throw new Error('Object name is required for object generation');
}
log(
"debug",
'debug',
`Generating ${this.name} object ('${params.objectName}') with model: ${params.modelId}`
);
@@ -189,13 +189,13 @@ export class BaseAIProvider {
model: client(params.modelId),
messages: params.messages,
schema: params.schema,
mode: "auto",
mode: 'auto',
maxTokens: params.maxTokens,
temperature: params.temperature,
temperature: params.temperature
});
log(
"debug",
'debug',
`${this.name} generateObject completed successfully for model: ${params.modelId}`
);
@@ -204,11 +204,11 @@ export class BaseAIProvider {
usage: {
inputTokens: result.usage?.promptTokens,
outputTokens: result.usage?.completionTokens,
totalTokens: result.usage?.totalTokens,
},
totalTokens: result.usage?.totalTokens
}
};
} catch (error) {
this.handleError("object generation", error);
this.handleError('object generation', error);
}
}
}

View File

@@ -1,145 +1,149 @@
/**
* Tests for the add-task.js module
*/
import { jest } from '@jest/globals';
import { jest } from "@jest/globals";
// Mock the dependencies before importing the module under test
jest.unstable_mockModule('../../../../../scripts/modules/utils.js', () => ({
jest.unstable_mockModule("../../../../../scripts/modules/utils.js", () => ({
readJSON: jest.fn(),
writeJSON: jest.fn(),
log: jest.fn(),
CONFIG: {
model: 'mock-claude-model',
model: "mock-claude-model",
maxTokens: 4000,
temperature: 0.7,
debug: false
debug: false,
},
truncate: jest.fn((text) => text)
truncate: jest.fn((text) => text),
}));
jest.unstable_mockModule('../../../../../scripts/modules/ui.js', () => ({
jest.unstable_mockModule("../../../../../scripts/modules/ui.js", () => ({
displayBanner: jest.fn(),
getStatusWithColor: jest.fn((status) => status),
startLoadingIndicator: jest.fn(),
stopLoadingIndicator: jest.fn(),
displayAiUsageSummary: jest.fn()
succeedLoadingIndicator: jest.fn(),
failLoadingIndicator: jest.fn(),
warnLoadingIndicator: jest.fn(),
infoLoadingIndicator: jest.fn(),
displayAiUsageSummary: jest.fn(),
}));
jest.unstable_mockModule(
'../../../../../scripts/modules/ai-services-unified.js',
"../../../../../scripts/modules/ai-services-unified.js",
() => ({
generateObjectService: jest.fn().mockResolvedValue({
mainResult: {
object: {
title: 'Task from prompt: Create a new authentication system',
title: "Task from prompt: Create a new authentication system",
description:
'Task generated from: Create a new authentication system',
"Task generated from: Create a new authentication system",
details:
'Implementation details for task generated from prompt: Create a new authentication system',
testStrategy: 'Write unit tests to verify functionality',
dependencies: []
}
"Implementation details for task generated from prompt: Create a new authentication system",
testStrategy: "Write unit tests to verify functionality",
dependencies: [],
},
},
telemetryData: {
timestamp: new Date().toISOString(),
userId: '1234567890',
commandName: 'add-task',
modelUsed: 'claude-3-5-sonnet',
providerName: 'anthropic',
userId: "1234567890",
commandName: "add-task",
modelUsed: "claude-3-5-sonnet",
providerName: "anthropic",
inputTokens: 1000,
outputTokens: 500,
totalTokens: 1500,
totalCost: 0.012414,
currency: 'USD'
}
})
currency: "USD",
},
}),
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/config-manager.js',
"../../../../../scripts/modules/config-manager.js",
() => ({
getDefaultPriority: jest.fn(() => 'medium')
getDefaultPriority: jest.fn(() => "medium"),
})
);
jest.unstable_mockModule(
'../../../../../scripts/modules/task-manager/generate-task-files.js',
"../../../../../scripts/modules/task-manager/generate-task-files.js",
() => ({
default: jest.fn().mockResolvedValue()
default: jest.fn().mockResolvedValue(),
})
);
// Mock external UI libraries
jest.unstable_mockModule('chalk', () => ({
jest.unstable_mockModule("chalk", () => ({
default: {
white: { bold: jest.fn((text) => text) },
cyan: Object.assign(
jest.fn((text) => text),
{
bold: jest.fn((text) => text)
bold: jest.fn((text) => text),
}
),
green: jest.fn((text) => text),
yellow: jest.fn((text) => text),
bold: jest.fn((text) => text)
}
bold: jest.fn((text) => text),
},
}));
jest.unstable_mockModule('boxen', () => ({
default: jest.fn((text) => text)
jest.unstable_mockModule("boxen", () => ({
default: jest.fn((text) => text),
}));
jest.unstable_mockModule('cli-table3', () => ({
jest.unstable_mockModule("cli-table3", () => ({
default: jest.fn().mockImplementation(() => ({
push: jest.fn(),
toString: jest.fn(() => 'mocked table')
}))
toString: jest.fn(() => "mocked table"),
})),
}));
// Import the mocked modules
const { readJSON, writeJSON, log } = await import(
'../../../../../scripts/modules/utils.js'
"../../../../../scripts/modules/utils.js"
);
const { generateObjectService } = await import(
'../../../../../scripts/modules/ai-services-unified.js'
"../../../../../scripts/modules/ai-services-unified.js"
);
const generateTaskFiles = await import(
'../../../../../scripts/modules/task-manager/generate-task-files.js'
"../../../../../scripts/modules/task-manager/generate-task-files.js"
);
// Import the module under test
const { default: addTask } = await import(
'../../../../../scripts/modules/task-manager/add-task.js'
"../../../../../scripts/modules/task-manager/add-task.js"
);
describe('addTask', () => {
describe("addTask", () => {
const sampleTasks = {
tasks: [
{
id: 1,
title: 'Task 1',
description: 'First task',
status: 'pending',
dependencies: []
title: "Task 1",
description: "First task",
status: "pending",
dependencies: [],
},
{
id: 2,
title: 'Task 2',
description: 'Second task',
status: 'pending',
dependencies: []
title: "Task 2",
description: "Second task",
status: "pending",
dependencies: [],
},
{
id: 3,
title: 'Task 3',
description: 'Third task',
status: 'pending',
dependencies: [1]
}
]
title: "Task 3",
description: "Third task",
status: "pending",
dependencies: [1],
},
],
};
// Create a helper function for consistent mcpLog mock
@@ -148,7 +152,7 @@ describe('addTask', () => {
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
success: jest.fn()
success: jest.fn(),
});
beforeEach(() => {
@@ -156,195 +160,195 @@ describe('addTask', () => {
readJSON.mockReturnValue(JSON.parse(JSON.stringify(sampleTasks)));
// Mock console.log to avoid output during tests
jest.spyOn(console, 'log').mockImplementation(() => {});
jest.spyOn(console, "log").mockImplementation(() => {});
});
afterEach(() => {
console.log.mockRestore();
});
test('should add a new task using AI', async () => {
test("should add a new task using AI", async () => {
// Arrange
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act
const result = await addTask(
'tasks/tasks.json',
"tasks/tasks.json",
prompt,
[],
'medium',
"medium",
context,
'json'
"json"
);
// Assert
expect(readJSON).toHaveBeenCalledWith('tasks/tasks.json');
expect(readJSON).toHaveBeenCalledWith("tasks/tasks.json");
expect(generateObjectService).toHaveBeenCalledWith(expect.any(Object));
expect(writeJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
"tasks/tasks.json",
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 4, // Next ID after existing tasks
title: expect.stringContaining(
'Create a new authentication system'
"Create a new authentication system"
),
status: 'pending'
})
])
status: "pending",
}),
]),
})
);
expect(generateTaskFiles.default).toHaveBeenCalled();
expect(result).toEqual(
expect.objectContaining({
newTaskId: 4,
telemetryData: expect.any(Object)
telemetryData: expect.any(Object),
})
);
});
test('should validate dependencies when adding a task', async () => {
test("should validate dependencies when adding a task", async () => {
// Arrange
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const validDependencies = [1, 2]; // These exist in sampleTasks
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act
const result = await addTask(
'tasks/tasks.json',
"tasks/tasks.json",
prompt,
validDependencies,
'medium',
"medium",
context,
'json'
"json"
);
// Assert
expect(writeJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
"tasks/tasks.json",
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 4,
dependencies: validDependencies
})
])
dependencies: validDependencies,
}),
]),
})
);
});
test('should filter out invalid dependencies', async () => {
test("should filter out invalid dependencies", async () => {
// Arrange
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const invalidDependencies = [999]; // Non-existent task ID
const context = { mcpLog: createMcpLogMock() };
// Act
const result = await addTask(
'tasks/tasks.json',
"tasks/tasks.json",
prompt,
invalidDependencies,
'medium',
"medium",
context,
'json'
"json"
);
// Assert
expect(writeJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
"tasks/tasks.json",
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 4,
dependencies: [] // Invalid dependencies should be filtered out
})
])
dependencies: [], // Invalid dependencies should be filtered out
}),
]),
})
);
expect(context.mcpLog.warn).toHaveBeenCalledWith(
expect.stringContaining(
'The following dependencies do not exist or are invalid: 999'
"The following dependencies do not exist or are invalid: 999"
)
);
});
test('should use specified priority', async () => {
test("should use specified priority", async () => {
// Arrange
const prompt = 'Create a new authentication system';
const priority = 'high';
const prompt = "Create a new authentication system";
const priority = "high";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act
await addTask('tasks/tasks.json', prompt, [], priority, context, 'json');
await addTask("tasks/tasks.json", prompt, [], priority, context, "json");
// Assert
expect(writeJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
"tasks/tasks.json",
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
priority: priority
})
])
priority: priority,
}),
]),
})
);
});
test('should handle empty tasks file', async () => {
test("should handle empty tasks file", async () => {
// Arrange
readJSON.mockReturnValue({ tasks: [] });
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act
const result = await addTask(
'tasks/tasks.json',
"tasks/tasks.json",
prompt,
[],
'medium',
"medium",
context,
'json'
"json"
);
// Assert
expect(result.newTaskId).toBe(1); // First task should have ID 1
expect(writeJSON).toHaveBeenCalledWith(
'tasks/tasks.json',
"tasks/tasks.json",
expect.objectContaining({
tasks: expect.arrayContaining([
expect.objectContaining({
id: 1
})
])
id: 1,
}),
]),
})
);
});
test('should handle missing tasks file', async () => {
test("should handle missing tasks file", async () => {
// Arrange
readJSON.mockReturnValue(null);
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act
const result = await addTask(
'tasks/tasks.json',
"tasks/tasks.json",
prompt,
[],
'medium',
"medium",
context,
'json'
"json"
);
// Assert
@@ -352,49 +356,49 @@ describe('addTask', () => {
expect(writeJSON).toHaveBeenCalledTimes(2); // Once to create file, once to add task
});
test('should handle AI service errors', async () => {
test("should handle AI service errors", async () => {
// Arrange
generateObjectService.mockRejectedValueOnce(new Error('AI service failed'));
const prompt = 'Create a new authentication system';
generateObjectService.mockRejectedValueOnce(new Error("AI service failed"));
const prompt = "Create a new authentication system";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act & Assert
await expect(
addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
).rejects.toThrow('AI service failed');
addTask("tasks/tasks.json", prompt, [], "medium", context, "json")
).rejects.toThrow("AI service failed");
});
test('should handle file read errors', async () => {
test("should handle file read errors", async () => {
// Arrange
readJSON.mockImplementation(() => {
throw new Error('File read failed');
throw new Error("File read failed");
});
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act & Assert
await expect(
addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
).rejects.toThrow('File read failed');
addTask("tasks/tasks.json", prompt, [], "medium", context, "json")
).rejects.toThrow("File read failed");
});
test('should handle file write errors', async () => {
test("should handle file write errors", async () => {
// Arrange
writeJSON.mockImplementation(() => {
throw new Error('File write failed');
throw new Error("File write failed");
});
const prompt = 'Create a new authentication system';
const prompt = "Create a new authentication system";
const context = {
mcpLog: createMcpLogMock()
mcpLog: createMcpLogMock(),
};
// Act & Assert
await expect(
addTask('tasks/tasks.json', prompt, [], 'medium', context, 'json')
).rejects.toThrow('File write failed');
addTask("tasks/tasks.json", prompt, [], "medium", context, "json")
).rejects.toThrow("File write failed");
});
});

View File

@@ -82,19 +82,19 @@ describe("UI Module", () => {
test("should return done status with emoji for console output", () => {
const result = getStatusWithColor("done");
expect(result).toMatch(/done/);
expect(result).toContain("");
expect(result).toContain("");
});
test("should return pending status with emoji for console output", () => {
const result = getStatusWithColor("pending");
expect(result).toMatch(/pending/);
expect(result).toContain("⏱️");
expect(result).toContain("");
});
test("should return deferred status with emoji for console output", () => {
const result = getStatusWithColor("deferred");
expect(result).toMatch(/deferred/);
expect(result).toContain("⏱️");
expect(result).toContain("x");
});
test("should return in-progress status with emoji for console output", () => {