diff --git a/scripts/modules/utils.js b/scripts/modules/utils.js index e8a92dd9..0454ea81 100644 --- a/scripts/modules/utils.js +++ b/scripts/modules/utils.js @@ -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'; + COMPLEXITY_REPORT_FILE, + LEGACY_COMPLEXITY_REPORT_FILE, + LEGACY_CONFIG_FILE, +} from "../../src/constants/paths.js"; // Global silent mode flag let silentMode = false; @@ -32,36 +32,36 @@ let silentMode = false; * @returns {string|undefined} The value of the environment variable or undefined if not found. */ function resolveEnvVariable(key, session = null, projectRoot = null) { - // 1. Check session.env - if (session?.env?.[key]) { - return session.env[key]; - } + // 1. Check session.env + if (session?.env?.[key]) { + return session.env[key]; + } - // 2. Read .env file at projectRoot - if (projectRoot) { - const envPath = path.join(projectRoot, '.env'); - if (fs.existsSync(envPath)) { - try { - 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 - return parsedEnv[key]; - } - } 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}`); - } - } - } + // 2. Read .env file at projectRoot + if (projectRoot) { + const envPath = path.join(projectRoot, ".env"); + if (fs.existsSync(envPath)) { + try { + 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 + return parsedEnv[key]; + } + } 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}`); + } + } + } - // 3. Fallback: Check process.env - if (process.env[key]) { - return process.env[key]; - } + // 3. Fallback: Check process.env + if (process.env[key]) { + return process.env[key]; + } - // Not found anywhere - return undefined; + // Not found anywhere + return undefined; } // --- Tag-Aware Path Resolution Utility --- @@ -72,17 +72,17 @@ 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 - .toLowerCase() // Convert to lowercase - .substring(0, 50); // Limit length to prevent overly long filenames + // 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 + .toLowerCase() // Convert to lowercase + .substring(0, 50); // Limit length to prevent overly long filenames } /** @@ -93,20 +93,20 @@ function slugifyTagForFilePath(tagName) { * @param {string} [projectRoot='.'] - The project root directory * @returns {string} The resolved file path */ -function getTagAwareFilePath(basePath, tag, projectRoot = '.') { - // Use path.parse and format for clean tag insertion - const parsedPath = path.parse(basePath); - if (!tag || tag === 'master') { - return path.join(projectRoot, basePath); - } +function getTagAwareFilePath(basePath, tag, projectRoot = ".") { + // Use path.parse and format for clean tag insertion + const parsedPath = path.parse(basePath); + if (!tag || tag === "master") { + return path.join(projectRoot, basePath); + } - // Slugify the tag for filesystem safety - const slugifiedTag = slugifyTagForFilePath(tag); + // Slugify the tag for filesystem safety + const slugifiedTag = slugifyTagForFilePath(tag); - // Append slugified tag before file extension - parsedPath.base = `${parsedPath.name}_${slugifiedTag}${parsedPath.ext}`; - const relativePath = path.format(parsedPath); - return path.join(projectRoot, relativePath); + // Append slugified tag before file extension + parsedPath.base = `${parsedPath.name}_${slugifiedTag}${parsedPath.ext}`; + const relativePath = path.format(parsedPath); + return path.join(projectRoot, relativePath); } // --- Project Root Finding Utility --- @@ -117,34 +117,34 @@ function getTagAwareFilePath(basePath, tag, projectRoot = '.') { * @returns {string|null} The path to the project root, or null if not found. */ function findProjectRoot( - startDir = process.cwd(), - markers = ['package.json', 'pyproject.toml', '.git', LEGACY_CONFIG_FILE] + startDir = process.cwd(), + markers = ["package.json", "pyproject.toml", ".git", LEGACY_CONFIG_FILE], ) { - let currentPath = path.resolve(startDir); - const rootPath = path.parse(currentPath).root; + let currentPath = path.resolve(startDir); + const rootPath = path.parse(currentPath).root; - while (currentPath !== rootPath) { - // Check if any marker exists in the current directory - const hasMarker = markers.some((marker) => { - const markerPath = path.join(currentPath, marker); - return fs.existsSync(markerPath); - }); + while (currentPath !== rootPath) { + // Check if any marker exists in the current directory + const hasMarker = markers.some((marker) => { + const markerPath = path.join(currentPath, marker); + return fs.existsSync(markerPath); + }); - if (hasMarker) { - return currentPath; - } + if (hasMarker) { + return currentPath; + } - // Move up one directory - currentPath = path.dirname(currentPath); - } + // Move up one directory + currentPath = path.dirname(currentPath); + } - // Check the root directory as well - const hasMarkerInRoot = markers.some((marker) => { - const markerPath = path.join(rootPath, marker); - return fs.existsSync(markerPath); - }); + // Check the root directory as well + const hasMarkerInRoot = markers.some((marker) => { + const markerPath = path.join(rootPath, marker); + return fs.existsSync(markerPath); + }); - return hasMarkerInRoot ? rootPath : null; + return hasMarkerInRoot ? rootPath : null; } // --- Dynamic Configuration Function --- (REMOVED) @@ -153,11 +153,11 @@ function findProjectRoot( // Set up logging based on log level const LOG_LEVELS = { - debug: 0, - info: 1, - warn: 2, - error: 3, - success: 1 // Treat success like info level + debug: 0, + info: 1, + warn: 2, + error: 3, + success: 1, // Treat success like info level }; /** @@ -165,21 +165,21 @@ const LOG_LEVELS = { * @returns {Promise} The task manager module object */ async function getTaskManager() { - return import('./task-manager.js'); + return import("./task-manager.js"); } /** * Enable silent logging mode */ function enableSilentMode() { - silentMode = true; + silentMode = true; } /** * Disable silent logging mode */ function disableSilentMode() { - silentMode = false; + silentMode = false; } /** @@ -187,7 +187,7 @@ function disableSilentMode() { * @returns {boolean} True if silent mode is enabled */ function isSilentMode() { - return silentMode; + return silentMode; } /** @@ -196,47 +196,71 @@ function isSilentMode() { * @param {...any} args - Arguments to log */ function log(level, ...args) { - // Immediately return if silentMode is enabled - if (isSilentMode()) { - return; - } + // Immediately return if silentMode is enabled + if (isSilentMode()) { + return; + } - // GUARD: Prevent circular dependency during config loading - // Use a simple fallback log level instead of calling getLogLevel() - 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'; - } catch (error) { - // If getLogLevel() fails (likely due to circular dependency), - // use default 'info' level and continue - configLevel = 'info'; - } + // GUARD: Prevent circular dependency during config loading + // Use a simple fallback log level instead of calling getLogLevel() + 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"; + } catch (error) { + // If getLogLevel() fails (likely due to circular dependency), + // use default 'info' level and continue + 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]') - }; + // 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]"), + }; - // Ensure level exists, default to info if not - const currentLevel = LOG_LEVELS.hasOwnProperty(level) ? level : 'info'; + // Ensure level exists, default to info if not + 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] || ''; - // 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(' '); - console.log(`${prefix} ${message}`); - } + // Check log level configuration + if ( + LOG_LEVELS[currentLevel] >= (LOG_LEVELS[configLevel] ?? LOG_LEVELS.info) + ) { + 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(" "); + 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); + } + }); + } + }); } /** @@ -245,21 +269,21 @@ function log(level, ...args) { * @returns {boolean} True if the data has a tagged structure */ function hasTaggedStructure(data) { - if (!data || typeof data !== 'object') { - return false; - } + if (!data || typeof data !== "object") { + return false; + } - // Check if any top-level properties are objects with tasks arrays - for (const key in data) { - if ( - data.hasOwnProperty(key) && - typeof data[key] === 'object' && - Array.isArray(data[key].tasks) - ) { - return true; - } - } - return false; + // Check if any top-level properties are objects with tasks arrays + for (const key in data) { + if ( + data.hasOwnProperty(key) && + typeof data[key] === "object" && + Array.isArray(data[key].tasks) + ) { + return true; + } + } + return false; } /** @@ -270,246 +294,252 @@ function hasTaggedStructure(data) { * @returns {Object|null} The parsed JSON data or null if error */ function readJSON(filepath, projectRoot = null, tag = null) { - // GUARD: Prevent circular dependency during config loading - let isDebug = false; // Default fallback - try { - // Only try to get debug flag if we're not in the middle of config loading - isDebug = getDebugFlag(); - } catch (error) { - // If getDebugFlag() fails (likely due to circular dependency), - // use default false and continue - } + // GUARD: Prevent circular dependency during config loading + let isDebug = false; // Default fallback + try { + // Only try to get debug flag if we're not in the middle of config loading + isDebug = getDebugFlag(); + } catch (error) { + // If getDebugFlag() fails (likely due to circular dependency), + // use default false and continue + } - if (isDebug) { - console.log( - `readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}` - ); - } + if (isDebug) { + console.log( + `readJSON called with: ${filepath}, projectRoot: ${projectRoot}, tag: ${tag}`, + ); + } - if (!filepath) { - return null; - } + if (!filepath) { + return null; + } - let data; - try { - data = JSON.parse(fs.readFileSync(filepath, 'utf8')); - if (isDebug) { - console.log(`Successfully read JSON from ${filepath}`); - } - } catch (err) { - if (isDebug) { - console.log(`Failed to read JSON from ${filepath}: ${err.message}`); - } - return null; - } + let data; + try { + data = JSON.parse(fs.readFileSync(filepath, "utf8")); + if (isDebug) { + console.log(`Successfully read JSON from ${filepath}`); + } + } catch (err) { + if (isDebug) { + console.log(`Failed to read JSON from ${filepath}: ${err.message}`); + } + return null; + } - // If it's not a tasks.json file, return as-is - if (!filepath.includes('tasks.json') || !data) { - if (isDebug) { - console.log(`File is not tasks.json or data is null, returning as-is`); - } - return data; - } + // If it's not a tasks.json file, return as-is + if (!filepath.includes("tasks.json") || !data) { + if (isDebug) { + console.log(`File is not tasks.json or data is null, returning as-is`); + } + return data; + } - // Check if this is legacy format that needs migration - // Only migrate if we have tasks at the ROOT level AND no tag-like structure - if ( - Array.isArray(data.tasks) && - !data._rawTaggedData && - !hasTaggedStructure(data) - ) { - if (isDebug) { - console.log(`File is in legacy format, performing migration...`); - } + // Check if this is legacy format that needs migration + // Only migrate if we have tasks at the ROOT level AND no tag-like structure + if ( + Array.isArray(data.tasks) && + !data._rawTaggedData && + !hasTaggedStructure(data) + ) { + if (isDebug) { + console.log(`File is in legacy format, performing migration...`); + } - // This is legacy format - migrate it to tagged format - const migratedData = { - master: { - tasks: data.tasks, - metadata: data.metadata || { - created: new Date().toISOString(), - updated: new Date().toISOString(), - description: 'Tasks for master context' - } - } - }; + // This is legacy format - migrate it to tagged format + const migratedData = { + master: { + tasks: data.tasks, + metadata: data.metadata || { + created: new Date().toISOString(), + updated: new Date().toISOString(), + description: "Tasks for master context", + }, + }, + }; - // Write the migrated data back to the file - try { - writeJSON(filepath, migratedData); - if (isDebug) { - console.log(`Successfully migrated legacy format to tagged format`); - } + // Write the migrated data back to the file + try { + writeJSON(filepath, migratedData); + if (isDebug) { + console.log(`Successfully migrated legacy format to tagged format`); + } - // Perform complete migration (config.json, state.json) - performCompleteTagMigration(filepath); + // Perform complete migration (config.json, state.json) + performCompleteTagMigration(filepath); - // Check and auto-switch git tags if enabled (after migration) - // This needs to run synchronously BEFORE tag resolution - if (projectRoot) { - try { - // Run git integration synchronously - gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath); - } catch (error) { - // Silent fail - don't break normal operations - } - } + // Check and auto-switch git tags if enabled (after migration) + // This needs to run synchronously BEFORE tag resolution + if (projectRoot) { + try { + // Run git integration synchronously + gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath); + } catch (error) { + // Silent fail - don't break normal operations + } + } - // Mark for migration notice - markMigrationForNotice(filepath); - } catch (writeError) { - if (isDebug) { - console.log(`Error writing migrated data: ${writeError.message}`); - } - // If write fails, continue with the original data - } + // Mark for migration notice + markMigrationForNotice(filepath); + } catch (writeError) { + if (isDebug) { + console.log(`Error writing migrated data: ${writeError.message}`); + } + // If write fails, continue with the original data + } - // Continue processing with the migrated data structure - data = migratedData; - } + // Continue processing with the migrated data structure + data = migratedData; + } - // If we have tagged data, we need to resolve which tag to use - if (typeof data === 'object' && !data.tasks) { - // This is tagged format - if (isDebug) { - console.log(`File is in tagged format, resolving tag...`); - } + // If we have tagged data, we need to resolve which tag to use + if (typeof data === "object" && !data.tasks) { + // This is tagged format + if (isDebug) { + console.log(`File is in tagged format, resolving tag...`); + } - // Ensure all tags have proper metadata before proceeding - for (const tagName in data) { - if ( - data.hasOwnProperty(tagName) && - 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 - }); - } catch (error) { - // If ensureTagMetadata fails, continue without metadata - if (isDebug) { - console.log( - `Failed to ensure metadata for tag ${tagName}: ${error.message}` - ); - } - } - } - } + // Ensure all tags have proper metadata before proceeding + for (const tagName in data) { + if ( + data.hasOwnProperty(tagName) && + 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 + }); + } catch (error) { + // If ensureTagMetadata fails, continue without metadata + if (isDebug) { + console.log( + `Failed to ensure metadata for tag ${tagName}: ${error.message}`, + ); + } + } + } + } - // Store reference to the raw tagged data for functions that need it - const originalTaggedData = JSON.parse(JSON.stringify(data)); + // Store reference to the raw tagged data for functions that need it + const originalTaggedData = JSON.parse(JSON.stringify(data)); - // Check and auto-switch git tags if enabled (for existing tagged format) - // This needs to run synchronously BEFORE tag resolution - if (projectRoot) { - try { - // Run git integration synchronously - gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath); - } catch (error) { - // Silent fail - don't break normal operations - } - } + // Check and auto-switch git tags if enabled (for existing tagged format) + // This needs to run synchronously BEFORE tag resolution + if (projectRoot) { + try { + // Run git integration synchronously + gitUtils.checkAndAutoSwitchGitTagSync(projectRoot, filepath); + } catch (error) { + // Silent fail - don't break normal operations + } + } - try { - // Default to master tag if anything goes wrong - let resolvedTag = 'master'; + try { + // Default to master tag if anything goes wrong + let resolvedTag = "master"; - // Try to resolve the correct tag, but don't fail if it doesn't work - try { - // If tag is provided, use it directly - if (tag) { - resolvedTag = tag; - } else if (projectRoot) { - // Use provided projectRoot - resolvedTag = resolveTag({ projectRoot }); - } else { - // Try to derive projectRoot from filepath - const derivedProjectRoot = findProjectRoot(path.dirname(filepath)); - if (derivedProjectRoot) { - resolvedTag = resolveTag({ projectRoot: derivedProjectRoot }); - } - // If derivedProjectRoot is null, stick with 'master' - } - } catch (tagResolveError) { - if (isDebug) { - console.log( - `Tag resolution failed, using master: ${tagResolveError.message}` - ); - } - // resolvedTag stays as 'master' - } + // Try to resolve the correct tag, but don't fail if it doesn't work + try { + // If tag is provided, use it directly + if (tag) { + resolvedTag = tag; + } else if (projectRoot) { + // Use provided projectRoot + resolvedTag = resolveTag({ projectRoot }); + } else { + // Try to derive projectRoot from filepath + const derivedProjectRoot = findProjectRoot(path.dirname(filepath)); + if (derivedProjectRoot) { + resolvedTag = resolveTag({ projectRoot: derivedProjectRoot }); + } + // If derivedProjectRoot is null, stick with 'master' + } + } catch (tagResolveError) { + if (isDebug) { + console.log( + `Tag resolution failed, using master: ${tagResolveError.message}`, + ); + } + // resolvedTag stays as 'master' + } - if (isDebug) { - console.log(`Resolved tag: ${resolvedTag}`); - } + if (isDebug) { + console.log(`Resolved tag: ${resolvedTag}`); + } - // Get the data for the resolved tag - const tagData = data[resolvedTag]; - if (tagData && tagData.tasks) { - // Add the _rawTaggedData property and the resolved tag to the returned data - const result = { - ...tagData, - tag: resolvedTag, - _rawTaggedData: originalTaggedData - }; - if (isDebug) { - console.log( - `Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks` - ); - } - return result; - } else { - // If the resolved tag doesn't exist, fall back to master - const masterData = data.master; - if (masterData && masterData.tasks) { - if (isDebug) { - console.log( - `Tag '${resolvedTag}' not found, falling back to master with ${masterData.tasks.length} tasks` - ); - } - return { - ...masterData, - tag: 'master', - _rawTaggedData: originalTaggedData - }; - } else { - if (isDebug) { - console.log(`No valid tag data found, returning empty structure`); - } - // Return empty structure if no valid data - return { - tasks: [], - tag: 'master', - _rawTaggedData: originalTaggedData - }; - } - } - } catch (error) { - if (isDebug) { - console.log(`Error during tag resolution: ${error.message}`); - } - // If anything goes wrong, try to return master or empty - const masterData = data.master; - if (masterData && masterData.tasks) { - return { - ...masterData, - _rawTaggedData: originalTaggedData - }; - } - return { - tasks: [], - _rawTaggedData: originalTaggedData - }; - } - } + // Get the data for the resolved tag + const tagData = data[resolvedTag]; + if (tagData && tagData.tasks) { + normalizeTaskIds(tagData.tasks); - // If we reach here, it's some other format - if (isDebug) { - console.log(`File format not recognized, returning as-is`); - } - return data; + // Add the _rawTaggedData property and the resolved tag to the returned data + const result = { + ...tagData, + tag: resolvedTag, + _rawTaggedData: originalTaggedData, + }; + if (isDebug) { + console.log( + `Returning data for tag '${resolvedTag}' with ${tagData.tasks.length} tasks`, + ); + } + return result; + } else { + // 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`, + ); + } + return { + ...masterData, + tag: "master", + _rawTaggedData: originalTaggedData, + }; + } else { + if (isDebug) { + console.log(`No valid tag data found, returning empty structure`); + } + // Return empty structure if no valid data + return { + tasks: [], + tag: "master", + _rawTaggedData: originalTaggedData, + }; + } + } + } catch (error) { + if (isDebug) { + console.log(`Error during tag resolution: ${error.message}`); + } + // 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, + }; + } + return { + tasks: [], + _rawTaggedData: originalTaggedData, + }; + } + } + + // If we reach here, it's some other format + if (isDebug) { + console.log(`File format not recognized, returning as-is`); + } + return data; } /** @@ -517,35 +547,35 @@ function readJSON(filepath, projectRoot = null, tag = null) { * @param {string} tasksJsonPath - Path to the tasks.json file that was migrated */ function performCompleteTagMigration(tasksJsonPath) { - try { - // Derive project root from tasks.json path - const projectRoot = - findProjectRoot(path.dirname(tasksJsonPath)) || - path.dirname(tasksJsonPath); + try { + // Derive project root from tasks.json path + const projectRoot = + findProjectRoot(path.dirname(tasksJsonPath)) || + path.dirname(tasksJsonPath); - // 1. Migrate config.json - add defaultTag and tags section - const configPath = path.join(projectRoot, '.taskmaster', 'config.json'); - if (fs.existsSync(configPath)) { - migrateConfigJson(configPath); - } + // 1. Migrate config.json - add defaultTag and tags section + 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'); - if (!fs.existsSync(statePath)) { - createStateJson(statePath); - } + // 2. Create state.json if it doesn't exist + 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}` - ); - } - } catch (error) { - if (getDebugFlag()) { - log('warn', `Error during complete tag migration: ${error.message}`); - } - } + if (getDebugFlag()) { + log( + "debug", + `Complete tag migration performed for project: ${projectRoot}`, + ); + } + } catch (error) { + if (getDebugFlag()) { + log("warn", `Error during complete tag migration: ${error.message}`); + } + } } /** @@ -553,35 +583,35 @@ function performCompleteTagMigration(tasksJsonPath) { * @param {string} configPath - Path to the config.json file */ function migrateConfigJson(configPath) { - try { - const rawConfig = fs.readFileSync(configPath, 'utf8'); - const config = JSON.parse(rawConfig); - if (!config) return; + try { + const rawConfig = fs.readFileSync(configPath, "utf8"); + const config = JSON.parse(rawConfig); + if (!config) return; - let modified = false; + let modified = false; - // Add global.defaultTag if missing - if (!config.global) { - config.global = {}; - } - if (!config.global.defaultTag) { - config.global.defaultTag = 'master'; - modified = true; - } + // Add global.defaultTag if missing + if (!config.global) { + config.global = {}; + } + if (!config.global.defaultTag) { + config.global.defaultTag = "master"; + modified = true; + } - if (modified) { - 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' - ); - } - } - } catch (error) { - if (process.env.TASKMASTER_DEBUG === 'true') { - console.warn(`[WARN] Error migrating config.json: ${error.message}`); - } - } + if (modified) { + 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", + ); + } + } + } catch (error) { + if (process.env.TASKMASTER_DEBUG === "true") { + console.warn(`[WARN] Error migrating config.json: ${error.message}`); + } + } } /** @@ -589,23 +619,23 @@ function migrateConfigJson(configPath) { * @param {string} statePath - Path where state.json should be created */ function createStateJson(statePath) { - try { - const initialState = { - currentTag: 'master', - lastSwitched: new Date().toISOString(), - branchTagMapping: {}, - migrationNoticeShown: false - }; + try { + const initialState = { + currentTag: "master", + lastSwitched: new Date().toISOString(), + branchTagMapping: {}, + 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'); - } - } catch (error) { - if (process.env.TASKMASTER_DEBUG === 'true') { - console.warn(`[WARN] Error creating state.json: ${error.message}`); - } - } + 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") { + console.warn(`[WARN] Error creating state.json: ${error.message}`); + } + } } /** @@ -613,38 +643,38 @@ function createStateJson(statePath) { * @param {string} tasksJsonPath - Path to the tasks.json file */ function markMigrationForNotice(tasksJsonPath) { - try { - const projectRoot = path.dirname(path.dirname(tasksJsonPath)); - const statePath = path.join(projectRoot, '.taskmaster', 'state.json'); + try { + const projectRoot = path.dirname(path.dirname(tasksJsonPath)); + const statePath = path.join(projectRoot, ".taskmaster", "state.json"); - // Ensure state.json exists - if (!fs.existsSync(statePath)) { - createStateJson(statePath); - } + // Ensure state.json exists + if (!fs.existsSync(statePath)) { + createStateJson(statePath); + } - // Read and update state to mark migration occurred using fs directly - try { - 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'); - } - } catch (stateError) { - if (process.env.TASKMASTER_DEBUG === 'true') { - console.warn( - `[WARN] Error updating state for migration notice: ${stateError.message}` - ); - } - } - } catch (error) { - if (process.env.TASKMASTER_DEBUG === 'true') { - console.warn( - `[WARN] Error marking migration for notice: ${error.message}` - ); - } - } + // Read and update state to mark migration occurred using fs directly + try { + 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"); + } + } catch (stateError) { + if (process.env.TASKMASTER_DEBUG === "true") { + console.warn( + `[WARN] Error updating state for migration notice: ${stateError.message}`, + ); + } + } + } catch (error) { + if (process.env.TASKMASTER_DEBUG === "true") { + console.warn( + `[WARN] Error marking migration for notice: ${error.message}`, + ); + } + } } /** @@ -655,111 +685,111 @@ 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; + try { + let finalData = data; - // If data represents resolved tag data but lost _rawTaggedData (edge-case observed in MCP path) - if ( - !data._rawTaggedData && - projectRoot && - Array.isArray(data.tasks) && - !hasTaggedStructure(data) - ) { - const resolvedTag = tag || getCurrentTag(projectRoot); + // If data represents resolved tag data but lost _rawTaggedData (edge-case observed in MCP path) + if ( + !data._rawTaggedData && + projectRoot && + Array.isArray(data.tasks) && + !hasTaggedStructure(data) + ) { + const resolvedTag = tag || getCurrentTag(projectRoot); - if (isDebug) { - console.log( - `writeJSON: Detected resolved tag data missing _rawTaggedData. Re-reading raw data to prevent data loss for tag '${resolvedTag}'.` - ); - } + if (isDebug) { + console.log( + `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')); + // Re-read the full file to get the complete tagged structure + const rawFullData = JSON.parse(fs.readFileSync(filepath, "utf8")); - // Merge the updated data into the full structure - finalData = { - ...rawFullData, - [resolvedTag]: { - // 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 - } - }; - } - // If we have _rawTaggedData, this means we're working with resolved tag data - // and need to merge it back into the full tagged structure - else if (data && data._rawTaggedData && projectRoot) { - const resolvedTag = tag || getCurrentTag(projectRoot); + // Merge the updated data into the full structure + finalData = { + ...rawFullData, + [resolvedTag]: { + // 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 + }, + }; + } + // If we have _rawTaggedData, this means we're working with resolved tag data + // and need to merge it back into the full tagged structure + else if (data && data._rawTaggedData && projectRoot) { + const resolvedTag = tag || getCurrentTag(projectRoot); - // Get the original tagged data - const originalTaggedData = data._rawTaggedData; + // Get the original tagged data + const originalTaggedData = data._rawTaggedData; - // Create a clean copy of the current resolved data (without internal properties) - const { _rawTaggedData, tag: _, ...cleanResolvedData } = data; + // Create a clean copy of the current resolved data (without internal properties) + const { _rawTaggedData, tag: _, ...cleanResolvedData } = data; - // Update the specific tag with the resolved data - finalData = { - ...originalTaggedData, - [resolvedTag]: cleanResolvedData - }; + // Update the specific tag with the resolved data + finalData = { + ...originalTaggedData, + [resolvedTag]: cleanResolvedData, + }; - if (isDebug) { - console.log( - `writeJSON: Merging resolved data back into tag '${resolvedTag}'` - ); - } - } + if (isDebug) { + console.log( + `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') { - // Remove any _rawTaggedData or tag properties from root level - const { _rawTaggedData, tag: tagProp, ...rootCleanData } = cleanData; - cleanData = rootCleanData; + // Clean up any internal properties that shouldn't be persisted + let cleanData = finalData; + 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)) { - const finalCleanData = {}; - for (const [key, value] of Object.entries(cleanData)) { - if ( - value && - typeof value === 'object' && - Array.isArray(value.tasks) - ) { - // This is a tag object - clean up any rogue root-level properties - const { created, description, ...cleanTagData } = value; + // Additional cleanup for tag objects + if (typeof cleanData === "object" && !Array.isArray(cleanData)) { + const finalCleanData = {}; + for (const [key, value] of Object.entries(cleanData)) { + if ( + value && + typeof value === "object" && + Array.isArray(value.tasks) + ) { + // This is a tag object - clean up any rogue root-level properties + const { created, description, ...cleanTagData } = value; - // Only keep the description if there's no metadata.description - if ( - description && - (!cleanTagData.metadata || !cleanTagData.metadata.description) - ) { - cleanTagData.description = description; - } + // Only keep the description if there's no metadata.description + if ( + description && + (!cleanTagData.metadata || !cleanTagData.metadata.description) + ) { + cleanTagData.description = description; + } - finalCleanData[key] = cleanTagData; - } else { - finalCleanData[key] = value; - } - } - cleanData = finalCleanData; - } - } + finalCleanData[key] = cleanTagData; + } else { + finalCleanData[key] = value; + } + } + cleanData = finalCleanData; + } + } - 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); - if (isDebug) { - log('error', 'Full error details:', error); - } - } + if (isDebug) { + console.log(`writeJSON: Successfully wrote to ${filepath}`); + } + } catch (error) { + log("error", `Error writing JSON file ${filepath}:`, error.message); + if (isDebug) { + log("error", "Full error details:", error); + } + } } /** @@ -768,8 +798,8 @@ function writeJSON(filepath, data, projectRoot = null, tag = null) { * @returns {string} Sanitized prompt */ function sanitizePrompt(prompt) { - // Replace double quotes with escaped double quotes - return prompt.replace(/"/g, '\\"'); + // Replace double quotes with escaped double quotes + return prompt.replace(/"/g, '\\"'); } /** @@ -778,50 +808,50 @@ function sanitizePrompt(prompt) { * @returns {Object|null} The parsed complexity report or null if not found */ function readComplexityReport(customPath = null) { - // GUARD: Prevent circular dependency during config loading - let isDebug = false; // Default fallback - try { - // Only try to get debug flag if we're not in the middle of config loading - isDebug = getDebugFlag(); - } catch (error) { - // If getDebugFlag() fails (likely due to circular dependency), - // use default false and continue - isDebug = false; - } + // GUARD: Prevent circular dependency during config loading + let isDebug = false; // Default fallback + try { + // Only try to get debug flag if we're not in the middle of config loading + isDebug = getDebugFlag(); + } catch (error) { + // If getDebugFlag() fails (likely due to circular dependency), + // use default false and continue + isDebug = false; + } - try { - let reportPath; - if (customPath) { - reportPath = customPath; - } else { - // Try new location first, then fall back to legacy - const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE); - const legacyPath = path.join( - process.cwd(), - LEGACY_COMPLEXITY_REPORT_FILE - ); + try { + let reportPath; + if (customPath) { + reportPath = customPath; + } else { + // Try new location first, then fall back to legacy + const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE); + const legacyPath = path.join( + process.cwd(), + LEGACY_COMPLEXITY_REPORT_FILE, + ); - reportPath = fs.existsSync(newPath) ? newPath : legacyPath; - } + reportPath = fs.existsSync(newPath) ? newPath : legacyPath; + } - if (!fs.existsSync(reportPath)) { - if (isDebug) { - log('debug', `Complexity report not found at ${reportPath}`); - } - return null; - } + if (!fs.existsSync(reportPath)) { + if (isDebug) { + log("debug", `Complexity report not found at ${reportPath}`); + } + return null; + } - const reportData = readJSON(reportPath); - if (isDebug) { - log('debug', `Successfully read complexity report from ${reportPath}`); - } - return reportData; - } catch (error) { - if (isDebug) { - log('error', `Error reading complexity report: ${error.message}`); - } - return null; - } + const reportData = readJSON(reportPath); + if (isDebug) { + log("debug", `Successfully read complexity report from ${reportPath}`); + } + return reportData; + } catch (error) { + if (isDebug) { + log("error", `Error reading complexity report: ${error.message}`); + } + return null; + } } /** @@ -831,31 +861,31 @@ function readComplexityReport(customPath = null) { * @returns {Object|null} The task analysis or null if not found */ function findTaskInComplexityReport(report, taskId) { - if ( - !report || - !report.complexityAnalysis || - !Array.isArray(report.complexityAnalysis) - ) { - return null; - } + if ( + !report || + !report.complexityAnalysis || + !Array.isArray(report.complexityAnalysis) + ) { + return null; + } - return report.complexityAnalysis.find((task) => task.taskId === taskId); + return report.complexityAnalysis.find((task) => task.taskId === taskId); } function addComplexityToTask(task, complexityReport) { - let taskId; - if (task.isSubtask) { - taskId = task.parentTask.id; - } else if (task.parentId) { - taskId = task.parentId; - } else { - taskId = task.id; - } + let taskId; + if (task.isSubtask) { + taskId = task.parentTask.id; + } else if (task.parentId) { + taskId = task.parentId; + } else { + taskId = task.id; + } - const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId); - if (taskAnalysis) { - task.complexityScore = taskAnalysis.complexityScore; - } + const taskAnalysis = findTaskInComplexityReport(complexityReport, taskId); + if (taskAnalysis) { + task.complexityScore = taskAnalysis.complexityScore; + } } /** @@ -865,26 +895,26 @@ function addComplexityToTask(task, complexityReport) { * @returns {boolean} True if the task exists, false otherwise */ function taskExists(tasks, taskId) { - if (!taskId || !tasks || !Array.isArray(tasks)) { - return false; - } + if (!taskId || !tasks || !Array.isArray(tasks)) { + return false; + } - // Handle both regular task IDs and subtask IDs (e.g., "1.2") - if (typeof taskId === 'string' && taskId.includes('.')) { - const [parentId, subtaskId] = taskId - .split('.') - .map((id) => parseInt(id, 10)); - const parentTask = tasks.find((t) => t.id === parentId); + // Handle both regular task IDs and subtask IDs (e.g., "1.2") + if (typeof taskId === "string" && taskId.includes(".")) { + const [parentId, subtaskId] = taskId + .split(".") + .map((id) => parseInt(id, 10)); + const parentTask = tasks.find((t) => t.id === parentId); - if (!parentTask || !parentTask.subtasks) { - return false; - } + if (!parentTask || !parentTask.subtasks) { + return false; + } - return parentTask.subtasks.some((st) => st.id === subtaskId); - } + return parentTask.subtasks.some((st) => st.id === subtaskId); + } - const id = parseInt(taskId, 10); - return tasks.some((t) => t.id === id); + const id = parseInt(taskId, 10); + return tasks.some((t) => t.id === id); } /** @@ -893,15 +923,15 @@ function taskExists(tasks, taskId) { * @returns {string} The formatted task ID */ function formatTaskId(id) { - if (typeof id === 'string' && id.includes('.')) { - return id; // Already formatted as a string with a dot (e.g., "1.2") - } + if (typeof id === "string" && id.includes(".")) { + return id; // Already formatted as a string with a dot (e.g., "1.2") + } - if (typeof id === 'number') { - return id.toString(); - } + if (typeof id === "number") { + return id.toString(); + } - return id; + return id; } /** @@ -913,89 +943,89 @@ function formatTaskId(id) { * @returns {{task: Object|null, originalSubtaskCount: number|null, originalSubtasks: Array|null}} The task object (potentially with filtered subtasks), the original subtask count, and original subtasks array if filtered, or nulls if not found. */ function findTaskById( - tasks, - taskId, - complexityReport = null, - statusFilter = null + tasks, + taskId, + complexityReport = null, + statusFilter = null, ) { - if (!taskId || !tasks || !Array.isArray(tasks)) { - return { task: null, originalSubtaskCount: 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 looking for a subtask, statusFilter doesn't apply directly here. - const [parentId, subtaskId] = taskId - .split('.') - .map((id) => parseInt(id, 10)); - const parentTask = tasks.find((t) => t.id === parentId); + // Check if it's a subtask ID (e.g., "1.2") + if (typeof taskId === "string" && taskId.includes(".")) { + // If looking for a subtask, statusFilter doesn't apply directly here. + const [parentId, subtaskId] = taskId + .split(".") + .map((id) => parseInt(id, 10)); + const parentTask = tasks.find((t) => t.id === parentId); - if (!parentTask || !parentTask.subtasks) { - return { task: null, originalSubtaskCount: null, originalSubtasks: null }; - } + if (!parentTask || !parentTask.subtasks) { + return { task: null, originalSubtaskCount: null, originalSubtasks: null }; + } - const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); - if (subtask) { - // Add reference to parent task for context - subtask.parentTask = { - id: parentTask.id, - title: parentTask.title, - status: parentTask.status - }; - subtask.isSubtask = true; - } + const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); + if (subtask) { + // Add reference to parent task for context + subtask.parentTask = { + id: parentTask.id, + title: parentTask.title, + status: parentTask.status, + }; + subtask.isSubtask = true; + } - // If we found a task, check for complexity data - if (subtask && complexityReport) { - addComplexityToTask(subtask, complexityReport); - } + // If we found a task, check for complexity data + if (subtask && complexityReport) { + addComplexityToTask(subtask, complexityReport); + } - return { - task: subtask || null, - originalSubtaskCount: null, - originalSubtasks: null - }; - } + return { + task: subtask || null, + originalSubtaskCount: null, + originalSubtasks: null, + }; + } - let taskResult = null; - let originalSubtaskCount = null; - let originalSubtasks = null; + let taskResult = null; + let originalSubtaskCount = null; + let originalSubtasks = null; - // Find the main task - const id = parseInt(taskId, 10); - const task = tasks.find((t) => t.id === id) || null; + // Find the main task + const id = parseInt(taskId, 10); + const task = tasks.find((t) => t.id === id) || null; - // If task not found, return nulls - if (!task) { - return { task: null, originalSubtaskCount: null, originalSubtasks: null }; - } + // If task not found, return nulls + if (!task) { + return { task: null, originalSubtaskCount: null, originalSubtasks: null }; + } - taskResult = task; + taskResult = task; - // If task found and statusFilter provided, filter its subtasks - if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) { - // Store original subtasks and count before filtering - originalSubtasks = [...task.subtasks]; // Clone the original subtasks array - originalSubtaskCount = task.subtasks.length; + // If task found and statusFilter provided, filter its subtasks + if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) { + // Store original subtasks and count before filtering + originalSubtasks = [...task.subtasks]; // Clone the original subtasks array + originalSubtaskCount = task.subtasks.length; - // Clone the task to avoid modifying the original array - const filteredTask = { ...task }; - filteredTask.subtasks = task.subtasks.filter( - (subtask) => - subtask.status && - subtask.status.toLowerCase() === statusFilter.toLowerCase() - ); + // Clone the task to avoid modifying the original array + const filteredTask = { ...task }; + filteredTask.subtasks = task.subtasks.filter( + (subtask) => + subtask.status && + subtask.status.toLowerCase() === statusFilter.toLowerCase(), + ); - taskResult = filteredTask; - } + taskResult = filteredTask; + } - // If task found and complexityReport provided, add complexity data - if (taskResult && complexityReport) { - addComplexityToTask(taskResult, complexityReport); - } + // If task found and complexityReport provided, add complexity data + if (taskResult && complexityReport) { + addComplexityToTask(taskResult, complexityReport); + } - // Return the found task, original subtask count, and original subtasks - return { task: taskResult, originalSubtaskCount, originalSubtasks }; + // Return the found task, original subtask count, and original subtasks + return { task: taskResult, originalSubtaskCount, originalSubtasks }; } /** @@ -1005,11 +1035,11 @@ function findTaskById( * @returns {string} The truncated text */ function truncate(text, maxLength) { - if (!text || text.length <= maxLength) { - return text; - } + if (!text || text.length <= maxLength) { + return text; + } - return `${text.slice(0, maxLength - 3)}...`; + return `${text.slice(0, maxLength - 3)}...`; } /** @@ -1018,13 +1048,13 @@ function truncate(text, maxLength) { * @returns {boolean} True if empty, false otherwise */ function isEmpty(value) { - if (Array.isArray(value)) { - return value.length === 0; - } else if (typeof value === 'object' && value !== null) { - return Object.keys(value).length === 0; - } + if (Array.isArray(value)) { + return value.length === 0; + } else if (typeof value === "object" && value !== null) { + return Object.keys(value).length === 0; + } - return false; // Not an array or object, or is null + return false; // Not an array or object, or is null } /** @@ -1036,46 +1066,46 @@ function isEmpty(value) { * @returns {Array} - List of dependency edges that need to be removed to break cycles */ function findCycles( - subtaskId, - dependencyMap, - visited = new Set(), - recursionStack = new Set(), - path = [] + subtaskId, + dependencyMap, + visited = new Set(), + recursionStack = new Set(), + path = [], ) { - // Mark the current node as visited and part of recursion stack - visited.add(subtaskId); - recursionStack.add(subtaskId); - path.push(subtaskId); + // Mark the current node as visited and part of recursion stack + visited.add(subtaskId); + recursionStack.add(subtaskId); + path.push(subtaskId); - const cyclesToBreak = []; + const cyclesToBreak = []; - // Get all dependencies of the current subtask - const dependencies = dependencyMap.get(subtaskId) || []; + // Get all dependencies of the current subtask + const dependencies = dependencyMap.get(subtaskId) || []; - // For each dependency - for (const depId of dependencies) { - // If not visited, recursively check for cycles - if (!visited.has(depId)) { - const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [ - ...path - ]); - cyclesToBreak.push(...cycles); - } - // If the dependency is in the recursion stack, we found a cycle - else if (recursionStack.has(depId)) { - // Find the position of the dependency in the path - const cycleStartIndex = path.indexOf(depId); - // The last edge in the cycle is what we want to remove - const cycleEdges = path.slice(cycleStartIndex); - // We'll remove the last edge in the cycle (the one that points back) - cyclesToBreak.push(depId); - } - } + // For each dependency + for (const depId of dependencies) { + // If not visited, recursively check for cycles + if (!visited.has(depId)) { + const cycles = findCycles(depId, dependencyMap, visited, recursionStack, [ + ...path, + ]); + cyclesToBreak.push(...cycles); + } + // If the dependency is in the recursion stack, we found a cycle + else if (recursionStack.has(depId)) { + // Find the position of the dependency in the path + const cycleStartIndex = path.indexOf(depId); + // The last edge in the cycle is what we want to remove + const cycleEdges = path.slice(cycleStartIndex); + // We'll remove the last edge in the cycle (the one that points back) + cyclesToBreak.push(depId); + } + } - // Remove the node from recursion stack before returning - recursionStack.delete(subtaskId); + // Remove the node from recursion stack before returning + recursionStack.delete(subtaskId); - return cyclesToBreak; + return cyclesToBreak; } /** @@ -1084,23 +1114,23 @@ function findCycles( * @returns {string} The kebab-case version of the string */ 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'); + // 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"); - // Insert hyphens before capital letters and convert to lowercase - return withReplacedAcronyms - .replace(/([A-Z])/g, '-$1') - .toLowerCase() - .replace(/^-/, ''); // Remove leading hyphen if present + // Insert hyphens before capital letters and convert to lowercase + return withReplacedAcronyms + .replace(/([A-Z])/g, "-$1") + .toLowerCase() + .replace(/^-/, ""); // Remove leading hyphen if present }; /** @@ -1109,29 +1139,29 @@ const toKebabCase = (str) => { * @returns {Array<{original: string, kebabCase: string}>} - List of flags that should be converted */ function detectCamelCaseFlags(args) { - const camelCaseFlags = []; - for (const arg of args) { - if (arg.startsWith('--')) { - const flagName = arg.split('=')[0].slice(2); // Remove -- and anything after = + const camelCaseFlags = []; + for (const arg of args) { + 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)) { - continue; - } + // Skip single-word flags - they can't be camelCase + if (!flagName.includes("-") && !/[A-Z]/.test(flagName)) { + continue; + } - // Check for camelCase pattern (lowercase followed by uppercase) - if (/[a-z][A-Z]/.test(flagName)) { - const kebabVersion = toKebabCase(flagName); - if (kebabVersion !== flagName) { - camelCaseFlags.push({ - original: flagName, - kebabCase: kebabVersion - }); - } - } - } - } - return camelCaseFlags; + // Check for camelCase pattern (lowercase followed by uppercase) + if (/[a-z][A-Z]/.test(flagName)) { + const kebabVersion = toKebabCase(flagName); + if (kebabVersion !== flagName) { + camelCaseFlags.push({ + original: flagName, + kebabCase: kebabVersion, + }); + } + } + } + } + return camelCaseFlags; } /** @@ -1141,52 +1171,52 @@ function detectCamelCaseFlags(args) { * @returns {Object|null} Aggregated telemetry object or null if input is empty. */ function aggregateTelemetry(telemetryArray, overallCommandName) { - if (!telemetryArray || telemetryArray.length === 0) { - return null; - } + if (!telemetryArray || telemetryArray.length === 0) { + return null; + } - const aggregated = { - 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 - inputTokens: 0, - outputTokens: 0, - totalTokens: 0, - totalCost: 0, - currency: telemetryArray[0].currency || 'USD' // Assume consistent currency or default - }; + const aggregated = { + 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 + inputTokens: 0, + outputTokens: 0, + totalTokens: 0, + totalCost: 0, + currency: telemetryArray[0].currency || "USD", // Assume consistent currency or default + }; - const uniqueModels = new Set(); - const uniqueProviders = new Set(); - const uniqueCurrencies = new Set(); + const uniqueModels = new Set(); + const uniqueProviders = new Set(); + const uniqueCurrencies = new Set(); - telemetryArray.forEach((item) => { - aggregated.inputTokens += item.inputTokens || 0; - aggregated.outputTokens += item.outputTokens || 0; - aggregated.totalCost += item.totalCost || 0; - uniqueModels.add(item.modelUsed); - uniqueProviders.add(item.providerName); - uniqueCurrencies.add(item.currency || 'USD'); - }); + telemetryArray.forEach((item) => { + aggregated.inputTokens += item.inputTokens || 0; + aggregated.outputTokens += item.outputTokens || 0; + aggregated.totalCost += item.totalCost || 0; + uniqueModels.add(item.modelUsed); + uniqueProviders.add(item.providerName); + uniqueCurrencies.add(item.currency || "USD"); + }); - aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens; - aggregated.totalCost = parseFloat(aggregated.totalCost.toFixed(6)); // Fix precision + aggregated.totalTokens = aggregated.inputTokens + aggregated.outputTokens; + aggregated.totalCost = parseFloat(aggregated.totalCost.toFixed(6)); // Fix precision - if (uniqueModels.size === 1) { - aggregated.modelUsed = [...uniqueModels][0]; - } - if (uniqueProviders.size === 1) { - aggregated.providerName = [...uniqueProviders][0]; - } - if (uniqueCurrencies.size > 1) { - aggregated.currency = 'Multiple'; // Mark if currencies actually differ - } else if (uniqueCurrencies.size === 1) { - aggregated.currency = [...uniqueCurrencies][0]; - } + if (uniqueModels.size === 1) { + aggregated.modelUsed = [...uniqueModels][0]; + } + if (uniqueProviders.size === 1) { + aggregated.providerName = [...uniqueProviders][0]; + } + if (uniqueCurrencies.size > 1) { + aggregated.currency = "Multiple"; // Mark if currencies actually differ + } else if (uniqueCurrencies.size === 1) { + aggregated.currency = [...uniqueCurrencies][0]; + } - return aggregated; + return aggregated; } /** @@ -1196,40 +1226,40 @@ function aggregateTelemetry(telemetryArray, overallCommandName) { * @returns {string} The current tag name */ function getCurrentTag(projectRoot) { - if (!projectRoot) { - throw new Error('projectRoot is required for getCurrentTag'); - } + if (!projectRoot) { + 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'); - if (fs.existsSync(statePath)) { - const rawState = fs.readFileSync(statePath, 'utf8'); - const stateData = JSON.parse(rawState); - if (stateData && stateData.currentTag) { - return stateData.currentTag; - } - } - } catch (error) { - // Ignore errors, fall back to default - } + try { + // Try to read current tag from state.json using fs directly + const statePath = path.join(projectRoot, ".taskmaster", "state.json"); + if (fs.existsSync(statePath)) { + const rawState = fs.readFileSync(statePath, "utf8"); + const stateData = JSON.parse(rawState); + if (stateData && stateData.currentTag) { + return stateData.currentTag; + } + } + } catch (error) { + // Ignore errors, fall back to default + } - // Fall back to defaultTag from config using fs directly - try { - const configPath = path.join(projectRoot, '.taskmaster', 'config.json'); - if (fs.existsSync(configPath)) { - const rawConfig = fs.readFileSync(configPath, 'utf8'); - const configData = JSON.parse(rawConfig); - if (configData && configData.global && configData.global.defaultTag) { - return configData.global.defaultTag; - } - } - } catch (error) { - // Ignore errors, use hardcoded default - } + // Fall back to defaultTag from config using fs directly + try { + const configPath = path.join(projectRoot, ".taskmaster", "config.json"); + if (fs.existsSync(configPath)) { + const rawConfig = fs.readFileSync(configPath, "utf8"); + const configData = JSON.parse(rawConfig); + if (configData && configData.global && configData.global.defaultTag) { + return configData.global.defaultTag; + } + } + } catch (error) { + // Ignore errors, use hardcoded default + } - // Final fallback - return 'master'; + // Final fallback + return "master"; } /** @@ -1240,19 +1270,19 @@ function getCurrentTag(projectRoot) { * @returns {string} The resolved tag name */ function resolveTag(options = {}) { - const { projectRoot, tag } = options; + const { projectRoot, tag } = options; - if (!projectRoot) { - throw new Error('projectRoot is required for resolveTag'); - } + if (!projectRoot) { + throw new Error("projectRoot is required for resolveTag"); + } - // If explicit tag provided, use it - if (tag) { - return tag; - } + // If explicit tag provided, use it + if (tag) { + return tag; + } - // Otherwise get current tag from state/config - return getCurrentTag(projectRoot); + // Otherwise get current tag from state/config + return getCurrentTag(projectRoot); } /** @@ -1262,20 +1292,20 @@ function resolveTag(options = {}) { * @returns {Array} The tasks array for the specified tag, or empty array if not found */ function getTasksForTag(data, tagName) { - if (!data || !tagName) { - return []; - } + if (!data || !tagName) { + return []; + } - // Handle migrated format: { "master": { "tasks": [...] }, "otherTag": { "tasks": [...] } } - if ( - data[tagName] && - data[tagName].tasks && - Array.isArray(data[tagName].tasks) - ) { - return data[tagName].tasks; - } + // Handle migrated format: { "master": { "tasks": [...] }, "otherTag": { "tasks": [...] } } + if ( + data[tagName] && + data[tagName].tasks && + Array.isArray(data[tagName].tasks) + ) { + return data[tagName].tasks; + } - return []; + return []; } /** @@ -1286,16 +1316,16 @@ function getTasksForTag(data, tagName) { * @returns {Object} The updated data object */ function setTasksForTag(data, tagName, tasks) { - if (!data) { - data = {}; - } + if (!data) { + data = {}; + } - if (!data[tagName]) { - data[tagName] = {}; - } + if (!data[tagName]) { + data[tagName] = {}; + } - data[tagName].tasks = tasks || []; - return data; + data[tagName].tasks = tasks || []; + return data; } /** @@ -1304,34 +1334,34 @@ function setTasksForTag(data, tagName, tasks) { * @returns {Array} Flattened array including both tasks and subtasks */ function flattenTasksWithSubtasks(tasks) { - const flattened = []; + const flattened = []; - for (const task of tasks) { - // Add the main task - flattened.push({ - ...task, - searchableId: task.id.toString(), // For consistent ID handling - isSubtask: false - }); + for (const task of tasks) { + // Add the main task + flattened.push({ + ...task, + searchableId: task.id.toString(), // For consistent ID handling + isSubtask: false, + }); - // Add subtasks if they exist - if (task.subtasks && task.subtasks.length > 0) { - for (const subtask of task.subtasks) { - flattened.push({ - ...subtask, - searchableId: `${task.id}.${subtask.id}`, // Format: "15.2" - isSubtask: true, - parentId: task.id, - parentTitle: task.title, - // Enhance subtask context with parent information - title: `${subtask.title} (subtask of: ${task.title})`, - description: `${subtask.description} [Parent: ${task.description}]` - }); - } - } - } + // Add subtasks if they exist + if (task.subtasks && task.subtasks.length > 0) { + for (const subtask of task.subtasks) { + flattened.push({ + ...subtask, + searchableId: `${task.id}.${subtask.id}`, // Format: "15.2" + isSubtask: true, + parentId: task.id, + parentTitle: task.title, + // Enhance subtask context with parent information + title: `${subtask.title} (subtask of: ${task.title})`, + description: `${subtask.description} [Parent: ${task.description}]`, + }); + } + } + } - return flattened; + return flattened; } /** @@ -1343,74 +1373,74 @@ 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(); + const now = new Date().toISOString(); - if (!tagObj.metadata) { - // Create new metadata object - tagObj.metadata = { - created: now, - updated: now, - ...(opts.description ? { description: opts.description } : {}) - }; - } else { - // Ensure existing metadata has required fields - if (!tagObj.metadata.created) { - tagObj.metadata.created = now; - } + if (!tagObj.metadata) { + // Create new metadata object + tagObj.metadata = { + created: now, + updated: now, + ...(opts.description ? { description: opts.description } : {}), + }; + } else { + // Ensure existing metadata has required fields + if (!tagObj.metadata.created) { + tagObj.metadata.created = now; + } - // Update timestamp unless explicitly skipped - if (!opts.skipUpdate) { - tagObj.metadata.updated = now; - } + // Update timestamp unless explicitly skipped + if (!opts.skipUpdate) { + tagObj.metadata.updated = now; + } - // Add description if provided and not already present - if (opts.description && !tagObj.metadata.description) { - tagObj.metadata.description = opts.description; - } - } + // Add description if provided and not already present + if (opts.description && !tagObj.metadata.description) { + tagObj.metadata.description = opts.description; + } + } - return tagObj; + return tagObj; } // Export all utility functions and configuration export { - LOG_LEVELS, - log, - readJSON, - writeJSON, - sanitizePrompt, - readComplexityReport, - findTaskInComplexityReport, - taskExists, - formatTaskId, - findTaskById, - truncate, - isEmpty, - findCycles, - toKebabCase, - detectCamelCaseFlags, - disableSilentMode, - enableSilentMode, - getTaskManager, - isSilentMode, - addComplexityToTask, - resolveEnvVariable, - findProjectRoot, - getTagAwareFilePath, - slugifyTagForFilePath, - aggregateTelemetry, - getCurrentTag, - resolveTag, - getTasksForTag, - setTasksForTag, - performCompleteTagMigration, - migrateConfigJson, - createStateJson, - markMigrationForNotice, - flattenTasksWithSubtasks, - ensureTagMetadata + LOG_LEVELS, + log, + readJSON, + writeJSON, + sanitizePrompt, + readComplexityReport, + findTaskInComplexityReport, + taskExists, + formatTaskId, + findTaskById, + truncate, + isEmpty, + findCycles, + toKebabCase, + detectCamelCaseFlags, + disableSilentMode, + enableSilentMode, + getTaskManager, + isSilentMode, + addComplexityToTask, + resolveEnvVariable, + findProjectRoot, + getTagAwareFilePath, + slugifyTagForFilePath, + aggregateTelemetry, + getCurrentTag, + resolveTag, + getTasksForTag, + setTasksForTag, + performCompleteTagMigration, + migrateConfigJson, + createStateJson, + markMigrationForNotice, + flattenTasksWithSubtasks, + ensureTagMetadata, };