fix: normalize task IDs to numbers on load to fix comparison issues

When tasks.json contains string IDs (e.g., "5" instead of 5), task lookups
fail because the code uses parseInt() and strict equality (===) for comparisons.

This fix normalizes all task and subtask IDs to numbers when loading the JSON,
ensuring consistent comparisons throughout the codebase without requiring
changes to multiple comparison locations.

Fixes task not found errors when using string IDs in tasks.json.
This commit is contained in:
Carl Mercier
2025-07-25 23:59:55 -05:00
committed by Ralph Khreish
parent 45a14c323d
commit b79eb4f7a2

View File

@@ -3,18 +3,18 @@
* Utility functions for the Task Master CLI
*/
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import dotenv from 'dotenv';
import fs from "fs";
import path from "path";
import chalk from "chalk";
import dotenv from "dotenv";
// Import specific config getters needed here
import { getLogLevel, getDebugFlag } from './config-manager.js';
import * as gitUtils from './utils/git-utils.js';
import { getLogLevel, getDebugFlag } from "./config-manager.js";
import * as gitUtils from "./utils/git-utils.js";
import {
COMPLEXITY_REPORT_FILE,
LEGACY_COMPLEXITY_REPORT_FILE,
LEGACY_CONFIG_FILE
} from '../../src/constants/paths.js';
LEGACY_CONFIG_FILE,
} from "../../src/constants/paths.js";
// Global silent mode flag
let silentMode = false;
@@ -39,10 +39,10 @@ function resolveEnvVariable(key, session = null, projectRoot = null) {
// 2. Read .env file at projectRoot
if (projectRoot) {
const envPath = path.join(projectRoot, '.env');
const envPath = path.join(projectRoot, ".env");
if (fs.existsSync(envPath)) {
try {
const envFileContent = fs.readFileSync(envPath, 'utf-8');
const envFileContent = fs.readFileSync(envPath, "utf-8");
const parsedEnv = dotenv.parse(envFileContent); // Use dotenv to parse
if (parsedEnv && parsedEnv[key]) {
// console.log(`DEBUG: Found key ${key} in ${envPath}`); // Optional debug log
@@ -50,7 +50,7 @@ function resolveEnvVariable(key, session = null, projectRoot = null) {
}
} catch (error) {
// Log error but don't crash, just proceed as if key wasn't found in file
log('warn', `Could not read or parse ${envPath}: ${error.message}`);
log("warn", `Could not read or parse ${envPath}: ${error.message}`);
}
}
}
@@ -72,15 +72,15 @@ function resolveEnvVariable(key, session = null, projectRoot = null) {
* @returns {string} Slugified tag name safe for filesystem use
*/
function slugifyTagForFilePath(tagName) {
if (!tagName || typeof tagName !== 'string') {
return 'unknown-tag';
if (!tagName || typeof tagName !== "string") {
return "unknown-tag";
}
// Replace invalid filesystem characters with hyphens and clean up
return tagName
.replace(/[^a-zA-Z0-9_-]/g, '-') // Replace invalid chars with hyphens
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
.replace(/-+/g, '-') // Collapse multiple hyphens
.replace(/[^a-zA-Z0-9_-]/g, "-") // Replace invalid chars with hyphens
.replace(/^-+|-+$/g, "") // Remove leading/trailing hyphens
.replace(/-+/g, "-") // Collapse multiple hyphens
.toLowerCase() // Convert to lowercase
.substring(0, 50); // Limit length to prevent overly long filenames
}
@@ -93,10 +93,10 @@ function slugifyTagForFilePath(tagName) {
* @param {string} [projectRoot='.'] - The project root directory
* @returns {string} The resolved file path
*/
function getTagAwareFilePath(basePath, tag, projectRoot = '.') {
function getTagAwareFilePath(basePath, tag, projectRoot = ".") {
// Use path.parse and format for clean tag insertion
const parsedPath = path.parse(basePath);
if (!tag || tag === 'master') {
if (!tag || tag === "master") {
return path.join(projectRoot, basePath);
}
@@ -118,7 +118,7 @@ function getTagAwareFilePath(basePath, tag, projectRoot = '.') {
*/
function findProjectRoot(
startDir = process.cwd(),
markers = ['package.json', 'pyproject.toml', '.git', LEGACY_CONFIG_FILE]
markers = ["package.json", "pyproject.toml", ".git", LEGACY_CONFIG_FILE],
) {
let currentPath = path.resolve(startDir);
const rootPath = path.parse(currentPath).root;
@@ -157,7 +157,7 @@ const LOG_LEVELS = {
info: 1,
warn: 2,
error: 3,
success: 1 // Treat success like info level
success: 1, // Treat success like info level
};
/**
@@ -165,7 +165,7 @@ const LOG_LEVELS = {
* @returns {Promise<Object>} The task manager module object
*/
async function getTaskManager() {
return import('./task-manager.js');
return import("./task-manager.js");
}
/**
@@ -203,49 +203,73 @@ function log(level, ...args) {
// GUARD: Prevent circular dependency during config loading
// Use a simple fallback log level instead of calling getLogLevel()
let configLevel = 'info'; // Default fallback
let configLevel = "info"; // Default fallback
try {
// Only try to get config level if we're not in the middle of config loading
configLevel = getLogLevel() || 'info';
configLevel = getLogLevel() || "info";
} catch (error) {
// If getLogLevel() fails (likely due to circular dependency),
// use default 'info' level and continue
configLevel = 'info';
configLevel = "info";
}
// Use text prefixes instead of emojis
const prefixes = {
debug: chalk.gray('[DEBUG]'),
info: chalk.blue('[INFO]'),
warn: chalk.yellow('[WARN]'),
error: chalk.red('[ERROR]'),
success: chalk.green('[SUCCESS]')
debug: chalk.gray("[DEBUG]"),
info: chalk.blue("[INFO]"),
warn: chalk.yellow("[WARN]"),
error: chalk.red("[ERROR]"),
success: chalk.green("[SUCCESS]"),
};
// Ensure level exists, default to info if not
const currentLevel = LOG_LEVELS.hasOwnProperty(level) ? level : 'info';
const currentLevel = LOG_LEVELS.hasOwnProperty(level) ? level : "info";
// Check log level configuration
if (
LOG_LEVELS[currentLevel] >= (LOG_LEVELS[configLevel] ?? LOG_LEVELS.info)
) {
const prefix = prefixes[currentLevel] || '';
const prefix = prefixes[currentLevel] || "";
// Use console.log for all levels, let chalk handle coloring
// Construct the message properly
const message = args
.map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : arg))
.join(' ');
.map((arg) => (typeof arg === "object" ? JSON.stringify(arg) : arg))
.join(" ");
console.log(`${prefix} ${message}`);
}
}
/**
* Normalizes task and subtask IDs to numbers to ensure consistent comparisons for Integers and Strings
* @param {Array} tasks - The tasks array to normalize
*/
function normalizeTaskIds(tasks) {
if (!Array.isArray(tasks)) return;
tasks.forEach((task) => {
// Convert task ID to number
if (task.id !== undefined) {
task.id = parseInt(task.id, 10);
}
// Convert subtask IDs to numbers
if (Array.isArray(task.subtasks)) {
task.subtasks.forEach((subtask) => {
if (subtask.id !== undefined) {
subtask.id = parseInt(subtask.id, 10);
}
});
}
});
}
/**
* Checks if the data object has a tagged structure (contains tag objects with tasks arrays)
* @param {Object} data - The data object to check
* @returns {boolean} True if the data has a tagged structure
*/
function hasTaggedStructure(data) {
if (!data || typeof data !== 'object') {
if (!data || typeof data !== "object") {
return false;
}
@@ -253,7 +277,7 @@ function hasTaggedStructure(data) {
for (const key in data) {
if (
data.hasOwnProperty(key) &&
typeof data[key] === 'object' &&
typeof data[key] === "object" &&
Array.isArray(data[key].tasks)
) {
return true;
@@ -282,7 +306,7 @@ function readJSON(filepath, projectRoot = null, tag = null) {
if (isDebug) {
console.log(
`readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`
`readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`,
);
}
@@ -292,7 +316,7 @@ function readJSON(filepath, projectRoot = null, tag = null) {
let data;
try {
data = JSON.parse(fs.readFileSync(filepath, 'utf8'));
data = JSON.parse(fs.readFileSync(filepath, "utf8"));
if (isDebug) {
console.log(`Successfully read JSON from ${filepath}`);
}
@@ -304,7 +328,7 @@ function readJSON(filepath, projectRoot = null, tag = null) {
}
// If it's not a tasks.json file, return as-is
if (!filepath.includes('tasks.json') || !data) {
if (!filepath.includes("tasks.json") || !data) {
if (isDebug) {
console.log(`File is not tasks.json or data is null, returning as-is`);
}
@@ -329,9 +353,9 @@ function readJSON(filepath, projectRoot = null, tag = null) {
metadata: data.metadata || {
created: new Date().toISOString(),
updated: new Date().toISOString(),
description: 'Tasks for master context'
}
}
description: "Tasks for master context",
},
},
};
// Write the migrated data back to the file
@@ -369,7 +393,7 @@ function readJSON(filepath, projectRoot = null, tag = null) {
}
// If we have tagged data, we need to resolve which tag to use
if (typeof data === 'object' && !data.tasks) {
if (typeof data === "object" && !data.tasks) {
// This is tagged format
if (isDebug) {
console.log(`File is in tagged format, resolving tag...`);
@@ -379,19 +403,19 @@ function readJSON(filepath, projectRoot = null, tag = null) {
for (const tagName in data) {
if (
data.hasOwnProperty(tagName) &&
typeof data[tagName] === 'object' &&
typeof data[tagName] === "object" &&
data[tagName].tasks
) {
try {
ensureTagMetadata(data[tagName], {
description: `Tasks for ${tagName} context`,
skipUpdate: true // Don't update timestamp during read operations
skipUpdate: true, // Don't update timestamp during read operations
});
} catch (error) {
// If ensureTagMetadata fails, continue without metadata
if (isDebug) {
console.log(
`Failed to ensure metadata for tag ${tagName}: ${error.message}`
`Failed to ensure metadata for tag ${tagName}: ${error.message}`,
);
}
}
@@ -414,7 +438,7 @@ function readJSON(filepath, projectRoot = null, tag = null) {
try {
// Default to master tag if anything goes wrong
let resolvedTag = 'master';
let resolvedTag = "master";
// Try to resolve the correct tag, but don't fail if it doesn't work
try {
@@ -435,7 +459,7 @@ function readJSON(filepath, projectRoot = null, tag = null) {
} catch (tagResolveError) {
if (isDebug) {
console.log(
`Tag resolution failed, using master: ${tagResolveError.message}`
`Tag resolution failed, using master: ${tagResolveError.message}`,
);
}
// resolvedTag stays as 'master'
@@ -448,15 +472,17 @@ function readJSON(filepath, projectRoot = null, tag = null) {
// Get the data for the resolved tag
const tagData = data[resolvedTag];
if (tagData && tagData.tasks) {
normalizeTaskIds(tagData.tasks);
// Add the _rawTaggedData property and the resolved tag to the returned data
const result = {
...tagData,
tag: resolvedTag,
_rawTaggedData: originalTaggedData
_rawTaggedData: originalTaggedData,
};
if (isDebug) {
console.log(
`Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`
`Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`,
);
}
return result;
@@ -464,15 +490,17 @@ function readJSON(filepath, projectRoot = null, tag = null) {
// If the resolved tag doesn't exist, fall back to master
const masterData = data.master;
if (masterData && masterData.tasks) {
normalizeTaskIds(masterData.tasks);
if (isDebug) {
console.log(
`Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`
`Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks`,
);
}
return {
...masterData,
tag: 'master',
_rawTaggedData: originalTaggedData
tag: "master",
_rawTaggedData: originalTaggedData,
};
} else {
if (isDebug) {
@@ -481,8 +509,8 @@ function readJSON(filepath, projectRoot = null, tag = null) {
// Return empty structure if no valid data
return {
tasks: [],
tag: 'master',
_rawTaggedData: originalTaggedData
tag: "master",
_rawTaggedData: originalTaggedData,
};
}
}
@@ -493,14 +521,16 @@ function readJSON(filepath, projectRoot = null, tag = null) {
// If anything goes wrong, try to return master or empty
const masterData = data.master;
if (masterData && masterData.tasks) {
normalizeTaskIds(masterData.tasks);
return {
...masterData,
_rawTaggedData: originalTaggedData
_rawTaggedData: originalTaggedData,
};
}
return {
tasks: [],
_rawTaggedData: originalTaggedData
_rawTaggedData: originalTaggedData,
};
}
}
@@ -524,26 +554,26 @@ function performCompleteTagMigration(tasksJsonPath) {
path.dirname(tasksJsonPath);
// 1. Migrate config.json - add defaultTag and tags section
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
const configPath = path.join(projectRoot, ".taskmaster", "config.json");
if (fs.existsSync(configPath)) {
migrateConfigJson(configPath);
}
// 2. Create state.json if it doesn't exist
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
const statePath = path.join(projectRoot, ".taskmaster", "state.json");
if (!fs.existsSync(statePath)) {
createStateJson(statePath);
}
if (getDebugFlag()) {
log(
'debug',
`Complete tag migration performed for project: ${projectRoot}`
"debug",
`Complete tag migration performed for project: ${projectRoot}`,
);
}
} catch (error) {
if (getDebugFlag()) {
log('warn', `Error during complete tag migration: ${error.message}`);
log("warn", `Error during complete tag migration: ${error.message}`);
}
}
}
@@ -554,7 +584,7 @@ function performCompleteTagMigration(tasksJsonPath) {
*/
function migrateConfigJson(configPath) {
try {
const rawConfig = fs.readFileSync(configPath, 'utf8');
const rawConfig = fs.readFileSync(configPath, "utf8");
const config = JSON.parse(rawConfig);
if (!config) return;
@@ -565,20 +595,20 @@ function migrateConfigJson(configPath) {
config.global = {};
}
if (!config.global.defaultTag) {
config.global.defaultTag = 'master';
config.global.defaultTag = "master";
modified = true;
}
if (modified) {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf8");
if (process.env.TASKMASTER_DEBUG === "true") {
console.log(
'[DEBUG] Updated config.json with tagged task system settings'
"[DEBUG] Updated config.json with tagged task system settings",
);
}
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
if (process.env.TASKMASTER_DEBUG === "true") {
console.warn(`[WARN] Error migrating config.json: ${error.message}`);
}
}
@@ -591,18 +621,18 @@ function migrateConfigJson(configPath) {
function createStateJson(statePath) {
try {
const initialState = {
currentTag: 'master',
currentTag: "master",
lastSwitched: new Date().toISOString(),
branchTagMapping: {},
migrationNoticeShown: false
migrationNoticeShown: false,
};
fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), 'utf8');
if (process.env.TASKMASTER_DEBUG === 'true') {
console.log('[DEBUG] Created initial state.json for tagged task system');
fs.writeFileSync(statePath, JSON.stringify(initialState, null, 2), "utf8");
if (process.env.TASKMASTER_DEBUG === "true") {
console.log("[DEBUG] Created initial state.json for tagged task system");
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
if (process.env.TASKMASTER_DEBUG === "true") {
console.warn(`[WARN] Error creating state.json: ${error.message}`);
}
}
@@ -615,7 +645,7 @@ function createStateJson(statePath) {
function markMigrationForNotice(tasksJsonPath) {
try {
const projectRoot = path.dirname(path.dirname(tasksJsonPath));
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
const statePath = path.join(projectRoot, ".taskmaster", "state.json");
// Ensure state.json exists
if (!fs.existsSync(statePath)) {
@@ -624,24 +654,24 @@ function markMigrationForNotice(tasksJsonPath) {
// Read and update state to mark migration occurred using fs directly
try {
const rawState = fs.readFileSync(statePath, 'utf8');
const rawState = fs.readFileSync(statePath, "utf8");
const stateData = JSON.parse(rawState) || {};
// Only set to false if it's not already set (i.e., first time migration)
if (stateData.migrationNoticeShown === undefined) {
stateData.migrationNoticeShown = false;
fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), 'utf8');
fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2), "utf8");
}
} catch (stateError) {
if (process.env.TASKMASTER_DEBUG === 'true') {
if (process.env.TASKMASTER_DEBUG === "true") {
console.warn(
`[WARN] Error updating state for migration notice: ${stateError.message}`
`[WARN] Error updating state for migration notice: ${stateError.message}`,
);
}
}
} catch (error) {
if (process.env.TASKMASTER_DEBUG === 'true') {
if (process.env.TASKMASTER_DEBUG === "true") {
console.warn(
`[WARN] Error marking migration for notice: ${error.message}`
`[WARN] Error marking migration for notice: ${error.message}`,
);
}
}
@@ -655,7 +685,7 @@ function markMigrationForNotice(tasksJsonPath) {
* @param {string} tag - Optional tag for tag context
*/
function writeJSON(filepath, data, projectRoot = null, tag = null) {
const isDebug = process.env.TASKMASTER_DEBUG === 'true';
const isDebug = process.env.TASKMASTER_DEBUG === "true";
try {
let finalData = data;
@@ -671,12 +701,12 @@ function writeJSON(filepath, data, projectRoot = null, tag = null) {
if (isDebug) {
console.log(
`writeJSON: Detected resolved tag data missing _rawTaggedData. Re-reading raw data to prevent data loss for tag '${resolvedTag}'.`
`writeJSON: Detected resolved tag data missing _rawTaggedData. Re-reading raw data to prevent data loss for tag '${resolvedTag}'.`,
);
}
// Re-read the full file to get the complete tagged structure
const rawFullData = JSON.parse(fs.readFileSync(filepath, 'utf8'));
const rawFullData = JSON.parse(fs.readFileSync(filepath, "utf8"));
// Merge the updated data into the full structure
finalData = {
@@ -685,8 +715,8 @@ function writeJSON(filepath, data, projectRoot = null, tag = null) {
// Preserve existing tag metadata if it exists, otherwise use what's passed
...(rawFullData[resolvedTag]?.metadata || {}),
...(data.metadata ? { metadata: data.metadata } : {}),
tasks: data.tasks // The updated tasks array is the source of truth here
}
tasks: data.tasks, // The updated tasks array is the source of truth here
},
};
}
// If we have _rawTaggedData, this means we're working with resolved tag data
@@ -703,30 +733,30 @@ function writeJSON(filepath, data, projectRoot = null, tag = null) {
// Update the specific tag with the resolved data
finalData = {
...originalTaggedData,
[resolvedTag]: cleanResolvedData
[resolvedTag]: cleanResolvedData,
};
if (isDebug) {
console.log(
`writeJSON: Merging resolved data back into tag '${resolvedTag}'`
`writeJSON: Merging resolved data back into tag '${resolvedTag}'`,
);
}
}
// Clean up any internal properties that shouldn't be persisted
let cleanData = finalData;
if (cleanData && typeof cleanData === 'object') {
if (cleanData && typeof cleanData === "object") {
// Remove any _rawTaggedData or tag properties from root level
const { _rawTaggedData, tag: tagProp, ...rootCleanData } = cleanData;
cleanData = rootCleanData;
// Additional cleanup for tag objects
if (typeof cleanData === 'object' && !Array.isArray(cleanData)) {
if (typeof cleanData === "object" && !Array.isArray(cleanData)) {
const finalCleanData = {};
for (const [key, value] of Object.entries(cleanData)) {
if (
value &&
typeof value === 'object' &&
typeof value === "object" &&
Array.isArray(value.tasks)
) {
// This is a tag object - clean up any rogue root-level properties
@@ -749,15 +779,15 @@ function writeJSON(filepath, data, projectRoot = null, tag = null) {
}
}
fs.writeFileSync(filepath, JSON.stringify(cleanData, null, 2), 'utf8');
fs.writeFileSync(filepath, JSON.stringify(cleanData, null, 2), "utf8");
if (isDebug) {
console.log(`writeJSON: Successfully wrote to ${filepath}`);
}
} catch (error) {
log('error', `Error writing JSON file ${filepath}:`, error.message);
log("error", `Error writing JSON file ${filepath}:`, error.message);
if (isDebug) {
log('error', 'Full error details:', error);
log("error", "Full error details:", error);
}
}
}
@@ -798,7 +828,7 @@ function readComplexityReport(customPath = null) {
const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE);
const legacyPath = path.join(
process.cwd(),
LEGACY_COMPLEXITY_REPORT_FILE
LEGACY_COMPLEXITY_REPORT_FILE,
);
reportPath = fs.existsSync(newPath) ? newPath : legacyPath;
@@ -806,19 +836,19 @@ function readComplexityReport(customPath = null) {
if (!fs.existsSync(reportPath)) {
if (isDebug) {
log('debug', `Complexity report not found at ${reportPath}`);
log("debug", `Complexity report not found at ${reportPath}`);
}
return null;
}
const reportData = readJSON(reportPath);
if (isDebug) {
log('debug', `Successfully read complexity report from ${reportPath}`);
log("debug", `Successfully read complexity report from ${reportPath}`);
}
return reportData;
} catch (error) {
if (isDebug) {
log('error', `Error reading complexity report: ${error.message}`);
log("error", `Error reading complexity report: ${error.message}`);
}
return null;
}
@@ -870,9 +900,9 @@ function taskExists(tasks, taskId) {
}
// Handle both regular task IDs and subtask IDs (e.g., "1.2")
if (typeof taskId === 'string' && taskId.includes('.')) {
if (typeof taskId === "string" && taskId.includes(".")) {
const [parentId, subtaskId] = taskId
.split('.')
.split(".")
.map((id) => parseInt(id, 10));
const parentTask = tasks.find((t) => t.id === parentId);
@@ -893,11 +923,11 @@ function taskExists(tasks, taskId) {
* @returns {string} The formatted task ID
*/
function formatTaskId(id) {
if (typeof id === 'string' && id.includes('.')) {
if (typeof id === "string" && id.includes(".")) {
return id; // Already formatted as a string with a dot (e.g., "1.2")
}
if (typeof id === 'number') {
if (typeof id === "number") {
return id.toString();
}
@@ -916,17 +946,17 @@ function findTaskById(
tasks,
taskId,
complexityReport = null,
statusFilter = null
statusFilter = null,
) {
if (!taskId || !tasks || !Array.isArray(tasks)) {
return { task: null, originalSubtaskCount: null };
}
// Check if it's a subtask ID (e.g., "1.2")
if (typeof taskId === 'string' && taskId.includes('.')) {
if (typeof taskId === "string" && taskId.includes(".")) {
// If looking for a subtask, statusFilter doesn't apply directly here.
const [parentId, subtaskId] = taskId
.split('.')
.split(".")
.map((id) => parseInt(id, 10));
const parentTask = tasks.find((t) => t.id === parentId);
@@ -940,7 +970,7 @@ function findTaskById(
subtask.parentTask = {
id: parentTask.id,
title: parentTask.title,
status: parentTask.status
status: parentTask.status,
};
subtask.isSubtask = true;
}
@@ -953,7 +983,7 @@ function findTaskById(
return {
task: subtask || null,
originalSubtaskCount: null,
originalSubtasks: null
originalSubtasks: null,
};
}
@@ -983,7 +1013,7 @@ function findTaskById(
filteredTask.subtasks = task.subtasks.filter(
(subtask) =>
subtask.status &&
subtask.status.toLowerCase() === statusFilter.toLowerCase()
subtask.status.toLowerCase() === statusFilter.toLowerCase(),
);
taskResult = filteredTask;
@@ -1020,7 +1050,7 @@ function truncate(text, maxLength) {
function isEmpty(value) {
if (Array.isArray(value)) {
return value.length === 0;
} else if (typeof value === 'object' && value !== null) {
} else if (typeof value === "object" && value !== null) {
return Object.keys(value).length === 0;
}
@@ -1040,7 +1070,7 @@ function findCycles(
dependencyMap,
visited = new Set(),
recursionStack = new Set(),
path = []
path = [],
) {
// Mark the current node as visited and part of recursion stack
visited.add(subtaskId);
@@ -1057,7 +1087,7 @@ function findCycles(
// If not visited, recursively check for cycles
if (!visited.has(depId)) {
const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [
...path
...path,
]);
cyclesToBreak.push(...cycles);
}
@@ -1086,21 +1116,21 @@ function findCycles(
const toKebabCase = (str) => {
// Special handling for common acronyms
const withReplacedAcronyms = str
.replace(/ID/g, 'Id')
.replace(/API/g, 'Api')
.replace(/UI/g, 'Ui')
.replace(/URL/g, 'Url')
.replace(/URI/g, 'Uri')
.replace(/JSON/g, 'Json')
.replace(/XML/g, 'Xml')
.replace(/HTML/g, 'Html')
.replace(/CSS/g, 'Css');
.replace(/ID/g, "Id")
.replace(/API/g, "Api")
.replace(/UI/g, "Ui")
.replace(/URL/g, "Url")
.replace(/URI/g, "Uri")
.replace(/JSON/g, "Json")
.replace(/XML/g, "Xml")
.replace(/HTML/g, "Html")
.replace(/CSS/g, "Css");
// Insert hyphens before capital letters and convert to lowercase
return withReplacedAcronyms
.replace(/([A-Z])/g, '-$1')
.replace(/([A-Z])/g, "-$1")
.toLowerCase()
.replace(/^-/, ''); // Remove leading hyphen if present
.replace(/^-/, ""); // Remove leading hyphen if present
};
/**
@@ -1111,11 +1141,11 @@ const toKebabCase = (str) => {
function detectCamelCaseFlags(args) {
const camelCaseFlags = [];
for (const arg of args) {
if (arg.startsWith('--')) {
const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after =
if (arg.startsWith("--")) {
const flagName = arg.split("=")[0].slice(2); // Remove -- and anything after =
// Skip single-word flags - they can't be camelCase
if (!flagName.includes('-') && !/[A-Z]/.test(flagName)) {
if (!flagName.includes("-") && !/[A-Z]/.test(flagName)) {
continue;
}
@@ -1125,7 +1155,7 @@ function detectCamelCaseFlags(args) {
if (kebabVersion !== flagName) {
camelCaseFlags.push({
original: flagName,
kebabCase: kebabVersion
kebabCase: kebabVersion,
});
}
}
@@ -1149,13 +1179,13 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
timestamp: new Date().toISOString(), // Use current time for aggregation time
userId: telemetryArray[0].userId, // Assume userId is consistent
commandName: overallCommandName,
modelUsed: 'Multiple', // Default if models vary
providerName: 'Multiple', // Default if providers vary
modelUsed: "Multiple", // Default if models vary
providerName: "Multiple", // Default if providers vary
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
totalCost: 0,
currency: telemetryArray[0].currency || 'USD' // Assume consistent currency or default
currency: telemetryArray[0].currency || "USD", // Assume consistent currency or default
};
const uniqueModels = new Set();
@@ -1168,7 +1198,7 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
aggregated.totalCost += item.totalCost || 0;
uniqueModels.add(item.modelUsed);
uniqueProviders.add(item.providerName);
uniqueCurrencies.add(item.currency || 'USD');
uniqueCurrencies.add(item.currency || "USD");
});
aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens;
@@ -1181,7 +1211,7 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
aggregated.providerName = [...uniqueProviders][0];
}
if (uniqueCurrencies.size > 1) {
aggregated.currency = 'Multiple'; // Mark if currencies actually differ
aggregated.currency = "Multiple"; // Mark if currencies actually differ
} else if (uniqueCurrencies.size === 1) {
aggregated.currency = [...uniqueCurrencies][0];
}
@@ -1197,14 +1227,14 @@ function aggregateTelemetry(telemetryArray, overallCommandName) {
*/
function getCurrentTag(projectRoot) {
if (!projectRoot) {
throw new Error('projectRoot is required for getCurrentTag');
throw new Error("projectRoot is required for getCurrentTag");
}
try {
// Try to read current tag from state.json using fs directly
const statePath = path.join(projectRoot, '.taskmaster', 'state.json');
const statePath = path.join(projectRoot, ".taskmaster", "state.json");
if (fs.existsSync(statePath)) {
const rawState = fs.readFileSync(statePath, 'utf8');
const rawState = fs.readFileSync(statePath, "utf8");
const stateData = JSON.parse(rawState);
if (stateData && stateData.currentTag) {
return stateData.currentTag;
@@ -1216,9 +1246,9 @@ function getCurrentTag(projectRoot) {
// Fall back to defaultTag from config using fs directly
try {
const configPath = path.join(projectRoot, '.taskmaster', 'config.json');
const configPath = path.join(projectRoot, ".taskmaster", "config.json");
if (fs.existsSync(configPath)) {
const rawConfig = fs.readFileSync(configPath, 'utf8');
const rawConfig = fs.readFileSync(configPath, "utf8");
const configData = JSON.parse(rawConfig);
if (configData && configData.global && configData.global.defaultTag) {
return configData.global.defaultTag;
@@ -1229,7 +1259,7 @@ function getCurrentTag(projectRoot) {
}
// Final fallback
return 'master';
return "master";
}
/**
@@ -1243,7 +1273,7 @@ function resolveTag(options = {}) {
const { projectRoot, tag } = options;
if (!projectRoot) {
throw new Error('projectRoot is required for resolveTag');
throw new Error("projectRoot is required for resolveTag");
}
// If explicit tag provided, use it
@@ -1311,7 +1341,7 @@ function flattenTasksWithSubtasks(tasks) {
flattened.push({
...task,
searchableId: task.id.toString(), // For consistent ID handling
isSubtask: false
isSubtask: false,
});
// Add subtasks if they exist
@@ -1325,7 +1355,7 @@ function flattenTasksWithSubtasks(tasks) {
parentTitle: task.title,
// Enhance subtask context with parent information
title: `${subtask.title} (subtask of: ${task.title})`,
description: `${subtask.description} [Parent: ${task.description}]`
description: `${subtask.description} [Parent: ${task.description}]`,
});
}
}
@@ -1343,8 +1373,8 @@ function flattenTasksWithSubtasks(tasks) {
* @returns {Object} The updated tag object (for chaining)
*/
function ensureTagMetadata(tagObj, opts = {}) {
if (!tagObj || typeof tagObj !== 'object') {
throw new Error('tagObj must be a valid object');
if (!tagObj || typeof tagObj !== "object") {
throw new Error("tagObj must be a valid object");
}
const now = new Date().toISOString();
@@ -1354,7 +1384,7 @@ function ensureTagMetadata(tagObj, opts = {}) {
tagObj.metadata = {
created: now,
updated: now,
...(opts.description ? { description: opts.description } : {})
...(opts.description ? { description: opts.description } : {}),
};
} else {
// Ensure existing metadata has required fields
@@ -1412,5 +1442,5 @@ export {
createStateJson,
markMigrationForNotice,
flattenTasksWithSubtasks,
ensureTagMetadata
ensureTagMetadata,
};