Don't mess up formatting
This commit is contained in:
committed by
Ralph Khreish
parent
0609b9ae28
commit
3fa0c7f7af
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user