chore: apply requested changes

This commit is contained in:
Ralph Khreish
2025-11-25 18:26:29 +01:00
parent b0c0a780a1
commit 23b49a24f5
9 changed files with 98 additions and 51 deletions

View File

@@ -8,4 +8,4 @@ Bring back `task-master generate` as a command and mcp tool (after popular deman
- They also follow the markdownlint format making them look like more standard md files - They also follow the markdownlint format making them look like more standard md files
- added parameters to generate allowing you to generate with the `--tag` flag - added parameters to generate allowing you to generate with the `--tag` flag
- If I am on an active tag and want to generate files from another tag, I can with the tag parameter - If I am on an active tag and want to generate files from another tag, I can with the tag parameter
- See `--help` on more information on how `task-master generate --help` works - See `task-master generate --help` for more information.

View File

@@ -181,17 +181,7 @@ describe('generate command', () => {
const testData = { const testData = {
master: { tasks: [], metadata: {} }, master: { tasks: [], metadata: {} },
'feature-branch': { 'feature-branch': {
tasks: [ tasks: [createTask({ id: 1, title: 'Test Task', status: 'pending' })],
{
id: '1',
title: 'Test Task',
description: 'Test',
status: 'pending',
priority: 'medium',
dependencies: [],
subtasks: []
}
],
metadata: {} metadata: {}
} }
}; };
@@ -219,4 +209,16 @@ describe('generate command', () => {
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
expect(output).toContain('.taskmaster/tasks'); expect(output).toContain('.taskmaster/tasks');
}); });
it('should exit with non-zero code for invalid --format value', () => {
const testData = createTasksFile({
tasks: [createTask({ id: 1, title: 'Test Task', status: 'pending' })]
});
writeTasks(testData);
const { output, exitCode } = runGenerate('--format invalid');
expect(exitCode).not.toBe(0);
expect(output.toLowerCase()).toMatch(/invalid|error|format/);
});
}); });

View File

@@ -10,10 +10,6 @@ import type { ToolContext } from '../../shared/types.js';
import type { FastMCP } from 'fastmcp'; import type { FastMCP } from 'fastmcp';
const GenerateSchema = z.object({ const GenerateSchema = z.object({
file: z
.string()
.optional()
.describe('Absolute path to the tasks file (default: tasks/tasks.json)'),
output: z output: z
.string() .string()
.optional() .optional()

View File

@@ -177,17 +177,7 @@ describe('generate MCP tool', () => {
const testData = { const testData = {
master: { tasks: [], metadata: {} }, master: { tasks: [], metadata: {} },
'feature-branch': { 'feature-branch': {
tasks: [ tasks: [createTask({ id: 1, title: 'Test Task', status: 'pending' })],
{
id: '1',
title: 'Test Task',
description: 'Test',
status: 'pending',
priority: 'medium',
dependencies: [],
subtasks: []
}
],
metadata: {} metadata: {}
} }
}; };

View File

@@ -53,7 +53,7 @@ import {
} from '@tm/mcp'; } from '@tm/mcp';
/** /**
* Comprehensive tool registry mapping all 44 tool names to their registration functions * Comprehensive tool registry mapping tool names to their registration functions
* Used for dynamic tool registration and validation * Used for dynamic tool registration and validation
*/ */
export const toolRegistry = { export const toolRegistry = {

View File

@@ -3,7 +3,7 @@
* Generates individual markdown task files from tasks.json * Generates individual markdown task files from tasks.json
*/ */
import fs from 'node:fs'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import type { Task, Subtask, TaskStatus } from '../../../common/types/index.js'; import type { Task, Subtask, TaskStatus } from '../../../common/types/index.js';
import type { IStorage } from '../../../common/interfaces/storage.interface.js'; import type { IStorage } from '../../../common/interfaces/storage.interface.js';
@@ -24,14 +24,16 @@ export interface GenerateTaskFilesOptions {
*/ */
export interface GenerateTaskFilesResult { export interface GenerateTaskFilesResult {
success: boolean; success: boolean;
/** Number of task files generated */ /** Number of task files successfully generated */
count: number; count: number;
/** Output directory where files were written */ /** Output directory where files were written */
directory: string; directory: string;
/** Number of orphaned files cleaned up */ /** Number of orphaned files cleaned up */
orphanedFilesRemoved: number; orphanedFilesRemoved: number;
/** Error message if generation failed */ /** Error message if generation failed completely */
error?: string; error?: string;
/** Individual file write errors (task ID -> error message) */
fileErrors?: Record<string, string>;
} }
/** /**
@@ -60,9 +62,7 @@ export class TaskFileGeneratorService {
try { try {
// Ensure output directory exists // Ensure output directory exists
if (!fs.existsSync(outputDir)) { await fs.mkdir(outputDir, { recursive: true });
fs.mkdirSync(outputDir, { recursive: true });
}
// Load tasks from storage // Load tasks from storage
const tasks = await this.storage.loadTasks(tag); const tasks = await this.storage.loadTasks(tag);
@@ -77,26 +77,43 @@ export class TaskFileGeneratorService {
} }
// Clean up orphaned task files // Clean up orphaned task files
const orphanedCount = this.cleanupOrphanedFiles( const orphanedCount = await this.cleanupOrphanedFiles(
outputDir, outputDir,
tasks, tasks,
tag tag
); );
// Generate task files // Generate task files in parallel with individual error handling
for (const task of tasks) { // This allows partial success - some files can be written even if others fail
const content = this.formatTaskContent(task, tasks); const fileErrors: Record<string, string> = {};
const fileName = this.getTaskFileName(task.id, tag); const results = await Promise.allSettled(
const filePath = path.join(outputDir, fileName); tasks.map(async (task) => {
const content = this.formatTaskContent(task, tasks);
const fileName = this.getTaskFileName(task.id, tag);
const filePath = path.join(outputDir, fileName);
await fs.writeFile(filePath, content, 'utf-8');
return task.id;
})
);
fs.writeFileSync(filePath, content, 'utf-8'); // Count successes and collect errors
let successCount = 0;
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.status === 'fulfilled') {
successCount++;
} else {
const taskId = String(tasks[i].id);
fileErrors[taskId] = result.reason?.message || 'Unknown error';
}
} }
return { return {
success: true, success: Object.keys(fileErrors).length === 0,
count: tasks.length, count: successCount,
directory: outputDir, directory: outputDir,
orphanedFilesRemoved: orphanedCount orphanedFilesRemoved: orphanedCount,
...(Object.keys(fileErrors).length > 0 && { fileErrors })
}; };
} catch (error: any) { } catch (error: any) {
return { return {
@@ -132,21 +149,24 @@ export class TaskFileGeneratorService {
* Clean up orphaned task files (files for tasks that no longer exist) * Clean up orphaned task files (files for tasks that no longer exist)
* @returns Number of files removed * @returns Number of files removed
*/ */
private cleanupOrphanedFiles( private async cleanupOrphanedFiles(
outputDir: string, outputDir: string,
tasks: Task[], tasks: Task[],
tag: string tag: string
): number { ): Promise<number> {
let removedCount = 0; let removedCount = 0;
try { try {
const files = fs.readdirSync(outputDir); const files = await fs.readdir(outputDir);
const validTaskIds = tasks.map((task) => String(task.id)); const validTaskIds = tasks.map((task) => String(task.id));
// Tag-aware file patterns // Tag-aware file patterns
const masterFilePattern = /^task_(\d+)\.md$/; const masterFilePattern = /^task_(\d+)\.md$/;
const taggedFilePattern = new RegExp(`^task_(\\d+)_${this.escapeRegExp(tag)}\\.md$`); const taggedFilePattern = new RegExp(`^task_(\\d+)_${this.escapeRegExp(tag)}\\.md$`);
// Collect files to remove
const filesToRemove: string[] = [];
for (const file of files) { for (const file of files) {
let match = null; let match = null;
let fileTaskId: string | null = null; let fileTaskId: string | null = null;
@@ -169,12 +189,19 @@ export class TaskFileGeneratorService {
// Convert to integer for comparison (removes leading zeros) // Convert to integer for comparison (removes leading zeros)
const normalizedId = String(parseInt(fileTaskId, 10)); const normalizedId = String(parseInt(fileTaskId, 10));
if (!validTaskIds.includes(normalizedId)) { if (!validTaskIds.includes(normalizedId)) {
const filePath = path.join(outputDir, file); filesToRemove.push(file);
fs.unlinkSync(filePath);
removedCount++;
} }
} }
} }
// Remove files in parallel
await Promise.all(
filesToRemove.map(async (file) => {
const filePath = path.join(outputDir, file);
await fs.unlink(filePath);
})
);
removedCount = filesToRemove.length;
} catch (error) { } catch (error) {
// Ignore errors during cleanup - non-critical operation // Ignore errors during cleanup - non-critical operation
} }

View File

@@ -792,4 +792,15 @@ export class TaskService {
); );
} }
} }
/**
* Close and cleanup resources
* Releases file locks and other storage resources
*/
async close(): Promise<void> {
if (this.storage) {
await this.storage.close();
}
this.initialized = false;
}
} }

View File

@@ -393,7 +393,7 @@ export class TasksDomain {
/** /**
* Generate individual task markdown files from tasks.json * Generate individual task markdown files from tasks.json
* This writes .txt files for each task in the tasks directory. * This writes .md files for each task in the tasks directory.
* *
* Note: Only applicable for file storage. API storage throws an error. * Note: Only applicable for file storage. API storage throws an error.
* *
@@ -417,4 +417,14 @@ export class TasksDomain {
return this.taskFileGenerator.generateTaskFiles(options); return this.taskFileGenerator.generateTaskFiles(options);
} }
// ========== Cleanup ==========
/**
* Close and cleanup resources
* Releases file locks and other storage resources
*/
async close(): Promise<void> {
await this.taskService.close();
}
} }

View File

@@ -203,6 +203,17 @@ export class TmCore {
get projectPath(): string { get projectPath(): string {
return this._projectPath; return this._projectPath;
} }
/**
* Close and cleanup resources
* Releases file locks and other storage resources
* Should be called when done using TmCore, especially in tests
*/
async close(): Promise<void> {
if (this._tasks) {
await this._tasks.close();
}
}
} }
/** /**