mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-30 06:12:05 +00:00
chore: apply requested changes
This commit is contained in:
@@ -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
|
||||
- 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
|
||||
- See `--help` on more information on how `task-master generate --help` works
|
||||
- See `task-master generate --help` for more information.
|
||||
|
||||
@@ -181,17 +181,7 @@ describe('generate command', () => {
|
||||
const testData = {
|
||||
master: { tasks: [], metadata: {} },
|
||||
'feature-branch': {
|
||||
tasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
tasks: [createTask({ id: 1, title: 'Test Task', status: 'pending' })],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
@@ -219,4 +209,16 @@ describe('generate command', () => {
|
||||
expect(exitCode).toBe(0);
|
||||
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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,10 +10,6 @@ import type { ToolContext } from '../../shared/types.js';
|
||||
import type { FastMCP } from 'fastmcp';
|
||||
|
||||
const GenerateSchema = z.object({
|
||||
file: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Absolute path to the tasks file (default: tasks/tasks.json)'),
|
||||
output: z
|
||||
.string()
|
||||
.optional()
|
||||
|
||||
@@ -177,17 +177,7 @@ describe('generate MCP tool', () => {
|
||||
const testData = {
|
||||
master: { tasks: [], metadata: {} },
|
||||
'feature-branch': {
|
||||
tasks: [
|
||||
{
|
||||
id: '1',
|
||||
title: 'Test Task',
|
||||
description: 'Test',
|
||||
status: 'pending',
|
||||
priority: 'medium',
|
||||
dependencies: [],
|
||||
subtasks: []
|
||||
}
|
||||
],
|
||||
tasks: [createTask({ id: 1, title: 'Test Task', status: 'pending' })],
|
||||
metadata: {}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -53,7 +53,7 @@ import {
|
||||
} 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
|
||||
*/
|
||||
export const toolRegistry = {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 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 type { Task, Subtask, TaskStatus } from '../../../common/types/index.js';
|
||||
import type { IStorage } from '../../../common/interfaces/storage.interface.js';
|
||||
@@ -24,14 +24,16 @@ export interface GenerateTaskFilesOptions {
|
||||
*/
|
||||
export interface GenerateTaskFilesResult {
|
||||
success: boolean;
|
||||
/** Number of task files generated */
|
||||
/** Number of task files successfully generated */
|
||||
count: number;
|
||||
/** Output directory where files were written */
|
||||
directory: string;
|
||||
/** Number of orphaned files cleaned up */
|
||||
orphanedFilesRemoved: number;
|
||||
/** Error message if generation failed */
|
||||
/** Error message if generation failed completely */
|
||||
error?: string;
|
||||
/** Individual file write errors (task ID -> error message) */
|
||||
fileErrors?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,9 +62,7 @@ export class TaskFileGeneratorService {
|
||||
|
||||
try {
|
||||
// Ensure output directory exists
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
// Load tasks from storage
|
||||
const tasks = await this.storage.loadTasks(tag);
|
||||
@@ -77,26 +77,43 @@ export class TaskFileGeneratorService {
|
||||
}
|
||||
|
||||
// Clean up orphaned task files
|
||||
const orphanedCount = this.cleanupOrphanedFiles(
|
||||
const orphanedCount = await this.cleanupOrphanedFiles(
|
||||
outputDir,
|
||||
tasks,
|
||||
tag
|
||||
);
|
||||
|
||||
// Generate task files
|
||||
for (const task of tasks) {
|
||||
const content = this.formatTaskContent(task, tasks);
|
||||
const fileName = this.getTaskFileName(task.id, tag);
|
||||
const filePath = path.join(outputDir, fileName);
|
||||
// Generate task files in parallel with individual error handling
|
||||
// This allows partial success - some files can be written even if others fail
|
||||
const fileErrors: Record<string, string> = {};
|
||||
const results = await Promise.allSettled(
|
||||
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 {
|
||||
success: true,
|
||||
count: tasks.length,
|
||||
success: Object.keys(fileErrors).length === 0,
|
||||
count: successCount,
|
||||
directory: outputDir,
|
||||
orphanedFilesRemoved: orphanedCount
|
||||
orphanedFilesRemoved: orphanedCount,
|
||||
...(Object.keys(fileErrors).length > 0 && { fileErrors })
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
@@ -132,21 +149,24 @@ export class TaskFileGeneratorService {
|
||||
* Clean up orphaned task files (files for tasks that no longer exist)
|
||||
* @returns Number of files removed
|
||||
*/
|
||||
private cleanupOrphanedFiles(
|
||||
private async cleanupOrphanedFiles(
|
||||
outputDir: string,
|
||||
tasks: Task[],
|
||||
tag: string
|
||||
): number {
|
||||
): Promise<number> {
|
||||
let removedCount = 0;
|
||||
|
||||
try {
|
||||
const files = fs.readdirSync(outputDir);
|
||||
const files = await fs.readdir(outputDir);
|
||||
const validTaskIds = tasks.map((task) => String(task.id));
|
||||
|
||||
// Tag-aware file patterns
|
||||
const masterFilePattern = /^task_(\d+)\.md$/;
|
||||
const taggedFilePattern = new RegExp(`^task_(\\d+)_${this.escapeRegExp(tag)}\\.md$`);
|
||||
|
||||
// Collect files to remove
|
||||
const filesToRemove: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
let match = null;
|
||||
let fileTaskId: string | null = null;
|
||||
@@ -169,12 +189,19 @@ export class TaskFileGeneratorService {
|
||||
// Convert to integer for comparison (removes leading zeros)
|
||||
const normalizedId = String(parseInt(fileTaskId, 10));
|
||||
if (!validTaskIds.includes(normalizedId)) {
|
||||
const filePath = path.join(outputDir, file);
|
||||
fs.unlinkSync(filePath);
|
||||
removedCount++;
|
||||
filesToRemove.push(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
// Ignore errors during cleanup - non-critical operation
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,7 +393,7 @@ export class TasksDomain {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
@@ -417,4 +417,14 @@ export class TasksDomain {
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,6 +203,17 @@ export class TmCore {
|
||||
get projectPath(): string {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user