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