Don't mess up formatting

This commit is contained in:
Carl Mercier
2025-07-26 00:13:54 -05:00
committed by Ralph Khreish
parent 0609b9ae28
commit 3fa0c7f7af

View File

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