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
|
- 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.
|
||||||
|
|||||||
@@ -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/);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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: {}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
* 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user