diff --git a/.changeset/add-modifyjson-utils.md b/.changeset/add-modifyjson-utils.md new file mode 100644 index 00000000..01721485 --- /dev/null +++ b/.changeset/add-modifyjson-utils.md @@ -0,0 +1,5 @@ +--- +"task-master-ai": patch +--- + +Add modifyJSON function for safer file updates diff --git a/packages/tm-core/src/index.ts b/packages/tm-core/src/index.ts index 14636e6d..4314a491 100644 --- a/packages/tm-core/src/index.ts +++ b/packages/tm-core/src/index.ts @@ -49,6 +49,9 @@ export type { // Storage adapters - FileStorage for direct local file access export { FileStorage } from './modules/storage/index.js'; +// File operations - for atomic file modifications +export { FileOperations } from './modules/storage/adapters/file-storage/file-operations.js'; + // Constants export * from './common/constants/index.js'; diff --git a/scripts/modules/utils.js b/scripts/modules/utils.js index 5ef1f814..51365a4f 100644 --- a/scripts/modules/utils.js +++ b/scripts/modules/utils.js @@ -14,6 +14,8 @@ import { } from '../../src/constants/paths.js'; // Import specific config getters needed here import { getDebugFlag, getLogLevel } from './config-manager.js'; +// Import FileOperations from tm-core for atomic file modifications +import { FileOperations } from '@tm/core'; import * as gitUtils from './utils/git-utils.js'; // Global silent mode flag @@ -973,9 +975,45 @@ function markMigrationForNotice(tasksJsonPath) { } } +// Shared FileOperations instance for modifyJSON +let _fileOps = null; + +/** + * Gets or creates the shared FileOperations instance + * @returns {FileOperations} The shared FileOperations instance + */ +function getFileOps() { + if (!_fileOps) { + _fileOps = new FileOperations(); + } + return _fileOps; +} + +/** + * Atomically modifies a JSON file using a callback pattern. + * This is the safe way to update JSON files - it reads, modifies, and writes + * all within a single lock, preventing race conditions. + * + * Uses FileOperations from @tm/core for proper cross-process locking. + * + * @param {string} filepath - Path to the JSON file + * @param {Function} modifier - Async callback that receives current data and returns modified data. + * Signature: (currentData: Object) => Object | Promise + * @returns {Promise} + */ +async function modifyJSON(filepath, modifier) { + const fileOps = getFileOps(); + await fileOps.modifyJson(filepath, modifier); +} + /** * Writes and saves a JSON file. Handles tagged task lists properly. * Uses cross-process file locking and atomic writes to prevent race conditions. + * + * @deprecated For new code, prefer modifyJSON() which provides atomic read-modify-write. + * This function is maintained for backwards compatibility but callers should migrate + * to modifyJSON() to prevent race conditions from stale reads. + * * @param {string} filepath - Path to the JSON file * @param {Object} data - Data to write (can be resolved tag data or raw tagged data) * @param {string} projectRoot - Optional project root for tag context @@ -1921,6 +1959,7 @@ export { log, readJSON, writeJSON, + modifyJSON, sanitizePrompt, readComplexityReport, findTaskInComplexityReport,